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,一经查实,立即删除!

相关文章

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文件夹 如果…

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

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

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

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

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.【人脸识别与管理系统开发…

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

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

设计模式-结构型模式-组合模式

1.组合模式的定义 将对象组合成树形结构以表示整个部分的层次结构,组合模式可以让用户统一对待单个对象和对象的组合;其更像是一种数据结构和算法的抽象,其中数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来…

基于机器学习的商品评论情感分析

从淘宝爬取评论 使用Selenium模拟真实登录行为,并爬取数据。 数据清理 如果文本中有“666“,”好好好“等无用词语,去掉评论中的标点符号。 分词 使用jieba精确模式进行分词,构造词典 将词汇向量化 创建词语字典,并…

Java技术栈 —— Spark入门(三)之实时视频流

Java技术栈 —— Spark入门(三)之实时视频流转灰度图像 一、将摄像头数据发送至kafka二、Kafka准备topic三、spark读取kafka图像数据并处理四、本地显示灰度图像(存在卡顿现象,待优化) 项目整体结构图如下 参考文章或视频链接[1] Architectur…

Python-MNE-源空间和正模型07:修复BEM和头表面

有时在创建BEM模型时,由于可能出现的一系列问题(例如,表面之间的交叉),表面需要手动校正。在这里,我们将看到如何通过将表面导出到3D建模程序blender,编辑它们,并重新导入它们来实现这一点。我们还将给出一…

鸿蒙(API 12 Beta3版)【通过字节数组生成码图】

基本概念 码图生成能力支持将字节数组转换为自定义格式的码图。 场景介绍 码图生成能力支持将字节数组转换为自定义格式的码图。 例如:调用码图生成能力, 将字节数组转换成交通一卡通二维码使用。 约束与限制 只支持QR Code生成,根据纠错水平不同对…

【已解决】win11笔记本电脑突然无法检测到其他显示器 / 无法使用扩展屏(2024.8.29 / 驱动更新问题)

我们点击 winx ,找到设备管理器,查看显示适配器: 主要问题就出现在 NVIDIA GeForce RTX 3060 Laptop GPU 上(虽然我把所有驱动都重新更新了一遍😭)。 常用驱动更新: dell 驱动更新&#xff08…

HTML <template> 标签的基本技巧

前言 HTML中的<template>标记是 Web 开发中一个功能强大但经常未得到充分利用的元素。它允许你定义可重复使用的内容&#xff0c;这些内容可以克隆并插入 DOM 中而无需最初渲染。 此功能对于创建动态、交互式 Web 应用程序特别有用。 在本文中&#xff0c;我们将探讨有…