10、播放器集成:ExoPlayer在TV上的配置、播放控制与字幕支持

说到Android TV上的视频播放,ExoPlayer几乎是绕不开的选择。我刚开始做TV开发时,也纠结过要不要直接用MediaPlayer,后来发现ExoPlayer在自定义能力、字幕支持和DASH流播放上,确实比系统自带的播放器灵活太多。说白了,Google官方都在推它,咱们跟着走准没错。

这一章,我会把ExoPlayer在TV上的配置、播放控制、字幕支持这些核心点,结合我实际踩过的坑,给你讲透。

10.1 为什么TV上推荐ExoPlayer?

你想想看,TV端的播放场景和手机端其实差别挺大的。手机用户可能看个短视频就划走了,但TV用户是坐在沙发上,盯着大屏幕看半小时甚至更久。这就对播放器的稳定性、字幕渲染、焦点控制提出了更高的要求。

ExoPlayer相比MediaPlayer,有几个明显的优势:

  • 可定制性强:MediaPlayer是个黑盒子,出了问题你很难干预。ExoPlayer的组件都是可替换的,比如你可以自定义加载策略、渲染器、甚至网络栈。
  • 字幕支持完善:ExoPlayer原生支持SRT、TTML、WebVTT等多种字幕格式,而且渲染效果在TV大屏上表现很好。
  • 自适应码率:DASH和HLS流的支持非常成熟,TV端网络环境复杂,自适应切换能保证播放流畅。
  • 生命周期友好:TV应用经常涉及焦点切换、息屏、休眠等场景,ExoPlayer的状态管理比MediaPlayer清晰得多。

核心结论:如果你在TV上做视频播放,ExoPlayer是首选。MediaPlayer只适合最简单的播放场景,但凡涉及字幕、自适应、自定义UI,ExoPlayer都能省你不少事。

10.2 ExoPlayer在TV上的基础配置

先说说怎么把ExoPlayer集成到TV项目里。其实步骤不复杂,但有几个细节容易忽略。

10.2.1 添加依赖

在build.gradle里加上ExoPlayer的依赖。我个人习惯用最新的稳定版,但要注意版本号之间的兼容性。

dependencies {
    // ExoPlayer核心库
    implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
    // DASH支持
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
    // HLS支持
    implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
    // UI组件(包括字幕渲染)
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
}

嗯,这里要注意一点:如果你只需要播放本地文件,其实exoplayer-core就够了。但TV应用通常要播在线流,所以DASH和HLS的依赖最好都加上,免得后面临时加依赖出问题。

10.2.2 创建播放器实例

ExoPlayer的创建方式比较灵活,我建议用Builder模式,这样方便配置各种参数。

// 创建播放器
ExoPlayer player = new ExoPlayer.Builder(context)
        .setRenderersFactory(new DefaultRenderersFactory(context))
        .setMediaSourceFactory(new DefaultMediaSourceFactory(context))
        .setLoadControl(new DefaultLoadControl())
        .build();

// 绑定到PlayerView
PlayerView playerView = findViewById(R.id.player_view);
playerView.setPlayer(player);

这里有个坑:TV上PlayerView的焦点管理。默认情况下,PlayerView会拦截焦点,导致遥控器无法控制其他UI元素。我遇到过这个问题,后来发现需要在布局里设置android:focusable="false",然后把焦点交给自定义的控制栏。

10.2.3 配置MediaSource

MediaSource是ExoPlayer播放的数据源。对于TV应用,最常见的场景是播放在线视频流。

// 创建DASH媒体源
MediaItem mediaItem = MediaItem.fromUri("https://example.com/manifest.mpd");
player.setMediaItem(mediaItem);
player.prepare();
player.play();

如果是本地文件,直接用Uri.fromFile()就行。但要注意,TV设备通常存储空间有限,不建议把大视频文件放在本地。

10.3 播放控制:TV遥控器的适配

