车载音视频App框架设计

简介

统一播放器提供媒体播放一致性的交互和视觉体验,减少各个媒体应用和场景独自开发的重复工作量,实现媒体播放链路的一致性,减少碎片化的Bug。本文面向应用开发者介绍如何快速接入媒体播放器。

主要功能:

  1. 新设计的统一播放UI组件,视频支持的手势操作,包括左右滑拖动进度、左上下滑调节亮度、右上下滑调节音量、双击暂停/播放,同时对外暴露了长按手势接口应用可以实现类似快进快退功能;
  2. 支持方控、Mini播放器、PSD的播控和状态显示,应用只需要申请媒体中心权限的license即可,支持媒体中心的状态保持和语音控制通道,需要APP中接入对应的接口;
  3. 支持视频行车娱乐限制,非P档播放视频会暂停播放弹框提示;
  4. 使用原生的的media3 1.3.0版本,支持原生的androidx 和framework media session;
  5. 支持音频焦点自管理,包括电话状态的播控也基于音频焦点统一处理;

整体架构图如下

  1. 应用通过配置UI,然后创建MediaController播放媒体;
  2. controler端接入了行车娱乐限制的检测和提示;
  3. service端实现了google原生的media3 session service;
  4. 媒体中心通过mediasession和播放器连接;
  5. 底层使用media3 exoplayer播放和音频焦点管理;

接入流程

1、配置依赖
implementation 'com.max.mediaplayer:uniteplayer:1.x.x

备注:uniteplayer中包含了视频控制UI组件,当只需要修复控制UI组件的bug时,可以更新整体uniteplayer播放器组件,也可以只增加UI组件的依赖,比如:implementation ('com.max.media:media-video:1.2.0')。

2、初始化

建议在application创建的时候调用初始化接口:

init(context: Context, isVideo: Boolean, enableMediaCenter: Boolean = false)

参数 说明
context 应用context
isVideo 是否视频应用,视频会初始化车机相关的adapterapi,音乐目前不会初始化
enableMediaCenter 需要远程方控、语音控制、mini播放器控制这些能力时设置为true,否则为false,默认为false
3、接入 UI 组件 (视频应用)

这里的UI组件只针对视频应用,音频应用的UI组件在媒体组件库中,参考媒体视频组件

1)配置PlayerView

在布局中增加FlexPlayerView,视频内容显示和播控UI都在此view中,一般默认配置如下即可:

<com.max.uniteplayer.ui.FlexPlayerView
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

其中播控UI是可单独接入的,若需定制,接入见视频组件接入文档

2)配置视频渲染view

一般情况下不需要特别配置,支持的定制视频显示view参数如下:

比如在上面FlexPlayerView xml中用app:surface_type="surface_view"

surface_type:surface类型,取值如下,默认是surface_view类型:

<attr name="surface_type" format="enum"> 
     <enum name="none" value="0"/>   
     <enum name="surface_view" value="1"/>  
     <enum name="texture_view" value="2"/>  
     <enum name="spherical_gl_surface_view" value="3"/>  
     <enum name="video_decoder_gl_surface_view" value="4"/>
 </attr>

resize_mode,取值如下,默认fit模式:

  <attr name="resize_mode" format="enum">  
    <enum name="fit" value="0"/>  
    <enum name="fixed_width" value="1"/>
    <enum name="fixed_height" value="2"/> 
    <enum name="fill" value="3"/> 
    <enum name="zoom" value="4"/> 
 </attr>

first_render_delay,首帧显示延迟时长,用于规避首帧渲染慢闪拉伸的问题,int型,单位ms,一舨不需要设置,默认0;

3)配置视频播控View

FlexPlayerView本身对外暴露了两个接口:

接口 说明
fun setTitleBarVisible(visible: Boolean) 因此视频播放器中顶部title和关闭按钮,一般应用自己实现的可以通过此接口隐藏
fun enableUpDownGesture(enable: Boolean) 使能上下滑动,默认播放器是支持的,可以设置false关闭
bindControlView(listener: FlexPlayerView.ControlViewListener) 获取视频播控view控制器,提供更多配置能力,比如增加手势监听回调、添加自定义的播控按钮等,具体可以参考视频组件接入文档
4、使用播放器播放

有3种播放接口,选择其中一个:

1)FlexSimplePlayer

封装的最高层播放接口,内部封装好了原生MediaItem对象,简单的视频播放场景推荐使用此接口:

class FlexSimplePlayer(context: Context?,
    isVideo: Boolean,
    enableParkDetect: Boolean = true,
    enableParkDialog: Boolean = true,
    controllerCallback: FlexControllerCallback? = null)

参数说明:

参数 说明
context 上下文;
enableParkDetect 只针对视频生效,是否启用行车娱乐限制,非驻车档会自动暂停视频播放(默认是强制要求的);
enableParkDialog 只针对视频生效,是否弹出行车娱乐限制框,设置为disable后需要应用可以自己实现提示界面;

开始播放:

player.startPlay(context: Context,
        mediaData: List<MediaData>,
        view: FlexPlayerView?,
        listener: FlexMediaListener?,
        mediaCenterData: MediaCenterData? = null)

参数说明:

参数 说明
context 上下文,注意视频播放要使用activity的context,因为行车娱乐限制需要弹框
mediaData 媒体列表,每个媒体item包括url、metadata、mimetiype,其中url是媒体地址,可以是本地、在线点播或直播地址,medadata是media3的原始类型,title和artist字段会显示在mini播放器和PSD上,mimeTypes指定媒体类型,url是对应后缀的无需设置,有些比如优酷投屏中有一个直播url是/m3u8结尾 而不是.m3u8结尾 需要设置mimeTypes = MimeTypes.APPLICATION_M3U8;
view 类型为FlexPlayerView,自实现UI的此参数设置为null
listener 媒体播放监听回调,具体回调接口后面详细展开,不需要监听设置为null
mediaCenterData 媒体中心中需要用的数据,比如应用包名、图标等,具体类型后面详细展开,这里需要注意的是sourceType要设置为6,在媒体中心中对应SourceType.SOURCE_TYPE_ONLINE。
controllerCallback 媒体中心回调的方法,包括播放/暂停、上/下曲切换,在此回调中可以实现自定义处理逻辑,并屏蔽底层播放器的响应;

退出界面停止播放,停止播放后会释放所有播放资源:

player.stopPlay()

页面切换的处理建议:

视频应用界面退到后台(onPuse)暂停: player.pause()

视频应用从后台回到前台(onResume): player.play()

更新播放列表:

fun updateMediaItems(mediaData: List<MediaData>)

在已经进入播放状态下,更换播放的视频建议使用该接口,避免重新startPlay()会更慢。

2)FlexMediaController

音频类应用复杂场景建议使用该接口。

FlexSimplePlayer下层的接口,使用该接口需要自己创建MediaItem,mimeType需要调用util接口设置。适合需要自己构建比较复杂的mediaitem场景,另外没有直接提供暂停和播放接口,需要调用成员player的暂停和播放接口。

构造方法解释同FlexSimplePlayer。

开始播放:

fun startPlay(context: Context,
    mediaItems: List<MediaItem>,
    view: FlexPlayerView?,
    listener: FlexMediaListener?,
    customData: MediaCenterData?,
    controllerCallback: FlexControllerCallback? = null)

参数说明:

参数 说明
context 上下文,注意视频播放要使用activity的context,因为行车娱乐限制需要弹框
mediaItems 媒体列表,原生类型,包括媒体url,metadata,mimetype等
view 类型为FlexPlayerView,自实现UI的此参数设置为null
listener 媒体播放监听回调,具体回调接口后面详细展开,不需要监听设置为null
mediaCenterData 媒体中心中需要用的数据,比如应用包名、图标等,具体类型后面详细展开,这里需要注意的是sourceType要设置为6,在媒体中心中对应SourceType.SOURCE_TYPE_ONLINE。
controllerCallback 媒体中心回调的方法,包括播放/暂停、上/下曲切换,在此回调中可以实现自定义处理逻辑,并屏蔽底层播放器的响应;

FlexControllerCallback接口:

interface FlexControllerCallback {
    companion object {

        const val KEY_ACTION_TYPE = "actionType"
        const val KEY_CALLBACK_RESULT = "callbackResult"

        const val TYPE_PLAY = 1
        const val TYPE_NEXT = 2
        const val TYPE_PREVIOUS = 3

        const val RESULT_OK = 0
        const val RESULT_BLOCK = 1
    }

    fun onDefaultCallback(bundle: Bundle): Bundle?{
        return Bundle.EMPTY
    }

    fun onPlayAction(): Int?{
        return RESULT_OK
    }

    fun onSeekToNext(): Int?{
        return RESULT_OK
    }

    fun onSeekToPrevious(): Int?{
        return RESULT_OK
    }
}

接口描述:

