Commit ab6f9aea by ojw28 Committed by GitHub

Merge pull request #3381 from google/dev-v2-r2.5.4

r2.5.4
parents 1fdc11f2 04862bcc
Showing with 290 additions and 114 deletions
# Release notes #
### r2.5.4 ###
* Remove unnecessary media playlist fetches during playback of live HLS streams.
* Add the ability to inject a HLS playlist parser through `HlsMediaSource`.
* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource`
([#3334](https://github.com/google/ExoPlayer/issues/3334)).
* Fix an issue parsing MP4 content containing non-CENC sinf boxes.
* Fix memory leak when seeking with repeated periods.
* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition`
set to false.
* Ignore MP4 edit lists that seem invalid
([#3351](https://github.com/google/ExoPlayer/issues/3351)).
* Add extractor flag for ignoring all MP4 edit lists
([#3358](https://github.com/google/ExoPlayer/issues/3358)).
* Improve extensibility by exposing public constructors for
`FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk`
non-final.
### r2.5.3 ###
* IMA extension: Support skipping of skippable ads on AndroidTV and other
......
......@@ -24,7 +24,7 @@ project.ext {
supportLibraryVersion = '25.4.0'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
releaseVersion = 'r2.5.3'
releaseVersion = 'r2.5.4'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
......
......@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2503"
android:versionName="2.5.3">
android:versionCode="2504"
android:versionName="2.5.4">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
......@@ -344,11 +344,11 @@
"samples": [
{
"name": "Apple 4x3 basic stream",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
},
{
"name": "Apple 16x9 basic stream",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
},
{
"name": "Apple master playlist advanced (TS)",
......@@ -360,11 +360,11 @@
},
{
"name": "Apple TS media playlist",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
},
{
"name": "Apple AAC media playlist",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
},
{
"name": "Apple ID3 metadata",
......@@ -381,11 +381,11 @@
},
{
"name": "Apple AAC 10s",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
},
{
"name": "Apple TS 10s",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
},
{
"name": "Android screens (Matroska)",
......
......@@ -34,18 +34,18 @@ public final class AtomParsersTest extends TestCase {
+ SAMPLE_COUNT + "0001000200030004");
public void testStz2Parsing4BitFieldSize() {
verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2)));
verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2)));
}
public void testStz2Parsing8BitFieldSize() {
verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2)));
verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2)));
}
public void testStz2Parsing16BitFieldSize() {
verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2)));
verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2)));
}
private void verifyParsing(Atom.LeafAtom stz2Atom) {
private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) {
AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom);
assertEquals(4, box.getSampleCount());
assertFalse(box.isFixedSampleSize());
......
......@@ -356,17 +356,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public boolean isPlayingAd() {
return pendingSeekAcks == 0 && playbackInfo.periodId.isAd();
return !timeline.isEmpty() && pendingSeekAcks == 0 && playbackInfo.periodId.isAd();
}
@Override
public int getCurrentAdGroupIndex() {
return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
}
@Override
public int getCurrentAdIndexInAdGroup() {
return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
}
@Override
......
......@@ -430,6 +430,10 @@ import java.io.IOException;
loadControl.onPrepared();
if (resetPosition) {
playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
} else {
// The new start position is the current playback position.
playbackInfo = new PlaybackInfo(playbackInfo.periodId, playbackInfo.positionUs,
playbackInfo.contentPositionUs);
}
this.mediaSource = mediaSource;
mediaSource.prepareSource(player, true, this);
......@@ -719,7 +723,8 @@ import java.io.IOException;
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder periodHolder = playingPeriodHolder;
while (periodHolder != null) {
if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) {
if (newPlayingPeriodHolder == null
&& shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) {
newPlayingPeriodHolder = periodHolder;
} else {
periodHolder.release();
......
......@@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo {
* The version of the library expressed as a string, for example "1.2.3".
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.5.3";
public static final String VERSION = "2.5.4";
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.3";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.4";
/**
* The version of the library expressed as an integer, for example 1002003.
......@@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2005003;
public static final int VERSION_INT = 2005004;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
......@@ -28,12 +28,29 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto {
private final MediaCrypto mediaCrypto;
private final boolean forceAllowInsecureDecoderComponents;
/* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto,
/**
* @param mediaCrypto The {@link MediaCrypto} to wrap.
*/
public FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
this(mediaCrypto, false);
}
/**
* @param mediaCrypto The {@link MediaCrypto} to wrap.
* @param forceAllowInsecureDecoderComponents Whether to force
* {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than
* {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped
* {@link MediaCrypto}.
*/
public FrameworkMediaCrypto(MediaCrypto mediaCrypto,
boolean forceAllowInsecureDecoderComponents) {
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
}
/**
* Returns the wrapped {@link MediaCrypto}.
*/
public MediaCrypto getWrappedMediaCrypto() {
return mediaCrypto;
}
......
......@@ -67,6 +67,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
}
private @MatroskaExtractor.Flags int matroskaFlags;
private @Mp4Extractor.Flags int mp4Flags;
private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
private @Mp3Extractor.Flags int mp3Flags;
private @TsExtractor.Mode int tsMode;
......@@ -90,6 +91,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
}
/**
* Sets flags for {@link Mp4Extractor} instances created by the factory.
*
* @see Mp4Extractor#Mp4Extractor(int)
* @param flags The flags to use.
* @return The factory, for convenience.
*/
public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) {
this.mp4Flags = flags;
return this;
}
/**
* Sets flags for {@link FragmentedMp4Extractor} instances created by the factory.
*
* @see FragmentedMp4Extractor#FragmentedMp4Extractor(int)
......@@ -145,7 +158,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12];
extractors[0] = new MatroskaExtractor(matroskaFlags);
extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
extractors[2] = new Mp4Extractor();
extractors[2] = new Mp4Extractor(mp4Flags);
extractors[3] = new Mp3Extractor(mp3Flags);
extractors[4] = new AdtsExtractor();
extractors[5] = new Ac3Extractor();
......
......@@ -60,11 +60,13 @@ import java.util.List;
* @param duration The duration in units of the timescale declared in the mvhd atom, or
* {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom.
* @param drmInitData {@link DrmInitData} to be included in the format.
* @param ignoreEditLists Whether to ignore any edit lists in the trak box.
* @param isQuickTime True for QuickTime media. False otherwise.
* @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
*/
public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration,
DrmInitData drmInitData, boolean isQuickTime) throws ParserException {
DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
throws ParserException {
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
if (trackType == C.TRACK_TYPE_UNKNOWN) {
......@@ -88,11 +90,17 @@ import java.util.List;
Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id,
tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime);
Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
long[] editListDurations = null;
long[] editListMediaTimes = null;
if (!ignoreEditLists) {
Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
editListDurations = edtsData.first;
editListMediaTimes = edtsData.second;
}
return stsdData.format == null ? null
: new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second);
stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes);
}
/**
......@@ -395,7 +403,11 @@ import java.util.List;
hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
}
if (!hasSyncSample) {
throw new ParserException("The edited sample sequence does not contain a sync sample.");
// We don't support edit lists where the edited sample sequence doesn't contain a sync sample.
// Such edit lists are often (although not always) broken, so we ignore it and continue.
Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
}
return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps,
......@@ -779,7 +791,7 @@ import java.util.List;
*
* @param edtsAtom edts (edit box) atom to decode.
* @return Pair of edit list durations and edit list media times, or a pair of nulls if they are
* not present.
* not present.
*/
private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) {
Atom.LeafAtom elst;
......@@ -1060,8 +1072,8 @@ import java.util.List;
Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_sinf) {
Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition,
childAtomSize);
Pair<Integer, TrackEncryptionBox> result = parseCommonEncryptionSinfFromParent(parent,
childPosition, childAtomSize);
if (result != null) {
return result;
}
......@@ -1071,8 +1083,8 @@ import java.util.List;
return null;
}
private static Pair<Integer, TrackEncryptionBox> parseSinfFromParent(ParsableByteArray parent,
int position, int size) {
/* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent(
ParsableByteArray parent, int position, int size) {
int childPosition = position + Atom.HEADER_SIZE;
int schemeInformationBoxPosition = C.POSITION_UNSET;
int schemeInformationBoxSize = 0;
......@@ -1086,7 +1098,7 @@ import java.util.List;
dataFormat = parent.readInt();
} else if (childAtomType == Atom.TYPE_schm) {
parent.skipBytes(4);
// scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1.
// Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1.
schemeType = parent.readString(4);
} else if (childAtomType == Atom.TYPE_schi) {
schemeInformationBoxPosition = childPosition;
......@@ -1095,7 +1107,8 @@ import java.util.List;
childPosition += childAtomSize;
}
if (schemeType != null) {
if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType)
|| C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET,
"schi atom is mandatory");
......
......@@ -74,7 +74,7 @@ public final class FragmentedMp4Extractor implements Extractor {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
FLAG_SIDELOADED})
FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
public @interface Flags {}
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
......@@ -103,6 +103,10 @@ public final class FragmentedMp4Extractor implements Extractor {
* container.
*/
private static final int FLAG_SIDELOADED = 16;
/**
* Flag to ignore any edit lists in the stream.
*/
public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32;
private static final String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
......@@ -426,7 +430,7 @@ public final class FragmentedMp4Extractor implements Extractor {
Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type == Atom.TYPE_trak) {
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration,
drmInitData, false);
drmInitData, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, false);
if (track != null) {
tracks.put(track.id, track);
}
......
......@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor.Flags;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.NalUnitUtil;
......@@ -58,6 +59,17 @@ public final class Mp4Extractor implements Extractor, SeekMap {
};
/**
* Flags controlling the behavior of the extractor.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
public @interface Flags {}
/**
* Flag to ignore any edit lists in the stream.
*/
public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1;
/**
* Parser states.
*/
@Retention(RetentionPolicy.SOURCE)
......@@ -76,6 +88,8 @@ public final class Mp4Extractor implements Extractor, SeekMap {
*/
private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
private final @Flags int flags;
// Temporary arrays.
private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength;
......@@ -98,7 +112,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private long durationUs;
private boolean isQuickTime;
/**
* Creates a new extractor for unfragmented MP4 streams.
*/
public Mp4Extractor() {
this(0);
}
/**
* Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the
* extractor's behavior.
*
* @param flags Flags that control the extractor's behavior.
*/
public Mp4Extractor(@Flags int flags) {
this.flags = flags;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
containerAtoms = new Stack<>();
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
......@@ -345,7 +373,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd),
C.TIME_UNSET, null, isQuickTime);
C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime);
if (track == null) {
continue;
}
......
......@@ -81,12 +81,12 @@ public final class Track {
/**
* Durations of edit list segments in the movie timescale. Null if there is no edit list.
*/
public final long[] editListDurations;
@Nullable public final long[] editListDurations;
/**
* Media times for edit list segments in the track timescale. Null if there is no edit list.
*/
public final long[] editListMediaTimes;
@Nullable public final long[] editListMediaTimes;
/**
* For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for
......@@ -99,7 +99,7 @@ public final class Track {
public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
Format format, @Transformation int sampleTransformation,
@Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
long[] editListDurations, long[] editListMediaTimes) {
@Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) {
this.id = id;
this.type = type;
this.timescale = timescale;
......
......@@ -23,6 +23,7 @@ import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Looper;
import android.os.SystemClock;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.BaseRenderer;
......@@ -43,6 +44,9 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
......@@ -158,9 +162,26 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ADAPTATION_WORKAROUND_MODE_NEVER, ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION,
ADAPTATION_WORKAROUND_MODE_ALWAYS})
private @interface AdaptationWorkaroundMode {}
/**
* The adaptation workaround is never used.
*/
private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0;
/**
* The adaptation workaround is used when adapting between formats of the same resolution only.
*/
private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1;
/**
* The adaptation workaround is always used when adapting between formats.
*/
private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2;
/**
* H.264/AVC buffer to queue when using the adaptation workaround (see
* {@link #codecNeedsAdaptationWorkaround(String)}. Consists of three NAL units with start codes:
* {@link #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes:
* Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be
* queued to force a resolution change when adapting to a new format.
*/
......@@ -182,9 +203,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private DrmSession<FrameworkMediaCrypto> pendingDrmSession;
private MediaCodec codec;
private MediaCodecInfo codecInfo;
private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode;
private boolean codecNeedsDiscardToSpsWorkaround;
private boolean codecNeedsFlushWorkaround;
private boolean codecNeedsAdaptationWorkaround;
private boolean codecNeedsEosPropagationWorkaround;
private boolean codecNeedsEosFlushWorkaround;
private boolean codecNeedsEosOutputExceptionWorkaround;
......@@ -355,9 +376,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
String codecName = codecInfo.name;
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
......@@ -458,7 +479,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecReceivedBuffers = false;
codecNeedsDiscardToSpsWorkaround = false;
codecNeedsFlushWorkaround = false;
codecNeedsAdaptationWorkaround = false;
codecAdaptationWorkaroundMode = ADAPTATION_WORKAROUND_MODE_NEVER;
codecNeedsEosPropagationWorkaround = false;
codecNeedsEosFlushWorkaround = false;
codecNeedsMonoChannelCountWorkaround = false;
......@@ -802,8 +823,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround
&& format.width == oldFormat.width && format.height == oldFormat.height;
codecNeedsAdaptationWorkaroundBuffer =
codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
|| (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
&& format.width == oldFormat.width && format.height == oldFormat.height);
} else {
if (codecReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
......@@ -989,7 +1012,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private void processOutputFormat() throws ExoPlaybackException {
MediaFormat format = codec.getOutputFormat();
if (codecNeedsAdaptationWorkaround
if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER
&& format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT
&& format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) {
// We assume this format changed event was caused by the adaptation workaround.
......@@ -1122,22 +1145,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
* Returns whether the decoder is known to get stuck during some adaptations where the resolution
* does not change.
* Returns a mode that specifies when the adaptation workaround should be enabled.
* <p>
* If true is returned, the renderer will work around the issue by queueing and discarding a blank
* frame at a different resolution, which resets the codec's internal state.
* When enabled, the workaround queues and discards a blank frame with a resolution whose width
* and height both equal {@link #ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT}, to reset the codec's
* internal state when a format change occurs.
* <p>
* See [Internal: b/27807182].
* See <a href="https://github.com/google/ExoPlayer/issues/3257">GitHub issue #3257</a>.
*
* @param name The name of the decoder.
* @return True if the decoder is known to get stuck during some adaptations.
* @return The mode specifying when the adaptation workaround should be enabled.
*/
private static boolean codecNeedsAdaptationWorkaround(String name) {
return Util.SDK_INT < 24
private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) {
if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name)
&& (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A520"))) {
return ADAPTATION_WORKAROUND_MODE_ALWAYS;
} else if (Util.SDK_INT < 24
&& ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name))
&& ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE)
|| "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE));
|| "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE))) {
return ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION;
} else {
return ADAPTATION_WORKAROUND_MODE_NEVER;
}
}
/**
......
......@@ -973,9 +973,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* If true is returned then we fall back to releasing and re-instantiating the codec instead.
*/
private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
// Work around https://github.com/google/ExoPlayer/issues/3236
return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE))
&& "OMX.qcom.video.decoder.avc".equals(name);
// Work around https://github.com/google/ExoPlayer/issues/3236 and
// https://github.com/google/ExoPlayer/issues/3355.
return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE))
&& "OMX.qcom.video.decoder.avc".equals(name))
|| ("tcl_eu".equals(Util.DEVICE) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name));
}
/**
......
......@@ -173,7 +173,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
@Override
public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) {
public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) {
if (fatalError != null) {
return;
}
......@@ -300,7 +300,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
trackSelection.indexOf(chunk.trackFormat), e);
}
// Private methods.
// Internal methods.
private ArrayList<Representation> getRepresentations() {
List<AdaptationSet> manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets;
......@@ -319,7 +319,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
}
private static Chunk newInitializationChunk(RepresentationHolder representationHolder,
protected static Chunk newInitializationChunk(RepresentationHolder representationHolder,
DataSource dataSource, Format trackFormat, int trackSelectionReason,
Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) {
RangedUri requestUri;
......@@ -340,7 +340,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);
}
private static Chunk newMediaChunk(RepresentationHolder representationHolder,
protected static Chunk newMediaChunk(RepresentationHolder representationHolder,
DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason,
Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) {
Representation representation = representationHolder.representation;
......
......@@ -28,7 +28,7 @@ import java.util.List;
import junit.framework.TestCase;
/**
* Test for {@link HlsMasterPlaylistParserTest}
* Test for {@link HlsMasterPlaylistParserTest}.
*/
public class HlsMasterPlaylistParserTest extends TestCase {
......
......@@ -27,7 +27,7 @@ import java.util.Locale;
import junit.framework.TestCase;
/**
* Test for {@link HlsMediaPlaylistParserTest}
* Test for {@link HlsMediaPlaylistParserTest}.
*/
public class HlsMediaPlaylistParserTest extends TestCase {
......
......@@ -26,9 +26,12 @@ import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.List;
......@@ -52,6 +55,7 @@ public final class HlsMediaSource implements MediaSource,
private final HlsDataSourceFactory dataSourceFactory;
private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private HlsPlaylistTracker playlistTracker;
private Listener sourceListener;
......@@ -72,9 +76,18 @@ public final class HlsMediaSource implements MediaSource,
public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory,
int minLoadableRetryCount, Handler eventHandler,
AdaptiveMediaSourceEventListener eventListener) {
this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener,
new HlsPlaylistParser());
}
public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory,
int minLoadableRetryCount, Handler eventHandler,
AdaptiveMediaSourceEventListener eventListener,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
this.manifestUri = manifestUri;
this.dataSourceFactory = dataSourceFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
this.playlistParser = playlistParser;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
}
......@@ -82,7 +95,7 @@ public final class HlsMediaSource implements MediaSource,
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Assertions.checkState(playlistTracker == null);
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
minLoadableRetryCount, this);
minLoadableRetryCount, this, playlistParser);
sourceListener = listener;
playlistTracker.start();
}
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
......@@ -109,6 +110,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
}
/**
* Returns a copy of this playlist which includes only the renditions identified by the given
* urls.
*
* @param renditionUrls List of rendition urls.
* @return A copy of this playlist which includes only the renditions identified by the given
* urls.
*/
public HlsMasterPlaylist copy(List<String> renditionUrls) {
return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls),
copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls),
muxedAudioFormat, muxedCaptionFormats);
}
/**
* Creates a playlist with a single variant.
*
* @param variantUrl The url of the single variant.
......@@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
emptyList, null, null);
}
private static List<HlsUrl> copyRenditionsList(List<HlsUrl> renditions, List<String> urls) {
List<HlsUrl> copiedRenditions = new ArrayList<>(urls.size());
for (int i = 0; i < renditions.size(); i++) {
HlsUrl rendition = renditions.get(i);
if (urls.contains(rendition.url)) {
copiedRenditions.add(rendition);
}
}
return copiedRenditions;
}
}
......@@ -111,4 +111,5 @@ public class FakeMediaSource implements MediaSource {
}
return new TrackGroupArray(trackGroups);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment