HarmonyOS鸿蒙开发:在线短视频流畅切换最佳实践

简介

为了帮助开发者解决在应用中在线短视频快速切换时容易出现快速切换播放时延过长的问题,将提供对应场景的解决方案。

该解决方案使用:

  • 视频播放框架AVPlayer和滑块视图容器Swiper进行短视频滑动轮播切换。
  • 绘制组件XComponent的Surface类型动态渲染视频流。
  • 使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果,(在冷启动过程中创建一个AVPlayer并进行数据初始化到prepared阶段,在轮播过程中,每次异步创建一个播放器为下一个视频播放做准备)。

最终实现短视频快速切换起播时延达到≤200ms的效果。

如果开发者使用自研播放器引擎而非AVPlayer,也可以参考该解决方案思路达成最佳实践

效果展示

在线短视频滑动切换

场景说明

适用范围

适用于应用中在线短视频快速切换,容易出现快速切换播放起播慢体验不佳的场景。

场景性能指标

起播时延计时标准

  1. 一般以用户滑动屏幕后抬手,手指离屏时刻为起点,以视频第二帧画面显示时刻为终点(不是封面帧)。

  2. 转场动画时长一般设置为300ms。

  3. 在动画开始时使用预先准备的播放器起播,起播时延一般在200ms内。

视频播放器时延优化前优化后
AVPlayer在线短视频切换播放起播时延1100ms200ms

场景分析

典型场景及优化方案

典型场景描述

短视频:一般小于5分钟,以移动状态和短时间休闲状态下观看为主

  1. 应用内滑动视频,新视频起播时延≤X(200ms/400ms/500ms)。
  2. 起点时间:滑动离手;时间终点:视频内容开始播放,画面发生变化。

场景优化方案

AVPlayer:

  1. 数据懒加载

    在线短视频预加载,冷启动时创建第一个播放器,播放当前视频时预加载下一个播放视频,绘制组件XComponent的Surface类型将视频流进行动态渲染、使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果。

  2. 异步在线视频预加载

    在轮播过程中,对下一个视频提前进入AVPlayer的prepared状态。

  3. 在线视频播放预接力

    滑动过程中手指离开屏幕,此时滑动动效开始播放,在动效开始时就可以调用AVPlayer的play方法进行播放。

自研播放器:

  1. 数据懒加载

    在线短视频预加载,冷启动时创建第一个播放器,播放当前视频时预加载下一个播放视频,绘制组件XComponent的Surface类型将视频流进行动态渲染、使用LazyForEach进行数据懒加载,设置cachedCount属性来指定缓存数量,同时搭配组件复用能力以达到高性能效果

  2. 异步在线视频预加载

    在轮播过程中,对下一个视频提前初始化播放器所需内容(视频源下载、AudioRender初始化、解码器初始化等),并对视频提前预解析首帧画面。

  3. 在线视频播放预接力

    滑动过程中手指离开屏幕,此时滑动动效开始播放,在动效开始时就可以调用播放引擎进行播放。 为了保证用户的起播体验,在前几帧画面送显时应优先送显,而不是等AudioRender写入音频数据才送显,因为音频硬件时延比显示时延大。播放起始几帧建议不要做强音画同步,而是采用慢追帧策略进行同步,视频帧稍微增大送显间隔,直到完成音画同步。

场景实现

场景整体介绍

基于AVPlayer实现了在线流媒体的短视频流畅播放和控制功能。基于对应的播放器,使用滑块视图容器Swiper进行短视频滑动轮播切换、绘制组件XComponent的Surface类型将视频流进行动态渲染、懒加载,最终实现短视频快速切换,实现起播≤200ms,提供开发者解决此类问题的方案。

功能时序图

在线短视频快速切换

实现流程图

关键点

AVPlayer

AVPlayer可以将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。

LazyForEach数据懒加载

