Commit 6314a0ec by aquilescanta Committed by Oliver Woodman

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
parent 26d789e6
...@@ -58,7 +58,15 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -58,7 +58,15 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
*/ */
public DrmInitData(SchemeData... schemeDatas) { 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, private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas,
......
...@@ -120,6 +120,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -120,6 +120,9 @@ public final class FragmentedMp4Extractor implements Extractor {
@Flags private final int flags; @Flags private final int flags;
private final Track sideloadedTrack; private final Track sideloadedTrack;
// Manifest DRM data.
private final DrmInitData sideloadedDrmInitData;
// Track-linked data bundle, accessible as a whole through trackID. // Track-linked data bundle, accessible as a whole through trackID.
private final SparseArray<TrackBundle> trackBundles; private final SparseArray<TrackBundle> trackBundles;
...@@ -179,7 +182,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -179,7 +182,7 @@ public final class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/ */
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { 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 { ...@@ -187,12 +190,14 @@ public final class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor * @param sideloadedTrack Sideloaded track information, in the case that the extractor
* will not receive a moov box in the input data. * 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, public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
Track sideloadedTrack) { Track sideloadedTrack, DrmInitData sideloadedDrmInitData) {
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster; this.timestampAdjuster = timestampAdjuster;
this.sideloadedTrack = sideloadedTrack; this.sideloadedTrack = sideloadedTrack;
this.sideloadedDrmInitData = sideloadedDrmInitData;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalPrefix = new ParsableByteArray(5); nalPrefix = new ParsableByteArray(5);
...@@ -402,7 +407,8 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -402,7 +407,8 @@ public final class FragmentedMp4Extractor implements Extractor {
private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); 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. // Read declaration of track fragments in the Moov box.
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
...@@ -456,7 +462,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -456,7 +462,9 @@ public final class FragmentedMp4Extractor implements Extractor {
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
parseMoof(moof, trackBundles, flags, extendedTypeScratch); 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) { if (drmInitData != null) {
int trackCount = trackBundles.size(); int trackCount = trackBundles.size();
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
......
...@@ -85,9 +85,8 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -85,9 +85,8 @@ public class HlsMediaPlaylistParserTest extends TestCase {
Segment segment = segments.get(0); Segment segment = segments.get(0);
assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence); assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs); assertEquals(7975000, segment.durationUs);
assertFalse(segment.isEncrypted); assertNull(segment.fullSegmentEncryptionKeyUri);
assertEquals(null, segment.encryptionKeyUri); assertNull(segment.encryptionIV);
assertEquals(null, segment.encryptionIV);
assertEquals(51370, segment.byterangeLength); assertEquals(51370, segment.byterangeLength);
assertEquals(0, segment.byterangeOffset); assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url); assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
...@@ -95,8 +94,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -95,8 +94,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
segment = segments.get(1); segment = segments.get(1);
assertEquals(0, segment.relativeDiscontinuitySequence); assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs); assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted); assertEquals("https://priv.example.com/key.php?r=2680", segment.fullSegmentEncryptionKeyUri);
assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
assertEquals("0x1566B", segment.encryptionIV); assertEquals("0x1566B", segment.encryptionIV);
assertEquals(51501, segment.byterangeLength); assertEquals(51501, segment.byterangeLength);
assertEquals(2147483648L, segment.byterangeOffset); assertEquals(2147483648L, segment.byterangeOffset);
...@@ -105,8 +103,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -105,8 +103,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
segment = segments.get(2); segment = segments.get(2);
assertEquals(0, segment.relativeDiscontinuitySequence); assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(7941000, segment.durationUs); assertEquals(7941000, segment.durationUs);
assertFalse(segment.isEncrypted); assertNull(segment.fullSegmentEncryptionKeyUri);
assertEquals(null, segment.encryptionKeyUri);
assertEquals(null, segment.encryptionIV); assertEquals(null, segment.encryptionIV);
assertEquals(51501, segment.byterangeLength); assertEquals(51501, segment.byterangeLength);
assertEquals(2147535149L, segment.byterangeOffset); assertEquals(2147535149L, segment.byterangeOffset);
...@@ -115,8 +112,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -115,8 +112,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
segment = segments.get(3); segment = segments.get(3);
assertEquals(1, segment.relativeDiscontinuitySequence); assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs); assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted); assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7A == 2682. // 0xA7A == 2682.
assertNotNull(segment.encryptionIV); assertNotNull(segment.encryptionIV);
assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault())); assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
...@@ -127,8 +123,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -127,8 +123,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
segment = segments.get(4); segment = segments.get(4);
assertEquals(1, segment.relativeDiscontinuitySequence); assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs); assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted); assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7B == 2683. // 0xA7B == 2683.
assertNotNull(segment.encryptionIV); assertNotNull(segment.encryptionIV);
assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault())); assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
......
...@@ -276,9 +276,9 @@ import java.util.List; ...@@ -276,9 +276,9 @@ import java.util.List;
// Handle encryption. // Handle encryption.
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
// Check if encryption is specified. // Check if the segment is completely encrypted using the identity key format.
if (segment.isEncrypted) { if (segment.fullSegmentEncryptionKeyUri != null) {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.fullSegmentEncryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) { if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed. // Encryption is specified and the key has changed.
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex,
...@@ -314,7 +314,7 @@ import java.util.List; ...@@ -314,7 +314,7 @@ import java.util.List;
out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl,
muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
isTimestampMaster, timestampAdjuster, previous, segment.keyFormat, encryptionKey, isTimestampMaster, timestampAdjuster, previous, mediaPlaylist.drmInitData, encryptionKey,
encryptionIv); encryptionIv);
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls;
import android.text.TextUtils; 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.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
...@@ -32,7 +33,6 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; ...@@ -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.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.source.chunk.MediaChunk; 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.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -88,6 +88,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -88,6 +88,7 @@ import java.util.concurrent.atomic.AtomicInteger;
private final boolean shouldSpliceIn; private final boolean shouldSpliceIn;
private final boolean needNewExtractor; private final boolean needNewExtractor;
private final List<Format> muxedCaptionFormats; private final List<Format> muxedCaptionFormats;
private final DrmInitData drmInitData;
private final boolean isPackedAudio; private final boolean isPackedAudio;
private final Id3Decoder id3Decoder; private final Id3Decoder id3Decoder;
...@@ -117,20 +118,21 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -117,20 +118,21 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
* @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. * @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 * @param drmInitData A {@link DrmInitData} to sideload to the extractor.
* not encrypted. * @param fullSegmentEncryptionKey The key to decrypt the full segment, or null if the segment is
* @param keyData Data specifying how to obtain the keys to decrypt the chunk, or null if the * not fully encrypted.
* chunk is not encrypted. * @param encryptionIv The AES initialization vector, or null if the segment is not fully
* @param encryptionIv The AES initialization vector, or null if the chunk is not encrypted. * encrypted.
*/ */
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
HlsUrl hlsUrl, List<Format> muxedCaptionFormats, int trackSelectionReason, HlsUrl hlsUrl, List<Format> muxedCaptionFormats, int trackSelectionReason,
Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex,
int discontinuitySequenceNumber, boolean isMasterTimestampSource, int discontinuitySequenceNumber, boolean isMasterTimestampSource,
TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, String keyFormat, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, DrmInitData drmInitData,
byte[] keyData, byte[] encryptionIv) { byte[] fullSegmentEncryptionKey, byte[] encryptionIv) {
super(buildDataSource(dataSource, keyFormat, keyData, encryptionIv), dataSpec, hlsUrl.format, super(buildDataSource(dataSource, fullSegmentEncryptionKey, encryptionIv), dataSpec,
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs,
chunkIndex);
this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.initDataSpec = initDataSpec; this.initDataSpec = initDataSpec;
this.hlsUrl = hlsUrl; this.hlsUrl = hlsUrl;
...@@ -139,6 +141,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -139,6 +141,7 @@ import java.util.concurrent.atomic.AtomicInteger;
this.timestampAdjuster = timestampAdjuster; this.timestampAdjuster = timestampAdjuster;
// Note: this.dataSource and dataSource may be different. // Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource; this.isEncrypted = this.dataSource instanceof Aes128DataSource;
this.drmInitData = drmInitData;
lastPathSegment = dataSpec.uri.getLastPathSegment(); lastPathSegment = dataSpec.uri.getLastPathSegment();
isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION)
|| lastPathSegment.endsWith(AC3_FILE_EXTENSION) || lastPathSegment.endsWith(AC3_FILE_EXTENSION)
...@@ -331,14 +334,13 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -331,14 +334,13 @@ import java.util.concurrent.atomic.AtomicInteger;
// Internal factory methods. // Internal factory methods.
/** /**
* If the content is encrypted using the "identity" key format, returns an * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original
* {@link Aes128DataSource} that wraps the original in order to decrypt the loaded data. Else * in order to decrypt the loaded data. Else returns the original.
* returns the original.
*/ */
private static DataSource buildDataSource(DataSource dataSource, String keyFormat, byte[] keyData, private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey,
byte[] encryptionIv) { byte[] encryptionIv) {
if (HlsMediaPlaylist.KEYFORMAT_IDENTITY.equals(keyFormat)) { if (fullSegmentEncryptionKey != null) {
return new Aes128DataSource(dataSource, keyData, encryptionIv); return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv);
} }
return dataSource; return dataSource;
} }
...@@ -357,7 +359,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -357,7 +359,7 @@ import java.util.concurrent.atomic.AtomicInteger;
extractor = previousExtractor; extractor = previousExtractor;
} else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) {
extractor = new FragmentedMp4Extractor(0, timestampAdjuster); extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData);
} else { } else {
// MPEG-2 TS segments, but we need a new extractor. // 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. // 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 ...@@ -117,8 +117,9 @@ public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, St
HlsMediaPlaylist.Segment hlsSegment, HashSet<Uri> encryptionKeyUris) HlsMediaPlaylist.Segment hlsSegment, HashSet<Uri> encryptionKeyUris)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs;
if (hlsSegment.isEncrypted) { if (hlsSegment.fullSegmentEncryptionKeyUri != null) {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri); Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri,
hlsSegment.fullSegmentEncryptionKeyUri);
if (encryptionKeyUris.add(keyUri)) { if (encryptionKeyUris.add(keyUri)) {
segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); segments.add(new Segment(startTimeUs, new DataSpec(keyUri)));
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Collections; import java.util.Collections;
...@@ -50,17 +51,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -50,17 +51,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/ */
public final long relativeStartTimeUs; 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; public final String fullSegmentEncryptionKeyUri;
/**
* 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;
/** /**
* The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not
* encrypted. * encrypted.
...@@ -77,7 +71,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -77,7 +71,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final long byterangeLength; public final long byterangeLength;
public Segment(String uri, long byterangeOffset, 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 { ...@@ -85,23 +79,19 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param durationUs See {@link #durationUs}. * @param durationUs See {@link #durationUs}.
* @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}.
* @param relativeStartTimeUs See {@link #relativeStartTimeUs}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}.
* @param isEncrypted See {@link #isEncrypted}. * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
* @param keyFormat See {@link #keyFormat}.
* @param encryptionKeyUri See {@link #encryptionKeyUri}.
* @param encryptionIV See {@link #encryptionIV}. * @param encryptionIV See {@link #encryptionIV}.
* @param byterangeOffset See {@link #byterangeOffset}. * @param byterangeOffset See {@link #byterangeOffset}.
* @param byterangeLength See {@link #byterangeLength}. * @param byterangeLength See {@link #byterangeLength}.
*/ */
public Segment(String url, long durationUs, int relativeDiscontinuitySequence, 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) { String encryptionIV, long byterangeOffset, long byterangeLength) {
this.url = url; this.url = url;
this.durationUs = durationUs; this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs; this.relativeStartTimeUs = relativeStartTimeUs;
this.isEncrypted = isEncrypted; this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
this.keyFormat = keyFormat;
this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV; this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset; this.byterangeOffset = byterangeOffset;
this.byterangeLength = byterangeLength; this.byterangeLength = byterangeLength;
...@@ -116,11 +106,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -116,11 +106,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. * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -177,6 +162,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -177,6 +162,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/ */
public final boolean hasProgramDateTime; 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. * The initialization segment, as defined by #EXT-X-MAP.
*/ */
public final Segment initializationSegment; public final Segment initializationSegment;
...@@ -203,6 +193,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -203,6 +193,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}. * @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}.
* @param hasEndTag See {@link #hasEndTag}. * @param hasEndTag See {@link #hasEndTag}.
* @param hasProgramDateTime See {@link #hasProgramDateTime}. * @param hasProgramDateTime See {@link #hasProgramDateTime}.
* @param drmInitData See {@link #drmInitData}.
* @param initializationSegment See {@link #initializationSegment}. * @param initializationSegment See {@link #initializationSegment}.
* @param segments See {@link #segments}. * @param segments See {@link #segments}.
*/ */
...@@ -210,7 +201,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -210,7 +201,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence,
int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, int discontinuitySequence, int mediaSequence, int version, long targetDurationUs,
boolean hasIndependentSegmentsTag, boolean hasEndTag, boolean hasProgramDateTime, boolean hasIndependentSegmentsTag, boolean hasEndTag, boolean hasProgramDateTime,
Segment initializationSegment, List<Segment> segments) { DrmInitData drmInitData, Segment initializationSegment, List<Segment> segments) {
super(baseUri, tags); super(baseUri, tags);
this.playlistType = playlistType; this.playlistType = playlistType;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
...@@ -222,6 +213,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -222,6 +213,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.hasIndependentSegmentsTag = hasIndependentSegmentsTag; this.hasIndependentSegmentsTag = hasIndependentSegmentsTag;
this.hasEndTag = hasEndTag; this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime; this.hasProgramDateTime = hasProgramDateTime;
this.drmInitData = drmInitData;
this.initializationSegment = initializationSegment; this.initializationSegment = initializationSegment;
this.segments = Collections.unmodifiableList(segments); this.segments = Collections.unmodifiableList(segments);
if (!segments.isEmpty()) { if (!segments.isEmpty()) {
...@@ -273,7 +265,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -273,7 +265,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true, return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true,
discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag, discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag,
hasEndTag, hasProgramDateTime, initializationSegment, segments); hasEndTag, hasProgramDateTime, drmInitData, initializationSegment, segments);
} }
/** /**
...@@ -288,7 +280,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -288,7 +280,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} }
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs,
hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs,
hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments); hasIndependentSegmentsTag, true, hasProgramDateTime, drmInitData, initializationSegment,
segments);
} }
} }
...@@ -16,9 +16,12 @@ ...@@ -16,9 +16,12 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import android.util.Base64;
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.ParserException; 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.UnrecognizedInputFormatException;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
...@@ -28,6 +31,7 @@ import java.io.BufferedReader; ...@@ -28,6 +31,7 @@ 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.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
...@@ -71,6 +75,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -71,6 +75,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String METHOD_NONE = "NONE"; private static final String METHOD_NONE = "NONE";
private static final String METHOD_AES_128 = "AES-128"; 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 = "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_TRUE = "YES";
private static final String BOOLEAN_FALSE = "NO"; private static final String BOOLEAN_FALSE = "NO";
...@@ -315,10 +324,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -315,10 +324,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long segmentByteRangeLength = C.LENGTH_UNSET; long segmentByteRangeLength = C.LENGTH_UNSET;
int segmentMediaSequence = 0; int segmentMediaSequence = 0;
boolean isEncrypted = false;
String keyFormat = null;
String encryptionKeyUri = null; String encryptionKeyUri = null;
String encryptionIV = null; String encryptionIV = null;
DrmInitData drmInitData = null;
String line; String line;
while (iterator.hasNext()) { while (iterator.hasNext()) {
...@@ -363,18 +371,26 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -363,18 +371,26 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND); (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_KEY)) { } else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD); String method = parseStringAttr(line, REGEX_METHOD);
isEncrypted = METHOD_AES_128.equals(method) || METHOD_SAMPLE_AES.equals(method); String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT);
if (isEncrypted) { encryptionKeyUri = null;
keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT); encryptionIV = null;
if (keyFormat == null) { if (!METHOD_NONE.equals(method)) {
keyFormat = HlsMediaPlaylist.KEYFORMAT_IDENTITY;
}
encryptionKeyUri = parseStringAttr(line, REGEX_URI);
encryptionIV = parseOptionalStringAttr(line, REGEX_IV); encryptionIV = parseOptionalStringAttr(line, REGEX_IV);
} else { if (KEYFORMAT_IDENTITY.equals(keyFormat) || keyFormat == null) {
keyFormat = null; if (METHOD_AES_128.equals(method)) {
encryptionKeyUri = null; // The segment is fully encrypted using an identity key.
encryptionIV = null; 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)) { } else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE); String byteRange = parseStringAttr(line, REGEX_BYTERANGE);
...@@ -396,7 +412,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -396,7 +412,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
} else if (!line.startsWith("#")) { } else if (!line.startsWith("#")) {
String segmentEncryptionIV; String segmentEncryptionIV;
if (!isEncrypted) { if (encryptionKeyUri == null) {
segmentEncryptionIV = null; segmentEncryptionIV = null;
} else if (encryptionIV != null) { } else if (encryptionIV != null) {
segmentEncryptionIV = encryptionIV; segmentEncryptionIV = encryptionIV;
...@@ -408,7 +424,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -408,7 +424,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentByteRangeOffset = 0; segmentByteRangeOffset = 0;
} }
segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence, segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
segmentStartTimeUs, isEncrypted, keyFormat, encryptionKeyUri, segmentEncryptionIV, segmentStartTimeUs, encryptionKeyUri, segmentEncryptionIV,
segmentByteRangeOffset, segmentByteRangeLength)); segmentByteRangeOffset, segmentByteRangeLength));
segmentStartTimeUs += segmentDurationUs; segmentStartTimeUs += segmentDurationUs;
segmentDurationUs = 0; segmentDurationUs = 0;
...@@ -425,7 +441,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -425,7 +441,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, playlistStartTimeUs, return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, playlistStartTimeUs,
hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version, hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version,
targetDurationUs, hasIndependentSegmentsTag, hasEndTag, playlistStartTimeUs != 0, 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 { private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
......
...@@ -101,7 +101,7 @@ public class DefaultSsChunkSource implements SsChunkSource { ...@@ -101,7 +101,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
trackEncryptionBoxes, nalUnitLengthFieldLength, null, null); trackEncryptionBoxes, nalUnitLengthFieldLength, null, null);
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME 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); extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment