Google ExoPlayer播放器框架详解及应用实践

在这里插入图片描述
作者:谭东


我们都知道,音视频的播放处理在各个平台都是一个常用的操作和功能,尤其在移动Android平台音视频播放变得复杂得多,要处理不同操作系统版本间的API差别、软硬件的不同、直播点播流的处理、不同音视频编解码的处理、不同流协议的支持等等复杂的操作。以前大多数人对简单的音视频都使用MediaPlayer来处理,不过对于一些企业应用级别的应用来说,MediaPlayer是完全不行的。所以就要基于FFMPEG进行相关的开发,目前开源的大型播放框架有:VLC、IjkPlayer、Google ExoPlayer等。接下来的内容里我们将主要给大家介绍目前最强大的应用级开源媒体播放器框架:Google ExoPlayer。本文将主要介绍:

  • ExoPlayer的特点及简单介绍
  • ExoPlayer支持的媒体类型和格式
  • ExoPlayer的简单使用
  • ExoPlayer的高级应用实践
  • ExoPlayer总结

ExoPlayer的特点及简单介绍

ExoPlayer是Google官方推出的一款开源的应用级别的音视频播放框架,它是一个独立的库,所以我们可以在我们的项目中进行相应的库引用,非常的方便。也可以自己通过开源代码进行定制、修改、扩展。

ExoPlayer的标准音频和视频组件基于Android的MediaCodec API构建,所以不支持Android 4.1(API级别16)及以下的版本中使用。所以我们一般我们是将ExoPlayer应用于Android4.4及以上系统中进行使用。ExoPlayer支持在Android MediaPlayer API中所不支持的特性和格式,性能也要远远高于MediaPlayer。ExoPlayer支持更多的音视频格式、协议并支持字幕功能、FFMEPG扩充。

当然,ExoPlayer框架仅提供了一些基础的音视频播放操作API,如果使用的话我们需要自己定制封装相应的播放器、并修改扩展功能等。

接下来我们看下ExoPlayer框架的优缺点:

优点:

  • 支持HTTP动态自适应流媒体(DASH)和SmoothStreaming(这两者在MediaPlayer上都不支持),并支持HLS协议和TS流的播放。当然ExoPlayer支持的远不止这些,更多格式后面会介绍;
  • 不同版本兼容性好,不会由于不同设备和Android版本间的变化而出现问题,更加的稳定;
  • 是独立的库,体积小、升级方便;
  • 支持自定义扩展,支持FFMPEG扩展;
  • 支持播放列表功能,使得音视频可以无缝播放、支持剪辑和合并播放功能;
  • 在Android 4.4(API级别19)及更高版本上支持Widevine通用加密;
  • 支持快速和其他库的集成;
  • 支持字幕;
  • 支持媒体下载。

缺点:

  • 对于某些设备上的纯音频播放,ExoPlayer可能比MediaPlayer消耗更多的电量。

