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.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.EncryptionData;
|
||||
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.TrackData;
|
||||
|
@ -87,6 +88,12 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
lsp.totalDuration += trackData.getTrackInfo().duration;
|
||||
lsp.lastSegDuration = trackData.getTrackInfo().duration;
|
||||
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;
|
||||
}
|
||||
|
@ -155,6 +162,9 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
public float lastSegDuration = 0;
|
||||
public float targetDuration = 0;
|
||||
public List<String> segments = new ArrayList<>();
|
||||
public boolean encrypted = false;
|
||||
public String encryptionMethod = "AES-128";
|
||||
public String encryptionKeyUrl;
|
||||
|
||||
public SegmentPlaylist(String 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 waitFactor = 1;
|
||||
while(running) {
|
||||
SegmentPlaylist lsp = getNextSegments(segments);
|
||||
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
||||
SegmentPlaylist playlist = getNextSegments(segments);
|
||||
if(nextSegment > 0 && playlist.seq > nextSegment) {
|
||||
// TODO switch to a lower bitrate/resolution ?!?
|
||||
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;
|
||||
for (String segment : lsp.segments) {
|
||||
int skip = nextSegment - playlist.seq;
|
||||
for (String segment : playlist.segments) {
|
||||
if(skip > 0) {
|
||||
skip--;
|
||||
} else {
|
||||
URL segmentUrl = new URL(segment);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
long wait = 0;
|
||||
if(lastSegment == lsp.seq) {
|
||||
if(lastSegment == playlist.seq) {
|
||||
// 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);
|
||||
} else {
|
||||
// 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
|
||||
// streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79
|
||||
lastSegment = lsp.seq;
|
||||
if(lastSegment + lsp.segments.size() > nextSegment) {
|
||||
nextSegment = lastSegment + lsp.segments.size();
|
||||
lastSegment = playlist.seq;
|
||||
if(lastSegment + playlist.segments.size() > nextSegment) {
|
||||
nextSegment = lastSegment + playlist.segments.size();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -170,8 +170,10 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
private URL url;
|
||||
private Path file;
|
||||
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.client = client;
|
||||
File path = new File(url.getPath());
|
||||
|
@ -185,10 +187,12 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
for (int i = 1; i <= maxTries; i++) {
|
||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||
Response response = client.execute(request);
|
||||
try (
|
||||
FileOutputStream fos = new FileOutputStream(file.toFile());
|
||||
InputStream in = response.body().byteStream())
|
||||
{
|
||||
InputStream in = null;
|
||||
try (FileOutputStream fos = new FileOutputStream(file.toFile())) {
|
||||
in = response.body().byteStream();
|
||||
if(playlist.encrypted) {
|
||||
in = new Crypto(playlist.encryptionKeyUrl, client).wrap(in);
|
||||
}
|
||||
byte[] b = new byte[1024 * 100];
|
||||
int length = -1;
|
||||
while( (length = in.read(b)) >= 0 ) {
|
||||
|
@ -205,6 +209,9 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
LOG.warn("Error while downloading segment on try {}", i);
|
||||
}
|
||||
} finally {
|
||||
if(in != null) {
|
||||
in.close();
|
||||
}
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,7 +233,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException {
|
||||
for (String segment : lsp.segments) {
|
||||
URL segmentUrl = new URL(segment);
|
||||
SegmentDownload segmentDownload = new SegmentDownload(segmentUrl, client);
|
||||
SegmentDownload segmentDownload = new SegmentDownload(lsp, segmentUrl, client);
|
||||
byte[] segmentData = segmentDownload.call();
|
||||
writeSegment(segmentData);
|
||||
}
|
||||
|
@ -258,7 +258,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
skip--;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -448,8 +448,10 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
private class SegmentDownload implements Callable<byte[]> {
|
||||
private URL url;
|
||||
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.client = client;
|
||||
}
|
||||
|
@ -463,15 +465,18 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
try (Response response = client.execute(request)) {
|
||||
if(response.isSuccessful()) {
|
||||
byte[] segment = response.body().bytes();
|
||||
if(lsp.encrypted) {
|
||||
segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment);
|
||||
}
|
||||
return segment;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
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 {
|
||||
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()) {
|
||||
break;
|
||||
|
|
Loading…
Reference in New Issue