diff --git a/app/src/main/java/com/fongmi/android/tv/player/Source.java b/app/src/main/java/com/fongmi/android/tv/player/Source.java
index 61477f573..e34ef7f2c 100644
--- a/app/src/main/java/com/fongmi/android/tv/player/Source.java
+++ b/app/src/main/java/com/fongmi/android/tv/player/Source.java
@@ -2,7 +2,6 @@ package com.fongmi.android.tv.player;
import com.fongmi.android.tv.bean.Channel;
import com.fongmi.android.tv.bean.Result;
-import com.fongmi.android.tv.player.extractor.BiliBili;
import com.fongmi.android.tv.player.extractor.Force;
import com.fongmi.android.tv.player.extractor.JianPian;
import com.fongmi.android.tv.player.extractor.Push;
@@ -30,7 +29,6 @@ public class Source {
public Source() {
extractors = new ArrayList<>();
- extractors.add(new BiliBili());
extractors.add(new Force());
extractors.add(new JianPian());
extractors.add(new Push());
diff --git a/app/src/main/java/com/fongmi/android/tv/player/extractor/BiliBili.java b/app/src/main/java/com/fongmi/android/tv/player/extractor/BiliBili.java
deleted file mode 100644
index b5b3b319d..000000000
--- a/app/src/main/java/com/fongmi/android/tv/player/extractor/BiliBili.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.fongmi.android.tv.player.extractor;
-
-import android.net.Uri;
-
-import com.fongmi.android.tv.player.Source;
-import com.github.catvod.net.OkHttp;
-import com.github.catvod.utils.Json;
-import com.github.catvod.utils.Util;
-import com.google.common.net.HttpHeaders;
-
-import okhttp3.Headers;
-
-public class BiliBili implements Source.Extractor {
-
- @Override
- public boolean match(String scheme, String host) {
- return "live.bilibili.com".equals(host);
- }
-
- @Override
- public String fetch(String url) throws Exception {
- String room = Uri.parse(url).getPath().replace("/", "");
- String api = String.format("https://api.live.bilibili.com/room/v1/Room/playUrl?cid=%s&qn=20000&platform=h5", room);
- String result = OkHttp.newCall(api, Headers.of(HttpHeaders.USER_AGENT, Util.CHROME)).execute().body().string();
- return Json.parse(result).getAsJsonObject().get("data").getAsJsonObject().get("durl").getAsJsonArray().get(0).getAsJsonObject().get("url").getAsString();
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- public void exit() {
- }
-}
diff --git a/app/src/main/java/com/fongmi/android/tv/player/extractor/Youtube.java b/app/src/main/java/com/fongmi/android/tv/player/extractor/Youtube.java
index 75b5bb553..a5ff3c579 100644
--- a/app/src/main/java/com/fongmi/android/tv/player/extractor/Youtube.java
+++ b/app/src/main/java/com/fongmi/android/tv/player/extractor/Youtube.java
@@ -1,21 +1,24 @@
package com.fongmi.android.tv.player.extractor;
+import android.text.TextUtils;
+import android.util.Base64;
+
import com.fongmi.android.tv.impl.NewPipeImpl;
import com.fongmi.android.tv.player.Source;
import com.github.catvod.net.OkHttp;
+import com.github.catvod.utils.Json;
import com.github.catvod.utils.Util;
import com.google.common.net.HttpHeaders;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.ServiceList;
-import org.schabi.newpipe.extractor.exceptions.ExtractionException;
-import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
-import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
+import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptPlayerManager;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.utils.Parser;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -23,6 +26,9 @@ import okhttp3.Headers;
public class Youtube implements Source.Extractor {
+ private static final String MPD = "\n" + "\n" + "%s\n" + "%s\n" + "\n" + "";
+ private static final String ADAPTATION_SET = "\n" + "\n" + "\n" + "%s\n" + "\n" + "\n" + "\n" + "\n" + "";
+
@Override
public boolean match(String scheme, String host) {
return host.contains("youtube.com") || host.contains("youtu.be");
@@ -34,37 +40,80 @@ public class Youtube implements Source.Extractor {
@Override
public String fetch(String url) throws Exception {
+ String id = YoutubeStreamLinkHandlerFactory.getInstance().getId(url);
String html = OkHttp.newCall(url, Headers.of(HttpHeaders.USER_AGENT, Util.CHROME)).execute().body().string();
- Matcher matcher = Pattern.compile("hlsManifestUrl\\S*?(https\\S*?\\.m3u8)").matcher(html);
- if (matcher.find()) {
- html = OkHttp.newCall(matcher.group(1), Headers.of(HttpHeaders.USER_AGENT, Util.CHROME)).execute().body().string();
- return find(html);
- } else {
- LinkHandler handler = YoutubeStreamLinkHandlerFactory.getInstance().fromUrl(url);
- YoutubeStreamExtractor extractor = new YoutubeStreamExtractor(ServiceList.YouTube, handler);
- extractor.fetchPage();
- return find(extractor);
+ Matcher matcher = Pattern.compile("var ytInitialPlayerResponse =(.*?\\});").matcher(html);
+ if (!matcher.find()) return "";
+ JsonObject streamingData = Json.parse(matcher.group(1)).getAsJsonObject().get("streamingData").getAsJsonObject();
+ if (streamingData.has("hlsManifestUrl")) return getHlsManifestUrl(streamingData);
+ if (streamingData.has("adaptiveFormats")) return getMpdWithBase64(streamingData, id);
+ return url;
+ }
+
+ private String getHlsManifestUrl(JsonObject streamingData) {
+ JsonElement hlsManifestUrl = streamingData.get("hlsManifestUrl");
+ if (hlsManifestUrl.isJsonArray()) return hlsManifestUrl.getAsJsonArray().get(0).getAsString();
+ return hlsManifestUrl.getAsString();
+ }
+
+ private String getMpdWithBase64(JsonObject streamingData, String videoId) {
+ String approxDurationMs = "";
+ StringBuilder video = new StringBuilder();
+ StringBuilder audio = new StringBuilder();
+ for (JsonElement element : streamingData.get("adaptiveFormats").getAsJsonArray()) {
+ JsonObject adaptiveFormat = element.getAsJsonObject();
+ String mimeType = adaptiveFormat.get("mimeType").getAsString();
+ if (mimeType.contains("video")) video.append(getAdaptationSet(videoId, adaptiveFormat, "video", mimeType.split(";")));
+ if (mimeType.contains("audio")) audio.append(getAdaptationSet(videoId, adaptiveFormat, "audio", mimeType.split(";")));
+ if (TextUtils.isEmpty(approxDurationMs)) approxDurationMs = adaptiveFormat.get("approxDurationMs").getAsString();
}
+ String duration = String.format(Locale.getDefault(), "PT%.3fS", Integer.parseInt(approxDurationMs) / 1000.0);
+ String finalMpd = String.format(Locale.getDefault(), MPD, duration, duration, video, audio);
+ return "data:application/dash+xml;base64," + Base64.encodeToString(finalMpd.getBytes(), 0);
}
- private String find(YoutubeStreamExtractor extractor) throws ExtractionException {
- VideoStream item = extractor.getVideoStreams().get(0);
- for (VideoStream stream : extractor.getVideoStreams()) if (!stream.isVideoOnly() && stream.getHeight() >= item.getHeight()) item = stream;
- return item.getContent();
+ private String getAdaptationSet(String videoId, JsonObject adaptiveFormat, String contentType, String[] split) {
+ String mediaParam = "";
+ String mimeType = split[0];
+ String baseUrl = getBaseUrl(videoId, adaptiveFormat);
+ String iTag = adaptiveFormat.get("itag").getAsString();
+ int bitrate = adaptiveFormat.get("bitrate").getAsInt();
+ String codecs = split[1].split("=")[1].replace("\"", "");
+ JsonObject initRange = adaptiveFormat.get("initRange").getAsJsonObject();
+ JsonObject indexRange = adaptiveFormat.get("indexRange").getAsJsonObject();
+ String initParam = initRange.get("start").getAsString() + "-" + initRange.get("end").getAsString();
+ String indexParam = indexRange.get("start").getAsString() + "-" + indexRange.get("end").getAsString();
+
+ if (mimeType.contains("video")) {
+ int fps = adaptiveFormat.get("fps").getAsInt();
+ int width = adaptiveFormat.get("width").getAsInt();
+ int height = adaptiveFormat.get("height").getAsInt();
+ mediaParam = String.format(Locale.getDefault(), "height='%d' width='%d' frameRate='%d'", height, width, fps);
+ }
+
+ if (mimeType.contains("audio")) {
+ int audioSamplingRate = adaptiveFormat.get("audioSampleRate").getAsInt();
+ mediaParam = String.format(Locale.getDefault(), "subsegmentAlignment='true' audioSamplingRate='%d'", audioSamplingRate);
+ }
+
+ return String.format(Locale.getDefault(), ADAPTATION_SET, contentType, iTag, bitrate, codecs, mimeType, mediaParam, baseUrl, indexParam, initParam);
}
- private String find(String html) {
- String url = "";
- List items = Arrays.asList("301", "300", "96", "95", "94");
- for (String item : items) if (!(url = find(html, "https:/.*/" + item + "/.*index.m3u8")).isEmpty()) break;
- return url;
+ private String getBaseUrl(String videoId, JsonObject adaptiveFormat) {
+ String baseUrl;
+ if (adaptiveFormat.has("url")) baseUrl = adaptiveFormat.get("url").getAsString();
+ else baseUrl = decodeCipher(videoId, adaptiveFormat);
+ return baseUrl.replace("&", "&");
}
- private String find(String html, String rule) {
- Pattern pattern = Pattern.compile(rule);
- Matcher matcher = pattern.matcher(html);
- if (matcher.find()) return matcher.group();
- return "";
+ private String decodeCipher(String videoId, JsonObject adaptiveFormat) {
+ try {
+ String cipherString = adaptiveFormat.has("cipher") ? adaptiveFormat.get("cipher").getAsString() : adaptiveFormat.get("signatureCipher").getAsString();
+ Map cipher = Parser.compatParseMap(cipherString);
+ return cipher.get("url") + "&" + cipher.get("sp") + "=" + YoutubeJavaScriptPlayerManager.deobfuscateSignature(videoId, cipher.get("s"));
+ } catch (Exception e) {
+ return "";
+ }
}
@Override
diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SyncDialog.java b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SyncDialog.java
index 86826ac10..969c53b7a 100644
--- a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SyncDialog.java
+++ b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SyncDialog.java
@@ -11,7 +11,6 @@ import androidx.viewbinding.ViewBinding;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.Constant;
-import com.fongmi.android.tv.R;
import com.fongmi.android.tv.api.config.VodConfig;
import com.fongmi.android.tv.bean.Config;
import com.fongmi.android.tv.bean.Device;
@@ -118,10 +117,6 @@ public class SyncDialog extends BaseDialog implements DeviceAdapter.OnClickListe
dismiss();
}
- private void onError() {
- Notify.show(R.string.device_offline);
- }
-
@Subscribe(threadMode = ThreadMode.MAIN)
public void onScanEvent(ScanEvent event) {
ScanTask.create(this).start(event.getAddress());
@@ -152,7 +147,7 @@ public class SyncDialog extends BaseDialog implements DeviceAdapter.OnClickListe
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
- App.post(() -> onError());
+ App.post(() -> Notify.show(e.getMessage()));
}
};
}