Commit 8982da4b by aquilescanta Committed by Andrew Lewis

Support encrypted initialization segment

Defined in RFC 8216 Section 4.3.2.5.

Issue:#5441
PiperOrigin-RevId: 234114119
parent d61171a1
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
* HLS: * HLS:
* Form an adaptive track group out of audio renditions with matching name. * Form an adaptive track group out of audio renditions with matching name.
* Support encrypted initialization segments
([#5441](https://github.com/google/ExoPlayer/issues/5441)).
* DASH: * DASH:
* Fix issue handling large `EventStream` presentation timestamps * Fix issue handling large `EventStream` presentation timestamps
([#5490](https://github.com/google/ExoPlayer/issues/5490)). ([#5490](https://github.com/google/ExoPlayer/issues/5490)).
......
...@@ -40,9 +40,11 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; ...@@ -40,9 +40,11 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Source of Hls (possibly adaptive) chunks. * Source of Hls (possibly adaptive) chunks.
...@@ -84,6 +86,12 @@ import java.util.List; ...@@ -84,6 +86,12 @@ import java.util.List;
} }
/**
* The maximum number of keys that the key cache can hold. This value must be 2 or greater in
* order to hold initialization segment and media segment keys simultaneously.
*/
private static final int KEY_CACHE_SIZE = 4;
private final HlsExtractorFactory extractorFactory; private final HlsExtractorFactory extractorFactory;
private final DataSource mediaDataSource; private final DataSource mediaDataSource;
private final DataSource encryptionDataSource; private final DataSource encryptionDataSource;
...@@ -92,6 +100,7 @@ import java.util.List; ...@@ -92,6 +100,7 @@ import java.util.List;
private final HlsPlaylistTracker playlistTracker; private final HlsPlaylistTracker playlistTracker;
private final TrackGroup trackGroup; private final TrackGroup trackGroup;
private final List<Format> muxedCaptionFormats; private final List<Format> muxedCaptionFormats;
private final FullSegmentEncryptionKeyCache keyCache;
private boolean isTimestampMaster; private boolean isTimestampMaster;
private byte[] scratchSpace; private byte[] scratchSpace;
...@@ -99,11 +108,6 @@ import java.util.List; ...@@ -99,11 +108,6 @@ import java.util.List;
private HlsUrl expectedPlaylistUrl; private HlsUrl expectedPlaylistUrl;
private boolean independentSegments; private boolean independentSegments;
private Uri encryptionKeyUri;
private byte[] encryptionKey;
private String encryptionIvString;
private byte[] encryptionIv;
// Note: The track group in the selection is typically *not* equal to trackGroup. This is due to // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to
// the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
// in TrackSelection to avoid unexpected behavior. // in TrackSelection to avoid unexpected behavior.
...@@ -139,6 +143,7 @@ import java.util.List; ...@@ -139,6 +143,7 @@ import java.util.List;
this.variants = variants; this.variants = variants;
this.timestampAdjusterProvider = timestampAdjusterProvider; this.timestampAdjusterProvider = timestampAdjusterProvider;
this.muxedCaptionFormats = muxedCaptionFormats; this.muxedCaptionFormats = muxedCaptionFormats;
keyCache = new FullSegmentEncryptionKeyCache();
liveEdgeInPeriodTimeUs = C.TIME_UNSET; liveEdgeInPeriodTimeUs = C.TIME_UNSET;
Format[] variantFormats = new Format[variants.length]; Format[] variantFormats = new Format[variants.length];
int[] initialTrackSelection = new int[variants.length]; int[] initialTrackSelection = new int[variants.length];
...@@ -308,20 +313,16 @@ import java.util.List; ...@@ -308,20 +313,16 @@ import java.util.List;
// Handle encryption. // Handle encryption.
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist); HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
// Check if the segment is completely encrypted using the identity key format. // Check if the segment or its initialization segment are fully encrypted.
if (segment.fullSegmentEncryptionKeyUri != null) { out.chunk =
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.fullSegmentEncryptionKeyUri); maybeCreateEncryptionChunkFor(
if (!keyUri.equals(encryptionKeyUri)) { segment.initializationSegment, mediaPlaylist, selectedVariantIndex);
// Encryption is specified and the key has changed. if (out.chunk != null) {
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, return;
trackSelection.getSelectionReason(), trackSelection.getSelectionData()); }
return; out.chunk = maybeCreateEncryptionChunkFor(segment, mediaPlaylist, selectedVariantIndex);
} if (out.chunk != null) {
if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { return;
setEncryptionData(keyUri, segment.encryptionIV, encryptionKey);
}
} else {
clearEncryptionData();
} }
out.chunk = out.chunk =
...@@ -338,8 +339,7 @@ import java.util.List; ...@@ -338,8 +339,7 @@ import java.util.List;
isTimestampMaster, isTimestampMaster,
timestampAdjusterProvider, timestampAdjusterProvider,
previous, previous,
encryptionKey, keyCache.asUnmodifiable());
encryptionIv);
} }
/** /**
...@@ -352,8 +352,7 @@ import java.util.List; ...@@ -352,8 +352,7 @@ import java.util.List;
if (chunk instanceof EncryptionKeyChunk) { if (chunk instanceof EncryptionKeyChunk) {
EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;
scratchSpace = encryptionKeyChunk.getDataHolder(); scratchSpace = encryptionKeyChunk.getDataHolder();
setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv, keyCache.put(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.getResult());
encryptionKeyChunk.getResult());
} }
} }
...@@ -486,38 +485,27 @@ import java.util.List; ...@@ -486,38 +485,27 @@ import java.util.List;
: (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs()); : (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs());
} }
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, private Chunk maybeCreateEncryptionChunkFor(
int trackSelectionReason, Object trackSelectionData) { @Nullable Segment segment, HlsMediaPlaylist mediaPlaylist, int selectedVariantIndex) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); if (segment == null || segment.fullSegmentEncryptionKeyUri == null) {
return new EncryptionKeyChunk(encryptionDataSource, dataSpec, variants[variantIndex].format, return null;
trackSelectionReason, trackSelectionData, scratchSpace, iv);
}
private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) {
String trimmedIv;
if (Util.toLowerInvariant(iv).startsWith("0x")) {
trimmedIv = iv.substring(2);
} else {
trimmedIv = iv;
} }
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.fullSegmentEncryptionKeyUri);
byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray(); if (keyCache.containsKey(keyUri)) {
byte[] ivDataWithPadding = new byte[16]; // The key is present in the key cache. We re-insert it to prevent it from being evicted by
int offset = ivData.length > 16 ? ivData.length - 16 : 0; // the following key addition. Note that removal of the key is necessary to affect the
System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length // eviction order.
+ offset, ivData.length - offset); keyCache.put(keyUri, keyCache.remove(keyUri));
return null;
encryptionKeyUri = keyUri; }
encryptionKey = secretKey; DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP);
encryptionIvString = iv; return new EncryptionKeyChunk(
encryptionIv = ivDataWithPadding; encryptionDataSource,
} dataSpec,
variants[selectedVariantIndex].format,
private void clearEncryptionData() { trackSelection.getSelectionReason(),
encryptionKeyUri = null; trackSelection.getSelectionData(),
encryptionKey = null; scratchSpace);
encryptionIvString = null;
encryptionIv = null;
} }
// Private classes. // Private classes.
...@@ -575,19 +563,21 @@ import java.util.List; ...@@ -575,19 +563,21 @@ import java.util.List;
private static final class EncryptionKeyChunk extends DataChunk { private static final class EncryptionKeyChunk extends DataChunk {
public final String iv;
private byte[] result; private byte[] result;
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, public EncryptionKeyChunk(
int trackSelectionReason, Object trackSelectionData, byte[] scratchSpace, String iv) { DataSource dataSource,
DataSpec dataSpec,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
byte[] scratchSpace) {
super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason, super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason,
trackSelectionData, scratchSpace); trackSelectionData, scratchSpace);
this.iv = iv;
} }
@Override @Override
protected void consume(byte[] data, int limit) throws IOException { protected void consume(byte[] data, int limit) {
result = Arrays.copyOf(data, limit); result = Arrays.copyOf(data, limit);
} }
...@@ -642,4 +632,29 @@ import java.util.List; ...@@ -642,4 +632,29 @@ import java.util.List;
return segmentStartTimeInPeriodUs + segment.durationUs; return segmentStartTimeInPeriodUs + segment.durationUs;
} }
} }
/**
* LRU cache that holds up to {@link #KEY_CACHE_SIZE} full-segment-encryption keys. Which each
* addition, once the cache's size exceeds {@link #KEY_CACHE_SIZE}, the oldest item (according to
* insertion order) is removed.
*/
private static final class FullSegmentEncryptionKeyCache extends LinkedHashMap<Uri, byte[]> {
private final Map<Uri, byte[]> unmodifiableView;
public FullSegmentEncryptionKeyCache() {
super(
/* initialCapacity= */ KEY_CACHE_SIZE * 2, /* loadFactor= */ 1, /* accessOrder= */ false);
unmodifiableView = Collections.unmodifiableMap(this);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Uri, byte[]> entry) {
return size() > KEY_CACHE_SIZE;
}
public Map<Uri, byte[]> asUnmodifiable() {
return unmodifiableView;
}
}
} }
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