HLS: Fix key rotation

Passing EXT-X-KEY DrmInitData through the FragmentedMp4Extractor
doesn't work for streams with key rotation, because an extractor
instance is used for multiple segments, but is only passed the
EXT-X-KEY DrmInitData corresponding to the first segment.

This change removes passing DrmInitData through the extractor,
and instead passes it via FormatAdjustingSampleQueue. This is
in-line with how manifest DrmInitData is handled during DASH
playbacks.

Issue: #6903
PiperOrigin-RevId: 292323429
This commit is contained in:
olly 2020-01-30 12:37:28 +00:00 committed by Oliver Woodman
parent cf06589029
commit ff822ff9fd
8 changed files with 99 additions and 52 deletions

View File

@ -25,6 +25,8 @@
* Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct color * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct color
([#6724](https://github.com/google/ExoPlayer/pull/6724)). ([#6724](https://github.com/google/ExoPlayer/pull/6724)).
* DRM: Add support for attaching DRM sessions to clear content in the demo app. * DRM: Add support for attaching DRM sessions to clear content in the demo app.
* HLS: Fix playback of DRM protected content that uses key rotation
([#6903](https://github.com/google/ExoPlayer/issues/6903)).
* Downloads: Merge downloads in `SegmentDownloader` to improve overall download * Downloads: Merge downloads in `SegmentDownloader` to improve overall download
speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* MP3: Add `IndexSeeker` for accurate seeks in VBR streams * MP3: Add `IndexSeeker` for accurate seeks in VBR streams

View File

@ -804,10 +804,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
extractor = extractor =
new FragmentedMp4Extractor( new FragmentedMp4Extractor(
flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); flags,
/* timestampAdjuster= */ null,
/* sideloadedTrack= */ null,
closedCaptionFormats,
playerEmsgTrackOutput);
} }
// Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
// as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
return new ChunkExtractorWrapper(extractor, trackType, representation.format); return new ChunkExtractorWrapper(extractor, trackType, representation.format);
} }
} }

View File