LazyForEach数据懒加载可以通过设置cachedCount属性来指定缓存数量(目前设置为3),同时搭配组件复用能力以达到高性能效果。SurfaceID每次都会创建,不共用SurfaceID,AVPlayer也会同时创建,不共用AVPlayer,进而将提前加载好的视频(prepared阶段)放到缓存池中。 在通过Swiper切换时,会根据当前轮询滑动的窗口索引index到缓存池中找到对应的视频(prepared阶段),直接进行播放,从而提升切换性能。

异步视频预加载

异步视频预加载:在Swiper轮播过程中,在播放当前视频时,提前加载好下一个视频,在缓存中同时存在多个播放器实例,根据视频当前的索引来确定使用缓存中的哪个播放器来播放,从而达到流畅切换的效果。

(1)本地播放一个短视频的耗时。

(2)播放视频A的时候,提前预加载视频B。在切换短视频时,可以马上开始播放已预加载完成的视频B,从而减少了切换时间,提高了切换性能。

视频播放预启动能力

为了进一步提升滑动播放体验,在动效开始时就开始播放,做到动效和播放并行进行:

(1)在收到AnimationStart回调时开始播放,而不是动效结束再播放;

(2)不要用默认的弹簧曲线(弹簧动效有560ms,视频窗口在400ms左右已经全面铺开了,最后150ms位移随时间变化较小),可以把curve改成Curve.Ease,duration改为300ms(视APP UX确定);

视频播放预启动接力:类似于4*100接力赛,想要尽快完成接力赛,当第一个选手快到达终点时,第二个选手就提前起跑并且和第一个选手完美完成接力棒,从而减少整个接力赛过程中的时间。短视频切换也是如此,如下图所示:

