|
|
@@ -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 变化
|