From a76addba5d59637dec40653698e7dc3551534ced Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 4 Nov 2014 13:38:22 -0500 Subject: [PATCH] Add AES-128 encryption support for HLS #69 and parsing logic for CODECS and RESOLUTION attributes. --- .../demo/full/player/HlsRendererBuilder.java | 2 +- .../demo/simple/HlsRendererBuilder.java | 2 +- .../android/exoplayer/hls/HlsChunkSource.java | 75 +++++++++++++-- .../exoplayer/hls/HlsMasterPlaylist.java | 8 +- .../hls/HlsMasterPlaylistParser.java | 32 ++++++- .../exoplayer/hls/HlsMediaPlaylist.java | 12 ++- .../exoplayer/hls/HlsMediaPlaylistParser.java | 37 ++++++- .../android/exoplayer/hls/HlsParserUtil.java | 8 ++ .../exoplayer/hls/HlsSampleSource.java | 10 +- .../exoplayer/upstream/Aes128DataSource.java | 96 +++++++++++++++++++ 10 files changed, 267 insertions(+), 15 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index dd85f933c81..5306dedd2c2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -111,7 +111,7 @@ public void onManifest(String contentId, HlsMasterPlaylist manifest) { private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { return new HlsMasterPlaylist(Uri.parse(""), - Collections.singletonList(new Variant(mediaPlaylistUrl, 0))); + Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1))); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java index 4de39d93d66..4845444fbb2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java @@ -109,7 +109,7 @@ public void onManifest(String contentId, HlsMasterPlaylist manifest) { private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { return new HlsMasterPlaylist(Uri.parse(""), - Collections.singletonList(new Variant(mediaPlaylistUrl, 0))); + Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1))); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 14e99611224..8ea6b27e29e 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -18,6 +18,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; @@ -28,7 +29,9 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.math.BigInteger; import java.util.List; +import java.util.Locale; /** * A temporary test source of HLS chunks. @@ -38,7 +41,7 @@ */ public class HlsChunkSource { - private final DataSource dataSource; + private final DataSource upstreamDataSource; private final HlsMasterPlaylist masterPlaylist; private final HlsMediaPlaylistParser mediaPlaylistParser; @@ -47,9 +50,12 @@ public class HlsChunkSource { /* package */ boolean mediaPlaylistWasLive; /* package */ long lastMediaPlaylistLoadTimeMs; + private DataSource encryptedDataSource; + private String encryptionKeyUri; + // TODO: Once proper m3u8 parsing is in place, actually use the url! public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { - this.dataSource = dataSource; + this.upstreamDataSource = dataSource; this.masterPlaylist = masterPlaylist; mediaPlaylistParser = new HlsMediaPlaylistParser(); } @@ -144,8 +150,22 @@ public void getChunkOperation(List queue, long seekPositionUs, long pla } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); - Uri chunkUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.url); + + // Check if encryption is specified. + if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) { + if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) { + // Encryption is specified and the key has changed. + Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV); + encryptionKeyUri = segment.encryptionKeyUri; + return; + } + } else { + encryptedDataSource = null; + encryptionKeyUri = null; + } + DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); long startTimeUs = segment.startTimeUs; @@ -168,8 +188,15 @@ public void getChunkOperation(List queue, long seekPositionUs, long pla } } - out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, nextChunkMediaSequence, - segment.discontinuity); + DataSource dataSource; + if (encryptedDataSource != null) { + dataSource = encryptedDataSource; + } else { + dataSource = upstreamDataSource; + } + + out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, + nextChunkMediaSequence, segment.discontinuity); } private boolean shouldRerequestMediaPlaylist() { @@ -190,7 +217,12 @@ private MediaPlaylistChunk newMediaPlaylistChunk() { masterPlaylist.variants.get(0).url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); - return new MediaPlaylistChunk(dataSource, dataSpec, 0, mediaPlaylistBaseUri); + return new MediaPlaylistChunk(upstreamDataSource, dataSpec, 0, mediaPlaylistBaseUri); + } + + private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { + DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null); + return new EncryptionKeyChunk(upstreamDataSource, dataSpec, 0, iv); } private class MediaPlaylistChunk extends HlsChunk { @@ -214,4 +246,35 @@ protected void consumeStream(NonBlockingInputStream stream) throws IOException { } + private class EncryptionKeyChunk extends HlsChunk { + + private final String iv; + + public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, int trigger, String iv) { + super(dataSource, dataSpec, trigger); + if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { + this.iv = iv.substring(2); + } else { + this.iv = iv; + } + } + + @Override + protected void consumeStream(NonBlockingInputStream stream) throws IOException { + byte[] keyData = new byte[(int) stream.getAvailableByteCount()]; + stream.read(keyData, 0, keyData.length); + + int ivParsed = Integer.parseInt(iv, 16); + String iv = String.format("%032X", ivParsed); + + byte[] ivData = new BigInteger(iv, 16).toByteArray(); + byte[] ivDataWithPadding = new byte[iv.length() / 2]; + System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length, + ivData.length); + + encryptedDataSource = new Aes128DataSource(keyData, ivDataWithPadding, upstreamDataSource); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java index f118734defe..8d7ff1bc61f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -30,10 +30,16 @@ public final class HlsMasterPlaylist { public static final class Variant { public final int bandwidth; public final String url; + public final String[] codecs; + public final int width; + public final int height; - public Variant(String url, int bandwidth) { + public Variant(String url, int bandwidth, String[] codecs, int width, int height) { this.bandwidth = bandwidth; this.url = url; + this.codecs = codecs; + this.width = width; + this.height = height; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java index d00eecc28b0..191ec069969 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java @@ -36,9 +36,15 @@ public final class HlsMasterPlaylistParser implements ManifestParser variants = new ArrayList(); int bandwidth = 0; + String[] codecs = null; + int width = -1; + int height = -1; + String line; while ((line = reader.readLine()) != null) { line = line.trim(); @@ -60,9 +70,29 @@ private static HlsMasterPlaylist parseMasterPlaylist(InputStream inputStream, } if (line.startsWith(STREAM_INF_TAG)) { bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); + String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX, + CODECS_ATTR); + if (codecsString != null) { + codecs = codecsString.split(","); + } else { + codecs = null; + } + String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, RESOLUTION_ATTR_REGEX, + RESOLUTION_ATTR); + if (resolutionString != null) { + String[] widthAndHeight = resolutionString.split("x"); + width = Integer.parseInt(widthAndHeight[0]); + height = Integer.parseInt(widthAndHeight[1]); + } else { + width = -1; + height = -1; + } } else if (!line.startsWith("#")) { - variants.add(new Variant(line, bandwidth)); + variants.add(new Variant(line, bandwidth, codecs, width, height)); bandwidth = 0; + codecs = null; + width = -1; + height = -1; } } return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants)); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java index e4631d42f6a..7e8136cab3c 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java @@ -32,12 +32,19 @@ public static final class Segment implements Comparable { public final double durationSecs; public final String url; public final long startTimeUs; + public final String encryptionMethod; + public final String encryptionKeyUri; + public final String encryptionIV; - public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs) { + public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs, + String encryptionMethod, String encryptionKeyUri, String encryptionIV) { this.url = uri; this.durationSecs = durationSecs; this.discontinuity = discontinuity; this.startTimeUs = startTimeUs; + this.encryptionMethod = encryptionMethod; + this.encryptionKeyUri = encryptionKeyUri; + this.encryptionIV = encryptionIV; } @Override @@ -46,6 +53,9 @@ public int compareTo(Long startTimeUs) { } } + public static final String ENCRYPTION_METHOD_NONE = "NONE"; + public static final String ENCRYPTION_METHOD_AES_128 = "AES-128"; + public final Uri baseUri; public final int mediaSequence; public final int targetDurationSecs; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java index d2d514c01eb..a747ab19c97 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java @@ -40,6 +40,11 @@ public final class HlsMediaPlaylistParser implements ManifestParser