Skip to content

Commit

Permalink
Add AES-128 encryption support for HLS #69 and parsing logic for CODE…
Browse files Browse the repository at this point in the history
…CS and RESOLUTION attributes.
  • Loading branch information
andudo committed Nov 4, 2014
1 parent a21c9eb commit a76addb
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -38,7 +41,7 @@
*/
public class HlsChunkSource {

private final DataSource dataSource;
private final DataSource upstreamDataSource;
private final HlsMasterPlaylist masterPlaylist;
private final HlsMediaPlaylistParser mediaPlaylistParser;

Expand All @@ -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();
}
Expand Down Expand Up @@ -144,8 +150,22 @@ public void getChunkOperation(List<TsChunk> 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;
Expand All @@ -168,8 +188,15 @@ public void getChunkOperation(List<TsChunk> 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() {
Expand All @@ -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 {
Expand All @@ -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);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -52,6 +58,10 @@ private static HlsMasterPlaylist parseMasterPlaylist(InputStream inputStream,
? 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();
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,19 @@ public static final class Segment implements Comparable<Long> {
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
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.]+),");
Expand All @@ -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 {
Expand All @@ -70,6 +82,11 @@ private static HlsMediaPlaylist parseMediaPlaylist(InputStream inputStream, Stri
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) {
Expand All @@ -82,16 +99,34 @@ private static HlsMediaPlaylist parseMediaPlaylist(InputStream inputStream, Stri
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public static String parseStringAttr(String line, Pattern pattern, String tag)
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,14 @@ private boolean continueBufferingInternal() throws IOException {
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;
}
}
}
}

Expand Down
Loading

0 comments on commit a76addb

Please sign in to comment.