@ -129,7 +129,6 @@ public class FragmentedMp4Extractor implements Extractor {
// Sideloaded data. // Sideloaded data.
private final List<Format> closedCaptionFormats; private final List<Format> closedCaptionFormats;
@Nullable private final DrmInitData sideloadedDrmInitData;
// Track-linked data bundle, accessible as a whole through trackID. // Track-linked data bundle, accessible as a whole through trackID.
private final SparseArray<TrackBundle> trackBundles; private final SparseArray<TrackBundle> trackBundles;
@ -185,7 +184,7 @@ public class FragmentedMp4Extractor implements Extractor {
* @param flags Flags that control the extractor's behavior. * @param flags Flags that control the extractor's behavior.
*/ */
public FragmentedMp4Extractor(@Flags int flags) { public FragmentedMp4Extractor(@Flags int flags) {
this(flags, null); this(flags, /* timestampAdjuster= */ null);
} }
/** /**
@ -193,7 +192,7 @@ public class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/ */
public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) {
this(flags, timestampAdjuster, null, null); this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList());
} }
/** /**
@ -201,15 +200,12 @@ public class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
* receive a moov box in the input data. Null if a moov box is expected. * receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
*/ */
public FragmentedMp4Extractor( public FragmentedMp4Extractor(
@Flags int flags, @Flags int flags,
@Nullable TimestampAdjuster timestampAdjuster, @Nullable TimestampAdjuster timestampAdjuster,
@Nullable Track sideloadedTrack, @Nullable Track sideloadedTrack) {
@Nullable DrmInitData sideloadedDrmInitData) { this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList());
this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList());
} }
/** /**
@ -217,8 +213,6 @@ public class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
* receive a moov box in the input data. Null if a moov box is expected. * receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
* @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
* caption channels to expose. * caption channels to expose.
*/ */
@ -226,10 +220,13 @@ public class FragmentedMp4Extractor implements Extractor {
@Flags int flags, @Flags int flags,
@Nullable TimestampAdjuster timestampAdjuster, @Nullable TimestampAdjuster timestampAdjuster,
@Nullable Track sideloadedTrack, @Nullable Track sideloadedTrack,
@Nullable DrmInitData sideloadedDrmInitData,
List<Format> closedCaptionFormats) { List<Format> closedCaptionFormats) {
this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, this(
closedCaptionFormats, null); flags,
timestampAdjuster,
sideloadedTrack,
closedCaptionFormats,
/* additionalEmsgTrackOutput= */ null);
} }
/** /**
@ -237,8 +234,6 @@ public class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not
* receive a moov box in the input data. Null if a moov box is expected. * receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
* @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
* caption channels to expose. * caption channels to expose.
* @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages
@ -249,13 +244,11 @@ public class FragmentedMp4Extractor implements Extractor {
@Flags int flags, @Flags int flags,
@Nullable TimestampAdjuster timestampAdjuster, @Nullable TimestampAdjuster timestampAdjuster,
@Nullable Track sideloadedTrack, @Nullable Track sideloadedTrack,
@Nullable DrmInitData sideloadedDrmInitData,
List<Format> closedCaptionFormats, List<Format> closedCaptionFormats,
@Nullable TrackOutput additionalEmsgTrackOutput) { @Nullable TrackOutput additionalEmsgTrackOutput) {
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster; this.timestampAdjuster = timestampAdjuster;
this.sideloadedTrack = sideloadedTrack; this.sideloadedTrack = sideloadedTrack;
this.sideloadedDrmInitData = sideloadedDrmInitData;
this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);
this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; this.additionalEmsgTrackOutput = additionalEmsgTrackOutput;
eventMessageEncoder = new EventMessageEncoder(); eventMessageEncoder = new EventMessageEncoder();
@ -470,8 +463,7 @@ public class FragmentedMp4Extractor implements Extractor {
private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); Assertions.checkState(sideloadedTrack == null, "Unexpected moov box.");
DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);
: getDrmInitDataFromAtoms(moov.leafChildren);
// Read declaration of track fragments in the Moov box. // Read declaration of track fragments in the Moov box.
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
@ -550,9 +542,8 @@ public class FragmentedMp4Extractor implements Extractor {
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
parseMoof(moof, trackBundles, flags, scratchBytes); parseMoof(moof, trackBundles, flags, scratchBytes);
// If drm init data is sideloaded, we ignore pssh boxes.
DrmInitData drmInitData = sideloadedDrmInitData != null ? null @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren);
: getDrmInitDataFromAtoms(moof.leafChildren);
if (drmInitData != null) { if (drmInitData != null) {
int trackCount = trackBundles.size(); int trackCount = trackBundles.size();
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
@ -1417,6 +1408,7 @@ public class FragmentedMp4Extractor implements Extractor {
} }
/** Returns DrmInitData from leaf atoms. */ /** Returns DrmInitData from leaf atoms. */
@Nullable
private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) { private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) {
@Nullable ArrayList<SchemeData> schemeDatas = null; @Nullable ArrayList<SchemeData> schemeDatas = null;
int leafChildrenSize = leafChildren.size(); int leafChildrenSize = leafChildren.size();

View File

@ -47,7 +47,11 @@ public final class FragmentedMp4ExtractorTest {
ExtractorFactory extractorFactory = ExtractorFactory extractorFactory =
getExtractorFactory( getExtractorFactory(
Collections.singletonList( Collections.singletonList(
Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); Format.createTextSampleFormat(
null,
MimeTypes.APPLICATION_CEA608,
/* selectionFlags= */ 0,
/* language= */ null)));
ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4"); ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4");
} }
@ -64,6 +68,11 @@ public final class FragmentedMp4ExtractorTest {
} }
private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) { private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); return () ->
new FragmentedMp4Extractor(
/* flags= */ 0,
/* timestampAdjuster= */ null,
/* sideloadedTrack= */ null,
closedCaptionFormats);
} }
} }

View File

