Media3切换视频轨道问题:视频仅显示在屏幕局部
问题描述
使用Media3框架实现视频播放功能,点击齿轮图标可打开视频轨道选择对话框,但切换视频分辨率后出现两个问题:
- 播放器循环卡顿
- 视频仅显示在屏幕部分区域,无法保持全屏宽度
相关代码实现
ExoplayerScreen代码
@UnstableApi @Preview(device = "id:pixel_5", showSystemUi = true, showBackground = true) @Composable fun ExoplayerScreenPreview() { ExoplayerScreen() } @UnstableApi @Composable fun ExoplayerScreen() { MyStatusBar(color = colorResource(id = R.color.colorPrimaryDark)) Scaffold(topBar = { MyToolBar() }, content = { padding -> ExoplayerScreenContent(Modifier.padding(padding)) }) } @UnstableApi @Composable fun ExoplayerScreenContent(modifier: Modifier) { Box( modifier = modifier .fillMaxWidth() .height(230.dp) ) { VideoScreen() } }
VideoScreen代码
@SuppressLint("OpaqueUnitKey") @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @Composable fun VideoScreen() { val context = LocalContext.current val playerView = PlayerView(context) val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { setMediaItem( MediaItem.fromUri( "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8" ) ) prepare() play() videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING } } var isPlaying by remember { mutableStateOf(false) } exoPlayer.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlayingValue: Boolean) { isPlaying = isPlayingValue } }) DisposableEffect(Box { AndroidView(modifier = Modifier.fillMaxSize(), factory = { playerView.apply { // Resizes the video in order to be able to fill the whole screen resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT player = exoPlayer // Hides the default Player Controller useController = false // Fills the whole screen layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) } }) VideoLayout( isPlaying = isPlaying, onPlay = { exoPlayer.play() }, onPause = { exoPlayer.pause() }, exoPlayer = exoPlayer, ) }) { onDispose { exoPlayer.release() } } }
VideoLayout代码
@Composable fun VideoLayout( isPlaying: Boolean, onPlay: () -> Unit, onPause: () -> Unit, exoPlayer: ExoPlayer, ) { Box(modifier = Modifier .fillMaxSize() .clickable { if (isPlaying) { onPause() } else { onPlay() } }) { AnimatedVisibility( visible = !isPlaying, enter = fadeIn(tween(200)), exit = fadeOut(tween(200)) ) { Box( modifier = Modifier .fillMaxSize() .background( Color.Black.copy(alpha = 0.6f) ), contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Rounded.Pause, contentDescription = null, tint = Color.White ) } Box( modifier = Modifier .fillMaxSize() .padding(vertical = 15.dp, horizontal = 20.dp), contentAlignment = Alignment.BottomEnd ) { val context = LocalContext.current Column( verticalArrangement = Arrangement.spacedBy(15.dp) ) { IconButton(onClick = { trackSeleccion(exoPlayer, context) }) { Icon( imageVector = Icons.Rounded.Settings, contentDescription = null, tint = Color.White ) } IconButton(onClick = { /*TODO*/ }) { Icon( imageVector = Icons.Rounded.Comment, contentDescription = null, tint = Color.White ) } IconButton(onClick = { /*TODO*/ }) { Icon( imageVector = Icons.Rounded.IosShare, contentDescription = null, tint = Color.White ) } } } } } }
trackSeleccion代码
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) fun trackSeleccion(exoplayer: ExoPlayer, context: Context) { val trackSelector = exoplayer.trackSelector as DefaultTrackSelector val mappedTrackInfo = trackSelector.currentMappedTrackInfo if (mappedTrackInfo != null) { val rendererIndex = 2 val rendererType = mappedTrackInfo.getRendererType(rendererIndex) val allowAdaptiveSelections = rendererType == C.TRACK_TYPE_VIDEO || (rendererType == C.TRACK_TYPE_AUDIO && mappedTrackInfo.getTypeSupport( C.TRACK_TYPE_VIDEO ) == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS) val builder = TrackSelectionDialogBuilder(context, "Selección de pistas", exoplayer, rendererIndex) builder.setShowDisableOption(false) builder.setAllowAdaptiveSelections(allowAdaptiveSelections) builder.setOverrides(exoplayer.trackSelectionParameters.overrides) builder.build().show() } exoplayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING Log.e("trackSeleccion", "trackSeleccion: ") }
问题修复方案
1. 修正视频Renderer索引
trackSeleccion中使用的rendererIndex = 2错误,Media3默认Renderer顺序为视频(0)、音频(1)、字幕(2),选择视频轨道需用索引0:
val rendererIndex = 0
错误索引会导致轨道切换逻辑异常,引发卡顿或显示问题。
2. 统一缩放模式配置
当前代码同时设置PlayerView.resizeMode和ExoPlayer.videoScalingMode,两者冲突导致视频无法全屏。建议只保留PlayerView的resizeMode设置,并改为适合全屏的模式:
在VideoScreen的AndroidView工厂中修改:
playerView.apply { // 填充父容器并保持比例,超出部分裁剪,确保全屏显示 resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL player = exoPlayer useController = false layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) }
同时移除exoPlayer.videoScalingMode的设置,避免双重控制。
3. 修复DisposableEffect的Key问题
原代码中DisposableEffect的Key是Box组件,会导致重组时频繁创建/释放播放器引发卡顿。将Key改为exoPlayer,确保仅播放器实例变化时触发释放逻辑:
DisposableEffect(exoPlayer) { AndroidView(modifier = Modifier.fillMaxSize(), factory = { playerView.apply { resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL player = exoPlayer useController = false layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) } }) VideoLayout( isPlaying = isPlaying, onPlay = { exoPlayer.play() }, onPause = { exoPlayer.pause() }, exoPlayer = exoPlayer, ) onDispose { exoPlayer.release() } }
4. 用remember缓存PlayerView
原代码中PlayerView未用remember包裹,每次重组都会创建新实例导致播放器状态异常,修改为:
val playerView = remember(context) { PlayerView(context) }
5. 轨道切换后强制刷新显示
在trackSeleccion中,添加轨道切换监听器确保视频显示正常:
builder.setTrackSelectionListener { _, _ -> // 轨道切换完成后刷新缩放模式与播放状态 exoplayer.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING) exoplayer.playWhenReady = exoplayer.playWhenReady }
内容的提问来源于stack exchange,提问作者Eduardo Lynch Araya
相关产品推荐
相关产品推荐