当然,基于ExoPlayer开发的一些开源项目我们也可以参考学习下。如Google官方的开源基于ExoPlayer的音频播放器Universal Android Music Player Sample(https://github.com/android/uamp ) 、Android TV端的电视播放器(https://github.com/jaychou2012/TV_ExoPlayer ) 等。

ExoPlayer支持的媒体类型和格式

之前提到过ExoPlayer相比MediaPlayer支持更多的音视频格式、协议,接下来我们就给大家列举下ExoPlayer支持的一些媒体类型和格式:

分别从:DASH、SmoothStreaming、HLS、Progressive container formats来看。

先看DASH:

ExoPlayer支持多种容器格式的DASH。媒体流的音频、视频、字幕必须是在独立轨道上索引上的。

(Containers:容器格式;Closed captions/subtitles:字幕格式;Metadata:元数据;Content protection:内容版权保护)
Google ExoPlayer播放器框架详解及应用实践_第1张图片
默认是不支持TS流播放的,不过我们可以进行扩展进行支持。

SmoothStreaming:

(Containers:容器格式;Closed captions/subtitles:字幕格式;Content protection:内容版权保护)
Google ExoPlayer播放器框架详解及应用实践_第2张图片
HLS:

ExoPlayer支持多种容器格式的HLS流,非常强大。

(Containers:容器格式;Closed captions/subtitles:字幕格式;Metadata:元数据;Content protection:内容版权保护)
Google ExoPlayer播放器框架详解及应用实践_第3张图片
Progressive container formats:

ExoPlayer可以直接播放以下容器格式的流。

(Containers:容器格式)
Google ExoPlayer播放器框架详解及应用实践_第4张图片
除了以上这些以外,Google ExoPlayer也内置支持FFMEPG的扩展,FFmpeg的扩展库支持解码各种不同的音频视频格式。我们可以通过将命令行参数传递给FFmpeg的configure来选择要支持的解码器:
Google ExoPlayer播放器框架详解及应用实践_第5张图片
ExoPlayer也支持独立字幕格式,如下:
Google ExoPlayer播放器框架详解及应用实践_第6张图片
最后我们看下ExoPlayer库各个功能所支持的最低Android设备版本:
Google ExoPlayer播放器框架详解及应用实践_第7张图片
基本上Android 4.1以后的系统版本的主要功能都支持。

ExoPlayer的简单使用

通过以上简单的对ExoPlayer的介绍,相信大家对ExoPlayer框架有了一个大致的了解了。官方Github开源地址:https://github.com/google/ExoPlayer

建议大家可以将官方的源码下载下来,里面也包含了一个官方例子,大家可以进行相应的源码分析和学习。

官方Demo运行图:
Google ExoPlayer播放器框架详解及应用实践_第8张图片
Demo包含了ExoPlayer的一些基本用法,如播放、暂停、快进、列表播放切换等。

官方的ExoPlayer源码主要包含以下几个部分:
Google ExoPlayer播放器框架详解及应用实践_第9张图片
ExoPlayer源码核心就是在library里。

接下来我们就进行ExoPlayer的简单使用吧。

如果不进行源码修改的话,我们可以直接通过依赖库方式进行引用:
项目根目录的build.gradle里添加仓库地址。

repositories {
    google()
    jcenter()
}

项目app目录的下build.gradle里添加ExoPlayer库地址。

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'

// 例如我这里使用2.10.7版本:
implementation 'com.google.android.exoplayer:exoplayer:2.10.7'

具体的版本号信息和更新的概要可以在这里查看:https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md

这样引用的话,是将ExoPlayer的完整版本库都引入进来了。

如果只需要引入其中的几个功能模块的话,我们也可以分拆开进行引用:

implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.X.X'

根据自己的需要进行引用,core核心包必须引用,ui包也建议引用。

开启Java8语法支持:

compileOptions {
  targetCompatibility JavaVersion.VERSION_1_8
}

如果需要源码引用依赖的话,直接下载源码引用即可:

git clone https://github.com/google/ExoPlayer.git
cd ExoPlayer
git checkout release-v2

ExoPlayer的FFmpeg扩展提供FfmpegAudioRenderer,使用FFmpeg进行解码,并可以呈现各种格式编码的音频。

ExoPlayer库的核心是ExoPlayer接口,ExoPlayer的API暴露了基本上大部分的媒体播放操作功能,比如缓冲媒体、播放、暂停和快进、媒体监听等功能。

基本功能使用的话我们只需要关心这几个类:

  • PlayerView:播放器的渲染界面UI;
  • SimpleExoPlayer/ExoPlayer:播放器核心API类;
  • MediaSource:用于加载音视频的播放源地址,MediaSource有很多扩展类,如 ConcatenatingMediaSource、ClippingMediaSource、LoopingMediaSource、MergingMediaSource、DashMediaSource、SsMediaSource、HlsMediaSource、ProgressiveMediaSource等,都有不同的功能。
  • DefaultTrackSelector:音轨设置,一般使用DefaultTrackSelector 即可。

接下来我们看下具体使用步骤:

布局中引入PlayerView:

 <com.google.android.exoplayer2.ui.PlayerView 
      android:id="@+id/player_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent"/>

最基础的核心播放步骤:

playerView = findViewById(R.id.player_view);

Uri uris = new Uri[1];
uris[0] = Uri.parse("https://www.apple.com/105/media/us/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-tpl-cc-us-20170912_1280x720h.mp4");

SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(this);

DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this,
                Util.getUserAgent(this, "yourApplicationName"));

