diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index 98af2c5b..77a5a9f8 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -12,8 +12,8 @@ android { dependencies { provided 'com.facebook.react:react-native:+' - compile 'com.google.android.exoplayer:exoplayer:r2.1.1' - compile('com.google.android.exoplayer:extension-okhttp:r2.1.1') { + compile 'com.google.android.exoplayer:exoplayer:r2.2.0' + compile('com.google.android.exoplayer:extension-okhttp:r2.2.0') { exclude group: 'com.squareup.okhttp3', module: 'okhttp' } compile 'com.squareup.okhttp3:okhttp:3.4.2' diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index babe0f7f..96a02cba 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -51,14 +52,12 @@ import java.net.CookieManager; import java.net.CookiePolicy; @SuppressLint("ViewConstructor") - class ReactExoplayerView extends FrameLayout implements LifecycleEventListener, ExoPlayer.EventListener, BecomingNoisyListener, AudioManager.OnAudioFocusChangeListener, - MetadataRenderer.Output - { + MetadataRenderer.Output { private static final String TAG = "ReactExoplayerView"; @@ -74,7 +73,6 @@ class ReactExoplayerView extends FrameLayout implements private final VideoEventEmitter eventEmitter; private Handler mainHandler; - private Timeline.Window window; private ExoPlayerView exoPlayerView; private DataSource.Factory mediaDataSourceFactory; @@ -82,13 +80,11 @@ class ReactExoplayerView extends FrameLayout implements private MappingTrackSelector trackSelector; private boolean playerNeedsSource; - private boolean shouldRestorePosition; - private int playerWindow; - private long playerPosition; + private int resumeWindow; + private long resumePosition; private boolean loadVideoStarted; private boolean isPaused = true; private boolean isBuffering; - private boolean isTimelineStatic; // Props from React private Uri srcUri; @@ -129,6 +125,8 @@ class ReactExoplayerView extends FrameLayout implements audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); + + initializePlayer(); } @@ -139,9 +137,9 @@ class ReactExoplayerView extends FrameLayout implements } private void createViews() { + clearResumePosition(); mediaDataSourceFactory = buildDataSourceFactory(true); mainHandler = new Handler(); - window = new Timeline.Window(); if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); } @@ -199,13 +197,6 @@ class ReactExoplayerView extends FrameLayout implements player.addListener(this); player.setMetadataOutput(this); exoPlayerView.setPlayer(player); - if (isTimelineStatic) { - if (playerPosition == C.TIME_UNSET) { - player.seekToDefaultPosition(playerWindow); - } else { - player.seekTo(playerWindow, playerPosition); - } - } audioBecomingNoisyReceiver.setListener(this); setPlayWhenReady(!isPaused); playerNeedsSource = true; @@ -213,7 +204,11 @@ class ReactExoplayerView extends FrameLayout implements if (playerNeedsSource && srcUri != null) { MediaSource mediaSource = buildMediaSource(srcUri, extension); mediaSource = repeat ? new LoopingMediaSource(mediaSource) : mediaSource; - player.prepare(mediaSource, !shouldRestorePosition, true); + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, false); playerNeedsSource = false; eventEmitter.loadStart(); @@ -245,13 +240,7 @@ class ReactExoplayerView extends FrameLayout implements private void releasePlayer() { if (player != null) { isPaused = player.getPlayWhenReady(); - shouldRestorePosition = false; - playerWindow = player.getCurrentWindowIndex(); - playerPosition = C.TIME_UNSET; - Timeline timeline = player.getCurrentTimeline(); - if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) { - playerPosition = player.getCurrentPosition(); - } + updateResumePosition(); player.release(); player.setMetadataOutput(null); player = null; @@ -331,6 +320,17 @@ class ReactExoplayerView extends FrameLayout implements audioManager.abandonAudioFocus(this); } + private void updateResumePosition() { + resumeWindow = player.getCurrentWindowIndex(); + resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition()) + : C.TIME_UNSET; + } + + private void clearResumePosition() { + resumeWindow = C.INDEX_UNSET; + resumePosition = C.TIME_UNSET; + } + /** * Returns a new DataSource factory. * @@ -442,13 +442,17 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onPositionDiscontinuity() { - // Do nothing. + if (playerNeedsSource) { + // This will only occur if the user has performed a seek whilst in the error state. Update the + // resume position so that if the user then retries, playback will resume from the position to + // which they seeked. + updateResumePosition(); + } } @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - isTimelineStatic = !timeline.isEmpty() - && !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic; + // Do nothing. } @Override @@ -485,6 +489,26 @@ class ReactExoplayerView extends FrameLayout implements eventEmitter.error(errorString, e); } playerNeedsSource = true; + if (isBehindLiveWindow(e)) { + clearResumePosition(); + initializePlayer(); + } else { + updateResumePosition(); + } + } + + private static boolean isBehindLiveWindow(ExoPlaybackException e) { + if (e.type != ExoPlaybackException.TYPE_SOURCE) { + return false; + } + Throwable cause = e.getSourceException(); + while (cause != null) { + if (cause instanceof BehindLiveWindowException) { + return true; + } + cause = cause.getCause(); + } + return false; } @Override diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index 029660ea..6fe6ab61 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -9,14 +9,11 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.events.RCTEventEmitter; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.TxxxFrame; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.HashMap; -import java.util.Map; class VideoEventEmitter { @@ -191,8 +188,8 @@ class VideoEventEmitter { String value = ""; - if (frame instanceof TxxxFrame) { - TxxxFrame txxxFrame = (TxxxFrame) frame; + if (frame instanceof TextInformationFrame) { + TextInformationFrame txxxFrame = (TextInformationFrame) frame; value = txxxFrame.value; }