mirror of
https://github.com/samsonjs/media.git
synced 2026-04-07 11:35:46 +00:00
Merge commit '99dbb76455a21f19e0db4399101039b33a6057a0' into dev-v2-r2.12.0
This commit is contained in:
commit
7e7a33a851
38 changed files with 788 additions and 405 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue