ExoPlayer 동작 분석 (prepare)
Simple example code
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
inner class PlayerStateListener : Player.EventListener {
override fun onPlaybackStateChanged(state: Int) {
when (state) {
ExoPlayer.STATE_IDLE -> Log.d(TAG, "STATE_IDLE")
ExoPlayer.STATE_BUFFERING -> Log.d(TAG, "STATE_BUFFERING")
ExoPlayer.STATE_READY -> Log.d(TAG, "STATE_READY")
ExoPlayer.STATE_ENDED -> Log.d(TAG, "STATE_ENDED")
}
}
}
private lateinit var playerView: PlayerView
private var player: ExoPlayer? = null
private val listener = PlayerStateListener()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
playerView = findViewById(R.id.playerView)
}
fun onClickPlay(view: View) {
if (player == null) {
player = SimpleExoPlayer.Builder(this).build()
playerView.player = player
}
player?.apply {
if (isPlaying) {
stop()
}
setMediaItem(MediaItem.fromUri("asset:///video/20211028_134942113.mp4"))
//addMediaItem(MediaItem.fromUri("asset:///video/20211028_134942113.mp4"))
addListener(listener)
prepare()
play()
seekTo(2000L)
}
}
fun onClickRelease(view: View) {
player?.apply {
stop()
release()
removeListener(listener)
}
player = null
}
기본 player 생성 (별도 설정 없이) 후, MediaItem을 지정한다.
이후 prepare -> play를 수행한다.
멈춰야할 경우 stop -> release를 수행한다.
1. Source 생성
1) Media source의 종류
media source types
DashMediaSource
HlsMediaSource
RtspMediaSource
SsMediaSource
ProgressiveMediaSource
FakeMediaSource
AdsMediaSource
SilenceMediaSource
MergingMediaSource
SingleSampleMediaSource
MaskingMediaSource
ConcatenatingMediaSource
LoopingMediaSource
ClippingMediaSource
2) HLS Media Source 생성
private static HlsMediaSource.Factory createHlsMediaSourceFactory(
String playlistUri, String playlist) {
FakeDataSet fakeDataSet =
new FakeDataSet().setData(playlistUri, Util.getUtf8Bytes(playlist));
return new HlsMediaSource.Factory(
dataType -> new FakeDataSource.Factory().setFakeDataSet(fakeDataSet).createDataSource())
.setElapsedRealTimeOffsetMs(0);
}
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24";
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = MediaItem.fromUri(playlistUri);
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
HlsMediaSource.Factory를 사용해서 HlsMediaSourceFactory를 생성한다.
HlsMediaSource 생성 후 이를 Timeline을 생성하고,
Timeline으로 Winodw를 생성한다.
이렇게 생성한 Media Source를 Player에 set 한다.
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.setMediaSource(mediaSource);
player.prepare();
player.play();
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
videoProcessingGLSurfaceView.setVideoComponent(
Assertions.checkNotNull(player.getVideoComponent()));
HlsMediaSourceFactory는 DefaultDrmSessionManager, DefaultHlsPlaylist를 사용한다.
public static final class Factory implements MediaSourceFactory {
public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
this.hlsDataSourceFactory = checkNotNull(hlsDataSourceFactory);
drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
playlistParserFactory = new DefaultHlsPlaylistParserFactory();
playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;
extractorFactory = HlsExtractorFactory.DEFAULT;
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
compositeSequenceableLoaderFactory =
new DefaultCompositeSequenceableLoaderFactory();
metadataType = METADATA_TYPE_ID3;
streamKeys = Collections.emptyList();
elapsedRealTimeOffsetMs = C.TIME_UNSET;
}
public MediaSource createMediaSource(MediaItem mediaItem) {
@C.ContentType
int type =
Util.inferContentTypeForUriAndMimeType(
mediaItem.playbackProperties.uri, mediaItem.playbackProperties.mimeType);
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type);
설정된 live min/max speed, min/max offset으로 media source를 다시 build
MediaSource mediaSource = mediaSourceFactory.createMediaSource(mediaItem);
subtitle이 존재하면, subtitle media source를 만든 후,
두 media source를 merge
return maybeWrapWithAdsMediaSource(mediaItem, maybeClipMediaSource(mediaItem, mediaSource));
3) DASH or PD Media Source 생성
@C.ContentType int type =
Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
mediaSource = new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
}
2. prepare
SimpleExoPlayer.prepare
public void prepare() {
verifyApplicationThread();
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
updatePlayWhenReady( player.setPlayWhenReady(playWhenReady...
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
player.prepare();
}
ExoPlayerImplInternal.prepare
private void prepareInternal() {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
resetInternal(
/* resetRenderers= */ false,
/* resetPosition= */ false,
/* releaseMediaSourceList= */ false,
/* resetError= */ true);
loadControl.onPrepared();
setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
mediaSourceList.prepare(bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
prepare를 수행하면 (예를들어 HLS playback인 경우) 아래와 같이 source들을 생성한다.
이후 doSomeWork를 주기적으로 수행하면서 renderer가 render를 수행하게 된다.
render가 media source가 생성한 ES stream을 소비하는 역할을 수행한다.
여기서 소비란 decode 및 출력 rendering을 의미한다.
private void doSomeWork() throws ExoPlaybackException, IOException {
updatePeriods();
updatePlaybackPositions();
if (playingPeriodHolder.prepared) {
playingPeriodHolder.mediaPeriod.discardBuffer(
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
재생의 finish, buffering, ready 등의 처리 수행
doSomeWork의 동작설명
n updatePeriods n updatePlaybackPositions n if playingPeriodHolder.prepared u 여러 renderer들을 render(rendererPositionUs, …) n rendering 종료 중 && 마지막 period 에서 u setPlayWhenReadyInternal n rendering 종료 중 && period holder가 종료 u stopRenderers n buffering 상태 u setState(Player.STATE_READY); u ready 시 재생해야 한다면, u startRenderers(); n ready 상태 u setState(Player.STATE_BUFFERING); u stopRenderers(); n buffering 이고 READY 상태 u maybeScheduleWakeup n else 아직 종료 아님 u scheduleNextWork |
3. MediaCodecRenderer 동작
renderer의 render는
1) drainOutputBuffer를 호출하여 decode 된 buffer가 있다면 이를 sink로 출력한다.
2) feedInputBuffer를 수행하여 ES stream을 읽어온다.
읽은 ES stream은 MediaCodec에 전달하여 decode를 수행한다. (queueInputBuffer)
MediaCodec은 ACodec이 관리하는 IOMX component에 decode 수행을 요청한다.
(fillThisBuffer, emptyThisBuffer와 같은 OMX 표준 API를 사용한다)
code flow는 다음과 같다.
ExoPlayerImplInternal.doSomeWork
renderer.render(
MediaCodecRenderer.render
drainOutputBuffer
feedInputBuffer
MediaCodecRenderer.feedInputBuffer
inputIndex = codec.dequeueInputBufferIndex();
buffer.data = codec.getInputBuffer(inputIndex);
readSource(formatHolder, buffer, ...)
BaseRenderer.readSource
stream.readData(formatHolder, buffer, ...)
if buffer.isEncrypted()
buffer.cryptoInfo.increaseClearDataFirstSubSampleBy(...)
if hasSupplementalData
handleInputBufferSupplementalData(buffer);
onQueueInputBuffer(buffer);
= MediaCodecAudioRenderer.onQueueInputBuffer
currentPositionUs = buffer.timeUs;
= MediaCodecVideoRenderer.onQueueInputBuffer
onProcessedTunneledBuffer(buffer.timeUs);
if (bufferEncrypted)
codec.queueSecureInputBuffer(inputIdx, 0, buffer.cryptoInfo, ...)
else
codec.queueInputBuffer(inputIdx, 0, buffer.data.limt(), ..
MediaCodecRenderer.drainOutputBuffer
outputIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
if eos
processEndOfStream();
outputBuffer = codec.getOutputBuffer(outputIndex);
processOutputBuffer(..., outputBuffer, ....)
if (processedOutputBuffer)
onProcessedOutputBuffer(...)
resetOutputBuffer()
if eos
processEndOfStream()
MediaCodecVideoRenderer
processOutputBuffer
renderOutputBuffer
renderOutputBufferV21
MediaCodecAudioRenderer
processOutputBuffer
AudioSink.handleBuffer -> DefaultAuidoSink.handleBuffer -> AudioTrack.write
댓글