从QQ音乐开发,探讨如何利用腾讯云SDK在直播中加入视频动画
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~
看着精彩的德甲赛事,突然裁判一声口哨,球赛断掉了,屏幕开始自动播放“吃麦趣鸡盒,看德甲比赛”的视频广告
那么问题来了,如何在直播流中,无缝的插入点播视频文件呢?
本文介绍了QQ音乐基于腾讯云AVSDK,实现互动直播插播动画的方案以及踩过的坑。
01
从产品经理给的需求说起
“开场动画?插播广告?”
不久之前,产品同学说我们要在音视频直播中,加一个开场动画。

要播放插播动画,怎么做呢?对于视频直播来说,当前直播画面流怎么处理?对于音频来说,又怎么输入一路流呢?
02
梳理技术方案
互动直播的方式,是把主播的画面推送到观众面前,而主播端的画面,既可以来自摄像头采集的数据,也可以来自其它的输入流。那么如果腾讯云的AVSDK能支持到播放输入流,就能通过在主播端本地解码一个视频文件,然后把这路流的数据推到观众端的方式,让所有的角色都能播放插播动画了。幸运的是,腾讯云AVSDK可以支持到这个特性,具体的方法有下面两种:
第一种:替换视频画面
/*!
@abstract 对本地采集视频进行预处理的回调。
@discussion 主线程回调,方面直接在回调中实现视频渲染。
@param frameData 本地采集的视频帧,对其中data数据的美颜、滤镜、特效等图像处理,会回传给SDK编码、发送,在远端收到的视频中生效。
@see QAVVideoFrame
*/
- (void)OnLocalVideoPreProcess:(QAVVideoFrame *)frameData;
主播侧本地在采集到摄像头的数据后,在编码上行到服务器之前,会提供一个接口给予业务侧做预处理的回调,所以,对于视频直播,我们可以利用这个接口,把上行输入的视频画面修改为要插播进来动画的视频帧,这样,从观众角度看,被插播了视频动画。
第二种:使用外部输入流
/*!
@abstract 开启外部视频采集功能时,向SDK传入外部采集的视频帧。
@return QAV_OK 成功。
QAV_ERR_ROOM_NOT_EXIST 房间不存在,进房后调用才生效。
QAV_ERR_DEVICE_NOT_EXIST 视频设备不存在。
QAV_ERR_FAIL 失败。
@see QAVVideoFrame
*/
- (int)fillExternalCaptureFrame:(QAVVideoFrame *)frame;
最开始时,我错误的认为,仅仅使用第二种方式就能够满足同时在音视频两种直播中插播动画的需求,但是实际实践的时候发现,如果要播放外部输入流,必须要先关闭摄像头画面。这个操作会引起腾讯云后台的视频位切换,并通过下面这个函数通知到观众端:
/*!
@abstract 房间成员状态变化通知的函数。
@discussion 当房间成员发生状态变化(如是否发音频、是否发视频等)时,会通过该函数通知业务侧。
@param eventID 状态变化id,详见QAVUpdateEvent的定义。
@param endpoints 发生状态变化的成员id列表。
*/
- (void)OnEndpointsUpdateInfo:(QAVUpdateEvent)eventID endpointlist:(NSArray *)endpoints;
视频位短时间内的切换,会导致一些时序上的问题,跟SDK侧讨论也认为不建议这样做。最终,QQ音乐采用了两个方案共存的方式。
03
视频格式选型
对于插播动画的视频文件,如果考虑到如果需要支持流式播放,码率低,高画质,可以使用H264裸流+VideoToolBox硬解的方式。如果说只播放本地文件,可以采用H264编码的mp4+AVURLAsset解码的方式。因为目前还没有流式播放的需求,而设计同学直接给到的是一个mp4文件,所以后者则看起来更合理。笔者出于个人兴趣,对两种方案的实现都做了尝试,但是也遇到了下面的一些坑,总结一下,希望能让其它同学少走点弯路:
1.分辨率与帧率的配置
视频的分辨率需要与腾讯云后台的SPEAR引擎配置中的上行分辨率一致,QQ音乐选择的视频上行配置是960x540,帧率是15帧。但是实际的播放中,发现效果并不理想,所以需要播放更高分辨率的数据,这一步可以通过更换AVSDK的角色RoleName来实现,这里不做延伸。
另外一个问题是从摄像头采集上来的数据,是下图的角度为1的图像,在渲染的时候,会默认被旋转90度,在更改视频画面时,需要保持两者的一致性。摄像头采集的数据格式是NV12,而本地填充画面的格式可以是I420。在绘制时,可以根据数据格式来判断是否需要旋转图像展示。

