需求
解析文件中的音视频流以解码同步并将视频渲染到屏幕上, 音频通过扬声器输出. 对于仅仅需要单纯播放一个视频文件可直接使用 AVFoundation 中上层播放器, 这里是用最底层的方式实现, 可获取原始音视频帧数据.
实现原理
本文主要分为三大块, 解析模块使用 FFMPEG parse 文件中的音视频流, 解码模块使用 FFMPEG 或苹果原生解码器解码音视频, 渲染模块使用 OpenGL 将视频流渲染到屏幕, 使用 Audio Queue Player 将音频以扬声器形式输出.
阅读前提
注意: 本文涉及到的所有模块具体实现均在如下链接中, 可根据需求自行查看讲解部分.
音视频基础
iOS FFMPEG 环境搭建 https://juejin.im/post/5ceff73df265da1bb13f16f4
FFMPEG 解析视频数据 https://juejin.im/post/5cffac756fb9a07f08708d20
VideoToolbox 实现视频硬解码 https://juejin.im/post/5d1243b75188257b791527d5
Audio Converter 音频解码
FFMPEG 音频解码 https://juejin.im/post/5d187cb5f265da1ba25274ce
FFMPEG 视频解码 https://juejin.im/post/5d0a5a6c518825431f5e5ed8
OpenGL 渲染视频数据 https://juejin.im/post/5d08f861518825699040ecac
H.264,H.265 码流结构 https://juejin.im/post/5ce9f36bf265da1bbd4b5084
传输音频数据队列实现 https://juejin.im/post/5a91315c6fb9a063395c8944
Audio Queue 播放器
代码地址 : iOS File Player
掘金地址 : iOS File Player https://juejin.im/post/5d216633e51d4555fd20a3b9
简书地址 : iOS File Player https://www.jianshu.com/p/854a1bb47173
博客地址 :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)];
- }
具体实现
本文中每一部分的具体实现均有详细介绍, 如需帮助请参考阅读前提中附带的链接地址.
注意
因为不同文件中压缩的音视频数据格式不同, 这里仅仅兼容部分格式, 可自定义进行扩展.
来源: http://www.tuicool.com/articles/yuAnemJ