猫耳 Android 播放框架开发实践

这篇具有很好参考价值的文章主要介绍了猫耳 Android 播放框架开发实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

概述

猫耳FM是中国最大的 95 后声音内容分享平台,是B站重要平台之一,深度合作国内顶级声优工作室,打造了数百部精品广播剧,全站播放总量超过百亿次。

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

MEPlayer 是猫耳 Android 技术团队研发的一款适用于音视频、直播、特效播放等多种场景的跨进程播放框架。目前支持:

  • 音视频、直播、特效播放。
  • 支持自定义播放内核,目前内置了 exo、bbp(多媒体部门开发的轻量级播放内核),都添加了边下边播支持,可以自行扩展支持 ijk 等内核,实现固定的几个接口即可。
  • 与业务完全解耦,已在公司内多个团队使用。
  • 跨进程播放音视频、直播、特效,播放进程被杀后自动恢复。
  • 自动管理音频焦点,支持忽略焦点抢占(与其他音视频应用同时播放)。
  • 支持显示在通知栏和播控中心,已适配包括鸿蒙(猫耳FM 已通过华为测试,配置在鸿蒙系统源码白名单里)在内的国内系统。
  • 后台播放优化:bbp 后台播视频支持暂停视频解码;播放时后台保持网络连接;能同时提升主进程和播放进程优先级保证应用播放时存活更久。
  • 支持切换清晰度、播放中途音视频互切、起播出错或者播放中途出错自动重试。
  • 基础功能:播放列表、循环模式、进度跳转、快进、快退、倍速播放、设置音量、跳过片头片尾、定时暂停、播放完整首暂停等。

具体使用场景可以参考猫耳FM APP:

音视频主播放场景: 音视频播放页;

直播、特效: 直播间;

短视频场景: 首页推荐 -> 小梦乡;

列表播放以及过渡到播放页场景: 首页推荐 -> 播放大卡;

配音秀: 发现 -> 活动 -> 配音活动;

单个音视频、特效播放场景: 个人主页头像音、首页点击盲盒剧场、活动 -> 运势语音、首页声音恋人 tab 下的推荐 UP 主播放、我的 -> 启动音等。

起源

旧版本猫耳FM APP 内的大量的音视频播放场景,使用了 ijk、ExoPlayer、MediaPlayer 等多种播放器方案,且播放逻辑和业务逻辑高度耦合,当播放场景出现新的需求,改动成本巨大,且编写需求代码的过程中易产生 bug;原播放场景相关的代码缺少模块化,代码复用程度低,进而影响后期维护。因此,项目迫切需要一套统一的播放框架,以满足不同场景的需要。调研了主流的播放框架之后,发现很难同时满足我们的多样化场景。在调研了主流的播放框架后,发现没有现成方案能够满足项目的多样化场景,于是我们开发了 MEPlayer,0 重复逻辑、0 业务耦合,API 友好,开发的理解和接入成本都极小。

播放器流程

下图是一个简单的播放流程图。

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

MEPlayer、MEDirectPlayer 是音视频和直播业务直接接触的两个播放器入口,MEPlayer 支持跨进程播放,MEDirectPlayer 则直接在主进程播放,这两个 Player 的基础 API 和播放逻辑代码都是共享的,差异部分在于播放器入口实例和内核封装 Player 的连接,相比于 MEPlayer,MEDirectPlayer 缺少连接播控中心的能力。

为什么需要 MEDirectPlayer 呢?因为对于闪屏、启动音等在启动 APP 一两秒内就要播放的场景,跨进程播放是来不及的,可能会出现需要播的时候进程还没连接好的状况。而跨进程部分逻辑是比较复杂的,所以还是分离一个播放器入口对于后期维护和业务理解都更友好。

对于视频和特效播放,需要绑定视频/特效容器的 Surface,SurfaceListener 是在播放器内部管理的,业务只需要传递容器 View 给播放框架即可,目前支持 TextureView 和 SurfaceView,业务如果设置过 SurfaceListener,框架里也会兼容,在对应方法回调时,会给老的 listener 同时回调,在列表场景视频卡片切换时,会把业务设置的 listener 还给上一个卡片。特效播放比较特殊,播放器入口是 AlphaVideoPlayer,用到的播放内核 API 也不一样,在跨进程 AIDL 调用中都是独立的方法,但是业务调用的 API 跟音视频播放是一致的。

播放框架的状态机见下图:

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

