Merge pull request #5986 from google/dev-v2-r2.10.2

r2.10.2
This commit is contained in:
Oliver Woodman 2019-06-23 16:15:27 +01:00 committed by GitHub
commit f6297f4f51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1433 additions and 493 deletions

View File

@ -1,5 +1,55 @@
# Release notes #
### 2.10.2 ###
* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s
([#5779](https://github.com/google/ExoPlayer/issues/5779)).
* Add `SilenceMediaSource` that can be used to play silence of a given
duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)).
* Offline:
* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after
preparation of a `DownloadHelper` fails
([#5915](https://github.com/google/ExoPlayer/issues/5915)).
* Fix `CacheUtil.cache()` downloading too much data
([#5927](https://github.com/google/ExoPlayer/issues/5927)).
* Fix misreporting cached bytes when caching is paused
([#5573](https://github.com/google/ExoPlayer/issues/5573)).
* UI:
* Allow setting `DefaultTimeBar` attributes on `PlayerView` and
`PlayerControlView`.
* Change playback controls toggle from touch down to touch up events
([#5784](https://github.com/google/ExoPlayer/issues/5784)).
* Fix issue where playback controls were not kept visible on key presses
([#5963](https://github.com/google/ExoPlayer/issues/5963)).
* Subtitles:
* CEA-608: Handle XDS and TEXT modes
([#5807](https://github.com/google/ExoPlayer/pull/5807)).
* TTML: Fix bitmap rendering
([#5633](https://github.com/google/ExoPlayer/pull/5633)).
* IMA: Fix ad pod index offset calculation without preroll
([#5928](https://github.com/google/ExoPlayer/issues/5928)).
* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods
to indicate whether a controller sent a play or only a prepare command. This
allows to take advantage of decoder reuse with the MediaSessionConnector
([#5891](https://github.com/google/ExoPlayer/issues/5891)).
* Add `ProgressUpdateListener` to `PlayerControlView`
([#5834](https://github.com/google/ExoPlayer/issues/5834)).
* Add support for auto-detecting UDP streams in `DefaultDataSource`
([#6036](https://github.com/google/ExoPlayer/pull/6036)).
* Allow enabling decoder fallback with `DefaultRenderersFactory`
([#5942](https://github.com/google/ExoPlayer/issues/5942)).
* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission
([#6019](https://github.com/google/ExoPlayer/issues/6019)).
* Fix decoding problems when seeking back after seeking beyond a mid-roll ad
([#6009](https://github.com/google/ExoPlayer/issues/6009)).
* Fix application of `maxAudioBitrate` for adaptive audio track groups
([#6006](https://github.com/google/ExoPlayer/issues/6006)).
* Fix bug caused by parallel adaptive track selection using `Format`s without
bitrate information
([#5971](https://github.com/google/ExoPlayer/issues/5971)).
* Fix bug in `CastPlayer.getCurrentWindowIndex()`
([#5955](https://github.com/google/ExoPlayer/issues/5955)).
### 2.10.1 ###
* Offline: Add option to remove all downloads.

View File

@ -36,7 +36,7 @@ allprojects {
jcenter()
}
project.ext {
exoplayerPublishEnabled = true
exoplayerPublishEnabled = false
}
if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) {

View File

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.10.1'
releaseVersionCode = 2010001
releaseVersion = '2.10.2'
releaseVersionCode = 2010002
minSdkVersion = 16
targetSdkVersion = 28
compileSdkVersion = 28

View File

@ -66,7 +66,6 @@ import java.util.ArrayList;
private final Listener listener;
private final ConcatenatingMediaSource concatenatingMediaSource;
private boolean castMediaQueueCreationPending;
private int currentItemIndex;
private Player currentPlayer;
@ -268,9 +267,6 @@ import java.util.ArrayList;
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (currentPlayer == castPlayer && timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
}
// CastPlayer.SessionAvailabilityListener implementation.
@ -332,7 +328,6 @@ import java.util.ArrayList;
this.currentPlayer = currentPlayer;
// Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) {
exoPlayer.prepare(concatenatingMediaSource);
}
@ -352,12 +347,11 @@ import java.util.ArrayList;
*/
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
maybeSetCurrentItemAndNotify(itemIndex);
if (castMediaQueueCreationPending) {
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = buildMediaQueueItem(mediaQueue.get(i));
}
castMediaQueueCreationPending = false;
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
} else {
currentPlayer.seekTo(itemIndex, positionMs);

View File

@ -31,7 +31,7 @@ android {
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
api 'com.google.android.gms:play-services-cast-framework:16.2.0'
implementation 'androidx.annotation:annotation:1.0.2'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')

View File

@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* {@link Player} implementation that communicates with a Cast receiver app.
@ -86,8 +89,10 @@ public final class CastPlayer extends BasePlayer {
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
// Listeners.
private final CopyOnWriteArraySet<EventListener> listeners;
// Listeners and notification.
private final CopyOnWriteArrayList<ListenerHolder> listeners;
private final ArrayList<ListenerNotificationTask> notificationsBatch;
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
@ -113,7 +118,9 @@ public final class CastPlayer extends BasePlayer {
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
listeners = new CopyOnWriteArraySet<>();
listeners = new CopyOnWriteArrayList<>();
notificationsBatch = new ArrayList<>();
ongoingNotificationsTasks = new ArrayDeque<>();
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
@ -296,12 +303,17 @@ public final class CastPlayer extends BasePlayer {
@Override
public void addListener(EventListener listener) {
listeners.add(listener);
listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(EventListener listener) {
listeners.remove(listener);
for (ListenerHolder listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release();
listeners.remove(listenerHolder);
}
}
}
@Override
@ -347,14 +359,13 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
}
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
} else if (pendingSeekCount == 0) {
for (EventListener listener : listeners) {
listener.onSeekProcessed();
}
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
}
flushNotifications();
}
@Override
@ -530,30 +541,40 @@ public final class CastPlayer extends BasePlayer {
|| this.playWhenReady != playWhenReady) {
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
for (EventListener listener : listeners) {
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
}
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
for (EventListener listener : listeners) {
listener.onRepeatModeChanged(repeatMode);
}
}
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
}
}
if (updateTracksAndSelections()) {
for (EventListener listener : listeners) {
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
}
notificationsBatch.add(
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
}
maybeUpdateTimelineAndNotify();
int currentWindowIndex = C.INDEX_UNSET;
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
if (currentItem != null) {
currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
}
if (currentWindowIndex == C.INDEX_UNSET) {
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
currentWindowIndex = 0;
}
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
notificationsBatch.add(
new ListenerNotificationTask(
listener ->
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
}
if (updateTracksAndSelections()) {
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
}
flushNotifications();
}
private void maybeUpdateTimelineAndNotify() {
@ -561,9 +582,10 @@ public final class CastPlayer extends BasePlayer {
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
for (EventListener listener : listeners) {
listener.onTimelineChanged(currentTimeline, null, reason);
}
notificationsBatch.add(
new ListenerNotificationTask(
listener ->
listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason)));
}
}
@ -701,16 +723,6 @@ public final class CastPlayer extends BasePlayer {
}
}
/**
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
* there is no media session, returns 0.
*/
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
Integer currentItemId = mediaStatus != null
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
return currentItemId != null ? currentItemId : 0;
}
private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) {
@ -826,7 +838,23 @@ public final class CastPlayer extends BasePlayer {
}
// Result callbacks hooks.
// Internal methods.
private void flushNotifications() {
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
ongoingNotificationsTasks.addAll(notificationsBatch);
notificationsBatch.clear();
if (recursiveNotification) {
// This will be handled once the current notification task is finished.
return;
}
while (!ongoingNotificationsTasks.isEmpty()) {
ongoingNotificationsTasks.peekFirst().execute();
ongoingNotificationsTasks.removeFirst();
}
}
// Internal classes.
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
@ -840,11 +868,27 @@ public final class CastPlayer extends BasePlayer {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
for (EventListener listener : listeners) {
listener.onSeekProcessed();
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
flushNotifications();
}
}
}
private final class ListenerNotificationTask {
private final Iterator<ListenerHolder> listenersSnapshot;
private final ListenerInvocation listenerInvocation;
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
this.listenersSnapshot = listeners.iterator();
this.listenerInvocation = listenerInvocation;
}
public void execute() {
while (listenersSnapshot.hasNext()) {
listenersSnapshot.next().invoke(listenerInvocation);
}
}
}
}

View File

@ -34,6 +34,7 @@ android {
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
}

View File

@ -1054,13 +1054,8 @@ public final class ImaAdsLoader
long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
if (adGroupIndexForPosition == 0) {
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There is no preroll and midroll pod indices start at 1.
podIndexOffset = -1;
} else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position.
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
// Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
@ -1070,9 +1065,18 @@ public final class ImaAdsLoader
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
}
// We're removing one or more ads, which means that the earliest ad (if any) will be a
// midroll/postroll. Midroll pod indices start at 1.
// IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
// Store an index offset as we want to index all ads (including skipped ones) from 0.
if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
// We are playing a preroll.
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There's no ad to play which means there's no preroll.
podIndexOffset = -1;
} else {
// We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1;
}

View File

@ -172,7 +172,7 @@ public final class MediaSessionConnector {
ResultReceiver cb);
}
/** Interface to which playback preparation actions are delegated. */
/** Interface to which playback preparation and play actions are delegated. */
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS =
@ -197,14 +197,36 @@ public final class MediaSessionConnector {
* @return The bitmask of the supported media actions.
*/
long getSupportedPrepareActions();
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
void onPrepare();
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
void onPrepareFromMediaId(String mediaId, Bundle extras);
/** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
void onPrepareFromSearch(String query, Bundle extras);
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
void onPrepareFromUri(Uri uri, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepare()}.
*
* @param playWhenReady Whether playback should be started after preparation.
*/
void onPrepare(boolean playWhenReady);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
*
* @param mediaId The media id of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
*
* @param query The search query.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
*
* @param uri The {@link Uri} of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
}
/**
@ -834,13 +856,6 @@ public final class MediaSessionConnector {
return player != null && mediaButtonEventHandler != null;
}
private void stopPlayerForPrepare(boolean playWhenReady) {
if (player != null) {
player.stop();
player.setPlayWhenReady(playWhenReady);
}
}
private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs);
@ -1047,14 +1062,14 @@ public final class MediaSessionConnector {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.onPrepare();
playbackPreparer.onPrepare(/* playWhenReady= */ true);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
}
}
}
@Override
public void onPause() {
@ -1182,56 +1197,49 @@ public final class MediaSessionConnector {
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepare();
playbackPreparer.onPrepare(/* playWhenReady= */ false);
}
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromSearch(query, extras);
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromUri(uri, extras);
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromSearch(query, extras);
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromUri(uri, extras);
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
}
}

View File

@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
The build configuration has been tested with Android NDK r19c.
```
NDK_PATH="<path to Android NDK>"
@ -54,7 +55,7 @@ git checkout tags/v1.8.0 -b v1.8.0
```
cd ${VP9_EXT_PATH}/jni && \
./generate_libvpx_android_configs.sh "${NDK_PATH}"
./generate_libvpx_android_configs.sh
```
* Build the JNI native libraries from the command line:
@ -66,7 +67,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#3520]: https://github.com/google/ExoPlayer/issues/3520
## Notes ##

View File

@ -60,8 +60,8 @@ public final class VpxOutputBuffer extends OutputBuffer {
* Initializes the buffer.
*
* @param timeUs The presentation timestamp for the buffer, in microseconds.
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
* VpxDecoder#OUTPUT_MODE_YUV}.
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link
* VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}.
*/
public void init(long timeUs, int mode) {
this.timeUs = timeUs;
@ -110,6 +110,15 @@ public final class VpxOutputBuffer extends OutputBuffer {
return true;
}
/**
* Configures the buffer for the given frame dimensions when passing actual frame data via {@link
* #decoderPrivate}. Called via JNI after decoding completes.
*/
public void initForPrivateFrame(int width, int height) {
this.width = width;
this.height = height;
}
private void initData(int size) {
if (data == null || data.capacity() < size) {
data = ByteBuffer.allocateDirect(size);

View File

@ -15,6 +15,6 @@
#
APP_OPTIM := release
APP_STL := gnustl_static
APP_STL := c++_static
APP_CPPFLAGS := -frtti
APP_PLATFORM := android-9
APP_PLATFORM := android-16

View File

@ -20,46 +20,33 @@
set -e
if [ $# -ne 1 ]; then
echo "Usage: ${0} <path_to_android_ndk>"
if [ $# -ne 0 ]; then
echo "Usage: ${0}"
exit
fi
ndk="${1}"
shift 1
# configuration parameters common to all architectures
common_params="--disable-examples --disable-docs --enable-realtime-only"
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
common_params+=" --enable-external-build"
# configuration parameters for various architectures
arch[0]="armeabi-v7a"
config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon"
config[0]+=" --enable-neon-asm"
config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm"
arch[1]="armeabi"
config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
config[1]+=" --disable-neon-asm"
arch[1]="x86"
config[1]="--force-target=x86-android-gcc --disable-sse2"
config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
config[1]+=" --disable-avx2 --enable-pic"
arch[2]="mips"
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
arch[2]="arm64-v8a"
config[2]="--force-target=armv8-android-gcc --enable-neon"
arch[3]="x86"
config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2"
arch[3]="x86_64"
config[3]="--force-target=x86_64-android-gcc --disable-sse2"
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
config[3]+=" --disable-avx2 --enable-pic"
arch[4]="arm64-v8a"
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
arch[5]="x86_64"
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
arch[6]="mips64"
config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk"
config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
limit=$((${#arch[@]} - 1))
@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do
# configure and make
echo "build_android_configs: "
echo "configure ${config[${i}]} ${common_params}"
../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
-isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
-isystem $ndk/sysroot/usr/include \
"
../../libvpx/configure ${config[${i}]} ${common_params}
rm -f libvpx_srcs.txt
for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid

View File

@ -60,6 +60,7 @@
// JNI references for VpxOutputBuffer class.
static jmethodID initForYuvFrame;
static jmethodID initForPrivateFrame;
static jfieldID dataField;
static jfieldID outputModeField;
static jfieldID decoderPrivateField;
@ -481,6 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)Z");
initForPrivateFrame =
env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
@ -602,6 +605,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
jfb->d_w = img->d_w;
jfb->d_h = img->d_h;
env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h);
if (env->ExceptionCheck()) {
return -1;
}
env->SetIntField(jOutputBuffer, decoderPrivateField,
id + kDecoderPrivateBase);
}

View File

@ -146,8 +146,8 @@ public final class C {
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
* #ENCODING_E_AC3}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or
* {@link #ENCODING_DOLBY_TRUEHD}.
* #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@ -163,6 +163,7 @@ public final class C {
ENCODING_PCM_A_LAW,
ENCODING_AC3,
ENCODING_E_AC3,
ENCODING_E_AC3_JOC,
ENCODING_AC4,
ENCODING_DTS,
ENCODING_DTS_HD,
@ -210,6 +211,8 @@ public final class C {
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/** @see AudioFormat#ENCODING_E_AC3_JOC */
public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
/** @see AudioFormat#ENCODING_AC4 */
public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
/** @see AudioFormat#ENCODING_DTS */

View File

@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@ -90,6 +91,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
@ExtensionRendererMode private int extensionRendererMode;
private long allowedVideoJoiningTimeMs;
private boolean playClearSamplesWithoutKeys;
private boolean enableDecoderFallback;
private MediaCodecSelector mediaCodecSelector;
/** @param context A {@link Context}. */
@ -202,6 +204,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
return this;
}
/**
* Sets whether to enable fallback to lower-priority decoders if decoder initialization fails.
* This may result in using a decoder that is less efficient or slower than the primary decoder.
*
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
* initialization fails.
* @return This factory, for convenience.
*/
public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) {
this.enableDecoderFallback = enableDecoderFallback;
return this;
}
/**
* Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
*
@ -248,6 +263,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
enableDecoderFallback,
eventHandler,
videoRendererEventListener,
allowedVideoJoiningTimeMs,
@ -258,6 +274,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
enableDecoderFallback,
buildAudioProcessors(),
eventHandler,
audioRendererEventListener,
@ -282,6 +299,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
* initialization fails. This may result in using a decoder that is slower/less efficient than
* the primary decoder.
* @param eventHandler A handler associated with the main thread's looper.
* @param eventListener An event listener.
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
@ -294,6 +314,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean enableDecoderFallback,
Handler eventHandler,
VideoRendererEventListener eventListener,
long allowedVideoJoiningTimeMs,
@ -305,6 +326,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
allowedVideoJoiningTimeMs,
drmSessionManager,
playClearSamplesWithoutKeys,
enableDecoderFallback,
eventHandler,
eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
@ -356,6 +378,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
* initialization fails. This may result in using a decoder that is slower/less efficient than
* the primary decoder.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
* before output. May be empty.
* @param eventHandler A handler to use when invoking event listeners and outputs.
@ -368,6 +393,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean enableDecoderFallback,
AudioProcessor[] audioProcessors,
Handler eventHandler,
AudioRendererEventListener eventListener,
@ -378,10 +404,10 @@ public class DefaultRenderersFactory implements RenderersFactory {
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
enableDecoderFallback,
eventHandler,
eventListener,
AudioCapabilities.getCapabilities(context),
audioProcessors));
new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;

View File

@ -510,7 +510,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Override
public long getTotalBufferedDuration() {
return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs));
return C.usToMs(playbackInfo.totalBufferedDurationUs);
}
@Override

View File

@ -729,13 +729,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
newPlayingPeriodHolder = queue.advancePlayingPeriod();
}
// Disable all the renderers if the period being played is changing, or if forced.
if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) {
// Disable all renderers if the period being played is changing, if the seek results in negative
// renderer timestamps, or if forced.
if (forceDisableRenderers
|| oldPlayingPeriodHolder != newPlayingPeriodHolder
|| (newPlayingPeriodHolder != null
&& newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
for (Renderer renderer : enabledRenderers) {
disableRenderer(renderer);
}
enabledRenderers = new Renderer[0];
oldPlayingPeriodHolder = null;
if (newPlayingPeriodHolder != null) {
newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
}
}
// Update the holders.
@ -1798,9 +1805,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
return loadingPeriodHolder == null
? 0
: bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
if (loadingPeriodHolder == null) {
return 0;
}
long totalBufferedDurationUs =
bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
return Math.max(0, totalBufferedDurationUs);
}
private void updateLoadControlTrackSelection(

View File

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.10.1";
public static final String VERSION = "2.10.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.2";
/**
* The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2010001;
public static final int VERSION_INT = 2010002;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -67,8 +67,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* Creates a new holder with information required to play it as part of a timeline.
*
* @param rendererCapabilities The renderer capabilities.
* @param rendererPositionOffsetUs The time offset of the start of the media period to provide to
* renderers.
* @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
* @param trackSelector The track selector.
* @param allocator The allocator.
* @param mediaSource The media source that produced the media period.
@ -82,7 +81,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
MediaSource mediaSource,
MediaPeriodInfo info) {
this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs;
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
this.trackSelector = trackSelector;
this.mediaSource = mediaSource;
this.uid = info.id.periodUid;
@ -115,6 +114,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return rendererPositionOffsetUs;
}
/**
* Sets the renderer time of the start of the period, in microseconds.
*
* @param rendererPositionOffsetUs The new renderer position offset, in microseconds.
*/
public void setRendererOffset(long rendererPositionOffsetUs) {
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
}
/** Returns start position of period in renderer time. */
public long getStartPositionRendererTime() {
return info.startPositionUs + rendererPositionOffsetUs;

View File

@ -144,8 +144,8 @@ import com.google.android.exoplayer2.util.Assertions;
MediaPeriodInfo info) {
long rendererPositionOffsetUs =
loading == null
? info.startPositionUs
: (loading.getRendererOffset() + loading.info.durationUs);
? (info.id.isAd() ? info.contentPositionUs : 0)
: (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
MediaPeriodHolder newPeriodHolder =
new MediaPeriodHolder(
rendererCapabilities,

View File

@ -1125,6 +1125,7 @@ public final class DefaultAudioSink implements AudioSink {
case C.ENCODING_AC3:
return 640 * 1000 / 8;
case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC:
return 6144 * 1000 / 8;
case C.ENCODING_AC4:
return 2688 * 1000 / 8;
@ -1154,7 +1155,7 @@ public final class DefaultAudioSink implements AudioSink {
return DtsUtil.parseDtsAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC3) {
return Ac3Util.getAc3SyncframeAudioSampleCount();
} else if (encoding == C.ENCODING_E_AC3) {
} else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) {
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC4) {
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
@ -1177,11 +1178,10 @@ public final class DefaultAudioSink implements AudioSink {
@TargetApi(21)
private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size,
long presentationTimeUs) {
// TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed.
// if (Util.SDK_INT >= 23) {
// // The underlying platform AudioTrack writes AV sync headers directly.
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
// }
if (Util.SDK_INT >= 26) {
// The underlying platform AudioTrack writes AV sync headers directly.
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
}
if (avSyncHeader == null) {
avSyncHeader = ByteBuffer.allocate(16);
avSyncHeader.order(ByteOrder.BIG_ENDIAN);

View File

@ -245,12 +245,50 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink) {
this(
context,
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
/* enableDecoderFallback= */ false,
eventHandler,
eventListener,
audioSink);
}
/**
* @param context A context.
* @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
* initialization fails. This may result in using a decoder that is slower/less efficient than
* the primary decoder.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioSink The sink to which audio will be output.
*/
public MediaCodecAudioRenderer(
Context context,
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean enableDecoderFallback,
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink) {
super(
C.TRACK_TYPE_AUDIO,
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
/* enableDecoderFallback= */ false,
enableDecoderFallback,
/* assumedMinimumCodecOperatingRate= */ 44100);
this.context = context.getApplicationContext();
this.audioSink = audioSink;
@ -341,7 +379,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @return Whether passthrough playback is supported.
*/
protected boolean allowPassthrough(int channelCount, String mimeType) {
return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType));
return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID;
}
@Override
@ -437,11 +475,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@C.Encoding int encoding;
MediaFormat format;
if (passthroughMediaFormat != null) {
encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME));
format = passthroughMediaFormat;
encoding =
getPassthroughEncoding(
format.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
format.getString(MediaFormat.KEY_MIME));
} else {
encoding = pcmEncoding;
format = outputFormat;
encoding = pcmEncoding;
}
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
@ -463,6 +504,28 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
/**
* Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link
* C#ENCODING_INVALID} if passthrough is not possible.
*/
@C.Encoding
protected int getPassthroughEncoding(int channelCount, String mimeType) {
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {
if (audioSink.supportsOutput(channelCount, C.ENCODING_E_AC3_JOC)) {
return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC);
}
// E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back.
mimeType = MimeTypes.AUDIO_E_AC3;
}
@C.Encoding int encoding = MimeTypes.getEncoding(mimeType);
if (audioSink.supportsOutput(channelCount, encoding)) {
return encoding;
} else {
return C.ENCODING_INVALID;
}
}
/**
* Called when the audio session id becomes known. The default implementation is a no-op. One
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in

View File

@ -53,7 +53,6 @@ import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@ -454,15 +453,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
* no codec operating rate should be set.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
protected abstract void configureCodec(
MediaCodecInfo codecInfo,
MediaCodec codec,
Format format,
MediaCrypto crypto,
float codecOperatingRate)
throws DecoderQueryException;
float codecOperatingRate);
protected final void maybeInitCodec() throws ExoPlaybackException {
if (codec != null || inputFormat == null) {
@ -742,11 +739,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
try {
List<MediaCodecInfo> allAvailableCodecInfos =
getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder);
availableCodecInfos = new ArrayDeque<>();
if (enableDecoderFallback) {
availableCodecInfos = new ArrayDeque<>(allAvailableCodecInfos);
} else {
availableCodecInfos =
new ArrayDeque<>(Collections.singletonList(allAvailableCodecInfos.get(0)));
availableCodecInfos.addAll(allAvailableCodecInfos);
} else if (!allAvailableCodecInfos.isEmpty()) {
availableCodecInfos.add(allAvailableCodecInfos.get(0));
}
preferredDecoderInitializationException = null;
} catch (DecoderQueryException e) {

View File

@ -176,7 +176,7 @@ public final class MediaCodecUtil {
// E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling);
ArrayList<MediaCodecInfo> eac3DecoderInfos =
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
getDecoderInfosInternal(eac3Key, mediaCodecList, MimeTypes.AUDIO_E_AC3);
decoderInfos.addAll(eac3DecoderInfos);
}
applyWorkarounds(mimeType, decoderInfos);

View File

@ -951,6 +951,7 @@ public final class DownloadHelper {
downloadHelper.onMediaPrepared();
return true;
case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:
release();
downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj));
return true;
default:

View File

@ -27,6 +27,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.os.PowerManager;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -128,7 +129,7 @@ public final class Requirements implements Parcelable {
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo();
if (networkInfo == null
|| !networkInfo.isConnected()
|| !isInternetConnectivityValidated(connectivityManager)) {

View File

@ -28,6 +28,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
@ -126,7 +127,8 @@ public final class RequirementsWatcher {
@TargetApi(23)
private void registerNetworkCallbackV23() {
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Assertions.checkNotNull(
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
NetworkRequest request =
new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)

View File

@ -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;
}
}

View File

@ -80,6 +80,11 @@ public final class Cea608Decoder extends CeaDecoder {
* at which point the non-displayed memory becomes the displayed memory (and vice versa).
*/
private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20;
private static final byte CTRL_BACKSPACE = 0x21;
private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
/**
* Command initiating roll-up style captioning, with the maximum of 2 rows displayed
* simultaneously.
@ -95,25 +100,31 @@ public final class Cea608Decoder extends CeaDecoder {
* simultaneously.
*/
private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
/**
* Command initiating paint-on style captioning. Subsequent data should be addressed immediately
* to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.
*/
private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;
/**
* Command indicating the end of a pop-on style caption. At this point the caption loaded in
* non-displayed memory should be swapped with the one in displayed memory. If no
* {@link #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the
* receiver into pop-on style.
* TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out
* until a command is received that switches back to the CAPTION service.
*/
private static final byte CTRL_END_OF_CAPTION = 0x2F;
private static final byte CTRL_TEXT_RESTART = 0x2A;
private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B;
private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
private static final byte CTRL_BACKSPACE = 0x21;
/**
* Command indicating the end of a pop-on style caption. At this point the caption loaded in
* non-displayed memory should be swapped with the one in displayed memory. If no {@link
* #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into
* pop-on style.
*/
private static final byte CTRL_END_OF_CAPTION = 0x2F;
// Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
private static final int[] BASIC_CHARACTER_SET = new int[] {
@ -237,6 +248,11 @@ public final class Cea608Decoder extends CeaDecoder {
private byte repeatableControlCc2;
private int currentChannel;
// The incoming characters may belong to 3 different services based on the last received control
// codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning
// service bytes and drops the rest.
private boolean isInCaptionService;
public Cea608Decoder(String mimeType, int accessibilityChannel) {
ccData = new ParsableByteArray();
cueBuilders = new ArrayList<>();
@ -268,6 +284,7 @@ public final class Cea608Decoder extends CeaDecoder {
setCaptionMode(CC_MODE_UNKNOWN);
resetCueBuilders();
isInCaptionService = true;
}
@Override
@ -288,6 +305,7 @@ public final class Cea608Decoder extends CeaDecoder {
repeatableControlCc1 = 0;
repeatableControlCc2 = 0;
currentChannel = NTSC_CC_CHANNEL_1;
isInCaptionService = true;
}
@Override
@ -363,6 +381,12 @@ public final class Cea608Decoder extends CeaDecoder {
continue;
}
maybeUpdateIsInCaptionService(ccData1, ccData2);
if (!isInCaptionService) {
// Only the Captioning service is supported. Drop all other bytes.
continue;
}
// Special North American character set.
// ccData1 - 0|0|0|1|C|0|0|1
// ccData2 - 0|0|1|1|X|X|X|X
@ -629,6 +653,29 @@ public final class Cea608Decoder extends CeaDecoder {
cueBuilders.add(currentCueBuilder);
}
private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) {
if (isXdsControlCode(cc1)) {
isInCaptionService = false;
} else if (isServiceSwitchCommand(cc1)) {
switch (cc2) {
case CTRL_TEXT_RESTART:
case CTRL_RESUME_TEXT_DISPLAY:
isInCaptionService = false;
break;
case CTRL_END_OF_CAPTION:
case CTRL_RESUME_CAPTION_LOADING:
case CTRL_RESUME_DIRECT_CAPTIONING:
case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
isInCaptionService = true;
break;
default:
// No update.
}
}
}
private static char getChar(byte ccData) {
int index = (ccData & 0x7F) - 0x20;
return (char) BASIC_CHARACTER_SET[index];
@ -683,6 +730,15 @@ public final class Cea608Decoder extends CeaDecoder {
return (cc1 & 0xF0) == 0x10;
}
private static boolean isXdsControlCode(byte cc1) {
return 0x01 <= cc1 && cc1 <= 0x0F;
}
private static boolean isServiceSwitchCommand(byte cc1) {
// cc1 - 0|0|0|1|C|1|0|0
return (cc1 & 0xF7) == 0x14;
}
private static class CueBuilder {
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608

View File

@ -429,6 +429,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
/* lineType= */ Cue.LINE_TYPE_FRACTION,
lineAnchor,
width,
height,
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
/* textSize= */ regionTextHeight);
}

View File

@ -231,11 +231,11 @@ import java.util.TreeSet;
new Cue(
bitmap,
region.position,
Cue.ANCHOR_TYPE_MIDDLE,
Cue.ANCHOR_TYPE_START,
region.line,
region.lineAnchor,
region.width,
/* height= */ Cue.DIMEN_UNSET));
region.height));
}
// Create text based cues.

View File

@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue;
public final @Cue.LineType int lineType;
public final @Cue.AnchorType int lineAnchor;
public final float width;
public final float height;
public final @Cue.TextSizeType int textSizeType;
public final float textSize;
@ -39,6 +40,7 @@ import com.google.android.exoplayer2.text.Cue;
/* lineType= */ Cue.TYPE_UNSET,
/* lineAnchor= */ Cue.TYPE_UNSET,
/* width= */ Cue.DIMEN_UNSET,
/* height= */ Cue.DIMEN_UNSET,
/* textSizeType= */ Cue.TYPE_UNSET,
/* textSize= */ Cue.DIMEN_UNSET);
}
@ -50,6 +52,7 @@ import com.google.android.exoplayer2.text.Cue;
@Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor,
float width,
float height,
int textSizeType,
float textSize) {
this.id = id;
@ -58,6 +61,7 @@ import com.google.android.exoplayer2.text.Cue;
this.lineType = lineType;
this.lineAnchor = lineAnchor;
this.width = width;
this.height = height;
this.textSizeType = textSizeType;
this.textSize = textSize;
}

View File

@ -20,6 +20,8 @@ import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -35,8 +37,8 @@ import java.util.regex.Pattern;
private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
private static final String VALUE_BOLD = "bold";
private static final String VALUE_UNDERLINE = "underline";
private static final String BLOCK_START = "{";
private static final String BLOCK_END = "}";
private static final String RULE_START = "{";
private static final String RULE_END = "}";
private static final String PROPERTY_FONT_STYLE = "font-style";
private static final String VALUE_ITALIC = "italic";
@ -52,22 +54,27 @@ import java.util.regex.Pattern;
}
/**
* Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the
* contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or
* {@code null} otherwise.
* Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents
* of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If
* parsing fails, it returns a list including only the styles which have been successfully parsed
* up to the style rule which was malformed.
*
* @param input The input from which the style block should be read.
* @return A {@link WebvttCssStyle} that represents the parsed block.
* @return A list of {@link WebvttCssStyle}s that represents the parsed block, or a list
* containing the styles up to the parsing failure.
*/
public WebvttCssStyle parseBlock(ParsableByteArray input) {
public List<WebvttCssStyle> parseBlock(ParsableByteArray input) {
stringBuilder.setLength(0);
int initialInputPosition = input.getPosition();
skipStyleBlock(input);
styleInput.reset(input.data, input.getPosition());
styleInput.setPosition(initialInputPosition);
String selector = parseSelector(styleInput, stringBuilder);
if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
return null;
List<WebvttCssStyle> styles = new ArrayList<>();
String selector;
while ((selector = parseSelector(styleInput, stringBuilder)) != null) {
if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) {
return styles;
}
WebvttCssStyle style = new WebvttCssStyle();
applySelectorToStyle(style, selector);
@ -76,13 +83,18 @@ import java.util.regex.Pattern;
while (!blockEndFound) {
int position = styleInput.getPosition();
token = parseNextToken(styleInput, stringBuilder);
blockEndFound = token == null || BLOCK_END.equals(token);
blockEndFound = token == null || RULE_END.equals(token);
if (!blockEndFound) {
styleInput.setPosition(position);
parseStyleDeclaration(styleInput, style, stringBuilder);
}
}
return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
// Check that the style rule ended correctly.
if (RULE_END.equals(token)) {
styles.add(style);
}
}
return styles;
}
/**
@ -107,7 +119,7 @@ import java.util.regex.Pattern;
if (token == null) {
return null;
}
if (BLOCK_START.equals(token)) {
if (RULE_START.equals(token)) {
input.setPosition(position);
return "";
}
@ -156,7 +168,7 @@ import java.util.regex.Pattern;
String token = parseNextToken(input, stringBuilder);
if (";".equals(token)) {
// The style declaration is well formed.
} else if (BLOCK_END.equals(token)) {
} else if (RULE_END.equals(token)) {
// The style declaration is well formed and we can go on, but the closing bracket had to be
// fed back.
input.setPosition(position);
@ -250,7 +262,7 @@ import java.util.regex.Pattern;
// Syntax error.
return null;
}
if (BLOCK_END.equals(token) || ";".equals(token)) {
if (RULE_END.equals(token) || ";".equals(token)) {
input.setPosition(position);
expressionEndFound = true;
} else {

View File

@ -80,10 +80,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
throw new SubtitleDecoderException("A style block was found after the first cue.");
}
parsableWebvttData.readLine(); // Consume the "STYLE" header.
WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
if (styleBlock != null) {
definedStyles.add(styleBlock);
}
definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
} else if (event == EVENT_CUE) {
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
subtitles.add(webvttCueBuilder.build());

View File

@ -757,7 +757,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
for (int i = 0; i < values.length; i++) {
logValues[i] = new double[values[i].length];
for (int j = 0; j < values[i].length; j++) {
logValues[i][j] = Math.log(values[i][j]);
logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]);
}
}
return logValues;
@ -779,7 +779,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];
for (int j = 0; j < logBitrates[i].length - 1; j++) {
double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]);
switchPoints[i][j] = (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;
switchPoints[i][j] =
totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;
}
}
return switchPoints;

View File

@ -1934,6 +1934,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
getAdaptiveAudioTracks(
selectedGroup,
formatSupports[selectedGroupIndex],
params.maxAudioBitrate,
params.allowAudioMixedMimeTypeAdaptiveness,
params.allowAudioMixedSampleRateAdaptiveness);
if (adaptiveTracks.length > 0) {
@ -1951,6 +1952,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private static int[] getAdaptiveAudioTracks(
TrackGroup group,
int[] formatSupport,
int maxAudioBitrate,
boolean allowMixedMimeTypeAdaptiveness,
boolean allowMixedSampleRateAdaptiveness) {
int selectedConfigurationTrackCount = 0;
@ -1967,6 +1969,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
group,
formatSupport,
configuration,
maxAudioBitrate,
allowMixedMimeTypeAdaptiveness,
allowMixedSampleRateAdaptiveness);
if (configurationCount > selectedConfigurationTrackCount) {
@ -1977,13 +1980,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
if (selectedConfigurationTrackCount > 1) {
Assertions.checkNotNull(selectedConfiguration);
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
int index = 0;
for (int i = 0; i < group.length; i++) {
Format format = group.getFormat(i);
if (isSupportedAdaptiveAudioTrack(
group.getFormat(i),
format,
formatSupport[i],
Assertions.checkNotNull(selectedConfiguration),
selectedConfiguration,
maxAudioBitrate,
allowMixedMimeTypeAdaptiveness,
allowMixedSampleRateAdaptiveness)) {
adaptiveIndices[index++] = i;
@ -1998,6 +2004,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup group,
int[] formatSupport,
AudioConfigurationTuple configuration,
int maxAudioBitrate,
boolean allowMixedMimeTypeAdaptiveness,
boolean allowMixedSampleRateAdaptiveness) {
int count = 0;
@ -2006,6 +2013,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
group.getFormat(i),
formatSupport[i],
configuration,
maxAudioBitrate,
allowMixedMimeTypeAdaptiveness,
allowMixedSampleRateAdaptiveness)) {
count++;
@ -2018,9 +2026,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
Format format,
int formatSupport,
AudioConfigurationTuple configuration,
int maxAudioBitrate,
boolean allowMixedMimeTypeAdaptiveness,
boolean allowMixedSampleRateAdaptiveness) {
return isSupported(formatSupport, false)
&& (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate)
&& (format.channelCount != Format.NO_VALUE
&& format.channelCount == configuration.channelCount)
&& (allowMixedMimeTypeAdaptiveness

View File

@ -42,6 +42,7 @@ import java.util.Map;
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
* explicit dependency on ExoPlayer's RTMP extension.
* <li>data: For parsing data inlined in the URI as defined in RFC 2397.
* <li>udp: For fetching data over UDP (e.g. udp://something.com/media).
* <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4),
* if constructed using {@link #DefaultDataSource(Context, TransferListener, String,
* boolean)}, or any other schemes supported by a base data source if constructed using {@link
@ -55,6 +56,7 @@ public final class DefaultDataSource implements DataSource {
private static final String SCHEME_ASSET = "asset";
private static final String SCHEME_CONTENT = "content";
private static final String SCHEME_RTMP = "rtmp";
private static final String SCHEME_UDP = "udp";
private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
private final Context context;
@ -62,12 +64,13 @@ public final class DefaultDataSource implements DataSource {
private final DataSource baseDataSource;
// Lazily initialized.
private @Nullable DataSource fileDataSource;
private @Nullable DataSource assetDataSource;
private @Nullable DataSource contentDataSource;
private @Nullable DataSource rtmpDataSource;
private @Nullable DataSource dataSchemeDataSource;
private @Nullable DataSource rawResourceDataSource;
@Nullable private DataSource fileDataSource;
@Nullable private DataSource assetDataSource;
@Nullable private DataSource contentDataSource;
@Nullable private DataSource rtmpDataSource;
@Nullable private DataSource udpDataSource;
@Nullable private DataSource dataSchemeDataSource;
@Nullable private DataSource rawResourceDataSource;
private @Nullable DataSource dataSource;
@ -218,6 +221,7 @@ public final class DefaultDataSource implements DataSource {
maybeAddListenerToDataSource(assetDataSource, transferListener);
maybeAddListenerToDataSource(contentDataSource, transferListener);
maybeAddListenerToDataSource(rtmpDataSource, transferListener);
maybeAddListenerToDataSource(udpDataSource, transferListener);
maybeAddListenerToDataSource(dataSchemeDataSource, transferListener);
maybeAddListenerToDataSource(rawResourceDataSource, transferListener);
}
@ -240,6 +244,8 @@ public final class DefaultDataSource implements DataSource {
dataSource = getContentDataSource();
} else if (SCHEME_RTMP.equals(scheme)) {
dataSource = getRtmpDataSource();
} else if (SCHEME_UDP.equals(scheme)) {
dataSource = getUdpDataSource();
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
dataSource = getDataSchemeDataSource();
} else if (SCHEME_RAW.equals(scheme)) {
@ -277,6 +283,14 @@ public final class DefaultDataSource implements DataSource {
}
}
private DataSource getUdpDataSource() {
if (udpDataSource == null) {
udpDataSource = new UdpDataSource();
addListenersToDataSource(udpDataSource);
}
return udpDataSource;
}
private DataSource getFileDataSource() {
if (fileDataSource == null) {
fileDataSource = new FileDataSource();

View File

@ -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();
}
}
}

View File

@ -134,9 +134,9 @@ public final class CacheDataSource implements DataSource {
private @Nullable DataSource currentDataSource;
private boolean currentDataSpecLengthUnset;
private @Nullable Uri uri;
private @Nullable Uri actualUri;
private @HttpMethod int httpMethod;
@Nullable private Uri uri;
@Nullable private Uri actualUri;
@HttpMethod private int httpMethod;
private int flags;
private @Nullable String key;
private long readPosition;
@ -319,7 +319,7 @@ public final class CacheDataSource implements DataSource {
}
return bytesRead;
} catch (IOException e) {
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) {
setNoBytesRemainingAndMaybeStoreLength();
return C.RESULT_END_OF_INPUT;
}
@ -484,20 +484,6 @@ public final class CacheDataSource implements DataSource {
return redirectedUri != null ? redirectedUri : defaultUri;
}
private static boolean isCausedByPositionOutOfRange(IOException e) {
Throwable cause = e;
while (cause != null) {
if (cause instanceof DataSourceException) {
int reason = ((DataSourceException) cause).reason;
if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
return true;
}
}
cause = cause.getCause();
}
return false;
}
private boolean isReadingFromUpstream() {
return !isReadingFromCache();
}

View File

@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;
@ -78,13 +79,7 @@ public final class CacheUtil {
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long position = dataSpec.absoluteStreamPosition;
long requestLength;
if (dataSpec.length != C.LENGTH_UNSET) {
requestLength = dataSpec.length;
} else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position;
}
long requestLength = getRequestLength(dataSpec, cache, key);
long bytesAlreadyCached = 0;
long bytesLeft = requestLength;
while (bytesLeft != 0) {
@ -179,53 +174,66 @@ public final class CacheUtil {
Assertions.checkNotNull(dataSource);
Assertions.checkNotNull(buffer);
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long bytesLeft;
ProgressNotifier progressNotifier = null;
if (progressListener != null) {
progressNotifier = new ProgressNotifier(progressListener);
Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
bytesLeft = lengthAndBytesAlreadyCached.first;
} else {
bytesLeft = getRequestLength(dataSpec, cache, key);
}
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long position = dataSpec.absoluteStreamPosition;
long bytesLeft;
if (dataSpec.length != C.LENGTH_UNSET) {
bytesLeft = dataSpec.length;
} else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position;
}
boolean lengthUnset = bytesLeft == C.LENGTH_UNSET;
while (bytesLeft != 0) {
throwExceptionIfInterruptedOrCancelled(isCanceled);
long blockLength =
cache.getCachedLength(
key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft);
if (blockLength > 0) {
// Skip already cached data.
} else {
// There is a hole in the cache which is at least "-blockLength" long.
blockLength = -blockLength;
long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength;
boolean isLastBlock = length == bytesLeft;
long read =
readAndDiscard(
dataSpec,
position,
blockLength,
length,
dataSource,
buffer,
priorityTaskManager,
priority,
progressNotifier,
isLastBlock,
isCanceled);
if (read < blockLength) {
// Reached to the end of the data.
if (enableEOFException && bytesLeft != C.LENGTH_UNSET) {
if (enableEOFException && !lengthUnset) {
throw new EOFException();
}
break;
}
}
position += blockLength;
bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength;
if (!lengthUnset) {
bytesLeft -= blockLength;
}
}
}
private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) {
if (dataSpec.length != C.LENGTH_UNSET) {
return dataSpec.length;
} else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
return contentLength == C.LENGTH_UNSET
? C.LENGTH_UNSET
: contentLength - dataSpec.absoluteStreamPosition;
}
}
@ -242,6 +250,7 @@ public final class CacheUtil {
* caching.
* @param priority The priority of this task.
* @param progressNotifier A notifier through which to report progress updates, or {@code null}.
* @param isLastBlock Whether this read block is the last block of the content.
* @param isCanceled An optional flag that will interrupt caching if set to true.
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
* has been reached.
@ -255,54 +264,64 @@ public final class CacheUtil {
PriorityTaskManager priorityTaskManager,
int priority,
@Nullable ProgressNotifier progressNotifier,
boolean isLastBlock,
AtomicBoolean isCanceled)
throws IOException, InterruptedException {
long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;
long initialPositionOffset = positionOffset;
long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;
while (true) {
if (priorityTaskManager != null) {
// Wait for any other thread with higher priority to finish its job.
priorityTaskManager.proceed(priority);
}
try {
throwExceptionIfInterruptedOrCancelled(isCanceled);
// Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in
// case the given length exceeds the end of input.
dataSpec =
new DataSpec(
dataSpec.uri,
dataSpec.httpMethod,
dataSpec.httpBody,
absoluteStreamPosition,
/* position= */ dataSpec.position + positionOffset,
C.LENGTH_UNSET,
dataSpec.key,
dataSpec.flags);
long resolvedLength = dataSource.open(dataSpec);
if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
try {
long resolvedLength = C.LENGTH_UNSET;
boolean isDataSourceOpen = false;
if (endOffset != C.POSITION_UNSET) {
// If a specific length is given, first try to open the data source for that length to
// avoid more data then required to be requested. If the given length exceeds the end of
// input we will get a "position out of range" error. In that case try to open the source
// again with unset length.
try {
resolvedLength =
dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset));
isDataSourceOpen = true;
} catch (IOException exception) {
if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) {
throw exception;
}
Util.closeQuietly(dataSource);
}
}
if (!isDataSourceOpen) {
resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET));
}
if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);
}
long totalBytesRead = 0;
while (totalBytesRead != length) {
while (positionOffset != endOffset) {
throwExceptionIfInterruptedOrCancelled(isCanceled);
int bytesRead =
dataSource.read(
buffer,
0,
length != C.LENGTH_UNSET
? (int) Math.min(buffer.length, length - totalBytesRead)
endOffset != C.POSITION_UNSET
? (int) Math.min(buffer.length, endOffset - positionOffset)
: buffer.length);
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (progressNotifier != null) {
progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead);
progressNotifier.onRequestLengthResolved(positionOffset);
}
break;
}
totalBytesRead += bytesRead;
positionOffset += bytesRead;
if (progressNotifier != null) {
progressNotifier.onBytesCached(bytesRead);
}
}
return totalBytesRead;
return positionOffset - initialPositionOffset;
} catch (PriorityTaskManager.PriorityTooLowException exception) {
// catch and try again
} finally {
@ -340,6 +359,20 @@ public final class CacheUtil {
}
}
/*package*/ static boolean isCausedByPositionOutOfRange(IOException e) {
Throwable cause = e;
while (cause != null) {
if (cause instanceof DataSourceException) {
int reason = ((DataSourceException) cause).reason;
if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
return true;
}
}
cause = cause.getCause();
}
return false;
}
private static String buildCacheKey(
DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {
return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)

View File

@ -348,8 +348,9 @@ public final class MimeTypes {
case MimeTypes.AUDIO_AC3:
return C.ENCODING_AC3;
case MimeTypes.AUDIO_E_AC3:
case MimeTypes.AUDIO_E_AC3_JOC:
return C.ENCODING_E_AC3;
case MimeTypes.AUDIO_E_AC3_JOC:
return C.ENCODING_E_AC3_JOC;
case MimeTypes.AUDIO_AC4:
return C.ENCODING_AC4;
case MimeTypes.AUDIO_DTS:

View File

@ -1713,7 +1713,12 @@ public final class Util {
if (connectivityManager == null) {
return C.NETWORK_TYPE_UNKNOWN;
}
try {
networkInfo = connectivityManager.getActiveNetworkInfo();
} catch (SecurityException e) {
// Expected if permission was revoked.
return C.NETWORK_TYPE_UNKNOWN;
}
if (networkInfo == null || !networkInfo.isConnected()) {
return C.NETWORK_TYPE_OFFLINE;
}

View File

@ -550,8 +550,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
MediaCodec codec,
Format format,
MediaCrypto crypto,
float codecOperatingRate)
throws DecoderQueryException {
float codecOperatingRate) {
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
MediaFormat mediaFormat =
getMediaFormat(
@ -1173,11 +1172,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @param format The format for which the codec is being configured.
* @param streamFormats The possible stream formats.
* @return Suitable {@link CodecMaxValues}.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
protected CodecMaxValues getCodecMaxValues(
MediaCodecInfo codecInfo, Format format, Format[] streamFormats)
throws DecoderQueryException {
MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {
int maxWidth = format.width;
int maxHeight = format.height;
int maxInputSize = getMaxInputSize(codecInfo, format);
@ -1227,17 +1224,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
* Returns a maximum video size to use when configuring a codec for {@code format} in a way
* that will allow possible adaptation to other compatible formats that are expected to have the
* same aspect ratio, but whose sizes are unknown.
* Returns a maximum video size to use when configuring a codec for {@code format} in a way that
* will allow possible adaptation to other compatible formats that are expected to have the same
* aspect ratio, but whose sizes are unknown.
*
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The format for which the codec is being configured.
* @return The maximum video size to use, or null if the size of {@code format} should be used.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format)
throws DecoderQueryException {
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {
boolean isVerticalVideo = format.height > format.width;
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
int formatShortEdgePx = isVerticalVideo ? format.width : format.height;
@ -1255,13 +1250,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return alignedSize;
}
} else {
try {
// Conservatively assume the codec requires 16px width and height alignment.
longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16;
shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16;
if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) {
return new Point(isVerticalVideo ? shortEdgePx : longEdgePx,
return new Point(
isVerticalVideo ? shortEdgePx : longEdgePx,
isVerticalVideo ? longEdgePx : shortEdgePx);
}
} catch (DecoderQueryException e) {
// We tried our best. Give up!
return null;
}
}
}
return null;

View File

@ -13,8 +13,6 @@ STYLE
::cue(#id2) {
color: peachpuff;
}
STYLE
::cue(v[voice="LaGord"]) { background-color: lime }
STYLE

View File

@ -514,7 +514,7 @@ public final class TtmlDecoderTest {
assertThat(cue.position).isEqualTo(24f / 100f);
assertThat(cue.line).isEqualTo(28f / 100f);
assertThat(cue.size).isEqualTo(51f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
@ -524,7 +524,7 @@ public final class TtmlDecoderTest {
assertThat(cue.position).isEqualTo(21f / 100f);
assertThat(cue.line).isEqualTo(35f / 100f);
assertThat(cue.size).isEqualTo(57f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(6f / 100f);
cues = subtitle.getCues(7500000);
assertThat(cues).hasSize(1);
@ -534,7 +534,7 @@ public final class TtmlDecoderTest {
assertThat(cue.position).isEqualTo(24f / 100f);
assertThat(cue.line).isEqualTo(28f / 100f);
assertThat(cue.size).isEqualTo(51f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
}
@Test
@ -549,7 +549,7 @@ public final class TtmlDecoderTest {
assertThat(cue.position).isEqualTo(307f / 1280f);
assertThat(cue.line).isEqualTo(562f / 720f);
assertThat(cue.size).isEqualTo(653f / 1280f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(86f / 720f);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
@ -559,7 +559,7 @@ public final class TtmlDecoderTest {
assertThat(cue.position).isEqualTo(269f / 1280f);
assertThat(cue.line).isEqualTo(612f / 720f);
assertThat(cue.size).isEqualTo(730f / 1280f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(43f / 720f);
}
@Test

View File

@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -87,21 +88,32 @@ public final class CssParserTest {
@Test
public void testParseMethodSimpleInput() {
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
WebvttCssStyle expectedStyle = new WebvttCssStyle();
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
expectedStyle.setFontColor(0xFF000000);
expectedStyle.setBackgroundColor(0xFFFFEFD5);
assertParserProduces(expectedStyle, styleBlock1);
assertParserProduces(styleBlock1, expectedStyle);
String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
expectedStyle = new WebvttCssStyle();
expectedStyle.setFontColor(0xFF000000);
assertParserProduces(expectedStyle, styleBlock2);
assertParserProduces(styleBlock2, expectedStyle);
String styleBlock3 = " \n::cue {\n background-color\n:#00fFFe}";
String styleBlock3 = "::cue {\n background-color\n:#00fFFe}";
expectedStyle = new WebvttCssStyle();
expectedStyle.setBackgroundColor(0xFF00FFFE);
assertParserProduces(expectedStyle, styleBlock3);
assertParserProduces(styleBlock3, expectedStyle);
}
@Test
public void testParseMethodMultipleRulesInBlockInput() {
String styleBlock =
"::cue {\n background-color\n:#00fFFe} \n::cue {\n background-color\n:#00000000}\n";
WebvttCssStyle expectedStyle = new WebvttCssStyle();
expectedStyle.setBackgroundColor(0xFF00FFFE);
WebvttCssStyle secondExpectedStyle = new WebvttCssStyle();
secondExpectedStyle.setBackgroundColor(0x000000);
assertParserProduces(styleBlock, expectedStyle, secondExpectedStyle);
}
@Test
@ -116,7 +128,7 @@ public final class CssParserTest {
expectedStyle.setFontFamily("courier");
expectedStyle.setBold(true);
assertParserProduces(expectedStyle, styleBlock);
assertParserProduces(styleBlock, expectedStyle);
}
@Test
@ -128,7 +140,7 @@ public final class CssParserTest {
expectedStyle.setBackgroundColor(0x190A0B0C);
expectedStyle.setFontColor(0xFF010101);
assertParserProduces(expectedStyle, styleBlock);
assertParserProduces(styleBlock, expectedStyle);
}
@Test
@ -203,10 +215,13 @@ public final class CssParserTest {
assertThat(input.readLine()).isEqualTo(expectedLine);
}
private void assertParserProduces(WebvttCssStyle expected,
String styleBlock){
private void assertParserProduces(String styleBlock, WebvttCssStyle... expectedStyles) {
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock));
WebvttCssStyle actualElem = parser.parseBlock(input);
List<WebvttCssStyle> styles = parser.parseBlock(input);
assertThat(styles.size()).isEqualTo(expectedStyles.length);
for (int i = 0; i < expectedStyles.length; i++) {
WebvttCssStyle expected = expectedStyles[i];
WebvttCssStyle actualElem = styles.get(i);
assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor());
if (expected.hasBackgroundColor()) {
assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor());
@ -223,5 +238,6 @@ public final class CssParserTest {
assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline());
assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign());
}
}
}

View File

@ -341,6 +341,76 @@ public final class DefaultTrackSelectorTest {
assertFixedSelection(result.selections.get(0), trackGroups, formatWithSelectionFlag);
}
/** Tests that adaptive audio track selections respect the maximum audio bitrate. */
public void testSelectAdaptiveAudioTrackGroupWithMaxBitrate() throws ExoPlaybackException {
Format format128k =
Format.createAudioSampleFormat(
/* id= */ "128",
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
/* codecs= */ "mp4a.40.2",
/* bitrate= */ 128 * 1024,
/* maxInputSize= */ Format.NO_VALUE,
/* channelCount= */ 2,
/* sampleRate= */ 44100,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
Format format192k =
Format.createAudioSampleFormat(
/* id= */ "192",
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
/* codecs= */ "mp4a.40.2",
/* bitrate= */ 192 * 1024,
/* maxInputSize= */ Format.NO_VALUE,
/* channelCount= */ 2,
/* sampleRate= */ 44100,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
Format format256k =
Format.createAudioSampleFormat(
/* id= */ "256",
/* sampleMimeType= */ MimeTypes.AUDIO_AAC,
/* codecs= */ "mp4a.40.2",
/* bitrate= */ 256 * 1024,
/* maxInputSize= */ Format.NO_VALUE,
/* channelCount= */ 2,
/* sampleRate= */ 44100,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
RendererCapabilities[] rendererCapabilities = {
ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES
};
TrackGroupArray trackGroups =
new TrackGroupArray(new TrackGroup(format192k, format128k, format256k));
TrackSelectorResult result =
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1, 2);
trackSelector.setParameters(
trackSelector.buildUponParameters().setMaxAudioBitrate(256 * 1024 - 1));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
trackSelector.setParameters(
trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024 - 1));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1);
trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(10));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1);
}
/**
* Tests that track selector will select audio track with language that match preferred language
* given by {@link Parameters}.
@ -893,7 +963,6 @@ public final class DefaultTrackSelectorTest {
Format forcedDefault =
buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT);
Format defaultOnly = buildTextFormat("defaultOnly", "eng", C.SELECTION_FLAG_DEFAULT);
Format forcedOnlySpanish = buildTextFormat("forcedOnlySpanish", "spa", C.SELECTION_FLAG_FORCED);
Format noFlag = buildTextFormat("noFlag", "eng");
RendererCapabilities[] textRendererCapabilities =

View File

@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.Downloader;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
@ -108,7 +109,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test
@ -127,7 +128,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test
@ -146,7 +147,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader =
getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test
@ -167,7 +168,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test
@ -256,7 +257,7 @@ public class DashDownloaderTest {
// Expected.
}
dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test

View File

@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;
import com.google.android.exoplayer2.testutil.FakeDataSet;
@ -154,7 +155,7 @@ public class DownloadManagerDashTest {
public void testHandleDownloadRequest() throws Throwable {
handleDownloadRequest(fakeStreamKey1, fakeStreamKey2);
blockUntilTasksCompleteAndThrowAnyDownloadError();
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test
@ -162,7 +163,7 @@ public class DownloadManagerDashTest {
handleDownloadRequest(fakeStreamKey1);
handleDownloadRequest(fakeStreamKey2);
blockUntilTasksCompleteAndThrowAnyDownloadError();
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test
@ -176,7 +177,7 @@ public class DownloadManagerDashTest {
handleDownloadRequest(fakeStreamKey1);
blockUntilTasksCompleteAndThrowAnyDownloadError();
assertCachedData(cache, fakeDataSet);
assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data"));
}
@Test

View File

@ -322,6 +322,7 @@ import java.util.Map;
if (enabledTrackGroupCount == 0) {
chunkSource.reset();
downstreamTrackFormat = null;
pendingResetUpstreamFormats = true;
mediaChunks.clear();
if (loader.isLoading()) {
if (sampleQueuesBuilt) {

View File

@ -44,6 +44,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.upstream.DummyDataSource;
@ -129,12 +130,13 @@ public class HlsDownloaderTest {
assertCachedData(
cache,
fakeDataSet,
new RequestSet(fakeDataSet)
.subset(
MASTER_PLAYLIST_URI,
MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"));
}
@Test
@ -186,11 +188,12 @@ public class HlsDownloaderTest {
assertCachedData(
cache,
fakeDataSet,
new RequestSet(fakeDataSet)
.subset(
MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"));
}
@Test

View File

@ -220,11 +220,26 @@ public class DefaultTimeBar extends View implements TimeBar {
private @Nullable long[] adGroupTimesMs;
private @Nullable boolean[] playedAdGroups;
/** Creates a new time bar. */
public DefaultTimeBar(Context context) {
this(context, null);
}
public DefaultTimeBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, attrs);
}
// Suppress warnings due to usage of View methods in the constructor.
@SuppressWarnings("nullness:method.invocation.invalid")
public DefaultTimeBar(Context context, AttributeSet attrs) {
super(context, attrs);
public DefaultTimeBar(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet timebarAttrs) {
super(context, attrs, defStyleAttr);
seekBounds = new Rect();
progressBar = new Rect();
bufferedBar = new Rect();
@ -251,9 +266,9 @@ public class DefaultTimeBar extends View implements TimeBar {
int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP);
int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP);
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0,
0);
if (timebarAttrs != null) {
TypedArray a =
context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0);
try {
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
if (scrubberDrawable != null) {

View File

@ -28,6 +28,7 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@ -97,6 +98,9 @@ import java.util.Locale;
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_player_control_view}
* </ul>
* <li>All attributes that can be set on {@link DefaultTimeBar} can also be set on a
* PlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar} unless the
* layout is overridden to specify a custom {@code exo_progress} (see below).
* </ul>
*
* <h3>Overriding the layout file</h3>
@ -154,7 +158,15 @@ import java.util.Locale;
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_progress_placeholder}</b> - A placeholder that's replaced with the inflated
* {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
* {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically
* propagated through to this instance. If a view exists with this id, any {@code
* exo_progress_placeholder} view will be ignored.
* <ul>
* <li>Type: {@link TimeBar}
* </ul>
@ -188,6 +200,18 @@ public class PlayerControlView extends FrameLayout {
void onVisibilityChange(int visibility);
}
/** Listener to be notified when progress has been updated. */
public interface ProgressUpdateListener {
/**
* Called when progress needs to be updated.
*
* @param position The current position.
* @param bufferedPosition The current buffered position.
*/
void onProgressUpdate(long position, long bufferedPosition);
}
/** The default fast forward increment, in milliseconds. */
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
/** The default rewind increment, in milliseconds. */
@ -235,7 +259,8 @@ public class PlayerControlView extends FrameLayout {
@Nullable private Player player;
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
private VisibilityListener visibilityListener;
@Nullable private VisibilityListener visibilityListener;
@Nullable private ProgressUpdateListener progressUpdateListener;
@Nullable private PlaybackPreparer playbackPreparer;
private boolean isAttachedToWindow;
@ -317,9 +342,27 @@ public class PlayerControlView extends FrameLayout {
LayoutInflater.from(context).inflate(controllerLayoutId, this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
TimeBar customTimeBar = findViewById(R.id.exo_progress);
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
if (customTimeBar != null) {
timeBar = customTimeBar;
} else if (timeBarPlaceholder != null) {
// Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,
// but standard attributes (e.g. background) are not.
DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs);
defaultTimeBar.setId(R.id.exo_progress);
defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
int timeBarIndex = parent.indexOfChild(timeBarPlaceholder);
parent.removeView(timeBarPlaceholder);
parent.addView(defaultTimeBar, timeBarIndex);
timeBar = defaultTimeBar;
} else {
timeBar = null;
}
durationView = findViewById(R.id.exo_duration);
positionView = findViewById(R.id.exo_position);
timeBar = findViewById(R.id.exo_progress);
if (timeBar != null) {
timeBar.addListener(componentListener);
}
@ -454,6 +497,15 @@ public class PlayerControlView extends FrameLayout {
this.visibilityListener = listener;
}
/**
* Sets the {@link ProgressUpdateListener}.
*
* @param listener The listener to be notified about when progress is updated.
*/
public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) {
this.progressUpdateListener = listener;
}
/**
* Sets the {@link PlaybackPreparer}.
*
@ -855,6 +907,9 @@ public class PlayerControlView extends FrameLayout {
timeBar.setPosition(position);
timeBar.setBufferedPosition(bufferedPosition);
}
if (progressUpdateListener != null) {
progressUpdateListener.onProgressUpdate(position, bufferedPosition);
}
// Cancel any pending updates and schedule a new one if necessary.
removeCallbacks(updateProgressAction);

View File

@ -966,7 +966,8 @@ public class PlayerNotificationManager {
@Nullable NotificationCompat.Builder builder,
boolean ongoing,
@Nullable Bitmap largeIcon) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (player.getPlaybackState() == Player.STATE_IDLE
&& (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) {
builderActions = null;
return null;
}

View File

@ -35,7 +35,6 @@ import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
@ -50,7 +49,6 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.VideoComponent;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.source.TrackGroupArray;
@ -165,9 +163,10 @@ import java.util.List;
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_player_control_view}
* </ul>
* <li>All attributes that can be set on a {@link PlayerControlView} can also be set on a
* PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the
* layout is overridden to specify a custom {@code exo_controller} (see below).
* <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can
* also be set on a PlayerView, and will be propagated to the inflated {@link
* PlayerControlView} unless the layout is overridden to specify a custom {@code
* exo_controller} (see below).
* </ul>
*
* <h3>Overriding the layout file</h3>
@ -217,9 +216,10 @@ import java.util.List;
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_controller}</b> - An already inflated {@link PlayerControlView}. Allows use
* of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code
* rewind_increment} will not be automatically propagated through to this instance. If a view
* exists with this id, any {@code exo_controller_placeholder} view will be ignored.
* of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link
* DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated
* through to this instance. If a view exists with this id, any {@code
* exo_controller_placeholder} view will be ignored.
* <ul>
* <li>Type: {@link PlayerControlView}
* </ul>
@ -303,6 +303,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
private boolean controllerHideDuringAds;
private boolean controllerHideOnTouch;
private int textureViewRotation;
private boolean isTouching;
public PlayerView(Context context) {
this(context, null);
@ -405,7 +406,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
break;
case SURFACE_TYPE_MONO360_VIEW:
SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context);
sphericalSurfaceView.setSurfaceListener(componentListener);
sphericalSurfaceView.setSingleTapListener(componentListener);
surfaceView = sphericalSurfaceView;
break;
@ -459,8 +459,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
this.controller = customController;
} else if (controllerPlaceholder != null) {
// Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are
// transferred, but standard FrameLayout attributes (e.g. background) are not.
// transferred, but standard attributes (e.g. background) are not.
this.controller = new PlayerControlView(context, null, 0, attrs);
controller.setId(R.id.exo_controller);
controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
int controllerIndex = parent.indexOfChild(controllerPlaceholder);
@ -771,11 +772,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
if (player != null && player.isPlayingAd()) {
return super.dispatchKeyEvent(event);
}
boolean isDpadWhenControlHidden =
isDpadKey(event.getKeyCode()) && useController && !controller.isVisible();
boolean handled =
isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
if (handled) {
boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController;
boolean handled = false;
if (isDpadAndUseController && !controller.isVisible()) {
// Handle the key event by showing the controller.
maybeShowController(true);
handled = true;
} else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) {
// The key event was handled as a media key or by the super class. We should also show the
// controller, or extend its show timeout if already visible.
maybeShowController(true);
handled = true;
} else if (isDpadAndUseController) {
// The key event wasn't handled, but we should extend the controller's show timeout.
maybeShowController(true);
}
return handled;
@ -1039,11 +1049,21 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouching = true;
return true;
case MotionEvent.ACTION_UP:
if (isTouching) {
isTouching = false;
performClick();
return true;
}
return false;
default:
return false;
}
return performClick();
}
@Override
@ -1359,7 +1379,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
TextOutput,
VideoListener,
OnLayoutChangeListener,
SphericalSurfaceView.SurfaceListener,
SingleTapListener {
// TextOutput implementation
@ -1449,18 +1468,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
applyTextureViewRotation((TextureView) view, textureViewRotation);
}
// SphericalSurfaceView.SurfaceTextureListener implementation
@Override
public void surfaceChanged(@Nullable Surface surface) {
if (player != null) {
VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.setVideoSurface(surface);
}
}
}
// SingleTapListener implementation
@Override

View File

@ -362,10 +362,16 @@ import com.google.android.exoplayer2.util.Util;
int width = Math.round(parentWidth * cueSize);
int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight)
: Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));
int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width)
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);
int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height)
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY);
int x =
Math.round(
cuePositionAnchor == Cue.ANCHOR_TYPE_END
? (anchorX - width)
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);
int y =
Math.round(
cueLineAnchor == Cue.ANCHOR_TYPE_END
? (anchorY - height)
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY);
bitmapRect = new Rect(x, y, x + width, y + height);
}

View File

@ -53,20 +53,6 @@ import javax.microedition.khronos.opengles.GL10;
*/
public final class SphericalSurfaceView extends GLSurfaceView {
/**
* This listener can be used to be notified when the {@link Surface} associated with this view is
* changed.
*/
public interface SurfaceListener {
/**
* Invoked when the surface is changed or there isn't one anymore. Any previous surface
* shouldn't be used after this call.
*
* @param surface The new surface or null if there isn't one anymore.
*/
void surfaceChanged(@Nullable Surface surface);
}
// Arbitrary vertical field of view.
private static final int FIELD_OF_VIEW_DEGREES = 90;
private static final float Z_NEAR = .1f;
@ -84,7 +70,6 @@ public final class SphericalSurfaceView extends GLSurfaceView {
private final Handler mainHandler;
private final TouchTracker touchTracker;
private final SceneRenderer scene;
private @Nullable SurfaceListener surfaceListener;
private @Nullable SurfaceTexture surfaceTexture;
private @Nullable Surface surface;
private @Nullable Player.VideoComponent videoComponent;
@ -156,15 +141,6 @@ public final class SphericalSurfaceView extends GLSurfaceView {
}
}
/**
* Sets the {@link SurfaceListener} used to listen to surface events.
*
* @param listener The listener for surface events.
*/
public void setSurfaceListener(@Nullable SurfaceListener listener) {
surfaceListener = listener;
}
/** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */
public void setSingleTapListener(@Nullable SingleTapListener listener) {
touchTracker.setSingleTapListener(listener);
@ -196,8 +172,8 @@ public final class SphericalSurfaceView extends GLSurfaceView {
mainHandler.post(
() -> {
if (surface != null) {
if (surfaceListener != null) {
surfaceListener.surfaceChanged(null);
if (videoComponent != null) {
videoComponent.clearVideoSurface(surface);
}
releaseSurface(surfaceTexture, surface);
surfaceTexture = null;
@ -214,8 +190,8 @@ public final class SphericalSurfaceView extends GLSurfaceView {
Surface oldSurface = this.surface;
this.surfaceTexture = surfaceTexture;
this.surface = new Surface(surfaceTexture);
if (surfaceListener != null) {
surfaceListener.surfaceChanged(surface);
if (videoComponent != null) {
videoComponent.setVideoSurface(surface);
}
releaseSurface(oldSurfaceTexture, oldSurface);
});

View File

@ -76,8 +76,7 @@
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
<View android:id="@id/exo_progress_placeholder"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="26dp"/>

View File

@ -24,25 +24,43 @@
<enum name="zoom" value="4"/>
</attr>
<!-- Must be kept in sync with SimpleExoPlayerView -->
<!-- Must be kept in sync with PlayerView -->
<attr name="surface_type" format="enum">
<enum name="none" value="0"/>
<enum name="surface_view" value="1"/>
<enum name="texture_view" value="2"/>
<enum name="spherical_view" value="3"/>
</attr>
<attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/>
<attr name="fastforward_increment" format="integer"/>
<attr name="player_layout_id" format="reference"/>
<attr name="controller_layout_id" format="reference"/>
<!-- Must be kept in sync with RepeatModeUtil -->
<attr name="repeat_toggle_modes">
<flag name="none" value="0"/>
<flag name="one" value="1"/>
<flag name="all" value="2"/>
</attr>
<!-- PlayerControlView attributes -->
<attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/>
<attr name="fastforward_increment" format="integer"/>
<attr name="show_shuffle_button" format="boolean"/>
<attr name="time_bar_min_update_interval" format="integer"/>
<attr name="controller_layout_id" format="reference"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height" format="dimension"/>
<attr name="touch_target_height" format="dimension"/>
<attr name="ad_marker_width" format="dimension"/>
<attr name="scrubber_enabled_size" format="dimension"/>
<attr name="scrubber_disabled_size" format="dimension"/>
<attr name="scrubber_dragged_size" format="dimension"/>
<attr name="scrubber_drawable" format="reference"/>
<attr name="played_color" format="color"/>
<attr name="scrubber_color" format="color"/>
<attr name="buffered_color" format="color"/>
<attr name="unplayed_color" format="color"/>
<attr name="ad_marker_color" format="color"/>
<attr name="played_ad_marker_color" format="color"/>
<declare-styleable name="PlayerView">
<attr name="use_artwork" format="boolean"/>
@ -58,9 +76,11 @@
<enum name="always" value="2"/>
</attr>
<attr name="keep_content_on_player_reset" format="boolean"/>
<attr name="resize_mode"/>
<attr name="player_layout_id" format="reference"/>
<attr name="surface_type"/>
<attr name="player_layout_id"/>
<!-- AspectRatioFrameLayout attributes -->
<attr name="resize_mode"/>
<!-- PlayerControlView attributes -->
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
@ -69,6 +89,20 @@
<attr name="show_shuffle_button"/>
<attr name="time_bar_min_update_interval"/>
<attr name="controller_layout_id"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
<attr name="scrubber_disabled_size"/>
<attr name="scrubber_dragged_size"/>
<attr name="scrubber_drawable"/>
<attr name="played_color"/>
<attr name="scrubber_color"/>
<attr name="buffered_color" />
<attr name="unplayed_color"/>
<attr name="ad_marker_color"/>
<attr name="played_ad_marker_color"/>
</declare-styleable>
<declare-styleable name="AspectRatioFrameLayout">
@ -83,22 +117,36 @@
<attr name="show_shuffle_button"/>
<attr name="time_bar_min_update_interval"/>
<attr name="controller_layout_id"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
<attr name="scrubber_disabled_size"/>
<attr name="scrubber_dragged_size"/>
<attr name="scrubber_drawable"/>
<attr name="played_color"/>
<attr name="scrubber_color"/>
<attr name="buffered_color" />
<attr name="unplayed_color"/>
<attr name="ad_marker_color"/>
<attr name="played_ad_marker_color"/>
</declare-styleable>
<declare-styleable name="DefaultTimeBar">
<attr name="bar_height" format="dimension"/>
<attr name="touch_target_height" format="dimension"/>
<attr name="ad_marker_width" format="dimension"/>
<attr name="scrubber_enabled_size" format="dimension"/>
<attr name="scrubber_disabled_size" format="dimension"/>
<attr name="scrubber_dragged_size" format="dimension"/>
<attr name="scrubber_drawable" format="reference"/>
<attr name="played_color" format="color"/>
<attr name="scrubber_color" format="color"/>
<attr name="buffered_color" format="color"/>
<attr name="unplayed_color" format="color"/>
<attr name="ad_marker_color" format="color"/>
<attr name="played_ad_marker_color" format="color"/>
<attr name="bar_height"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
<attr name="scrubber_disabled_size"/>
<attr name="scrubber_dragged_size"/>
<attr name="scrubber_drawable"/>
<attr name="played_color"/>
<attr name="scrubber_color"/>
<attr name="buffered_color" />
<attr name="unplayed_color"/>
<attr name="ad_marker_color"/>
<attr name="played_ad_marker_color"/>
</declare-styleable>
</resources>

View File

@ -33,6 +33,7 @@
<item name="exo_repeat_toggle" type="id"/>
<item name="exo_duration" type="id"/>
<item name="exo_position" type="id"/>
<item name="exo_progress_placeholder" type="id"/>
<item name="exo_progress" type="id"/>
<item name="exo_buffering" type="id"/>
<item name="exo_error_message" type="id"/>

View File

@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.nio.ByteBuffer;
@ -55,6 +54,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean enableDecoderFallback,
Handler eventHandler,
VideoRendererEventListener eventListener,
long allowedVideoJoiningTimeMs,
@ -113,8 +113,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
MediaCodec codec,
Format format,
MediaCrypto crypto,
float operatingRate)
throws DecoderQueryException {
float operatingRate) {
// If the codec is being initialized whilst the renderer is started, default behavior is to
// render the first frame (i.e. the keyframe before the current position), then drop frames up
// to the current playback position. For test runs that place a maximum limit on the number of

View File

@ -166,7 +166,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName()));
setContentView(
getResources().getIdentifier("exo_testutils_host_activity", "layout", getPackageName()));
surfaceView = findViewById(
getResources().getIdentifier("surface_view", "id", getPackageName()));
surfaceView.getHolder().addCallback(this);

View File

@ -33,58 +33,89 @@ import java.util.ArrayList;
/** Assertion methods for {@link Cache}. */
public final class CacheAsserts {
/**
* Asserts that the cache content is equal to the data in the {@code fakeDataSet}.
*
* @throws IOException If an error occurred reading from the Cache.
*/
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {
/** Defines a set of data requests. */
public static final class RequestSet {
private final FakeDataSet fakeDataSet;
private DataSpec[] dataSpecs;
public RequestSet(FakeDataSet fakeDataSet) {
this.fakeDataSet = fakeDataSet;
ArrayList<FakeData> allData = fakeDataSet.getAllData();
Uri[] uris = new Uri[allData.size()];
for (int i = 0; i < allData.size(); i++) {
uris[i] = allData.get(i).uri;
dataSpecs = new DataSpec[allData.size()];
for (int i = 0; i < dataSpecs.length; i++) {
dataSpecs[i] = new DataSpec(allData.get(i).uri);
}
}
public RequestSet subset(String... uriStrings) {
dataSpecs = new DataSpec[uriStrings.length];
for (int i = 0; i < dataSpecs.length; i++) {
dataSpecs[i] = new DataSpec(Uri.parse(uriStrings[i]));
}
return this;
}
public RequestSet subset(Uri... uris) {
dataSpecs = new DataSpec[uris.length];
for (int i = 0; i < dataSpecs.length; i++) {
dataSpecs[i] = new DataSpec(uris[i]);
}
return this;
}
public RequestSet subset(DataSpec... dataSpecs) {
this.dataSpecs = dataSpecs;
return this;
}
public int getCount() {
return dataSpecs.length;
}
public byte[] getData(int i) {
return fakeDataSet.getData(dataSpecs[i].uri).getData();
}
public DataSpec getDataSpec(int i) {
return dataSpecs[i];
}
public RequestSet useBoundedDataSpecFor(String uriString) {
FakeData data = fakeDataSet.getData(uriString);
for (int i = 0; i < dataSpecs.length; i++) {
DataSpec spec = dataSpecs[i];
if (spec.uri.getPath().equals(uriString)) {
dataSpecs[i] = spec.subrange(0, data.getData().length);
return this;
}
}
throw new IllegalStateException();
}
assertCachedData(cache, fakeDataSet, uris);
}
/**
* Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}.
* Asserts that the cache contains necessary data for the {@code requestSet}.
*
* @throws IOException If an error occurred reading from the Cache.
*/
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings)
throws IOException {
Uri[] uris = new Uri[uriStrings.length];
for (int i = 0; i < uriStrings.length; i++) {
uris[i] = Uri.parse(uriStrings[i]);
}
assertCachedData(cache, fakeDataSet, uris);
}
/**
* Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}.
*
* @throws IOException If an error occurred reading from the Cache.
*/
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris)
throws IOException {
public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException {
int totalLength = 0;
for (Uri uri : uris) {
byte[] data = fakeDataSet.getData(uri).getData();
assertDataCached(cache, uri, data);
for (int i = 0; i < requestSet.getCount(); i++) {
byte[] data = requestSet.getData(i);
assertDataCached(cache, requestSet.getDataSpec(i), data);
totalLength += data.length;
}
assertThat(cache.getCacheSpace()).isEqualTo(totalLength);
}
/**
* Asserts that the cache contains the given data for {@code uriString}.
* Asserts that the cache content is equal to the data in the {@code fakeDataSet}.
*
* @throws IOException If an error occurred reading from the Cache.
*/
public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException {
DataSpec dataSpec = new DataSpec(uri);
assertDataCached(cache, dataSpec, expected);
public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {
assertCachedData(cache, new RequestSet(fakeDataSet));
}
/**
@ -95,15 +126,18 @@ public final class CacheAsserts {
public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected)
throws IOException {
DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0);
dataSource.open(dataSpec);
byte[] bytes;
try {
byte[] bytes = TestUtil.readToEnd(dataSource);
assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',")
.that(bytes)
.isEqualTo(expected);
dataSource.open(dataSpec);
bytes = TestUtil.readToEnd(dataSource);
} catch (IOException e) {
throw new IOException("Opening/reading cache failed: " + dataSpec, e);
} finally {
dataSource.close();
}
assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',")
.that(bytes)
.isEqualTo(expected);
}
/**