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

Android Jetpack Compose中Media3 HLS流后台播放及自定义HTTP头问题

解决Media3结合后台服务播放带自定义HTTP头的HLS流问题

问题描述

在Jetpack Compose中使用Media3结合后台播放服务实现HLS流播放时,遇到以下问题:

  • MediaController没有setMediaSource方法,无法直接在Composable中传入自定义HTTP头的MediaSource
  • MediaItem.Builder不支持直接接收MediaSource(使用Media3 1.3.0版本)
  • 不确定应该在Composable还是后台服务中处理带自定义头的播放逻辑

用户提供的Composable代码如下:

@Composable
fun PlayerTest(
    token: String,
    url: String,
) {
    val context = LocalContext.current

    val dataSourceFactory = DataSource.Factory {
        val dataSource = DefaultHttpDataSource.Factory().createDataSource()
        dataSource.setRequestProperty("Authorization", token)
        dataSource
    }

    val hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory)
        .createMediaSource(MediaItem.fromUri(url))

    val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
    val controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()

    DisposableEffect(
        Box {
            AndroidView(
                factory = {
                    PlayerView(context).apply {
                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
                        useController = false
                        layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                    }
                },
                update = { playerView ->
                    controllerFuture.addListener({
                        val controller = controllerFuture.get()
                        controller.setMediaSource(hlsMediaSource) // <--- NOT FOUND
                        playerView.setPlayer(controller)
                    }, MoreExecutors.directExecutor())
                }
            )

            AudioPlayer(...)
        },
    ) {
        onDispose {
            MediaController.releaseFuture(controllerFuture)
        }
    }
}

核心原因

  • MediaController是远程播放控制器,仅负责与后台MediaSession通信发送控制指令,无法直接传递本地构造的MediaSource(MediaSource无法跨进程序列化)
  • MediaItem与MediaSource职责分离:MediaItem用于描述媒体的元数据和地址,MediaSource是具体的播放数据源,因此MediaItem.Builder不支持直接传入MediaSource

正确解决方案:在后台服务中构建带自定义头的MediaSource

所有涉及自定义数据源的逻辑必须放在后台播放服务中实现,Composable仅负责传递播放参数和控制播放状态。

1. 改造后台播放服务(PlaybackService)

在服务内创建ExoPlayer实例,接收播放参数并构建带自定义HTTP头的MediaSource:

class PlaybackService : MediaSessionService() {
    private lateinit var exoPlayer: ExoPlayer
    private lateinit var mediaSession: MediaSession

    override fun onCreate() {
        super.onCreate()
        exoPlayer = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession.Builder(this, exoPlayer).build()
        setSessionToken(mediaSession.sessionToken)
        
        // 注册自定义命令回调
        mediaSession.setCallback(object : MediaSession.Callback() {
            override fun onCustomCommand(
                session: MediaSession,
                controller: MediaSession.ControllerInfo,
                command: String,
                extras: Bundle
            ): ListenableFuture<Bundle> {
                if (command == "START_HLS_PLAYBACK") {
                    val token = extras.getString("TOKEN") ?: return Futures.immediateFailedFuture(IllegalArgumentException("Token缺失"))
                    val url = extras.getString("URL") ?: return Futures.immediateFailedFuture(IllegalArgumentException("URL缺失"))
                    startHlsPlayback(token, url)
                    return Futures.immediateFuture(Bundle())
                }
                return super.onCustomCommand(session, controller, command, extras)
            }
        })
    }

    // 内部实现HLS播放逻辑
    private fun startHlsPlayback(token: String, url: String) {
        // 构建带Authorization头的DataSourceFactory
        val dataSourceFactory = DataSource.Factory {
            DefaultHttpDataSource.Factory().createDataSource().apply {
                setRequestProperty("Authorization", token)
            }
        }
        // 构建HLS MediaSource并设置到ExoPlayer
        val mediaItem = MediaItem.fromUri(url)
        val hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory)
            .createMediaSource(mediaItem)
        
        exoPlayer.setMediaSource(hlsMediaSource)
        exoPlayer.prepare()
        exoPlayer.play()
    }

    override fun onDestroy() {
        exoPlayer.release()
        mediaSession.release()
        super.onDestroy()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        return mediaSession
    }
}

2. 在Composable中传递参数并控制播放

不再在Composable中构建MediaSource,而是通过MediaController向服务发送自定义命令传递参数:

@Composable
fun PlayerTest(
    token: String,
    url: String,
) {
    val context = LocalContext.current
    val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
    val controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()

    DisposableEffect(token, url) {
        controllerFuture.addListener({
            val controller = controllerFuture.get()
            // 发送自定义命令给服务,传递播放参数
            val extras = Bundle().apply {
                putString("TOKEN", token)
                putString("URL", url)
            }
            controller.sendCustomCommand(
                MediaCommand("START_HLS_PLAYBACK", extras),
                null,
                null
            )
        }, MoreExecutors.directExecutor())

        onDispose {
            MediaController.releaseFuture(controllerFuture)
        }
    }

    // 渲染PlayerView并绑定MediaController
    Box {
        AndroidView(
            factory = {
                PlayerView(context).apply {
                    resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
                    useController = false
                    layoutParams = FrameLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                    )
                }
            },
            update = { playerView ->
                controllerFuture.addListener({
                    playerView.setPlayer(controllerFuture.get())
                }, MoreExecutors.directExecutor())
            }
        )
        AudioPlayer(...)
    }
}

补充说明

  • 如果是首次启动播放,也可以直接通过Intent向服务传递参数,无需使用自定义命令
  • 确保后台服务已在Manifest中注册,并声明FOREGROUND_SERVICE权限(后台播放需要前台服务)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 13:35:26