From 07e3509dd9e4fbf2077a8cf3222efe68b73d00c2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 26 Mar 2019 13:11:55 +0000 Subject: [PATCH] Support providing all keys via EXT-X-SESSION-KEY tags PiperOrigin-RevId: 240333415 --- .../android/exoplayer2/drm/DrmInitData.java | 20 +++++ .../google/android/exoplayer2/util/Util.java | 19 +++++ .../exoplayer2/source/hls/HlsMediaPeriod.java | 83 ++++++++++++++++--- .../exoplayer2/source/hls/HlsMediaSource.java | 22 ++++- .../source/hls/HlsSampleStreamWrapper.java | 41 ++++++--- .../hls/playlist/HlsMasterPlaylist.java | 17 +++- .../hls/playlist/HlsPlaylistParser.java | 67 ++++++++------- .../source/hls/HlsMediaPeriodTest.java | 6 +- .../playlist/HlsMediaPlaylistParserTest.java | 6 +- 9 files changed, 217 insertions(+), 64 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 60701be63c..4fde9f05d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; +import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; @@ -183,6 +184,25 @@ public final class DrmInitData implements Comparator, Parcelable { 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 public int hashCode() { if (hashCode == 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index d0f6766e34..854d63ae04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -314,6 +314,25 @@ public final class Util { 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[] 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 * Looper} thread. The method accepts partially initialized objects as callback under the diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index c6df726c2c..78551e6079 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -16,9 +16,11 @@ package com.google.android.exoplayer2.source.hls; import androidx.annotation.Nullable; +import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; 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.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; @@ -43,9 +45,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; /** * 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 CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; + private final boolean useSessionKeys; private @Nullable Callback callback; private int pendingPrepareCount; @@ -90,6 +95,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * @param compositeSequenceableLoaderFactory A factory to create composite {@link * SequenceableLoader}s for when this media source loads data from multiple streams. * @param allowChunklessPreparation Whether chunkless preparation is allowed. + * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags. */ public HlsMediaPeriod( HlsExtractorFactory extractorFactory, @@ -100,7 +106,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper EventDispatcher eventDispatcher, Allocator allocator, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - boolean allowChunklessPreparation) { + boolean allowChunklessPreparation, + boolean useSessionKeys) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; @@ -110,6 +117,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.allowChunklessPreparation = allowChunklessPreparation; + this.useSessionKeys = useSessionKeys; compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamWrapperIndices = new IdentityHashMap<>(); @@ -427,7 +435,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Internal methods. private void buildAndPrepareSampleStreamWrappers(long positionUs) { - HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); + HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); + Map overridingDrmInitData = + useSessionKeys + ? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData) + : Collections.emptyMap(); + boolean hasVariants = !masterPlaylist.variants.isEmpty(); List audioRenditions = masterPlaylist.audios; List subtitleRenditions = masterPlaylist.subtitles; @@ -438,20 +451,33 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (hasVariants) { buildAndPrepareMainSampleStreamWrapper( - masterPlaylist, positionUs, sampleStreamWrappers, manifestUrlIndicesPerWrapper); + masterPlaylist, + positionUs, + sampleStreamWrappers, + manifestUrlIndicesPerWrapper, + overridingDrmInitData); } // TODO: Build video stream wrappers here. buildAndPrepareAudioSampleStreamWrappers( - positionUs, audioRenditions, sampleStreamWrappers, manifestUrlIndicesPerWrapper); + positionUs, + audioRenditions, + sampleStreamWrappers, + manifestUrlIndicesPerWrapper, + overridingDrmInitData); // Subtitle stream wrappers. We can always use master playlist information to prepare these. for (int i = 0; i < subtitleRenditions.size(); i++) { HlsUrl url = subtitleRenditions.get(i); HlsSampleStreamWrapper sampleStreamWrapper = 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}); sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrapper.prepareWithMasterPlaylistInfo( @@ -495,12 +521,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * which downloading should start. Ignored otherwise. * @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 overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type + * (i.e. {@link DrmInitData#schemeType}). */ private void buildAndPrepareMainSampleStreamWrapper( HlsMasterPlaylist masterPlaylist, long positionUs, List sampleStreamWrappers, - List manifestUrlIndicesPerWrapper) { + List manifestUrlIndicesPerWrapper, + Map overridingDrmInitData) { int[] variantTypes = new int[masterPlaylist.variants.size()]; int videoVariantCount = 0; int audioVariantCount = 0; @@ -549,6 +578,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper selectedVariants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, + overridingDrmInitData, positionUs); sampleStreamWrappers.add(sampleStreamWrapper); manifestUrlIndicesPerWrapper.add(selectedVariantIndices); @@ -616,8 +646,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper long positionUs, List audioRenditions, List sampleStreamWrappers, - List manifestUrlsIndicesPerWrapper) { - + List manifestUrlsIndicesPerWrapper, + Map overridingDrmInitData) { ArrayList scratchRenditionList = new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); ArrayList scratchIndicesList = @@ -651,6 +681,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper scratchRenditionList.toArray(new HlsUrl[0]), /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ Collections.emptyList(), + overridingDrmInitData, positionUs); manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList)); sampleStreamWrappers.add(sampleStreamWrapper); @@ -666,8 +697,13 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } - private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) { + private HlsSampleStreamWrapper buildSampleStreamWrapper( + int trackType, + HlsUrl[] variants, + Format muxedAudioFormat, + List muxedCaptionFormats, + Map overridingDrmInitData, + long positionUs) { HlsChunkSource defaultChunkSource = new HlsChunkSource( extractorFactory, @@ -681,6 +717,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper trackType, /* callback= */ this, defaultChunkSource, + overridingDrmInitData, allocator, positionUs, muxedAudioFormat, @@ -688,6 +725,32 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper eventDispatcher); } + private static Map deriveOverridingDrmInitData( + List sessionKeyDrmInitData) { + ArrayList mutableSessionKeyDrmInitData = new ArrayList<>(sessionKeyDrmInitData); + HashMap 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) { String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); String sampleMimeType = MimeTypes.getMediaMimeType(codecs); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 114a64f99f..b6b874b293 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -67,6 +67,7 @@ public final class HlsMediaSource extends BaseMediaSource private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; + private boolean useSessionKeys; private boolean isCreateCalled; @Nullable private Object tag; @@ -236,6 +237,20 @@ public final class HlsMediaSource extends BaseMediaSource 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. * @@ -257,6 +272,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), allowChunklessPreparation, + useSessionKeys, tag); } @@ -289,6 +305,7 @@ public final class HlsMediaSource extends BaseMediaSource private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; + private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; private final @Nullable Object tag; @@ -302,6 +319,7 @@ public final class HlsMediaSource extends BaseMediaSource LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, + boolean useSessionKeys, @Nullable Object tag) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; @@ -310,6 +328,7 @@ public final class HlsMediaSource extends BaseMediaSource this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; + this.useSessionKeys = useSessionKeys; this.tag = tag; } @@ -343,7 +362,8 @@ public final class HlsMediaSource extends BaseMediaSource eventDispatcher, allocator, compositeSequenceableLoaderFactory, - allowChunklessPreparation); + allowChunklessPreparation, + useSessionKeys); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index c5e0c02925..dc72458747 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; 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.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; @@ -52,6 +53,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; /** * 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 Handler handler; private final ArrayList hlsSampleStreams; + private final Map overridingDrmInitData; private SampleQueue[] sampleQueues; 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 callback A callback for the wrapper. * @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 positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. @@ -154,6 +161,7 @@ import java.util.List; int trackType, Callback callback, HlsChunkSource chunkSource, + Map overridingDrmInitData, Allocator allocator, long positionUs, Format muxedAudioFormat, @@ -162,6 +170,7 @@ import java.util.List; this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; + this.overridingDrmInitData = overridingDrmInitData; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; @@ -484,18 +493,28 @@ import java.util.List; int result = sampleQueues[sampleQueueIndex].read( formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); - if (result == C.RESULT_FORMAT_READ && sampleQueueIndex == primarySampleQueueIndex) { - // Fill in primary sample format with information from the track format. - int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId(); - int chunkIndex = 0; - while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) { - chunkIndex++; + if (result == C.RESULT_FORMAT_READ) { + Format format = formatHolder.format; + if (sampleQueueIndex == primarySampleQueueIndex) { + // Fill in primary sample format with information from the track format. + int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId(); + 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 = - chunkIndex < mediaChunks.size() - ? mediaChunks.get(chunkIndex).trackFormat - : upstreamTrackFormat; - formatHolder.format = formatHolder.format.copyWithManifestFormatInfo(trackFormat); + if (format.drmInitData != null) { + DrmInitData drmInitData = overridingDrmInitData.get(format.drmInitData.schemeType); + if (drmInitData != null) { + format = format.copyWithDrmInitData(drmInitData); + } + } + formatHolder.format = format; } return result; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 0e7f07cd7c..5ebfc48e4d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls.playlist; 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.util.MimeTypes; import java.util.ArrayList; @@ -37,7 +38,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ Collections.emptyList(), /* 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_AUDIO = 1; @@ -122,6 +124,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List muxedCaptionFormats; /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ public final Map variableDefinitions; + /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ + public final List sessionKeyDrmInitData; /** * @param baseUri See {@link #baseUri}. @@ -133,6 +137,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. * @param hasIndependentSegments See {@link #hasIndependentSegments}. * @param variableDefinitions See {@link #variableDefinitions}. + * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. */ public HlsMasterPlaylist( String baseUri, @@ -143,7 +148,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { Format muxedAudioFormat, List muxedCaptionFormats, boolean hasIndependentSegments, - Map variableDefinitions) { + Map variableDefinitions, + List sessionKeyDrmInitData) { super(baseUri, tags, hasIndependentSegments); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); @@ -152,6 +158,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { this.muxedCaptionFormats = muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null; this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); + this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); } @Override @@ -165,7 +172,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { muxedAudioFormat, muxedCaptionFormats, hasIndependentSegments, - variableDefinitions); + variableDefinitions, + sessionKeyDrmInitData); } /** @@ -186,7 +194,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ null, /* hasIndependentSegments= */ false, - /* variableDefinitions= */ Collections.emptyMap()); + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); } private static List copyRenditionsList( diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index fd49e10bea..215d80c5c0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -34,7 +34,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -73,6 +72,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); ArrayList mediaTags = new ArrayList<>(); + ArrayList sessionKeyDrmInitData = new ArrayList<>(); ArrayList tags = new ArrayList<>(); Format muxedAudioFormat = null; List muxedCaptionFormats = null; @@ -278,6 +279,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) throws ParserException { - 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( + @Nullable + private static SchemeData parseDrmSchemeData( String line, String keyFormat, Map variableDefinitions) throws ParserException { + String keyFormatVersions = + parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions); if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); return new SchemeData( C.WIDEVINE_UUID, MimeTypes.VIDEO_MP4, Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); - } - if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { - try { - return new SchemeData(C.WIDEVINE_UUID, "hls", line.getBytes(C.UTF8_NAME)); - } catch (UnsupportedEncodingException e) { - throw new ParserException(e); - } + } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { + return new SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line)); + } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) { + 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); } 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 { return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap())); } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index a3e2af53b6..35d0c53bbf 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -88,7 +88,8 @@ public final class HlsMediaPeriodTest { /* mediaTimeOffsetMs= */ 0), mock(Allocator.class), mock(CompositeSequenceableLoaderFactory.class), - /* allowChunklessPreparation =*/ true); + /* allowChunklessPreparation =*/ true, + /* useSessionKeys= */ false); }; MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( @@ -110,7 +111,8 @@ public final class HlsMediaPeriodTest { muxedAudioFormat, muxedCaptionFormats, /* hasIndependentSegments= */ true, - /* variableDefinitions= */ Collections.emptyMap()); + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); } private static HlsUrl createMuxedVideoAudioVariantHlsUrl(int bitrate) { diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 83d9731dca..384408a0af 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -456,7 +456,8 @@ public class HlsMediaPlaylistParserTest { /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ null, /* hasIndependentSegments= */ true, - /* variableDefinitions */ Collections.emptyMap()); + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); HlsMediaPlaylist playlistWithInheritance = (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); @@ -515,7 +516,8 @@ public class HlsMediaPlaylistParserTest { /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ Collections.emptyList(), /* hasIndependentSegments= */ false, - variableDefinitions); + variableDefinitions, + /* sessionKeyDrmInitData= */ Collections.emptyList()); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); for (int i = 1; i <= 4; i++) {