|
@@ -1,24 +1,23 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="video-js-container">
|
|
<div class="video-js-container">
|
|
|
- <!-- 封面图片 -->
|
|
|
|
|
|
|
+ <!-- 封面图片 - 只在没有视频源时显示 -->
|
|
|
<img
|
|
<img
|
|
|
|
|
+ v-if="!hasVideoSource && processedCoverUrl"
|
|
|
:src="processedCoverUrl"
|
|
:src="processedCoverUrl"
|
|
|
:alt="alt"
|
|
:alt="alt"
|
|
|
:class="coverClass"
|
|
:class="coverClass"
|
|
|
@error="handleCoverError"
|
|
@error="handleCoverError"
|
|
|
@load="handleCoverLoad"
|
|
@load="handleCoverLoad"
|
|
|
style="
|
|
style="
|
|
|
- display: block !important;
|
|
|
|
|
width: 100% !important;
|
|
width: 100% !important;
|
|
|
height: 100% !important;
|
|
height: 100% !important;
|
|
|
opacity: 1 !important;
|
|
opacity: 1 !important;
|
|
|
- z-index: 999 !important;
|
|
|
|
|
"
|
|
"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <!-- Video.js 播放器容器 -->
|
|
|
|
|
|
|
+ <!-- Video.js 播放器容器 - 只在有视频源时显示 -->
|
|
|
<div
|
|
<div
|
|
|
- v-if="isVideoMode || forceShowPlayer"
|
|
|
|
|
|
|
+ v-if="hasVideoSource"
|
|
|
ref="videoContainer"
|
|
ref="videoContainer"
|
|
|
class="video-js-wrapper"
|
|
class="video-js-wrapper"
|
|
|
:class="[videoClass, { 'loading-video': loading }]"
|
|
:class="[videoClass, { 'loading-video': loading }]"
|
|
@@ -94,16 +93,14 @@ const videoContainer = ref<HTMLDivElement>();
|
|
|
const player = ref<any>(null);
|
|
const player = ref<any>(null);
|
|
|
|
|
|
|
|
// 计算属性
|
|
// 计算属性
|
|
|
-const isVideoMode = computed(
|
|
|
|
|
|
|
+const hasVideoSource = computed(
|
|
|
() => !!props.m3u8Url && props.m3u8Url.trim() !== ""
|
|
() => !!props.m3u8Url && props.m3u8Url.trim() !== ""
|
|
|
);
|
|
);
|
|
|
|
|
+
|
|
|
const showRetryButton = computed(
|
|
const showRetryButton = computed(
|
|
|
() => props.enableRetry && retryCount.value < maxRetries
|
|
() => props.enableRetry && retryCount.value < maxRetries
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
-// 强制显示播放器的状态
|
|
|
|
|
-const forceShowPlayer = ref(false);
|
|
|
|
|
-
|
|
|
|
|
// 核心解密函数(仅用于封面)
|
|
// 核心解密函数(仅用于封面)
|
|
|
const loader = async (url: string): Promise<Blob | string> => {
|
|
const loader = async (url: string): Promise<Blob | string> => {
|
|
|
const response = await fetch(url, { mode: "cors" });
|
|
const response = await fetch(url, { mode: "cors" });
|
|
@@ -168,7 +165,6 @@ const processCover = async (url: string): Promise<void> => {
|
|
|
try {
|
|
try {
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
error.value = "";
|
|
error.value = "";
|
|
|
- console.log("处理封面URL:", url);
|
|
|
|
|
|
|
|
|
|
// 检查是否需要解密(仅对封面进行解密)
|
|
// 检查是否需要解密(仅对封面进行解密)
|
|
|
if (url.includes("cover")) {
|
|
if (url.includes("cover")) {
|
|
@@ -179,10 +175,8 @@ const processCover = async (url: string): Promise<void> => {
|
|
|
// 直接使用解密后的 Blob 创建 URL
|
|
// 直接使用解密后的 Blob 创建 URL
|
|
|
const blobUrl = URL.createObjectURL(decryptedData);
|
|
const blobUrl = URL.createObjectURL(decryptedData);
|
|
|
processedCoverUrl.value = blobUrl;
|
|
processedCoverUrl.value = blobUrl;
|
|
|
- console.log("封面解密成功,创建Blob URL:", blobUrl);
|
|
|
|
|
} else {
|
|
} else {
|
|
|
processedCoverUrl.value = decryptedData;
|
|
processedCoverUrl.value = decryptedData;
|
|
|
- console.log("封面解密成功,使用解密后的URL");
|
|
|
|
|
}
|
|
}
|
|
|
} catch (decryptErr) {
|
|
} catch (decryptErr) {
|
|
|
console.error("封面解密失败,使用原始URL:", decryptErr);
|
|
console.error("封面解密失败,使用原始URL:", decryptErr);
|
|
@@ -196,21 +190,6 @@ const processCover = async (url: string): Promise<void> => {
|
|
|
// 预加载图片以确保它可以正确显示
|
|
// 预加载图片以确保它可以正确显示
|
|
|
const img = new Image();
|
|
const img = new Image();
|
|
|
img.onload = () => {
|
|
img.onload = () => {
|
|
|
- console.log("封面图片加载成功,URL:", processedCoverUrl.value);
|
|
|
|
|
-
|
|
|
|
|
- // 如果不是视频模式,确保封面图片立即显示
|
|
|
|
|
- if (!isVideoMode.value) {
|
|
|
|
|
- nextTick(() => {
|
|
|
|
|
- const imgElement = document.querySelector(
|
|
|
|
|
- ".video-js-container img"
|
|
|
|
|
- ) as HTMLImageElement;
|
|
|
|
|
- if (imgElement) {
|
|
|
|
|
- imgElement.style.display = "block";
|
|
|
|
|
- imgElement.style.opacity = "1";
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
emit("coverLoaded", processedCoverUrl.value);
|
|
emit("coverLoaded", processedCoverUrl.value);
|
|
|
};
|
|
};
|
|
|
img.onerror = () => {
|
|
img.onerror = () => {
|
|
@@ -239,12 +218,12 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
processedVideoUrl.value = url;
|
|
processedVideoUrl.value = url;
|
|
|
await nextTick();
|
|
await nextTick();
|
|
|
await initVideoJSPlayer();
|
|
await initVideoJSPlayer();
|
|
|
- emit("videoLoaded", processedVideoUrl.value);
|
|
|
|
|
|
|
+ emit("videoLoaded", url);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 初始化 Video.js 播放器
|
|
// 初始化 Video.js 播放器
|
|
|
const initVideoJSPlayer = async (): Promise<void> => {
|
|
const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
if (!videoContainer.value || !processedVideoUrl.value) {
|
|
if (!videoContainer.value || !processedVideoUrl.value) {
|
|
|
console.log("初始化失败:缺少容器或视频URL");
|
|
console.log("初始化失败:缺少容器或视频URL");
|
|
|
reject(new Error("缺少容器或视频URL"));
|
|
reject(new Error("缺少容器或视频URL"));
|
|
@@ -256,17 +235,12 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
videoContainer.value.innerHTML = "";
|
|
videoContainer.value.innerHTML = "";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- console.log("开始初始化 Video.js 播放器");
|
|
|
|
|
- console.log("视频URL:", processedVideoUrl.value);
|
|
|
|
|
-
|
|
|
|
|
- // 先测试 URL 是否可访问
|
|
|
|
|
- fetch(processedVideoUrl.value, { method: "HEAD" })
|
|
|
|
|
- .then((response) => {
|
|
|
|
|
- console.log("URL 可访问性测试:", response.status, response.ok);
|
|
|
|
|
- })
|
|
|
|
|
- .catch((error) => {
|
|
|
|
|
- console.error("URL 访问测试失败:", error);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 处理封面 URL
|
|
|
|
|
+ let posterUrl = "";
|
|
|
|
|
+ if (props.coverUrl) {
|
|
|
|
|
+ await processCover(props.coverUrl);
|
|
|
|
|
+ posterUrl = processedCoverUrl.value;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 创建 video 元素
|
|
// 创建 video 元素
|
|
|
const videoElement = document.createElement("video");
|
|
const videoElement = document.createElement("video");
|
|
@@ -274,13 +248,14 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
videoElement.controls = true;
|
|
videoElement.controls = true;
|
|
|
videoElement.preload = "auto";
|
|
videoElement.preload = "auto";
|
|
|
|
|
|
|
|
- // 确保设置封面
|
|
|
|
|
- if (processedCoverUrl.value) {
|
|
|
|
|
- videoElement.poster = processedCoverUrl.value;
|
|
|
|
|
- console.log("设置视频封面:", processedCoverUrl.value);
|
|
|
|
|
- } else if (props.coverUrl) {
|
|
|
|
|
- videoElement.poster = props.coverUrl;
|
|
|
|
|
- console.log("使用原始封面URL:", props.coverUrl);
|
|
|
|
|
|
|
+ // iOS 特殊配置,防止自动全屏
|
|
|
|
|
+ videoElement.setAttribute("playsinline", "true");
|
|
|
|
|
+ videoElement.setAttribute("webkit-playsinline", "true");
|
|
|
|
|
+ videoElement.setAttribute("x-webkit-airplay", "allow");
|
|
|
|
|
+
|
|
|
|
|
+ // 设置封面
|
|
|
|
|
+ if (posterUrl) {
|
|
|
|
|
+ videoElement.poster = posterUrl;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 添加到容器
|
|
// 添加到容器
|
|
@@ -293,16 +268,20 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
preload: "auto",
|
|
preload: "auto",
|
|
|
techOrder: ["html5"],
|
|
techOrder: ["html5"],
|
|
|
bigPlayButton: true, // 启用大型播放按钮
|
|
bigPlayButton: true, // 启用大型播放按钮
|
|
|
|
|
+ // iOS 特殊配置
|
|
|
|
|
+ playsinline: true,
|
|
|
|
|
+ webkitPlaysinline: true,
|
|
|
userActions: {
|
|
userActions: {
|
|
|
- // 禁用默认的点击暂停行为,但允许点击进度条
|
|
|
|
|
- click: false,
|
|
|
|
|
- doubleClick: false,
|
|
|
|
|
|
|
+ // 保持默认的点击行为,让控制栏可以正常显示/隐藏
|
|
|
|
|
+ click: true,
|
|
|
|
|
+ doubleClick: true,
|
|
|
hotkeys: true,
|
|
hotkeys: true,
|
|
|
},
|
|
},
|
|
|
controlBar: {
|
|
controlBar: {
|
|
|
// 自定义控制栏
|
|
// 自定义控制栏
|
|
|
children: [
|
|
children: [
|
|
|
"playToggle", // 播放/暂停按钮
|
|
"playToggle", // 播放/暂停按钮
|
|
|
|
|
+ "forward10Button", // 快进 10 秒按钮
|
|
|
"replayButton", // 回退按钮
|
|
"replayButton", // 回退按钮
|
|
|
"forwardButton", // 快进按钮
|
|
"forwardButton", // 快进按钮
|
|
|
"currentTimeDisplay", // 当前时间
|
|
"currentTimeDisplay", // 当前时间
|
|
@@ -318,13 +297,15 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
// 使用默认的 HTML5 配置,Video.js 内置 HLS 支持
|
|
// 使用默认的 HTML5 配置,Video.js 内置 HLS 支持
|
|
|
- sources: [
|
|
|
|
|
- {
|
|
|
|
|
- src: processedVideoUrl.value,
|
|
|
|
|
- type: "application/x-mpegURL",
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- poster: processedCoverUrl.value || props.coverUrl,
|
|
|
|
|
|
|
+ sources: processedVideoUrl.value
|
|
|
|
|
+ ? [
|
|
|
|
|
+ {
|
|
|
|
|
+ src: processedVideoUrl.value,
|
|
|
|
|
+ type: "application/x-mpegURL",
|
|
|
|
|
+ },
|
|
|
|
|
+ ]
|
|
|
|
|
+ : [],
|
|
|
|
|
+ poster: posterUrl, // 使用处理后的封面URL
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -375,6 +356,33 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
videojs.registerComponent("ForwardButton", ForwardButtonComponent);
|
|
videojs.registerComponent("ForwardButton", ForwardButtonComponent);
|
|
|
|
|
+
|
|
|
|
|
+ // 注册快进 10 秒按钮
|
|
|
|
|
+ const Forward10Button = videojs.getComponent("Button");
|
|
|
|
|
+ class Forward10ButtonComponent extends Forward10Button {
|
|
|
|
|
+ constructor(player: any, options: any) {
|
|
|
|
|
+ super(player, options);
|
|
|
|
|
+ (this as any).controlText("快进10秒");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ handleClick() {
|
|
|
|
|
+ const time = (this as any).player().currentTime();
|
|
|
|
|
+ const duration = (this as any).player().duration();
|
|
|
|
|
+ if (typeof time === "number" && typeof duration === "number") {
|
|
|
|
|
+ (this as any)
|
|
|
|
|
+ .player()
|
|
|
|
|
+ .currentTime(Math.min(duration, time + 10));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ buildCSSClass() {
|
|
|
|
|
+ return `vjs-forward-10-button ${super.buildCSSClass()}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ videojs.registerComponent(
|
|
|
|
|
+ "Forward10Button",
|
|
|
|
|
+ Forward10ButtonComponent
|
|
|
|
|
+ );
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error("注册快进快退按钮失败:", err);
|
|
console.error("注册快进快退按钮失败:", err);
|
|
|
}
|
|
}
|
|
@@ -382,67 +390,66 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
|
|
|
|
|
registerSeekButtons();
|
|
registerSeekButtons();
|
|
|
player.value = videojs(videoElement, options);
|
|
player.value = videojs(videoElement, options);
|
|
|
- console.log("Video.js 播放器创建成功");
|
|
|
|
|
|
|
|
|
|
// 自定义点击行为
|
|
// 自定义点击行为
|
|
|
player.value.ready(() => {
|
|
player.value.ready(() => {
|
|
|
- console.log("Video.js 播放器准备就绪");
|
|
|
|
|
error.value = "";
|
|
error.value = "";
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
emit("canplay");
|
|
emit("canplay");
|
|
|
|
|
|
|
|
- // 禁用默认的点击暂停行为
|
|
|
|
|
- player.value.tech_.off("tap");
|
|
|
|
|
-
|
|
|
|
|
- // 添加自定义点击处理
|
|
|
|
|
- const playerEl = player.value.el();
|
|
|
|
|
- let clickTimeout: number | null = null;
|
|
|
|
|
- let clickCount = 0;
|
|
|
|
|
-
|
|
|
|
|
- playerEl.addEventListener("click", (event: MouseEvent) => {
|
|
|
|
|
- // 忽略控制栏和进度条上的点击
|
|
|
|
|
- const target = event.target as HTMLElement;
|
|
|
|
|
- if (
|
|
|
|
|
- target.closest(".vjs-control-bar") ||
|
|
|
|
|
- target.closest(".vjs-big-play-button") ||
|
|
|
|
|
- target.closest(".vjs-progress-control") ||
|
|
|
|
|
- target.closest(".vjs-progress-holder") ||
|
|
|
|
|
- target.closest(".vjs-play-progress") ||
|
|
|
|
|
- target.closest(".vjs-load-progress") ||
|
|
|
|
|
- target.classList.contains("vjs-progress-control") ||
|
|
|
|
|
- target.classList.contains("vjs-progress-holder") ||
|
|
|
|
|
- target.classList.contains("vjs-play-progress") ||
|
|
|
|
|
- target.classList.contains("vjs-load-progress")
|
|
|
|
|
- ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 使用 Video.js 默认的点击行为,不需要自定义处理
|
|
|
|
|
|
|
|
- clickCount++;
|
|
|
|
|
- console.log("点击计数:", clickCount);
|
|
|
|
|
-
|
|
|
|
|
- if (clickCount === 1) {
|
|
|
|
|
- clickTimeout = window.setTimeout(() => {
|
|
|
|
|
- // 单击 - 只显示控制栏
|
|
|
|
|
- console.log("单击 - 显示控制栏");
|
|
|
|
|
- player.value.userActive(true);
|
|
|
|
|
- clickCount = 0;
|
|
|
|
|
- clickTimeout = null;
|
|
|
|
|
- }, 300);
|
|
|
|
|
- } else if (clickCount === 2) {
|
|
|
|
|
- // 双击 - 暂停/播放
|
|
|
|
|
- console.log("双击 - 切换播放/暂停状态");
|
|
|
|
|
- if (clickTimeout) {
|
|
|
|
|
- clearTimeout(clickTimeout);
|
|
|
|
|
- clickTimeout = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 确保 iOS 设备上的内联播放设置
|
|
|
|
|
+ const videoEl = player.value.el().querySelector("video");
|
|
|
|
|
+ if (videoEl) {
|
|
|
|
|
+ videoEl.setAttribute("playsinline", "true");
|
|
|
|
|
+ videoEl.setAttribute("webkit-playsinline", "true");
|
|
|
|
|
+ videoEl.setAttribute("x-webkit-airplay", "allow");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (player.value.paused()) {
|
|
|
|
|
- player.value.play();
|
|
|
|
|
- } else {
|
|
|
|
|
- player.value.pause();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 监听播放和暂停事件,动态更新 poster
|
|
|
|
|
+ player.value.on("play", () => {
|
|
|
|
|
+ // 播放时隐藏 poster
|
|
|
|
|
+ if (player.value.poster()) {
|
|
|
|
|
+ player.value.poster("");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- clickCount = 0;
|
|
|
|
|
|
|
+ player.value.on("pause", () => {
|
|
|
|
|
+ // 暂停时显示当前帧作为 poster
|
|
|
|
|
+ const videoElement = player.value.el().querySelector("video");
|
|
|
|
|
+ if (videoElement) {
|
|
|
|
|
+ // 创建 canvas 来捕获当前帧
|
|
|
|
|
+ const canvas = document.createElement("canvas");
|
|
|
|
|
+ const ctx = canvas.getContext("2d");
|
|
|
|
|
+
|
|
|
|
|
+ if (ctx) {
|
|
|
|
|
+ canvas.width = videoElement.videoWidth;
|
|
|
|
|
+ canvas.height = videoElement.videoHeight;
|
|
|
|
|
+ ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
+
|
|
|
|
|
+ // 将 canvas 转换为 blob URL
|
|
|
|
|
+ canvas.toBlob(
|
|
|
|
|
+ (blob) => {
|
|
|
|
|
+ if (blob) {
|
|
|
|
|
+ const currentFrameUrl = URL.createObjectURL(blob);
|
|
|
|
|
+ player.value.poster(currentFrameUrl);
|
|
|
|
|
+
|
|
|
|
|
+ // 清理之前的 blob URL
|
|
|
|
|
+ const oldPoster = player.value.poster();
|
|
|
|
|
+ if (
|
|
|
|
|
+ oldPoster &&
|
|
|
|
|
+ oldPoster.startsWith("blob:") &&
|
|
|
|
|
+ oldPoster !== currentFrameUrl
|
|
|
|
|
+ ) {
|
|
|
|
|
+ URL.revokeObjectURL(oldPoster);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "image/jpeg",
|
|
|
|
|
+ 0.8
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -450,24 +457,20 @@ const initVideoJSPlayer = async (): Promise<void> => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
player.value.on("loadstart", () => {
|
|
player.value.on("loadstart", () => {
|
|
|
- console.log("开始加载视频");
|
|
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
player.value.on("loadeddata", () => {
|
|
player.value.on("loadeddata", () => {
|
|
|
- console.log("视频数据加载完成");
|
|
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
player.value.on("canplay", () => {
|
|
player.value.on("canplay", () => {
|
|
|
- console.log("视频可以播放");
|
|
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
error.value = "";
|
|
error.value = "";
|
|
|
emit("canplay");
|
|
emit("canplay");
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
player.value.on("play", () => {
|
|
player.value.on("play", () => {
|
|
|
- console.log("开始播放");
|
|
|
|
|
emit("play");
|
|
emit("play");
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -522,7 +525,6 @@ const stopVideo = (): void => {
|
|
|
player.value.currentTime(0);
|
|
player.value.currentTime(0);
|
|
|
}
|
|
}
|
|
|
destroyPlayer();
|
|
destroyPlayer();
|
|
|
- forceShowPlayer.value = false;
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 事件处理
|
|
// 事件处理
|
|
@@ -534,9 +536,8 @@ const handleCoverError = (event: Event): void => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleCoverLoad = (): void => {
|
|
const handleCoverLoad = (): void => {
|
|
|
- console.log("封面图片加载完成,显示封面");
|
|
|
|
|
// 确保图片可见
|
|
// 确保图片可见
|
|
|
- if (!isVideoMode.value && !forceShowPlayer.value) {
|
|
|
|
|
|
|
+ if (!hasVideoSource.value) {
|
|
|
const img = document.querySelector(
|
|
const img = document.querySelector(
|
|
|
".video-js-container img"
|
|
".video-js-container img"
|
|
|
) as HTMLImageElement;
|
|
) as HTMLImageElement;
|
|
@@ -582,6 +583,14 @@ watch(
|
|
|
|
|
|
|
|
// 组件卸载时清理资源
|
|
// 组件卸载时清理资源
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
|
|
+ // 清理播放器的 poster blob URL
|
|
|
|
|
+ if (player.value && player.value.poster()) {
|
|
|
|
|
+ const posterUrl = player.value.poster();
|
|
|
|
|
+ if (posterUrl.startsWith("blob:")) {
|
|
|
|
|
+ URL.revokeObjectURL(posterUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
destroyPlayer();
|
|
destroyPlayer();
|
|
|
|
|
|
|
|
// 清理 Blob URL
|
|
// 清理 Blob URL
|
|
@@ -594,146 +603,11 @@ onUnmounted(() => {
|
|
|
const playVideo = (): void => {
|
|
const playVideo = (): void => {
|
|
|
console.log("手动播放被调用");
|
|
console.log("手动播放被调用");
|
|
|
|
|
|
|
|
- // 强制显示播放器
|
|
|
|
|
- forceShowPlayer.value = true;
|
|
|
|
|
-
|
|
|
|
|
- // 先确保销毁现有播放器
|
|
|
|
|
- destroyPlayer();
|
|
|
|
|
-
|
|
|
|
|
- // 等待 DOM 更新后再初始化播放器
|
|
|
|
|
- nextTick(async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- // 重新初始化播放器
|
|
|
|
|
- await initVideoJSPlayer();
|
|
|
|
|
-
|
|
|
|
|
- // 确保播放器准备就绪后再播放
|
|
|
|
|
- if (player.value) {
|
|
|
|
|
- player.value.ready(() => {
|
|
|
|
|
- console.log("播放器准备就绪,开始播放");
|
|
|
|
|
-
|
|
|
|
|
- // 检查播放器状态
|
|
|
|
|
- console.log("播放器技术:", player.value.techName_);
|
|
|
|
|
- console.log("播放器源:", player.value.currentSource());
|
|
|
|
|
-
|
|
|
|
|
- // 禁用默认的点击暂停行为
|
|
|
|
|
- if (player.value.tech_) {
|
|
|
|
|
- player.value.tech_.off("tap");
|
|
|
|
|
-
|
|
|
|
|
- // 添加自定义点击处理
|
|
|
|
|
- const playerEl = player.value.el();
|
|
|
|
|
- let clickTimeout: number | null = null;
|
|
|
|
|
- let clickCount = 0;
|
|
|
|
|
-
|
|
|
|
|
- playerEl.addEventListener("click", (event: MouseEvent) => {
|
|
|
|
|
- // 忽略控制栏和进度条上的点击
|
|
|
|
|
- const target = event.target as HTMLElement;
|
|
|
|
|
- if (
|
|
|
|
|
- target.closest(".vjs-control-bar") ||
|
|
|
|
|
- target.closest(".vjs-big-play-button") ||
|
|
|
|
|
- target.closest(".vjs-progress-control") ||
|
|
|
|
|
- target.closest(".vjs-progress-holder") ||
|
|
|
|
|
- target.closest(".vjs-play-progress") ||
|
|
|
|
|
- target.closest(".vjs-load-progress") ||
|
|
|
|
|
- target.classList.contains("vjs-progress-control") ||
|
|
|
|
|
- target.classList.contains("vjs-progress-holder") ||
|
|
|
|
|
- target.classList.contains("vjs-play-progress") ||
|
|
|
|
|
- target.classList.contains("vjs-load-progress")
|
|
|
|
|
- ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- clickCount++;
|
|
|
|
|
- console.log("点击计数:", clickCount);
|
|
|
|
|
-
|
|
|
|
|
- if (clickCount === 1) {
|
|
|
|
|
- clickTimeout = window.setTimeout(() => {
|
|
|
|
|
- // 单击 - 只显示控制栏
|
|
|
|
|
- console.log("单击 - 显示控制栏");
|
|
|
|
|
- player.value.userActive(true);
|
|
|
|
|
- clickCount = 0;
|
|
|
|
|
- clickTimeout = null;
|
|
|
|
|
- }, 300);
|
|
|
|
|
- } else if (clickCount === 2) {
|
|
|
|
|
- // 双击 - 暂停/播放
|
|
|
|
|
- console.log("双击 - 切换播放/暂停状态");
|
|
|
|
|
- if (clickTimeout) {
|
|
|
|
|
- clearTimeout(clickTimeout);
|
|
|
|
|
- clickTimeout = null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (player.value.paused()) {
|
|
|
|
|
- player.value.play();
|
|
|
|
|
- } else {
|
|
|
|
|
- player.value.pause();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- clickCount = 0;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 检查浏览器 HLS 支持
|
|
|
|
|
- const testVideo = document.createElement("video");
|
|
|
|
|
- console.log("浏览器 HLS 支持:", {
|
|
|
|
|
- "application/x-mpegURL": testVideo.canPlayType(
|
|
|
|
|
- "application/x-mpegURL"
|
|
|
|
|
- ),
|
|
|
|
|
- "application/vnd.apple.mpegurl": testVideo.canPlayType(
|
|
|
|
|
- "application/vnd.apple.mpegurl"
|
|
|
|
|
- ),
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 延迟一点时间再播放
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (player.value) {
|
|
|
|
|
- // 确保源已经设置
|
|
|
|
|
- player.value.src(player.value.currentSource());
|
|
|
|
|
-
|
|
|
|
|
- console.log("尝试播放...");
|
|
|
|
|
-
|
|
|
|
|
- const playPromise = player.value.play();
|
|
|
|
|
-
|
|
|
|
|
- if (playPromise !== undefined) {
|
|
|
|
|
- playPromise
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- console.log("播放成功启动");
|
|
|
|
|
- })
|
|
|
|
|
- .catch((err: any) => {
|
|
|
|
|
- console.error("播放失败:", err);
|
|
|
|
|
-
|
|
|
|
|
- // 尝试使用原生 video 元素播放
|
|
|
|
|
- console.log("尝试使用原生 video 元素播放");
|
|
|
|
|
- const video = document.createElement("video");
|
|
|
|
|
- video.src = processedVideoUrl.value;
|
|
|
|
|
- video.controls = true;
|
|
|
|
|
- video.style.width = "100%";
|
|
|
|
|
- video.style.height = "100%";
|
|
|
|
|
-
|
|
|
|
|
- if (videoContainer.value) {
|
|
|
|
|
- // 清空容器并添加原生视频
|
|
|
|
|
- destroyPlayer();
|
|
|
|
|
- videoContainer.value.innerHTML = "";
|
|
|
|
|
- videoContainer.value.appendChild(video);
|
|
|
|
|
-
|
|
|
|
|
- // 尝试播放
|
|
|
|
|
- video
|
|
|
|
|
- .play()
|
|
|
|
|
- .catch((e) => console.error("原生播放也失败:", e));
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error("播放时发生异常:", e);
|
|
|
|
|
- }
|
|
|
|
|
- }, 500);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error("初始化播放器失败:", err);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (player.value) {
|
|
|
|
|
+ player.value.play().catch((err: any) => {
|
|
|
|
|
+ console.error("播放失败:", err);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 暂停播放方法
|
|
// 暂停播放方法
|
|
@@ -747,7 +621,6 @@ defineExpose({
|
|
|
stopVideo,
|
|
stopVideo,
|
|
|
playVideo,
|
|
playVideo,
|
|
|
pauseVideo,
|
|
pauseVideo,
|
|
|
- processedCoverUrl: computed(() => processedCoverUrl.value),
|
|
|
|
|
processedVideoUrl: computed(() => processedVideoUrl.value),
|
|
processedVideoUrl: computed(() => processedVideoUrl.value),
|
|
|
loading: computed(() => loading.value),
|
|
loading: computed(() => loading.value),
|
|
|
error: computed(() => error.value),
|
|
error: computed(() => error.value),
|
|
@@ -764,7 +637,6 @@ defineExpose({
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.video-js-container img {
|
|
.video-js-container img {
|
|
|
- display: block;
|
|
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
object-fit: cover;
|
|
@@ -788,23 +660,25 @@ defineExpose({
|
|
|
background-color: #000;
|
|
background-color: #000;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 确保封面图片始终显示 */
|
|
|
|
|
|
|
+/* Video.js poster 样式 */
|
|
|
:deep(.video-js .vjs-poster) {
|
|
:deep(.video-js .vjs-poster) {
|
|
|
background-size: cover;
|
|
background-size: cover;
|
|
|
opacity: 1 !important;
|
|
opacity: 1 !important;
|
|
|
- display: block !important;
|
|
|
|
|
background-position: center center;
|
|
background-position: center center;
|
|
|
background-repeat: no-repeat;
|
|
background-repeat: no-repeat;
|
|
|
z-index: 1;
|
|
z-index: 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 确保封面图片在加载中状态下也显示 */
|
|
|
|
|
-:deep(.video-js.vjs-has-started .vjs-poster) {
|
|
|
|
|
- display: none;
|
|
|
|
|
|
|
+/* 播放时隐藏 poster */
|
|
|
|
|
+:deep(.video-js.vjs-playing .vjs-poster) {
|
|
|
|
|
+ display: none !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 暂停时显示 poster */
|
|
|
:deep(.video-js.vjs-paused .vjs-poster) {
|
|
:deep(.video-js.vjs-paused .vjs-poster) {
|
|
|
display: block !important;
|
|
display: block !important;
|
|
|
|
|
+ opacity: 1 !important;
|
|
|
|
|
+ z-index: 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 自定义中央播放按钮样式 */
|
|
/* 自定义中央播放按钮样式 */
|
|
@@ -833,182 +707,40 @@ defineExpose({
|
|
|
transform: scale(1.1);
|
|
transform: scale(1.1);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.video-js .vjs-control-bar) {
|
|
|
|
|
- background: linear-gradient(
|
|
|
|
|
- to top,
|
|
|
|
|
- rgba(0, 0, 0, 0.95) 0%,
|
|
|
|
|
- rgba(0, 0, 0, 0.7) 60%,
|
|
|
|
|
- rgba(0, 0, 0, 0) 100%
|
|
|
|
|
- );
|
|
|
|
|
- height: 4em; /* 增加控制栏高度 */
|
|
|
|
|
- padding: 0 1.2em; /* 增加左右内边距 */
|
|
|
|
|
- opacity: 1; /* 始终显示控制栏 */
|
|
|
|
|
- transform: translateY(0);
|
|
|
|
|
- transition: opacity 0.3s ease;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 确保控制栏在用户交互时始终可见 */
|
|
|
|
|
-:deep(.video-js.vjs-user-active .vjs-control-bar) {
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- visibility: visible;
|
|
|
|
|
- pointer-events: auto;
|
|
|
|
|
- animation: fadeIn 0.3s ease;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-@keyframes fadeIn {
|
|
|
|
|
- from {
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- transform: translateY(5px);
|
|
|
|
|
- }
|
|
|
|
|
- to {
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- transform: translateY(0);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 控制栏按钮样式 */
|
|
|
|
|
-:deep(.video-js .vjs-control) {
|
|
|
|
|
- width: 3.2em;
|
|
|
|
|
- opacity: 0.8;
|
|
|
|
|
- transition: opacity 0.2s ease, transform 0.2s ease;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-control:hover) {
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- transform: scale(1.1);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 全屏按钮样式 */
|
|
|
|
|
-:deep(.video-js .vjs-fullscreen-control) {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- right: 0.5em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 播放按钮样式 */
|
|
|
|
|
-:deep(.video-js .vjs-play-control) {
|
|
|
|
|
- font-size: 1.3em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 进度条样式 */
|
|
|
|
|
-:deep(.video-js .vjs-progress-control) {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 4em;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- height: 1em;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.3);
|
|
|
|
|
- transition: height 0.2s ease;
|
|
|
|
|
- opacity: 1 !important;
|
|
|
|
|
- visibility: visible !important;
|
|
|
|
|
- display: flex !important;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
- pointer-events: auto !important; /* 确保进度条可点击 */
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js.vjs-user-active .vjs-progress-control),
|
|
|
|
|
-:deep(.video-js:hover .vjs-progress-control) {
|
|
|
|
|
- height: 1.2em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 音量控制样式 */
|
|
|
|
|
-:deep(.video-js .vjs-volume-panel) {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- right: 3.5em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-progress-holder) {
|
|
|
|
|
- height: 0.8em;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
- border-radius: 1em;
|
|
|
|
|
- margin: 0 0.5em;
|
|
|
|
|
- transition: height 0.2s ease;
|
|
|
|
|
- display: flex !important;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- pointer-events: auto !important; /* 确保进度条可点击 */
|
|
|
|
|
- cursor: pointer !important; /* 显示点击光标 */
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js.vjs-user-active .vjs-progress-holder),
|
|
|
|
|
-:deep(.video-js:hover .vjs-progress-holder) {
|
|
|
|
|
- height: 1em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 确保进度条上的所有元素都可以点击 */
|
|
|
|
|
-:deep(.video-js .vjs-progress-control .vjs-mouse-display),
|
|
|
|
|
-:deep(.video-js .vjs-progress-control .vjs-play-progress),
|
|
|
|
|
-:deep(.video-js .vjs-progress-control .vjs-load-progress),
|
|
|
|
|
-:deep(.video-js .vjs-progress-control .vjs-load-progress div) {
|
|
|
|
|
- pointer-events: auto !important;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-play-progress) {
|
|
|
|
|
- background: linear-gradient(to right, #3b82f6, #60a5fa);
|
|
|
|
|
- border-radius: 1em;
|
|
|
|
|
- box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-play-progress:before) {
|
|
|
|
|
- font-size: 0.9em;
|
|
|
|
|
- top: -0.3em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-load-progress) {
|
|
|
|
|
- background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
- border-radius: 1em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 时间显示样式 */
|
|
|
|
|
-:deep(.video-js .vjs-time-control) {
|
|
|
|
|
- padding-left: 0.5em;
|
|
|
|
|
- padding-right: 0.5em;
|
|
|
|
|
- min-width: 2.2em;
|
|
|
|
|
- font-family: "Arial", sans-serif;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- font-size: 0.9em;
|
|
|
|
|
- opacity: 0.9;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-current-time) {
|
|
|
|
|
- padding-right: 0.1em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-duration) {
|
|
|
|
|
- padding-left: 0.1em;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:deep(.video-js .vjs-time-divider) {
|
|
|
|
|
- padding: 0;
|
|
|
|
|
- min-width: 1em;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 快进快退按钮 */
|
|
|
|
|
-:deep(.video-js .vjs-replay-button) {
|
|
|
|
|
|
|
+/* 快进 10 秒按钮样式 */
|
|
|
|
|
+:deep(.video-js .vjs-forward-10-button) {
|
|
|
font-size: 1.3em;
|
|
font-size: 1.3em;
|
|
|
position: relative;
|
|
position: relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.video-js .vjs-replay-button::before) {
|
|
|
|
|
- content: "⟲";
|
|
|
|
|
|
|
+:deep(.video-js .vjs-forward-10-button::before) {
|
|
|
|
|
+ content: "\f11d";
|
|
|
|
|
+ font-family: VideoJS;
|
|
|
text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
|
|
text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.video-js .vjs-forward-button) {
|
|
|
|
|
- font-size: 1.3em;
|
|
|
|
|
- position: relative;
|
|
|
|
|
|
|
+/* 确保进度条有足够的显示长度 */
|
|
|
|
|
+:deep(.video-js .vjs-progress-control) {
|
|
|
|
|
+ flex: 1 !important;
|
|
|
|
|
+ min-width: 0 !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.video-js .vjs-forward-button::before) {
|
|
|
|
|
- content: "⟳";
|
|
|
|
|
- text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
|
+:deep(.video-js .vjs-progress-holder) {
|
|
|
|
|
+ flex: 1 !important;
|
|
|
|
|
+ width: 100% !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 美化播放器整体外观 */
|
|
|
|
|
-:deep(.video-js) {
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
|
+/* iOS 设备特殊样式 */
|
|
|
|
|
+@supports (-webkit-touch-callout: none) {
|
|
|
|
|
+ :deep(.video-js video) {
|
|
|
|
|
+ -webkit-playsinline: true;
|
|
|
|
|
+ playsinline: true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.video-js) {
|
|
|
|
|
+ -webkit-transform: translateZ(0);
|
|
|
|
|
+ transform: translateZ(0);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.loading-overlay {
|
|
.loading-overlay {
|
|
@@ -1155,32 +887,6 @@ defineExpose({
|
|
|
margin-left: -0.75em;
|
|
margin-left: -0.75em;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- :deep(.video-js .vjs-control-bar) {
|
|
|
|
|
- height: 3.5em;
|
|
|
|
|
- padding: 0 0.8em;
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- :deep(.video-js .vjs-control) {
|
|
|
|
|
- width: 2.8em;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- :deep(.video-js .vjs-progress-control) {
|
|
|
|
|
- bottom: 3.5em;
|
|
|
|
|
- height: 0.8em;
|
|
|
|
|
- opacity: 1 !important;
|
|
|
|
|
- visibility: visible !important;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- :deep(.video-js .vjs-progress-holder) {
|
|
|
|
|
- height: 0.8em;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- :deep(.video-js .vjs-time-control) {
|
|
|
|
|
- min-width: 1.8em;
|
|
|
|
|
- font-size: 0.8em;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
.error-content {
|
|
.error-content {
|
|
|
padding: 18px;
|
|
padding: 18px;
|
|
|
max-width: 260px;
|
|
max-width: 260px;
|