2.ffmpeg 转h264裸流解码问题
从iOS8开始,苹果开放了VideoToolBox,使得应用程序拥有了硬解码h264格式的能力。具体的实现与分析,可以参考《iOS-H264 硬解码》这篇文章。因为设计同学给到的是一个mp4文件,所以首先需要先把mp4转为H264的裸码流,再做解码。这里我使用ffmpeg来做转换:
ffmpeg -i test.mp4 -codec copy -bsf: h264_mp4toannexb -s 960*540 -f h264 output.264
其中,annexb就是h264裸码流Elementary Stream的格式。对于Elementary Stream,sps跟pps并没有单独的包,而是附加在I帧前面,一般长这样:
00 00 00 01 sps 00 00 00 01 pps 00 00 00 01 I 帧
VideoToolBox的硬解码一般通过以下几个步骤:
1. 读取视频流
2. 找出sps,pps的信息,创建CMVideoFormatDescriptionRef,传入下一步作为参数
3. VTDecompressionSessionCreate:创建解码会话
4. VTDecompressionSessionDecodeFrame:解码一个视频帧
5. VTDecompressionSessionInvalidate:释放解码会话
但是对上面转换后的裸码流解码,发现总是会遇到解不出来数据的问题。分析转换后的文件发现,转换后的格式并不是纯码流,而被ffmpeg加入了一些无关的信息:

但是也不是没有办法,可以使用这个工具H264Naked来找出二进制文件中的这一段数据一并删掉。再尝试,发现依然播放不了,原因是在上面的第3步解码会话创建失败了,错误码OSStatus = -5。很坑的是,这个错误码在OSStatus.com中无法查到对应的错误信息,通过对比好坏两个文件的差异发现,解码失败的文件中,pps 前面的 startcode并不是3个0开头的,而是这样子
00 00 00 01 sps 00 00 01 pps 00 00 00 01 I 帧
但是实际上,通过查看h264的官方文档,发现两种形式都是正确的

而我只考虑了第一种情况,却忽略了第二种,导致解出来的pps数据错了。通过手动插入一个00,或者解码器兼容这种情况,都可以解决这个问题。但是同时也看出,这种方式很不直观。所以也就引入了下面的第二种方法。
\3. AVAssetReader 解码视频
使用AVAssetReader解码出yuv比较简单,下面直接贴出代码:
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:path] options:nil];
NSError *error;
AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack* videoTrack = [videoTracks objectAtIndex:0];
int m_pixelFormatType = kCVPixelFormatType_420YpCbCr8Planar;
NSDictionary* options = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt: (int)m_pixelFormatType] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput* videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];
[reader addOutput:videoReaderOutput];
[reader startReading];
// 读取视频每一个buffer转换成CGImageRef
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
CMSampleBufferRef sampleBuff = [videoReaderOutput copyNextSampleBuffer];
// 对sampleBuff 做点什么
});
这里只说遇到的坑,有的mp4视频解码后绘制时会有一个迷之绿条,就像下面这个图

这是为什么,代码实现如下所示,我们先取出y分量的数据,再取出uv分量的数据,看起来没有问题,但是这实际上却不是我们的视频格式对应的数据存储方式。
// 首先把Samplebuff转成cvBufferRef, cvBufferRef中存储了像素缓冲区的数据
CVImageBufferRef cvBufferRef = CMSampleBufferGetImageBuffer(sampleBuff);
// 锁定地址,这样才能之后从主存访问到数据
CVPixelBufferLockBaseAddress(cvBufferRef, kCVPixelBufferLock_ReadOnly);
// 获取y分量的数据
unsigned char *y_frame = (unsigned char*)CVPixelBufferGetBaseAddressOfPlane(cvBufferRef, 0);
// 获取uv分量的数据
unsigned char *uv_frame = (unsigned char*)CVPixelBufferGetBaseAddressOfPlane(cvBufferRef, 1);
这份代码cvBufferRef中存储数据格式应该是:
typedef struct CVPlanarPixelBufferInfo_YCbCrPlanar CVPlanarPixelBufferInfo_YCbCrPlanar;
struct CVPlanarPixelBufferInfo_YCbCrBiPlanar {
CVPlanarComponentInfo componentInfoY;
CVPlanarComponentInfo componentInfoCbCr;
};
然而第一份代码中,使用的pixelFormatType是kCVPixelFormatType_420YpCbCr8Planar,存储的数据格式却是:
typedef struct CVPlanarPixelBufferInfo CVPlanarPixelBufferInfo;
struct CVPlanarPixelBufferInfo_YCbCrPlanar {
CVPlanarComponentInfo componentInfoY;
CVPlanarComponentInfo componentInfoCb;
CVPlanarComponentInfo componentInfoCr;
};
也就是说,这里应该把yuv按照三个分量来解码,而不是两个分量。 实现正确的解码方式,成功消除了绿条。

