Support providing all keys via EXT-X-SESSION-KEY tags

PiperOrigin-RevId: 240333415
This commit is contained in:
olly 2019-03-26 13:11:55 +00:00 committed by Oliver Woodman
parent c04f5b9c5d
commit 07e3509dd9
9 changed files with 217 additions and 64 deletions

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -183,6 +184,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return new DrmInitData(schemeType, false, schemeDatas); return new DrmInitData(schemeType, false, schemeDatas);
} }
/**
* Returns an instance containing the {@link #schemeDatas} from both this and {@code other}. The
* {@link #schemeType} of the instances being merged must either match, or at least one scheme
* type must be {@code null}.
*
* @param drmInitData The instance to merge.
* @return The merged result.
*/
public DrmInitData merge(DrmInitData drmInitData) {
Assertions.checkState(
schemeType == null
|| drmInitData.schemeType == null
|| TextUtils.equals(schemeType, drmInitData.schemeType));
String mergedSchemeType = schemeType != null ? this.schemeType : drmInitData.schemeType;
SchemeData[] mergedSchemeDatas =
Util.nullSafeArrayConcatenation(schemeDatas, drmInitData.schemeDatas);
return new DrmInitData(mergedSchemeType, mergedSchemeDatas);
}
@Override @Override
public int hashCode() { public int hashCode() {
if (hashCode == 0) { if (hashCode == 0) {

View File

@ -314,6 +314,25 @@ public final class Util {
return Arrays.copyOf(input, length); return Arrays.copyOf(input, length);
} }
/**
* Concatenates two non-null type arrays.
*
* @param first The first array.
* @param second The second array.
* @return The concatenated result.
*/
@SuppressWarnings({"nullness:assignment.type.incompatible"})
public static <T> T[] nullSafeArrayConcatenation(T[] first, T[] second) {
T[] concatenation = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(
/* src= */ second,
/* srcPos= */ 0,
/* dest= */ concatenation,
/* destPos= */ first.length,
/* length= */ second.length);
return concatenation;
}
/** /**
* Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link * Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link
* Looper} thread. The method accepts partially initialized objects as callback under the * Looper} thread. The method accepts partially initialized objects as callback under the

View File

@ -16,9 +16,11 @@
package com.google.android.exoplayer2.source.hls; package com.google.android.exoplayer2.source.hls;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
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.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
@ -43,9 +45,11 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A {@link MediaPeriod} that loads an HLS stream. * A {@link MediaPeriod} that loads an HLS stream.
@ -64,6 +68,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final TimestampAdjusterProvider timestampAdjusterProvider; private final TimestampAdjusterProvider timestampAdjusterProvider;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final boolean allowChunklessPreparation; private final boolean allowChunklessPreparation;
private final boolean useSessionKeys;
private @Nullable Callback callback; private @Nullable Callback callback;
private int pendingPrepareCount; private int pendingPrepareCount;
@ -90,6 +95,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
* @param compositeSequenceableLoaderFactory A factory to create composite {@link * @param compositeSequenceableLoaderFactory A factory to create composite {@link
* SequenceableLoader}s for when this media source loads data from multiple streams. * SequenceableLoader}s for when this media source loads data from multiple streams.
* @param allowChunklessPreparation Whether chunkless preparation is allowed. * @param allowChunklessPreparation Whether chunkless preparation is allowed.
* @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
*/ */
public HlsMediaPeriod( public HlsMediaPeriod(
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
@ -100,7 +106,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
EventDispatcher eventDispatcher, EventDispatcher eventDispatcher,
Allocator allocator, Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
boolean allowChunklessPreparation) { boolean allowChunklessPreparation,
boolean useSessionKeys) {
this.extractorFactory = extractorFactory; this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker; this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
@ -110,6 +117,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
this.allocator = allocator; this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.allowChunklessPreparation = allowChunklessPreparation; this.allowChunklessPreparation = allowChunklessPreparation;
this.useSessionKeys = useSessionKeys;
compositeSequenceableLoader = compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
streamWrapperIndices = new IdentityHashMap<>(); streamWrapperIndices = new IdentityHashMap<>();
@ -427,7 +435,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
// Internal methods. // Internal methods.
private void buildAndPrepareSampleStreamWrappers(long positionUs) { private void buildAndPrepareSampleStreamWrappers(long positionUs) {
HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
Map<String, DrmInitData> overridingDrmInitData =
useSessionKeys
? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData)
: Collections.emptyMap();
boolean hasVariants = !masterPlaylist.variants.isEmpty(); boolean hasVariants = !masterPlaylist.variants.isEmpty();
List<HlsUrl> audioRenditions = masterPlaylist.audios; List<HlsUrl> audioRenditions = masterPlaylist.audios;
List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles; List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
@ -438,20 +451,33 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
if (hasVariants) { if (hasVariants) {
buildAndPrepareMainSampleStreamWrapper( buildAndPrepareMainSampleStreamWrapper(
masterPlaylist, positionUs, sampleStreamWrappers, manifestUrlIndicesPerWrapper); masterPlaylist,
positionUs,
sampleStreamWrappers,
manifestUrlIndicesPerWrapper,
overridingDrmInitData);
} }
// TODO: Build video stream wrappers here. // TODO: Build video stream wrappers here.
buildAndPrepareAudioSampleStreamWrappers( buildAndPrepareAudioSampleStreamWrappers(
positionUs, audioRenditions, sampleStreamWrappers, manifestUrlIndicesPerWrapper); positionUs,
audioRenditions,
sampleStreamWrappers,
manifestUrlIndicesPerWrapper,
overridingDrmInitData);
// Subtitle stream wrappers. We can always use master playlist information to prepare these. // Subtitle stream wrappers. We can always use master playlist information to prepare these.
for (int i = 0; i < subtitleRenditions.size(); i++) { for (int i = 0; i < subtitleRenditions.size(); i++) {
HlsUrl url = subtitleRenditions.get(i); HlsUrl url = subtitleRenditions.get(i);
HlsSampleStreamWrapper sampleStreamWrapper = HlsSampleStreamWrapper sampleStreamWrapper =
buildSampleStreamWrapper( buildSampleStreamWrapper(
C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, Collections.emptyList(), positionUs); C.TRACK_TYPE_TEXT,
new HlsUrl[] {url},
null,
Collections.emptyList(),
overridingDrmInitData,
positionUs);
manifestUrlIndicesPerWrapper.add(new int[] {i}); manifestUrlIndicesPerWrapper.add(new int[] {i});
sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrappers.add(sampleStreamWrapper);
sampleStreamWrapper.prepareWithMasterPlaylistInfo( sampleStreamWrapper.prepareWithMasterPlaylistInfo(
@ -495,12 +521,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
* which downloading should start. Ignored otherwise. * which downloading should start. Ignored otherwise.
* @param sampleStreamWrappers List to which the built main sample stream wrapper should be added. * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added.
* @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added. * @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added.
* @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type
* (i.e. {@link DrmInitData#schemeType}).
*/ */
private void buildAndPrepareMainSampleStreamWrapper( private void buildAndPrepareMainSampleStreamWrapper(
HlsMasterPlaylist masterPlaylist, HlsMasterPlaylist masterPlaylist,
long positionUs, long positionUs,
List<HlsSampleStreamWrapper> sampleStreamWrappers, List<HlsSampleStreamWrapper> sampleStreamWrappers,
List<int[]> manifestUrlIndicesPerWrapper) { List<int[]> manifestUrlIndicesPerWrapper,
Map<String, DrmInitData> overridingDrmInitData) {
int[] variantTypes = new int[masterPlaylist.variants.size()]; int[] variantTypes = new int[masterPlaylist.variants.size()];
int videoVariantCount = 0; int videoVariantCount = 0;
int audioVariantCount = 0; int audioVariantCount = 0;
@ -549,6 +578,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
selectedVariants, selectedVariants,
masterPlaylist.muxedAudioFormat, masterPlaylist.muxedAudioFormat,
masterPlaylist.muxedCaptionFormats, masterPlaylist.muxedCaptionFormats,
overridingDrmInitData,
positionUs); positionUs);
sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrappers.add(sampleStreamWrapper);
manifestUrlIndicesPerWrapper.add(selectedVariantIndices); manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
@ -616,8 +646,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
long positionUs, long positionUs,
List<HlsUrl> audioRenditions, List<HlsUrl> audioRenditions,
List<HlsSampleStreamWrapper> sampleStreamWrappers, List<HlsSampleStreamWrapper> sampleStreamWrappers,
List<int[]> manifestUrlsIndicesPerWrapper) { List<int[]> manifestUrlsIndicesPerWrapper,
Map<String, DrmInitData> overridingDrmInitData) {
ArrayList<HlsUrl> scratchRenditionList = ArrayList<HlsUrl> scratchRenditionList =
new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
ArrayList<Integer> scratchIndicesList = ArrayList<Integer> scratchIndicesList =
@ -651,6 +681,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
scratchRenditionList.toArray(new HlsUrl[0]), scratchRenditionList.toArray(new HlsUrl[0]),
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(), /* muxedCaptionFormats= */ Collections.emptyList(),
overridingDrmInitData,
positionUs); positionUs);
manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList)); manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList));
sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrappers.add(sampleStreamWrapper);
@ -666,8 +697,13 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
} }
} }
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, private HlsSampleStreamWrapper buildSampleStreamWrapper(
Format muxedAudioFormat, List<Format> muxedCaptionFormats, long positionUs) { int trackType,
HlsUrl[] variants,
Format muxedAudioFormat,
List<Format> muxedCaptionFormats,
Map<String, DrmInitData> overridingDrmInitData,
long positionUs) {
HlsChunkSource defaultChunkSource = HlsChunkSource defaultChunkSource =
new HlsChunkSource( new HlsChunkSource(
extractorFactory, extractorFactory,
@ -681,6 +717,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
trackType, trackType,
/* callback= */ this, /* callback= */ this,
defaultChunkSource, defaultChunkSource,
overridingDrmInitData,
allocator, allocator,
positionUs, positionUs,
muxedAudioFormat, muxedAudioFormat,
@ -688,6 +725,32 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
eventDispatcher); eventDispatcher);
} }
private static Map<String, DrmInitData> deriveOverridingDrmInitData(
List<DrmInitData> sessionKeyDrmInitData) {
ArrayList<DrmInitData> mutableSessionKeyDrmInitData = new ArrayList<>(sessionKeyDrmInitData);
HashMap<String, DrmInitData> drmInitDataBySchemeType = new HashMap<>();
for (int i = 0; i < mutableSessionKeyDrmInitData.size(); i++) {
DrmInitData drmInitData = sessionKeyDrmInitData.get(i);
String scheme = drmInitData.schemeType;
// Merge any subsequent drmInitData instances that have the same scheme type. This is valid
// due to the assumptions documented on HlsMediaSource.Builder.setUseSessionKeys, and is
// necessary to get data for different CDNs (e.g. Widevine and PlayReady) into a single
// drmInitData.
int j = i + 1;
while (j < mutableSessionKeyDrmInitData.size()) {
DrmInitData nextDrmInitData = mutableSessionKeyDrmInitData.get(j);
if (TextUtils.equals(nextDrmInitData.schemeType, scheme)) {
drmInitData = drmInitData.merge(nextDrmInitData);
mutableSessionKeyDrmInitData.remove(j);
} else {
j++;
}
}
drmInitDataBySchemeType.put(scheme, drmInitData);
}
return drmInitDataBySchemeType;
}
private static Format deriveVideoFormat(Format variantFormat) { private static Format deriveVideoFormat(Format variantFormat) {
String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
String sampleMimeType = MimeTypes.getMediaMimeType(codecs); String sampleMimeType = MimeTypes.getMediaMimeType(codecs);

View File

@ -67,6 +67,7 @@ public final class HlsMediaSource extends BaseMediaSource
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation; private boolean allowChunklessPreparation;
private boolean useSessionKeys;
private boolean isCreateCalled; private boolean isCreateCalled;
@Nullable private Object tag; @Nullable private Object tag;
@ -236,6 +237,20 @@ public final class HlsMediaSource extends BaseMediaSource
return this; return this;
} }
/**
* Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's
* assumed that any single session key declared in the master playlist can be used to obtain all
* of the keys required for playback. For media where this is not true, this option should not
* be enabled.
*
* @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
* @return This factory, for convenience.
*/
public Factory setUseSessionKeys(boolean useSessionKeys) {
this.useSessionKeys = useSessionKeys;
return this;
}
/** /**
* Returns a new {@link HlsMediaSource} using the current parameters. * Returns a new {@link HlsMediaSource} using the current parameters.
* *
@ -257,6 +272,7 @@ public final class HlsMediaSource extends BaseMediaSource
playlistTrackerFactory.createTracker( playlistTrackerFactory.createTracker(
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
allowChunklessPreparation, allowChunklessPreparation,
useSessionKeys,
tag); tag);
} }
@ -289,6 +305,7 @@ public final class HlsMediaSource extends BaseMediaSource
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final boolean allowChunklessPreparation; private final boolean allowChunklessPreparation;
private final boolean useSessionKeys;
private final HlsPlaylistTracker playlistTracker; private final HlsPlaylistTracker playlistTracker;
private final @Nullable Object tag; private final @Nullable Object tag;
@ -302,6 +319,7 @@ public final class HlsMediaSource extends BaseMediaSource
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
HlsPlaylistTracker playlistTracker, HlsPlaylistTracker playlistTracker,
boolean allowChunklessPreparation, boolean allowChunklessPreparation,
boolean useSessionKeys,
@Nullable Object tag) { @Nullable Object tag) {
this.manifestUri = manifestUri; this.manifestUri = manifestUri;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
@ -310,6 +328,7 @@ public final class HlsMediaSource extends BaseMediaSource
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.playlistTracker = playlistTracker; this.playlistTracker = playlistTracker;
this.allowChunklessPreparation = allowChunklessPreparation; this.allowChunklessPreparation = allowChunklessPreparation;
this.useSessionKeys = useSessionKeys;
this.tag = tag; this.tag = tag;
} }
@ -343,7 +362,8 @@ public final class HlsMediaSource extends BaseMediaSource
eventDispatcher, eventDispatcher,
allocator, allocator,
compositeSequenceableLoaderFactory, compositeSequenceableLoaderFactory,
allowChunklessPreparation); allowChunklessPreparation,
useSessionKeys);
} }
@Override @Override

View File

@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
@ -52,6 +53,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
@ -102,6 +104,7 @@ import java.util.List;
private final Runnable onTracksEndedRunnable; private final Runnable onTracksEndedRunnable;
private final Handler handler; private final Handler handler;
private final ArrayList<HlsSampleStream> hlsSampleStreams; private final ArrayList<HlsSampleStream> hlsSampleStreams;
private final Map<String, DrmInitData> overridingDrmInitData;
private SampleQueue[] sampleQueues; private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds; private int[] sampleQueueTrackIds;
@ -144,6 +147,10 @@ import java.util.List;
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
* @param callback A callback for the wrapper. * @param callback A callback for the wrapper.
* @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained.
* @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type
* (i.e. {@link DrmInitData#schemeType}). If the stream has {@link DrmInitData} and uses a
* protection scheme type for which overriding {@link DrmInitData} is provided, then the
* stream's {@link DrmInitData} will be overridden.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param positionUs The position from which to start loading media. * @param positionUs The position from which to start loading media.
* @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist.
@ -154,6 +161,7 @@ import java.util.List;
int trackType, int trackType,
Callback callback, Callback callback,
HlsChunkSource chunkSource, HlsChunkSource chunkSource,
Map<String, DrmInitData> overridingDrmInitData,
Allocator allocator, Allocator allocator,
long positionUs, long positionUs,
Format muxedAudioFormat, Format muxedAudioFormat,
@ -162,6 +170,7 @@ import java.util.List;
this.trackType = trackType; this.trackType = trackType;
this.callback = callback; this.callback = callback;
this.chunkSource = chunkSource; this.chunkSource = chunkSource;
this.overridingDrmInitData = overridingDrmInitData;
this.allocator = allocator; this.allocator = allocator;
this.muxedAudioFormat = muxedAudioFormat; this.muxedAudioFormat = muxedAudioFormat;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
@ -484,18 +493,28 @@ import java.util.List;
int result = int result =
sampleQueues[sampleQueueIndex].read( sampleQueues[sampleQueueIndex].read(
formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs);
if (result == C.RESULT_FORMAT_READ && sampleQueueIndex == primarySampleQueueIndex) { if (result == C.RESULT_FORMAT_READ) {
// Fill in primary sample format with information from the track format. Format format = formatHolder.format;
int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId(); if (sampleQueueIndex == primarySampleQueueIndex) {
int chunkIndex = 0; // Fill in primary sample format with information from the track format.
while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) { int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId();
chunkIndex++; int chunkIndex = 0;
while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) {
chunkIndex++;
}
Format trackFormat =
chunkIndex < mediaChunks.size()
? mediaChunks.get(chunkIndex).trackFormat
: upstreamTrackFormat;
format = format.copyWithManifestFormatInfo(trackFormat);
} }
Format trackFormat = if (format.drmInitData != null) {
chunkIndex < mediaChunks.size() DrmInitData drmInitData = overridingDrmInitData.get(format.drmInitData.schemeType);
? mediaChunks.get(chunkIndex).trackFormat if (drmInitData != null) {
: upstreamTrackFormat; format = format.copyWithDrmInitData(drmInitData);
formatHolder.format = formatHolder.format.copyWithManifestFormatInfo(trackFormat); }
}
formatHolder.format = format;
} }
return result; return result;
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
@ -37,7 +38,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(), /* muxedCaptionFormats= */ Collections.emptyList(),
/* hasIndependentSegments= */ false, /* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap()); /* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
public static final int GROUP_INDEX_VARIANT = 0; public static final int GROUP_INDEX_VARIANT = 0;
public static final int GROUP_INDEX_AUDIO = 1; public static final int GROUP_INDEX_AUDIO = 1;
@ -122,6 +124,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Format> muxedCaptionFormats; public final List<Format> muxedCaptionFormats;
/** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
public final Map<String, String> variableDefinitions; public final Map<String, String> variableDefinitions;
/** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
public final List<DrmInitData> sessionKeyDrmInitData;
/** /**
* @param baseUri See {@link #baseUri}. * @param baseUri See {@link #baseUri}.
@ -133,6 +137,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
* @param muxedCaptionFormats See {@link #muxedCaptionFormats}. * @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
* @param hasIndependentSegments See {@link #hasIndependentSegments}. * @param hasIndependentSegments See {@link #hasIndependentSegments}.
* @param variableDefinitions See {@link #variableDefinitions}. * @param variableDefinitions See {@link #variableDefinitions}.
* @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.
*/ */
public HlsMasterPlaylist( public HlsMasterPlaylist(
String baseUri, String baseUri,
@ -143,7 +148,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
Format muxedAudioFormat, Format muxedAudioFormat,
List<Format> muxedCaptionFormats, List<Format> muxedCaptionFormats,
boolean hasIndependentSegments, boolean hasIndependentSegments,
Map<String, String> variableDefinitions) { Map<String, String> variableDefinitions,
List<DrmInitData> sessionKeyDrmInitData) {
super(baseUri, tags, hasIndependentSegments); super(baseUri, tags, hasIndependentSegments);
this.variants = Collections.unmodifiableList(variants); this.variants = Collections.unmodifiableList(variants);
this.audios = Collections.unmodifiableList(audios); this.audios = Collections.unmodifiableList(audios);
@ -152,6 +158,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
this.muxedCaptionFormats = muxedCaptionFormats != null this.muxedCaptionFormats = muxedCaptionFormats != null
? Collections.unmodifiableList(muxedCaptionFormats) : null; ? Collections.unmodifiableList(muxedCaptionFormats) : null;
this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
} }
@Override @Override
@ -165,7 +172,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
muxedAudioFormat, muxedAudioFormat,
muxedCaptionFormats, muxedCaptionFormats,
hasIndependentSegments, hasIndependentSegments,
variableDefinitions); variableDefinitions,
sessionKeyDrmInitData);
} }
/** /**
@ -186,7 +194,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ false, /* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap()); /* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
} }
private static List<HlsUrl> copyRenditionsList( private static List<HlsUrl> copyRenditionsList(

View File

@ -34,7 +34,6 @@ import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -73,6 +72,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String TAG_START = "#EXT-X-START"; private static final String TAG_START = "#EXT-X-START";
private static final String TAG_ENDLIST = "#EXT-X-ENDLIST"; private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
private static final String TAG_KEY = "#EXT-X-KEY"; private static final String TAG_KEY = "#EXT-X-KEY";
private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY";
private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE"; private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
private static final String TAG_GAP = "#EXT-X-GAP"; private static final String TAG_GAP = "#EXT-X-GAP";
@ -253,6 +253,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
ArrayList<String> mediaTags = new ArrayList<>(); ArrayList<String> mediaTags = new ArrayList<>();
ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
ArrayList<String> tags = new ArrayList<>(); ArrayList<String> tags = new ArrayList<>();
Format muxedAudioFormat = null; Format muxedAudioFormat = null;
List<Format> muxedCaptionFormats = null; List<Format> muxedCaptionFormats = null;
@ -278,6 +279,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
// Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF
// tags. // tags.
mediaTags.add(line); mediaTags.add(line);
} else if (line.startsWith(TAG_SESSION_KEY)) {
String keyFormat =
parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
if (schemeData != null) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String scheme = parseEncryptionScheme(method);
sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));
}
} else if (line.startsWith(TAG_STREAM_INF)) { } else if (line.startsWith(TAG_STREAM_INF)) {
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
@ -423,6 +433,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (noClosedCaptions) { if (noClosedCaptions) {
muxedCaptionFormats = Collections.emptyList(); muxedCaptionFormats = Collections.emptyList();
} }
return new HlsMasterPlaylist( return new HlsMasterPlaylist(
baseUri, baseUri,
tags, tags,
@ -432,7 +443,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
muxedAudioFormat, muxedAudioFormat,
muxedCaptionFormats, muxedCaptionFormats,
hasIndependentSegmentsTag, hasIndependentSegmentsTag,
variableDefinitions); variableDefinitions,
sessionKeyDrmInitData);
} }
private static HlsMediaPlaylist parseMediaPlaylist( private static HlsMediaPlaylist parseMediaPlaylist(
@ -557,17 +569,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
} else { } else {
if (encryptionScheme == null) { if (encryptionScheme == null) {
encryptionScheme = encryptionScheme = parseEncryptionScheme(method);
METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
? C.CENC_TYPE_cenc
: C.CENC_TYPE_cbcs;
}
SchemeData schemeData;
if (KEYFORMAT_PLAYREADY.equals(keyFormat)) {
schemeData = parsePlayReadySchemeData(line, variableDefinitions);
} else {
schemeData = parseWidevineSchemeData(line, keyFormat, variableDefinitions);
} }
SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
if (schemeData != null) { if (schemeData != null) {
cachedDrmInitData = null; cachedDrmInitData = null;
currentSchemeDatas.put(keyFormat, schemeData); currentSchemeDatas.put(keyFormat, schemeData);
@ -713,40 +717,35 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
: Format.NO_VALUE; : Format.NO_VALUE;
} }
private static @Nullable SchemeData parsePlayReadySchemeData( @Nullable
String line, Map<String, String> variableDefinitions) throws ParserException { private static SchemeData parseDrmSchemeData(
String keyFormatVersions =
parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions);
if (!"1".equals(keyFormatVersions)) {
// Not supported.
return null;
}
String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);
byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);
return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);
}
private static @Nullable SchemeData parseWidevineSchemeData(
String line, String keyFormat, Map<String, String> variableDefinitions) String line, String keyFormat, Map<String, String> variableDefinitions)
throws ParserException { throws ParserException {
String keyFormatVersions =
parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions);
if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
return new SchemeData( return new SchemeData(
C.WIDEVINE_UUID, C.WIDEVINE_UUID,
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_MP4,
Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT));
} } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) {
if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { return new SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line));
try { } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) {
return new SchemeData(C.WIDEVINE_UUID, "hls", line.getBytes(C.UTF8_NAME)); String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions);
} catch (UnsupportedEncodingException e) { byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT);
throw new ParserException(e); byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data);
} return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData);
} }
return null; return null;
} }
private static String parseEncryptionScheme(String method) {
return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method)
? C.CENC_TYPE_cenc
: C.CENC_TYPE_cbcs;
}
private static int parseIntAttr(String line, Pattern pattern) throws ParserException { private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap())); return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap()));
} }

View File

@ -88,7 +88,8 @@ public final class HlsMediaPeriodTest {
/* mediaTimeOffsetMs= */ 0), /* mediaTimeOffsetMs= */ 0),
mock(Allocator.class), mock(Allocator.class),
mock(CompositeSequenceableLoaderFactory.class), mock(CompositeSequenceableLoaderFactory.class),
/* allowChunklessPreparation =*/ true); /* allowChunklessPreparation =*/ true,
/* useSessionKeys= */ false);
}; };
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
@ -110,7 +111,8 @@ public final class HlsMediaPeriodTest {
muxedAudioFormat, muxedAudioFormat,
muxedCaptionFormats, muxedCaptionFormats,
/* hasIndependentSegments= */ true, /* hasIndependentSegments= */ true,
/* variableDefinitions= */ Collections.emptyMap()); /* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
} }
private static HlsUrl createMuxedVideoAudioVariantHlsUrl(int bitrate) { private static HlsUrl createMuxedVideoAudioVariantHlsUrl(int bitrate) {

View File

@ -456,7 +456,8 @@ public class HlsMediaPlaylistParserTest {
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ true, /* hasIndependentSegments= */ true,
/* variableDefinitions */ Collections.emptyMap()); /* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
HlsMediaPlaylist playlistWithInheritance = HlsMediaPlaylist playlistWithInheritance =
(HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream);
assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();
@ -515,7 +516,8 @@ public class HlsMediaPlaylistParserTest {
/* muxedAudioFormat= */ null, /* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(), /* muxedCaptionFormats= */ Collections.emptyList(),
/* hasIndependentSegments= */ false, /* hasIndependentSegments= */ false,
variableDefinitions); variableDefinitions,
/* sessionKeyDrmInitData= */ Collections.emptyList());
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream);
for (int i = 1; i <= 4; i++) { for (int i = 1; i <= 4; i++) {