mirror of
https://github.com/androidx/media.git
synced 2025-05-17 04:29:55 +08:00
commit
3ada4e178d
@ -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
|
||||
|
@ -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')) {
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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)?)?$");
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user