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

Android Auto媒体应用中PlaybackState的ACTION_STOP无法正常工作

问题:Android Auto媒体应用无法暂停/停止,控制按钮显示异常

开发的Android Auto媒体应用已实现MediaItem创建、元数据展示及基础播放功能,但存在以下问题:

  • 仅能播放媒体,无法执行暂停或停止操作
  • 播放启动后,界面显示停止按钮而非预期的暂停按钮
  • 点击停止按钮无任何响应

完整MusicService代码

//  imports here

class MusicService : MediaBrowserServiceCompat() {
    private lateinit var mediaSession: MediaSessionCompat
    private lateinit var mediaController: MediaControllerCompat
    private var playbackStateBuilder = PlaybackStateCompat.Builder()

    override fun onCreate() {
        super.onCreate()

        mediaSession = MediaSessionCompat(baseContext, "MusicService").apply {
            setCallback(MyMediaSessionCallback())
            setPlaybackState(
                playbackStateBuilder.apply {
                    setState(PlaybackStateCompat.STATE_NONE, 0L, 1f)
                    addActionsForPlayback(this)
//                    addCustomActionsForPlayback(this)
                }.build()
            )
//            setFlags(
//                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or 
//  MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
            )
            isActive = true
        }
        sessionToken = mediaSession.sessionToken

        mediaController = MediaControllerCompat(baseContext, mediaSession.sessionToken).apply {
            registerCallback(MyMediaControllerCallback())
        }
    }

    override fun onGetRoot(
        clientPackageName: String, clientUid: Int, rootHints: Bundle?
    ): BrowserRoot {
        val extras = Bundle()
        extras.putBoolean(
            MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
        )
        extras.putInt(
            MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
            MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
        )
        extras.putInt(
            MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
            MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
        )

        return BrowserRoot("/", extras)
    }

    override fun onLoadChildren(
        parentId: String, result: Result<MutableList<MediaItem>>
    ) {
        val treeResult = BrowseTree(baseContext)[parentId]

        if (treeResult.isNullOrEmpty()) {
            result.detach()
        } else {
            result.sendResult(treeResult)
        }
    }

    private fun addActionsForPlayback(playbackStateCompatBuilder: PlaybackStateCompat.Builder) {
        playbackStateCompatBuilder.apply {
            setActions(PlaybackStateCompat.ACTION_PLAY)
            setActions(PlaybackStateCompat.ACTION_PAUSE)
//            setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
            setActions(PlaybackStateCompat.ACTION_STOP)
            setActions(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)
            setActions(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)
            setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        }
    }

    private fun addCustomActionsForPlayback(playbackStateCompatBuilder: PlaybackStateCompat.Builder) {
        //  custom actions defined here
    }

    inner class MyMediaSessionCallback : MediaSessionCompat.Callback() {
        private val musicSource = MusicSource(this@MusicService)
        private var seekedPos: Long? = null

        override fun onPrepare() {
            println("Preparing")

            super.onPrepare()
        }

        override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) {
            println("Preparing from media id")

            super.onPrepareFromMediaId(mediaId, extras)
        }

        override fun onPlay() {
            println("Playing")
            mediaSession.setPlaybackState(
                playbackStateBuilder.apply {
                    setState(PlaybackStateCompat.STATE_PLAYING, seekedPos ?: 0L, 1f)

                    setActions(PlaybackStateCompat.ACTION_PAUSE)
                    setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
                    setActions(PlaybackStateCompat.ACTION_STOP)
                    setActions(PlaybackStateCompat.ACTION_SEEK_TO)
                    setActions(PlaybackStateCompat.ACTION_PLAY)
                }.build()
            )

            super.onPlay()
        }

        override fun onPause() {
            println("Paused")

            super.onPause()
        }

        override fun onStop() {
            println("Stopped")
            mediaSession.release()

            super.onStop()
        }

        override fun onSeekTo(pos: Long) {
            println("Seeked to: $pos")
            seekedPos = pos
            if (mediaController.playbackState.state == 3) {
                mediaSession.setPlaybackState(
                    playbackStateBuilder.apply {
                        setState(PlaybackStateCompat.STATE_PLAYING, pos, 1f)

                        addActionsForPlayback(this)
                    }.build()
                )
            } else {
                mediaSession.setPlaybackState(
                    playbackStateBuilder.apply {
                        setState(PlaybackStateCompat.STATE_PAUSED, pos, 1f)

                        addActionsForPlayback(this)
                    }.build()
                )
            }

            super.onSeekTo(pos)
        }

        override fun onRewind() {
            println("Rewind command")

            super.onRewind()
        }

        override fun onPlayFromMediaId(mediaId: String?, extras: Bundle) {
            seekedPos = null

            val defaultArt = BitmapFactory.decodeResource(
                this@MusicService.resources, R.drawable.music_library_icon_2
            )
            val albumCover = musicSource.getAlbumCover(extras.getString("PATH", ""))

            mediaSession.apply {
                setPlaybackState(playbackStateBuilder.apply {
                    setState(PlaybackStateCompat.STATE_STOPPED, 0L, 1f)
                    setBufferedPosition(extras.getLong(MediaMetadataCompat.METADATA_KEY_DURATION))

                    setActions(PlaybackStateCompat.ACTION_PLAY)
                    setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
                    setActions(PlaybackStateCompat.ACTION_STOP)
                    setActions(PlaybackStateCompat.ACTION_SEEK_TO)
                    setActions(PlaybackStateCompat.ACTION_PAUSE)

                    val playbackStateExtras = Bundle().apply {
                        putString(
                            MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
                        )
                    }
                    setExtras(playbackStateExtras)

                    addActionsForPlayback(this)
                }.build())
                setMetadata(
                    MediaMetadataCompat.Builder().apply {
                        putString(
                            MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
                        )
                        putString(
                            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)
                        )
                        putString(
                            MediaMetadataCompat.METADATA_KEY_ARTIST,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
                        )
                        putString(
                            MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
                        )
                        putString(
                            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
                        )
                        putLong(
                            MediaMetadataCompat.METADATA_KEY_DURATION,
                            extras.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
                        )
                        putString(
                            MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
                            extras.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI)
                        )
                        if (albumCover != null) {
                            putBitmap(
                                MediaMetadataCompat.METADATA_KEY_ART, albumCover
                            )
                        } else {
                            putBitmap(
                                MediaMetadataCompat.METADATA_KEY_ART, defaultArt
                            )
                        }
                        putLong(
                            MediaConstants.METADATA_KEY_IS_EXPLICIT,
                            extras.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT)
                        )
                    }.build()
                )
            }

            println("Playing from Media Id")

            super.onPlayFromMediaId(mediaId, extras)
        }

        override fun onPlayFromSearch(query: String?, extras: Bundle?) {
            println("Playing from Search")

            super.onPlayFromSearch(query, extras)
        }

        override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
            println("Playing from Uri")

            super.onPlayFromUri(uri, extras)
        }

        override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
            println("Command: $command")

            super.onCommand(command, extras, cb)
        }

        override fun onCustomAction(action: String?, extras: Bundle?) {
            println("Action: $action")

            super.onCustomAction(action, extras)
        }

        override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
            println("Media button event")

            return super.onMediaButtonEvent(mediaButtonEvent)
        }
    }

    inner class MyMediaControllerCallback : MediaControllerCompat.Callback() {
        override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
            println("Controller media id: ${metadata?.description?.mediaId}")

            super.onMetadataChanged(metadata)
        }

        override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
            println("Controller playback state : ${state?.state}")

            super.onPlaybackStateChanged(state)
        }

        override fun onSessionReady() {
            println("Controller session is ready")

            super.onSessionReady()
        }
    }
}