起播处理流程采用的拦截器模式,对于全局的 https、免流处理等操作,可以自定义一个拦截器注入到播放器中,对于列表播放中某一条 item 没有 url 信息时,也可以在默认的拦截器回调中请求接口返回一个新的 url 来播放。

interface PlayerPreProcessor {
    val name: String
    /**
     * Processor id,业务自定义的 id 从 100 开始定,前 100 是给框架预留的
     */
    val id: Int
    /**
     * 处理器调用优先级,值越大优先级越大,最大为 100。设置的时候注意查看现有的其他处理器的优先级,尽量不要重复
     */
    @get:IntRange(from = 0L, to = 100L)
    val priority: Int
    /**
     * @param url 原始 url
     * @param playItem 播放列表中的当前 item,如果没有列表则为空
     * @param playParam 播放参数
     * @param scope 协程作用域
     * @return 输出的结果
     */
    suspend fun process(url: String?, playItem: PlayItem?, playParam: PlayParam?, scope: CoroutineScope): PlayerPreProcessResult
}

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

播放器回调统一采用 kotlin dsl 的形式,简单示例如下:

private val mPlayer = MEPlayer(this).apply {
    onReady {
        // 打开 url 资源成功回调
    }
    onDuration {
        // 更新时长
    }
    onPlayingStateChanged { isPlaying, from ->
        // 更新播放状态
    }
    onPositionUpdate {
        // 更新播放进度
    }
    onCompletion { 
        // 播放结束
    }
    onRetry {
        // 播放出错会自动调用 onRetry 进行重试,如果业务没有实现则跳转到 onError
        // onRetry 是一个 suspend 方法,可以进行耗时操作,需要返回一个 url,可以是 player.originUrl,也可以是请求后端返回的一个新 url
    }
    onError {
        // 错误处理
    }
}

MEPlayer 支持传入 LifecycleOwner,可以在 LifecycleOwner onDestroy 的时候自动释放。构造方法为:

/**
 * 播放器构造方法,大多数场景都应该使用 MEPlayer,会跨进程播放
 *
 * @param lifecycleOwner LifecycleOwner 对象,对于可以在退出页面后继续播放的场景,可以传 ProcessLifecycleOwner.get(),其他场景可以传页面的 LifecycleOwner
 * @param from 用于在日志 tag 上显示业务来源,可以传页面的 TAG,默认使用 lifecycleOwner 所在页面的 className
 * @param type 播放器类型,默认值为 PLAYER_TYPE_AUTO
 *        PLAYER_TYPE_AUTO -> 根据磁盘缓存键值对里 “player_type” 对应的值来选择播放器,如果是 “exo” 则使用 ExoPlayer,
 *                            如果是 “bbp” 则使用 BBP 播放器,默认使用 ExoPlayer。
 *        PLAYER_TYPE_BB_PLAYER -> 使用 BBP 播放器
 *        PLAYER_TYPE_EXO_PLAYER -> 使用 ExoPlayer
 * @param scope 协程作用域,用于播放器对象里创建协程,管理协程生命周期,默认值为 lifecycleOwner.lifecycleScope
 */
class MEPlayer @JvmOverloads constructor(
    lifecycleOwner: LifecycleOwner,
    from: String = lifecycleOwner.tagName(),
    @PlayerType type: String = PLAYER_TYPE_AUTO,
    scope: CoroutineScope = lifecycleOwner.lifecycleScope
)

播放框架还支持多实例场景,配音秀和小梦乡场景都是无声视频配合音频一起播放的,所以跨进程播放的时候要支持多个实例同时播放。先看下播放器的一段日志:

// 音频
// 播放进程
I/ServicePlayer.Hypnosis.bbp.core1 onReady
I/ServicePlayer.Hypnosis.bbp.core1 onPlaying, needRequestFocus: true
I/ServicePlayer.Hypnosis.bbp.core1 updatePlaybackState, shouldShowInMediaSession: true, enableNotification: true, enableRating: false, enableLyric: false
// 主进程
I/MEPlayer.Hypnosis.bbp.core1 onReady
I/MEPlayer.Hypnosis.bbp.core1 updatePlayingState, isPlaying: true, reason: 1 (open), position: 12 (00:00), notifyCallback: true, notifyNotification: true
// 视频
// 播放进程
I/ServicePlayer.HypnosisHomeFragment.bbp.core2 onReady
I/ServicePlayer.HypnosisHomeFragment.bbp.core2 onPlaying, needRequestFocus: false
I/ServicePlayer.HypnosisHomeFragment.bbp.core2 updatePlaybackState, shouldShowInMediaSession: false, enableNotification: false, enableRating: false, enableLyric: false
// 主进程
I/MEPlayer.HypnosisHomeFragment.bbp.core2 onReady
I/MEPlayer.HypnosisHomeFragment.bbp.core2 updatePlayingState, isPlaying: true, reason: 1 (open), position: 21 (00:00), notifyCallback: true, 

