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

This commit is contained in:
Oliver Woodman 2020-09-07 20:46:16 +01:00
commit 7e7a33a851
38 changed files with 788 additions and 405 deletions

View file

@ -137,7 +137,7 @@
* Recreate the decoder when handling and swallowing decode errors in
`TextRenderer`. This fixes a case where playback would never end when
playing content with malformed subtitles
([#7590](https://github.com/google/ExoPlayer/issues/790)).
([#7590](https://github.com/google/ExoPlayer/issues/7590)).
* Only apply `CaptionManager` font scaling in
`SubtitleView.setUserDefaultTextSize` if the `CaptionManager` is
enabled.
@ -315,6 +315,8 @@
* Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set
companion ad slots without accessing the `AdDisplayContainer`.
* Add missing notification of `VideoAdPlayerCallback.onLoaded`.
* Fix handling of incompatible VPAID ads
([#7832](https://github.com/google/ExoPlayer/issues/7832)).
* Demo app:
* Replace the `extensions` variant with `decoderExtensions` and update the
demo app use the Cronet and IMA extensions by default.

View file

@ -16,10 +16,12 @@
package com.google.android.exoplayer2.demo;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -170,6 +172,7 @@ public class DownloadTracker {
private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo;
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
@Nullable private byte[] keySetId;
public StartDownloadDialogHelper(
@ -185,6 +188,9 @@ public class DownloadTracker {
if (trackSelectionDialog != null) {
trackSelectionDialog.dismiss();
}
if (widevineOfflineLicenseFetchTask != null) {
widevineOfflineLicenseFetchTask.cancel(false);
}
}
// DownloadHelper.Callback implementation.
@ -192,59 +198,32 @@ public class DownloadTracker {
@Override
public void onPrepared(@NonNull DownloadHelper helper) {
@Nullable Format format = getFirstFormatWithDrmInitData(helper);
if (format != null) {
if (Util.SDK_INT < 18) {
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
return;
}
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
if (!hasSchemaData(format.drmInitData)) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(
TAG,
"Downloading content where DRM scheme data is not located in the manifest is not"
+ " supported");
return;
}
try {
// TODO(internal b/163107948): Download the license on another thread to keep the UI
// thread unblocked.
fetchOfflineLicense(format);
} catch (DrmSession.DrmSessionException e) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to fetch offline DRM license", e);
return;
}
}
if (helper.getPeriodCount() == 0) {
Log.d(TAG, "No periods found. Downloading entire stream.");
startDownload();
downloadHelper.release();
if (format == null) {
onDownloadPrepared(helper);
return;
}
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
Log.d(TAG, "No dialog content. Downloading entire stream.");
startDownload();
downloadHelper.release();
// The content is DRM protected. We need to acquire an offline license.
if (Util.SDK_INT < 18) {
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
return;
}
trackSelectionDialog =
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
trackSelectorParameters,
/* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
if (!hasSchemaData(format.drmInitData)) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(
TAG,
"Downloading content where DRM scheme data is not located in the manifest is not"
+ " supported");
return;
}
widevineOfflineLicenseFetchTask =
new WidevineOfflineLicenseFetchTask(
format, mediaItem.playbackProperties.drmConfiguration.licenseUri, this, helper);
widevineOfflineLicenseFetchTask.execute();
}
@Override
@ -292,6 +271,44 @@ public class DownloadTracker {
// Internal methods.
private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) {
this.keySetId = keySetId;
onDownloadPrepared(helper);
}
private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) {
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to fetch offline DRM license", e);
}
private void onDownloadPrepared(DownloadHelper helper) {
if (helper.getPeriodCount() == 0) {
Log.d(TAG, "No periods found. Downloading entire stream.");
startDownload();
downloadHelper.release();
return;
}
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
Log.d(TAG, "No dialog content. Downloading entire stream.");
startDownload();
downloadHelper.release();
return;
}
trackSelectionDialog =
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
trackSelectorParameters,
/* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
}
private void startDownload() {
startDownload(buildDownloadRequest());
}
@ -306,15 +323,54 @@ public class DownloadTracker {
.getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)))
.copyWithKeySetId(keySetId);
}
}
@RequiresApi(18)
private void fetchOfflineLicense(Format format) throws DrmSession.DrmSessionException {
/** Downloads a Widevine offline license in a background thread. */
@RequiresApi(18)
private final class WidevineOfflineLicenseFetchTask extends AsyncTask<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.newWidevineInstance(
mediaItem.playbackProperties.drmConfiguration.licenseUri.toString(),
licenseUri.toString(),
httpDataSourceFactory,
new DrmSessionEventListener.EventDispatcher());
keySetId = offlineLicenseHelper.downloadLicense(format);
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
} catch (DrmSession.DrmSessionException e) {
drmSessionException = e;
} finally {
offlineLicenseHelper.release();
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else {
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId));
}
}
}

View file

@ -271,13 +271,14 @@ public class PlayerActivity extends AppCompatActivity
setContentView(R.layout.player_activity);
}
protected void initializePlayer() {
/** @return Whether initialization was successful. */
protected boolean initializePlayer() {
if (player == null) {
Intent intent = getIntent();
mediaItems = createMediaItems(intent);
if (mediaItems.isEmpty()) {
return;
return false;
}
boolean preferExtensionDecoders =
@ -297,9 +298,9 @@ public class PlayerActivity extends AppCompatActivity
.setTrackSelector(trackSelector)
.build();
player.addListener(new PlayerEventListener());
player.addAnalyticsListener(new EventLogger(trackSelector));
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
@ -312,6 +313,7 @@ public class PlayerActivity extends AppCompatActivity
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
player.prepare();
updateButtonVisibility();
return true;
}
private List<MediaItem> createMediaItems(Intent intent) {
@ -548,17 +550,7 @@ public class PlayerActivity extends AppCompatActivity
@Nullable
DownloadRequest downloadRequest =
downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri);
if (downloadRequest != null) {
MediaItem mediaItem =
item.buildUpon()
.setStreamKeys(downloadRequest.streamKeys)
.setCustomCacheKey(downloadRequest.customCacheKey)
.setDrmKeySetId(downloadRequest.keySetId)
.build();
mediaItems.add(mediaItem);
} else {
mediaItems.add(item);
}
mediaItems.add(downloadRequest != null ? downloadRequest.toMediaItem() : item);
}
return mediaItems;
}

View file

@ -1096,6 +1096,11 @@ public final class ImaAdsLoader
if (imaAdInfo != null) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
updateAdPlaybackState();
} else if (adPlaybackState.adGroupCount == 1 && adPlaybackState.adGroupTimesUs[0] == 0) {
// For incompatible VPAID ads with one preroll, content is resumed immediately. In this case
// we haven't received ad info (the ad never loaded), but there is only one ad group to skip.
adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ 0);
updateAdPlaybackState();
}
}

View file

@ -1,12 +1,10 @@
# ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][]
instead.**
**This extension is deprecated. Use the [WorkManager extension][] instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##

View file

@ -185,17 +185,20 @@ public class SessionPlayerConnectorTest {
}
};
SimpleExoPlayer simpleExoPlayer = null;
SessionPlayerConnector playerConnector = null;
try {
simpleExoPlayer =
new SimpleExoPlayer.Builder(context)
.setLooper(Looper.myLooper())
.build();
try (SessionPlayerConnector player =
new SessionPlayerConnector(
simpleExoPlayer, new DefaultMediaItemConverter(), controlDispatcher)) {
assertPlayerResult(player.play(), RESULT_INFO_SKIPPED);
}
playerConnector =
new SessionPlayerConnector(simpleExoPlayer, new DefaultMediaItemConverter());
playerConnector.setControlDispatcher(controlDispatcher);
assertPlayerResult(playerConnector.play(), RESULT_INFO_SKIPPED);
} finally {
if (playerConnector != null) {
playerConnector.close();
}
if (simpleExoPlayer != null) {
simpleExoPlayer.release();
}

View file

@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.ext.media2;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.os.Handler;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@ -129,7 +131,7 @@ import java.util.concurrent.Callable;
// Should be only used on the handler.
private final PlayerWrapper player;
private final PlayerHandler handler;
private final Handler handler;
private final Object lock;
@GuardedBy("lock")
@ -141,7 +143,7 @@ import java.util.concurrent.Callable;
// Should be only used on the handler.
@Nullable private AsyncPlayerCommandResult pendingAsyncPlayerCommandResult;
public PlayerCommandQueue(PlayerWrapper player, PlayerHandler handler) {
public PlayerCommandQueue(PlayerWrapper player, Handler handler) {
this.player = player;
this.handler = handler;
lock = new Object();
@ -209,7 +211,7 @@ import java.util.concurrent.Callable;
}
processPendingCommandOnHandler();
},
handler::postOrRun);
(runnable) -> postOrRun(handler, runnable));
if (DEBUG) {
Log.d(TAG, "adding " + playerCommand);
}
@ -220,7 +222,8 @@ import java.util.concurrent.Callable;
}
public void notifyCommandError() {
handler.postOrRun(
postOrRun(
handler,
() -> {
@Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult;
if (pendingResult == null) {
@ -243,7 +246,8 @@ import java.util.concurrent.Callable;
if (DEBUG) {
Log.d(TAG, "notifyCommandCompleted, completedCommandCode=" + completedCommandCode);
}
handler.postOrRun(
postOrRun(
handler,
() -> {
@Nullable AsyncPlayerCommandResult pendingResult = pendingAsyncPlayerCommandResult;
if (pendingResult == null || pendingResult.commandCode != completedCommandCode) {
@ -267,7 +271,7 @@ import java.util.concurrent.Callable;
}
private void processPendingCommand() {
handler.postOrRun(this::processPendingCommandOnHandler);
postOrRun(handler, this::processPendingCommandOnHandler);
}
private void processPendingCommandOnHandler() {

View file

@ -1,41 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import android.os.Handler;
import android.os.Looper;
/** A {@link Handler} that provides {@link #postOrRun(Runnable)}. */
/* package */ final class PlayerHandler extends Handler {
public PlayerHandler(Looper looper) {
super(looper);
}
/**
* Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of this
* handler. Otherwise, runs the runnable directly.
*
* @param r A runnable to either post or run.
* @return {@code true} if it's successfully run. {@code false} otherwise.
*/
public boolean postOrRun(Runnable r) {
if (Thread.currentThread() != getLooper().getThread()) {
return post(r);
}
r.run();
return true;
}
}

View file

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.ext.media2;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
@ -24,6 +27,7 @@ import androidx.media2.common.MediaMetadata;
import androidx.media2.common.SessionPlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
@ -100,12 +104,11 @@ import java.util.List;
private static final int POLL_BUFFER_INTERVAL_MS = 1000;
private final Listener listener;
private final PlayerHandler handler;
private final Handler handler;
private final Runnable pollBufferRunnable;
private final Player player;
private final MediaItemConverter mediaItemConverter;
private final ControlDispatcher controlDispatcher;
private final ComponentListener componentListener;
@Nullable private MediaMetadata playlistMetadata;
@ -114,6 +117,7 @@ import java.util.List;
private final List<androidx.media2.common.MediaItem> media2Playlist;
private final List<MediaItem> exoPlayerPlaylist;
private ControlDispatcher controlDispatcher;
private boolean prepared;
private boolean rebuffering;
private int currentWindowIndex;
@ -125,18 +129,13 @@ import java.util.List;
* @param listener A {@link Listener}.
* @param player The {@link Player}.
* @param mediaItemConverter The {@link MediaItemConverter}.
* @param controlDispatcher A {@link ControlDispatcher}.
*/
public PlayerWrapper(
Listener listener,
Player player,
MediaItemConverter mediaItemConverter,
ControlDispatcher controlDispatcher) {
public PlayerWrapper(Listener listener, Player player, MediaItemConverter mediaItemConverter) {
this.listener = listener;
this.player = player;
this.mediaItemConverter = mediaItemConverter;
this.controlDispatcher = controlDispatcher;
controlDispatcher = new DefaultControlDispatcher();
componentListener = new ComponentListener();
player.addListener(componentListener);
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
@ -144,7 +143,7 @@ import java.util.List;
audioComponent.addAudioListener(componentListener);
}
handler = new PlayerHandler(player.getApplicationLooper());
handler = new Handler(player.getApplicationLooper());
pollBufferRunnable = new PollBufferRunnable();
media2Playlist = new ArrayList<>();
@ -157,6 +156,10 @@ import java.util.List;
updatePlaylist(player.getCurrentTimeline());
}
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
this.controlDispatcher = controlDispatcher;
}
public boolean setMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
return setPlaylist(Collections.singletonList(media2MediaItem), /* metadata= */ null);
}
@ -436,7 +439,7 @@ import java.util.List;
private void handlePlayerStateChanged(@Player.State int state) {
if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) {
handler.postOrRun(pollBufferRunnable);
postOrRun(handler, pollBufferRunnable);
} else {
handler.removeCallbacks(pollBufferRunnable);
}

View file

@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.ext.media2;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler;
import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
@ -64,7 +67,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
private static final int END_OF_PLAYLIST = -1;
private final Object stateLock = new Object();
private final PlayerHandler taskHandler;
private final Handler taskHandler;
private final Executor taskHandlerExecutor;
private final PlayerWrapper player;
private final PlayerCommandQueue playerCommandQueue;
@ -89,7 +92,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
* @param player The player to wrap.
*/
public SessionPlayerConnector(Player player) {
this(player, new DefaultMediaItemConverter(), new DefaultControlDispatcher());
this(player, new DefaultMediaItemConverter());
}
/**
@ -97,22 +100,28 @@ public final class SessionPlayerConnector extends SessionPlayer {
*
* @param player The player to wrap.
* @param mediaItemConverter The {@link MediaItemConverter}.
* @param controlDispatcher The {@link ControlDispatcher}.
*/
public SessionPlayerConnector(
Player player, MediaItemConverter mediaItemConverter, ControlDispatcher controlDispatcher) {
public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) {
Assertions.checkNotNull(player);
Assertions.checkNotNull(mediaItemConverter);
Assertions.checkNotNull(controlDispatcher);
state = PLAYER_STATE_IDLE;
taskHandler = new PlayerHandler(player.getApplicationLooper());
taskHandlerExecutor = taskHandler::postOrRun;
ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener();
this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
taskHandler = new Handler(player.getApplicationLooper());
taskHandlerExecutor = (runnable) -> postOrRun(taskHandler, runnable);
this.player = new PlayerWrapper(new ExoPlayerWrapperListener(), player, mediaItemConverter);
playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
}
/**
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
player.setControlDispatcher(controlDispatcher);
}
@Override
public ListenableFuture<PlayerResult> play() {
return playerCommandQueue.addCommand(
@ -598,7 +607,8 @@ public final class SessionPlayerConnector extends SessionPlayer {
private <T> T runPlayerCallableBlocking(Callable<T> callable) {
SettableFuture<T> future = SettableFuture.create();
boolean success =
taskHandler.postOrRun(
postOrRun(
taskHandler,
() -> {
try {
future.set(callable.call());

View file

@ -170,15 +170,16 @@ public final class MimeTypes {
}
/**
* Returns true if it is known that all samples in a stream of the given MIME type are guaranteed
* to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on every
* sample).
* Returns true if it is known that all samples in a stream of the given MIME type and codec are
* guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on
* every sample).
*
* @param mimeType A MIME type.
* @return True if it is known that all samples in a stream of the given MIME type are guaranteed
* to be sync samples. False otherwise, including if {@code null} is passed.
* @param mimeType The MIME type of the stream.
* @param codec The RFC 6381 codec string of the stream, or {@code null} if unknown.
* @return Whether it is known that all samples in the stream are guaranteed to be sync samples.
*/
public static boolean allSamplesAreSyncSamples(@Nullable String mimeType) {
public static boolean allSamplesAreSyncSamples(
@Nullable String mimeType, @Nullable String codec) {
if (mimeType == null) {
return false;
}
@ -198,6 +199,20 @@ public final class MimeTypes {
case AUDIO_E_AC3:
case AUDIO_E_AC3_JOC:
return true;
case AUDIO_AAC:
if (codec == null) {
return false;
}
@Nullable Mp4aObjectType objectType = getObjectTypeFromMp4aRFC6381CodecString(codec);
if (objectType == null) {
return false;
}
@C.Encoding
int encoding = AacUtil.getEncodingForAudioObjectType(objectType.audioObjectTypeIndication);
// xHE-AAC is an exception in which it's not true that all samples will be sync samples.
// Also return false for ENCODING_INVALID, which indicates we weren't able to parse the
// encoding from the codec string.
return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_AAC_XHE;
default:
return false;
}

View file

@ -493,6 +493,24 @@ public final class Util {
return new Handler(looper, callback);
}
/**
* Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of the {@link
* Handler}. Otherwise, runs the {@link Runnable} directly.
*
* @param handler The handler to which the {@link Runnable} will be posted.
* @param runnable The runnable to either post or run.
* @return {@code true} if the {@link Runnable} was successfully posted to the {@link Handler} or
* run. {@code false} otherwise.
*/
public static boolean postOrRun(Handler handler, Runnable runnable) {
if (handler.getLooper() == Looper.myLooper()) {
runnable.run();
return true;
} else {
return handler.post(runnable);
}
}
/**
* Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the
* application's main thread if the current thread doesn't have a {@link Looper}.

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.util;
import static android.media.MediaCodecInfo.CodecProfileLevel.AACObjectHE;
import static android.media.MediaCodecInfo.CodecProfileLevel.AACObjectXHE;
import static com.google.common.truth.Truth.assertThat;
import androidx.annotation.Nullable;
@ -171,4 +173,17 @@ public final class MimeTypesTest {
assertThat(objectType.objectTypeIndication).isEqualTo(expectedObjectTypeIndicator);
assertThat(objectType.audioObjectTypeIndication).isEqualTo(expectedAudioObjectTypeIndicator);
}
@Test
public void allSamplesAreSyncSamples_forAac_usesCodec() {
assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40." + AACObjectHE))
.isTrue();
assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40." + AACObjectXHE))
.isFalse();
assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40")).isFalse();
assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "mp4a.40.")).isFalse();
assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, "invalid")).isFalse();
assertThat(MimeTypes.allSamplesAreSyncSamples(MimeTypes.AUDIO_AAC, /* codec= */ null))
.isFalse();
}
}

View file

@ -374,8 +374,6 @@ public interface Player {
}
/** The device component of a {@link Player}. */
// Note: It's mostly from the androidx.media.VolumeProviderCompat and
// androidx.media.MediaControllerCompat.PlaybackInfo.
interface DeviceComponent {
/** Adds a listener to receive device events. */

View file

@ -48,62 +48,74 @@ import com.google.android.exoplayer2.util.Util;
* <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
* single file"> A timeline for a single media file or on-demand stream consists of a single period
* and window. The window spans the whole period, indicating that all parts of the media are
* available for playback. The window's default position is typically at the start of the period
* (indicated by the black dot in the figure above).
* single file">
*
* <p>A timeline for a single media file or on-demand stream consists of a single period and window.
* The window spans the whole period, indicating that all parts of the media are available for
* playback. The window's default position is typically at the start of the period (indicated by the
* black dot in the figure above).
*
* <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
* playlist of files"> A timeline for a playlist of media files or on-demand streams consists of
* multiple periods, each with its own window. Each window spans the whole of the corresponding
* period, and typically has a default position at the start of the period. The properties of the
* periods and windows (e.g. their durations and whether the window is seekable) will often only
* become known when the player starts buffering the corresponding file or stream.
* playlist of files">
*
* <p>A timeline for a playlist of media files or on-demand streams consists of multiple periods,
* each with its own window. Each window spans the whole of the corresponding period, and typically
* has a default position at the start of the period. The properties of the periods and windows
* (e.g. their durations and whether the window is seekable) will often only become known when the
* player starts buffering the corresponding file or stream.
*
* <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
* a live stream with limited availability"> A timeline for a live stream consists of a period whose
* duration is unknown, since it's continually extending as more content is broadcast. If content
* only remains available for a limited period of time then the window may start at a non-zero
* position, defining the region of content that can still be played. The window will have {@link
* Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to
* true as long as we expect changes to the live window. Its default position is typically near to
* the live edge (indicated by the black dot in the figure above).
* a live stream with limited availability">
*
* <p>A timeline for a live stream consists of a period whose duration is unknown, since it's
* continually extending as more content is broadcast. If content only remains available for a
* limited period of time then the window may start at a non-zero position, defining the region of
* content that can still be played. The window will have {@link Window#isLive} set to true to
* indicate it's a live stream and {@link Window#isDynamic} set to true as long as we expect changes
* to the live window. Its default position is typically near to the live edge (indicated by the
* black dot in the figure above).
*
* <h3>Live stream with indefinite availability</h3>
*
* <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
* 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
* previously broadcast content can still be played.
* for a live stream with indefinite availability">
*
* <p>A timeline for a live stream with indefinite 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 previously broadcast content
* can still be played.
*
* <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
* for a live stream with multiple periods"> This case arises when a live stream is explicitly
* divided into separate periods, for example at content boundaries. This case is similar to the <a
* href="#live-limited">Live stream with limited availability</a> case, except that the window may
* span more than one period. Multiple periods are also possible in the indefinite availability
* case.
* for a live stream with multiple periods">
*
* <p>This case arises when a live stream is explicitly 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 span more than one period. Multiple
* periods are also possible in the indefinite availability case.
*
* <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
* on-demand stream followed by a live stream"> This case is the concatenation of the <a
* 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
* the live stream will start from its default position near the live edge.
* on-demand stream followed by a live stream">
*
* <p>This case is the concatenation of the <a 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 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>
*
* <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,
* which are defined as part of the timeline's single period. The period can be queried for
* information about the ad groups and the ads they contain.
* timeline for an on-demand stream with mid-roll ad groups">
*
* <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 {

View file

@ -15,8 +15,9 @@
*/
package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
@ -207,15 +208,6 @@ public interface DrmSessionEventListener {
}
}
/** Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId)}. */
private static void postOrRun(Handler handler, Runnable runnable) {
if (handler.getLooper() == Looper.myLooper()) {
runnable.run();
} else {
handler.post(runnable);
}
}
private static final class ListenerAndHandler {
public Handler handler;

View file

@ -1442,6 +1442,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
if (codec == null) {
if (!legacyKeepAvailableCodecInfosWithoutCodec()) {
availableCodecInfos = null;
}
maybeInitCodecOrBypass();
return;
}
@ -1506,6 +1509,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
/**
* Returns whether to keep available codec infos when the codec hasn't been initialized, which is
* the behavior before a bug fix. See also [Internal: b/162837741].
*/
protected boolean legacyKeepAvailableCodecInfosWithoutCodec() {
return false;
}
/**
* Called when one of the output formats changes.
*

View file

@ -77,7 +77,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <p>A typical usage of DownloadHelper follows these steps:
*
* <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>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
* #getTrackSelections(int, int)}, and make adjustments using {@link
@ -448,14 +448,7 @@ public final class DownloadHelper {
DataSource.Factory dataSourceFactory,
@Nullable DrmSessionManager drmSessionManager) {
return createMediaSourceInternal(
new MediaItem.Builder()
.setUri(downloadRequest.uri)
.setCustomCacheKey(downloadRequest.customCacheKey)
.setMimeType(downloadRequest.mimeType)
.setStreamKeys(downloadRequest.streamKeys)
.build(),
dataSourceFactory,
drmSessionManager);
downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager);
}
private final MediaItem.PlaybackProperties playbackProperties;

View file

@ -22,6 +22,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
@ -220,6 +221,18 @@ public final class DownloadRequest implements Parcelable {
newRequest.data);
}
/** Returns a {@link MediaItem} for the content defined by the request. */
public MediaItem toMediaItem() {
return new MediaItem.Builder()
.setMediaId(id)
.setUri(uri)
.setCustomCacheKey(customCacheKey)
.setMimeType(mimeType)
.setStreamKeys(streamKeys)
.setDrmKeySetId(keySetId)
.build();
}
@Override
public String toString() {
return mimeType + ":" + id;

View file

@ -264,7 +264,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
for (TrackSelection trackSelection : selections) {
if (trackSelection != null) {
Format selectedFormat = trackSelection.getSelectedFormat();
if (!MimeTypes.allSamplesAreSyncSamples(selectedFormat.sampleMimeType)) {
if (!MimeTypes.allSamplesAreSyncSamples(
selectedFormat.sampleMimeType, selectedFormat.codecs)) {
return true;
}
}

View file

@ -15,8 +15,9 @@
*/
package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@ -471,14 +472,6 @@ public interface MediaSourceEventListener {
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
}
private static void postOrRun(Handler handler, Runnable runnable) {
if (handler.getLooper() == Looper.myLooper()) {
runnable.run();
} else {
handler.post(runnable);
}
}
private static final class ListenerAndHandler {
public Handler handler;

View file

@ -215,6 +215,22 @@ public class SampleQueue implements TrackOutput {
sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex));
}
/**
* Discards samples from the write side of the queue.
*
* @param timeUs Samples will be discarded from the write end of the queue until a sample with a
* timestamp smaller than timeUs is encountered (this sample is not discarded). Must be larger
* than {@link #getLargestReadTimestampUs()}.
*/
public final void discardUpstreamFrom(long timeUs) {
if (length == 0) {
return;
}
checkArgument(timeUs > getLargestReadTimestampUs());
int retainCount = countUnreadSamplesBefore(timeUs);
discardUpstreamSamples(absoluteFirstIndex + retainCount);
}
// Called by the consuming thread.
/** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */
@ -278,6 +294,16 @@ public class SampleQueue implements TrackOutput {
return largestQueuedTimestampUs;
}
/**
* Returns the largest sample timestamp that has been read since the last {@link #reset}.
*
* @return The largest sample timestamp that has been read, or {@link Long#MIN_VALUE} if no
* samples have been read.
*/
public final synchronized long getLargestReadTimestampUs() {
return max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition));
}
/**
* Returns whether the last sample of the stream has knowingly been queued. A return value of
* {@code false} means that the last sample had not been queued or that it's unknown whether the
@ -659,7 +685,7 @@ public class SampleQueue implements TrackOutput {
upstreamFormat = format;
}
upstreamAllSamplesAreSyncSamples =
MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType);
MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType, upstreamFormat.codecs);
loggedUnexpectedNonSyncSample = false;
return true;
}
@ -777,20 +803,10 @@ public class SampleQueue implements TrackOutput {
if (length == 0) {
return timeUs > largestDiscardedTimestampUs;
}
long largestReadTimestampUs =
max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition));
if (largestReadTimestampUs >= timeUs) {
if (getLargestReadTimestampUs() >= timeUs) {
return false;
}
int retainCount = length;
int relativeSampleIndex = getRelativeIndex(length - 1);
while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
retainCount--;
relativeSampleIndex--;
if (relativeSampleIndex == -1) {
relativeSampleIndex = capacity - 1;
}
}
int retainCount = countUnreadSamplesBefore(timeUs);
discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount);
return true;
}
@ -887,6 +903,26 @@ public class SampleQueue implements TrackOutput {
return sampleCountToTarget;
}
/**
* Counts the number of samples that haven't been read that have a timestamp smaller than {@code
* timeUs}.
*
* @param timeUs The specified time.
* @return The number of unread samples with a timestamp smaller than {@code timeUs}.
*/
private int countUnreadSamplesBefore(long timeUs) {
int count = length;
int relativeSampleIndex = getRelativeIndex(length - 1);
while (count > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
count--;
relativeSampleIndex--;
if (relativeSampleIndex == -1) {
relativeSampleIndex = capacity - 1;
}
}
return count;
}
/**
* Discards the specified number of samples.
*

View file

@ -318,12 +318,6 @@ public class EventLogger implements AnalyticsListener {
logd(eventTime, "audioInputFormat", Format.toLogString(format));
}
@Override
public void onAudioPositionAdvancing(EventTime eventTime, long playoutStartSystemTimeMs) {
long timeSincePlayoutStartMs = System.currentTimeMillis() - playoutStartSystemTimeMs;
logd(eventTime, "audioPositionAdvancing", "timeSincePlayoutStartMs=" + timeSincePlayoutStartMs);
}
@Override
public void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
import static com.google.android.exoplayer2.testutil.TestExoPlayer.playUntilStartOfWindow;
import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilPlaybackState;
import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilTimelineChanged;
import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil;
@ -114,9 +115,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.mockito.Mockito;
@ -439,49 +442,41 @@ public final class ExoPlayerTest {
public void repeatModeChanges() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.playUntilStartOfWindow(/* windowIndex= */ 1)
.setRepeatMode(Player.REPEAT_MODE_ONE)
.playUntilStartOfWindow(/* windowIndex= */ 1)
.setRepeatMode(Player.REPEAT_MODE_OFF)
.playUntilStartOfWindow(/* windowIndex= */ 2)
.setRepeatMode(Player.REPEAT_MODE_ONE)
.playUntilStartOfWindow(/* windowIndex= */ 2)
.setRepeatMode(Player.REPEAT_MODE_ALL)
.playUntilStartOfWindow(/* windowIndex= */ 0)
.setRepeatMode(Player.REPEAT_MODE_ONE)
.playUntilStartOfWindow(/* windowIndex= */ 0)
.playUntilStartOfWindow(/* windowIndex= */ 0)
.setRepeatMode(Player.REPEAT_MODE_OFF)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setRenderers(renderer)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
testRunner.assertTimelinesSame(new FakeMediaSource.InitialTimeline(timeline), timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(renderer).build();
AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockAnalyticsListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
runUntilTimelineChanged(player);
playUntilStartOfWindow(player, /* windowIndex= */ 1);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
playUntilStartOfWindow(player, /* windowIndex= */ 1);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
playUntilStartOfWindow(player, /* windowIndex= */ 2);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
playUntilStartOfWindow(player, /* windowIndex= */ 2);
player.setRepeatMode(Player.REPEAT_MODE_ALL);
playUntilStartOfWindow(player, /* windowIndex= */ 0);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
playUntilStartOfWindow(player, /* windowIndex= */ 0);
playUntilStartOfWindow(player, /* windowIndex= */ 0);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
playUntilStartOfWindow(player, /* windowIndex= */ 1);
playUntilStartOfWindow(player, /* windowIndex= */ 2);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<AnalyticsListener.EventTime> eventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(mockAnalyticsListener, times(10))
.onMediaItemTransition(eventTimes.capture(), any(), anyInt());
assertThat(
eventTimes.getAllValues().stream()
.map(eventTime -> eventTime.currentWindowIndex)
.collect(Collectors.toList()))
.containsExactly(0, 1, 1, 2, 2, 0, 0, 0, 1, 2)
.inOrder();
assertThat(renderer.isEnded).isTrue();
}

View file

@ -21,8 +21,8 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doAnswer;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
@ -31,15 +31,12 @@ import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowLooper;
/** Unit tests for {@link AsynchronousMediaCodecBufferEnqueuer}. */
@RunWith(AndroidJUnit4.class)
@ -54,6 +51,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
@Before
public void setUp() throws IOException {
codec = MediaCodec.createByCodecName("h264");
codec.configure(new MediaFormat(), /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
codec.start();
handlerThread = new TestHandlerThread("TestHandlerThread");
enqueuer =
new AsynchronousMediaCodecBufferEnqueuer(codec, handlerThread, mockConditionVariable);
@ -62,7 +61,8 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
@After
public void tearDown() {
enqueuer.shutdown();
codec.stop();
codec.release();
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
}
@ -98,32 +98,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
/* 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
public void queueSecureInputBuffer_withPendingCryptoException_throwsCryptoException() {
enqueuer.setPendingRuntimeException(
@ -159,33 +133,6 @@ public class AsynchronousMediaCodecBufferEnqueuerTest {
/* 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
public void flush_withoutStart_works() {
enqueuer.flush();

View file

@ -878,6 +878,118 @@ public final class SampleQueueTest {
assertReadTestData(FORMAT_1, 1, 7);
}
@Test
public void discardUpstreamFrom() {
writeTestData();
sampleQueue.discardUpstreamFrom(8000);
assertAllocationCount(10);
sampleQueue.discardUpstreamFrom(7000);
assertAllocationCount(9);
sampleQueue.discardUpstreamFrom(6000);
assertAllocationCount(7);
sampleQueue.discardUpstreamFrom(5000);
assertAllocationCount(5);
sampleQueue.discardUpstreamFrom(4000);
assertAllocationCount(4);
sampleQueue.discardUpstreamFrom(3000);
assertAllocationCount(3);
sampleQueue.discardUpstreamFrom(2000);
assertAllocationCount(2);
sampleQueue.discardUpstreamFrom(1000);
assertAllocationCount(1);
sampleQueue.discardUpstreamFrom(0);
assertAllocationCount(0);
assertReadFormat(false, FORMAT_2);
assertNoSamplesToRead(FORMAT_2);
}
@Test
public void discardUpstreamFromMulti() {
writeTestData();
sampleQueue.discardUpstreamFrom(4000);
assertAllocationCount(4);
sampleQueue.discardUpstreamFrom(0);
assertAllocationCount(0);
assertReadFormat(false, FORMAT_2);
assertNoSamplesToRead(FORMAT_2);
}
@Test
public void discardUpstreamFromNonSampleTimestamps() {
writeTestData();
sampleQueue.discardUpstreamFrom(3500);
assertAllocationCount(4);
sampleQueue.discardUpstreamFrom(500);
assertAllocationCount(1);
sampleQueue.discardUpstreamFrom(0);
assertAllocationCount(0);
assertReadFormat(false, FORMAT_2);
assertNoSamplesToRead(FORMAT_2);
}
@Test
public void discardUpstreamFromBeforeRead() {
writeTestData();
sampleQueue.discardUpstreamFrom(4000);
assertAllocationCount(4);
assertReadTestData(null, 0, 4);
assertReadFormat(false, FORMAT_2);
assertNoSamplesToRead(FORMAT_2);
}
@Test
public void discardUpstreamFromAfterRead() {
writeTestData();
assertReadTestData(null, 0, 3);
sampleQueue.discardUpstreamFrom(8000);
assertAllocationCount(10);
sampleQueue.discardToRead();
assertAllocationCount(7);
sampleQueue.discardUpstreamFrom(7000);
assertAllocationCount(6);
sampleQueue.discardUpstreamFrom(6000);
assertAllocationCount(4);
sampleQueue.discardUpstreamFrom(5000);
assertAllocationCount(2);
sampleQueue.discardUpstreamFrom(4000);
assertAllocationCount(1);
sampleQueue.discardUpstreamFrom(3000);
assertAllocationCount(0);
assertReadFormat(false, FORMAT_2);
assertNoSamplesToRead(FORMAT_2);
}
@Test
public void largestQueuedTimestampWithDiscardUpstreamFrom() {
writeTestData();
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
sampleQueue.discardUpstreamFrom(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 1]);
// Discarding from upstream should reduce the largest timestamp.
assertThat(sampleQueue.getLargestQueuedTimestampUs())
.isEqualTo(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 2]);
sampleQueue.discardUpstreamFrom(0);
// Discarding everything from upstream without reading should unset the largest timestamp.
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE);
}
@Test
public void largestQueuedTimestampWithDiscardUpstreamFromDecodeOrder() {
long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};
writeTestData(
DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, decodeOrderTimestamps, SAMPLE_FORMATS, SAMPLE_FLAGS);
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);
sampleQueue.discardUpstreamFrom(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 2]);
// Discarding the last two samples should not change the largest timestamp, due to the decode
// ordering of the timestamps.
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);
sampleQueue.discardUpstreamFrom(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 3]);
// Once a third sample is discarded, the largest timestamp should have changed.
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000);
sampleQueue.discardUpstreamFrom(0);
// Discarding everything from upstream without reading should unset the largest timestamp.
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE);
}
@Test
public void discardUpstream() {
writeTestData();
@ -986,6 +1098,43 @@ public final class SampleQueueTest {
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
}
@Test
public void largestReadTimestampWithReadAll() {
writeTestData();
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE);
assertReadTestData();
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
}
@Test
public void largestReadTimestampWithReads() {
writeTestData();
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE);
assertReadTestData(/* startFormat= */ null, 0, 2);
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[1]);
assertReadTestData(SAMPLE_FORMATS[1], 2, 3);
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[4]);
}
@Test
public void largestReadTimestampWithDiscard() {
// Discarding shouldn't change the read timestamp.
writeTestData();
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE);
sampleQueue.discardUpstreamSamples(5);
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(MIN_VALUE);
assertReadTestData(/* startFormat= */ null, 0, 3);
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[2]);
sampleQueue.discardUpstreamSamples(3);
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[2]);
sampleQueue.discardToRead();
assertThat(sampleQueue.getLargestReadTimestampUs()).isEqualTo(SAMPLE_TIMESTAMPS[2]);
}
@Test
public void setSampleOffsetBeforeData() {
long sampleOffsetUs = 1000;

View file

@ -1,8 +1,20 @@
# ExoPlayer DASH library module #
Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To
play DASH content, instantiate a `DashMediaSource` and pass it to
`ExoPlayer.prepare`.
Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content.
Adding a dependency to this module is all that's required to enable playback of
DASH `MediaItem`s added to an `ExoPlayer` or `SimpleExoPlayer` in their default
configurations. Internally, `DefaultMediaSourceFactory` will automatically
detect the presence of the module and convert DASH `MediaItem`s into
`DashMediaSource` instances for playback.
Similarly, a `DownloadManager` in its default configuration will use
`DefaultDownloaderFactory`, which will automatically detect the presence of
the module and build `DashDownloader` instances to download DASH content.
For advanced playback use cases, applications can build `DashMediaSource`
instances and pass them directly to the player. For advanced download use cases,
`DashDownloader` can be used directly.
## Links ##

View file

@ -1,7 +1,20 @@
# ExoPlayer HLS library module #
Provides support for HTTP Live Streaming (HLS) content. To play HLS content,
instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`.
Provides support for HTTP Live Streaming (HLS) content.
Adding a dependency to this module is all that's required to enable playback of
HLS `MediaItem`s added to an `ExoPlayer` or `SimpleExoPlayer` in their default
configurations. Internally, `DefaultMediaSourceFactory` will automatically
detect the presence of the module and convert HLS `MediaItem`s into
`HlsMediaSource` instances for playback.
Similarly, a `DownloadManager` in its default configuration will use
`DefaultDownloaderFactory`, which will automatically detect the presence of
the module and build `HlsDownloader` instances to download HLS content.
For advanced playback use cases, applications can build `HlsMediaSource`
instances and pass them directly to the player. For advanced download use cases,
`HlsDownloader` can be used directly.
## Links ##

View file

@ -1,7 +1,21 @@
# ExoPlayer SmoothStreaming library module #
Provides support for Smooth Streaming content. To play Smooth Streaming content,
instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`.
Provides support for SmoothStreaming content.
Adding a dependency to this module is all that's required to enable playback of
SmoothStreaming `MediaItem`s added to an `ExoPlayer` or `SimpleExoPlayer` in
their default configurations. Internally, `DefaultMediaSourceFactory` will
automatically detect the presence of the module and convert SmoothStreaming
`MediaItem`s into `SsMediaSource` instances for playback.
Similarly, a `DownloadManager` in its default configuration will use
`DefaultDownloaderFactory`, which will automatically detect the presence of
the module and build `SsDownloader` instances to download SmoothStreaming
content.
For advanced playback use cases, applications can build `SsMediaSource`
instances and pass them directly to the player. For advanced download use cases,
`SsDownloader` can be used directly.
## Links ##

View file

@ -38,6 +38,7 @@ import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.State;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.RepeatModeUtil;
@ -480,6 +481,7 @@ public class PlayerControlView extends FrameLayout {
}
vrButton = findViewById(R.id.exo_vr);
setShowVrButton(false);
updateButton(false, false, vrButton);
Resources resources = context.getResources();
@ -793,6 +795,7 @@ public class PlayerControlView extends FrameLayout {
public void setVrButtonListener(@Nullable OnClickListener onClickListener) {
if (vrButton != null) {
vrButton.setOnClickListener(onClickListener);
updateButton(getShowVrButton(), onClickListener != null, vrButton);
}
}
@ -1204,19 +1207,22 @@ public class PlayerControlView extends FrameLayout {
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
controlDispatcher.dispatchFastForward(player);
if (player.getPlaybackState() != Player.STATE_ENDED) {
controlDispatcher.dispatchFastForward(player);
}
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
controlDispatcher.dispatchRewind(player);
} else if (event.getRepeatCount() == 0) {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
case KeyEvent.KEYCODE_HEADSETHOOK:
dispatchPlayPause(player);
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true);
dispatchPlay(player);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false);
dispatchPause(player);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
controlDispatcher.dispatchNext(player);
@ -1239,11 +1245,37 @@ public class PlayerControlView extends FrameLayout {
&& player.getPlayWhenReady();
}
private void dispatchPlayPause(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) {
dispatchPlay(player);
} else {
dispatchPause(player);
}
}
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (state == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
}
private void dispatchPause(Player player) {
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_HEADSETHOOK
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
@ -1349,20 +1381,15 @@ public class PlayerControlView extends FrameLayout {
} else if (previousButton == view) {
controlDispatcher.dispatchPrevious(player);
} else if (fastForwardButton == view) {
controlDispatcher.dispatchFastForward(player);
if (player.getPlaybackState() != Player.STATE_ENDED) {
controlDispatcher.dispatchFastForward(player);
}
} else if (rewindButton == view) {
controlDispatcher.dispatchRewind(player);
} else if (playButton == view) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, true);
dispatchPlay(player);
} else if (pauseButton == view) {
controlDispatcher.dispatchSetPlayWhenReady(player, false);
dispatchPause(player);
} else if (repeatToggleButton == view) {
controlDispatcher.dispatchSetRepeatMode(
player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ui;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -877,6 +878,10 @@ public class PlayerNotificationManager {
*
* <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},
* {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link
* NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set

View file

@ -45,6 +45,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.State;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroup;
@ -396,7 +397,7 @@ public class StyledPlayerControlView extends FrameLayout {
private final String fullScreenEnterContentDescription;
@Nullable private Player player;
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
private ControlDispatcher controlDispatcher;
@Nullable private ProgressUpdateListener progressUpdateListener;
@Nullable private PlaybackPreparer playbackPreparer;
@ -537,8 +538,7 @@ public class StyledPlayerControlView extends FrameLayout {
extraAdGroupTimesMs = new long[0];
extraPlayedAdGroups = new boolean[0];
componentListener = new ComponentListener();
controlDispatcher =
new com.google.android.exoplayer2.DefaultControlDispatcher(fastForwardMs, rewindMs);
controlDispatcher = new DefaultControlDispatcher(fastForwardMs, rewindMs);
updateProgressAction = this::updateProgress;
LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
@ -635,6 +635,7 @@ public class StyledPlayerControlView extends FrameLayout {
vrButton = findViewById(R.id.exo_vr);
if (vrButton != null) {
setShowVrButton(showVrButton);
updateButton(/* enabled= */ false, vrButton);
}
// Related to Settings List View
@ -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) {
if (this.controlDispatcher != controlDispatcher) {
@ -1638,19 +1639,22 @@ public class StyledPlayerControlView extends FrameLayout {
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
controlDispatcher.dispatchFastForward(player);
if (player.getPlaybackState() != Player.STATE_ENDED) {
controlDispatcher.dispatchFastForward(player);
}
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
controlDispatcher.dispatchRewind(player);
} else if (event.getRepeatCount() == 0) {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
case KeyEvent.KEYCODE_HEADSETHOOK:
dispatchPlayPause(player);
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true);
dispatchPlay(player);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false);
dispatchPause(player);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
controlDispatcher.dispatchNext(player);
@ -1673,11 +1677,37 @@ public class StyledPlayerControlView extends FrameLayout {
&& player.getPlayWhenReady();
}
private void dispatchPlayPause(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) {
dispatchPlay(player);
} else {
dispatchPause(player);
}
}
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (state == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
}
private void dispatchPause(Player player) {
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_HEADSETHOOK
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
@ -1806,18 +1836,13 @@ public class StyledPlayerControlView extends FrameLayout {
} else if (previousButton == view) {
controlDispatcher.dispatchPrevious(player);
} else if (fastForwardButton == view) {
controlDispatcher.dispatchFastForward(player);
if (player.getPlaybackState() != Player.STATE_ENDED) {
controlDispatcher.dispatchFastForward(player);
}
} else if (rewindButton == view) {
controlDispatcher.dispatchRewind(player);
} else if (playPauseButton == view) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
dispatchPlayPause(player);
} else if (repeatToggleButton == view) {
controlDispatcher.dispatchSetRepeatMode(
player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));

View file

@ -35,6 +35,8 @@
android:minHeight="@dimen/exo_settings_height"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:paddingEnd="4dp"
android:paddingRight="4dp"
android:gravity="center|start"
android:orientation="vertical">

View file

@ -36,8 +36,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="@dimen/exo_settings_height"
android:paddingStart="2dp"
android:paddingLeft="2dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:gravity="center|start"
android:textColor="@color/exo_white"
android:textSize="@dimen/exo_settings_main_text_size"/>

View file

@ -13,6 +13,12 @@
// limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
defaultConfig {
multiDexEnabled true
}
}
dependencies {
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion

View file

@ -17,11 +17,12 @@ package com.google.android.exoplayer2.playbacktests.gts;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import android.media.MediaDrm.MediaDrmStateException;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import com.google.android.exoplayer2.Format;
@ -121,19 +122,19 @@ public final class DashWidevineOfflineTest {
downloadLicense();
releaseLicense(); // keySetId no longer valid.
Throwable error =
assertThrows(
"Playback should fail because the license has been released.",
Throwable.class,
() -> testRunner.run());
// Get the root cause
Throwable cause = error.getCause();
while (cause != null && cause != error) {
error = cause;
cause = error.getCause();
try {
testRunner.run();
fail("Playback should fail because the license has been released.");
} catch (RuntimeException expected) {
// Get the root cause
Throwable error = expected;
@Nullable Throwable cause = error.getCause();
while (cause != null && cause != error) {
error = cause;
cause = error.getCause();
}
assertThat(error).isInstanceOf(MediaDrmStateException.class);
}
assertThat(error).isInstanceOf(MediaDrmStateException.class);
}
@Test
@ -144,18 +145,19 @@ public final class DashWidevineOfflineTest {
downloadLicense();
releaseLicense(); // keySetId no longer valid.
Throwable error =
assertThrows(
"Playback should fail because the license has been released.",
Throwable.class,
() -> testRunner.run());
// Get the root cause
Throwable cause = error.getCause();
while (cause != null && cause != error) {
error = cause;
cause = error.getCause();
try {
testRunner.run();
fail("Playback should fail because the license has been released.");
} catch (RuntimeException expected) {
// Get the root cause
Throwable error = expected;
@Nullable Throwable cause = error.getCause();
while (cause != null && cause != error) {
error = cause;
cause = error.getCause();
}
assertThat(error).isInstanceOf(IllegalArgumentException.class);
}
assertThat(error).isInstanceOf(IllegalArgumentException.class);
}
@Test

View file

@ -34,7 +34,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.nio.ByteBuffer;
@ -85,9 +84,9 @@ import java.util.ArrayList;
private final long[] timestampsList;
private final ArrayDeque<Long> inputFormatChangeTimesUs;
private final boolean shouldMediaFormatChangeTimesBeChecked;
private boolean skipToPositionBeforeRenderingFirstFrame;
private boolean shouldMediaFormatChangeTimesBeChecked;
private int startIndex;
private int queueSize;
@ -114,6 +113,16 @@ import java.util.ArrayList;
maxDroppedFrameCountToNotify);
timestampsList = new long[ARRAY_SIZE];
inputFormatChangeTimesUs = new ArrayDeque<>();
/*
// Output MediaFormat changes are known to occur too early until API 30 (see [internal:
// b/149818050, b/149751672]).
shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30;
*/
// [Internal ref: b/149751672] Seeking currently causes an unexpected MediaFormat change, so
// this check is disabled until that is deemed fixed.
shouldMediaFormatChangeTimesBeChecked = false;
}
@Override
@ -135,10 +144,6 @@ import java.util.ArrayList;
// frames up to the current playback position [Internal: b/66494991].
skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED;
super.configureCodec(codecInfo, codecAdapter, format, crypto, operatingRate);
// Output MediaFormat changes are known to occur too early until API 30 (see [internal:
// b/149818050, b/149751672]).
shouldMediaFormatChangeTimesBeChecked = Util.SDK_INT > 30;
}
@Override
@ -186,6 +191,8 @@ import java.util.ArrayList;
if (mediaFormat != null && !mediaFormat.equals(currentMediaFormat)) {
outputMediaFormatChanged = true;
currentMediaFormat = mediaFormat;
} else {
inputFormatChangeTimesUs.remove();
}
}

View file

@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
@ -37,6 +38,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener;
import java.util.concurrent.TimeoutException;
@ -396,6 +398,7 @@ public class TestExoPlayer {
*/
public static void runUntilPositionDiscontinuity(
Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException {
verifyMainTestThread(player);
AtomicBoolean receivedCallback = new AtomicBoolean(false);
Player.EventListener listener =
new Player.EventListener() {
@ -458,6 +461,59 @@ public class TestExoPlayer {
runMainLooperUntil(receivedCallback::get);
}
/**
* Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
* reaches the specified position and then pauses the {@code player}.
*
* @param player The {@link Player}.
* @param windowIndex The window.
* @param positionMs The position within the window, in milliseconds.
* @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static void playUntilPosition(ExoPlayer player, int windowIndex, long positionMs)
throws TimeoutException {
verifyMainTestThread(player);
Handler testHandler = Util.createHandlerForCurrentOrMainLooper();
AtomicBoolean messageHandled = new AtomicBoolean();
player
.createMessage(
(messageType, payload) -> {
// Block playback thread until pause command has been sent from test thread.
ConditionVariable blockPlaybackThreadCondition = new ConditionVariable();
testHandler.post(
() -> {
player.pause();
messageHandled.set(true);
blockPlaybackThreadCondition.open();
});
try {
blockPlaybackThreadCondition.block();
} catch (InterruptedException e) {
// Ignore.
}
})
.setPosition(windowIndex, positionMs)
.send();
player.play();
runMainLooperUntil(messageHandled::get);
}
/**
* Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
* reaches the specified window and then pauses the {@code player}.
*
* @param player The {@link Player}.
* @param windowIndex The window.
* @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static void playUntilStartOfWindow(ExoPlayer player, int windowIndex)
throws TimeoutException {
playUntilPosition(player, windowIndex, /* positionMs= */ 0);
}
/**
* Runs tasks of the main {@link Looper} until the player completely handled all previously issued
* commands on the internal playback thread.