useVideoPlaybackOverlays.js 8.38 KB
import { ref } from "vue";

/**
 * @description 播放器叠层逻辑:弱网提示、HLS 下载速率(基于 VHS 带宽)与调试信息。
 * @param {{
 *   props: any,
 *   player: import("vue").Ref<any>,
 *   is_m3u8: import("vue").Ref<boolean> | import("vue").ComputedRef<boolean>,
 *   use_native_player: import("vue").Ref<boolean> | import("vue").ComputedRef<boolean>,
 *   show_error_overlay: import("vue").Ref<boolean>,
 *   has_started_playback: import("vue").Ref<boolean>
 * }} params
 * @returns {{
 *   showNetworkSpeedOverlay: import("vue").Ref<boolean>,
 *   networkSpeedText: import("vue").Ref<string>,
 *   hlsDownloadSpeedText: import("vue").Ref<string>,
 *   hlsSpeedDebugText: import("vue").Ref<string>,
 *   setHlsDebug: (event_name: string, extra?: string) => void,
 *   showNetworkSpeed: () => void,
 *   hideNetworkSpeed: () => void,
 *   startHlsDownloadSpeed: () => void,
 *   stopHlsDownloadSpeed: (reason?: string) => void,
 *   disposeOverlays: () => void
 * }}
 */
