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

news2024/9/23 5:17:14

简介

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

该解决方案使用:

  • 视频播放框架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.coloradmin.cn/o/2090729.html

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

相关文章

挂载5T大容量外接硬盘到ubuntu

挂载5T大容量外接硬盘到ubuntu S1:查看硬盘 使用 $ sudo fdisk -l找到对应盘,例如下图所示 /dev/sdc S2: 创建分区 使用 $ sudo fdisk /dev/sdc对上硬盘进行创建分区;可以依次使用以下指令 m :查看命令; g &…

前端篇-html

day1: 超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言。 作用:可以使用 HTML 来建立自己的 WEB 站点,HTML 运行在浏览器上,由浏览器…

基于贝叶斯优化CNN-LSTM网络的数据分类识别算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 卷积神经网络(CNN) 4.2 长短期记忆网络(LSTM) 4.3 BO-CNN-LSTM 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) B…

基于物联网的低成本便携式传感器节点用于火灾和空气污染的检测与报警

目录 摘要 引言 材料和方法 传感器节点 IoT 微控制器 颗粒物传感器 环境和气体传感器 MQTT代理 Node-Red监控平台 系统结构 数据存储 工作描述 实验结果 讨论 结论 致谢 参考文献 这篇论文的标题是《Low-cost IoT-based Portable Sensor Node for Fire and Air…

区块链媒体套餐发稿:世媒讯引领项目推广新风潮

在区块链技术迅猛发展的今天,越来越多的企业和项目涌现出来,希望通过区块链技术改变传统行业,并在全球范围内获得更多关注和支持。然而,在这个竞争激烈的市场中,如何快速有效地推广和传播项目变得尤为重要。选择合适的…

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

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

macos Homebrew brew 安装 下载 国内加速镜像配置 - 可彻底解决使用brew命令时github.com无法访问相关问题

macos中的brew的默认仓库是github.com , 由于种种原因gh的访问速度很慢或者干脆被和谐,所以设置一个国内的brew加速非常有必要。 masos brew国内加速镜像配置 设置方法: 将下面的代码放到 ~/.bash_profile 文件中(没有就手动创建…

003.Python爬虫系列_HTTPHTTPS协议

我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈 入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈 虚 拟 环 境 搭 建 :👉&…

uniapp小程序实现横屏手写签名

<template><view class"signBox column-me"><!-- 这个是自定义的title-可根据自己封装的title的作为调整 --><status-bar title"电子签名" :bgColor"null"></status-bar><view class"topHint">请…

x264 编码器 AArch64汇编系列:zigzag 扫描相关汇编函数

zigzag 在x264_zigzag_init函数中初始化具体的 zigzag 实现函数: 以scan_4x4为例 c 语言实现 4x4 变换块扫描:zigzag_scan_4x4_frame。#define ZIGZAG4_FRAME\ZIGDC( 0,

QGraphicsview相关学习

文章学习自&#xff1a; Qt_绘图框架_QGraphicsview实现缩放移动图片_Livy0123的博客-CSDN博客 这里进行一些自己的分析和理解。 (1) 自定义类MyGraphicsView继承自QGraphicsView 核心是重定义的滚轮事件。 void MyGraphicsView::wheelEvent(QWheelEvent *ev) {if(Qt::CT…

房产报备小程序房产报备系统源码搭建方案

房产客户报备小程序开发&#xff0c;php开发语言&#xff0c;前端是uniapp。 房产报备小程序三个端&#xff1a;报备端&#xff08;经纪人报备客户&#xff09;&#xff0c;确客端&#xff08;员工确认报备的客户&#xff09;&#xff0c;管理后台 一 报备端 经纪人报备客户…

生物制药中的AI困境:创新突破还是过度炒作?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Your Diffusion Model is Secretly a Zero-Shot Classifier论文阅读笔记

Your Diffusion Model is Secretly a Zero-Shot Classifier论文阅读笔记 这篇文章我感觉在智源大会上听到无数个大佬讨论&#xff0c;包括OpenAI Sora团队负责人&#xff0c;谢赛宁&#xff0c;好像还有杨植麟。虽然这个文章好像似乎被引量不是特别高&#xff0c;但是和AI甚至…

使用 streamlink 把 m3u8 转为 mp4

问题描述&#xff0c; 背景&#xff0c; 来源&#xff1a; 下载 m3u8 ts —> 转为mp4, 按照以往的做法&#xff0c; 就是使用 python requests 一步一步地下载 m3u8, ts&#xff0c; 然后转换。 但是个人写的东西&#xff0c;毕竟问题比较多。 而且&#xff0c; 但是&…

坑——Redis集群key事件通知

redis集群的key事件通知不像普通的订阅发布消息&#xff0c;普通的订阅发布&#xff0c;在任何一个节点订阅通道&#xff0c;都可以收到消息&#xff0c;但是集群的key事件通知只在key所在的主从节点发出通知&#xff0c;只能通过订阅这个key所在的主从节点收到消息&#xff1b…

Leetcode3240. 最少翻转次数使二进制矩阵回文 II

Every day a Leetcode 题目来源&#xff1a;3240. 最少翻转次数使二进制矩阵回文 II 解法1&#xff1a;分类讨论 特殊情况&#xff1a; 讨论正中间一排&#xff08;如果 m 是奇数&#xff09;和正中间一列&#xff08;如果 n 是奇数&#xff09;中的格子要如何翻转。 综上所…

Qt Creator 配置pcl1.14.1

1.安装pcl1.14.1 地址&#xff1a;Releases PointCloudLibrary/pcl (github.com) 下载前两个 下载完成后双击exe文件&#xff0c;修改一下安装路径&#xff0c;然后一路下一步。 OpenNI2 默认安装到C:\Program Files 下了&#xff0c;想修改OpenNI2 的安装路径&#xff0c;可…

Unity 动态光照贴图,加载后显示变暗或者变白问题 ReflectionProbe的使用

动态加载光照贴图代码&#xff0c;可参考这个帖子 Unity 预制动态绑定光照贴图遇到变白问题_unity urp 动态加载光照信息 变黑-CSDN博客 这次遇到的问题是&#xff0c;在编辑器下光照贴图能正常显示&#xff0c;打出apk后光照贴图加载后变黑的问题 以下4张图代表4种状态&…

网页正文提取算法:行块分布算法 Readability

前提 爬取百度、搜狗、必应等搜索引擎时&#xff0c;详情页的正文因来源多样而无法简单通过通用的规则来匹配&#xff0c;这就需要相关的提取算法。 本文在此介绍两种网页正文提取算法&#xff1a;行块分布算法 & Readability。 行块分布算法 算法流程 算法依据 HTML 每…