往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)
✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✏️ 记录一场鸿蒙开发岗位面试经历~
✏️ 持续更新中……
概述
在应用中短视频快速切换时容易出现时延过长的问题,本文就此问题提供了相应的解决方案。
该解决方案使用:
- 视频播放框架AVPlayer和滑块视图容器Swiper进行短视频滑动轮播切换。
- 绘制组件XComponent的Surface类型动态渲染视频流。
- 使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果,(在冷启动过程中创建一个AVPlayer并进行数据初始化到prepared阶段,在轮播过程中,每次异步创建一个播放器为下一个视频播放做准备)。
最终实现短视频快速切换起播时延达到≤230ms的目标效果。
说明
如果开发者使用自研播放器引擎而非AVPlayer,也可以参考该解决方案思路实现优化。
效果展示
图1 在线短视频滑动切换效果图
场景说明
适用范围
适用于应用中在线短视频快速切换,容易出现快速切换播放起播慢体验不佳的场景。
场景体验指标
起播时延计时标准
1、以用户滑动屏幕后抬手,手指离屏时刻为起点,以视频第二帧画面显示时刻为终点(不是封面帧)。
2、转场动画时长建议设置300ms。
3、在动画开始时使用预先准备的播放器起播,起播时延控制在230ms内。
描述 | 应用内滑动视频,新视频起播时延应≤230ms。 |
---|---|
类型 | 规则 |
适用设备 | 手机、折叠屏、平板 |
说明 | 无 |
场景分析
典型场景及优化方案
典型场景描述
短视频:以小于5分钟的短视频为例进行说明
- 应用内滑动视频,新视频起播时延≤230ms(不包含滑动动画效果耗时)。
- 起点时间:滑动离手时间。
- 终点时间:视频内容开始播放,画面发生变化的时间。
场景优化方案
AVPlayer:
- 数据懒加载
在线短视频预加载,冷启动时创建第一个播放器,播放当前视频时预加载下一个播放视频(预加载下一个视频的时候会使用户的流量消耗增加,需要开发者自行决策),绘制组件XComponent的Surface类型将视频流进行动态渲染、使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果。
- 异步在线视频预加载
在轮播过程中,对下一个视频提前进入AVPlayer的prepared状态。
- 在线视频播放预接力
滑动过程中手指离开屏幕,此时滑动动效开始播放,在动效开始时就可以调用AVPlayer的play方法进行播放。
三方自研播放器:
- 数据懒加载
在线短视频预加载,冷启动时创建第一个播放器,播放当前视频时预加载下一个播放视频(预加载下一个视频的时候会使用户的流量消耗增加,需要开发者自行决策),绘制组件XComponent的Surface类型将视频流进行动态渲染、使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果。
- 异步在线视频预加载
在轮播过程中,对下一个视频提前初始化播放器所需内容(视频源下载、AudioRender初始化、解码器初始化等),并对视频提前预解析首帧画面。
- 在线视频播放预接力
滑动过程中手指离开屏幕,此时滑动动效开始播放,在动效开始时就可以调用播放引擎进行播放。 为了保证用户的起播体验,在前几帧画面送显时应优先送显,而不是等AudioRender写入音频数据才送显,因为音频硬件时延比显示时延大。播放起始几帧建议不要做强音画同步,而是采用慢追帧策略进行同步,视频帧稍微增大送显间隔,直到完成音画同步。
场景实现
场景整体介绍
基于AVPlayer实现了在线流媒体的短视频流畅播放和控制功能。基于对应的播放器,使用滑块视图容器Swiper进行短视频滑动轮播切换、绘制组件XComponent的Surface类型将视频流进行动态渲染、懒加载,最终实现短视频快速切换,实现起播≤230ms,提供开发者解决此类问题的方案。
图2 功能时序图
在线短视频快速切换
图3 实现流程图
关键点
AVPlayer
AVPlayer可以将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。
LazyForEach数据懒加载
LazyForEach懒加载可以通过设置cachedCount属性来指定缓存数量(目前设置为3),同时搭配组件复用能力以达到高性能效果。
SurfaceID每次都会创建,不共用SurfaceID,AVPlayer也会同时创建, 不共用AVPlayer,进而将提前加载好的视频(prepared阶段)放到缓存池中。
在通过Swiper切换时,会根据当前轮询滑动的窗口索引index到缓存池中找到对应的视频(prepared阶段),直接进行播放,从而能提高切换性能。
图4 视频懒加载示意图
异步视频预加载
异步视频预加载:在Swiper轮播过程中,在播放当前视频时,提前加载好下一个视频,在缓存中同时存在多个播放器实例,根据视频当前的索引来确定使用缓存中的哪个播放器来播放,从而达到流畅切换的效果。
(1)本地播放一个短视频的耗时。
图5 单视频加载示意图
(2)播放视频A的时候,提前预加载视频B。在切换短视频时,可以马上开始播放已预加载完成的视频B,从而减少了切换时间,提高了切换性能。
图6 异步视频预加载示意图
视频播放预启动接力
为了进一步提升滑动播放体验,在动效开始时就开始播放,做到动效和播放并行进行:
(1)在收到AnimationStart回调时开始播放,而不是动效结束再播放;
(2)不要用默认的弹簧曲线(弹簧动效有560ms,视频窗口在400ms左右已经完全铺开了,最后150ms位移随时间变化较小),可以把curve改成Curve.Ease, duration改成300(视APP UX确定);
视频播放预启动接力:类似于4*100接力赛,想要尽快完成接力赛,当第一个选手快到达终点时,第二个选手就提前起跑并且和第一个选手完美完成接力棒,从而减少整个接力赛过程中的时间。短视频切换也是如此,如下图所示:
图7 视频播放预启动接力示意图
开发步骤
- 通过组件复用的形式实现单个视频播放的自定义组件VideoPlayView。
// Key point: Reuse custom video playback components by using the @Reusable decorator.
@Reusable
@Component
export struct VideoPlayView {
@Prop @Watch('onIndexChange') curIndex: number = -1;
在自定义组件VideoPlayView中设置XComponent组件用于视频流渲染,获取并设置SurfaceID用于设置显示画面。在onLoad时异步创建并初始化AVPlayer播放器使其提前进入prepared状态以实现视频的异步预加载。
XComponent({
id: 'player',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.width(this.XComponentWidth)
.height(this.XComponentHeight)
.onLoad(async () => {
this.surfaceID = this.xComponentController.getXComponentSurfaceId();
hilog.info(0x0000, TAG,
`surfaceID: ${this.surfaceID}, curIndex: ${this.curIndex}, index: ${this.index}.`);
// Key point: Initialize the AVPlayer asynchronously so that the AVPlayer enters the prepared state in advance to implement asynchronous video preloading.
this.initAVPlayer();
})
- 在Swiper组件中使用LazyForEach懒加载自定义组件VideoPlayView 的方式进行视频轮播。
通过LazyForEach懒加载VideoPlayView自定义组件方式为保证每一个视频有单独的SurfaceID和AVPlayer(位于VideoPlayView自定义组件中)。
通过设置cachedCount属性,结合上一步在XComponent的onLoad中异步初始化AVPlayer,使视频提前进入prepared状态以实现视频的异步预加载。
视频切换时,在onAnimationStart阶段即更新当前窗口索引curIndex,开始播放下一个视频,从而实现视频播放预接力。
设置弹簧曲线为.curve(Curve.Ease)。
Swiper(this.swiperController) {
// Key point: Use LazyForEach to create an independent SurfaceID in the VideoPlayView component. (The AVPlayer is created in the VideoPlayView and does not share the AVPlayer.)
LazyForEach(new AVDataSource(Const.VIDEO_SOURCE), (item: string, index: number) => {
VideoPlayView({
curSource: item,
curIndex: this.curIndex,
index: index,
firstFlag: this.firstFlag,
isPageShow: this.isPageShow,
foldStatus: this.foldStatus
})
}, (item: string, index: number) => JSON.stringify(item) + index)
}
// Key point: Set cachedCount to implement preloading.
.cachedCount(this.firstFlag ? 0 : 2)
.width('100%')
.height('100%')
.vertical(true)
.loop(true)
// Key point: Change the spring curve to Curve.Ease.
.curve(Curve.Ease)
.duration(300)
.indicator(false)
.backgroundColor(Color.Black)
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
hilog.info(0x0000, TAG, `onGestureSwipe index: ${index}, extraInfo: ${extraInfo}.`);
})
.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {
hilog.info(0x0000, TAG,
`onAnimationStart index: ${index}, targetIndex: ${targetIndex}, extraInfo: ${extraInfo}.`);
// Key point: The curIndex is updated at AnimationStart and the next video starts to be played.
this.curIndex = targetIndex;
})
- 视频播放预接力。
在自定义组件VideoPlayView中通过@Watch装饰器监听Swiper轮播的this.curIndex值,在视频缓存流中跟this.index进行比较,从而判断视频流中哪个播放,其余的均暂停。
@Prop @Watch('onIndexChange') curIndex: number = -1;
onIndexChange() {
hilog.info(0x0000, TAG,
`enter onIndexChange. curIndex: ${this.curIndex}, index: ${this.index}, isPageShow: ${this.isPageShow}.`);
if (this.curIndex !== this.index) {
pauseVideo(this.avPlayer, this.curIndex, this.index);
this.isPlaying = false;
this.trackThicknessSize = Const.TRACK_SIZE_MIN;
} else {
hilog.info(0x0000, TAG,
`enter indexChange play. curIndex: ${this.curIndex}, index: ${this.index}, isPageShow: ${this.isPageShow}.`);
// Key point: When the index(curIndex) of the current window is the same as the index of the this, the playback starts.
if (this.flag === true) {
playVideo(this.avPlayer, this.curIndex, this.index);
this.isPlaying = true;
this.trackThicknessSize = Const.TRACK_SIZE_MIN;
} else {
let countNum = 0;
let intervalFlag = setInterval(() => {
countNum++;
if (this.curIndex !== this.index) {
hilog.info(0x0000, TAG, `enter indexChange play error, clearIntreval. flag: ${this.flag},
curIndex: ${this.curIndex}, index: ${this.index}.`);
clearInterval(intervalFlag);
}
if (this.flag === true && this.isPageShow) {
countNum = 0;
playVideo(this.avPlayer, this.curIndex, this.index);
this.isPlaying = true;
this.trackThicknessSize = Const.TRACK_SIZE_MIN;
clearInterval(intervalFlag);
} else {
hilog.info(0x0000, TAG, `enter indexChange play error, clearIntreval. countNum: ${countNum},
flag: ${this.flag}, curIndex: ${this.curIndex}, index: ${this.index}.`);
if (countNum > 15) {
hilog.info(0x0000, TAG,
`enter indexChange play error, reinit initAVPlayer. countNum: ${countNum}, flag: ${this.flag},
curIndex: ${this.curIndex}, index: ${this.index}.`);
countNum = 0;
this.initAVPlayer();
}
}
}, 100);
}
}
}
总结
本文介绍了数据懒加载、异步在线视频预加载以及在线视频播放预接力等优化方案,可以帮助开发者解决快速切换播放时延过长的问题。另外,开发者可以基于 SwipePlayer 库快速实现短视频流畅滑动的场景开发体验,可以更加聚焦实际场景业务的开发。