Commit 7e7a33a8 by Oliver Woodman

Merge commit '99dbb764' into dev-v2-r2.12.0

parents 9778898b 99dbb764
Showing with 787 additions and 404 deletions
...@@ -137,7 +137,7 @@ ...@@ -137,7 +137,7 @@
* Recreate the decoder when handling and swallowing decode errors in * Recreate the decoder when handling and swallowing decode errors in
`TextRenderer`. This fixes a case where playback would never end when `TextRenderer`. This fixes a case where playback would never end when
playing content with malformed subtitles 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 * Only apply `CaptionManager` font scaling in
`SubtitleView.setUserDefaultTextSize` if the `CaptionManager` is `SubtitleView.setUserDefaultTextSize` if the `CaptionManager` is
enabled. enabled.
...@@ -315,6 +315,8 @@ ...@@ -315,6 +315,8 @@
* Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set * Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set
companion ad slots without accessing the `AdDisplayContainer`. companion ad slots without accessing the `AdDisplayContainer`.
* Add missing notification of `VideoAdPlayerCallback.onLoaded`. * Add missing notification of `VideoAdPlayerCallback.onLoaded`.
* Fix handling of incompatible VPAID ads
([#7832](https://github.com/google/ExoPlayer/issues/7832)).
* Demo app: * Demo app:
* Replace the `extensions` variant with `decoderExtensions` and update the * Replace the `extensions` variant with `decoderExtensions` and update the
demo app use the Cronet and IMA extensions by default. demo app use the Cronet and IMA extensions by default.
......
...@@ -16,10 +16,12 @@ ...@@ -16,10 +16,12 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 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.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -170,6 +172,7 @@ public class DownloadTracker { ...@@ -170,6 +172,7 @@ public class DownloadTracker {
private TrackSelectionDialog trackSelectionDialog; private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo; private MappedTrackInfo mappedTrackInfo;
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
@Nullable private byte[] keySetId; @Nullable private byte[] keySetId;
public StartDownloadDialogHelper( public StartDownloadDialogHelper(
...@@ -185,6 +188,9 @@ public class DownloadTracker { ...@@ -185,6 +188,9 @@ public class DownloadTracker {
if (trackSelectionDialog != null) { if (trackSelectionDialog != null) {
trackSelectionDialog.dismiss(); trackSelectionDialog.dismiss();
} }
if (widevineOfflineLicenseFetchTask != null) {
widevineOfflineLicenseFetchTask.cancel(false);
}
} }
// DownloadHelper.Callback implementation. // DownloadHelper.Callback implementation.
...@@ -192,59 +198,32 @@ public class DownloadTracker { ...@@ -192,59 +198,32 @@ public class DownloadTracker {
@Override @Override
public void onPrepared(@NonNull DownloadHelper helper) { public void onPrepared(@NonNull DownloadHelper helper) {
@Nullable Format format = getFirstFormatWithDrmInitData(helper); @Nullable Format format = getFirstFormatWithDrmInitData(helper);
if (format != null) { if (format == null) {
if (Util.SDK_INT < 18) { onDownloadPrepared(helper);
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG) return;
.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) { // The content is DRM protected. We need to acquire an offline license.
Log.d(TAG, "No periods found. Downloading entire stream."); if (Util.SDK_INT < 18) {
startDownload(); Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
downloadHelper.release(); .show();
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
return; return;
} }
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); if (!hasSchemaData(format.drmInitData)) {
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
Log.d(TAG, "No dialog content. Downloading entire stream."); .show();
startDownload(); Log.e(
downloadHelper.release(); TAG,
"Downloading content where DRM scheme data is not located in the manifest is not"
+ " supported");
return; return;
} }
trackSelectionDialog = widevineOfflineLicenseFetchTask =
TrackSelectionDialog.createForMappedTrackInfoAndParameters( new WidevineOfflineLicenseFetchTask(
/* titleId= */ R.string.exo_download_description, format, mediaItem.playbackProperties.drmConfiguration.licenseUri, this, helper);
mappedTrackInfo, widevineOfflineLicenseFetchTask.execute();
trackSelectorParameters,
/* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
} }
@Override @Override
...@@ -292,6 +271,44 @@ public class DownloadTracker { ...@@ -292,6 +271,44 @@ public class DownloadTracker {
// Internal methods. // 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() { private void startDownload() {
startDownload(buildDownloadRequest()); startDownload(buildDownloadRequest());
} }
...@@ -306,15 +323,54 @@ public class DownloadTracker { ...@@ -306,15 +323,54 @@ public class DownloadTracker {
.getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title))) .getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)))
.copyWithKeySetId(keySetId); .copyWithKeySetId(keySetId);
} }
}
@RequiresApi(18) /** Downloads a Widevine offline license in a background thread. */
private void fetchOfflineLicense(Format format) throws DrmSession.DrmSessionException { @RequiresApi(18)
private final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
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 offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance( OfflineLicenseHelper.newWidevineInstance(
mediaItem.playbackProperties.drmConfiguration.licenseUri.toString(), licenseUri.toString(),
httpDataSourceFactory, httpDataSourceFactory,
new DrmSessionEventListener.EventDispatcher()); 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));
}
} }
} }
......
...@@ -271,13 +271,14 @@ public class PlayerActivity extends AppCompatActivity ...@@ -271,13 +271,14 @@ public class PlayerActivity extends AppCompatActivity
setContentView(R.layout.player_activity); setContentView(R.layout.player_activity);
} }
protected void initializePlayer() { /** @return Whether initialization was successful. */
protected boolean initializePlayer() {
if (player == null) { if (player == null) {
Intent intent = getIntent(); Intent intent = getIntent();
mediaItems = createMediaItems(intent); mediaItems = createMediaItems(intent);
if (mediaItems.isEmpty()) { if (mediaItems.isEmpty()) {
return; return false;
} }
boolean preferExtensionDecoders = boolean preferExtensionDecoders =
...@@ -297,9 +298,9 @@ public class PlayerActivity extends AppCompatActivity ...@@ -297,9 +298,9 @@ public class PlayerActivity extends AppCompatActivity
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.build(); .build();
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.addAnalyticsListener(new EventLogger(trackSelector));
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay); player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player); playerView.setPlayer(player);
playerView.setPlaybackPreparer(this); playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper = new DebugTextViewHelper(player, debugTextView);
...@@ -312,6 +313,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -312,6 +313,7 @@ public class PlayerActivity extends AppCompatActivity
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
player.prepare(); player.prepare();
updateButtonVisibility(); updateButtonVisibility();
return true;
} }
private List<MediaItem> createMediaItems(Intent intent) { private List<MediaItem> createMediaItems(Intent intent) {
...@@ -548,17 +550,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -548,17 +550,7 @@ public class PlayerActivity extends AppCompatActivity
@Nullable @Nullable
DownloadRequest downloadRequest = DownloadRequest downloadRequest =
downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri); downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri);
if (downloadRequest != null) { mediaItems.add(downloadRequest != null ? downloadRequest.toMediaItem() : item);
MediaItem mediaItem =
item.buildUpon()
.setStreamKeys(downloadRequest.streamKeys)
.setCustomCacheKey(downloadRequest.customCacheKey)
.setDrmKeySetId(downloadRequest.keySetId)
.build();
mediaItems.add(mediaItem);
} else {
mediaItems.add(item);
}
} }
return mediaItems; return mediaItems;
} }
......
...@@ -1096,6 +1096,11 @@ public final class ImaAdsLoader ...@@ -1096,6 +1096,11 @@ public final class ImaAdsLoader
if (imaAdInfo != null) { if (imaAdInfo != null) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
updateAdPlaybackState(); 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();
} }
} }
......
# ExoPlayer Firebase JobDispatcher extension # # ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] **This extension is deprecated. Use the [WorkManager extension][] instead.**
instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md [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 [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ## ## Getting the extension ##
......
...@@ -185,17 +185,20 @@ public class SessionPlayerConnectorTest { ...@@ -185,17 +185,20 @@ public class SessionPlayerConnectorTest {
} }
}; };
SimpleExoPlayer simpleExoPlayer = null; SimpleExoPlayer simpleExoPlayer = null;
SessionPlayerConnector playerConnector = null;
try { try {
simpleExoPlayer = simpleExoPlayer =
new SimpleExoPlayer.Builder(context) new SimpleExoPlayer.Builder(context)
.setLooper(Looper.myLooper()) .setLooper(Looper.myLooper())
.build(); .build();
try (SessionPlayerConnector player = playerConnector =
new SessionPlayerConnector( new SessionPlayerConnector(simpleExoPlayer, new DefaultMediaItemConverter());
simpleExoPlayer, new DefaultMediaItemConverter(), controlDispatcher)) { playerConnector.setControlDispatcher(controlDispatcher);
assertPlayerResult(player.play(), RESULT_INFO_SKIPPED); assertPlayerResult(playerConnector.play(), RESULT_INFO_SKIPPED);
}
} finally { } finally {
if (playerConnector != null) {
playerConnector.close();
}
if (simpleExoPlayer != null) { if (simpleExoPlayer != null) {
simpleExoPlayer.release(); simpleExoPlayer.release();
} }
......
...@@ -15,8 +15,10 @@ ...@@ -15,8 +15,10 @@
*/ */
package com.google.android.exoplayer2.ext.media2; package com.google.android.exoplayer2.ext.media2;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.os.Handler;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -129,7 +131,7 @@ import java.util.concurrent.Callable; ...@@ -129,7 +131,7 @@ import java.util.concurrent.Callable;
// Should be only used on the handler. // Should be only used on the handler.
private final PlayerWrapper player; private final PlayerWrapper player;
private final PlayerHandler handler; private final Handler handler;
private final Object lock; private final Object lock;
@GuardedBy("lock") @GuardedBy("lock")
...@@ -141,7 +143,7 @@ import java.util.concurrent.Callable; ...@@ -141,7 +143,7 @@ import java.util.concurrent.Callable;
// Should be only used on the handler. // Should be only used on the handler.
@Nullable private AsyncPlayerCommandResult pendingAsyncPlayerCommandResult; @Nullable private AsyncPlayerCommandResult pendingAsyncPlayerCommandResult;
public PlayerCommandQueue(PlayerWrapper player, PlayerHandler handler) { public PlayerCommandQueue(PlayerWrapper player, Handler handler) {
this.player = player; this.player = player;
this.handler = handler; this.handler = handler;
lock = new Object(); lock = new Object();
...@@ -209,7 +211,7 @@ import java.util.concurrent.Callable; ...@@ -209,7 +211,7 @@ import java.util.concurrent.Callable;
} }
processPendingCommandOnHandler(); processPendingCommandOnHandler();
}, },
handler::postOrRun); (runnable) -> postOrRun(handler, runnable));
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "adding " + playerCommand); Log.d(TAG, "adding " + playerCommand);
} }
...@@ -220,7 +222,8 @@ import java.util.concurrent.Callable; ...@@ -220,7 +222,8 @@ import java.util.concurrent.Callable;
} }
public void notifyCommandError() { public void notifyCommandError() {
handler.postOrRun( postOrRun(
handler,
() -> { () -> {
@Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult; @Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult;
if (pendingResult == null) { if (pendingResult == null) {
...@@ -243,7 +246,8 @@ import java.util.concurrent.Callable; ...@@ -243,7 +246,8 @@ import java.util.concurrent.Callable;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "notifyCommandCompleted, completedCommandCode=" + completedCommandCode); Log.d(TAG, "notifyCommandCompleted, completedCommandCode=" + completedCommandCode);
} }
handler.postOrRun( postOrRun(
handler,
() -> { () -> {
@Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult; @Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult;
if (pendingResult == null || pendingResult.commandCode != completedCommandCode) { if (pendingResult == null || pendingResult.commandCode != completedCommandCode) {
...@@ -267,7 +271,7 @@ import java.util.concurrent.Callable; ...@@ -267,7 +271,7 @@ import java.util.concurrent.Callable;
} }
private void processPendingCommand() { private void processPendingCommand() {
handler.postOrRun(this::processPendingCommandOnHandler); postOrRun(handler, this::processPendingCommandOnHandler);
} }
private void processPendingCommandOnHandler() { private void processPendingCommandOnHandler() {
......
/*
* 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;
}
}
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
*/ */
package com.google.android.exoplayer2.ext.media2; 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.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat; import androidx.core.util.ObjectsCompat;
...@@ -24,6 +27,7 @@ import androidx.media2.common.MediaMetadata; ...@@ -24,6 +27,7 @@ import androidx.media2.common.MediaMetadata;
import androidx.media2.common.SessionPlayer; import androidx.media2.common.SessionPlayer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
...@@ -100,12 +104,11 @@ import java.util.List; ...@@ -100,12 +104,11 @@ import java.util.List;
private static final int POLL_BUFFER_INTERVAL_MS = 1000; private static final int POLL_BUFFER_INTERVAL_MS = 1000;
private final Listener listener; private final Listener listener;
private final PlayerHandler handler; private final Handler handler;
private final Runnable pollBufferRunnable; private final Runnable pollBufferRunnable;
private final Player player; private final Player player;
private final MediaItemConverter mediaItemConverter; private final MediaItemConverter mediaItemConverter;
private final ControlDispatcher controlDispatcher;
private final ComponentListener componentListener; private final ComponentListener componentListener;
@Nullable private MediaMetadata playlistMetadata; @Nullable private MediaMetadata playlistMetadata;
...@@ -114,6 +117,7 @@ import java.util.List; ...@@ -114,6 +117,7 @@ import java.util.List;
private final List<androidx.media2.common.MediaItem> media2Playlist; private final List<androidx.media2.common.MediaItem> media2Playlist;
private final List<MediaItem> exoPlayerPlaylist; private final List<MediaItem> exoPlayerPlaylist;
private ControlDispatcher controlDispatcher;
private boolean prepared; private boolean prepared;
private boolean rebuffering; private boolean rebuffering;
private int currentWindowIndex; private int currentWindowIndex;
...@@ -125,18 +129,13 @@ import java.util.List; ...@@ -125,18 +129,13 @@ import java.util.List;
* @param listener A {@link Listener}. * @param listener A {@link Listener}.
* @param player The {@link Player}. * @param player The {@link Player}.
* @param mediaItemConverter The {@link MediaItemConverter}. * @param mediaItemConverter The {@link MediaItemConverter}.
* @param controlDispatcher A {@link ControlDispatcher}.
*/ */
public PlayerWrapper( public PlayerWrapper(Listener listener, Player player, MediaItemConverter mediaItemConverter) {
Listener listener,
Player player,
MediaItemConverter mediaItemConverter,
ControlDispatcher controlDispatcher) {
this.listener = listener; this.listener = listener;
this.player = player; this.player = player;
this.mediaItemConverter = mediaItemConverter; this.mediaItemConverter = mediaItemConverter;
this.controlDispatcher = controlDispatcher;
controlDispatcher = new DefaultControlDispatcher();
componentListener = new ComponentListener(); componentListener = new ComponentListener();
player.addListener(componentListener); player.addListener(componentListener);
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent(); @Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
...@@ -144,7 +143,7 @@ import java.util.List; ...@@ -144,7 +143,7 @@ import java.util.List;
audioComponent.addAudioListener(componentListener); audioComponent.addAudioListener(componentListener);
} }
handler = new PlayerHandler(player.getApplicationLooper()); handler = new Handler(player.getApplicationLooper());
pollBufferRunnable = new PollBufferRunnable(); pollBufferRunnable = new PollBufferRunnable();
media2Playlist = new ArrayList<>(); media2Playlist = new ArrayList<>();
...@@ -157,6 +156,10 @@ import java.util.List; ...@@ -157,6 +156,10 @@ import java.util.List;
updatePlaylist(player.getCurrentTimeline()); updatePlaylist(player.getCurrentTimeline());
} }
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
this.controlDispatcher = controlDispatcher;
}
public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) { public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null); return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null);
} }
...@@ -436,7 +439,7 @@ import java.util.List; ...@@ -436,7 +439,7 @@ import java.util.List;
private void handlePlayerStateChanged(@Player.State int state) { private void handlePlayerStateChanged(@Player.State int state) {
if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) {
handler.postOrRun(pollBufferRunnable); postOrRun(handler, pollBufferRunnable);
} else { } else {
handler.removeCallbacks(pollBufferRunnable); handler.removeCallbacks(pollBufferRunnable);
} }
......
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
*/ */
package com.google.android.exoplayer2.ext.media2; 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.FloatRange;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
...@@ -64,7 +67,7 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -64,7 +67,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
private static final int END_OF_PLAYLIST = -1; private static final int END_OF_PLAYLIST = -1;
private final Object stateLock = new Object(); private final Object stateLock = new Object();
private final PlayerHandler taskHandler; private final Handler taskHandler;
private final Executor taskHandlerExecutor; private final Executor taskHandlerExecutor;
private final PlayerWrapper player; private final PlayerWrapper player;
private final PlayerCommandQueue playerCommandQueue; private final PlayerCommandQueue playerCommandQueue;
...@@ -89,7 +92,7 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -89,7 +92,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
* @param player The player to wrap. * @param player The player to wrap.
*/ */
public SessionPlayerConnector(Player player) { public SessionPlayerConnector(Player player) {
this(player, new DefaultMediaItemConverter(), new DefaultControlDispatcher()); this(player, new DefaultMediaItemConverter());
} }
/** /**
...@@ -97,22 +100,28 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -97,22 +100,28 @@ public final class SessionPlayerConnector extends SessionPlayer {
* *
* @param player The player to wrap. * @param player The player to wrap.
* @param mediaItemConverter The {@link MediaItemConverter}. * @param mediaItemConverter The {@link MediaItemConverter}.
* @param controlDispatcher The {@link ControlDispatcher}.
*/ */
public SessionPlayerConnector( public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) {
Player player, MediaItemConverter mediaItemConverter, ControlDispatcher controlDispatcher) {
Assertions.checkNotNull(player); Assertions.checkNotNull(player);
Assertions.checkNotNull(mediaItemConverter); Assertions.checkNotNull(mediaItemConverter);
Assertions.checkNotNull(controlDispatcher);
state = PLAYER_STATE_IDLE; state = PLAYER_STATE_IDLE;
taskHandler = new PlayerHandler(player.getApplicationLooper()); taskHandler = new Handler(player.getApplicationLooper());
taskHandlerExecutor = taskHandler::postOrRun; 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); playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
} }
/**
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
player.setControlDispatcher(controlDispatcher);
}
@Override @Override
public ListenableFuture<PlayerResult> play() { public ListenableFuture<PlayerResult> play() {
return playerCommandQueue.addCommand( return playerCommandQueue.addCommand(
...@@ -598,7 +607,8 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -598,7 +607,8 @@ public final class SessionPlayerConnector extends SessionPlayer {
private <T> T runPlayerCallableBlocking(Callable<T> callable) { private <T> T runPlayerCallableBlocking(Callable<T> callable) {
SettableFuture<T> future = SettableFuture.create(); SettableFuture<T> future = SettableFuture.create();
boolean success = boolean success =
taskHandler.postOrRun( postOrRun(
taskHandler,
() -> { () -> {
try { try {
future.set(callable.call()); future.set(callable.call());
......
...@@ -170,15 +170,16 @@ public final class MimeTypes { ...@@ -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 * Returns true if it is known that all samples in a stream of the given MIME type and codec are
* to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on every * guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on
* sample). * every sample).
* *
* @param mimeType A MIME type. * @param mimeType The MIME type of the stream.
* @return True if it is known that all samples in a stream of the given MIME type are guaranteed * @param codec The RFC 6381 codec string of the stream, or {@code null} if unknown.
* to be sync samples. False otherwise, including if {@code null} is passed. * @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) { if (mimeType == null) {
return false; return false;
} }
...@@ -198,6 +199,20 @@ public final class MimeTypes { ...@@ -198,6 +199,20 @@ public final class MimeTypes {
case AUDIO_E_AC3: case AUDIO_E_AC3:
case AUDIO_E_AC3_JOC: case AUDIO_E_AC3_JOC:
return true; 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: default:
return false; return false;
} }
......
...@@ -494,6 +494,24 @@ public final class Util { ...@@ -494,6 +494,24 @@ public final class Util {
} }
/** /**
* 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 * 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}. * application's main thread if the current thread doesn't have a {@link Looper}.
*/ */
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.util; 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 static com.google.common.truth.Truth.assertThat;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -171,4 +173,17 @@ public final class MimeTypesTest { ...@@ -171,4 +173,17 @@ public final class MimeTypesTest {
assertThat(objectType.objectTypeIndication).isEqualTo(expectedObjectTypeIndicator); assertThat(objectType.objectTypeIndication).isEqualTo(expectedObjectTypeIndicator);
assertThat(objectType.audioObjectTypeIndication).isEqualTo(expectedAudioObjectTypeIndicator); 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();
}
} }
...@@ -374,8 +374,6 @@ public interface Player { ...@@ -374,8 +374,6 @@ public interface Player {
} }
/** The device component of a {@link Player}. */ /** The device component of a {@link Player}. */
// Note: It's mostly from the androidx.media.VolumeProviderCompat and
// androidx.media.MediaControllerCompat.PlaybackInfo.
interface DeviceComponent { interface DeviceComponent {
/** Adds a listener to receive device events. */ /** Adds a listener to receive device events. */
......
...@@ -48,62 +48,74 @@ import com.google.android.exoplayer2.util.Util; ...@@ -48,62 +48,74 @@ import com.google.android.exoplayer2.util.Util;
* <h3 id="single-file">Single media file or on-demand stream</h3> * <h3 id="single-file">Single media file or on-demand stream</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-single-file.svg" alt="Example timeline for a * <p style="align:center"><img src="doc-files/timeline-single-file.svg" alt="Example timeline for a
* single file"> A timeline for a single media file or on-demand stream consists of a single period * single file">
* 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 * <p>A timeline for a single media file or on-demand stream consists of a single period and window.
* (indicated by the black dot in the figure above). * 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).
* *
* <h3>Playlist of media files or on-demand streams</h3> * <h3>Playlist of media files or on-demand streams</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-playlist.svg" alt="Example timeline for a * <p style="align:center"><img src="doc-files/timeline-playlist.svg" alt="Example timeline for a
* playlist of files"> A timeline for a playlist of media files or on-demand streams consists of * playlist of files">
* 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 * <p>A timeline for a playlist of media files or on-demand streams consists of multiple periods,
* periods and windows (e.g. their durations and whether the window is seekable) will often only * each with its own window. Each window spans the whole of the corresponding period, and typically
* become known when the player starts buffering the corresponding file or stream. * 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.
* *
* <h3 id="live-limited">Live stream with limited availability</h3> * <h3 id="live-limited">Live stream with limited availability</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-live-limited.svg" alt="Example timeline for * <p style="align:center"><img src="doc-files/timeline-live-limited.svg" alt="Example timeline for
* a live stream with limited availability"> A timeline for a live stream consists of a period whose * a live stream with limited availability">
* 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 * <p>A timeline for a live stream consists of a period whose duration is unknown, since it's
* position, defining the region of content that can still be played. The window will have {@link * continually extending as more content is broadcast. If content only remains available for a
* Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to * limited period of time then the window may start at a non-zero position, defining the region of
* true as long as we expect changes to the live window. Its default position is typically near to * content that can still be played. The window will have {@link Window#isLive} set to true to
* the live edge (indicated by the black dot in the figure above). * 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).
* *
* <h3>Live stream with indefinite availability</h3> * <h3>Live stream with indefinite availability</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline * <p style="align:center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline
* for a live stream with indefinite availability"> A timeline for a live stream with indefinite * for a live stream with indefinite availability">
* availability is similar to the <a href="#live-limited">Live stream with limited availability</a> *
* case, except that the window starts at the beginning of the period to indicate that all of the * <p>A timeline for a live stream with indefinite availability is similar to the <a
* previously broadcast content can still be played. * href="#live-limited">Live stream with limited availability</a> 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.
* *
* <h3 id="live-multi-period">Live stream with multiple periods</h3> * <h3 id="live-multi-period">Live stream with multiple periods</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline * <p style="align:center"><img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline
* for a live stream with multiple periods"> This case arises when a live stream is explicitly * for a live stream with multiple periods">
* divided into separate periods, for example at content boundaries. This case is similar to the <a *
* href="#live-limited">Live stream with limited availability</a> case, except that the window may * <p>This case arises when a live stream is explicitly divided into separate periods, for example
* span more than one period. Multiple periods are also possible in the indefinite availability * at content boundaries. This case is similar to the <a href="#live-limited">Live stream with
* case. * limited availability</a> case, except that the window may span more than one period. Multiple
* periods are also possible in the indefinite availability case.
* *
* <h3>On-demand stream followed by live stream</h3> * <h3>On-demand stream followed by live stream</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-advanced.svg" alt="Example timeline for an * <p style="align:center"><img src="doc-files/timeline-advanced.svg" alt="Example timeline for an
* on-demand stream followed by a live stream"> This case is the concatenation of the <a * on-demand stream followed by a live stream">
* href="#single-file">Single media file or on-demand stream</a> and <a href="#multi-period">Live *
* stream with multiple periods</a> cases. When playback of the on-demand stream ends, playback of * <p>This case is the concatenation of the <a href="#single-file">Single media file or on-demand
* the live stream will start from its default position near the live edge. * stream</a> and <a href="#multi-period">Live stream with multiple periods</a> cases. When playback
* of the on-demand stream ends, playback of the live stream will start from its default position
* near the live edge.
* *
* <h3 id="single-file-midrolls">On-demand stream with mid-roll ads</h3> * <h3 id="single-file-midrolls">On-demand stream with mid-roll ads</h3>
* *
* <p style="align:center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="Example * <p style="align:center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="Example
* timeline for an on-demand stream with mid-roll ad groups"> This case includes mid-roll ad groups, * timeline for an on-demand stream with 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. * <p>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 { public abstract class Timeline {
......
...@@ -15,8 +15,9 @@ ...@@ -15,8 +15,9 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
...@@ -207,15 +208,6 @@ public interface DrmSessionEventListener { ...@@ -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 { private static final class ListenerAndHandler {
public Handler handler; public Handler handler;
......
...@@ -1442,6 +1442,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1442,6 +1442,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
if (codec == null) { if (codec == null) {
if (!legacyKeepAvailableCodecInfosWithoutCodec()) {
availableCodecInfos = null;
}
maybeInitCodecOrBypass(); maybeInitCodecOrBypass();
return; return;
} }
...@@ -1507,6 +1510,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1507,6 +1510,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. * Called when one of the output formats changes.
* *
* <p>The default implementation is a no-op. * <p>The default implementation is a no-op.
......
...@@ -77,7 +77,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -77,7 +77,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <p>A typical usage of DownloadHelper follows these steps: * <p>A typical usage of DownloadHelper follows these steps:
* *
* <ol> * <ol>
* <li>Build the helper using one of the {@code forXXX} methods. * <li>Build the helper using one of the {@code forMediaItem} methods.
* <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback. * <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
* <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
* #getTrackSelections(int, int)}, and make adjustments using {@link * #getTrackSelections(int, int)}, and make adjustments using {@link
...@@ -448,14 +448,7 @@ public final class DownloadHelper { ...@@ -448,14 +448,7 @@ public final class DownloadHelper {
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
@Nullable DrmSessionManager drmSessionManager) { @Nullable DrmSessionManager drmSessionManager) {
return createMediaSourceInternal( return createMediaSourceInternal(
new MediaItem.Builder() downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager);
.setUri(downloadRequest.uri)
.setCustomCacheKey(downloadRequest.customCacheKey)
.setMimeType(downloadRequest.mimeType)
.setStreamKeys(downloadRequest.streamKeys)
.build(),
dataSourceFactory,
drmSessionManager);
} }
private final MediaItem.PlaybackProperties playbackProperties; private final MediaItem.PlaybackProperties playbackProperties;
......
...@@ -22,6 +22,7 @@ import android.os.Parcel; ...@@ -22,6 +22,7 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; 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.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
...@@ -220,6 +221,18 @@ public final class DownloadRequest implements Parcelable { ...@@ -220,6 +221,18 @@ public final class DownloadRequest implements Parcelable {
newRequest.data); 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 @Override
public String toString() { public String toString() {
return mimeType + ":" + id; return mimeType + ":" + id;
......
...@@ -264,7 +264,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -264,7 +264,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
for (TrackSelection trackSelection : selections) { for (TrackSelection trackSelection : selections) {
if (trackSelection != null) { if (trackSelection != null) {
Format selectedFormat = trackSelection.getSelectedFormat(); Format selectedFormat = trackSelection.getSelectedFormat();
if (!MimeTypes.allSamplesAreSyncSamples(selectedFormat.sampleMimeType)) { if (!MimeTypes.allSamplesAreSyncSamples(
selectedFormat.sampleMimeType, selectedFormat.codecs)) {
return true; return true;
} }
} }
......
...@@ -15,8 +15,9 @@ ...@@ -15,8 +15,9 @@
*/ */
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -471,14 +472,6 @@ public interface MediaSourceEventListener { ...@@ -471,14 +472,6 @@ public interface MediaSourceEventListener {
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; 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 { private static final class ListenerAndHandler {
public Handler handler; public Handler handler;
......
...@@ -215,6 +215,22 @@ public class SampleQueue implements TrackOutput { ...@@ -215,6 +215,22 @@ public class SampleQueue implements TrackOutput {
sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex)); 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. // Called by the consuming thread.
/** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */ /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */
...@@ -279,6 +295,16 @@ public class SampleQueue implements TrackOutput { ...@@ -279,6 +295,16 @@ public class SampleQueue implements TrackOutput {
} }
/** /**
* 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 * 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 * {@code false} means that the last sample had not been queued or that it's unknown whether the
* last sample has been queued. * last sample has been queued.
...@@ -659,7 +685,7 @@ public class SampleQueue implements TrackOutput { ...@@ -659,7 +685,7 @@ public class SampleQueue implements TrackOutput {
upstreamFormat = format; upstreamFormat = format;
} }
upstreamAllSamplesAreSyncSamples = upstreamAllSamplesAreSyncSamples =
MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType); MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType, upstreamFormat.codecs);
loggedUnexpectedNonSyncSample = false; loggedUnexpectedNonSyncSample = false;
return true; return true;
} }
...@@ -777,20 +803,10 @@ public class SampleQueue implements TrackOutput { ...@@ -777,20 +803,10 @@ public class SampleQueue implements TrackOutput {
if (length == 0) { if (length == 0) {
return timeUs > largestDiscardedTimestampUs; return timeUs > largestDiscardedTimestampUs;
} }
long largestReadTimestampUs = if (getLargestReadTimestampUs() >= timeUs) {
max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition));
if (largestReadTimestampUs >= timeUs) {
return false; return false;
} }
int retainCount = length; int retainCount = countUnreadSamplesBefore(timeUs);
int relativeSampleIndex = getRelativeIndex(length - 1);
while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
retainCount--;
relativeSampleIndex--;
if (relativeSampleIndex == -1) {
relativeSampleIndex = capacity - 1;
}
}
discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount); discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount);
return true; return true;
} }
...@@ -888,6 +904,26 @@ public class SampleQueue implements TrackOutput { ...@@ -888,6 +904,26 @@ public class SampleQueue implements TrackOutput {
} }
/** /**
* 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. * Discards the specified number of samples.
* *
* @param discardCount The number of samples to discard. * @param discardCount The number of samples to discard.
......
...@@ -319,12 +319,6 @@ public class EventLogger implements AnalyticsListener { ...@@ -319,12 +319,6 @@ public class EventLogger implements AnalyticsListener {
} }
@Override @Override
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs;
logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs);
}
@Override
public void onAudioUnderrun( public void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
loge( loge(
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2; ...@@ -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.END_OF_STREAM_ITEM;
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; 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.runUntilPlaybackState;
import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilTimelineChanged; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilTimelineChanged;
import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil;
...@@ -114,9 +115,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -114,9 +115,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mockito; import org.mockito.Mockito;
...@@ -439,49 +442,41 @@ public final class ExoPlayerTest { ...@@ -439,49 +442,41 @@ public final class ExoPlayerTest {
public void repeatModeChanges() throws Exception { public void repeatModeChanges() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3); Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ActionSchedule actionSchedule = SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(renderer).build();
new ActionSchedule.Builder(TAG) AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class);
.pause() player.addAnalyticsListener(mockAnalyticsListener);
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
.playUntilStartOfWindow(/* windowIndex= */ 1) player.prepare();
.setRepeatMode(Player.REPEAT_MODE_ONE) runUntilTimelineChanged(player);
.playUntilStartOfWindow(/* windowIndex= */ 1) playUntilStartOfWindow(player, /* windowIndex= */ 1);
.setRepeatMode(Player.REPEAT_MODE_OFF) player.setRepeatMode(Player.REPEAT_MODE_ONE);
.playUntilStartOfWindow(/* windowIndex= */ 2) playUntilStartOfWindow(player, /* windowIndex= */ 1);
.setRepeatMode(Player.REPEAT_MODE_ONE) player.setRepeatMode(Player.REPEAT_MODE_OFF);
.playUntilStartOfWindow(/* windowIndex= */ 2) playUntilStartOfWindow(player, /* windowIndex= */ 2);
.setRepeatMode(Player.REPEAT_MODE_ALL) player.setRepeatMode(Player.REPEAT_MODE_ONE);
.playUntilStartOfWindow(/* windowIndex= */ 0) playUntilStartOfWindow(player, /* windowIndex= */ 2);
.setRepeatMode(Player.REPEAT_MODE_ONE) player.setRepeatMode(Player.REPEAT_MODE_ALL);
.playUntilStartOfWindow(/* windowIndex= */ 0) playUntilStartOfWindow(player, /* windowIndex= */ 0);
.playUntilStartOfWindow(/* windowIndex= */ 0) player.setRepeatMode(Player.REPEAT_MODE_ONE);
.setRepeatMode(Player.REPEAT_MODE_OFF) playUntilStartOfWindow(player, /* windowIndex= */ 0);
.play() playUntilStartOfWindow(player, /* windowIndex= */ 0);
.build(); player.setRepeatMode(Player.REPEAT_MODE_OFF);
ExoPlayerTestRunner testRunner = playUntilStartOfWindow(player, /* windowIndex= */ 1);
new ExoPlayerTestRunner.Builder(context) playUntilStartOfWindow(player, /* windowIndex= */ 2);
.setTimeline(timeline) player.play();
.setRenderers(renderer) runUntilPlaybackState(player, Player.STATE_ENDED);
.setActionSchedule(actionSchedule)
.build() ArgumentCaptor<AnalyticsListener.EventTime> eventTimes =
.start() ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
.blockUntilEnded(TIMEOUT_MS); verify(mockAnalyticsListener, times(10))
testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); .onMediaItemTransition(eventTimes.capture(), any(), anyInt());
testRunner.assertPositionDiscontinuityReasonsEqual( assertThat(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, eventTimes.getAllValues().stream()
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, .map(eventTime -> eventTime.currentWindowIndex)
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, .collect(Collectors.toList()))
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, .containsExactly(0, 1, 1, 2, 2, 0, 0, 0, 1, 2)
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, .inOrder();
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);
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
} }
......
...@@ -21,8 +21,8 @@ import static org.junit.Assert.assertThrows; ...@@ -21,8 +21,8 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
...@@ -31,15 +31,12 @@ import java.io.IOException; ...@@ -31,15 +31,12 @@ import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowLooper;
/** Unit tests for {@link AsynchronousMediaCodecBufferEnqueuer}. */ /** Unit tests for {@link AsynchronousMediaCodecBufferEnqueuer}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
...@@ -54,6 +51,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { ...@@ -54,6 +51,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
@Before @Before
public void setUp() throws IOException { public void setUp() throws IOException {
codec = MediaCodec.createByCodecName("h264"); codec = MediaCodec.createByCodecName("h264");
codec.configure(new MediaFormat(), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
codec.start();
handlerThread = new TestHandlerThread("TestHandlerThread"); handlerThread = new TestHandlerThread("TestHandlerThread");
enqueuer = enqueuer =
new AsynchronousMediaCodecBufferEnqueuer(codec, handlerThread, mockConditionVariable); new AsynchronousMediaCodecBufferEnqueuer(codec, handlerThread, mockConditionVariable);
...@@ -62,7 +61,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { ...@@ -62,7 +61,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
@After @After
public void tearDown() { public void tearDown() {
enqueuer.shutdown(); enqueuer.shutdown();
codec.stop();
codec.release();
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
} }
...@@ -98,32 +98,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { ...@@ -98,32 +98,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
/* flags= */ 0)); /* flags= */ 0));
} }
@Ignore
@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 @Test
public void queueSecureInputBuffer_withPendingCryptoException_throwsCryptoException() { public void queueSecureInputBuffer_withPendingCryptoException_throwsCryptoException() {
enqueuer.setPendingRuntimeException( enqueuer.setPendingRuntimeException(
...@@ -159,33 +133,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest { ...@@ -159,33 +133,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
/* flags= */ 0)); /* flags= */ 0));
} }
@Ignore
@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 @Test
public void flush_withoutStart_works() { public void flush_withoutStart_works() {
enqueuer.flush(); enqueuer.flush();
......
...@@ -879,6 +879,118 @@ public final class SampleQueueTest { ...@@ -879,6 +879,118 @@ public final class SampleQueueTest {
} }
@Test @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() { public void discardUpstream() {
writeTestData(); writeTestData();
sampleQueue.discardUpstreamSamples(8); sampleQueue.discardUpstreamSamples(8);
...@@ -987,6 +1099,43 @@ public final class SampleQueueTest { ...@@ -987,6 +1099,43 @@ public final class SampleQueueTest {
} }
@Test @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() { public void setSampleOffsetBeforeData() {
long sampleOffsetUs = 1000; long sampleOffsetUs = 1000;
sampleQueue.setSampleOffsetUs(sampleOffsetUs); sampleQueue.setSampleOffsetUs(sampleOffsetUs);
......
# ExoPlayer DASH library module # # ExoPlayer DASH library module #
Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content.
play DASH content, instantiate a `DashMediaSource` and pass it to
`ExoPlayer.prepare`. 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 ## ## Links ##
......
# ExoPlayer HLS library module # # ExoPlayer HLS library module #
Provides support for HTTP Live Streaming (HLS) content. To play HLS content, Provides support for HTTP Live Streaming (HLS) content.
instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`.
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 ## ## Links ##
......
# ExoPlayer SmoothStreaming library module # # ExoPlayer SmoothStreaming library module #
Provides support for Smooth Streaming content. To play Smooth Streaming content, Provides support for SmoothStreaming content.
instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`.
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 ## ## Links ##
......
...@@ -38,6 +38,7 @@ import com.google.android.exoplayer2.DefaultControlDispatcher; ...@@ -38,6 +38,7 @@ import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.State;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.RepeatModeUtil;
...@@ -480,6 +481,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -480,6 +481,7 @@ public class PlayerControlView extends FrameLayout {
} }
vrButton = findViewById(R.id.exo_vr); vrButton = findViewById(R.id.exo_vr);
setShowVrButton(false); setShowVrButton(false);
updateButton(false, false, vrButton);
Resources resources = context.getResources(); Resources resources = context.getResources();
...@@ -793,6 +795,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -793,6 +795,7 @@ public class PlayerControlView extends FrameLayout {
public void setVrButtonListener(@Nullable OnClickListener onClickListener) { public void setVrButtonListener(@Nullable OnClickListener onClickListener) {
if (vrButton != null) { if (vrButton != null) {
vrButton.setOnClickListener(onClickListener); vrButton.setOnClickListener(onClickListener);
updateButton(getShowVrButton(), onClickListener != null, vrButton);
} }
} }
...@@ -1204,19 +1207,22 @@ public class PlayerControlView extends FrameLayout { ...@@ -1204,19 +1207,22 @@ public class PlayerControlView extends FrameLayout {
} }
if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { 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) { } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
controlDispatcher.dispatchRewind(player); controlDispatcher.dispatchRewind(player);
} else if (event.getRepeatCount() == 0) { } else if (event.getRepeatCount() == 0) {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); case KeyEvent.KEYCODE_HEADSETHOOK:
dispatchPlayPause(player);
break; break;
case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true); dispatchPlay(player);
break; break;
case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false); dispatchPause(player);
break; break;
case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_NEXT:
controlDispatcher.dispatchNext(player); controlDispatcher.dispatchNext(player);
...@@ -1239,11 +1245,37 @@ public class PlayerControlView extends FrameLayout { ...@@ -1239,11 +1245,37 @@ public class PlayerControlView extends FrameLayout {
&& player.getPlayWhenReady(); && 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") @SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) { private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_HEADSETHOOK
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_NEXT || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
...@@ -1349,20 +1381,15 @@ public class PlayerControlView extends FrameLayout { ...@@ -1349,20 +1381,15 @@ public class PlayerControlView extends FrameLayout {
} else if (previousButton == view) { } else if (previousButton == view) {
controlDispatcher.dispatchPrevious(player); controlDispatcher.dispatchPrevious(player);
} else if (fastForwardButton == view) { } else if (fastForwardButton == view) {
controlDispatcher.dispatchFastForward(player); if (player.getPlaybackState() != Player.STATE_ENDED) {
controlDispatcher.dispatchFastForward(player);
}
} else if (rewindButton == view) { } else if (rewindButton == view) {
controlDispatcher.dispatchRewind(player); controlDispatcher.dispatchRewind(player);
} else if (playButton == view) { } else if (playButton == view) {
if (player.getPlaybackState() == Player.STATE_IDLE) { dispatchPlay(player);
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, true);
} else if (pauseButton == view) { } else if (pauseButton == view) {
controlDispatcher.dispatchSetPlayWhenReady(player, false); dispatchPause(player);
} else if (repeatToggleButton == view) { } else if (repeatToggleButton == view) {
controlDispatcher.dispatchSetRepeatMode( controlDispatcher.dispatchSetRepeatMode(
player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
...@@ -877,6 +878,10 @@ public class PlayerNotificationManager { ...@@ -877,6 +878,10 @@ public class PlayerNotificationManager {
* *
* <p>See {@link NotificationCompat.Builder#setPriority(int)}. * <p>See {@link NotificationCompat.Builder#setPriority(int)}.
* *
* <p>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}, * @param priority The priority which can be one of {@link NotificationCompat#PRIORITY_DEFAULT},
* {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link * {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link
* NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set * NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set
......
...@@ -45,6 +45,7 @@ import com.google.android.exoplayer2.Format; ...@@ -45,6 +45,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.State;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
...@@ -396,7 +397,7 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -396,7 +397,7 @@ public class StyledPlayerControlView extends FrameLayout {
private final String fullScreenEnterContentDescription; private final String fullScreenEnterContentDescription;
@Nullable private Player player; @Nullable private Player player;
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; private ControlDispatcher controlDispatcher;
@Nullable private ProgressUpdateListener progressUpdateListener; @Nullable private ProgressUpdateListener progressUpdateListener;
@Nullable private PlaybackPreparer playbackPreparer; @Nullable private PlaybackPreparer playbackPreparer;
...@@ -537,8 +538,7 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -537,8 +538,7 @@ public class StyledPlayerControlView extends FrameLayout {
extraAdGroupTimesMs = new long[0]; extraAdGroupTimesMs = new long[0];
extraPlayedAdGroups = new boolean[0]; extraPlayedAdGroups = new boolean[0];
componentListener = new ComponentListener(); componentListener = new ComponentListener();
controlDispatcher = controlDispatcher = new DefaultControlDispatcher(fastForwardMs, rewindMs);
new com.google.android.exoplayer2.DefaultControlDispatcher(fastForwardMs, rewindMs);
updateProgressAction = this::updateProgress; updateProgressAction = this::updateProgress;
LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this); LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
...@@ -635,6 +635,7 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -635,6 +635,7 @@ public class StyledPlayerControlView extends FrameLayout {
vrButton = findViewById(R.id.exo_vr); vrButton = findViewById(R.id.exo_vr);
if (vrButton != null) { if (vrButton != null) {
setShowVrButton(showVrButton); setShowVrButton(showVrButton);
updateButton(/* enabled= */ false, vrButton);
} }
// Related to Settings List View // Related to Settings List View
...@@ -839,9 +840,9 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -839,9 +840,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) { public void setControlDispatcher(ControlDispatcher controlDispatcher) {
if (this.controlDispatcher != controlDispatcher) { if (this.controlDispatcher != controlDispatcher) {
...@@ -1638,19 +1639,22 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -1638,19 +1639,22 @@ public class StyledPlayerControlView extends FrameLayout {
} }
if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { 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) { } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
controlDispatcher.dispatchRewind(player); controlDispatcher.dispatchRewind(player);
} else if (event.getRepeatCount() == 0) { } else if (event.getRepeatCount() == 0) {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); case KeyEvent.KEYCODE_HEADSETHOOK:
dispatchPlayPause(player);
break; break;
case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true); dispatchPlay(player);
break; break;
case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false); dispatchPause(player);
break; break;
case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_NEXT:
controlDispatcher.dispatchNext(player); controlDispatcher.dispatchNext(player);
...@@ -1673,11 +1677,37 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -1673,11 +1677,37 @@ public class StyledPlayerControlView extends FrameLayout {
&& player.getPlayWhenReady(); && 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") @SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) { private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_HEADSETHOOK
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_NEXT || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
...@@ -1806,18 +1836,13 @@ public class StyledPlayerControlView extends FrameLayout { ...@@ -1806,18 +1836,13 @@ public class StyledPlayerControlView extends FrameLayout {
} else if (previousButton == view) { } else if (previousButton == view) {
controlDispatcher.dispatchPrevious(player); controlDispatcher.dispatchPrevious(player);
} else if (fastForwardButton == view) { } else if (fastForwardButton == view) {
controlDispatcher.dispatchFastForward(player); if (player.getPlaybackState() != Player.STATE_ENDED) {
controlDispatcher.dispatchFastForward(player);
}
} else if (rewindButton == view) { } else if (rewindButton == view) {
controlDispatcher.dispatchRewind(player); controlDispatcher.dispatchRewind(player);
} else if (playPauseButton == view) { } else if (playPauseButton == view) {
if (player.getPlaybackState() == Player.STATE_IDLE) { dispatchPlayPause(player);
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
} else if (repeatToggleButton == view) { } else if (repeatToggleButton == view) {
controlDispatcher.dispatchSetRepeatMode( controlDispatcher.dispatchSetRepeatMode(
player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));
......
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
android:minHeight="@dimen/exo_settings_height" android:minHeight="@dimen/exo_settings_height"
android:layout_marginLeft="2dp" android:layout_marginLeft="2dp"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:paddingEnd="4dp"
android:paddingRight="4dp"
android:gravity="center|start" android:gravity="center|start"
android:orientation="vertical"> android:orientation="vertical">
......
...@@ -36,8 +36,8 @@ ...@@ -36,8 +36,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/exo_settings_height" android:minHeight="@dimen/exo_settings_height"
android:paddingStart="2dp" android:layout_marginEnd="4dp"
android:paddingLeft="2dp" android:layout_marginRight="4dp"
android:gravity="center|start" android:gravity="center|start"
android:textColor="@color/exo_white" android:textColor="@color/exo_white"
android:textSize="@dimen/exo_settings_main_text_size"/> android:textSize="@dimen/exo_settings_main_text_size"/>
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
// limitations under the License. // limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
defaultConfig {
multiDexEnabled true
}
}
dependencies { dependencies {
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
......
...@@ -17,11 +17,12 @@ package com.google.android.exoplayer2.playbacktests.gts; ...@@ -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.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; 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.media.MediaDrm.MediaDrmStateException;
import android.net.Uri; import android.net.Uri;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -121,19 +122,19 @@ public final class DashWidevineOfflineTest { ...@@ -121,19 +122,19 @@ public final class DashWidevineOfflineTest {
downloadLicense(); downloadLicense();
releaseLicense(); // keySetId no longer valid. releaseLicense(); // keySetId no longer valid.
Throwable error = try {
assertThrows( testRunner.run();
"Playback should fail because the license has been released.", fail("Playback should fail because the license has been released.");
Throwable.class, } catch (RuntimeException expected) {
() -> testRunner.run()); // Get the root cause
Throwable error = expected;
// Get the root cause @Nullable Throwable cause = error.getCause();
Throwable cause = error.getCause(); while (cause != null && cause != error) {
while (cause != null && cause != error) { error = cause;
error = cause; cause = error.getCause();
cause = error.getCause(); }
assertThat(error).isInstanceOf(MediaDrmStateException.class);
} }
assertThat(error).isInstanceOf(MediaDrmStateException.class);
} }
@Test @Test
...@@ -144,18 +145,19 @@ public final class DashWidevineOfflineTest { ...@@ -144,18 +145,19 @@ public final class DashWidevineOfflineTest {
downloadLicense(); downloadLicense();
releaseLicense(); // keySetId no longer valid. releaseLicense(); // keySetId no longer valid.
Throwable error = try {
assertThrows( testRunner.run();
"Playback should fail because the license has been released.", fail("Playback should fail because the license has been released.");
Throwable.class, } catch (RuntimeException expected) {
() -> testRunner.run()); // Get the root cause
// Get the root cause Throwable error = expected;
Throwable cause = error.getCause(); @Nullable Throwable cause = error.getCause();
while (cause != null && cause != error) { while (cause != null && cause != error) {
error = cause; error = cause;
cause = error.getCause(); cause = error.getCause();
}
assertThat(error).isInstanceOf(IllegalArgumentException.class);
} }
assertThat(error).isInstanceOf(IllegalArgumentException.class);
} }
@Test @Test
......
...@@ -34,7 +34,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; ...@@ -34,7 +34,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; 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.MediaCodecVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -85,9 +84,9 @@ import java.util.ArrayList; ...@@ -85,9 +84,9 @@ import java.util.ArrayList;
private final long[] timestampsList; private final long[] timestampsList;
private final ArrayDeque<Long> inputFormatChangeTimesUs; private final ArrayDeque<Long> inputFormatChangeTimesUs;
private final boolean shouldMediaFormatChangeTimesBeChecked;
private boolean skipToPositionBeforeRenderingFirstFrame; private boolean skipToPositionBeforeRenderingFirstFrame;
private boolean shouldMediaFormatChangeTimesBeChecked;
private int startIndex; private int startIndex;
private int queueSize; private int queueSize;
...@@ -114,6 +113,16 @@ import java.util.ArrayList; ...@@ -114,6 +113,16 @@ import java.util.ArrayList;
maxDroppedFrameCountToNotify); maxDroppedFrameCountToNotify);
timestampsList = new long[ARRAY_SIZE]; timestampsList = new long[ARRAY_SIZE];
inputFormatChangeTimesUs = new ArrayDeque<>(); 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 @Override
...@@ -135,10 +144,6 @@ import java.util.ArrayList; ...@@ -135,10 +144,6 @@ import java.util.ArrayList;
// frames up to the current playback position [Internal: b/66494991]. // frames up to the current playback position [Internal: b/66494991].
skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED;
super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate); 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 @Override
...@@ -186,6 +191,8 @@ import java.util.ArrayList; ...@@ -186,6 +191,8 @@ import java.util.ArrayList;
if (mediaFormat != null && !mediaFormat.equals(currentMediaFormat)) { if (mediaFormat != null && !mediaFormat.equals(currentMediaFormat)) {
outputMediaFormatChanged = true; outputMediaFormatChanged = true;
currentMediaFormat = mediaFormat; currentMediaFormat = mediaFormat;
} else {
inputFormatChangeTimesUs.remove();
} }
} }
......
...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil ...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultLoadControl;
...@@ -37,6 +38,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; ...@@ -37,6 +38,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; 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.util.Util;
import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoListener;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
...@@ -396,6 +398,7 @@ public class TestExoPlayer { ...@@ -396,6 +398,7 @@ public class TestExoPlayer {
*/ */
public static void runUntilPositionDiscontinuity( public static void runUntilPositionDiscontinuity(
Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException { Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException {
verifyMainTestThread(player);
AtomicBoolean receivedCallback = new AtomicBoolean(false); AtomicBoolean receivedCallback = new AtomicBoolean(false);
Player.EventListener listener = Player.EventListener listener =
new Player.EventListener() { new Player.EventListener() {
...@@ -459,6 +462,59 @@ public class TestExoPlayer { ...@@ -459,6 +462,59 @@ public class TestExoPlayer {
} }
/** /**
* 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 * Runs tasks of the main {@link Looper} until the player completely handled all previously issued
* commands on the internal playback thread. * commands on the internal playback thread.
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment