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:內容版權保護)

默認是不支持TS流播放的,不過我們可以進行擴展進行支持。

SmoothStreaming:

(Containers:容器格式;Closed captions/subtitles:字幕格式;Content protection:內容版權保護)
在這裏插入圖片描述
HLS:

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

(Containers:容器格式;Closed captions/subtitles:字幕格式;Metadata:元數據;Content protection:內容版權保護)
在這裏插入圖片描述
Progressive container formats:

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

(Containers:容器格式)
在這裏插入圖片描述
除了以上這些以外,Google ExoPlayer也內置支持FFMEPG的擴展,FFmpeg的擴展庫支持解碼各種不同的音頻視頻格式。我們可以通過將命令行參數傳遞給FFmpeg的configure來選擇要支持的解碼器:
在這裏插入圖片描述
ExoPlayer也支持獨立字幕格式,如下:

最後我們看下ExoPlayer庫各個功能所支持的最低Android設備版本:
在這裏插入圖片描述
基本上Android 4.1以後的系統版本的主要功能都支持。

ExoPlayer的簡單使用

通過以上簡單的對ExoPlayer的介紹,相信大家對ExoPlayer框架有了一個大致的瞭解了。官方Github開源地址:https://github.com/google/ExoPlayer

建議大家可以將官方的源碼下載下來,裏面也包含了一個官方例子,大家可以進行相應的源碼分析和學習。

官方Demo運行圖:
在這裏插入圖片描述
Demo包含了ExoPlayer的一些基本用法,如播放、暫停、快進、列表播放切換等。

官方的ExoPlayer源碼主要包含以下幾個部分:
在這裏插入圖片描述
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);

播放效果如下圖:
在這裏插入圖片描述
怎麼樣是不是很簡單?

如果想監聽播放相關的:

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的不同類的功能:
在這裏插入圖片描述
官方文檔寫的很詳細了。對應的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支持視頻的離線緩衝下載功能。
在這裏插入圖片描述
下載部分的使用,大家可以參考官方Demo,裏面有下載相關的類和代碼。這裏就不在重複講解和說明。

FFMPEG音頻解碼器擴展:

我們需要把官方的這幾個類拷貝到項目中去:https://github.com/google/ExoPlayer/tree/release-v2/extensions/ffmpeg

包名和路徑不可以改:
在這裏插入圖片描述
創建自己的渲染工廠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庫放置到項目中才可以:
在這裏插入圖片描述
大功告成,這樣就支持大部分的視頻中音頻解碼了。

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);
            }
        }
    }

如下圖:
在這裏插入圖片描述
其實就是增加個類型值,我們寫一個固定的int類型數字也可以,主要就是判斷視頻源地址後綴。

更多擴展還有很多,這裏暫時講這麼多。例如我們可以將ExoPlayer加入片頭廣告功能,自動無縫連播、Android TV遙控器焦點控制操作等等。

之前自己寫好的二次封裝的框架Github’地址:https://github.com/jaychou2012/TV_ExoPlayer,支持Android TV播放和手機端播放使用。大家可以進行參考學習。

ExoPlayer總結

通過以上的ExoPlayer的講解和介紹,相信大家對ExoPlayer有了一個更加詳細的瞭解了。的確,ExoPlayer框架性能優秀、穩定、功能強大、容易擴展並且體積小。大家在瞭解ExoPlayer後,可以嘗試將ExoPlayer進行一些企業級應用開發。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章