useVideoProbe.js
4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { ref } from "vue";
/**
* @description 视频资源探测(同源才探测)。用于在播放器初始化前尽可能拿到 content-type、文件大小等信息。
* @param {import("vue").Ref<string>} video_url_value_ref 视频地址的 ref(会读取 .value)
* @returns {{
* probeInfo: import("vue").Ref<{ok: boolean|null, status: number|null, content_type: string, content_length: number|null, accept_ranges: string}>,
* probeLoading: import("vue").Ref<boolean>,
* probeVideo: () => Promise<void>
* }}
*/
export const useVideoProbe = (video_url_value_ref) => {
const probe_info = ref({
ok: null,
status: null,
content_type: "",
content_length: null,
accept_ranges: "",
});
const probe_loading = ref(false);
const can_probe_video_url = (url) => {
const url_text = (url || "").trim();
if (!url_text) return false;
if (typeof window === "undefined" || typeof window.location === "undefined") return false;
if (typeof fetch === "undefined") return false;
// blob/data 资源无法通过 fetch 拿到 headers,直接跳过
if (/^(blob:|data:)/i.test(url_text)) return false;
try {
const u = new URL(url_text, window.location.href);
if (!u.protocol || !u.origin) return false;
// 只允许同源探测,避免跨域触发 CORS 失败导致控制台噪声/影响体验
if (u.origin !== window.location.origin) return false;
return true;
} catch (e) {
return false;
}
};
const probe_video = async () => {
const url = video_url_value_ref.value;
if (!can_probe_video_url(url)) return;
if (probe_loading.value) return;
probe_loading.value = true;
// 单次探测设置超时,避免弱网下长时间卡住
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
const timeout_id = setTimeout(() => controller?.abort?.(), 8000);
let timeout_id_2 = null;
try {
try {
// 先 HEAD,理论上成本最低,能直接拿到 content-length / content-type
const head_res = await fetch(url, {
method: "HEAD",
mode: "cors",
cache: "no-store",
signal: controller?.signal,
});
const content_length = head_res.headers.get("content-length");
probe_info.value = {
ok: head_res.ok,
status: head_res.status,
content_type: head_res.headers.get("content-type") || "",
content_length: content_length ? Number(content_length) || null : null,
accept_ranges: head_res.headers.get("accept-ranges") || "",
};
if (head_res.ok && probe_info.value.content_length) return;
} catch (e) {
void e;
}
// 部分服务器不支持 HEAD 或不返回 content-length,这里用 Range GET 兜底
const controller2 = typeof AbortController !== "undefined" ? new AbortController() : null;
timeout_id_2 = setTimeout(() => controller2?.abort?.(), 8000);
try {
const range_res = await fetch(url, {
method: "GET",
mode: "cors",
cache: "no-store",
// 只取前 2 字节,尽量减少流量;通过 content-range 推断总大小
headers: { Range: "bytes=0-1" },
signal: controller2?.signal,
});
const content_range = range_res.headers.get("content-range") || "";
const match = content_range.match(/\/(\d+)\s*$/);
const total = match ? Number(match[1]) || null : null;
const content_length = range_res.headers.get("content-length");
probe_info.value = {
ok: range_res.ok,
status: range_res.status,
content_type: range_res.headers.get("content-type") || "",
content_length: total || (content_length ? Number(content_length) || null : null),
accept_ranges: range_res.headers.get("accept-ranges") || "",
};
} catch (e) {
void e;
}
} finally {
if (timeout_id_2) clearTimeout(timeout_id_2);
clearTimeout(timeout_id);
probe_loading.value = false;
}
};
return {
probeInfo: probe_info,
probeLoading: probe_loading,
probeVideo: probe_video,
};
};