草庐IT

如何实现 Android 短视频跨页面的流畅续播?

阿里云视频云 2023-03-28 原文
在一切皆可视频化的今天,短视频内容作为移动端产品新的促活点,受到了越来越多的重视与投入,同时短视频也是增加用户粘性、增加用户停留时长的一把利器。那么如何快速实现移动端短视频功能呢?前两篇我们介绍了盒马短视频秒播优化(iOS 篇 / Android 篇),本篇我们聊聊秒播之外,另一个从体感上增加短视频用户体验的能力 - 续播。

作者|少阳

审校|泰一

短视频作为内容重要的承载方式,是吸引用户的重点,短视频的内容与体验直接关系到用户是否愿意长时停留。因此,体验的优化就显得尤为重要。

跨页面续播

跨页面续播是除秒播外另一个可以从体感上增加用户体验的能力。由于一些业务场景需要在不同页面上播放同一个视频内容的场景,而这些场景页面切换往往是连续的,这就要求短视频的播放也是连续。这样才能使得体验上会有连贯性,让用户在进入沉浸式页面时,能流畅的过度,并无感知的继续播放,从而产生连续不间断的感受。

在优化前,盒马沉浸式短视频播放页面的体验与主流短视频 App 有明显差距。从卡片列表页面跳转视频沉浸式页面时,相同视频无法续播,影响用户观赏和体验。下面主要介绍盒马短视频从普通展示页进入沉浸式页面时跨页面续播能力和流畅的动画切换效果的实现过程。

环境

  • 手机:Pixel 4
  • os:Android 10
  • 播放器:淘宝播放器

效果对比

首先我们来看一下盒马优化前后与主流短视频 App 的效果对比

https://v.youku.com/v_show/id_XNTgwNDIwOTM1Ng==.html

问题分析

从对比可以看出,续播的关键在于视频流的复用以及页面转场动画。

视频流的复用

要解决流的复用,同时又要保证进入新的页面时可以立即播放,不产生声音和画面的顿挫,这里根据上一篇《揭秘盒马鲜生 APP Android 短视频秒播优化方案》的分析,必须要解决视频下载,加载解码的耗时。

  • 根据《揭秘盒马鲜生 APP Android 短视频秒播优化方案》里讲到缓存原理,这里可以利用播放器播放同一个视频(注意统一 URL,盒马全部转为 H.265)来避免多次下载。
  • 加载解码的耗时则需要播放器复用来解决。这里涉及到实现方案,可参照下一章的续播方案选型。

转场动画

转场动画能显著提高体感流畅度,但实现过程中需要考虑各种兼容问题。

续播方案选型

在优化前期,我们考虑了三种续播方案。

  1. 播放器 View 跨页面传递 优点:思路简单,体验效果好。 缺点:业务侵入严重,不具通用性,播放器业务回调无法隔离,不利于续播放器管控。

  2. 基于 Surface (View) 级别的全局播放器管理 优点:体验效果好,能扩展内存管控,侵入性低。 缺点:实现复杂,需要改写底层 HMVideoView 的封装逻辑;改造中易出现内存泄漏,较难排查。

  3. 基于 MediaPlayer 级别的全局播放器管理 优点:无侵入,能扩展内存管控,实现快(可复用和扩展淘宝播放器底层 token 机制) 缺点:需要一定的改造,体验比方案 1、2 略差(声音有一瞬间的顿挫,不明显)

盒马最终选择方案 3,这里方案 2 和 3 原理是相同的,没有明显的优劣之分,最终选择方案 3 是因为这是目前稳定性最高,成本最低的方法。后续的播放器续播、复用、管理的分析同样适用于方案 2

播放器续播、复用和管理

业务上,我们需要实现续播,通过问题分析,我们已经知道,通过视频流的复用即可实现,而视频流的复用这里选择通过复用 MediaPlayer 实现(也可以复用 Surface+MediaPlayer)。

解耦播放器 View 与 MediaPlayer 层

将 MediaPlayer 从 TaobaoPlayerView 中拆解出来,通过 MediaPlayerManager 进行全局管理。全局管理后,所有的播放器的 MediaPlayer 都由 MediaPlayerManager 分配和控制。

各组建间关系

业务流程

确保业务流程中,只需要关心业务与 VideoView 之间的交互,底层播放器复用由 MediaPlayerManager 实现。

