Răsfoiți Sursa

增强视频播放器对Safari浏览器的兼容性,新增加密HLS流处理逻辑,优化错误处理机制,提升用户体验。

wuyi 3 luni în urmă
părinte
comite
30809abdea
1 a modificat fișierele cu 160 adăugiri și 40 ștergeri
  1. 160 40
      src/components/VideoProcessor.vue

+ 160 - 40
src/components/VideoProcessor.vue

@@ -231,8 +231,18 @@ const processVideo = async (url: string): Promise<void> => {
 
       if (typeof decryptedData === "string") {
         const playlist = processM3u8Content(decryptedData, processedUrl);
-        const blob = new Blob([playlist], { type: "application/x-mpegURL" });
-        processedVideoUrl.value = URL.createObjectURL(blob);
+        console.log("playlist", playlist);
+
+        // Safari 特殊处理:对于加密的 HLS 流,Safari 原生播放器可能无法处理
+        // 我们需要确保使用 HLS.js 来处理加密流
+        if (isSafari() || isIOSSafari()) {
+          // Safari 下,我们仍然创建 blob URL,但会在 initVideoPlayer 中特殊处理
+          const blob = new Blob([playlist], { type: "application/x-mpegURL" });
+          processedVideoUrl.value = URL.createObjectURL(blob);
+        } else {
+          const blob = new Blob([playlist], { type: "application/x-mpegURL" });
+          processedVideoUrl.value = URL.createObjectURL(blob);
+        }
       } else {
         processedVideoUrl.value = url;
       }
@@ -286,6 +296,18 @@ const processM3u8Content = (m3u8Text: string, baseUrl: string): string => {
   return playlist.join("\n");
 };
 
+// 检测是否为 Safari 浏览器
+const isSafari = (): boolean => {
+  return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
+};
+
+// 检测是否为 iOS Safari
+const isIOSSafari = (): boolean => {
+  return (
+    /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
+  );
+};
+
 // 初始化视频播放器
 const initVideoPlayer = async (): Promise<void> => {
   if (!videoElement.value || !processedVideoUrl.value) {
@@ -295,46 +317,96 @@ const initVideoPlayer = async (): Promise<void> => {
   const video = videoElement.value;
   destroyHls();
 
-  if (video.canPlayType("application/vnd.apple.mpegurl")) {
+  // Safari 特殊处理
+  if (isSafari() || isIOSSafari()) {
+    // Safari 对 blob URL 的 HLS 支持有限,特别是加密流
+    if (processedVideoUrl.value.startsWith("blob:")) {
+      // 对于 Safari,加密的 HLS 流需要使用 HLS.js
+      if (Hls.isSupported()) {
+        initHlsPlayer();
+        return;
+      } else {
+        error.value = "Safari不支持此加密视频格式";
+        return;
+      }
+    }
+
+    // Safari 原生支持 HLS(非加密流)
+    if (video.canPlayType("application/vnd.apple.mpegurl")) {
+      video.src = processedVideoUrl.value;
+
+      // Safari 错误处理
+      video.addEventListener("error", (e) => {
+        error.value = "Safari 视频播放失败,尝试使用 HLS.js";
+
+        // 如果原生播放失败,尝试使用 HLS.js
+        if (Hls.isSupported()) {
+          initHlsPlayer();
+        }
+      });
+
+      return;
+    }
+  }
+
+  // 非 Safari 或 Safari 原生播放失败时使用 HLS.js
+  if (Hls.isSupported()) {
+    initHlsPlayer();
+  } else {
     video.src = processedVideoUrl.value;
-  } else if (Hls.isSupported()) {
-    hlsInstance.value = new Hls({
-      debug: false,
-      enableWorker: true,
-      lowLatencyMode: true,
-    });
-
-    hlsInstance.value.loadSource(processedVideoUrl.value);
-    hlsInstance.value.attachMedia(video);
-
-    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?.destroy();
-              hlsInstance.value = null;
-              video.src = processedVideoUrl.value;
-            } else {
-              hlsInstance.value?.startLoad();
-            }
-            break;
-          case Hls.ErrorTypes.MEDIA_ERROR:
-            hlsInstance.value?.recoverMediaError();
-            break;
-          default:
-            error.value = "视频加载失败";
+  }
+};
+
+// 初始化 HLS.js 播放器
+const initHlsPlayer = (): void => {
+  if (!videoElement.value || !processedVideoUrl.value) {
+    return;
+  }
+
+  const video = videoElement.value;
+
+  hlsInstance.value = new Hls({
+    debug: false,
+    enableWorker: true,
+    lowLatencyMode: true,
+    // Safari 兼容性配置
+    maxBufferLength: 30,
+    maxMaxBufferLength: 60,
+    backBufferLength: 10,
+  });
+
+  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?.destroy();
             hlsInstance.value = null;
             video.src = processedVideoUrl.value;
-            break;
-        }
+          } else {
+            hlsInstance.value?.startLoad();
+          }
+          break;
+        case Hls.ErrorTypes.MEDIA_ERROR:
+          hlsInstance.value?.recoverMediaError();
+          break;
+        default:
+          error.value = "视频加载失败";
+          hlsInstance.value?.destroy();
+          hlsInstance.value = null;
+          video.src = processedVideoUrl.value;
+          break;
       }
-    });
-  } else {
-    video.src = processedVideoUrl.value;
-  }
+    }
+  });
 };
 
 // 销毁 HLS 实例
@@ -353,12 +425,24 @@ const retry = (): void => {
   error.value = "";
   emit("retry");
 
+  // 清理现有资源
+  destroyHls();
+  if (processedVideoUrl.value && processedVideoUrl.value.startsWith("blob:")) {
+    URL.revokeObjectURL(processedVideoUrl.value);
+  }
+
   // 重新处理
   if (props.coverUrl) {
     processCover(props.coverUrl);
   }
   if (props.m3u8Url) {
-    processVideo(props.m3u8Url);
+    // 对于 Safari,在重试时可以考虑使用不同的策略
+    if ((isSafari() || isIOSSafari()) && retryCount.value > 1) {
+      // 强制使用 HLS.js
+      processVideo(props.m3u8Url);
+    } else {
+      processVideo(props.m3u8Url);
+    }
   }
 };
 
@@ -394,9 +478,45 @@ const onVideoSeeking = (): void => {
   emit("seeking");
 };
 
-const onVideoError = (): void => {
-  error.value = "视频播放失败";
+const onVideoError = (event: Event): void => {
+  const video = event.target as HTMLVideoElement;
+  const errorCode = video.error?.code;
+  const errorMessage = video.error?.message;
+
+  let errorText = "视频播放失败";
+
+  if (errorCode) {
+    switch (errorCode) {
+      case 1: // MEDIA_ERR_ABORTED
+        errorText = "视频播放被中止";
+        break;
+      case 2: // MEDIA_ERR_NETWORK
+        errorText = "网络错误,无法加载视频";
+        break;
+      case 3: // MEDIA_ERR_DECODE
+        errorText = "视频解码失败";
+        break;
+      case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
+        errorText = "视频格式不支持";
+        break;
+      default:
+        errorText = `视频播放失败 (错误代码: ${errorCode})`;
+    }
+  }
+
+  error.value = errorText;
   emit("error", error.value);
+
+  // 如果是 Safari 且使用原生播放器失败,尝试使用 HLS.js
+  if (
+    (isSafari() || isIOSSafari()) &&
+    !hlsInstance.value &&
+    Hls.isSupported()
+  ) {
+    setTimeout(() => {
+      initHlsPlayer();
+    }, 1000);
+  }
 };
 
 // 监听 props 变化