diff --git a/.gitignore b/.gitignore index 4731d5ba991..2ec73a6fd8d 100644 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,3 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md - -# Cast receiver -cast_receiver_app/external-js -cast_receiver_app/bazel-cast_receiver_app diff --git a/.hgignore b/.hgignore index 36d3268005e..5889f43b8df 100644 --- a/.hgignore +++ b/.hgignore @@ -12,13 +12,14 @@ libs obj lint.xml -# IntelliJ IDEA +# IntelliJ IDEA & Android Studio .idea *.iml *.ipr *.iws classes gen-external-apklibs +*.li # Eclipse .project @@ -75,7 +76,3 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md - -# Cast receiver -cast_receiver_app/external-js -cast_receiver_app/bazel-cast_receiver_app diff --git a/README.md b/README.md index a369b077f4b..d488f4113e6 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ branch: ```sh git clone https://github.com/google/ExoPlayer.git +cd ExoPlayer git checkout release-v2 ``` diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3169dac565a..aac69e14380 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,32 @@ # Release notes # +### 2.10.6 (2019-10-18) ### + +* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to + detect playbacks suppressions (e.g. transient audio focus loss) directly + ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* DASH: + * Support `Label` elements + ([#6297](https://github.com/google/ExoPlayer/issues/6297)). + * Support legacy audio channel configuration + ([#6523](https://github.com/google/ExoPlayer/issues/6523)). +* HLS: Add support for ID3 in EMSG when using FMP4 streams + ([spec](https://aomediacodec.github.io/av1-id3/)). +* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast + live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537), + [#6315](https://github.com/google/ExoPlayer/issues/6315) and + [#5658](https://github.com/google/ExoPlayer/issues/5658)). +* Metadata: Expose the raw ICY metadata through `IcyInfo` + ([#6476](https://github.com/google/ExoPlayer/issues/6476)). +* UI: + * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer + adjusts the colors of the scrubber handle , buffered and unplayed parts of + the time bar. These can be set separately using `app:scrubber_color`, + `app:buffered_color` and `app_unplayed_color` respectively. + * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no + longer adjusts the color of played ad markers. The color of played ad + markers can be set separately using `app:played_ad_marker_color`. + ### 2.10.5 (2019-09-20) ### * Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether diff --git a/build.gradle b/build.gradle index 1d0b459bf5b..a4823b94eec 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' - classpath 'com.novoda:bintray-release:0.9' - classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' + classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.novoda:bintray-release:0.9.1' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0' } } allprojects { diff --git a/constants.gradle b/constants.gradle index 5334adcb398..2a8d0c5776e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.5' - releaseVersionCode = 2010005 + releaseVersion = '2.10.6' + releaseVersionCode = 2010006 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 23d625b9eef..30d4d6ce109 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.BasePlayer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -67,6 +68,10 @@ */ public final class CastPlayer extends BasePlayer { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.cast"); + } + private static final String TAG = "CastPlayer"; private static final int RENDERER_COUNT = 3; diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 5b68f1e352c..e65fc581ae5 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -25,8 +25,7 @@ follows: ``` cd "" -EXOPLAYER_ROOT="$(pwd)" -FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" +FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" ``` * Download the [Android NDK][] and set its location in an environment variable. @@ -69,7 +68,7 @@ COMMON_OPTIONS="\ --enable-decoder=opus \ --enable-decoder=flac \ " && \ -cd "${FFMPEG_EXT_PATH}/jni" && \ +cd "${FFMPEG_EXT_PATH}" && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ cd ffmpeg && git checkout release/4.0 && \ ./configure \ @@ -112,7 +111,7 @@ make clean built in the previous step. For example: ``` -cd "${FFMPEG_EXT_PATH}"/jni && \ +cd "${FFMPEG_EXT_PATH}" && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index f454e28c68f..5e020175e76 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -19,6 +19,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -216,15 +218,25 @@ public long getNextFrameFirstSampleIndex() { } /** - * Maps a seek position in microseconds to a corresponding position (byte offset) in the flac + * Maps a seek position in microseconds to the corresponding {@link SeekMap.SeekPoints} in the * stream. * * @param timeUs A seek position in microseconds. - * @return The corresponding position (byte offset) in the flac stream or -1 if the stream doesn't - * have a seek table. + * @return The corresponding {@link SeekMap.SeekPoints} obtained from the seek table, or {@code + * null} if the stream doesn't have a seek table. */ - public long getSeekPosition(long timeUs) { - return flacGetSeekPosition(nativeDecoderContext, timeUs); + @Nullable + public SeekMap.SeekPoints getSeekPoints(long timeUs) { + long[] seekPoints = new long[4]; + if (!flacGetSeekPoints(nativeDecoderContext, timeUs, seekPoints)) { + return null; + } + SeekPoint firstSeekPoint = new SeekPoint(seekPoints[0], seekPoints[1]); + SeekPoint secondSeekPoint = + seekPoints[2] == seekPoints[0] + ? firstSeekPoint + : new SeekPoint(seekPoints[2], seekPoints[3]); + return new SeekMap.SeekPoints(firstSeekPoint, secondSeekPoint); } public String getStateString() { @@ -283,7 +295,7 @@ private native int flacDecodeToArray(long context, byte[] outputArray) private native long flacGetNextFrameFirstSampleIndex(long context); - private native long flacGetSeekPosition(long context, long timeUs); + private native boolean flacGetSeekPoints(long context, long timeUs, long[] outSeekPoints); private native String flacGetStateString(long context); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index cd91b062888..59fb7b48353 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -276,10 +276,10 @@ private static FlacBinarySearchSeeker outputSeekMap( FlacStreamMetadata streamMetadata, long streamLength, ExtractorOutput output) { - boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null; FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; - if (hasSeekTable) { + if (haveSeekTable) { seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); @@ -341,8 +341,8 @@ public boolean isSeekable() { @Override public SeekPoints getSeekPoints(long timeUs) { - // TODO: Access the seek table via JNI to return two seek points when appropriate. - return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs))); + @Nullable SeekPoints seekPoints = decoderJni.getSeekPoints(timeUs); + return seekPoints == null ? new SeekPoints(SeekPoint.START) : seekPoints; } @Override diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index d60a7cead2a..f0a33f323cd 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -46,7 +47,6 @@ class JavaDataSource : public DataSource { if (mid == NULL) { jclass cls = env->GetObjectClass(flacDecoderJni); mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I"); - env->DeleteLocalRef(cls); } } @@ -57,7 +57,6 @@ class JavaDataSource : public DataSource { // Exception is thrown in Java when returning from the native call. result = -1; } - env->DeleteLocalRef(byteBuffer); return result; } @@ -200,9 +199,15 @@ DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) { return context->parser->getNextFrameFirstSampleIndex(); } -DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { +DECODER_FUNC(jboolean, flacGetSeekPoints, jlong jContext, jlong timeUs, + jlongArray outSeekPoints) { Context *context = reinterpret_cast(jContext); - return context->parser->getSeekPosition(timeUs); + std::array result; + bool success = context->parser->getSeekPositions(timeUs, result); + if (success) { + env->SetLongArrayRegion(outSeekPoints, 0, result.size(), result.data()); + } + return success; } DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 830f3e2178a..7c69119fe40 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -438,22 +438,41 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { return bufferSize; } -int64_t FLACParser::getSeekPosition(int64_t timeUs) { +bool FLACParser::getSeekPositions(int64_t timeUs, + std::array &result) { if (!mSeekTable) { - return -1; + return false; } - int64_t sample = (timeUs * getSampleRate()) / 1000000LL; - if (sample >= getTotalSamples()) { - sample = getTotalSamples(); + unsigned sampleRate = getSampleRate(); + int64_t totalSamples = getTotalSamples(); + int64_t targetSampleNumber = (timeUs * sampleRate) / 1000000LL; + if (targetSampleNumber >= totalSamples) { + targetSampleNumber = totalSamples - 1; } FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; - for (unsigned i = mSeekTable->num_points; i > 0; ) { - i--; - if (points[i].sample_number <= sample) { - return firstFrameOffset + points[i].stream_offset; + unsigned length = mSeekTable->num_points; + + for (unsigned i = length; i != 0; i--) { + int64_t sampleNumber = points[i - 1].sample_number; + if (sampleNumber <= targetSampleNumber) { + result[0] = (sampleNumber * 1000000LL) / sampleRate; + result[1] = firstFrameOffset + points[i - 1].stream_offset; + if (sampleNumber == targetSampleNumber || i >= length) { + // exact seek, or no following seek point. + result[2] = result[0]; + result[3] = result[1]; + } else { + result[2] = (points[i].sample_number * 1000000LL) / sampleRate; + result[3] = firstFrameOffset + points[i].stream_offset; + } + return true; } } - return firstFrameOffset; + result[0] = 0; + result[1] = firstFrameOffset; + result[2] = 0; + result[3] = firstFrameOffset; + return true; } diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index 14ba9e8725a..1468bc0543c 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -19,6 +19,7 @@ #include +#include #include #include #include @@ -82,7 +83,7 @@ class FLACParser { bool decodeMetadata(); size_t readBuffer(void *output, size_t output_size); - int64_t getSeekPosition(int64_t timeUs); + bool getSeekPositions(int64_t timeUs, std::array &result); void flush() { reset(mCurrentPos); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 5183c8a673b..f42e0509918 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -868,26 +868,27 @@ private boolean canDispatchMediaButtonEvent() { private void rewind(Player player) { if (player.isCurrentWindowSeekable() && rewindMs > 0) { - seekTo(player, player.getCurrentPosition() - rewindMs); + seekToOffset(player, /* offsetMs= */ -rewindMs); } } private void fastForward(Player player) { if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { - seekTo(player, player.getCurrentPosition() + fastForwardMs); + seekToOffset(player, /* offsetMs= */ fastForwardMs); } } - private void seekTo(Player player, long positionMs) { - seekTo(player, player.getCurrentWindowIndex(), positionMs); - } - - private void seekTo(Player player, int windowIndex, long positionMs) { + private void seekToOffset(Player player, long offsetMs) { + long positionMs = player.getCurrentPosition() + offsetMs; long durationMs = player.getDuration(); if (durationMs != C.TIME_UNSET) { positionMs = Math.min(positionMs, durationMs); } positionMs = Math.max(positionMs, 0); + seekTo(player, player.getCurrentWindowIndex(), positionMs); + } + + private void seekTo(Player player, int windowIndex, long positionMs) { controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); } @@ -1096,7 +1097,7 @@ public void onPlay() { playbackPreparer.onPrepare(/* playWhenReady= */ true); } } else if (player.getPlaybackState() == Player.STATE_ENDED) { - controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } controlDispatcher.dispatchSetPlayWhenReady( Assertions.checkNotNull(player), /* playWhenReady= */ true); @@ -1113,7 +1114,7 @@ public void onPause() { @Override public void onSeekTo(long positionMs) { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) { - seekTo(player, positionMs); + seekTo(player, player.getCurrentWindowIndex(), positionMs); } } diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java index 36abf825d67..efa8ca9ac44 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java @@ -37,7 +37,7 @@ public RtmpDataSourceFactory(@Nullable TransferListener listener) { } @Override - public DataSource createDataSource() { + public RtmpDataSource createDataSource() { RtmpDataSource dataSource = new RtmpDataSource(); if (listener != null) { dataSource.addTransferListener(listener); diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6d00e1ce970..7fefd1c665b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 25 13:15:25 BST 2019 +#Mon Oct 07 17:24:00 BST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 5010faaa9d4..552eefcef91 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -260,17 +260,21 @@ public void setPlayWhenReady( internalPlayer.setPlayWhenReady(internalPlayWhenReady); } boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; + boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason; this.playWhenReady = playWhenReady; this.playbackSuppressionReason = playbackSuppressionReason; boolean isPlaying = isPlaying(); boolean isPlayingChanged = oldIsPlaying != isPlaying; - if (playWhenReadyChanged || isPlayingChanged) { + if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) { int playbackState = playbackInfo.playbackState; notifyListeners( listener -> { if (playWhenReadyChanged) { listener.onPlayerStateChanged(playWhenReady, playbackState); } + if (suppressionReasonChanged) { + listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); + } if (isPlayingChanged) { listener.onIsPlayingChanged(isPlaying); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index bf64a142250..adc05eb2047 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -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.5"; + public static final String VERSION = "2.10.6"; /** 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.5"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.6"; /** * 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 = 2010005; + public static final int VERSION_INT = 2010006; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index d12c7ea18e4..85b8230d602 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -1066,6 +1066,38 @@ public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { accessibilityChannel); } + public Format copyWithLabel(@Nullable String label) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel); + } + public Format copyWithContainerInfo( @Nullable String id, @Nullable String label, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index dad0945d7b2..a0218c48b9c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -365,6 +365,14 @@ default void onLoadingChanged(boolean isLoading) {} */ default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} + /** + * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes. + * + * @param playbackSuppressionReason The current {@link PlaybackSuppressionReason}. + */ + default void onPlaybackSuppressionReasonChanged( + @PlaybackSuppressionReason int playbackSuppressionReason) {} + /** * Called when the value of {@link #isPlaying()} changes. * @@ -470,18 +478,21 @@ public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { int STATE_ENDED = 4; /** - * Reason why playback is suppressed even if {@link #getPlaybackState()} is {@link #STATE_READY} - * and {@link #getPlayWhenReady()} is {@code true}. One of {@link - * #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link #PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS}. + * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One + * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link + * #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}. */ @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({PLAYBACK_SUPPRESSION_REASON_NONE, PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS}) + @IntDef({ + PLAYBACK_SUPPRESSION_REASON_NONE, + PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS + }) @interface PlaybackSuppressionReason {} /** Playback is not suppressed. */ int PLAYBACK_SUPPRESSION_REASON_NONE = 0; - /** Playback is suppressed because audio focus is lost or can't be acquired. */ - int PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS = 1; + /** Playback is suppressed due to transient audio focus loss. */ + int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; /** * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link @@ -609,13 +620,10 @@ public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { int getPlaybackState(); /** - * Returns reason why playback is suppressed even if {@link #getPlaybackState()} is {@link - * #STATE_READY} and {@link #getPlayWhenReady()} is {@code true}. - * - *

