17、ExoPlayer基础使用:播放本地/网络视频、自定义UI、事件监听

说实话,ExoPlayer 是我在 Android 音视频开发里用得最顺手的播放器。Google 官方出品,比 MediaPlayer 灵活太多。我记得第一次接手一个视频播放项目,产品经理要求支持多种格式、自定义控制栏、还要能监听各种播放状态。我当时第一反应就是:用 ExoPlayer 吧,省心。

这一章,我会带你从零开始,把 ExoPlayer 的基本用法捋一遍。包括怎么播本地视频、怎么播网络视频、怎么把 UI 改成你想要的样子,以及怎么监听播放器里那些关键事件。嗯,都是实战里天天要用的东西。

17.1 ExoPlayer 是什么?为什么选它?

ExoPlayer 是 Google 开源的媒体播放器库。它不像 MediaPlayer 那样是系统组件,而是跑在应用进程里的。这意味着你可以自己控制很多东西。

我个人习惯用 ExoPlayer 的原因有三:

  • 支持格式多:DASH、HLS、SmoothStreaming、MP4、MKV、FLV……基本你能想到的它都支持。
  • 可定制性强:UI、渲染器、解码器、加载策略,全都可以自己换。
  • 更新快:Google 一直在维护,新特性、新格式支持都很及时。

我在项目中遇到过一个问题:用 MediaPlayer 播一个 HLS 流,总是卡在某个分片加载上。换成 ExoPlayer 后,调整一下加载策略,问题就解决了。说白了,ExoPlayer 给了你更多控制权。

17.2 快速集成 ExoPlayer

首先,在 build.gradle 里加上依赖:

implementation 'com.google.android.exoplayer:exoplayer:2.19.1'

如果你只需要核心功能,也可以按模块引入:

implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'

我个人建议直接用全量依赖,省得后面缺模块再补。

17.3 播放本地视频

本地视频播放是最基础的场景。你想想看,用户从手机相册里选一个视频,或者 App 自带的 demo 视频,都需要本地播放能力。

代码其实很简单:

// 1. 创建播放器
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();

// 2. 创建媒体项(本地文件)
Uri uri = Uri.parse("file:///storage/emulated/0/Movies/sample.mp4");
MediaItem mediaItem = MediaItem.fromUri(uri);

// 3. 设置媒体项并准备播放
player.setMediaItem(mediaItem);
player.prepare();
player.play();

嗯,这里要注意:本地文件路径一定要有读取权限。Android 6.0 以上需要动态申请 READ_EXTERNAL_STORAGE 权限。我曾经踩过这个坑,用户反馈说视频播不了,结果发现是权限没给。

17.4 播放网络视频

网络视频播放和本地几乎一样,只是把 URI 换成网络地址:

Uri uri = Uri.parse("https://example.com/video.mp4");
MediaItem mediaItem = MediaItem.fromUri(uri);

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

但这里有几个坑,我帮你列一下:

⚠️ 网络播放注意事项:
  • 网络权限:别忘了在 AndroidManifest 里加 INTERNET 权限。
  • 网络变化:WiFi 切到移动网络时,播放可能会中断。建议监听网络状态,做自动暂停或提示。
  • 缓存策略:如果视频比较大,建议用 CacheDataSourceFactory 做缓存,避免重复下载。
  • HTTPS 问题:有些服务器证书有问题,ExoPlayer 默认会拒绝连接。这时候需要自定义 HttpDataSource.Factory

我在项目中遇到过一个问题:用户用 4G 网络播一个 1080p 的视频,卡得不行。后来我加了码率自适应(AdaptiveTrackSelection),根据网络状况自动切换清晰度,体验就好多了。

17.5 自定义 UI

ExoPlayer 默认带了一套 UI,叫 PlayerView。但说实话,大部分项目都需要改 UI。比如换播放按钮的图标、加个倍速切换、改进度条颜色等等。

自定义 UI 的核心思路是:用 PlayerView 作为容器,然后通过 setControllerLayoutId() 替换掉默认的控制栏布局。

举个例子:

<com.google.android.exoplayer2.ui.PlayerView
    android:id="@+id/playerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:controller_layout_id="@layout/custom_controller" />

然后在 custom_controller.xml 里,你可以自由设计控制栏:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center">

    <ImageButton
        android:id="@+id/play_button"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_custom_play" />

    <TextView
        android:id="@+id/speed_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1.0x"
        android:padding="8dp" />

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

</LinearLayout>

然后通过代码绑定这些控件:

PlayerView playerView = findViewById(R.id.playerView);
playerView.setPlayer(player);

// 获取自定义控制栏里的按钮
ImageButton playButton = playerView.findViewById(R.id.play_button);
playButton.setOnClickListener(v -> {
    if (player.isPlaying()) {
        player.pause();
    } else {
        player.play();
    }
});
💡 小技巧:如果你只想改个别控件的样式,不用完全重写控制栏。可以用 app:controller_show_timeout_ms 控制控制栏显示时长,或者用 app:controller_show_rewind_button 隐藏快退按钮。说白了,能不改布局就别改,省事。

17.6 事件监听

事件监听是播放器开发里很重要的一环。你需要知道播放器什么时候准备好了、什么时候出错了、缓冲进度是多少等等。

ExoPlayer 提供了 Player.Listener 接口,你可以重写里面的方法:

player.addListener(new Player.Listener() {
    @Override
    public void onPlaybackStateChanged(int playbackState) {
        switch (playbackState) {
            case Player.STATE_IDLE:
                // 空闲状态
                break;
            case Player.STATE_BUFFERING:
                // 缓冲中
                showBuffering();
                break;
            case Player.STATE_READY:
                // 准备就绪
                hideBuffering();
                break;
            case Player.STATE_ENDED:
                // 播放结束
                onVideoEnded();
                break;
        }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
        // 播放出错
        handleError(error);
    }

    @Override
    public void onIsPlayingChanged(boolean isPlaying) {
        // 播放/暂停状态变化
        updatePlayButton(isPlaying);
    }
});

我在项目中遇到过一个问题:用户反馈说视频播到一半突然停了,但没报错。后来我加了 onIsPlayingChanged 监听,发现是网络波动导致播放器自动暂停了。嗯,这个监听真的很实用。

17.7 核心知识体系

下面这张图帮你梳理了 ExoPlayer 基础使用的核心逻辑:

ExoPlayer 基础使用知识体系 ExoPlayer 本地视频播放 网络视频播放 自定义 UI 事件监听 MediaItem.fromUri 权限申请 网络权限 缓存策略 controller_layout_id 自定义控制栏 Player.Listener 状态/错误监听 核心:灵活、可定制、事件驱动

17.8 完整示例代码

最后,给你一个完整的 Activity 示例,把上面讲的东西串起来:

public class PlayerActivity extends AppCompatActivity {

    private SimpleExoPlayer player;
    private PlayerView playerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);

        playerView = findViewById(R.id.playerView);

        // 创建播放器
        player = new SimpleExoPlayer.Builder(this).build();
        playerView.setPlayer(player);

        // 设置媒体项(网络视频)
        Uri uri = Uri.parse("https://example.com/video.mp4");
        MediaItem mediaItem = MediaItem.fromUri(uri);
        player.setMediaItem(mediaItem);

        // 添加事件监听
        player.addListener(new Player.Listener() {
            @Override
            public void onPlaybackStateChanged(int state) {
                if (state == Player.STATE_READY) {
                    // 准备完成,可以播放了
                }
            }

            @Override
            public void onPlayerError(PlaybackException error) {
                Toast.makeText(PlayerActivity.this, 
                    "播放出错:" + error.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });

        // 开始播放
        player.prepare();
        player.play();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (player != null) {
            player.release();
            player = null;
        }
    }
}

📌 核心要点回顾:

  • ExoPlayer 比 MediaPlayer 更灵活,适合复杂播放场景。
  • 本地和网络播放的代码几乎一样,只是 URI 不同。
  • 自定义 UI 通过替换 controller_layout_id 实现。
  • 事件监听用 Player.Listener,可以监听状态、错误、播放变化等。
  • 别忘了在 onDestroy 里释放播放器,避免内存泄漏。

好了,这一章的内容就到这里。ExoPlayer 的基础用法其实不难,但细节很多。你动手写一遍,遇到问题再回来翻翻,慢慢就熟了。


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