@ -19,7 +19,6 @@ import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
@ -89,7 +88,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
Uri uri, Uri uri,
Format format, Format format,
@Nullable List<Format> muxedCaptionFormats, @Nullable List<Format> muxedCaptionFormats,
@Nullable DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster, TimestampAdjuster timestampAdjuster,
Map<String, List<String>> responseHeaders, Map<String, List<String>> responseHeaders,
ExtractorInput extractorInput) ExtractorInput extractorInput)
@ -111,8 +109,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// Try selecting the extractor by the file extension. // Try selecting the extractor by the file extension.
Extractor extractorByFileExtension = Extractor extractorByFileExtension =
createExtractorByFileExtension( createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster);
uri, format, muxedCaptionFormats, drmInitData, timestampAdjuster);
extractorInput.resetPeekPosition(); extractorInput.resetPeekPosition();
if (sniffQuietly(extractorByFileExtension, extractorInput)) { if (sniffQuietly(extractorByFileExtension, extractorInput)) {
return buildResult(extractorByFileExtension); return buildResult(extractorByFileExtension);
@ -159,7 +156,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
FragmentedMp4Extractor fragmentedMp4Extractor = FragmentedMp4Extractor fragmentedMp4Extractor =
createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats); createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
return buildResult(fragmentedMp4Extractor); return buildResult(fragmentedMp4Extractor);
} }
@ -186,7 +183,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
Uri uri, Uri uri,
Format format, Format format,
@Nullable List<Format> muxedCaptionFormats, @Nullable List<Format> muxedCaptionFormats,
@Nullable DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster) { TimestampAdjuster timestampAdjuster) {
String lastPathSegment = uri.getLastPathSegment(); String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) { if (lastPathSegment == null) {
@ -209,8 +205,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
|| lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5) || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)
|| lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
return createFragmentedMp4Extractor( return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
timestampAdjuster, format, drmInitData, muxedCaptionFormats);
} else { } else {
// For any other file extension, we assume TS format. // For any other file extension, we assume TS format.
return createTsExtractor( return createTsExtractor(
@ -270,7 +265,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
private static FragmentedMp4Extractor createFragmentedMp4Extractor( private static FragmentedMp4Extractor createFragmentedMp4Extractor(
TimestampAdjuster timestampAdjuster, TimestampAdjuster timestampAdjuster,
Format format, Format format,
@Nullable DrmInitData drmInitData,
@Nullable List<Format> muxedCaptionFormats) { @Nullable List<Format> muxedCaptionFormats) {
// Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid
// creating a separate EMSG track for every audio track in a video stream. // creating a separate EMSG track for every audio track in a video stream.
@ -278,7 +272,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
/* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,
timestampAdjuster, timestampAdjuster,
/* sideloadedTrack= */ null, /* sideloadedTrack= */ null,
drmInitData,
muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());
} }

View File

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.hls;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
@ -71,7 +70,6 @@ public interface HlsExtractorFactory {
* @param format A {@link Format} associated with the chunk to extract. * @param format A {@link Format} associated with the chunk to extract.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist. * information is available in the master playlist.
* @param drmInitData {@link DrmInitData} associated with the chunk.
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
* @param responseHeaders The HTTP response headers associated with the media segment or * @param responseHeaders The HTTP response headers associated with the media segment or
* initialization section to extract. * initialization section to extract.
@ -87,7 +85,6 @@ public interface HlsExtractorFactory {
Uri uri, Uri uri,
Format format, Format format,
@Nullable List<Format> muxedCaptionFormats, @Nullable List<Format> muxedCaptionFormats,
@Nullable DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster, TimestampAdjuster timestampAdjuster,
Map<String, List<String>> responseHeaders, Map<String, List<String>> responseHeaders,
ExtractorInput sniffingExtractorInput) ExtractorInput sniffingExtractorInput)

View File

@ -388,7 +388,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long bytesToRead = dataSource.open(dataSpec); long bytesToRead = dataSource.open(dataSpec);
DefaultExtractorInput extractorInput = DefaultExtractorInput extractorInput =
new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead); new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead);
@ -402,7 +401,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
dataSpec.uri, dataSpec.uri,
trackFormat, trackFormat,
muxedCaptionFormats, muxedCaptionFormats,
drmInitData,
timestampAdjuster, timestampAdjuster,
dataSource.getResponseHeaders(), dataSource.getResponseHeaders(),
extractorInput); extractorInput);
@ -421,7 +419,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
output.onNewExtractor(); output.onNewExtractor();
extractor.init(output); extractor.init(output);
} }
output.setDrmInitData(drmInitData);
return extractorInput; return extractorInput;
} }

View File

