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())); } }; }