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

Jetpack Compose自定义ExoPlayer:播放控件、画质等定制问题求助

Jetpack Compose自定义ExoPlayer完全指南

1. 基础集成:在Compose中接入ExoPlayer

首先添加ExoPlayer依赖到build.gradle:

implementation "com.google.android.exoplayer:exoplayer:2.19.1"
implementation "com.google.android.exoplayer:exoplayer-ui:2.19.1"

在Compose中通过remember和DisposableEffect管理ExoPlayer生命周期,避免内存泄漏:

@Composable
fun VideoPlayer(url: String) {
    val context = LocalContext.current
    val exoPlayer = remember {
        ExoPlayer.Builder(context).build().apply {
            setMediaItem(MediaItem.fromUri(url))
            prepare()
        }
    }

    DisposableEffect(Unit) {
        onDispose { exoPlayer.release() }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        // 播放器视图 + 自定义控件将在这里组合
    }
}

2. 自定义播放控件(替换默认UI)

ExoPlayer默认的PlayerView基于View系统,我们用AndroidView包裹它并隐藏默认控制器,然后用Compose实现自定义控件:

@Composable
fun CustomPlayerControls(
    exoPlayer: ExoPlayer,
    onToggleFullscreen: () -> Unit,
    onShowQualityMenu: () -> Unit,
    onShowSpeedMenu: () -> Unit
) {
    val isPlaying by remember { derivedStateOf { exoPlayer.isPlaying } }
    val currentPosition by remember { derivedStateOf { exoPlayer.currentPosition } }
    val duration by remember { derivedStateOf { exoPlayer.duration } }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.BottomCenter
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.Black.copy(alpha = 0.7f))
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            // 播放/暂停按钮
            IconButton(onClick = { if (isPlaying) exoPlayer.pause() else exoPlayer.play() }) {
                Icon(
                    imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
                    contentDescription = if (isPlaying) "暂停" else "播放",
                    tint = Color.White
                )
            }

            // 进度条与时间显示
            Row(
                modifier = Modifier.weight(1f),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(formatTime(currentPosition), color = Color.White, fontSize = 12.sp)
                Slider(
                    modifier = Modifier.padding(horizontal = 8.dp),
                    value = currentPosition.toFloat(),
                    onValueChange = { exoPlayer.seekTo(it.toLong()) },
                    valueRange = 0f..duration.toFloat(),
                    colors = SliderDefaults.colors(
                        thumbColor = Color.White,
                        activeTrackColor = Color.White,
                        inactiveTrackColor = Color.Gray
                    )
                )
                Text(formatTime(duration), color = Color.White, fontSize = 12.sp)
            }

            // 功能按钮组
            Row {
                IconButton(onClick = onShowSpeedMenu) {
                    Icon(Icons.Default.Speed, "播放速度", tint = Color.White)
                }
                IconButton(onClick = onShowQualityMenu) {
                    Icon(Icons.Default.HighQuality, "画质选择", tint = Color.White)
                }
                IconButton(onClick = onToggleFullscreen) {
                    val icon = if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE)
                        Icons.Default.FullscreenExit else Icons.Default.Fullscreen
                    Icon(icon, if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) "退出全屏" else "进入全屏", tint = Color.White)
                }
            }
        }
    }
}

// 时间格式化辅助函数
private fun formatTime(millis: Long): String {
    val seconds = (millis / 1000) % 60
    val minutes = (millis / (1000 * 60)) % 60
    return String.format("%02d:%02d", minutes, seconds)
}

将自定义控件与播放器视图组合:

@Composable
fun VideoPlayer(url: String) {
    // ... 之前的ExoPlayer初始化代码 ...

    Box(modifier = Modifier.fillMaxSize()) {
        AndroidView(
            factory = { context ->
                PlayerView(context).apply {
                    player = exoPlayer
                    useController = false // 禁用默认控件
                }
            },
            modifier = Modifier.fillMaxSize()
        )
        CustomPlayerControls(
            exoPlayer = exoPlayer,
            onToggleFullscreen = { /* 全屏逻辑后面实现 */ },
            onShowQualityMenu = { /* 画质逻辑后面实现 */ },
            onShowSpeedMenu = { /* 速度逻辑后面实现 */ }
        )
    }
}

3. 多画质切换实现

通过ExoPlayer的TrackSelector获取视频轨道信息,提供切换选项:

