init
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user