MediaSource videoSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(uris[0]);

playerView.setPlayer(player);
player.setPlayWhenReady(true);//是否自动播放
player.prepare(videoSource);

播放效果如下图:
Google ExoPlayer播放器框架详解及应用实践_第10张图片
怎么样是不是很简单?

如果想监听播放相关的:

player.addListener(new PlayerEventListener());//播放监听

private class PlayerEventListener implements Player.EventListener {

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            if (playbackState == Player.STATE_ENDED) {
                //播放完毕
            }
        }

        @Override
        public void onPlayerError(ExoPlaybackException e) {
            //播放错误
        }

        @Override
        @SuppressWarnings("ReferenceEquality")
        public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
            //音轨变化
            if (trackGroups != lastSeenTrackGroupArray) {
                MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
                if (mappedTrackInfo != null) {
                    if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
                            == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
                        showToast(R.string.error_unsupported_video);
                    }
                    if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
                            == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
                        showToast(R.string.error_unsupported_audio);
                    }
                }
                lastSeenTrackGroupArray = trackGroups;
            }
        }
    }

其他基本操作的API类似MediaPlayer:

//恢复播放
playerView.onResume();
//暂停
playerView.onPause();
//停止播放
player.stop();
//停止并释放资源
player.release();
//快进
player.seekTo(1000);
... ...

接下来给一个基础应用中的比较完善的代码,稍微复杂些:

public class PlayActivity extends AppCompatActivity implements PlaybackPreparer {
    private PlayerView playerView;
    private DataSource.Factory dataSourceFactory;
    private SimpleExoPlayer player;
    private MediaSource mediaSource;
    private DefaultTrackSelector trackSelector;
    private DefaultTrackSelector.Parameters trackSelectorParameters;
    private TrackGroupArray lastSeenTrackGroupArray;
    private int stereoMode;//声道模式:左声道、右声道、立体声等
    private Uri[] uris;
    private String[] extensions;

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

    private void initView() {
        playerView = findViewById(R.id.player_view);

        uris = new Uri[1];
        uris[0] = Uri.parse("https://www.apple.com/105/media/us/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-tpl-cc-us-20170912_1280x720h.mp4");
        extensions = new String[1];
        extensions[0] = ".mp4";

        dataSourceFactory = buildDataSourceFactory();

        playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
        playerView.requestFocus();
        stereoMode = C.STEREO_MODE_MONO;
        stereoMode = C.STEREO_MODE_TOP_BOTTOM;
        stereoMode = C.STEREO_MODE_LEFT_RIGHT;
        //设置声道模式
//        ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);

        RenderersFactory renderersFactory = buildRenderersFactory(false);

        trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
        TrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory();
        trackSelector = new DefaultTrackSelector(trackSelectionFactory);
        trackSelector.setParameters(trackSelectorParameters);

        player =
                ExoPlayerFactory.newSimpleInstance(
                        /* context= */ this, renderersFactory, trackSelector);
        player.addListener(new PlayerEventListener());//播放监听
        player.setPlayWhenReady(true);//是否自动播放
        //事件分析监听
        player.addAnalyticsListener(new EventLogger(trackSelector));
        playerView.setPlayer(player);
        playerView.setPlaybackPreparer(this);

        MediaSource[] mediaSources = new MediaSource[uris.length];
        for (int i = 0; i < uris.length; i++) {
            //根据每个播放地址构建对应的MediaSource
            mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
            //也可以不传后缀参数
//            mediaSources[i] = buildMediaSource(uris[i]);
        }
        //如果只有一个视频地址就直接赋值,如果有多个就封装一层ConcatenatingMediaSource,进行列表播放
        mediaSource =
                mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
        //准备播放
        player.prepare(mediaSource, true, false);
    }

