You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中Media3 MediaSession Service初始化崩溃问题

解决Jetpack Compose中Media3 MediaController未初始化崩溃问题

崩溃原因分析

  1. Compose初始渲染提前访问未初始化变量:onCreate中Compose直接调用mediaController.toString(),但mediaController是在onStart的异步回调中才完成初始化,此时变量未赋值,直接触发lateinit property has not been initialized异常。
  2. 异步初始化竞态问题:即使UI不直接访问,点击播放按钮时play函数也可能在mediaController初始化完成前被调用,同样会抛出异常。
  3. MediaSessionService未正确启动:MediaController需要连接到运行中的服务,如果服务未启动,controllerFuture的初始化会延迟甚至失败,导致mediaController一直无法赋值。
  4. Service逻辑错误:onGetSession中提前调用player.prepare()不符合设计逻辑,播放控制应由MediaController发起,而非服务主动执行。

修复方案

1. 用Compose状态管理MediaController

将mediaController改为可空状态变量,让Compose根据初始化状态渲染UI,避免直接访问未初始化的变量。

2. 提前启动MediaSessionService

在初始化SessionToken前启动服务,保证MediaController能顺利连接到Session。

3. 调整异步初始化逻辑

初始化完成后更新状态变量,让Compose感知变化并重新渲染。

4. 修正Service逻辑

移除onGetSession中的player.prepare(),同时添加前台服务逻辑(Android 12+要求媒体播放服务必须为前台服务)。

修正后的代码

MainActivity.kt

class MainActivity : ComponentActivity() {
    private val _mediaController = mutableStateOf<MediaController?>(null)
    private val mediaController: MediaController? get() = _mediaController.value
    private lateinit var controllerFuture: ListenableFuture<MediaController>

    private fun play() {
        mediaController?.let { controller ->
            val mediaItem = MediaItem.Builder()
                .setMediaId("https://sd2.djjohal.com/128/519928/Sip%20Sip%20-%20Geeta%20Zaildar%20(DJJOhAL.Com).mp3")
                .build()

            controller.setMediaItem(mediaItem)
            controller.prepare()
            controller.play()
        }
    }

    override fun onStart() {
        super.onStart()
        // 提前启动MediaSessionService
        startService(Intent(this, AudioPlayBackService::class.java))
        
        val sessionToken = SessionToken(this, ComponentName(this, AudioPlayBackService::class.java))
        controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
        
        controllerFuture.addListener({
            try {
                _mediaController.value = controllerFuture.get()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }, MoreExecutors.directExecutor())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            Media3testTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    // 根据MediaController状态渲染UI
                    mediaController?.let { controller ->
                        Text(text = "Controller已初始化: ${controller.sessionToken}")
                    } ?: run {
                        Text(text = "Controller加载中...")
                    }

                    PlayPause(
                        modifier = Modifier.padding(innerPadding),
                        isPlaying = mediaController?.isPlaying ?: false,
                        onPlayPause = ::play,
                    )
                }
            }
        }
    }

    override fun onStop() {
        MediaController.releaseFuture(controllerFuture)
        _mediaController.value = null
        super.onStop()
    }
}

@Composable
fun PlayPause(
    modifier: Modifier = Modifier,
    isPlaying: Boolean,
    onPlayPause: () -> Unit,
) {
    IconButton(modifier = modifier, onClick = onPlayPause) {
        if (isPlaying) {
            Icon(Icons.Default.Pause, contentDescription = "暂停")
        } else {
            Icon(Icons.Default.PlayArrow, "播放")
        }
    }
}

AudioPlayBackService.kt

class AudioPlayBackService: MediaSessionService(), MediaSession.Callback {
    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()

        val player: Player = ExoPlayer.Builder(this)
            .build()

        mediaSession = MediaSession
            .Builder(this, player)
            .setCallback(this)
            .setId("BeatBloomPlayBackService")
            .build()
        
        // Android 12+要求媒体播放服务必须为前台服务,需提前创建通知渠道
        val notification = Notification.Builder(this, "media_playback_channel")
            .setContentTitle("BeatBloom")
            .setContentText("正在播放媒体")
            .setSmallIcon(R.drawable.ic_launcher)
            .setStyle(Notification.MediaStyle())
            .build()
        
        startForeground(1, notification)
    }

    override fun onAddMediaItems(
        mediaSession: MediaSession,
        controller: MediaSession.ControllerInfo,
        mediaItems: MutableList<MediaItem>,
    ): ListenableFuture<MutableList<MediaItem>> {
        val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
        return Futures.immediateFuture(updatedMediaItems)
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        // 移除提前prepare逻辑,播放流程由Controller控制
        return mediaSession
    }

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        stopForeground(STOP_FOREGROUND_REMOVE)
        super.onDestroy()
    }
}

额外说明

  • 需提前创建通知渠道:在应用启动时添加渠道创建代码,否则Android 8.0+无法显示前台通知。
  • 播放按钮图标逻辑修正:原逻辑图标显示错误,已调整为播放状态显示暂停图标,未播放状态显示播放图标。

内容的提问来源于stack exchange,提问作者Prabh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 01:15:12