MEPlayer – An Android Cross‑Process Media Playback Framework for Maoer FM
MEPlayer is a cross‑process Android playback framework for Maoer FM that unifies audio, video, live‑stream and special‑effect playback by abstracting kernels such as ExoPlayer, BBP and IJK behind a simple API, offering business‑level decoupling, automatic recovery, audio‑focus handling, notification integration, network optimizations and both cross‑process (MEPlayer) and same‑process (MEDirectPlayer) entry points.
Maoer FM is one of the largest post‑95 Chinese audio content platforms and a key partner of Bilibili. Its Android app contains many audio, video, live‑stream and special‑effect playback scenarios, originally built with a mixture of IJK, ExoPlayer and MediaPlayer. The tightly coupled playback logic caused high maintenance cost and frequent bugs, prompting the need for a unified, decoupled playback framework.
The resulting framework, MEPlayer, is a cross‑process playback solution that abstracts the underlying player kernels (ExoPlayer, BBP, IJK, etc.) behind a simple API. It supports audio/video, live streaming, special‑effect playback, short‑video, playlist transitions, dubbing shows, and more. MEPlayer eliminates duplicated logic, reduces business coupling, and provides a consistent development experience.
Key Features
Support for audio, video, live, and special‑effect playback.
Customizable playback kernel – built‑in ExoPlayer and BBP, extensible to IJK or others.
Business‑level decoupling – the framework is used across multiple internal teams.
Cross‑process playback with automatic recovery when the playback process is killed.
Automatic audio‑focus management with optional focus‑stealing suppression.
Foreground‑service handling for both main and playback processes to improve background survival.
Notification and media‑session integration for Android, HarmonyOS and other ROMs.
Network optimization using Cronet (HTTP/2, HTTP/3) for faster start‑up.
Continuous caching support for ExoPlayer (custom modifications).
The framework provides two entry points: MEPlayer for cross‑process playback and MEDirectPlayer for same‑process playback (used for splash screens or very short start‑up sounds where cross‑process latency is unacceptable).
Surface handling is abstracted: developers only need to pass a View (TextureView or SurfaceView). The framework manages listeners, supports switching between containers, and ensures seamless transition when a video card moves from a list to a detail page.
The internal state machine and playback flow are illustrated by the following diagrams (omitted here). The start‑up flow uses an interceptor pattern, allowing custom interceptors for HTTPS, traffic‑free handling, or dynamic URL replacement.
Example of a player pre‑processor interface (kept unchanged):
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
}Typical usage of MEPlayer with Kotlin DSL callbacks:
private val mPlayer = MEPlayer(this).apply {
onReady {
// 打开 url 资源成功回调
}
onDuration {
// 更新时长
}
onPlayingStateChanged { isPlaying, from ->
// 更新播放状态
}
onPositionUpdate {
// 更新播放进度
}
onCompletion {
// 播放结束
}
onRetry {
// 播放出错会自动调用 onRetry 进行重试,如果业务没有实现则跳转到 onError
// onRetry 是一个 suspend 方法,可以进行耗时操作,需要返回一个 url,可以是 player.originUrl,也可以是请求后端返回的一个新 url
}
onError {
// 错误处理
}
}The constructor of MEPlayer allows passing a LifecycleOwner so that the player releases resources automatically when the owner is destroyed:
/**
* 播放器构造方法,大多数场景都应该使用 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
) {
// implementation omitted
}Audio‑focus can be configured globally:
player.run {
audioFocusGain = AUDIO_FOCUS_GAIN_TRANSIENT
ignoreFocusLoss = true
}Notification data for audio/video and live streams is set via a DSL as well:
// 音频通知栏
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
}Background‑play optimizations include using startForeground with a shared notification, acquiring WifiLock and WakeLock , and handling process‑kill recovery by persisting playback state in the main process.
Retry logic is centralized via a playParamApplier lambda that re‑uses previous parameters and adjusts behavior based on error codes:
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 = [email protected]
}
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
}
}
}In summary, the article presents the design, implementation, and optimization details of the MEPlayer framework, offering Android developers practical insights into building a robust, modular, and high‑performance media playback solution.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.