    /**
     * Returns a new DataSource factory.
     */
    private DataSource.Factory buildDataSourceFactory() {
        return ((BaseApplication) getApplication()).buildDataSourceFactory();
    }

    public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
        @DefaultRenderersFactory.ExtensionRendererMode
        int extensionRendererMode = DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON;
        return new DefaultRenderersFactory(/* context= */ this)
                .setExtensionRendererMode(extensionRendererMode);
    }

    @Override
    public void onResume() {
        super.onResume();
        if (Util.SDK_INT <= 23 || player == null) {
            if (playerView != null) {
                playerView.onResume();
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (Util.SDK_INT <= 23) {
            if (playerView != null) {
                playerView.onPause();
            }
            releasePlayer();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (Util.SDK_INT > 23) {
            if (playerView != null) {
                playerView.onPause();
                player.stop();
            }
            releasePlayer();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        releasePlayer();
    }

    private void releasePlayer() {
        if (player != null) {
            player.release();
            player = null;
            mediaSource = null;
            trackSelector = null;
        }
    }

    private MediaSource buildMediaSource(Uri uri) {
        return buildMediaSource(uri, null);
    }
    // 根据视频的协议和封装格式类型进行自动的创建对应的MediaSource
    private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
        @C.ContentType int type = Util.inferContentType(uri, overrideExtension);
        switch (type) {
            case C.TYPE_DASH:
                return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
            case C.TYPE_SS:
                return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
            case C.TYPE_HLS:
                return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
            case C.TYPE_OTHER:
//                return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
                return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
            default:
                throw new IllegalStateException("Unsupported type: " + type);
        }
    }

    @Override
    public void preparePlayback() {

    }

    private class PlayerEventListener implements Player.EventListener {

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            if (playbackState == Player.STATE_ENDED) {
                //播放完毕
            }
        }

        @Override
        public void onPlayerError(ExoPlaybackException e) {
            //播放错误
        }

        @Override
        @SuppressWarnings("ReferenceEquality")
        public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
            //音轨变化
            if (trackGroups != lastSeenTrackGroupArray) {
                MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
                if (mappedTrackInfo != null) {
                    if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
                            == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
                        showToast(R.string.error_unsupported_video);
                    }
                    if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
                            == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
                        showToast(R.string.error_unsupported_audio);
                    }
                }
                lastSeenTrackGroupArray = trackGroups;
            }
        }
    }

    private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {

        @Override
        public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
            String errorString = getString(R.string.error_generic);
            if (e.type == ExoPlaybackException.TYPE_RENDERER) {
                Exception cause = e.getRendererException();
                if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
                    // Special case for decoder initialization failures.
                    MediaCodecRenderer.DecoderInitializationException decoderInitializationException =
                            (MediaCodecRenderer.DecoderInitializationException) cause;
                    if (decoderInitializationException.decoderName == null) {
                        if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) {
                            errorString = getString(R.string.error_querying_decoders);
                        } else if (decoderInitializationException.secureDecoderRequired) {
                            errorString =
                                    getString(
                                            R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
                        } else {
                            errorString =
                                    getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
                        }
                    } else {
                        errorString =
                                getString(
                                        R.string.error_instantiating_decoder,
                                        decoderInitializationException.decoderName);
                    }
                }
            }
            return Pair.create(0, errorString);
        }
    }

    private void showToast(int string) {
        Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
    }
}

Application配置:


public class BaseApplication extends Application {
    private static final String TAG = "DemoApplication";
    private static final String DOWNLOAD_ACTION_FILE = "actions";
    private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
    private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";

