1# 在线短视频流畅切换
2
3## 简介
4为了帮助开发者解决在应用中在线短视频快速切换时容易出现快速切换播放时延过长的问题,将提供对应场景的解决方案。
5
6该解决方案使用:
7- 视频播放框架AVPlayer和滑块视图容器Swiper进行短视频滑动轮播切换。
8- 绘制组件XComponent的Surface类型动态渲染视频流。
9- 使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果,(在冷启动过程中创建一个AVPlayer并进行数据初始化到prepared阶段,在轮播过程中,每次异步创建一个播放器为下一个视频播放做准备)。
10
11最终实现短视频快速切换起播时延达到≤230ms的效果。
12
13**如果开发者使用自研播放器引擎而非AVPlayer,也可以参考该解决方案思路达成最佳实践**。
14
15## 效果展示
16
17在线短视频滑动切换
18
19![performance-quick-switch-short-video-image0.gif](figures/performance-quick-switch-short-video-image0.gif)
20
21## 场景说明
22
23### 适用范围
24
25适用于应用中在线短视频快速切换,容易出现快速切换播放起播慢体验不佳的场景。
26
27### 场景体验指标
28
29起播时延计时标准
30
311. 以用户滑动屏幕后抬手,手指离屏时刻为起点,以视频第二帧画面显示时刻为终点(不是封面帧)。
32
332. 转场动画时长建议设置300ms。
34
353. 在动画开始时使用预先准备的播放器起播,起播时延控制在230ms内。
36
37| **描述**   | **类型**        | **适用设备** | **说明** |
38|----------|---------------|----------|--------|
39| 应用内滑动视频,新视频起播时延应≤230ms。 | 规则 | 手机、折叠屏、平板   | 无  |
40
41## 场景分析
42### 典型场景及优化方案
43
44**典型场景描述**
45
46短视频:以小于5分钟的短视频为例进行说明
47
481. 应用内滑动视频,新视频起播时延≤230ms(不包含滑动动画效果耗时)。
492. 起点时间:滑动离手;时间终点:视频内容开始播放,画面发生变化。
50
51**场景优化方案**
52
53AVPlayer:
54
551. 数据懒加载
56
57   在线短视频预加载,冷启动时创建第一个播放器,播放当前视频时预加载下一个播放视频,绘制组件XComponent的Surface类型将视频流进行动态渲染、使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果。
582. 异步在线视频预加载
59
60   在轮播过程中,对下一个视频提前进入AVPlayer的prepared状态。
613. 在线视频播放预接力
62
63   滑动过程中手指离开屏幕,此时滑动动效开始播放,在动效开始时就可以调用AVPlayer的play方法进行播放。
64
65三方自研播放器:
66
671. 数据懒加载
68
69   在线短视频预加载,冷启动时创建第一个播放器,播放当前视频时预加载下一个播放视频,绘制组件XComponent的Surface类型将视频流进行动态渲染、使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果。
702. 异步在线视频预加载
71
72   在轮播过程中,对下一个视频提前初始化播放器所需内容(视频源下载、AudioRender初始化、解码器初始化等),并对视频提前预解析首帧画面。
733. 在线视频播放预接力
74
75   滑动过程中手指离开屏幕,此时滑动动效开始播放,在动效开始时就可以调用播放引擎进行播放。
76   为了保证用户的起播体验,在前几帧画面送显时应优先送显,而不是等AudioRender写入音频数据才送显,因为音频硬件时延比显示时延大。播放起始几帧建议不要做强音画同步,而是采用慢追帧策略进行同步,视频帧稍微增大送显间隔,直到完成音画同步。
77
78## 场景实现
79
80### 场景整体介绍
81基于AVPlayer实现了在线流媒体的短视频流畅播放和控制功能。基于对应的播放器,使用滑块视图容器Swiper进行短视频滑动轮播切换、绘制组件XComponent的Surface类型将视频流进行动态渲染、懒加载,最终实现短视频快速切换,实现起播≤230ms,提供开发者解决此类问题的方案。
82
83**功能时序图**
84
85![](../performance/figures/performance-quick-switch-short-video-image1.png)
86
87
88### 在线短视频快速切换
89**实现流程图**
90
91![](../performance/figures/performance-quick-switch-short-video-image2.png)
92
93**关键点**
94
95**AVPlayer**
96
97AVPlayer可以将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。
98
99**LazyForEach数据懒加载**
100
101LazyForEach数据懒加载可以通过设置cachedCount属性来指定缓存数量(目前设置为3),同时搭配组件复用能力以达到高性能效果。SurfaceID每次都会创建,不共用SurfaceID,AVPlayer也会同时创建,不共用AVPlayer,进而将提前加载好的视频(prepared阶段)放到缓存池中。
102在通过Swiper切换时,会根据当前轮询滑动的窗口索引index到缓存池中找到对应的视频(prepared阶段),直接进行播放,从而提升切换性能。
103
104![](../performance/figures/performance-quick-switch-short-video-image3.png)
105
106**异步视频预加载**
107
108异步视频预加载:在Swiper轮播过程中,在播放当前视频时,提前加载好下一个视频,在缓存中同时存在多个播放器实例,根据视频当前的索引来确定使用缓存中的哪个播放器来播放,从而达到流畅切换的效果。
109
110(1)本地播放一个短视频的耗时。
111
112![](../performance/figures/performance-quick-switch-short-video-image4.png)
113
114(2)播放视频A的时候,提前预加载视频B。在切换短视频时,可以马上开始播放已预加载完成的视频B,从而减少了切换时间,提高了切换性能。
115
116![](../performance/figures/performance-quick-switch-short-video-image5.png)
117
118
119**视频播放预启动能力**
120
121为了进一步提升滑动播放体验,在动效开始时就开始播放,做到动效和播放并行进行:
122
123(1)在收到AnimationStart回调时开始播放,而不是动效结束再播放;
124
125(2)不要用默认的弹簧曲线(弹簧动效有560ms,视频窗口在400ms左右已经全面铺开了,最后150ms位移随时间变化较小),可以把curve改成Curve.Ease,duration改为300ms(视APP UX确定);
126
127视频播放预启动接力:类似于4*100接力赛,想要尽快完成接力赛,当第一个选手快到达终点时,第二个选手就提前起跑并且和第一个选手完美完成接力棒,从而减少整个接力赛过程中的时间。短视频切换也是如此,如下图所示:
128
129![](../performance/figures/performance-quick-switch-short-video-image6.png)
130
131**关键代码片段**
132
1331. 初始化AVPlayer播放器。
134   ```typescript
135   async initAVPlayer() {
136     Logger.info(TAG, 'createAVPlayer begin');
137     media.createAVPlayer().then((video: media.AVPlayer) => {
138       if (video !== null) {
139         this.avPlayer = video;
140         this.setAVPlayerCallback(this.avPlayer);
141         // 设置播放源,使其进入initialized状态
142         if (typeof this.curSource === 'string') {
143           this.avPlayer.url = this.curSource;
144         } else {
145           this.avPlayer.fdSrc = this.curSource;
146         }
147         Loggor.info(TAG, 'createAVPlayer success');
148       } else {
149         Loggor.error(TAG, 'createAVPlayer fail');
150       }
151     }).catch((error: BusinessError) => {
152       Logger.error(TAG, `AVPlayer catchCallback,error message:${error.message}`);
153     })
154   }
155   ```
1562. 设置业务需要的监听事件。
157   ```typescript
158   setAVPlayerCallback(avPlayer: media.avPlayer) {
159     // 用于进度条,监听进度条当前位置,刷新当前时间
160     avPlayer.on('timeUpdate', (time: number) => {
161       if (!this.isSliderMoving) {
162         this.currentTime = Math.floor(time * this.durationTime / this.duration);
163         this.currentStringTime = secondToTime(Math.floor((time / CommConstants.SECOND_TO_MS)));
164       }
165     })
166     // 适配一多,根据屏幕尺寸的变化同步更新视频的长宽
167     avPlayer.on('videoSizeChange', (width: number, height: number) => {
168       this.viewHeight = height;
169       this.viewWidth = width;
170       this.autoVideoSize();
171     })
172     // 必要事件,监听播放器的错误信息
173     avPlayer.on('error', (error: BusinessError) => {
174       Logger.error(TAG,
175         `Invoke avPlayer failed, code is ${error.code},message is ${error.message}` + `---state:${avPlayer.state}`);
176       avPlayer.reset();
177     })
178     this.setAVPlayerStateListen(avPlayer);
179   }
180   ```
1813. 设置状态机变化回调函数。
182   ```typescript
183   setAVPlayerStateListen(avPlayer: media.AVPlayer) {
184     avPlayer.on('stateChange', async (state: string) => {
185       switch (state) {
186         case 'idle': // 成功调用reset接口后触发该状态机上报
187           Logger.info(TAG, 'AVPlayer state idle called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
188           break;
189         case 'initialized': // avplayer 设置播放源后触发该状态上报
190           Logger.info(TAG,
191             'AVPlayer state initialized called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
192           avPlayer.surfaceId = this.surfaceID;
193           avPlayer.prepare();
194           break;
195         case 'prepared': // prepare调用成功后上报该状态机
196           Logger.info(TAG,
197             'AVPlayer state prepared called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
198           avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE; // 避免同时出现两个视频的声音
199           this.flag = true;
200           avPlayer.loop = true;
201           this.duration = avPlayer.duration;
202           this.durationTime = Math.floor(this.duration / CommConstants.SECOND_TO_MS);
203           this.currentStringTime = secondToTime(this.durationTime);
204           if (this.firstFlag && this.index === 0 && this.isPageShow) {
205             avPlayer.play(); // 应用启动后的第一个视频启动播放
206             this.firstFlag = false;
207           }
208           break;
209         case 'completed': // 播放结束后触发该状态机上报
210           Logger.info(TAG,
211             'AVPlayer state completed called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
212           this.isPlaying = false;
213           break;
214         case 'playing': // play成功调用后触发该状态机上报
215           Logger.info(TAG,
216             'AVPlayer state playing called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}` +
217               `source:${this.curSource}`);
218           this.isPlaying = true;
219           break;
220         case 'paused': // pause成功调用后触发该状态机上报
221           Logger.info(TAG,
222             'AVPlayer state paused called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
223           break;
224         case 'stopped': // stop接口成功调用后触发该状态机上报
225           Logger.info(TAG,
226             'AVPlayer state stopped called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
227           break;
228         case 'released':
229           Logger.info(TAG,
230             'AVPlayer state released called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
231           break;
232         case 'error':
233           Logger.info(TAG,
234             'AVPlayer state released called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);
235           avPlayer.reset();
236           break;
237         default:
238           Logger.info(TAG, 'AVPlayer state unknown called.' + state);
239           break;
240       }
241     })
242   }
243   ```
2444. 视频轮播:使用Swiper组件进行视频轮播,设置cachedCount(3)缓存视频数量。
245
246   ```typescript
247   build() {
248     Swiper(this.swiperController) {
249       LazyForEach(new MyDataSource(this.sources), (item: string, index: number) => {
250         VideoPlayer({
251           curSource: item,
252           curIndex: this.curIndex,
253           index: index,
254           firstFlag: this.firstFlag,
255           isPageShow: this.isPageShow,
256           foldStatus: this.foldStatus
257         })
258       }, (item: string, index: number) => JSON.stringify(item) + index)
259     }
260     .cachedCount(3) // 缓存视频数量
261     .width(CommComstants.WIDTH_FULL_PERCENT)
262     .height(CommComstants.HEIGHT_FULL_PERCENT)
263     .vertical(true)
264     .loop(true)
265     .curve(Curve.Ease)
266     .duration(CommComstants.DURATION_TIME)
267     .indicator(false)
268     .backgroundColor(Color.Black)
269     .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
270       Logger.info(TAG, `onGestureSwipe index:${index}}`);
271     })
272     .onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {
273       this.curIndex = targetIndex; // 优化点:视频播放和动画启动同步进行,覆盖动画效果
274     })
275     .onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {
276       Logger.info(TAG, `onAnimationEnd index:${index}}`);
277     })
278   }
279   ```
280
2815. 窗口设置:设置XComponent组件用于视频流渲染,获取并设置SurfaceID,用户设置显示画面,在onLoad时异步创建并初始化AVPlayer播放器。
282
283   ```typescript
284   XComponent({
285     id: 'XComponent',
286     type: XComponentType.SURFACE,
287     controller: this.xComponentController
288     })
289       .width(this.XComponentWidth)
290       .height(this.XComponentHeight)
291       .onLoad(async () => {
292         this.surfaceID = this.xComponentController.getXComponentSurfaceId();
293         this.initAVPlayer(); // 优化点:创建AVPlayer的播放器放入到缓存池中,不可共用播放器。
294       })
295   ```
296
2976. 视频播放设置:监听Swiper轮播的this.curIndex值,在视频缓存流中跟this.index进行比较,从而判断视频流中哪个播放,其余的均暂停。
298
299   ```typescript
300   onIndexChange() {
301     if (this.curIndex !== this.index) {
302       pauseVideo(this.avPlayer, this.curIndex, this.index);
303       this.isPlaying = false;
304       this.trackThicknessSize = CommConstants.TRACK_SIZE_MIN;
305     } else {
306       if (this.flag === true) {
307         playVideo(this.avPlayer, this.curIndex, this.index);
308         this.isPlaying = true;
309         this.trackThicknessSize = CommConstants.TRACK_SIZE_MIN;
310       } else {
311         let countNum = 0;
312         let interValFlag = setInterval(() => {
313           countNum++;
314           // 此处有必要再次判断索引,否则会出现索引错乱导致播放异常
315           if (this.curIndex !== this.index) {
316             clearInterval(interValFlag);
317           }
318           if (this.flag === true && this.isPageShow) {
319             countNum = 0;
320             playVideo(this.avPlayer, this.curIndex, this.index);
321             this.isPlaying = true;
322             this.trackThicknessSize = CommConstants.TRACK_SIZE_MIN;
323             clearInterval(interValFlag);
324           } else {
325             if (countNum > 15) {
326               countNum = 0;
327               this.initAVPlayer();
328             }
329           }
330         }, 100);
331       }
332     }
333   }
334   ```
335
3367. 设置AVPlayer监听关闭并释放资源。
337
338   ```typescript
339   export function releaseVideo(avPlayer: media.AVPlayer | undefined, curIndex: number, index: number) {
340     if (avPlayer) {
341       Logger.info(TAG, 'releaseVideo:' + `state:${avPlayer.state}` + `curIndex:${curIndex},index:${index}`);
342       avPlayer.off('timeUpdate');
343       avPlayer.off('seekDone');
344       avPlayer.off('speedDone');
345       avPlayer.off('error');
346       avPlayer.off('stateChange');
347       avPlayer.release();
348     }
349   }
350   aboutToDisappear(): void {
351     releaseVideo(this.avPlayer, this.curIndex, this.index);
352   }
353   ```
354
355## 参考文档
356
357[使用AVPlayer开发音频播放功能(ArkTS)](../media/media/using-avplayer-for-playback.md)
358
359[LazyForEach:数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)
360
361[容器组件:Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md)
362
363[基础组件:Slider](../reference/apis-arkui/arkui-ts/ts-basic-components-slider.md)