Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions crates/engine/src/js_dom_bridge_tests/part09.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6163,15 +6163,48 @@ fn test_media_source_mse_audio_face_t8s3() {
globalThis.__t2 = [
String(MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')),
String(MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')),
String(MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.5"')),
String(MediaSource.isTypeSupported('audio/mp4; codecs="opus"')),
String(MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,vp9"')),
String(MediaSource.isTypeSupported('audio/mp4'))];"#,
)
.unwrap();
assert_eq!(
sandbox.execute("globalThis.__t2.join(',')").unwrap().value,
"true,true,false,false,true",
"有 mseAppendAudio:bilibili 组合探针翻 true + audio/mp4 aac true + opus/复合 vp9 false + 裸 audio/mp4 true"
"true,true,false,false,false,true",
"有 mseAppendAudio:bilibili 组合探针翻 true + audio/mp4 AAC-LC(40.2) true + HE-AAC(40.5)/opus/复合 vp9 false(symphonia 只解 LC——解码面不虚报,t8s-4)+ 裸 audio/mp4 true"
);

// ①b 双面设计(t8s-4):isTypeSupported 解码面 40.5 false(symphonia 只解 LC);
// addSourceBuffer ingest 面受理 40.5(SB 创建成功——nano handler 不因音频 SB
// 硬拒中断,音频解码失败走静音降级,视频轨照常)。
sandbox.execute(
r#"
globalThis.__t3 = [
String(MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.5"'))];
var msD = new MediaSource();
globalThis.__msD = msD;
var urlD = URL.createObjectURL(msD);
msD.addEventListener('sourceopen', function () {
try {
var asb = msD.addSourceBuffer('audio/mp4; codecs="mp4a.40.5"');
globalThis.__asbD = asb ? 'created:' + asb._zwTrackKind : 'null';
} catch (e) { globalThis.__asbD = 'threw:' + e.name; }
});
var vd = document.createElement('video');
vd.src = urlD;"#,
)
.unwrap();
sandbox.execute("void 0").unwrap();
assert_eq!(
sandbox.execute("globalThis.__t3.join(',')").unwrap().value,
"false",
"isTypeSupported(HE-AAC 40.5) = false(解码面诚实——symphonia 只解 LC)"
);
assert_eq!(
sandbox.execute("globalThis.__asbD").unwrap().value,
"created:audio",
"addSourceBuffer(HE-AAC 40.5) ingest 面受理(轨章 audio)"
);

// ② 双 SourceBuffer 轨章 + appendBuffer 路由:video SB 走 mseAppend、audio SB
Expand Down
96 changes: 96 additions & 0 deletions crates/engine/src/js_dom_bridge_tests/part10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2838,7 +2838,103 @@ fn test_response_body_used_redirect_and_blob_formdata_cache_put_support() {
);
}

/// t8s-4(Range CORS-safelisted):fetch spec CORS-safelisted request-header 的
/// Range 面——单一 simple range(bytes=start-end / start- / -suffix)免 preflight
///(媒体 CDN 拉取形态);多段 range 与普通自定义头仍触发 OPTIONS。mock 宿主
/// 回调记录每次 __zw_fetch 派发的 method 序列。
#[test]
fn test_fetch_range_safelisted_preflight_t8s4() {
use zero_script_sandbox::{Sandbox, V8Sandbox};
let mut sandbox = V8Sandbox::with_config(zero_script_sandbox::SandboxConfig {
persistent_context: true,
..Default::default()
})
.unwrap();
sandbox.execute(generate_js_dom_shim()).unwrap();
let mutations: Arc<Mutex<Vec<DomMutation>>> = Arc::new(Mutex::new(vec![]));
let dom_html: Arc<Mutex<String>> = Arc::new(Mutex::new("<html><body></body></html>".to_string()));
let page_url: Arc<Mutex<String>> = Arc::new(Mutex::new("https://www.test.com/page".to_string()));
let canvas_registry: std::sync::Arc<std::sync::Mutex<crate::js_dom_bridge::CanvasRegistry>> =
std::sync::Arc::new(std::sync::Mutex::new(crate::js_dom_bridge::CanvasRegistry::new()));
register_dom_callbacks(&mut sandbox, &mutations, &dom_html, &page_url, &canvas_registry, None);

let methods_seen: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
{
let methods_seen = Arc::clone(&methods_seen);
sandbox
.register_callback(
"__zw_fetch",
Box::new(move |args| {
let method = args.get(1).cloned().unwrap_or_default();
methods_seen
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(method.clone());
// 同步 wire 契约(宿主 __zw_fetch 同步返响应 wire):预检 200 +
// ACAM/ACAH 覆盖;主请求 200 + ACAO。
if method == "OPTIONS" {
// headersWire = name\u{001e}value 全 \\u{001e} 连接(_headersToWire 同构)。
"__zwfr:200\u{001f}OK\u{001f}Access-Control-Allow-Origin\u{001e}*\u{001e}Access-Control-Allow-Methods\u{001e}GET, OPTIONS\u{001e}Access-Control-Allow-Headers\u{001e}*\u{001f}ok".to_string()
} else {
"__zwfr:200\u{001f}OK\u{001f}Access-Control-Allow-Origin\u{001e}*\u{001f}ok".to_string()
}
}),
);
}
let seen_all = |methods_seen: &Arc<Mutex<Vec<String>>>| -> Vec<String> {
std::mem::take(&mut *methods_seen.lock().unwrap_or_else(|e| e.into_inner()))
};

// ① 单一 simple range:免 preflight——仅主 GET 一次派发。
sandbox
.execute("globalThis.__fl = []; var __of2 = globalThis.fetch; globalThis.__ofType = typeof __of2; globalThis.fetch = function (a, b) { globalThis.__fl.push('call:' + a); return __of2.call(this, a, b).then(function (r) { globalThis.__fl.push('res:' + r.status + ':' + r.type); return r; }, function (e) { globalThis.__fl.push('rej:' + e); throw e; }); };")
.unwrap();
sandbox
.execute("globalThis.__st1='pending'; fetch('https://cdn.test/seg.m4s', { headers: { Range: 'bytes=0-1547' } }).then(function (r) { globalThis.__st1 = 'ok:' + r.status + ':' + r.type; }, function (e) { globalThis.__st1 = 'err:' + e; }); void 0;")
.unwrap();
let seen1 = seen_all(&methods_seen);
assert_eq!(
seen1,
vec!["GET".to_string()],
"simple range → 零 OPTIONS(safelisted 免预检)(st1={})",
sandbox.execute("globalThis.__st1").unwrap().value
);

// ② 多段 range:非 safelisted → OPTIONS 先行。
sandbox
.execute("globalThis.__st2='pending'; fetch('https://cdn.test/seg.m4s', { headers: { Range: 'bytes=0-1,2-3' } }).then(function (r) { globalThis.__st2 = 'ok:' + r.status + ':' + r.type; }, function (e) { globalThis.__st2 = 'err:' + e + ':' + (e && e.stack || '').slice(0, 300); }); void 0;")
.unwrap();
let seen2 = seen_all(&methods_seen);
assert_eq!(
seen2,
vec!["OPTIONS".to_string(), "GET".to_string()],
"多段 range → preflight OPTIONS 先行"
);

// ②b suffix range(bytes=-500):spec 明言不 safelist → preflight OPTIONS
// 先行(t8s-4 返修 D1 收窄的回归守卫;新 origin 避开 ② 的 preflight cache)。
sandbox
.execute("fetch('https://suffix.test/seg.m4s', { headers: { Range: 'bytes=-500' } }).then(function () {}, function () {}); void 0;")
.unwrap();
let seen2b = seen_all(&methods_seen);
assert_eq!(
seen2b,
vec!["OPTIONS".to_string(), "GET".to_string()],
"suffix range → preflight(spec 拒收 suffix safelist 面)"
);

// ③ 普通自定义头:OPTIONS 先行(既有面回归守卫;新 origin——②的 preflight
// cache 对同 origin 合法命中会跳过 OPTIONS)。
sandbox
.execute("fetch('https://other.test/api', { headers: { 'X-Custom': 'v' } }).then(function () {}, function () {}); void 0;")
.unwrap();
let seen3 = seen_all(&methods_seen);
assert_eq!(
seen3,
vec!["OPTIONS".to_string(), "GET".to_string()],
"普通自定义头 → preflight(既有面守卫)"
);
}
fn test_request_null_body_text_does_not_mark_body_used() {
// https://fetch.spec.whatwg.org/#body-mixin
// WPT Cache.add reads a body-less GET Request; null body consumption must
Expand Down
22 changes: 19 additions & 3 deletions crates/engine/src/js_dom_shim/part01.js
Original file line number Diff line number Diff line change
Expand Up @@ -2198,8 +2198,10 @@
// ACRH 覆盖检查;content-type 走 essence safelist 判定,亦不入)。
// net-api M4-S26:CORS 变量头名单(fetch spec CORS-safelisted request-header 值面)——
// accept/accept-language/content-language/content-type **值条件安全名单**(值破格即
// 入列——长度 <128、无 forbidden 字节、accept 无 `"`、CT essence 三形),range 恒入列
// (强制 preflight 面),accept-language/content-language 2024 spec 移出安全名单恒入列;
// 入列——长度 <128、无 forbidden 字节、accept 无 `"`、CT essence 三形),range 为
// 值条件 safelist(单一 simple range `bytes=\d+-\d*` 免预检;suffix/多段/空白/畸形
// 入列——t8s-4 修正:旧「恒入列」使媒体 CDN Range 拉取误预检),accept-language/
// content-language 2024 spec 移出安全名单恒入列;
// origin/content-length/access-control-*/referer/last-event-id/user-agent 恒跳过。
// preflight 触发与 ACRH/ACAH 覆盖检查共用本名单(单一事实源)。
function _zwPreNamesOf(headersWire) {
Expand Down Expand Up @@ -2231,7 +2233,21 @@
}
continue;
}
push(ln); // accept-language/content-language/range/其余自定义头
if (ln === 'range') {
// fetch spec CORS-safelisted request-header(2022+ Range 面):值经
// 「parse a single range header value(allowWhitespace=false)」成功且
// start 非空才 safelist——`bytes=\d+-\d*`(无空白、字面量大小写敏感、
// start 必填);suffix(`bytes=-500`,spec 注释明言不 safelist——浏览器
// 历史上不发)、多段、空白、畸形仍触发 preflight。Chrome/FF 同面——
// 媒体 CDN 拉取(MSE/DASH init+media segment 的 Range 请求)从不预检;
// 误预检使按 Origin 条件回 ACAO 的 CDN 对 OPTIONS 拒答 → 全候选
// Failed to fetch(t8s-4 bilibili 起播断链面)。
// https://fetch.spec.whatwg.org/#cors-safelisted-request-header
if (/^bytes=\d+-\d*$/.test(val)) continue;
push(ln);
continue;
}
push(ln); // accept-language/content-language/其余自定义头
}
return names;
}
Expand Down
46 changes: 39 additions & 7 deletions crates/engine/src/js_dom_shim/part06.js
Original file line number Diff line number Diff line change
Expand Up @@ -8908,8 +8908,9 @@
if (msObj._zwUrl && typeof globalThis.__zwVideoBridge === 'object'
&& typeof globalThis.__zwVideoBridge.mseAppend === 'function') {
// t8s-3 轨路由:video SB → 视频流;audio SB → 独立音频流;audio SB
// 遇门面缺席(不可达面——addSourceBuffer 同源守卫已挡)走失败面,
// 绝不把音频字节写入视频流(能力诚实)。
// 遇 mseAppendAudio 门面缺席(decode-h264 关闭构建——addSourceBuffer
// 桥门放行的唯一形态)走失败面,绝不把音频字节写入视频流(能力诚实);
// 该形态下 SB 创建成功而 append 恒败(error 事件面),非「不可达」。
if (self._zwTrackKind === 'audio') {
ok = typeof globalThis.__zwVideoBridge.mseAppendAudio === 'function'
&& globalThis.__zwVideoBridge.mseAppendAudio(msObj._zwUrl, b64) === true;
Expand Down Expand Up @@ -8960,8 +8961,9 @@
};
globalThis.MediaSource.prototype = Object.create(_zwMseEt.prototype);
// spec isTypeSupported 真值表不虚报:video/mp4 + H.264(avc1/avc3)恒真值;
// audio/mp4 + AAC 与组合串音频腿以「音频 append 流在位」(mseAppendAudio
// 门面 = 音频解码链可用,t8s-3)为支持面真值——门面缺席恒 false(能力诚实)。
// audio/mp4 + AAC-LC(mp4a.40.2 / 'aac' 别名)与组合串音频腿以「音频 append
// 流在位」(mseAppendAudio 门面 = 音频解码链可用,t8s-3)为支持面真值——门面
// 缺席恒 false(能力诚实);HE-AAC/SBR(40.5)等非 LC 面恒 false(解码面不虚报,t8s-4)。
// 其他容器恒 false;容器无 codecs 查询按 Chromium 口径 true(载荷不可解时
// appendBuffer error 事件如实报)。decode-h264 feature 关闭(桥缺失)→ 恒 false。
// https://www.w3.org/TR/media-source/#dom-mediasource-istypesupported
Expand All @@ -8985,8 +8987,10 @@
for (var i = 0; i < parts.length; i++) {
var c = parts[i].trim();
if (!c) continue;
// AAC 家族:mp4a.40.x(MPEG-4 Audio,AAC-LC 主形态)与 'aac' 别名。
var isAac = audioOk && /^(mp4a\.40|aac)\b/.test(c);
// 音频解码面真值:symphonia aac 仅 AAC-LC(mp4a.40.2;aac/mod.rs 显式拒
// Main/SSR/LTP/SBR——40.5 HE-AAC append 必 "aac too complex" 失败)→
// 只宣称 40.2 与 'aac' 别名(t8s-4 诚实面收窄,40.5 等恒 false)。
var isAac = audioOk && /^(mp4a\.40\.2|aac)\b/.test(c);
if (isAudio) {
if (!isAac) return false;
} else if (!/^avc[13]\./.test(c) && !isAac) {
Expand All @@ -9002,7 +9006,35 @@
// spec addSourceBuffer:type 不支持 → NotSupportedError;readyState 非 open →
// InvalidStateError。
// https://www.w3.org/TR/media-source/#dom-mediasource-addsourcebuffer
if (!globalThis.MediaSource.isTypeSupported(t)) {
// 受理面(ingest face)与 isTypeSupported 的解码面**解耦**(UA 纬度——Chrome 同
// 款:SB 创建按容器/家族受理,解码失败走 error/静音降级):音频家族 mp4a.40.x/aac
// 全受理(HE-AAC 40.5 创建成功但解码不支持 → 音频静音降级,视频轨照常——t8s-4
// bilibili 未登录默认音轨 30216=40.5,硬拒使 nano sourceopen handler 中断、
// 双轨 append 全不发生 → 起播死循环)。视频面 avc1/avc3 不变。
// 桥存在门(feature-detect 单点,t8n 契约):无 MSE 通路 → NotSupportedError。
if (typeof globalThis.__zwVideoBridge !== 'object'
|| typeof globalThis.__zwVideoBridge.mseCreate !== 'function') {
_zwMseThrow('NotSupportedError', 'unsupported source buffer type: ' + t);
}
var _tl = t.trim().toLowerCase();
var _m = /codecs\s*=\s*"([^"]*)"/.exec(_tl) || /codecs\s*=\s*'([^']*)'/.exec(_tl)
|| /codecs\s*=\s*([^;]+)/.exec(_tl);
var _accepted = false;
if (_tl.indexOf('video/mp4') === 0 || _tl.indexOf('audio/mp4') === 0) {
if (!_m) _accepted = true;
else {
var _parts = _m[1].split(',');
_accepted = true;
for (var _pi = 0; _pi < _parts.length; _pi++) {
var _pc = _parts[_pi].trim();
if (!_pc) continue;
if (/^avc[13]\./.test(_pc) || /^(mp4a\.40|aac)\b/.test(_pc)) continue;
_accepted = false;
break;
}
}
}
if (!_accepted) {
_zwMseThrow('NotSupportedError', 'unsupported source buffer type: ' + t);
}
if (this.readyState !== 'open') {
Expand Down
Loading