iOS AVSampleBufferDisplayLayer在ijkplayer中的实现

目前iOS端播放器在视频播放上大多采用VideoToolBox硬解码+OpenGL ES渲染的方案,但如果只是为了渲染而没有其他的后处理过程,推荐使用iOS 8.0推出的AVSampleBufferDisplayLayer,它既可以用来渲染解码后的视频数据,也可以直接送入未解码的视频帧,它能够同时完成解码和渲染工作,和Android MediaCodecSurface解码渲染逻辑一致。

经过金山云多媒体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不支持该文件的解码,则再次尝试创建软件解码器,流程图如下:

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

图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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容