作者:谭东


我们都知道,音视频的播放处理在各个平台都是一个常用的操作和功能,尤其在移动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 {@Overridepublic void onPlayerStateChanged(boolean playWhenReady, int playbackState) {if (playbackState == Player.STATE_ENDED) {//播放完毕}}@Overridepublic 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;@Overrideprotected 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++) {//根据每个播放地址构建对应的MediaSourcemediaSources[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.ExtensionRendererModeint extensionRendererMode = DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON;return new DefaultRenderersFactory(/* context= */ this).setExtensionRendererMode(extensionRendererMode);}@Overridepublic void onResume() {super.onResume();if (Util.SDK_INT <= 23 || player == null) {if (playerView != null) {playerView.onResume();}}}@Overridepublic void onPause() {super.onPause();if (Util.SDK_INT <= 23) {if (playerView != null) {playerView.onPause();}releasePlayer();}}@Overridepublic void onStop() {super.onStop();if (Util.SDK_INT > 23) {if (playerView != null) {playerView.onPause();player.stop();}releasePlayer();}}@Overridepublic 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);}// 根据视频的协议和封装格式类型进行自动的创建对应的MediaSourceprivate 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);}}@Overridepublic void preparePlayback() {}private class PlayerEventListener implements Player.EventListener {@Overridepublic void onPlayerStateChanged(boolean playWhenReady, int playbackState) {if (playbackState == Player.STATE_ENDED) {//播放完毕}}@Overridepublic 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> {@Overridepublic 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;@Overridepublic 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.ExtensionRendererModeint 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.PlayerViewandroid: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);}@Overrideprotected 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进行一些企业级应用开发。

Google ExoPlayer播放器框架详解及应用实践相关推荐

  1. WMV网页播放器代码详解

    WMV网页播放器代码详解 WMV是流媒体,用专门的代码播放,效果会更好一些. 这里只举WMV(MediaPlayer9.0及以后)的网页内嵌播放器代码.(默认0为否,-1或1为是) 程序代码: < ...

  2. ffmpeg播放器实现详解 - 音频同步控制

    ffplay是ffmpeg源码中一个自带的开源播放器实例,同时支持本地视频文件的播放以及在线流媒体播放,功能非常强大. FFplay: FFplay is a very simple and port ...

  3. 1.基于S5PV210的图片解码播放器(详解)

    有道云笔记详细地址: 文档:图片解码播放器小项目(详解).note 链接:http://note.youdao.com/noteshare?id=9f9a43ac5ec6828cf467940dfa1 ...

  4. Boom 3D播放器功能详解

    音乐播放器作为音乐播放的载体,起到将音乐文件转换为声音输出的功能.Boom 3D作为一款3D环绕音效软件,既能充当音乐播放器的功能,也能充当电脑的声音输出设备. 音乐播放器,需要具备直观而简洁的操作界 ...

  5. 【JQ】-jPlayer视频、音乐播放器使用详解!

    下载官网:http://www.jplayer.org/ 当前版本:2.3.0 功能:视频播放(可全屏).音乐播放 全部原教程,说明并不详细,要结合查看其网页源代码来学习:http://www.jpl ...

  6. ffmpeg播放器实现详解 - 音频播放

    1.生产者-消费者线程模型 本文主要讨论posix标准下的生产者-消费者线程模型,posix标准多用于类linux相关环境 POSIX: The Portable Operating System I ...

  7. ExoPlayer播放器剖析(六)ExoPlayer同步机制分析

    关联博客 ExoPlayer播放器剖析(一)进入ExoPlayer的世界 ExoPlayer播放器剖析(二)编写exoplayer的demo ExoPlayer播放器剖析(三)流程分析-从build到 ...

  8. shiro放行_Shiro框架详解 tagline

    部分面试资料链接:https://pan.baidu.com/s/1qDb2YoCopCHoQXH15jiLhA 密码:jsam 想获得全部面试必看资料,关注公众号,大家可以在公众号后台回复" ...

  9. Lesson 8.1Lesson 8.2 决策树的核心思想与建模流程CART分类树的建模流程与sklearn评估器参数详解

    Lesson 8.1 决策树的核心思想与建模流程 从本节课开始,我们将介绍经典机器学习领域中最重要的一类有监督学习算法--树模型(决策树). 可此前的聚类算法类似,树模型也同样不是一个模型,而是一类模 ...

  10. [Cocoa]深入浅出 Cocoa 之 Core Data(1)- 框架详解

    深入浅出 Cocoa 之 Core Data(1)- 框架详解 罗朝辉(http://blog.csdn.net/kesalin) CC 许可,转载请注明出处 Core data 是 Cocoa 中处 ...

最新文章

  1. poj1651(区间dp)
  2. mongodb AND查询遇到多个index时候可能会做交集——和复合索引不同
  3. sublime无法输入中文(转)
  4. Ubuntu18.04.3虚拟机安装步骤图文教程
  5. 揭秘阿里秒级百万TPS平台架构实现
  6. PHP 从结果集中取得一行作为关联数组:
  7. 工作207:修改表头按钮样式
  8. hadoop 常用hdfs命令
  9. LeetCode7——Reverse Integer(将一个整数反转,注意溢出的处理)
  10. Bootstrap 列平移/列偏移
  11. Java实现的一个简单的模板渲染
  12. 送女朋友的java小程序_用C编写一个送给女朋友的情人节小程序 可爱!
  13. 多目标跟踪——MOT数据集的学习笔记
  14. QSerialport多线程方法
  15. 中考英语听说计算机考试满分,2020北京中考英语听说机考题型分值及满分技巧...
  16. 学完python能做什么-学完Python开发可以从事哪些行业?
  17. hihocoder 1866 XOR
  18. 如何预防 CSRF 攻击
  19. php编写程序制作图形验证码,php实现图形验证码详解!
  20. Windows快捷键大全(2020年版)

热门文章

  1. 【控制系统的数学模型——传递函数】
  2. oracle恢复几天前的数据,恢复oracle数据到以前的某个时间点
  3. jmeter接口测试
  4. ijkplayer 自定义解码器
  5. ManualResetEvent类的用法
  6. 伴风网易博客linux,伙伴 - 成凹的日志 - 网易博客
  7. 海德汉heidenhain开放式光栅尺AKLIDA27/28/47/48选型资料
  8. (转)Wap开发FAQ大全
  9. 不用找,你想要的人物Flash动画素材都在这里
  10. 计算机键盘字母记忆,键盘26个字母口诀是怎样的?