接口 说明
onPlayAction(): Int? 媒体中心调用过来的播放/暂停接口
onSeekToNext(): Int? 媒体中心调用过来的下一首接口
onSeekToPrevious(): Int? 媒体中心调用过来的上一首接口

退出界面停止播放,停止播放后会释放所有播放资源:

player.stopPlay()

页面切换的处理建议:

视频应用界面退到后台(onPuse)暂停: player.player?.pause()

视频应用从后台回到前台(onResume): player.player?.play()

更新播放列表:

fun updateMediaItems(mediaItems: List<MediaItem>)

在已经进入播放状态下,更换播放的视频建议使用该接口,避免重新startPlay()会更慢。

3)FlexExoPlayerController

此接口一般不会用到,前面两个接口默认会启动sessionservice,支持媒体中心的连接,此接口直接返回exoplayer播放器,不会创建mediasession,不支持媒体中心连接,支持视频行车娱乐限制。使用媒体UI组件和播放器需要在应用中自行对接,适用于定制化比较多,不需要远程播放支持的视频播放场景,目前只用在赛道模式。建议优先选用前面两种方式播放。

构造方法解释同FlexSimplePlayer。

fun getExoPlayer(audioFocus: Boolean = false,
 mediaListener: FlexMediaListener? = null): ExoPlayer?

参数和返回值:

参数 说明
audioFocus 是否打开音频焦点自管理,默认是false。在赛道模式情况存在两个视频同时播的情况,需要设置为false否则因为音频焦点抢占不能同时播放,其他场景建议设置为true;
mediaListener 状态回调,同前面两个接口,具体参数后面详细描述;
返回值 media3 ExoPlayer对象。
5、状态回调接口

自定义的播放回调接口:

interface FlexMediaListener{

    //player加载完成
    fun onLoadPlayerFinished(player: Player?) {}

    //返回true不会自动播放,需要调用play()后才能播放,默认false
    fun pauseWhenStart(): Boolean {
        return false
    }

    //直接返回player的状态接口,更多复杂场景建议拿player对象处理
    fun onPlaybackStateChange(state: Int){}
    fun onPlayWhenReadyChanged(ready: Boolean, reason: Int) {}
    fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {}

    //播放异常
    fun onPlayerError(errorCode: Int, errorData: Bundle?) {}

    //只针对视频,播放视频时检测到非驻车状态回调,会自动暂停播放
    fun onStartVideoWhenNoPark() {}

    //只针对视频,非驻车播放视频,弹框点击关闭按钮后的回调
    fun onStopVideoWhenNoPark() {}

    //关闭播放页面
    fun onClose(){}

    //退出应用
    fun onExitApp() {}

    //Mote: This is callback from media center, not from player
    fun onSeekToNext(): Boolean {
        return false
    }
    fun onSeekToPrevious(): Boolean {
        return false
    }
}

接口描述:

接口 说明
onLoadPlayerFinished(player: Player?) 开始播放是异步的调用,此回调表示已完成了到服务端的连接返回了有效的player接口
fun pauseWhenStart(): Boolean 开始播放后是否自动暂停,需要用户手动点击播放,默认false
fun onPlaybackStateChange(state: Int) fun onPlayWhenReadyChanged(ready: Boolean, reason: Int) fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) 原生Player接口播放状态变化的透传,state取值:Player.STATE_IDLE:还未开始播放STATE_BUFFERING:缓冲中STATE_READY:准备好播放,结合PlayWhenReady状态来判断,true为播放中,false为暂停STATE_ENDED:播放结束onPlayWhenReadyChanged取值: true为播放中,false为暂停onMediaItemTransition:切歌可以通过这里判断
fun onPlayerError(errorCode: Int, errorData: Bundle?) 播放异常,errorCode时原生的错误码,errorData暂时没有用到
fun onStartVideoWhenNoPark() 只针对视频,档检测到非P档播放视频时会回调此接口
fun onStopVideoWhenNoPark() 只针对视频,非P播放视频弹框后,点击关闭按钮或弹框自动退出时的回调
fun onClose() 用户点击左上角关闭按钮
fun onExitApp() 退出应用,暂未用到
fun onSeekToNext() 方控或PSD切换下一曲操作会回调,返回true表示应用来接管下一曲操作不会调用底层播放器的下一曲接口,返回false底层会调用播放器的下一曲接口, 目前投屏用到
fun onSeekToPrevious() 方控或PSD切换上一曲操作会回调,返回true表示应用来接管上一曲操作不会调用底层播放器的上一曲接口,返回false底层会调用播放器的上一曲接口,,投屏用到
6、语音控制

