forked from j62/ctbrec
Add support for HLS AES encryption
This commit is contained in:
parent
151b6a531d
commit
ca2ceb7f43
|
@ -24,6 +24,7 @@ import com.iheartradio.m3u8.ParseException;
|
||||||
import com.iheartradio.m3u8.ParsingMode;
|
import com.iheartradio.m3u8.ParsingMode;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
import com.iheartradio.m3u8.PlaylistParser;
|
import com.iheartradio.m3u8.PlaylistParser;
|
||||||
|
import com.iheartradio.m3u8.data.EncryptionData;
|
||||||
import com.iheartradio.m3u8.data.MediaPlaylist;
|
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||||
import com.iheartradio.m3u8.data.Playlist;
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
import com.iheartradio.m3u8.data.TrackData;
|
import com.iheartradio.m3u8.data.TrackData;
|
||||||
|
@ -87,6 +88,12 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
lsp.totalDuration += trackData.getTrackInfo().duration;
|
lsp.totalDuration += trackData.getTrackInfo().duration;
|
||||||
lsp.lastSegDuration = trackData.getTrackInfo().duration;
|
lsp.lastSegDuration = trackData.getTrackInfo().duration;
|
||||||
lsp.segments.add(uri);
|
lsp.segments.add(uri);
|
||||||
|
if(trackData.hasEncryptionData()) {
|
||||||
|
lsp.encrypted = true;
|
||||||
|
EncryptionData data = trackData.getEncryptionData();
|
||||||
|
lsp.encryptionKeyUrl = data.getUri();
|
||||||
|
lsp.encryptionMethod = data.getMethod().getValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return lsp;
|
return lsp;
|
||||||
}
|
}
|
||||||
|
@ -155,6 +162,9 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
public float lastSegDuration = 0;
|
public float lastSegDuration = 0;
|
||||||
public float targetDuration = 0;
|
public float targetDuration = 0;
|
||||||
public List<String> segments = new ArrayList<>();
|
public List<String> segments = new ArrayList<>();
|
||||||
|
public boolean encrypted = false;
|
||||||
|
public String encryptionMethod = "AES-128";
|
||||||
|
public String encryptionKeyUrl;
|
||||||
|
|
||||||
public SegmentPlaylist(String url) {
|
public SegmentPlaylist(String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package ctbrec.recorder.download;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherInputStream;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Crypto {
|
||||||
|
|
||||||
|
private byte[] iv = new byte[16];
|
||||||
|
private byte[] key = new byte[16];
|
||||||
|
private Cipher cipher;
|
||||||
|
private HttpClient client;
|
||||||
|
|
||||||
|
public Crypto(String encryptionKeyUrl, HttpClient client) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
|
||||||
|
this.client = client;
|
||||||
|
loadKey(encryptionKeyUrl);
|
||||||
|
initCypher();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void initCypher() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||||
|
cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
|
SecretKey secretKey = new SecretKeySpec(key, "AES");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void loadKey(String url) throws IOException {
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
try (Response response = client.execute(request)) {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
key = response.body().bytes();
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] decrypt(byte[] input) throws IllegalBlockSizeException, BadPaddingException {
|
||||||
|
return cipher.doFinal(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public CipherInputStream wrap(InputStream in) {
|
||||||
|
return new CipherInputStream(in, cipher);
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,28 +78,28 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
int nextSegment = 0;
|
int nextSegment = 0;
|
||||||
int waitFactor = 1;
|
int waitFactor = 1;
|
||||||
while(running) {
|
while(running) {
|
||||||
SegmentPlaylist lsp = getNextSegments(segments);
|
SegmentPlaylist playlist = getNextSegments(segments);
|
||||||
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
if(nextSegment > 0 && playlist.seq > nextSegment) {
|
||||||
// TODO switch to a lower bitrate/resolution ?!?
|
// TODO switch to a lower bitrate/resolution ?!?
|
||||||
waitFactor *= 2;
|
waitFactor *= 2;
|
||||||
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegment, lsp.seq, model, waitFactor);
|
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegment, playlist.seq, model, waitFactor);
|
||||||
}
|
}
|
||||||
int skip = nextSegment - lsp.seq;
|
int skip = nextSegment - playlist.seq;
|
||||||
for (String segment : lsp.segments) {
|
for (String segment : playlist.segments) {
|
||||||
if(skip > 0) {
|
if(skip > 0) {
|
||||||
skip--;
|
skip--;
|
||||||
} else {
|
} else {
|
||||||
URL segmentUrl = new URL(segment);
|
URL segmentUrl = new URL(segment);
|
||||||
String prefix = nf.format(segmentCounter++);
|
String prefix = nf.format(segmentCounter++);
|
||||||
downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client, prefix));
|
downloadThreadPool.submit(new SegmentDownload(playlist, segmentUrl, downloadDir, client, prefix));
|
||||||
//new SegmentDownload(segment, downloadDir).call();
|
//new SegmentDownload(segment, downloadDir).call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long wait = 0;
|
long wait = 0;
|
||||||
if(lastSegment == lsp.seq) {
|
if(lastSegment == playlist.seq) {
|
||||||
// playlist didn't change -> wait for at least half the target duration
|
// playlist didn't change -> wait for at least half the target duration
|
||||||
wait = (long) lsp.targetDuration * 1000 / waitFactor;
|
wait = (long) playlist.targetDuration * 1000 / waitFactor;
|
||||||
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
||||||
} else {
|
} else {
|
||||||
// playlist did change -> wait for at least last segment duration
|
// playlist did change -> wait for at least last segment duration
|
||||||
|
@ -117,9 +117,9 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
// this if check makes sure, that we don't decrease nextSegment. for some reason
|
// this if check makes sure, that we don't decrease nextSegment. for some reason
|
||||||
// streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79
|
// streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79
|
||||||
lastSegment = lsp.seq;
|
lastSegment = playlist.seq;
|
||||||
if(lastSegment + lsp.segments.size() > nextSegment) {
|
if(lastSegment + playlist.segments.size() > nextSegment) {
|
||||||
nextSegment = lastSegment + lsp.segments.size();
|
nextSegment = lastSegment + playlist.segments.size();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -170,8 +170,10 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
private URL url;
|
private URL url;
|
||||||
private Path file;
|
private Path file;
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
|
private SegmentPlaylist playlist;
|
||||||
|
|
||||||
public SegmentDownload(URL url, Path dir, HttpClient client, String prefix) {
|
public SegmentDownload(SegmentPlaylist playlist, URL url, Path dir, HttpClient client, String prefix) {
|
||||||
|
this.playlist = playlist;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
File path = new File(url.getPath());
|
File path = new File(url.getPath());
|
||||||
|
@ -185,10 +187,12 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
for (int i = 1; i <= maxTries; i++) {
|
for (int i = 1; i <= maxTries; i++) {
|
||||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||||
Response response = client.execute(request);
|
Response response = client.execute(request);
|
||||||
try (
|
InputStream in = null;
|
||||||
FileOutputStream fos = new FileOutputStream(file.toFile());
|
try (FileOutputStream fos = new FileOutputStream(file.toFile())) {
|
||||||
InputStream in = response.body().byteStream())
|
in = response.body().byteStream();
|
||||||
{
|
if(playlist.encrypted) {
|
||||||
|
in = new Crypto(playlist.encryptionKeyUrl, client).wrap(in);
|
||||||
|
}
|
||||||
byte[] b = new byte[1024 * 100];
|
byte[] b = new byte[1024 * 100];
|
||||||
int length = -1;
|
int length = -1;
|
||||||
while( (length = in.read(b)) >= 0 ) {
|
while( (length = in.read(b)) >= 0 ) {
|
||||||
|
@ -205,6 +209,9 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
LOG.warn("Error while downloading segment on try {}", i);
|
LOG.warn("Error while downloading segment on try {}", i);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if(in != null) {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
response.close();
|
response.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,7 +233,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException {
|
private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException {
|
||||||
for (String segment : lsp.segments) {
|
for (String segment : lsp.segments) {
|
||||||
URL segmentUrl = new URL(segment);
|
URL segmentUrl = new URL(segment);
|
||||||
SegmentDownload segmentDownload = new SegmentDownload(segmentUrl, client);
|
SegmentDownload segmentDownload = new SegmentDownload(lsp, segmentUrl, client);
|
||||||
byte[] segmentData = segmentDownload.call();
|
byte[] segmentData = segmentDownload.call();
|
||||||
writeSegment(segmentData);
|
writeSegment(segmentData);
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
skip--;
|
skip--;
|
||||||
} else {
|
} else {
|
||||||
URL segmentUrl = new URL(segment);
|
URL segmentUrl = new URL(segment);
|
||||||
Future<byte[]> download = downloadThreadPool.submit(new SegmentDownload(segmentUrl, client));
|
Future<byte[]> download = downloadThreadPool.submit(new SegmentDownload(lsp, segmentUrl, client));
|
||||||
downloads.add(download);
|
downloads.add(download);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -448,8 +448,10 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private class SegmentDownload implements Callable<byte[]> {
|
private class SegmentDownload implements Callable<byte[]> {
|
||||||
private URL url;
|
private URL url;
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
|
private SegmentPlaylist lsp;
|
||||||
|
|
||||||
public SegmentDownload(URL url, HttpClient client) {
|
public SegmentDownload(SegmentPlaylist lsp, URL url, HttpClient client) {
|
||||||
|
this.lsp = lsp;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -463,15 +465,18 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
if(response.isSuccessful()) {
|
if(response.isSuccessful()) {
|
||||||
byte[] segment = response.body().bytes();
|
byte[] segment = response.body().bytes();
|
||||||
|
if(lsp.encrypted) {
|
||||||
|
segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment);
|
||||||
|
}
|
||||||
return segment;
|
return segment;
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(response.code(), response.message());
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
if (i == maxTries) {
|
if (i == maxTries) {
|
||||||
LOG.warn("Error while downloading segment. Segment {} finally failed", url.getFile());
|
LOG.error("Error while downloading segment. Segment {} finally failed", url.getFile());
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("Error while downloading segment {} on try {}", url.getFile(), i, e);
|
LOG.trace("Error while downloading segment {} on try {}", url.getFile(), i, e);
|
||||||
}
|
}
|
||||||
if(model != null && !isModelOnline()) {
|
if(model != null && !isModelOnline()) {
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in New Issue