@Composable
fun QualityDialog(exoPlayer: ExoPlayer, onDismiss: () -> Unit) {
    val trackSelector = exoPlayer.trackSelector as DefaultTrackSelector
    val trackGroups = trackSelector.currentMappedTrackInfo?.trackGroups ?: return

    // 提取去重后的分辨率选项
    val qualityOptions = mutableListOf<Pair<String, Int>>()
    for (groupIndex in 0 until trackGroups.length) {
        val group = trackGroups.get(groupIndex)
        if (group.type == C.TRACK_TYPE_VIDEO) {
            for (trackIndex in 0 until group.length) {
                val format = group.getFormat(trackIndex)
                val resolution = "${format.width}x${format.height}"
                if (!qualityOptions.any { it.first == resolution }) {
                    qualityOptions.add(resolution to trackIndex)
                }
            }
        }
    }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("选择画质") },
        text = {
            Column {
                qualityOptions.forEach { (resolution, trackIndex) ->
                    TextButton(onClick = {
                        val parameters = trackSelector.buildUponParameters()
                            .setTrackSelectionOverride(
                                trackGroups.get(0),
                                DefaultTrackSelector.SelectionOverride(trackGroups.get(0), trackIndex)
                            )
                            .build()
                        trackSelector.setParameters(parameters)
                        onDismiss()
                    }) {
                        Text(resolution)
                    }
                }
            }
        },
        confirmButton = { TextButton(onClick = onDismiss) { Text("取消") } }
    )
}

在VideoPlayer中添加弹窗状态控制:

@Composable
fun VideoPlayer(url: String) {
    // ... 其他代码 ...
    val showQualityDialog = remember { mutableStateOf(false) }

    if (showQualityDialog.value) {
        QualityDialog(exoPlayer = exoPlayer, onDismiss = { showQualityDialog.value = false })
    }

    Box(modifier = Modifier.fillMaxSize()) {
        // ... AndroidView ...
        CustomPlayerControls(
            exoPlayer = exoPlayer,
            onShowQualityMenu = { showQualityDialog.value = true },
            // 其他回调
        )
    }
}

4. 播放速度调整

直接调用ExoPlayer的setPlaybackSpeed方法实现速度切换:

@Composable
fun SpeedDialog(exoPlayer: ExoPlayer, onDismiss: () -> Unit) {
    val speedOptions = listOf(0.5f, 1.0f, 1.5f, 2.0f)
    val currentSpeed by remember { derivedStateOf { exoPlayer.playbackParameters.speed } }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("播放速度") },
        text = {
            Column {
                speedOptions.forEach { speed ->
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        RadioButton(
                            selected = currentSpeed == speed,
                            onClick = {
                                exoPlayer.setPlaybackSpeed(speed)
                                onDismiss()
                            }
                        )
                        Text("${speed}x", modifier = Modifier.padding(start = 8.dp))
                    }
                }
            }
        },
        confirmButton = { TextButton(onClick = onDismiss) { Text("确定") } }
    )
}

同样在VideoPlayer中添加弹窗状态:

@Composable
fun VideoPlayer(url: String) {
    // ... 其他代码 ...
    val showSpeedDialog = remember { mutableStateOf(false) }

    if (showSpeedDialog.value) {
        SpeedDialog(exoPlayer = exoPlayer, onDismiss = { showSpeedDialog.value = false })
    }

    Box(modifier = Modifier.fillMaxSize()) {
        // ... AndroidView ...
        CustomPlayerControls(
            exoPlayer = exoPlayer,
            onShowSpeedMenu = { showSpeedDialog.value = true },
            // 其他回调
        )
    }
}

5. 全屏模式实现

通过调整Activity屏幕方向和系统UI实现全屏:

@Composable
fun VideoPlayer(url: String) {
    val context = LocalContext.current
    val activity = context.findActivity() ?: return
    val configuration = LocalConfiguration.current
    val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

    val toggleFullscreen = {
        activity.requestedOrientation = if (isLandscape) {
            ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        } else {
            ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
        }
    }

    // 全屏时隐藏状态栏与导航栏
    DisposableEffect(isLandscape) {
        val window = activity.window
        val decorView = window.decorView
        val flags = if (isLandscape) {
            View.SYSTEM_UI_FLAG_FULLSCREEN or
                    View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
                    View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
        } else {
            0
        }
        decorView.systemUiVisibility = flags
        onDispose { decorView.systemUiVisibility = 0 }
    }

    // ... 其他代码 ...
    Box(modifier = Modifier.fillMaxSize()) {
        // ... AndroidView ...
        CustomPlayerControls(
            exoPlayer = exoPlayer,
            onToggleFullscreen = toggleFullscreen,
            // 其他回调
        )
    }
}

// 获取Activity辅助函数
fun Context.findActivity(): Activity? {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    return null
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 22:08:18