TV上的播放控制和手机完全不同。手机靠触摸,TV靠遥控器。遥控器的按键事件需要映射到播放器的控制逻辑上。

10.3.1 按键映射

我一般会在Activity或Fragment里重写onKeyDown方法,把遥控器的按键转换成播放器的操作。

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    switch (keyCode) {
        case KeyEvent.KEYCODE_DPAD_CENTER:
        case KeyEvent.KEYCODE_ENTER:
            // 播放/暂停切换
            if (player.isPlaying()) {
                player.pause();
            } else {
                player.play();
            }
            return true;
        case KeyEvent.KEYCODE_DPAD_LEFT:
            // 快退10秒
            player.seekTo(player.getCurrentPosition() - 10000);
            return true;
        case KeyEvent.KEYCODE_DPAD_RIGHT:
            // 快进10秒
            player.seekTo(player.getCurrentPosition() + 10000);
            return true;
        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            // 媒体键播放/暂停
            if (player.isPlaying()) {
                player.pause();
            } else {
                player.play();
            }
            return true;
        default:
            return super.onKeyDown(keyCode, event);
    }
}

这里有个细节:快进快退的步长,我建议TV上设10秒,手机端可以设5秒。因为TV屏幕大,用户操作频率低,10秒的步长更符合使用习惯。

10.3.2 自定义控制栏

ExoPlayer自带的PlayerControlView在TV上表现一般,因为它的布局是为触摸设计的。我建议自己写一个控制栏,用Leanback的样式。

// 自定义控制栏布局
<LinearLayout
    android:id="@+id/control_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center"
    android:background="#80000000">

    <ImageButton
        android:id="@+id/btn_rewind"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_rewind"
        android:focusable="true" />

    <ImageButton
        android:id="@+id/btn_play_pause"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:src="@drawable/ic_play"
        android:focusable="true" />

    <ImageButton
        android:id="@+id/btn_forward"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_forward"
        android:focusable="true" />

    <!-- 进度条 -->
    <TextView
        android:id="@+id/tv_current_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#FFFFFF" />

    <SeekBar
        android:id="@+id/seek_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:focusable="true" />

    <TextView
        android:id="@+id/tv_total_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#FFFFFF" />
</LinearLayout>

控制栏的显示和隐藏,我建议用Handler做延时隐藏。用户操作后显示3秒,然后自动消失。这样既不影响观看,又能及时响应。

10.4 字幕支持:TV大屏上的关键体验

字幕在TV上比手机重要得多。手机屏幕小,字幕可有可无;但TV大屏上,字幕是很多用户的刚需,尤其是外语内容或听力障碍用户。

10.4.1 字幕格式与加载

ExoPlayer支持的字幕格式很全,我列个表给你看:

格式 说明 TV上的表现
SRT 最通用的字幕格式 渲染稳定,字体清晰
WebVTT HTML5标准字幕格式 支持样式,可定制性强
TTML MPEG-4标准字幕 适合DASH流,支持多语言
Embedded 视频内嵌字幕 依赖视频源,兼容性一般

加载字幕的方式有两种:一种是字幕文件单独提供,另一种是字幕嵌入在视频流里。我建议优先用单独的字幕文件,因为这样方便切换和更新。

// 加载外部字幕文件
MediaItem.SubtitleConfiguration subtitleConfig = 
    new MediaItem.SubtitleConfiguration.Builder(Uri.parse("https://example.com/subtitle.srt"))
        .setMimeType(MimeTypes.APPLICATION_SUBRIP)
        .setLanguage("zh")
        .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
        .build();

MediaItem mediaItem = new MediaItem.Builder()
        .setUri("https://example.com/video.mp4")
        .setSubtitleConfigurations(Collections.singletonList(subtitleConfig))
        .build();

player.setMediaItem(mediaItem);
player.prepare();

10.4.2 字幕样式定制

