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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 17:14:57