媒体中心语音控制接口,声明支持的语音语义,以及处理语音控制的回调

fun declareVrSemanticsCapability(channelInfo: MCVrChannelInfo?,
 vrSemanticCallback: VrSemanticCallback)

参数说明:

参数 说明
channelInfo MCVrChannelInfo类型,具体字段: mediaPackageName: 应用包名 mediaVersion: 应用版本 mediaDescription: 应用描述 channelDataType: 通道类型,一般设置为0 semantics: IntArray 支持的语义数组,MCSemanticsType.CONTROL_PLAY等
vrSemanticCallback 语音的回调,具体的实现可参考媒体中心接入文档
7、播放状态保持

媒体中心的状态保持功能可实现应用的自启动(通过媒体中心拉起),首先在开始播放的时候需要在mediacenterdata中设置recoveryIntent;

示例:

        val intent = Intent()
        intent.setPackage("com.max.example")
        intent.action = "com.max.example.action.RecoverService"
        intent.putExtra("type", "StateRecover")

后面在车机重启后,通过如下接口获取之前的播放状态:

fun getRecoveryPlaybackInfo (infoCallback: Consumer<MCPlaybackInfo?>)

其中MCPlaybackInfo类型和媒体中心中的MusicPlaybackInfo对应,通过其中的包名可以判断上一次是否自己播放然后更新媒体中心到迷你播放器,实现重启播放恢复的功能。

8、自定义通道
1)媒体中心通道

媒体中心通道用于更新Mini播放器和PSD状态

接口:FlexMediaController.sendCustomAction(action: Int, bundle: Bundle? = null)

  1. 更新歌词:

     val bd = Bundle()
     bd.putString(Constants.KEY_LYRIC_STRING, "hello lyric")
     mediaController?.sendCustomAction(Constants.ACTION_UPDATE_LYRIC, bd)
    
  2. 更新播放状态:

用于在非播放状态下强制更新媒体信息到媒体中心。

    mediaController?.sendCustomAction(Constants.ACTION_UPDATE_MEDIACENTER, null)
2)远程MediaSession通道

远程Mediasession主要用于更新RSD的信息。

Androidx原生接口:MediaController.sendCustomCommand(command: SessionCommand, bundle: Bundle)

  1. 设置歌词:

     val bd = Bundle()
     bd.putString("lyrics", lyric)
     bd.putString("lyrics_tr", lyricTr)  //英文歌词
     it.sendCustomCommand(
         SessionCommand(
             Constants.COMMAND_SET_SESSION_EXTRA,
             Bundle.EMPTY
         ), bd
     )
    
  2. 设置试看点:

     val bd = Bundle()
     bd.putBoolean("isCanTrail", isCanTrail)//true试听歌曲,false非试听歌曲
     bd.putLong("trail_start", start)
     bd.putLong("trail_end", end)
     it.sendCustomCommand(
         SessionCommand(
             Constants.COMMAND_SET_SESSION_EXTRA,
             Bundle.EMPTY
         ), bd
     )
    
9、更多功能使用原生Player/MediaController接口

有其他更多播放场景的需求,可以通过FlexSimplePlayer/FlexMediaController/player直接拿到media3的原始player对象实现。

注意事项

  1. Media3要求工程的compileSDK>=34,这个改动会影响编译过程,不影响运行时。可能会涉及少量系统接口参数适配否则编译会报错,targetSDK建议不动;
  2. Player的接口调用在主线程进行,对应的状态listener回调也会在主线程中(需要在同一个线程中,否则会报异常);
  3. startPlay()和stopPlay()调用时机需要匹配,比如在onCreateView中调用startPlay() 在onDestoryView中调用stopPlay(),或者在startPlay()之前先调用stopPlay();
  4. 全屏的视频播放,activity的decorView.windowSystemUiVisibility需要设置View.SYSTEM_UI_FLAG_FULLSCREEN,这样行车娱乐限制的弹框会根据全屏的状态隐藏状态栏和docker栏;
  5. 当前版本还不支持mediasession service多进程;

参考Demo

uniteplayerdemo(视频)

音频类可参考杜比播放器和网易云音乐APP等应用,附网易云APP的播放架构:

遗留问题

1、电话中播放控制还未在框架中实现,需要应用来处理;

相关推荐
m0_571957581 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
姑苏风2 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
数据猎手小k5 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天6 小时前
java的threadlocal为何内存泄漏
java
caridle6 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express