|
|
@@ -199,6 +199,8 @@ const isFullscreen = ref(false);
|
|
|
const showControls = ref(true);
|
|
|
const showVolumeSlider = ref(true); // 桌面端显示音量滑块,移动端通过CSS隐藏
|
|
|
let controlsTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
+let errorDebounceTimer: ReturnType<typeof setTimeout> | null = null; // 错误防抖定时器
|
|
|
+const isVideoLoading = ref(false); // 视频是否正在加载
|
|
|
|
|
|
// 计算属性
|
|
|
const isVideoMode = computed(() => !!props.m3u8Url);
|
|
|
@@ -336,6 +338,13 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
if (isQuarkBrowser() && !isIOSQuarkBrowser()) {
|
|
|
// Android 夸克使用 Apple 格式
|
|
|
mimeType = "application/vnd.apple.mpegurl";
|
|
|
+ console.log("夸克浏览器使用 Apple HLS 格式");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isQQBrowser()) {
|
|
|
+ // QQ 浏览器(X5内核)必须使用 Apple 格式
|
|
|
+ mimeType = "application/x-mpegURL";
|
|
|
+ console.log("QQ浏览器使用 Apple HLS 格式(X5内核要求)");
|
|
|
}
|
|
|
|
|
|
// iOS 夸克浏览器:使用 Data URL(iOS 不支持 Blob URL + HLS.js)
|
|
|
@@ -343,6 +352,7 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
try {
|
|
|
const base64 = btoa(unescape(encodeURIComponent(playlist)));
|
|
|
processedVideoUrl.value = `data:${mimeType};base64,${base64}`;
|
|
|
+ console.log("iOS 夸克浏览器使用 Data URL");
|
|
|
} catch (e) {
|
|
|
console.error("Data URL 生成失败:", e);
|
|
|
error.value = "抱歉,iOS 夸克浏览器暂不支持播放此视频,建议使用 Safari 打开";
|
|
|
@@ -350,7 +360,34 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
loading.value = false;
|
|
|
return;
|
|
|
}
|
|
|
- } else {
|
|
|
+ }
|
|
|
+ // else if (isQQBrowser()) {
|
|
|
+ // // QQ浏览器:使用正确的MIME类型
|
|
|
+ // console.log("检测到QQ浏览器,使用HLS.js兼容模式");
|
|
|
+
|
|
|
+ // try {
|
|
|
+ // // 使用正确的HLS MIME类型
|
|
|
+ // const blob = new Blob([playlist], { type: "application/vnd.apple.mpegurl" });
|
|
|
+ // processedVideoUrl.value = URL.createObjectURL(blob);
|
|
|
+ // console.log("QQ浏览器使用Blob URL,MIME类型:", "application/vnd.apple.mpegurl");
|
|
|
+ // } catch (e) {
|
|
|
+ // console.error("QQ浏览器Blob URL生成失败,尝试Data URL:", e);
|
|
|
+
|
|
|
+ // // Blob失败则尝试Data URL
|
|
|
+ // try {
|
|
|
+ // const base64 = btoa(unescape(encodeURIComponent(playlist)));
|
|
|
+ // processedVideoUrl.value = `data:application/vnd.apple.mpegurl;base64,${base64}`;
|
|
|
+ // console.log("QQ浏览器降级使用Data URL");
|
|
|
+ // } catch (e2) {
|
|
|
+ // console.error("QQ浏览器Data URL生成失败:", e2);
|
|
|
+ // error.value = "QQ浏览器视频处理失败,建议使用Chrome浏览器";
|
|
|
+ // emit("error", error.value);
|
|
|
+ // loading.value = false;
|
|
|
+ // return;
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ else {
|
|
|
// 其他浏览器使用 Blob URL
|
|
|
const blob = new Blob([playlist], { type: mimeType });
|
|
|
processedVideoUrl.value = URL.createObjectURL(blob);
|
|
|
@@ -431,6 +468,18 @@ const isIOSQuarkBrowser = (): boolean => {
|
|
|
return isQuarkBrowser() && (/iPad|iPhone|iPod/.test(navigator.userAgent));
|
|
|
};
|
|
|
|
|
|
+// 检测是否为 QQ 浏览器
|
|
|
+const isQQBrowser = (): boolean => {
|
|
|
+ const ua = navigator.userAgent.toLowerCase();
|
|
|
+ return ua.includes('mqqbrowser') || ua.includes('qq/');
|
|
|
+};
|
|
|
+
|
|
|
+// 检测是否为 UC 浏览器
|
|
|
+const isUCBrowser = (): boolean => {
|
|
|
+ const ua = navigator.userAgent.toLowerCase();
|
|
|
+ return ua.includes('ucbrowser') || ua.includes('ubrowser') || ua.includes('ucweb');
|
|
|
+};
|
|
|
+
|
|
|
// 初始化视频播放器
|
|
|
const initVideoPlayer = async (): Promise<void> => {
|
|
|
if (!videoElement.value || !processedVideoUrl.value) {
|
|
|
@@ -440,13 +489,29 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
const video = videoElement.value;
|
|
|
destroyHls();
|
|
|
|
|
|
+ // 设置加载状态
|
|
|
+ isVideoLoading.value = true;
|
|
|
+ error.value = "";
|
|
|
+
|
|
|
+ // 清除错误防抖定时器
|
|
|
+ if (errorDebounceTimer) {
|
|
|
+ clearTimeout(errorDebounceTimer);
|
|
|
+ errorDebounceTimer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除之前的src,避免缓存问题
|
|
|
+ if (video.src) {
|
|
|
+ video.removeAttribute('src');
|
|
|
+ video.load();
|
|
|
+ }
|
|
|
+
|
|
|
// iOS 夸克浏览器特殊处理(最优先)
|
|
|
// iOS 不支持 HLS.js,且夸克的自定义播放器对 Blob/Data URL 支持有限
|
|
|
// 直接使用原生播放器,让 iOS 处理
|
|
|
if (isIOSQuarkBrowser()) {
|
|
|
console.log("检测到 iOS 夸克浏览器,使用原生播放器");
|
|
|
- video.crossOrigin = "anonymous";
|
|
|
video.src = processedVideoUrl.value;
|
|
|
+ video.load(); // 强制重新加载
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -460,8 +525,34 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
initHlsPlayer();
|
|
|
} else {
|
|
|
// 降级到原生播放
|
|
|
- video.crossOrigin = "anonymous";
|
|
|
+ console.log("HLS.js 不支持,使用原生播放器");
|
|
|
video.src = processedVideoUrl.value;
|
|
|
+ video.load(); // 强制重新加载
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // QQ 浏览器不支持提示
|
|
|
+ if (isQQBrowser()) {
|
|
|
+ console.log("检测到 QQ 浏览器,不支持加密视频播放");
|
|
|
+ error.value = "QQ浏览器暂不支持播放此类视频\n请使用Chrome、UC或其他浏览器访问";
|
|
|
+ // 不提供重试选项,直接返回
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // UC 浏览器特殊处理
|
|
|
+ if (isUCBrowser()) {
|
|
|
+ console.log("检测到 UC 浏览器");
|
|
|
+
|
|
|
+ // UC浏览器优先使用HLS.js
|
|
|
+ if (Hls.isSupported()) {
|
|
|
+ console.log("UC浏览器使用HLS.js播放器");
|
|
|
+ initHlsPlayer();
|
|
|
+ } else {
|
|
|
+ // 如果HLS.js不支持,使用原生播放器
|
|
|
+ console.log("UC浏览器使用原生播放器");
|
|
|
+ video.src = processedVideoUrl.value;
|
|
|
+ video.load();
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
@@ -483,26 +574,33 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
// Safari 原生支持 HLS(非加密流)
|
|
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
|
video.src = processedVideoUrl.value;
|
|
|
+ video.load();
|
|
|
|
|
|
- // Safari 错误处理
|
|
|
- video.addEventListener("error", (e) => {
|
|
|
- error.value = "Safari 视频播放失败,尝试使用 HLS.js";
|
|
|
-
|
|
|
- // 如果原生播放失败,尝试使用 HLS.js
|
|
|
+ // Safari 错误处理 - 使用一次性事件监听器
|
|
|
+ const handleError = () => {
|
|
|
+ console.log("Safari原生播放失败,尝试HLS.js");
|
|
|
+ error.value = "";
|
|
|
if (Hls.isSupported()) {
|
|
|
initHlsPlayer();
|
|
|
+ } else {
|
|
|
+ error.value = "Safari 视频播放失败";
|
|
|
}
|
|
|
- });
|
|
|
+ };
|
|
|
+ video.addEventListener("error", handleError, { once: true });
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 非 Safari 或 Safari 原生播放失败时使用 HLS.js
|
|
|
+ // 其他浏览器使用 HLS.js(性能更好)
|
|
|
if (Hls.isSupported()) {
|
|
|
+ console.log("使用 HLS.js 播放");
|
|
|
initHlsPlayer();
|
|
|
} else {
|
|
|
+ // 降级到原生播放器
|
|
|
+ console.log("HLS.js 不支持,使用原生播放器");
|
|
|
video.src = processedVideoUrl.value;
|
|
|
+ video.load();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -521,40 +619,72 @@ const initHlsPlayer = (): void => {
|
|
|
maxBufferLength: 30,
|
|
|
maxMaxBufferLength: 60,
|
|
|
backBufferLength: 10,
|
|
|
+ // 添加更宽松的配置以提高兼容性
|
|
|
+ manifestLoadingTimeOut: 10000,
|
|
|
+ manifestLoadingMaxRetry: 3,
|
|
|
+ levelLoadingTimeOut: 10000,
|
|
|
+ levelLoadingMaxRetry: 3,
|
|
|
};
|
|
|
|
|
|
- hlsInstance.value = new Hls(hlsConfig);
|
|
|
+ try {
|
|
|
+ hlsInstance.value = new Hls(hlsConfig);
|
|
|
|
|
|
- hlsInstance.value.loadSource(processedVideoUrl.value);
|
|
|
- hlsInstance.value.attachMedia(video);
|
|
|
+ hlsInstance.value.loadSource(processedVideoUrl.value);
|
|
|
+ hlsInstance.value.attachMedia(video);
|
|
|
|
|
|
- hlsInstance.value.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
- error.value = "";
|
|
|
- });
|
|
|
-
|
|
|
- hlsInstance.value.on(Hls.Events.ERROR, (event, data) => {
|
|
|
- if (data.fatal) {
|
|
|
- switch (data.type) {
|
|
|
- case Hls.ErrorTypes.NETWORK_ERROR:
|
|
|
- if (data.details === "manifestParsingError") {
|
|
|
- error.value = "视频清单解析失败";
|
|
|
+ hlsInstance.value.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
+ console.log("HLS manifest 解析成功");
|
|
|
+ error.value = "";
|
|
|
+ });
|
|
|
+
|
|
|
+ hlsInstance.value.on(Hls.Events.ERROR, (event, data) => {
|
|
|
+ console.error("HLS 错误:", data);
|
|
|
+
|
|
|
+ if (data.fatal) {
|
|
|
+ switch (data.type) {
|
|
|
+ case Hls.ErrorTypes.NETWORK_ERROR:
|
|
|
+ console.log("网络错误,尝试恢复");
|
|
|
+ if (data.details === "manifestParsingError") {
|
|
|
+ error.value = "视频清单解析失败";
|
|
|
+ hlsInstance.value?.destroy();
|
|
|
+ hlsInstance.value = null;
|
|
|
+ } else {
|
|
|
+ // 尝试重新加载
|
|
|
+ hlsInstance.value?.startLoad();
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case Hls.ErrorTypes.MEDIA_ERROR:
|
|
|
+ console.log("媒体错误,尝试恢复");
|
|
|
+ hlsInstance.value?.recoverMediaError();
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ console.log("其他致命错误,销毁 HLS");
|
|
|
+ error.value = "视频加载失败";
|
|
|
hlsInstance.value?.destroy();
|
|
|
hlsInstance.value = null;
|
|
|
- } else {
|
|
|
- hlsInstance.value?.startLoad();
|
|
|
- }
|
|
|
- break;
|
|
|
- case Hls.ErrorTypes.MEDIA_ERROR:
|
|
|
- hlsInstance.value?.recoverMediaError();
|
|
|
- break;
|
|
|
- default:
|
|
|
- error.value = "视频加载失败";
|
|
|
- hlsInstance.value?.destroy();
|
|
|
- hlsInstance.value = null;
|
|
|
- break;
|
|
|
+
|
|
|
+ // 尝试降级到原生播放器
|
|
|
+ setTimeout(() => {
|
|
|
+ if (videoElement.value && processedVideoUrl.value) {
|
|
|
+ console.log("降级到原生播放器");
|
|
|
+ error.value = "";
|
|
|
+ videoElement.value.src = processedVideoUrl.value;
|
|
|
+ videoElement.value.load();
|
|
|
+ }
|
|
|
+ }, 500);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ console.error("初始化 HLS.js 失败:", err);
|
|
|
+ error.value = "播放器初始化失败";
|
|
|
+ // 降级到原生播放器
|
|
|
+ if (videoElement.value && processedVideoUrl.value) {
|
|
|
+ videoElement.value.src = processedVideoUrl.value;
|
|
|
+ videoElement.value.load();
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
// 销毁 HLS 实例
|
|
|
@@ -707,11 +837,30 @@ const handleCoverError = (event: Event): void => {
|
|
|
|
|
|
const handleCoverLoad = (): void => {};
|
|
|
|
|
|
-const onVideoLoadStart = (): void => {};
|
|
|
+const onVideoLoadStart = (): void => {
|
|
|
+ console.log("视频开始加载");
|
|
|
+ isVideoLoading.value = true;
|
|
|
+ // 清除之前的错误
|
|
|
+ error.value = "";
|
|
|
+};
|
|
|
|
|
|
-const onVideoLoadedData = (): void => {};
|
|
|
+const onVideoLoadedData = (): void => {
|
|
|
+ console.log("视频数据已加载");
|
|
|
+};
|
|
|
|
|
|
const onVideoCanPlay = (): void => {
|
|
|
+ console.log("视频可以播放");
|
|
|
+
|
|
|
+ // 清除加载状态和错误
|
|
|
+ isVideoLoading.value = false;
|
|
|
+ error.value = "";
|
|
|
+
|
|
|
+ // 清除错误防抖定时器
|
|
|
+ if (errorDebounceTimer) {
|
|
|
+ clearTimeout(errorDebounceTimer);
|
|
|
+ errorDebounceTimer = null;
|
|
|
+ }
|
|
|
+
|
|
|
if (props.autoPlay && videoElement.value) {
|
|
|
videoElement.value.play().catch(() => {});
|
|
|
}
|
|
|
@@ -770,6 +919,42 @@ const onVideoError = (event: Event): void => {
|
|
|
const errorCode = video.error?.code;
|
|
|
const errorMessage = video.error?.message;
|
|
|
|
|
|
+ console.log("视频错误事件:", errorCode, errorMessage, "加载中:", isVideoLoading.value);
|
|
|
+
|
|
|
+ // 清除之前的错误防抖定时器
|
|
|
+ if (errorDebounceTimer) {
|
|
|
+ clearTimeout(errorDebounceTimer);
|
|
|
+ errorDebounceTimer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果视频正在加载中,延迟显示错误(给视频更多时间初始化)
|
|
|
+ if (isVideoLoading.value) {
|
|
|
+ console.log("视频正在初始化,延迟错误显示");
|
|
|
+
|
|
|
+ // 对于夸克浏览器,给更长的初始化时间(从2s提升至3s)
|
|
|
+ const delayTime = isQuarkBrowser() ? 3000 : 1000;
|
|
|
+
|
|
|
+ errorDebounceTimer = setTimeout(() => {
|
|
|
+ // 再次检查视频状态
|
|
|
+ if (video.readyState >= 2) {
|
|
|
+ // 视频已经准备好,忽略错误
|
|
|
+ console.log("视频已准备好,忽略错误");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示错误
|
|
|
+ showVideoError(errorCode, errorMessage);
|
|
|
+ }, delayTime);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果不是加载中,立即显示错误
|
|
|
+ showVideoError(errorCode, errorMessage);
|
|
|
+};
|
|
|
+
|
|
|
+// 显示视频错误的辅助函数
|
|
|
+const showVideoError = (errorCode?: number, errorMessage?: string): void => {
|
|
|
let errorText = "视频播放失败";
|
|
|
|
|
|
if (errorCode) {
|
|
|
@@ -795,7 +980,7 @@ const onVideoError = (event: Event): void => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- console.error("视频播放错误:", errorCode, errorMessage);
|
|
|
+ console.error("显示视频错误:", errorCode, errorMessage);
|
|
|
error.value = errorText;
|
|
|
emit("error", error.value);
|
|
|
|
|
|
@@ -873,6 +1058,11 @@ onUnmounted(() => {
|
|
|
clearTimeout(controlsTimer);
|
|
|
}
|
|
|
|
|
|
+ // 清理错误防抖定时器
|
|
|
+ if (errorDebounceTimer) {
|
|
|
+ clearTimeout(errorDebounceTimer);
|
|
|
+ }
|
|
|
+
|
|
|
// 清理 Blob URL
|
|
|
if (processedCoverUrl.value?.startsWith("blob:")) {
|
|
|
URL.revokeObjectURL(processedCoverUrl.value);
|