Note that {@link #PLAYBACK_SUPPRESSION_REASON_NONE} indicates that playback is not - * suppressed. + * Returns the reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code + * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed. * - * @return The current {@link PlaybackSuppressionReason}. + * @return The current {@link PlaybackSuppressionReason playback suppression reason}. */ @PlaybackSuppressionReason int getPlaybackSuppressionReason(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 33dfbae3ec8..4696e7cc43e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1228,13 +1228,13 @@ private void sendVolumeToRenderers() { private void updatePlayWhenReady( boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY; + @PlaybackSuppressionReason int playbackSuppressionReason = - playerCommand == AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY - ? Player.PLAYBACK_SUPPRESSION_REASON_NONE - : Player.PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS; - player.setPlayWhenReady( - playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY, - playbackSuppressionReason); + playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY + ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS + : Player.PLAYBACK_SUPPRESSION_REASON_NONE; + player.setPlayWhenReady(playWhenReady, playbackSuppressionReason); } private void verifyApplicationThread() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index c0d96e8e885..bdb98261f3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; @@ -471,6 +472,23 @@ public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) } } + @Override + public void onPlaybackSuppressionReasonChanged( + @PlaybackSuppressionReason int playbackSuppressionReason) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason); + } + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onIsPlayingChanged(eventTime, isPlaying); + } + } + @Override public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 3400cf25b69..2ffafbe90d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -132,6 +133,23 @@ public EventTime( default void onPlayerStateChanged( EventTime eventTime, boolean playWhenReady, int playbackState) {} + /** + * Called when playback suppression reason changed. + * + * @param eventTime The event time. + * @param playbackSuppressionReason The new {@link PlaybackSuppressionReason}. + */ + default void onPlaybackSuppressionReasonChanged( + EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {} + + /** + * Called when the player starts or stops playing. + * + * @param eventTime The event time. + * @param isPlaying Whether the player is playing. + */ + default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {} + /** * Called when the timeline changed. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 0cb55dffa50..15a98ab5add 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -25,7 +25,7 @@ public interface SeekMap { /** A {@link SeekMap} that does not support seeking. */ - final class Unseekable implements SeekMap { + class Unseekable implements SeekMap { private final long durationUs; private final SeekPoints startSeekPoints; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index f4007207728..4a5feb5096b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -22,8 +22,7 @@ /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. */ -/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap - implements Mp3Extractor.Seeker { +/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker { /** * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java index 868c1d9fbff..1b627483f08 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.util.Util; /** MP3 seeker that uses metadata from an {@link MlltFrame}. */ -/* package */ final class MlltSeeker implements Mp3Extractor.Seeker { +/* package */ final class MlltSeeker implements Seeker { /** * Returns an {@link MlltSeeker} for seeking in the stream. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index bc218e26ad6..6134f042c29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -28,8 +28,8 @@ import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; @@ -114,7 +114,8 @@ public final class Mp3Extractor implements Extractor { private int synchronizedHeaderData; private Metadata metadata; - private Seeker seeker; + @Nullable private Seeker seeker; + private boolean disableSeeking; private long basisTimeUs; private long samplesRead; private long firstSamplePosition; @@ -188,14 +189,19 @@ public int read(ExtractorInput input, PositionHolder seekPosition) // takes priority as it can provide greater precision. Seeker seekFrameSeeker = maybeReadSeekFrame(input); Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); - if (metadataSeeker != null) { - seeker = metadataSeeker; - } else if (seekFrameSeeker != null) { - seeker = seekFrameSeeker; - } - if (seeker == null - || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { - seeker = getConstantBitrateSeeker(input); + + if (disableSeeking) { + seeker = new UnseekableSeeker(); + } else { + if (metadataSeeker != null) { + seeker = metadataSeeker; + } else if (seekFrameSeeker != null) { + seeker = seekFrameSeeker; + } + if (seeker == null + || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + seeker = getConstantBitrateSeeker(input); + } } extractorOutput.seekMap(seeker); trackOutput.format( @@ -226,6 +232,15 @@ public int read(ExtractorInput input, PositionHolder seekPosition) return readSample(input); } + /** + * Disables the extractor from being able to seek through the media. + * + *

Please note that this needs to be called before {@link #read}. + */ + public void disableSeeking() { + disableSeeking = true; + } + // Internal methods. private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { @@ -464,26 +479,5 @@ private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstF return null; } - /** - * {@link SeekMap} that provides the end position of audio data and also allows mapping from - * position (byte offset) back to time, which can be used to work out the new sample basis - * timestamp after seeking and resynchronization. - */ - /* package */ interface Seeker extends SeekMap { - - /** - * Maps a position (byte offset) to a corresponding sample timestamp. - * - * @param position A seek position (byte offset) relative to the start of the stream. - * @return The corresponding timestamp of the next sample to be read, in microseconds. - */ - long getTimeUs(long position); - - /** - * Returns the position (byte offset) in the stream that is immediately after audio data, or - * {@link C#POSITION_UNSET} if not known. - */ - long getDataEndPosition(); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java new file mode 100644 index 00000000000..c5b7550f2d8 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Seeker.java @@ -0,0 +1,60 @@ +/* + * 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.extractor.mp3; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; + +/** + * {@link SeekMap} that provides the end position of audio data and also allows mapping from + * position (byte offset) back to time, which can be used to work out the new sample basis timestamp + * after seeking and resynchronization. + */ +/* package */ interface Seeker extends SeekMap { + + /** + * Maps a position (byte offset) to a corresponding sample timestamp. + * + * @param position A seek position (byte offset) relative to the start of the stream. + * @return The corresponding timestamp of the next sample to be read, in microseconds. + */ + long getTimeUs(long position); + + /** + * Returns the position (byte offset) in the stream that is immediately after audio data, or + * {@link C#POSITION_UNSET} if not known. + */ + long getDataEndPosition(); + + /** A {@link Seeker} that does not support seeking through audio data. */ + /* package */ class UnseekableSeeker extends SeekMap.Unseekable implements Seeker { + + public UnseekableSeeker() { + super(/* durationUs= */ C.TIME_UNSET); + } + + @Override + public long getTimeUs(long position) { + return 0; + } + + @Override + public long getDataEndPosition() { + // Position unset as we do not know the data end position. Note that returning 0 doesn't work. + return C.POSITION_UNSET; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index ba8b26b7c19..86551319e10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -23,10 +23,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -/** - * MP3 seeker that uses metadata from a VBRI header. - */ -/* package */ final class VbriSeeker implements Mp3Extractor.Seeker { +/** MP3 seeker that uses metadata from a VBRI header. */ +/* package */ final class VbriSeeker implements Seeker { private static final String TAG = "VbriSeeker"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 116a1230944..7094f327c82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -24,10 +24,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -/** - * MP3 seeker that uses metadata from a Xing header. - */ -/* package */ final class XingSeeker implements Mp3Extractor.Seeker { +/** MP3 seeker that uses metadata from a Xing header. */ +/* package */ final class XingSeeker implements Seeker { private static final String TAG = "XingSeeker"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 7fc748485b9..8581f279d0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -377,18 +377,13 @@ public boolean isVideoSizeAndRateSupportedV21(int width, int height, double fram @TargetApi(21) public Point alignVideoSizeV21(int width, int height) { if (capabilities == null) { - logNoSupport("align.caps"); return null; } VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); if (videoCapabilities == null) { - logNoSupport("align.vCaps"); return null; } - int widthAlignment = videoCapabilities.getWidthAlignment(); - int heightAlignment = videoCapabilities.getHeightAlignment(); - return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment, - Util.ceilDivide(height, heightAlignment) * heightAlignment); + return alignVideoSizeV21(videoCapabilities, width, height); } /** @@ -519,6 +514,11 @@ private static boolean isSecureV21(CodecCapabilities capabilities) { @TargetApi(21) private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, int height, double frameRate) { + // Don't ever fail due to alignment. See: https://github.com/google/ExoPlayer/issues/6551. + Point alignedSize = alignVideoSizeV21(capabilities, width, height); + width = alignedSize.x; + height = alignedSize.y; + if (frameRate == Format.NO_VALUE || frameRate <= 0) { return capabilities.isSizeSupported(width, height); } else { @@ -530,6 +530,15 @@ private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities } } + @TargetApi(21) + private static Point alignVideoSizeV21(VideoCapabilities capabilities, int width, int height) { + int widthAlignment = capabilities.getWidthAlignment(); + int heightAlignment = capabilities.getHeightAlignment(); + return new Point( + Util.ceilDivide(width, widthAlignment) * widthAlignment, + Util.ceilDivide(height, heightAlignment) * heightAlignment); + } + @TargetApi(23) private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { return capabilities.getMaxSupportedInstances(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index a8cf0f12e2f..7a6ca82c2a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -277,13 +277,17 @@ private static String buildCustomDiagnosticInfo(int errorCode) { private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; /** - * H.264/AVC buffer to queue when using the adaptation workaround (see - * {@link #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: - * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be - * queued to force a resolution change when adapting to a new format. + * H.264/AVC buffer to queue when using the adaptation workaround (see {@link + * #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: Baseline + * sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be queued to + * force a resolution change when adapting to a new format. */ - private static final byte[] ADAPTATION_WORKAROUND_BUFFER = Util.getBytesFromHexString( - "0000016742C00BDA259000000168CE0F13200000016588840DCE7118A0002FBF1C31C3275D78"); + private static final byte[] ADAPTATION_WORKAROUND_BUFFER = + new byte[] { + 0, 0, 1, 103, 66, -64, 11, -38, 37, -112, 0, 0, 1, 104, -50, 15, 19, 32, 0, 0, 1, 101, -120, + -124, 13, -50, 113, 24, -96, 0, 47, -65, 28, 49, -61, 39, 93, 120 + }; + private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 3d873926bbe..a148c03b65d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -15,12 +15,10 @@ */ package com.google.android.exoplayer2.metadata.icy; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.regex.Matcher; @@ -36,7 +34,6 @@ public final class IcyDecoder implements MetadataDecoder { private static final String STREAM_KEY_URL = "streamurl"; @Override - @Nullable @SuppressWarnings("ByteBufferBackingArray") public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; @@ -45,7 +42,6 @@ public Metadata decode(MetadataInputBuffer inputBuffer) { return decode(Util.fromUtf8Bytes(data, 0, length)); } - @Nullable @VisibleForTesting /* package */ Metadata decode(String metadata) { String name = null; @@ -62,12 +58,9 @@ public Metadata decode(MetadataInputBuffer inputBuffer) { case STREAM_KEY_URL: url = value; break; - default: - Log.w(TAG, "Unrecognized ICY tag: " + name); - break; } index = matcher.end(); } - return (name != null || url != null) ? new Metadata(new IcyInfo(name, url)) : null; + return new Metadata(new IcyInfo(metadata, name, url)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java index e6b915a6c80..1198d1af8be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java @@ -19,26 +19,35 @@ import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; /** ICY in-stream information. */ public final class IcyInfo implements Metadata.Entry { + /** The complete metadata string used to construct this IcyInfo. */ + public final String rawMetadata; /** The stream title if present, or {@code null}. */ @Nullable public final String title; - /** The stream title if present, or {@code null}. */ + /** The stream URL if present, or {@code null}. */ @Nullable public final String url; /** + * Construct a new IcyInfo from the source metadata string, and optionally a StreamTitle and + * StreamUrl that have been extracted. + * + * @param rawMetadata See {@link #rawMetadata}. * @param title See {@link #title}. * @param url See {@link #url}. */ - public IcyInfo(@Nullable String title, @Nullable String url) { + public IcyInfo(String rawMetadata, @Nullable String title, @Nullable String url) { + this.rawMetadata = rawMetadata; this.title = title; this.url = url; } /* package */ IcyInfo(Parcel in) { + rawMetadata = Assertions.checkNotNull(in.readString()); title = in.readString(); url = in.readString(); } @@ -52,26 +61,27 @@ public boolean equals(@Nullable Object obj) { return false; } IcyInfo other = (IcyInfo) obj; - return Util.areEqual(title, other.title) && Util.areEqual(url, other.url); + // title & url are derived from rawMetadata, so no need to include them in the comparison. + return Util.areEqual(rawMetadata, other.rawMetadata); } @Override public int hashCode() { - int result = 17; - result = 31 * result + (title != null ? title.hashCode() : 0); - result = 31 * result + (url != null ? url.hashCode() : 0); - return result; + // title & url are derived from rawMetadata, so no need to include them in the hash. + return rawMetadata.hashCode(); } @Override public String toString() { - return "ICY: title=\"" + title + "\", url=\"" + url + "\""; + return String.format( + "ICY: title=\"%s\", url=\"%s\", rawMetadata=\"%s\"", title, url, rawMetadata); } // Parcelable implementation. @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(rawMetadata); dest.writeString(title); dest.writeString(url); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index a5a3efa2876..f7f8a4506b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.SeekMap.Unseekable; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -949,6 +950,12 @@ public void load() throws IOException, InterruptedException { } input = new DefaultExtractorInput(extractorDataSource, position, length); Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri); + + // MP3 live streams commonly have seekable metadata, despite being unseekable. + if (icyHeaders != null && extractor instanceof Mp3Extractor) { + ((Mp3Extractor) extractor).disableSeeking(); + } + if (pendingExtractorSeek) { extractor.seek(position, seekTimeUs); pendingExtractorSeek = false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 0adadd87c2d..a5c08d53990 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -308,7 +308,7 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection( public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; - public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f; public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index 3a47df7654f..c077c887605 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -33,7 +33,7 @@ public FileDataSourceFactory(@Nullable TransferListener listener) { } @Override - public DataSource createDataSource() { + public FileDataSource createDataSource() { FileDataSource dataSource = new FileDataSource(); if (listener != null) { dataSource.addTransferListener(listener); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java index 99f0dee2072..412f866e99e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java @@ -64,9 +64,7 @@ public static final class Factory implements DataSource.Factory { private final Resolver resolver; /** - * Creates factory for {@link ResolvingDataSource} instances. - * - * @param upstreamFactory The wrapped {@link DataSource.Factory} handling the resolved {@link + * @param upstreamFactory The wrapped {@link DataSource.Factory} for handling resolved {@link * DataSpec DataSpecs}. * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. */ @@ -76,7 +74,7 @@ public Factory(DataSource.Factory upstreamFactory, Resolver resolver) { } @Override - public DataSource createDataSource() { + public ResolvingDataSource createDataSource() { return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 566c928b207..319128b6237 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -53,7 +53,6 @@ public final class CacheDataSink implements DataSink { private long dataSpecFragmentSize; private File file; private OutputStream outputStream; - private FileOutputStream underlyingFileOutputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; private ReusableBufferedOutputStream bufferedOutputStream; @@ -171,7 +170,7 @@ private void openNextOutputStream() throws IOException { file = cache.startFile( dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length); - underlyingFileOutputStream = new FileOutputStream(file); + FileOutputStream underlyingFileOutputStream = new FileOutputStream(file); if (bufferSize > 0) { if (bufferedOutputStream == null) { bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java index ccf9a5b3f50..2f241404d2f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -49,10 +49,11 @@ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) { * * @param secretKey The key data. * @param wrappedDataSink The wrapped {@link DataSink}. - * @param scratch Scratch space. Data is decrypted into this array before being written to the + * @param scratch Scratch space. Data is encrypted into this array before being written to the * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a * write is larger than the size of this array the write will still succeed, but multiple - * cipher calls will be required to complete the operation. + * cipher calls will be required to complete the operation. If {@code null} then encryption + * will overwrite the input {@code data}. */ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { this.wrappedDataSink = wrappedDataSink; @@ -91,5 +92,4 @@ public void close() throws IOException { cipher = null; wrappedDataSink.close(); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 7fc46dc363b..c7feff516ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -42,7 +42,7 @@ public static void checkGlError() { int lastError = GLES20.GL_NO_ERROR; int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { - Log.e(TAG, "glError " + gluErrorString(lastError)); + Log.e(TAG, "glError " + gluErrorString(error)); lastError = error; } if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2203b34e869..38f5b741379 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -63,6 +63,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -2659,6 +2660,34 @@ public void run(SimpleExoPlayer player) { assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); } + @Test + public void simplePlaybackHasNoPlaybackSuppression() throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder("simplePlaybackHasNoPlaybackSuppression") + .play() + .waitForPlaybackState(Player.STATE_READY) + .pause() + .play() + .build(); + AtomicBoolean seenPlaybackSuppression = new AtomicBoolean(); + EventListener listener = + new EventListener() { + @Override + public void onPlaybackSuppressionReasonChanged( + @Player.PlaybackSuppressionReason int playbackSuppressionReason) { + seenPlaybackSuppression.set(true); + } + }; + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(seenPlaybackSuppression.get()).isFalse(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 4602d172a66..72237d665c8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -29,10 +29,12 @@ public final class IcyDecoderTest { @Test public void decode() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test title';StreamURL='test_url';"); + String icyContent = "StreamTitle='test title';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test title"); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -40,21 +42,39 @@ public void decode() { @Test public void decode_titleOnly() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test title';"); + String icyContent = "StreamTitle='test title';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test title"); assertThat(streamInfo.url).isNull(); } + @Test + public void decode_extraTags() { + String icyContent = + "StreamTitle='test title';StreamURL='test_url';CustomTag|withWeirdSeparator"; + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode(icyContent); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); + assertThat(streamInfo.title).isEqualTo("test title"); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_emptyTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + String icyContent = "StreamTitle='';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEmpty(); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -62,10 +82,12 @@ public void decode_emptyTitle() { @Test public void decode_semiColonInTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test; title';StreamURL='test_url';"); + String icyContent = "StreamTitle='test; title';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test; title"); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -73,10 +95,12 @@ public void decode_semiColonInTitle() { @Test public void decode_quoteInTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test' title';StreamURL='test_url';"); + String icyContent = "StreamTitle='test' title';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test' title"); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -84,19 +108,25 @@ public void decode_quoteInTitle() { @Test public void decode_lineTerminatorInTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';"); + String icyContent = "StreamTitle='test\r\ntitle';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); assertThat(streamInfo.url).isEqualTo("test_url"); } @Test - public void decode_notIcy() { + public void decode_noReconisedHeaders() { IcyDecoder decoder = new IcyDecoder(); Metadata metadata = decoder.decode("NotIcyData"); - assertThat(metadata).isNull(); + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo("NotIcyData"); + assertThat(streamInfo.title).isNull(); + assertThat(streamInfo.url).isNull(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyStreamInfoTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java similarity index 91% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyStreamInfoTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java index 2bffe171d35..2c8e6616c97 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyStreamInfoTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java @@ -24,11 +24,11 @@ /** Test for {@link IcyInfo}. */ @RunWith(AndroidJUnit4.class) -public final class IcyStreamInfoTest { +public final class IcyInfoTest { @Test public void parcelEquals() { - IcyInfo streamInfo = new IcyInfo("name", "url"); + IcyInfo streamInfo = new IcyInfo("StreamName='name';StreamUrl='url'", "name", "url"); // Write to parcel. Parcel parcel = Parcel.obtain(); streamInfo.writeToParcel(parcel, 0); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 396d16968f8..95e0199d323 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -686,7 +686,9 @@ private RepresentationHolder( newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex); } - long oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; + long oldIndexFirstSegmentNum = oldIndex.getFirstSegmentNum(); + long oldIndexStartTimeUs = oldIndex.getTimeUs(oldIndexFirstSegmentNum); + long oldIndexLastSegmentNum = oldIndexFirstSegmentNum + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs); @@ -700,8 +702,14 @@ private RepresentationHolder( // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. throw new BehindLiveWindowException(); + } else if (newIndexStartTimeUs < oldIndexStartTimeUs) { + // The new index overlaps with (but does not have a start position contained within) the old + // index. This can only happen if extra segments have been added to the start of the index. + newSegmentNumShift -= + newIndex.getSegmentNum(oldIndexStartTimeUs, newPeriodDurationUs) + - oldIndexFirstSegmentNum; } else { - // The new index overlaps with the old one. + // The new index overlaps with (and has a start position contained within) the old index. newSegmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs) - newIndexFirstSegmentNum; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 09313965097..95e8c7f942a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -313,7 +313,6 @@ protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, parseRepresentation( xpp, baseUrl, - label, mimeType, codecs, width, @@ -338,6 +337,8 @@ protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { + label = parseLabel(xpp); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -348,7 +349,11 @@ protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, for (int i = 0; i < representationInfos.size(); i++) { representations.add( buildRepresentation( - representationInfos.get(i), drmSchemeType, drmSchemeDatas, inbandEventStreams)); + representationInfos.get(i), + label, + drmSchemeType, + drmSchemeDatas, + inbandEventStreams)); } return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, @@ -484,7 +489,6 @@ protected void parseAdaptationSetChild(XmlPullParser xpp) protected RepresentationInfo parseRepresentation( XmlPullParser xpp, String baseUrl, - String label, String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth, @@ -551,7 +555,6 @@ protected RepresentationInfo parseRepresentation( Format format = buildFormat( id, - label, mimeType, width, height, @@ -572,7 +575,6 @@ protected RepresentationInfo parseRepresentation( protected Format buildFormat( String id, - String label, String containerMimeType, int width, int height, @@ -596,7 +598,7 @@ protected Format buildFormat( if (MimeTypes.isVideo(sampleMimeType)) { return Format.createVideoContainerFormat( id, - label, + /* label= */ null, containerMimeType, sampleMimeType, codecs, @@ -611,7 +613,7 @@ protected Format buildFormat( } else if (MimeTypes.isAudio(sampleMimeType)) { return Format.createAudioContainerFormat( id, - label, + /* label= */ null, containerMimeType, sampleMimeType, codecs, @@ -634,7 +636,7 @@ protected Format buildFormat( } return Format.createTextContainerFormat( id, - label, + /* label= */ null, containerMimeType, sampleMimeType, codecs, @@ -647,7 +649,7 @@ protected Format buildFormat( } return Format.createContainerFormat( id, - label, + /* label= */ null, containerMimeType, sampleMimeType, codecs, @@ -659,10 +661,14 @@ protected Format buildFormat( protected Representation buildRepresentation( RepresentationInfo representationInfo, + String label, String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; + if (label != null) { + format = format.copyWithLabel(label); + } String drmSchemeType = representationInfo.drmSchemeType != null ? representationInfo.drmSchemeType : extraDrmSchemeType; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; @@ -1076,15 +1082,44 @@ protected ProgramInformation parseProgramInformation(XmlPullParser xpp) return new ProgramInformation(title, source, copyright, moreInformationURL, lang); } + /** + * Parses a Label element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed label. + */ + protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { + return parseText(xpp, "Label"); + } + + /** + * Parses a BaseURL element. + * + * @param xpp The parser from which to read. + * @param parentBaseUrl A base URL for resolving the parsed URL. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed and resolved URL. + */ + protected String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) + throws XmlPullParserException, IOException { + return UriUtil.resolve(parentBaseUrl, parseText(xpp, "BaseURL")); + } + // AudioChannelConfiguration parsing. protected int parseAudioChannelConfiguration(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); - int audioChannels = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseInt(xpp, "value", Format.NO_VALUE) - : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseDolbyChannelConfiguration(xpp) : Format.NO_VALUE); + int audioChannels = + "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) + ? parseInt(xpp, "value", Format.NO_VALUE) + : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) + || "urn:dolby:dash:audio_channel_configuration:2011".equals(schemeIdUri) + ? parseDolbyChannelConfiguration(xpp) + : Format.NO_VALUE); do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); @@ -1427,10 +1462,18 @@ protected static long parseDateTime(XmlPullParser xpp, String name, long default } } - protected static String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) + protected static String parseText(XmlPullParser xpp, String label) throws XmlPullParserException, IOException { - xpp.next(); - return UriUtil.resolve(parentBaseUrl, xpp.getText()); + String text = ""; + do { + xpp.next(); + if (xpp.getEventType() == XmlPullParser.TEXT) { + text = xpp.getText(); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, label)); + return text; } protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) { @@ -1451,7 +1494,8 @@ protected static String parseString(XmlPullParser xpp, String name, String defau /** * Parses the number of channels from the value attribute of an AudioElementConfiguration with * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5 - * in ETSI TS 102 366. + * in ETSI TS 102 366, or the legacy schemeIdUri + * "urn:dolby:dash:audio_channel_configuration:2011". * * @param xpp The parser from which to read. * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could diff --git a/library/dash/src/test/assets/sample_mpd_1 b/library/dash/src/test/assets/sample_mpd similarity index 99% rename from library/dash/src/test/assets/sample_mpd_1 rename to library/dash/src/test/assets/sample_mpd index ccd3ab4dd6c..8417d2f7c4c 100644 --- a/library/dash/src/test/assets/sample_mpd_1 +++ b/library/dash/src/test/assets/sample_mpd @@ -102,4 +102,3 @@ http://www.test.com/vtt - diff --git a/library/dash/src/test/assets/sample_mpd_4_event_stream b/library/dash/src/test/assets/sample_mpd_event_stream similarity index 99% rename from library/dash/src/test/assets/sample_mpd_4_event_stream rename to library/dash/src/test/assets/sample_mpd_event_stream index e4c927260d8..4148b420f1d 100644 --- a/library/dash/src/test/assets/sample_mpd_4_event_stream +++ b/library/dash/src/test/assets/sample_mpd_event_stream @@ -44,4 +44,3 @@ - diff --git a/library/dash/src/test/assets/sample_mpd_labels b/library/dash/src/test/assets/sample_mpd_labels new file mode 100644 index 00000000000..58eceb8c42e --- /dev/null +++ b/library/dash/src/test/assets/sample_mpd_labels @@ -0,0 +1,21 @@ + + + + + + + + + + + https://test.com/0 + + + + + https://test.com/1 + + + + + diff --git a/library/dash/src/test/assets/sample_mpd_3_segment_template b/library/dash/src/test/assets/sample_mpd_segment_template similarity index 99% rename from library/dash/src/test/assets/sample_mpd_3_segment_template rename to library/dash/src/test/assets/sample_mpd_segment_template index a9147b54dfa..d45ab14f52c 100644 --- a/library/dash/src/test/assets/sample_mpd_3_segment_template +++ b/library/dash/src/test/assets/sample_mpd_segment_template @@ -35,4 +35,3 @@ - diff --git a/library/dash/src/test/assets/sample_mpd_2_unknown_mime_type b/library/dash/src/test/assets/sample_mpd_unknown_mime_type similarity index 99% rename from library/dash/src/test/assets/sample_mpd_2_unknown_mime_type rename to library/dash/src/test/assets/sample_mpd_unknown_mime_type index c6f00965e33..4645e3c859c 100644 --- a/library/dash/src/test/assets/sample_mpd_2_unknown_mime_type +++ b/library/dash/src/test/assets/sample_mpd_unknown_mime_type @@ -115,4 +115,3 @@ http://www.test.com/vtt - diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index d1e795e643d..790df39037c 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -26,42 +26,49 @@ import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.io.StringReader; import java.nio.charset.Charset; import java.util.Collections; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; /** Unit tests for {@link DashManifestParser}. */ @RunWith(AndroidJUnit4.class) public class DashManifestParserTest { - private static final String SAMPLE_MPD_1 = "sample_mpd_1"; - private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type"; - private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template"; - private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream"; + private static final String SAMPLE_MPD = "sample_mpd"; + private static final String SAMPLE_MPD_UNKNOWN_MIME_TYPE = "sample_mpd_unknown_mime_type"; + private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = "sample_mpd_segment_template"; + private static final String SAMPLE_MPD_EVENT_STREAM = "sample_mpd_event_stream"; + private static final String SAMPLE_MPD_LABELS = "sample_mpd_labels"; + + private static final String NEXT_TAG_NAME = "Next"; + private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */ @Test - public void testParseMediaPresentationDescription() throws IOException { + public void parseMediaPresentationDescription() throws IOException { DashManifestParser parser = new DashManifestParser(); parser.parse( Uri.parse("https://example.com/test.mpd"), - TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_1)); + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD)); parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_UNKNOWN_MIME_TYPE)); } @Test - public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException { + public void parseMediaPresentationDescription_segmentTemplate() throws IOException { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), SAMPLE_MPD_3_SEGMENT_TEMPLATE)); + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_SEGMENT_TEMPLATE)); assertThat(mpd.getPeriodCount()).isEqualTo(1); @@ -87,13 +94,13 @@ public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IO } @Test - public void testParseMediaPresentationDescriptionCanParseEventStream() throws IOException { + public void parseMediaPresentationDescription_eventStream() throws IOException { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), SAMPLE_MPD_4_EVENT_STREAM)); + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_EVENT_STREAM)); Period period = mpd.getPeriod(0); assertThat(period.eventStreams).hasSize(3); @@ -157,12 +164,12 @@ public void testParseMediaPresentationDescriptionCanParseEventStream() throws IO } @Test - public void testParseMediaPresentationDescriptionCanParseProgramInformation() throws IOException { + public void parseMediaPresentationDescription_programInformation() throws IOException { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( Uri.parse("Https://example.com/test.mpd"), - TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_1)); + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD)); ProgramInformation expectedProgramInformation = new ProgramInformation( "MediaTitle", "MediaSource", "MediaCopyright", "www.example.com", "enUs"); @@ -170,7 +177,46 @@ public void testParseMediaPresentationDescriptionCanParseProgramInformation() th } @Test - public void testParseCea608AccessibilityChannel() { + public void parseMediaPresentationDescription_labels() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_LABELS)); + + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + assertThat(adaptationSets.get(0).representations.get(0).format.label).isEqualTo("audio label"); + assertThat(adaptationSets.get(1).representations.get(0).format.label).isEqualTo("video label"); + } + + @Test + public void parseLabel() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader("" + NEXT_TAG)); + xpp.next(); + + String label = parser.parseLabel(xpp); + assertThat(label).isEqualTo("test label"); + assertNextTag(xpp); + } + + @Test + public void parseLabel_noText() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader("

HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is + * wrapped in an EMSG box [spec]. + * + *

If this is set to {@link HlsMetadataType#ID3} then raw ID3 metadata of will be extracted + * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant + * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be + * dropped. + * + *

If this is set to {@link HlsMetadataType#EMSG} then all EMSG data from the fMP4 variant + * stream will be extracted. No metadata will be extracted from TS streams, since they don't + * support EMSG. + * + * @param metadataType The type of metadata to extract. + * @return This factory, for convenience. + */ + public Factory setMetadataType(@HlsMetadataType int metadataType) { + Assertions.checkState(!isCreateCalled); + this.metadataType = metadataType; + return this; + } + /** * Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's * assumed that any single session key declared in the master playlist can be used to obtain all @@ -272,6 +299,7 @@ public HlsMediaSource createMediaSource(Uri playlistUri) { playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), allowChunklessPreparation, + metadataType, useSessionKeys, tag); } @@ -305,6 +333,7 @@ public int[] getSupportedTypes() { private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; + private final @HlsMetadataType int metadataType; private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; private final @Nullable Object tag; @@ -319,6 +348,7 @@ private HlsMediaSource( LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, + @HlsMetadataType int metadataType, boolean useSessionKeys, @Nullable Object tag) { this.manifestUri = manifestUri; @@ -328,6 +358,7 @@ private HlsMediaSource( this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; + this.metadataType = metadataType; this.useSessionKeys = useSessionKeys; this.tag = tag; } @@ -363,6 +394,7 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star allocator, compositeSequenceableLoaderFactory, allowChunklessPreparation, + metadataType, useSessionKeys); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java new file mode 100644 index 00000000000..e445466e672 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java @@ -0,0 +1,34 @@ +/* + * 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.hls; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import java.lang.annotation.Retention; + +/** + * The types of metadata that can be extracted from HLS streams. + * + *

See {@link HlsMediaSource.Factory#setMetadataType(int)}. + */ +@Retention(SOURCE) +@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG}) +public @interface HlsMetadataType { + int ID3 = 1; + int EMSG = 3; +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 7c7f0ed823b..67abb7bf014 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -17,6 +17,7 @@ import android.net.Uri; import android.os.Handler; +import android.util.SparseIntArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -24,10 +25,13 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DummyTrackOutput; +import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; @@ -46,13 +50,18 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -87,6 +96,11 @@ public interface Callback extends SequenceableLoader.Callback MAPPABLE_TYPES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_METADATA))); + private final int trackType; private final Callback callback; private final HlsChunkSource chunkSource; @@ -95,6 +109,7 @@ public interface Callback extends SequenceableLoader.Callback mediaChunks; private final List readOnlyMediaChunks; @@ -106,10 +121,9 @@ public interface Callback extends SequenceableLoader.Callback sampleQueueMappingDoneByType; + private SparseIntArray sampleQueueIndicesByType; + private TrackOutput emsgUnwrappingTrackOutput; private int primarySampleQueueType; private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; @@ -119,7 +133,7 @@ public interface Callback extends SequenceableLoader.Callback(MAPPABLE_TYPES.size()); + sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); sampleQueues = new SampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; @@ -766,8 +782,7 @@ public LoadErrorAction onLoadError( */ public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) { if (!reusingExtractor) { - audioSampleQueueMappingDone = false; - videoSampleQueueMappingDone = false; + sampleQueueMappingDoneByType.clear(); } this.chunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { @@ -784,45 +799,71 @@ public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) @Override public TrackOutput track(int id, int type) { - int trackCount = sampleQueues.length; - - // Audio and video tracks are handled manually to ignore ids. - if (type == C.TRACK_TYPE_AUDIO) { - if (audioSampleQueueIndex != C.INDEX_UNSET) { - if (audioSampleQueueMappingDone) { - return sampleQueueTrackIds[audioSampleQueueIndex] == id - ? sampleQueues[audioSampleQueueIndex] - : createDummyTrackOutput(id, type); - } - audioSampleQueueMappingDone = true; - sampleQueueTrackIds[audioSampleQueueIndex] = id; - return sampleQueues[audioSampleQueueIndex]; - } else if (tracksEnded) { - return createDummyTrackOutput(id, type); - } - } else if (type == C.TRACK_TYPE_VIDEO) { - if (videoSampleQueueIndex != C.INDEX_UNSET) { - if (videoSampleQueueMappingDone) { - return sampleQueueTrackIds[videoSampleQueueIndex] == id - ? sampleQueues[videoSampleQueueIndex] - : createDummyTrackOutput(id, type); - } - videoSampleQueueMappingDone = true; - sampleQueueTrackIds[videoSampleQueueIndex] = id; - return sampleQueues[videoSampleQueueIndex]; - } else if (tracksEnded) { - return createDummyTrackOutput(id, type); - } - } else /* sparse track */ { - for (int i = 0; i < trackCount; i++) { + @Nullable TrackOutput trackOutput = null; + if (MAPPABLE_TYPES.contains(type)) { + // Track types in MAPPABLE_TYPES are handled manually to ignore IDs. + trackOutput = getMappedTrackOutput(id, type); + } else /* non-mappable type track */ { + for (int i = 0; i < sampleQueues.length; i++) { if (sampleQueueTrackIds[i] == id) { - return sampleQueues[i]; + trackOutput = sampleQueues[i]; + break; } } + } + + if (trackOutput == null) { if (tracksEnded) { return createDummyTrackOutput(id, type); + } else { + // The relevant SampleQueue hasn't been constructed yet - so construct it. + trackOutput = createSampleQueue(id, type); + } + } + + if (type == C.TRACK_TYPE_METADATA) { + if (emsgUnwrappingTrackOutput == null) { + emsgUnwrappingTrackOutput = new EmsgUnwrappingTrackOutput(trackOutput, metadataType); } + return emsgUnwrappingTrackOutput; + } + return trackOutput; + } + + /** + * Returns the {@link TrackOutput} for the provided {@code type} and {@code id}, or null if none + * has been created yet. + * + *

If a {@link SampleQueue} for {@code type} has been created and is mapped, but it has a + * different ID, then return a {@link DummyTrackOutput} that does nothing. + * + *

If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to + * this {@code id} and return it. This situation can happen after a call to {@link #init} with + * {@code reusingExtractor=false}. + * + * @param id The ID of the track. + * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}. + * @return The the mapped {@link TrackOutput}, or null if it's not been created yet. + */ + @Nullable + private TrackOutput getMappedTrackOutput(int id, int type) { + Assertions.checkArgument(MAPPABLE_TYPES.contains(type)); + int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET); + if (sampleQueueIndex == C.INDEX_UNSET) { + return null; + } + + if (sampleQueueMappingDoneByType.add(type)) { + sampleQueueTrackIds[sampleQueueIndex] = id; } + return sampleQueueTrackIds[sampleQueueIndex] == id + ? sampleQueues[sampleQueueIndex] + : createDummyTrackOutput(id, type); + } + + private SampleQueue createSampleQueue(int id, int type) { + int trackCount = sampleQueues.length; + SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); @@ -832,16 +873,11 @@ public TrackOutput track(int id, int type) { sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); - sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO - || type == C.TRACK_TYPE_VIDEO; + sampleQueueIsAudioVideoFlags[trackCount] = + type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; - if (type == C.TRACK_TYPE_AUDIO) { - audioSampleQueueMappingDone = true; - audioSampleQueueIndex = trackCount; - } else if (type == C.TRACK_TYPE_VIDEO) { - videoSampleQueueMappingDone = true; - videoSampleQueueIndex = trackCount; - } + sampleQueueMappingDoneByType.add(type); + sampleQueueIndicesByType.append(type, trackCount); if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) { primarySampleQueueIndex = trackCount; primarySampleQueueType = type; @@ -1213,4 +1249,141 @@ private Metadata getAdjustedMetadata(@Nullable Metadata metadata) { return new Metadata(newMetadataEntries); } } + + private static class EmsgUnwrappingTrackOutput implements TrackOutput { + + private static final String TAG = "EmsgUnwrappingTrackOutput"; + + // TODO(ibaker): Create a Formats util class with common constants like this. + private static final Format ID3_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + private static final Format EMSG_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + + private final EventMessageDecoder emsgDecoder; + private final TrackOutput delegate; + private final Format delegateFormat; + @MonotonicNonNull private Format format; + + private byte[] buffer; + private int bufferPosition; + + public EmsgUnwrappingTrackOutput(TrackOutput delegate, @HlsMetadataType int metadataType) { + this.emsgDecoder = new EventMessageDecoder(); + this.delegate = delegate; + switch (metadataType) { + case HlsMetadataType.ID3: + delegateFormat = ID3_FORMAT; + break; + case HlsMetadataType.EMSG: + delegateFormat = EMSG_FORMAT; + break; + default: + throw new IllegalArgumentException("Unknown metadataType: " + metadataType); + } + + this.buffer = new byte[0]; + this.bufferPosition = 0; + } + + @Override + public void format(Format format) { + this.format = format; + delegate.format(delegateFormat); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + ensureBufferCapacity(bufferPosition + length); + int numBytesRead = input.read(buffer, bufferPosition, length); + if (numBytesRead == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } else { + throw new EOFException(); + } + } + bufferPosition += numBytesRead; + return numBytesRead; + } + + @Override + public void sampleData(ParsableByteArray buffer, int length) { + ensureBufferCapacity(bufferPosition + length); + buffer.readBytes(this.buffer, bufferPosition, length); + bufferPosition += length; + } + + @Override + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { + Assertions.checkState(format != null); + ParsableByteArray sample = getSampleAndTrimBuffer(size, offset); + ParsableByteArray sampleForDelegate; + if (Util.areEqual(format.sampleMimeType, delegateFormat.sampleMimeType)) { + // Incoming format matches delegate track's format, so pass straight through. + sampleForDelegate = sample; + } else if (MimeTypes.APPLICATION_EMSG.equals(format.sampleMimeType)) { + // Incoming sample is EMSG, and delegate track is not expecting EMSG, so try unwrapping. + EventMessage emsg = emsgDecoder.decode(sample); + if (!emsgContainsExpectedWrappedFormat(emsg)) { + Log.w( + TAG, + String.format( + "Ignoring EMSG. Expected it to contain wrapped %s but actual wrapped format: %s", + delegateFormat.sampleMimeType, emsg.getWrappedMetadataFormat())); + return; + } + sampleForDelegate = + new ParsableByteArray(Assertions.checkNotNull(emsg.getWrappedMetadataBytes())); + } else { + Log.w(TAG, "Ignoring sample for unsupported format: " + format.sampleMimeType); + return; + } + + int sampleSize = sampleForDelegate.bytesLeft(); + + delegate.sampleData(sampleForDelegate, sampleSize); + delegate.sampleMetadata(timeUs, flags, sampleSize, offset, cryptoData); + } + + private boolean emsgContainsExpectedWrappedFormat(EventMessage emsg) { + @Nullable Format wrappedMetadataFormat = emsg.getWrappedMetadataFormat(); + return wrappedMetadataFormat != null + && Util.areEqual(delegateFormat.sampleMimeType, wrappedMetadataFormat.sampleMimeType); + } + + private void ensureBufferCapacity(int requiredLength) { + if (buffer.length < requiredLength) { + buffer = Arrays.copyOf(buffer, requiredLength + requiredLength / 2); + } + } + + /** + * Removes a complete sample from the {@link #buffer} field & reshuffles the tail data skipped + * by {@code offset} to the head of the array. + * + * @param size see {@code size} param of {@link #sampleMetadata}. + * @param offset see {@code offset} param of {@link #sampleMetadata}. + * @return A {@link ParsableByteArray} containing the sample removed from {@link #buffer}. + */ + private ParsableByteArray getSampleAndTrimBuffer(int size, int offset) { + int sampleEnd = bufferPosition - offset; + int sampleStart = sampleEnd - size; + + byte[] sampleBytes = Arrays.copyOfRange(buffer, sampleStart, sampleEnd); + ParsableByteArray sample = new ParsableByteArray(sampleBytes); + + System.arraycopy(buffer, sampleEnd, buffer, 0, offset); + bufferPosition = offset; + return sample; + } + } } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index 1789b8a4df5..d0a1e593f4c 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -91,6 +91,7 @@ public void getSteamKeys_isCompatibleWithHlsMasterPlaylistFilter() { mock(Allocator.class), mock(CompositeSequenceableLoaderFactory.class), /* allowChunklessPreparation =*/ true, + HlsMetadataType.ID3, /* useSessionKeys= */ false); }; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 5c70203788f..718ac5e552d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -98,19 +98,19 @@ *

  • {@code scrubber_color} - Color for the scrubber handle. *
      *
    • Corresponding method: {@link #setScrubberColor(int)} - *
    • Default: see {@link #getDefaultScrubberColor(int)} + *
    • Default: {@link #DEFAULT_SCRUBBER_COLOR} *
    *
  • {@code buffered_color} - Color for the portion of the time bar after the current * played position up to the current buffered position. *
      *
    • Corresponding method: {@link #setBufferedColor(int)} - *
    • Default: see {@link #getDefaultBufferedColor(int)} + *
    • Default: {@link #DEFAULT_BUFFERED_COLOR} *
    *
  • {@code unplayed_color} - Color for the portion of the time bar after the current * buffered position. *
      *
    • Corresponding method: {@link #setUnplayedColor(int)} - *
    • Default: see {@link #getDefaultUnplayedColor(int)} + *
    • Default: {@link #DEFAULT_UNPLAYED_COLOR} *
    *
  • {@code ad_marker_color} - Color for unplayed ad markers. *
      @@ -120,7 +120,7 @@ *
    • {@code played_ad_marker_color} - Color for played ad markers. *
        *
      • Corresponding method: {@link #setPlayedAdMarkerColor(int)} - *
      • Default: see {@link #getDefaultPlayedAdMarkerColor(int)} + *
      • Default: {@link #DEFAULT_PLAYED_AD_MARKER_COLOR} *
      *
    */ @@ -154,10 +154,16 @@ public class DefaultTimeBar extends View implements TimeBar { * Default color for the played portion of the time bar. */ public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; - /** - * Default color for ad markers. - */ + /** Default color for the played portion of the time bar. */ + public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF; + /** Default color for the buffered portion of the time bar. */ + public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; + /** Default color for the scrubber handle. */ + public static final int DEFAULT_SCRUBBER_COLOR = 0xFFFFFFFF; + /** Default color for ad markers. */ public static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; + /** Default color for played ad markers. */ + public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00; /** * The threshold in dps above the bar at which touch events trigger fine scrub mode. @@ -289,16 +295,17 @@ public DefaultTimeBar( scrubberDraggedSize = a.getDimensionPixelSize( R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); - int scrubberColor = a.getInt(R.styleable.DefaultTimeBar_scrubber_color, - getDefaultScrubberColor(playedColor)); - int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, - getDefaultBufferedColor(playedColor)); - int unplayedColor = a.getInt(R.styleable.DefaultTimeBar_unplayed_color, - getDefaultUnplayedColor(playedColor)); + int scrubberColor = + a.getInt(R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR); + int bufferedColor = + a.getInt(R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR); + int unplayedColor = + a.getInt(R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR); int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR); - int playedAdMarkerColor = a.getInt(R.styleable.DefaultTimeBar_played_ad_marker_color, - getDefaultPlayedAdMarkerColor(adMarkerColor)); + int playedAdMarkerColor = + a.getInt( + R.styleable.DefaultTimeBar_played_ad_marker_color, DEFAULT_PLAYED_AD_MARKER_COLOR); playedPaint.setColor(playedColor); scrubberPaint.setColor(scrubberColor); bufferedPaint.setColor(bufferedColor); @@ -316,10 +323,11 @@ public DefaultTimeBar( scrubberDisabledSize = defaultScrubberDisabledSize; scrubberDraggedSize = defaultScrubberDraggedSize; playedPaint.setColor(DEFAULT_PLAYED_COLOR); - scrubberPaint.setColor(getDefaultScrubberColor(DEFAULT_PLAYED_COLOR)); - bufferedPaint.setColor(getDefaultBufferedColor(DEFAULT_PLAYED_COLOR)); - unplayedPaint.setColor(getDefaultUnplayedColor(DEFAULT_PLAYED_COLOR)); + scrubberPaint.setColor(DEFAULT_SCRUBBER_COLOR); + bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); + unplayedPaint.setColor(DEFAULT_UNPLAYED_COLOR); adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); + playedAdMarkerPaint.setColor(DEFAULT_PLAYED_AD_MARKER_COLOR); scrubberDrawable = null; } formatBuilder = new StringBuilder(); @@ -856,22 +864,6 @@ private static boolean setDrawableLayoutDirection(Drawable drawable, int layoutD return Util.SDK_INT >= 23 && drawable.setLayoutDirection(layoutDirection); } - public static int getDefaultScrubberColor(int playedColor) { - return 0xFF000000 | playedColor; - } - - public static int getDefaultUnplayedColor(int playedColor) { - return 0x33000000 | (playedColor & 0x00FFFFFF); - } - - public static int getDefaultBufferedColor(int playedColor) { - return 0xCC000000 | (playedColor & 0x00FFFFFF); - } - - public static int getDefaultPlayedAdMarkerColor(int adMarkerColor) { - return 0x33000000 | (adMarkerColor & 0x00FFFFFF); - } - private static int dpToPx(float density, int dps) { return (int) (dps * density + 0.5f); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 358dd14576b..85411d5c5bb 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -748,14 +748,14 @@ private void updatePlayPauseButton() { return; } boolean requestPlayPauseFocus = false; - boolean playing = isPlaying(); + boolean shouldShowPauseButton = shouldShowPauseButton(); if (playButton != null) { - requestPlayPauseFocus |= playing && playButton.isFocused(); - playButton.setVisibility(playing ? GONE : VISIBLE); + requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused(); + playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE); } if (pauseButton != null) { - requestPlayPauseFocus |= !playing && pauseButton.isFocused(); - pauseButton.setVisibility(!playing ? GONE : VISIBLE); + requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused(); + pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE); } if (requestPlayPauseFocus) { requestPlayPauseFocus(); @@ -943,7 +943,7 @@ private void updateProgress() { // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); - if (playbackState == Player.STATE_READY && player.getPlayWhenReady()) { + if (player != null && player.isPlaying()) { long mediaTimeDelayMs = timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS; @@ -965,10 +965,10 @@ private void updateProgress() { } private void requestPlayPauseFocus() { - boolean playing = isPlaying(); - if (!playing && playButton != null) { + boolean shouldShowPauseButton = shouldShowPauseButton(); + if (!shouldShowPauseButton && playButton != null) { playButton.requestFocus(); - } else if (playing && pauseButton != null) { + } else if (shouldShowPauseButton && pauseButton != null) { pauseButton.requestFocus(); } } @@ -995,7 +995,7 @@ private void previous(Player player) { || (window.isDynamic && !window.isSeekable))) { seekTo(player, previousWindowIndex, C.TIME_UNSET); } else { - seekTo(player, 0); + seekTo(player, windowIndex, /* positionMs= */ 0); } } @@ -1015,27 +1015,24 @@ private void next(Player player) { private void rewind(Player player) { if (player.isCurrentWindowSeekable() && rewindMs > 0) { - seekTo(player, player.getCurrentPosition() - rewindMs); + seekToOffset(player, -rewindMs); } } private void fastForward(Player player) { if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { - seekTo(player, player.getCurrentPosition() + fastForwardMs); + seekToOffset(player, fastForwardMs); } } - private void seekTo(Player player, long positionMs) { - seekTo(player, player.getCurrentWindowIndex(), positionMs); - } - - private boolean seekTo(Player player, int windowIndex, long positionMs) { + private void seekToOffset(Player player, long offsetMs) { + long positionMs = player.getCurrentPosition() + offsetMs; long durationMs = player.getDuration(); if (durationMs != C.TIME_UNSET) { positionMs = Math.min(positionMs, durationMs); } positionMs = Math.max(positionMs, 0); - return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); + seekTo(player, player.getCurrentWindowIndex(), positionMs); } private void seekToTimeBarPosition(Player player, long positionMs) { @@ -1067,6 +1064,10 @@ private void seekToTimeBarPosition(Player player, long positionMs) { } } + private boolean seekTo(Player player, int windowIndex, long positionMs) { + return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); + } + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); @@ -1149,7 +1150,7 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { return true; } - private boolean isPlaying() { + private boolean shouldShowPauseButton() { return player != null && player.getPlaybackState() != Player.STATE_ENDED && player.getPlaybackState() != Player.STATE_IDLE @@ -1219,6 +1220,11 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { updateProgress(); } + @Override + public void onIsPlayingChanged(boolean isPlaying) { + updateProgress(); + } + @Override public void onRepeatModeChanged(int repeatMode) { updateRepeatModeButton(); @@ -1264,7 +1270,7 @@ public void onClick(View view) { playbackPreparer.preparePlayback(); } } else if (player.getPlaybackState() == Player.STATE_ENDED) { - controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } controlDispatcher.dispatchSetPlayWhenReady(player, true); } else if (pauseButton == view) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 7fa4b603141..8b20557d3d5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -382,8 +382,6 @@ public void onBitmap(final Bitmap bitmap) { private int visibility; @Priority private int priority; private boolean useChronometer; - private boolean wasPlayWhenReady; - private int lastPlaybackState; /** * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, @@ -663,8 +661,6 @@ public final void setPlayer(@Nullable Player player) { } this.player = player; if (player != null) { - wasPlayWhenReady = player.getPlayWhenReady(); - lastPlaybackState = player.getPlaybackState(); player.addListener(playerListener); startOrUpdateNotification(); } @@ -1070,10 +1066,9 @@ protected NotificationCompat.Builder createNotification( // Changing "showWhen" causes notification flicker if SDK_INT < 21. if (Util.SDK_INT >= 21 && useChronometer + && player.isPlaying() && !player.isPlayingAd() - && !player.isCurrentWindowDynamic() - && player.getPlayWhenReady() - && player.getPlaybackState() == Player.STATE_READY) { + && !player.isCurrentWindowDynamic()) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) @@ -1138,7 +1133,7 @@ protected List getActions(Player player) { stringActions.add(ACTION_REWIND); } if (usePlayPauseActions) { - if (isPlaying(player)) { + if (shouldShowPauseButton(player)) { stringActions.add(ACTION_PAUSE); } else { stringActions.add(ACTION_PLAY); @@ -1182,10 +1177,10 @@ protected int[] getActionIndicesForCompactView(List actionNames, Player if (skipPreviousActionIndex != -1) { actionIndices[actionCounter++] = skipPreviousActionIndex; } - boolean isPlaying = isPlaying(player); - if (pauseActionIndex != -1 && isPlaying) { + boolean shouldShowPauseButton = shouldShowPauseButton(player); + if (pauseActionIndex != -1 && shouldShowPauseButton) { actionIndices[actionCounter++] = pauseActionIndex; - } else if (playActionIndex != -1 && !isPlaying) { + } else if (playActionIndex != -1 && !shouldShowPauseButton) { actionIndices[actionCounter++] = playActionIndex; } if (skipNextActionIndex != -1) { @@ -1214,7 +1209,7 @@ private void previous(Player player) { || (window.isDynamic && !window.isSeekable))) { seekTo(player, previousWindowIndex, C.TIME_UNSET); } else { - seekTo(player, 0); + seekTo(player, windowIndex, /* positionMs= */ 0); } } @@ -1234,30 +1229,31 @@ private void next(Player player) { private void rewind(Player player) { if (player.isCurrentWindowSeekable() && rewindMs > 0) { - seekTo(player, Math.max(player.getCurrentPosition() - rewindMs, 0)); + seekToOffset(player, /* offsetMs= */ -rewindMs); } } private void fastForward(Player player) { if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { - seekTo(player, player.getCurrentPosition() + fastForwardMs); + seekToOffset(player, /* offsetMs= */ fastForwardMs); } } - private void seekTo(Player player, long positionMs) { + private void seekToOffset(Player player, long offsetMs) { + long positionMs = player.getCurrentPosition() + offsetMs; + long durationMs = player.getDuration(); + if (durationMs != C.TIME_UNSET) { + positionMs = Math.min(positionMs, durationMs); + } + positionMs = Math.max(positionMs, 0); seekTo(player, player.getCurrentWindowIndex(), positionMs); } private void seekTo(Player player, int windowIndex, long positionMs) { - long duration = player.getDuration(); - if (duration != C.TIME_UNSET) { - positionMs = Math.min(positionMs, duration); - } - positionMs = Math.max(positionMs, 0); controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); } - private boolean isPlaying(Player player) { + private boolean shouldShowPauseButton(Player player) { return player.getPlaybackState() != Player.STATE_ENDED && player.getPlaybackState() != Player.STATE_IDLE && player.getPlayWhenReady(); @@ -1328,11 +1324,12 @@ private class PlayerListener implements Player.EventListener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) { - startOrUpdateNotification(); - wasPlayWhenReady = playWhenReady; - lastPlaybackState = playbackState; - } + startOrUpdateNotification(); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + startOrUpdateNotification(); } @Override @@ -1373,7 +1370,7 @@ public void onReceive(Context context, Intent intent) { playbackPreparer.preparePlayback(); } } else if (player.getPlaybackState() == Player.STATE_ENDED) { - controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } else if (ACTION_PAUSE.equals(action)) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 9f6fdc9d499..ab7c5be5b2b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -52,7 +52,7 @@ public final Factory setIsNetwork(boolean isNetwork) { } @Override - public DataSource createDataSource() { + public FakeDataSource createDataSource() { return new FakeDataSource(fakeDataSet, isNetwork); } }