上一篇写了关于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会很大,下载的时候比较费时。
- 谢谢阅读
网友评论