This commit is contained in:
Vologhat
2023-05-05 20:48:39 +03:00
committed by gdlbo
parent cc55d5ddaa
commit 64c55d4b01
3 changed files with 149 additions and 80 deletions
@@ -71,9 +71,8 @@ public class M3UDownloader implements ITrackDownloader {
}
VKM3UParser parser = new VKM3UParser(payload);
List<TransportStream> tses = parser.getTransportStreams();
AtomicInteger progress = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = new ArrayList<>(tses.size());
List<CompletableFuture<Void>> futures = new ArrayList<>(parser.mTransportStreams.size());
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
var tsesDir = new File(filesDir, String.valueOf(track.d));
@@ -84,23 +83,23 @@ public class M3UDownloader implements ITrackDownloader {
var resultMp3 = new File(outDir, fileName);
callback.onProgress(5);
for (TransportStream ts : tses) {
String tsURL = parser.getBaseUrl() + URLEncoder.encode(ts.getName(), "UTF-8");
for (int i = 0; i < parser.mTransportStreams.size(); i++) {
var ts=parser.mTransportStreams.get(i);
var index=i;
futures.add(CompletableFuture.runAsync(() -> {
try {
InputStream is = IOUtils.openStream(tsURL);
InputStream is = IOUtils.openStream(ts.getMediaSegmentUri());
byte[] content;
if (TransportStream.METHOD_AES128.equals(ts.getMethod())) {
if (ts.needDecoding()) {
String key = IOUtils.readAllLines(IOUtils.openStream(ts.getKeyURL()));
content = IOUtils.decodeStream(is, key);
} else {
content = IOUtils.readFully(is);
}
File tsDump = new File(tsesDir, ts.getName());
File tsDump = new File(tsesDir,index+".ts");
IOUtils.writeToFile(tsDump, content);
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size()));
callback.onSizeReceived((long) content.length * tses.size(), parser.getHeapSize());
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / parser.mTransportStreams.size()));
callback.onSizeReceived((long) content.length * parser.mTransportStreams.size(), parser.mHeapSize);
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | InvalidKeyException e) {
callback.onFailure();
@@ -115,7 +114,7 @@ public class M3UDownloader implements ITrackDownloader {
return null;
}).thenApply(v -> TSMerger.merge(tsesDir, resultTs)).thenApply(mergeResult -> mergeResult && FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track)).thenApply(convertResult -> {
if (convertResult)
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size()));
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / parser.mTransportStreams.size()));
else callback.onFailure();
return convertResult;
}).thenRun(() -> IOUtils.deleteRecursive(tsesDir)).thenRun(callback::onSuccess);
@@ -1,36 +1,42 @@
package ru.vtosters.lite.music;
import android.text.TextUtils;
public class TransportStream {
public static final String METHOD_NONE = "NONE";
public static final String METHOD_AES128 = "AES-128";
private String mKeyUri;
private String mMediaSegmentUri;
private String mMediaSegmentName;
private final String mMethod;
private String mName;
private String mKeyURL;
public TransportStream() {
this.mMethod = METHOD_NONE;
public TransportStream(String baseURI,String mediaSegmentName)
{
this.mMediaSegmentUri=baseURI+mediaSegmentName;
}
public TransportStream(String method, String keyURL) {
this.mMethod = method;
this.mKeyURL = keyURL;
public TransportStream(String keyURI,String baseURI,String mediaSegmentName)
{
this.mKeyUri=keyURI;
this.mMediaSegmentUri=baseURI+mediaSegmentName;
this.mMediaSegmentName=mediaSegmentName;
}
public String getMethod() {
return mMethod;
public boolean needDecoding()
{
return !TextUtils.isEmpty(mKeyUri);
}
public String getName() {
return mName;
public String getKeyURL()
{
return mKeyUri;
}
public void setName(String name) {
mName = name;
public String getMediaSegmentUri()
{
return this.mMediaSegmentUri;
}
public String getKeyURL() {
return mKeyURL;
public String getMediaSegmentName()
{
return this.mMediaSegmentName;
}
}
@@ -1,69 +1,133 @@
package ru.vtosters.lite.music;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* The parser for extended M3U playlist format used by VK.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#ref-M3U">M3U</a>
**/
public class VKM3UParser {
private final String mData;
private final List<TransportStream> mTransportStreams = new ArrayList<>();
//Playlist types
final static public int PLAYLIST_TYPE_VOD=0;
final static public int PLAYLIST_TYPE_EVENT=1;
// maybe will be used
private int mDuration;
private int mHeapSize = 0;
private String mBaseURL;
//region Extended M3U global extensions
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.1.2">EXT-X-VERSION</a>
**/
public int mVersion=-1;
/**
* The EXT-X-ALLOW-CACHE tag is used in protocol version 6 or lower.
* If the protocol version is upgraded by VK, we will not support that.
**/
public boolean mAllowCache=false;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1">EXT-X-TARGETDURATION</a>
**/
public int mTargetDuration=-1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2">EXT-X-MEDIA-SEQUENCE</a>
**/
public int mMediaSequence=-1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.3">EXT-X-DISCONTINUITY-SEQUENCE</a>
**/
public int mDiscontinuitySequence=-1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.5">EXT-X-PLAYLIST-TYPE</a>
**/
public int mPlaylistType=-1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.6">EXT-X-I-FRAMES-ONLY</a>
**/
public boolean mIFramesOnly=false;
//endregion
public long mHeapSize=0;
final private String mData;
final public List<TransportStream> mTransportStreams = new ArrayList<>();
public VKM3UParser(String data) {
mData = data;
mData=data;
init();
}
public static boolean isM3U8(String str) {
public static boolean isTS(String str) {
return str.endsWith(".ts") || str.endsWith(".tp") || str.endsWith(".mpeg-ts") || str.endsWith(".m2ts");
}
private void init() {
int i = 0;
Scanner scanner = new Scanner(mData);
while (scanner.hasNext()) {
String line = scanner.nextLine();
mHeapSize += line.getBytes().length;
if (line.startsWith("#EXT-X-TARGETDURATION")) {
mDuration = Integer.parseInt(getDirectiveValue(line));
} else if (line.startsWith("#EXT-X-KEY")) {
String base = getDirectiveValue(line);
final TransportStream ts;
if (!base.startsWith("METHOD=NONE")) {
String keyURL = base.substring(base.indexOf("\"") + 1, base.lastIndexOf("\""));
mBaseURL = keyURL.substring(0, keyURL.lastIndexOf("/") + 1);
ts = new TransportStream(TransportStream.METHOD_AES128, keyURL);
} else {
ts = new TransportStream();
}
mTransportStreams.add(ts);
} else if (line.startsWith("#EXTINF")) {
if (mTransportStreams.size() == i)
mTransportStreams.add(new TransportStream());
} else if (isM3U8(line)) {
mTransportStreams.get(i).setName(line.substring(line.lastIndexOf("/") + 1));
++i;
}
}
}
private void init()
{
if(TextUtils.isEmpty(mData))throw new NullPointerException("mData==null");
Log.e("Parser",mData);
final var scanner=new Scanner(mData);
var line=scanner.nextLine();
if(!"#EXTM3U".equals(line))throw new IllegalStateException(String.format("Unknown initial M3U tag: %s",line));
//indicators for EXT-X-KEY and EXTINF tags
boolean aes128,extinf;
aes128=extinf=false;
/**
* VK follows own URI attribute for EXT-X-KEY tag.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.2.4">EXT-X-KEY</a>
**/
String keyUri;
String baseUri;//At least one media segment must be encrypted to get the base uri
keyUri=baseUri="";
while(scanner.hasNextLine())
{
line=scanner.nextLine();
if('#'==line.charAt(0))
{
if(line.startsWith("#EXT-X-VERSION:"))
mVersion=Integer.parseInt(line.substring(15));
else if(line.startsWith("#EXT-X-ALLOW-CACHE:"))
mAllowCache="YES".equalsIgnoreCase(line.substring(19));
else if(line.startsWith("#EXT-X-TARGETDURATION:"))
mTargetDuration=Integer.parseInt(line.substring(22));
else if(line.startsWith("#EXT-X-MEDIA-SEQUENCE:"))
mMediaSequence=Integer.parseInt(line.substring(22));
else if(line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:"))
mDiscontinuitySequence=Integer.parseInt(line.substring(30));
else if(line.startsWith("#EXT-X-PLAYLIST-TYPE:"))
switch (line.substring(21))
{
case "VOD"->mPlaylistType=PLAYLIST_TYPE_VOD;
case "EVENT"->mPlaylistType=PLAYLIST_TYPE_EVENT;
default->throw new IllegalStateException(String.format("Unknown playlist type: %s", line.substring(21)));
}
else if(line.startsWith("#EXT-X-I-FRAMES-ONLY"))
mIFramesOnly=true;
else if(line.startsWith("#EXT-X-KEY:"))
{
var substr=line.substring(11);
if((aes128 = substr.startsWith("METHOD=AES-128")))
{
substr=substr.substring(20,substr.length()-1);
if(!substr.startsWith("http"))throw new IllegalStateException(String.format("Failed to parse URI: %s",substr));
keyUri=substr;
baseUri=substr.substring(0,substr.lastIndexOf("/")+1);
}
}
else if(line.startsWith("#EXTINF"))
extinf=true;
}
else if(extinf&&isTS(line))
{
var ts=aes128
?new TransportStream(keyUri,baseUri,line)
:new TransportStream(baseUri,line);
mTransportStreams.add(ts);
private String getDirectiveValue(String directive) {
return directive.substring(directive.indexOf(":") + 1);
}
public int getHeapSize() {
return mHeapSize;
}
public String getBaseUrl() {
return mBaseURL;
}
public List<TransportStream> getTransportStreams() {
return mTransportStreams;
}
else throw new RuntimeException(String.format("Failed to parse: %s",line));
mHeapSize+=line.getBytes().length;
}
for(var ts:mTransportStreams)Log.e("Parser",String.format("Added segment:\n method: %s,\n key_uri: %s,\n segment_uri: %s", ts.needDecoding(),ts.getKeyURL(),ts.getMediaSegmentUri()));
}
}