    protected String userAgent;

    private DatabaseProvider databaseProvider;
    private File downloadDirectory;
    private Cache downloadCache;
    private DownloadManager downloadManager;
    private DownloadTracker downloadTracker;

    @Override
    public void onCreate() {
        super.onCreate();
        userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
    }

    /**
     * Returns a {@link DataSource.Factory}.
     */
    public DataSource.Factory buildDataSourceFactory() {
        DefaultDataSourceFactory upstreamFactory =
                new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
        return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
    }

    /**
     * Returns a {@link HttpDataSource.Factory}.
     */
    public HttpDataSource.Factory buildHttpDataSourceFactory() {
        return new DefaultHttpDataSourceFactory(userAgent);
    }

    /**
     * Returns whether extension renderers should be used.
     */
    public boolean useExtensionRenderers() {
        return "withExtensions".equals(BuildConfig.FLAVOR);
    }

    public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
        @DefaultRenderersFactory.ExtensionRendererMode
        int extensionRendererMode =
                useExtensionRenderers()
                        ? (preferExtensionRenderer
                        ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
                        : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
                        : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
        return new DefaultRenderersFactory(/* context= */ this)
                .setExtensionRendererMode(extensionRendererMode);
    }

    public DownloadManager getDownloadManager() {
        initDownloadManager();
        return downloadManager;
    }

    public DownloadTracker getDownloadTracker() {
        initDownloadManager();
        return downloadTracker;
    }

    protected synchronized Cache getDownloadCache() {
        if (downloadCache == null) {
            File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
            downloadCache =
                    new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
        }
        return downloadCache;
    }

