系列文章目录
ExoPlayer架构详解与源码分析(1)——前言
文章目录
前言
如果让你去开发一款播放器,第一步当然想到的就是设计。使用面向对象的思路,去确定ExoPlayer应该具有哪些功能,对外暴露哪些操作,需要解决哪些问题。将这些功能进一步抽象,最终就产生了本文要说的Player接口,Player接口位于整个播放器的最顶层,相当于描绘了整个播放器的蓝图。
设计播放器
Player接口除了定义了一些用于播放的高阶函数(如play、pause、seek、获取某些状态等)。还对对播放器设计了以下特性:
-
所有方法(除非有特殊说明)必须在应用线程调用,大部分是主线程,同样回调必须注册在同一线程里。
-
所提供的方法可能有是否可用的控制,播放器会提供一个可用方法集,用户只能调用里面的可用方法。
private void setPlaybackSpeed(float speed) { if (player == null || !player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) { return; } player.setPlaybackParameters(player.getPlaybackParameters().withSpeed(speed)); }
-
用户通过注册Listener 来监听播放状态变化。
@Override public void addListener(Listener listener) { // 这里的方法调用前没有像其他方法一样校验是否在主线程,这个方法可以在任何线程调用,因为添加的listener最终都会被转发到主线程执行 listeners.add(checkNotNull(listener)); } //ExoPlayerImpl初始化时,会将主线程的Looper传入,用于转发Listener转发到主线程 listeners = new ListenerSet<>( applicationLooper,//主线程Looper clock, (listener, flags) -> listener.onEvents(this.wrappingPlayer, new Events(flags)));
-
必须在方法调用后立即更新播放状态或者信息,即使实际发生变化的代码执行在后台线程甚至是其他设备上。这样是为了方便调用方法,无需考虑异步处理。
@Override public void prepare() { verifyApplicationThread();//判断主线程调用 ... PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackError(null);//创建副本,防止多线程问题 playbackInfo =//设置状态为STATE_ENDED 或者STATE_BUFFERING playbackInfo.copyWithPlaybackState( playbackInfo.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING); pendingOperationAcks++; internalPlayer.prepare(); updatePlaybackInfo( playbackInfo, /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, /* ignored */ C.TIME_UNSET, /* ignored */ C.INDEX_UNSET, /* repeatCurrentMediaItem= */ false); }
-
可以操作播放列表,如设置、获取、添加、删除、移动或者替换播放列表。播放器还可以设置重复模式和随机播放。
-
可以获取轨道及正在播放的轨道,还可以选择设置轨道。
-
可以获取当前播放内容的元数据。
-
可以获取当前媒体中的广告信息,如当前播放的是否为广告。
-
可以支持不同的视频渲染输出,如SurfaceView、TextureView。
-
可以倍数播放,音频参数调节,音量调节。
//DefaultAudioSink @RequiresApi(23) private void setAudioTrackPlaybackParametersV23() { ... audioTrack.setPlaybackParams(playbackParams); ... } //StandaloneMediaClock @Override public long getPositionUs() { long positionUs = baseUs; if (started) { long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs; if (playbackParameters.speed == 1f) { positionUs += Util.msToUs(elapsedSinceBaseMs); } else { positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); } } return positionUs; }
-
可以获取播放设备的信息,即使是远程设备,这样可以设置这些设备的音量。
确定播放需要维护的状态和信息
根据上面的设计需要,播放器主要需要维护以下的状态和信息:
-
播放列表信息
- 媒体信息封装在MediaItem类里,可以设置定义播放器需要播放的内容。
- 当前的播放列表可以通过getCurrentTimeline获取,Timeline是ExoPlayer中一个重要的概念,抽象了播放时序,使其适应各种类型的媒体播放,后续系列文章会讲到。
- 当播放列表为空的时候,播放器状态只能是STATE_IDLE或者STATE_ENDED
-
播放状态
- STATE_IDLE: 初始状态,播放器停止时的状态,以及播放失败时的状态。在这种状态下,播放器只能拥有有限的资源。必须调用prepare 方法才能脱离此状态。
- STATE_BUFFERING: 播放器无法立即从当前位置开始播放。出现这种情况主要是因为需要加载更多数据。
- STATE_READY: 播放器可以立即从当前位置开始播放。
- STATE_ENDED: 播放器以及播放完成所有内容,或者没有内容需要播放。
-
播放/暂停,播放限制和正在播放状态
- playWhenReady: 一个boolen值,用于标记用户是否要开始播放,可以通过调用play 或者 pause方法设置。
- playback suppression: 用于标记播放被限制(即使playWhenReady=true)的原因。
- isPlaying: 一个boolen值 ,用于标记播放器是否正在播放(播放进度在前进且播放的数据正在读取)。 这个值为true的条件是上面的 播放状态=STATE_READY 且 playWhenReady =true 且 播放没有被限制,可以看到这个状态是由上面三个状态计算而来的。
@Override public final boolean isPlaying() { return getPlaybackState() == Player.STATE_READY && getPlayWhenReady() && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE; }
-
播放位置进度
- media item index: 播放列表的索引。
- ad insertion: 插入的广告是否在播放,当前正在播放的广告组索引,以及当前广告在组中的索引,这里可以看到广告是分组播放的,一组可以包含一个或者多个广告。
- current position: 当前播放的进度。 如果没有播放插入的广告这个进度等于content position,content position比current position多了一个广告的时长。
- 需要注意的是Play不提供播放进度状态的回调的,需要以适当的频率去查询播放进度。
总结
本篇介绍相关的设计特性已经播放器维护的状态,分别进行一个简单的介绍,因为在后面的文章这些概念都会涉及到,到时候会详细解读。