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