-
关于
FFmpeg
的资源网上有很多,但是在iOS平台的FFmpeg
入门的资源却很少,刚开始学习的时候也是像闷头苍蝇,周旋了很久,所以很久之前就想出一个可以让新手也可以看懂的,基于iOS的FFmpeg
教程了。 -
看见了优秀的第三方库,总有一种想去探究其如何实现的冲动,半年前公司项目的需求,接触了基于
FFmpeg
库的Kxmovie
这个音视频播放的第三方库,不得不说这个确实是一个很简单实用,代码有简单流畅的库。 -
接下来,我们来一步一步的解析它,后三篇文章中的代码都是分离出比较精简的代码,所以有时候在逻辑上可能有一些漏洞,希望大家只可以指出我的错误。
-
关于音视频的基础我就不再啰嗦了,建议去看雷神的博客。
基于iOS平台的最简单的FFmpeg视频播放器(一)
基于iOS平台的最简单的FFmpeg视频播放器(二)
基于iOS平台的最简单的FFmpeg视频播放器(三)
音视频解码的步骤
- 把文件分成视频和音频,初始化解码器
- 解码视频和音频
- 显示视频和播放音频
接下来我们一步一步的分开解析,今天的第一部分要做的就是:
- 从文件中分离出视频流,然后初始化解码器
正式开始
以下内容都是基于Kxmovie
写的,应该说是对这个第三方库的分解
初始化文件和解码器
- (void)start
{
_path = [[NSBundle mainBundle] pathForResource:@"cuc_ieschool2" ofType:@"mp4"];
__weak Aie1Controller * weakSelf = self;
AieDecoder * decoder = [[AieDecoder alloc] init];
decoder.delegate = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSError * error = nil;
[decoder openFile:_path error:&error];
__strong Aie1Controller * strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf setMovieDecoder:decoder];
});
}
});
}
1. 初始化文件流并分离出视频流
- 在初始化解码器之前,需要从视频文件中获取视频流,然后使用视频流中获信息去初始化解码器。
1.1 读取文件信息
- (BOOL)openInput:(NSString *)path
{
AVFormatContext * formatCtx = NULL;
formatCtx = avformat_alloc_context();
if (!formatCtx) {
NSLog(@"打开文件失败");
return NO;
}
if (avformat_open_input(&formatCtx, [path cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL) < 0) {
if (formatCtx) {
avformat_free_context(formatCtx);
}
NSLog(@"打开文件失败");
return NO;
}
if (avformat_find_stream_info(formatCtx, NULL) < 0) {
avformat_close_input(&formatCtx);
NSLog(@"无法获取流信息");
return NO;
}
av_dump_format(formatCtx, 0, [path.lastPathComponent cStringUsingEncoding:NSUTF8StringEncoding], false);
_formatCtx = formatCtx;
return YES;
}
-
AVFormatContext
这个结构体很重要(FFmpeg里的结构体都很重要),jump进去,官方的解释是Format I/O context
,是一个格式化输入输出的上下文。 -
AVFormatContext
方法以及解释
avformat_alloc_context()
是初始化方法。
avformat_free_context()
是释放方法。
avformat_open_input()
是打开文件方法。
avformat_find_stream_info()
是从文件中获取流信息的方法。
avformat_close_input()
是关闭输入,然后是释放。那么它和之前的avformat_free_context()
有什么区别呢?细心的同学已经已经发现了,如果用打开文件avformat_open_input()
之后那就需要close,如果没有打开,那就直接free。
av_dump_format()
其实就是一个打印输出输出的一个方法,可有可无。
1.2 打开视频流
// 打开视频流
- (BOOL)openVideoStream
{
BOOL resual = YES;
_videoStream = -1;
_videoStreams = collectStreams(_formatCtx, AVMEDIA_TYPE_VIDEO);
for (NSNumber * n in _videoStreams) {
const NSUInteger iStream = n.integerValue;
if (0 == (_formatCtx->streams[iStream]->disposition &
AV_DISPOSITION_ATTACHED_PIC)) {
resual = [self openVideoStream:iStream];
if (resual) {
break;
}
}
}
return YES;
}
- 这个过程并不重要,接下来看下一步,才是重头戏。
1.3 分离出视频裸流
static NSArray * collectStreams(AVFormatContext * formatCtx, enum AVMediaType codecType)
{
NSMutableArray * ma = [NSMutableArray array];
for (NSInteger i = 0; i < formatCtx->nb_streams; i++) {
if (codecType == formatCtx->streams[i]->codec->codec_type) {
[ma addObject:[NSNumber numberWithInteger:i]];
}
}
return [ma copy];
}
- 上上一步我们已经通过
AVFormatContext
打开了文件,现在这一步我们正式从文件中分离出真正的视频流。看上面这个函数,我们之前传入的第二个参数是AVMEDIA_TYPE_VIDEO
,这个就是循环遍历文件中的所有的流,然后从文件流中提取出格式是AVMEDIA_TYPE_VIDEO
的视频流,显而易见,音频流就是AVMEDIA_TYPE_AUDIO
,其实这方面,音视频是共通的。
2. 初始化视频解码器并计算帧率
2.1 初始化视频解码器
- 终于到了本文的小高潮了,这一步的参数是怎么来的呢?就是在上一步分离出来的视频流,循环遍历一帧有效的数据流,来初始化解码器,记住只要初始化一次就够了。
- (BOOL)openVideoStream:(NSInteger)videoStream
{
AVCodecContext * codecCtx = _formatCtx->streams[videoStream]->codec;
AVCodec * codec = avcodec_find_decoder(codecCtx->codec_id);
if (!codec) {
NSLog(@"无法找到解码器");
return NO;
}
if (avcodec_open2(codecCtx, codec, NULL) < 0) {
NSLog(@"打开解码器失败");
return YES;
}
_videoFrame = av_frame_alloc();
if (!_videoFrame) {
avcodec_close(codecCtx);
NSLog(@"创建视频帧失败");
return NO;
}
_videoStream = videoStream;
_videoCodecCtx = codecCtx;
// 计算 fps 帧率
AVStream * st = _formatCtx->streams[_videoStream];
avStreamFPSTimeBase(st, 0.04, &_fps, &_videoTimeBase);
return YES;
}
-
AVCodecContext
官方解释是main external API structure
,主要外部API结构体,翻译起来怪怪的。但是雷神曾经说过:‘’AVCodecContext中很多的参数是编码的时候使用的,而不是解码的时候使用的。
‘’,那我也可以理解成AVCodecContext
结构体主要是存放编解码时候的的参数,到底有什么参数,我们到时候遇到再解释。 -
AVCodec
官方解释是,哦官方没有解释,他们的意思估计是这个不需要解释,你懂的。确实这个结构体里面的信息比其他的结构体少的多,综合的来说。AVCodec
是一个储存编解码器信息的结构体,会用就好了。 -
AVFrame
官方解释是This structure describes decoded (raw) audio or video data
,意思就是说这个是用来 存储解码的音视频数据(原始的音视频数据,就是YUV,PCM这些没有压缩过的,体积很大的数据) 的结构体。 -
AVStream
官方解释是Stream structure
,流结构体,就是之前从文件中取出来的那个文件流,也是音视频流,记录这些流数据中的参数和信息。 -
AVCodecContext
AVFrame
方法以及解释
avcodec_find_decoder()
通过对应匹配的编解码器ID找到已经注册的编解码器。
avcodec_open2()
通过AVCodecContext
打开解码器。
avcodec_close()
通过AVCodecContext
关闭解码器,注意了这里的close是释放AVCodecContext
结构体的中的所有数据,不是释放AVCodecContext
本身。
av_frame_alloc()
初始化AVFrame
。
av_frame_free()
释放AVFrame
。
2.2 计算FPS帧率
- 终于到了这一段的结尾了
static void avStreamFPSTimeBase(AVStream *st, CGFloat defaultTimeBase, CGFloat *pFPS, CGFloat *pTimeBase)
{
CGFloat fps, timebase;
// ffmpeg提供了一个把AVRatioal结构转换成double的函数
// 默认0.04 意思就是25帧
if (st->time_base.den && st->time_base.num)
timebase = av_q2d(st->time_base);
else if(st->codec->time_base.den && st->codec->time_base.num)
timebase = av_q2d(st->codec->time_base);
else
timebase = defaultTimeBase;
if (st->codec->ticks_per_frame != 1) {
}
// 平均帧率
if (st->avg_frame_rate.den && st->avg_frame_rate.num)
fps = av_q2d(st->avg_frame_rate);
else if (st->r_frame_rate.den && st->r_frame_rate.num)
fps = av_q2d(st->r_frame_rate);
else
fps = 1.0 / timebase;
if (pFPS)
*pFPS = fps;
if (pTimeBase)
*pTimeBase = timebase;
}
-
timebase
的意思就是播放一帧需要的时间,默认是0.04,也可以说是0.04秒播放一帧,所以帧率就是1/0.04 = 25帧。 -
AVRational
是AVCodecContext
和AVStream
结构体中的一个结构里,里面只有两个参数,num
、den
就是分子和分母的意思,两个参数同时存在的时候,才可以求出帧率。 -
av_q2d()
是把AVRatioal
中的参数转换成double
类型的一个函数,方便我们计算。
结尾
- 代码会在整个播放器讲完之后再给出完整的。
- 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
- 谢谢阅读
网友评论