mirror of
https://github.com/androidx/media.git
synced 2025-05-05 06:30:24 +08:00
commit
f6297f4f51
@ -1,5 +1,55 @@
|
|||||||
# Release notes #
|
# 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 ###
|
### 2.10.1 ###
|
||||||
|
|
||||||
* Offline: Add option to remove all downloads.
|
* Offline: Add option to remove all downloads.
|
||||||
|
@ -36,7 +36,7 @@ allprojects {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
project.ext {
|
project.ext {
|
||||||
exoplayerPublishEnabled = true
|
exoplayerPublishEnabled = false
|
||||||
}
|
}
|
||||||
if (it.hasProperty('externalBuildDir')) {
|
if (it.hasProperty('externalBuildDir')) {
|
||||||
if (!new File(externalBuildDir).isAbsolute()) {
|
if (!new File(externalBuildDir).isAbsolute()) {
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.10.1'
|
releaseVersion = '2.10.2'
|
||||||
releaseVersionCode = 2010001
|
releaseVersionCode = 2010002
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
targetSdkVersion = 28
|
targetSdkVersion = 28
|
||||||
compileSdkVersion = 28
|
compileSdkVersion = 28
|
||||||
|
@ -66,7 +66,6 @@ import java.util.ArrayList;
|
|||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
|
||||||
private boolean castMediaQueueCreationPending;
|
|
||||||
private int currentItemIndex;
|
private int currentItemIndex;
|
||||||
private Player currentPlayer;
|
private Player currentPlayer;
|
||||||
|
|
||||||
@ -268,9 +267,6 @@ import java.util.ArrayList;
|
|||||||
public void onTimelineChanged(
|
public void onTimelineChanged(
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||||
updateCurrentItemIndex();
|
updateCurrentItemIndex();
|
||||||
if (currentPlayer == castPlayer && timeline.isEmpty()) {
|
|
||||||
castMediaQueueCreationPending = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CastPlayer.SessionAvailabilityListener implementation.
|
// CastPlayer.SessionAvailabilityListener implementation.
|
||||||
@ -332,7 +328,6 @@ import java.util.ArrayList;
|
|||||||
this.currentPlayer = currentPlayer;
|
this.currentPlayer = currentPlayer;
|
||||||
|
|
||||||
// Media queue management.
|
// Media queue management.
|
||||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
|
||||||
if (currentPlayer == exoPlayer) {
|
if (currentPlayer == exoPlayer) {
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
exoPlayer.prepare(concatenatingMediaSource);
|
||||||
}
|
}
|
||||||
@ -352,12 +347,11 @@ import java.util.ArrayList;
|
|||||||
*/
|
*/
|
||||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||||
maybeSetCurrentItemAndNotify(itemIndex);
|
maybeSetCurrentItemAndNotify(itemIndex);
|
||||||
if (castMediaQueueCreationPending) {
|
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||||
for (int i = 0; i < items.length; i++) {
|
for (int i = 0; i < items.length; i++) {
|
||||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
||||||
}
|
}
|
||||||
castMediaQueueCreationPending = false;
|
|
||||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
currentPlayer.seekTo(itemIndex, positionMs);
|
||||||
|
@ -31,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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 'androidx.annotation:annotation:1.0.2'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
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.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||||
import com.google.android.gms.common.api.PendingResult;
|
import com.google.android.gms.common.api.PendingResult;
|
||||||
import com.google.android.gms.common.api.ResultCallback;
|
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.List;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
* {@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 StatusListener statusListener;
|
||||||
private final SeekResultCallback seekResultCallback;
|
private final SeekResultCallback seekResultCallback;
|
||||||
|
|
||||||
// Listeners.
|
// Listeners and notification.
|
||||||
private final CopyOnWriteArraySet<EventListener> listeners;
|
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||||
|
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
||||||
|
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
||||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||||
|
|
||||||
// Internal state.
|
// Internal state.
|
||||||
@ -113,7 +118,9 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
statusListener = new StatusListener();
|
statusListener = new StatusListener();
|
||||||
seekResultCallback = new SeekResultCallback();
|
seekResultCallback = new SeekResultCallback();
|
||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArrayList<>();
|
||||||
|
notificationsBatch = new ArrayList<>();
|
||||||
|
ongoingNotificationsTasks = new ArrayDeque<>();
|
||||||
|
|
||||||
SessionManager sessionManager = castContext.getSessionManager();
|
SessionManager sessionManager = castContext.getSessionManager();
|
||||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||||
@ -296,12 +303,17 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(EventListener listener) {
|
public void addListener(EventListener listener) {
|
||||||
listeners.add(listener);
|
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeListener(EventListener listener) {
|
public void removeListener(EventListener listener) {
|
||||||
listeners.remove(listener);
|
for (ListenerHolder listenerHolder : listeners) {
|
||||||
|
if (listenerHolder.listener.equals(listener)) {
|
||||||
|
listenerHolder.release();
|
||||||
|
listeners.remove(listenerHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -347,14 +359,13 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
pendingSeekCount++;
|
pendingSeekCount++;
|
||||||
pendingSeekWindowIndex = windowIndex;
|
pendingSeekWindowIndex = windowIndex;
|
||||||
pendingSeekPositionMs = positionMs;
|
pendingSeekPositionMs = positionMs;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
new ListenerNotificationTask(
|
||||||
}
|
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
|
||||||
} else if (pendingSeekCount == 0) {
|
} else if (pendingSeekCount == 0) {
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||||
listener.onSeekProcessed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
flushNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -530,30 +541,40 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
|| this.playWhenReady != playWhenReady) {
|
|| this.playWhenReady != playWhenReady) {
|
||||||
this.playbackState = playbackState;
|
this.playbackState = playbackState;
|
||||||
this.playWhenReady = playWhenReady;
|
this.playWhenReady = playWhenReady;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
|
new ListenerNotificationTask(
|
||||||
}
|
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
|
||||||
}
|
}
|
||||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||||
if (this.repeatMode != repeatMode) {
|
if (this.repeatMode != repeatMode) {
|
||||||
this.repeatMode = repeatMode;
|
this.repeatMode = repeatMode;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onRepeatModeChanged(repeatMode);
|
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
maybeUpdateTimelineAndNotify();
|
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() {
|
private void maybeUpdateTimelineAndNotify() {
|
||||||
@ -561,9 +582,10 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||||
waitingForInitialTimeline = false;
|
waitingForInitialTimeline = false;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onTimelineChanged(currentTimeline, null, reason);
|
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) {
|
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
||||||
for (long activeTrackId : activeTrackIds) {
|
for (long activeTrackId : activeTrackIds) {
|
||||||
if (activeTrackId == id) {
|
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> {
|
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||||
|
|
||||||
@ -840,9 +868,25 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
if (--pendingSeekCount == 0) {
|
if (--pendingSeekCount == 0) {
|
||||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||||
pendingSeekPositionMs = C.TIME_UNSET;
|
pendingSeekPositionMs = C.TIME_UNSET;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||||
listener.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 {
|
dependencies {
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation 'androidx.annotation:annotation:1.0.2'
|
||||||
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
|
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||||
}
|
}
|
||||||
|
@ -1054,13 +1054,8 @@ public final class ImaAdsLoader
|
|||||||
long contentPositionMs = player.getCurrentPosition();
|
long contentPositionMs = player.getCurrentPosition();
|
||||||
int adGroupIndexForPosition =
|
int adGroupIndexForPosition =
|
||||||
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
|
||||||
if (adGroupIndexForPosition == 0) {
|
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
|
||||||
podIndexOffset = 0;
|
// Skip any ad groups before the one at or immediately before the playback position.
|
||||||
} 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.
|
|
||||||
for (int i = 0; i < adGroupIndexForPosition; i++) {
|
for (int i = 0; i < adGroupIndexForPosition; i++) {
|
||||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
|
||||||
}
|
}
|
||||||
@ -1070,9 +1065,18 @@ public final class ImaAdsLoader
|
|||||||
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
|
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
|
||||||
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
|
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
|
||||||
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
|
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
|
// IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
|
||||||
// midroll/postroll. Midroll pod indices start at 1.
|
// 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;
|
podIndexOffset = adGroupIndexForPosition - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ public final class MediaSessionConnector {
|
|||||||
ResultReceiver cb);
|
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 {
|
public interface PlaybackPreparer extends CommandReceiver {
|
||||||
|
|
||||||
long ACTIONS =
|
long ACTIONS =
|
||||||
@ -197,14 +197,36 @@ public final class MediaSessionConnector {
|
|||||||
* @return The bitmask of the supported media actions.
|
* @return The bitmask of the supported media actions.
|
||||||
*/
|
*/
|
||||||
long getSupportedPrepareActions();
|
long getSupportedPrepareActions();
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
|
/**
|
||||||
void onPrepare();
|
* See {@link MediaSessionCompat.Callback#onPrepare()}.
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
|
*
|
||||||
void onPrepareFromMediaId(String mediaId, Bundle extras);
|
* @param playWhenReady Whether playback should be started after preparation.
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
|
*/
|
||||||
void onPrepareFromSearch(String query, Bundle extras);
|
void onPrepare(boolean playWhenReady);
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
|
/**
|
||||||
void onPrepareFromUri(Uri uri, Bundle extras);
|
* 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;
|
return player != null && mediaButtonEventHandler != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopPlayerForPrepare(boolean playWhenReady) {
|
|
||||||
if (player != null) {
|
|
||||||
player.stop();
|
|
||||||
player.setPlayWhenReady(playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rewind(Player player) {
|
private void rewind(Player player) {
|
||||||
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
||||||
seekTo(player, player.getCurrentPosition() - rewindMs);
|
seekTo(player, player.getCurrentPosition() - rewindMs);
|
||||||
@ -1047,12 +1062,12 @@ public final class MediaSessionConnector {
|
|||||||
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
|
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.onPrepare();
|
playbackPreparer.onPrepare(/* playWhenReady= */ true);
|
||||||
}
|
}
|
||||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
|
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
|
||||||
}
|
}
|
||||||
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1182,56 +1197,49 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPrepare() {
|
public void onPrepare() {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
playbackPreparer.onPrepare(/* playWhenReady= */ false);
|
||||||
playbackPreparer.onPrepare();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
|
||||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
|
||||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ false);
|
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
|
||||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ true);
|
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
|
||||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayFromSearch(String query, Bundle extras) {
|
public void onPlayFromSearch(String query, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ true);
|
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
|
||||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayFromUri(Uri uri, Bundle extras) {
|
public void onPlayFromUri(Uri uri, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
||||||
stopPlayerForPrepare(/* playWhenReady= */ true);
|
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
|
||||||
playbackPreparer.onPrepareFromUri(uri, 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.
|
* 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>"
|
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 && \
|
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:
|
* 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
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
[#3520]: https://github.com/google/ExoPlayer/issues/3520
|
|
||||||
|
|
||||||
## Notes ##
|
## Notes ##
|
||||||
|
|
||||||
|
@ -60,8 +60,8 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
|||||||
* Initializes the buffer.
|
* Initializes the buffer.
|
||||||
*
|
*
|
||||||
* @param timeUs The presentation timestamp for the buffer, in microseconds.
|
* @param timeUs The presentation timestamp for the buffer, in microseconds.
|
||||||
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
|
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link
|
||||||
* VpxDecoder#OUTPUT_MODE_YUV}.
|
* VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}.
|
||||||
*/
|
*/
|
||||||
public void init(long timeUs, int mode) {
|
public void init(long timeUs, int mode) {
|
||||||
this.timeUs = timeUs;
|
this.timeUs = timeUs;
|
||||||
@ -110,6 +110,15 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
|||||||
return true;
|
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) {
|
private void initData(int size) {
|
||||||
if (data == null || data.capacity() < size) {
|
if (data == null || data.capacity() < size) {
|
||||||
data = ByteBuffer.allocateDirect(size);
|
data = ByteBuffer.allocateDirect(size);
|
||||||
|
@ -15,6 +15,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
APP_OPTIM := release
|
APP_OPTIM := release
|
||||||
APP_STL := gnustl_static
|
APP_STL := c++_static
|
||||||
APP_CPPFLAGS := -frtti
|
APP_CPPFLAGS := -frtti
|
||||||
APP_PLATFORM := android-9
|
APP_PLATFORM := android-16
|
||||||
|
@ -20,46 +20,33 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ $# -ne 1 ]; then
|
if [ $# -ne 0 ]; then
|
||||||
echo "Usage: ${0} <path_to_android_ndk>"
|
echo "Usage: ${0}"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ndk="${1}"
|
|
||||||
shift 1
|
|
||||||
|
|
||||||
# configuration parameters common to all architectures
|
# configuration parameters common to all architectures
|
||||||
common_params="--disable-examples --disable-docs --enable-realtime-only"
|
common_params="--disable-examples --disable-docs --enable-realtime-only"
|
||||||
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
|
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
|
||||||
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
|
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
|
||||||
|
common_params+=" --enable-external-build"
|
||||||
|
|
||||||
# configuration parameters for various architectures
|
# configuration parameters for various architectures
|
||||||
arch[0]="armeabi-v7a"
|
arch[0]="armeabi-v7a"
|
||||||
config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon"
|
config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm"
|
||||||
config[0]+=" --enable-neon-asm"
|
|
||||||
|
|
||||||
arch[1]="armeabi"
|
arch[1]="x86"
|
||||||
config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
|
config[1]="--force-target=x86-android-gcc --disable-sse2"
|
||||||
config[1]+=" --disable-neon-asm"
|
config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
||||||
|
config[1]+=" --disable-avx2 --enable-pic"
|
||||||
|
|
||||||
arch[2]="mips"
|
arch[2]="arm64-v8a"
|
||||||
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
|
config[2]="--force-target=armv8-android-gcc --enable-neon"
|
||||||
|
|
||||||
arch[3]="x86"
|
arch[3]="x86_64"
|
||||||
config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2"
|
config[3]="--force-target=x86_64-android-gcc --disable-sse2"
|
||||||
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
||||||
config[3]+=" --disable-avx2 --enable-pic"
|
config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
limit=$((${#arch[@]} - 1))
|
limit=$((${#arch[@]} - 1))
|
||||||
|
|
||||||
@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do
|
|||||||
# configure and make
|
# configure and make
|
||||||
echo "build_android_configs: "
|
echo "build_android_configs: "
|
||||||
echo "configure ${config[${i}]} ${common_params}"
|
echo "configure ${config[${i}]} ${common_params}"
|
||||||
../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
|
../../libvpx/configure ${config[${i}]} ${common_params}
|
||||||
-isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
|
|
||||||
-isystem $ndk/sysroot/usr/include \
|
|
||||||
"
|
|
||||||
rm -f libvpx_srcs.txt
|
rm -f libvpx_srcs.txt
|
||||||
for f in ${allowed_files}; do
|
for f in ${allowed_files}; do
|
||||||
# the build system supports multiple different configurations. avoid
|
# the build system supports multiple different configurations. avoid
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
|
|
||||||
// JNI references for VpxOutputBuffer class.
|
// JNI references for VpxOutputBuffer class.
|
||||||
static jmethodID initForYuvFrame;
|
static jmethodID initForYuvFrame;
|
||||||
|
static jmethodID initForPrivateFrame;
|
||||||
static jfieldID dataField;
|
static jfieldID dataField;
|
||||||
static jfieldID outputModeField;
|
static jfieldID outputModeField;
|
||||||
static jfieldID decoderPrivateField;
|
static jfieldID decoderPrivateField;
|
||||||
@ -481,6 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
|||||||
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
|
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
|
||||||
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
|
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
|
||||||
"(IIIII)Z");
|
"(IIIII)Z");
|
||||||
|
initForPrivateFrame =
|
||||||
|
env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
|
||||||
dataField = env->GetFieldID(outputBufferClass, "data",
|
dataField = env->GetFieldID(outputBufferClass, "data",
|
||||||
"Ljava/nio/ByteBuffer;");
|
"Ljava/nio/ByteBuffer;");
|
||||||
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
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_w = img->d_w;
|
||||||
jfb->d_h = img->d_h;
|
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,
|
env->SetIntField(jOutputBuffer, decoderPrivateField,
|
||||||
id + kDecoderPrivateBase);
|
id + kDecoderPrivateBase);
|
||||||
}
|
}
|
||||||
|
@ -146,8 +146,8 @@ public final class C {
|
|||||||
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
|
* {@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_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
|
||||||
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@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
|
* #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
|
||||||
* {@link #ENCODING_DOLBY_TRUEHD}.
|
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@ -163,6 +163,7 @@ public final class C {
|
|||||||
ENCODING_PCM_A_LAW,
|
ENCODING_PCM_A_LAW,
|
||||||
ENCODING_AC3,
|
ENCODING_AC3,
|
||||||
ENCODING_E_AC3,
|
ENCODING_E_AC3,
|
||||||
|
ENCODING_E_AC3_JOC,
|
||||||
ENCODING_AC4,
|
ENCODING_AC4,
|
||||||
ENCODING_DTS,
|
ENCODING_DTS,
|
||||||
ENCODING_DTS_HD,
|
ENCODING_DTS_HD,
|
||||||
@ -210,6 +211,8 @@ public final class C {
|
|||||||
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
|
||||||
/** @see AudioFormat#ENCODING_E_AC3 */
|
/** @see AudioFormat#ENCODING_E_AC3 */
|
||||||
public static final int ENCODING_E_AC3 = 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 */
|
/** @see AudioFormat#ENCODING_AC4 */
|
||||||
public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
|
public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
|
||||||
/** @see AudioFormat#ENCODING_DTS */
|
/** @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.AudioCapabilities;
|
||||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
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.audio.MediaCodecAudioRenderer;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
@ -90,6 +91,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
@ExtensionRendererMode private int extensionRendererMode;
|
@ExtensionRendererMode private int extensionRendererMode;
|
||||||
private long allowedVideoJoiningTimeMs;
|
private long allowedVideoJoiningTimeMs;
|
||||||
private boolean playClearSamplesWithoutKeys;
|
private boolean playClearSamplesWithoutKeys;
|
||||||
|
private boolean enableDecoderFallback;
|
||||||
private MediaCodecSelector mediaCodecSelector;
|
private MediaCodecSelector mediaCodecSelector;
|
||||||
|
|
||||||
/** @param context A {@link Context}. */
|
/** @param context A {@link Context}. */
|
||||||
@ -202,6 +204,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
return this;
|
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.
|
* Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
|
||||||
*
|
*
|
||||||
@ -248,6 +263,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
mediaCodecSelector,
|
mediaCodecSelector,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
playClearSamplesWithoutKeys,
|
playClearSamplesWithoutKeys,
|
||||||
|
enableDecoderFallback,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
videoRendererEventListener,
|
videoRendererEventListener,
|
||||||
allowedVideoJoiningTimeMs,
|
allowedVideoJoiningTimeMs,
|
||||||
@ -258,6 +274,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
mediaCodecSelector,
|
mediaCodecSelector,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
playClearSamplesWithoutKeys,
|
playClearSamplesWithoutKeys,
|
||||||
|
enableDecoderFallback,
|
||||||
buildAudioProcessors(),
|
buildAudioProcessors(),
|
||||||
eventHandler,
|
eventHandler,
|
||||||
audioRendererEventListener,
|
audioRendererEventListener,
|
||||||
@ -282,6 +299,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
|
* @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
|
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||||
* the media.
|
* 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 eventHandler A handler associated with the main thread's looper.
|
||||||
* @param eventListener An event listener.
|
* @param eventListener An event listener.
|
||||||
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
||||||
@ -294,6 +314,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
MediaCodecSelector mediaCodecSelector,
|
MediaCodecSelector mediaCodecSelector,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
boolean playClearSamplesWithoutKeys,
|
boolean playClearSamplesWithoutKeys,
|
||||||
|
boolean enableDecoderFallback,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
VideoRendererEventListener eventListener,
|
VideoRendererEventListener eventListener,
|
||||||
long allowedVideoJoiningTimeMs,
|
long allowedVideoJoiningTimeMs,
|
||||||
@ -305,6 +326,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
allowedVideoJoiningTimeMs,
|
allowedVideoJoiningTimeMs,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
playClearSamplesWithoutKeys,
|
playClearSamplesWithoutKeys,
|
||||||
|
enableDecoderFallback,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
|
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
|
* @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
|
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||||
* the media.
|
* 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
|
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
|
||||||
* before output. May be empty.
|
* before output. May be empty.
|
||||||
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
||||||
@ -368,6 +393,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
MediaCodecSelector mediaCodecSelector,
|
MediaCodecSelector mediaCodecSelector,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
boolean playClearSamplesWithoutKeys,
|
boolean playClearSamplesWithoutKeys,
|
||||||
|
boolean enableDecoderFallback,
|
||||||
AudioProcessor[] audioProcessors,
|
AudioProcessor[] audioProcessors,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
AudioRendererEventListener eventListener,
|
AudioRendererEventListener eventListener,
|
||||||
@ -378,10 +404,10 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
mediaCodecSelector,
|
mediaCodecSelector,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
playClearSamplesWithoutKeys,
|
playClearSamplesWithoutKeys,
|
||||||
|
enableDecoderFallback,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
AudioCapabilities.getCapabilities(context),
|
new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
|
||||||
audioProcessors));
|
|
||||||
|
|
||||||
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
|
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
|
||||||
return;
|
return;
|
||||||
|
@ -510,7 +510,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getTotalBufferedDuration() {
|
public long getTotalBufferedDuration() {
|
||||||
return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs));
|
return C.usToMs(playbackInfo.totalBufferedDurationUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -729,13 +729,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
newPlayingPeriodHolder = queue.advancePlayingPeriod();
|
newPlayingPeriodHolder = queue.advancePlayingPeriod();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable all the renderers if the period being played is changing, or if forced.
|
// Disable all renderers if the period being played is changing, if the seek results in negative
|
||||||
if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) {
|
// renderer timestamps, or if forced.
|
||||||
|
if (forceDisableRenderers
|
||||||
|
|| oldPlayingPeriodHolder != newPlayingPeriodHolder
|
||||||
|
|| (newPlayingPeriodHolder != null
|
||||||
|
&& newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
|
||||||
for (Renderer renderer : enabledRenderers) {
|
for (Renderer renderer : enabledRenderers) {
|
||||||
disableRenderer(renderer);
|
disableRenderer(renderer);
|
||||||
}
|
}
|
||||||
enabledRenderers = new Renderer[0];
|
enabledRenderers = new Renderer[0];
|
||||||
oldPlayingPeriodHolder = null;
|
oldPlayingPeriodHolder = null;
|
||||||
|
if (newPlayingPeriodHolder != null) {
|
||||||
|
newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the holders.
|
// Update the holders.
|
||||||
@ -1798,9 +1805,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
|
|
||||||
private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
|
private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
|
||||||
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
||||||
return loadingPeriodHolder == null
|
if (loadingPeriodHolder == null) {
|
||||||
? 0
|
return 0;
|
||||||
: bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
|
}
|
||||||
|
long totalBufferedDurationUs =
|
||||||
|
bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
|
||||||
|
return Math.max(0, totalBufferedDurationUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLoadControlTrackSelection(
|
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". */
|
/** 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.
|
// 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}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// 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.
|
* 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).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// 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}
|
* 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.
|
* Creates a new holder with information required to play it as part of a timeline.
|
||||||
*
|
*
|
||||||
* @param rendererCapabilities The renderer capabilities.
|
* @param rendererCapabilities The renderer capabilities.
|
||||||
* @param rendererPositionOffsetUs The time offset of the start of the media period to provide to
|
* @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
|
||||||
* renderers.
|
|
||||||
* @param trackSelector The track selector.
|
* @param trackSelector The track selector.
|
||||||
* @param allocator The allocator.
|
* @param allocator The allocator.
|
||||||
* @param mediaSource The media source that produced the media period.
|
* @param mediaSource The media source that produced the media period.
|
||||||
@ -82,7 +81,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
MediaSource mediaSource,
|
MediaSource mediaSource,
|
||||||
MediaPeriodInfo info) {
|
MediaPeriodInfo info) {
|
||||||
this.rendererCapabilities = rendererCapabilities;
|
this.rendererCapabilities = rendererCapabilities;
|
||||||
this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs;
|
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
||||||
this.trackSelector = trackSelector;
|
this.trackSelector = trackSelector;
|
||||||
this.mediaSource = mediaSource;
|
this.mediaSource = mediaSource;
|
||||||
this.uid = info.id.periodUid;
|
this.uid = info.id.periodUid;
|
||||||
@ -115,6 +114,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
return rendererPositionOffsetUs;
|
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. */
|
/** Returns start position of period in renderer time. */
|
||||||
public long getStartPositionRendererTime() {
|
public long getStartPositionRendererTime() {
|
||||||
return info.startPositionUs + rendererPositionOffsetUs;
|
return info.startPositionUs + rendererPositionOffsetUs;
|
||||||
|
@ -144,8 +144,8 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
MediaPeriodInfo info) {
|
MediaPeriodInfo info) {
|
||||||
long rendererPositionOffsetUs =
|
long rendererPositionOffsetUs =
|
||||||
loading == null
|
loading == null
|
||||||
? info.startPositionUs
|
? (info.id.isAd() ? info.contentPositionUs : 0)
|
||||||
: (loading.getRendererOffset() + loading.info.durationUs);
|
: (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
|
||||||
MediaPeriodHolder newPeriodHolder =
|
MediaPeriodHolder newPeriodHolder =
|
||||||
new MediaPeriodHolder(
|
new MediaPeriodHolder(
|
||||||
rendererCapabilities,
|
rendererCapabilities,
|
||||||
|
@ -1125,6 +1125,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
case C.ENCODING_AC3:
|
case C.ENCODING_AC3:
|
||||||
return 640 * 1000 / 8;
|
return 640 * 1000 / 8;
|
||||||
case C.ENCODING_E_AC3:
|
case C.ENCODING_E_AC3:
|
||||||
|
case C.ENCODING_E_AC3_JOC:
|
||||||
return 6144 * 1000 / 8;
|
return 6144 * 1000 / 8;
|
||||||
case C.ENCODING_AC4:
|
case C.ENCODING_AC4:
|
||||||
return 2688 * 1000 / 8;
|
return 2688 * 1000 / 8;
|
||||||
@ -1154,7 +1155,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
return DtsUtil.parseDtsAudioSampleCount(buffer);
|
return DtsUtil.parseDtsAudioSampleCount(buffer);
|
||||||
} else if (encoding == C.ENCODING_AC3) {
|
} else if (encoding == C.ENCODING_AC3) {
|
||||||
return Ac3Util.getAc3SyncframeAudioSampleCount();
|
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);
|
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
|
||||||
} else if (encoding == C.ENCODING_AC4) {
|
} else if (encoding == C.ENCODING_AC4) {
|
||||||
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
|
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
|
||||||
@ -1177,11 +1178,10 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
@TargetApi(21)
|
@TargetApi(21)
|
||||||
private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size,
|
private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size,
|
||||||
long presentationTimeUs) {
|
long presentationTimeUs) {
|
||||||
// TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed.
|
if (Util.SDK_INT >= 26) {
|
||||||
// if (Util.SDK_INT >= 23) {
|
// The underlying platform AudioTrack writes AV sync headers directly.
|
||||||
// // The underlying platform AudioTrack writes AV sync headers directly.
|
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
|
||||||
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
|
}
|
||||||
// }
|
|
||||||
if (avSyncHeader == null) {
|
if (avSyncHeader == null) {
|
||||||
avSyncHeader = ByteBuffer.allocate(16);
|
avSyncHeader = ByteBuffer.allocate(16);
|
||||||
avSyncHeader.order(ByteOrder.BIG_ENDIAN);
|
avSyncHeader.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
@ -245,12 +245,50 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
@Nullable Handler eventHandler,
|
@Nullable Handler eventHandler,
|
||||||
@Nullable AudioRendererEventListener eventListener,
|
@Nullable AudioRendererEventListener eventListener,
|
||||||
AudioSink audioSink) {
|
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(
|
super(
|
||||||
C.TRACK_TYPE_AUDIO,
|
C.TRACK_TYPE_AUDIO,
|
||||||
mediaCodecSelector,
|
mediaCodecSelector,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
playClearSamplesWithoutKeys,
|
playClearSamplesWithoutKeys,
|
||||||
/* enableDecoderFallback= */ false,
|
enableDecoderFallback,
|
||||||
/* assumedMinimumCodecOperatingRate= */ 44100);
|
/* assumedMinimumCodecOperatingRate= */ 44100);
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.audioSink = audioSink;
|
this.audioSink = audioSink;
|
||||||
@ -341,7 +379,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
* @return Whether passthrough playback is supported.
|
* @return Whether passthrough playback is supported.
|
||||||
*/
|
*/
|
||||||
protected boolean allowPassthrough(int channelCount, String mimeType) {
|
protected boolean allowPassthrough(int channelCount, String mimeType) {
|
||||||
return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType));
|
return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -437,11 +475,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
@C.Encoding int encoding;
|
@C.Encoding int encoding;
|
||||||
MediaFormat format;
|
MediaFormat format;
|
||||||
if (passthroughMediaFormat != null) {
|
if (passthroughMediaFormat != null) {
|
||||||
encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME));
|
|
||||||
format = passthroughMediaFormat;
|
format = passthroughMediaFormat;
|
||||||
|
encoding =
|
||||||
|
getPassthroughEncoding(
|
||||||
|
format.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
|
||||||
|
format.getString(MediaFormat.KEY_MIME));
|
||||||
} else {
|
} else {
|
||||||
encoding = pcmEncoding;
|
|
||||||
format = outputFormat;
|
format = outputFormat;
|
||||||
|
encoding = pcmEncoding;
|
||||||
}
|
}
|
||||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||||
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
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
|
* 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
|
* 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.nio.ByteBuffer;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
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 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
|
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
|
||||||
* no codec operating rate should be set.
|
* no codec operating rate should be set.
|
||||||
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
|
|
||||||
*/
|
*/
|
||||||
protected abstract void configureCodec(
|
protected abstract void configureCodec(
|
||||||
MediaCodecInfo codecInfo,
|
MediaCodecInfo codecInfo,
|
||||||
MediaCodec codec,
|
MediaCodec codec,
|
||||||
Format format,
|
Format format,
|
||||||
MediaCrypto crypto,
|
MediaCrypto crypto,
|
||||||
float codecOperatingRate)
|
float codecOperatingRate);
|
||||||
throws DecoderQueryException;
|
|
||||||
|
|
||||||
protected final void maybeInitCodec() throws ExoPlaybackException {
|
protected final void maybeInitCodec() throws ExoPlaybackException {
|
||||||
if (codec != null || inputFormat == null) {
|
if (codec != null || inputFormat == null) {
|
||||||
@ -742,11 +739,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
try {
|
try {
|
||||||
List<MediaCodecInfo> allAvailableCodecInfos =
|
List<MediaCodecInfo> allAvailableCodecInfos =
|
||||||
getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);
|
getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);
|
||||||
|
availableCodecInfos = new ArrayDeque<>();
|
||||||
if (enableDecoderFallback) {
|
if (enableDecoderFallback) {
|
||||||
availableCodecInfos = new ArrayDeque<>(allAvailableCodecInfos);
|
availableCodecInfos.addAll(allAvailableCodecInfos);
|
||||||
} else {
|
} else if (!allAvailableCodecInfos.isEmpty()) {
|
||||||
availableCodecInfos =
|
availableCodecInfos.add(allAvailableCodecInfos.get(0));
|
||||||
new ArrayDeque<>(Collections.singletonList(allAvailableCodecInfos.get(0)));
|
|
||||||
}
|
}
|
||||||
preferredDecoderInitializationException = null;
|
preferredDecoderInitializationException = null;
|
||||||
} catch (DecoderQueryException e) {
|
} 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.
|
// 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);
|
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling);
|
||||||
ArrayList<MediaCodecInfo> eac3DecoderInfos =
|
ArrayList<MediaCodecInfo> eac3DecoderInfos =
|
||||||
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
|
getDecoderInfosInternal(eac3Key, mediaCodecList, MimeTypes.AUDIO_E_AC3);
|
||||||
decoderInfos.addAll(eac3DecoderInfos);
|
decoderInfos.addAll(eac3DecoderInfos);
|
||||||
}
|
}
|
||||||
applyWorkarounds(mimeType, decoderInfos);
|
applyWorkarounds(mimeType, decoderInfos);
|
||||||
|
@ -951,6 +951,7 @@ public final class DownloadHelper {
|
|||||||
downloadHelper.onMediaPrepared();
|
downloadHelper.onMediaPrepared();
|
||||||
return true;
|
return true;
|
||||||
case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:
|
case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:
|
||||||
|
release();
|
||||||
downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj));
|
downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj));
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
|
@ -27,6 +27,7 @@ import android.os.Parcel;
|
|||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@ -128,7 +129,7 @@ public final class Requirements implements Parcelable {
|
|||||||
|
|
||||||
ConnectivityManager connectivityManager =
|
ConnectivityManager connectivityManager =
|
||||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
|
NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo();
|
||||||
if (networkInfo == null
|
if (networkInfo == null
|
||||||
|| !networkInfo.isConnected()
|
|| !networkInfo.isConnected()
|
||||||
|| !isInternetConnectivityValidated(connectivityManager)) {
|
|| !isInternetConnectivityValidated(connectivityManager)) {
|
||||||
|
@ -28,6 +28,7 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,7 +127,8 @@ public final class RequirementsWatcher {
|
|||||||
@TargetApi(23)
|
@TargetApi(23)
|
||||||
private void registerNetworkCallbackV23() {
|
private void registerNetworkCallbackV23() {
|
||||||
ConnectivityManager connectivityManager =
|
ConnectivityManager connectivityManager =
|
||||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
Assertions.checkNotNull(
|
||||||
|
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
|
||||||
NetworkRequest request =
|
NetworkRequest request =
|
||||||
new NetworkRequest.Builder()
|
new NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
.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).
|
* 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_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
|
* Command initiating roll-up style captioning, with the maximum of 2 rows displayed
|
||||||
* simultaneously.
|
* simultaneously.
|
||||||
@ -95,25 +100,31 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
* simultaneously.
|
* simultaneously.
|
||||||
*/
|
*/
|
||||||
private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
|
private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command initiating paint-on style captioning. Subsequent data should be addressed immediately
|
* 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.
|
* to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.
|
||||||
*/
|
*/
|
||||||
private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;
|
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
|
* TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out
|
||||||
* non-displayed memory should be swapped with the one in displayed memory. If no
|
* until a command is received that switches back to the CAPTION service.
|
||||||
* {@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;
|
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_ERASE_DISPLAYED_MEMORY = 0x2C;
|
||||||
private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
|
private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
|
||||||
private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
|
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).
|
// Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
|
||||||
private static final int[] BASIC_CHARACTER_SET = new int[] {
|
private static final int[] BASIC_CHARACTER_SET = new int[] {
|
||||||
@ -237,6 +248,11 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
private byte repeatableControlCc2;
|
private byte repeatableControlCc2;
|
||||||
private int currentChannel;
|
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) {
|
public Cea608Decoder(String mimeType, int accessibilityChannel) {
|
||||||
ccData = new ParsableByteArray();
|
ccData = new ParsableByteArray();
|
||||||
cueBuilders = new ArrayList<>();
|
cueBuilders = new ArrayList<>();
|
||||||
@ -268,6 +284,7 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
|
|
||||||
setCaptionMode(CC_MODE_UNKNOWN);
|
setCaptionMode(CC_MODE_UNKNOWN);
|
||||||
resetCueBuilders();
|
resetCueBuilders();
|
||||||
|
isInCaptionService = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -288,6 +305,7 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
repeatableControlCc1 = 0;
|
repeatableControlCc1 = 0;
|
||||||
repeatableControlCc2 = 0;
|
repeatableControlCc2 = 0;
|
||||||
currentChannel = NTSC_CC_CHANNEL_1;
|
currentChannel = NTSC_CC_CHANNEL_1;
|
||||||
|
isInCaptionService = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -363,6 +381,12 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maybeUpdateIsInCaptionService(ccData1, ccData2);
|
||||||
|
if (!isInCaptionService) {
|
||||||
|
// Only the Captioning service is supported. Drop all other bytes.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Special North American character set.
|
// Special North American character set.
|
||||||
// ccData1 - 0|0|0|1|C|0|0|1
|
// ccData1 - 0|0|0|1|C|0|0|1
|
||||||
// ccData2 - 0|0|1|1|X|X|X|X
|
// ccData2 - 0|0|1|1|X|X|X|X
|
||||||
@ -629,6 +653,29 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
cueBuilders.add(currentCueBuilder);
|
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) {
|
private static char getChar(byte ccData) {
|
||||||
int index = (ccData & 0x7F) - 0x20;
|
int index = (ccData & 0x7F) - 0x20;
|
||||||
return (char) BASIC_CHARACTER_SET[index];
|
return (char) BASIC_CHARACTER_SET[index];
|
||||||
@ -683,6 +730,15 @@ public final class Cea608Decoder extends CeaDecoder {
|
|||||||
return (cc1 & 0xF0) == 0x10;
|
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 {
|
private static class CueBuilder {
|
||||||
|
|
||||||
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
|
// 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,
|
/* lineType= */ Cue.LINE_TYPE_FRACTION,
|
||||||
lineAnchor,
|
lineAnchor,
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
|
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
|
||||||
/* textSize= */ regionTextHeight);
|
/* textSize= */ regionTextHeight);
|
||||||
}
|
}
|
||||||
|
@ -231,11 +231,11 @@ import java.util.TreeSet;
|
|||||||
new Cue(
|
new Cue(
|
||||||
bitmap,
|
bitmap,
|
||||||
region.position,
|
region.position,
|
||||||
Cue.ANCHOR_TYPE_MIDDLE,
|
Cue.ANCHOR_TYPE_START,
|
||||||
region.line,
|
region.line,
|
||||||
region.lineAnchor,
|
region.lineAnchor,
|
||||||
region.width,
|
region.width,
|
||||||
/* height= */ Cue.DIMEN_UNSET));
|
region.height));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create text based cues.
|
// Create text based cues.
|
||||||
|
@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue;
|
|||||||
public final @Cue.LineType int lineType;
|
public final @Cue.LineType int lineType;
|
||||||
public final @Cue.AnchorType int lineAnchor;
|
public final @Cue.AnchorType int lineAnchor;
|
||||||
public final float width;
|
public final float width;
|
||||||
|
public final float height;
|
||||||
public final @Cue.TextSizeType int textSizeType;
|
public final @Cue.TextSizeType int textSizeType;
|
||||||
public final float textSize;
|
public final float textSize;
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ import com.google.android.exoplayer2.text.Cue;
|
|||||||
/* lineType= */ Cue.TYPE_UNSET,
|
/* lineType= */ Cue.TYPE_UNSET,
|
||||||
/* lineAnchor= */ Cue.TYPE_UNSET,
|
/* lineAnchor= */ Cue.TYPE_UNSET,
|
||||||
/* width= */ Cue.DIMEN_UNSET,
|
/* width= */ Cue.DIMEN_UNSET,
|
||||||
|
/* height= */ Cue.DIMEN_UNSET,
|
||||||
/* textSizeType= */ Cue.TYPE_UNSET,
|
/* textSizeType= */ Cue.TYPE_UNSET,
|
||||||
/* textSize= */ Cue.DIMEN_UNSET);
|
/* textSize= */ Cue.DIMEN_UNSET);
|
||||||
}
|
}
|
||||||
@ -50,6 +52,7 @@ import com.google.android.exoplayer2.text.Cue;
|
|||||||
@Cue.LineType int lineType,
|
@Cue.LineType int lineType,
|
||||||
@Cue.AnchorType int lineAnchor,
|
@Cue.AnchorType int lineAnchor,
|
||||||
float width,
|
float width,
|
||||||
|
float height,
|
||||||
int textSizeType,
|
int textSizeType,
|
||||||
float textSize) {
|
float textSize) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@ -58,6 +61,7 @@ import com.google.android.exoplayer2.text.Cue;
|
|||||||
this.lineType = lineType;
|
this.lineType = lineType;
|
||||||
this.lineAnchor = lineAnchor;
|
this.lineAnchor = lineAnchor;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
this.textSizeType = textSizeType;
|
this.textSizeType = textSizeType;
|
||||||
this.textSize = textSize;
|
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.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
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 PROPERTY_TEXT_DECORATION = "text-decoration";
|
||||||
private static final String VALUE_BOLD = "bold";
|
private static final String VALUE_BOLD = "bold";
|
||||||
private static final String VALUE_UNDERLINE = "underline";
|
private static final String VALUE_UNDERLINE = "underline";
|
||||||
private static final String BLOCK_START = "{";
|
private static final String RULE_START = "{";
|
||||||
private static final String BLOCK_END = "}";
|
private static final String RULE_END = "}";
|
||||||
private static final String PROPERTY_FONT_STYLE = "font-style";
|
private static final String PROPERTY_FONT_STYLE = "font-style";
|
||||||
private static final String VALUE_ITALIC = "italic";
|
private static final String VALUE_ITALIC = "italic";
|
||||||
|
|
||||||
@ -52,37 +54,47 @@ import java.util.regex.Pattern;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the
|
* Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents
|
||||||
* contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or
|
* of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If
|
||||||
* {@code null} otherwise.
|
* 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.
|
* @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);
|
stringBuilder.setLength(0);
|
||||||
int initialInputPosition = input.getPosition();
|
int initialInputPosition = input.getPosition();
|
||||||
skipStyleBlock(input);
|
skipStyleBlock(input);
|
||||||
styleInput.reset(input.data, input.getPosition());
|
styleInput.reset(input.data, input.getPosition());
|
||||||
styleInput.setPosition(initialInputPosition);
|
styleInput.setPosition(initialInputPosition);
|
||||||
String selector = parseSelector(styleInput, stringBuilder);
|
|
||||||
if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
|
List<WebvttCssStyle> styles = new ArrayList<>();
|
||||||
return null;
|
String selector;
|
||||||
}
|
while ((selector = parseSelector(styleInput, stringBuilder)) != null) {
|
||||||
WebvttCssStyle style = new WebvttCssStyle();
|
if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) {
|
||||||
applySelectorToStyle(style, selector);
|
return styles;
|
||||||
String token = null;
|
}
|
||||||
boolean blockEndFound = false;
|
WebvttCssStyle style = new WebvttCssStyle();
|
||||||
while (!blockEndFound) {
|
applySelectorToStyle(style, selector);
|
||||||
int position = styleInput.getPosition();
|
String token = null;
|
||||||
token = parseNextToken(styleInput, stringBuilder);
|
boolean blockEndFound = false;
|
||||||
blockEndFound = token == null || BLOCK_END.equals(token);
|
while (!blockEndFound) {
|
||||||
if (!blockEndFound) {
|
int position = styleInput.getPosition();
|
||||||
styleInput.setPosition(position);
|
token = parseNextToken(styleInput, stringBuilder);
|
||||||
parseStyleDeclaration(styleInput, style, stringBuilder);
|
blockEndFound = token == null || RULE_END.equals(token);
|
||||||
|
if (!blockEndFound) {
|
||||||
|
styleInput.setPosition(position);
|
||||||
|
parseStyleDeclaration(styleInput, style, stringBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check that the style rule ended correctly.
|
||||||
|
if (RULE_END.equals(token)) {
|
||||||
|
styles.add(style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,7 +119,7 @@ import java.util.regex.Pattern;
|
|||||||
if (token == null) {
|
if (token == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (BLOCK_START.equals(token)) {
|
if (RULE_START.equals(token)) {
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -156,7 +168,7 @@ import java.util.regex.Pattern;
|
|||||||
String token = parseNextToken(input, stringBuilder);
|
String token = parseNextToken(input, stringBuilder);
|
||||||
if (";".equals(token)) {
|
if (";".equals(token)) {
|
||||||
// The style declaration is well formed.
|
// 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
|
// The style declaration is well formed and we can go on, but the closing bracket had to be
|
||||||
// fed back.
|
// fed back.
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
@ -250,7 +262,7 @@ import java.util.regex.Pattern;
|
|||||||
// Syntax error.
|
// Syntax error.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (BLOCK_END.equals(token) || ";".equals(token)) {
|
if (RULE_END.equals(token) || ";".equals(token)) {
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
expressionEndFound = true;
|
expressionEndFound = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -80,10 +80,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
|||||||
throw new SubtitleDecoderException("A style block was found after the first cue.");
|
throw new SubtitleDecoderException("A style block was found after the first cue.");
|
||||||
}
|
}
|
||||||
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
||||||
WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
|
definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
|
||||||
if (styleBlock != null) {
|
|
||||||
definedStyles.add(styleBlock);
|
|
||||||
}
|
|
||||||
} else if (event == EVENT_CUE) {
|
} else if (event == EVENT_CUE) {
|
||||||
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
|
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
|
||||||
subtitles.add(webvttCueBuilder.build());
|
subtitles.add(webvttCueBuilder.build());
|
||||||
|
@ -757,7 +757,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
|||||||
for (int i = 0; i < values.length; i++) {
|
for (int i = 0; i < values.length; i++) {
|
||||||
logValues[i] = new double[values[i].length];
|
logValues[i] = new double[values[i].length];
|
||||||
for (int j = 0; j < values[i].length; j++) {
|
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;
|
return logValues;
|
||||||
@ -779,7 +779,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
|||||||
double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];
|
double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];
|
||||||
for (int j = 0; j < logBitrates[i].length - 1; j++) {
|
for (int j = 0; j < logBitrates[i].length - 1; j++) {
|
||||||
double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]);
|
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;
|
return switchPoints;
|
||||||
|
@ -1934,6 +1934,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
getAdaptiveAudioTracks(
|
getAdaptiveAudioTracks(
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
formatSupports[selectedGroupIndex],
|
formatSupports[selectedGroupIndex],
|
||||||
|
params.maxAudioBitrate,
|
||||||
params.allowAudioMixedMimeTypeAdaptiveness,
|
params.allowAudioMixedMimeTypeAdaptiveness,
|
||||||
params.allowAudioMixedSampleRateAdaptiveness);
|
params.allowAudioMixedSampleRateAdaptiveness);
|
||||||
if (adaptiveTracks.length > 0) {
|
if (adaptiveTracks.length > 0) {
|
||||||
@ -1951,6 +1952,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
private static int[] getAdaptiveAudioTracks(
|
private static int[] getAdaptiveAudioTracks(
|
||||||
TrackGroup group,
|
TrackGroup group,
|
||||||
int[] formatSupport,
|
int[] formatSupport,
|
||||||
|
int maxAudioBitrate,
|
||||||
boolean allowMixedMimeTypeAdaptiveness,
|
boolean allowMixedMimeTypeAdaptiveness,
|
||||||
boolean allowMixedSampleRateAdaptiveness) {
|
boolean allowMixedSampleRateAdaptiveness) {
|
||||||
int selectedConfigurationTrackCount = 0;
|
int selectedConfigurationTrackCount = 0;
|
||||||
@ -1967,6 +1969,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
group,
|
group,
|
||||||
formatSupport,
|
formatSupport,
|
||||||
configuration,
|
configuration,
|
||||||
|
maxAudioBitrate,
|
||||||
allowMixedMimeTypeAdaptiveness,
|
allowMixedMimeTypeAdaptiveness,
|
||||||
allowMixedSampleRateAdaptiveness);
|
allowMixedSampleRateAdaptiveness);
|
||||||
if (configurationCount > selectedConfigurationTrackCount) {
|
if (configurationCount > selectedConfigurationTrackCount) {
|
||||||
@ -1977,13 +1980,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedConfigurationTrackCount > 1) {
|
if (selectedConfigurationTrackCount > 1) {
|
||||||
|
Assertions.checkNotNull(selectedConfiguration);
|
||||||
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
|
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (int i = 0; i < group.length; i++) {
|
for (int i = 0; i < group.length; i++) {
|
||||||
|
Format format = group.getFormat(i);
|
||||||
if (isSupportedAdaptiveAudioTrack(
|
if (isSupportedAdaptiveAudioTrack(
|
||||||
group.getFormat(i),
|
format,
|
||||||
formatSupport[i],
|
formatSupport[i],
|
||||||
Assertions.checkNotNull(selectedConfiguration),
|
selectedConfiguration,
|
||||||
|
maxAudioBitrate,
|
||||||
allowMixedMimeTypeAdaptiveness,
|
allowMixedMimeTypeAdaptiveness,
|
||||||
allowMixedSampleRateAdaptiveness)) {
|
allowMixedSampleRateAdaptiveness)) {
|
||||||
adaptiveIndices[index++] = i;
|
adaptiveIndices[index++] = i;
|
||||||
@ -1998,6 +2004,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
TrackGroup group,
|
TrackGroup group,
|
||||||
int[] formatSupport,
|
int[] formatSupport,
|
||||||
AudioConfigurationTuple configuration,
|
AudioConfigurationTuple configuration,
|
||||||
|
int maxAudioBitrate,
|
||||||
boolean allowMixedMimeTypeAdaptiveness,
|
boolean allowMixedMimeTypeAdaptiveness,
|
||||||
boolean allowMixedSampleRateAdaptiveness) {
|
boolean allowMixedSampleRateAdaptiveness) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@ -2006,6 +2013,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
group.getFormat(i),
|
group.getFormat(i),
|
||||||
formatSupport[i],
|
formatSupport[i],
|
||||||
configuration,
|
configuration,
|
||||||
|
maxAudioBitrate,
|
||||||
allowMixedMimeTypeAdaptiveness,
|
allowMixedMimeTypeAdaptiveness,
|
||||||
allowMixedSampleRateAdaptiveness)) {
|
allowMixedSampleRateAdaptiveness)) {
|
||||||
count++;
|
count++;
|
||||||
@ -2018,9 +2026,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
|||||||
Format format,
|
Format format,
|
||||||
int formatSupport,
|
int formatSupport,
|
||||||
AudioConfigurationTuple configuration,
|
AudioConfigurationTuple configuration,
|
||||||
|
int maxAudioBitrate,
|
||||||
boolean allowMixedMimeTypeAdaptiveness,
|
boolean allowMixedMimeTypeAdaptiveness,
|
||||||
boolean allowMixedSampleRateAdaptiveness) {
|
boolean allowMixedSampleRateAdaptiveness) {
|
||||||
return isSupported(formatSupport, false)
|
return isSupported(formatSupport, false)
|
||||||
|
&& (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate)
|
||||||
&& (format.channelCount != Format.NO_VALUE
|
&& (format.channelCount != Format.NO_VALUE
|
||||||
&& format.channelCount == configuration.channelCount)
|
&& format.channelCount == configuration.channelCount)
|
||||||
&& (allowMixedMimeTypeAdaptiveness
|
&& (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
|
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
|
||||||
* explicit dependency on ExoPlayer's RTMP extension.
|
* explicit dependency on ExoPlayer's RTMP extension.
|
||||||
* <li>data: For parsing data inlined in the URI as defined in RFC 2397.
|
* <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),
|
* <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,
|
* if constructed using {@link #DefaultDataSource(Context, TransferListener, String,
|
||||||
* boolean)}, or any other schemes supported by a base data source if constructed using {@link
|
* 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_ASSET = "asset";
|
||||||
private static final String SCHEME_CONTENT = "content";
|
private static final String SCHEME_CONTENT = "content";
|
||||||
private static final String SCHEME_RTMP = "rtmp";
|
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 static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -62,12 +64,13 @@ public final class DefaultDataSource implements DataSource {
|
|||||||
private final DataSource baseDataSource;
|
private final DataSource baseDataSource;
|
||||||
|
|
||||||
// Lazily initialized.
|
// Lazily initialized.
|
||||||
private @Nullable DataSource fileDataSource;
|
@Nullable private DataSource fileDataSource;
|
||||||
private @Nullable DataSource assetDataSource;
|
@Nullable private DataSource assetDataSource;
|
||||||
private @Nullable DataSource contentDataSource;
|
@Nullable private DataSource contentDataSource;
|
||||||
private @Nullable DataSource rtmpDataSource;
|
@Nullable private DataSource rtmpDataSource;
|
||||||
private @Nullable DataSource dataSchemeDataSource;
|
@Nullable private DataSource udpDataSource;
|
||||||
private @Nullable DataSource rawResourceDataSource;
|
@Nullable private DataSource dataSchemeDataSource;
|
||||||
|
@Nullable private DataSource rawResourceDataSource;
|
||||||
|
|
||||||
private @Nullable DataSource dataSource;
|
private @Nullable DataSource dataSource;
|
||||||
|
|
||||||
@ -218,6 +221,7 @@ public final class DefaultDataSource implements DataSource {
|
|||||||
maybeAddListenerToDataSource(assetDataSource, transferListener);
|
maybeAddListenerToDataSource(assetDataSource, transferListener);
|
||||||
maybeAddListenerToDataSource(contentDataSource, transferListener);
|
maybeAddListenerToDataSource(contentDataSource, transferListener);
|
||||||
maybeAddListenerToDataSource(rtmpDataSource, transferListener);
|
maybeAddListenerToDataSource(rtmpDataSource, transferListener);
|
||||||
|
maybeAddListenerToDataSource(udpDataSource, transferListener);
|
||||||
maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);
|
maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);
|
||||||
maybeAddListenerToDataSource(rawResourceDataSource, transferListener);
|
maybeAddListenerToDataSource(rawResourceDataSource, transferListener);
|
||||||
}
|
}
|
||||||
@ -240,6 +244,8 @@ public final class DefaultDataSource implements DataSource {
|
|||||||
dataSource = getContentDataSource();
|
dataSource = getContentDataSource();
|
||||||
} else if (SCHEME_RTMP.equals(scheme)) {
|
} else if (SCHEME_RTMP.equals(scheme)) {
|
||||||
dataSource = getRtmpDataSource();
|
dataSource = getRtmpDataSource();
|
||||||
|
} else if (SCHEME_UDP.equals(scheme)) {
|
||||||
|
dataSource = getUdpDataSource();
|
||||||
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
|
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
|
||||||
dataSource = getDataSchemeDataSource();
|
dataSource = getDataSchemeDataSource();
|
||||||
} else if (SCHEME_RAW.equals(scheme)) {
|
} 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() {
|
private DataSource getFileDataSource() {
|
||||||
if (fileDataSource == null) {
|
if (fileDataSource == null) {
|
||||||
fileDataSource = new FileDataSource();
|
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 @Nullable DataSource currentDataSource;
|
||||||
private boolean currentDataSpecLengthUnset;
|
private boolean currentDataSpecLengthUnset;
|
||||||
private @Nullable Uri uri;
|
@Nullable private Uri uri;
|
||||||
private @Nullable Uri actualUri;
|
@Nullable private Uri actualUri;
|
||||||
private @HttpMethod int httpMethod;
|
@HttpMethod private int httpMethod;
|
||||||
private int flags;
|
private int flags;
|
||||||
private @Nullable String key;
|
private @Nullable String key;
|
||||||
private long readPosition;
|
private long readPosition;
|
||||||
@ -319,7 +319,7 @@ public final class CacheDataSource implements DataSource {
|
|||||||
}
|
}
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
|
if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) {
|
||||||
setNoBytesRemainingAndMaybeStoreLength();
|
setNoBytesRemainingAndMaybeStoreLength();
|
||||||
return C.RESULT_END_OF_INPUT;
|
return C.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
@ -484,20 +484,6 @@ public final class CacheDataSource implements DataSource {
|
|||||||
return redirectedUri != null ? redirectedUri : defaultUri;
|
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() {
|
private boolean isReadingFromUpstream() {
|
||||||
return !isReadingFromCache();
|
return !isReadingFromCache();
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
|
|||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
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.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
||||||
@ -78,13 +79,7 @@ public final class CacheUtil {
|
|||||||
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
|
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
|
||||||
String key = buildCacheKey(dataSpec, cacheKeyFactory);
|
String key = buildCacheKey(dataSpec, cacheKeyFactory);
|
||||||
long position = dataSpec.absoluteStreamPosition;
|
long position = dataSpec.absoluteStreamPosition;
|
||||||
long requestLength;
|
long requestLength = getRequestLength(dataSpec, cache, key);
|
||||||
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 bytesAlreadyCached = 0;
|
long bytesAlreadyCached = 0;
|
||||||
long bytesLeft = requestLength;
|
long bytesLeft = requestLength;
|
||||||
while (bytesLeft != 0) {
|
while (bytesLeft != 0) {
|
||||||
@ -179,53 +174,66 @@ public final class CacheUtil {
|
|||||||
Assertions.checkNotNull(dataSource);
|
Assertions.checkNotNull(dataSource);
|
||||||
Assertions.checkNotNull(buffer);
|
Assertions.checkNotNull(buffer);
|
||||||
|
|
||||||
|
String key = buildCacheKey(dataSpec, cacheKeyFactory);
|
||||||
|
long bytesLeft;
|
||||||
ProgressNotifier progressNotifier = null;
|
ProgressNotifier progressNotifier = null;
|
||||||
if (progressListener != null) {
|
if (progressListener != null) {
|
||||||
progressNotifier = new ProgressNotifier(progressListener);
|
progressNotifier = new ProgressNotifier(progressListener);
|
||||||
Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
|
Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
|
||||||
progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
|
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 position = dataSpec.absoluteStreamPosition;
|
||||||
long bytesLeft;
|
boolean lengthUnset = bytesLeft == C.LENGTH_UNSET;
|
||||||
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;
|
|
||||||
}
|
|
||||||
while (bytesLeft != 0) {
|
while (bytesLeft != 0) {
|
||||||
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
||||||
long blockLength =
|
long blockLength =
|
||||||
cache.getCachedLength(
|
cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft);
|
||||||
key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
|
|
||||||
if (blockLength > 0) {
|
if (blockLength > 0) {
|
||||||
// Skip already cached data.
|
// Skip already cached data.
|
||||||
} else {
|
} else {
|
||||||
// There is a hole in the cache which is at least "-blockLength" long.
|
// There is a hole in the cache which is at least "-blockLength" long.
|
||||||
blockLength = -blockLength;
|
blockLength = -blockLength;
|
||||||
|
long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength;
|
||||||
|
boolean isLastBlock = length == bytesLeft;
|
||||||
long read =
|
long read =
|
||||||
readAndDiscard(
|
readAndDiscard(
|
||||||
dataSpec,
|
dataSpec,
|
||||||
position,
|
position,
|
||||||
blockLength,
|
length,
|
||||||
dataSource,
|
dataSource,
|
||||||
buffer,
|
buffer,
|
||||||
priorityTaskManager,
|
priorityTaskManager,
|
||||||
priority,
|
priority,
|
||||||
progressNotifier,
|
progressNotifier,
|
||||||
|
isLastBlock,
|
||||||
isCanceled);
|
isCanceled);
|
||||||
if (read < blockLength) {
|
if (read < blockLength) {
|
||||||
// Reached to the end of the data.
|
// Reached to the end of the data.
|
||||||
if (enableEOFException && bytesLeft != C.LENGTH_UNSET) {
|
if (enableEOFException && !lengthUnset) {
|
||||||
throw new EOFException();
|
throw new EOFException();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
position += blockLength;
|
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.
|
* caching.
|
||||||
* @param priority The priority of this task.
|
* @param priority The priority of this task.
|
||||||
* @param progressNotifier A notifier through which to report progress updates, or {@code null}.
|
* @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.
|
* @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
|
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
|
||||||
* has been reached.
|
* has been reached.
|
||||||
@ -255,54 +264,64 @@ public final class CacheUtil {
|
|||||||
PriorityTaskManager priorityTaskManager,
|
PriorityTaskManager priorityTaskManager,
|
||||||
int priority,
|
int priority,
|
||||||
@Nullable ProgressNotifier progressNotifier,
|
@Nullable ProgressNotifier progressNotifier,
|
||||||
|
boolean isLastBlock,
|
||||||
AtomicBoolean isCanceled)
|
AtomicBoolean isCanceled)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;
|
long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;
|
||||||
|
long initialPositionOffset = positionOffset;
|
||||||
|
long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (priorityTaskManager != null) {
|
if (priorityTaskManager != null) {
|
||||||
// Wait for any other thread with higher priority to finish its job.
|
// Wait for any other thread with higher priority to finish its job.
|
||||||
priorityTaskManager.proceed(priority);
|
priorityTaskManager.proceed(priority);
|
||||||
}
|
}
|
||||||
|
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
||||||
try {
|
try {
|
||||||
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
long resolvedLength = C.LENGTH_UNSET;
|
||||||
// Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in
|
boolean isDataSourceOpen = false;
|
||||||
// case the given length exceeds the end of input.
|
if (endOffset != C.POSITION_UNSET) {
|
||||||
dataSpec =
|
// If a specific length is given, first try to open the data source for that length to
|
||||||
new DataSpec(
|
// avoid more data then required to be requested. If the given length exceeds the end of
|
||||||
dataSpec.uri,
|
// input we will get a "position out of range" error. In that case try to open the source
|
||||||
dataSpec.httpMethod,
|
// again with unset length.
|
||||||
dataSpec.httpBody,
|
try {
|
||||||
absoluteStreamPosition,
|
resolvedLength =
|
||||||
/* position= */ dataSpec.position + positionOffset,
|
dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset));
|
||||||
C.LENGTH_UNSET,
|
isDataSourceOpen = true;
|
||||||
dataSpec.key,
|
} catch (IOException exception) {
|
||||||
dataSpec.flags);
|
if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) {
|
||||||
long resolvedLength = dataSource.open(dataSpec);
|
throw exception;
|
||||||
if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
|
}
|
||||||
|
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);
|
progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);
|
||||||
}
|
}
|
||||||
long totalBytesRead = 0;
|
while (positionOffset != endOffset) {
|
||||||
while (totalBytesRead != length) {
|
|
||||||
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
throwExceptionIfInterruptedOrCancelled(isCanceled);
|
||||||
int bytesRead =
|
int bytesRead =
|
||||||
dataSource.read(
|
dataSource.read(
|
||||||
buffer,
|
buffer,
|
||||||
0,
|
0,
|
||||||
length != C.LENGTH_UNSET
|
endOffset != C.POSITION_UNSET
|
||||||
? (int) Math.min(buffer.length, length - totalBytesRead)
|
? (int) Math.min(buffer.length, endOffset - positionOffset)
|
||||||
: buffer.length);
|
: buffer.length);
|
||||||
if (bytesRead == C.RESULT_END_OF_INPUT) {
|
if (bytesRead == C.RESULT_END_OF_INPUT) {
|
||||||
if (progressNotifier != null) {
|
if (progressNotifier != null) {
|
||||||
progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead);
|
progressNotifier.onRequestLengthResolved(positionOffset);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
totalBytesRead += bytesRead;
|
positionOffset += bytesRead;
|
||||||
if (progressNotifier != null) {
|
if (progressNotifier != null) {
|
||||||
progressNotifier.onBytesCached(bytesRead);
|
progressNotifier.onBytesCached(bytesRead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return totalBytesRead;
|
return positionOffset - initialPositionOffset;
|
||||||
} catch (PriorityTaskManager.PriorityTooLowException exception) {
|
} catch (PriorityTaskManager.PriorityTooLowException exception) {
|
||||||
// catch and try again
|
// catch and try again
|
||||||
} finally {
|
} 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(
|
private static String buildCacheKey(
|
||||||
DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {
|
DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {
|
||||||
return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)
|
return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)
|
||||||
|
@ -348,8 +348,9 @@ public final class MimeTypes {
|
|||||||
case MimeTypes.AUDIO_AC3:
|
case MimeTypes.AUDIO_AC3:
|
||||||
return C.ENCODING_AC3;
|
return C.ENCODING_AC3;
|
||||||
case MimeTypes.AUDIO_E_AC3:
|
case MimeTypes.AUDIO_E_AC3:
|
||||||
case MimeTypes.AUDIO_E_AC3_JOC:
|
|
||||||
return C.ENCODING_E_AC3;
|
return C.ENCODING_E_AC3;
|
||||||
|
case MimeTypes.AUDIO_E_AC3_JOC:
|
||||||
|
return C.ENCODING_E_AC3_JOC;
|
||||||
case MimeTypes.AUDIO_AC4:
|
case MimeTypes.AUDIO_AC4:
|
||||||
return C.ENCODING_AC4;
|
return C.ENCODING_AC4;
|
||||||
case MimeTypes.AUDIO_DTS:
|
case MimeTypes.AUDIO_DTS:
|
||||||
|
@ -1713,7 +1713,12 @@ public final class Util {
|
|||||||
if (connectivityManager == null) {
|
if (connectivityManager == null) {
|
||||||
return C.NETWORK_TYPE_UNKNOWN;
|
return C.NETWORK_TYPE_UNKNOWN;
|
||||||
}
|
}
|
||||||
networkInfo = connectivityManager.getActiveNetworkInfo();
|
try {
|
||||||
|
networkInfo = connectivityManager.getActiveNetworkInfo();
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
// Expected if permission was revoked.
|
||||||
|
return C.NETWORK_TYPE_UNKNOWN;
|
||||||
|
}
|
||||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||||
return C.NETWORK_TYPE_OFFLINE;
|
return C.NETWORK_TYPE_OFFLINE;
|
||||||
}
|
}
|
||||||
|
@ -550,8 +550,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
MediaCodec codec,
|
MediaCodec codec,
|
||||||
Format format,
|
Format format,
|
||||||
MediaCrypto crypto,
|
MediaCrypto crypto,
|
||||||
float codecOperatingRate)
|
float codecOperatingRate) {
|
||||||
throws DecoderQueryException {
|
|
||||||
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
|
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
|
||||||
MediaFormat mediaFormat =
|
MediaFormat mediaFormat =
|
||||||
getMediaFormat(
|
getMediaFormat(
|
||||||
@ -1173,11 +1172,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
* @param format The format for which the codec is being configured.
|
* @param format The format for which the codec is being configured.
|
||||||
* @param streamFormats The possible stream formats.
|
* @param streamFormats The possible stream formats.
|
||||||
* @return Suitable {@link CodecMaxValues}.
|
* @return Suitable {@link CodecMaxValues}.
|
||||||
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
|
|
||||||
*/
|
*/
|
||||||
protected CodecMaxValues getCodecMaxValues(
|
protected CodecMaxValues getCodecMaxValues(
|
||||||
MediaCodecInfo codecInfo, Format format, Format[] streamFormats)
|
MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {
|
||||||
throws DecoderQueryException {
|
|
||||||
int maxWidth = format.width;
|
int maxWidth = format.width;
|
||||||
int maxHeight = format.height;
|
int maxHeight = format.height;
|
||||||
int maxInputSize = getMaxInputSize(codecInfo, format);
|
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
|
* Returns a maximum video size to use when configuring a codec for {@code format} in a way that
|
||||||
* that will allow possible adaptation to other compatible formats that are expected to have the
|
* will allow possible adaptation to other compatible formats that are expected to have the same
|
||||||
* same aspect ratio, but whose sizes are unknown.
|
* aspect ratio, but whose sizes are unknown.
|
||||||
*
|
*
|
||||||
* @param codecInfo Information about the {@link MediaCodec} being configured.
|
* @param codecInfo Information about the {@link MediaCodec} being configured.
|
||||||
* @param format The format for which the codec is 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.
|
* @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)
|
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {
|
||||||
throws DecoderQueryException {
|
|
||||||
boolean isVerticalVideo = format.height > format.width;
|
boolean isVerticalVideo = format.height > format.width;
|
||||||
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
|
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
|
||||||
int formatShortEdgePx = isVerticalVideo ? format.width : format.height;
|
int formatShortEdgePx = isVerticalVideo ? format.width : format.height;
|
||||||
@ -1255,12 +1250,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
return alignedSize;
|
return alignedSize;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Conservatively assume the codec requires 16px width and height alignment.
|
try {
|
||||||
longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16;
|
// Conservatively assume the codec requires 16px width and height alignment.
|
||||||
shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16;
|
longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16;
|
||||||
if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) {
|
shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16;
|
||||||
return new Point(isVerticalVideo ? shortEdgePx : longEdgePx,
|
if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) {
|
||||||
isVerticalVideo ? longEdgePx : shortEdgePx);
|
return new Point(
|
||||||
|
isVerticalVideo ? shortEdgePx : longEdgePx,
|
||||||
|
isVerticalVideo ? longEdgePx : shortEdgePx);
|
||||||
|
}
|
||||||
|
} catch (DecoderQueryException e) {
|
||||||
|
// We tried our best. Give up!
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ STYLE
|
|||||||
::cue(#id2) {
|
::cue(#id2) {
|
||||||
color: peachpuff;
|
color: peachpuff;
|
||||||
}
|
}
|
||||||
|
|
||||||
STYLE
|
|
||||||
::cue(v[voice="LaGord"]) { background-color: lime }
|
::cue(v[voice="LaGord"]) { background-color: lime }
|
||||||
|
|
||||||
STYLE
|
STYLE
|
||||||
|
@ -514,7 +514,7 @@ public final class TtmlDecoderTest {
|
|||||||
assertThat(cue.position).isEqualTo(24f / 100f);
|
assertThat(cue.position).isEqualTo(24f / 100f);
|
||||||
assertThat(cue.line).isEqualTo(28f / 100f);
|
assertThat(cue.line).isEqualTo(28f / 100f);
|
||||||
assertThat(cue.size).isEqualTo(51f / 100f);
|
assertThat(cue.size).isEqualTo(51f / 100f);
|
||||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
|
||||||
|
|
||||||
cues = subtitle.getCues(4000000);
|
cues = subtitle.getCues(4000000);
|
||||||
assertThat(cues).hasSize(1);
|
assertThat(cues).hasSize(1);
|
||||||
@ -524,7 +524,7 @@ public final class TtmlDecoderTest {
|
|||||||
assertThat(cue.position).isEqualTo(21f / 100f);
|
assertThat(cue.position).isEqualTo(21f / 100f);
|
||||||
assertThat(cue.line).isEqualTo(35f / 100f);
|
assertThat(cue.line).isEqualTo(35f / 100f);
|
||||||
assertThat(cue.size).isEqualTo(57f / 100f);
|
assertThat(cue.size).isEqualTo(57f / 100f);
|
||||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
assertThat(cue.bitmapHeight).isEqualTo(6f / 100f);
|
||||||
|
|
||||||
cues = subtitle.getCues(7500000);
|
cues = subtitle.getCues(7500000);
|
||||||
assertThat(cues).hasSize(1);
|
assertThat(cues).hasSize(1);
|
||||||
@ -534,7 +534,7 @@ public final class TtmlDecoderTest {
|
|||||||
assertThat(cue.position).isEqualTo(24f / 100f);
|
assertThat(cue.position).isEqualTo(24f / 100f);
|
||||||
assertThat(cue.line).isEqualTo(28f / 100f);
|
assertThat(cue.line).isEqualTo(28f / 100f);
|
||||||
assertThat(cue.size).isEqualTo(51f / 100f);
|
assertThat(cue.size).isEqualTo(51f / 100f);
|
||||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -549,7 +549,7 @@ public final class TtmlDecoderTest {
|
|||||||
assertThat(cue.position).isEqualTo(307f / 1280f);
|
assertThat(cue.position).isEqualTo(307f / 1280f);
|
||||||
assertThat(cue.line).isEqualTo(562f / 720f);
|
assertThat(cue.line).isEqualTo(562f / 720f);
|
||||||
assertThat(cue.size).isEqualTo(653f / 1280f);
|
assertThat(cue.size).isEqualTo(653f / 1280f);
|
||||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
assertThat(cue.bitmapHeight).isEqualTo(86f / 720f);
|
||||||
|
|
||||||
cues = subtitle.getCues(4000000);
|
cues = subtitle.getCues(4000000);
|
||||||
assertThat(cues).hasSize(1);
|
assertThat(cues).hasSize(1);
|
||||||
@ -559,7 +559,7 @@ public final class TtmlDecoderTest {
|
|||||||
assertThat(cue.position).isEqualTo(269f / 1280f);
|
assertThat(cue.position).isEqualTo(269f / 1280f);
|
||||||
assertThat(cue.line).isEqualTo(612f / 720f);
|
assertThat(cue.line).isEqualTo(612f / 720f);
|
||||||
assertThat(cue.size).isEqualTo(730f / 1280f);
|
assertThat(cue.size).isEqualTo(730f / 1280f);
|
||||||
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
|
assertThat(cue.bitmapHeight).isEqualTo(43f / 720f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.List;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -87,21 +88,32 @@ public final class CssParserTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseMethodSimpleInput() {
|
public void testParseMethodSimpleInput() {
|
||||||
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
|
|
||||||
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
||||||
|
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
|
||||||
expectedStyle.setFontColor(0xFF000000);
|
expectedStyle.setFontColor(0xFF000000);
|
||||||
expectedStyle.setBackgroundColor(0xFFFFEFD5);
|
expectedStyle.setBackgroundColor(0xFFFFEFD5);
|
||||||
assertParserProduces(expectedStyle, styleBlock1);
|
assertParserProduces(styleBlock1, expectedStyle);
|
||||||
|
|
||||||
String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
|
String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
|
||||||
expectedStyle = new WebvttCssStyle();
|
expectedStyle = new WebvttCssStyle();
|
||||||
expectedStyle.setFontColor(0xFF000000);
|
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 = new WebvttCssStyle();
|
||||||
expectedStyle.setBackgroundColor(0xFF00FFFE);
|
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
|
@Test
|
||||||
@ -116,7 +128,7 @@ public final class CssParserTest {
|
|||||||
expectedStyle.setFontFamily("courier");
|
expectedStyle.setFontFamily("courier");
|
||||||
expectedStyle.setBold(true);
|
expectedStyle.setBold(true);
|
||||||
|
|
||||||
assertParserProduces(expectedStyle, styleBlock);
|
assertParserProduces(styleBlock, expectedStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -128,7 +140,7 @@ public final class CssParserTest {
|
|||||||
expectedStyle.setBackgroundColor(0x190A0B0C);
|
expectedStyle.setBackgroundColor(0x190A0B0C);
|
||||||
expectedStyle.setFontColor(0xFF010101);
|
expectedStyle.setFontColor(0xFF010101);
|
||||||
|
|
||||||
assertParserProduces(expectedStyle, styleBlock);
|
assertParserProduces(styleBlock, expectedStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -203,25 +215,29 @@ public final class CssParserTest {
|
|||||||
assertThat(input.readLine()).isEqualTo(expectedLine);
|
assertThat(input.readLine()).isEqualTo(expectedLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertParserProduces(WebvttCssStyle expected,
|
private void assertParserProduces(String styleBlock, WebvttCssStyle... expectedStyles) {
|
||||||
String styleBlock){
|
|
||||||
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock));
|
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock));
|
||||||
WebvttCssStyle actualElem = parser.parseBlock(input);
|
List<WebvttCssStyle> styles = parser.parseBlock(input);
|
||||||
assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor());
|
assertThat(styles.size()).isEqualTo(expectedStyles.length);
|
||||||
if (expected.hasBackgroundColor()) {
|
for (int i = 0; i < expectedStyles.length; i++) {
|
||||||
assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor());
|
WebvttCssStyle expected = expectedStyles[i];
|
||||||
|
WebvttCssStyle actualElem = styles.get(i);
|
||||||
|
assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor());
|
||||||
|
if (expected.hasBackgroundColor()) {
|
||||||
|
assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor());
|
||||||
|
}
|
||||||
|
assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor());
|
||||||
|
if (expected.hasFontColor()) {
|
||||||
|
assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor());
|
||||||
|
}
|
||||||
|
assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily());
|
||||||
|
assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize());
|
||||||
|
assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit());
|
||||||
|
assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle());
|
||||||
|
assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough());
|
||||||
|
assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline());
|
||||||
|
assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign());
|
||||||
}
|
}
|
||||||
assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor());
|
|
||||||
if (expected.hasFontColor()) {
|
|
||||||
assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor());
|
|
||||||
}
|
|
||||||
assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily());
|
|
||||||
assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize());
|
|
||||||
assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit());
|
|
||||||
assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle());
|
|
||||||
assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough());
|
|
||||||
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);
|
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
|
* Tests that track selector will select audio track with language that match preferred language
|
||||||
* given by {@link Parameters}.
|
* given by {@link Parameters}.
|
||||||
@ -893,7 +963,6 @@ public final class DefaultTrackSelectorTest {
|
|||||||
Format forcedDefault =
|
Format forcedDefault =
|
||||||
buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT);
|
buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT);
|
||||||
Format defaultOnly = buildTextFormat("defaultOnly", "eng", 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");
|
Format noFlag = buildTextFormat("noFlag", "eng");
|
||||||
|
|
||||||
RendererCapabilities[] textRendererCapabilities =
|
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.DownloaderConstructorHelper;
|
||||||
import com.google.android.exoplayer2.offline.DownloaderFactory;
|
import com.google.android.exoplayer2.offline.DownloaderFactory;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
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.FakeDataSet;
|
||||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||||
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
|
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 dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
|
||||||
dashDownloader.download(progressListener);
|
dashDownloader.download(progressListener);
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -127,7 +128,7 @@ public class DashDownloaderTest {
|
|||||||
|
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
|
||||||
dashDownloader.download(progressListener);
|
dashDownloader.download(progressListener);
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -146,7 +147,7 @@ public class DashDownloaderTest {
|
|||||||
DashDownloader dashDownloader =
|
DashDownloader dashDownloader =
|
||||||
getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
|
getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
|
||||||
dashDownloader.download(progressListener);
|
dashDownloader.download(progressListener);
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -167,7 +168,7 @@ public class DashDownloaderTest {
|
|||||||
|
|
||||||
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
|
||||||
dashDownloader.download(progressListener);
|
dashDownloader.download(progressListener);
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -256,7 +257,7 @@ public class DashDownloaderTest {
|
|||||||
// Expected.
|
// Expected.
|
||||||
}
|
}
|
||||||
dashDownloader.download(progressListener);
|
dashDownloader.download(progressListener);
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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.DownloaderConstructorHelper;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.scheduler.Requirements;
|
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;
|
||||||
import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;
|
import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;
|
||||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||||
@ -154,7 +155,7 @@ public class DownloadManagerDashTest {
|
|||||||
public void testHandleDownloadRequest() throws Throwable {
|
public void testHandleDownloadRequest() throws Throwable {
|
||||||
handleDownloadRequest(fakeStreamKey1, fakeStreamKey2);
|
handleDownloadRequest(fakeStreamKey1, fakeStreamKey2);
|
||||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -162,7 +163,7 @@ public class DownloadManagerDashTest {
|
|||||||
handleDownloadRequest(fakeStreamKey1);
|
handleDownloadRequest(fakeStreamKey1);
|
||||||
handleDownloadRequest(fakeStreamKey2);
|
handleDownloadRequest(fakeStreamKey2);
|
||||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -176,7 +177,7 @@ public class DownloadManagerDashTest {
|
|||||||
handleDownloadRequest(fakeStreamKey1);
|
handleDownloadRequest(fakeStreamKey1);
|
||||||
|
|
||||||
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||||
assertCachedData(cache, fakeDataSet);
|
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -322,6 +322,7 @@ import java.util.Map;
|
|||||||
if (enabledTrackGroupCount == 0) {
|
if (enabledTrackGroupCount == 0) {
|
||||||
chunkSource.reset();
|
chunkSource.reset();
|
||||||
downstreamTrackFormat = null;
|
downstreamTrackFormat = null;
|
||||||
|
pendingResetUpstreamFormats = true;
|
||||||
mediaChunks.clear();
|
mediaChunks.clear();
|
||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
if (sampleQueuesBuilt) {
|
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.DownloaderFactory;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
|
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.FakeDataSet;
|
||||||
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
|
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
|
||||||
import com.google.android.exoplayer2.upstream.DummyDataSource;
|
import com.google.android.exoplayer2.upstream.DummyDataSource;
|
||||||
@ -129,12 +130,13 @@ public class HlsDownloaderTest {
|
|||||||
|
|
||||||
assertCachedData(
|
assertCachedData(
|
||||||
cache,
|
cache,
|
||||||
fakeDataSet,
|
new RequestSet(fakeDataSet)
|
||||||
MASTER_PLAYLIST_URI,
|
.subset(
|
||||||
MEDIA_PLAYLIST_1_URI,
|
MASTER_PLAYLIST_URI,
|
||||||
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
|
MEDIA_PLAYLIST_1_URI,
|
||||||
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
|
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
|
||||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
|
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
|
||||||
|
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -186,11 +188,12 @@ public class HlsDownloaderTest {
|
|||||||
|
|
||||||
assertCachedData(
|
assertCachedData(
|
||||||
cache,
|
cache,
|
||||||
fakeDataSet,
|
new RequestSet(fakeDataSet)
|
||||||
MEDIA_PLAYLIST_1_URI,
|
.subset(
|
||||||
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
|
MEDIA_PLAYLIST_1_URI,
|
||||||
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
|
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
|
||||||
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
|
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
|
||||||
|
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -220,11 +220,26 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
private @Nullable long[] adGroupTimesMs;
|
private @Nullable long[] adGroupTimesMs;
|
||||||
private @Nullable boolean[] playedAdGroups;
|
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.
|
// Suppress warnings due to usage of View methods in the constructor.
|
||||||
@SuppressWarnings("nullness:method.invocation.invalid")
|
@SuppressWarnings("nullness:method.invocation.invalid")
|
||||||
public DefaultTimeBar(Context context, AttributeSet attrs) {
|
public DefaultTimeBar(
|
||||||
super(context, attrs);
|
Context context,
|
||||||
|
@Nullable AttributeSet attrs,
|
||||||
|
int defStyleAttr,
|
||||||
|
@Nullable AttributeSet timebarAttrs) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
seekBounds = new Rect();
|
seekBounds = new Rect();
|
||||||
progressBar = new Rect();
|
progressBar = new Rect();
|
||||||
bufferedBar = 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 defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP);
|
||||||
int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP);
|
int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP);
|
||||||
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
|
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
|
||||||
if (attrs != null) {
|
if (timebarAttrs != null) {
|
||||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0,
|
TypedArray a =
|
||||||
0);
|
context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0);
|
||||||
try {
|
try {
|
||||||
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
|
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
|
||||||
if (scrubberDrawable != null) {
|
if (scrubberDrawable != null) {
|
||||||
|
@ -28,6 +28,7 @@ import android.view.KeyEvent;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@ -97,6 +98,9 @@ import java.util.Locale;
|
|||||||
* <li>Corresponding method: None
|
* <li>Corresponding method: None
|
||||||
* <li>Default: {@code R.layout.exo_player_control_view}
|
* <li>Default: {@code R.layout.exo_player_control_view}
|
||||||
* </ul>
|
* </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>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <h3>Overriding the layout file</h3>
|
* <h3>Overriding the layout file</h3>
|
||||||
@ -154,7 +158,15 @@ import java.util.Locale;
|
|||||||
* <ul>
|
* <ul>
|
||||||
* <li>Type: {@link TextView}
|
* <li>Type: {@link TextView}
|
||||||
* </ul>
|
* </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.
|
* <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>
|
* <ul>
|
||||||
* <li>Type: {@link TimeBar}
|
* <li>Type: {@link TimeBar}
|
||||||
* </ul>
|
* </ul>
|
||||||
@ -188,6 +200,18 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
void onVisibilityChange(int visibility);
|
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. */
|
/** The default fast forward increment, in milliseconds. */
|
||||||
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
|
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
|
||||||
/** The default rewind increment, in milliseconds. */
|
/** The default rewind increment, in milliseconds. */
|
||||||
@ -235,7 +259,8 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
|
|
||||||
@Nullable private Player player;
|
@Nullable private Player player;
|
||||||
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
|
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
|
||||||
private VisibilityListener visibilityListener;
|
@Nullable private VisibilityListener visibilityListener;
|
||||||
|
@Nullable private ProgressUpdateListener progressUpdateListener;
|
||||||
@Nullable private PlaybackPreparer playbackPreparer;
|
@Nullable private PlaybackPreparer playbackPreparer;
|
||||||
|
|
||||||
private boolean isAttachedToWindow;
|
private boolean isAttachedToWindow;
|
||||||
@ -317,9 +342,27 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
LayoutInflater.from(context).inflate(controllerLayoutId, this);
|
LayoutInflater.from(context).inflate(controllerLayoutId, this);
|
||||||
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
|
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);
|
durationView = findViewById(R.id.exo_duration);
|
||||||
positionView = findViewById(R.id.exo_position);
|
positionView = findViewById(R.id.exo_position);
|
||||||
timeBar = findViewById(R.id.exo_progress);
|
|
||||||
if (timeBar != null) {
|
if (timeBar != null) {
|
||||||
timeBar.addListener(componentListener);
|
timeBar.addListener(componentListener);
|
||||||
}
|
}
|
||||||
@ -454,6 +497,15 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
this.visibilityListener = listener;
|
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}.
|
* Sets the {@link PlaybackPreparer}.
|
||||||
*
|
*
|
||||||
@ -855,6 +907,9 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
timeBar.setPosition(position);
|
timeBar.setPosition(position);
|
||||||
timeBar.setBufferedPosition(bufferedPosition);
|
timeBar.setBufferedPosition(bufferedPosition);
|
||||||
}
|
}
|
||||||
|
if (progressUpdateListener != null) {
|
||||||
|
progressUpdateListener.onProgressUpdate(position, bufferedPosition);
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel any pending updates and schedule a new one if necessary.
|
// Cancel any pending updates and schedule a new one if necessary.
|
||||||
removeCallbacks(updateProgressAction);
|
removeCallbacks(updateProgressAction);
|
||||||
|
@ -966,7 +966,8 @@ public class PlayerNotificationManager {
|
|||||||
@Nullable NotificationCompat.Builder builder,
|
@Nullable NotificationCompat.Builder builder,
|
||||||
boolean ongoing,
|
boolean ongoing,
|
||||||
@Nullable Bitmap largeIcon) {
|
@Nullable Bitmap largeIcon) {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE
|
||||||
|
&& (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) {
|
||||||
builderActions = null;
|
builderActions = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@ import android.util.AttributeSet;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
import android.view.TextureView;
|
import android.view.TextureView;
|
||||||
import android.view.View;
|
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.PlaybackPreparer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
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.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
@ -165,9 +163,10 @@ import java.util.List;
|
|||||||
* <li>Corresponding method: None
|
* <li>Corresponding method: None
|
||||||
* <li>Default: {@code R.layout.exo_player_control_view}
|
* <li>Default: {@code R.layout.exo_player_control_view}
|
||||||
* </ul>
|
* </ul>
|
||||||
* <li>All attributes that can be set on a {@link PlayerControlView} can also be set on a
|
* <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can
|
||||||
* PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the
|
* also be set on a PlayerView, and will be propagated to the inflated {@link
|
||||||
* layout is overridden to specify a custom {@code exo_controller} (see below).
|
* PlayerControlView} unless the layout is overridden to specify a custom {@code
|
||||||
|
* exo_controller} (see below).
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <h3>Overriding the layout file</h3>
|
* <h3>Overriding the layout file</h3>
|
||||||
@ -217,9 +216,10 @@ import java.util.List;
|
|||||||
* <li>Type: {@link View}
|
* <li>Type: {@link View}
|
||||||
* </ul>
|
* </ul>
|
||||||
* <li><b>{@code exo_controller}</b> - An already inflated {@link PlayerControlView}. Allows use
|
* <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
|
* of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link
|
||||||
* rewind_increment} will not be automatically propagated through to this instance. If a view
|
* DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated
|
||||||
* exists with this id, any {@code exo_controller_placeholder} view will be ignored.
|
* through to this instance. If a view exists with this id, any {@code
|
||||||
|
* exo_controller_placeholder} view will be ignored.
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Type: {@link PlayerControlView}
|
* <li>Type: {@link PlayerControlView}
|
||||||
* </ul>
|
* </ul>
|
||||||
@ -303,6 +303,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
private boolean controllerHideDuringAds;
|
private boolean controllerHideDuringAds;
|
||||||
private boolean controllerHideOnTouch;
|
private boolean controllerHideOnTouch;
|
||||||
private int textureViewRotation;
|
private int textureViewRotation;
|
||||||
|
private boolean isTouching;
|
||||||
|
|
||||||
public PlayerView(Context context) {
|
public PlayerView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@ -405,7 +406,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
break;
|
break;
|
||||||
case SURFACE_TYPE_MONO360_VIEW:
|
case SURFACE_TYPE_MONO360_VIEW:
|
||||||
SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context);
|
SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context);
|
||||||
sphericalSurfaceView.setSurfaceListener(componentListener);
|
|
||||||
sphericalSurfaceView.setSingleTapListener(componentListener);
|
sphericalSurfaceView.setSingleTapListener(componentListener);
|
||||||
surfaceView = sphericalSurfaceView;
|
surfaceView = sphericalSurfaceView;
|
||||||
break;
|
break;
|
||||||
@ -459,8 +459,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
this.controller = customController;
|
this.controller = customController;
|
||||||
} else if (controllerPlaceholder != null) {
|
} else if (controllerPlaceholder != null) {
|
||||||
// Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are
|
// 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);
|
this.controller = new PlayerControlView(context, null, 0, attrs);
|
||||||
|
controller.setId(R.id.exo_controller);
|
||||||
controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
|
controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
|
||||||
ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
|
ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
|
||||||
int controllerIndex = parent.indexOfChild(controllerPlaceholder);
|
int controllerIndex = parent.indexOfChild(controllerPlaceholder);
|
||||||
@ -771,11 +772,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
if (player != null && player.isPlayingAd()) {
|
if (player != null && player.isPlayingAd()) {
|
||||||
return super.dispatchKeyEvent(event);
|
return super.dispatchKeyEvent(event);
|
||||||
}
|
}
|
||||||
boolean isDpadWhenControlHidden =
|
|
||||||
isDpadKey(event.getKeyCode()) && useController && !controller.isVisible();
|
boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController;
|
||||||
boolean handled =
|
boolean handled = false;
|
||||||
isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
|
if (isDpadAndUseController && !controller.isVisible()) {
|
||||||
if (handled) {
|
// 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);
|
maybeShowController(true);
|
||||||
}
|
}
|
||||||
return handled;
|
return handled;
|
||||||
@ -1039,11 +1049,21 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent ev) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
|
switch (event.getAction()) {
|
||||||
return false;
|
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
|
@Override
|
||||||
@ -1359,7 +1379,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
TextOutput,
|
TextOutput,
|
||||||
VideoListener,
|
VideoListener,
|
||||||
OnLayoutChangeListener,
|
OnLayoutChangeListener,
|
||||||
SphericalSurfaceView.SurfaceListener,
|
|
||||||
SingleTapListener {
|
SingleTapListener {
|
||||||
|
|
||||||
// TextOutput implementation
|
// TextOutput implementation
|
||||||
@ -1449,18 +1468,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
applyTextureViewRotation((TextureView) view, textureViewRotation);
|
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
|
// SingleTapListener implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -362,10 +362,16 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
int width = Math.round(parentWidth * cueSize);
|
int width = Math.round(parentWidth * cueSize);
|
||||||
int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight)
|
int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight)
|
||||||
: Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));
|
: Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));
|
||||||
int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width)
|
int x =
|
||||||
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);
|
Math.round(
|
||||||
int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height)
|
cuePositionAnchor == Cue.ANCHOR_TYPE_END
|
||||||
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY);
|
? (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);
|
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 {
|
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.
|
// Arbitrary vertical field of view.
|
||||||
private static final int FIELD_OF_VIEW_DEGREES = 90;
|
private static final int FIELD_OF_VIEW_DEGREES = 90;
|
||||||
private static final float Z_NEAR = .1f;
|
private static final float Z_NEAR = .1f;
|
||||||
@ -84,7 +70,6 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
|||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private final TouchTracker touchTracker;
|
private final TouchTracker touchTracker;
|
||||||
private final SceneRenderer scene;
|
private final SceneRenderer scene;
|
||||||
private @Nullable SurfaceListener surfaceListener;
|
|
||||||
private @Nullable SurfaceTexture surfaceTexture;
|
private @Nullable SurfaceTexture surfaceTexture;
|
||||||
private @Nullable Surface surface;
|
private @Nullable Surface surface;
|
||||||
private @Nullable Player.VideoComponent videoComponent;
|
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. */
|
/** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */
|
||||||
public void setSingleTapListener(@Nullable SingleTapListener listener) {
|
public void setSingleTapListener(@Nullable SingleTapListener listener) {
|
||||||
touchTracker.setSingleTapListener(listener);
|
touchTracker.setSingleTapListener(listener);
|
||||||
@ -196,8 +172,8 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
|||||||
mainHandler.post(
|
mainHandler.post(
|
||||||
() -> {
|
() -> {
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
if (surfaceListener != null) {
|
if (videoComponent != null) {
|
||||||
surfaceListener.surfaceChanged(null);
|
videoComponent.clearVideoSurface(surface);
|
||||||
}
|
}
|
||||||
releaseSurface(surfaceTexture, surface);
|
releaseSurface(surfaceTexture, surface);
|
||||||
surfaceTexture = null;
|
surfaceTexture = null;
|
||||||
@ -214,8 +190,8 @@ public final class SphericalSurfaceView extends GLSurfaceView {
|
|||||||
Surface oldSurface = this.surface;
|
Surface oldSurface = this.surface;
|
||||||
this.surfaceTexture = surfaceTexture;
|
this.surfaceTexture = surfaceTexture;
|
||||||
this.surface = new Surface(surfaceTexture);
|
this.surface = new Surface(surfaceTexture);
|
||||||
if (surfaceListener != null) {
|
if (videoComponent != null) {
|
||||||
surfaceListener.surfaceChanged(surface);
|
videoComponent.setVideoSurface(surface);
|
||||||
}
|
}
|
||||||
releaseSurface(oldSurfaceTexture, oldSurface);
|
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||||
});
|
});
|
||||||
|
@ -76,8 +76,7 @@
|
|||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
android:textColor="#FFBEBEBE"/>
|
android:textColor="#FFBEBEBE"/>
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
<View android:id="@id/exo_progress_placeholder"
|
||||||
android:id="@id/exo_progress"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="26dp"/>
|
android:layout_height="26dp"/>
|
||||||
|
@ -24,25 +24,43 @@
|
|||||||
<enum name="zoom" value="4"/>
|
<enum name="zoom" value="4"/>
|
||||||
</attr>
|
</attr>
|
||||||
|
|
||||||
<!-- Must be kept in sync with SimpleExoPlayerView -->
|
<!-- Must be kept in sync with PlayerView -->
|
||||||
<attr name="surface_type" format="enum">
|
<attr name="surface_type" format="enum">
|
||||||
<enum name="none" value="0"/>
|
<enum name="none" value="0"/>
|
||||||
<enum name="surface_view" value="1"/>
|
<enum name="surface_view" value="1"/>
|
||||||
<enum name="texture_view" value="2"/>
|
<enum name="texture_view" value="2"/>
|
||||||
<enum name="spherical_view" value="3"/>
|
<enum name="spherical_view" value="3"/>
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="show_timeout" format="integer"/>
|
|
||||||
<attr name="rewind_increment" format="integer"/>
|
<!-- Must be kept in sync with RepeatModeUtil -->
|
||||||
<attr name="fastforward_increment" format="integer"/>
|
|
||||||
<attr name="player_layout_id" format="reference"/>
|
|
||||||
<attr name="controller_layout_id" format="reference"/>
|
|
||||||
<attr name="repeat_toggle_modes">
|
<attr name="repeat_toggle_modes">
|
||||||
<flag name="none" value="0"/>
|
<flag name="none" value="0"/>
|
||||||
<flag name="one" value="1"/>
|
<flag name="one" value="1"/>
|
||||||
<flag name="all" value="2"/>
|
<flag name="all" value="2"/>
|
||||||
</attr>
|
</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="show_shuffle_button" format="boolean"/>
|
||||||
<attr name="time_bar_min_update_interval" format="integer"/>
|
<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">
|
<declare-styleable name="PlayerView">
|
||||||
<attr name="use_artwork" format="boolean"/>
|
<attr name="use_artwork" format="boolean"/>
|
||||||
@ -58,9 +76,11 @@
|
|||||||
<enum name="always" value="2"/>
|
<enum name="always" value="2"/>
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="keep_content_on_player_reset" format="boolean"/>
|
<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="surface_type"/>
|
||||||
<attr name="player_layout_id"/>
|
<!-- AspectRatioFrameLayout attributes -->
|
||||||
|
<attr name="resize_mode"/>
|
||||||
<!-- PlayerControlView attributes -->
|
<!-- PlayerControlView attributes -->
|
||||||
<attr name="show_timeout"/>
|
<attr name="show_timeout"/>
|
||||||
<attr name="rewind_increment"/>
|
<attr name="rewind_increment"/>
|
||||||
@ -69,6 +89,20 @@
|
|||||||
<attr name="show_shuffle_button"/>
|
<attr name="show_shuffle_button"/>
|
||||||
<attr name="time_bar_min_update_interval"/>
|
<attr name="time_bar_min_update_interval"/>
|
||||||
<attr name="controller_layout_id"/>
|
<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>
|
||||||
|
|
||||||
<declare-styleable name="AspectRatioFrameLayout">
|
<declare-styleable name="AspectRatioFrameLayout">
|
||||||
@ -83,22 +117,36 @@
|
|||||||
<attr name="show_shuffle_button"/>
|
<attr name="show_shuffle_button"/>
|
||||||
<attr name="time_bar_min_update_interval"/>
|
<attr name="time_bar_min_update_interval"/>
|
||||||
<attr name="controller_layout_id"/>
|
<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>
|
||||||
|
|
||||||
<declare-styleable name="DefaultTimeBar">
|
<declare-styleable name="DefaultTimeBar">
|
||||||
<attr name="bar_height" format="dimension"/>
|
<attr name="bar_height"/>
|
||||||
<attr name="touch_target_height" format="dimension"/>
|
<attr name="touch_target_height"/>
|
||||||
<attr name="ad_marker_width" format="dimension"/>
|
<attr name="ad_marker_width"/>
|
||||||
<attr name="scrubber_enabled_size" format="dimension"/>
|
<attr name="scrubber_enabled_size"/>
|
||||||
<attr name="scrubber_disabled_size" format="dimension"/>
|
<attr name="scrubber_disabled_size"/>
|
||||||
<attr name="scrubber_dragged_size" format="dimension"/>
|
<attr name="scrubber_dragged_size"/>
|
||||||
<attr name="scrubber_drawable" format="reference"/>
|
<attr name="scrubber_drawable"/>
|
||||||
<attr name="played_color" format="color"/>
|
<attr name="played_color"/>
|
||||||
<attr name="scrubber_color" format="color"/>
|
<attr name="scrubber_color"/>
|
||||||
<attr name="buffered_color" format="color"/>
|
<attr name="buffered_color" />
|
||||||
<attr name="unplayed_color" format="color"/>
|
<attr name="unplayed_color"/>
|
||||||
<attr name="ad_marker_color" format="color"/>
|
<attr name="ad_marker_color"/>
|
||||||
<attr name="played_ad_marker_color" format="color"/>
|
<attr name="played_ad_marker_color"/>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
<item name="exo_repeat_toggle" type="id"/>
|
<item name="exo_repeat_toggle" type="id"/>
|
||||||
<item name="exo_duration" type="id"/>
|
<item name="exo_duration" type="id"/>
|
||||||
<item name="exo_position" type="id"/>
|
<item name="exo_position" type="id"/>
|
||||||
|
<item name="exo_progress_placeholder" type="id"/>
|
||||||
<item name="exo_progress" type="id"/>
|
<item name="exo_progress" type="id"/>
|
||||||
<item name="exo_buffering" type="id"/>
|
<item name="exo_buffering" type="id"/>
|
||||||
<item name="exo_error_message" 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.drm.FrameworkMediaCrypto;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
|
||||||
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@ -55,6 +54,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
|
|||||||
MediaCodecSelector mediaCodecSelector,
|
MediaCodecSelector mediaCodecSelector,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
boolean playClearSamplesWithoutKeys,
|
boolean playClearSamplesWithoutKeys,
|
||||||
|
boolean enableDecoderFallback,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
VideoRendererEventListener eventListener,
|
VideoRendererEventListener eventListener,
|
||||||
long allowedVideoJoiningTimeMs,
|
long allowedVideoJoiningTimeMs,
|
||||||
@ -113,8 +113,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
|
|||||||
MediaCodec codec,
|
MediaCodec codec,
|
||||||
Format format,
|
Format format,
|
||||||
MediaCrypto crypto,
|
MediaCrypto crypto,
|
||||||
float operatingRate)
|
float operatingRate) {
|
||||||
throws DecoderQueryException {
|
|
||||||
// If the codec is being initialized whilst the renderer is started, default behavior is to
|
// 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
|
// 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
|
// 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) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName()));
|
setContentView(
|
||||||
|
getResources().getIdentifier("exo_testutils_host_activity", "layout", getPackageName()));
|
||||||
surfaceView = findViewById(
|
surfaceView = findViewById(
|
||||||
getResources().getIdentifier("surface_view", "id", getPackageName()));
|
getResources().getIdentifier("surface_view", "id", getPackageName()));
|
||||||
surfaceView.getHolder().addCallback(this);
|
surfaceView.getHolder().addCallback(this);
|
||||||
|
@ -33,58 +33,89 @@ import java.util.ArrayList;
|
|||||||
/** Assertion methods for {@link Cache}. */
|
/** Assertion methods for {@link Cache}. */
|
||||||
public final class CacheAsserts {
|
public final class CacheAsserts {
|
||||||
|
|
||||||
/**
|
/** Defines a set of data requests. */
|
||||||
* Asserts that the cache content is equal to the data in the {@code fakeDataSet}.
|
public static final class RequestSet {
|
||||||
*
|
|
||||||
* @throws IOException If an error occurred reading from the Cache.
|
private final FakeDataSet fakeDataSet;
|
||||||
*/
|
private DataSpec[] dataSpecs;
|
||||||
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {
|
|
||||||
ArrayList<FakeData> allData = fakeDataSet.getAllData();
|
public RequestSet(FakeDataSet fakeDataSet) {
|
||||||
Uri[] uris = new Uri[allData.size()];
|
this.fakeDataSet = fakeDataSet;
|
||||||
for (int i = 0; i < allData.size(); i++) {
|
ArrayList<FakeData> allData = fakeDataSet.getAllData();
|
||||||
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.
|
* @throws IOException If an error occurred reading from the Cache.
|
||||||
*/
|
*/
|
||||||
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings)
|
public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException {
|
||||||
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 {
|
|
||||||
int totalLength = 0;
|
int totalLength = 0;
|
||||||
for (Uri uri : uris) {
|
for (int i = 0; i < requestSet.getCount(); i++) {
|
||||||
byte[] data = fakeDataSet.getData(uri).getData();
|
byte[] data = requestSet.getData(i);
|
||||||
assertDataCached(cache, uri, data);
|
assertDataCached(cache, requestSet.getDataSpec(i), data);
|
||||||
totalLength += data.length;
|
totalLength += data.length;
|
||||||
}
|
}
|
||||||
assertThat(cache.getCacheSpace()).isEqualTo(totalLength);
|
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.
|
* @throws IOException If an error occurred reading from the Cache.
|
||||||
*/
|
*/
|
||||||
public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException {
|
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {
|
||||||
DataSpec dataSpec = new DataSpec(uri);
|
assertCachedData(cache, new RequestSet(fakeDataSet));
|
||||||
assertDataCached(cache, dataSpec, expected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,15 +126,18 @@ public final class CacheAsserts {
|
|||||||
public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected)
|
public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0);
|
DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0);
|
||||||
dataSource.open(dataSpec);
|
byte[] bytes;
|
||||||
try {
|
try {
|
||||||
byte[] bytes = TestUtil.readToEnd(dataSource);
|
dataSource.open(dataSpec);
|
||||||
assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',")
|
bytes = TestUtil.readToEnd(dataSource);
|
||||||
.that(bytes)
|
} catch (IOException e) {
|
||||||
.isEqualTo(expected);
|
throw new IOException("Opening/reading cache failed: " + dataSpec, e);
|
||||||
} finally {
|
} finally {
|
||||||
dataSource.close();
|
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