需求
在陪玩平台源码的解析文件中,音视频流以解码同步并将视频渲染到屏幕上,音频经过扬声器输出.对于仅仅须要单纯播放一个视频文件可直接使用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