|
|
@@ -38,20 +38,16 @@
|
|
|
</video>
|
|
|
|
|
|
<!-- 自定义播放控制条 -->
|
|
|
- <div
|
|
|
- v-if="isVideoMode && !error"
|
|
|
+ <div
|
|
|
+ v-if="isVideoMode && !error"
|
|
|
class="custom-controls"
|
|
|
- :class="{ 'show': showControls }"
|
|
|
+ :class="{ show: showControls }"
|
|
|
@click.stop
|
|
|
>
|
|
|
<!-- 播放/暂停按钮(中央) -->
|
|
|
- <div
|
|
|
- v-if="!isPlaying"
|
|
|
- class="play-button-center"
|
|
|
- @click="togglePlay"
|
|
|
- >
|
|
|
+ <div v-if="!isPlaying" class="play-button-center" @click="togglePlay">
|
|
|
<svg class="w-16 h-16" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M8 5v14l11-7z"/>
|
|
|
+ <path d="M8 5v14l11-7z" />
|
|
|
</svg>
|
|
|
</div>
|
|
|
|
|
|
@@ -60,8 +56,14 @@
|
|
|
<!-- 进度条 -->
|
|
|
<div class="progress-container" @click="seekVideo">
|
|
|
<div class="progress-bar">
|
|
|
- <div class="progress-played" :style="{ width: progress + '%' }"></div>
|
|
|
- <div class="progress-buffered" :style="{ width: buffered + '%' }"></div>
|
|
|
+ <div
|
|
|
+ class="progress-played"
|
|
|
+ :style="{ width: progress + '%' }"
|
|
|
+ ></div>
|
|
|
+ <div
|
|
|
+ class="progress-buffered"
|
|
|
+ :style="{ width: buffered + '%' }"
|
|
|
+ ></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -70,14 +72,21 @@
|
|
|
<!-- 左侧:播放/暂停 + 时间 -->
|
|
|
<div class="controls-left">
|
|
|
<button class="control-btn" @click="togglePlay">
|
|
|
- <svg v-if="!isPlaying" class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M8 5v14l11-7z"/>
|
|
|
+ <svg
|
|
|
+ v-if="!isPlaying"
|
|
|
+ class="w-6 h-6"
|
|
|
+ fill="white"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ >
|
|
|
+ <path d="M8 5v14l11-7z" />
|
|
|
</svg>
|
|
|
<svg v-else class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
|
|
|
+ <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
|
|
|
</svg>
|
|
|
</button>
|
|
|
- <span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
|
+ <span class="time-display"
|
|
|
+ >{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span
|
|
|
+ >
|
|
|
</div>
|
|
|
|
|
|
<!-- 右侧:音量 + 全屏 -->
|
|
|
@@ -85,34 +94,60 @@
|
|
|
<!-- 音量控制 -->
|
|
|
<div class="volume-control">
|
|
|
<button class="control-btn" @click="toggleMute">
|
|
|
- <svg v-if="!isMuted && volume > 0.5" class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
|
|
+ <svg
|
|
|
+ v-if="!isMuted && volume > 0.5"
|
|
|
+ class="w-6 h-6"
|
|
|
+ fill="white"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"
|
|
|
+ />
|
|
|
</svg>
|
|
|
- <svg v-else-if="!isMuted && volume > 0" class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M7 9v6h4l5 5V4l-5 5H7z"/>
|
|
|
+ <svg
|
|
|
+ v-else-if="!isMuted && volume > 0"
|
|
|
+ class="w-6 h-6"
|
|
|
+ fill="white"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ >
|
|
|
+ <path d="M7 9v6h4l5 5V4l-5 5H7z" />
|
|
|
</svg>
|
|
|
<svg v-else class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
|
|
+ <path
|
|
|
+ d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"
|
|
|
+ />
|
|
|
</svg>
|
|
|
</button>
|
|
|
- <input
|
|
|
+ <input
|
|
|
v-if="showVolumeSlider"
|
|
|
- type="range"
|
|
|
- class="volume-slider"
|
|
|
- min="0"
|
|
|
- max="100"
|
|
|
+ type="range"
|
|
|
+ class="volume-slider"
|
|
|
+ min="0"
|
|
|
+ max="100"
|
|
|
:value="volume * 100"
|
|
|
@input="changeVolume"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
<!-- 全屏按钮 -->
|
|
|
- <button class="control-btn fullscreen-btn" @click="toggleFullscreen">
|
|
|
- <svg v-if="!isFullscreen" class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
|
|
+ <button
|
|
|
+ class="control-btn fullscreen-btn"
|
|
|
+ @click="toggleFullscreen"
|
|
|
+ >
|
|
|
+ <svg
|
|
|
+ v-if="!isFullscreen"
|
|
|
+ class="w-6 h-6"
|
|
|
+ fill="white"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
|
|
|
+ />
|
|
|
</svg>
|
|
|
<svg v-else class="w-6 h-6" fill="white" viewBox="0 0 24 24">
|
|
|
- <path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
|
|
|
+ <path
|
|
|
+ d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
|
|
|
+ />
|
|
|
</svg>
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -175,6 +210,7 @@ const emit = defineEmits<{
|
|
|
play: [];
|
|
|
timeupdate: [];
|
|
|
seeking: [];
|
|
|
+ canplay: [];
|
|
|
}>();
|
|
|
|
|
|
// 响应式数据
|
|
|
@@ -312,7 +348,11 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
error.value = "";
|
|
|
|
|
|
// 检查是否为标准的 HLS 流地址(不需要解密的)
|
|
|
- if (isStandardHlsUrl(url) && !url.includes("play") && !url.includes("cover")) {
|
|
|
+ if (
|
|
|
+ isStandardHlsUrl(url) &&
|
|
|
+ !url.includes("play") &&
|
|
|
+ !url.includes("cover")
|
|
|
+ ) {
|
|
|
processedVideoUrl.value = url;
|
|
|
await nextTick();
|
|
|
await initVideoPlayer();
|
|
|
@@ -334,19 +374,19 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
|
|
|
// 根据不同浏览器选择合适的 MIME 类型和 URL 格式
|
|
|
let mimeType = "application/x-mpegURL";
|
|
|
-
|
|
|
+
|
|
|
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)
|
|
|
if (isIOSQuarkBrowser()) {
|
|
|
try {
|
|
|
@@ -355,16 +395,17 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
console.log("iOS 夸克浏览器使用 Data URL");
|
|
|
} catch (e) {
|
|
|
console.error("Data URL 生成失败:", e);
|
|
|
- error.value = "抱歉,iOS 夸克浏览器暂不支持播放此视频,建议使用 Safari 打开";
|
|
|
+ error.value =
|
|
|
+ "抱歉,iOS 夸克浏览器暂不支持播放此视频,建议使用 Safari 打开";
|
|
|
emit("error", error.value);
|
|
|
loading.value = false;
|
|
|
return;
|
|
|
}
|
|
|
- }
|
|
|
+ }
|
|
|
// else if (isQQBrowser()) {
|
|
|
// // QQ浏览器:使用正确的MIME类型
|
|
|
// console.log("检测到QQ浏览器,使用HLS.js兼容模式");
|
|
|
-
|
|
|
+
|
|
|
// try {
|
|
|
// // 使用正确的HLS MIME类型
|
|
|
// const blob = new Blob([playlist], { type: "application/vnd.apple.mpegurl" });
|
|
|
@@ -372,7 +413,7 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
// 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)));
|
|
|
@@ -387,7 +428,7 @@ const processVideo = async (url: string): Promise<void> => {
|
|
|
// }
|
|
|
// }
|
|
|
// }
|
|
|
-
|
|
|
+
|
|
|
// 其他所有浏览器(Chrome、Firefox、Edge、UC等)使用 Blob URL
|
|
|
if (!isIOSQuarkBrowser()) {
|
|
|
const blob = new Blob([playlist], { type: mimeType });
|
|
|
@@ -462,24 +503,26 @@ const isIOSSafari = (): boolean => {
|
|
|
// 检测是否为夸克浏览器
|
|
|
const isQuarkBrowser = (): boolean => {
|
|
|
const ua = navigator.userAgent.toLowerCase();
|
|
|
- return ua.includes('quark') || ua.includes('quarks');
|
|
|
+ return ua.includes("quark") || ua.includes("quarks");
|
|
|
};
|
|
|
|
|
|
// 检测是否为 iOS 夸克浏览器
|
|
|
const isIOSQuarkBrowser = (): boolean => {
|
|
|
- return isQuarkBrowser() && (/iPad|iPhone|iPod/.test(navigator.userAgent));
|
|
|
+ return isQuarkBrowser() && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
|
};
|
|
|
|
|
|
// 检测是否为 QQ 浏览器
|
|
|
const isQQBrowser = (): boolean => {
|
|
|
const ua = navigator.userAgent.toLowerCase();
|
|
|
- return ua.includes('mqqbrowser') || ua.includes('qq/');
|
|
|
+ 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');
|
|
|
+ return (
|
|
|
+ ua.includes("ucbrowser") || ua.includes("ubrowser") || ua.includes("ucweb")
|
|
|
+ );
|
|
|
};
|
|
|
|
|
|
// 初始化视频播放器
|
|
|
@@ -494,7 +537,7 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
// 设置加载状态
|
|
|
isVideoLoading.value = true;
|
|
|
error.value = "";
|
|
|
-
|
|
|
+
|
|
|
// 清除错误防抖定时器
|
|
|
if (errorDebounceTimer) {
|
|
|
clearTimeout(errorDebounceTimer);
|
|
|
@@ -503,7 +546,7 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
|
|
|
// 清除之前的src,避免缓存问题
|
|
|
if (video.src) {
|
|
|
- video.removeAttribute('src');
|
|
|
+ video.removeAttribute("src");
|
|
|
video.load();
|
|
|
}
|
|
|
|
|
|
@@ -520,7 +563,7 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
// Android 夸克浏览器特殊处理
|
|
|
if (isQuarkBrowser()) {
|
|
|
console.log("检测到 Android 夸克浏览器");
|
|
|
-
|
|
|
+
|
|
|
// Android 夸克支持 HLS.js,直接使用
|
|
|
if (Hls.isSupported()) {
|
|
|
console.log("使用 HLS.js 播放");
|
|
|
@@ -537,7 +580,8 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
// QQ 浏览器不支持提示
|
|
|
if (isQQBrowser()) {
|
|
|
console.log("检测到 QQ 浏览器,不支持加密视频播放");
|
|
|
- error.value = "QQ浏览器暂不支持播放此类视频\n请使用Chrome、UC或其他浏览器访问";
|
|
|
+ error.value =
|
|
|
+ "QQ浏览器暂不支持播放此类视频\n请使用Chrome、UC或其他浏览器访问";
|
|
|
// 不提供重试选项,直接返回
|
|
|
return;
|
|
|
}
|
|
|
@@ -545,7 +589,7 @@ const initVideoPlayer = async (): Promise<void> => {
|
|
|
// UC 浏览器特殊处理 - UC通常支持标准的HLS播放
|
|
|
if (isUCBrowser()) {
|
|
|
console.log("检测到 UC 浏览器");
|
|
|
-
|
|
|
+
|
|
|
// UC浏览器优先使用HLS.js,这样可以使用自定义控制条
|
|
|
if (Hls.isSupported()) {
|
|
|
console.log("UC浏览器使用HLS.js播放器");
|
|
|
@@ -641,7 +685,7 @@ const initHlsPlayer = (): void => {
|
|
|
|
|
|
hlsInstance.value.on(Hls.Events.ERROR, (event, data) => {
|
|
|
console.error("HLS 错误:", data);
|
|
|
-
|
|
|
+
|
|
|
if (data.fatal) {
|
|
|
switch (data.type) {
|
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
|
@@ -664,7 +708,7 @@ const initHlsPlayer = (): void => {
|
|
|
error.value = "视频加载失败";
|
|
|
hlsInstance.value?.destroy();
|
|
|
hlsInstance.value = null;
|
|
|
-
|
|
|
+
|
|
|
// 尝试降级到原生播放器
|
|
|
setTimeout(() => {
|
|
|
if (videoElement.value && processedVideoUrl.value) {
|
|
|
@@ -726,7 +770,7 @@ const retry = (): void => {
|
|
|
// 播放器控制方法
|
|
|
const togglePlay = (): void => {
|
|
|
if (!videoElement.value) return;
|
|
|
-
|
|
|
+
|
|
|
if (isPlaying.value) {
|
|
|
videoElement.value.pause();
|
|
|
} else {
|
|
|
@@ -736,19 +780,19 @@ const togglePlay = (): void => {
|
|
|
|
|
|
const toggleMute = (): void => {
|
|
|
if (!videoElement.value) return;
|
|
|
-
|
|
|
+
|
|
|
isMuted.value = !isMuted.value;
|
|
|
videoElement.value.muted = isMuted.value;
|
|
|
};
|
|
|
|
|
|
const changeVolume = (event: Event): void => {
|
|
|
if (!videoElement.value) return;
|
|
|
-
|
|
|
+
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
const newVolume = parseInt(target.value) / 100;
|
|
|
volume.value = newVolume;
|
|
|
videoElement.value.volume = newVolume;
|
|
|
-
|
|
|
+
|
|
|
if (newVolume > 0 && isMuted.value) {
|
|
|
isMuted.value = false;
|
|
|
videoElement.value.muted = false;
|
|
|
@@ -797,30 +841,32 @@ const toggleFullscreen = async (): Promise<void> => {
|
|
|
|
|
|
const seekVideo = (event: MouseEvent): void => {
|
|
|
if (!videoElement.value || !duration.value) return;
|
|
|
-
|
|
|
+
|
|
|
const progressBar = event.currentTarget as HTMLElement;
|
|
|
const rect = progressBar.getBoundingClientRect();
|
|
|
const pos = (event.clientX - rect.left) / rect.width;
|
|
|
const seekTime = pos * duration.value;
|
|
|
-
|
|
|
+
|
|
|
videoElement.value.currentTime = seekTime;
|
|
|
};
|
|
|
|
|
|
const formatTime = (seconds: number): string => {
|
|
|
if (isNaN(seconds)) return "00:00";
|
|
|
-
|
|
|
+
|
|
|
const mins = Math.floor(seconds / 60);
|
|
|
const secs = Math.floor(seconds % 60);
|
|
|
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
|
+ return `${mins.toString().padStart(2, "0")}:${secs
|
|
|
+ .toString()
|
|
|
+ .padStart(2, "0")}`;
|
|
|
};
|
|
|
|
|
|
const toggleControls = (): void => {
|
|
|
showControls.value = true;
|
|
|
-
|
|
|
+
|
|
|
if (controlsTimer) {
|
|
|
clearTimeout(controlsTimer);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 如果正在播放,3秒后自动隐藏控制条
|
|
|
if (isPlaying.value) {
|
|
|
controlsTimer = setTimeout(() => {
|
|
|
@@ -852,17 +898,20 @@ const onVideoLoadedData = (): void => {
|
|
|
|
|
|
const onVideoCanPlay = (): void => {
|
|
|
console.log("视频可以播放");
|
|
|
-
|
|
|
+
|
|
|
// 清除加载状态和错误
|
|
|
isVideoLoading.value = false;
|
|
|
error.value = "";
|
|
|
-
|
|
|
+
|
|
|
// 清除错误防抖定时器
|
|
|
if (errorDebounceTimer) {
|
|
|
clearTimeout(errorDebounceTimer);
|
|
|
errorDebounceTimer = null;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ // 发出canplay事件
|
|
|
+ emit("canplay");
|
|
|
+
|
|
|
if (props.autoPlay && videoElement.value) {
|
|
|
videoElement.value.play().catch(() => {});
|
|
|
}
|
|
|
@@ -896,19 +945,21 @@ const onVideoEnded = (): void => {
|
|
|
const onVideoTimeUpdate = (): void => {
|
|
|
if (videoElement.value) {
|
|
|
currentTime.value = videoElement.value.currentTime;
|
|
|
-
|
|
|
+
|
|
|
// 更新进度
|
|
|
if (duration.value > 0) {
|
|
|
progress.value = (currentTime.value / duration.value) * 100;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新缓冲进度
|
|
|
if (videoElement.value.buffered.length > 0) {
|
|
|
- const bufferedEnd = videoElement.value.buffered.end(videoElement.value.buffered.length - 1);
|
|
|
+ const bufferedEnd = videoElement.value.buffered.end(
|
|
|
+ videoElement.value.buffered.length - 1
|
|
|
+ );
|
|
|
buffered.value = (bufferedEnd / duration.value) * 100;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
emit("timeupdate");
|
|
|
};
|
|
|
|
|
|
@@ -921,7 +972,13 @@ const onVideoError = (event: Event): void => {
|
|
|
const errorCode = video.error?.code;
|
|
|
const errorMessage = video.error?.message;
|
|
|
|
|
|
- console.log("视频错误事件:", errorCode, errorMessage, "加载中:", isVideoLoading.value);
|
|
|
+ console.log(
|
|
|
+ "视频错误事件:",
|
|
|
+ errorCode,
|
|
|
+ errorMessage,
|
|
|
+ "加载中:",
|
|
|
+ isVideoLoading.value
|
|
|
+ );
|
|
|
|
|
|
// 清除之前的错误防抖定时器
|
|
|
if (errorDebounceTimer) {
|
|
|
@@ -932,10 +989,10 @@ const onVideoError = (event: Event): void => {
|
|
|
// 如果视频正在加载中,延迟显示错误(给视频更多时间初始化)
|
|
|
if (isVideoLoading.value) {
|
|
|
console.log("视频正在初始化,延迟错误显示");
|
|
|
-
|
|
|
+
|
|
|
// 对于夸克浏览器,给更长的初始化时间(从2s提升至3s)
|
|
|
const delayTime = isQuarkBrowser() ? 3000 : 1000;
|
|
|
-
|
|
|
+
|
|
|
errorDebounceTimer = setTimeout(() => {
|
|
|
// 再次检查视频状态
|
|
|
if (video.readyState >= 2) {
|
|
|
@@ -943,11 +1000,11 @@ const onVideoError = (event: Event): void => {
|
|
|
console.log("视频已准备好,忽略错误");
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 显示错误
|
|
|
showVideoError(errorCode, errorMessage);
|
|
|
}, delayTime);
|
|
|
-
|
|
|
+
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -968,7 +1025,7 @@ const showVideoError = (errorCode?: number, errorMessage?: string): void => {
|
|
|
errorText = "网络错误,无法加载视频";
|
|
|
break;
|
|
|
case 3: // MEDIA_ERR_DECODE
|
|
|
- errorText = isIOSQuarkBrowser()
|
|
|
+ errorText = isIOSQuarkBrowser()
|
|
|
? "抱歉,iOS 夸克浏览器暂不支持此视频,建议使用 Safari 打开"
|
|
|
: "视频解码失败";
|
|
|
break;
|
|
|
@@ -1039,32 +1096,35 @@ const handleFullscreenChange = (): void => {
|
|
|
|
|
|
// 组件挂载时添加全屏监听
|
|
|
onMounted(() => {
|
|
|
- document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
|
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|
|
- document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
|
|
- document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
|
|
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
|
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
|
+ document.addEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
|
+ document.addEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
|
});
|
|
|
|
|
|
// 组件卸载时清理资源
|
|
|
onUnmounted(() => {
|
|
|
destroyHls();
|
|
|
-
|
|
|
+
|
|
|
// 清理全屏监听
|
|
|
- document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
|
- document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|
|
- document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
|
|
|
- document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
|
|
|
-
|
|
|
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
|
+ document.removeEventListener(
|
|
|
+ "webkitfullscreenchange",
|
|
|
+ handleFullscreenChange
|
|
|
+ );
|
|
|
+ document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
|
|
|
+ document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
|
|
|
+
|
|
|
// 清理控制条定时器
|
|
|
if (controlsTimer) {
|
|
|
clearTimeout(controlsTimer);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 清理错误防抖定时器
|
|
|
if (errorDebounceTimer) {
|
|
|
clearTimeout(errorDebounceTimer);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 清理 Blob URL
|
|
|
if (processedCoverUrl.value?.startsWith("blob:")) {
|
|
|
URL.revokeObjectURL(processedCoverUrl.value);
|
|
|
@@ -1159,7 +1219,12 @@ defineExpose({
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
bottom: 0;
|
|
|
- background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 70%, transparent 100%);
|
|
|
+ background: linear-gradient(
|
|
|
+ to top,
|
|
|
+ rgba(0, 0, 0, 0.8) 0%,
|
|
|
+ rgba(0, 0, 0, 0.4) 70%,
|
|
|
+ transparent 100%
|
|
|
+ );
|
|
|
padding: 10px 15px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
@@ -1315,7 +1380,12 @@ defineExpose({
|
|
|
.video-processor:fullscreen .controls-bottom,
|
|
|
.video-processor:-webkit-full-screen .controls-bottom,
|
|
|
.video-processor:-moz-full-screen .controls-bottom {
|
|
|
- background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.6) 70%, transparent 100%);
|
|
|
+ background: linear-gradient(
|
|
|
+ to top,
|
|
|
+ rgba(0, 0, 0, 0.9) 0%,
|
|
|
+ rgba(0, 0, 0, 0.6) 70%,
|
|
|
+ transparent 100%
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|