Skip to content

Commit

Permalink
Add support for Widevine encrypted HLS
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AquilesCanta authored and ojw28 committed Sep 20, 2017
1 parent 26d789e commit 6314a0e
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ public DrmInitData(List<SchemeData> schemeDatas) {
* @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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ public Extractor[] createExtractors() {
@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;

Expand Down Expand Up @@ -179,20 +182,22 @@ public FragmentedMp4Extractor(@Flags int flags) {
* @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);
}

/**
* @param flags Flags that control the extractor's behavior.
* @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);
Expand Down Expand Up @@ -402,7 +407,8 @@ private void onContainerAtomRead(ContainerAtom container) throws ParserException
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);
Expand Down Expand Up @@ -456,7 +462,9 @@ private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException

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++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,16 @@ public void testParseMediaPlaylist() {
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);

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);
Expand All @@ -105,8 +103,7 @@ public void testParseMediaPlaylist() {
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);
Expand All @@ -115,8 +112,7 @@ public void testParseMediaPlaylist() {
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()));
Expand All @@ -127,8 +123,7 @@ public void testParseMediaPlaylist() {
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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,9 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu
// 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,
Expand Down Expand Up @@ -314,7 +314,7 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;
Expand All @@ -32,7 +33,6 @@
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;
Expand Down Expand Up @@ -88,6 +88,7 @@
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;
Expand Down Expand Up @@ -117,20 +118,21 @@
* @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;
Expand All @@ -139,6 +141,7 @@ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initData
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)
Expand Down Expand Up @@ -331,14 +334,13 @@ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, Inte
// 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;
}
Expand All @@ -357,7 +359,7 @@ private Extractor createExtractor() {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ private static void addSegment(ArrayList<Segment> segments, HlsMediaPlaylist med
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)));
}
Expand Down
Loading

0 comments on commit 6314a0e

Please sign in to comment.