

iOS完整文件拉流解析解码同步渲染音视频流
source link: https://www.tuicool.com/articles/yuAnemJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

需求
解析文件中的音视频流以解码同步并将视频渲染到屏幕上,音频通过扬声器输出.对于仅仅需要单纯播放一个视频文件可直接使用 AVFoundation
中上层播放器,这里是用最底层的方式实现,可获取原始音视频帧数据.
实现原理
本文主要分为三大块,解析模块使用FFmpeg parse文件中的音视频流,解码模块使用FFmpeg或苹果原生解码器解码音视频,渲染模块使用OpenGL将视频流渲染到屏幕,使用Audio Queue Player将音频以扬声器形式输出.
阅读前提
注意: 本文涉及到的所有模块具体实现均在如下链接中,可根据需求自行查看讲解部分.
- 音视频基础
- iOS FFmpeg环境搭建
- FFmpeg解析视频数据
- VideoToolbox实现视频硬解码
- Audio Converter音频解码
- FFmpeg音频解码
- FFmpeg视频解码
- OpenGL渲染视频数据
- H.264,H.265码流结构
- 传输音频数据队列实现
- Audio Queue 播放器
代码地址 : iOS File Player
掘金地址 : iOS File Player
简书地址 : iOS File Player
博客地址 :iOS File Player
总体架构
本文以解码一个.MOV媒体文件为例, 该文件中包含H.264编码的视频数据, AAC编码的音频数据,首先要通过FFmpeg去parse文件中的音视频流信息,parse出来的结果保存在 AVPacket
结构体中,然后分别提取音视频帧数据,音频帧通过FFmpeg解码器或苹果原生框架中的Audio Converter进行解码,视频通过FFmpeg或苹果原生框架VideoToolbox中的解码器可将数据解码,解码后的音频数据格式为PCM,解码后的视频数据格式为YUV原始数据,根据时间戳对音视频数据进行同步,最后将PCM数据音频传给Audio Queue以实现音频的播放,将YUV视频原始数据封装为 CMSampleBufferRef
数据结构并传给OpenGL以将视频渲染到屏幕上,至此一个完整拉取文件视频流的操作完成.
注意: 通过网址拉取一个RTMP流进行解码播放的流程与拉取文件流基本相同, 只是需要通过socket接收音视频数据后再完成解码及后续流程.
简易流程
Parse
- 创建
AVFormatContext
上下文对象:AVFormatContext *avformat_alloc_context(void);
- 从文件中获取上下文对象并赋值给指定对象:
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)
- 读取文件中的流信息:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
- 获取文件中音视频流:
m_formatContext->streams[audio/video index]e
- 开始parse以获取文件中视频帧帧:
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
- 如果是视频帧通过
av_bitstream_filter_filter
生成sps,pps等关键信息. - 读取到的
AVPacket
即包含文件中所有的音视频压缩数据.
解码
通过FFmpeg解码
formatContext->streams[a/v index]->codec; AVCodec *avcodec_find_decoder(enum AVCodecID id); int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options); int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt); int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame); SwrContext
通过VideoToolbox解码视频
- 将从FFmpeg中parse到的extra data中分离提取中NALU头关键信息sps,pps等
- 通过上面提取的关键信息创建视频描述信息:
CMVideoFormatDescriptionRef
,CMVideoFormatDescriptionCreateFromH264ParameterSets / CMVideoFormatDescriptionCreateFromHEVCParameterSets
- 创建解码器:
VTDecompressionSessionCreate
,并指定一系列相关参数. - 将压缩数据放入CMBlockBufferRef中:
CMBlockBufferCreateWithMemoryBlock
- 开始解码:
VTDecompressionSessionDecodeFrame
- 在回调中接收解码后的视频数据
通过AudioConvert解码音频
AudioConverterNewSpecific AudioClassDescription AudioConverterFillComplexBuffer
同步
因为这里解码的是本地文件中的音视频, 也就是说只要本地文件中音视频的时间戳打的完全正确,我们解码出来的数据是可以直接播放以实现同步的效果.而我们要做的仅仅是保证音视频解码后同时渲染.
注意: 比如通过一个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)]; }
具体实现
本文中每一部分的具体实现均有详细介绍, 如需帮助请参考阅读前提中附带的链接地址.
注意
因为不同文件中压缩的音视频数据格式不同,这里仅仅兼容部分格式,可自定义进行扩展.
Recommend
-
68
解析数据 (SPS PPS NALU Unit) 初始化解码器 将解析后的H264 NALU Unit输入到解码器 解码完成后回调,输出解码数据 解码数据显示(OpenGL ES)
-
10
SRS 流媒体服务器运行流程(推流端使用OBS,拉流端使用VLC) ...
-
10
# 工具简介
-
11
音视频同步!RTCP 协议解析及代码实现RTCP 是实时控制协议(Real-...
-
18
即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙。原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的。 本文主要讲解实时音视频技术中视频技术的预测技术理论知识...
-
9
即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙。原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的。 本文主要讲解实时音视频技术中视频技术的编码理论知识。
-
9
即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙。原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的。 本文主要讲解实时音视频技术中视频技术的数字视频知识。
-
3
点击关注 发现更多音视频技术干货? 前言 直播是一个庞大而复杂的业务形态,一个优秀的直播系统涉及众多团队的共同协作,有非常完整的直播链路。 那么,直播链路中都有哪些角色?这些角色要解决的是哪些问题?要优化...
-
4
前言 直播是一个庞大而复杂的业务形态,一个优秀的直播系统涉及众多团队的共同协作,有非常完整的直播链路。 那么,直播链路中都有哪些角色?这些角色要解决的是哪些问题?要优化某个环节时需要哪些角色的配合?这些角色...
-
5
video.js/videojs-contrib-hls 实现hls拉流播放 精选 原创
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK