From 603dcf49780c1285e5739de8dd44ae2b5f3c7900 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 28 Aug 2020 10:27:15 +0100 Subject: [PATCH 01/30] Download offline DRM license from non-UI thread PiperOrigin-RevId: 328904901 --- .../exoplayer2/demo/DownloadTracker.java | 160 ++++++++++++------ 1 file changed, 108 insertions(+), 52 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 8109263e55..b448dd40de 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -16,10 +16,12 @@ package com.google.android.exoplayer2.demo; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; +import android.os.AsyncTask; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -170,6 +172,7 @@ public class DownloadTracker { private TrackSelectionDialog trackSelectionDialog; private MappedTrackInfo mappedTrackInfo; + private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; @Nullable private byte[] keySetId; public StartDownloadDialogHelper( @@ -185,6 +188,9 @@ public class DownloadTracker { if (trackSelectionDialog != null) { trackSelectionDialog.dismiss(); } + if (widevineOfflineLicenseFetchTask != null) { + widevineOfflineLicenseFetchTask.cancel(false); + } } // DownloadHelper.Callback implementation. @@ -192,59 +198,32 @@ public class DownloadTracker { @Override public void onPrepared(@NonNull DownloadHelper helper) { @Nullable Format format = getFirstFormatWithDrmInitData(helper); - if (format != null) { - if (Util.SDK_INT < 18) { - Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18"); - return; - } - // TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest. - if (!hasSchemaData(format.drmInitData)) { - Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) - .show(); - Log.e( - TAG, - "Downloading content where DRM scheme data is not located in the manifest is not" - + " supported"); - return; - } - try { - // TODO(internal b/163107948): Download the license on another thread to keep the UI - // thread unblocked. - fetchOfflineLicense(format); - } catch (DrmSession.DrmSessionException e) { - Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Failed to fetch offline DRM license", e); - return; - } - } - - if (helper.getPeriodCount() == 0) { - Log.d(TAG, "No periods found. Downloading entire stream."); - startDownload(); - downloadHelper.release(); + if (format == null) { + onDownloadPrepared(helper); return; } - mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { - Log.d(TAG, "No dialog content. Downloading entire stream."); - startDownload(); - downloadHelper.release(); + // The content is DRM protected. We need to acquire an offline license. + if (Util.SDK_INT < 18) { + Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18"); return; } - trackSelectionDialog = - TrackSelectionDialog.createForMappedTrackInfoAndParameters( - /* titleId= */ R.string.exo_download_description, - mappedTrackInfo, - trackSelectorParameters, - /* allowAdaptiveSelections =*/ false, - /* allowMultipleOverrides= */ true, - /* onClickListener= */ this, - /* onDismissListener= */ this); - trackSelectionDialog.show(fragmentManager, /* tag= */ null); + // TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest. + if (!hasSchemaData(format.drmInitData)) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e( + TAG, + "Downloading content where DRM scheme data is not located in the manifest is not" + + " supported"); + return; + } + widevineOfflineLicenseFetchTask = + new WidevineOfflineLicenseFetchTask( + format, mediaItem.playbackProperties.drmConfiguration.licenseUri, this, helper); + widevineOfflineLicenseFetchTask.execute(); } @Override @@ -292,6 +271,44 @@ public class DownloadTracker { // Internal methods. + private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) { + this.keySetId = keySetId; + onDownloadPrepared(helper); + } + + private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Failed to fetch offline DRM license", e); + } + + private void onDownloadPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + trackSelectionDialog = + TrackSelectionDialog.createForMappedTrackInfoAndParameters( + /* titleId= */ R.string.exo_download_description, + mappedTrackInfo, + trackSelectorParameters, + /* allowAdaptiveSelections =*/ false, + /* allowMultipleOverrides= */ true, + /* onClickListener= */ this, + /* onDismissListener= */ this); + trackSelectionDialog.show(fragmentManager, /* tag= */ null); + } + private void startDownload() { startDownload(buildDownloadRequest()); } @@ -306,15 +323,54 @@ public class DownloadTracker { .getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title))) .copyWithKeySetId(keySetId); } + } - @RequiresApi(18) - private void fetchOfflineLicense(Format format) throws DrmSession.DrmSessionException { + /** Downloads a Widevine offline license in a background thread. */ + @RequiresApi(18) + private final class WidevineOfflineLicenseFetchTask extends AsyncTask { + private final Format format; + private final Uri licenseUri; + private final StartDownloadDialogHelper dialogHelper; + private final DownloadHelper downloadHelper; + + @Nullable private byte[] keySetId; + @Nullable private DrmSession.DrmSessionException drmSessionException; + + public WidevineOfflineLicenseFetchTask( + Format format, + Uri licenseUri, + StartDownloadDialogHelper dialogHelper, + DownloadHelper downloadHelper) { + this.format = format; + this.licenseUri = licenseUri; + this.dialogHelper = dialogHelper; + this.downloadHelper = downloadHelper; + } + + @Override + protected Void doInBackground(Void... voids) { OfflineLicenseHelper offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance( - mediaItem.playbackProperties.drmConfiguration.licenseUri.toString(), + licenseUri.toString(), httpDataSourceFactory, new DrmSessionEventListener.EventDispatcher()); - keySetId = offlineLicenseHelper.downloadLicense(format); + try { + keySetId = offlineLicenseHelper.downloadLicense(format); + } catch (DrmSession.DrmSessionException e) { + drmSessionException = e; + } finally { + offlineLicenseHelper.release(); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + if (drmSessionException != null) { + dialogHelper.onOfflineLicenseFetchedError(drmSessionException); + } else { + dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId)); + } } } From 8a8a1517ec8f9fde96840cc0245b6cff859c75a2 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 28 Aug 2020 11:16:36 +0100 Subject: [PATCH 02/30] Handle KEYCODE_HEADSETHOOK as alias of KEYCODE_MEDIA_PLAY_PAUSE ISSUE: #7813 PiperOrigin-RevId: 328910220 --- .../com/google/android/exoplayer2/ui/PlayerControlView.java | 2 ++ .../google/android/exoplayer2/ui/StyledPlayerControlView.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index fe802f9c0e..cea6bcecc6 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -1210,6 +1210,7 @@ public class PlayerControlView extends FrameLayout { } else if (event.getRepeatCount() == 0) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); break; case KeyEvent.KEYCODE_MEDIA_PLAY: @@ -1244,6 +1245,7 @@ public class PlayerControlView extends FrameLayout { return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 07106686ad..0f5594b8a2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -1644,6 +1644,7 @@ public class StyledPlayerControlView extends FrameLayout { } else if (event.getRepeatCount() == 0) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); break; case KeyEvent.KEYCODE_MEDIA_PLAY: @@ -1678,6 +1679,7 @@ public class StyledPlayerControlView extends FrameLayout { return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT From 0045d20d2cba0e2470342f2b96a42bd8096267c1 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 28 Aug 2020 12:31:36 +0100 Subject: [PATCH 03/30] Document how to set the priority on API level above 25 Issue: #7820 PiperOrigin-RevId: 328917063 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 06a5341499..e23c91cd16 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ui; import android.app.Notification; +import android.app.NotificationChannel; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; @@ -877,6 +878,10 @@ public class PlayerNotificationManager { * *

See {@link NotificationCompat.Builder#setPriority(int)}. * + *

To set the priority for API levels above 25, you can create your own {@link + * NotificationChannel} with a given importance level and pass the id of the channel to the {@link + * #PlayerNotificationManager(Context, String, int, MediaDescriptionAdapter) constructor}. + * * @param priority The priority which can be one of {@link NotificationCompat#PRIORITY_DEFAULT}, * {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link * NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set From 3dbb4c4da90a136960823cb844e98ca12ad29404 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 28 Aug 2020 12:45:45 +0100 Subject: [PATCH 04/30] Migrate test repeatModeChanges() to TestExoPlayer PiperOrigin-RevId: 328918314 --- .../android/exoplayer2/ExoPlayerTest.java | 81 +++++++++---------- .../exoplayer2/testutil/TestExoPlayer.java | 56 +++++++++++++ 2 files changed, 94 insertions(+), 43 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 0043cb9e74..66557753f4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static com.google.android.exoplayer2.testutil.TestExoPlayer.playUntilStartOfWindow; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilPlaybackState; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilTimelineChanged; import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; @@ -114,9 +115,11 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mockito; @@ -439,49 +442,41 @@ public final class ExoPlayerTest { public void repeatModeChanges() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .playUntilStartOfWindow(/* windowIndex= */ 1) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .playUntilStartOfWindow(/* windowIndex= */ 1) - .setRepeatMode(Player.REPEAT_MODE_OFF) - .playUntilStartOfWindow(/* windowIndex= */ 2) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .playUntilStartOfWindow(/* windowIndex= */ 2) - .setRepeatMode(Player.REPEAT_MODE_ALL) - .playUntilStartOfWindow(/* windowIndex= */ 0) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .playUntilStartOfWindow(/* windowIndex= */ 0) - .playUntilStartOfWindow(/* windowIndex= */ 0) - .setRepeatMode(Player.REPEAT_MODE_OFF) - .play() - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder(context) - .setTimeline(timeline) - .setRenderers(renderer) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); - testRunner.assertPositionDiscontinuityReasonsEqual( - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(renderer).build(); + AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class); + player.addAnalyticsListener(mockAnalyticsListener); + + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + player.prepare(); + runUntilTimelineChanged(player); + playUntilStartOfWindow(player, /* windowIndex= */ 1); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + playUntilStartOfWindow(player, /* windowIndex= */ 1); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + playUntilStartOfWindow(player, /* windowIndex= */ 2); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + playUntilStartOfWindow(player, /* windowIndex= */ 2); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + playUntilStartOfWindow(player, /* windowIndex= */ 0); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + playUntilStartOfWindow(player, /* windowIndex= */ 0); + playUntilStartOfWindow(player, /* windowIndex= */ 0); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + playUntilStartOfWindow(player, /* windowIndex= */ 1); + playUntilStartOfWindow(player, /* windowIndex= */ 2); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + + ArgumentCaptor eventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(mockAnalyticsListener, times(10)) + .onMediaItemTransition(eventTimes.capture(), any(), anyInt()); + assertThat( + eventTimes.getAllValues().stream() + .map(eventTime -> eventTime.currentWindowIndex) + .collect(Collectors.toList())) + .containsExactly(0, 1, 1, 2, 2, 0, 0, 0, 1, 2) + .inOrder(); assertThat(renderer.isEnded).isTrue(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java index 5441d25cd9..6feea08a02 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java @@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil import static com.google.common.truth.Truth.assertThat; import android.content.Context; +import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; @@ -37,6 +38,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoListener; import java.util.concurrent.TimeoutException; @@ -396,6 +398,7 @@ public class TestExoPlayer { */ public static void runUntilPositionDiscontinuity( Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException { + verifyMainTestThread(player); AtomicBoolean receivedCallback = new AtomicBoolean(false); Player.EventListener listener = new Player.EventListener() { @@ -458,6 +461,59 @@ public class TestExoPlayer { runMainLooperUntil(receivedCallback::get); } + /** + * Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player} + * reaches the specified position and then pauses the {@code player}. + * + * @param player The {@link Player}. + * @param windowIndex The window. + * @param positionMs The position within the window, in milliseconds. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public static void playUntilPosition(ExoPlayer player, int windowIndex, long positionMs) + throws TimeoutException { + verifyMainTestThread(player); + Handler testHandler = Util.createHandlerForCurrentOrMainLooper(); + + AtomicBoolean messageHandled = new AtomicBoolean(); + player + .createMessage( + (messageType, payload) -> { + // Block playback thread until pause command has been sent from test thread. + ConditionVariable blockPlaybackThreadCondition = new ConditionVariable(); + testHandler.post( + () -> { + player.pause(); + messageHandled.set(true); + blockPlaybackThreadCondition.open(); + }); + try { + blockPlaybackThreadCondition.block(); + } catch (InterruptedException e) { + // Ignore. + } + }) + .setPosition(windowIndex, positionMs) + .send(); + player.play(); + runMainLooperUntil(messageHandled::get); + } + + /** + * Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player} + * reaches the specified window and then pauses the {@code player}. + * + * @param player The {@link Player}. + * @param windowIndex The window. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public static void playUntilStartOfWindow(ExoPlayer player, int windowIndex) + throws TimeoutException { + playUntilPosition(player, windowIndex, /* positionMs= */ 0); + } + /** * Runs tasks of the main {@link Looper} until the player completely handled all previously issued * commands on the internal playback thread. From e163fe6949544dc55d3a3d50d47027c3888e7621 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 28 Aug 2020 17:23:11 +0100 Subject: [PATCH 05/30] Optimize AAC seeking except for xHE-AAC In 2.11.2 to 2.11.5, we considered all AAC streams as consisting only of sync samples. In 2.11.6+, we considered no AAC streams as consisting of sync samples, because the property is not guaranteed specifically for xHE-AAC. This will have caused a small regression is seek speed for some types of media. This change brings back the optimization for AAC, specifically excluding only xHE-AAC (and cases where we don't know what type of AAC we're dealing with). PiperOrigin-RevId: 328950697 --- .../android/exoplayer2/util/MimeTypes.java | 29 ++++++++++++++----- .../exoplayer2/util/MimeTypesTest.java | 15 ++++++++++ .../source/ClippingMediaPeriod.java | 3 +- .../exoplayer2/source/SampleQueue.java | 2 +- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 10e8d19063..6d5f167047 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -170,15 +170,16 @@ public final class MimeTypes { } /** - * Returns true if it is known that all samples in a stream of the given MIME type are guaranteed - * to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on every - * sample). + * Returns true if it is known that all samples in a stream of the given MIME type and codec are + * guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on + * every sample). * - * @param mimeType A MIME type. - * @return True if it is known that all samples in a stream of the given MIME type are guaranteed - * to be sync samples. False otherwise, including if {@code null} is passed. + * @param mimeType The MIME type of the stream. + * @param codec The RFC 6381 codec string of the stream, or {@code null} if unknown. + * @return Whether it is known that all samples in the stream are guaranteed to be sync samples. */ - public static boolean allSamplesAreSyncSamples(@Nullable String mimeType) { + public static boolean allSamplesAreSyncSamples( + @Nullable String mimeType, @Nullable String codec) { if (mimeType == null) { return false; } @@ -198,6 +199,20 @@ public final class MimeTypes { case AUDIO_E_AC3: case AUDIO_E_AC3_JOC: return true; + case AUDIO_AAC: + if (codec == null) { + return false; + } + @Nullable Mp4aObjectType objectType = getObjectTypeFromMp4aRFC6381CodecString(codec); + if (objectType == null) { + return false; + } + @C.Encoding + int encoding = AacUtil.getEncodingForAudioObjectType(objectType.audioObjectTypeIndication); + // xHE-AAC is an exception in which it's not true that all samples will be sync samples. + // Also return false for ENCODING_INVALID, which indicates we weren't able to parse the + // encoding from the codec string. + return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_AAC_XHE; default: return false; } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java index 34ad0a5946..46202a5991 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import static android.media.MediaCodecInfo.CodecProfileLevel.AACObjectHE; +import static android.media.MediaCodecInfo.CodecProfileLevel.AACObjectXHE; import static com.google.common.truth.Truth.assertThat; import androidx.annotation.Nullable; @@ -171,4 +173,17 @@ public final class MimeTypesTest { assertThat(objectType.objectTypeIndication).isEqualTo(expectedObjectTypeIndicator); assertThat(objectType.audioObjectTypeIndication).isEqualTo(expectedAudioObjectTypeIndicator); } + + @Test + public void allSamplesAreSyncSamples_forAac_usesCodec() { + assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40." + AACObjectHE)) + .isTrue(); + assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40." + AACObjectXHE)) + .isFalse(); + assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40")).isFalse(); + assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40.")).isFalse(); + assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "invalid")).isFalse(); + assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, /* codec= */ null)) + .isFalse(); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 31254907fe..7bb6a83add 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -264,7 +264,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb for (TrackSelection trackSelection : selections) { if (trackSelection != null) { Format selectedFormat = trackSelection.getSelectedFormat(); - if (!MimeTypes.allSamplesAreSyncSamples(selectedFormat.sampleMimeType)) { + if (!MimeTypes.allSamplesAreSyncSamples( + selectedFormat.sampleMimeType, selectedFormat.codecs)) { return true; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 28d25feb03..3eefd8634e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -659,7 +659,7 @@ public class SampleQueue implements TrackOutput { upstreamFormat = format; } upstreamAllSamplesAreSyncSamples = - MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType); + MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType, upstreamFormat.codecs); loggedUnexpectedNonSyncSample = false; return true; } From 440fd1cf6217aa6978e887db35a3c44a97fa7d15 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 28 Aug 2020 20:06:11 +0100 Subject: [PATCH 06/30] Some minor tweaks 1. Add EventLogger right away in PlayerActivity, else it doesn't log playWhenReady being initially set to true. 2. Remove EventLogger logging for the audio position advancing. It's redundant with isPlaying logging unless you're very specifically interested in the timing difference. 3. Remove unnecessary comment in Player. 4. Fix Timeline Javadoc. PiperOrigin-RevId: 328983944 --- .../exoplayer2/demo/PlayerActivity.java | 2 +- .../com/google/android/exoplayer2/Player.java | 2 - .../google/android/exoplayer2/Timeline.java | 76 +++++++++++-------- .../android/exoplayer2/util/EventLogger.java | 6 -- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 797eb503dd..211e22deff 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -297,9 +297,9 @@ public class PlayerActivity extends AppCompatActivity .setTrackSelector(trackSelector) .build(); player.addListener(new PlayerEventListener()); + player.addAnalyticsListener(new EventLogger(trackSelector)); player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setPlayWhenReady(startAutoPlay); - player.addAnalyticsListener(new EventLogger(trackSelector)); playerView.setPlayer(player); playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 490022de93..9d9d9cdc2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -374,8 +374,6 @@ public interface Player { } /** The device component of a {@link Player}. */ - // Note: It's mostly from the androidx.media.VolumeProviderCompat and - // androidx.media.MediaControllerCompat.PlaybackInfo. interface DeviceComponent { /** Adds a listener to receive device events. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index b2268e1fcc..e992eb588d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -48,62 +48,74 @@ import com.google.android.exoplayer2.util.Util; *

Single media file or on-demand stream

* *

Example timeline for a
- * single file A timeline for a single media file or on-demand stream consists of a single period - * and window. The window spans the whole period, indicating that all parts of the media are - * available for playback. The window's default position is typically at the start of the period - * (indicated by the black dot in the figure above). + * single file"> + * + *

A timeline for a single media file or on-demand stream consists of a single period and window. + * The window spans the whole period, indicating that all parts of the media are available for + * playback. The window's default position is typically at the start of the period (indicated by the + * black dot in the figure above). * *

Playlist of media files or on-demand streams

* *

Example timeline for a
- * playlist of files A timeline for a playlist of media files or on-demand streams consists of - * multiple periods, each with its own window. Each window spans the whole of the corresponding - * period, and typically has a default position at the start of the period. The properties of the - * periods and windows (e.g. their durations and whether the window is seekable) will often only - * become known when the player starts buffering the corresponding file or stream. + * playlist of files"> + * + *

A timeline for a playlist of media files or on-demand streams consists of multiple periods, + * each with its own window. Each window spans the whole of the corresponding period, and typically + * has a default position at the start of the period. The properties of the periods and windows + * (e.g. their durations and whether the window is seekable) will often only become known when the + * player starts buffering the corresponding file or stream. * *

Live stream with limited availability

* *

Example timeline for
- * a live stream with limited availability A timeline for a live stream consists of a period whose - * duration is unknown, since it's continually extending as more content is broadcast. If content - * only remains available for a limited period of time then the window may start at a non-zero - * position, defining the region of content that can still be played. The window will have {@link - * Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to - * true as long as we expect changes to the live window. Its default position is typically near to - * the live edge (indicated by the black dot in the figure above). + * a live stream with limited availability"> + * + *

A timeline for a live stream consists of a period whose duration is unknown, since it's + * continually extending as more content is broadcast. If content only remains available for a + * limited period of time then the window may start at a non-zero position, defining the region of + * content that can still be played. The window will have {@link Window#isLive} set to true to + * indicate it's a live stream and {@link Window#isDynamic} set to true as long as we expect changes + * to the live window. Its default position is typically near to the live edge (indicated by the + * black dot in the figure above). * *

Live stream with indefinite availability

* *

Example timeline
- * for a live stream with indefinite availability A timeline for a live stream with indefinite - * availability is similar to the Live stream with limited availability - * case, except that the window starts at the beginning of the period to indicate that all of the - * previously broadcast content can still be played. + * for a live stream with indefinite availability"> + * + *

A timeline for a live stream with indefinite availability is similar to the Live stream with limited availability case, except that the window + * starts at the beginning of the period to indicate that all of the previously broadcast content + * can still be played. * *

Live stream with multiple periods

* *

Example timeline
- * for a live stream with multiple periods This case arises when a live stream is explicitly - * divided into separate periods, for example at content boundaries. This case is similar to the Live stream with limited availability case, except that the window may - * span more than one period. Multiple periods are also possible in the indefinite availability - * case. + * for a live stream with multiple periods"> + * + *

This case arises when a live stream is explicitly divided into separate periods, for example + * at content boundaries. This case is similar to the Live stream with + * limited availability case, except that the window may span more than one period. Multiple + * periods are also possible in the indefinite availability case. * *

On-demand stream followed by live stream

* *

Example timeline for an
- * on-demand stream followed by a live stream This case is the concatenation of the Single media file or on-demand stream and Live - * stream with multiple periods cases. When playback of the on-demand stream ends, playback of - * the live stream will start from its default position near the live edge. + * on-demand stream followed by a live stream"> + * + *

This case is the concatenation of the Single media file or on-demand + * stream and Live stream with multiple periods cases. When playback + * of the on-demand stream ends, playback of the live stream will start from its default position + * near the live edge. * *

On-demand stream with mid-roll ads

* *

Example
- * timeline for an on-demand stream with mid-roll ad groups This case includes mid-roll ad groups, - * which are defined as part of the timeline's single period. The period can be queried for - * information about the ad groups and the ads they contain. + * timeline for an on-demand stream with mid-roll ad groups"> + * + *

This case includes mid-roll ad groups, which are defined as part of the timeline's single + * period. The period can be queried for information about the ad groups and the ads they contain. */ public abstract class Timeline { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index aa82d41414..a441e81bc4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -318,12 +318,6 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "audioInputFormat", Format.toLogString(format)); } - @Override - public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) { - long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs; - logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs); - } - @Override public void onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { From 17b370d00c346205a9d3edc264c9f7f96a73f5b2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Aug 2020 16:07:21 +0100 Subject: [PATCH 07/30] Allow upstream discards from the SampleQueue by time. Add a SampleQueue method to discard from the write side of the queue by timestamp PiperOrigin-RevId: 329303714 --- RELEASENOTES.md | 5 +- .../exoplayer2/source/SampleQueue.java | 60 +++++-- .../exoplayer2/source/SampleQueueTest.java | 149 ++++++++++++++++++ 3 files changed, 201 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c5e43a5e49..a680b7c4ca 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,10 @@ ### dev-v2 (not yet released) -* New release notes go here! +* Core library: + * Add `SampleQueue.discardUpstreamFrom` so upstream samples can be + discarded by timestamp. + * Add `SampleQueue.getLargestReadTimestampUs`. ### 2.12.0 (not yet released - targeted for 2020-09-03) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 3eefd8634e..20d9f44562 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -215,6 +215,22 @@ public class SampleQueue implements TrackOutput { sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex)); } + /** + * Discards samples from the write side of the queue. + * + * @param timeUs Samples will be discarded from the write end of the queue until a sample with a + * timestamp smaller than timeUs is encountered (this sample is not discarded). Must be larger + * than {@link #getLargestReadTimestampUs()}. + */ + public final void discardUpstreamFrom(long timeUs) { + if (length == 0) { + return; + } + checkArgument(timeUs > getLargestReadTimestampUs()); + int retainCount = countUnreadSamplesBefore(timeUs); + discardUpstreamSamples(absoluteFirstIndex + retainCount); + } + // Called by the consuming thread. /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */ @@ -278,6 +294,16 @@ public class SampleQueue implements TrackOutput { return largestQueuedTimestampUs; } + /** + * Returns the largest sample timestamp that has been read since the last {@link #reset}. + * + * @return The largest sample timestamp that has been read, or {@link Long#MIN_VALUE} if no + * samples have been read. + */ + public final synchronized long getLargestReadTimestampUs() { + return max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition)); + } + /** * Returns whether the last sample of the stream has knowingly been queued. A return value of * {@code false} means that the last sample had not been queued or that it's unknown whether the @@ -777,20 +803,10 @@ public class SampleQueue implements TrackOutput { if (length == 0) { return timeUs > largestDiscardedTimestampUs; } - long largestReadTimestampUs = - max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition)); - if (largestReadTimestampUs >= timeUs) { + if (getLargestReadTimestampUs() >= timeUs) { return false; } - int retainCount = length; - int relativeSampleIndex = getRelativeIndex(length - 1); - while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { - retainCount--; - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } + int retainCount = countUnreadSamplesBefore(timeUs); discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount); return true; } @@ -887,6 +903,26 @@ public class SampleQueue implements TrackOutput { return sampleCountToTarget; } + /** + * Counts the number of samples that haven't been read that have a timestamp smaller than {@code + * timeUs}. + * + * @param timeUs The specified time. + * @return The number of unread samples with a timestamp smaller than {@code timeUs}. + */ + private int countUnreadSamplesBefore(long timeUs) { + int count = length; + int relativeSampleIndex = getRelativeIndex(length - 1); + while (count > readPosition && timesUs[relativeSampleIndex] >= timeUs) { + count--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + return count; + } + /** * Discards the specified number of samples. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 54e2dd902d..241834fab5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -878,6 +878,118 @@ public final class SampleQueueTest { assertReadTestData(FORMAT_1, 1, 7); } + @Test + public void discardUpstreamFrom() { + writeTestData(); + sampleQueue.discardUpstreamFrom(8000); + assertAllocationCount(10); + sampleQueue.discardUpstreamFrom(7000); + assertAllocationCount(9); + sampleQueue.discardUpstreamFrom(6000); + assertAllocationCount(7); + sampleQueue.discardUpstreamFrom(5000); + assertAllocationCount(5); + sampleQueue.discardUpstreamFrom(4000); + assertAllocationCount(4); + sampleQueue.discardUpstreamFrom(3000); + assertAllocationCount(3); + sampleQueue.discardUpstreamFrom(2000); + assertAllocationCount(2); + sampleQueue.discardUpstreamFrom(1000); + assertAllocationCount(1); + sampleQueue.discardUpstreamFrom(0); + assertAllocationCount(0); + assertReadFormat(false, FORMAT_2); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void discardUpstreamFromMulti() { + writeTestData(); + sampleQueue.discardUpstreamFrom(4000); + assertAllocationCount(4); + sampleQueue.discardUpstreamFrom(0); + assertAllocationCount(0); + assertReadFormat(false, FORMAT_2); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void discardUpstreamFromNonSampleTimestamps() { + writeTestData(); + sampleQueue.discardUpstreamFrom(3500); + assertAllocationCount(4); + sampleQueue.discardUpstreamFrom(500); + assertAllocationCount(1); + sampleQueue.discardUpstreamFrom(0); + assertAllocationCount(0); + assertReadFormat(false, FORMAT_2); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void discardUpstreamFromBeforeRead() { + writeTestData(); + sampleQueue.discardUpstreamFrom(4000); + assertAllocationCount(4); + assertReadTestData(null, 0, 4); + assertReadFormat(false, FORMAT_2); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void discardUpstreamFromAfterRead() { + writeTestData(); + assertReadTestData(null, 0, 3); + sampleQueue.discardUpstreamFrom(8000); + assertAllocationCount(10); + sampleQueue.discardToRead(); + assertAllocationCount(7); + sampleQueue.discardUpstreamFrom(7000); + assertAllocationCount(6); + sampleQueue.discardUpstreamFrom(6000); + assertAllocationCount(4); + sampleQueue.discardUpstreamFrom(5000); + assertAllocationCount(2); + sampleQueue.discardUpstreamFrom(4000); + assertAllocationCount(1); + sampleQueue.discardUpstreamFrom(3000); + assertAllocationCount(0); + assertReadFormat(false, FORMAT_2); + assertNoSamplesToRead(FORMAT_2); + } + + @Test + public void largestQueuedTimestampWithDiscardUpstreamFrom() { + writeTestData(); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); + sampleQueue.discardUpstreamFrom(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 1]); + // Discarding from upstream should reduce the largest timestamp. + assertThat(sampleQueue.getLargestQueuedTimestampUs()) + .isEqualTo(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 2]); + sampleQueue.discardUpstreamFrom(0); + // Discarding everything from upstream without reading should unset the largest timestamp. + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); + } + + @Test + public void largestQueuedTimestampWithDiscardUpstreamFromDecodeOrder() { + long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000}; + writeTestData( + DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, decodeOrderTimestamps, SAMPLE_FORMATS, SAMPLE_FLAGS); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000); + sampleQueue.discardUpstreamFrom(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 2]); + // Discarding the last two samples should not change the largest timestamp, due to the decode + // ordering of the timestamps. + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000); + sampleQueue.discardUpstreamFrom(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 3]); + // Once a third sample is discarded, the largest timestamp should have changed. + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000); + sampleQueue.discardUpstreamFrom(0); + // Discarding everything from upstream without reading should unset the largest timestamp. + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); + } + @Test public void discardUpstream() { writeTestData(); @@ -986,6 +1098,43 @@ public final class SampleQueueTest { assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); } + @Test + public void largestReadTimestampWithReadAll() { + writeTestData(); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE); + assertReadTestData(); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); + } + + @Test + public void largestReadTimestampWithReads() { + writeTestData(); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE); + + assertReadTestData(/* startFormat= */ null, 0, 2); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[1]); + + assertReadTestData(SAMPLE_FORMATS[1], 2, 3); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[4]); + } + + @Test + public void largestReadTimestampWithDiscard() { + // Discarding shouldn't change the read timestamp. + writeTestData(); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE); + sampleQueue.discardUpstreamSamples(5); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE); + + assertReadTestData(/* startFormat= */ null, 0, 3); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[2]); + + sampleQueue.discardUpstreamSamples(3); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[2]); + sampleQueue.discardToRead(); + assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[2]); + } + @Test public void setSampleOffsetBeforeData() { long sampleOffsetUs = 1000; From 91185500a1242b99b86b18bc9f3449d3dac1fa01 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 1 Sep 2020 09:36:43 +0100 Subject: [PATCH 08/30] Remove usage of assertThrows from ExoPlayer GTS tests The environment these tests are executed in is only using JUnit 4.10, which doesn't have assertThrows. PiperOrigin-RevId: 329462985 --- .../gts/DashWidevineOfflineTest.java | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index 81425c34ea..b657c10d22 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -17,11 +17,12 @@ package com.google.android.exoplayer2.playbacktests.gts; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; import android.media.MediaDrm.MediaDrmStateException; import android.net.Uri; import android.util.Pair; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import com.google.android.exoplayer2.Format; @@ -121,19 +122,19 @@ public final class DashWidevineOfflineTest { downloadLicense(); releaseLicense(); // keySetId no longer valid. - Throwable error = - assertThrows( - "Playback should fail because the license has been released.", - Throwable.class, - () -> testRunner.run()); - - // Get the root cause - Throwable cause = error.getCause(); - while (cause != null && cause != error) { - error = cause; - cause = error.getCause(); + try { + testRunner.run(); + fail("Playback should fail because the license has been released."); + } catch (RuntimeException expected) { + // Get the root cause + Throwable error = expected; + @Nullable Throwable cause = error.getCause(); + while (cause != null && cause != error) { + error = cause; + cause = error.getCause(); + } + assertThat(error).isInstanceOf(MediaDrmStateException.class); } - assertThat(error).isInstanceOf(MediaDrmStateException.class); } @Test @@ -144,18 +145,19 @@ public final class DashWidevineOfflineTest { downloadLicense(); releaseLicense(); // keySetId no longer valid. - Throwable error = - assertThrows( - "Playback should fail because the license has been released.", - Throwable.class, - () -> testRunner.run()); - // Get the root cause - Throwable cause = error.getCause(); - while (cause != null && cause != error) { - error = cause; - cause = error.getCause(); + try { + testRunner.run(); + fail("Playback should fail because the license has been released."); + } catch (RuntimeException expected) { + // Get the root cause + Throwable error = expected; + @Nullable Throwable cause = error.getCause(); + while (cause != null && cause != error) { + error = cause; + cause = error.getCause(); + } + assertThat(error).isInstanceOf(IllegalArgumentException.class); } - assertThat(error).isInstanceOf(IllegalArgumentException.class); } @Test From d155416c54a48f64bf3994609b7b56aff5e252ba Mon Sep 17 00:00:00 2001 From: gyumin Date: Tue, 1 Sep 2020 11:04:47 +0100 Subject: [PATCH 09/30] Add Util.postOrRun To avoid repetition, it adds Util.postOrRun and replaces existing instances with the util method. PiperOrigin-RevId: 329472769 --- .../ext/media2/PlayerCommandQueue.java | 16 +++++--- .../exoplayer2/ext/media2/PlayerHandler.java | 41 ------------------- .../exoplayer2/ext/media2/PlayerWrapper.java | 9 ++-- .../ext/media2/SessionPlayerConnector.java | 12 ++++-- .../google/android/exoplayer2/util/Util.java | 18 ++++++++ .../drm/DrmSessionEventListener.java | 12 +----- .../source/MediaSourceEventListener.java | 11 +---- 7 files changed, 46 insertions(+), 73 deletions(-) delete mode 100644 extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerHandler.java diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java index 7d804282ce..fc80c85856 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.ext.media2; +import static com.google.android.exoplayer2.util.Util.postOrRun; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import android.os.Handler; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -129,7 +131,7 @@ import java.util.concurrent.Callable; // Should be only used on the handler. private final PlayerWrapper player; - private final PlayerHandler handler; + private final Handler handler; private final Object lock; @GuardedBy("lock") @@ -141,7 +143,7 @@ import java.util.concurrent.Callable; // Should be only used on the handler. @Nullable private AsyncPlayerCommandResult pendingAsyncPlayerCommandResult; - public PlayerCommandQueue(PlayerWrapper player, PlayerHandler handler) { + public PlayerCommandQueue(PlayerWrapper player, Handler handler) { this.player = player; this.handler = handler; lock = new Object(); @@ -209,7 +211,7 @@ import java.util.concurrent.Callable; } processPendingCommandOnHandler(); }, - handler::postOrRun); + (runnable) -> postOrRun(handler, runnable)); if (DEBUG) { Log.d(TAG, "adding " + playerCommand); } @@ -220,7 +222,8 @@ import java.util.concurrent.Callable; } public void notifyCommandError() { - handler.postOrRun( + postOrRun( + handler, () -> { @Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult; if (pendingResult == null) { @@ -243,7 +246,8 @@ import java.util.concurrent.Callable; if (DEBUG) { Log.d(TAG, "notifyCommandCompleted, completedCommandCode=" + completedCommandCode); } - handler.postOrRun( + postOrRun( + handler, () -> { @Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult; if (pendingResult == null || pendingResult.commandCode != completedCommandCode) { @@ -267,7 +271,7 @@ import java.util.concurrent.Callable; } private void processPendingCommand() { - handler.postOrRun(this::processPendingCommandOnHandler); + postOrRun(handler, this::processPendingCommandOnHandler); } private void processPendingCommandOnHandler() { diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerHandler.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerHandler.java deleted file mode 100644 index eca8964804..0000000000 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.media2; - -import android.os.Handler; -import android.os.Looper; - -/** A {@link Handler} that provides {@link #postOrRun(Runnable)}. */ -/* package */ final class PlayerHandler extends Handler { - public PlayerHandler(Looper looper) { - super(looper); - } - - /** - * Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of this - * handler. Otherwise, runs the runnable directly. - * - * @param r A runnable to either post or run. - * @return {@code true} if it's successfully run. {@code false} otherwise. - */ - public boolean postOrRun(Runnable r) { - if (Thread.currentThread() != getLooper().getThread()) { - return post(r); - } - r.run(); - return true; - } -} diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java index 453a7b6d55..65bffb26be 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.ext.media2; +import static com.google.android.exoplayer2.util.Util.postOrRun; + +import android.os.Handler; import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; @@ -100,7 +103,7 @@ import java.util.List; private static final int POLL_BUFFER_INTERVAL_MS = 1000; private final Listener listener; - private final PlayerHandler handler; + private final Handler handler; private final Runnable pollBufferRunnable; private final Player player; @@ -144,7 +147,7 @@ import java.util.List; audioComponent.addAudioListener(componentListener); } - handler = new PlayerHandler(player.getApplicationLooper()); + handler = new Handler(player.getApplicationLooper()); pollBufferRunnable = new PollBufferRunnable(); media2Playlist = new ArrayList<>(); @@ -436,7 +439,7 @@ import java.util.List; private void handlePlayerStateChanged(@Player.State int state) { if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { - handler.postOrRun(pollBufferRunnable); + postOrRun(handler, pollBufferRunnable); } else { handler.removeCallbacks(pollBufferRunnable); } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java index d4aa888a1a..2bb762a971 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.ext.media2; +import static com.google.android.exoplayer2.util.Util.postOrRun; + +import android.os.Handler; import androidx.annotation.FloatRange; import androidx.annotation.GuardedBy; import androidx.annotation.IntRange; @@ -64,7 +67,7 @@ public final class SessionPlayerConnector extends SessionPlayer { private static final int END_OF_PLAYLIST = -1; private final Object stateLock = new Object(); - private final PlayerHandler taskHandler; + private final Handler taskHandler; private final Executor taskHandlerExecutor; private final PlayerWrapper player; private final PlayerCommandQueue playerCommandQueue; @@ -106,8 +109,8 @@ public final class SessionPlayerConnector extends SessionPlayer { Assertions.checkNotNull(controlDispatcher); state = PLAYER_STATE_IDLE; - taskHandler = new PlayerHandler(player.getApplicationLooper()); - taskHandlerExecutor = taskHandler::postOrRun; + taskHandler = new Handler(player.getApplicationLooper()); + taskHandlerExecutor = (runnable) -> postOrRun(taskHandler, runnable); ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener(); this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher); playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler); @@ -598,7 +601,8 @@ public final class SessionPlayerConnector extends SessionPlayer { private T runPlayerCallableBlocking(Callable callable) { SettableFuture future = SettableFuture.create(); boolean success = - taskHandler.postOrRun( + postOrRun( + taskHandler, () -> { try { future.set(callable.call()); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 7fa26a94f4..f954b60c45 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -493,6 +493,24 @@ public final class Util { return new Handler(looper, callback); } + /** + * Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of the {@link + * Handler}. Otherwise, runs the {@link Runnable} directly. + * + * @param handler The handler to which the {@link Runnable} will be posted. + * @param runnable The runnable to either post or run. + * @return {@code true} if the {@link Runnable} was successfully posted to the {@link Handler} or + * run. {@code false} otherwise. + */ + public static boolean postOrRun(Handler handler, Runnable runnable) { + if (handler.getLooper() == Looper.myLooper()) { + runnable.run(); + return true; + } else { + return handler.post(runnable); + } + } + /** * Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the * application's main thread if the current thread doesn't have a {@link Looper}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java index c32a6fc1a3..0720d9677f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionEventListener.java @@ -15,8 +15,9 @@ */ package com.google.android.exoplayer2.drm; +import static com.google.android.exoplayer2.util.Util.postOrRun; + import android.os.Handler; -import android.os.Looper; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player; @@ -207,15 +208,6 @@ public interface DrmSessionEventListener { } } - /** Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId)}. */ - private static void postOrRun(Handler handler, Runnable runnable) { - if (handler.getLooper() == Looper.myLooper()) { - runnable.run(); - } else { - handler.post(runnable); - } - } - private static final class ListenerAndHandler { public Handler handler; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 00ea71efd4..39fd6d53a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -15,8 +15,9 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.util.Util.postOrRun; + import android.os.Handler; -import android.os.Looper; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -471,14 +472,6 @@ public interface MediaSourceEventListener { return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; } - private static void postOrRun(Handler handler, Runnable runnable) { - if (handler.getLooper() == Looper.myLooper()) { - runnable.run(); - } else { - handler.post(runnable); - } - } - private static final class ListenerAndHandler { public Handler handler; From 47561f200f948930cce2e66e45b11eed1acbad7d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 1 Sep 2020 14:42:58 +0100 Subject: [PATCH 10/30] Fix handling of incompatible VPAID ads Issue: #7832 PiperOrigin-RevId: 329497598 --- RELEASENOTES.md | 2 ++ .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a680b7c4ca..576dbb848c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -322,6 +322,8 @@ * Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set companion ad slots without accessing the `AdDisplayContainer`. * Add missing notification of `VideoAdPlayerCallback.onLoaded`. + * Fix handling of incompatible VPAID ads + ([#7832](https://github.com/google/ExoPlayer/issues/7832)). * Demo app: * Replace the `extensions` variant with `decoderExtensions` and update the demo app use the Cronet and IMA extensions by default. diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 81e21bc753..351ad43d2c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -1096,6 +1096,11 @@ public final class ImaAdsLoader if (imaAdInfo != null) { adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); updateAdPlaybackState(); + } else if (adPlaybackState.adGroupCount == 1 && adPlaybackState.adGroupTimesUs[0] == 0) { + // For incompatible VPAID ads with one preroll, content is resumed immediately. In this case + // we haven't received ad info (the ad never loaded), but there is only one ad group to skip. + adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ 0); + updateAdPlaybackState(); } } From 5b80eb8eccd13d7bfdb7f3a5cdb3b7233e7baa1c Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 1 Sep 2020 17:58:07 +0100 Subject: [PATCH 11/30] Enable multidex for the playback tests PiperOrigin-RevId: 329527311 --- playbacktests/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 105427250b..7fc5d637cb 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -13,6 +13,12 @@ // limitations under the License. apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" +android { + defaultConfig { + multiDexEnabled true + } +} + dependencies { androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion From f68fc3e7014c9659036aea9ea52e1ff16ef72938 Mon Sep 17 00:00:00 2001 From: insun Date: Wed, 2 Sep 2020 04:17:10 +0100 Subject: [PATCH 12/30] Prevent buffering when clicking ffwd button at the end of stream PiperOrigin-RevId: 329634001 --- .../android/exoplayer2/ui/StyledPlayerControlView.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 0f5594b8a2..45c3059585 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -1638,7 +1638,9 @@ public class StyledPlayerControlView extends FrameLayout { } if (event.getAction() == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - controlDispatcher.dispatchFastForward(player); + if (player.getPlaybackState() != Player.STATE_ENDED) { + controlDispatcher.dispatchFastForward(player); + } } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { controlDispatcher.dispatchRewind(player); } else if (event.getRepeatCount() == 0) { @@ -1808,7 +1810,9 @@ public class StyledPlayerControlView extends FrameLayout { } else if (previousButton == view) { controlDispatcher.dispatchPrevious(player); } else if (fastForwardButton == view) { - controlDispatcher.dispatchFastForward(player); + if (player.getPlaybackState() != Player.STATE_ENDED) { + controlDispatcher.dispatchFastForward(player); + } } else if (rewindButton == view) { controlDispatcher.dispatchRewind(player); } else if (playPauseButton == view) { From 3ab9bc8e7fcc283d7ffc5eeba44421dfb45530f3 Mon Sep 17 00:00:00 2001 From: insun Date: Wed, 2 Sep 2020 07:21:23 +0100 Subject: [PATCH 13/30] Fix internal demo app crash Internal demo app crashed when device does not support required DRM scheme. This CL fixes it. PiperOrigin-RevId: 329653841 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 211e22deff..5c8d3cc934 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -271,13 +271,14 @@ public class PlayerActivity extends AppCompatActivity setContentView(R.layout.player_activity); } - protected void initializePlayer() { + /** @return Whether initialization was successful. */ + protected boolean initializePlayer() { if (player == null) { Intent intent = getIntent(); mediaItems = createMediaItems(intent); if (mediaItems.isEmpty()) { - return; + return false; } boolean preferExtensionDecoders = @@ -312,6 +313,7 @@ public class PlayerActivity extends AppCompatActivity player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); player.prepare(); updateButtonVisibility(); + return true; } private List createMediaItems(Intent intent) { From 56e622a2e58db14578b7e53e16883fd40974ff33 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 2 Sep 2020 09:17:09 +0100 Subject: [PATCH 14/30] Fix release notes link PiperOrigin-RevId: 329665766 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 576dbb848c..7daa5c9da5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -144,7 +144,7 @@ * Recreate the decoder when handling and swallowing decode errors in `TextRenderer`. This fixes a case where playback would never end when playing content with malformed subtitles - ([#7590](https://github.com/google/ExoPlayer/issues/790)). + ([#7590](https://github.com/google/ExoPlayer/issues/7590)). * Only apply `CaptionManager` font scaling in `SubtitleView.setUserDefaultTextSize` if the `CaptionManager` is enabled. From b79e2e069ff56fad7a89d321c5f0b6ea2d091099 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Sep 2020 09:17:09 +0100 Subject: [PATCH 15/30] SessionPlayerConnector: Use setter for ControlDispatcher This brings SessionPlayerConnector in line with other components that use a ControlDispatcher in the ExoPlayer library, all of which have a setter. PiperOrigin-RevId: 329665767 --- .../media2/SessionPlayerConnectorTest.java | 13 +++++++----- .../exoplayer2/ext/media2/PlayerWrapper.java | 16 +++++++-------- .../ext/media2/SessionPlayerConnector.java | 20 ++++++++++++------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java index 45a0c59645..b80cbe5a5f 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java @@ -185,17 +185,20 @@ public class SessionPlayerConnectorTest { } }; SimpleExoPlayer simpleExoPlayer = null; + SessionPlayerConnector playerConnector = null; try { simpleExoPlayer = new SimpleExoPlayer.Builder(context) .setLooper(Looper.myLooper()) .build(); - try (SessionPlayerConnector player = - new SessionPlayerConnector( - simpleExoPlayer, new DefaultMediaItemConverter(), controlDispatcher)) { - assertPlayerResult(player.play(), RESULT_INFO_SKIPPED); - } + playerConnector = + new SessionPlayerConnector(simpleExoPlayer, new DefaultMediaItemConverter()); + playerConnector.setControlDispatcher(controlDispatcher); + assertPlayerResult(playerConnector.play(), RESULT_INFO_SKIPPED); } finally { + if (playerConnector != null) { + playerConnector.close(); + } if (simpleExoPlayer != null) { simpleExoPlayer.release(); } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java index 65bffb26be..09e0325e93 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java @@ -27,6 +27,7 @@ import androidx.media2.common.MediaMetadata; import androidx.media2.common.SessionPlayer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; @@ -108,7 +109,6 @@ import java.util.List; private final Player player; private final MediaItemConverter mediaItemConverter; - private final ControlDispatcher controlDispatcher; private final ComponentListener componentListener; @Nullable private MediaMetadata playlistMetadata; @@ -117,6 +117,7 @@ import java.util.List; private final List media2Playlist; private final List exoPlayerPlaylist; + private ControlDispatcher controlDispatcher; private boolean prepared; private boolean rebuffering; private int currentWindowIndex; @@ -128,18 +129,13 @@ import java.util.List; * @param listener A {@link Listener}. * @param player The {@link Player}. * @param mediaItemConverter The {@link MediaItemConverter}. - * @param controlDispatcher A {@link ControlDispatcher}. */ - public PlayerWrapper( - Listener listener, - Player player, - MediaItemConverter mediaItemConverter, - ControlDispatcher controlDispatcher) { + public PlayerWrapper(Listener listener, Player player, MediaItemConverter mediaItemConverter) { this.listener = listener; this.player = player; this.mediaItemConverter = mediaItemConverter; - this.controlDispatcher = controlDispatcher; + controlDispatcher = new DefaultControlDispatcher(); componentListener = new ComponentListener(); player.addListener(componentListener); @Nullable Player.AudioComponent audioComponent = player.getAudioComponent(); @@ -160,6 +156,10 @@ import java.util.List; updatePlaylist(player.getCurrentTimeline()); } + public void setControlDispatcher(ControlDispatcher controlDispatcher) { + this.controlDispatcher = controlDispatcher; + } + public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) { return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null); } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java index 2bb762a971..1c6cc151c9 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java @@ -92,7 +92,7 @@ public final class SessionPlayerConnector extends SessionPlayer { * @param player The player to wrap. */ public SessionPlayerConnector(Player player) { - this(player, new DefaultMediaItemConverter(), new DefaultControlDispatcher()); + this(player, new DefaultMediaItemConverter()); } /** @@ -100,22 +100,28 @@ public final class SessionPlayerConnector extends SessionPlayer { * * @param player The player to wrap. * @param mediaItemConverter The {@link MediaItemConverter}. - * @param controlDispatcher The {@link ControlDispatcher}. */ - public SessionPlayerConnector( - Player player, MediaItemConverter mediaItemConverter, ControlDispatcher controlDispatcher) { + public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) { Assertions.checkNotNull(player); Assertions.checkNotNull(mediaItemConverter); - Assertions.checkNotNull(controlDispatcher); state = PLAYER_STATE_IDLE; taskHandler = new Handler(player.getApplicationLooper()); taskHandlerExecutor = (runnable) -> postOrRun(taskHandler, runnable); - ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener(); - this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher); + + this.player = new PlayerWrapper(new ExoPlayerWrapperListener(), player, mediaItemConverter); playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler); } + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}. + */ + public void setControlDispatcher(ControlDispatcher controlDispatcher) { + player.setControlDispatcher(controlDispatcher); + } + @Override public ListenableFuture play() { return playerCommandQueue.addCommand( From 5c24d1ccea3f49ba3d3408ef59b6d37c64c169d0 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Sep 2020 09:23:02 +0100 Subject: [PATCH 16/30] Clean up unnecessary use of full package name PiperOrigin-RevId: 329666401 --- .../android/exoplayer2/ui/StyledPlayerControlView.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 45c3059585..91151b5347 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -396,7 +396,7 @@ public class StyledPlayerControlView extends FrameLayout { private final String fullScreenEnterContentDescription; @Nullable private Player player; - private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; + private ControlDispatcher controlDispatcher; @Nullable private ProgressUpdateListener progressUpdateListener; @Nullable private PlaybackPreparer playbackPreparer; @@ -537,8 +537,7 @@ public class StyledPlayerControlView extends FrameLayout { extraAdGroupTimesMs = new long[0]; extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); - controlDispatcher = - new com.google.android.exoplayer2.DefaultControlDispatcher(fastForwardMs, rewindMs); + controlDispatcher = new DefaultControlDispatcher(fastForwardMs, rewindMs); updateProgressAction = this::updateProgress; LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this); @@ -839,9 +838,9 @@ public class StyledPlayerControlView extends FrameLayout { } /** - * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. + * Sets the {@link ControlDispatcher}. * - * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}. + * @param controlDispatcher The {@link ControlDispatcher}. */ public void setControlDispatcher(ControlDispatcher controlDispatcher) { if (this.controlDispatcher != controlDispatcher) { From be103ac553b810b5bb5ece923e7abc2fd9e334d9 Mon Sep 17 00:00:00 2001 From: insun Date: Wed, 2 Sep 2020 10:12:32 +0100 Subject: [PATCH 17/30] Add padding to styled widget's settings items PiperOrigin-RevId: 329671522 --- .../ui/src/main/res/layout/exo_styled_settings_list_item.xml | 2 ++ .../src/main/res/layout/exo_styled_sub_settings_list_item.xml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml b/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml index 37ba31a244..b7dc40120a 100644 --- a/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml +++ b/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml @@ -35,6 +35,8 @@ android:minHeight="@dimen/exo_settings_height" android:layout_marginLeft="2dp" android:layout_marginRight="2dp" + android:paddingEnd="4dp" + android:paddingRight="4dp" android:gravity="center|start" android:orientation="vertical"> diff --git a/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml b/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml index ba156ac8b9..9d184f3bbe 100644 --- a/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml +++ b/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml @@ -36,8 +36,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:minHeight="@dimen/exo_settings_height" - android:paddingStart="2dp" - android:paddingLeft="2dp" + android:layout_marginEnd="4dp" + android:layout_marginRight="4dp" android:gravity="center|start" android:textColor="@color/exo_white" android:textSize="@dimen/exo_settings_main_text_size"/> From f31411b1287201f635a62db547b0d4d08075524a Mon Sep 17 00:00:00 2001 From: insun Date: Wed, 2 Sep 2020 10:46:24 +0100 Subject: [PATCH 18/30] Revise Play/Pause button toggling logic This CL fixes two bugs: - Play/pause button toggling looked inconsistent when playback fails. When player state goes into idle, play button should dispatch playwhenready again. - Clicking play button at the end of stream should restart playback. But previously it changed playwhenready state and so playback has been paused. This CL fix it. PiperOrigin-RevId: 329675660 --- .../ui/StyledPlayerControlView.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 91151b5347..153f8c04ca 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -1646,7 +1646,7 @@ public class StyledPlayerControlView extends FrameLayout { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + dispatchPlayPause(player); break; case KeyEvent.KEYCODE_MEDIA_PLAY: controlDispatcher.dispatchSetPlayWhenReady(player, true); @@ -1815,14 +1815,7 @@ public class StyledPlayerControlView extends FrameLayout { } else if (rewindButton == view) { controlDispatcher.dispatchRewind(player); } else if (playPauseButton == view) { - if (player.getPlaybackState() == Player.STATE_IDLE) { - if (playbackPreparer != null) { - playbackPreparer.preparePlayback(); - } - } else if (player.getPlaybackState() == Player.STATE_ENDED) { - seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); - } - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + dispatchPlayPause(player); } else if (repeatToggleButton == view) { controlDispatcher.dispatchSetRepeatMode( player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); @@ -1838,6 +1831,20 @@ public class StyledPlayerControlView extends FrameLayout { } } + private void dispatchPlayPause(Player player) { + if (player.getPlaybackState() == Player.STATE_IDLE) { + if (playbackPreparer != null) { + playbackPreparer.preparePlayback(); + } + controlDispatcher.dispatchSetPlayWhenReady(player, true); + } else if (player.getPlaybackState() == Player.STATE_ENDED) { + seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + controlDispatcher.dispatchSetPlayWhenReady(player, true); + } else { + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + } + } + private class SettingsAdapter extends RecyclerView.Adapter { private final String[] mainTexts; private final String[] subTexts; From 29f2a31af73fa222f744405681661f0f88aca94b Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 2 Sep 2020 10:47:43 +0100 Subject: [PATCH 19/30] Re-enable ignored unit tests PiperOrigin-RevId: 329675833 --- .../AsynchronousMediaCodecBufferEnqueuerTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java index ed34a4eca8..5af10f2e12 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.doAnswer; import android.media.MediaCodec; +import android.media.MediaFormat; import android.os.HandlerThread; import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -31,7 +32,6 @@ import java.io.IOException; import java.util.concurrent.atomic.AtomicLong; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,6 +54,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { @Before public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); + codec.configure(new MediaFormat(), /* surface= */ null, /* crypto= */ null, /* flags= */ 0); + codec.start(); handlerThread = new TestHandlerThread("TestHandlerThread"); enqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, handlerThread, mockConditionVariable); @@ -62,7 +64,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { @After public void tearDown() { enqueuer.shutdown(); - + codec.stop(); + codec.release(); assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -98,7 +101,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { /* flags= */ 0)); } - @Ignore @Test public void queueInputBuffer_multipleTimes_limitsObjectsAllocation() { enqueuer.start(); @@ -159,7 +161,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { /* flags= */ 0)); } - @Ignore @Test public void queueSecureInputBuffer_multipleTimes_limitsObjectsAllocation() { enqueuer.start(); From 0f29b9e84d762878a1436a78fdcafab3859425f5 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Wed, 2 Sep 2020 16:07:52 +0100 Subject: [PATCH 20/30] Change GTS test boolean to a final variable. PiperOrigin-RevId: 329714283 --- .../playbacktests/gts/DebugRenderersFactory.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 71270d21c5..4db109b146 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -85,9 +85,9 @@ import java.util.ArrayList; private final long[] timestampsList; private final ArrayDeque inputFormatChangeTimesUs; + private final boolean shouldMediaFormatChangeTimesBeChecked; private boolean skipToPositionBeforeRenderingFirstFrame; - private boolean shouldMediaFormatChangeTimesBeChecked; private int startIndex; private int queueSize; @@ -114,6 +114,10 @@ import java.util.ArrayList; maxDroppedFrameCountToNotify); timestampsList = new long[ARRAY_SIZE]; inputFormatChangeTimesUs = new ArrayDeque<>(); + + // Output MediaFormat changes are known to occur too early until API 30 (see [internal: + // b/149818050, b/149751672]). + shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30; } @Override @@ -135,10 +139,6 @@ import java.util.ArrayList; // frames up to the current playback position [Internal: b/66494991]. skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate); - - // Output MediaFormat changes are known to occur too early until API 30 (see [internal: - // b/149818050, b/149751672]). - shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30; } @Override From 8e5336c59e1b27ec476a2717088ba82fdf6a8f54 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Sep 2020 16:42:00 +0100 Subject: [PATCH 21/30] Dev guide: Start updating the download page PiperOrigin-RevId: 329719402 --- extensions/jobdispatcher/README.md | 4 +--- .../com/google/android/exoplayer2/offline/DownloadHelper.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index 613277bad2..9e26c07c5d 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,12 +1,10 @@ # ExoPlayer Firebase JobDispatcher extension # -**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] -instead.** +**This extension is deprecated. Use the [WorkManager extension][] instead.** This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. [WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md -[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android ## Getting the extension ## diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 8cb619f2a2..5b21573eca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -77,7 +77,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

A typical usage of DownloadHelper follows these steps: * *

    - *
  1. Build the helper using one of the {@code forXXX} methods. + *
  2. Build the helper using one of the {@code forMediaItem} methods. *
  3. Prepare the helper using {@link #prepare(Callback)} and wait for the callback. *
  4. Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * #getTrackSelections(int, int)}, and make adjustments using {@link From b5a4dc8f36d2e28671e9d88feab778f63bbce1cb Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Sep 2020 16:58:32 +0100 Subject: [PATCH 22/30] Fix dispatch of play when in IDLE or ENDED PiperOrigin-RevId: 329722282 --- .../exoplayer2/ui/PlayerControlView.java | 43 +++++++++++++----- .../ui/StyledPlayerControlView.java | 44 ++++++++++++------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index cea6bcecc6..9c33fa9092 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.State; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; @@ -1211,13 +1212,13 @@ public class PlayerControlView extends FrameLayout { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + dispatchPlayPause(player); break; case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); + dispatchPlay(player); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); + dispatchPause(player); break; case KeyEvent.KEYCODE_MEDIA_NEXT: controlDispatcher.dispatchNext(player); @@ -1240,6 +1241,31 @@ public class PlayerControlView extends FrameLayout { && player.getPlayWhenReady(); } + private void dispatchPlayPause(Player player) { + @State int state = player.getPlaybackState(); + if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) { + dispatchPlay(player); + } else { + dispatchPause(player); + } + } + + private void dispatchPlay(Player player) { + @State int state = player.getPlaybackState(); + if (state == Player.STATE_IDLE) { + if (playbackPreparer != null) { + playbackPreparer.preparePlayback(); + } + } else if (state == Player.STATE_ENDED) { + seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + } + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); + } + + private void dispatchPause(Player player) { + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); + } + @SuppressLint("InlinedApi") private static boolean isHandledMediaKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD @@ -1355,16 +1381,9 @@ public class PlayerControlView extends FrameLayout { } else if (rewindButton == view) { controlDispatcher.dispatchRewind(player); } else if (playButton == view) { - if (player.getPlaybackState() == Player.STATE_IDLE) { - if (playbackPreparer != null) { - playbackPreparer.preparePlayback(); - } - } else if (player.getPlaybackState() == Player.STATE_ENDED) { - seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); - } - controlDispatcher.dispatchSetPlayWhenReady(player, true); + dispatchPlay(player); } else if (pauseButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, false); + dispatchPause(player); } else if (repeatToggleButton == view) { controlDispatcher.dispatchSetRepeatMode( player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 153f8c04ca..ab27f74579 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -45,6 +45,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.State; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroup; @@ -1649,10 +1650,10 @@ public class StyledPlayerControlView extends FrameLayout { dispatchPlayPause(player); break; case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); + dispatchPlay(player); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); + dispatchPause(player); break; case KeyEvent.KEYCODE_MEDIA_NEXT: controlDispatcher.dispatchNext(player); @@ -1675,6 +1676,31 @@ public class StyledPlayerControlView extends FrameLayout { && player.getPlayWhenReady(); } + private void dispatchPlayPause(Player player) { + @State int state = player.getPlaybackState(); + if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) { + dispatchPlay(player); + } else { + dispatchPause(player); + } + } + + private void dispatchPlay(Player player) { + @State int state = player.getPlaybackState(); + if (state == Player.STATE_IDLE) { + if (playbackPreparer != null) { + playbackPreparer.preparePlayback(); + } + } else if (state == Player.STATE_ENDED) { + seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + } + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); + } + + private void dispatchPause(Player player) { + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); + } + @SuppressLint("InlinedApi") private static boolean isHandledMediaKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD @@ -1831,20 +1857,6 @@ public class StyledPlayerControlView extends FrameLayout { } } - private void dispatchPlayPause(Player player) { - if (player.getPlaybackState() == Player.STATE_IDLE) { - if (playbackPreparer != null) { - playbackPreparer.preparePlayback(); - } - controlDispatcher.dispatchSetPlayWhenReady(player, true); - } else if (player.getPlaybackState() == Player.STATE_ENDED) { - seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); - controlDispatcher.dispatchSetPlayWhenReady(player, true); - } else { - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); - } - } - private class SettingsAdapter extends RecyclerView.Adapter { private final String[] mainTexts; private final String[] subTexts; From fe2fc8b73f2b288cc1d4245b341d660238fa8f1a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Sep 2020 17:01:01 +0100 Subject: [PATCH 23/30] Add useful DownloadRequest to MediaItem conversion method PiperOrigin-RevId: 329722775 --- .../android/exoplayer2/demo/PlayerActivity.java | 12 +----------- .../android/exoplayer2/offline/DownloadHelper.java | 9 +-------- .../android/exoplayer2/offline/DownloadRequest.java | 13 +++++++++++++ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 5c8d3cc934..370db4ac70 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -550,17 +550,7 @@ public class PlayerActivity extends AppCompatActivity @Nullable DownloadRequest downloadRequest = downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri); - if (downloadRequest != null) { - MediaItem mediaItem = - item.buildUpon() - .setStreamKeys(downloadRequest.streamKeys) - .setCustomCacheKey(downloadRequest.customCacheKey) - .setDrmKeySetId(downloadRequest.keySetId) - .build(); - mediaItems.add(mediaItem); - } else { - mediaItems.add(item); - } + mediaItems.add(downloadRequest != null ? downloadRequest.toMediaItem() : item); } return mediaItems; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 5b21573eca..df2d10ae53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -448,14 +448,7 @@ public final class DownloadHelper { DataSource.Factory dataSourceFactory, @Nullable DrmSessionManager drmSessionManager) { return createMediaSourceInternal( - new MediaItem.Builder() - .setUri(downloadRequest.uri) - .setCustomCacheKey(downloadRequest.customCacheKey) - .setMimeType(downloadRequest.mimeType) - .setStreamKeys(downloadRequest.streamKeys) - .build(), - dataSourceFactory, - drmSessionManager); + downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager); } private final MediaItem.PlaybackProperties playbackProperties; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java index 31d86349ce..1fa1655445 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java @@ -22,6 +22,7 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; @@ -220,6 +221,18 @@ public final class DownloadRequest implements Parcelable { newRequest.data); } + /** Returns a {@link MediaItem} for the content defined by the request. */ + public MediaItem toMediaItem() { + return new MediaItem.Builder() + .setMediaId(id) + .setUri(uri) + .setCustomCacheKey(customCacheKey) + .setMimeType(mimeType) + .setStreamKeys(streamKeys) + .setDrmKeySetId(keySetId) + .build(); + } + @Override public String toString() { return mimeType + ":" + id; From fd71e38db3cc06a28b7cc887f75a6eda60976ddd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Sep 2020 19:24:44 +0100 Subject: [PATCH 24/30] Fix DASH/SS/HLS module readme files PiperOrigin-RevId: 329751934 --- library/dash/README.md | 18 +++++++++++++++--- library/hls/README.md | 17 +++++++++++++++-- library/smoothstreaming/README.md | 18 ++++++++++++++++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/library/dash/README.md b/library/dash/README.md index 1076716684..2ae77c41aa 100644 --- a/library/dash/README.md +++ b/library/dash/README.md @@ -1,8 +1,20 @@ # ExoPlayer DASH library module # -Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To -play DASH content, instantiate a `DashMediaSource` and pass it to -`ExoPlayer.prepare`. +Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. + +Adding a dependency to this module is all that's required to enable playback of +DASH `MediaItem`s added to an `ExoPlayer` or `SimpleExoPlayer` in their default +configurations. Internally, `DefaultMediaSourceFactory` will automatically +detect the presence of the module and convert DASH `MediaItem`s into +`DashMediaSource` instances for playback. + +Similarly, a `DownloadManager` in its default configuration will use +`DefaultDownloaderFactory`, which will automatically detect the presence of +the module and build `DashDownloader` instances to download DASH content. + +For advanced playback use cases, applications can build `DashMediaSource` +instances and pass them directly to the player. For advanced download use cases, +`DashDownloader` can be used directly. ## Links ## diff --git a/library/hls/README.md b/library/hls/README.md index 3470c29e3c..b7eecc1ff8 100644 --- a/library/hls/README.md +++ b/library/hls/README.md @@ -1,7 +1,20 @@ # ExoPlayer HLS library module # -Provides support for HTTP Live Streaming (HLS) content. To play HLS content, -instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. +Provides support for HTTP Live Streaming (HLS) content. + +Adding a dependency to this module is all that's required to enable playback of +HLS `MediaItem`s added to an `ExoPlayer` or `SimpleExoPlayer` in their default +configurations. Internally, `DefaultMediaSourceFactory` will automatically +detect the presence of the module and convert HLS `MediaItem`s into +`HlsMediaSource` instances for playback. + +Similarly, a `DownloadManager` in its default configuration will use +`DefaultDownloaderFactory`, which will automatically detect the presence of +the module and build `HlsDownloader` instances to download HLS content. + +For advanced playback use cases, applications can build `HlsMediaSource` +instances and pass them directly to the player. For advanced download use cases, +`HlsDownloader` can be used directly. ## Links ## diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md index d53471d17c..2fab69c756 100644 --- a/library/smoothstreaming/README.md +++ b/library/smoothstreaming/README.md @@ -1,7 +1,21 @@ # ExoPlayer SmoothStreaming library module # -Provides support for Smooth Streaming content. To play Smooth Streaming content, -instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. +Provides support for SmoothStreaming content. + +Adding a dependency to this module is all that's required to enable playback of +SmoothStreaming `MediaItem`s added to an `ExoPlayer` or `SimpleExoPlayer` in +their default configurations. Internally, `DefaultMediaSourceFactory` will +automatically detect the presence of the module and convert SmoothStreaming +`MediaItem`s into `SsMediaSource` instances for playback. + +Similarly, a `DownloadManager` in its default configuration will use +`DefaultDownloaderFactory`, which will automatically detect the presence of +the module and build `SsDownloader` instances to download SmoothStreaming +content. + +For advanced playback use cases, applications can build `SsMediaSource` +instances and pass them directly to the player. For advanced download use cases, +`SsDownloader` can be used directly. ## Links ## From f68faff30bdace5f1974b18fc396ac90cb3fe5d6 Mon Sep 17 00:00:00 2001 From: insun Date: Thu, 3 Sep 2020 06:07:31 +0100 Subject: [PATCH 25/30] Set VR button disabled when listener is not registered (styled view) PiperOrigin-RevId: 329851964 --- .../google/android/exoplayer2/ui/StyledPlayerControlView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index ab27f74579..86a802323a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -635,6 +635,7 @@ public class StyledPlayerControlView extends FrameLayout { vrButton = findViewById(R.id.exo_vr); if (vrButton != null) { setShowVrButton(showVrButton); + updateButton(/* enabled= */ false, vrButton); } // Related to Settings List View From 8dcab1d20ada9d4b8e18a1d64e2b552ef0b7447a Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 3 Sep 2020 12:39:09 +0100 Subject: [PATCH 26/30] Remove fragile tests PiperOrigin-RevId: 329894431 --- ...nchronousMediaCodecBufferEnqueuerTest.java | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java index 5af10f2e12..37d31569c3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java @@ -23,7 +23,6 @@ import static org.mockito.Mockito.doAnswer; import android.media.MediaCodec; import android.media.MediaFormat; import android.os.HandlerThread; -import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; @@ -38,8 +37,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import org.robolectric.Shadows; -import org.robolectric.shadows.ShadowLooper; /** Unit tests for {@link AsynchronousMediaCodecBufferEnqueuer}. */ @RunWith(AndroidJUnit4.class) @@ -101,31 +98,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { /* flags= */ 0)); } - @Test - public void queueInputBuffer_multipleTimes_limitsObjectsAllocation() { - enqueuer.start(); - Looper looper = handlerThread.getLooper(); - ShadowLooper shadowLooper = Shadows.shadowOf(looper); - - for (int cycle = 0; cycle < 100; cycle++) { - // This test assumes that the shadow MediaCodec implementation can dequeue at least - // 10 input buffers before queueing them back. - for (int i = 0; i < 10; i++) { - int inputBufferIndex = codec.dequeueInputBuffer(0); - enqueuer.queueInputBuffer( - /* index= */ inputBufferIndex, - /* offset= */ 0, - /* size= */ 0, - /* presentationTimeUs= */ i, - /* flags= */ 0); - } - // Execute all messages, queues input buffers back to MediaCodec. - shadowLooper.idle(); - } - - assertThat(AsynchronousMediaCodecBufferEnqueuer.getInstancePoolSize()).isEqualTo(10); - } - @Test public void queueSecureInputBuffer_withPendingCryptoException_throwsCryptoException() { enqueuer.setPendingRuntimeException( @@ -161,32 +133,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { /* flags= */ 0)); } - @Test - public void queueSecureInputBuffer_multipleTimes_limitsObjectsAllocation() { - enqueuer.start(); - Looper looper = handlerThread.getLooper(); - CryptoInfo info = createCryptoInfo(); - ShadowLooper shadowLooper = Shadows.shadowOf(looper); - - for (int cycle = 0; cycle < 100; cycle++) { - // This test assumes that the shadow MediaCodec implementation can dequeue at least - // 10 input buffers before queueing them back. - int inputBufferIndex = codec.dequeueInputBuffer(0); - for (int i = 0; i < 10; i++) { - enqueuer.queueSecureInputBuffer( - /* index= */ inputBufferIndex, - /* offset= */ 0, - /* info= */ info, - /* presentationTimeUs= */ i, - /* flags= */ 0); - } - // Execute all messages, queues input buffers back to MediaCodec. - shadowLooper.idle(); - } - - assertThat(AsynchronousMediaCodecBufferEnqueuer.getInstancePoolSize()).isEqualTo(10); - } - @Test public void flush_withoutStart_works() { enqueuer.flush(); From d1631cf86f4bec3d4480d531a86faa86980daaf4 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Thu, 3 Sep 2020 19:18:01 +0100 Subject: [PATCH 27/30] Disable MediaFormat time check GTS test temporarily. PiperOrigin-RevId: 329956142 --- .../playbacktests/gts/DebugRenderersFactory.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 4db109b146..8a1edc42eb 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.nio.ByteBuffer; @@ -115,9 +114,15 @@ import java.util.ArrayList; timestampsList = new long[ARRAY_SIZE]; inputFormatChangeTimesUs = new ArrayDeque<>(); + /* // Output MediaFormat changes are known to occur too early until API 30 (see [internal: // b/149818050, b/149751672]). shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30; + */ + + // [Internal ref: b/149751672] Seeking currently causes an unexpected MediaFormat change, so + // this check is disabled until that is deemed fixed. + shouldMediaFormatChangeTimesBeChecked = false; } @Override From 45dc66ef2f69254b32c520ee7fb4f324c5374bc1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Sep 2020 20:35:35 +0100 Subject: [PATCH 28/30] Clear CodecInfos on InputFormat if codec is null In maybeInitCodecWithFallback, it caches availableCodecInfos with mediaCryptoRequiresSecureDecoder and inputFormat as inputs, and won't clear it if shouldInitCodec is false, resulting in a case where availableCodecInfos is not null and codec is null. When we have a new format, it's reasonable to clear availableCodecInfos if codec is null. Otherwise we might not be able to properly initialize a new codec. PiperOrigin-RevId: 329971796 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 761e101ce0..de3f595976 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1442,6 +1442,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (codec == null) { + if (!legacyKeepAvailableCodecInfosWithoutCodec()) { + availableCodecInfos = null; + } maybeInitCodecOrBypass(); return; } @@ -1506,6 +1509,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + /** + * Returns whether to keep available codec infos when the codec hasn't been initialized, which is + * the behavior before a bug fix. See also [Internal: b/162837741]. + */ + protected boolean legacyKeepAvailableCodecInfosWithoutCodec() { + return false; + } + /** * Called when one of the output formats changes. * From ea158dcdb05b99bcd9cfa555947cefd48d8beded Mon Sep 17 00:00:00 2001 From: insun Date: Fri, 4 Sep 2020 03:38:56 +0100 Subject: [PATCH 29/30] Apply styled widget's bug fixes to legacy widget - Prevent buffering when clicking ffwd button at the end of stream - Set VR button disabled when listener is not registered PiperOrigin-RevId: 330039336 --- .../android/exoplayer2/ui/PlayerControlView.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 9c33fa9092..65a9a5ed8f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -481,6 +481,7 @@ public class PlayerControlView extends FrameLayout { } vrButton = findViewById(R.id.exo_vr); setShowVrButton(false); + updateButton(false, false, vrButton); Resources resources = context.getResources(); @@ -794,6 +795,7 @@ public class PlayerControlView extends FrameLayout { public void setVrButtonListener(@Nullable OnClickListener onClickListener) { if (vrButton != null) { vrButton.setOnClickListener(onClickListener); + updateButton(getShowVrButton(), onClickListener != null, vrButton); } } @@ -1205,7 +1207,9 @@ public class PlayerControlView extends FrameLayout { } if (event.getAction() == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - controlDispatcher.dispatchFastForward(player); + if (player.getPlaybackState() != Player.STATE_ENDED) { + controlDispatcher.dispatchFastForward(player); + } } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { controlDispatcher.dispatchRewind(player); } else if (event.getRepeatCount() == 0) { @@ -1377,7 +1381,9 @@ public class PlayerControlView extends FrameLayout { } else if (previousButton == view) { controlDispatcher.dispatchPrevious(player); } else if (fastForwardButton == view) { - controlDispatcher.dispatchFastForward(player); + if (player.getPlaybackState() != Player.STATE_ENDED) { + controlDispatcher.dispatchFastForward(player); + } } else if (rewindButton == view) { controlDispatcher.dispatchRewind(player); } else if (playButton == view) { From 99dbb76455a21f19e0db4399101039b33a6057a0 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Mon, 7 Sep 2020 10:21:23 +0100 Subject: [PATCH 30/30] Allow Format change in GTS test for MediaFormat time check. PiperOrigin-RevId: 330348510 --- .../exoplayer2/playbacktests/gts/DebugRenderersFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 8a1edc42eb..c34390bc03 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -191,6 +191,8 @@ import java.util.ArrayList; if (mediaFormat != null && !mediaFormat.equals(currentMediaFormat)) { outputMediaFormatChanged = true; currentMediaFormat = mediaFormat; + } else { + inputFormatChangeTimesUs.remove(); } }