mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add support for Widevine encrypted HLS
This includes both cbcs and cenc. Will only work for streams that require a single pssh. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169382884
This commit is contained in:
parent
26d789e6d3
commit
6314a0ec82
9 changed files with 122 additions and 82 deletions
|
|
@ -58,7 +58,15 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
|||
* @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
|
||||
*/
|
||||
public DrmInitData(SchemeData... schemeDatas) {
|
||||
this(null, true, schemeDatas);
|
||||
this(null, schemeDatas);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param schemeType The protection scheme type, or null if not applicable or unknown.
|
||||
* @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
|
||||
*/
|
||||
public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) {
|
||||
this(schemeType, true, schemeDatas);
|
||||
}
|
||||
|
||||
private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
@Flags private final int flags;
|
||||
private final Track sideloadedTrack;
|
||||
|
||||
// Manifest DRM data.
|
||||
private final DrmInitData sideloadedDrmInitData;
|
||||
|
||||
// Track-linked data bundle, accessible as a whole through trackID.
|
||||
private final SparseArray<TrackBundle> trackBundles;
|
||||
|
||||
|
|
@ -179,7 +182,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
|
||||
this(flags, timestampAdjuster, null);
|
||||
this(flags, timestampAdjuster, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -187,12 +190,14 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
|
||||
* will not receive a moov box in the input data.
|
||||
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
|
||||
Track sideloadedTrack) {
|
||||
Track sideloadedTrack, DrmInitData sideloadedDrmInitData) {
|
||||
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.sideloadedTrack = sideloadedTrack;
|
||||
this.sideloadedDrmInitData = sideloadedDrmInitData;
|
||||
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
||||
nalPrefix = new ParsableByteArray(5);
|
||||
|
|
@ -402,7 +407,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
|
||||
Assertions.checkState(sideloadedTrack == null, "Unexpected moov box.");
|
||||
|
||||
DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);
|
||||
DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData
|
||||
: getDrmInitDataFromAtoms(moov.leafChildren);
|
||||
|
||||
// Read declaration of track fragments in the Moov box.
|
||||
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
||||
|
|
@ -456,7 +462,9 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
|
||||
parseMoof(moof, trackBundles, flags, extendedTypeScratch);
|
||||
DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren);
|
||||
// If drm init data is sideloaded, we ignore pssh boxes.
|
||||
DrmInitData drmInitData = sideloadedDrmInitData != null ? null
|
||||
: getDrmInitDataFromAtoms(moof.leafChildren);
|
||||
if (drmInitData != null) {
|
||||
int trackCount = trackBundles.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
|
|
|
|||
|
|
@ -85,9 +85,8 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
Segment segment = segments.get(0);
|
||||
assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertFalse(segment.isEncrypted);
|
||||
assertEquals(null, segment.encryptionKeyUri);
|
||||
assertEquals(null, segment.encryptionIV);
|
||||
assertNull(segment.fullSegmentEncryptionKeyUri);
|
||||
assertNull(segment.encryptionIV);
|
||||
assertEquals(51370, segment.byterangeLength);
|
||||
assertEquals(0, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
|
||||
|
|
@ -95,8 +94,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
segment = segments.get(1);
|
||||
assertEquals(0, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segment.fullSegmentEncryptionKeyUri);
|
||||
assertEquals("0x1566B", segment.encryptionIV);
|
||||
assertEquals(51501, segment.byterangeLength);
|
||||
assertEquals(2147483648L, segment.byterangeOffset);
|
||||
|
|
@ -105,8 +103,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
segment = segments.get(2);
|
||||
assertEquals(0, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7941000, segment.durationUs);
|
||||
assertFalse(segment.isEncrypted);
|
||||
assertEquals(null, segment.encryptionKeyUri);
|
||||
assertNull(segment.fullSegmentEncryptionKeyUri);
|
||||
assertEquals(null, segment.encryptionIV);
|
||||
assertEquals(51501, segment.byterangeLength);
|
||||
assertEquals(2147535149L, segment.byterangeOffset);
|
||||
|
|
@ -115,8 +112,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
segment = segments.get(3);
|
||||
assertEquals(1, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri);
|
||||
// 0xA7A == 2682.
|
||||
assertNotNull(segment.encryptionIV);
|
||||
assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
|
|
@ -127,8 +123,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
segment = segments.get(4);
|
||||
assertEquals(1, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri);
|
||||
// 0xA7B == 2683.
|
||||
assertNotNull(segment.encryptionIV);
|
||||
assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
|
|
|
|||
|
|
@ -276,9 +276,9 @@ import java.util.List;
|
|||
// Handle encryption.
|
||||
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
|
||||
|
||||
// Check if encryption is specified.
|
||||
if (segment.isEncrypted) {
|
||||
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
|
||||
// Check if the segment is completely encrypted using the identity key format.
|
||||
if (segment.fullSegmentEncryptionKeyUri != null) {
|
||||
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.fullSegmentEncryptionKeyUri);
|
||||
if (!keyUri.equals(encryptionKeyUri)) {
|
||||
// Encryption is specified and the key has changed.
|
||||
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex,
|
||||
|
|
@ -314,7 +314,7 @@ import java.util.List;
|
|||
out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl,
|
||||
muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
|
||||
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
|
||||
isTimestampMaster, timestampAdjuster, previous, segment.keyFormat, encryptionKey,
|
||||
isTimestampMaster, timestampAdjuster, previous, mediaPlaylist.drmInitData, encryptionKey,
|
||||
encryptionIv);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls;
|
|||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
|
|
@ -32,7 +33,6 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
|||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -88,6 +88,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
private final boolean shouldSpliceIn;
|
||||
private final boolean needNewExtractor;
|
||||
private final List<Format> muxedCaptionFormats;
|
||||
private final DrmInitData drmInitData;
|
||||
|
||||
private final boolean isPackedAudio;
|
||||
private final Id3Decoder id3Decoder;
|
||||
|
|
@ -117,20 +118,21 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
|
||||
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
|
||||
* @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
|
||||
* @param keyFormat A string describing the format for {@code keyData}, or null if the chunk is
|
||||
* not encrypted.
|
||||
* @param keyData Data specifying how to obtain the keys to decrypt the chunk, or null if the
|
||||
* chunk is not encrypted.
|
||||
* @param encryptionIv The AES initialization vector, or null if the chunk is not encrypted.
|
||||
* @param drmInitData A {@link DrmInitData} to sideload to the extractor.
|
||||
* @param fullSegmentEncryptionKey The key to decrypt the full segment, or null if the segment is
|
||||
* not fully encrypted.
|
||||
* @param encryptionIv The AES initialization vector, or null if the segment is not fully
|
||||
* encrypted.
|
||||
*/
|
||||
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
|
||||
HlsUrl hlsUrl, List<Format> muxedCaptionFormats, int trackSelectionReason,
|
||||
Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex,
|
||||
int discontinuitySequenceNumber, boolean isMasterTimestampSource,
|
||||
TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, String keyFormat,
|
||||
byte[] keyData, byte[] encryptionIv) {
|
||||
super(buildDataSource(dataSource, keyFormat, keyData, encryptionIv), dataSpec, hlsUrl.format,
|
||||
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
|
||||
TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, DrmInitData drmInitData,
|
||||
byte[] fullSegmentEncryptionKey, byte[] encryptionIv) {
|
||||
super(buildDataSource(dataSource, fullSegmentEncryptionKey, encryptionIv), dataSpec,
|
||||
hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs,
|
||||
chunkIndex);
|
||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||
this.initDataSpec = initDataSpec;
|
||||
this.hlsUrl = hlsUrl;
|
||||
|
|
@ -139,6 +141,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
this.timestampAdjuster = timestampAdjuster;
|
||||
// Note: this.dataSource and dataSource may be different.
|
||||
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
|
||||
this.drmInitData = drmInitData;
|
||||
lastPathSegment = dataSpec.uri.getLastPathSegment();
|
||||
isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION)
|
||||
|| lastPathSegment.endsWith(AC3_FILE_EXTENSION)
|
||||
|
|
@ -331,14 +334,13 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
// Internal factory methods.
|
||||
|
||||
/**
|
||||
* If the content is encrypted using the "identity" key format, returns an
|
||||
* {@link Aes128DataSource} that wraps the original in order to decrypt the loaded data. Else
|
||||
* returns the original.
|
||||
* If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original
|
||||
* in order to decrypt the loaded data. Else returns the original.
|
||||
*/
|
||||
private static DataSource buildDataSource(DataSource dataSource, String keyFormat, byte[] keyData,
|
||||
private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey,
|
||||
byte[] encryptionIv) {
|
||||
if (HlsMediaPlaylist.KEYFORMAT_IDENTITY.equals(keyFormat)) {
|
||||
return new Aes128DataSource(dataSource, keyData, encryptionIv);
|
||||
if (fullSegmentEncryptionKey != null) {
|
||||
return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv);
|
||||
}
|
||||
return dataSource;
|
||||
}
|
||||
|
|
@ -357,7 +359,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
extractor = previousExtractor;
|
||||
} else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
|
||||
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) {
|
||||
extractor = new FragmentedMp4Extractor(0, timestampAdjuster);
|
||||
extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData);
|
||||
} else {
|
||||
// MPEG-2 TS segments, but we need a new extractor.
|
||||
// This flag ensures the change of pid between streams does not affect the sample queues.
|
||||
|
|
|
|||
|
|
@ -117,8 +117,9 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, St
|
|||
HlsMediaPlaylist.Segment hlsSegment, HashSet<Uri> encryptionKeyUris)
|
||||
throws IOException, InterruptedException {
|
||||
long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs;
|
||||
if (hlsSegment.isEncrypted) {
|
||||
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri);
|
||||
if (hlsSegment.fullSegmentEncryptionKeyUri != null) {
|
||||
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri,
|
||||
hlsSegment.fullSegmentEncryptionKeyUri);
|
||||
if (encryptionKeyUris.add(keyUri)) {
|
||||
segments.add(new Segment(startTimeUs, new DataSpec(keyUri)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
|
|||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Collections;
|
||||
|
|
@ -50,17 +51,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
*/
|
||||
public final long relativeStartTimeUs;
|
||||
/**
|
||||
* Whether the segment is encrypted, as defined by #EXT-X-KEY.
|
||||
* The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use
|
||||
* full segment encryption with identity key.
|
||||
*/
|
||||
public final boolean isEncrypted;
|
||||
/**
|
||||
* The key format as defined by #EXT-X-KEY, or null if the segment is not encrypted.
|
||||
*/
|
||||
public final String keyFormat;
|
||||
/**
|
||||
* The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted.
|
||||
*/
|
||||
public final String encryptionKeyUri;
|
||||
public final String fullSegmentEncryptionKeyUri;
|
||||
/**
|
||||
* The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not
|
||||
* encrypted.
|
||||
|
|
@ -77,7 +71,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
public final long byterangeLength;
|
||||
|
||||
public Segment(String uri, long byterangeOffset, long byterangeLength) {
|
||||
this(uri, 0, -1, C.TIME_UNSET, false, null, null, null, byterangeOffset, byterangeLength);
|
||||
this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,23 +79,19 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
* @param durationUs See {@link #durationUs}.
|
||||
* @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.
|
||||
* @param relativeStartTimeUs See {@link #relativeStartTimeUs}.
|
||||
* @param isEncrypted See {@link #isEncrypted}.
|
||||
* @param keyFormat See {@link #keyFormat}.
|
||||
* @param encryptionKeyUri See {@link #encryptionKeyUri}.
|
||||
* @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
|
||||
* @param encryptionIV See {@link #encryptionIV}.
|
||||
* @param byterangeOffset See {@link #byterangeOffset}.
|
||||
* @param byterangeLength See {@link #byterangeLength}.
|
||||
*/
|
||||
public Segment(String url, long durationUs, int relativeDiscontinuitySequence,
|
||||
long relativeStartTimeUs, boolean isEncrypted, String keyFormat, String encryptionKeyUri,
|
||||
long relativeStartTimeUs, String fullSegmentEncryptionKeyUri,
|
||||
String encryptionIV, long byterangeOffset, long byterangeLength) {
|
||||
this.url = url;
|
||||
this.durationUs = durationUs;
|
||||
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
|
||||
this.relativeStartTimeUs = relativeStartTimeUs;
|
||||
this.isEncrypted = isEncrypted;
|
||||
this.keyFormat = keyFormat;
|
||||
this.encryptionKeyUri = encryptionKeyUri;
|
||||
this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
|
||||
this.encryptionIV = encryptionIV;
|
||||
this.byterangeOffset = byterangeOffset;
|
||||
this.byterangeLength = byterangeLength;
|
||||
|
|
@ -115,11 +105,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* The identity key format, as defined by #EXT-X-KEY.
|
||||
*/
|
||||
public static final String KEYFORMAT_IDENTITY = "identity";
|
||||
|
||||
/**
|
||||
* Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE.
|
||||
*/
|
||||
|
|
@ -176,6 +161,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
* Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag.
|
||||
*/
|
||||
public final boolean hasProgramDateTime;
|
||||
/**
|
||||
* DRM initialization data for sample decryption, or null if none of the segment uses sample
|
||||
* encryption.
|
||||
*/
|
||||
public final DrmInitData drmInitData;
|
||||
/**
|
||||
* The initialization segment, as defined by #EXT-X-MAP.
|
||||
*/
|
||||
|
|
@ -203,6 +193,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
* @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}.
|
||||
* @param hasEndTag See {@link #hasEndTag}.
|
||||
* @param hasProgramDateTime See {@link #hasProgramDateTime}.
|
||||
* @param drmInitData See {@link #drmInitData}.
|
||||
* @param initializationSegment See {@link #initializationSegment}.
|
||||
* @param segments See {@link #segments}.
|
||||
*/
|
||||
|
|
@ -210,7 +201,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence,
|
||||
int discontinuitySequence, int mediaSequence, int version, long targetDurationUs,
|
||||
boolean hasIndependentSegmentsTag, boolean hasEndTag, boolean hasProgramDateTime,
|
||||
Segment initializationSegment, List<Segment> segments) {
|
||||
DrmInitData drmInitData, Segment initializationSegment, List<Segment> segments) {
|
||||
super(baseUri, tags);
|
||||
this.playlistType = playlistType;
|
||||
this.startTimeUs = startTimeUs;
|
||||
|
|
@ -222,6 +213,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
this.hasIndependentSegmentsTag = hasIndependentSegmentsTag;
|
||||
this.hasEndTag = hasEndTag;
|
||||
this.hasProgramDateTime = hasProgramDateTime;
|
||||
this.drmInitData = drmInitData;
|
||||
this.initializationSegment = initializationSegment;
|
||||
this.segments = Collections.unmodifiableList(segments);
|
||||
if (!segments.isEmpty()) {
|
||||
|
|
@ -273,7 +265,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
|
||||
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true,
|
||||
discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag,
|
||||
hasEndTag, hasProgramDateTime, initializationSegment, segments);
|
||||
hasEndTag, hasProgramDateTime, drmInitData, initializationSegment, segments);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -288,7 +280,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
}
|
||||
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs,
|
||||
hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs,
|
||||
hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments);
|
||||
hasIndependentSegmentsTag, true, hasProgramDateTime, drmInitData, initializationSegment,
|
||||
segments);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@
|
|||
package com.google.android.exoplayer2.source.hls.playlist;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||
|
|
@ -28,6 +31,7 @@ import java.io.BufferedReader;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
|
@ -71,6 +75,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
private static final String METHOD_NONE = "NONE";
|
||||
private static final String METHOD_AES_128 = "AES-128";
|
||||
private static final String METHOD_SAMPLE_AES = "SAMPLE-AES";
|
||||
private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC";
|
||||
private static final String KEYFORMAT_IDENTITY = "identity";
|
||||
private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =
|
||||
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
|
||||
private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine";
|
||||
|
||||
private static final String BOOLEAN_TRUE = "YES";
|
||||
private static final String BOOLEAN_FALSE = "NO";
|
||||
|
|
@ -315,10 +324,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
long segmentByteRangeLength = C.LENGTH_UNSET;
|
||||
int segmentMediaSequence = 0;
|
||||
|
||||
boolean isEncrypted = false;
|
||||
String keyFormat = null;
|
||||
String encryptionKeyUri = null;
|
||||
String encryptionIV = null;
|
||||
DrmInitData drmInitData = null;
|
||||
|
||||
String line;
|
||||
while (iterator.hasNext()) {
|
||||
|
|
@ -363,18 +371,26 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
|
||||
} else if (line.startsWith(TAG_KEY)) {
|
||||
String method = parseStringAttr(line, REGEX_METHOD);
|
||||
isEncrypted = METHOD_AES_128.equals(method) || METHOD_SAMPLE_AES.equals(method);
|
||||
if (isEncrypted) {
|
||||
keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT);
|
||||
if (keyFormat == null) {
|
||||
keyFormat = HlsMediaPlaylist.KEYFORMAT_IDENTITY;
|
||||
}
|
||||
encryptionKeyUri = parseStringAttr(line, REGEX_URI);
|
||||
String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT);
|
||||
encryptionKeyUri = null;
|
||||
encryptionIV = null;
|
||||
if (!METHOD_NONE.equals(method)) {
|
||||
encryptionIV = parseOptionalStringAttr(line, REGEX_IV);
|
||||
} else {
|
||||
keyFormat = null;
|
||||
encryptionKeyUri = null;
|
||||
encryptionIV = null;
|
||||
if (KEYFORMAT_IDENTITY.equals(keyFormat) || keyFormat == null) {
|
||||
if (METHOD_AES_128.equals(method)) {
|
||||
// The segment is fully encrypted using an identity key.
|
||||
encryptionKeyUri = parseStringAttr(line, REGEX_URI);
|
||||
} else {
|
||||
// Do nothing. Samples are encrypted using an identity key, but this is not supported.
|
||||
// Hopefully, a traditional DRM alternative is also provided.
|
||||
}
|
||||
} else {
|
||||
SchemeData schemeData = parseWidevineSchemeData(line, keyFormat);
|
||||
if (schemeData != null) {
|
||||
drmInitData = new DrmInitData(METHOD_SAMPLE_AES_CENC.equals(method)
|
||||
? C.CENC_TYPE_cenc : C.CENC_TYPE_cbcs, schemeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith(TAG_BYTERANGE)) {
|
||||
String byteRange = parseStringAttr(line, REGEX_BYTERANGE);
|
||||
|
|
@ -396,7 +412,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
}
|
||||
} else if (!line.startsWith("#")) {
|
||||
String segmentEncryptionIV;
|
||||
if (!isEncrypted) {
|
||||
if (encryptionKeyUri == null) {
|
||||
segmentEncryptionIV = null;
|
||||
} else if (encryptionIV != null) {
|
||||
segmentEncryptionIV = encryptionIV;
|
||||
|
|
@ -408,7 +424,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
segmentByteRangeOffset = 0;
|
||||
}
|
||||
segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
|
||||
segmentStartTimeUs, isEncrypted, keyFormat, encryptionKeyUri, segmentEncryptionIV,
|
||||
segmentStartTimeUs, encryptionKeyUri, segmentEncryptionIV,
|
||||
segmentByteRangeOffset, segmentByteRangeLength));
|
||||
segmentStartTimeUs += segmentDurationUs;
|
||||
segmentDurationUs = 0;
|
||||
|
|
@ -425,7 +441,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, playlistStartTimeUs,
|
||||
hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version,
|
||||
targetDurationUs, hasIndependentSegmentsTag, hasEndTag, playlistStartTimeUs != 0,
|
||||
initializationSegment, segments);
|
||||
drmInitData, initializationSegment, segments);
|
||||
}
|
||||
|
||||
private static SchemeData parseWidevineSchemeData(String line, String keyFormat)
|
||||
throws ParserException {
|
||||
if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) {
|
||||
String uriString = parseStringAttr(line, REGEX_URI);
|
||||
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);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||
trackEncryptionBoxes, nalUnitLengthFieldLength, null, null);
|
||||
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
||||
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
|
||||
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track);
|
||||
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track, null);
|
||||
extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue