diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..860d1e17d02 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +.fabric/ @unacademy/sre \ No newline at end of file diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c698c2ee707..7b7b842dfd4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,67 @@ # Release notes +### dev-v2 (not yet released) + +* Core library: + * Fix bug where streams with highly uneven durations may get stuck in a + buffering state + ([#7943](https://github.com/google/ExoPlayer/issues/7943)). + * Verify correct thread usage in `SimpleExoPlayer` by default. Opt-out is + still possible until the next major release using + `setThrowsWhenUsingWrongThread(false)` + ([#4463](https://github.com/google/ExoPlayer/issues/4463)). + * Add a getter and callback for static metadata to the player + ([#7266](https://github.com/google/ExoPlayer/issues/7266)). + * Time out on release to prevent ANRs if the underlying platform call + is stuck ([#4352](https://github.com/google/ExoPlayer/issues/4352)). + * Time out when detaching a surface to prevent ANRs if the underlying + platform call is stuck + ([#5887](https://github.com/google/ExoPlayer/issues/5887)). +* Track selection: + * Add option to specify multiple preferred audio or text languages. +* Data sources: + * Add support for `android.resource` URI scheme in `RawResourceDataSource` + ([#7866](https://github.com/google/ExoPlayer/issues/7866)). +* Text: + * Add support for `\h` SSA/ASS style override code (non-breaking space). + * Fix WebVTT subtitles in MP4 containers in DASH streams + ([#7985](https://github.com/google/ExoPlayer/issues/7985)). + * Fix NPE in `TextRenderer` when playing content with a single subtitle + buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). +* UI: + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). + * Add the option to sort tracks by `Format` in `TrackSelectionView` and + `TrackSelectionDialogBuilder` + ([#7709](https://github.com/google/ExoPlayer/issues/7709)). +* Audio: + * Retry playback after some types of `AudioTrack` error. + * Fix the default audio sink position not advancing correctly when using + `AudioTrack`-based speed adjustment + ([#7982](https://github.com/google/ExoPlayer/issues/7982)). +* Extractors: + * Add support for .mp2 boxes in the `AtomParsers` + ([#7967](https://github.com/google/ExoPlayer/issues/7967)). + * Use TLEN ID3 tag to compute the duration in Mp3Extractor + ([#7949](https://github.com/google/ExoPlayer/issues/7949)). + * Fix regression for Ogg files with packets that span multiple pages + ([#7992](https://github.com/google/ExoPlayer/issues/7992)). + * Add TS extractor parameter to configure the number of bytes in which + to search for a timestamp to determine the duration and to seek. + ([#7988](https://github.com/google/ExoPlayer/issues/7988)). + * Ignore negative payload size in PES packets + ([#8005](https://github.com/google/ExoPlayer/issues/8005)). +* IMA extension: + * Fix position reporting after fetch errors + ([#7956](https://github.com/google/ExoPlayer/issues/7956)). + * Allow apps to specify a `VideoAdPlayerCallback` + ([#7944](https://github.com/google/ExoPlayer/issues/7944)). + * Accept ad tags via the `AdsMediaSource` constructor and deprecate + passing them via the `ImaAdsLoader` constructor/builders. Passing the + ad tag via media item playback properties continues to be supported. + This is in preparation for supporting ads in playlists + ([#3750](https://github.com/google/ExoPlayer/issues/3750)). + ### 2.12.0 (2020-09-11) ### To learn more about what's new in 2.12, read the corresponding diff --git a/core_settings.gradle b/core_settings.gradle index b5082433712..bb953ac484e 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -31,22 +31,22 @@ include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' include modulePrefix + 'testdata' -include modulePrefix + 'extension-av1' -include modulePrefix + 'extension-ffmpeg' -include modulePrefix + 'extension-flac' -include modulePrefix + 'extension-gvr' -include modulePrefix + 'extension-ima' -include modulePrefix + 'extension-cast' -include modulePrefix + 'extension-cronet' -include modulePrefix + 'extension-mediasession' -include modulePrefix + 'extension-media2' +// include modulePrefix + 'extension-av1' +// include modulePrefix + 'extension-ffmpeg' +// include modulePrefix + 'extension-flac' +// include modulePrefix + 'extension-gvr' +// include modulePrefix + 'extension-ima' +// include modulePrefix + 'extension-cast' +// include modulePrefix + 'extension-cronet' +// include modulePrefix + 'extension-mediasession' +// include modulePrefix + 'extension-media2' include modulePrefix + 'extension-okhttp' -include modulePrefix + 'extension-opus' -include modulePrefix + 'extension-vp9' -include modulePrefix + 'extension-rtmp' -include modulePrefix + 'extension-leanback' -include modulePrefix + 'extension-jobdispatcher' -include modulePrefix + 'extension-workmanager' +// include modulePrefix + 'extension-opus' +// include modulePrefix + 'extension-vp9' +// include modulePrefix + 'extension-rtmp' +// include modulePrefix + 'extension-leanback' +// include modulePrefix + 'extension-jobdispatcher' +// include modulePrefix + 'extension-workmanager' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common') @@ -58,19 +58,19 @@ project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata') -project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') -project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') -project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') -project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') -project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') -project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') -project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') -project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') -project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2') +// project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') +// project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') +// project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') +// project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') +// project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') +// project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') +// project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') +// project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') +// project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2') project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') -project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') -project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') -project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') -project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') -project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') -project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager') +// project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') +// project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') +// project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') +// project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') +// project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') +// project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager') diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 93fb4b01186..0826740de1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -85,16 +85,17 @@ public final class ExoPlaybackException extends Exception { /** * The operation which produced the timeout error. One of {@link #TIMEOUT_OPERATION_RELEASE}, - * {@link #TIMEOUT_OPERATION_SET_FOREGROUND_MODE} or {@link #TIMEOUT_OPERATION_UNDEFINED}. Note - * that new operations may be added in the future and error handling should handle unknown - * operation values. + * {@link #TIMEOUT_OPERATION_SET_FOREGROUND_MODE}, {@link #TIMEOUT_OPERATION_DETACH_SURFACE} or + * {@link #TIMEOUT_OPERATION_UNDEFINED}. Note that new operations may be added in the future and + * error handling should handle unknown operation values. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ TIMEOUT_OPERATION_UNDEFINED, TIMEOUT_OPERATION_RELEASE, - TIMEOUT_OPERATION_SET_FOREGROUND_MODE + TIMEOUT_OPERATION_SET_FOREGROUND_MODE, + TIMEOUT_OPERATION_DETACH_SURFACE }) public @interface TimeoutOperation {} @@ -104,6 +105,8 @@ public final class ExoPlaybackException extends Exception { public static final int TIMEOUT_OPERATION_RELEASE = 1; /** The error occurred in {@link ExoPlayer#setForegroundMode}. */ public static final int TIMEOUT_OPERATION_SET_FOREGROUND_MODE = 2; + /** The error occurred while detaching a surface from the player. */ + public static final int TIMEOUT_OPERATION_DETACH_SURFACE = 3; /** If {@link #type} is {@link #TYPE_RENDERER}, this is the name of the renderer. */ @Nullable public final String rendererName; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index b5489186bc8..a7db8bf66e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -131,6 +131,12 @@ */ public interface ExoPlayer extends Player { + /** + * The default timeout for calls to {@link #release} and {@link #setForegroundMode}, in + * milliseconds. + */ + long DEFAULT_RELEASE_TIMEOUT_MS = 500; + /** * A builder for {@link ExoPlayer} instances. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b1f57364658..1040669bdbb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -665,18 +665,28 @@ public void setForegroundMode(boolean foregroundMode) { if (this.foregroundMode != foregroundMode) { this.foregroundMode = foregroundMode; if (!internalPlayer.setForegroundMode(foregroundMode)) { - notifyListeners( - listener -> - listener.onPlayerError( - ExoPlaybackException.createForTimeout( - new TimeoutException("Setting foreground mode timed out."), - ExoPlaybackException.TIMEOUT_OPERATION_SET_FOREGROUND_MODE))); + stop( + /* reset= */ false, + ExoPlaybackException.createForTimeout( + new TimeoutException("Setting foreground mode timed out."), + ExoPlaybackException.TIMEOUT_OPERATION_SET_FOREGROUND_MODE)); } } } @Override public void stop(boolean reset) { + stop(reset, /* error= */ null); + } + + /** + * Stops the player. + * + * @param reset Whether the playlist should be cleared and whether the playback position and + * playback error should be reset. + * @param error An optional {@link ExoPlaybackException} to set. + */ + public void stop(boolean reset, @Nullable ExoPlaybackException error) { PlaybackInfo playbackInfo; if (reset) { playbackInfo = @@ -689,6 +699,9 @@ public void stop(boolean reset) { playbackInfo.totalBufferedDurationUs = 0; } playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE); + if (error != null) { + playbackInfo = playbackInfo.copyWithPlaybackError(error); + } pendingOperationAcks++; internalPlayer.stop(); updatePlaybackInfo( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 7e2cb69bc69..6f81a35dd82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -269,6 +269,20 @@ public synchronized boolean isCanceled() { return isCanceled; } + /** + * Marks the message as processed. Should only be called by a {@link Sender} and may be called + * multiple times. + * + * @param isDelivered Whether the message has been delivered to its target. The message is + * considered as being delivered when this method has been called with {@code isDelivered} set + * to true at least once. + */ + public synchronized void markAsProcessed(boolean isDelivered) { + this.isDelivered |= isDelivered; + isProcessed = true; + notifyAll(); + } + /** * Blocks until after the message has been delivered or the player is no longer able to deliver * the message. @@ -292,44 +306,30 @@ public synchronized boolean blockUntilDelivered() throws InterruptedException { return isDelivered; } - /** - * Marks the message as processed. Should only be called by a {@link Sender} and may be called - * multiple times. - * - * @param isDelivered Whether the message has been delivered to its target. The message is - * considered as being delivered when this method has been called with {@code isDelivered} set - * to true at least once. - */ - public synchronized void markAsProcessed(boolean isDelivered) { - this.isDelivered |= isDelivered; - isProcessed = true; - notifyAll(); - } - /** * Blocks until after the message has been delivered or the player is no longer able to deliver - * the message or the specified waiting time elapses. + * the message or the specified timeout elapsed. * *
Note that this method can't be called if the current thread is the same thread used by the * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. * - * @param timeoutMs the maximum time to wait in milliseconds. + * @param timeoutMs The timeout in milliseconds. * @return Whether the message was delivered successfully. * @throws IllegalStateException If this method is called before {@link #send()}. * @throws IllegalStateException If this method is called on the same thread used by the message * handler set with {@link #setHandler(Handler)}. - * @throws TimeoutException If the waiting time elapsed and this message has not been delivered - * and the player is still able to deliver the message. + * @throws TimeoutException If the {@code timeoutMs} elapsed and this message has not been + * delivered and the player is still able to deliver the message. * @throws InterruptedException If the current thread is interrupted while waiting for the message * to be delivered. */ - public synchronized boolean experimentalBlockUntilDelivered(long timeoutMs) + public synchronized boolean blockUntilDelivered(long timeoutMs) throws InterruptedException, TimeoutException { - return experimentalBlockUntilDelivered(timeoutMs, Clock.DEFAULT); + return blockUntilDelivered(timeoutMs, Clock.DEFAULT); } @VisibleForTesting() - /* package */ synchronized boolean experimentalBlockUntilDelivered(long timeoutMs, Clock clock) + /* package */ synchronized boolean blockUntilDelivered(long timeoutMs, Clock clock) throws InterruptedException, TimeoutException { Assertions.checkState(isSent); Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 6652cbb03d0..c641ec4d50e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -67,6 +67,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeoutException; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can @@ -80,6 +81,9 @@ public class SimpleExoPlayer extends BasePlayer Player.MetadataComponent, Player.DeviceComponent { + /** The default timeout for detaching a surface from the player, in milliseconds. */ + public static final long DEFAULT_DETACH_SURFACE_TIMEOUT_MS = 2_000; + /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ @Deprecated public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {} @@ -110,6 +114,8 @@ public static final class Builder { @Renderer.VideoScalingMode private int videoScalingMode; private boolean useLazyPreparation; private SeekParameters seekParameters; + private long releaseTimeoutMs; + private long detachSurfaceTimeoutMs; private boolean pauseAtEndOfMediaItems; private boolean throwWhenStuckBuffering; private boolean buildCalled; @@ -143,6 +149,8 @@ public static final class Builder { *
If a call to {@link #release} or {@link #setForegroundMode} takes more than {@code + * timeoutMs} to complete, the player will report an error via {@link + * Player.EventListener#onPlayerError}. + * + * @param releaseTimeoutMs The release timeout, in milliseconds. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setReleaseTimeoutMs(long releaseTimeoutMs) { + Assertions.checkState(!buildCalled); + this.releaseTimeoutMs = releaseTimeoutMs; + return this; + } + + /** + * Sets a timeout for detaching a surface from the player. + * + *
If detaching a surface or replacing a surface takes more than {@code + * detachSurfaceTimeoutMs} to complete, the player will report an error via {@link + * Player.EventListener#onPlayerError}. + * + * @param detachSurfaceTimeoutMs The timeout for detaching a surface, in milliseconds. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDetachSurfaceTimeoutMs(long detachSurfaceTimeoutMs) { + Assertions.checkState(!buildCalled); + this.detachSurfaceTimeoutMs = detachSurfaceTimeoutMs; + return this; + } + /** * Sets whether to pause playback at the end of each media item. * @@ -537,6 +581,7 @@ public SimpleExoPlayer build() { private final StreamVolumeManager streamVolumeManager; private final WakeLockManager wakeLockManager; private final WifiLockManager wifiLockManager; + private final long detachSurfaceTimeoutMs; @Nullable private Format videoFormat; @Nullable private Format audioFormat; @@ -597,6 +642,7 @@ protected SimpleExoPlayer(Builder builder) { audioAttributes = builder.audioAttributes; videoScalingMode = builder.videoScalingMode; skipSilenceEnabled = builder.skipSilenceEnabled; + detachSurfaceTimeoutMs = builder.detachSurfaceTimeoutMs; componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); audioListeners = new CopyOnWriteArraySet<>(); @@ -1991,10 +2037,16 @@ private void setVideoSurfaceInternal(@Nullable Surface surface, boolean ownsSurf // We're replacing a surface. Block to ensure that it's not accessed after the method returns. try { for (PlayerMessage message : messages) { - message.blockUntilDelivered(); + message.blockUntilDelivered(detachSurfaceTimeoutMs); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } catch (TimeoutException e) { + player.stop( + /* reset= */ false, + ExoPlaybackException.createForTimeout( + new TimeoutException("Detaching surface timed out."), + ExoPlaybackException.TIMEOUT_OPERATION_DETACH_SURFACE)); } // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java index 490cc520fe7..70fd5445e18 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java @@ -17,7 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -66,31 +66,27 @@ public void tearDown() { } @Test - public void experimentalBlockUntilDelivered_timesOut() throws Exception { + public void blockUntilDelivered_timesOut() throws Exception { when(clock.elapsedRealtime()).thenReturn(0L).thenReturn(TIMEOUT_MS * 2); - try { - message.send().experimentalBlockUntilDelivered(TIMEOUT_MS, clock); - fail(); - } catch (TimeoutException expected) { - } + assertThrows( + TimeoutException.class, () -> message.send().blockUntilDelivered(TIMEOUT_MS, clock)); - // Ensure experimentalBlockUntilDelivered() entered the blocking loop + // Ensure blockUntilDelivered() entered the blocking loop. verify(clock, Mockito.times(2)).elapsedRealtime(); } @Test - public void experimentalBlockUntilDelivered_onAlreadyProcessed_succeeds() throws Exception { + public void blockUntilDelivered_onAlreadyProcessed_succeeds() throws Exception { when(clock.elapsedRealtime()).thenReturn(0L); message.send().markAsProcessed(/* isDelivered= */ true); - assertThat(message.experimentalBlockUntilDelivered(TIMEOUT_MS, clock)).isTrue(); + assertThat(message.blockUntilDelivered(TIMEOUT_MS, clock)).isTrue(); } @Test - public void experimentalBlockUntilDelivered_markAsProcessedWhileBlocked_succeeds() - throws Exception { + public void blockUntilDelivered_markAsProcessedWhileBlocked_succeeds() throws Exception { message.send(); // Use a separate Thread to mark the message as processed. @@ -114,8 +110,8 @@ public void experimentalBlockUntilDelivered_markAsProcessedWhileBlocked_succeeds }); try { - assertThat(message.experimentalBlockUntilDelivered(TIMEOUT_MS, clock)).isTrue(); - // Ensure experimentalBlockUntilDelivered() entered the blocking loop. + assertThat(message.blockUntilDelivered(TIMEOUT_MS, clock)).isTrue(); + // Ensure blockUntilDelivered() entered the blocking loop. verify(clock, Mockito.atLeast(2)).elapsedRealtime(); future.get(1, SECONDS); } finally {