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
相关产品推荐
相关产品推荐

