diff --git a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java index 092864ac3..cbd10af88 100644 --- a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java @@ -71,9 +71,8 @@ public class M3UDownloader implements ITrackDownloader { } VKM3UParser parser = new VKM3UParser(payload); - List tses = parser.getTransportStreams(); AtomicInteger progress = new AtomicInteger(0); - List> futures = new ArrayList<>(tses.size()); + List> 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); diff --git a/app/src/main/java/ru/vtosters/lite/music/TransportStream.java b/app/src/main/java/ru/vtosters/lite/music/TransportStream.java index 72df073bb..12404a398 100644 --- a/app/src/main/java/ru/vtosters/lite/music/TransportStream.java +++ b/app/src/main/java/ru/vtosters/lite/music/TransportStream.java @@ -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; } } diff --git a/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java b/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java index 83e62f035..8393c86eb 100644 --- a/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java +++ b/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java @@ -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 M3U + **/ public class VKM3UParser { - private final String mData; - private final List 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 EXT-X-VERSION + **/ + 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 EXT-X-TARGETDURATION + **/ + public int mTargetDuration=-1; + /** + * @see EXT-X-MEDIA-SEQUENCE + **/ + public int mMediaSequence=-1; + /** + * @see EXT-X-DISCONTINUITY-SEQUENCE + **/ + public int mDiscontinuitySequence=-1; + /** + * @see EXT-X-PLAYLIST-TYPE + **/ + public int mPlaylistType=-1; + /** + * @see EXT-X-I-FRAMES-ONLY + **/ + public boolean mIFramesOnly=false; + //endregion + + public long mHeapSize=0; + + final private String mData; + final public List 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 EXT-X-KEY + **/ + 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 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())); } }