export const useVideoPlaybackOverlays = ({
    props,
    player,
    is_m3u8,
    use_native_player,
    show_error_overlay,
    has_started_playback,
}) => {
    const show_network_speed_overlay = ref(false);
    const network_speed_text = ref("");
    let network_speed_timer = null;
    let last_weixin_network_type_at = 0;

    const hls_download_speed_text = ref("");
    let hls_speed_timer = null;
    const hls_speed_debug_text = ref("");

    const set_hls_debug = (event_name, extra) => {
        if (!props || props.debug !== true) return;

        // tech(true) 在部分版本可能抛错,这里必须兜底,避免影响正常播放流程
        const p = player.value;
        const has_player = !!p && !p.isDisposed?.();
        let tech = null;
        try {
            tech = has_player && typeof p.tech === "function" ? p.tech(true) : null;
        } catch (e) {
            tech = null;
        }
        const stable_tech = tech || (has_player ? p.tech_ : null);
        const tech_name = has_player
            ? (p.techName_ || (stable_tech && stable_tech.name_) || (stable_tech && stable_tech.constructor && stable_tech.constructor.name) || "unknown")
            : "none";
        const mode = use_native_player.value ? "native" : "videojs";
        const vhs = stable_tech && stable_tech.vhs ? stable_tech.vhs : null;
        const hls = stable_tech && stable_tech.hls ? stable_tech.hls : null;
        const bw_kbps = vhs && typeof vhs.bandwidth === "number" ? Math.round(vhs.bandwidth / 1000) : 0;
        const hls_bw_kbps = hls && typeof hls.bandwidth === "number" ? Math.round(hls.bandwidth / 1000) : 0;

        const extra_text = extra ? ` ${extra}` : "";
        hls_speed_debug_text.value = `${event_name}${extra_text}\nmode:${mode} m3u8:${is_m3u8.value ? "1" : "0"} native:${use_native_player.value ? "1" : "0"}\ntech:${tech_name} vhs:${vhs ? "1" : "0"} bw:${bw_kbps}kbps hls_bw:${hls_bw_kbps}kbps`;
    };

    const update_network_speed = () => {
        if (typeof navigator === "undefined") {
            network_speed_text.value = "未知";
            return false;
        }

        // 优先使用 Network Information API(部分浏览器不支持)
        const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
        const downlink = connection && typeof connection.downlink === "number" ? connection.downlink : null;
        if (downlink && downlink > 0) {
            network_speed_text.value = `${downlink.toFixed(1)} Mbps`;
            return true;
        }

        const effective_type = connection && typeof connection.effectiveType === "string" ? connection.effectiveType : "";
        network_speed_text.value = effective_type ? `${effective_type}` : "未知";
        return effective_type ? true : false;
    };

    const update_weixin_network_type = () => {
        if (typeof window === "undefined") return;
        if (!window.WeixinJSBridge || typeof window.WeixinJSBridge.invoke !== "function") return;

        const now = Date.now();
        // 微信接口调用频率过高会卡顿/被限流,这里做节流
        if (now - last_weixin_network_type_at < 3000) return;
        last_weixin_network_type_at = now;

        window.WeixinJSBridge.invoke("getNetworkType", {}, (res) => {
            const type = (res && (res.networkType || res.network_type)) ? String(res.networkType || res.network_type) : "";
            if (type) network_speed_text.value = type;
        });
    };

    const show_network_speed = () => {
        // 没有进入过播放阶段时不显示,避免一加载就出现“弱网提示”造成误导
        if (!has_started_playback.value) return;
        if (show_error_overlay.value) return;
        if (show_network_speed_overlay.value) return;

        show_network_speed_overlay.value = true;
        const ok = update_network_speed();
        if (!ok) update_weixin_network_type();

        if (network_speed_timer) clearInterval(network_speed_timer);
        network_speed_timer = setInterval(() => {
            const ok2 = update_network_speed();
            if (!ok2) update_weixin_network_type();
        }, 800);
    };

    const hide_network_speed = () => {
        show_network_speed_overlay.value = false;
        if (network_speed_timer) {
            clearInterval(network_speed_timer);
            network_speed_timer = null;
        }
    };

    const format_speed = (bytes_per_second) => {
        const size = Number(bytes_per_second) || 0;
        if (!size) return "";

        const kb = 1024;
        const mb = kb * 1024;
        if (size >= mb) return `${(size / mb).toFixed(1)}MB/s`;
        if (size >= kb) return `${Math.round(size / kb)}kB/s`;
        return `${Math.round(size)}B/s`;
    };

    const update_hls_download_speed = () => {
        if (!player.value || player.value.isDisposed()) {
            hls_download_speed_text.value = "";
            set_hls_debug("update", "player:empty");
            return;
        }
        if (!is_m3u8.value || use_native_player.value) {
            // 非 m3u8 或原生播放器走系统内核,这里拿不到 VHS 带宽,直接隐藏
            hls_download_speed_text.value = "";
            set_hls_debug("update", "skip");
            return;
        }

        let tech = null;
        try {
            tech = typeof player.value.tech === "function" ? player.value.tech(true) : null;
        } catch (e) {
            tech = null;
        }
        const stable_tech = tech || player.value.tech_ || null;
        const vhs = stable_tech && stable_tech.vhs ? stable_tech.vhs : null;
        const hls = stable_tech && stable_tech.hls ? stable_tech.hls : null;
        const bandwidth_bits_per_second = (vhs && typeof vhs.bandwidth === "number" ? vhs.bandwidth : null)
            || (hls && typeof hls.bandwidth === "number" ? hls.bandwidth : null);
        if (!bandwidth_bits_per_second || bandwidth_bits_per_second <= 0) {
            hls_download_speed_text.value = "";
            set_hls_debug("update", "bw:0");
            return;
        }

        // VHS bandwidth 单位是 bits/s,这里换算成 bytes/s 再格式化展示
        hls_download_speed_text.value = format_speed(bandwidth_bits_per_second / 8);
        set_hls_debug("update", `speed:${hls_download_speed_text.value}`);
    };

    const start_hls_download_speed = () => {
        if (hls_speed_timer) return;
        if (!is_m3u8.value || use_native_player.value) return;

        set_hls_debug("start");
        update_hls_download_speed();
        hls_speed_timer = setInterval(() => {
            update_hls_download_speed();
        }, 1000);
    };

    const stop_hls_download_speed = (reason) => {
        if (hls_speed_timer) {
            clearInterval(hls_speed_timer);
            hls_speed_timer = null;
        }
        hls_download_speed_text.value = "";
        set_hls_debug("stop", reason || "");
    };

    const dispose_overlays = () => {
        hide_network_speed();
        stop_hls_download_speed("dispose");
    };

    return {
        showNetworkSpeedOverlay: show_network_speed_overlay,
        networkSpeedText: network_speed_text,
        hlsDownloadSpeedText: hls_download_speed_text,
        hlsSpeedDebugText: hls_speed_debug_text,
        setHlsDebug: set_hls_debug,
        showNetworkSpeed: show_network_speed,
        hideNetworkSpeed: hide_network_speed,
        startHlsDownloadSpeed: start_hls_download_speed,
        stopHlsDownloadSpeed: stop_hls_download_speed,
        disposeOverlays: dispose_overlays,
    };
};