mirror of
https://github.com/androidx/media.git
synced 2025-05-04 22:20:47 +08:00
commit
f6297f4f51
@ -1,5 +1,55 @@
|
||||
# Release notes #
|
||||
|
||||
### 2.10.2 ###
|
||||
|
||||
* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s
|
||||
([#5779](https://github.com/google/ExoPlayer/issues/5779)).
|
||||
* Add `SilenceMediaSource` that can be used to play silence of a given
|
||||
duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)).
|
||||
* Offline:
|
||||
* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after
|
||||
preparation of a `DownloadHelper` fails
|
||||
([#5915](https://github.com/google/ExoPlayer/issues/5915)).
|
||||
* Fix `CacheUtil.cache()` downloading too much data
|
||||
([#5927](https://github.com/google/ExoPlayer/issues/5927)).
|
||||
* Fix misreporting cached bytes when caching is paused
|
||||
([#5573](https://github.com/google/ExoPlayer/issues/5573)).
|
||||
* UI:
|
||||
* Allow setting `DefaultTimeBar` attributes on `PlayerView` and
|
||||
`PlayerControlView`.
|
||||
* Change playback controls toggle from touch down to touch up events
|
||||
([#5784](https://github.com/google/ExoPlayer/issues/5784)).
|
||||
* Fix issue where playback controls were not kept visible on key presses
|
||||
([#5963](https://github.com/google/ExoPlayer/issues/5963)).
|
||||
* Subtitles:
|
||||
* CEA-608: Handle XDS and TEXT modes
|
||||
([#5807](https://github.com/google/ExoPlayer/pull/5807)).
|
||||
* TTML: Fix bitmap rendering
|
||||
([#5633](https://github.com/google/ExoPlayer/pull/5633)).
|
||||
* IMA: Fix ad pod index offset calculation without preroll
|
||||
([#5928](https://github.com/google/ExoPlayer/issues/5928)).
|
||||
* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods
|
||||
to indicate whether a controller sent a play or only a prepare command. This
|
||||
allows to take advantage of decoder reuse with the MediaSessionConnector
|
||||
([#5891](https://github.com/google/ExoPlayer/issues/5891)).
|
||||
* Add `ProgressUpdateListener` to `PlayerControlView`
|
||||
([#5834](https://github.com/google/ExoPlayer/issues/5834)).
|
||||
* Add support for auto-detecting UDP streams in `DefaultDataSource`
|
||||
([#6036](https://github.com/google/ExoPlayer/pull/6036)).
|
||||
* Allow enabling decoder fallback with `DefaultRenderersFactory`
|
||||
([#5942](https://github.com/google/ExoPlayer/issues/5942)).
|
||||
* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission
|
||||
([#6019](https://github.com/google/ExoPlayer/issues/6019)).
|
||||
* Fix decoding problems when seeking back after seeking beyond a mid-roll ad
|
||||
([#6009](https://github.com/google/ExoPlayer/issues/6009)).
|
||||
* Fix application of `maxAudioBitrate` for adaptive audio track groups
|
||||
([#6006](https://github.com/google/ExoPlayer/issues/6006)).
|
||||
* Fix bug caused by parallel adaptive track selection using `Format`s without
|
||||
bitrate information
|
||||
([#5971](https://github.com/google/ExoPlayer/issues/5971)).
|
||||
* Fix bug in `CastPlayer.getCurrentWindowIndex()`
|
||||
([#5955](https://github.com/google/ExoPlayer/issues/5955)).
|
||||
|
||||
### 2.10.1 ###
|
||||
|
||||
* Offline: Add option to remove all downloads.
|
||||
|
@ -36,7 +36,7 @@ allprojects {
|
||||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = true
|
||||
exoplayerPublishEnabled = false
|
||||
}
|
||||
if (it.hasProperty('externalBuildDir')) {
|
||||
if (!new File(externalBuildDir).isAbsolute()) {
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.10.1'
|
||||
releaseVersionCode = 2010001
|
||||
releaseVersion = '2.10.2'
|
||||
releaseVersionCode = 2010002
|
||||
minSdkVersion = 16
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 28
|
||||
|
@ -66,7 +66,6 @@ import java.util.ArrayList;
|
||||
private final Listener listener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
|
||||
private boolean castMediaQueueCreationPending;
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
|
||||
@ -268,9 +267,6 @@ import java.util.ArrayList;
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
if (currentPlayer == castPlayer && timeline.isEmpty()) {
|
||||
castMediaQueueCreationPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
// CastPlayer.SessionAvailabilityListener implementation.
|
||||
@ -332,7 +328,6 @@ import java.util.ArrayList;
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
||||
if (currentPlayer == exoPlayer) {
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
@ -352,12 +347,11 @@ import java.util.ArrayList;
|
||||
*/
|
||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (castMediaQueueCreationPending) {
|
||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castMediaQueueCreationPending = false;
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
|
@ -31,7 +31,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
|
||||
api 'com.google.android.gms:play-services-cast-framework:16.2.0'
|
||||
implementation 'androidx.annotation:annotation:1.0.2'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
|
@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||
import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||
@ -86,8 +89,10 @@ public final class CastPlayer extends BasePlayer {
|
||||
private final StatusListener statusListener;
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
|
||||
// Listeners.
|
||||
private final CopyOnWriteArraySet<EventListener> listeners;
|
||||
// Listeners and notification.
|
||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
||||
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
@ -113,7 +118,9 @@ public final class CastPlayer extends BasePlayer {
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
seekResultCallback = new SeekResultCallback();
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
notificationsBatch = new ArrayList<>();
|
||||
ongoingNotificationsTasks = new ArrayDeque<>();
|
||||
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
@ -296,12 +303,17 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
for (ListenerHolder listenerHolder : listeners) {
|
||||
if (listenerHolder.listener.equals(listener)) {
|
||||
listenerHolder.release();
|
||||
listeners.remove(listenerHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -347,14 +359,13 @@ public final class CastPlayer extends BasePlayer {
|
||||
pendingSeekCount++;
|
||||
pendingSeekWindowIndex = windowIndex;
|
||||
pendingSeekPositionMs = positionMs;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
||||
}
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
|
||||
} else if (pendingSeekCount == 0) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||
}
|
||||
flushNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -530,30 +541,40 @@ public final class CastPlayer extends BasePlayer {
|
||||
|| this.playWhenReady != playWhenReady) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady = playWhenReady;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
|
||||
}
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
|
||||
}
|
||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
}
|
||||
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||
}
|
||||
}
|
||||
if (updateTracksAndSelections()) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
||||
}
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
|
||||
}
|
||||
maybeUpdateTimelineAndNotify();
|
||||
|
||||
int currentWindowIndex = C.INDEX_UNSET;
|
||||
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
|
||||
if (currentItem != null) {
|
||||
currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
|
||||
}
|
||||
if (currentWindowIndex == C.INDEX_UNSET) {
|
||||
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
|
||||
currentWindowIndex = 0;
|
||||
}
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
|
||||
}
|
||||
if (updateTracksAndSelections()) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
|
||||
}
|
||||
flushNotifications();
|
||||
}
|
||||
|
||||
private void maybeUpdateTimelineAndNotify() {
|
||||
@ -561,9 +582,10 @@ public final class CastPlayer extends BasePlayer {
|
||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
waitingForInitialTimeline = false;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTimelineChanged(currentTimeline, null, reason);
|
||||
}
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -701,16 +723,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
|
||||
* there is no media session, returns 0.
|
||||
*/
|
||||
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
|
||||
Integer currentItemId = mediaStatus != null
|
||||
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
|
||||
return currentItemId != null ? currentItemId : 0;
|
||||
}
|
||||
|
||||
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
||||
for (long activeTrackId : activeTrackIds) {
|
||||
if (activeTrackId == id) {
|
||||
@ -826,7 +838,23 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
}
|
||||
|
||||
// Result callbacks hooks.
|
||||
// Internal methods.
|
||||
|
||||
private void flushNotifications() {
|
||||
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
||||
ongoingNotificationsTasks.addAll(notificationsBatch);
|
||||
notificationsBatch.clear();
|
||||
if (recursiveNotification) {
|
||||
// This will be handled once the current notification task is finished.
|
||||
return;
|
||||
}
|
||||
while (!ongoingNotificationsTasks.isEmpty()) {
|
||||
ongoingNotificationsTasks.peekFirst().execute();
|
||||
ongoingNotificationsTasks.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@ -840,11 +868,27 @@ public final class CastPlayer extends BasePlayer {
|
||||
if (--pendingSeekCount == 0) {
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||
flushNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListenerNotificationTask {
|
||||
|
||||
private final Iterator<ListenerHolder> listenersSnapshot;
|
||||
private final ListenerInvocation listenerInvocation;
|
||||
|
||||
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
|
||||
this.listenersSnapshot = listeners.iterator();
|
||||
this.listenerInvocation = listenerInvocation;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
while (listenersSnapshot.hasNext()) {
|
||||
listenersSnapshot.next().invoke(listenerInvocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ android {
|
||||
dependencies {
|
||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:1.0.2'
|
||||
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -1054,13 +1054,8 @@ public final class ImaAdsLoader
|
||||
long contentPositionMs = player.getCurrentPosition();
|
||||
int adGroupIndexForPosition =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||
if (adGroupIndexForPosition == 0) {
|
||||
podIndexOffset = 0;
|
||||
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
|
||||
// There is no preroll and midroll pod indices start at 1.
|
||||
podIndexOffset = -1;
|
||||
} else /* adGroupIndexForPosition > 0 */ {
|
||||
// Skip ad groups before the one at or immediately before the playback position.
|
||||
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
|
||||
// Skip any ad groups before the one at or immediately before the playback position.
|
||||
for (int i = 0; i < adGroupIndexForPosition; i++) {
|
||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
||||
}
|
||||
@ -1070,9 +1065,18 @@ public final class ImaAdsLoader
|
||||
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
|
||||
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
|
||||
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
|
||||
}
|
||||
|
||||
// We're removing one or more ads, which means that the earliest ad (if any) will be a
|
||||
// midroll/postroll. Midroll pod indices start at 1.
|
||||
// IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
|
||||
// Store an index offset as we want to index all ads (including skipped ones) from 0.
|
||||
if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
|
||||
// We are playing a preroll.
|
||||
podIndexOffset = 0;
|
||||
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
|
||||
// There's no ad to play which means there's no preroll.
|
||||
podIndexOffset = -1;
|
||||
} else {
|
||||
// We are playing a midroll and any ads before it were skipped.
|
||||
podIndexOffset = adGroupIndexForPosition - 1;
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ public final class MediaSessionConnector {
|
||||
ResultReceiver cb);
|
||||
}
|
||||
|
||||
/** Interface to which playback preparation actions are delegated. */
|
||||
/** Interface to which playback preparation and play actions are delegated. */
|
||||
public interface PlaybackPreparer extends CommandReceiver {
|
||||
|
||||
long ACTIONS =
|
||||
@ -197,14 +197,36 @@ public final class MediaSessionConnector {
|
||||
* @return The bitmask of the supported media actions.
|
||||
*/
|
||||
long getSupportedPrepareActions();
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
|
||||
void onPrepare();
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
|
||||
void onPrepareFromMediaId(String mediaId, Bundle extras);
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
|
||||
void onPrepareFromSearch(String query, Bundle extras);
|
||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
|
||||
void onPrepareFromUri(Uri uri, Bundle extras);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepare()}.
|
||||
*
|
||||
* @param playWhenReady Whether playback should be started after preparation.
|
||||
*/
|
||||
void onPrepare(boolean playWhenReady);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
|
||||
*
|
||||
* @param mediaId The media id of the media item to be prepared.
|
||||
* @param playWhenReady Whether playback should be started after preparation.
|
||||
* @param extras A {@link Bundle} of extras passed by the media controller.
|
||||
*/
|
||||
void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
|
||||
*
|
||||
* @param query The search query.
|
||||
* @param playWhenReady Whether playback should be started after preparation.
|
||||
* @param extras A {@link Bundle} of extras passed by the media controller.
|
||||
*/
|
||||
void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
|
||||
/**
|
||||
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
|
||||
*
|
||||
* @param uri The {@link Uri} of the media item to be prepared.
|
||||
* @param playWhenReady Whether playback should be started after preparation.
|
||||
* @param extras A {@link Bundle} of extras passed by the media controller.
|
||||
*/
|
||||
void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -834,13 +856,6 @@ public final class MediaSessionConnector {
|
||||
return player != null && mediaButtonEventHandler != null;
|
||||
}
|
||||
|
||||
private void stopPlayerForPrepare(boolean playWhenReady) {
|
||||
if (player != null) {
|
||||
player.stop();
|
||||
player.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
}
|
||||
|
||||
private void rewind(Player player) {
|
||||
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
||||
seekTo(player, player.getCurrentPosition() - rewindMs);
|
||||
@ -1047,14 +1062,14 @@ public final class MediaSessionConnector {
|
||||
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
if (playbackPreparer != null) {
|
||||
playbackPreparer.onPrepare();
|
||||
playbackPreparer.onPrepare(/* playWhenReady= */ true);
|
||||
}
|
||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||
}
|
||||
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
@ -1182,56 +1197,49 @@ public final class MediaSessionConnector {
|
||||
@Override
|
||||
public void onPrepare() {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||
playbackPreparer.onPrepare();
|
||||
playbackPreparer.onPrepare(/* playWhenReady= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
||||
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
||||
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
||||
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ true);
|
||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
||||
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayFromSearch(String query, Bundle extras) {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ true);
|
||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
||||
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayFromUri(Uri uri, Bundle extras) {
|
||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
||||
stopPlayerForPrepare(/* playWhenReady= */ true);
|
||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
||||
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
The build configuration has been tested with Android NDK r19c.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
@ -54,7 +55,7 @@ git checkout tags/v1.8.0 -b v1.8.0
|
||||
|
||||
```
|
||||
cd ${VP9_EXT_PATH}/jni && \
|
||||
./generate_libvpx_android_configs.sh "${NDK_PATH}"
|
||||
./generate_libvpx_android_configs.sh
|
||||
```
|
||||
|
||||
* Build the JNI native libraries from the command line:
|
||||
@ -66,7 +67,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
[#3520]: https://github.com/google/ExoPlayer/issues/3520
|
||||
|
||||
## Notes ##
|
||||
|
||||
|
@ -60,8 +60,8 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
||||
* Initializes the buffer.
|
||||
*
|
||||
* @param timeUs The presentation timestamp for the buffer, in microseconds.
|
||||
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
|
||||
* VpxDecoder#OUTPUT_MODE_YUV}.
|
||||
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link
|
||||
* VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}.
|
||||
*/
|
||||
public void init(long timeUs, int mode) {
|
||||
this.timeUs = timeUs;
|
||||
@ -110,6 +110,15 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the buffer for the given frame dimensions when passing actual frame data via {@link
|
||||
* #decoderPrivate}. Called via JNI after decoding completes.
|
||||
*/
|
||||
public void initForPrivateFrame(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
private void initData(int size) {
|
||||
if (data == null || data.capacity() < size) {
|
||||
data = ByteBuffer.allocateDirect(size);
|
||||
|
@ -15,6 +15,6 @@
|
||||
#
|
||||
|
||||
APP_OPTIM := release
|
||||
APP_STL := gnustl_static
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -frtti
|
||||
APP_PLATFORM := android-9
|
||||
APP_PLATFORM := android-16
|
||||
|
@ -20,46 +20,33 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: ${0} <path_to_android_ndk>"
|
||||
if [ $# -ne 0 ]; then
|
||||
echo "Usage: ${0}"
|
||||
exit
|
||||
fi
|
||||
|
||||
ndk="${1}"
|
||||
shift 1
|
||||
|
||||
# configuration parameters common to all architectures
|
||||
common_params="--disable-examples --disable-docs --enable-realtime-only"
|
||||
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
|
||||
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
|
||||
common_params+=" --enable-external-build"
|
||||
|
||||
# configuration parameters for various architectures
|
||||
arch[0]="armeabi-v7a"
|
||||
config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon"
|
||||
config[0]+=" --enable-neon-asm"
|
||||
config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm"
|
||||
|
||||
arch[1]="armeabi"
|
||||
config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
|
||||
config[1]+=" --disable-neon-asm"
|
||||
arch[1]="x86"
|
||||
config[1]="--force-target=x86-android-gcc --disable-sse2"
|
||||
config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
||||
config[1]+=" --disable-avx2 --enable-pic"
|
||||
|
||||
arch[2]="mips"
|
||||
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
|
||||
arch[2]="arm64-v8a"
|
||||
config[2]="--force-target=armv8-android-gcc --enable-neon"
|
||||
|
||||
arch[3]="x86"
|
||||
config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2"
|
||||
arch[3]="x86_64"
|
||||
config[3]="--force-target=x86_64-android-gcc --disable-sse2"
|
||||
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
||||
config[3]+=" --disable-avx2 --enable-pic"
|
||||
|
||||
arch[4]="arm64-v8a"
|
||||
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
|
||||
|
||||
arch[5]="x86_64"
|
||||
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
|
||||
config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
||||
config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
|
||||
|
||||
arch[6]="mips64"
|
||||
config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk"
|
||||
config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
|
||||
|
||||
limit=$((${#arch[@]} - 1))
|
||||
|
||||
@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do
|
||||
# configure and make
|
||||
echo "build_android_configs: "
|
||||
echo "configure ${config[${i}]} ${common_params}"
|
||||
../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
|
||||
-isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
|
||||
-isystem $ndk/sysroot/usr/include \
|
||||
"
|
||||
../../libvpx/configure ${config[${i}]} ${common_params}
|
||||
rm -f libvpx_srcs.txt
|
||||
for f in ${allowed_files}; do
|
||||
# the build system supports multiple different configurations. avoid
|
||||
|
@ -60,6 +60,7 @@
|
||||
|
||||
// JNI references for VpxOutputBuffer class.
|
||||
static jmethodID initForYuvFrame;
|
||||
static jmethodID initForPrivateFrame;
|
||||
static jfieldID dataField;
|
||||
static jfieldID outputModeField;
|
||||
static jfieldID decoderPrivateField;
|
||||
@ -481,6 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
||||
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
|
||||
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
|
||||
"(IIIII)Z");
|
||||
initForPrivateFrame =
|
||||
env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
|
||||
dataField = env->GetFieldID(outputBufferClass, "data",
|
||||
"Ljava/nio/ByteBuffer;");
|
||||
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||
@ -602,6 +605,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
||||
}
|
||||
jfb->d_w = img->d_w;
|
||||
jfb->d_h = img->d_h;
|
||||
env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h);
|
||||
if (env->ExceptionCheck()) {
|
||||
return -1;
|
||||
}
|
||||
env->SetIntField(jOutputBuffer, decoderPrivateField,
|
||||
id + kDecoderPrivateBase);
|
||||
}
|
||||
|
@ -146,8 +146,8 @@ public final class C {
|
||||
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
|
||||
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
|
||||
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
|
||||
* #ENCODING_E_AC3}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or
|
||||
* {@link #ENCODING_DOLBY_TRUEHD}.
|
||||
* #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
|
||||
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@ -163,6 +163,7 @@ public final class C {
|
||||
ENCODING_PCM_A_LAW,
|
||||
ENCODING_AC3,
|
||||
ENCODING_E_AC3,
|
||||
ENCODING_E_AC3_JOC,
|
||||
ENCODING_AC4,
|
||||
ENCODING_DTS,
|
||||
ENCODING_DTS_HD,
|
||||
@ -210,6 +211,8 @@ public final class C {
|
||||
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
||||
/** @see AudioFormat#ENCODING_E_AC3 */
|
||||
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
|
||||
/** @see AudioFormat#ENCODING_E_AC3_JOC */
|
||||
public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
|
||||
/** @see AudioFormat#ENCODING_AC4 */
|
||||
public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
|
||||
/** @see AudioFormat#ENCODING_DTS */
|
||||
|
@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
@ -90,6 +91,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
@ExtensionRendererMode private int extensionRendererMode;
|
||||
private long allowedVideoJoiningTimeMs;
|
||||
private boolean playClearSamplesWithoutKeys;
|
||||
private boolean enableDecoderFallback;
|
||||
private MediaCodecSelector mediaCodecSelector;
|
||||
|
||||
/** @param context A {@link Context}. */
|
||||
@ -202,6 +204,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to enable fallback to lower-priority decoders if decoder initialization fails.
|
||||
* This may result in using a decoder that is less efficient or slower than the primary decoder.
|
||||
*
|
||||
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
|
||||
* initialization fails.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) {
|
||||
this.enableDecoderFallback = enableDecoderFallback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
|
||||
*
|
||||
@ -248,6 +263,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
enableDecoderFallback,
|
||||
eventHandler,
|
||||
videoRendererEventListener,
|
||||
allowedVideoJoiningTimeMs,
|
||||
@ -258,6 +274,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
enableDecoderFallback,
|
||||
buildAudioProcessors(),
|
||||
eventHandler,
|
||||
audioRendererEventListener,
|
||||
@ -282,6 +299,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
|
||||
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||
* the media.
|
||||
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
|
||||
* initialization fails. This may result in using a decoder that is slower/less efficient than
|
||||
* the primary decoder.
|
||||
* @param eventHandler A handler associated with the main thread's looper.
|
||||
* @param eventListener An event listener.
|
||||
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
||||
@ -294,6 +314,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
boolean enableDecoderFallback,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
@ -305,6 +326,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
allowedVideoJoiningTimeMs,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
enableDecoderFallback,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
|
||||
@ -356,6 +378,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
|
||||
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||
* the media.
|
||||
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
|
||||
* initialization fails. This may result in using a decoder that is slower/less efficient than
|
||||
* the primary decoder.
|
||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
|
||||
* before output. May be empty.
|
||||
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
||||
@ -368,6 +393,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
boolean enableDecoderFallback,
|
||||
AudioProcessor[] audioProcessors,
|
||||
Handler eventHandler,
|
||||
AudioRendererEventListener eventListener,
|
||||
@ -378,10 +404,10 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
enableDecoderFallback,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
AudioCapabilities.getCapabilities(context),
|
||||
audioProcessors));
|
||||
new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
|
||||
|
||||
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
|
||||
return;
|
||||
|
@ -510,7 +510,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
@Override
|
||||
public long getTotalBufferedDuration() {
|
||||
return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs));
|
||||
return C.usToMs(playbackInfo.totalBufferedDurationUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -729,13 +729,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
newPlayingPeriodHolder = queue.advancePlayingPeriod();
|
||||
}
|
||||
|
||||
// Disable all the renderers if the period being played is changing, or if forced.
|
||||
if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) {
|
||||
// Disable all renderers if the period being played is changing, if the seek results in negative
|
||||
// renderer timestamps, or if forced.
|
||||
if (forceDisableRenderers
|
||||
|| oldPlayingPeriodHolder != newPlayingPeriodHolder
|
||||
|| (newPlayingPeriodHolder != null
|
||||
&& newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
|
||||
for (Renderer renderer : enabledRenderers) {
|
||||
disableRenderer(renderer);
|
||||
}
|
||||
enabledRenderers = new Renderer[0];
|
||||
oldPlayingPeriodHolder = null;
|
||||
if (newPlayingPeriodHolder != null) {
|
||||
newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the holders.
|
||||
@ -1798,9 +1805,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
|
||||
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
||||
return loadingPeriodHolder == null
|
||||
? 0
|
||||
: bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
|
||||
if (loadingPeriodHolder == null) {
|
||||
return 0;
|
||||
}
|
||||
long totalBufferedDurationUs =
|
||||
bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
|
||||
return Math.max(0, totalBufferedDurationUs);
|
||||
}
|
||||
|
||||
private void updateLoadControlTrackSelection(
|
||||
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.10.1";
|
||||
public static final String VERSION = "2.10.2";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.2";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2010001;
|
||||
public static final int VERSION_INT = 2010002;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
@ -67,8 +67,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
* Creates a new holder with information required to play it as part of a timeline.
|
||||
*
|
||||
* @param rendererCapabilities The renderer capabilities.
|
||||
* @param rendererPositionOffsetUs The time offset of the start of the media period to provide to
|
||||
* renderers.
|
||||
* @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
|
||||
* @param trackSelector The track selector.
|
||||
* @param allocator The allocator.
|
||||
* @param mediaSource The media source that produced the media period.
|
||||
@ -82,7 +81,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
MediaSource mediaSource,
|
||||
MediaPeriodInfo info) {
|
||||
this.rendererCapabilities = rendererCapabilities;
|
||||
this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs;
|
||||
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
||||
this.trackSelector = trackSelector;
|
||||
this.mediaSource = mediaSource;
|
||||
this.uid = info.id.periodUid;
|
||||
@ -115,6 +114,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
return rendererPositionOffsetUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the renderer time of the start of the period, in microseconds.
|
||||
*
|
||||
* @param rendererPositionOffsetUs The new renderer position offset, in microseconds.
|
||||
*/
|
||||
public void setRendererOffset(long rendererPositionOffsetUs) {
|
||||
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
||||
}
|
||||
|
||||
/** Returns start position of period in renderer time. */
|
||||
public long getStartPositionRendererTime() {
|
||||
return info.startPositionUs + rendererPositionOffsetUs;
|
||||
|
@ -144,8 +144,8 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
MediaPeriodInfo info) {
|
||||
long rendererPositionOffsetUs =
|
||||
loading == null
|
||||
? info.startPositionUs
|
||||
: (loading.getRendererOffset() + loading.info.durationUs);
|
||||
? (info.id.isAd() ? info.contentPositionUs : 0)
|
||||
: (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
|
||||
MediaPeriodHolder newPeriodHolder =
|
||||
new MediaPeriodHolder(
|
||||
rendererCapabilities,
|
||||
|
@ -1125,6 +1125,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
case C.ENCODING_AC3:
|
||||
return 640 * 1000 / 8;
|
||||
case C.ENCODING_E_AC3:
|
||||
case C.ENCODING_E_AC3_JOC:
|
||||
return 6144 * 1000 / 8;
|
||||
case C.ENCODING_AC4:
|
||||
return 2688 * 1000 / 8;
|
||||
@ -1154,7 +1155,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
return DtsUtil.parseDtsAudioSampleCount(buffer);
|
||||
} else if (encoding == C.ENCODING_AC3) {
|
||||
return Ac3Util.getAc3SyncframeAudioSampleCount();
|
||||
} else if (encoding == C.ENCODING_E_AC3) {
|
||||
} else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) {
|
||||
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
|
||||
} else if (encoding == C.ENCODING_AC4) {
|
||||
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
|
||||
@ -1177,11 +1178,10 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
@TargetApi(21)
|
||||
private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size,
|
||||
long presentationTimeUs) {
|
||||
// TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed.
|
||||
// if (Util.SDK_INT >= 23) {
|
||||
// // The underlying platform AudioTrack writes AV sync headers directly.
|
||||
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
|
||||
// }
|
||||
if (Util.SDK_INT >= 26) {
|
||||
// The underlying platform AudioTrack writes AV sync headers directly.
|
||||
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
|
||||
}
|
||||
if (avSyncHeader == null) {
|
||||
avSyncHeader = ByteBuffer.allocate(16);
|
||||
avSyncHeader.order(ByteOrder.BIG_ENDIAN);
|
||||
|
@ -245,12 +245,50 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink) {
|
||||
this(
|
||||
context,
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
/* enableDecoderFallback= */ false,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
audioSink);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context A context.
|
||||
* @param mediaCodecSelector A decoder selector.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
|
||||
* initialization fails. This may result in using a decoder that is slower/less efficient than
|
||||
* the primary decoder.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioSink The sink to which audio will be output.
|
||||
*/
|
||||
public MediaCodecAudioRenderer(
|
||||
Context context,
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
boolean enableDecoderFallback,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink) {
|
||||
super(
|
||||
C.TRACK_TYPE_AUDIO,
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
/* enableDecoderFallback= */ false,
|
||||
enableDecoderFallback,
|
||||
/* assumedMinimumCodecOperatingRate= */ 44100);
|
||||
this.context = context.getApplicationContext();
|
||||
this.audioSink = audioSink;
|
||||
@ -341,7 +379,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
* @return Whether passthrough playback is supported.
|
||||
*/
|
||||
protected boolean allowPassthrough(int channelCount, String mimeType) {
|
||||
return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType));
|
||||
return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -437,11 +475,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
@C.Encoding int encoding;
|
||||
MediaFormat format;
|
||||
if (passthroughMediaFormat != null) {
|
||||
encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME));
|
||||
format = passthroughMediaFormat;
|
||||
encoding =
|
||||
getPassthroughEncoding(
|
||||
format.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
|
||||
format.getString(MediaFormat.KEY_MIME));
|
||||
} else {
|
||||
encoding = pcmEncoding;
|
||||
format = outputFormat;
|
||||
encoding = pcmEncoding;
|
||||
}
|
||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||
@ -463,6 +504,28 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link
|
||||
* C#ENCODING_INVALID} if passthrough is not possible.
|
||||
*/
|
||||
@C.Encoding
|
||||
protected int getPassthroughEncoding(int channelCount, String mimeType) {
|
||||
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {
|
||||
if (audioSink.supportsOutput(channelCount, C.ENCODING_E_AC3_JOC)) {
|
||||
return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC);
|
||||
}
|
||||
// E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back.
|
||||
mimeType = MimeTypes.AUDIO_E_AC3;
|
||||
}
|
||||
|
||||
@C.Encoding int encoding = MimeTypes.getEncoding(mimeType);
|
||||
if (audioSink.supportsOutput(channelCount, encoding)) {
|
||||
return encoding;
|
||||
} else {
|
||||
return C.ENCODING_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the audio session id becomes known. The default implementation is a no-op. One
|
||||
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
|
||||
|
@ -53,7 +53,6 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -454,15 +453,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
* @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
|
||||
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
|
||||
* no codec operating rate should be set.
|
||||
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
|
||||
*/
|
||||
protected abstract void configureCodec(
|
||||
MediaCodecInfo codecInfo,
|
||||
MediaCodec codec,
|
||||
Format format,
|
||||
MediaCrypto crypto,
|
||||
float codecOperatingRate)
|
||||
throws DecoderQueryException;
|
||||
float codecOperatingRate);
|
||||
|
||||
protected final void maybeInitCodec() throws ExoPlaybackException {
|
||||
if (codec != null || inputFormat == null) {
|
||||
@ -742,11 +739,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
try {
|
||||
List<MediaCodecInfo> allAvailableCodecInfos =
|
||||
getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);
|
||||
availableCodecInfos = new ArrayDeque<>();
|
||||
if (enableDecoderFallback) {
|
||||
availableCodecInfos = new ArrayDeque<>(allAvailableCodecInfos);
|
||||
} else {
|
||||
availableCodecInfos =
|
||||
new ArrayDeque<>(Collections.singletonList(allAvailableCodecInfos.get(0)));
|
||||
availableCodecInfos.addAll(allAvailableCodecInfos);
|
||||
} else if (!allAvailableCodecInfos.isEmpty()) {
|
||||
availableCodecInfos.add(allAvailableCodecInfos.get(0));
|
||||
}
|
||||
preferredDecoderInitializationException = null;
|
||||
} catch (DecoderQueryException e) {
|
||||
|
@ -176,7 +176,7 @@ public final class MediaCodecUtil {
|
||||
// E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
|
||||
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling);
|
||||
ArrayList<MediaCodecInfo> eac3DecoderInfos =
|
||||
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
|
||||
getDecoderInfosInternal(eac3Key, mediaCodecList, MimeTypes.AUDIO_E_AC3);
|
||||
decoderInfos.addAll(eac3DecoderInfos);
|
||||
}
|
||||
applyWorkarounds(mimeType, decoderInfos);
|
||||
|
@ -951,6 +951,7 @@ public final class DownloadHelper {
|
||||
downloadHelper.onMediaPrepared();
|
||||
return true;
|
||||
case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:
|
||||
release();
|
||||
downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj));
|
||||
return true;
|
||||
default:
|
||||
|
@ -27,6 +27,7 @@ import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import androidx.annotation.IntDef;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -128,7 +129,7 @@ public final class Requirements implements Parcelable {
|
||||
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
|
||||
NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo();
|
||||
if (networkInfo == null
|
||||
|| !networkInfo.isConnected()
|
||||
|| !isInternetConnectivityValidated(connectivityManager)) {
|
||||
|
@ -28,6 +28,7 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
@ -126,7 +127,8 @@ public final class RequirementsWatcher {
|
||||
@TargetApi(23)
|
||||
private void registerNetworkCallbackV23() {
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
Assertions.checkNotNull(
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
|
||||
NetworkRequest request =
|
||||
new NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
|
@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright (C) 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.source;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/** Media source with a single period consisting of silent raw audio of a given duration. */
|
||||
public final class SilenceMediaSource extends BaseMediaSource {
|
||||
|
||||
private static final int SAMPLE_RATE_HZ = 44100;
|
||||
@C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT;
|
||||
private static final int CHANNEL_COUNT = 2;
|
||||
private static final Format FORMAT =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id=*/ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* maxInputSize= */ Format.NO_VALUE,
|
||||
CHANNEL_COUNT,
|
||||
SAMPLE_RATE_HZ,
|
||||
ENCODING,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
private static final byte[] SILENCE_SAMPLE =
|
||||
new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024];
|
||||
|
||||
private final long durationUs;
|
||||
|
||||
/**
|
||||
* Creates a new media source providing silent audio of the given duration.
|
||||
*
|
||||
* @param durationUs The duration of silent audio to output, in microseconds.
|
||||
*/
|
||||
public SilenceMediaSource(long durationUs) {
|
||||
Assertions.checkArgument(durationUs >= 0);
|
||||
this.durationUs = durationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
refreshSourceInfo(
|
||||
new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false),
|
||||
/* manifest= */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() {}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return new SilenceMediaPeriod(durationUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {}
|
||||
|
||||
@Override
|
||||
public void releaseSourceInternal() {}
|
||||
|
||||
private static final class SilenceMediaPeriod implements MediaPeriod {
|
||||
|
||||
private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT));
|
||||
|
||||
private final long durationUs;
|
||||
private final ArrayList<SampleStream> sampleStreams;
|
||||
|
||||
public SilenceMediaPeriod(long durationUs) {
|
||||
this.durationUs = durationUs;
|
||||
sampleStreams = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Callback callback, long positionUs) {
|
||||
callback.onPrepared(/* mediaPeriod= */ this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowPrepareError() {}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getTrackGroups() {
|
||||
return TRACKS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long selectTracks(
|
||||
@NullableType TrackSelection[] selections,
|
||||
boolean[] mayRetainStreamFlags,
|
||||
@NullableType SampleStream[] streams,
|
||||
boolean[] streamResetFlags,
|
||||
long positionUs) {
|
||||
for (int i = 0; i < selections.length; i++) {
|
||||
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
|
||||
sampleStreams.remove(streams[i]);
|
||||
streams[i] = null;
|
||||
}
|
||||
if (streams[i] == null && selections[i] != null) {
|
||||
SilenceSampleStream stream = new SilenceSampleStream(durationUs);
|
||||
stream.seekTo(positionUs);
|
||||
sampleStreams.add(stream);
|
||||
streams[i] = stream;
|
||||
streamResetFlags[i] = true;
|
||||
}
|
||||
}
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discardBuffer(long positionUs, boolean toKeyframe) {}
|
||||
|
||||
@Override
|
||||
public long readDiscontinuity() {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long seekToUs(long positionUs) {
|
||||
for (int i = 0; i < sampleStreams.size(); i++) {
|
||||
((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);
|
||||
}
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextLoadPositionUs() {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueLoading(long positionUs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reevaluateBuffer(long positionUs) {}
|
||||
}
|
||||
|
||||
private static final class SilenceSampleStream implements SampleStream {
|
||||
|
||||
private final long durationBytes;
|
||||
|
||||
private boolean sentFormat;
|
||||
private long positionBytes;
|
||||
|
||||
public SilenceSampleStream(long durationUs) {
|
||||
durationBytes = getAudioByteCount(durationUs);
|
||||
seekTo(0);
|
||||
}
|
||||
|
||||
public void seekTo(long positionUs) {
|
||||
positionBytes = getAudioByteCount(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowError() {}
|
||||
|
||||
@Override
|
||||
public int readData(
|
||||
FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {
|
||||
if (!sentFormat || formatRequired) {
|
||||
formatHolder.format = FORMAT;
|
||||
sentFormat = true;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
|
||||
long bytesRemaining = durationBytes - positionBytes;
|
||||
if (bytesRemaining == 0) {
|
||||
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
|
||||
int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining);
|
||||
buffer.ensureSpaceForWrite(bytesToWrite);
|
||||
buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
|
||||
buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite);
|
||||
buffer.timeUs = getAudioPositionUs(positionBytes);
|
||||
positionBytes += bytesToWrite;
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int skipData(long positionUs) {
|
||||
long oldPositionBytes = positionBytes;
|
||||
seekTo(positionUs);
|
||||
return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length);
|
||||
}
|
||||
}
|
||||
|
||||
private static long getAudioByteCount(long durationUs) {
|
||||
long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND;
|
||||
return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount;
|
||||
}
|
||||
|
||||
private static long getAudioPositionUs(long bytes) {
|
||||
long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT);
|
||||
return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ;
|
||||
}
|
||||
}
|
@ -80,6 +80,11 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
* at which point the non-displayed memory becomes the displayed memory (and vice versa).
|
||||
*/
|
||||
private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20;
|
||||
|
||||
private static final byte CTRL_BACKSPACE = 0x21;
|
||||
|
||||
private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
|
||||
|
||||
/**
|
||||
* Command initiating roll-up style captioning, with the maximum of 2 rows displayed
|
||||
* simultaneously.
|
||||
@ -95,25 +100,31 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
* simultaneously.
|
||||
*/
|
||||
private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
|
||||
|
||||
/**
|
||||
* Command initiating paint-on style captioning. Subsequent data should be addressed immediately
|
||||
* to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.
|
||||
*/
|
||||
private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;
|
||||
/**
|
||||
* Command indicating the end of a pop-on style caption. At this point the caption loaded in
|
||||
* non-displayed memory should be swapped with the one in displayed memory. If no
|
||||
* {@link #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the
|
||||
* receiver into pop-on style.
|
||||
* TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out
|
||||
* until a command is received that switches back to the CAPTION service.
|
||||
*/
|
||||
private static final byte CTRL_END_OF_CAPTION = 0x2F;
|
||||
private static final byte CTRL_TEXT_RESTART = 0x2A;
|
||||
|
||||
private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B;
|
||||
|
||||
private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
|
||||
private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
|
||||
private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
|
||||
private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
|
||||
|
||||
private static final byte CTRL_BACKSPACE = 0x21;
|
||||
/**
|
||||
* Command indicating the end of a pop-on style caption. At this point the caption loaded in
|
||||
* non-displayed memory should be swapped with the one in displayed memory. If no {@link
|
||||
* #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into
|
||||
* pop-on style.
|
||||
*/
|
||||
private static final byte CTRL_END_OF_CAPTION = 0x2F;
|
||||
|
||||
// Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
|
||||
private static final int[] BASIC_CHARACTER_SET = new int[] {
|
||||
@ -237,6 +248,11 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
private byte repeatableControlCc2;
|
||||
private int currentChannel;
|
||||
|
||||
// The incoming characters may belong to 3 different services based on the last received control
|
||||
// codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning
|
||||
// service bytes and drops the rest.
|
||||
private boolean isInCaptionService;
|
||||
|
||||
public Cea608Decoder(String mimeType, int accessibilityChannel) {
|
||||
ccData = new ParsableByteArray();
|
||||
cueBuilders = new ArrayList<>();
|
||||
@ -268,6 +284,7 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
|
||||
setCaptionMode(CC_MODE_UNKNOWN);
|
||||
resetCueBuilders();
|
||||
isInCaptionService = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -288,6 +305,7 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
repeatableControlCc1 = 0;
|
||||
repeatableControlCc2 = 0;
|
||||
currentChannel = NTSC_CC_CHANNEL_1;
|
||||
isInCaptionService = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -363,6 +381,12 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
continue;
|
||||
}
|
||||
|
||||
maybeUpdateIsInCaptionService(ccData1, ccData2);
|
||||
if (!isInCaptionService) {
|
||||
// Only the Captioning service is supported. Drop all other bytes.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special North American character set.
|
||||
// ccData1 - 0|0|0|1|C|0|0|1
|
||||
// ccData2 - 0|0|1|1|X|X|X|X
|
||||
@ -629,6 +653,29 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
cueBuilders.add(currentCueBuilder);
|
||||
}
|
||||
|
||||
private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) {
|
||||
if (isXdsControlCode(cc1)) {
|
||||
isInCaptionService = false;
|
||||
} else if (isServiceSwitchCommand(cc1)) {
|
||||
switch (cc2) {
|
||||
case CTRL_TEXT_RESTART:
|
||||
case CTRL_RESUME_TEXT_DISPLAY:
|
||||
isInCaptionService = false;
|
||||
break;
|
||||
case CTRL_END_OF_CAPTION:
|
||||
case CTRL_RESUME_CAPTION_LOADING:
|
||||
case CTRL_RESUME_DIRECT_CAPTIONING:
|
||||
case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
|
||||
case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
|
||||
case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
|
||||
isInCaptionService = true;
|
||||
break;
|
||||
default:
|
||||
// No update.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static char getChar(byte ccData) {
|
||||
int index = (ccData & 0x7F) - 0x20;
|
||||
return (char) BASIC_CHARACTER_SET[index];
|
||||
@ -683,6 +730,15 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
return (cc1 & 0xF0) == 0x10;
|
||||
}
|
||||
|
||||
private static boolean isXdsControlCode(byte cc1) {
|
||||
return 0x01 <= cc1 && cc1 <= 0x0F;
|
||||
}
|
||||
|
||||
private static boolean isServiceSwitchCommand(byte cc1) {
|
||||
// cc1 - 0|0|0|1|C|1|0|0
|
||||
return (cc1 & 0xF7) == 0x14;
|
||||
}
|
||||
|
||||
private static class CueBuilder {
|
||||
|
||||
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
|
||||
|
@ -429,6 +429,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
/* lineType= */ Cue.LINE_TYPE_FRACTION,
|
||||
lineAnchor,
|
||||
width,
|
||||
height,
|
||||
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
|
||||
/* textSize= */ regionTextHeight);
|
||||
}
|
||||
|
@ -231,11 +231,11 @@ import java.util.TreeSet;
|
||||
new Cue(
|
||||
bitmap,
|
||||
region.position,
|
||||
Cue.ANCHOR_TYPE_MIDDLE,
|
||||
Cue.ANCHOR_TYPE_START,
|
||||
region.line,
|
||||
region.lineAnchor,
|
||||
region.width,
|
||||
/* height= */ Cue.DIMEN_UNSET));
|
||||
region.height));
|
||||
}
|
||||
|
||||
// Create text based cues.
|
||||
|
@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue;
|
||||
public final @Cue.LineType int lineType;
|
||||
public final @Cue.AnchorType int lineAnchor;
|
||||
public final float width;
|
||||
public final float height;
|
||||
public final @Cue.TextSizeType int textSizeType;
|
||||
public final float textSize;
|
||||
|
||||
@ -39,6 +40,7 @@ import com.google.android.exoplayer2.text.Cue;
|
||||
/* lineType= */ Cue.TYPE_UNSET,
|
||||
/* lineAnchor= */ Cue.TYPE_UNSET,
|
||||
/* width= */ Cue.DIMEN_UNSET,
|
||||
/* height= */ Cue.DIMEN_UNSET,
|
||||
/* textSizeType= */ Cue.TYPE_UNSET,
|
||||
/* textSize= */ Cue.DIMEN_UNSET);
|
||||
}
|
||||
@ -50,6 +52,7 @@ import com.google.android.exoplayer2.text.Cue;
|
||||
@Cue.LineType int lineType,
|
||||
@Cue.AnchorType int lineAnchor,
|
||||
float width,
|
||||
float height,
|
||||
int textSizeType,
|
||||
float textSize) {
|
||||
this.id = id;
|
||||
@ -58,6 +61,7 @@ import com.google.android.exoplayer2.text.Cue;
|
||||
this.lineType = lineType;
|
||||
this.lineAnchor = lineAnchor;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.textSizeType = textSizeType;
|
||||
this.textSize = textSize;
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import com.google.android.exoplayer2.util.ColorParser;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -35,8 +37,8 @@ import java.util.regex.Pattern;
|
||||
private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
|
||||
private static final String VALUE_BOLD = "bold";
|
||||
private static final String VALUE_UNDERLINE = "underline";
|
||||
private static final String BLOCK_START = "{";
|
||||
private static final String BLOCK_END = "}";
|
||||
private static final String RULE_START = "{";
|
||||
private static final String RULE_END = "}";
|
||||
private static final String PROPERTY_FONT_STYLE = "font-style";
|
||||
private static final String VALUE_ITALIC = "italic";
|
||||
|
||||
@ -52,22 +54,27 @@ import java.util.regex.Pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the
|
||||
* contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or
|
||||
* {@code null} otherwise.
|
||||
* Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents
|
||||
* of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If
|
||||
* parsing fails, it returns a list including only the styles which have been successfully parsed
|
||||
* up to the style rule which was malformed.
|
||||
*
|
||||
* @param input The input from which the style block should be read.
|
||||
* @return A {@link WebvttCssStyle} that represents the parsed block.
|
||||
* @return A list of {@link WebvttCssStyle}s that represents the parsed block, or a list
|
||||
* containing the styles up to the parsing failure.
|
||||
*/
|
||||
public WebvttCssStyle parseBlock(ParsableByteArray input) {
|
||||
public List<WebvttCssStyle> parseBlock(ParsableByteArray input) {
|
||||
stringBuilder.setLength(0);
|
||||
int initialInputPosition = input.getPosition();
|
||||
skipStyleBlock(input);
|
||||
styleInput.reset(input.data, input.getPosition());
|
||||
styleInput.setPosition(initialInputPosition);
|
||||
String selector = parseSelector(styleInput, stringBuilder);
|
||||
if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
|
||||
return null;
|
||||
|
||||
List<WebvttCssStyle> styles = new ArrayList<>();
|
||||
String selector;
|
||||
while ((selector = parseSelector(styleInput, stringBuilder)) != null) {
|
||||
if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) {
|
||||
return styles;
|
||||
}
|
||||
WebvttCssStyle style = new WebvttCssStyle();
|
||||
applySelectorToStyle(style, selector);
|
||||
@ -76,13 +83,18 @@ import java.util.regex.Pattern;
|
||||
while (!blockEndFound) {
|
||||
int position = styleInput.getPosition();
|
||||
token = parseNextToken(styleInput, stringBuilder);
|
||||
blockEndFound = token == null || BLOCK_END.equals(token);
|
||||
blockEndFound = token == null || RULE_END.equals(token);
|
||||
if (!blockEndFound) {
|
||||
styleInput.setPosition(position);
|
||||
parseStyleDeclaration(styleInput, style, stringBuilder);
|
||||
}
|
||||
}
|
||||
return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
|
||||
// Check that the style rule ended correctly.
|
||||
if (RULE_END.equals(token)) {
|
||||
styles.add(style);
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,7 +119,7 @@ import java.util.regex.Pattern;
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
if (BLOCK_START.equals(token)) {
|
||||
if (RULE_START.equals(token)) {
|
||||
input.setPosition(position);
|
||||
return "";
|
||||
}
|
||||
@ -156,7 +168,7 @@ import java.util.regex.Pattern;
|
||||
String token = parseNextToken(input, stringBuilder);
|
||||
if (";".equals(token)) {
|
||||
// The style declaration is well formed.
|
||||
} else if (BLOCK_END.equals(token)) {
|
||||
} else if (RULE_END.equals(token)) {
|
||||
// The style declaration is well formed and we can go on, but the closing bracket had to be
|
||||
// fed back.
|
||||
input.setPosition(position);
|
||||
@ -250,7 +262,7 @@ import java.util.regex.Pattern;
|
||||
// Syntax error.
|
||||
return null;
|
||||
}
|
||||
if (BLOCK_END.equals(token) || ";".equals(token)) {
|
||||
if (RULE_END.equals(token) || ";".equals(token)) {
|
||||
input.setPosition(position);
|
||||
expressionEndFound = true;
|
||||
} else {
|
||||
|
@ -80,10 +80,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
||||
throw new SubtitleDecoderException("A style block was found after the first cue.");
|
||||
}
|
||||
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
||||
WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
|
||||
if (styleBlock != null) {
|
||||
definedStyles.add(styleBlock);
|
||||
}
|
||||
definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
|
||||
} else if (event == EVENT_CUE) {
|
||||
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
|
||||
subtitles.add(webvttCueBuilder.build());
|
||||
|
@ -757,7 +757,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
logValues[i] = new double[values[i].length];
|
||||
for (int j = 0; j < values[i].length; j++) {
|
||||
logValues[i][j] = Math.log(values[i][j]);
|
||||
logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]);
|
||||
}
|
||||
}
|
||||
return logValues;
|
||||
@ -779,7 +779,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||
double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];
|
||||
for (int j = 0; j < logBitrates[i].length - 1; j++) {
|
||||
double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]);
|
||||
switchPoints[i][j] = (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;
|
||||
switchPoints[i][j] =
|
||||
totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;
|
||||
}
|
||||
}
|
||||
return switchPoints;
|
||||
|
@ -1934,6 +1934,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
getAdaptiveAudioTracks(
|
||||
selectedGroup,
|
||||
formatSupports[selectedGroupIndex],
|
||||
params.maxAudioBitrate,
|
||||
params.allowAudioMixedMimeTypeAdaptiveness,
|
||||
params.allowAudioMixedSampleRateAdaptiveness);
|
||||
if (adaptiveTracks.length > 0) {
|
||||
@ -1951,6 +1952,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
private static int[] getAdaptiveAudioTracks(
|
||||
TrackGroup group,
|
||||
int[] formatSupport,
|
||||
int maxAudioBitrate,
|
||||
boolean allowMixedMimeTypeAdaptiveness,
|
||||
boolean allowMixedSampleRateAdaptiveness) {
|
||||
int selectedConfigurationTrackCount = 0;
|
||||
@ -1967,6 +1969,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
group,
|
||||
formatSupport,
|
||||
configuration,
|
||||
maxAudioBitrate,
|
||||
allowMixedMimeTypeAdaptiveness,
|
||||
allowMixedSampleRateAdaptiveness);
|
||||
if (configurationCount > selectedConfigurationTrackCount) {
|
||||
@ -1977,13 +1980,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
}
|
||||
|
||||
if (selectedConfigurationTrackCount > 1) {
|
||||
Assertions.checkNotNull(selectedConfiguration);
|
||||
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
|
||||
int index = 0;
|
||||
for (int i = 0; i < group.length; i++) {
|
||||
Format format = group.getFormat(i);
|
||||
if (isSupportedAdaptiveAudioTrack(
|
||||
group.getFormat(i),
|
||||
format,
|
||||
formatSupport[i],
|
||||
Assertions.checkNotNull(selectedConfiguration),
|
||||
selectedConfiguration,
|
||||
maxAudioBitrate,
|
||||
allowMixedMimeTypeAdaptiveness,
|
||||
allowMixedSampleRateAdaptiveness)) {
|
||||
adaptiveIndices[index++] = i;
|
||||
@ -1998,6 +2004,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
TrackGroup group,
|
||||
int[] formatSupport,
|
||||
AudioConfigurationTuple configuration,
|
||||
int maxAudioBitrate,
|
||||
boolean allowMixedMimeTypeAdaptiveness,
|
||||
boolean allowMixedSampleRateAdaptiveness) {
|
||||
int count = 0;
|
||||
@ -2006,6 +2013,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
group.getFormat(i),
|
||||
formatSupport[i],
|
||||
configuration,
|
||||
maxAudioBitrate,
|
||||
allowMixedMimeTypeAdaptiveness,
|
||||
allowMixedSampleRateAdaptiveness)) {
|
||||
count++;
|
||||
@ -2018,9 +2026,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
Format format,
|
||||
int formatSupport,
|
||||
AudioConfigurationTuple configuration,
|
||||
int maxAudioBitrate,
|
||||
boolean allowMixedMimeTypeAdaptiveness,
|
||||
boolean allowMixedSampleRateAdaptiveness) {
|
||||
return isSupported(formatSupport, false)
|
||||
&& (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate)
|
||||
&& (format.channelCount != Format.NO_VALUE
|
||||
&& format.channelCount == configuration.channelCount)
|
||||
&& (allowMixedMimeTypeAdaptiveness
|
||||
|
@ -42,6 +42,7 @@ import java.util.Map;
|
||||
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
|
||||
* explicit dependency on ExoPlayer's RTMP extension.
|
||||
* <li>data: For parsing data inlined in the URI as defined in RFC 2397.
|
||||
* <li>udp: For fetching data over UDP (e.g. udp://something.com/media).
|
||||
* <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4),
|
||||
* if constructed using {@link #DefaultDataSource(Context, TransferListener, String,
|
||||
* boolean)}, or any other schemes supported by a base data source if constructed using {@link
|
||||
@ -55,6 +56,7 @@ public final class DefaultDataSource implements DataSource {
|
||||
private static final String SCHEME_ASSET = "asset";
|
||||
private static final String SCHEME_CONTENT = "content";
|
||||
private static final String SCHEME_RTMP = "rtmp";
|
||||
private static final String SCHEME_UDP = "udp";
|
||||
private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
|
||||
|
||||
private final Context context;
|
||||
@ -62,12 +64,13 @@ public final class DefaultDataSource implements DataSource {
|
||||
private final DataSource baseDataSource;
|
||||
|
||||
// Lazily initialized.
|
||||
private @Nullable DataSource fileDataSource;
|
||||
private @Nullable DataSource assetDataSource;
|
||||
private @Nullable DataSource contentDataSource;
|
||||
private @Nullable DataSource rtmpDataSource;
|
||||
private @Nullable DataSource dataSchemeDataSource;
|
||||
private @Nullable DataSource rawResourceDataSource;
|
||||
@Nullable private DataSource fileDataSource;
|
||||
@Nullable private DataSource assetDataSource;
|
||||
@Nullable private DataSource contentDataSource;
|
||||
@Nullable private DataSource rtmpDataSource;
|
||||
@Nullable private DataSource udpDataSource;
|
||||
@Nullable private DataSource dataSchemeDataSource;
|
||||
@Nullable private DataSource rawResourceDataSource;
|
||||
|
||||
private @Nullable DataSource dataSource;
|
||||
|
||||
@ -218,6 +221,7 @@ public final class DefaultDataSource implements DataSource {
|
||||
maybeAddListenerToDataSource(assetDataSource, transferListener);
|
||||
maybeAddListenerToDataSource(contentDataSource, transferListener);
|
||||
maybeAddListenerToDataSource(rtmpDataSource, transferListener);
|
||||
maybeAddListenerToDataSource(udpDataSource, transferListener);
|
||||
maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);
|
||||
maybeAddListenerToDataSource(rawResourceDataSource, transferListener);
|
||||
}
|
||||
@ -240,6 +244,8 @@ public final class DefaultDataSource implements DataSource {
|
||||
dataSource = getContentDataSource();
|
||||
} else if (SCHEME_RTMP.equals(scheme)) {
|
||||
dataSource = getRtmpDataSource();
|
||||
} else if (SCHEME_UDP.equals(scheme)) {
|
||||
dataSource = getUdpDataSource();
|
||||
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
|
||||
dataSource = getDataSchemeDataSource();
|
||||
} else if (SCHEME_RAW.equals(scheme)) {
|
||||
@ -277,6 +283,14 @@ public final class DefaultDataSource implements DataSource {
|
||||
}
|
||||
}
|
||||
|
||||
private DataSource getUdpDataSource() {
|
||||
if (udpDataSource == null) {
|
||||
udpDataSource = new UdpDataSource();
|
||||
addListenersToDataSource(udpDataSource);
|
||||
}
|
||||
return udpDataSource;
|
||||
}
|
||||
|
||||
private DataSource getFileDataSource() {
|
||||
if (fileDataSource == null) {
|
||||
fileDataSource = new FileDataSource();
|
||||
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 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.upstream;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** {@link DataSource} wrapper allowing just-in-time resolution of {@link DataSpec DataSpecs}. */
|
||||
public final class ResolvingDataSource implements DataSource {
|
||||
|
||||
/** Resolves {@link DataSpec DataSpecs}. */
|
||||
public interface Resolver {
|
||||
|
||||
/**
|
||||
* Resolves a {@link DataSpec} before forwarding it to the wrapped {@link DataSource}. This
|
||||
* method is allowed to block until the {@link DataSpec} has been resolved.
|
||||
*
|
||||
* <p>Note that this method is called for every new connection, so caching of results is
|
||||
* recommended, especially if network operations are involved.
|
||||
*
|
||||
* @param dataSpec The original {@link DataSpec}.
|
||||
* @return The resolved {@link DataSpec}.
|
||||
* @throws IOException If an {@link IOException} occurred while resolving the {@link DataSpec}.
|
||||
*/
|
||||
DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException;
|
||||
|
||||
/**
|
||||
* Resolves a URI reported by {@link DataSource#getUri()} for event reporting and caching
|
||||
* purposes.
|
||||
*
|
||||
* <p>Implementations do not need to overwrite this method unless they want to change the
|
||||
* reported URI.
|
||||
*
|
||||
* <p>This method is <em>not</em> allowed to block.
|
||||
*
|
||||
* @param uri The URI as reported by {@link DataSource#getUri()}.
|
||||
* @return The resolved URI used for event reporting and caching.
|
||||
*/
|
||||
default Uri resolveReportedUri(Uri uri) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
/** {@link DataSource.Factory} for {@link ResolvingDataSource} instances. */
|
||||
public static final class Factory implements DataSource.Factory {
|
||||
|
||||
private final DataSource.Factory upstreamFactory;
|
||||
private final Resolver resolver;
|
||||
|
||||
/**
|
||||
* Creates factory for {@link ResolvingDataSource} instances.
|
||||
*
|
||||
* @param upstreamFactory The wrapped {@link DataSource.Factory} handling the resolved {@link
|
||||
* DataSpec DataSpecs}.
|
||||
* @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.
|
||||
*/
|
||||
public Factory(DataSource.Factory upstreamFactory, Resolver resolver) {
|
||||
this.upstreamFactory = upstreamFactory;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource createDataSource() {
|
||||
return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver);
|
||||
}
|
||||
}
|
||||
|
||||
private final DataSource upstreamDataSource;
|
||||
private final Resolver resolver;
|
||||
|
||||
private boolean upstreamOpened;
|
||||
|
||||
/**
|
||||
* @param upstreamDataSource The wrapped {@link DataSource}.
|
||||
* @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.
|
||||
*/
|
||||
public ResolvingDataSource(DataSource upstreamDataSource, Resolver resolver) {
|
||||
this.upstreamDataSource = upstreamDataSource;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTransferListener(TransferListener transferListener) {
|
||||
upstreamDataSource.addTransferListener(transferListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws IOException {
|
||||
DataSpec resolvedDataSpec = resolver.resolveDataSpec(dataSpec);
|
||||
upstreamOpened = true;
|
||||
return upstreamDataSource.open(resolvedDataSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||
return upstreamDataSource.read(buffer, offset, readLength);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
Uri reportedUri = upstreamDataSource.getUri();
|
||||
return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getResponseHeaders() {
|
||||
return upstreamDataSource.getResponseHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (upstreamOpened) {
|
||||
upstreamOpened = false;
|
||||
upstreamDataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
@ -134,9 +134,9 @@ public final class CacheDataSource implements DataSource {
|
||||
|
||||
private @Nullable DataSource currentDataSource;
|
||||
private boolean currentDataSpecLengthUnset;
|
||||
private @Nullable Uri uri;
|
||||
private @Nullable Uri actualUri;
|
||||
private @HttpMethod int httpMethod;
|
||||
@Nullable private Uri uri;
|
||||
@Nullable private Uri actualUri;
|
||||
@HttpMethod private int httpMethod;
|
||||
private int flags;
|
||||
private @Nullable String key;
|
||||
private long readPosition;
|
||||
@ -319,7 +319,7 @@ public final class CacheDataSource implements DataSource {
|
||||
}
|
||||
return bytesRead;
|
||||
} catch (IOException e) {
|
||||
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
|
||||
if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) {
|
||||
setNoBytesRemainingAndMaybeStoreLength();
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
@ -484,20 +484,6 @@ public final class CacheDataSource implements DataSource {
|
||||
return redirectedUri != null ? redirectedUri : defaultUri;
|
||||
}
|
||||
|
||||
private static boolean isCausedByPositionOutOfRange(IOException e) {
|
||||
Throwable cause = e;
|
||||
while (cause != null) {
|
||||
if (cause instanceof DataSourceException) {
|
||||
int reason = ((DataSourceException) cause).reason;
|
||||
if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isReadingFromUpstream() {
|
||||
return !isReadingFromCache();
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
||||
@ -78,13 +79,7 @@ public final class CacheUtil {
|
||||
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
|
||||
String key = buildCacheKey(dataSpec, cacheKeyFactory);
|
||||
long position = dataSpec.absoluteStreamPosition;
|
||||
long requestLength;
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
requestLength = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
|
||||
requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position;
|
||||
}
|
||||
long requestLength = getRequestLength(dataSpec, cache, key);
|
||||
long bytesAlreadyCached = 0;
|
||||
long bytesLeft = requestLength;
|
||||
while (bytesLeft != 0) {
|
||||
@ -179,53 +174,66 @@ public final class CacheUtil {
|
||||
Assertions.checkNotNull(dataSource);
|
||||
Assertions.checkNotNull(buffer);
|
||||
|
||||
String key = buildCacheKey(dataSpec, cacheKeyFactory);
|
||||
long bytesLeft;
|
||||
ProgressNotifier progressNotifier = null;
|
||||
if (progressListener != null) {
|
||||
progressNotifier = new ProgressNotifier(progressListener);
|
||||
Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
|
||||
progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
|
||||
bytesLeft = lengthAndBytesAlreadyCached.first;
|
||||
} else {
|
||||
bytesLeft = getRequestLength(dataSpec, cache, key);
|
||||
}
|
||||
|
||||
String key = buildCacheKey(dataSpec, cacheKeyFactory);
|
||||
long position = dataSpec.absoluteStreamPosition;
|
||||
long bytesLeft;
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesLeft = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
|
||||
bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position;
|
||||
}
|
||||
boolean lengthUnset = bytesLeft == C.LENGTH_UNSET;
|
||||
while (bytesLeft != 0) {
|
||||
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
||||
long blockLength =
|
||||
cache.getCachedLength(
|
||||
key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
|
||||
cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft);
|
||||
if (blockLength > 0) {
|
||||
// Skip already cached data.
|
||||
} else {
|
||||
// There is a hole in the cache which is at least "-blockLength" long.
|
||||
blockLength = -blockLength;
|
||||
long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength;
|
||||
boolean isLastBlock = length == bytesLeft;
|
||||
long read =
|
||||
readAndDiscard(
|
||||
dataSpec,
|
||||
position,
|
||||
blockLength,
|
||||
length,
|
||||
dataSource,
|
||||
buffer,
|
||||
priorityTaskManager,
|
||||
priority,
|
||||
progressNotifier,
|
||||
isLastBlock,
|
||||
isCanceled);
|
||||
if (read < blockLength) {
|
||||
// Reached to the end of the data.
|
||||
if (enableEOFException && bytesLeft != C.LENGTH_UNSET) {
|
||||
if (enableEOFException && !lengthUnset) {
|
||||
throw new EOFException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
position += blockLength;
|
||||
bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength;
|
||||
if (!lengthUnset) {
|
||||
bytesLeft -= blockLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) {
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
return dataSpec.length;
|
||||
} else {
|
||||
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
|
||||
return contentLength == C.LENGTH_UNSET
|
||||
? C.LENGTH_UNSET
|
||||
: contentLength - dataSpec.absoluteStreamPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,6 +250,7 @@ public final class CacheUtil {
|
||||
* caching.
|
||||
* @param priority The priority of this task.
|
||||
* @param progressNotifier A notifier through which to report progress updates, or {@code null}.
|
||||
* @param isLastBlock Whether this read block is the last block of the content.
|
||||
* @param isCanceled An optional flag that will interrupt caching if set to true.
|
||||
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
|
||||
* has been reached.
|
||||
@ -255,54 +264,64 @@ public final class CacheUtil {
|
||||
PriorityTaskManager priorityTaskManager,
|
||||
int priority,
|
||||
@Nullable ProgressNotifier progressNotifier,
|
||||
boolean isLastBlock,
|
||||
AtomicBoolean isCanceled)
|
||||
throws IOException, InterruptedException {
|
||||
long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;
|
||||
long initialPositionOffset = positionOffset;
|
||||
long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;
|
||||
while (true) {
|
||||
if (priorityTaskManager != null) {
|
||||
// Wait for any other thread with higher priority to finish its job.
|
||||
priorityTaskManager.proceed(priority);
|
||||
}
|
||||
try {
|
||||
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
||||
// Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in
|
||||
// case the given length exceeds the end of input.
|
||||
dataSpec =
|
||||
new DataSpec(
|
||||
dataSpec.uri,
|
||||
dataSpec.httpMethod,
|
||||
dataSpec.httpBody,
|
||||
absoluteStreamPosition,
|
||||
/* position= */ dataSpec.position + positionOffset,
|
||||
C.LENGTH_UNSET,
|
||||
dataSpec.key,
|
||||
dataSpec.flags);
|
||||
long resolvedLength = dataSource.open(dataSpec);
|
||||
if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
|
||||
try {
|
||||
long resolvedLength = C.LENGTH_UNSET;
|
||||
boolean isDataSourceOpen = false;
|
||||
if (endOffset != C.POSITION_UNSET) {
|
||||
// If a specific length is given, first try to open the data source for that length to
|
||||
// avoid more data then required to be requested. If the given length exceeds the end of
|
||||
// input we will get a "position out of range" error. In that case try to open the source
|
||||
// again with unset length.
|
||||
try {
|
||||
resolvedLength =
|
||||
dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset));
|
||||
isDataSourceOpen = true;
|
||||
} catch (IOException exception) {
|
||||
if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) {
|
||||
throw exception;
|
||||
}
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
if (!isDataSourceOpen) {
|
||||
resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET));
|
||||
}
|
||||
if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
|
||||
progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);
|
||||
}
|
||||
long totalBytesRead = 0;
|
||||
while (totalBytesRead != length) {
|
||||
while (positionOffset != endOffset) {
|
||||
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
||||
int bytesRead =
|
||||
dataSource.read(
|
||||
buffer,
|
||||
0,
|
||||
length != C.LENGTH_UNSET
|
||||
? (int) Math.min(buffer.length, length - totalBytesRead)
|
||||
endOffset != C.POSITION_UNSET
|
||||
? (int) Math.min(buffer.length, endOffset - positionOffset)
|
||||
: buffer.length);
|
||||
if (bytesRead == C.RESULT_END_OF_INPUT) {
|
||||
if (progressNotifier != null) {
|
||||
progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead);
|
||||
progressNotifier.onRequestLengthResolved(positionOffset);
|
||||
}
|
||||
break;
|
||||
}
|
||||
totalBytesRead += bytesRead;
|
||||
positionOffset += bytesRead;
|
||||
if (progressNotifier != null) {
|
||||
progressNotifier.onBytesCached(bytesRead);
|
||||
}
|
||||
}
|
||||
return totalBytesRead;
|
||||
return positionOffset - initialPositionOffset;
|
||||
} catch (PriorityTaskManager.PriorityTooLowException exception) {
|
||||
// catch and try again
|
||||
} finally {
|
||||
@ -340,6 +359,20 @@ public final class CacheUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ static boolean isCausedByPositionOutOfRange(IOException e) {
|
||||
Throwable cause = e;
|
||||
while (cause != null) {
|
||||
if (cause instanceof DataSourceException) {
|
||||
int reason = ((DataSourceException) cause).reason;
|
||||
if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String buildCacheKey(
|
||||
DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {
|
||||
return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)
|
||||
|
@ -348,8 +348,9 @@ public final class MimeTypes {
|
||||
case MimeTypes.AUDIO_AC3:
|
||||
return C.ENCODING_AC3;
|
||||
case MimeTypes.AUDIO_E_AC3:
|
||||
case MimeTypes.AUDIO_E_AC3_JOC:
|
||||
return C.ENCODING_E_AC3;
|
||||
case MimeTypes.AUDIO_E_AC3_JOC:
|
||||
return C.ENCODING_E_AC3_JOC;
|
||||
case MimeTypes.AUDIO_AC4:
|
||||
return C.ENCODING_AC4;
|
||||
case MimeTypes.AUDIO_DTS:
|
||||
|
@ -1713,7 +1713,12 @@ public final class Util {
|
||||
if (connectivityManager == null) {
|
||||
return C.NETWORK_TYPE_UNKNOWN;
|
||||
}
|
||||
try {
|
||||
networkInfo = connectivityManager.getActiveNetworkInfo();
|
||||
} catch (SecurityException e) {
|
||||
// Expected if permission was revoked.
|
||||
return C.NETWORK_TYPE_UNKNOWN;
|
||||
}
|
||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||
return C.NETWORK_TYPE_OFFLINE;
|
||||
}
|
||||
|
@ -550,8 +550,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
MediaCodec codec,
|
||||
Format format,
|
||||
MediaCrypto crypto,
|
||||
float codecOperatingRate)
|
||||
throws DecoderQueryException {
|
||||
float codecOperatingRate) {
|
||||
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
|
||||
MediaFormat mediaFormat =
|
||||
getMediaFormat(
|
||||
@ -1173,11 +1172,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
* @param format The format for which the codec is being configured.
|
||||
* @param streamFormats The possible stream formats.
|
||||
* @return Suitable {@link CodecMaxValues}.
|
||||
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
|
||||
*/
|
||||
protected CodecMaxValues getCodecMaxValues(
|
||||
MediaCodecInfo codecInfo, Format format, Format[] streamFormats)
|
||||
throws DecoderQueryException {
|
||||
MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {
|
||||
int maxWidth = format.width;
|
||||
int maxHeight = format.height;
|
||||
int maxInputSize = getMaxInputSize(codecInfo, format);
|
||||
@ -1227,17 +1224,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a maximum video size to use when configuring a codec for {@code format} in a way
|
||||
* that will allow possible adaptation to other compatible formats that are expected to have the
|
||||
* same aspect ratio, but whose sizes are unknown.
|
||||
* Returns a maximum video size to use when configuring a codec for {@code format} in a way that
|
||||
* will allow possible adaptation to other compatible formats that are expected to have the same
|
||||
* aspect ratio, but whose sizes are unknown.
|
||||
*
|
||||
* @param codecInfo Information about the {@link MediaCodec} being configured.
|
||||
* @param format The format for which the codec is being configured.
|
||||
* @return The maximum video size to use, or null if the size of {@code format} should be used.
|
||||
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
|
||||
*/
|
||||
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format)
|
||||
throws DecoderQueryException {
|
||||
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {
|
||||
boolean isVerticalVideo = format.height > format.width;
|
||||
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
|
||||
int formatShortEdgePx = isVerticalVideo ? format.width : format.height;
|
||||
@ -1255,13 +1250,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
return alignedSize;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// Conservatively assume the codec requires 16px width and height alignment.
|
||||
longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16;
|
||||
shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16;
|
||||
if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) {
|
||||
return new Point(isVerticalVideo ? shortEdgePx : longEdgePx,
|
||||
return new Point(
|
||||
isVerticalVideo ? shortEdgePx : longEdgePx,
|
||||
isVerticalVideo ? longEdgePx : shortEdgePx);
|
||||
}
|
||||
} catch (DecoderQueryException e) {
|
||||
// We tried our best. Give up!
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -13,8 +13,6 @@ STYLE
|
||||
::cue(#id2) {
|
||||
color: peachpuff;
|
||||
}
|
||||
|
||||
STYLE
|
||||
::cue(v[voice="LaGord"]) { background-color: lime }
|
||||
|
||||
STYLE
|
||||
|
@ -514,7 +514,7 @@ public final class TtmlDecoderTest {
|
||||
assertThat(cue.position).isEqualTo(24f / 100f);
|
||||
assertThat(cue.line).isEqualTo(28f / 100f);
|
||||
assertThat(cue.size).isEqualTo(51f / 100f);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
|
||||
|
||||
cues = subtitle.getCues(4000000);
|
||||
assertThat(cues).hasSize(1);
|
||||
@ -524,7 +524,7 @@ public final class TtmlDecoderTest {
|
||||
assertThat(cue.position).isEqualTo(21f / 100f);
|
||||
assertThat(cue.line).isEqualTo(35f / 100f);
|
||||
assertThat(cue.size).isEqualTo(57f / 100f);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(6f / 100f);
|
||||
|
||||
cues = subtitle.getCues(7500000);
|
||||
assertThat(cues).hasSize(1);
|
||||
@ -534,7 +534,7 @@ public final class TtmlDecoderTest {
|
||||
assertThat(cue.position).isEqualTo(24f / 100f);
|
||||
assertThat(cue.line).isEqualTo(28f / 100f);
|
||||
assertThat(cue.size).isEqualTo(51f / 100f);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -549,7 +549,7 @@ public final class TtmlDecoderTest {
|
||||
assertThat(cue.position).isEqualTo(307f / 1280f);
|
||||
assertThat(cue.line).isEqualTo(562f / 720f);
|
||||
assertThat(cue.size).isEqualTo(653f / 1280f);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(86f / 720f);
|
||||
|
||||
cues = subtitle.getCues(4000000);
|
||||
assertThat(cues).hasSize(1);
|
||||
@ -559,7 +559,7 @@ public final class TtmlDecoderTest {
|
||||
assertThat(cue.position).isEqualTo(269f / 1280f);
|
||||
assertThat(cue.line).isEqualTo(612f / 720f);
|
||||
assertThat(cue.size).isEqualTo(730f / 1280f);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
||||
assertThat(cue.bitmapHeight).isEqualTo(43f / 720f);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -87,21 +88,32 @@ public final class CssParserTest {
|
||||
|
||||
@Test
|
||||
public void testParseMethodSimpleInput() {
|
||||
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
|
||||
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
||||
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
|
||||
expectedStyle.setFontColor(0xFF000000);
|
||||
expectedStyle.setBackgroundColor(0xFFFFEFD5);
|
||||
assertParserProduces(expectedStyle, styleBlock1);
|
||||
assertParserProduces(styleBlock1, expectedStyle);
|
||||
|
||||
String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
|
||||
expectedStyle = new WebvttCssStyle();
|
||||
expectedStyle.setFontColor(0xFF000000);
|
||||
assertParserProduces(expectedStyle, styleBlock2);
|
||||
assertParserProduces(styleBlock2, expectedStyle);
|
||||
|
||||
String styleBlock3 = " \n::cue {\n background-color\n:#00fFFe}";
|
||||
String styleBlock3 = "::cue {\n background-color\n:#00fFFe}";
|
||||
expectedStyle = new WebvttCssStyle();
|
||||
expectedStyle.setBackgroundColor(0xFF00FFFE);
|
||||
assertParserProduces(expectedStyle, styleBlock3);
|
||||
assertParserProduces(styleBlock3, expectedStyle);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseMethodMultipleRulesInBlockInput() {
|
||||
String styleBlock =
|
||||
"::cue {\n background-color\n:#00fFFe} \n::cue {\n background-color\n:#00000000}\n";
|
||||
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
||||
expectedStyle.setBackgroundColor(0xFF00FFFE);
|
||||
WebvttCssStyle secondExpectedStyle = new WebvttCssStyle();
|
||||
secondExpectedStyle.setBackgroundColor(0x000000);
|
||||
assertParserProduces(styleBlock, expectedStyle, secondExpectedStyle);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -116,7 +128,7 @@ public final class CssParserTest {
|
||||
expectedStyle.setFontFamily("courier");
|
||||
expectedStyle.setBold(true);
|
||||
|
||||
assertParserProduces(expectedStyle, styleBlock);
|
||||
assertParserProduces(styleBlock, expectedStyle);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -128,7 +140,7 @@ public final class CssParserTest {
|
||||
expectedStyle.setBackgroundColor(0x190A0B0C);
|
||||
expectedStyle.setFontColor(0xFF010101);
|
||||
|
||||
assertParserProduces(expectedStyle, styleBlock);
|
||||
assertParserProduces(styleBlock, expectedStyle);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -203,10 +215,13 @@ public final class CssParserTest {
|
||||
assertThat(input.readLine()).isEqualTo(expectedLine);
|
||||
}
|
||||
|
||||
private void assertParserProduces(WebvttCssStyle expected,
|
||||
String styleBlock){
|
||||
private void assertParserProduces(String styleBlock, WebvttCssStyle... expectedStyles) {
|
||||
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock));
|
||||
WebvttCssStyle actualElem = parser.parseBlock(input);
|
||||
List<WebvttCssStyle> styles = parser.parseBlock(input);
|
||||
assertThat(styles.size()).isEqualTo(expectedStyles.length);
|
||||
for (int i = 0; i < expectedStyles.length; i++) {
|
||||
WebvttCssStyle expected = expectedStyles[i];
|
||||
WebvttCssStyle actualElem = styles.get(i);
|
||||
assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor());
|
||||
if (expected.hasBackgroundColor()) {
|
||||
assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor());
|
||||
@ -223,5 +238,6 @@ public final class CssParserTest {
|
||||
assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline());
|
||||
assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -341,6 +341,76 @@ public final class DefaultTrackSelectorTest {
|
||||
assertFixedSelection(result.selections.get(0), trackGroups, formatWithSelectionFlag);
|
||||
}
|
||||
|
||||
/** Tests that adaptive audio track selections respect the maximum audio bitrate. */
|
||||
public void testSelectAdaptiveAudioTrackGroupWithMaxBitrate() throws ExoPlaybackException {
|
||||
Format format128k =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ "128",
|
||||
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
|
||||
/* codecs= */ "mp4a.40.2",
|
||||
/* bitrate= */ 128 * 1024,
|
||||
/* maxInputSize= */ Format.NO_VALUE,
|
||||
/* channelCount= */ 2,
|
||||
/* sampleRate= */ 44100,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
Format format192k =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ "192",
|
||||
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
|
||||
/* codecs= */ "mp4a.40.2",
|
||||
/* bitrate= */ 192 * 1024,
|
||||
/* maxInputSize= */ Format.NO_VALUE,
|
||||
/* channelCount= */ 2,
|
||||
/* sampleRate= */ 44100,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
Format format256k =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ "256",
|
||||
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
|
||||
/* codecs= */ "mp4a.40.2",
|
||||
/* bitrate= */ 256 * 1024,
|
||||
/* maxInputSize= */ Format.NO_VALUE,
|
||||
/* channelCount= */ 2,
|
||||
/* sampleRate= */ 44100,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
RendererCapabilities[] rendererCapabilities = {
|
||||
ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES
|
||||
};
|
||||
TrackGroupArray trackGroups =
|
||||
new TrackGroupArray(new TrackGroup(format192k, format128k, format256k));
|
||||
|
||||
TrackSelectorResult result =
|
||||
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
|
||||
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1, 2);
|
||||
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters().setMaxAudioBitrate(256 * 1024 - 1));
|
||||
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
|
||||
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
|
||||
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024));
|
||||
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
|
||||
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
|
||||
|
||||
trackSelector.setParameters(
|
||||
trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024 - 1));
|
||||
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
|
||||
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1);
|
||||
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(10));
|
||||
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
|
||||
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that track selector will select audio track with language that match preferred language
|
||||
* given by {@link Parameters}.
|
||||
@ -893,7 +963,6 @@ public final class DefaultTrackSelectorTest {
|
||||
Format forcedDefault =
|
||||
buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT);
|
||||
Format defaultOnly = buildTextFormat("defaultOnly", "eng", C.SELECTION_FLAG_DEFAULT);
|
||||
Format forcedOnlySpanish = buildTextFormat("forcedOnlySpanish", "spa", C.SELECTION_FLAG_FORCED);
|
||||
Format noFlag = buildTextFormat("noFlag", "eng");
|
||||
|
||||
RendererCapabilities[] textRendererCapabilities =
|
||||
|
@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.Downloader;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloaderFactory;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
|
||||
@ -108,7 +109,7 @@ public class DashDownloaderTest {
|
||||
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
|
||||
dashDownloader.download(progressListener);
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -127,7 +128,7 @@ public class DashDownloaderTest {
|
||||
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
|
||||
dashDownloader.download(progressListener);
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -146,7 +147,7 @@ public class DashDownloaderTest {
|
||||
DashDownloader dashDownloader =
|
||||
getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
|
||||
dashDownloader.download(progressListener);
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -167,7 +168,7 @@ public class DashDownloaderTest {
|
||||
|
||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||
dashDownloader.download(progressListener);
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -256,7 +257,7 @@ public class DashDownloaderTest {
|
||||
// Expected.
|
||||
}
|
||||
dashDownloader.download(progressListener);
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
|
||||
import com.google.android.exoplayer2.testutil.DummyMainThread;
|
||||
import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
@ -154,7 +155,7 @@ public class DownloadManagerDashTest {
|
||||
public void testHandleDownloadRequest() throws Throwable {
|
||||
handleDownloadRequest(fakeStreamKey1, fakeStreamKey2);
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -162,7 +163,7 @@ public class DownloadManagerDashTest {
|
||||
handleDownloadRequest(fakeStreamKey1);
|
||||
handleDownloadRequest(fakeStreamKey2);
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -176,7 +177,7 @@ public class DownloadManagerDashTest {
|
||||
handleDownloadRequest(fakeStreamKey1);
|
||||
|
||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -322,6 +322,7 @@ import java.util.Map;
|
||||
if (enabledTrackGroupCount == 0) {
|
||||
chunkSource.reset();
|
||||
downstreamTrackFormat = null;
|
||||
pendingResetUpstreamFormats = true;
|
||||
mediaChunks.clear();
|
||||
if (loader.isLoading()) {
|
||||
if (sampleQueuesBuilt) {
|
||||
|
@ -44,6 +44,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloaderFactory;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
|
||||
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.DummyDataSource;
|
||||
@ -129,12 +130,13 @@ public class HlsDownloaderTest {
|
||||
|
||||
assertCachedData(
|
||||
cache,
|
||||
fakeDataSet,
|
||||
new RequestSet(fakeDataSet)
|
||||
.subset(
|
||||
MASTER_PLAYLIST_URI,
|
||||
MEDIA_PLAYLIST_1_URI,
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -186,11 +188,12 @@ public class HlsDownloaderTest {
|
||||
|
||||
assertCachedData(
|
||||
cache,
|
||||
fakeDataSet,
|
||||
new RequestSet(fakeDataSet)
|
||||
.subset(
|
||||
MEDIA_PLAYLIST_1_URI,
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
|
||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -220,11 +220,26 @@ public class DefaultTimeBar extends View implements TimeBar {
|
||||
private @Nullable long[] adGroupTimesMs;
|
||||
private @Nullable boolean[] playedAdGroups;
|
||||
|
||||
/** Creates a new time bar. */
|
||||
public DefaultTimeBar(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public DefaultTimeBar(Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, attrs);
|
||||
}
|
||||
|
||||
// Suppress warnings due to usage of View methods in the constructor.
|
||||
@SuppressWarnings("nullness:method.invocation.invalid")
|
||||
public DefaultTimeBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
public DefaultTimeBar(
|
||||
Context context,
|
||||
@Nullable AttributeSet attrs,
|
||||
int defStyleAttr,
|
||||
@Nullable AttributeSet timebarAttrs) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
seekBounds = new Rect();
|
||||
progressBar = new Rect();
|
||||
bufferedBar = new Rect();
|
||||
@ -251,9 +266,9 @@ public class DefaultTimeBar extends View implements TimeBar {
|
||||
int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP);
|
||||
int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP);
|
||||
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0,
|
||||
0);
|
||||
if (timebarAttrs != null) {
|
||||
TypedArray a =
|
||||
context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0);
|
||||
try {
|
||||
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
|
||||
if (scrubberDrawable != null) {
|
||||
|
@ -28,6 +28,7 @@ import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
@ -97,6 +98,9 @@ import java.util.Locale;
|
||||
* <li>Corresponding method: None
|
||||
* <li>Default: {@code R.layout.exo_player_control_view}
|
||||
* </ul>
|
||||
* <li>All attributes that can be set on {@link DefaultTimeBar} can also be set on a
|
||||
* PlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar} unless the
|
||||
* layout is overridden to specify a custom {@code exo_progress} (see below).
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Overriding the layout file</h3>
|
||||
@ -154,7 +158,15 @@ import java.util.Locale;
|
||||
* <ul>
|
||||
* <li>Type: {@link TextView}
|
||||
* </ul>
|
||||
* <li><b>{@code exo_progress_placeholder}</b> - A placeholder that's replaced with the inflated
|
||||
* {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists.
|
||||
* <ul>
|
||||
* <li>Type: {@link View}
|
||||
* </ul>
|
||||
* <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
|
||||
* {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically
|
||||
* propagated through to this instance. If a view exists with this id, any {@code
|
||||
* exo_progress_placeholder} view will be ignored.
|
||||
* <ul>
|
||||
* <li>Type: {@link TimeBar}
|
||||
* </ul>
|
||||
@ -188,6 +200,18 @@ public class PlayerControlView extends FrameLayout {
|
||||
void onVisibilityChange(int visibility);
|
||||
}
|
||||
|
||||
/** Listener to be notified when progress has been updated. */
|
||||
public interface ProgressUpdateListener {
|
||||
|
||||
/**
|
||||
* Called when progress needs to be updated.
|
||||
*
|
||||
* @param position The current position.
|
||||
* @param bufferedPosition The current buffered position.
|
||||
*/
|
||||
void onProgressUpdate(long position, long bufferedPosition);
|
||||
}
|
||||
|
||||
/** The default fast forward increment, in milliseconds. */
|
||||
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
|
||||
/** The default rewind increment, in milliseconds. */
|
||||
@ -235,7 +259,8 @@ public class PlayerControlView extends FrameLayout {
|
||||
|
||||
@Nullable private Player player;
|
||||
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
|
||||
private VisibilityListener visibilityListener;
|
||||
@Nullable private VisibilityListener visibilityListener;
|
||||
@Nullable private ProgressUpdateListener progressUpdateListener;
|
||||
@Nullable private PlaybackPreparer playbackPreparer;
|
||||
|
||||
private boolean isAttachedToWindow;
|
||||
@ -317,9 +342,27 @@ public class PlayerControlView extends FrameLayout {
|
||||
LayoutInflater.from(context).inflate(controllerLayoutId, this);
|
||||
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
|
||||
|
||||
TimeBar customTimeBar = findViewById(R.id.exo_progress);
|
||||
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
|
||||
if (customTimeBar != null) {
|
||||
timeBar = customTimeBar;
|
||||
} else if (timeBarPlaceholder != null) {
|
||||
// Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,
|
||||
// but standard attributes (e.g. background) are not.
|
||||
DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs);
|
||||
defaultTimeBar.setId(R.id.exo_progress);
|
||||
defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
|
||||
ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
|
||||
int timeBarIndex = parent.indexOfChild(timeBarPlaceholder);
|
||||
parent.removeView(timeBarPlaceholder);
|
||||
parent.addView(defaultTimeBar, timeBarIndex);
|
||||
timeBar = defaultTimeBar;
|
||||
} else {
|
||||
timeBar = null;
|
||||
}
|
||||
durationView = findViewById(R.id.exo_duration);
|
||||
positionView = findViewById(R.id.exo_position);
|
||||
timeBar = findViewById(R.id.exo_progress);
|
||||
|
||||
if (timeBar != null) {
|
||||
timeBar.addListener(componentListener);
|
||||
}
|
||||
@ -454,6 +497,15 @@ public class PlayerControlView extends FrameLayout {
|
||||
this.visibilityListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ProgressUpdateListener}.
|
||||
*
|
||||
* @param listener The listener to be notified about when progress is updated.
|
||||
*/
|
||||
public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) {
|
||||
this.progressUpdateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PlaybackPreparer}.
|
||||
*
|
||||
@ -855,6 +907,9 @@ public class PlayerControlView extends FrameLayout {
|
||||
timeBar.setPosition(position);
|
||||
timeBar.setBufferedPosition(bufferedPosition);
|
||||
}
|
||||
if (progressUpdateListener != null) {
|
||||
progressUpdateListener.onProgressUpdate(position, bufferedPosition);
|
||||
}
|
||||
|
||||
// Cancel any pending updates and schedule a new one if necessary.
|
||||
removeCallbacks(updateProgressAction);
|
||||
|
@ -966,7 +966,8 @@ public class PlayerNotificationManager {
|
||||
@Nullable NotificationCompat.Builder builder,
|
||||
boolean ongoing,
|
||||
@Nullable Bitmap largeIcon) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE
|
||||
&& (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) {
|
||||
builderActions = null;
|
||||
return null;
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
@ -50,7 +49,6 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.VideoComponent;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
@ -165,9 +163,10 @@ import java.util.List;
|
||||
* <li>Corresponding method: None
|
||||
* <li>Default: {@code R.layout.exo_player_control_view}
|
||||
* </ul>
|
||||
* <li>All attributes that can be set on a {@link PlayerControlView} can also be set on a
|
||||
* PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the
|
||||
* layout is overridden to specify a custom {@code exo_controller} (see below).
|
||||
* <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can
|
||||
* also be set on a PlayerView, and will be propagated to the inflated {@link
|
||||
* PlayerControlView} unless the layout is overridden to specify a custom {@code
|
||||
* exo_controller} (see below).
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Overriding the layout file</h3>
|
||||
@ -217,9 +216,10 @@ import java.util.List;
|
||||
* <li>Type: {@link View}
|
||||
* </ul>
|
||||
* <li><b>{@code exo_controller}</b> - An already inflated {@link PlayerControlView}. Allows use
|
||||
* of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code
|
||||
* rewind_increment} will not be automatically propagated through to this instance. If a view
|
||||
* exists with this id, any {@code exo_controller_placeholder} view will be ignored.
|
||||
* of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link
|
||||
* DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated
|
||||
* through to this instance. If a view exists with this id, any {@code
|
||||
* exo_controller_placeholder} view will be ignored.
|
||||
* <ul>
|
||||
* <li>Type: {@link PlayerControlView}
|
||||
* </ul>
|
||||
@ -303,6 +303,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
private boolean controllerHideDuringAds;
|
||||
private boolean controllerHideOnTouch;
|
||||
private int textureViewRotation;
|
||||
private boolean isTouching;
|
||||
|
||||
public PlayerView(Context context) {
|
||||
this(context, null);
|
||||
@ -405,7 +406,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
break;
|
||||
case SURFACE_TYPE_MONO360_VIEW:
|
||||
SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context);
|
||||
sphericalSurfaceView.setSurfaceListener(componentListener);
|
||||
sphericalSurfaceView.setSingleTapListener(componentListener);
|
||||
surfaceView = sphericalSurfaceView;
|
||||
break;
|
||||
@ -459,8 +459,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
this.controller = customController;
|
||||
} else if (controllerPlaceholder != null) {
|
||||
// Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are
|
||||
// transferred, but standard FrameLayout attributes (e.g. background) are not.
|
||||
// transferred, but standard attributes (e.g. background) are not.
|
||||
this.controller = new PlayerControlView(context, null, 0, attrs);
|
||||
controller.setId(R.id.exo_controller);
|
||||
controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
|
||||
ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
|
||||
int controllerIndex = parent.indexOfChild(controllerPlaceholder);
|
||||
@ -771,11 +772,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
if (player != null && player.isPlayingAd()) {
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
boolean isDpadWhenControlHidden =
|
||||
isDpadKey(event.getKeyCode()) && useController && !controller.isVisible();
|
||||
boolean handled =
|
||||
isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
|
||||
if (handled) {
|
||||
|
||||
boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController;
|
||||
boolean handled = false;
|
||||
if (isDpadAndUseController && !controller.isVisible()) {
|
||||
// Handle the key event by showing the controller.
|
||||
maybeShowController(true);
|
||||
handled = true;
|
||||
} else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) {
|
||||
// The key event was handled as a media key or by the super class. We should also show the
|
||||
// controller, or extend its show timeout if already visible.
|
||||
maybeShowController(true);
|
||||
handled = true;
|
||||
} else if (isDpadAndUseController) {
|
||||
// The key event wasn't handled, but we should extend the controller's show timeout.
|
||||
maybeShowController(true);
|
||||
}
|
||||
return handled;
|
||||
@ -1039,11 +1049,21 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
isTouching = true;
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (isTouching) {
|
||||
isTouching = false;
|
||||
performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return performClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1359,7 +1379,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
TextOutput,
|
||||
VideoListener,
|
||||
OnLayoutChangeListener,
|
||||
SphericalSurfaceView.SurfaceListener,
|
||||
SingleTapListener {
|
||||
|
||||
// TextOutput implementation
|
||||
@ -1449,18 +1468,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
||||
applyTextureViewRotation((TextureView) view, textureViewRotation);
|
||||
}
|
||||
|
||||
// SphericalSurfaceView.SurfaceTextureListener implementation
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(@Nullable Surface surface) {
|
||||
if (player != null) {
|
||||
VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SingleTapListener implementation
|
||||
|
||||
@Override
|
||||
|
@ -362,10 +362,16 @@ import com.google.android.exoplayer2.util.Util;
|
||||
int width = Math.round(parentWidth * cueSize);
|
||||
int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight)
|
||||
: Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));
|
||||
int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width)
|
||||
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);
|
||||
int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height)
|
||||
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY);
|
||||
int x =
|
||||
Math.round(
|
||||
cuePositionAnchor == Cue.ANCHOR_TYPE_END
|
||||
? (anchorX - width)
|
||||
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);
|
||||
int y =
|
||||
Math.round(
|
||||
cueLineAnchor == Cue.ANCHOR_TYPE_END
|
||||
? (anchorY - height)
|
||||
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY);
|
||||
bitmapRect = new Rect(x, y, x + width, y + height);
|
||||
}
|
||||
|
||||
|
@ -53,20 +53,6 @@ import javax.microedition.khronos.opengles.GL10;
|
||||
*/
|
||||
public final class SphericalSurfaceView extends GLSurfaceView {
|
||||
|
||||
/**
|
||||
* This listener can be used to be notified when the {@link Surface} associated with this view is
|
||||
* changed.
|
||||
*/
|
||||
public interface SurfaceListener {
|
||||
/**
|
||||
* Invoked when the surface is changed or there isn't one anymore. Any previous surface
|
||||
* shouldn't be used after this call.
|
||||
*
|
||||
* @param surface The new surface or null if there isn't one anymore.
|
||||
*/
|
||||
void surfaceChanged(@Nullable Surface surface);
|
||||
}
|
||||
|
||||
// Arbitrary vertical field of view.
|
||||
private static final int FIELD_OF_VIEW_DEGREES = 90;
|
||||
private static final float Z_NEAR = .1f;
|
||||
@ -84,7 +70,6 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
||||
private final Handler mainHandler;
|
||||
private final TouchTracker touchTracker;
|
||||
private final SceneRenderer scene;
|
||||
private @Nullable SurfaceListener surfaceListener;
|
||||
private @Nullable SurfaceTexture surfaceTexture;
|
||||
private @Nullable Surface surface;
|
||||
private @Nullable Player.VideoComponent videoComponent;
|
||||
@ -156,15 +141,6 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SurfaceListener} used to listen to surface events.
|
||||
*
|
||||
* @param listener The listener for surface events.
|
||||
*/
|
||||
public void setSurfaceListener(@Nullable SurfaceListener listener) {
|
||||
surfaceListener = listener;
|
||||
}
|
||||
|
||||
/** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */
|
||||
public void setSingleTapListener(@Nullable SingleTapListener listener) {
|
||||
touchTracker.setSingleTapListener(listener);
|
||||
@ -196,8 +172,8 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
if (surface != null) {
|
||||
if (surfaceListener != null) {
|
||||
surfaceListener.surfaceChanged(null);
|
||||
if (videoComponent != null) {
|
||||
videoComponent.clearVideoSurface(surface);
|
||||
}
|
||||
releaseSurface(surfaceTexture, surface);
|
||||
surfaceTexture = null;
|
||||
@ -214,8 +190,8 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
||||
Surface oldSurface = this.surface;
|
||||
this.surfaceTexture = surfaceTexture;
|
||||
this.surface = new Surface(surfaceTexture);
|
||||
if (surfaceListener != null) {
|
||||
surfaceListener.surfaceChanged(surface);
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||
});
|
||||
|
@ -76,8 +76,7 @@
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="#FFBEBEBE"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
<View android:id="@id/exo_progress_placeholder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="26dp"/>
|
||||
|
@ -24,25 +24,43 @@
|
||||
<enum name="zoom" value="4"/>
|
||||
</attr>
|
||||
|
||||
<!-- Must be kept in sync with SimpleExoPlayerView -->
|
||||
<!-- Must be kept in sync with PlayerView -->
|
||||
<attr name="surface_type" format="enum">
|
||||
<enum name="none" value="0"/>
|
||||
<enum name="surface_view" value="1"/>
|
||||
<enum name="texture_view" value="2"/>
|
||||
<enum name="spherical_view" value="3"/>
|
||||
</attr>
|
||||
<attr name="show_timeout" format="integer"/>
|
||||
<attr name="rewind_increment" format="integer"/>
|
||||
<attr name="fastforward_increment" format="integer"/>
|
||||
<attr name="player_layout_id" format="reference"/>
|
||||
<attr name="controller_layout_id" format="reference"/>
|
||||
|
||||
<!-- Must be kept in sync with RepeatModeUtil -->
|
||||
<attr name="repeat_toggle_modes">
|
||||
<flag name="none" value="0"/>
|
||||
<flag name="one" value="1"/>
|
||||
<flag name="all" value="2"/>
|
||||
</attr>
|
||||
|
||||
<!-- PlayerControlView attributes -->
|
||||
<attr name="show_timeout" format="integer"/>
|
||||
<attr name="rewind_increment" format="integer"/>
|
||||
<attr name="fastforward_increment" format="integer"/>
|
||||
<attr name="show_shuffle_button" format="boolean"/>
|
||||
<attr name="time_bar_min_update_interval" format="integer"/>
|
||||
<attr name="controller_layout_id" format="reference"/>
|
||||
|
||||
<!-- DefaultTimeBar attributes -->
|
||||
<attr name="bar_height" format="dimension"/>
|
||||
<attr name="touch_target_height" format="dimension"/>
|
||||
<attr name="ad_marker_width" format="dimension"/>
|
||||
<attr name="scrubber_enabled_size" format="dimension"/>
|
||||
<attr name="scrubber_disabled_size" format="dimension"/>
|
||||
<attr name="scrubber_dragged_size" format="dimension"/>
|
||||
<attr name="scrubber_drawable" format="reference"/>
|
||||
<attr name="played_color" format="color"/>
|
||||
<attr name="scrubber_color" format="color"/>
|
||||
<attr name="buffered_color" format="color"/>
|
||||
<attr name="unplayed_color" format="color"/>
|
||||
<attr name="ad_marker_color" format="color"/>
|
||||
<attr name="played_ad_marker_color" format="color"/>
|
||||
|
||||
<declare-styleable name="PlayerView">
|
||||
<attr name="use_artwork" format="boolean"/>
|
||||
@ -58,9 +76,11 @@
|
||||
<enum name="always" value="2"/>
|
||||
</attr>
|
||||
<attr name="keep_content_on_player_reset" format="boolean"/>
|
||||
<attr name="resize_mode"/>
|
||||
<attr name="player_layout_id" format="reference"/>
|
||||
|
||||
<attr name="surface_type"/>
|
||||
<attr name="player_layout_id"/>
|
||||
<!-- AspectRatioFrameLayout attributes -->
|
||||
<attr name="resize_mode"/>
|
||||
<!-- PlayerControlView attributes -->
|
||||
<attr name="show_timeout"/>
|
||||
<attr name="rewind_increment"/>
|
||||
@ -69,6 +89,20 @@
|
||||
<attr name="show_shuffle_button"/>
|
||||
<attr name="time_bar_min_update_interval"/>
|
||||
<attr name="controller_layout_id"/>
|
||||
<!-- DefaultTimeBar attributes -->
|
||||
<attr name="bar_height"/>
|
||||
<attr name="touch_target_height"/>
|
||||
<attr name="ad_marker_width"/>
|
||||
<attr name="scrubber_enabled_size"/>
|
||||
<attr name="scrubber_disabled_size"/>
|
||||
<attr name="scrubber_dragged_size"/>
|
||||
<attr name="scrubber_drawable"/>
|
||||
<attr name="played_color"/>
|
||||
<attr name="scrubber_color"/>
|
||||
<attr name="buffered_color" />
|
||||
<attr name="unplayed_color"/>
|
||||
<attr name="ad_marker_color"/>
|
||||
<attr name="played_ad_marker_color"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AspectRatioFrameLayout">
|
||||
@ -83,22 +117,36 @@
|
||||
<attr name="show_shuffle_button"/>
|
||||
<attr name="time_bar_min_update_interval"/>
|
||||
<attr name="controller_layout_id"/>
|
||||
<!-- DefaultTimeBar attributes -->
|
||||
<attr name="bar_height"/>
|
||||
<attr name="touch_target_height"/>
|
||||
<attr name="ad_marker_width"/>
|
||||
<attr name="scrubber_enabled_size"/>
|
||||
<attr name="scrubber_disabled_size"/>
|
||||
<attr name="scrubber_dragged_size"/>
|
||||
<attr name="scrubber_drawable"/>
|
||||
<attr name="played_color"/>
|
||||
<attr name="scrubber_color"/>
|
||||
<attr name="buffered_color" />
|
||||
<attr name="unplayed_color"/>
|
||||
<attr name="ad_marker_color"/>
|
||||
<attr name="played_ad_marker_color"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="DefaultTimeBar">
|
||||
<attr name="bar_height" format="dimension"/>
|
||||
<attr name="touch_target_height" format="dimension"/>
|
||||
<attr name="ad_marker_width" format="dimension"/>
|
||||
<attr name="scrubber_enabled_size" format="dimension"/>
|
||||
<attr name="scrubber_disabled_size" format="dimension"/>
|
||||
<attr name="scrubber_dragged_size" format="dimension"/>
|
||||
<attr name="scrubber_drawable" format="reference"/>
|
||||
<attr name="played_color" format="color"/>
|
||||
<attr name="scrubber_color" format="color"/>
|
||||
<attr name="buffered_color" format="color"/>
|
||||
<attr name="unplayed_color" format="color"/>
|
||||
<attr name="ad_marker_color" format="color"/>
|
||||
<attr name="played_ad_marker_color" format="color"/>
|
||||
<attr name="bar_height"/>
|
||||
<attr name="touch_target_height"/>
|
||||
<attr name="ad_marker_width"/>
|
||||
<attr name="scrubber_enabled_size"/>
|
||||
<attr name="scrubber_disabled_size"/>
|
||||
<attr name="scrubber_dragged_size"/>
|
||||
<attr name="scrubber_drawable"/>
|
||||
<attr name="played_color"/>
|
||||
<attr name="scrubber_color"/>
|
||||
<attr name="buffered_color" />
|
||||
<attr name="unplayed_color"/>
|
||||
<attr name="ad_marker_color"/>
|
||||
<attr name="played_ad_marker_color"/>
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
@ -33,6 +33,7 @@
|
||||
<item name="exo_repeat_toggle" type="id"/>
|
||||
<item name="exo_duration" type="id"/>
|
||||
<item name="exo_position" type="id"/>
|
||||
<item name="exo_progress_placeholder" type="id"/>
|
||||
<item name="exo_progress" type="id"/>
|
||||
<item name="exo_buffering" type="id"/>
|
||||
<item name="exo_error_message" type="id"/>
|
||||
|
@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -55,6 +54,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
boolean enableDecoderFallback,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
@ -113,8 +113,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
|
||||
MediaCodec codec,
|
||||
Format format,
|
||||
MediaCrypto crypto,
|
||||
float operatingRate)
|
||||
throws DecoderQueryException {
|
||||
float operatingRate) {
|
||||
// If the codec is being initialized whilst the renderer is started, default behavior is to
|
||||
// render the first frame (i.e. the keyframe before the current position), then drop frames up
|
||||
// to the current playback position. For test runs that place a maximum limit on the number of
|
||||
|
@ -166,7 +166,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName()));
|
||||
setContentView(
|
||||
getResources().getIdentifier("exo_testutils_host_activity", "layout", getPackageName()));
|
||||
surfaceView = findViewById(
|
||||
getResources().getIdentifier("surface_view", "id", getPackageName()));
|
||||
surfaceView.getHolder().addCallback(this);
|
||||
|
@ -33,58 +33,89 @@ import java.util.ArrayList;
|
||||
/** Assertion methods for {@link Cache}. */
|
||||
public final class CacheAsserts {
|
||||
|
||||
/**
|
||||
* Asserts that the cache content is equal to the data in the {@code fakeDataSet}.
|
||||
*
|
||||
* @throws IOException If an error occurred reading from the Cache.
|
||||
*/
|
||||
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {
|
||||
/** Defines a set of data requests. */
|
||||
public static final class RequestSet {
|
||||
|
||||
private final FakeDataSet fakeDataSet;
|
||||
private DataSpec[] dataSpecs;
|
||||
|
||||
public RequestSet(FakeDataSet fakeDataSet) {
|
||||
this.fakeDataSet = fakeDataSet;
|
||||
ArrayList<FakeData> allData = fakeDataSet.getAllData();
|
||||
Uri[] uris = new Uri[allData.size()];
|
||||
for (int i = 0; i < allData.size(); i++) {
|
||||
uris[i] = allData.get(i).uri;
|
||||
dataSpecs = new DataSpec[allData.size()];
|
||||
for (int i = 0; i < dataSpecs.length; i++) {
|
||||
dataSpecs[i] = new DataSpec(allData.get(i).uri);
|
||||
}
|
||||
}
|
||||
|
||||
public RequestSet subset(String... uriStrings) {
|
||||
dataSpecs = new DataSpec[uriStrings.length];
|
||||
for (int i = 0; i < dataSpecs.length; i++) {
|
||||
dataSpecs[i] = new DataSpec(Uri.parse(uriStrings[i]));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestSet subset(Uri... uris) {
|
||||
dataSpecs = new DataSpec[uris.length];
|
||||
for (int i = 0; i < dataSpecs.length; i++) {
|
||||
dataSpecs[i] = new DataSpec(uris[i]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestSet subset(DataSpec... dataSpecs) {
|
||||
this.dataSpecs = dataSpecs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return dataSpecs.length;
|
||||
}
|
||||
|
||||
public byte[] getData(int i) {
|
||||
return fakeDataSet.getData(dataSpecs[i].uri).getData();
|
||||
}
|
||||
|
||||
public DataSpec getDataSpec(int i) {
|
||||
return dataSpecs[i];
|
||||
}
|
||||
|
||||
public RequestSet useBoundedDataSpecFor(String uriString) {
|
||||
FakeData data = fakeDataSet.getData(uriString);
|
||||
for (int i = 0; i < dataSpecs.length; i++) {
|
||||
DataSpec spec = dataSpecs[i];
|
||||
if (spec.uri.getPath().equals(uriString)) {
|
||||
dataSpecs[i] = spec.subrange(0, data.getData().length);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
assertCachedData(cache, fakeDataSet, uris);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}.
|
||||
* Asserts that the cache contains necessary data for the {@code requestSet}.
|
||||
*
|
||||
* @throws IOException If an error occurred reading from the Cache.
|
||||
*/
|
||||
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings)
|
||||
throws IOException {
|
||||
Uri[] uris = new Uri[uriStrings.length];
|
||||
for (int i = 0; i < uriStrings.length; i++) {
|
||||
uris[i] = Uri.parse(uriStrings[i]);
|
||||
}
|
||||
assertCachedData(cache, fakeDataSet, uris);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}.
|
||||
*
|
||||
* @throws IOException If an error occurred reading from the Cache.
|
||||
*/
|
||||
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris)
|
||||
throws IOException {
|
||||
public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException {
|
||||
int totalLength = 0;
|
||||
for (Uri uri : uris) {
|
||||
byte[] data = fakeDataSet.getData(uri).getData();
|
||||
assertDataCached(cache, uri, data);
|
||||
for (int i = 0; i < requestSet.getCount(); i++) {
|
||||
byte[] data = requestSet.getData(i);
|
||||
assertDataCached(cache, requestSet.getDataSpec(i), data);
|
||||
totalLength += data.length;
|
||||
}
|
||||
assertThat(cache.getCacheSpace()).isEqualTo(totalLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cache contains the given data for {@code uriString}.
|
||||
* Asserts that the cache content is equal to the data in the {@code fakeDataSet}.
|
||||
*
|
||||
* @throws IOException If an error occurred reading from the Cache.
|
||||
*/
|
||||
public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException {
|
||||
DataSpec dataSpec = new DataSpec(uri);
|
||||
assertDataCached(cache, dataSpec, expected);
|
||||
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {
|
||||
assertCachedData(cache, new RequestSet(fakeDataSet));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,15 +126,18 @@ public final class CacheAsserts {
|
||||
public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected)
|
||||
throws IOException {
|
||||
DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0);
|
||||
dataSource.open(dataSpec);
|
||||
byte[] bytes;
|
||||
try {
|
||||
byte[] bytes = TestUtil.readToEnd(dataSource);
|
||||
assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',")
|
||||
.that(bytes)
|
||||
.isEqualTo(expected);
|
||||
dataSource.open(dataSpec);
|
||||
bytes = TestUtil.readToEnd(dataSource);
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Opening/reading cache failed: " + dataSpec, e);
|
||||
} finally {
|
||||
dataSource.close();
|
||||
}
|
||||
assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',")
|
||||
.that(bytes)
|
||||
.isEqualTo(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user