|
@@ -0,0 +1,306 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="test-video-container">
|
|
|
|
|
+ <div class="videos-wrapper">
|
|
|
|
|
+ <!-- 上面的播放器:允许206处理(Range请求) -->
|
|
|
|
|
+ <div class="video-section">
|
|
|
|
|
+ <div class="video-label">上面播放器:允许206处理(Range请求)</div>
|
|
|
|
|
+ <div class="aspect-video video-container" ref="videoContainer1">
|
|
|
|
|
+ <VideoJSPlayer
|
|
|
|
|
+ ref="player1Ref"
|
|
|
|
|
+ :m3u8-url="testVideoUrl"
|
|
|
|
|
+ :auto-play="false"
|
|
|
|
|
+ :hide-error="false"
|
|
|
|
|
+ :enable-retry="true"
|
|
|
|
|
+ video-class="w-full h-full object-contain"
|
|
|
|
|
+ @video-loaded="onPlayer1Loaded"
|
|
|
|
|
+ @play="onPlayer1Play"
|
|
|
|
|
+ @canplay="onPlayer1CanPlay"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 下面的播放器:不允许206处理(移除Range请求头) -->
|
|
|
|
|
+ <div class="video-section">
|
|
|
|
|
+ <div class="video-label">下面播放器:不允许206处理(移除Range请求头)</div>
|
|
|
|
|
+ <div class="aspect-video video-container" ref="videoContainer2">
|
|
|
|
|
+ <VideoJSPlayer
|
|
|
|
|
+ ref="player2Ref"
|
|
|
|
|
+ :m3u8-url="testVideoUrl"
|
|
|
|
|
+ :auto-play="false"
|
|
|
|
|
+ :hide-error="false"
|
|
|
|
|
+ :enable-retry="true"
|
|
|
|
|
+ video-class="w-full h-full object-contain"
|
|
|
|
|
+ @video-loaded="onPlayer2Loaded"
|
|
|
|
|
+ @play="onPlayer2Play"
|
|
|
|
|
+ @canplay="onPlayer2CanPlay"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
|
|
|
|
+import VideoJSPlayer from "@/components/VideoJSPlayer.vue";
|
|
|
|
|
+
|
|
|
|
|
+// 测试视频 URL
|
|
|
|
|
+const testVideoUrl = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
|
|
|
|
|
+
|
|
|
|
|
+const videoContainer1 = ref<HTMLElement>();
|
|
|
|
|
+const videoContainer2 = ref<HTMLElement>();
|
|
|
|
|
+const player1Ref = ref<InstanceType<typeof VideoJSPlayer>>();
|
|
|
|
|
+const player2Ref = ref<InstanceType<typeof VideoJSPlayer>>();
|
|
|
|
|
+
|
|
|
|
|
+// 保存原始的fetch和XHR方法(来自api.ts的全局拦截器)
|
|
|
|
|
+let originalFetchFromApi: any = null;
|
|
|
|
|
+let originalXHRSetRequestHeaderFromApi: any = null;
|
|
|
|
|
+
|
|
|
|
|
+// 判断URL是否与测试视频相关
|
|
|
|
|
+const isTestVideoUrl = (url: string): boolean => {
|
|
|
|
|
+ if (!url) return false;
|
|
|
|
|
+ return url.includes("test-streams.mux.dev");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 设置测试页面的拦截器(在全局拦截器之后执行,但可以覆盖其行为)
|
|
|
|
|
+const setupTestPageInterceptor = (): void => {
|
|
|
|
|
+ // 获取api.ts中已经保存的原始方法
|
|
|
|
|
+ originalFetchFromApi = (window as any).__originalFetch;
|
|
|
|
|
+ originalXHRSetRequestHeaderFromApi = (window as any).__originalXHRSetRequestHeader;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有,尝试从XMLHttpRequest获取
|
|
|
|
|
+ if (!originalFetchFromApi) {
|
|
|
|
|
+ // 找到原始的fetch(可能是api.ts中的)
|
|
|
|
|
+ const proto = Object.getPrototypeOf(window.fetch);
|
|
|
|
|
+ // 这比较复杂,我们直接使用window.fetch,它已经被api.ts包装过
|
|
|
|
|
+ originalFetchFromApi = window.fetch;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重新包装setRequestHeader,在api.ts的拦截器之后执行
|
|
|
|
|
+ const currentXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
|
|
|
+
|
|
|
|
|
+ XMLHttpRequest.prototype.setRequestHeader = function (
|
|
|
|
|
+ header: string,
|
|
|
|
|
+ value: string
|
|
|
|
|
+ ) {
|
|
|
|
|
+ const url = (this as any)._url || "";
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是测试视频URL
|
|
|
|
|
+ if (isTestVideoUrl(url)) {
|
|
|
|
|
+ const allowRange = (window as any).__allowRangeForTestVideo === true;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果允许Range,且是Range头,则调用原始方法(不过滤)
|
|
|
|
|
+ if (allowRange && header.toLowerCase() === "range") {
|
|
|
|
|
+ // 跳过api.ts的拦截,直接调用原始方法
|
|
|
|
|
+ if (originalXHRSetRequestHeaderFromApi) {
|
|
|
|
|
+ return originalXHRSetRequestHeaderFromApi.apply(this, [header, value]);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果没有原始方法,调用当前方法(可能已经被api.ts包装)
|
|
|
|
|
+ return currentXHRSetRequestHeader.apply(this, [header, value]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果不允许Range且是Range头,则不设置(已经被api.ts移除了)
|
|
|
|
|
+ if (!allowRange && header.toLowerCase() === "range") {
|
|
|
|
|
+ return; // 不设置Range头
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 其他情况,调用当前方法(经过api.ts处理)
|
|
|
|
|
+ return currentXHRSetRequestHeader.apply(this, [header, value]);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 重新包装fetch
|
|
|
|
|
+ const currentFetch = window.fetch;
|
|
|
|
|
+
|
|
|
|
|
+ window.fetch = function (
|
|
|
|
|
+ input: RequestInfo | URL,
|
|
|
|
|
+ init?: RequestInit
|
|
|
|
|
+ ): Promise<Response> {
|
|
|
|
|
+ const url =
|
|
|
|
|
+ typeof input === "string"
|
|
|
|
|
+ ? input
|
|
|
|
|
+ : input instanceof URL
|
|
|
|
|
+ ? input.toString()
|
|
|
|
|
+ : (input as Request).url;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是测试视频URL
|
|
|
|
|
+ if (isTestVideoUrl(url)) {
|
|
|
|
|
+ const allowRange = (window as any).__allowRangeForTestVideo === true;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果允许Range,直接使用原始fetch(不过滤Range头)
|
|
|
|
|
+ if (allowRange) {
|
|
|
|
|
+ if (originalFetchFromApi && originalFetchFromApi !== currentFetch) {
|
|
|
|
|
+ return originalFetchFromApi.apply(this, [input, init]);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果允许Range,但找不到原始方法,说明Range头应该保留
|
|
|
|
|
+ return currentFetch.apply(this, [input, init]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果不允许Range,移除Range头(调用当前fetch,它已经由api.ts处理过)
|
|
|
|
|
+ const modifiedInit = { ...init };
|
|
|
|
|
+ if (modifiedInit.headers) {
|
|
|
|
|
+ const headers = new Headers(modifiedInit.headers);
|
|
|
|
|
+ headers.delete("Range");
|
|
|
|
|
+ headers.delete("range");
|
|
|
|
|
+ modifiedInit.headers = headers;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ modifiedInit.headers = new Headers();
|
|
|
|
|
+ }
|
|
|
|
|
+ return currentFetch.apply(this, [input, modifiedInit]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 其他URL,使用当前fetch(经过api.ts处理)
|
|
|
|
|
+ return currentFetch.apply(this, [input, init]);
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 播放器事件处理
|
|
|
|
|
+const onPlayer1Loaded = (): void => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = true;
|
|
|
|
|
+ console.log("第一个播放器:视频加载完成,启用Range请求");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onPlayer1Play = (): void => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = true;
|
|
|
|
|
+ console.log("第一个播放器:开始播放,启用Range请求");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onPlayer1CanPlay = (): void => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onPlayer2Loaded = (): void => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+ console.log("第二个播放器:视频加载完成,禁用Range请求(移除Range头)");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onPlayer2Play = (): void => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+ console.log("第二个播放器:开始播放,禁用Range请求(移除Range头)");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onPlayer2CanPlay = (): void => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 监控video元素以更准确地设置Range标记
|
|
|
|
|
+const setupVideoMonitoring = async (): Promise<void> => {
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+
|
|
|
|
|
+ // 定期检查video元素的状态
|
|
|
|
|
+ const checkVideos = () => {
|
|
|
|
|
+ const video1 = videoContainer1.value?.querySelector("video");
|
|
|
|
|
+ const video2 = videoContainer2.value?.querySelector("video");
|
|
|
|
|
+
|
|
|
|
|
+ // 如果第一个播放器正在加载或播放,启用Range
|
|
|
|
|
+ if (video1 && (video1.readyState > 0 || video1.networkState > 0)) {
|
|
|
|
|
+ // 检查是否是活跃状态(正在播放或用户交互)
|
|
|
|
|
+ if (!video2 || video2.paused || video2.readyState === 0) {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果第二个播放器正在加载或播放,禁用Range
|
|
|
|
|
+ if (video2 && (video2.readyState > 0 || video2.networkState > 0)) {
|
|
|
|
|
+ // 检查是否是活跃状态
|
|
|
|
|
+ if (!video1 || video1.paused || video1.readyState === 0) {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 使用定时器定期检查
|
|
|
|
|
+ const intervalId = setInterval(checkVideos, 200);
|
|
|
|
|
+
|
|
|
|
|
+ // 监听video元素的网络活动
|
|
|
|
|
+ const setupVideoListeners = () => {
|
|
|
|
|
+ const video1 = videoContainer1.value?.querySelector("video");
|
|
|
|
|
+ const video2 = videoContainer2.value?.querySelector("video");
|
|
|
|
|
+
|
|
|
|
|
+ if (video1) {
|
|
|
|
|
+ video1.addEventListener("loadstart", () => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = true;
|
|
|
|
|
+ });
|
|
|
|
|
+ video1.addEventListener("progress", () => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = true;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (video2) {
|
|
|
|
|
+ video2.addEventListener("loadstart", () => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ video2.addEventListener("progress", () => {
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟设置监听器,等待video元素创建
|
|
|
|
|
+ setTimeout(setupVideoListeners, 1000);
|
|
|
|
|
+
|
|
|
|
|
+ // 清理定时器(30秒后)
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ clearInterval(intervalId);
|
|
|
|
|
+ }, 30000);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ // 默认禁用Range(让全局拦截器生效)
|
|
|
|
|
+ (window as any).__allowRangeForTestVideo = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置测试页面特定的拦截器
|
|
|
|
|
+ setupTestPageInterceptor();
|
|
|
|
|
+
|
|
|
|
|
+ // 等待DOM更新后监控video元素
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ setupVideoMonitoring();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ // 清理全局标记
|
|
|
|
|
+ delete (window as any).__allowRangeForTestVideo;
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.test-video-container {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background-color: #000;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.videos-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: 1200px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 40px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.video-section {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.video-label {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.video-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ background-color: #000;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|