mirror of
https://github.com/androidx/media.git
synced 2025-05-11 09:39:52 +08:00
Support providing all keys via EXT-X-SESSION-KEY tags
PiperOrigin-RevId: 240333415
This commit is contained in:
parent
c04f5b9c5d
commit
07e3509dd9
@ -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<SchemeData>, 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) {
|
||||
|
@ -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> 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
|
||||
|
@ -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<String, DrmInitData> overridingDrmInitData =
|
||||
useSessionKeys
|
||||
? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData)
|
||||
: Collections.emptyMap();
|
||||
|
||||
boolean hasVariants = !masterPlaylist.variants.isEmpty();
|
||||
List<HlsUrl> audioRenditions = masterPlaylist.audios;
|
||||
List<HlsUrl> 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<HlsSampleStreamWrapper> sampleStreamWrappers,
|
||||
List<int[]> manifestUrlIndicesPerWrapper) {
|
||||
List<int[]> manifestUrlIndicesPerWrapper,
|
||||
Map<String, DrmInitData> 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<HlsUrl> audioRenditions,
|
||||
List<HlsSampleStreamWrapper> sampleStreamWrappers,
|
||||
List<int[]> manifestUrlsIndicesPerWrapper) {
|
||||
|
||||
List<int[]> manifestUrlsIndicesPerWrapper,
|
||||
Map<String, DrmInitData> overridingDrmInitData) {
|
||||
ArrayList<HlsUrl> scratchRenditionList =
|
||||
new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
|
||||
ArrayList<Integer> 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<Format> muxedCaptionFormats, long positionUs) {
|
||||
private HlsSampleStreamWrapper buildSampleStreamWrapper(
|
||||
int trackType,
|
||||
HlsUrl[] variants,
|
||||
Format muxedAudioFormat,
|
||||
List<Format> muxedCaptionFormats,
|
||||
Map<String, DrmInitData> 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<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) {
|
||||
String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
|
||||
String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
|
||||
|
@ -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
|
||||
|
@ -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<HlsSampleStream> hlsSampleStreams;
|
||||
private final Map<String, DrmInitData> 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<String, DrmInitData> 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,7 +493,9 @@ import java.util.List;
|
||||
int result =
|
||||
sampleQueues[sampleQueueIndex].read(
|
||||
formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs);
|
||||
if (result == C.RESULT_FORMAT_READ && sampleQueueIndex == primarySampleQueueIndex) {
|
||||
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;
|
||||
@ -495,7 +506,15 @@ import java.util.List;
|
||||
chunkIndex < mediaChunks.size()
|
||||
? mediaChunks.get(chunkIndex).trackFormat
|
||||
: upstreamTrackFormat;
|
||||
formatHolder.format = formatHolder.format.copyWithManifestFormatInfo(trackFormat);
|
||||
format = 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;
|
||||
}
|
||||
|
@ -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<Format> muxedCaptionFormats;
|
||||
/** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
|
||||
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}.
|
||||
@ -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<Format> muxedCaptionFormats,
|
||||
boolean hasIndependentSegments,
|
||||
Map<String, String> variableDefinitions) {
|
||||
Map<String, String> variableDefinitions,
|
||||
List<DrmInitData> 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<HlsUrl> copyRenditionsList(
|
||||
|
@ -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<HlsPlayli
|
||||
private static final String TAG_START = "#EXT-X-START";
|
||||
private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
|
||||
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_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> subtitles = new ArrayList<>();
|
||||
ArrayList<String> mediaTags = new ArrayList<>();
|
||||
ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
|
||||
ArrayList<String> tags = new ArrayList<>();
|
||||
Format muxedAudioFormat = 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
|
||||
// tags.
|
||||
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)) {
|
||||
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
|
||||
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
|
||||
@ -423,6 +433,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
if (noClosedCaptions) {
|
||||
muxedCaptionFormats = Collections.emptyList();
|
||||
}
|
||||
|
||||
return new HlsMasterPlaylist(
|
||||
baseUri,
|
||||
tags,
|
||||
@ -432,7 +443,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
muxedAudioFormat,
|
||||
muxedCaptionFormats,
|
||||
hasIndependentSegmentsTag,
|
||||
variableDefinitions);
|
||||
variableDefinitions,
|
||||
sessionKeyDrmInitData);
|
||||
}
|
||||
|
||||
private static HlsMediaPlaylist parseMediaPlaylist(
|
||||
@ -557,17 +569,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
}
|
||||
} else {
|
||||
if (encryptionScheme == null) {
|
||||
encryptionScheme =
|
||||
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);
|
||||
encryptionScheme = parseEncryptionScheme(method);
|
||||
}
|
||||
SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
|
||||
if (schemeData != null) {
|
||||
cachedDrmInitData = null;
|
||||
currentSchemeDatas.put(keyFormat, schemeData);
|
||||
@ -713,40 +717,35 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
: Format.NO_VALUE;
|
||||
}
|
||||
|
||||
private static @Nullable SchemeData parsePlayReadySchemeData(
|
||||
String line, Map<String, String> 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<String, String> 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()));
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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++) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user