Add AES-128 encryption support for HLS #69 and parsing logic for CODECS and RESOLUTION attributes.
This commit is contained in:
parent
a21c9ebc31
commit
a76addba5d
@ -111,7 +111,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ import java.util.Collections;
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls;
|
||||
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 android.os.SystemClock;
|
||||
|
||||
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 @@ import java.util.List;
|
||||
*/
|
||||
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 class HlsChunkSource {
|
||||
}
|
||||
|
||||
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 class HlsChunkSource {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public class HlsChunkSource {
|
||||
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 @@ public class HlsChunkSource {
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,9 +36,15 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
|
||||
|
||||
private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF";
|
||||
private static final String BANDWIDTH_ATTR = "BANDWIDTH";
|
||||
private static final String CODECS_ATTR = "CODECS";
|
||||
private static final String RESOLUTION_ATTR = "RESOLUTION";
|
||||
|
||||
private static final Pattern BANDWIDTH_ATTR_REGEX =
|
||||
Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b");
|
||||
private static final Pattern CODECS_ATTR_REGEX =
|
||||
Pattern.compile(CODECS_ATTR + "=\"(.+)\"");
|
||||
private static final Pattern RESOLUTION_ATTR_REGEX =
|
||||
Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)");
|
||||
|
||||
@Override
|
||||
public HlsMasterPlaylist parse(InputStream inputStream, String inputEncoding,
|
||||
@ -52,6 +58,10 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
|
||||
? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding));
|
||||
List<Variant> variants = new ArrayList<Variant>();
|
||||
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 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
|
||||
}
|
||||
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));
|
||||
|
@ -32,12 +32,19 @@ public final class HlsMediaPlaylist {
|
||||
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 final class HlsMediaPlaylist {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -40,6 +40,11 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
|
||||
private static final String TARGET_DURATION_TAG = "#EXT-X-TARGETDURATION";
|
||||
private static final String VERSION_TAG = "#EXT-X-VERSION";
|
||||
private static final String ENDLIST_TAG = "#EXT-X-ENDLIST";
|
||||
private static final String KEY_TAG = "#EXT-X-KEY";
|
||||
|
||||
private static final String METHOD_ATTR = "METHOD";
|
||||
private static final String URI_ATTR = "URI";
|
||||
private static final String IV_ATTR = "IV";
|
||||
|
||||
private static final Pattern MEDIA_DURATION_REGEX =
|
||||
Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),");
|
||||
@ -50,6 +55,13 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
|
||||
private static final Pattern VERSION_REGEX =
|
||||
Pattern.compile(VERSION_TAG + ":(\\d+)\\b");
|
||||
|
||||
private static final Pattern METHOD_ATTR_REGEX =
|
||||
Pattern.compile(METHOD_ATTR + "=([^,.*]+)");
|
||||
private static final Pattern URI_ATTR_REGEX =
|
||||
Pattern.compile(URI_ATTR + "=\"(.+)\"");
|
||||
private static final Pattern IV_ATTR_REGEX =
|
||||
Pattern.compile(IV_ATTR + "=([^,.*]+)");
|
||||
|
||||
@Override
|
||||
public HlsMediaPlaylist parse(InputStream inputStream, String inputEncoding,
|
||||
String contentId, Uri baseUri) throws IOException {
|
||||
@ -70,6 +82,11 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
|
||||
double segmentDurationSecs = 0.0;
|
||||
boolean segmentDiscontinuity = false;
|
||||
long segmentStartTimeUs = 0;
|
||||
String segmentEncryptionMethod = null;
|
||||
String segmentEncryptionKeyUri = null;
|
||||
String segmentEncryptionIV = null;
|
||||
|
||||
int segmentMediaSequence = 0;
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
@ -82,16 +99,34 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
|
||||
TARGET_DURATION_TAG);
|
||||
} else if (line.startsWith(MEDIA_SEQUENCE_TAG)) {
|
||||
mediaSequence = HlsParserUtil.parseIntAttr(line, MEDIA_SEQUENCE_REGEX, MEDIA_SEQUENCE_TAG);
|
||||
segmentMediaSequence = mediaSequence;
|
||||
} else if (line.startsWith(VERSION_TAG)) {
|
||||
version = HlsParserUtil.parseIntAttr(line, VERSION_REGEX, VERSION_TAG);
|
||||
} else if (line.startsWith(MEDIA_DURATION_TAG)) {
|
||||
segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX,
|
||||
MEDIA_DURATION_TAG);
|
||||
} else if (line.startsWith(KEY_TAG)) {
|
||||
segmentEncryptionMethod = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX,
|
||||
METHOD_ATTR);
|
||||
if (segmentEncryptionMethod.equals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE)) {
|
||||
segmentEncryptionKeyUri = null;
|
||||
segmentEncryptionIV = null;
|
||||
} else {
|
||||
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
|
||||
URI_ATTR);
|
||||
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX,
|
||||
IV_ATTR);
|
||||
if (segmentEncryptionIV == null) {
|
||||
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
|
||||
}
|
||||
}
|
||||
} else if (line.equals(DISCONTINUITY_TAG)) {
|
||||
segmentDiscontinuity = true;
|
||||
} else if (!line.startsWith("#")) {
|
||||
segmentMediaSequence++;
|
||||
segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity,
|
||||
segmentStartTimeUs));
|
||||
segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri,
|
||||
segmentEncryptionIV));
|
||||
segmentStartTimeUs += (long) (segmentDurationSecs * 1000000);
|
||||
segmentDiscontinuity = false;
|
||||
segmentDurationSecs = 0.0;
|
||||
|
@ -36,6 +36,14 @@ import java.util.regex.Pattern;
|
||||
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
|
||||
}
|
||||
|
||||
public static String parseOptionalStringAttr(String line, Pattern pattern, String tag) {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
if (matcher.find() && matcher.groupCount() == 1) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int parseIntAttr(String line, Pattern pattern, String tag)
|
||||
throws ParserException {
|
||||
return Integer.parseInt(parseStringAttr(line, pattern, tag));
|
||||
|
@ -184,10 +184,14 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||
haveSufficientSamples = true;
|
||||
} else {
|
||||
extractor.reset(mediaChunk.startTimeUs);
|
||||
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
||||
pendingDiscontinuities[i] = true;
|
||||
}
|
||||
mediaChunk.clearPendingDiscontinuity();
|
||||
if (pendingDiscontinuities == null) {
|
||||
// We're not prepared yet.
|
||||
} else {
|
||||
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
||||
pendingDiscontinuities[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.upstream;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* A {@link DataSource} that decrypts the data read from an upstream source, encrypted with AES-128
|
||||
* with a 128-bit key and PKCS7 padding.
|
||||
*
|
||||
*/
|
||||
public class Aes128DataSource implements DataSource {
|
||||
|
||||
private final DataSource upstream;
|
||||
private final byte[] secretKey;
|
||||
private final byte[] iv;
|
||||
|
||||
private Cipher cipher;
|
||||
private CipherInputStream cipherInputStream;
|
||||
|
||||
public Aes128DataSource(byte[] secretKey, byte[] iv, DataSource upstream) {
|
||||
this.upstream = upstream;
|
||||
this.secretKey = secretKey;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws IOException {
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Key cipherKey = new SecretKeySpec(secretKey, "AES");
|
||||
AlgorithmParameterSpec cipherIV = new IvParameterSpec(iv);
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
cipherInputStream = new CipherInputStream(
|
||||
new DataSourceInputStream(upstream, dataSpec), cipher);
|
||||
|
||||
return C.LENGTH_UNBOUNDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
upstream.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||
Assertions.checkState(cipherInputStream != null);
|
||||
int bytesRead = cipherInputStream.read(buffer, offset, readLength);
|
||||
if (bytesRead < 0) {
|
||||
return -1;
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user