可以看出,播放器日志采用了多级 TAG 结构,在播放框架的主流程的每一个类中,打印的日志都能直接看出当前打印日志时所在的类、业务、播放内核类型和内核实例索引。播放器实例采用 SparseArrayCompat 来存储,主进程和播放进程保证实例索引的一一对应关系。

在列表视频播放过渡到播放页场景中,需要做到实例无缝过渡,框架里会把播放页实例的参数传递给列表的实例,然后释放原实例,整个过程播放是持续进行的。

播放器优化

在网络连接上,ExoPlayer 官方已经支持了 Cronet,经过和多媒体部门、主站一起合作,bbp 也添加了 Cronet 支持,Cronet 是一个由 Google 开发的网络库, 也是 Chrome 的网络栈,它提供了高性能和可靠的网络访问能力,支持 HTTP、HTTP/2 以及 HTTP/3 协议,在 HTTP/3 下,90% 的用户起播速度提升了 100ms 以上。

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

另外 ExoPlayer 的缓存支持其实并不友好,音频 APP 的一个必备功能就是在播放的时候会持续缓存完整个音频,同时进度条会更新缓存进度,但是要想用 ExoPlayer 直接实现这点,很难,业内一般是用 AndroidVideoCache 来实现的,并不优雅,这里我修改了部分 ExoPlayer 的源码,添加了支持,内容较长不好展开讲。

音频焦点管理

音频焦点在框架内自动申请和释放,业务只需要在初始化播放器时设置音频焦点类型和是否忽略焦点抢占(即和其他应用同时播放)即可。

player.run {
    audioFocusGain = AUDIO_FOCUS_GAIN_TRANSIENT
	ignoreFocusLoss = true
}

在每个播放器实例中都会有焦点监听和处理

后台播放优化

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

在应用退到后台后,如果进程(包括主进程)不是前台进程,很可能会在几秒内被系统杀死。那么就需要在播放的时候通过调用 startForeground(int id, Notification notification) 将播放进程设置为前台进程,前台进程需要绑定一个通知,退到后台后,可以发现播放进程的存活率明显提升,但是播一会儿你会发现,主进程没了。就是说主进程和播放进程都需要设置为前台进程,但是产品需求上我们只有一个播放器通知,所以主进程要用和播放进程一样的通知内容开启前台进程,以保证用户切换音频的时候不会看到闪出一个非播放通知。这里我们主进程也开了个通知服务来更新通知,播放进程只需要开启前台进程的时候绑定通知就好了,后续通知的更新交由主进程完成。播放时退后台打印优先级可以看到两个进程都是较高的优先级。

> adb shell
$ cat /proc/`pidof cn.missevan`/oom_adj
3
$ cat /proc/`pidof cn.missevan:player`/oom_adj
3

还有一种情况是,主进程活着,但是播放进程被杀死了,或者播放进程出现问题崩溃了,这时候主进程需要恢复播放进程,不仅仅是启动进程,也需要维持原有的进度恢复播放,还需要创建新的通知开启前台进程。这些步骤都需要拿到原有的数据,在播放进程存放这些数据不靠谱,所以主进程执行的步骤,都需要保存数据,以供播放进程重连后使用。

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

播放失败重试包含中途网络断开媒体数据却没有缓存完、链接失效、seek 失败、切换清晰度失败、音视频切换失败等场景,这些场景的重试逻辑是有所区分的,要保证代码逻辑清晰符合需求又没有重复代码是比较困难的,好在梳理异同点后把逻辑都聚合到了一块,对于后期扩展也比较友好。这里通过 playType 区分场景,核心逻辑如下:

val playParamApplier: PlayParam.() -> Unit = {
    // 重试的时候复用上次的参数
    from(currentPlayParam)
    // 重试都是保持原来设置的 playWhenReady,即使原始请求是不要 keepPlayingState 的,重试也可以设为 true,因为原始请求已经生效了,重试就可以保持了
    keepPlayingState = true
    isSwitchUrl = true
    stopPrevious = false
    isRetry = true
    // 针对有的错误,转换播放类型
    when (errorCode) {
        PLAYER_ERROR_CODE_OPEN_FAILED -> {
            // 打开失败的情况直接按原来的参数重新打开即可,isSwitchUrl 要传 false,否则会没有 onReady、onDuration 回调
            isSwitchUrl = false
            position = this@BaseMediaPlayer.position
        }
        PLAYER_ERROR_CODE_SEEK_FAILED -> {
            playType = PLAYER_PLAY_TYPE_SEEK_RETRY
        }
        PLAYER_ERROR_CODE_SWITCH_QUALITY_FAILED -> {
            // bbp 切换清晰度第一次出错以后会走到这里执行重试,重试需要换播放类型
            playType = PLAYER_PLAY_TYPE_SWITCH_QUALITY_RETRY
        }
    }
}

进入后台和离开视频页后暂停视频解码,需要设置对应视频容器所在页面的 LifecycleOwner,调用 videoPageLifecycleOwner = this@XXXFragment 即可,如果没有设置则会使用构造方法里的 LifecycleOwner。在后台播放时使用 WifiLockManagerWakeLockManager 启用 Wi-Fi 锁和唤醒锁可以让应用在后台也能持续联网,保证播放的流畅性。

在国产的 ROM 里,要想在后台持续播放,保证应用运行的相关权限给够了才是最稳妥的,所以我们还加了个后台播放优化设置页,这个页面框架里不提供,需要业务自行实现。

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

通知栏和播控中心

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

对于通知栏,业务上既有使用系统媒体通知样式的需求,也有使用自定义布局的需求,这些不同样式的通知,基本只有 UI 展示、按钮点击处理上的区别,其他通知逻辑是基本一致的,猫耳播放框架做到了业务只需要设置差异部分,其他 API 调用保持一致。通知基础数据设置如下:

// 音视频通知栏
player.updateNotificationData {
    smallIcon = R.drawable.ic_player_notification
    actionList = arrayListOf(
        PLAYER_NOTIFICATION_ACTION_PLAY,
        PLAYER_NOTIFICATION_ACTION_PAUSE,
        PLAYER_NOTIFICATION_ACTION_PREVIOUS,
        PLAYER_NOTIFICATION_ACTION_NEXT,
        PLAYER_NOTIFICATION_ACTION_FAST_FORWARD,
        PLAYER_NOTIFICATION_ACTION_REWIND
    )
    showActionsInCompactView = arrayListOf(1, 2, 3)
    contentAction = AppConstants.PLAY_ACTION
    contentClassName = MainActivity::class.java.name
    bizType = PLAYER_FROM_MAIN
    groupId = NotificationChannels.Play.groupId
    channelId = NotificationChannels.Play.channelId
    channelName = NotificationChannels.Play.channelName
    channelDesc = NotificationChannels.Play.channelDescription
    visibility = NotificationCompat.VISIBILITY_PUBLIC
}
// 直播通知栏
updateNotificationData {
    smallIcon = R.drawable.ic_notification_small
    forceOngoing = true
    customLayout = R.layout.layout_notification_live_meplayer
    coverRadius = 4
    defaultCover = R.drawable.notification_live_default_avatar
    contentAction = AppConstants.PLAY_ACTION
    contentClassName = MainActivity::class.java.name
    bizType = PLAYER_FROM_LIVE
    groupId = NotificationChannels.Live.groupId
    channelId = NotificationChannels.Live.channelId
    channelName = NotificationChannels.Live.channelName
    channelDesc = NotificationChannels.Live.channelDescription
    visibility = NotificationCompat.VISIBILITY_PUBLIC
}

对于播控的适配主要是要考虑 MIUI、ColorOS 等国产 ROM 和鸿蒙的差异,除鸿蒙之外,基本按官方文档更新 MediaSession 即可,对于鸿蒙则要多一些适配,比如鸿蒙支持下图两种场景:

猫耳 Android 播放框架开发实践,移动开发,架构,Android,android,Android,移动开发,音视频,APP框架

这里面歌词、收藏、快进快退等逻辑都是需要根据不同的业务设置来处理的,目前业务只需要调用播放器对应的字段进行设置即可,使用比较简单。

总结

本文介绍了猫耳FM在 Android 平台上开发媒体播放框架的实践经验,包括架构设计、核心技术、优化改进等方面。希望通过这篇文章,能够给广大的 Android 开发者提供一些有用的参考和启发,也欢迎大家提出宝贵的意见和建议。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap
文章来源地址https://www.toymoban.com/news/detail-736592.html