尝试过的无效操作

多次调用setActions设置单个操作,未解决问题:

setActions(PlaybackStateCompat.ACTION_PLAY)
setActions(PlaybackStateCompat.ACTION_PAUSE)
setActions(PlaybackStateCompat.ACTION_STOP)
setActions(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)
setActions(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)
setActions(PlaybackStateCompat.ACTION_SEEK_TO)

问题根源及解决方案

1. setActions使用错误(核心问题)

PlaybackStateCompat.Builder.setActions()方法接收的是位掩码,多次调用会覆盖之前的设置,最终只有最后一次调用的action生效。必须通过位或(or)将所有支持的操作合并:

修改addActionsForPlayback方法:

private fun addActionsForPlayback(playbackStateCompatBuilder: PlaybackStateCompat.Builder) {
    playbackStateCompatBuilder.setActions(
        PlaybackStateCompat.ACTION_PLAY or
                PlaybackStateCompat.ACTION_PAUSE or
                PlaybackStateCompat.ACTION_STOP or
                PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
                PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
                PlaybackStateCompat.ACTION_SEEK_TO or
                PlaybackStateCompat.ACTION_PLAY_PAUSE
    )
}

同时,onPlay和onPlayFromMediaId中不要重复调用单个setActions,直接调用上述方法即可。

2. 未开启MediaSession的核心标志

注释掉的setFlags必须启用,否则MediaSession无法处理传输控制命令(暂停、停止等):

在MediaSession初始化时恢复并设置flags:

mediaSession = MediaSessionCompat(baseContext, "MusicService").apply {
    setCallback(MyMediaSessionCallback())
    setPlaybackState(
        playbackStateBuilder.apply {
            setState(PlaybackStateCompat.STATE_NONE, 0L, 1f)
            addActionsForPlayback(this)
        }.build()
    )
    setFlags(
        MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or 
        MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
    )
    isActive = true
}

3. onPause/onStop未更新PlaybackState状态

暂停或停止后,必须更新PlaybackState的状态,否则UI无法感知状态变化,按钮不会更新:

修改MyMediaSessionCallback中的onPause和onStop:

override fun onPause() {
    println("Paused")
    // 更新播放状态为暂停,并设置支持的操作
    mediaSession.setPlaybackState(
        playbackStateBuilder.apply {
            setState(PlaybackStateCompat.STATE_PAUSED, mediaController.playbackState.position, 1f)
            addActionsForPlayback(this)
        }.build()
    )
    super.onPause()
}

override fun onStop() {
    println("Stopped")
    // 更新播放状态为停止,再释放session
    mediaSession.setPlaybackState(
        playbackStateBuilder.apply {
            setState(PlaybackStateCompat.STATE_STOPPED, 0L, 1f)
            addActionsForPlayback(this)
        }.build()
    )
    mediaSession.release()
    super.onStop()
}

4. 避免复用全局PlaybackStateBuilder(可选优化)

全局的playbackStateBuilder会保留之前的设置,可能导致状态混乱,建议每次创建新的Builder:

// 移除全局变量,每次创建新的Builder
// private var playbackStateBuilder = PlaybackStateCompat.Builder()

// 使用时创建新实例
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder().apply {
        setState(PlaybackStateCompat.STATE_PLAYING, seekedPos ?: 0L, 1f)
        addActionsForPlayback(this)
    }.build()
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 13:59:54