播放器复用(管理)原理

播放器复用是管理的一个子集,所以这里一起介绍。主要原来有以下几个原则:

  1. 全局播放器(MediaPlayer)控制最多创建 4 个。
  2. 超过 4 个播放器,创建第 5 个时,先销毁最少使用的播放器的 MediaPlayer。
  3. 每个播放器随机分配一个 token(时间戳 + 随机数),也可以开发者指定。
  4. 相同 token 的播放器,共享 MediaPlayer。
  5. 一个 MediaPlayer 同时只能被 1 个播放器 Surface 所绑定和持有。
  6. 存在相同 token 的播放器,当前播放器在销毁时,保留 MediaPlayer 实例。
  7. 已创建的播放器恢复播放,但 MediaPlayer 被其他后创建的播放器占用时,解绑 MediaPlayer 并重新绑定当前播放器。

场景模拟

场景一:APP 共创建 4 个及以内播放器。

场景二:创建超过 4 个播放器时。

场景三:新创建的播放器 token 已存在时,复用 MediaPlayer。

场景四:存在 token 与当前即将被销毁的播放器 token 一致时(或已被解除 MediaPlayer 的播放器播放时)。

逻辑流程图

从场景总结,MediaPlayer 主要提供 复用、恢复、销毁、驱逐(创建) 四个能力。

转场动画

目前转场动画有两个方案可选:

  1. Android 自带的元素动画 优点:动画流畅顺滑,无需实现动画逻辑,由系统自己实现。 缺点:侵入严重,需要改写 Nav 层,在 View 复用的方案下有白屏和黑屏。

  2. 自定义实现属性动画 优点:侵入小,只需要前置页极少的坐标信息,如果是 View 复用方案,甚至不需要前置页提供坐标信息;兼容性好,适用于各种播放器复用场景。 缺点:需要自己实现动画,有一定的闪烁感。

动画原理

  1. 前置页跳转到沉浸式,传递播放器坐标 Rect 信息。
  2. 沉浸式默认透明,并根据 Rect 坐标信息创建播放器(复用)。
  3. 开始动画,将播放器 View 放大至正确位置,同时背景不透明度增加。 (注意:这里最后要将沉浸式页的主题设为不透明,否则前置页不会执行 onStop () 具体参考下一节,生命周期填坑。)
ps:返回动画同理,过程相反即可。

生命周期填坑

属性动画原理存在一个坑。

问题描述: 假设页面为 A->B,方案 3 要求 B 页面在动画过程中是全透明的。当 B 的 theme 中 windowIsTranslucent 为 true 时,A->B 过程 A 的生命周期无法走向 stop(即便 B 页面动画结束,完全遮盖 A 页面)。因此,A 的生命周期没有按照预期执行,一些需要 onStop 执行的场景下,业务就无法正常执行

B Ativity 的样式(注:示例代码):

<style name="MyTransparent" parent="xxxx"> <item name="android:windowFullscreen">false</item> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:colorBackgroundCacheHint">@null</item> <item name="android:windowIsTranslucent">true</item> <item name="android:background">@android:color/transparent</item> <item name="android:windowAnimationStyle">@style/noAnimation</item> </style> 解决方案

  1. 进入动画结束时,通过反射调用 Activity 的 convertFromTranslucent 方法, 使 activity 不透明。
  2. 返回动画开始时,通过反射调用 Activity 的 convertToTranslucent 方法,使 activity 透明。

后续优化展望

关于多媒体的优化工作还有很多可以做。除了续播和沉浸式秒播等场景外,我们还可以: 1. 对播放器的一般性场景进行秒播优化,如首页列表的卡片视频。 2. 对播放器的全局实例管控,控制播放器创建数量,从而优化内存。

未优化: 操作:连续开启 30~50 个页面及播放器。 现象:内存飙升,手机发烫,影响手机正常使用。

优化后: 操作:每秒开启 1 个页面和播放器,连续开启 100 个。 现象:内存呈锯齿状正常上升,无明显飙升现象,软件运行正常。

下一期我们将继续分享盒马 iOS 端短视频续播的体验优化实践。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云产品技术交流群,和业内大咖一起探讨音视频技术,获取更多行业最新信息。

有关如何实现 Android 短视频跨页面的流畅续播?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

随机推荐