iOS AVSampleBufferDisplayLayer在ijkplayer中的实现

目前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进行解码渲染。

启动播放后,播放器会优先根据用户选择创建对应的解码器,如果AVSampleBufferDisplayLayerVideoToolBox不支持该文件的解码,则再次尝试创建软件解码器,流程图如下:

iOS AVSampleBufferDisplayLayer在ijkplayer中的实现_第1张图片
图1. 解码器选择流程

1.2 AVSampleBufferDisplayLayer的创建与关联

我们选择在IJKSDLGLView初始化时创建AVSampleBufferDisplayLayer对象。由于此时并不知道播放实际使用的解码方式,因此只创建该对象,并不addIJKSDLGLView.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添加到IJKSDLGLViewlayer上:

[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,以达到音视频同步播放的效果。示意图如下:

iOS AVSampleBufferDisplayLayer在ijkplayer中的实现_第2张图片
图2. 同步机制

2.1 CMTimebaseRef创建

AVSampleBufferDisplayLayer中默认的controlTimebasenil,需要外部创建并设置:

//创建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,表示解码后立刻进行渲染,不考虑同步;如果设置为FalseAVSampleBufferDisplayLayer会根据时钟时间来渲染视频画面。

//同步显示
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帧。

四、性能比较

我们对VideoToolBoxAVSampleBufferDisplayLayer两种解码方式进行了性能测试,测试方案及结果如下:

4.1 测试环境

  • 机型:iPhone6s;
  • 网络:本地播放,无需网络;
  • 测试文件:
    1. 码率为2048kb/s,帧率为30fps1080P视频;
    2. 码率为1126kb/s,帧率为25fps720P视频;
  • 评估维度: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.264MPEG4HEVC的硬解和渲染,欢迎试用。

转载请注明:
作者金山视频云,首发 Jianshu.com


同时也欢迎大家使用我们的直播、短视频等SDK:
https://github.com/ksvc

金山云多媒体SDK相关的QQ交流群:

  • 视频云技术交流群:574179720
  • 视频云iOS技术交流:621137661

你可能感兴趣的:(iOS AVSampleBufferDisplayLayer在ijkplayer中的实现)