From 971b86ffb7808c64e852b048d194f1b46cfd234b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E4=BF=8A?= <215613905@qq.com> Date: Thu, 20 Feb 2025 18:00:02 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8E=BBbom=E5=A4=B4=20?= =?UTF-8?q?=E7=9B=B4=E6=92=AD=E5=A2=9E=E5=8A=A0=E5=A3=B3=E5=AD=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86/proxy=3Fgo=3Dlive;=E4=BF=AE=E6=AD=A3=E6=A8=A1?= =?UTF-8?q?=E6=8B=9F=E5=99=A8=E6=92=AD=E6=94=BE=E4=B8=AD=E6=96=AD=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tvbox/osc/player/IjkMediaPlayer.java | 93 +++++--- .../github/tvbox/osc/server/RemoteServer.java | 26 +++ .../tvbox/osc/ui/activity/PlayActivity.java | 22 +- .../com/github/tvbox/osc/util/OkGoHelper.java | 4 +- .../java/com/github/tvbox/osc/util/Proxy.java | 198 ++++++++++++++++++ 5 files changed, 315 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/github/tvbox/osc/util/Proxy.java diff --git a/app/src/main/java/com/github/tvbox/osc/player/IjkMediaPlayer.java b/app/src/main/java/com/github/tvbox/osc/player/IjkMediaPlayer.java index c15abd9a..2599f260 100644 --- a/app/src/main/java/com/github/tvbox/osc/player/IjkMediaPlayer.java +++ b/app/src/main/java/com/github/tvbox/osc/player/IjkMediaPlayer.java @@ -5,6 +5,7 @@ import android.text.TextUtils; import com.github.tvbox.osc.api.ApiConfig; import com.github.tvbox.osc.bean.IJKCode; +import com.github.tvbox.osc.server.ControlManager; import com.github.tvbox.osc.util.FileUtils; import com.github.tvbox.osc.util.HawkConfig; import com.github.tvbox.osc.util.LOG; @@ -12,6 +13,8 @@ import com.github.tvbox.osc.util.MD5; import com.orhanobut.hawk.Hawk; import java.io.File; +import java.net.URI; +import java.net.URLEncoder; import java.util.LinkedHashMap; import java.util.Map; @@ -74,33 +77,48 @@ public class IjkMediaPlayer extends IjkPlayer { } } + private static final String ITV_TARGET_DOMAIN = "gslbserv.itv.cmvideo.cn"; @Override public void setDataSource(String path, Map headers) { try { - if (path.contains("rtsp") || path.contains("udp") || path.contains("rtp")) { - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "infbuf", 1); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp"); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp"); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 512 * 1000); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 2 * 1000 * 1000); - } else if (!TextUtils.isEmpty(path) - && !path.contains(".m3u8") - && (path.contains(".mp4") || path.contains(".mkv") || path.contains(".avi"))) { - if (Hawk.get(HawkConfig.IJK_CACHE_PLAY, false)) { - String cachePath = FileUtils.getCachePath() + "/ijkcaches/"; - String cacheMapPath = cachePath; - File cacheFile = new File(cachePath); - if (!cacheFile.exists()) cacheFile.mkdirs(); - String tmpMd5 = MD5.string2MD5(path); - cachePath += tmpMd5 + ".file"; - cacheMapPath += tmpMd5 + ".map"; - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_file_path", cachePath); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_map_path", cacheMapPath); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "parse_cache_map", 1); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "auto_save_map", 1); - mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_max_capacity", 60 * 1024 * 1024); - path = "ijkio:cache:ffio:" + path; - } + switch (getStreamType(path)) { + case RTSP_UDP_RTP: + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "infbuf", 1); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp"); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp"); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 512 * 1000); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 2 * 1000 * 1000); + break; + + case CACHE_VIDEO: + if (Hawk.get(HawkConfig.IJK_CACHE_PLAY, false)) { + String cachePath = FileUtils.getCachePath() + "/ijkcaches/"; + File cacheFile = new File(cachePath); + if (!cacheFile.exists()) cacheFile.mkdirs(); + String tmpMd5 = MD5.string2MD5(path); + String cacheFilePath = cachePath + tmpMd5 + ".file"; + String cacheMapPath = cachePath + tmpMd5 + ".map"; + + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_file_path", cacheFilePath); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_map_path", cacheMapPath); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "parse_cache_map", 1); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "auto_save_map", 1); + mMediaPlayer.setOption(tv.danmaku.ijk.media.player.IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_max_capacity", 60 * 1024 * 1024); + path = "ijkio:cache:ffio:" + path; + } + break; + + case M3U8: + // 直播且是ijk的时候自动自动走代理解决DNS + if (Hawk.get(HawkConfig.PLAYER_IS_LIVE, false) ) { + URI uri = new URI(path); + String host = uri.getHost(); + if(ITV_TARGET_DOMAIN.equalsIgnoreCase(host))path = ControlManager.get().getAddress(true) + "proxy?go=live&type=m3u8&url="+ URLEncoder.encode(path,"UTF-8"); + } + break; + + default: + break; } } catch (Exception e) { e.printStackTrace(); @@ -110,6 +128,33 @@ public class IjkMediaPlayer extends IjkPlayer { super.setDataSource(path, null); } + /** + * 解析 URL + */ + private static final int RTSP_UDP_RTP = 1; + private static final int CACHE_VIDEO = 2; + private static final int M3U8 = 3; + private static final int OTHER = 0; + + private int getStreamType(String path) { + if (TextUtils.isEmpty(path)) { + return OTHER; + } + // 低成本检查 RTSP/UDP/RTP 类型 + String lowerPath = path.toLowerCase(); + if (lowerPath.startsWith("rtsp://") || lowerPath.startsWith("udp://") || lowerPath.startsWith("rtp://")) { + return RTSP_UDP_RTP; + } + String cleanUrl = path.split("\\?")[0]; + if (cleanUrl.endsWith(".m3u8")) { + return M3U8; + } + if (cleanUrl.endsWith(".mp4") || cleanUrl.endsWith(".mkv") || cleanUrl.endsWith(".avi")) { + return CACHE_VIDEO; + } + return OTHER; + } + private void setDataSourceHeader(Map headers) { if (headers != null && !headers.isEmpty()) { String userAgent = headers.get("User-Agent"); diff --git a/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java b/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java index 25b9d248..bd147d92 100644 --- a/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java +++ b/app/src/main/java/com/github/tvbox/osc/server/RemoteServer.java @@ -13,6 +13,7 @@ import com.github.tvbox.osc.event.RefreshEvent; import com.github.tvbox.osc.event.ServerEvent; import com.github.tvbox.osc.util.FileUtils; import com.github.tvbox.osc.util.OkGoHelper; +import com.github.tvbox.osc.util.Proxy; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -135,6 +136,31 @@ public class RemoteServer extends NanoHTTPD { return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "500"); } } + if (params.containsKey("go")) { + Object[] rs = Proxy.proxy(params); + try { + assert rs != null; + int code = (int) rs[0]; + String mime = (String) rs[1]; + InputStream stream = rs[2] != null ? (InputStream) rs[2] : null; + Response response = NanoHTTPD.newChunkedResponse( + NanoHTTPD.Response.Status.lookup(code), + mime, + stream + ); + if(rs.length>=4){ + HashMap mapHeader = (HashMap) rs[3]; + if(!mapHeader.isEmpty()){ + for (String key : mapHeader.keySet()) { + response.addHeader(key, mapHeader.get(key)); + } + } + } + return response; + } catch (Throwable th) { + return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "500"); + } + } } else if (fileName.startsWith("/file/")) { try { String f = fileName.substring(6); diff --git a/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java b/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java index a06a42c3..7472ce12 100644 --- a/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java +++ b/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java @@ -93,6 +93,7 @@ import org.xwalk.core.XWalkWebResourceResponse; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.Iterator; @@ -500,7 +501,13 @@ public class PlayActivity extends BaseActivity { void playUrl(String url, HashMap headers) { LOG.i("playUrl:" + url); if(autoRetryCount>1 && url.contains(".m3u8")){ - //todo + try { + String url_encode; + url_encode=URLEncoder.encode(url,"UTF-8"); + url = ControlManager.get().getAddress(true) + "proxy?go=bom&url="+ url_encode; + }catch (UnsupportedEncodingException e) { + + } } final String finalUrl = url; runOnUiThread(new Runnable() { @@ -826,12 +833,21 @@ public class PlayActivity extends BaseActivity { private int autoRetryCount = 0; + private long lastRetryTime = 0; // 记录上次调用时间(毫秒) + boolean autoRetry() { - if (loadFoundVideoUrls != null && loadFoundVideoUrls.size() > 0) { + long currentTime = System.currentTimeMillis(); + // 如果距离上次重试超过 10 秒(10000 毫秒),重置重试次数 + if (currentTime - lastRetryTime > 10_000) { + autoRetryCount = 0; + } + lastRetryTime = currentTime; // 更新上次调用时间 + if (loadFoundVideoUrls != null && !loadFoundVideoUrls.isEmpty()) { autoRetryFromLoadFoundVideoUrls(); return true; } - if (autoRetryCount < 1) { + + if (autoRetryCount < 2) { autoRetryCount++; play(false); return true; diff --git a/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java b/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java index 2dba074a..d29c1906 100644 --- a/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java +++ b/app/src/main/java/com/github/tvbox/osc/util/OkGoHelper.java @@ -46,6 +46,7 @@ import xyz.doikki.videoplayer.exo.ExoMediaSourceHelper; public class OkGoHelper { public static final long DEFAULT_MILLISECONDS = 8000; //默认的超时时间 + static OkHttpClient ItvClient = null; static void initExoOkHttpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor("OkExoPlayer"); @@ -72,8 +73,9 @@ public class OkGoHelper { // builder.dns(dnsOverHttps); builder.dns(new CustomDns()); + ItvClient=builder.build(); - ExoMediaSourceHelper.getInstance(App.getInstance()).setOkClient(builder.build()); + ExoMediaSourceHelper.getInstance(App.getInstance()).setOkClient(ItvClient); } public static DnsOverHttps dnsOverHttps = null; diff --git a/app/src/main/java/com/github/tvbox/osc/util/Proxy.java b/app/src/main/java/com/github/tvbox/osc/util/Proxy.java new file mode 100644 index 00000000..f94cb6f0 --- /dev/null +++ b/app/src/main/java/com/github/tvbox/osc/util/Proxy.java @@ -0,0 +1,198 @@ +package com.github.tvbox.osc.util; +import com.github.catvod.crawler.SpiderDebug; +import com.github.tvbox.osc.server.ControlManager; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Map; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class Proxy { + + public static Object[] proxy(Map params) { + try { + String what = params.get("go"); + assert what != null; + if (what.equals("live")) { + return itv(params); + } + else if (what.equals("bom")) { + return removeBOMFromM3U8(params); + } + else if (what.equals("ad")) { + //TODO + return null; + } + + } catch (Throwable ignored) { + + } + return null; + } + public static Object[] itv(Map params) throws Exception { + try { + Object[] result = new Object[3]; + String url = params.get("url"); + String type = params.get("type"); + url = URLDecoder.decode(url,"UTF-8"); + + OkHttpClient client = OkGoHelper.ItvClient; + assert type != null; + if (type.equals("m3u8")) { + String redirectUrl = getRedirectedUrl(url); +// LOG.i("echo-url"+redirectUrl); + + Request request = new Request.Builder().url(redirectUrl).build(); + try (Response response = executeRequest(client, request)) { + if (response.isSuccessful()) { + assert response.body() != null; + String respContent = response.body().string(); + String m3u8Content = processM3u8Content(respContent, redirectUrl); + result[0] = 200; + result[1] = "application/vnd.apple.mpegurl"; + result[2] = new ByteArrayInputStream(m3u8Content.getBytes()); + } else { + throw new IOException("M3U8 Request failed with code: " + response.code()); + } + } + } else if (type.equals("ts")) { + Request request = new Request.Builder().url(url).build(); + try (Response response = executeRequest(client, request)) { + if (response.isSuccessful()) { + result[0] = 200; + result[1] = "video/mp2t"; + assert response.body() != null; + result[2] = new ByteArrayInputStream(response.body().bytes()); + } else { + throw new IOException("TS Request failed with code: " + response.code()); + } + } + } else { + throw new IllegalArgumentException("Invalid type: " + type); + } + return result; + } catch (Exception e) { + SpiderDebug.log(e); + return null; + } + } + + public static Object[] removeBOMFromM3U8(Map params) throws Exception { + try { + Object[] result = new Object[3]; + String url = params.get("url"); + url = URLDecoder.decode(url,"UTF-8"); + + OkHttpClient client = OkGoHelper.ItvClient; + String redirectUrl = getRedirectedUrl(url); +// LOG.i("echo-url"+redirectUrl); + + Request request = new Request.Builder().url(redirectUrl).build(); + try (Response response = executeRequest(client, request)) { + if (response.isSuccessful()) { + assert response.body() != null; + String m3u8Content = response.body().string(); + // 检查并去除 UTF-8 BOM 头(BOM 为 \uFEFF) + if (m3u8Content.startsWith("\ufeff")) { + m3u8Content = m3u8Content.substring(1); + } + result[0] = 200; + result[1] = "application/vnd.apple.mpegurl"; + result[2] = new ByteArrayInputStream(m3u8Content.getBytes()); + } else { + throw new IOException("M3U8 Request failed with code: " + response.code()); + } + } + return result; + } catch (Exception e) { + SpiderDebug.log(e); + return null; + } + } + + private static Response executeRequest(OkHttpClient client, Request request) throws IOException { + try { + return client.newCall(request).execute(); + } catch (IOException e) { + System.err.println("网络请求异常:" + e.getMessage()); + throw e; // 重新抛出异常,让外层处理 + } + } + + private static String processM3u8Content(String m3u8Content, String m3u8Url) { + String[] m3u8Lines = m3u8Content.trim().split("\n"); + StringBuilder processedM3u8 = new StringBuilder(); + + for (String line : m3u8Lines) { + if (line.startsWith("#")) { + processedM3u8.append(line).append("\n"); + } else { + processedM3u8.append(joinUrl(m3u8Url, line)).append("\n"); + } + } + return processedM3u8.toString().replace("\\n\\n", "\n"); + } + + private static String joinUrl(String base, String url) { + if (base == null) base = ""; + if (url == null) url = ""; + try { + URI baseUri = new URI(base.trim()); + url = url.trim(); + URI urlUri = new URI(url); + String proxyUrl = ControlManager.get().getAddress(true) + "proxy?go=live&type=ts&url="; + if (url.startsWith("http://") || url.startsWith("https://")) { + return proxyUrl + URLEncoder.encode(urlUri.toString(),"UTF-8"); + } else if (url.startsWith("://")) { + return proxyUrl + URLEncoder.encode(new URI(baseUri.getScheme() + url).toString(),"UTF-8"); + } else if (url.startsWith("//")) { + return proxyUrl + URLEncoder.encode(new URI(baseUri.getScheme() + ":" + url).toString(),"UTF-8"); + } else { + URI resolvedUri = baseUri.resolve(url); + return proxyUrl + URLEncoder.encode(resolvedUri.toString(),"UTF-8"); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static String getRedirectedUrl(String url) throws IOException { + OkHttpClient client = new OkHttpClient.Builder() + .followRedirects(false) // 不自动跟随重定向 + .build(); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (response.isRedirect()) { // 判断是否为重定向 + return response.header("Location"); // 获取重定向后的地址 + } + return url; // 如果没有重定向,返回原 URL + } + } + + public static String getM3U8Content(String url) throws IOException { + Request request = new Request.Builder() + .url(url) + .build(); + + OkHttpClient client = OkGoHelper.ItvClient; + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + return response.body().string(); // 获取 m3u8 文件内容 + } else { + throw new IOException("请求失败,HTTP 状态码: " + response.code()); + } + } + } + +}