到了这里,关于猫耳 Android 播放框架开发实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android开发之音乐播放器

    我们大家平时长时间打代码的时候肯定会感到疲惫和乏味,这个时候一边播放自己喜欢的音乐,一边继续打代码,心情自然也愉快很多。音乐带给人的听觉享受是无可比拟的,动听的音乐可以愉悦人的身心,让人更加积极地去热爱生活。接下来就教大家如何用Android Studio自己

    2024年02月08日
    浏览(52)
  • 【Android入门到项目实战-- 11.4】—— ExoPlayer视频播放器框架的详细使用

    目录 什么是ExoPlayer 一、基本使用  1、添加依赖项  2、布局 3、Activity 二、自定义播放暂停 1、首先如何隐藏默认的开始暂停和快进? 2、自定义 三、控制视频画面旋转和比例调整 四、全屏放大和缩小 1、双击视频放大缩小 2、按钮放大缩小 五、完整的实现代码 XML Activity   

    2024年02月11日
    浏览(57)
  • 手机移动开发技术,,Android开发经典实战

    面试题库 按照系统分类 按照大厂分类 《2017-2020字节跳动Android面试真题解析》 大神手写整理笔记类 《Android框架体系架构》 书籍类 不需要太多,精就好! 《第一行代码第二版》 技能提升资料库 一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础

    2024年04月13日
    浏览(48)
  • 小项目开发——Android 音乐播放器

    ◼ 音乐播放器 . ◼ 要求 : Activity 编程、 ListView 编程、 SeekBar 编程、 ExoPlayer 编程( 播放 、 暂停 、 停止 、 上一首 、 下一首 ),音乐文件放在 assets/music 目录下,界面自拟. ◼ 期望最终效果: ◼ 分别对应 activity_music_list.xml 、 activity_my_music_player.xml 的视图. ◼ 点击列表任

    2024年01月21日
    浏览(45)
  • Android移动应用开发的学习路线

    Android移动应用开发的学习路线。以下是一个基本的学习路线,供你参考: 1. Java基础 学习Java的基本语法和面向对象编程(OOP)的概念 了解Java的数据类型、变量和常量 学习控制结构(如条件语句、循环语句)和函数 2. Android基础 了解Android平台的基本概念和架构 学习Android的

    2024年02月11日
    浏览(53)
  • 【Android开发】移动程序设计复习大纲

    一、 判断题 (共10小题,每题1分,共10分) 二、 单选题 (共10小题,每题1分,共10分) 三、 填空题 (共10小题,每空1分,共10分) 四、 简答题 (共4小题,每题10分,共40分) 五、 程序设计题 (共2小题,每空2分,共30分) 知识点: 1. Android 体系结构包含的层次及各层的特点。

    2024年02月01日
    浏览(52)
  • Android手机开发课程设计之音乐播放器

    一、音乐播放器概述与分析 目前手机的音乐播放功能已经是大家比较关注的一个部分,不少在人在购买手机的时候都会关心手机的音乐播放的能力,这也足以看出目前大家对音乐播放功能的重视,所以一款性能良好的手机音乐播放器软件一定会受到欢迎。和传统的音乐播放器

    2024年02月05日
    浏览(55)
  • 移动应用开发环境搭建Android Studio

    记得提前开启电脑虚拟化支持,具体方法可自行百度 查看是否启用虚拟化 JDK安装与卸载 由于Andriod开发使用的语言是javaKotlin,这里使用的是java语言所以需要先安装java的开发环境 所有开发 Android 应用程序需要的工具都是开源的,并且可以从互联网上下载 Android Studio 是谷歌推

    2023年04月08日
    浏览(71)
  • 基于android studio开发的火车票购票系统app,android移动开发课设,毕业设计

    基于android studio开发实现火车票购票系统app 适用于android移动开发学习项目,课程设计,毕业设计等 开发工具:android studio 或者intellij idea专业版 操作系统:windows10 java: JDK11 构建工具Gradle : gradle-7.0.0 模拟器AVD:pixel 3XL API 30 具体AVD配置详情如下 APP功能 该APP包含17个Activity,每

    2024年02月09日
    浏览(55)
  • 如何开发移动应用:iOS和Android的比较

    移动应用开发领域一直以来都备受关注,而iOS和Android作为两大主要的移动操作系统,各自拥有强大的生态系统和开发工具。在本文中,我们将比较iOS和Android移动应用开发的关键方面,以帮助开发者选择合适的平台,或者了解在不同平台上开发应用时需要考虑的因素。 iOS iO

    2024年02月07日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包