Merge pull request #2952 from google/dev-v2-r2.4.2

r2.4.2
This commit is contained in:
ojw28 2017-06-14 19:53:04 +01:00 committed by GitHub
commit 3ada4e178d
27 changed files with 369 additions and 237 deletions

View File

@ -1,5 +1,19 @@
# Release notes #
### r2.4.2 ###
* Stability: Work around Nexus 10 reboot when playing certain content
([2806](https://github.com/google/ExoPlayer/issues/2806)).
* MP3: Correctly treat MP3s with INFO headers as constant bitrate
([2895](https://github.com/google/ExoPlayer/issues/2895)).
* HLS: Use average rather than peak bandwidth when available
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
* SmoothStreaming: Fix timeline for live streams
([#2760](https://github.com/google/ExoPlayer/issues/2760)).
* UI: Fix DefaultTimeBar invalidation
([#2871](https://github.com/google/ExoPlayer/issues/2871)).
* Misc bugfixes.
### r2.4.1 ###
* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media

View File

@ -48,7 +48,7 @@ allprojects {
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.4.1'
releaseVersion = 'r2.4.2'
releaseWebsite = 'https://github.com/google/ExoPlayer'
}
if (it.hasProperty('externalBuildDir')) {

View File

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2401"
android:versionName="2.4.1">
android:versionCode="2402"
android:versionName="2.4.2">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

View File

@ -234,6 +234,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Intent intent = getIntent();
boolean needNewPlayer = player == null;
if (needNewPlayer) {
TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
lastSeenTrackGroupArray = null;
eventLogger = new EventLogger(trackSelector);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
@ -261,16 +268,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
drmSessionManager, extensionRendererMode);
TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
lastSeenTrackGroupArray = null;
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
player.addListener(this);
eventLogger = new EventLogger(trackSelector);
player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger);

View File

@ -146,6 +146,7 @@ public class UtilTest extends TestCase {
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-800"));
}
public void testUnescapeInvalidFileName() {

View File

@ -305,10 +305,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty()) {
return 0;
}
long bufferedPosition = getBufferedPosition();
long position = getBufferedPosition();
long duration = getDuration();
return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
: (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100));
}
@Override

View File

@ -24,13 +24,13 @@ public interface 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.
String VERSION = "2.4.1";
String VERSION = "2.4.2";
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
String VERSION_SLASHY = "ExoPlayerLib/2.4.1";
String VERSION_SLASHY = "ExoPlayerLib/2.4.2";
/**
* The version of the library expressed as an integer, for example 1002003.
@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
int VERSION_INT = 2004001;
int VERSION_INT = 2004002;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mkv;
import android.support.annotation.IntDef;
import android.util.Log;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@ -84,6 +85,8 @@ public final class MatroskaExtractor implements Extractor {
*/
public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1;
private static final String TAG = "MatroskaExtractor";
private static final int UNSET_ENTRY_ID = -1;
private static final int BLOCK_STATE_START = 0;
@ -1558,7 +1561,12 @@ public final class MatroskaExtractor implements Extractor {
break;
case CODEC_ID_FOURCC:
initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate));
mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1;
if (initializationData != null) {
mimeType = MimeTypes.VIDEO_VC1;
} else {
Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN);
mimeType = MimeTypes.VIDEO_UNKNOWN;
}
break;
case CODEC_ID_THEORA:
// TODO: This can be set to the real mimeType if/when we work out what initializationData
@ -1614,19 +1622,27 @@ public final class MatroskaExtractor implements Extractor {
break;
case CODEC_ID_ACM:
mimeType = MimeTypes.AUDIO_RAW;
if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {
throw new ParserException("Non-PCM MS/ACM is unsupported");
}
pcmEncoding = Util.getPcmEncoding(audioBitDepth);
if (pcmEncoding == C.ENCODING_INVALID) {
throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {
pcmEncoding = Util.getPcmEncoding(audioBitDepth);
if (pcmEncoding == C.ENCODING_INVALID) {
pcmEncoding = Format.NO_VALUE;
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ mimeType);
}
} else {
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType);
}
break;
case CODEC_ID_PCM_INT_LIT:
mimeType = MimeTypes.AUDIO_RAW;
pcmEncoding = Util.getPcmEncoding(audioBitDepth);
if (pcmEncoding == C.ENCODING_INVALID) {
throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
pcmEncoding = Format.NO_VALUE;
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ mimeType);
}
break;
case CODEC_ID_SUBRIP:

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
/**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
@ -41,8 +42,11 @@ import com.google.android.exoplayer2.C;
@Override
public long getPosition(long timeUs) {
return durationUs == C.TIME_UNSET ? 0
: firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
if (durationUs == C.TIME_UNSET) {
return 0;
}
timeUs = Util.constrainValue(timeUs, 0, durationUs);
return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
}
@Override

View File

@ -78,7 +78,7 @@ public final class Mp3Extractor implements Extractor {
/**
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/
private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
private static final int MAX_SNIFF_BYTES = 16 * 1024;
/**
* Maximum length of data read into {@link #scratch}.
*/
@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor {
/**
* Mask that includes the audio header values that must match between frames.
*/
private static final int HEADER_MASK = 0xFFFE0C00;
private static final int XING_HEADER = Util.getIntegerCodeForString("Xing");
private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00;
private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing");
private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info");
private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI");
private static final int SEEK_HEADER_UNSET = 0;
@Flags private final int flags;
private final long forcedFirstSampleTimestampUs;
@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor {
}
}
if (seeker == null) {
seeker = setupSeeker(input);
seeker = maybeReadSeekFrame(input);
if (seeker == null
|| (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
seeker = getConstantBitrateSeeker(input);
}
extractorOutput.seekMap(seeker);
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor {
}
scratch.setPosition(0);
int sampleHeaderData = scratch.readInt();
if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)
if (!headersMatch(sampleHeaderData, synchronizedHeaderData)
|| MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {
// We have lost synchronization, so attempt to resynchronize starting at the next byte.
extractorInput.skipFully(1);
@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor {
int headerData = scratch.readInt();
int frameSize;
if ((candidateSynchronizedHeaderData != 0
&& (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
&& !headersMatch(headerData, candidateSynchronizedHeaderData))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {
// The header doesn't match the candidate header or is invalid. Try the next byte offset.
if (searchedBytes++ == searchLimitBytes) {
@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor {
}
/**
* Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
* data from the start of the first frame in the stream. On returning, the input's position will
* be set to the start of the first frame of audio.
* Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,
* returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.
* After this method returns, the input position is the start of the first frame of audio.
*
* @param input The {@link ExtractorInput} from which to read.
* @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise.
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
* next two frames were already peeked during synchronization.
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
* the next two frames were already peeked during synchronization.
* @return a {@link Seeker}.
*/
private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
// Read the first frame which may contain a Xing or VBRI header with seeking metadata.
private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException {
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
long position = input.getPosition();
long length = input.getLength();
int headerData = 0;
Seeker seeker = null;
// Check if there is a Xing header.
int xingBase = (synchronizedHeader.version & 1) != 0
? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
: (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
if (frame.limit() >= xingBase + 4) {
frame.setPosition(xingBase);
headerData = frame.readInt();
}
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
int seekHeader = getSeekFrameHeader(frame, xingBase);
Seeker seeker;
if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {
seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition();
@ -377,28 +373,60 @@ public final class Mp3Extractor implements Extractor {
gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
}
input.skipFully(synchronizedHeader.frameSize);
} else if (frame.limit() >= 40) {
// Check if there is a VBRI header.
frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
headerData = frame.readInt();
if (headerData == VBRI_HEADER) {
seeker = VbriSeeker.create(synchronizedHeader, frame, position, length);
input.skipFully(synchronizedHeader.frameSize);
if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) {
// Fall back to constant bitrate seeking for Info headers missing a table of contents.
return getConstantBitrateSeeker(input);
}
} else if (seekHeader == SEEK_HEADER_VBRI) {
seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
input.skipFully(synchronizedHeader.frameSize);
} else { // seekerHeader == SEEK_HEADER_UNSET
// This frame doesn't contain seeking information, so reset the peek position.
seeker = null;
input.resetPeekPosition();
}
return seeker;
}
/**
* Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate.
*/
private Seeker getConstantBitrateSeeker(ExtractorInput input)
throws IOException, InterruptedException {
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate,
input.getLength());
}
/**
* Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}.
*/
private static boolean headersMatch(int headerA, long headerB) {
return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK);
}
/**
* Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if
* the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise.
* If seeking metadata is present, {@code frame}'s position is advanced past the header.
*/
private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) {
if (frame.limit() >= xingBase + 4) {
frame.setPosition(xingBase);
int headerData = frame.readInt();
if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) {
return headerData;
}
}
if (seeker == null || (!seeker.isSeekable()
&& (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
// Repopulate the synchronized header in case we had to skip an invalid seeking header, which
// would give an invalid CBR bitrate.
input.resetPeekPosition();
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length);
if (frame.limit() >= 40) {
frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
if (frame.readInt() == SEEK_HEADER_VBRI) {
return SEEK_HEADER_VBRI;
}
}
return seeker;
return SEEK_HEADER_UNSET;
}
/**

View File

@ -382,10 +382,14 @@ public final class TsExtractor implements Extractor {
private static final int TS_PMT_DESC_DVBSUBS = 0x59;
private final ParsableBitArray pmtScratch;
private final SparseArray<TsPayloadReader> trackIdToReaderScratch;
private final SparseIntArray trackIdToPidScratch;
private final int pid;
public PmtReader(int pid) {
pmtScratch = new ParsableBitArray(new byte[5]);
trackIdToReaderScratch = new SparseArray<>();
trackIdToPidScratch = new SparseIntArray();
this.pid = pid;
}
@ -436,6 +440,8 @@ public final class TsExtractor implements Extractor {
new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
}
trackIdToReaderScratch.clear();
trackIdToPidScratch.clear();
int remainingEntriesLength = sectionData.bytesLeft();
while (remainingEntriesLength > 0) {
sectionData.readBytes(pmtScratch, 5);
@ -454,23 +460,30 @@ public final class TsExtractor implements Extractor {
if (trackIds.get(trackId)) {
continue;
}
trackIds.put(trackId, true);
TsPayloadReader reader;
if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) {
reader = id3Reader;
} else {
reader = payloadReaderFactory.createPayloadReader(streamType, esInfo);
if (reader != null) {
TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader
: payloadReaderFactory.createPayloadReader(streamType, esInfo);
if (mode != MODE_HLS
|| elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {
trackIdToPidScratch.put(trackId, elementaryPid);
trackIdToReaderScratch.put(trackId, reader);
}
}
int trackIdCount = trackIdToPidScratch.size();
for (int i = 0; i < trackIdCount; i++) {
int trackId = trackIdToPidScratch.keyAt(i);
trackIds.put(trackId, true);
TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
if (reader != null) {
if (reader != id3Reader) {
reader.init(timestampAdjuster, output,
new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));
}
}
if (reader != null) {
tsPayloadReaders.put(elementaryPid, reader);
tsPayloadReaders.put(trackIdToPidScratch.valueAt(i), reader);
}
}
if (mode == MODE_HLS) {
if (!tracksEnded) {
output.endTracks();

View File

@ -71,7 +71,7 @@ public final class MediaCodecInfo {
* @return The created instance.
*/
public static MediaCodecInfo newPassthroughInstance(String name) {
return new MediaCodecInfo(name, null, null);
return new MediaCodecInfo(name, null, null, false);
}
/**
@ -84,18 +84,29 @@ public final class MediaCodecInfo {
*/
public static MediaCodecInfo newInstance(String name, String mimeType,
CodecCapabilities capabilities) {
return new MediaCodecInfo(name, mimeType, capabilities);
return new MediaCodecInfo(name, mimeType, capabilities, false);
}
/**
* @param name The name of the decoder.
* @param capabilities The capabilities of the decoder.
* Creates an instance.
*
* @param name The name of the {@link MediaCodec}.
* @param mimeType A mime type supported by the {@link MediaCodec}.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
* @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
* @return The created instance.
*/
private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) {
public static MediaCodecInfo newInstance(String name, String mimeType,
CodecCapabilities capabilities, boolean forceDisableAdaptive) {
return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive);
}
private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities,
boolean forceDisableAdaptive) {
this.name = Assertions.checkNotNull(name);
this.mimeType = mimeType;
this.capabilities = capabilities;
adaptive = capabilities != null && isAdaptive(capabilities);
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
tunneling = capabilities != null && isTunneling(capabilities);
}

View File

@ -339,7 +339,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
String codecName = decoderInfo.name;
codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName);
codecIsAdaptive = decoderInfo.adaptive;
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
@ -1188,18 +1188,4 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
}
/**
* Returns whether the decoder is known to fail when adapting, despite advertising itself as an
* adaptive decoder.
* <p>
* If true is returned then we explicitly disable adaptation for the decoder.
*
* @param name The decoder name.
* @return True if the decoder is known to fail when adapting.
*/
private static boolean codecNeedsDisableAdaptationWorkaround(String name) {
return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3")
&& ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name));
}
}

View File

@ -160,6 +160,55 @@ public final class MediaCodecUtil {
return decoderInfos;
}
/**
* Returns the maximum frame size supported by the default H264 decoder.
*
* @return The maximum frame size for an H264 stream that can be decoded on the device.
*/
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
if (maxH264DecodableFrameSize == -1) {
int result = 0;
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo != null) {
for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
}
// We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
// the levels mandated by the Android CDD.
result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));
}
maxH264DecodableFrameSize = result;
}
return maxH264DecodableFrameSize;
}
/**
* Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
* codec description string (as defined by RFC 6381).
*
* @param codec A codec description string, as defined by RFC 6381.
* @return A pair (profile constant, level constant) if {@code codec} is well-formed and
* recognized, or null otherwise
*/
public static Pair<Integer, Integer> getCodecProfileAndLevel(String codec) {
if (codec == null) {
return null;
}
String[] parts = codec.split("\\.");
switch (parts[0]) {
case CODEC_ID_HEV1:
case CODEC_ID_HVC1:
return getHevcProfileAndLevel(codec, parts);
case CODEC_ID_AVC1:
case CODEC_ID_AVC2:
return getAvcProfileAndLevel(codec, parts);
default:
return null;
}
}
// Internal methods.
private static List<MediaCodecInfo> getDecoderInfosInternal(
CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
try {
@ -177,12 +226,14 @@ public final class MediaCodecUtil {
try {
CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType);
boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities);
boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(codecName);
if ((secureDecodersExplicit && key.secure == secure)
|| (!secureDecodersExplicit && !key.secure)) {
decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities));
decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities,
forceDisableAdaptive));
} else if (!secureDecodersExplicit && secure) {
decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType,
capabilities));
capabilities, forceDisableAdaptive));
// It only makes sense to have one synthesized secure decoder, return immediately.
return decoderInfos;
}
@ -289,50 +340,16 @@ public final class MediaCodecUtil {
}
/**
* Returns the maximum frame size supported by the default H264 decoder.
* Returns whether the decoder is known to fail when adapting, despite advertising itself as an
* adaptive decoder.
*
* @return The maximum frame size for an H264 stream that can be decoded on the device.
* @param name The decoder name.
* @return True if the decoder is known to fail when adapting.
*/
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
if (maxH264DecodableFrameSize == -1) {
int result = 0;
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo != null) {
for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
}
// We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
// the levels mandated by the Android CDD.
result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));
}
maxH264DecodableFrameSize = result;
}
return maxH264DecodableFrameSize;
}
/**
* Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
* codec description string (as defined by RFC 6381).
*
* @param codec A codec description string, as defined by RFC 6381.
* @return A pair (profile constant, level constant) if {@code codec} is well-formed and
* recognized, or null otherwise
*/
public static Pair<Integer, Integer> getCodecProfileAndLevel(String codec) {
if (codec == null) {
return null;
}
String[] parts = codec.split("\\.");
switch (parts[0]) {
case CODEC_ID_HEV1:
case CODEC_ID_HVC1:
return getHevcProfileAndLevel(codec, parts);
case CODEC_ID_AVC1:
case CODEC_ID_AVC2:
return getAvcProfileAndLevel(codec, parts);
default:
return null;
}
private static boolean codecNeedsDisableAdaptationWorkaround(String name) {
return Util.SDK_INT <= 22
&& (Util.MODEL.equals("ODROID-XU3") || Util.MODEL.equals("Nexus 10"))
&& ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name));
}
private static Pair<Integer, Integer> getHevcProfileAndLevel(String codec, String[] parts) {

View File

@ -154,23 +154,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
public void updateSelectedTrack(long bufferedDurationUs) {
long nowMs = SystemClock.elapsedRealtime();
// Get the current and ideal selections.
// Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex;
Format currentFormat = getSelectedFormat();
int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
Format idealFormat = getFormat(idealSelectedIndex);
// Assume we can switch to the ideal selection.
selectedIndex = idealSelectedIndex;
// Revert back to the current selection if conditions are not suitable for switching.
if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {
if (idealFormat.bitrate > currentFormat.bitrate
selectedIndex = determineIdealSelectedIndex(nowMs);
if (selectedIndex == currentSelectedIndex) {
return;
}
if (!isBlacklisted(currentSelectedIndex, nowMs)) {
// Revert back to the current selection if conditions are not suitable for switching.
Format currentFormat = getFormat(currentSelectedIndex);
Format selectedFormat = getFormat(selectedIndex);
if (selectedFormat.bitrate > currentFormat.bitrate
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal track is a higher quality, but we have insufficient buffer to safely switch
// The selected track is a higher quality, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
selectedIndex = currentSelectedIndex;
} else if (idealFormat.bitrate < currentFormat.bitrate
} else if (selectedFormat.bitrate < currentFormat.bitrate
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// The ideal track is a lower quality, but we have sufficient buffer to defer switching
// The selected track is a lower quality, but we have sufficient buffer to defer switching
// down for now.
selectedIndex = currentSelectedIndex;
}

View File

@ -436,35 +436,48 @@ public class DefaultTrackSelector extends MappingTrackSelector {
int rendererCount = rendererCapabilities.length;
TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount];
Parameters params = paramsReference.get();
boolean videoTrackAndRendererPresent = false;
boolean seenVideoRendererWithMappedTracks = false;
boolean selectedVideoTracks = false;
for (int i = 0; i < rendererCount; i++) {
if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) {
rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness,
params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
params.orientationMayChange, adaptiveTrackSelectionFactory,
params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary);
videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0;
if (!selectedVideoTracks) {
rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness,
params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
params.orientationMayChange, adaptiveTrackSelectionFactory,
params.exceedVideoConstraintsIfNecessary,
params.exceedRendererCapabilitiesIfNecessary);
selectedVideoTracks = rendererTrackSelections[i] != null;
}
seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0;
}
}
boolean selectedAudioTracks = false;
boolean selectedTextTracks = false;
for (int i = 0; i < rendererCount; i++) {
switch (rendererCapabilities[i].getTrackType()) {
case C.TRACK_TYPE_VIDEO:
// Already done. Do nothing.
break;
case C.TRACK_TYPE_AUDIO:
rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
rendererFormatSupports[i], params.preferredAudioLanguage,
params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness,
videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory);
if (!selectedAudioTracks) {
rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
rendererFormatSupports[i], params.preferredAudioLanguage,
params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness,
seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory);
selectedAudioTracks = rendererTrackSelections[i] != null;
}
break;
case C.TRACK_TYPE_TEXT:
rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
rendererFormatSupports[i], params.preferredTextLanguage,
params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
if (!selectedTextTracks) {
rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
rendererFormatSupports[i], params.preferredTextLanguage,
params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
selectedTextTracks = rendererTrackSelections[i] != null;
}
break;
default:
rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(),
@ -626,7 +639,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
continue;
}
int trackScore = isWithinConstraints ? 2 : 1;
if (isSupported(trackFormatSupport[trackIndex], false)) {
boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false);
if (isWithinCapabilities) {
trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
}
boolean selectTrack = trackScore > selectedTrackScore;
@ -642,7 +656,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} else {
comparisonResult = compareFormatValues(format.bitrate, selectedBitrate);
}
selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0;
selectTrack = isWithinCapabilities && isWithinConstraints
? comparisonResult > 0 : comparisonResult < 0;
}
if (selectTrack) {
selectedGroup = trackGroup;

View File

@ -61,6 +61,7 @@ public final class MimeTypes {
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac";
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";

View File

@ -98,7 +98,7 @@ public final class Util {
private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(
"(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
+ "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?");
+ "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?");
private static final Pattern XS_DURATION_PATTERN =
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");

View File

@ -76,7 +76,7 @@ public final class DummySurface extends Surface {
if (Util.SDK_INT >= 17) {
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
SECURE_SUPPORTED = extensions.contains("EGL_EXT_protected_content");
SECURE_SUPPORTED = extensions != null && extensions.contains("EGL_EXT_protected_content");
} else {
SECURE_SUPPORTED = false;
}

View File

@ -650,7 +650,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @return Suitable {@link CodecMaxValues}.
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
*/
private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format,
protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format,
Format[] streamFormats) throws DecoderQueryException {
int maxWidth = format.width;
int maxHeight = format.height;
@ -838,7 +838,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees;
}
private static final class CodecMaxValues {
protected static final class CodecMaxValues {
public final int width;
public final int height;

View File

@ -410,12 +410,14 @@ public final class DashMediaSource implements MediaSource {
private void resolveUtcTimingElement(UtcTimingElement timingElement) {
String scheme = timingElement.schemeIdUri;
if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2014")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
resolveUtcTimingElementDirect(timingElement);
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) {
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2012")) {
resolveUtcTimingElementHttp(timingElement, new Iso8601Parser());
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) {
resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser());
} else {
// Unsupported scheme.

View File

@ -51,6 +51,15 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8";
private static final String AVG_BANDWIDTH_MASTER_PLAYLIST = " #EXTM3U \n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000,"
+ "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n";
private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
@ -70,42 +79,48 @@ public class HlsMasterPlaylistParserTest extends TestCase {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertNull(masterPlaylist.muxedCaptionFormats);
assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs);
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
assertEquals(304, variants.get(0).format.width);
assertEquals(128, variants.get(0).format.height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1280000, variants.get(1).format.bitrate);
assertNotNull(variants.get(1).format.codecs);
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2560000, variants.get(2).format.bitrate);
assertEquals(null, variants.get(2).format.codecs);
assertNull(variants.get(2).format.codecs);
assertEquals(384, variants.get(2).format.width);
assertEquals(160, variants.get(2).format.height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(7680000, variants.get(3).format.bitrate);
assertEquals(null, variants.get(3).format.codecs);
assertNull(variants.get(3).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(65000, variants.get(4).format.bitrate);
assertNotNull(variants.get(4).format.codecs);
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
}
public void testMasterPlaylistWithBandwdithAverage() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI,
AVG_BANDWIDTH_MASTER_PLAYLIST);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertEquals(1280000, variants.get(0).format.bitrate);
assertEquals(1270000, variants.get(1).format.bitrate);
}
public void testPlaylistWithInvalidHeader() throws IOException {
try {
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);

View File

@ -73,7 +73,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE";
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_AVERAGE_BANDWIDTH =
Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
@ -226,6 +228,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
} else if (line.startsWith(TAG_STREAM_INF)) {
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
String averageBandwidthString = parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH);
if (averageBandwidthString != null) {
// If available, the average bandwidth attribute is used as the variant's bitrate.
bitrate = Integer.parseInt(averageBandwidthString);
}
String codecs = parseOptionalStringAttr(line, REGEX_CODECS);
String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION);
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
@ -300,8 +307,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
} else if ("EVENT".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
} else {
throw new ParserException("Illegal playlist type: " + playlistTypeString);
}
} else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
@ -390,14 +395,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
dateRanges);
}
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
}
private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern));
}
@ -408,10 +405,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return matcher.find() ? matcher.group(1) : null;
}
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return null;
throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
}
private static boolean parseBooleanAttribute(String line, Pattern pattern, boolean defaultValue) {

View File

@ -287,39 +287,41 @@ public final class SsMediaSource implements MediaSource,
for (int i = 0; i < mediaPeriods.size(); i++) {
mediaPeriods.get(i).updateManifest(manifest);
}
long startTimeUs = Long.MAX_VALUE;
long endTimeUs = Long.MIN_VALUE;
for (StreamElement element : manifest.streamElements) {
if (element.chunkCount > 0) {
startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0));
endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1)
+ element.getChunkDurationUs(element.chunkCount - 1));
}
}
Timeline timeline;
if (manifest.isLive) {
long startTimeUs = Long.MAX_VALUE;
long endTimeUs = Long.MIN_VALUE;
for (int i = 0; i < manifest.streamElements.length; i++) {
StreamElement element = manifest.streamElements[i];
if (element.chunkCount > 0) {
startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0));
endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1)
+ element.getChunkDurationUs(element.chunkCount - 1));
}
if (startTimeUs == Long.MAX_VALUE) {
long periodDurationUs = manifest.isLive ? C.TIME_UNSET : 0;
timeline = new SinglePeriodTimeline(periodDurationUs, 0, 0, 0, true /* isSeekable */,
manifest.isLive /* isDynamic */);
} else if (manifest.isLive) {
if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) {
startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs);
}
if (startTimeUs == Long.MAX_VALUE) {
timeline = new SinglePeriodTimeline(C.TIME_UNSET, false);
} else {
if (manifest.dvrWindowLengthUs != C.TIME_UNSET
&& manifest.dvrWindowLengthUs > 0) {
startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs);
}
long durationUs = endTimeUs - startTimeUs;
long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs);
if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {
// The default start position is too close to the start of the live window. Set it to the
// minimum default start position provided the window is at least twice as big. Else set
// it to the middle of the window.
defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2);
}
timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs,
defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */);
long durationUs = endTimeUs - startTimeUs;
long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs);
if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {
// The default start position is too close to the start of the live window. Set it to the
// minimum default start position provided the window is at least twice as big. Else set
// it to the middle of the window.
defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2);
}
timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs,
defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */);
} else {
boolean isSeekable = manifest.durationUs != C.TIME_UNSET;
timeline = new SinglePeriodTimeline(manifest.durationUs, isSeekable);
long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs
: endTimeUs - startTimeUs;
timeline = new SinglePeriodTimeline(startTimeUs + durationUs, durationUs, startTimeUs, 0,
true /* isSeekable */, false /* isDynamic */);
}
sourceListener.onSourceInfoRefreshed(timeline, manifest);
}

View File

@ -220,11 +220,13 @@ public class DefaultTimeBar extends View implements TimeBar {
public void setPosition(long position) {
this.position = position;
setContentDescription(getProgressText());
update();
}
@Override
public void setBufferedPosition(long bufferedPosition) {
this.bufferedPosition = bufferedPosition;
update();
}
@Override
@ -235,6 +237,7 @@ public class DefaultTimeBar extends View implements TimeBar {
} else {
updateScrubberState();
}
update();
}
@Override
@ -242,6 +245,7 @@ public class DefaultTimeBar extends View implements TimeBar {
Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null);
this.adBreakCount = adBreakCount;
this.adBreakTimesMs = adBreakTimesMs;
update();
}
@Override
@ -438,7 +442,7 @@ public class DefaultTimeBar extends View implements TimeBar {
parent.requestDisallowInterceptTouchEvent(true);
}
if (listener != null) {
listener.onScrubStart(this);
listener.onScrubStart(this, getScrubberPosition());
}
}

View File

@ -875,7 +875,7 @@ public class PlaybackControlView extends FrameLayout {
OnClickListener {
@Override
public void onScrubStart(TimeBar timeBar) {
public void onScrubStart(TimeBar timeBar, long position) {
removeCallbacks(hideAction);
scrubbing = true;
}

View File

@ -95,8 +95,9 @@ public interface TimeBar {
* Called when the user starts moving the scrubber.
*
* @param timeBar The time bar.
* @param position The position of the scrubber, in milliseconds.
*/
void onScrubStart(TimeBar timeBar);
void onScrubStart(TimeBar timeBar, long position);
/**
* Called when the user moves the scrubber.