目前iOS端播放器在视频播放上大多采用VideoToolBox硬解码+OpenGL ES渲染的方案,但如果只是为了渲染而没有其他的后处理过程,推荐使用iOS 8.0推出的AVSampleBufferDisplayLayer,它既可以用来渲染解码后的视频数据,也可以直接送入未解码的视频帧,它能够同时完成解码和渲染工作,和Android MediaCodec接Surface解码渲染逻辑一致。
经过金山云多媒体SDK团队测试,AVSampleBufferDisplayLayer性能表现要优于使用VideoToolBox的方案。
ijkplayer由于其开源、跨平台、功能丰富稳定等特性,被广泛用于移动端。本文则要介绍如何在ijkplayer中集成AVSampleBufferDisplayLayer。
ijkplayer中的解码框架在我们的另一篇文章ijkplayer框架深入剖析中第3.2章节有介绍,本文不再赘述。
一、AVSampleBufferDisplayLayer集成
1.1 总体思路
IJKFF_Pipenode
结构体相当于C++中的虚基类,它定义了解码器的基本方法,在ijkplayer中,已经有两个文件实现了该基类:
-
ffpipenode_ffplay_vdec.c
,提供基于ffmpeg的软解功能; -
ffpipenode_ios_videotoolbox_vdec.m
,提供基于VideoToolBox的硬解功能;
如果要增加新的解码器,则要实现该基类,我们定义该文件为ffpipenode_ios_displaylayer.m
,其完成的主要功能是从PacketQueue
中获取视频数据,并送给AVSampleBufferDisplayLayer进行解码渲染。
启动播放后,播放器会优先根据用户选择创建对应的解码器,如果AVSampleBufferDisplayLayer和VideoToolBox不支持该文件的解码,则再次尝试创建软件解码器,流程图如下:
1.2 AVSampleBufferDisplayLayer的创建与关联
我们选择在IJKSDLGLView
初始化时创建AVSampleBufferDisplayLayer对象。由于此时并不知道播放实际使用的解码方式,因此只创建该对象,并不add到IJKSDLGLView.layer
上。
AVSampleBufferDisplayLayer *_avsplayer;
_avsplayer = [[AVSampleBufferDisplayLayer alloc] init];
_avsplayer.frame = self.bounds;
_avsplayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
_avsplayer.videoGravity = AVLayerVideoGravityResizeAspect;
_avsplayer.opaque = YES;
_avsplayer.backgroundColor = [UIColor blackColor].CGColor;
当启动播放后确定使用AVSampleBufferDisplayLayer解码方式,再将_avsplayer
添加到IJKSDLGLView
的layer
上:
[self.layer addSublayer:_avsplayer];
如果解码失败需要切换到软解,则从IJKSDLGLView
上移除_avsplayer
:
[_avsplayer removeFromSuperlayer];
1.3 送入待解码数据
由于送入待解码数据时,从PacketQueue
中获取到视频帧数据,并非是CMSampleBufferRef
封装的,首先需要将其封装为CMSampleBufferRef
形式:
CMBlockBufferRef newBBufOut = NULL;
CMSampleBufferRef sBufOut = NULL;
CMBlockBufferCreateWithMemoryBlock(NULL, buffer, size, kCFAllocatorNull, NULL, 0, size, FALSE, &newBBufOut);
CMSampleBufferCreate(NULL,newBBufOut, TRUE, 0, 0, fmt_desc, 1, 0, NULL, 0, NULL, &sBufOut);
然后调用enqueueSampleBuffer
方法送入待解码数据:
[_avsplayer enqueueSampleBuffer: sBufOut];
_avsplayer
就是在IJKSDLGLView
中创建的对象。
二、同步处理
同步处理是播放过程中必不可少的步骤,通常该步骤都是基于解码后的原始音视频频数据的pts进行的,但是使用AVSampleBufferDisplayer时,我们无法得到解码后的yuv数据,此时如何进行同步呢?
AVSampleBufferDisplayLayer中提供了一个时钟相关的属性:
@property (retain, nullable) __attribute__((NSObject)) CMTimebaseRef controlTimebase;
我们可以通过设置该属性完成同步的工作,基本思路就是在音频设备通过callback
获取Audio Frame
时,将待送入输出的Audio Frame PTS
设置给AVSampleBufferDisplayLayer.controlTimebase
,用于设置和校验MasterClock,以达到音视频同步播放的效果。示意图如下:
2.1 CMTimebaseRef创建
AVSampleBufferDisplayLayer中默认的controlTimebase
为nil
,需要外部创建并设置:
//创建CMTimebaseRef对象
CMTimebaseRef controlTimebase;
CMTimebaseCreateWithMasterClock( CFAllocatorGetDefault(), CMClockGetHostTimeClock(), &controlTimebase);
//设置给AVSampleBufferDisplayLayer对象
_avsplayer.controlTimebase = controlTimebase;
2.2 时钟的设置
CMTimebaseRef
提供了两个必用方法:
- 设置时钟基准时间
CM_EXPORT OSStatus
CMTimebaseSetTime(
CMTimebaseRef CM_NONNULL timebase,
CMTime time )
- 设置时钟速率
CM_EXPORT OSStatus
CMTimebaseSetRate(
CMTimebaseRef CM_NONNULL timebase,
Float64 rate )
在创建时钟时,音频可能还尚未开始播放,因此需先将时钟置于暂停状态,
CMTimebaseSetRate(_avslayer.controlTimebase, 0);
渲染第一帧音频时,将时钟置于1倍速状态,
CMTimebaseSetRate(_avslayer.controlTimebase, 1);
在渲染每一帧音频时,需要将音频时钟时间设置给时钟。
CMTimebaseSetTime(_avslayer.controlTimebase, CMTimeMake(audiopts, TIMEBASE_SCALE));
说明: 实际实现过程中不必每次都需要将音频时间戳设置给AVSamplebufferDisplayLayer的时钟,可以先判断下当前时间与音频时间戳的差值,如果在容忍范围内,则无需设置**
2.3 CMSampleBufferRef的设置
2.2章节中说明了AVSampleBufferDisplayLayer的时钟设置方法,同理需要给待解码的视频帧设置时间戳以达到同步效果。
CMSampleBuffer的附件字典中有关键字:
kCMSampleAttachmentKey_DisplayImmediately
如果该关键字的值为True
,表示解码后立刻进行渲染,不考虑同步;如果设置为False
,AVSampleBufferDisplayLayer
会根据时钟时间来渲染视频画面。
//同步显示
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanFalse);
//设置CMSampleBufferRef的时间戳
CMSampleBufferSetOutputPresentationTimeStamp(sampleBuffer, CMTimeMake(pts, TIMEBASE_SCALE));
三、后台播放
使用AVSampleBufferDisplayLayer解码时,如果APP切入后台,会造成渲染失败,出现黑屏的现象,再次回到前台时,依然是黑屏的现象,针对这个问题,并不需要重新创建AVSampleBufferDisplayLayer对象。
官方提供的解决方案是:如果AVSampleBufferDisplayLayer的状态为failed,可以使用flush方法来重新让layer渲染生效。
if(AVQueuedSampleBufferRenderingStatusFailed == _avslayer.status)
[_avslayer flush];
调用flush方法后,送给AVSampleBufferDisplayLayer的第一个视频帧应为I帧,如果不是I帧,则无法成功解码,会黑屏一段时间,直到送入新I帧。
四、性能比较
我们对VideoToolBox和AVSampleBufferDisplayLayer两种解码方式进行了性能测试,测试方案及结果如下:
4.1 测试环境
- 机型:iPhone6s;
- 网络:本地播放,无需网络;
- 测试文件:
- 码率为2048kb/s,帧率为30fps的1080P视频;
- 码率为1126kb/s,帧率为25fps的720P视频;
- 评估维度:cpu占用、内存占用、发热情况及耗电量(由于使用AVSampleBufferDisplayLayer解码方式时获取不到GPU占用,因此GPU不作为评估维度);
- 测试步骤:本地循环播放测试文件,总测试时长为60分钟,每分钟采集一次内存和cpu占用情况,开始测试和结束测试时采集温度(前屏)和电量。
4.2 测试结果
统计结果中的内存和cpu占用值是对每分钟采集的数据取均值,耗电量和发热为测试结束时与开始时对应的差值。
- 1080P视频的测试结果
Type | AVSampleBufferDisplayLayer | VideoToolBox |
---|---|---|
CPU占用 | 20.30% | 24% |
内存占用 | 32.14MB | 40.00MB |
耗电量 | -4% | -12% |
发热情况 | +1.8度 | +3.5度 |
- 720P视频的测试结果
Type | AVSampleBufferDisplayLayer | * VideoToolBox* |
---|---|---|
CPU占用 | 19.49% | 22.52% |
内存占用 | 30.87MB | 40.06MB |
耗电量 | -2% | -9% |
发热情况 | +1.5度 | +2.5度 |
从上述结果可以看出,使用AVSampleBufferDisplayLayer解码的播放器在性能表现上要明显优于使用VideoToolBox,上述数据是在全功能demo中测试得出的,相信在精简版上差距会更加明显。
五、结束语
金山云多媒体SDK团队定义本解码方案为高性能硬解,区别于
VideoToolBox硬解和FFmpeg软解。高性能硬解的实现,是牺牲了解码后YUV数据可见为前提的,这意味着解码后的画面旋转、画面镜像、实时裁剪、视频后处理滤镜、录屏等工作都没办法实现。在业务使用时,请根据需求取舍。
本文只是简单介绍了如何在ijkplayer中集成AVSampleDisplayDisplayLayer完成视频解码和渲染的工作,还有许多细节的地方尚未考虑和优化,希望您能够给提供更优秀的解决方法或思路。欢迎大家加入我们QQ群一起讨论。
KSYMediaPlayer_iOS已经完全实现了AVSampleBufferDisplayLayer,支持H.264、MPEG4、HEVC的硬解和渲染,欢迎试用。
转载请注明:
作者金山视频云,首发 Jianshu.com
同时也欢迎大家使用我们的直播、短视频等SDK:
https://github.com/ksvc
金山云多媒体SDK相关的QQ交流群:
- 视频云技术交流群:574179720
- 视频云iOS技术交流:621137661