需求
在陪玩平臺源碼的解析文件中,音視頻流以解碼同步並將視頻渲染到屏幕上,音頻經過揚聲器輸出.對於僅僅須要單純播放一個視頻文件可直接使用AVFoundation中上層播放器,這裏是用最底層的方式實現,可獲取原始音視頻幀數據.設計模式
實現原理網絡
本文主要分爲三大塊,陪玩平臺源碼的解析模塊使用FFmpeg parse文件中的音視頻流,陪玩平臺源碼的解碼模塊使用FFmpeg或蘋果原生解碼器解碼音視頻,陪玩平臺源碼的渲染模塊使用OpenGL將視頻流渲染到屏幕,使用Audio Queue Player將音頻以揚聲器形式輸出.數據結構
整體架構
本文以解碼一個陪玩平臺源碼中的.MOV媒體文件爲例, 該文件中包含H.264編碼的視頻數據, AAC編碼的音頻數據,首先要經過FFmpeg去parse文件中的音視頻流信息,parse出來的結果保存在AVPacket結構體中,而後分別提取音視頻幀數據,音頻幀經過FFmpeg解碼器或蘋果原生框架中的Audio Converter進行解碼,視頻經過FFmpeg或蘋果原生框架VideoToolbox中的解碼器可將數據解碼,解碼後的音頻數據格式爲PCM,解碼後的視頻數據格式爲YUV原始數據,根據陪玩平臺源碼時間戳對音視頻數據進行同步,最後將PCM數據音頻傳給Audio Queue以實現音頻的播放,將YUV視頻原始數據封裝爲CMSampleBufferRef數據結構並傳給OpenGL以將視頻渲染到陪玩平臺源碼屏幕上,至此一個完整拉取文件視頻流的操做完成.架構
注意: 經過網址拉取一個RTMP流進行解碼播放的流程與拉取文件流基本相同, 只是須要經過socket接收音視頻數據後再完成解碼及後續流程.框架
簡易流程
Parsesocket
解碼
經過FFmpeg解碼async
經過VideoToolbox解碼視頻ide
經過AudioConvert解碼音頻post
同步ui
由於這裏解碼的是陪玩平臺源碼文件中的音視頻, 也就是說只要陪玩平臺源碼文件中音視頻的時間戳打的徹底正確,咱們解碼出來的數據是能夠直接播放以實現同步的效果.而咱們要作的僅僅是保證音視頻解碼後同時渲染.
注意: 好比經過一個RTMP地址拉取的流由於存在網絡緣由可能形成某個時間段數據丟失,形成音視頻不一樣步,因此須要有一套機制來糾正時間戳.大致機制即爲視頻追趕音頻,後面會有文件專門介紹,這裏不做過多說明.
渲染
經過上面的步驟獲取到的陪玩平臺源碼視頻原始數據便可經過封裝好的OpenGL ES直接渲染到屏幕上,蘋果原生框架中也有GLKViewController能夠完成屏幕渲染.音頻這裏經過Audio Queue接收音頻幀數據以完成播放.
文件結構
快速使用
使用FFmpeg解碼
首先根據陪玩平臺源碼文件地址初始化FFmpeg以實現parse音視頻流.而後利用FFmpeg中的解碼器解碼音視頻數據,這裏須要注意的是,咱們將從讀取到的第一個I幀開始做爲起點,以實現音視頻同步.解碼後的音頻要先裝入傳輸隊列中,由於audio queue player設計模式是不斷從傳輸隊列中取數據以實現播放.視頻數據便可直接進行渲染.
- (void)startRenderAVByFFmpegWithFileName:(NSString *)fileName { NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"]; XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path]; XDXFFmpegVideoDecoder *videoDecoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]]; videoDecoder.delegate = self; XDXFFmpegAudioDecoder *audioDecoder = [[XDXFFmpegAudioDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] audioStreamIndex:[parseHandler getAudioStreamIndex]]; audioDecoder.delegate = self; static BOOL isFindIDR = NO; [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) { if (isFinish) { isFindIDR = NO; [videoDecoder stopDecoder]; [audioDecoder stopDecoder]; dispatch_async(dispatch_get_main_queue(), ^{ self.startWorkBtn.hidden = NO; }); return; } if (isVideoFrame) { // Video if (packet.flags == 1 && isFindIDR == NO) { isFindIDR = YES; } if (!isFindIDR) { return; } [videoDecoder startDecodeVideoDataWithAVPacket:packet]; }else { // Audio [audioDecoder startDecodeAudioDataWithAVPacket:packet]; } }]; } -(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer { CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer); [self.previewView displayPixelBuffer:pix]; } - (void)getDecodeAudioDataByFFmpeg:(void *)data size:(int)size pts:(int64_t)pts isFirstFrame:(BOOL)isFirstFrame { // NSLog(@"demon test - %d",size); // Put audio data from audio file into audio data queue [self addBufferToWorkQueueWithAudioData:data size:size pts:pts]; // control rate usleep(14.5*1000); }
使用原生框架解碼
首先根據陪玩平臺源碼文件地址初始化FFmpeg以實現parse音視頻流.這裏首先根據陪玩平臺源碼文件中實際的音頻流數據構造ASBD結構體以初始化音頻解碼器,而後將解碼後的音視頻數據分別渲染便可.這裏須要注意的是,若是要拉取的文件視頻是H.265編碼格式的,解碼出來的數據的由於含有B幀因此時間戳是亂序的,咱們須要藉助一個鏈表對其排序,而後再將排序後的數據渲染到屏幕上.
- (void)startRenderAVByOriginWithFileName:(NSString *)fileName { NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"]; XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path]; XDXVideoDecoder *videoDecoder = [[XDXVideoDecoder alloc] init]; videoDecoder.delegate = self; // Origin file aac format AudioStreamBasicDescription audioFormat = { .mSampleRate = 48000, .mFormatID = kAudioFormatMPEG4AAC, .mChannelsPerFrame = 2, .mFramesPerPacket = 1024, }; XDXAduioDecoder *audioDecoder = [[XDXAduioDecoder alloc] initWithSourceFormat:audioFormat destFormatID:kAudioFormatLinearPCM sampleRate:48000 isUseHardwareDecode:YES]; [parseHandler startParseWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, struct XDXParseVideoDataInfo *videoInfo, struct XDXParseAudioDataInfo *audioInfo) { if (isFinish) { [videoDecoder stopDecoder]; [audioDecoder freeDecoder]; dispatch_async(dispatch_get_main_queue(), ^{ self.startWorkBtn.hidden = NO; }); return; } if (isVideoFrame) { [videoDecoder startDecodeVideoData:videoInfo]; }else { [audioDecoder decodeAudioWithSourceBuffer:audioInfo->data sourceBufferSize:audioInfo->dataSize completeHandler:^(AudioBufferList * _Nonnull destBufferList, UInt32 outputPackets, AudioStreamPacketDescription * _Nonnull outputPacketDescriptions) { // Put audio data from audio file into audio data queue [self addBufferToWorkQueueWithAudioData:destBufferList->mBuffers->mData size:destBufferList->mBuffers->mDataByteSize pts:audioInfo->pts]; // control rate usleep(16.8*1000); }]; } }]; } - (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer isFirstFrame:(BOOL)isFirstFrame { if (self.hasBFrame) { // Note : the first frame not need to sort. if (isFirstFrame) { CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer); [self.previewView displayPixelBuffer:pix]; return; } [self.sortHandler addDataToLinkList:sampleBuffer]; }else { CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer); [self.previewView displayPixelBuffer:pix]; } } #pragma mark - Sort Callback - (void)getSortedVideoNode:(CMSampleBufferRef)sampleBuffer { int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000); static int64_t lastpts = 0; // NSLog(@"Test marigin - %lld",pts - lastpts); lastpts = pts; [self.previewView displayPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)]; }
注意
由於不一樣陪玩平臺源碼文件中壓縮的音視頻數據格式不一樣,這裏僅僅兼容部分格式,可自定義進行擴展.以上就是「陪玩平臺源碼中完整文件拉流解析,解碼同步渲染音視頻流」的所有內容,但願對你們有幫助。
本文轉載自網絡,轉載僅爲分享乾貨知識,若有侵權歡迎聯繫雲豹科技進行刪除處理原文連接:https://juejin.cn/post/6844903881818767374