diff --git a/app/ui/plugins/global.tsx b/app/ui/plugins/global.tsx index 208a65b..5bb3fdc 100644 --- a/app/ui/plugins/global.tsx +++ b/app/ui/plugins/global.tsx @@ -280,8 +280,8 @@ const Global: React.FC = () => {
浏览器直连 CDN:biliup 向平台另取一条直链(新 token,不影响录制那条),浏览器自己去 CDN 拉,媒体流量不经过 biliup,适合 biliup 部署在异地服务器、浏览器远程访问的情况,省服务器出口带宽。 - 只有 CDN 放行跨域的平台能直连(B 站 / 抖音 / 斗鱼 / 虎牙,FLV 与 HLS 都行);Twitch 等未放行跨域的 - 平台在直连模式下自动回落中转并在播放器角标标出原因。 + 只有 CDN 放行跨域的平台能直连(B 站 / 抖音 / 斗鱼 / 虎牙,FLV 与 HLS 都行);Twitch 等按 Origin 白名单放行 + 的平台在直连模式下自动回落中转并在播放器角标标出原因。
} diff --git a/crates/biliup-cli/src/server/api/live_preview.rs b/crates/biliup-cli/src/server/api/live_preview.rs index 37eef51..f9cc477 100644 --- a/crates/biliup-cli/src/server/api/live_preview.rs +++ b/crates/biliup-cli/src/server/api/live_preview.rs @@ -95,12 +95,14 @@ fn subscribe_error_response(id: i64, error: SubscribeError) -> Response { /// 直连**不复用录制那条直链**而是向平台另取一条(新 token),所以「同一直链两条连接」的限制 /// (斗鱼一 token 一连接、部分虎牙节点对第二条连接只给 GOP 缓存即断)不再是问题——实测新 token /// 的斗鱼 / 虎牙直链各读 25 s 稳定,录制那条不受影响。容器不影响判定:FLV 走 mpegts.js, -/// HLS(TS / fMP4 分片,`.m3u8`)走 hls.js,B 站 `.m4s` 分片的 CDN 同样 `ACAO: *`。 +/// HLS(TS / fMP4 分片,`.m3u8`)走 hls.js。HLS 实测:B 站 fMP4 的 m3u8 / init / `.m4s` 同域同 `ACAO: *`; +/// 抖音 TS 的主清单(`pull-hls-*.douyinliving.com`)与媒体清单 + 分片(`*.100ycdn.com`)不同域,都 `*`; +/// 虎牙 TS 的 m3u8 与分片同域,回显 Origin(清单轮询有边缘节点间歇 403,靠重取 / 回落)。 /// -/// | 平台 | ACAO | 判定 | +/// | 平台 | 跨域 | 判定 | /// | --- | --- | --- | -/// | B 站 / 抖音 / 虎牙 / 斗鱼 | `*` | 能 | -/// | Twitch | 200 响应无 ACAO | 不能 | +/// | B 站 / 抖音 / 虎牙 / 斗鱼 | FLV 与 HLS 的清单、分片都放行 | 能 | +/// | Twitch | usher 主清单无 ACAO;媒体清单 / 分片虽带 `ACAO: *`,但清单服务器按 Origin 白名单放行(只有 twitch.tv 与 localhost),其它站点 403 | 不能 | /// /// 其它平台没实测,按不能处理,回落中转。 pub fn direct_capability(platform: &str) -> DirectCapability { @@ -113,7 +115,9 @@ pub fn direct_capability(platform: &str) -> DirectCapability { capable: true, reason: None, }, - "twitch" => no("Twitch CDN 未放行跨域(响应无 Access-Control-Allow-Origin)"), + "twitch" => no( + "Twitch 的清单服务器按 Origin 白名单放行(只有 twitch.tv 与 localhost),其它站点跨域请求 403", + ), _ => no("该平台的 CDN 跨域放行未验证"), } } @@ -585,12 +589,7 @@ mod tests { } let twitch = direct_capability("twitch"); assert!(!twitch.capable); - assert!( - twitch - .reason - .unwrap() - .contains("Access-Control-Allow-Origin") - ); + assert!(twitch.reason.unwrap().contains("Origin")); assert!(!direct_capability("youtube").capable); // 容器由直链后缀决定播放器:flv → mpegts.js,m3u8 → hls.js assert_eq!( @@ -639,10 +638,9 @@ mod tests { use danmaku_client::message::EnterMessage; use danmaku_client::{ChatMessage, GiftMessage}; let (tx, rx) = tokio::sync::broadcast::channel(8); - let permit = danmaku_connection_limit() - .clone() - .try_acquire_owned() - .unwrap(); + // 各测试用自己的信号量,别和并行跑的其它测试抢全局那把 + let limit = Arc::new(Semaphore::new(1)); + let permit = limit.clone().try_acquire_owned().unwrap(); let response = danmaku_response(vec![(7, rx)], permit); assert_eq!( response.headers().get(header::CONTENT_TYPE).unwrap(), @@ -688,10 +686,8 @@ mod tests { "{text}" ); assert!(!text.contains("路人"), "{text}"); - assert_eq!( - danmaku_connection_limit().available_permits(), - MAX_DANMAKU_CONNECTIONS - ); + // 响应体读完即 drop,许可随之释放 + assert_eq!(limit.available_permits(), 1); } /// 复用:两路广播合成一条 SSE,每条事件带各自的 id;两路发送端都 drop 后流才结束。 @@ -700,10 +696,9 @@ mod tests { use danmaku_client::ChatMessage; let (tx_a, rx_a) = tokio::sync::broadcast::channel(8); let (tx_b, rx_b) = tokio::sync::broadcast::channel(8); - let permit = danmaku_connection_limit() - .clone() - .try_acquire_owned() - .unwrap(); + // 各测试用自己的信号量,别和并行跑的其它测试抢全局那把 + let limit = Arc::new(Semaphore::new(1)); + let permit = limit.clone().try_acquire_owned().unwrap(); let response = danmaku_response(vec![(1, rx_a), (3, rx_b)], permit); tx_a.send(DanmakuEvent::Chat(ChatMessage::new("来自1".into()))) .unwrap(); @@ -727,10 +722,8 @@ mod tests { "{text}" ); assert!(text.contains("b还在"), "{text}"); - assert_eq!( - danmaku_connection_limit().available_permits(), - MAX_DANMAKU_CONNECTIONS - ); + // 响应体读完即 drop,许可随之释放 + assert_eq!(limit.available_permits(), 1); } #[test]