关键代码片段

  1. 初始化AVPlayer播放器。

    async initAVPlayer() {Logger.info(TAG, 'createAVPlayer begin');media.createAVPlayer().then((video: media.AVPlayer) => {if (video !== null) {this.avPlayer = video;this.setAVPlayerCallback(this.avPlayer);// 设置播放源,使其进入initialized状态if (typeof this.curSource === 'string') {this.avPlayer.url = this.curSource;} else {this.avPlayer.fdSrc = this.curSource;}Loggor.info(TAG, 'createAVPlayer success');} else {Loggor.error(TAG, 'createAVPlayer fail');}}).catch((error: BusinessError) => {Logger.error(TAG, `AVPlayer catchCallback,error message:${error.message}`);})
    }
  2. 设置业务需要的监听事件。

    setAVPlayerCallback(avPlayer: media.avPlayer) {// 用于进度条,监听进度条当前位置,刷新当前时间avPlayer.on('timeUpdate', (time: number) => {if (!this.isSliderMoving) {this.currentTime = Math.floor(time * this.durationTime / this.duration);this.currentStringTime = secondToTime(Math.floor((time / CommConstants.SECOND_TO_MS)));}})// 适配一多,根据屏幕尺寸的变化同步更新视频的长宽avPlayer.on('videoSizeChange', (width: number, height: number) => {this.viewHeight = height;this.viewWidth = width;this.autoVideoSize();})// 必要事件,监听播放器的错误信息avPlayer.on('error', (error: BusinessError) => {Logger.error(TAG,`Invoke avPlayer failed, code is ${error.code},message is ${error.message}` + `---state:${avPlayer.state}`);avPlayer.reset();})this.setAVPlayerStateListen(avPlayer);
    }
  3. 设置状态机变化回调函数。

    setAVPlayerStateListen(avPlayer: media.AVPlayer) {avPlayer.on('stateChange', async (state: string) => {switch (state) {case 'idle': // 成功调用reset接口后触发该状态机上报Logger.info(TAG, 'AVPlayer state idle called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);break;case 'initialized': // avplayer 设置播放源后触发该状态上报Logger.info(TAG,'AVPlayer state initialized called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);avPlayer.surfaceId = this.surfaceID;avPlayer.prepare();break;case 'prepared': // prepare调用成功后上报该状态机Logger.info(TAG,'AVPlayer state prepared called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE; // 避免同时出现两个视频的声音this.flag = true;avPlayer.loop = true;this.duration = avPlayer.duration;this.durationTime = Math.floor(this.duration / CommConstants.SECOND_TO_MS);this.currentStringTime = secondToTime(this.durationTime);if (this.firstFlag && this.index === 0 && this.isPageShow) {avPlayer.play(); // 应用启动后的第一个视频启动播放this.firstFlag = false;}break;case 'completed': // 播放结束后触发该状态机上报Logger.info(TAG,'AVPlayer state completed called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);this.isPlaying = false;break;case 'playing': // play成功调用后触发该状态机上报Logger.info(TAG,'AVPlayer state playing called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}` +`source:${this.curSource}`);this.isPlaying = true;break;case 'paused': // pause成功调用后触发该状态机上报Logger.info(TAG,'AVPlayer state paused called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);break;case 'stopped': // stop接口成功调用后触发该状态机上报Logger.info(TAG,'AVPlayer state stopped called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);break;case 'released':Logger.info(TAG,'AVPlayer state released called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);break;case 'error':Logger.info(TAG,'AVPlayer state released called.' + `this.curIndex:${this.curIndex}` + `this.index:${this.index}`);avPlayer.reset();break;default:Logger.info(TAG, 'AVPlayer state unknown called.' + state);break;}})
    }
  4. 视频轮播:使用Swiper组件进行视频轮播,设置cachedCount(3)缓存视频数量。

    build() {Swiper(this.swiperController) {LazyForEach(new MyDataSource(this.sources), (item: string, index: number) => {VideoPlayer({curSource: item,curIndex: this.curIndex,index: index,firstFlag: this.firstFlag,isPageShow: this.isPageShow,foldStatus: this.foldStatus})}, (item: string, index: number) => JSON.stringify(item) + index)}.cachedCount(3) // 缓存视频数量.width(CommComstants.WIDTH_FULL_PERCENT).height(CommComstants.HEIGHT_FULL_PERCENT).vertical(true).loop(true).curve(Curve.Ease).duration(CommComstants.DURATION_TIME).indicator(false).backgroundColor(Color.Black).onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {Logger.info(TAG, `onGestureSwipe index:${index}}`);}).onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {this.curIndex = targetIndex; // 优化点:视频播放和动画启动同步进行,覆盖动画效果}).onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => {Logger.info(TAG, `onAnimationEnd index:${index}}`);})
    }
  5. 窗口设置:设置XComponent组件用于视频流渲染,获取并设置SurfaceID,用户设置显示画面,在onLoad时异步创建并初始化AVPlayer播放器。

    XComponent({id: 'XComponent',type: XComponentType.SURFACE,controller: this.xComponentController}).width(this.XComponentWidth).height(this.XComponentHeight).onLoad(async () => {this.surfaceID = this.xComponentController.getXComponentSurfaceId();this.initAVPlayer(); // 优化点:创建AVPlayer的播放器放入到缓存池中,不可共用播放器。})
  6. 视频播放设置:监听Swiper轮播的this.curIndex值,在视频缓存流中跟this.index进行比较,从而判断视频流中哪个播放,其余的均暂停。

    onIndexChange() {if (this.curIndex !== this.index) {pauseVideo(this.avPlayer, this.curIndex, this.index);this.isPlaying = false;this.trackThicknessSize = CommConstants.TRACK_SIZE_MIN;} else {if (this.flag === true) {playVideo(this.avPlayer, this.curIndex, this.index);this.isPlaying = true;this.trackThicknessSize = CommConstants.TRACK_SIZE_MIN;} else {let countNum = 0;let interValFlag = setInterval(() => {countNum++;// 此处有必要再次判断索引,否则会出现索引错乱导致播放异常if (this.curIndex !== this.index) {clearInterval(interValFlag);}if (this.flag === true && this.isPageShow) {countNum = 0;playVideo(this.avPlayer, this.curIndex, this.index);this.isPlaying = true;this.trackThicknessSize = CommConstants.TRACK_SIZE_MIN;clearInterval(interValFlag);} else {if (countNum > 15) {countNum = 0;this.initAVPlayer();}}}, 100);}}
    }
  7. 设置AVPlayer监听关闭并释放资源。

    export function releaseVideo(avPlayer: media.AVPlayer | undefined, curIndex: number, index: number) {if (avPlayer) {Logger.info(TAG, 'releaseVideo:' + `state:${avPlayer.state}` + `curIndex:${curIndex},index:${index}`);avPlayer.off('timeUpdate');avPlayer.off('seekDone');avPlayer.off('speedDone');avPlayer.off('error');avPlayer.off('stateChange');avPlayer.release();}
    }
    aboutToDisappear(): void {releaseVideo(this.avPlayer, this.curIndex, this.index);
    }

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。 

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:

希望这一份鸿蒙学习文档能够给大家带来帮助~


 鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频教程+学习PDF文档

(鸿蒙语法ArkTS、TypeScript、ArkUI教程……)

 纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

                   

鸿蒙APP开发必备

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/51714.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot项目中结合MyBatis详细使用

(一)、实现分布 1. Controller层 职责: 负责处理HTTP请求和响应,作为前端与后端服务交互的接口。 实现: 通过RestController或Controller注解定义控制器类,并使用如GetMapping, PostMapping等注解来映射HTTP请求。 2. Service层…

10 个最佳网络爬虫工具和软件,零基础入门到精通,收藏这一篇就够了

据 Strait Research 称,数据提取的需求正在不断增加,预计到 2031 年将达到 18 亿美元。 使用最好的网络爬行工具启动您的数据提取项目,并告别烦人的爬行头痛。我们研究和测试了数百种免费和付费软件,然后为您提出了十种最佳网络爬…

JVM 内存参数

文章目录 引言I JVM基础知识Java 语言是解释型的OpenJDK和Sun/Oracle JDK和hotspot的关系JDK、JRE、JVM 之间的关系JVM基础功能JVM组成JIT:Just In Time CompilerJVM内存区域JVM运行时数据区JVM 堆内存布局II JVM 内存参数常用参数JIT编译参数GC信息打印GC参数III 例子引言 J…

disk manager操作教程 如何使用Disk Manager组件 Mac如何打开ntfs格式文件

macOS系统有一个特别明显的弱点,即不能对NTFS格式磁盘写入数据。想要适合Mac系统使用来回转换磁盘格式又十分麻烦,这该怎么办呢?Tuxera ntfs for mac作为一款Mac完全读写软件,大家在安装该软件后,能充分使用它的磁盘管…

centos安装docker并配置加速器

docker安装与卸载: 1、检查当前是否安装docker yum list installed | grep docker2、卸载docker 根据yum list installed | grep docker查询出来的内容,逐个进行删除 yum remove docker.x86 64 -y3、启动与关闭docker 4、删除/etc/docker文件夹 如果…

4.3 python 编辑单元格

4.3.1 clear_contents()函数和clear()函数–清楚单元格的内容和格式 表达式.clear_contents() Range对象的clear_contects()函数用于清除单元格的内容,但不会清除单元格的格式设置 表达式.clear() Range对象的clear()用于清楚单元格的内容和格式设置。 # 清除指…

Unity面试:使用Unity3d实现2d游戏有几种方式?

在Unity 3D中实现2D游戏,有多种方法和工具可供选择。以下是一些常见的实现方式及其特点,适合不同类型的2D游戏开发需求。 1. 使用Unity的2D工具和特性 Unity提供了专门用于2D游戏开发的一系列工具和功能,以下是主要的几个方面: …

C#面试题系列--动态更新

C#面试题系列 排版排了半天,也是不好看,那就不排版了,尽量由易到难 高级一些 什么是MVC模式C#中特性是什么?如何使用?C#中什么是反射?C#中的委托是什么 事件是不是一种委托C# 不安全代码C# 隐式类型 varC# linqC# 匿名…

监控域名到期发送钉钉消息通知

目的 想象一下,域名到期都不知道,忘了续费,就像忘了交房租,房东(互联网)会毫不留情地把你扫地出门!所以,及时续费,让顾客轻松找到你,生意红红火火&#xff0…

【面试经验】美团基础研发部产品经理面试经验

3.12 投递 4.1 一面 4.11 二面 4.17 oc但拒 一面内容: 1、一个指数增长的脑经急转弯 2、对向量和向量值如何理解 ——类比函数,目的是映射和转化 3、transformer有没有看(问到了注意力机制) ——transformer的本质是一个编码…

智能合约漏洞(三)

前言 在前几篇文章中,我们探讨了智能合约中的逻辑漏洞和重放攻击。本篇将继续分析两种常见的漏洞类型:整数溢出/下溢和时间依赖漏洞。了解这些漏洞及其防范措施对于智能合约的安全开发至关重要。 5. 整数溢出/下溢(Integer Overflow/Underfl…

python反序列化

前言:最近打比赛遇到了就简单记录学习一下 一、概念 什么是序列化? 序列化是将 Python 对象转换为一种可以存储或传输的格式的过程。常见的序列化格式包括 JSON、XML、protobuf 以及 Python 自带的 pickle 模块。 什么是反序列化? 反序列化…

vs2022 C++ 使用MySQL Connector/C++访问mysql数据库

1、下载MySQL Connector/C,我这里下载的是debug版本,下载链接MySQL :: Download MySQL Connector/C (Archived Versions) 2、解压并且放到MySQL文件夹中,便于使用 3、打开vs2022,右键项目,点击属性 4、在 “C/C” ->…

视频技术未来展望:EasyCVR如何引领汇聚融合平台新趋势

随着科技的飞速发展,视频技术已成为现代社会不可或缺的一部分,广泛应用于安防监控、娱乐传播、在线教育、电商直播等多个领域。本文将探讨视频技术的未来发展趋势,并深入分析TSINGSEE青犀EasyCVR视频汇聚融合平台的技术优势,展现其…

HIVE 数据仓库工具之第二部分(数据库相关操作)

HIVE 数据仓库工具之第二部分(数据库相关操作) 一、Hive 对数据库的操作1.1 创建数据库1.1.1 创建数据库语法1.1.3 示例 1.2 使用数据库1.2.1 使用数据库语法1.2.2 示例 1.3 修改数据库1.3.1 修改数据库的语法1.3.2 示例 1.4 删除数据库1.4.1 删除数据库…

JetBrains WebStorm 2024.2 (macOS, Linux, Windows) - 最智能的 JavaScript IDE

JetBrains WebStorm 2024.2 (macOS, Linux, Windows) - 最智能的 JavaScript IDE JetBrains 跨平台开发者工具 请访问原文链接:ttps://sysin.org/blog/jetbrains-webstorm/,查看最新版。原创作品,转载请保留出处。 作者主页:sy…

能大致讲一下Chat GPT的原理吗?

AI视频生成:小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频百万播放量https://aitools.jurilu.com/ 话题群精选了三位网友的回答,从不同的角度阐释了Chat GPT的原理。 第一位网友的回答: 不给你扯长篇大论&#…

人工智能、机器学习和深度学习有什么区别?应用领域有哪些?

《博主简介》 小伙伴们好,我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 👍感谢小伙伴们点赞、关注! 《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发…

Python使用OpenCV识别图片人脸

在Python中,识别图片中的人脸并获取人脸区域的坐标,通常可以使用OpenCV库结合Haar特征分类器来实现。 安装OpenCV依赖 pip install opencv-python 识别图片中的人脸并获取其坐标 import cv2 def detect_faces(image_path): # 加载预训练的Haar级联…

LabVIEW项目中硬件选型与长期需求沟通

在LabVIEW项目中,选择合适的硬件和有效的需求沟通是成功的关键。大品牌硬件通常具备更高的稳定性和完善的售后服务,而小品牌虽然看似便宜,却可能带来通讯不稳定、技术支持不足等问题,增加开发难度。同时,在科研类项目中…