上一篇写了关于FFmpeg的对文件的处理以及初始化解码器,算是为本片做下了很重要的基础,需要看基础的同学还是推荐看雷神的博客。
基于iOS平台的最简单的FFmpeg视频播放器(一)
基于iOS平台的最简单的FFmpeg视频播放器(二)
基于iOS平台的最简单的FFmpeg视频播放器(三)
废话不多说,让我们
正式开始
- 粗略的来概括一下今天的内容,分为两步:
1.使用上一篇文章初始化的解码器,将原始数据进行解码。
2.保存解码后的数据到一个数组中。
1.1 热身运动
- 这是在解码视频之前的热身运动
- (void)setMovieDecoder:(AieDecoder *)decoder
{
if (decoder) {
_decoder = decoder;
_dispatchQueue = dispatch_queue_create("AieMovie", DISPATCH_QUEUE_SERIAL);
_videoFrames = [NSMutableArray array];
}
_minBufferedDuration = LOCAL_MIN_BUFFERED_DURATION;
_maxBufferedDuration = LOCAL_MAX_BUFFERED_DURATION;
if (self.isViewLoaded) {
[self setupPresentView];
}
}
-
_dispatchQueue手动创建的一个串行队列,用于之后解码的线程。 -
_videoFrames这个是一个用来存储解码后的数据的可变数组。 -
_minBufferedDuration、_maxBufferedDuration这两个函数是用来做什么的,我现在来简单解释一下,到后面有相关的代码就了解了。其实就是用来控制是否开始解码的两个参数,当小于_minBufferedDuration的时候,就开始解码,当大于_maxBufferedDuration的时候,就停止解码,当处于两者之间,那就一直解码,不要停。
1.2 再次热身(很重要)
- 引用马老师的话来说,这段代码真的是,
李时珍的皮。
- (void)asyncDecodeFrames
{
__weak Aie1Controller * weakSelf = self;
__weak AieDecoder * weakDecoder = _decoder;
dispatch_async(_dispatchQueue, ^{
// 当已经解码的视频总时间大于_maxBufferedDuration 停止解码
BOOL good = YES;
while (good) {
good = NO;
@autoreleasepool {
__strong AieDecoder * strongDecoder = weakDecoder;
if (strongDecoder) {
NSArray * frames = [strongDecoder decodeFrames:0.1];
if (frames.count) {
__strong Aie1Controller * strongSelf = weakSelf;
if (strongSelf) {
good = [strongSelf addFrames:frames];
}
}
}
}
}
});
}
- 很多人看这段代码的时候,可能看见
__weak , __strong, dispatch_async, while , @autoreleasepool,组合在一起的时候就已经蒙圈了,那现在我们一句句来解释。 - 一开始我们定义了一个
GCD的_dispatchQueue,现在就用到了,正因为用到了block,所以我们需要__weak来防止循环引用,__strong是相对应的,因为在block中是一个延时的,持续的操作,所以如果不使用__strong的话,会导致block中的对象被弱引用,而提早释放,所以需要__strong再次对block中的对象强引用。 -
while循环是为了让解码器可以持续的去解码(如果不出现异常情况下),就算跳出了循环,还会有其他的地方调用asyncDecodeFrames,再次进入循环,所以可以一直不停解码。 -
@autoreleasepool自动释放池,有些人会问,iOS项目main函数已经有@autoreleasepool,为什么还要加一个呢,是不是画蛇添足?当然不是,我们看@autoreleasepool中的代码,每一次的解码都会产生一个数组,所以如果不及时释放的话,内存就会一直变大(视频帧的数据量可不是开玩笑的),所以需要在这里加一个@autoreleasepool。
2.1 开始解码
- 终于等到你,本系列文章最重要的篇章
- (NSArray *)decodeFrames:(CGFloat)minDuration
{
if (_videoStream == -1) {
return nil;
}
NSMutableArray * result = [NSMutableArray array];
AVPacket packet;
CGFloat decodedDuration = 0;
BOOL finished = NO;
while (!finished) {
if (av_read_frame(_formatCtx, &packet) < 0) {
NSLog(@"读取Frame失败");
break;
}
if (packet.stream_index == _videoStream) {
int pktSize = packet.size;
while (pktSize > 0) {
int gotFrame = 0;
int len = avcodec_decode_video2(_videoCodecCtx, _videoFrame, &gotFrame, &packet);
if (len < 0) {
NSLog(@"解码失败");
break;
}
if (gotFrame) {
AieVideoFrame * frame = [self handleVideoFrame];
frame.type = AieFrameTypeVideo;
NSLog(@"当前帧的时间戳:%f, 当前帧的持续时间:%f", frame.position, frame.duration);
if (frame) {
[result addObject:frame];
_position = frame.position;
decodedDuration += frame.duration;
if (decodedDuration > minDuration) {
finished = YES;
}
}
}
if (0 == len) {
break;
}
pktSize -= len;
}
}
av_free_packet(&packet);
}
return result;
}
- 上面的代码相对来说比较多一些,所以为了可以容易看一点,所以我们再把代码细分一下。
- 在解析之前我们先要弄清楚几件事情。
1.从哪里来?
2.怎么来?
3.来干嘛?
4.到哪里去?
5.怎么去?
2.1.1 从哪里来?
AVPacket packet;
- 数据就存在
AVPacket里面,是解码前的数据,压缩过的数据。 -
AVPacket官方解释是This structure stores compressed data. It is typically exported by demuxers and then passed as input to decoders, or received as output from encoders and then passed to muxers.,这次的解释相对来说比较长,说明这个是一个很重要的机构体。大致意思就是说这是一个用来存储压缩数据以及相关信息的机构体,是一个把数据导入到解码器的分配器,也是用来接收编码后的数据的结构体。
2.1.2 怎么来?
if (av_read_frame(_formatCtx, &packet) < 0) {
NSLog(@"读取Frame失败");
break;
}
-
av_read_frame()作用是读取一帧视频帧或者是多帧音频帧。AVPacket中的数据会一直有效,除非读取到下一帧或者是AVFormatContext中的数据被彻底清空(调用avformat_close_input())。
2.1.3 来干嘛?(这就是最重要的解码)
if (packet.stream_index == _videoStream) {
int pktSize = packet.size;
while (pktSize > 0) {
int gotFrame = 0;
int len = avcodec_decode_video2(_videoCodecCtx, _videoFrame, &gotFrame, &packet);
if (len < 0) {
NSLog(@"解码失败");
break;
}
if (gotFrame) {
}
if (0 == len) {
break;
}
pktSize -= len;
}
}
- 如果读出的
AVPacket中的流的位置和当前这一帧的流的位置相同,那就开始解码。 - 接下来就开始解码
AVPacket中存储的所有的数据,如果解码成功一次就pktSize -= len;减去已经解码过的长度,直到解码完AVPacket中的所有数据,就结束循环。 -
avcodec_decode_video2 ()就是把AVPacket中的视频流数据解码成图片,解码后的数据就存储在之前定义的AVFrame中,返回的是已经被解码的数据的大小(不是解码后的数据大小)。 - 有兴趣的朋友们可以去看看
avcodec_decode_video2 ()的源码,也是很简单易懂的,以后有空可以单独拿出来讲讲。
2.1.4 怎么去?
- 到现在为止所有的解码都结束了
AieVideoFrame * frame = [self handleVideoFrame];
frame.type = AieFrameTypeVideo;
-
AieVideoFrame是自定义的一个存放解码后数据的类,里面的操作就是把解码后的数据AVFrame按照一定的格式存入自定义的frame,然后再标明它的类型。
2.1.5 去哪里?
if (frame) {
[result addObject:frame];
_position = frame.position;
decodedDuration += frame.duration;
if (decodedDuration > minDuration) {
finished = YES;
}
}
- 把数据存到之前定义的数组中,然后返回这个数组。
- 最后几句代码我来解释一下,
decodedDuration存的是当前这个一帧中解码后数据的时间的总和,如果这个总和大于minDuration,那就停止解码。在项目中我传入的是0.1,意思就是我这一帧的最大时长是0.1秒,如果帧长度很大,我也只解码0.1秒的数据。
结尾
- 解码相关的都已经讲完了,
AieVideoFrame是关于显示的类,里面的具体操作,到时候一起说。 - 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
- 谢谢阅读










网友评论