Commit 41b3fc11 by Oliver Woodman Committed by GitHub

Merge pull request #6542 from google/dev-v2-r2.10.6

r2.10.6
parents 176d211b 2671ff0c
Showing with 752 additions and 281 deletions
...@@ -71,7 +71,3 @@ extensions/cronet/jniLibs/* ...@@ -71,7 +71,3 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app
...@@ -12,13 +12,14 @@ libs ...@@ -12,13 +12,14 @@ libs
obj obj
lint.xml lint.xml
# IntelliJ IDEA # IntelliJ IDEA & Android Studio
.idea .idea
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
classes classes
gen-external-apklibs gen-external-apklibs
*.li
# Eclipse # Eclipse
.project .project
...@@ -75,7 +76,3 @@ extensions/cronet/jniLibs/* ...@@ -75,7 +76,3 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app
...@@ -107,6 +107,7 @@ branch: ...@@ -107,6 +107,7 @@ branch:
```sh ```sh
git clone https://github.com/google/ExoPlayer.git git clone https://github.com/google/ExoPlayer.git
cd ExoPlayer
git checkout release-v2 git checkout release-v2
``` ```
......
# Release notes # # 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) ### ### 2.10.5 (2019-09-20) ###
* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether * Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether
......
...@@ -17,9 +17,9 @@ buildscript { ...@@ -17,9 +17,9 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.4.0' classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.novoda:bintray-release:0.9' classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
} }
} }
allprojects { allprojects {
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.10.5' releaseVersion = '2.10.6'
releaseVersionCode = 2010005 releaseVersionCode = 2010006
minSdkVersion = 16 minSdkVersion = 16
targetSdkVersion = 28 targetSdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
......
...@@ -21,6 +21,7 @@ import androidx.annotation.Nullable; ...@@ -21,6 +21,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer; import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
...@@ -67,6 +68,10 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -67,6 +68,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
*/ */
public final class CastPlayer extends BasePlayer { public final class CastPlayer extends BasePlayer {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.cast");
}
private static final String TAG = "CastPlayer"; private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3; private static final int RENDERER_COUNT = 3;
......
...@@ -25,8 +25,7 @@ follows: ...@@ -25,8 +25,7 @@ follows:
``` ```
cd "<path to exoplayer checkout>" cd "<path to exoplayer checkout>"
EXOPLAYER_ROOT="$(pwd)" FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
``` ```
* Download the [Android NDK][] and set its location in an environment variable. * Download the [Android NDK][] and set its location in an environment variable.
...@@ -69,7 +68,7 @@ COMMON_OPTIONS="\ ...@@ -69,7 +68,7 @@ COMMON_OPTIONS="\
--enable-decoder=opus \ --enable-decoder=opus \
--enable-decoder=flac \ --enable-decoder=flac \
" && \ " && \
cd "${FFMPEG_EXT_PATH}/jni" && \ cd "${FFMPEG_EXT_PATH}" && \
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && git checkout release/4.0 && \ cd ffmpeg && git checkout release/4.0 && \
./configure \ ./configure \
...@@ -112,7 +111,7 @@ make clean ...@@ -112,7 +111,7 @@ make clean
built in the previous step. For example: 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 ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
``` ```
......
...@@ -19,6 +19,8 @@ import androidx.annotation.Nullable; ...@@ -19,6 +19,8 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; 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.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
...@@ -216,15 +218,25 @@ import java.nio.ByteBuffer; ...@@ -216,15 +218,25 @@ import java.nio.ByteBuffer;
} }
/** /**
* 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. * stream.
* *
* @param timeUs A seek position in microseconds. * @param timeUs A seek position in microseconds.
* @return The corresponding position (byte offset) in the flac stream or -1 if the stream doesn't * @return The corresponding {@link SeekMap.SeekPoints} obtained from the seek table, or {@code
* have a seek table. * null} if the stream doesn't have a seek table.
*/ */
public long getSeekPosition(long timeUs) { @Nullable
return flacGetSeekPosition(nativeDecoderContext, timeUs); 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() { public String getStateString() {
...@@ -283,7 +295,7 @@ import java.nio.ByteBuffer; ...@@ -283,7 +295,7 @@ import java.nio.ByteBuffer;
private native long flacGetNextFrameFirstSampleIndex(long context); 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); private native String flacGetStateString(long context);
......
...@@ -276,10 +276,10 @@ public final class FlacExtractor implements Extractor { ...@@ -276,10 +276,10 @@ public final class FlacExtractor implements Extractor {
FlacStreamMetadata streamMetadata, FlacStreamMetadata streamMetadata,
long streamLength, long streamLength,
ExtractorOutput output) { ExtractorOutput output) {
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null;
FlacBinarySearchSeeker binarySearchSeeker = null; FlacBinarySearchSeeker binarySearchSeeker = null;
SeekMap seekMap; SeekMap seekMap;
if (hasSeekTable) { if (haveSeekTable) {
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
} else if (streamLength != C.LENGTH_UNSET) { } else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition(); long firstFramePosition = decoderJni.getDecodePosition();
...@@ -341,8 +341,8 @@ public final class FlacExtractor implements Extractor { ...@@ -341,8 +341,8 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public SeekPoints getSeekPoints(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
// TODO: Access the seek table via JNI to return two seek points when appropriate. @Nullable SeekPoints seekPoints = decoderJni.getSeekPoints(timeUs);
return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs))); return seekPoints == null ? new SeekPoints(SeekPoint.START) : seekPoints;
} }
@Override @Override
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
#include <android/log.h> #include <android/log.h>
#include <jni.h> #include <jni.h>
#include <array>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
...@@ -46,7 +47,6 @@ class JavaDataSource : public DataSource { ...@@ -46,7 +47,6 @@ class JavaDataSource : public DataSource {
if (mid == NULL) { if (mid == NULL) {
jclass cls = env->GetObjectClass(flacDecoderJni); jclass cls = env->GetObjectClass(flacDecoderJni);
mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I"); mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I");
env->DeleteLocalRef(cls);
} }
} }
...@@ -57,7 +57,6 @@ class JavaDataSource : public DataSource { ...@@ -57,7 +57,6 @@ class JavaDataSource : public DataSource {
// Exception is thrown in Java when returning from the native call. // Exception is thrown in Java when returning from the native call.
result = -1; result = -1;
} }
env->DeleteLocalRef(byteBuffer);
return result; return result;
} }
...@@ -200,9 +199,15 @@ DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) { ...@@ -200,9 +199,15 @@ DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
return context->parser->getNextFrameFirstSampleIndex(); 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<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getSeekPosition(timeUs); std::array<int64_t, 4> 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) { DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
......
...@@ -438,22 +438,41 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { ...@@ -438,22 +438,41 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) {
return bufferSize; return bufferSize;
} }
int64_t FLACParser::getSeekPosition(int64_t timeUs) { bool FLACParser::getSeekPositions(int64_t timeUs,
std::array<int64_t, 4> &result) {
if (!mSeekTable) { if (!mSeekTable) {
return -1; return false;
} }
int64_t sample = (timeUs * getSampleRate()) / 1000000LL; unsigned sampleRate = getSampleRate();
if (sample >= getTotalSamples()) { int64_t totalSamples = getTotalSamples();
sample = getTotalSamples(); int64_t targetSampleNumber = (timeUs * sampleRate) / 1000000LL;
if (targetSampleNumber >= totalSamples) {
targetSampleNumber = totalSamples - 1;
} }
FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points;
for (unsigned i = mSeekTable->num_points; i > 0; ) { unsigned length = mSeekTable->num_points;
i--;
if (points[i].sample_number <= sample) { for (unsigned i = length; i != 0; i--) {
return firstFrameOffset + points[i].stream_offset; 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;
} }
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
#include <stdint.h> #include <stdint.h>
#include <array>
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
#include <vector> #include <vector>
...@@ -82,7 +83,7 @@ class FLACParser { ...@@ -82,7 +83,7 @@ class FLACParser {
bool decodeMetadata(); bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size); size_t readBuffer(void *output, size_t output_size);
int64_t getSeekPosition(int64_t timeUs); bool getSeekPositions(int64_t timeUs, std::array<int64_t, 4> &result);
void flush() { void flush() {
reset(mCurrentPos); reset(mCurrentPos);
......
...@@ -868,26 +868,27 @@ public final class MediaSessionConnector { ...@@ -868,26 +868,27 @@ public final class MediaSessionConnector {
private void rewind(Player player) { private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) { if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs); seekToOffset(player, /* offsetMs= */ -rewindMs);
} }
} }
private void fastForward(Player player) { private void fastForward(Player player) {
if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { 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) {
seekTo(player, player.getCurrentWindowIndex(), positionMs); long positionMs = player.getCurrentPosition() + offsetMs;
}
private void seekTo(Player player, int windowIndex, long positionMs) {
long durationMs = player.getDuration(); long durationMs = player.getDuration();
if (durationMs != C.TIME_UNSET) { if (durationMs != C.TIME_UNSET) {
positionMs = Math.min(positionMs, durationMs); positionMs = Math.min(positionMs, durationMs);
} }
positionMs = Math.max(positionMs, 0); positionMs = Math.max(positionMs, 0);
seekTo(player, player.getCurrentWindowIndex(), positionMs);
}
private void seekTo(Player player, int windowIndex, long positionMs) {
controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
} }
...@@ -1096,7 +1097,7 @@ public final class MediaSessionConnector { ...@@ -1096,7 +1097,7 @@ public final class MediaSessionConnector {
playbackPreparer.onPrepare(/* playWhenReady= */ true); playbackPreparer.onPrepare(/* playWhenReady= */ true);
} }
} else if (player.getPlaybackState() == Player.STATE_ENDED) { } else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
} }
controlDispatcher.dispatchSetPlayWhenReady( controlDispatcher.dispatchSetPlayWhenReady(
Assertions.checkNotNull(player), /* playWhenReady= */ true); Assertions.checkNotNull(player), /* playWhenReady= */ true);
...@@ -1113,7 +1114,7 @@ public final class MediaSessionConnector { ...@@ -1113,7 +1114,7 @@ public final class MediaSessionConnector {
@Override @Override
public void onSeekTo(long positionMs) { public void onSeekTo(long positionMs) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {
seekTo(player, positionMs); seekTo(player, player.getCurrentWindowIndex(), positionMs);
} }
} }
......
...@@ -37,7 +37,7 @@ public final class RtmpDataSourceFactory implements DataSource.Factory { ...@@ -37,7 +37,7 @@ public final class RtmpDataSourceFactory implements DataSource.Factory {
} }
@Override @Override
public DataSource createDataSource() { public RtmpDataSource createDataSource() {
RtmpDataSource dataSource = new RtmpDataSource(); RtmpDataSource dataSource = new RtmpDataSource();
if (listener != null) { if (listener != null) {
dataSource.addTransferListener(listener); dataSource.addTransferListener(listener);
......
#Thu Apr 25 13:15:25 BST 2019 #Mon Oct 07 17:24:00 BST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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
...@@ -260,17 +260,21 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -260,17 +260,21 @@ import java.util.concurrent.CopyOnWriteArrayList;
internalPlayer.setPlayWhenReady(internalPlayWhenReady); internalPlayer.setPlayWhenReady(internalPlayWhenReady);
} }
boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; boolean playWhenReadyChanged = this.playWhenReady != playWhenReady;
boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason;
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
this.playbackSuppressionReason = playbackSuppressionReason; this.playbackSuppressionReason = playbackSuppressionReason;
boolean isPlaying = isPlaying(); boolean isPlaying = isPlaying();
boolean isPlayingChanged = oldIsPlaying != isPlaying; boolean isPlayingChanged = oldIsPlaying != isPlaying;
if (playWhenReadyChanged || isPlayingChanged) { if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) {
int playbackState = playbackInfo.playbackState; int playbackState = playbackInfo.playbackState;
notifyListeners( notifyListeners(
listener -> { listener -> {
if (playWhenReadyChanged) { if (playWhenReadyChanged) {
listener.onPlayerStateChanged(playWhenReady, playbackState); listener.onPlayerStateChanged(playWhenReady, playbackState);
} }
if (suppressionReasonChanged) {
listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
}
if (isPlayingChanged) { if (isPlayingChanged) {
listener.onIsPlayingChanged(isPlaying); listener.onIsPlayingChanged(isPlaying);
} }
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.10.5"; public static final String VERSION = "2.10.6";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.5"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.6";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2010005; public static final int VERSION_INT = 2010006;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -1066,6 +1066,38 @@ public final class Format implements Parcelable { ...@@ -1066,6 +1066,38 @@ public final class Format implements Parcelable {
accessibilityChannel); 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( public Format copyWithContainerInfo(
@Nullable String id, @Nullable String id,
@Nullable String label, @Nullable String label,
......
...@@ -366,6 +366,14 @@ public interface Player { ...@@ -366,6 +366,14 @@ public interface Player {
default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} 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. * Called when the value of {@link #isPlaying()} changes.
* *
* @param isPlaying Whether the player is playing. * @param isPlaying Whether the player is playing.
...@@ -470,18 +478,21 @@ public interface Player { ...@@ -470,18 +478,21 @@ public interface Player {
int STATE_ENDED = 4; int STATE_ENDED = 4;
/** /**
* Reason why playback is suppressed even if {@link #getPlaybackState()} is {@link #STATE_READY} * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
* and {@link #getPlayWhenReady()} is {@code true}. One of {@link * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link
* #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link #PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS}. * #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @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 {} @interface PlaybackSuppressionReason {}
/** Playback is not suppressed. */ /** Playback is not suppressed. */
int PLAYBACK_SUPPRESSION_REASON_NONE = 0; int PLAYBACK_SUPPRESSION_REASON_NONE = 0;
/** Playback is suppressed because audio focus is lost or can't be acquired. */ /** Playback is suppressed due to transient audio focus loss. */
int PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS = 1; 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 * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
...@@ -609,13 +620,10 @@ public interface Player { ...@@ -609,13 +620,10 @@ public interface Player {
int getPlaybackState(); int getPlaybackState();
/** /**
* Returns reason why playback is suppressed even if {@link #getPlaybackState()} is {@link * Returns the reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code
* #STATE_READY} and {@link #getPlayWhenReady()} is {@code true}. * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
*
* <p>Note that {@link #PLAYBACK_SUPPRESSION_REASON_NONE} indicates that playback is not
* suppressed.
* *
* @return The current {@link PlaybackSuppressionReason}. * @return The current {@link PlaybackSuppressionReason playback suppression reason}.
*/ */
@PlaybackSuppressionReason @PlaybackSuppressionReason
int getPlaybackSuppressionReason(); int getPlaybackSuppressionReason();
......
...@@ -1228,13 +1228,13 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1228,13 +1228,13 @@ public class SimpleExoPlayer extends BasePlayer
private void updatePlayWhenReady( private void updatePlayWhenReady(
boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
@PlaybackSuppressionReason
int playbackSuppressionReason = int playbackSuppressionReason =
playerCommand == AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY
? Player.PLAYBACK_SUPPRESSION_REASON_NONE ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS
: Player.PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS; : Player.PLAYBACK_SUPPRESSION_REASON_NONE;
player.setPlayWhenReady( player.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY,
playbackSuppressionReason);
} }
private void verifyApplicationThread() { private void verifyApplicationThread() {
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; 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;
import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
...@@ -472,6 +473,23 @@ public class AnalyticsCollector ...@@ -472,6 +473,23 @@ public class AnalyticsCollector
} }
@Override @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) { public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format; ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
...@@ -133,6 +134,23 @@ public interface AnalyticsListener { ...@@ -133,6 +134,23 @@ public interface AnalyticsListener {
EventTime eventTime, boolean playWhenReady, int playbackState) {} 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. * Called when the timeline changed.
* *
* @param eventTime The event time. * @param eventTime The event time.
......
...@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Assertions;
public interface SeekMap { public interface SeekMap {
/** A {@link SeekMap} that does not support seeking. */ /** A {@link SeekMap} that does not support seeking. */
final class Unseekable implements SeekMap { class Unseekable implements SeekMap {
private final long durationUs; private final long durationUs;
private final SeekPoints startSeekPoints; private final SeekPoints startSeekPoints;
......
...@@ -22,8 +22,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader; ...@@ -22,8 +22,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader;
/** /**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
*/ */
/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap /* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker {
implements Mp3Extractor.Seeker {
/** /**
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
......
...@@ -22,7 +22,7 @@ import com.google.android.exoplayer2.metadata.id3.MlltFrame; ...@@ -22,7 +22,7 @@ import com.google.android.exoplayer2.metadata.id3.MlltFrame;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** MP3 seeker that uses metadata from an {@link MlltFrame}. */ /** 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. * Returns an {@link MlltSeeker} for seeking in the stream.
......
...@@ -28,8 +28,8 @@ import com.google.android.exoplayer2.extractor.GaplessInfoHolder; ...@@ -28,8 +28,8 @@ import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.Id3Peeker;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder; 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.TrackOutput;
import com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
...@@ -114,7 +114,8 @@ public final class Mp3Extractor implements Extractor { ...@@ -114,7 +114,8 @@ public final class Mp3Extractor implements Extractor {
private int synchronizedHeaderData; private int synchronizedHeaderData;
private Metadata metadata; private Metadata metadata;
private Seeker seeker; @Nullable private Seeker seeker;
private boolean disableSeeking;
private long basisTimeUs; private long basisTimeUs;
private long samplesRead; private long samplesRead;
private long firstSamplePosition; private long firstSamplePosition;
...@@ -188,6 +189,10 @@ public final class Mp3Extractor implements Extractor { ...@@ -188,6 +189,10 @@ public final class Mp3Extractor implements Extractor {
// takes priority as it can provide greater precision. // takes priority as it can provide greater precision.
Seeker seekFrameSeeker = maybeReadSeekFrame(input); Seeker seekFrameSeeker = maybeReadSeekFrame(input);
Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition());
if (disableSeeking) {
seeker = new UnseekableSeeker();
} else {
if (metadataSeeker != null) { if (metadataSeeker != null) {
seeker = metadataSeeker; seeker = metadataSeeker;
} else if (seekFrameSeeker != null) { } else if (seekFrameSeeker != null) {
...@@ -197,6 +202,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -197,6 +202,7 @@ public final class Mp3Extractor implements Extractor {
|| (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
seeker = getConstantBitrateSeeker(input); seeker = getConstantBitrateSeeker(input);
} }
}
extractorOutput.seekMap(seeker); extractorOutput.seekMap(seeker);
trackOutput.format( trackOutput.format(
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
...@@ -226,6 +232,15 @@ public final class Mp3Extractor implements Extractor { ...@@ -226,6 +232,15 @@ public final class Mp3Extractor implements Extractor {
return readSample(input); return readSample(input);
} }
/**
* Disables the extractor from being able to seek through the media.
*
* <p>Please note that this needs to be called before {@link #read}.
*/
public void disableSeeking() {
disableSeeking = true;
}
// Internal methods. // Internal methods.
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
...@@ -464,26 +479,5 @@ public final class Mp3Extractor implements Extractor { ...@@ -464,26 +479,5 @@ public final class Mp3Extractor implements Extractor {
return null; 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();
}
} }
/*
* 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;
}
}
}
...@@ -23,10 +23,8 @@ import com.google.android.exoplayer2.util.Log; ...@@ -23,10 +23,8 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /** MP3 seeker that uses metadata from a VBRI header. */
* MP3 seeker that uses metadata from a VBRI header. /* package */ final class VbriSeeker implements Seeker {
*/
/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "VbriSeeker"; private static final String TAG = "VbriSeeker";
......
...@@ -24,10 +24,8 @@ import com.google.android.exoplayer2.util.Log; ...@@ -24,10 +24,8 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /** MP3 seeker that uses metadata from a Xing header. */
* MP3 seeker that uses metadata from a Xing header. /* package */ final class XingSeeker implements Seeker {
*/
/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "XingSeeker"; private static final String TAG = "XingSeeker";
......
...@@ -377,18 +377,13 @@ public final class MediaCodecInfo { ...@@ -377,18 +377,13 @@ public final class MediaCodecInfo {
@TargetApi(21) @TargetApi(21)
public Point alignVideoSizeV21(int width, int height) { public Point alignVideoSizeV21(int width, int height) {
if (capabilities == null) { if (capabilities == null) {
logNoSupport("align.caps");
return null; return null;
} }
VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
if (videoCapabilities == null) { if (videoCapabilities == null) {
logNoSupport("align.vCaps");
return null; return null;
} }
int widthAlignment = videoCapabilities.getWidthAlignment(); return alignVideoSizeV21(videoCapabilities, width, height);
int heightAlignment = videoCapabilities.getHeightAlignment();
return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment,
Util.ceilDivide(height, heightAlignment) * heightAlignment);
} }
/** /**
...@@ -519,6 +514,11 @@ public final class MediaCodecInfo { ...@@ -519,6 +514,11 @@ public final class MediaCodecInfo {
@TargetApi(21) @TargetApi(21)
private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width,
int height, double frameRate) { 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) { if (frameRate == Format.NO_VALUE || frameRate <= 0) {
return capabilities.isSizeSupported(width, height); return capabilities.isSizeSupported(width, height);
} else { } else {
...@@ -530,6 +530,15 @@ public final class MediaCodecInfo { ...@@ -530,6 +530,15 @@ public final class MediaCodecInfo {
} }
} }
@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) @TargetApi(23)
private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) {
return capabilities.getMaxSupportedInstances(); return capabilities.getMaxSupportedInstances();
......
...@@ -277,13 +277,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -277,13 +277,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2;
/** /**
* H.264/AVC buffer to queue when using the adaptation workaround (see * H.264/AVC buffer to queue when using the adaptation workaround (see {@link
* {@link #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: * #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: Baseline
* Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be * sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be queued to
* queued to force a resolution change when adapting to a new format. * force a resolution change when adapting to a new format.
*/ */
private static final byte[] ADAPTATION_WORKAROUND_BUFFER = Util.getBytesFromHexString( private static final byte[] ADAPTATION_WORKAROUND_BUFFER =
"0000016742C00BDA259000000168CE0F13200000016588840DCE7118A0002FBF1C31C3275D78"); 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 static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32;
private final MediaCodecSelector mediaCodecSelector; private final MediaCodecSelector mediaCodecSelector;
......
...@@ -15,12 +15,10 @@ ...@@ -15,12 +15,10 @@
*/ */
package com.google.android.exoplayer2.metadata.icy; package com.google.android.exoplayer2.metadata.icy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.regex.Matcher; import java.util.regex.Matcher;
...@@ -36,7 +34,6 @@ public final class IcyDecoder implements MetadataDecoder { ...@@ -36,7 +34,6 @@ public final class IcyDecoder implements MetadataDecoder {
private static final String STREAM_KEY_URL = "streamurl"; private static final String STREAM_KEY_URL = "streamurl";
@Override @Override
@Nullable
@SuppressWarnings("ByteBufferBackingArray") @SuppressWarnings("ByteBufferBackingArray")
public Metadata decode(MetadataInputBuffer inputBuffer) { public Metadata decode(MetadataInputBuffer inputBuffer) {
ByteBuffer buffer = inputBuffer.data; ByteBuffer buffer = inputBuffer.data;
...@@ -45,7 +42,6 @@ public final class IcyDecoder implements MetadataDecoder { ...@@ -45,7 +42,6 @@ public final class IcyDecoder implements MetadataDecoder {
return decode(Util.fromUtf8Bytes(data, 0, length)); return decode(Util.fromUtf8Bytes(data, 0, length));
} }
@Nullable
@VisibleForTesting @VisibleForTesting
/* package */ Metadata decode(String metadata) { /* package */ Metadata decode(String metadata) {
String name = null; String name = null;
...@@ -62,12 +58,9 @@ public final class IcyDecoder implements MetadataDecoder { ...@@ -62,12 +58,9 @@ public final class IcyDecoder implements MetadataDecoder {
case STREAM_KEY_URL: case STREAM_KEY_URL:
url = value; url = value;
break; break;
default:
Log.w(TAG, "Unrecognized ICY tag: " + name);
break;
} }
index = matcher.end(); index = matcher.end();
} }
return (name != null || url != null) ? new Metadata(new IcyInfo(name, url)) : null; return new Metadata(new IcyInfo(metadata, name, url));
} }
} }
...@@ -19,26 +19,35 @@ import android.os.Parcel; ...@@ -19,26 +19,35 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** ICY in-stream information. */ /** ICY in-stream information. */
public final class IcyInfo implements Metadata.Entry { 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}. */ /** The stream title if present, or {@code null}. */
@Nullable public final String title; @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; @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 title See {@link #title}.
* @param url See {@link #url}. * @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.title = title;
this.url = url; this.url = url;
} }
/* package */ IcyInfo(Parcel in) { /* package */ IcyInfo(Parcel in) {
rawMetadata = Assertions.checkNotNull(in.readString());
title = in.readString(); title = in.readString();
url = in.readString(); url = in.readString();
} }
...@@ -52,26 +61,27 @@ public final class IcyInfo implements Metadata.Entry { ...@@ -52,26 +61,27 @@ public final class IcyInfo implements Metadata.Entry {
return false; return false;
} }
IcyInfo other = (IcyInfo) obj; 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 @Override
public int hashCode() { public int hashCode() {
int result = 17; // title & url are derived from rawMetadata, so no need to include them in the hash.
result = 31 * result + (title != null ? title.hashCode() : 0); return rawMetadata.hashCode();
result = 31 * result + (url != null ? url.hashCode() : 0);
return result;
} }
@Override @Override
public String toString() { public String toString() {
return "ICY: title=\"" + title + "\", url=\"" + url + "\""; return String.format(
"ICY: title=\"%s\", url=\"%s\", rawMetadata=\"%s\"", title, url, rawMetadata);
} }
// Parcelable implementation. // Parcelable implementation.
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeString(rawMetadata);
dest.writeString(title); dest.writeString(title);
dest.writeString(url); dest.writeString(url);
} }
......
...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekMap; ...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
import com.google.android.exoplayer2.extractor.SeekMap.Unseekable; import com.google.android.exoplayer2.extractor.SeekMap.Unseekable;
import com.google.android.exoplayer2.extractor.TrackOutput; 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.Metadata;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
...@@ -949,6 +950,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -949,6 +950,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
input = new DefaultExtractorInput(extractorDataSource, position, length); input = new DefaultExtractorInput(extractorDataSource, position, length);
Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri); 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) { if (pendingExtractorSeek) {
extractor.seek(position, seekTimeUs); extractor.seek(position, seekTimeUs);
pendingExtractorSeek = false; pendingExtractorSeek = false;
......
...@@ -308,7 +308,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { ...@@ -308,7 +308,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; 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_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_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 float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;
public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000;
......
...@@ -33,7 +33,7 @@ public final class FileDataSourceFactory implements DataSource.Factory { ...@@ -33,7 +33,7 @@ public final class FileDataSourceFactory implements DataSource.Factory {
} }
@Override @Override
public DataSource createDataSource() { public FileDataSource createDataSource() {
FileDataSource dataSource = new FileDataSource(); FileDataSource dataSource = new FileDataSource();
if (listener != null) { if (listener != null) {
dataSource.addTransferListener(listener); dataSource.addTransferListener(listener);
......
...@@ -64,9 +64,7 @@ public final class ResolvingDataSource implements DataSource { ...@@ -64,9 +64,7 @@ public final class ResolvingDataSource implements DataSource {
private final Resolver resolver; private final Resolver resolver;
/** /**
* Creates factory for {@link ResolvingDataSource} instances. * @param upstreamFactory The wrapped {@link DataSource.Factory} for handling resolved {@link
*
* @param upstreamFactory The wrapped {@link DataSource.Factory} handling the resolved {@link
* DataSpec DataSpecs}. * DataSpec DataSpecs}.
* @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}.
*/ */
...@@ -76,7 +74,7 @@ public final class ResolvingDataSource implements DataSource { ...@@ -76,7 +74,7 @@ public final class ResolvingDataSource implements DataSource {
} }
@Override @Override
public DataSource createDataSource() { public ResolvingDataSource createDataSource() {
return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver); return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver);
} }
} }
......
...@@ -53,7 +53,6 @@ public final class CacheDataSink implements DataSink { ...@@ -53,7 +53,6 @@ public final class CacheDataSink implements DataSink {
private long dataSpecFragmentSize; private long dataSpecFragmentSize;
private File file; private File file;
private OutputStream outputStream; private OutputStream outputStream;
private FileOutputStream underlyingFileOutputStream;
private long outputStreamBytesWritten; private long outputStreamBytesWritten;
private long dataSpecBytesWritten; private long dataSpecBytesWritten;
private ReusableBufferedOutputStream bufferedOutputStream; private ReusableBufferedOutputStream bufferedOutputStream;
...@@ -171,7 +170,7 @@ public final class CacheDataSink implements DataSink { ...@@ -171,7 +170,7 @@ public final class CacheDataSink implements DataSink {
file = file =
cache.startFile( cache.startFile(
dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length); dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length);
underlyingFileOutputStream = new FileOutputStream(file); FileOutputStream underlyingFileOutputStream = new FileOutputStream(file);
if (bufferSize > 0) { if (bufferSize > 0) {
if (bufferedOutputStream == null) { if (bufferedOutputStream == null) {
bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream, bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream,
......
...@@ -49,10 +49,11 @@ public final class AesCipherDataSink implements DataSink { ...@@ -49,10 +49,11 @@ public final class AesCipherDataSink implements DataSink {
* *
* @param secretKey The key data. * @param secretKey The key data.
* @param wrappedDataSink The wrapped {@link DataSink}. * @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 * 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 * 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) { public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) {
this.wrappedDataSink = wrappedDataSink; this.wrappedDataSink = wrappedDataSink;
...@@ -91,5 +92,4 @@ public final class AesCipherDataSink implements DataSink { ...@@ -91,5 +92,4 @@ public final class AesCipherDataSink implements DataSink {
cipher = null; cipher = null;
wrappedDataSink.close(); wrappedDataSink.close();
} }
} }
...@@ -42,7 +42,7 @@ public final class GlUtil { ...@@ -42,7 +42,7 @@ public final class GlUtil {
int lastError = GLES20.GL_NO_ERROR; int lastError = GLES20.GL_NO_ERROR;
int error; int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, "glError " + gluErrorString(lastError)); Log.e(TAG, "glError " + gluErrorString(error));
lastError = error; lastError = error;
} }
if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) { if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) {
......
...@@ -63,6 +63,7 @@ import java.util.Arrays; ...@@ -63,6 +63,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
...@@ -2659,6 +2660,34 @@ public final class ExoPlayerTest { ...@@ -2659,6 +2660,34 @@ public final class ExoPlayerTest {
assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); 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. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
...@@ -29,10 +29,12 @@ public final class IcyDecoderTest { ...@@ -29,10 +29,12 @@ public final class IcyDecoderTest {
@Test @Test
public void decode() { public void decode() {
IcyDecoder decoder = new IcyDecoder(); 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); assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0); IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);
assertThat(streamInfo.title).isEqualTo("test title"); assertThat(streamInfo.title).isEqualTo("test title");
assertThat(streamInfo.url).isEqualTo("test_url"); assertThat(streamInfo.url).isEqualTo("test_url");
} }
...@@ -40,21 +42,39 @@ public final class IcyDecoderTest { ...@@ -40,21 +42,39 @@ public final class IcyDecoderTest {
@Test @Test
public void decode_titleOnly() { public void decode_titleOnly() {
IcyDecoder decoder = new IcyDecoder(); 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); assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0); IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);
assertThat(streamInfo.title).isEqualTo("test title"); assertThat(streamInfo.title).isEqualTo("test title");
assertThat(streamInfo.url).isNull(); assertThat(streamInfo.url).isNull();
} }
@Test @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() { public void decode_emptyTitle() {
IcyDecoder decoder = new IcyDecoder(); 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); assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0); IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);
assertThat(streamInfo.title).isEmpty(); assertThat(streamInfo.title).isEmpty();
assertThat(streamInfo.url).isEqualTo("test_url"); assertThat(streamInfo.url).isEqualTo("test_url");
} }
...@@ -62,10 +82,12 @@ public final class IcyDecoderTest { ...@@ -62,10 +82,12 @@ public final class IcyDecoderTest {
@Test @Test
public void decode_semiColonInTitle() { public void decode_semiColonInTitle() {
IcyDecoder decoder = new IcyDecoder(); 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); assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0); IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);
assertThat(streamInfo.title).isEqualTo("test; title"); assertThat(streamInfo.title).isEqualTo("test; title");
assertThat(streamInfo.url).isEqualTo("test_url"); assertThat(streamInfo.url).isEqualTo("test_url");
} }
...@@ -73,10 +95,12 @@ public final class IcyDecoderTest { ...@@ -73,10 +95,12 @@ public final class IcyDecoderTest {
@Test @Test
public void decode_quoteInTitle() { public void decode_quoteInTitle() {
IcyDecoder decoder = new IcyDecoder(); 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); assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0); IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);
assertThat(streamInfo.title).isEqualTo("test' title"); assertThat(streamInfo.title).isEqualTo("test' title");
assertThat(streamInfo.url).isEqualTo("test_url"); assertThat(streamInfo.url).isEqualTo("test_url");
} }
...@@ -84,19 +108,25 @@ public final class IcyDecoderTest { ...@@ -84,19 +108,25 @@ public final class IcyDecoderTest {
@Test @Test
public void decode_lineTerminatorInTitle() { public void decode_lineTerminatorInTitle() {
IcyDecoder decoder = new IcyDecoder(); 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); assertThat(metadata.length()).isEqualTo(1);
IcyInfo streamInfo = (IcyInfo) metadata.get(0); IcyInfo streamInfo = (IcyInfo) metadata.get(0);
assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);
assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); assertThat(streamInfo.title).isEqualTo("test\r\ntitle");
assertThat(streamInfo.url).isEqualTo("test_url"); assertThat(streamInfo.url).isEqualTo("test_url");
} }
@Test @Test
public void decode_notIcy() { public void decode_noReconisedHeaders() {
IcyDecoder decoder = new IcyDecoder(); IcyDecoder decoder = new IcyDecoder();
Metadata metadata = decoder.decode("NotIcyData"); 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();
} }
} }
...@@ -24,11 +24,11 @@ import org.junit.runner.RunWith; ...@@ -24,11 +24,11 @@ import org.junit.runner.RunWith;
/** Test for {@link IcyInfo}. */ /** Test for {@link IcyInfo}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class IcyStreamInfoTest { public final class IcyInfoTest {
@Test @Test
public void parcelEquals() { public void parcelEquals() {
IcyInfo streamInfo = new IcyInfo("name", "url"); IcyInfo streamInfo = new IcyInfo("StreamName='name';StreamUrl='url'", "name", "url");
// Write to parcel. // Write to parcel.
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
streamInfo.writeToParcel(parcel, 0); streamInfo.writeToParcel(parcel, 0);
......
...@@ -686,7 +686,9 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -686,7 +686,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex); 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 = long oldIndexEndTimeUs =
oldIndex.getTimeUs(oldIndexLastSegmentNum) oldIndex.getTimeUs(oldIndexLastSegmentNum)
+ oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs); + oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs);
...@@ -700,8 +702,14 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -700,8 +702,14 @@ public class DefaultDashChunkSource implements DashChunkSource {
// There's a gap between the old index and the new one which means we've slipped behind the // 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. // live window and can't proceed.
throw new BehindLiveWindowException(); 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 { } 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 += newSegmentNumShift +=
oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs) oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs)
- newIndexFirstSegmentNum; - newIndexFirstSegmentNum;
......
...@@ -313,7 +313,6 @@ public class DashManifestParser extends DefaultHandler ...@@ -313,7 +313,6 @@ public class DashManifestParser extends DefaultHandler
parseRepresentation( parseRepresentation(
xpp, xpp,
baseUrl, baseUrl,
label,
mimeType, mimeType,
codecs, codecs,
width, width,
...@@ -338,6 +337,8 @@ public class DashManifestParser extends DefaultHandler ...@@ -338,6 +337,8 @@ public class DashManifestParser extends DefaultHandler
parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties); parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties);
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp, "Label")) {
label = parseLabel(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp)) { } else if (XmlPullParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp); parseAdaptationSetChild(xpp);
} }
...@@ -348,7 +349,11 @@ public class DashManifestParser extends DefaultHandler ...@@ -348,7 +349,11 @@ public class DashManifestParser extends DefaultHandler
for (int i = 0; i < representationInfos.size(); i++) { for (int i = 0; i < representationInfos.size(); i++) {
representations.add( representations.add(
buildRepresentation( buildRepresentation(
representationInfos.get(i), drmSchemeType, drmSchemeDatas, inbandEventStreams)); representationInfos.get(i),
label,
drmSchemeType,
drmSchemeDatas,
inbandEventStreams));
} }
return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors,
...@@ -484,7 +489,6 @@ public class DashManifestParser extends DefaultHandler ...@@ -484,7 +489,6 @@ public class DashManifestParser extends DefaultHandler
protected RepresentationInfo parseRepresentation( protected RepresentationInfo parseRepresentation(
XmlPullParser xpp, XmlPullParser xpp,
String baseUrl, String baseUrl,
String label,
String adaptationSetMimeType, String adaptationSetMimeType,
String adaptationSetCodecs, String adaptationSetCodecs,
int adaptationSetWidth, int adaptationSetWidth,
...@@ -551,7 +555,6 @@ public class DashManifestParser extends DefaultHandler ...@@ -551,7 +555,6 @@ public class DashManifestParser extends DefaultHandler
Format format = Format format =
buildFormat( buildFormat(
id, id,
label,
mimeType, mimeType,
width, width,
height, height,
...@@ -572,7 +575,6 @@ public class DashManifestParser extends DefaultHandler ...@@ -572,7 +575,6 @@ public class DashManifestParser extends DefaultHandler
protected Format buildFormat( protected Format buildFormat(
String id, String id,
String label,
String containerMimeType, String containerMimeType,
int width, int width,
int height, int height,
...@@ -596,7 +598,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -596,7 +598,7 @@ public class DashManifestParser extends DefaultHandler
if (MimeTypes.isVideo(sampleMimeType)) { if (MimeTypes.isVideo(sampleMimeType)) {
return Format.createVideoContainerFormat( return Format.createVideoContainerFormat(
id, id,
label, /* label= */ null,
containerMimeType, containerMimeType,
sampleMimeType, sampleMimeType,
codecs, codecs,
...@@ -611,7 +613,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -611,7 +613,7 @@ public class DashManifestParser extends DefaultHandler
} else if (MimeTypes.isAudio(sampleMimeType)) { } else if (MimeTypes.isAudio(sampleMimeType)) {
return Format.createAudioContainerFormat( return Format.createAudioContainerFormat(
id, id,
label, /* label= */ null,
containerMimeType, containerMimeType,
sampleMimeType, sampleMimeType,
codecs, codecs,
...@@ -634,7 +636,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -634,7 +636,7 @@ public class DashManifestParser extends DefaultHandler
} }
return Format.createTextContainerFormat( return Format.createTextContainerFormat(
id, id,
label, /* label= */ null,
containerMimeType, containerMimeType,
sampleMimeType, sampleMimeType,
codecs, codecs,
...@@ -647,7 +649,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -647,7 +649,7 @@ public class DashManifestParser extends DefaultHandler
} }
return Format.createContainerFormat( return Format.createContainerFormat(
id, id,
label, /* label= */ null,
containerMimeType, containerMimeType,
sampleMimeType, sampleMimeType,
codecs, codecs,
...@@ -659,10 +661,14 @@ public class DashManifestParser extends DefaultHandler ...@@ -659,10 +661,14 @@ public class DashManifestParser extends DefaultHandler
protected Representation buildRepresentation( protected Representation buildRepresentation(
RepresentationInfo representationInfo, RepresentationInfo representationInfo,
String label,
String extraDrmSchemeType, String extraDrmSchemeType,
ArrayList<SchemeData> extraDrmSchemeDatas, ArrayList<SchemeData> extraDrmSchemeDatas,
ArrayList<Descriptor> extraInbandEventStreams) { ArrayList<Descriptor> extraInbandEventStreams) {
Format format = representationInfo.format; Format format = representationInfo.format;
if (label != null) {
format = format.copyWithLabel(label);
}
String drmSchemeType = representationInfo.drmSchemeType != null String drmSchemeType = representationInfo.drmSchemeType != null
? representationInfo.drmSchemeType : extraDrmSchemeType; ? representationInfo.drmSchemeType : extraDrmSchemeType;
ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas; ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;
...@@ -1076,15 +1082,44 @@ public class DashManifestParser extends DefaultHandler ...@@ -1076,15 +1082,44 @@ public class DashManifestParser extends DefaultHandler
return new ProgramInformation(title, source, copyright, moreInformationURL, lang); 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. // AudioChannelConfiguration parsing.
protected int parseAudioChannelConfiguration(XmlPullParser xpp) protected int parseAudioChannelConfiguration(XmlPullParser xpp)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String schemeIdUri = parseString(xpp, "schemeIdUri", null); String schemeIdUri = parseString(xpp, "schemeIdUri", null);
int audioChannels = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) int audioChannels =
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri)
? parseInt(xpp, "value", Format.NO_VALUE) ? parseInt(xpp, "value", Format.NO_VALUE)
: ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri)
? parseDolbyChannelConfiguration(xpp) : Format.NO_VALUE); || "urn:dolby:dash:audio_channel_configuration:2011".equals(schemeIdUri)
? parseDolbyChannelConfiguration(xpp)
: Format.NO_VALUE);
do { do {
xpp.next(); xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
...@@ -1427,10 +1462,18 @@ public class DashManifestParser extends DefaultHandler ...@@ -1427,10 +1462,18 @@ public class DashManifestParser extends DefaultHandler
} }
} }
protected static String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) protected static String parseText(XmlPullParser xpp, String label)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String text = "";
do {
xpp.next(); xpp.next();
return UriUtil.resolve(parentBaseUrl, xpp.getText()); 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) { protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
...@@ -1451,7 +1494,8 @@ public class DashManifestParser extends DefaultHandler ...@@ -1451,7 +1494,8 @@ public class DashManifestParser extends DefaultHandler
/** /**
* Parses the number of channels from the value attribute of an AudioElementConfiguration with * 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 * 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. * @param xpp The parser from which to read.
* @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
......
...@@ -102,4 +102,3 @@ http://www.test.com/vtt ...@@ -102,4 +102,3 @@ http://www.test.com/vtt
</AdaptationSet> </AdaptationSet>
</Period> </Period>
</MPD> </MPD>
...@@ -44,4 +44,3 @@ ...@@ -44,4 +44,3 @@
</AdaptationSet> </AdaptationSet>
</Period> </Period>
</MPD> </MPD>
<?xml version="1.0" encoding="UTF-8"?>
<MPD type="static" duration="1s" mediaPresentationDuration="PT1S">
<Period>
<SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
<SegmentTimeline>
<S d="1000"/>
</SegmentTimeline>
</SegmentTemplate>
<AdaptationSet id="0" mimeType="audio/mp4" subsegmentAlignment="true" label="audio label">
<Representation id="0" codecs="mp4a.40.2" audioSamplingRate="48000" bandwidth="144000">
<BaseURL>https://test.com/0</BaseURL>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true" label="ignored label">
<Representation id="1" codecs="avc1.4d4015" width="426" height="240" bandwidth="258000">
<BaseURL>https://test.com/1</BaseURL>
</Representation>
<Label>video label</Label>
</AdaptationSet>
</Period>
</MPD>
...@@ -35,4 +35,3 @@ ...@@ -35,4 +35,3 @@
</AdaptationSet> </AdaptationSet>
</Period> </Period>
</MPD> </MPD>
...@@ -115,4 +115,3 @@ http://www.test.com/vtt ...@@ -115,4 +115,3 @@ http://www.test.com/vtt
</AdaptationSet> </AdaptationSet>
</Period> </Period>
</MPD> </MPD>
...@@ -26,42 +26,49 @@ import com.google.android.exoplayer2.metadata.emsg.EventMessage; ...@@ -26,42 +26,49 @@ import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
/** Unit tests for {@link DashManifestParser}. */ /** Unit tests for {@link DashManifestParser}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DashManifestParserTest { public class DashManifestParserTest {
private static final String SAMPLE_MPD_1 = "sample_mpd_1"; private static final String SAMPLE_MPD = "sample_mpd";
private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type"; private static final String SAMPLE_MPD_UNKNOWN_MIME_TYPE = "sample_mpd_unknown_mime_type";
private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template"; private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = "sample_mpd_segment_template";
private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream"; 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. */ /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */
@Test @Test
public void testParseMediaPresentationDescription() throws IOException { public void parseMediaPresentationDescription() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
parser.parse( parser.parse(
Uri.parse("https://example.com/test.mpd"), Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_1)); TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD));
parser.parse( parser.parse(
Uri.parse("https://example.com/test.mpd"), Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream( TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); ApplicationProvider.getApplicationContext(), SAMPLE_MPD_UNKNOWN_MIME_TYPE));
} }
@Test @Test
public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException { public void parseMediaPresentationDescription_segmentTemplate() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
DashManifest mpd = DashManifest mpd =
parser.parse( parser.parse(
Uri.parse("https://example.com/test.mpd"), Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream( TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_3_SEGMENT_TEMPLATE)); ApplicationProvider.getApplicationContext(), SAMPLE_MPD_SEGMENT_TEMPLATE));
assertThat(mpd.getPeriodCount()).isEqualTo(1); assertThat(mpd.getPeriodCount()).isEqualTo(1);
...@@ -87,13 +94,13 @@ public class DashManifestParserTest { ...@@ -87,13 +94,13 @@ public class DashManifestParserTest {
} }
@Test @Test
public void testParseMediaPresentationDescriptionCanParseEventStream() throws IOException { public void parseMediaPresentationDescription_eventStream() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
DashManifest mpd = DashManifest mpd =
parser.parse( parser.parse(
Uri.parse("https://example.com/test.mpd"), Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream( TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_4_EVENT_STREAM)); ApplicationProvider.getApplicationContext(), SAMPLE_MPD_EVENT_STREAM));
Period period = mpd.getPeriod(0); Period period = mpd.getPeriod(0);
assertThat(period.eventStreams).hasSize(3); assertThat(period.eventStreams).hasSize(3);
...@@ -157,12 +164,12 @@ public class DashManifestParserTest { ...@@ -157,12 +164,12 @@ public class DashManifestParserTest {
} }
@Test @Test
public void testParseMediaPresentationDescriptionCanParseProgramInformation() throws IOException { public void parseMediaPresentationDescription_programInformation() throws IOException {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
DashManifest mpd = DashManifest mpd =
parser.parse( parser.parse(
Uri.parse("Https://example.com/test.mpd"), Uri.parse("Https://example.com/test.mpd"),
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_1)); TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD));
ProgramInformation expectedProgramInformation = ProgramInformation expectedProgramInformation =
new ProgramInformation( new ProgramInformation(
"MediaTitle", "MediaSource", "MediaCopyright", "www.example.com", "enUs"); "MediaTitle", "MediaSource", "MediaCopyright", "www.example.com", "enUs");
...@@ -170,7 +177,46 @@ public class DashManifestParserTest { ...@@ -170,7 +177,46 @@ public class DashManifestParserTest {
} }
@Test @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<AdaptationSet> 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("<Label>test label</Label>" + 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("<Label/>" + NEXT_TAG));
xpp.next();
String label = parser.parseLabel(xpp);
assertThat(label).isEqualTo("");
assertNextTag(xpp);
}
@Test
public void parseCea608AccessibilityChannel() {
assertThat( assertThat(
DashManifestParser.parseCea608AccessibilityChannel( DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("CC1=eng"))) buildCea608AccessibilityDescriptors("CC1=eng")))
...@@ -211,7 +257,7 @@ public class DashManifestParserTest { ...@@ -211,7 +257,7 @@ public class DashManifestParserTest {
} }
@Test @Test
public void testParseCea708AccessibilityChannel() { public void parseCea708AccessibilityChannel() {
assertThat( assertThat(
DashManifestParser.parseCea708AccessibilityChannel( DashManifestParser.parseCea708AccessibilityChannel(
buildCea708AccessibilityDescriptors("1=lang:eng"))) buildCea708AccessibilityDescriptors("1=lang:eng")))
...@@ -262,4 +308,10 @@ public class DashManifestParserTest { ...@@ -262,4 +308,10 @@ public class DashManifestParserTest {
private static List<Descriptor> buildCea708AccessibilityDescriptors(String value) { private static List<Descriptor> buildCea708AccessibilityDescriptors(String value) {
return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null)); return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null));
} }
private static void assertNextTag(XmlPullParser xpp) throws Exception {
xpp.next();
assertThat(xpp.getEventType()).isEqualTo(XmlPullParser.START_TAG);
assertThat(xpp.getName()).isEqualTo(NEXT_TAG_NAME);
}
} }
...@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; ...@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.EOFException; import java.io.EOFException;
...@@ -158,7 +159,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -158,7 +159,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
FragmentedMp4Extractor fragmentedMp4Extractor = FragmentedMp4Extractor fragmentedMp4Extractor =
createFragmentedMp4Extractor(timestampAdjuster, drmInitData, muxedCaptionFormats); createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats);
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
return buildResult(fragmentedMp4Extractor); return buildResult(fragmentedMp4Extractor);
} }
...@@ -208,7 +209,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -208,7 +209,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
|| lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5) || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)
|| lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
return createFragmentedMp4Extractor(timestampAdjuster, drmInitData, muxedCaptionFormats); return createFragmentedMp4Extractor(
timestampAdjuster, format, drmInitData, muxedCaptionFormats);
} else { } else {
// For any other file extension, we assume TS format. // For any other file extension, we assume TS format.
return createTsExtractor( return createTsExtractor(
...@@ -267,10 +269,21 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -267,10 +269,21 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
private static FragmentedMp4Extractor createFragmentedMp4Extractor( private static FragmentedMp4Extractor createFragmentedMp4Extractor(
TimestampAdjuster timestampAdjuster, TimestampAdjuster timestampAdjuster,
Format format,
DrmInitData drmInitData, DrmInitData drmInitData,
@Nullable List<Format> muxedCaptionFormats) { @Nullable List<Format> muxedCaptionFormats) {
boolean isVariant = false;
for (int i = 0; i < format.metadata.length(); i++) {
Metadata.Entry entry = format.metadata.get(i);
if (entry instanceof HlsTrackMetadataEntry) {
isVariant = !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty();
break;
}
}
// Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid
// creating a separate EMSG track for every audio track in a video stream.
return new FragmentedMp4Extractor( return new FragmentedMp4Extractor(
/* flags= */ 0, /* flags= */ isVariant ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,
timestampAdjuster, timestampAdjuster,
/* sideloadedTrack= */ null, /* sideloadedTrack= */ null,
drmInitData, drmInitData,
......
...@@ -71,6 +71,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -71,6 +71,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final TimestampAdjusterProvider timestampAdjusterProvider; private final TimestampAdjusterProvider timestampAdjusterProvider;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final boolean allowChunklessPreparation; private final boolean allowChunklessPreparation;
private final @HlsMetadataType int metadataType;
private final boolean useSessionKeys; private final boolean useSessionKeys;
private @Nullable Callback callback; private @Nullable Callback callback;
...@@ -110,6 +111,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -110,6 +111,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
Allocator allocator, Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
boolean allowChunklessPreparation, boolean allowChunklessPreparation,
@HlsMetadataType int metadataType,
boolean useSessionKeys) { boolean useSessionKeys) {
this.extractorFactory = extractorFactory; this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker; this.playlistTracker = playlistTracker;
...@@ -120,6 +122,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -120,6 +122,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
this.allocator = allocator; this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.allowChunklessPreparation = allowChunklessPreparation; this.allowChunklessPreparation = allowChunklessPreparation;
this.metadataType = metadataType;
this.useSessionKeys = useSessionKeys; this.useSessionKeys = useSessionKeys;
compositeSequenceableLoader = compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
...@@ -736,7 +739,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -736,7 +739,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
positionUs, positionUs,
muxedAudioFormat, muxedAudioFormat,
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
eventDispatcher); eventDispatcher,
metadataType);
} }
private static Map<String, DrmInitData> deriveOverridingDrmInitData( private static Map<String, DrmInitData> deriveOverridingDrmInitData(
......
...@@ -67,6 +67,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -67,6 +67,7 @@ public final class HlsMediaSource extends BaseMediaSource
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation; private boolean allowChunklessPreparation;
@HlsMetadataType private int metadataType;
private boolean useSessionKeys; private boolean useSessionKeys;
private boolean isCreateCalled; private boolean isCreateCalled;
@Nullable private Object tag; @Nullable private Object tag;
...@@ -95,6 +96,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -95,6 +96,7 @@ public final class HlsMediaSource extends BaseMediaSource
extractorFactory = HlsExtractorFactory.DEFAULT; extractorFactory = HlsExtractorFactory.DEFAULT;
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
metadataType = HlsMetadataType.ID3;
} }
/** /**
...@@ -238,6 +240,31 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -238,6 +240,31 @@ public final class HlsMediaSource extends BaseMediaSource
} }
/** /**
* Sets the type of metadata to extract from the HLS source (defaults to {@link
* HlsMetadataType#ID3}).
*
* <p>HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is
* wrapped in an EMSG box [<a href="https://aomediacodec.github.io/av1-id3/">spec</a>].
*
* <p>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.
*
* <p>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 * 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 * assumed that any single session key declared in the master playlist can be used to obtain all
* of the keys required for playback. For media where this is not true, this option should not * of the keys required for playback. For media where this is not true, this option should not
...@@ -272,6 +299,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -272,6 +299,7 @@ public final class HlsMediaSource extends BaseMediaSource
playlistTrackerFactory.createTracker( playlistTrackerFactory.createTracker(
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
allowChunklessPreparation, allowChunklessPreparation,
metadataType,
useSessionKeys, useSessionKeys,
tag); tag);
} }
...@@ -305,6 +333,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -305,6 +333,7 @@ public final class HlsMediaSource extends BaseMediaSource
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final boolean allowChunklessPreparation; private final boolean allowChunklessPreparation;
private final @HlsMetadataType int metadataType;
private final boolean useSessionKeys; private final boolean useSessionKeys;
private final HlsPlaylistTracker playlistTracker; private final HlsPlaylistTracker playlistTracker;
private final @Nullable Object tag; private final @Nullable Object tag;
...@@ -319,6 +348,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -319,6 +348,7 @@ public final class HlsMediaSource extends BaseMediaSource
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
HlsPlaylistTracker playlistTracker, HlsPlaylistTracker playlistTracker,
boolean allowChunklessPreparation, boolean allowChunklessPreparation,
@HlsMetadataType int metadataType,
boolean useSessionKeys, boolean useSessionKeys,
@Nullable Object tag) { @Nullable Object tag) {
this.manifestUri = manifestUri; this.manifestUri = manifestUri;
...@@ -328,6 +358,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -328,6 +358,7 @@ public final class HlsMediaSource extends BaseMediaSource
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.playlistTracker = playlistTracker; this.playlistTracker = playlistTracker;
this.allowChunklessPreparation = allowChunklessPreparation; this.allowChunklessPreparation = allowChunklessPreparation;
this.metadataType = metadataType;
this.useSessionKeys = useSessionKeys; this.useSessionKeys = useSessionKeys;
this.tag = tag; this.tag = tag;
} }
...@@ -363,6 +394,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -363,6 +394,7 @@ public final class HlsMediaSource extends BaseMediaSource
allocator, allocator,
compositeSequenceableLoaderFactory, compositeSequenceableLoaderFactory,
allowChunklessPreparation, allowChunklessPreparation,
metadataType,
useSessionKeys); useSessionKeys);
} }
......
/*
* 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.
*
* <p>See {@link HlsMediaSource.Factory#setMetadataType(int)}.
*/
@Retention(SOURCE)
@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG})
public @interface HlsMetadataType {
int ID3 = 1;
int EMSG = 3;
}
...@@ -91,6 +91,7 @@ public final class HlsMediaPeriodTest { ...@@ -91,6 +91,7 @@ public final class HlsMediaPeriodTest {
mock(Allocator.class), mock(Allocator.class),
mock(CompositeSequenceableLoaderFactory.class), mock(CompositeSequenceableLoaderFactory.class),
/* allowChunklessPreparation =*/ true, /* allowChunklessPreparation =*/ true,
HlsMetadataType.ID3,
/* useSessionKeys= */ false); /* useSessionKeys= */ false);
}; };
......
...@@ -98,19 +98,19 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -98,19 +98,19 @@ import java.util.concurrent.CopyOnWriteArraySet;
* <li><b>{@code scrubber_color}</b> - Color for the scrubber handle. * <li><b>{@code scrubber_color}</b> - Color for the scrubber handle.
* <ul> * <ul>
* <li>Corresponding method: {@link #setScrubberColor(int)} * <li>Corresponding method: {@link #setScrubberColor(int)}
* <li>Default: see {@link #getDefaultScrubberColor(int)} * <li>Default: {@link #DEFAULT_SCRUBBER_COLOR}
* </ul> * </ul>
* <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current * <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current
* played position up to the current buffered position. * played position up to the current buffered position.
* <ul> * <ul>
* <li>Corresponding method: {@link #setBufferedColor(int)} * <li>Corresponding method: {@link #setBufferedColor(int)}
* <li>Default: see {@link #getDefaultBufferedColor(int)} * <li>Default: {@link #DEFAULT_BUFFERED_COLOR}
* </ul> * </ul>
* <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current * <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current
* buffered position. * buffered position.
* <ul> * <ul>
* <li>Corresponding method: {@link #setUnplayedColor(int)} * <li>Corresponding method: {@link #setUnplayedColor(int)}
* <li>Default: see {@link #getDefaultUnplayedColor(int)} * <li>Default: {@link #DEFAULT_UNPLAYED_COLOR}
* </ul> * </ul>
* <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers. * <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers.
* <ul> * <ul>
...@@ -120,7 +120,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -120,7 +120,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
* <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers. * <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers.
* <ul> * <ul>
* <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)} * <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)}
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)} * <li>Default: {@link #DEFAULT_PLAYED_AD_MARKER_COLOR}
* </ul> * </ul>
* </ul> * </ul>
*/ */
...@@ -154,10 +154,16 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -154,10 +154,16 @@ public class DefaultTimeBar extends View implements TimeBar {
* Default color for the played portion of the time bar. * Default color for the played portion of the time bar.
*/ */
public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF;
/** /** Default color for the played portion of the time bar. */
* Default color for ad markers. 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; 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. * The threshold in dps above the bar at which touch events trigger fine scrub mode.
...@@ -289,16 +295,17 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -289,16 +295,17 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberDraggedSize = a.getDimensionPixelSize( scrubberDraggedSize = a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);
int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR);
int scrubberColor = a.getInt(R.styleable.DefaultTimeBar_scrubber_color, int scrubberColor =
getDefaultScrubberColor(playedColor)); a.getInt(R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR);
int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, int bufferedColor =
getDefaultBufferedColor(playedColor)); a.getInt(R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR);
int unplayedColor = a.getInt(R.styleable.DefaultTimeBar_unplayed_color, int unplayedColor =
getDefaultUnplayedColor(playedColor)); a.getInt(R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR);
int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color,
DEFAULT_AD_MARKER_COLOR); DEFAULT_AD_MARKER_COLOR);
int playedAdMarkerColor = a.getInt(R.styleable.DefaultTimeBar_played_ad_marker_color, int playedAdMarkerColor =
getDefaultPlayedAdMarkerColor(adMarkerColor)); a.getInt(
R.styleable.DefaultTimeBar_played_ad_marker_color, DEFAULT_PLAYED_AD_MARKER_COLOR);
playedPaint.setColor(playedColor); playedPaint.setColor(playedColor);
scrubberPaint.setColor(scrubberColor); scrubberPaint.setColor(scrubberColor);
bufferedPaint.setColor(bufferedColor); bufferedPaint.setColor(bufferedColor);
...@@ -316,10 +323,11 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -316,10 +323,11 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberDisabledSize = defaultScrubberDisabledSize; scrubberDisabledSize = defaultScrubberDisabledSize;
scrubberDraggedSize = defaultScrubberDraggedSize; scrubberDraggedSize = defaultScrubberDraggedSize;
playedPaint.setColor(DEFAULT_PLAYED_COLOR); playedPaint.setColor(DEFAULT_PLAYED_COLOR);
scrubberPaint.setColor(getDefaultScrubberColor(DEFAULT_PLAYED_COLOR)); scrubberPaint.setColor(DEFAULT_SCRUBBER_COLOR);
bufferedPaint.setColor(getDefaultBufferedColor(DEFAULT_PLAYED_COLOR)); bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR);
unplayedPaint.setColor(getDefaultUnplayedColor(DEFAULT_PLAYED_COLOR)); unplayedPaint.setColor(DEFAULT_UNPLAYED_COLOR);
adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR);
playedAdMarkerPaint.setColor(DEFAULT_PLAYED_AD_MARKER_COLOR);
scrubberDrawable = null; scrubberDrawable = null;
} }
formatBuilder = new StringBuilder(); formatBuilder = new StringBuilder();
...@@ -856,22 +864,6 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -856,22 +864,6 @@ public class DefaultTimeBar extends View implements TimeBar {
return Util.SDK_INT >= 23 && drawable.setLayoutDirection(layoutDirection); 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) { private static int dpToPx(float density, int dps) {
return (int) (dps * density + 0.5f); return (int) (dps * density + 0.5f);
} }
......
...@@ -748,14 +748,14 @@ public class PlayerControlView extends FrameLayout { ...@@ -748,14 +748,14 @@ public class PlayerControlView extends FrameLayout {
return; return;
} }
boolean requestPlayPauseFocus = false; boolean requestPlayPauseFocus = false;
boolean playing = isPlaying(); boolean shouldShowPauseButton = shouldShowPauseButton();
if (playButton != null) { if (playButton != null) {
requestPlayPauseFocus |= playing && playButton.isFocused(); requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused();
playButton.setVisibility(playing ? GONE : VISIBLE); playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE);
} }
if (pauseButton != null) { if (pauseButton != null) {
requestPlayPauseFocus |= !playing && pauseButton.isFocused(); requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused();
pauseButton.setVisibility(!playing ? GONE : VISIBLE); pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE);
} }
if (requestPlayPauseFocus) { if (requestPlayPauseFocus) {
requestPlayPauseFocus(); requestPlayPauseFocus();
...@@ -943,7 +943,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -943,7 +943,7 @@ public class PlayerControlView extends FrameLayout {
// Cancel any pending updates and schedule a new one if necessary. // Cancel any pending updates and schedule a new one if necessary.
removeCallbacks(updateProgressAction); removeCallbacks(updateProgressAction);
int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
if (playbackState == Player.STATE_READY && player.getPlayWhenReady()) { if (player != null && player.isPlaying()) {
long mediaTimeDelayMs = long mediaTimeDelayMs =
timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS; timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS;
...@@ -965,10 +965,10 @@ public class PlayerControlView extends FrameLayout { ...@@ -965,10 +965,10 @@ public class PlayerControlView extends FrameLayout {
} }
private void requestPlayPauseFocus() { private void requestPlayPauseFocus() {
boolean playing = isPlaying(); boolean shouldShowPauseButton = shouldShowPauseButton();
if (!playing && playButton != null) { if (!shouldShowPauseButton && playButton != null) {
playButton.requestFocus(); playButton.requestFocus();
} else if (playing && pauseButton != null) { } else if (shouldShowPauseButton && pauseButton != null) {
pauseButton.requestFocus(); pauseButton.requestFocus();
} }
} }
...@@ -995,7 +995,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -995,7 +995,7 @@ public class PlayerControlView extends FrameLayout {
|| (window.isDynamic && !window.isSeekable))) { || (window.isDynamic && !window.isSeekable))) {
seekTo(player, previousWindowIndex, C.TIME_UNSET); seekTo(player, previousWindowIndex, C.TIME_UNSET);
} else { } else {
seekTo(player, 0); seekTo(player, windowIndex, /* positionMs= */ 0);
} }
} }
...@@ -1015,27 +1015,24 @@ public class PlayerControlView extends FrameLayout { ...@@ -1015,27 +1015,24 @@ public class PlayerControlView extends FrameLayout {
private void rewind(Player player) { private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) { if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs); seekToOffset(player, -rewindMs);
} }
} }
private void fastForward(Player player) { private void fastForward(Player player) {
if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {
seekTo(player, player.getCurrentPosition() + fastForwardMs); seekToOffset(player, fastForwardMs);
} }
} }
private void seekTo(Player player, long positionMs) { private void seekToOffset(Player player, long offsetMs) {
seekTo(player, player.getCurrentWindowIndex(), positionMs); long positionMs = player.getCurrentPosition() + offsetMs;
}
private boolean seekTo(Player player, int windowIndex, long positionMs) {
long durationMs = player.getDuration(); long durationMs = player.getDuration();
if (durationMs != C.TIME_UNSET) { if (durationMs != C.TIME_UNSET) {
positionMs = Math.min(positionMs, durationMs); positionMs = Math.min(positionMs, durationMs);
} }
positionMs = Math.max(positionMs, 0); positionMs = Math.max(positionMs, 0);
return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); seekTo(player, player.getCurrentWindowIndex(), positionMs);
} }
private void seekToTimeBarPosition(Player player, long positionMs) { private void seekToTimeBarPosition(Player player, long positionMs) {
...@@ -1067,6 +1064,10 @@ public class PlayerControlView extends FrameLayout { ...@@ -1067,6 +1064,10 @@ public class PlayerControlView extends FrameLayout {
} }
} }
private boolean seekTo(Player player, int windowIndex, long positionMs) {
return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
}
@Override @Override
public void onAttachedToWindow() { public void onAttachedToWindow() {
super.onAttachedToWindow(); super.onAttachedToWindow();
...@@ -1149,7 +1150,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -1149,7 +1150,7 @@ public class PlayerControlView extends FrameLayout {
return true; return true;
} }
private boolean isPlaying() { private boolean shouldShowPauseButton() {
return player != null return player != null
&& player.getPlaybackState() != Player.STATE_ENDED && player.getPlaybackState() != Player.STATE_ENDED
&& player.getPlaybackState() != Player.STATE_IDLE && player.getPlaybackState() != Player.STATE_IDLE
...@@ -1220,6 +1221,11 @@ public class PlayerControlView extends FrameLayout { ...@@ -1220,6 +1221,11 @@ public class PlayerControlView extends FrameLayout {
} }
@Override @Override
public void onIsPlayingChanged(boolean isPlaying) {
updateProgress();
}
@Override
public void onRepeatModeChanged(int repeatMode) { public void onRepeatModeChanged(int repeatMode) {
updateRepeatModeButton(); updateRepeatModeButton();
updateNavigation(); updateNavigation();
...@@ -1264,7 +1270,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -1264,7 +1270,7 @@ public class PlayerControlView extends FrameLayout {
playbackPreparer.preparePlayback(); playbackPreparer.preparePlayback();
} }
} else if (player.getPlaybackState() == Player.STATE_ENDED) { } 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); controlDispatcher.dispatchSetPlayWhenReady(player, true);
} else if (pauseButton == view) { } else if (pauseButton == view) {
......
...@@ -382,8 +382,6 @@ public class PlayerNotificationManager { ...@@ -382,8 +382,6 @@ public class PlayerNotificationManager {
private int visibility; private int visibility;
@Priority private int priority; @Priority private int priority;
private boolean useChronometer; private boolean useChronometer;
private boolean wasPlayWhenReady;
private int lastPlaybackState;
/** /**
* @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int,
...@@ -663,8 +661,6 @@ public class PlayerNotificationManager { ...@@ -663,8 +661,6 @@ public class PlayerNotificationManager {
} }
this.player = player; this.player = player;
if (player != null) { if (player != null) {
wasPlayWhenReady = player.getPlayWhenReady();
lastPlaybackState = player.getPlaybackState();
player.addListener(playerListener); player.addListener(playerListener);
startOrUpdateNotification(); startOrUpdateNotification();
} }
...@@ -1070,10 +1066,9 @@ public class PlayerNotificationManager { ...@@ -1070,10 +1066,9 @@ public class PlayerNotificationManager {
// Changing "showWhen" causes notification flicker if SDK_INT < 21. // Changing "showWhen" causes notification flicker if SDK_INT < 21.
if (Util.SDK_INT >= 21 if (Util.SDK_INT >= 21
&& useChronometer && useChronometer
&& player.isPlaying()
&& !player.isPlayingAd() && !player.isPlayingAd()
&& !player.isCurrentWindowDynamic() && !player.isCurrentWindowDynamic()) {
&& player.getPlayWhenReady()
&& player.getPlaybackState() == Player.STATE_READY) {
builder builder
.setWhen(System.currentTimeMillis() - player.getContentPosition()) .setWhen(System.currentTimeMillis() - player.getContentPosition())
.setShowWhen(true) .setShowWhen(true)
...@@ -1138,7 +1133,7 @@ public class PlayerNotificationManager { ...@@ -1138,7 +1133,7 @@ public class PlayerNotificationManager {
stringActions.add(ACTION_REWIND); stringActions.add(ACTION_REWIND);
} }
if (usePlayPauseActions) { if (usePlayPauseActions) {
if (isPlaying(player)) { if (shouldShowPauseButton(player)) {
stringActions.add(ACTION_PAUSE); stringActions.add(ACTION_PAUSE);
} else { } else {
stringActions.add(ACTION_PLAY); stringActions.add(ACTION_PLAY);
...@@ -1182,10 +1177,10 @@ public class PlayerNotificationManager { ...@@ -1182,10 +1177,10 @@ public class PlayerNotificationManager {
if (skipPreviousActionIndex != -1) { if (skipPreviousActionIndex != -1) {
actionIndices[actionCounter++] = skipPreviousActionIndex; actionIndices[actionCounter++] = skipPreviousActionIndex;
} }
boolean isPlaying = isPlaying(player); boolean shouldShowPauseButton = shouldShowPauseButton(player);
if (pauseActionIndex != -1 && isPlaying) { if (pauseActionIndex != -1 && shouldShowPauseButton) {
actionIndices[actionCounter++] = pauseActionIndex; actionIndices[actionCounter++] = pauseActionIndex;
} else if (playActionIndex != -1 && !isPlaying) { } else if (playActionIndex != -1 && !shouldShowPauseButton) {
actionIndices[actionCounter++] = playActionIndex; actionIndices[actionCounter++] = playActionIndex;
} }
if (skipNextActionIndex != -1) { if (skipNextActionIndex != -1) {
...@@ -1214,7 +1209,7 @@ public class PlayerNotificationManager { ...@@ -1214,7 +1209,7 @@ public class PlayerNotificationManager {
|| (window.isDynamic && !window.isSeekable))) { || (window.isDynamic && !window.isSeekable))) {
seekTo(player, previousWindowIndex, C.TIME_UNSET); seekTo(player, previousWindowIndex, C.TIME_UNSET);
} else { } else {
seekTo(player, 0); seekTo(player, windowIndex, /* positionMs= */ 0);
} }
} }
...@@ -1234,30 +1229,31 @@ public class PlayerNotificationManager { ...@@ -1234,30 +1229,31 @@ public class PlayerNotificationManager {
private void rewind(Player player) { private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) { if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, Math.max(player.getCurrentPosition() - rewindMs, 0)); seekToOffset(player, /* offsetMs= */ -rewindMs);
} }
} }
private void fastForward(Player player) { private void fastForward(Player player) {
if (player.isCurrentWindowSeekable() && fastForwardMs > 0) { 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); seekTo(player, player.getCurrentWindowIndex(), positionMs);
} }
private void seekTo(Player player, int windowIndex, long 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); controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
} }
private boolean isPlaying(Player player) { private boolean shouldShowPauseButton(Player player) {
return player.getPlaybackState() != Player.STATE_ENDED return player.getPlaybackState() != Player.STATE_ENDED
&& player.getPlaybackState() != Player.STATE_IDLE && player.getPlaybackState() != Player.STATE_IDLE
&& player.getPlayWhenReady(); && player.getPlayWhenReady();
...@@ -1328,11 +1324,12 @@ public class PlayerNotificationManager { ...@@ -1328,11 +1324,12 @@ public class PlayerNotificationManager {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) {
startOrUpdateNotification(); startOrUpdateNotification();
wasPlayWhenReady = playWhenReady;
lastPlaybackState = playbackState;
} }
@Override
public void onIsPlayingChanged(boolean isPlaying) {
startOrUpdateNotification();
} }
@Override @Override
...@@ -1373,7 +1370,7 @@ public class PlayerNotificationManager { ...@@ -1373,7 +1370,7 @@ public class PlayerNotificationManager {
playbackPreparer.preparePlayback(); playbackPreparer.preparePlayback();
} }
} else if (player.getPlaybackState() == Player.STATE_ENDED) { } 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); controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
} else if (ACTION_PAUSE.equals(action)) { } else if (ACTION_PAUSE.equals(action)) {
......
...@@ -52,7 +52,7 @@ public class FakeDataSource extends BaseDataSource { ...@@ -52,7 +52,7 @@ public class FakeDataSource extends BaseDataSource {
} }
@Override @Override
public DataSource createDataSource() { public FakeDataSource createDataSource() {
return new FakeDataSource(fakeDataSet, isNetwork); return new FakeDataSource(fakeDataSet, isNetwork);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment