useVideoProbe.js 4.7 KB
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,
    };
};