开始
简介
首先介绍一下今天的主角,Media3 ExoPlayer 是 Google 提供的一个 Android 媒体播放器组件,支持视频和音频文件的处理。并且提供了对各种媒体格式的支持,包括 MP4、MP3、WebM、M4A、MPEG-TS 和 AAC 等。
比较
在介绍 ExoPlayer 之前,我们先把老朋友 MediaPalyer 请出来对比一下。相对于 ExoPlayer ,开发者更为熟知的怕是 MediaPalyer,MediaPalyer 作为 Google 内置的媒体播放组件,API 简单且容易上手。对于 ExoPlayer 和 MediaPalyer 在项目中如何进行选择,不妨先看看各自的优缺点。
ExoPlayer
ExoPlayer 优点
- 自适应流支持:ExoPlayer 支持动态自适应流技术,如 DASH(Dynamic Adaptive Streaming over HTTP)和 SmoothStreaming,这意味着它能够根据网络状况自动调整视频质量,提供更流畅的观看体验;
- 高兼容性:除了支持常见的视频和音频格式(如 MP4, MP3, WebM, M4A, MPEG-TS, AAC 等),ExoPlayer 还额外支持 DASH 和 HLS(HTTP Live Streaming)等流媒体协议;
- 支持更新:ExoPlayer 可以跟随应用统一升级,避免因系统版本不同而导致的兼容性问题。
ExoPlayer 缺点
- 视频硬解依赖:ExoPlayer 主要依赖硬件解码,可能导致在某些设备上因硬件不支持特定编码格式而无法播放视频;
- 资源消耗和效率:由于数据流的请求和内存缓存在 Java 层实现,可能会受到虚拟机限制,影响内存效率和性能;
- 架构设计复杂:ExoPlayer 的架构相对复杂,不易上手。
MediaPlayer
MediaPlayer 优点
- 系统内置:MediaPlayer 是 Android 系统内置的播放器,无需配置依赖;
- 上手简单:MediaPlayer 的 API 相对简单,容易上手。
MediaPlayer 缺点
- 功能简单:相比于 ExoPlayer,MediaPlayer 支持的格式和协议较少,特别是不支持 DASH 和 SmoothStreaming 等自适应流技术;
- 版本更新依赖系统:MediaPlayer 作为系统组件,其功能和 bug 修复依赖于系统更新,开发者无法独立控制其版本,可能导致在旧系统上遇到已知问题。
所以,综合上述优缺点来讲。播放本地音视频时,推荐使用 MediaPlayer,其他业务场景推荐使用 ExoPlayer。
使用
接下来会用一个简单的 demo 进行示例,教大家如何快速集成和使用 ExoPlayer。
API介绍
在使用之前,先给大家罗列一下 ExoPlayer 常用的 API。
| 方法 | 描述 |
|---|---|
| prepare() | 准备播放器开始播放,是异步方法,播放器在准备完成后会改变其状态 |
| setMediaItem(MediaItem mediaItem)) | 设置播放器的媒体资源,需要穿入一个MediaItem对象,这个对象包含了媒体的URI和其他的元数据 |
| play() | 开始或者恢复播放,如果播放器还没有准备好,调用这个方法会使得播放器在准备完成后自动开始播放 |
| pause() | 暂停播放 |
| isPlaying() | 检查播放器是否正在播放 |
| getCurrentPosition() | 获取播放器当前的播放位置,单位是毫秒 |
| getDuration() | 获取当前媒体的总时长,单位是毫秒 |
| seekTo(long positionMs) | 播放器的播放位置跳转到指定的位置 |
| addListener(Player.Listener listener) | 添加一个监听,可以监听接收播放器的各种事件,例如播放状态的改变,播放错误等 |
| setVideoSurfaceHolder() | 设置播放器的视频输出的SurfaceHolder |
集成 ExoPlayer API
接下来在 build.gradle 中集成依赖库,其中 DASH 播放支持和 UI 组件库按需进行集成即可。
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.media3.ui)
如果尚未启用 Java 8,这里需要开启 Java 8 支持。
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
创建 ExoPlayer 播放器
第一步,创建 ExoPlayer 实例。
val exoPlayer = ExoPlayer.Builder(this).build()
第二步,设置媒体资源,这里使用本地视频资源。
val mediaItem = MediaItem.Builder()
.setUri("/storage/emulated/0/Movies/VID_20240522_064453.mp4")
.build()
exoPlayer.setMediaItem(mediaItem)
第三步,预加载视频资源。
exoPlayer.prepare()
第四步,绑定 SurfaceHolder,这里使用的是 SurfaceView。
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
exoPlayer.setVideoSurfaceHolder(holder)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
exoPlayer.clearVideoSurface()
}
})
第五步,开始播放媒体资源。
exoPlayer.play()
ExoPlayer 播放视频
播放视频的效果如下,视频有点 low ,请大家忽略。
源码剖析
通过上述 demo 让大家对 ExoPlayer 的有个简单和初步的了解,代码还是比较简单。接下来我会对 ExoPlayer 常用方法的源码实现进行分析,从中抽丝剥茧,内容比较多请大家跟着我的思路一起分析下去。
ExoPlayer build()
首先来看 build() 方法,我们通过建造者模式创建出 ExoPlayer,拿到 ExoPlayer 实例进行播放相关处理,下面看一下内部是如何实现的。
androidx.media3.exoplayer.ExoPlayer
ExoPlayer 类是一个接口,定义播放相关的方法。
public interface ExoPlayer extends Player {
...
public ExoPlayer build() {
checkState(!buildCalled);
buildCalled = true;
// 这里通过ExoPlayerImpl来实现
return new ExoPlayerImpl(/* builder= */ this, /* wrappingPlayer= */ null);
}
...
}
androidx.media3.exoplayer.ExoPlayerImpl
ExoPlayerImpl 类是 ExoPlayer 接口的默认实现。它是 ExoPlayer 库的核心类,负责管理和协调各种组件(如渲染器、媒体源、轨道选择器等)以实现媒体播放。
public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) {
...
// 渲染器用来处理媒体数据,包括视频和音频
renderers =
builder
.renderersFactorySupplier
.get()
.createRenderers(...);
...
// ExoPlayerImpl的内部类,封装一部分基础业务
internalPlayer =
new ExoPlayerImplInternal(
renderers,
trackSelector,
emptyTrackSelectorResult,
builder.loadControlSupplier.get(),
bandwidthMeter,
repeatMode,
shuffleModeEnabled,
analyticsCollector,
seekParameters,
builder.livePlaybackSpeedControl,
builder.releaseTimeoutMs,
pauseAtEndOfMediaItems,
applicationLooper,
clock,
playbackInfoUpdateListener,
playerId,
builder.playbackLooper);
...
}
上述代码比较简单,不用做过多阐述。需要关注的是构造方法里的 renderers 和 internalPlayer,后面会详细进行介绍。
ExoPlayer setMediaItem()
setMediaItem() 是用来设置媒体资源,这个方法接收一个 MediaItem 对象作为参数。MediaItem 对象作为媒体数据载体,包含有媒体的 URI、媒体的元数据等。我们在创建 MediaItem 对象时,至少需要提供媒体的 URI。
androidx.media3.common.Player
Player 类定义播放相关功能,包括开启播放、暂停播放、播放到指定位置,这里我们继续看 play() 方法。
void setMediaItem(MediaItem mediaItem)
androidx.media3.exoplayer.ExoPlayer
void setMediaItem(MediaItem mediaItem);
androidx.media3.common.BasePlayer
BasePlayer 是一个抽象类,实现了 Player 接口,我们继续往下看。
@Override
public final void setMediaItem(MediaItem mediaItem) {
// 这里将单条媒体数据放入集合中
setMediaItems(ImmutableList.of(mediaItem));
}
@Override
public final void setMediaItems(List<MediaItem> mediaItems) {
setMediaItems(mediaItems, /* resetPosition= */ true);
}
androidx.media3.exoplayer.ExoPlayerImpl
通过下面方法可知,ExoPlayer 同时支持设置多条媒体数据。
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
// 这里进行线程安全校验
verifyApplicationThread();
setMediaSources(createMediaSources(mediaItems), resetPosition);
}
@Override
public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
verifyApplicationThread();
// 继续调用方法进行传参
setMediaSourcesInternal(
mediaSources,
/* startWindowIndex= */ C.INDEX_UNSET,
/* startPositionMs= */ C.TIME_UNSET,
/* resetToDefaultPosition= */ resetPosition);
}
private void setMediaSourcesInternal(
List<MediaSource> mediaSources,
int startWindowIndex,
long startPositionMs,
boolean resetToDefaultPosition) {
...
// 据给定的时间线和期间位置信息,创建一个新的PlaybackInfo实例
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
playbackInfo,
timeline,
...
// 继续设置播放器的媒体源列表
internalPlayer.setMediaSources(
holders, startWindowIndex, Util.msToUs(startPositionMs), shuffleOrder);
...
}
androidx.media3.exoplayer.ExoPlayerImplInternal
public void setMediaSources(
List<MediaSourceList.MediaSourceHolder> mediaSources,
int windowIndex,
long positionUs,
ShuffleOrder shuffleOrder) {
// 发送Handler消息
handler
.obtainMessage(
MSG_SET_MEDIA_SOURCES,
new MediaSourceListUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs))
.sendToTarget();
}
@Override
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
// 接收Handler消息
case MSG_SET_MEDIA_SOURCES:
setMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj);
break;
...
}
private void setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
if (mediaSourceListUpdateMessage.windowIndex != C.INDEX_UNSET) {
// 刷新播放窗口索引和播放位置
pendingInitialSeekPosition =
new SeekPosition(
new PlaylistTimeline(
mediaSourceListUpdateMessage.mediaSourceHolders,
mediaSourceListUpdateMessage.shuffleOrder),
mediaSourceListUpdateMessage.windowIndex,
mediaSourceListUpdateMessage.positionUs);
}
// 更新播放器的媒体资源列表
Timeline timeline =
mediaSourceList.setMediaSources(
mediaSourceListUpdateMessage.mediaSourceHolders,
mediaSourceListUpdateMessage.shuffleOrder);
// 处理已更新的媒体资源列表
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
ExoPlayer prepare()
prepare() 方法在 ExoPlayer 中的作用是准备播放器开始播放媒体。当设置了媒体资源后,需要调用 prepare() 方法来初始化播放器和媒体数据。这个过程包括加载媒体数据、准备解码器等。
在调用 prepare() 方法后,ExoPlayer 会将其状态从空闲(IDLE)状态转变为缓冲(BUFFERING)状态。在缓冲状态,ExoPlayer 会尝试读取媒体数据并填充缓冲区。当缓冲区数据足够后,ExoPlayer 会将状态转变为准备好(READY)状态,此时可以开始播放媒体。
androidx.media3.common.Player
void prepare();
androidx.media3.exoplayer.ExoPlayerImpl
@Override
public void prepare() {
// 这里进行线程安全校验
verifyApplicationThread();
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
// 当前播放状态不是 Player.STATE_IDLE 就会返回
if (playbackInfo.playbackState != Player.STATE_IDLE) {
return;
}
...
// 这里调用 prepare 方法,其实现类是 ExoPlayerImplInternal
internalPlayer.prepare();
...
}
androidx.media3.exoplayer.ExoPlayerImplInternal
ExoPlayerImplInternal 是 ExoPlayer 的核心控制类,进一步封装和实现 ExoPlayer 的内部行为。主要是协调各个组件的工作,实现流畅的媒体播放。
public void prepare() {
// 发送 Handler 消息 MSG_PREPARE
handler.obtainMessage(MSG_PREPARE).sendToTarget();
}
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_PREPARE:
// 接收 Handler 消息进行处理
prepareInternal();
break;
...
}
private void prepareInternal() {
...
// 当播放准备完成时,这里发送 MSG_DO_SOME_WORK 消息开始进行资源的处理。
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_DO_SOME_WORK:
// 重要方法,后续会讲到
doSomeWork();
break;
...
}
ExoPlayer play()
play() 方法顾名思义是用开启播放或恢复播放,也是本文源码分析的重点,看看源码中是如何实现的。
androidx.media3.common.Player
void play();
androidx.media3.common.BasePlayer
@Override
public final void play() {
// 设置准备状态为true,一切准备就绪
setPlayWhenReady(true);
}
androidx.media3.common.Player
看了一圈,您猜怎么着,又回来了。Player 是一个接口类,我们继续往下看,寻找实现 setPlayWhenReady 方的类。
void setPlayWhenReady(boolean playWhenReady);
androidx.media3.exoplayer.ExoPlayerImpl
@Override
public void setPlayWhenReady(boolean playWhenReady) {
// 这里进行线程安全校验
verifyApplicationThread();
// 根据播放器状态,放弃或请求音频焦点。
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
// 更新状态
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
}
private void updatePlayWhenReady(
boolean playWhenReady,
@AudioFocusManager.PlayerCommand int playerCommand,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
@PlaybackSuppressionReason
int playbackSuppressionReason = computePlaybackSuppressionReason(playWhenReady, playerCommand);
// 当前状态已经是ready,则直接返回
if (playbackInfo.playWhenReady == playWhenReady
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
return;
}
// 更新状态
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
}
private void updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
boolean playWhenReady,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
@PlaybackSuppressionReason int playbackSuppressionReason) {
pendingOperationAcks++;
// 在改变播放状态之前,需要对播放进度进行复制或则估算
PlaybackInfo newPlaybackInfo =
this.playbackInfo.sleepingForOffload
? this.playbackInfo.copyWithEstimatedPosition()
: this.playbackInfo;
// 复制并保存播放信息,其中包含有关播放是否应在准备就绪时继续的新信息。
newPlaybackInfo =
newPlaybackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
// 发送 Handler 消息要准备播放了
internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
playWhenReadyChangeReason,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
androidx.media3.exoplayer.ExoPlayerImplInternal
ExoPlayerImplInternal 类是 ExoPlayerImpl 的内部实现类,进一步封装和处理播放器的内部行为和状态管理。
public void setPlayWhenReady(
boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {
// 发送Handler消息
handler
.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason)
.sendToTarget();
}
@Override
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
...
// 接收和处理准备播放的 Handler 消息
case MSG_SET_PLAY_WHEN_READY:
setPlayWhenReadyInternal(
/* playWhenReady= */ msg.arg1 != 0,
/* playbackSuppressionReason= */ msg.arg2,
/* operationAck= */ true,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
break;
...
}
private void setPlayWhenReadyInternal(...)
throws ExoPlaybackException {
...
if (!shouldPlayWhenReady()) {
// 未准备好播放,停止解码并更新当前播放进度
stopRenderers();
updatePlaybackPositions();
} else {
// 对当前播放状态进行校验
if (playbackInfo.playbackState == Player.STATE_READY) {
// 更新 isRebuffering 值,该值是用来判断当前媒体播放是否正在重新缓冲中
updateRebufferingState(
/* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ false);
// 开始播放计时
mediaClock.start();
// 启动所有媒体渲染器
startRenderers();
// 发送执行渲染的 Handler 消息,最终调用 doSomeWork() 方法
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
}
private void startRenderers() throws ExoPlaybackException {
...
for (int i = 0; i < renderers.length; i++) {
if (trackSelectorResult.isRendererEnabled(i) && renderers[i].getState() == STATE_ENABLED) {
// 启动媒体渲染器
renderers[i].start();
}
}
}
接下来到本文的重头戏了,千呼万唤始出来!
private void doSomeWork() throws ExoPlaybackException, IOException {
...
// 当前播放器状态已经完成准备
if (playingPeriodHolder.prepared) {
...
for (int i = 0; i < renderers.length; i++) {
// 开始遍历渲染器集合
Renderer renderer = renderers[i];
if (!isRendererEnabled(renderer)) {
continue;
}
// 开启渲染
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
...
}
} else {
playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
}
...
}
androidx.media3.exoplayer.Renderer
Renderer 类是一个接口,定义了媒体渲染的功能方法作和生命周期。
public interface Renderer extends PlayerMessage.Target {
/**
* 逐步进行渲染
* @param positionUs 当前媒体时间(以微秒为单位),在渲染循环的当前迭代开始时测量。
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} 以微秒为单位,
* 在渲染循环当前迭代开始时测量。
*/
void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException;
}
androidx.media3.exoplayer.mediacodec.MediaCodecRenderer
MediaCodecRenderer 是一个抽象类,使用 MediaCodec 来解码样本以进行渲染。这个类主要负责管理和协调媒体编解码器的生命周期,包括编解码器的初始化、配置、输入/输出缓冲区的处理、错误处理等。如果想要了解 MediaCodeC ,可以参考这篇文章。
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
...
try {
...
// 尝试进行进行初始化或者采用绕过解码模式
maybeInitCodecOrBypass();
if (bypassEnabled) {
// 采用绕过解码模式
TraceUtil.beginSection("bypassRender");
while (bypassRender(positionUs, elapsedRealtimeUs)) {}
TraceUtil.endSection();
} else if (codec != null) {
long renderStartTimeMs = getClock().elapsedRealtime();
TraceUtil.beginSection("drainAndFeed");
// 不断从解码器中取出数据,并进行处理
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
&& shouldContinueRendering(renderStartTimeMs)) {}
// 将源数据交由解码器进行解码
while (feedInputBuffer()
&& shouldContinueRendering(renderStartTimeMs)) {}
TraceUtil.endSection();
} else {
...
}
...
} catch (IllegalStateException e) {
...
}
}
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
// 通过MediaCodecAdapter间接使用MediaCodeC
MediaCodecAdapter codec = checkNotNull(this.codec);
if (!hasOutputBuffer()) {
int outputIndex;
if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
try {
// 获取下一个可用的输出缓冲区索引
outputIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
} catch (IllegalStateException e) {
processEndOfStream();
if (outputStreamEnded) {
// 发生异常时,及时释放MediaCodeC
releaseCodec();
}
return false;
}
} else {
outputIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
}
...
return false;
}
private boolean feedInputBuffer() throws ExoPlaybackException {
...
MediaCodecAdapter codec = checkNotNull(this.codec);
if (inputIndex < 0) {
// 获取下一个可用的输入缓冲区索引
inputIndex = codec.dequeueInputBufferIndex();
// 没有可用的输入缓冲区
if (inputIndex < 0) {
return false;
}
// 获取缓冲区Buffer数据
buffer.data = codec.getInputBuffer(inputIndex);
buffer.clear();
}
...
@SampleStream.ReadDataResult int result;
try {
// 从当前buffer读取数据
result = readSource(formatHolder, buffer, /* readFlags= */ 0);
} catch (InsufficientCapacityException e) {
...
}
...
// 重置buffer状态
resetInputBuffer();
...
return true;
}
总结
源码已经分析完了,相对来说还是比较枯燥和乏味的。不妨先看看 ExoPlayer 类图,通过类图熟悉类之间的关系,加深我们对源码的理解。
通过对 ExoPlayer 源码的分析,我们知道音视频的编解码是通过 MediaCodeC 来完成的。MediaCodec 是 Android 底层多媒体处理和编解码提供的一个 API,用于访问底层的多媒体硬件和软件编解码器,可以用于对音频和视频数据进行编码和解码。所以,了解和熟悉 MediaCodeC 对于音视频播放很重要。
最后,还是想说源码虽好,请勿贪杯。掌握源码其架构设计思想,才是看源码的初衷。由于内容比较多,有些地方或许存在纰漏,望大家不吝赐教!