    private synchronized void initDownloadManager() {
        if (downloadManager == null) {
            DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
            upgradeActionFile(
                    DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
            upgradeActionFile(
                    DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
            DownloaderConstructorHelper downloaderConstructorHelper =
                    new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
            downloadManager =
                    new DownloadManager(
                            this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
            downloadTracker =
                    new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
        }
    }

    private void upgradeActionFile(
            String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
        try {
            ActionFileUpgradeUtil.upgradeAndDelete(
                    new File(getDownloadDirectory(), fileName),
                    /* downloadIdProvider= */ null,
                    downloadIndex,
                    /* deleteOnFailure= */ true,
                    addNewDownloadsAsCompleted);
        } catch (IOException e) {
            Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
        }
    }

    private DatabaseProvider getDatabaseProvider() {
        if (databaseProvider == null) {
            databaseProvider = new ExoDatabaseProvider(this);
        }
        return databaseProvider;
    }

    private File getDownloadDirectory() {
        if (downloadDirectory == null) {
            downloadDirectory = getExternalFilesDir(null);
            if (downloadDirectory == null) {
                downloadDirectory = getFilesDir();
            }
        }
        return downloadDirectory;
    }

    protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
            DataSource.Factory upstreamFactory, Cache cache) {
        return new CacheDataSourceFactory(
                cache,
                upstreamFactory,
                new FileDataSourceFactory(),
                /* cacheWriteDataSinkFactory= */ null,
                CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
                /* eventListener= */ null);
    }
}

好了,ExoPlayer基础的播放功能就这么多。这里注意:PlayerView里面包含了封装好的PlayerControlView,我们可以自己自定义PlayerView。

ExoPlayer的高级应用实践

基础部分功能讲解的差不多了,我们再来拓展下ExoPlayer的功能。

首先我们要了解MediaSource的不同类的功能:
Google ExoPlayer播放器框架详解及应用实践_第11张图片
官方文档写的很详细了。对应的MediaSource的适用场景,其中ProgressiveMediaSource适用于常规的媒体文件播放,例如MP4封装格式。

除了以上几个大类外,ExoPlayer还提供了功能性的MediaSource封装类:ConcatenatingMediaSource、ClippingMediaSource、LoopingMediaSource、MergingMediaSource等。这几种我们可以进行组合形成不同的复杂的功能。

  • ConcatenatingMediaSource适用于列表顺序播放的MediaSource,我们也可以随时进行动态添加、删除更新这个播放列表,进行无缝播放。

  • ClippingMediaSource用于仅播放视频中指定的部分,例如从5秒到30秒之间的视频。

    MediaSource videoSource =
        new ProgressiveMediaSource.Factory(...).createMediaSource(videoUri);
    // 从5秒播放到30秒
    ClippingMediaSource clippingSource =
        new ClippingMediaSource(
            videoSource,
            /* startPositionUs= */ 5_000_000,
            /* endPositionUs= */ 30_000_000);
    
  • LoopingMediaSource用于循环播放视频,可以设置循环次数。

    MediaSource source =
        new ProgressiveMediaSource.Factory(...).createMediaSource(videoUri);
    //播放视频2次
    LoopingMediaSource loopingSource = new LoopingMediaSource(source, 2);
    
  • MergingMediaSource用于将视频文件和字幕文件合并进行播放。

    // 构建视频MediaSource
    MediaSource videoSource =
        new ProgressiveMediaSource.Factory(...).createMediaSource(videoUri);
    // 构建字幕MediaSource
    Format subtitleFormat = Format.createTextSampleFormat(
        id, // An identifier for the track. May be null.
        MimeTypes.APPLICATION_SUBRIP, // The mime type. Must be set correctly.
        selectionFlags, // Selection flags for the track.
        language); // The subtitle language. May be null.
    MediaSource subtitleSource =
        new SingleSampleMediaSource.Factory(...)
            .createMediaSource(subtitleUri, subtitleFormat, C.TIME_UNSET);
    // 合并视频和字幕进行播放视频
    MergingMediaSource mergedSource =
        new MergingMediaSource(videoSource, subtitleSource);
    

我们可以将这些MediaSource进行组合使用:

MediaSource firstSource =
    new ProgressiveMediaSource.Factory(...).createMediaSource(firstVideoUri);
MediaSource secondSource =
    new ProgressiveMediaSource.Factory(...).createMediaSource(secondVideoUri);
// 播放第一个视频2次.
LoopingMediaSource firstSourceTwice = new LoopingMediaSource(firstSource, 2);
// 播放第一个视频2次,然后播放第二个视频.
ConcatenatingMediaSource concatenatedSource =
    new ConcatenatingMediaSource(firstSourceTwice, secondSource)

想在播放列表里增加一个MediaSource的话:

 MediaSource mediaSource =
      new ProgressiveMediaSource.Factory(...)
          .setTag(mediaId)
          .createMediaSource(uri);
  concatenatedSource.addMediaSource(mediaSource);

播放界面配置
ExoPlayer支持在XML布局里配置一些信息:

<com.google.android.exoplayer2.ui.PlayerView
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:show_buffering="when_playing"
    app:show_shuffle_button="true"/>

如果我们想替换默认的播放控制UI的话,可以自定义一个覆盖默认的即可:

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

custom_controls.xml这个就是自定义的播放器控制UI界面。

播放状态

  • Player.STATE_IDLE:这是初始状态,即播放器停止和播放失败时的状态。
  • Player.STATE_BUFFERING:播放器缓冲中。
  • Player.STATE_READY:播放器可以立即从其当前位置播放。
  • Player.STATE_ENDED:播放器完成了所有媒体的播放。

除了播放状态监听器外,还支持以下监听:

  • addAnalyticsListener:聆听详细事件,这些事件可能对分析和报告目的有用。
  • addVideoListener:收听与视频渲染有关的事件,这些事件可能对调整UI有用(例如,Surface正在渲染视频的长宽比)。
  • addAudioListener:收听与音频有关的事件,例如设置音频会话ID的时间以及更改播放器音量的时间。
  • addTextOutput:收听字幕或字幕提示中的更改。
  • addMetadataOutput:收听定时的元数据事件,例如定时的ID3和EMSG数据。

视频离线缓冲下载功能

ExoPlayer支持视频的离线缓冲下载功能。
Google ExoPlayer播放器框架详解及应用实践_第12张图片
下载部分的使用,大家可以参考官方Demo,里面有下载相关的类和代码。这里就不在重复讲解和说明。

FFMPEG音频解码器扩展:

我们需要把官方的这几个类拷贝到项目中去:https://github.com/google/ExoPlayer/tree/release-v2/extensions/ffmpeg

包名和路径不可以改:
Google ExoPlayer播放器框架详解及应用实践_第13张图片
创建自己的渲染工厂FFMPEGRenderFactory类:

import android.content.Context;
import android.os.Handler;
import android.support.annotation.Nullable;

import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;

import java.util.ArrayList;

public class FFMPEGRenderFactory extends DefaultRenderersFactory {

    public FFMPEGRenderFactory(Context context) {
        super(context);
    }

    @Override
    protected void buildAudioRenderers(Context context, int extensionRendererMode, MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, ArrayList<Renderer> out) {
        super.buildAudioRenderers(context, extensionRendererMode, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, audioProcessors, eventHandler, eventListener, out);
        out.add(new FfmpegAudioRenderer());
    }
}

将RenderersFactory改成我们自定义就可以了:

RenderersFactory renderersFactory = new FFMPEGRenderFactory(this);

还有最重要的一点,我们需要把FFMPEG编译出来的so库放置到项目中才可以:
Google ExoPlayer播放器框架详解及应用实践_第14张图片
大功告成,这样就支持大部分的视频中音频解码了。

TS切片流播放的支持

默认ExoPlayer是不支持TS切片流的解码播放的。

private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
        @C.ContentType int type = Util.inferContentType(uri, overrideExtension);
        switch (type) {
            case C.TYPE_DASH:
                return new DashMediaSource.Factory(dataSourceFactory)
                        .setManifestParser(
                                new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
                        .createMediaSource(uri);
            case C.TYPE_SS:
                return new SsMediaSource.Factory(dataSourceFactory)
                        .setManifestParser(
                                new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
                        .createMediaSource(uri);
            case C.TYPE_HLS:
                return new HlsMediaSource.Factory(dataSourceFactory)
                        .setPlaylistParserFactory(
                                new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
                        .createMediaSource(uri);
            // 这里增加一条即可,支持TS流。注意这里的C.TYPE_TS是我自定义在源码里添加的
            case C.TYPE_TS:
                DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
                defaultExtractorsFactory.setTsExtractorFlags(FLAG_DETECT_ACCESS_UNITS | FLAG_ALLOW_NON_IDR_KEYFRAMES);
                return new ExtractorMediaSource.Factory(dataSourceFactory).setExtractorsFactory(defaultExtractorsFactory).createMediaSource(uri);
            case C.TYPE_OTHER:
                return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
            default: {
                throw new IllegalStateException("Unsupported type: " + type);
            }
        }
    }

如下图:
Google ExoPlayer播放器框架详解及应用实践_第15张图片
其实就是增加个类型值,我们写一个固定的int类型数字也可以,主要就是判断视频源地址后缀。

更多扩展还有很多,这里暂时讲这么多。例如我们可以将ExoPlayer加入片头广告功能,自动无缝连播、Android TV遥控器焦点控制操作等等。

之前自己写好的二次封装的框架Github’地址:https://github.com/jaychou2012/TV_ExoPlayer,支持Android TV播放和手机端播放使用。大家可以进行参考学习。

ExoPlayer总结

通过以上的ExoPlayer的讲解和介绍,相信大家对ExoPlayer有了一个更加详细的了解了。的确,ExoPlayer框架性能优秀、稳定、功能强大、容易扩展并且体积小。大家在了解ExoPlayer后,可以尝试将ExoPlayer进行一些企业级应用开发。

你可能感兴趣的:(Android)