TV大屏上,字幕的字体大小和颜色直接影响观看体验。ExoPlayer允许通过CaptionStyleCompat来定制字幕样式。

// 自定义字幕样式
CaptionStyleCompat captionStyle = new CaptionStyleCompat(
        Color.WHITE,          // 字体颜色
        Color.TRANSPARENT,    // 背景颜色(透明)
        Color.BLACK,          // 窗口颜色
        CaptionStyleCompat.EDGE_TYPE_OUTLINE, // 边缘类型
        Color.BLACK,          // 边缘颜色
        Typeface.DEFAULT_BOLD // 字体
);

// 应用到PlayerView
playerView.setCaptionStyle(captionStyle);

我曾经在项目里遇到过一个问题:字幕在TV上显示太小,用户根本看不清。后来发现是默认的字幕尺寸是按手机屏幕优化的。我建议TV上字幕的字体大小至少设到24sp以上,背景用半透明黑色,这样在任何画面上都能看清。

10.4.3 多字幕切换

TV应用经常需要支持多语言字幕。ExoPlayer的TrackSelector可以方便地切换字幕轨道。

// 获取可用的字幕轨道
TrackGroupArray trackGroups = player.getCurrentTrackGroups();
for (int i = 0; i < trackGroups.length; i++) {
    TrackGroup group = trackGroups.get(i);
    if (group.getFormat(0).sampleMimeType.startsWith("text/")) {
        // 这是字幕轨道
        // 切换到指定字幕
        player.setTrackSelectionParameters(
            player.getTrackSelectionParameters()
                .buildUpon()
                .setPreferredTextLanguage("en")
                .build()
        );
        break;
    }
}

这里要注意:切换字幕时,最好在UI上给用户一个反馈,比如显示一个Toast提示“字幕已切换到英文”。TV上用户看不到触摸反馈,视觉提示很重要。

10.5 知识体系总览

为了让你更直观地理解这一章的内容结构,我画了一张图:

ExoPlayer在TV上的核心知识体系 基础配置 • 添加依赖 • 创建播放器实例 • 配置MediaSource • 绑定PlayerView • 生命周期管理 播放控制 • 遥控器按键映射 • 播放/暂停切换 • 快进/快退逻辑 • 自定义控制栏 • 焦点管理 • 进度条同步 字幕支持 • 字幕格式支持 • 外部字幕加载 • 字幕样式定制 • 多字幕切换 • 字体大小适配 • 背景透明度控制 三者结合:打造流畅、可控、字幕清晰的TV播放体验

10.6 避坑指南与最佳实践

做TV播放器开发,有几个坑我不得不提:

坑1:播放器生命周期管理

我曾经在项目里遇到过播放器在Activity销毁后还在后台播放的情况。原因是忘了在onStop里释放播放器。记住:player.release()一定要在onStop或onDestroy里调用,否则会内存泄漏。

坑2:字幕与视频不同步

有些视频流的字幕时间戳不准,导致字幕提前或延迟。我建议在加载字幕时,加一个偏移量调整功能,让用户可以通过遥控器微调字幕时间。

小技巧:预加载提升体验

TV用户不喜欢等待。我建议在用户选择视频后,立即调用player.prepare(),但先不调用player.play()。等用户点击播放时,视频已经缓冲好了,几乎秒开。

10.7 总结

ExoPlayer在TV上的集成,说白了就是三件事:配置好播放器、处理好遥控器控制、渲染好字幕。这三件事做好了,用户的播放体验就不会差。

我个人觉得,字幕这块是TV播放器最容易出彩的地方。很多TV应用的字幕做得粗糙,字体小、背景刺眼、切换卡顿。你只要把字幕样式调好,用户就会觉得这个应用很专业。

嗯,这一章的内容就到这里。下一章我们会聊更进阶的话题,但先把基础打牢,后面才能走得更稳。


公众号:蓝海资料掘金营,微信deep3321