至此,遇到的坑就都踩完了,效果也不错。
最后,希望这篇文章能够对你有所帮助,在直播开发上,少走点弯路
相关阅读
欲练JS,必先攻CSS
交互微动效设计指南
【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识
此文已由作者授权腾讯云+社区发布,更多原文请点击
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
海量技术实践经验,尽在云加社区!
从QQ音乐开发,探讨如何利用腾讯云SDK在直播中加入视频动画的更多相关文章
- 如何利用腾讯云COS为静态博客添加动态相册
前言 本文首发于个人网站Jianger's Blog,欢迎访问订阅.个人博客小站刚建站不久,想着除了主题里的功能外再添加上相册模块,于是半搜索半摸索把相册模块搞出来了,最后采用了利用腾讯云对象存储作图 ...
- 在腾讯云容器服务 TKE 中利用 HPA 实现业务的弹性伸缩
在 TKE 上利用 HPA 实现业务的弹性伸缩 概述 Kubernetes Pod 水平自动扩缩(Horizontal Pod Autoscaler,以下简称 HPA)可以基于 CPU 利用率.内存利 ...
- Taro开发微信小程序之利用腾讯地图sdk标记
首先要下载腾讯地图提供的sdk,放在项目的对应目录下,引用. import QQMapWX from '../../sdks/qqmap-wx-jssdk' 设置好后,就可以开始使用了. let qq ...
- 利用腾讯云COS云对象存储定时远程备份网站
版权声明:本文由张戈 原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/942851001487125915 来源:腾云阁 h ...
- 利用腾讯云免费证书打造全https站
什么是https? 超文本传输安全协议(Hypertext Transfer Protocol Secure,缩写为HTTPS)是一种网络安全传输协议http是HTTP协议运行在TCP之上,所有传输的 ...
- 利用腾讯云为你的域名申请并配置免费SSL一年
我想,点进来的朋友,应该都知道SSL的重要性吧.这里就简单提一下,大型网站域名只有配置了SSL后,才会更加安全. 现在,微信小程序也开始要求后台必须是SSL配置后的域名了.说了这么多,估计有些人还是有 ...
- 利用腾讯云函数部署.Net 5米游社原神每日签到功能
自从GitHub批量禁止滥用Action功能后,项目不得不考虑另外方案执行应用.其中腾讯云函数被大家作为不错的选择(虽然马上也要收费了). 但对于.Net的部署目前资源很少,而且我也没学过bash.在 ...
- QQ音乐PB级ClickHouse实时数据平台架构演进之路
导语 | OLAP(On-Line Analytical Processing),是数据仓库系统的主要应用形式,帮助分析人员多角度分析数据,挖掘数据价值.本文基于QQ音乐海量大数据实时分析场景,通过Q ...
- QQ音乐Android客户端Web页面通用性能优化实践
QQ音乐 Android 客户端的 Web 页面日均 PV 达到千万量级,然而页面的打开耗时与 Native 页面相距甚远,需要系统性优化.本文将介绍 QQ 音乐 Android 客户端在进行 Web ...
随机推荐
- 计算几何---凸包问题(Graham/Andrew Scan )
概念 凸包(Convex Hull)是一个计算几何(图形学)中的概念.用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边型,它能包含点集中所有点的.严谨的定义和相关概念参 ...
- windows 10 下配置安装node.js
环境配置 node.js windows10 25.5k 次阅读 · 读完需要 6 分钟 5 在去年就自己配置安装过node.js,但是使用npm安装模块时安装成功后调用require('mo ...
- 数据统计--union all 执行多条sql
需求--统计hive某张表type字段不同取值的数据量 我们已知某张表的type的取值是1,2,3,4,5,想要统计不同type的数据量,并清晰的展现出来.可以通过union all 的方式,sql如 ...
- PICT用户手册 [转]
PICT 3.3 User's Guide Jacek Czerwonka, Test Lead, Microsoft Corporation Overview Using PICT to Combi ...
- 使用jQuery实现一个类似GridView的编辑,更新,取消和删除的功能
先来看看下面实时效果演示: 用户点击编辑时,在点击行下动态产生一行.编辑铵钮变为disabled.新产生的一行有更新和取消的铵钮,点击“取消”铵钮,删除刚刚动态产生的行.编辑铵钮状态恢复. 更新与删除 ...
- 「雅礼集训 2017 Day1」 解题报告
「雅礼集训 2017 Day1」市场 挺神仙的一题.涉及区间加.区间除.区间最小值和区间和.虽然标算就是暴力,但是复杂度是有保证的. 我们知道如果线段树上的一个结点,\(max=min\) 或者 \( ...
- Jmeter 结构、原理介绍
Jmeter结构.原理介绍 一.Jmeter 简介 1.是基于java语言的开源的应用软件. 2.可以进行接口测试.性能测试.接口及性能的自动化测试. 二.Jmeter体系结构 元件:可以理解为每一个 ...
- Swift5 语言参考(七) 属性
属性提供有关声明或类型的更多信息.Swift中有两种属性,即适用于声明的属性和适用于类型的属性. 您可以通过编写@符号后跟属性的名称以及属性接受的任何参数来指定属性: @attribute name ...
- poi 读取使用 Strict Open XML 保存的 excel 文档
poi 读取使用 Strict Open XML 保存的 excel 文档 某项目有一个功能需要读取 excel 报表内容,使用poi读取时报错: 具体错误为: org.apache.poi.POIX ...
- MyBatis全局配置文件标签详解
一.全局配置文件结构 configuration 配置 properties 属性:可以加载properties配置文件的信息 settings 设置:可以设置mybatis的全局属性 typeAli ...