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
([#6724](https://github.com/google/ExoPlayer/pull/6724)).
* 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
speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* MP3: Add `IndexSeeker` for accurate seeks in VBR streams

View File

@ -804,10 +804,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
extractor =
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);
}
}

View File

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

View File

@ -47,7 +47,11 @@ public final class FragmentedMp4ExtractorTest {
ExtractorFactory extractorFactory =
getExtractorFactory(
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");
}
@ -64,6 +68,11 @@ public final class FragmentedMp4ExtractorTest {
}
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 androidx.annotation.Nullable;
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.ExtractorInput;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
@ -89,7 +88,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
Uri uri,
Format format,
@Nullable List<Format> muxedCaptionFormats,
@Nullable DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster,
Map<String, List<String>> responseHeaders,
ExtractorInput extractorInput)
@ -111,8 +109,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// Try selecting the extractor by the file extension.
Extractor extractorByFileExtension =
createExtractorByFileExtension(
uri, format, muxedCaptionFormats, drmInitData, timestampAdjuster);
createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster);
extractorInput.resetPeekPosition();
if (sniffQuietly(extractorByFileExtension, extractorInput)) {
return buildResult(extractorByFileExtension);
@ -159,7 +156,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
FragmentedMp4Extractor fragmentedMp4Extractor =
createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats);
createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
return buildResult(fragmentedMp4Extractor);
}
@ -186,7 +183,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
Uri uri,
Format format,
@Nullable List<Format> muxedCaptionFormats,
@Nullable DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster) {
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
@ -209,8 +205,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
|| lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)
|| lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
return createFragmentedMp4Extractor(
timestampAdjuster, format, drmInitData, muxedCaptionFormats);
return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
} else {
// For any other file extension, we assume TS format.
return createTsExtractor(
@ -270,7 +265,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
private static FragmentedMp4Extractor createFragmentedMp4Extractor(
TimestampAdjuster timestampAdjuster,
Format format,
@Nullable DrmInitData drmInitData,
@Nullable List<Format> muxedCaptionFormats) {
// 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.
@ -278,7 +272,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
/* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,
timestampAdjuster,
/* sideloadedTrack= */ null,
drmInitData,
muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());
}

View File

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.hls;
import android.net.Uri;
import androidx.annotation.Nullable;
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.ExtractorInput;
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 muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* 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 responseHeaders The HTTP response headers associated with the media segment or
* initialization section to extract.
@ -87,7 +85,6 @@ public interface HlsExtractorFactory {
Uri uri,
Format format,
@Nullable List<Format> muxedCaptionFormats,
@Nullable DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster,
Map<String, List<String>> responseHeaders,
ExtractorInput sniffingExtractorInput)

View File

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

View File

@ -128,7 +128,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final ArrayList<HlsSampleStream> hlsSampleStreams;
private final Map<String, DrmInitData> overridingDrmInitData;
private SampleQueue[] sampleQueues;
private FormatAdjustingSampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds;
private Set<Integer> sampleQueueMappingDoneByType;
private SparseIntArray sampleQueueIndicesByType;
@ -162,6 +162,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// Accessed only by the loading thread.
private boolean tracksEnded;
private long sampleOffsetUs;
@Nullable private DrmInitData drmInitData;
private int chunkUid;
/**
@ -207,7 +208,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueTrackIds = new int[0];
sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size());
sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size());
sampleQueues = new SampleQueue[0];
sampleQueues = new FormatAdjustingSampleQueue[0];
sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>();
@ -904,8 +905,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private SampleQueue createSampleQueue(int id, int type) {
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);
if (isAudioVideo) {
trackOutput.setDrmInitData(drmInitData);
}
trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.sourceId(chunkUid);
trackOutput.setUpstreamFormatChangeListener(this);
@ -913,8 +918,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueTrackIds[trackCount] = id;
sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);
sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
sampleQueueIsAudioVideoFlags[trackCount] =
type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo;
haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount];
sampleQueueMappingDoneByType.add(type);
sampleQueueIndicesByType.append(type, trackCount);
@ -951,10 +955,53 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
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) {
this.sampleOffsetUs = sampleOffsetUs;
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
if (this.sampleOffsetUs != sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs;
for (SampleQueue sampleQueue : sampleQueues) {
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);
}
}
}
}
@ -1280,6 +1327,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private static final class FormatAdjustingSampleQueue extends SampleQueue {
private final Map<String, DrmInitData> overridingDrmInitData;
@Nullable private DrmInitData drmInitData;
public FormatAdjustingSampleQueue(
Allocator allocator,
@ -1289,9 +1337,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.overridingDrmInitData = overridingDrmInitData;
}
public void setDrmInitData(@Nullable DrmInitData drmInitData) {
this.drmInitData = drmInitData;
invalidateUpstreamFormatAdjustment();
}
@Override
public Format getAdjustedUpstreamFormat(Format format) {
@Nullable DrmInitData drmInitData = format.drmInitData;
@Nullable
DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData;
if (drmInitData != null) {
@Nullable
DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);