@ -128,7 +128,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final ArrayList<HlsSampleStream> hlsSampleStreams; private final ArrayList<HlsSampleStream> hlsSampleStreams;
private final Map<String, DrmInitData> overridingDrmInitData; private final Map<String, DrmInitData> overridingDrmInitData;
private SampleQueue[] sampleQueues; private FormatAdjustingSampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds; private int[] sampleQueueTrackIds;
private Set<Integer> sampleQueueMappingDoneByType; private Set<Integer> sampleQueueMappingDoneByType;
private SparseIntArray sampleQueueIndicesByType; private SparseIntArray sampleQueueIndicesByType;
@ -162,6 +162,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private boolean tracksEnded; private boolean tracksEnded;
private long sampleOffsetUs; private long sampleOffsetUs;
@Nullable private DrmInitData drmInitData;
private int chunkUid; private int chunkUid;
/** /**
@ -207,7 +208,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueTrackIds = new int[0]; sampleQueueTrackIds = new int[0];
sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size());
sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size());
sampleQueues = new SampleQueue[0]; sampleQueues = new FormatAdjustingSampleQueue[0];
sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0]; sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>(); mediaChunks = new ArrayList<>();
@ -904,8 +905,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private SampleQueue createSampleQueue(int id, int type) { private SampleQueue createSampleQueue(int id, int type) {
int trackCount = sampleQueues.length; int trackCount = sampleQueues.length;
SampleQueue trackOutput = boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
FormatAdjustingSampleQueue trackOutput =
new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData); new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData);
if (isAudioVideo) {
trackOutput.setDrmInitData(drmInitData);
}
trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.sourceId(chunkUid); trackOutput.sourceId(chunkUid);
trackOutput.setUpstreamFormatChangeListener(this); trackOutput.setUpstreamFormatChangeListener(this);
@ -913,8 +918,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueTrackIds[trackCount] = id; sampleQueueTrackIds[trackCount] = id;
sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput); sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);
sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
sampleQueueIsAudioVideoFlags[trackCount] = sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo;
type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];
sampleQueueMappingDoneByType.add(type); sampleQueueMappingDoneByType.add(type);
sampleQueueIndicesByType.append(type, trackCount); sampleQueueIndicesByType.append(type, trackCount);
@ -951,12 +955,55 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueMappingDoneByType.clear(); sampleQueueMappingDoneByType.clear();
} }
/**
* Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that
* are subsequently loaded by this wrapper.
*
* @param sampleOffsetUs The timestamp offset in microseconds.
*/
public void setSampleOffsetUs(long sampleOffsetUs) { public void setSampleOffsetUs(long sampleOffsetUs) {
if (this.sampleOffsetUs != sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
for (SampleQueue sampleQueue : sampleQueues) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.setSampleOffsetUs(sampleOffsetUs); sampleQueue.setSampleOffsetUs(sampleOffsetUs);
} }
} }
}
/**
* Sets default {@link DrmInitData} for samples that are subsequently loaded by this wrapper.
*
* <p>This method should be called prior to loading each {@link HlsMediaChunk}. The {@link
* DrmInitData} passed should be that of an EXT-X-KEY tag that applies to the chunk, or {@code
* null} otherwise.
*
* <p>The final {@link DrmInitData} for subsequently queued samples is determined as followed:
*
* <ol>
* <li>It is initially set to {@code drmInitData}, unless {@code drmInitData} is null in which
* case it's set to {@link Format#drmInitData} of the upstream {@link Format}.
* <li>If the initial {@link DrmInitData} is non-null and {@link #overridingDrmInitData}
* contains an entry whose key matches the {@link DrmInitData#schemeType}, then the sample's
* {@link DrmInitData} is overridden to be this entry's value.
* </ol>
*
* <p>
*
* @param drmInitData The default {@link DrmInitData} for samples that are subsequently queued. If
* non-null then it takes precedence over {@link Format#drmInitData} of the upstream {@link
* Format}, but will still be overridden by a matching override in {@link
* #overridingDrmInitData}.
*/
public void setDrmInitData(@Nullable DrmInitData drmInitData) {
if (!Util.areEqual(this.drmInitData, drmInitData)) {
this.drmInitData = drmInitData;
for (int i = 0; i < sampleQueues.length; i++) {
if (sampleQueueIsAudioVideoFlags[i]) {
sampleQueues[i].setDrmInitData(drmInitData);
}
}
}
}
// Internal methods. // Internal methods.
@ -1280,6 +1327,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private static final class FormatAdjustingSampleQueue extends SampleQueue { private static final class FormatAdjustingSampleQueue extends SampleQueue {
private final Map<String, DrmInitData> overridingDrmInitData; private final Map<String, DrmInitData> overridingDrmInitData;
@Nullable private DrmInitData drmInitData;
public FormatAdjustingSampleQueue( public FormatAdjustingSampleQueue(
Allocator allocator, Allocator allocator,
@ -1289,9 +1337,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.overridingDrmInitData = overridingDrmInitData; this.overridingDrmInitData = overridingDrmInitData;
} }
public void setDrmInitData(@Nullable DrmInitData drmInitData) {
this.drmInitData = drmInitData;
invalidateUpstreamFormatAdjustment();
}
@Override @Override
public Format getAdjustedUpstreamFormat(Format format) { public Format getAdjustedUpstreamFormat(Format format) {
@Nullable DrmInitData drmInitData = format.drmInitData; @Nullable
DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData;
if (drmInitData != null) { if (drmInitData != null) {
@Nullable @Nullable
DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);