需求
衆所周知,原始的音視訊資料無法直接在網絡上傳輸,推流需要編碼後的音視訊資料以合成的視訊流,如flv, mov, asf流等,根據接收方需要的格式進行合成并傳輸,這裡以合成asf流為例,講述一個完整推流過程:即音視訊從采集到編碼,同步合成asf視訊流,然後可以将流進行傳輸,為了友善,本例将合成的視訊流寫入一個asf檔案中,以供測試.
注意: 測試需要使用終端通過: ffplay播放demo中錄制好的檔案,因為asf是windows才支援的格式,mac自帶播放器無法播放.
1、實作原理
- 采集: 采集視訊幀使用AVCaptureSession,采集音頻幀使用Audio Unit
- 編碼: 編碼視訊資料使用VideoToolbox中vtCompresssion硬編,編碼音頻資料使用audio converter軟編.
- 同步: 根據時間戳生成政策
- 合成: 使用FFmpeg mux編碼的音視訊資料以合成視訊流
- 後續: 合成好的視訊流可以通過網絡傳輸或是錄制成檔案
2、閱讀前提
- 音視訊基礎知識
- 推薦必讀:H264, H265硬體編解碼基礎及碼流分析
- iOS視訊采集實戰(AVCaptureSession)
- Audio Unit采集音頻實戰
- 視訊編碼實戰
- 音頻編碼實戰
- iOS FFmpeg環境搭建
代碼位址 : iOS完整推流
掘金位址 : iOS完整推流
簡書位址 : iOS完整推流
部落格位址 : iOS完整推流
3、總體架構
1.mux
對于iOS而言,我們可以通過底層API捕獲視訊幀與音頻幀資料,捕獲視訊幀使用AVFoundation架構中的AVCaptureSession, 其實它同時也可以捕獲音頻資料,而因為我們想使用最低延時與最高音質的音頻, 是以需要借助最底層的音頻捕捉架構Audio Unit,然後使用VideoToolbox架構中的VTCompressionSessionRef可以對視訊資料進行編碼,使用AudioConverter可以對音頻資料進行編碼,我們在采集時可以将第一幀I幀産生時的系統時間作為音視訊時間戳的一個起點,往後的視訊說都基于此,由此可擴充做音視訊同步方案,最終,我們将比那編碼好的音視訊資料通過FFmpeg進行合成,這裡以asf流為例進行合成,并将生成好的asf流寫入檔案,以供測試. 生成好的asf流可直接用于網絡傳輸.
3.1 簡易流程
采集視訊
- 建立AVCaptureSession對象
- 指定分辨率:sessionPreset/activeFormat,指定幀率setActiveVideoMinFrameDuration/setActiveVideoMaxFrameDuration
- 指定攝像頭位置:AVCaptureDevice
- 指定相機其他屬性: 曝光,對焦,閃光燈,手電筒等等...
- 将攝像頭資料源加入session
- 指定采集視訊的格式:yuv,rgb....kCVPixelBufferPixelFormatTypeKey
- 将輸出源加入session
- 建立接收視訊幀隊列:- (void)setSampleBufferDelegate:(nullable id<AVCaptureVideoDataOutputSampleBufferDelegate>)sampleBufferDelegate queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue
- 将采集視訊資料渲染到螢幕:AVCaptureVideoPreviewLayer
- 在回調函數中擷取視訊幀資料: CMSampleBufferRef
采集音頻
- 配置音頻格式ASBD: 采樣率,聲道數,采樣位數,資料精度,每個包中位元組數等等...
- 設定采樣時間: setPreferredIOBufferDuration
- 建立audio unit對象,指定分類. AudioComponentInstanceNew
- 設定audio unit屬性: 打開輸入,禁止輸出...
- 為接收的音頻資料配置設定大小kAudioUnitProperty_ShouldAllocateBuffer
- 設定接收資料的回調
- 開始audio unit: AudioOutputUnitStart
- 在回調函數中擷取音頻資料: AudioUnitRender
編碼視訊資料
- 指定編碼器寬高類型回調并建立上下文對象: VTCompressionSessionCreate
- 設定編碼器屬性:緩存幀數, 幀率, 平均碼率, 最大碼率, 實時編碼, 是否重排序, 配置資訊, 編碼模式, I幀間隔時間等.
- 準備編碼資料: VTCompressionSessionPrepareToEncodeFrames
- 開始編碼: VTCompressionSessionEncodeFrame
- 回調函數中擷取編碼後的資料CMBlockBufferRef
- 根據合成碼流格式,這裡是asf是以需要Annex B格式,自己組裝sps,pps,start code.
編碼音頻資料
- 提供原始資料類型與編碼後資料類型的ASBD
- 指定編碼器類型kAudioEncoderComponentType
- 建立編碼器AudioConverterNewSpecific
- 設定編碼器屬性: 比特率, 編碼品質等
- 将1024個采樣點原始PCM資料傳入編碼器
- 開始編碼: AudioConverterFillComplexBuffer
- 擷取編碼後的AAC資料
音視訊同步
以編碼的第一幀視訊的系統時間作為音視訊資料的基準時間戳,随後将采集到音視訊資料中的時間戳減去該基準時間戳作為各自的時間戳, 同步有兩種政策,一種是以音頻時間戳為準, 即當出現錯誤時,讓視訊時間戳去追音頻時間戳,這樣做即會造成看到畫面會快進或快退,二是以視訊時間戳為準,即當出現錯誤時,讓音頻時間戳去追視時間戳,即聲音可能會刺耳,不推薦.是以一般使用第一種方案,通過估計下一幀視訊時間戳看看如果超出同步範圍則進行同步.
FFmpeg合成資料流
- 初始化FFmpeg相關參數: AVFormatContext (管理合成上下文), AVOutputFormat(合成流格式), AVStream(音視訊資料流)...
- 建立上下文對象AVFormatContext: avformat_alloc_context
- 根據資料類型生成編碼器AVCodec: avcodec_find_encoder 視訊:AV_CODEC_ID_H264/AV_CODEC_ID_HEVC,音頻:AV_CODEC_ID_AAC
- 生成流AVStream: avformat_new_stream
- 指定音視訊流中各個參數資訊, 如資料格式,視訊寬高幀率,比特率,基準時間,extra data, 音頻:采樣率,聲道數, 采樣位數等等.
- 指定上下文及流格式中的音視訊編碼器id: video_codec_id, audio_codec_id
- 生成視訊流頭資料: 當音視訊編碼器都填充到上下文對象後,即可生産該類型對應的頭資訊, 此頭資訊作為解碼音視訊資料的重要資訊,一定需要正确合成.avformat_write_header
- 将音視訊資料裝入動态數組中.
- 合成音視訊資料: 通過另一條線程取出動态數組中的音視訊資料,通過比較時間戳的方式進行同步合成.
- 将音視訊資料裝入AVPacket中
- 産生合成的資料av_write_frame
C++音視訊學習資料免費擷取方法:關注音視訊開發T哥,點選「連結」即可免費擷取2023年最新C++音視訊開發進階獨家免費學習大禮包!
3.2 檔案結構
2.file
3.3 快速使用
- 初始化相關子產品
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self configureCamera];
[self configureAudioCapture];
[self configureAudioEncoder];
[self configurevideoEncoder];
[self configureAVMuxHandler];
[self configureAVRecorder];
}
- 在相機回調中将原始yuv資料送去編碼
- (void)xdxCaptureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
if ([output isKindOfClass:[AVCaptureVideoDataOutput class]] == YES) {
if (self.videoEncoder) {
[self.videoEncoder startEncodeDataWithBuffer:sampleBuffer
isNeedFreeBuffer:NO];
}
}
}
- 通過回調函數接收編碼後的視訊資料并将其送給合成流類.
#pragma mark Video Encoder
- (void)receiveVideoEncoderData:(XDXVideEncoderDataRef)dataRef {
[self.muxHandler addVideoData:dataRef->data size:(int)dataRef->size timestamp:dataRef->timestamp isKeyFrame:dataRef->isKeyFrame isExtraData:dataRef->isExtraData videoFormat:XDXMuxVideoFormatH264];
}
- 在采集音頻回調中接收音頻資料并編碼,最終将編碼資料也送入合成流類
#pragma mark Audio Capture and Audio Encode
- (void)receiveAudioDataByDevice:(XDXCaptureAudioDataRef)audioDataRef {
[self.audioEncoder encodeAudioWithSourceBuffer:audioDataRef->data
sourceBufferSize:audioDataRef->size
pts:audioDataRef->pts
completeHandler:^(XDXAudioEncderDataRef dataRef) {
if (dataRef->size > 10) {
[self.muxHandler addAudioData:(uint8_t *)dataRef->data
size:dataRef->size
channelNum:1
sampleRate:44100
timestamp:dataRef->pts];
}
free(dataRef->data);
}];
}
- 先寫檔案後,随後接收合成後的資料并寫入檔案.
#pragma mark Mux
- (IBAction)startRecordBtnDidClicked:(id)sender {
int size = 0;
char *data = (char *)[self.muxHandler getAVStreamHeadWithSize:&size];
[self.recorder startRecordWithIsHead:YES data:data size:size];
self.isRecording = YES;
}
- (void)receiveAVStreamWithIsHead:(BOOL)isHead data:(uint8_t *)data size:(int)size {
if (isHead) {
return;
}
if (self.isRecording) {
[self.recorder startRecordWithIsHead:NO data:(char *)data size:size];
}
}
4、具體實作
本例中音視訊采集編碼子產品在前面文章中已經詳細介紹,這裡不再重複,如需幫助請參考上文的閱讀前提.下面僅介紹合成流.
4.1 初始化FFmpeg相關對象.
- AVFormatContext: 管理合成流上下文對象
- AVOutputFormat: 合成流的格式,這裡使用的asf資料流
- AVStream: 音視訊資料流具體資訊
- (void)configureFFmpegWithFormat:(const char *)format {
if(m_outputContext != NULL) {
av_free(m_outputContext);
m_outputContext = NULL;
}
m_outputContext = avformat_alloc_context();
m_outputFormat = av_guess_format(format, NULL, NULL);
m_outputContext->oformat = m_outputFormat;
m_outputFormat->audio_codec = AV_CODEC_ID_NONE;
m_outputFormat->video_codec = AV_CODEC_ID_NONE;
m_outputContext->nb_streams = 0;
m_video_stream = avformat_new_stream(m_outputContext, NULL);
m_video_stream->id = 0;
m_audio_stream = avformat_new_stream(m_outputContext, NULL);
m_audio_stream->id = 1;
log4cplus_info(kModuleName, "configure ffmpeg finish.");
}
4.2 配置視訊流的詳細資訊
設定該編碼的視訊流中詳細的資訊, 如編碼器類型,配置資訊,原始視訊資料格式,視訊的寬高,比特率,幀率,基準時間戳,extra data等.
這裡最重要的就是extra data,注意,因為我們要根據extra data才能生成正确的頭資料,而asf流需要的是annux b格式的資料,蘋果采集的視訊資料格式為avcc是以在編碼子產品中已經将其轉為annux b格式的資料,并通過參數傳入,這裡可以直接使用,關于這兩種格式差別也可以參考閱讀前提中的碼流介紹的文章.
- (void)configureVideoStreamWithVideoFormat:(XDXMuxVideoFormat)videoFormat extraData:(uint8_t *)extraData extraDataSize:(int)extraDataSize {
if (m_outputContext == NULL) {
log4cplus_error(kModuleName, "%s: m_outputContext is null",__func__);
return;
}
if(m_outputFormat == NULL){
log4cplus_error(kModuleName, "%s: m_outputFormat is null",__func__);
return;
}
AVFormatContext *formatContext = avformat_alloc_context();
AVStream *stream = NULL;
if(XDXMuxVideoFormatH264 == videoFormat) {
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
stream = avformat_new_stream(formatContext, codec);
stream->codecpar->codec_id = AV_CODEC_ID_H264;
}else if(XDXMuxVideoFormatH265 == videoFormat) {
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
stream = avformat_new_stream(formatContext, codec);
stream->codecpar->codec_tag = MKTAG('h', 'e', 'v', 'c');
stream->codecpar->profile = FF_PROFILE_HEVC_MAIN;
stream->codecpar->format = AV_PIX_FMT_YUV420P;
stream->codecpar->codec_id = AV_CODEC_ID_HEVC;
}
stream->codecpar->format = AV_PIX_FMT_YUVJ420P;
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream->codecpar->width = 1280;
stream->codecpar->height = 720;
stream->codecpar->bit_rate = 1024*1024;
stream->time_base.den = 1000;
stream->time_base.num = 1;
stream->time_base = (AVRational){1, 1000};
stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
memcpy(m_video_stream, stream, sizeof(AVStream));
if(extraData) {
int newExtraDataSize = extraDataSize + AV_INPUT_BUFFER_PADDING_SIZE;
m_video_stream->codecpar->extradata_size = extraDataSize;
m_video_stream->codecpar->extradata = (uint8_t *)av_mallocz(newExtraDataSize);
memcpy(m_video_stream->codecpar->extradata, extraData, extraDataSize);
}
av_free(stream);
m_outputContext->video_codec_id = m_video_stream->codecpar->codec_id;
m_outputFormat->video_codec = m_video_stream->codecpar->codec_id;
self.isReadyForVideo = YES;
[self productStreamHead];
}
4.3 配置音頻流的詳細資訊
首先根據編碼音頻的類型生成編碼器并生成流對象,然後 配置音頻流的詳細資訊,如壓縮資料格式,采樣率,聲道數,比特率,extra data等等.這裡要注意的是extra data是為了儲存mp4檔案時播放器能夠正确解碼播放準備的,可以參考這幾篇文章:audio extra data1,audio extra data2
- (void)configureAudioStreamWithChannelNum:(int)channelNum sampleRate:(int)sampleRate {
AVFormatContext *formatContext = avformat_alloc_context();
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
AVStream *stream = avformat_new_stream(formatContext, codec);
stream->index = 1;
stream->id = 1;
stream->duration = 0;
stream->time_base.num = 1;
stream->time_base.den = 1000;
stream->start_time = 0;
stream->priv_data = NULL;
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
stream->codecpar->codec_id = AV_CODEC_ID_AAC;
stream->codecpar->format = AV_SAMPLE_FMT_S16;
stream->codecpar->sample_rate = sampleRate;
stream->codecpar->channels = channelNum;
stream->codecpar->bit_rate = 0;
stream->codecpar->extradata_size = 2;
stream->codecpar->extradata = (uint8_t *)malloc(2);
stream->time_base.den = 25;
stream->time_base.num = 1;
/*
* why we put extra data here for audio: when save to MP4 file, the player can not decode it correctly
* http://ffmpeg-users.933282.n4.nabble.com/AAC-decoder-td1013071.html
* http://ffmpeg.org/doxygen/trunk/mpeg4audio_8c.html#aa654ec3126f37f3b8faceae3b92df50e
* extra data have 16 bits:
* Audio object type - normally 5 bits, but 11 bits if AOT_ESCAPE
* Sampling index - 4 bits
* if (Sampling index == 15)
* Sample rate - 24 bits
* Channel configuration - 4 bits
* last reserved- 3 bits
* for exmpale: "Low Complexity Sampling frequency 44100Hz, 1 channel mono":
* AOT_LC == 2 -> 00010
- * 44.1kHz == 4 -> 0100
+ * 44.1kHz == 4 -> 0100 48kHz == 3 -> 0011
* mono == 1 -> 0001
* so extra data: 00010 0100 0001 000 ->0x12 0x8
+ 00010 0011 0001 000 ->0x11 0x88
+
*/
if (stream->codecpar->sample_rate == 44100) {
stream->codecpar->extradata[0] = 0x12;
//iRig mic HD have two chanel 0x11
if(channelNum == 1)
stream->codecpar->extradata[1] = 0x8;
else
stream->codecpar->extradata[1] = 0x10;
}else if (stream->codecpar->sample_rate == 48000) {
stream->codecpar->extradata[0] = 0x11;
//iRig mic HD have two chanel 0x11
if(channelNum == 1)
stream->codecpar->extradata[1] = 0x88;
else
stream->codecpar->extradata[1] = 0x90;
}else if (stream->codecpar->sample_rate == 32000){
stream->codecpar->extradata[0] = 0x12;
if (channelNum == 1)
stream->codecpar->extradata[1] = 0x88;
else
stream->codecpar->extradata[1] = 0x90;
}
else if (stream->codecpar->sample_rate == 16000){
stream->codecpar->extradata[0] = 0x14;
if (channelNum == 1)
stream->codecpar->extradata[1] = 0x8;
else
stream->codecpar->extradata[1] = 0x10;
}else if(stream->codecpar->sample_rate == 8000){
stream->codecpar->extradata[0] = 0x15;
if (channelNum == 1)
stream->codecpar->extradata[1] = 0x88;
else
stream->codecpar->extradata[1] = 0x90;
}
stream->codec->flags|= AV_CODEC_FLAG_GLOBAL_HEADER;
memcpy(m_audio_stream, stream, sizeof(AVStream));
av_free(stream);
m_outputContext->audio_codec_id = stream->codecpar->codec_id;
m_outputFormat->audio_codec = stream->codecpar->codec_id;
self.isReadyForAudio = YES;
[self productStreamHead];
}
4.4 生成流頭資料
目前面2,3部都配置完成後,我們将音視訊流注入上下文對象及對象中的流格式中,即可開始生成頭資料.avformat_write_header
- (void)productStreamHead {
log4cplus_debug("record", "%s,line:%d",__func__,__LINE__);
if (m_outputFormat->video_codec == AV_CODEC_ID_NONE) {
log4cplus_error(kModuleName, "%s: video codec is NULL.",__func__);
return;
}
if(m_outputFormat->audio_codec == AV_CODEC_ID_NONE) {
log4cplus_error(kModuleName, "%s: audio codec is NULL.",__func__);
return;
}
/* prepare header and save header data in a stream */
if (avio_open_dyn_buf(&m_outputContext->pb) < 0) {
avio_close_dyn_buf(m_outputContext->pb, NULL);
log4cplus_error(kModuleName, "%s: AVFormat_HTTP_FF_OPEN_DYURL_ERROR.",__func__);
return;
}
/*
* HACK to avoid mpeg ps muxer to spit many underflow errors
* Default value from FFmpeg
* Try to set it use configuration option
*/
m_outputContext->max_delay = (int)(0.7*AV_TIME_BASE);
int result = avformat_write_header(m_outputContext,NULL);
if (result < 0) {
log4cplus_error(kModuleName, "%s: Error writing output header, res:%d",__func__,result);
return;
}
uint8_t * output = NULL;
int len = avio_close_dyn_buf(m_outputContext->pb, (uint8_t **)(&output));
if(len > 0 && output != NULL) {
av_free(output);
self.isReadyForHead = YES;
if (m_avhead_data) {
free(m_avhead_data);
}
m_avhead_data_size = len;
m_avhead_data = (uint8_t *)malloc(len);
memcpy(m_avhead_data, output, len);
if ([self.delegate respondsToSelector:@selector(receiveAVStreamWithIsHead:data:size:)]) {
[self.delegate receiveAVStreamWithIsHead:YES data:output size:len];
}
log4cplus_error(kModuleName, "%s: create head length = %d",__func__, len);
}else{
self.isReadyForHead = NO;
log4cplus_error(kModuleName, "%s: product stream header failed.",__func__);
}
}
4.5 然後将傳來的音視訊資料裝入數組中
該數組通過封裝C++中的vector實作一個輕量級資料結構以緩存資料.
4.6 合成音視訊資料
建立一條線程專門合成音視訊資料,合成政策即取出音視訊資料中時間戳較小的一幀先寫,因為音視訊資料總體偏差不大,是以理想情況應該是取一幀視訊,一幀音頻,當然因為音頻采樣較快,可能會相對多一兩幀,而當音視訊資料由于某種原因不同步時,則會等待,直至時間戳重新同步才能繼續進行合成.
int err = pthread_create(&m_muxThread,NULL,MuxAVPacket,(__bridge_retained void *)self);
if(err != 0){
log4cplus_error(kModuleName, "%s: create thread failed: %s",__func__, strerror(err));
}
void * MuxAVPacket(void *arg) {
pthread_setname_np("XDX_MUX_THREAD");
XDXAVStreamMuxHandler *instance = (__bridge_transfer XDXAVStreamMuxHandler *)arg;
if(instance != nil) {
[instance dispatchAVData];
}
return NULL;
}
#pragma mark Mux
- (void)dispatchAVData {
XDXMuxMediaList audioPack;
XDXMuxMediaList videoPack;
memset(&audioPack, 0, sizeof(XDXMuxMediaList));
memset(&videoPack, 0, sizeof(XDXMuxMediaList));
[m_AudioListPack reset];
[m_VideoListPack reset];
while (true) {
int videoCount = [m_VideoListPack count];
int audioCount = [m_AudioListPack count];
if(videoCount == 0 || audioCount == 0) {
usleep(5*1000);
log4cplus_debug(kModuleName, "%s: Mux dispatch list: v:%d, a:%d",__func__,videoCount, audioCount);
continue;
}
if(audioPack.timeStamp == 0) {
[m_AudioListPack popData:&audioPack];
}
if(videoPack.timeStamp == 0) {
[m_VideoListPack popData:&videoPack];
}
if(audioPack.timeStamp >= videoPack.timeStamp) {
log4cplus_debug(kModuleName, "%s: Mux dispatch input video time stamp = %llu",__func__,videoPack.timeStamp);
if(videoPack.data != NULL && videoPack.data->data != NULL){
[self addVideoPacket:videoPack.data
timestamp:videoPack.timeStamp
extraDataHasChanged:videoPack.extraDataHasChanged];
av_free(videoPack.data->data);
av_free(videoPack.data);
}else{
log4cplus_error(kModuleName, "%s: Mux Video AVPacket data abnormal",__func__);
}
videoPack.timeStamp = 0;
}else {
log4cplus_debug(kModuleName, "%s: Mux dispatch input audio time stamp = %llu",__func__,audioPack.timeStamp);
if(audioPack.data != NULL && audioPack.data->data != NULL) {
[self addAudioPacket:audioPack.data
timestamp:audioPack.timeStamp];
av_free(audioPack.data->data);
av_free(audioPack.data);
}else {
log4cplus_error(kModuleName, "%s: Mux audio AVPacket data abnormal",__func__);
}
audioPack.timeStamp = 0;
}
}
}
4.7 擷取合成好的視訊流
通過av_write_frame即可擷取合成好的資料.
- (void)productAVDataPacket:(AVPacket *)packet extraDataHasChanged:(BOOL)extraDataHasChanged {
BOOL isVideoIFrame = NO;
uint8_t *output = NULL;
int len = 0;
if (avio_open_dyn_buf(&m_outputContext->pb) < 0) {
return;
}
if(packet->stream_index == 0 && packet->flags != 0) {
isVideoIFrame = YES;
}
if (av_write_frame(m_outputContext, packet) < 0) {
avio_close_dyn_buf(m_outputContext->pb, (uint8_t **)(&output));
if(output != NULL)
free(output);
log4cplus_error(kModuleName, "%s: Error writing output data",__func__);
return;
}
len = avio_close_dyn_buf(m_outputContext->pb, (uint8_t **)(&output));
if(len == 0 || output == NULL) {
log4cplus_debug(kModuleName, "%s: mux len:%d or data abnormal",__func__,len);
if(output != NULL)
av_free(output);
return;
}
if ([self.delegate respondsToSelector:@selector(receiveAVStreamWithIsHead:data:size:)]) {
[self.delegate receiveAVStreamWithIsHead:NO data:output size:len];
}
if(output != NULL)
av_free(output);
}
原文連結:iOS瀹屾暣鎺ㄦ祦閲囬泦闊寵棰戞暟鎹紪鐮佸悓姝ュ悎鎴愭祦 - 绠€涔