|
|
@@ -1,5 +1,20 @@
|
|
|
<template>
|
|
|
+ <!-- 缩略图模式:不加载视频 src,只显示 poster 背景 + play 图标(和 BaseVideoElement 一致) -->
|
|
|
+ <div
|
|
|
+ v-if="thumbnail"
|
|
|
+ class="video-thumbnail"
|
|
|
+ :style="{
|
|
|
+ width: width * scale + 'px',
|
|
|
+ height: height * scale + 'px',
|
|
|
+ transform: `scale(${1 / scale})`,
|
|
|
+ backgroundImage: poster ? `url(${poster})` : '',
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <IconPlayOne class="thumb-play-icon" />
|
|
|
+ </div>
|
|
|
+
|
|
|
<div
|
|
|
+ v-else
|
|
|
class="video-player"
|
|
|
:class="{ 'hide-controller': hideController }"
|
|
|
:style="{
|
|
|
@@ -18,8 +33,6 @@
|
|
|
class="video"
|
|
|
preload="none"
|
|
|
ref="videoRef"
|
|
|
- :src="src"
|
|
|
- :autoplay="autoplay"
|
|
|
:poster="poster"
|
|
|
webkit-playsinline
|
|
|
playsinline
|
|
|
@@ -123,9 +136,12 @@
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { computed, ref, useTemplateRef, onMounted } from 'vue'
|
|
|
+import { computed, ref, useTemplateRef, onMounted, onBeforeUnmount } from 'vue'
|
|
|
import { lang } from '@/main'
|
|
|
import useMSE from './useMSE'
|
|
|
+import { useVideoManager } from '@/hooks/useVideoManager'
|
|
|
+
|
|
|
+const manager = useVideoManager()
|
|
|
|
|
|
const props = withDefaults(defineProps<{
|
|
|
width: number
|
|
|
@@ -201,15 +217,32 @@ const play = () => {
|
|
|
if (!videoRef.value) return
|
|
|
|
|
|
paused.value = false
|
|
|
- videoRef.value.play()
|
|
|
bezelTransition.value = true
|
|
|
+
|
|
|
+ // ① 先释放其他所有 video 的 src(解码器互斥)
|
|
|
+ manager.releaseAllOthers(videoRef.value)
|
|
|
+
|
|
|
+ // ② 如果自己还没挂 src,手动挂上去(Vue template 里不绑定 src,完全手动管理)
|
|
|
+ if (!videoRef.value.src) {
|
|
|
+ videoRef.value.muted = true // 绕过 iOS 自动播放策略
|
|
|
+ videoRef.value.setAttribute('preload', 'auto')
|
|
|
+ videoRef.value.src = props.src
|
|
|
+ videoRef.value.load()
|
|
|
+ }
|
|
|
+
|
|
|
+ // ③ play() 必须在用户手势任务内
|
|
|
+ const p = videoRef.value.play()
|
|
|
+ if (p && (p as Promise<void>).catch) (p as Promise<void>).catch(() => {})
|
|
|
}
|
|
|
|
|
|
const pause = () => {
|
|
|
if (!videoRef.value) return
|
|
|
|
|
|
paused.value = true
|
|
|
- videoRef.value.pause()
|
|
|
+ try {
|
|
|
+ videoRef.value.pause()
|
|
|
+ }
|
|
|
+ catch {}
|
|
|
bezelTransition.value = true
|
|
|
}
|
|
|
|
|
|
@@ -235,11 +268,26 @@ const speed = (rate: number) => {
|
|
|
}
|
|
|
|
|
|
const handleDurationchange = () => {
|
|
|
- duration.value = videoRef.value?.duration || 0
|
|
|
+ const v = videoRef.value
|
|
|
+ if (!v) return
|
|
|
+ // release 后 video 没 src、duration=0,回退到 manager 缓存
|
|
|
+ if (!v.src || !isFinite(v.duration)) {
|
|
|
+ duration.value = manager.getMeta(v).duration || 0
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ duration.value = v.duration
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const handleTimeupdate = () => {
|
|
|
- currentTime.value = videoRef.value?.currentTime || 0
|
|
|
+ const v = videoRef.value
|
|
|
+ if (!v) return
|
|
|
+ if (!v.src) {
|
|
|
+ currentTime.value = manager.getMeta(v).currentTime || 0
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ currentTime.value = v.currentTime || 0
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const handleEnded = () => {
|
|
|
@@ -251,7 +299,13 @@ const handleEnded = () => {
|
|
|
}
|
|
|
|
|
|
const handleProgress = () => {
|
|
|
- loaded.value = videoRef.value?.buffered.length ? videoRef.value.buffered.end(videoRef.value.buffered.length - 1) : 0
|
|
|
+ const v = videoRef.value
|
|
|
+ if (!v || !v.src) {
|
|
|
+ loaded.value = 0
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ loaded.value = v.buffered.length ? v.buffered.end(v.buffered.length - 1) : 0
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const loadError = ref(false)
|
|
|
@@ -363,15 +417,31 @@ const autoHideController = () => {
|
|
|
|
|
|
const bgCanvasRef = useTemplateRef<HTMLCanvasElement>('bgCanvasRef')
|
|
|
onMounted(() => {
|
|
|
- if (!bgCanvasRef.value || !videoRef.value) return
|
|
|
- const ctx = bgCanvasRef.value.getContext('2d')
|
|
|
- if (!ctx) return
|
|
|
-
|
|
|
- videoRef.value.addEventListener('loadedmetadata', () => {
|
|
|
- videoRef.value!.requestVideoFrameCallback(() => {
|
|
|
- ctx.drawImage(videoRef.value!, 0, 0, bgCanvasRef.value!.width, bgCanvasRef.value!.height)
|
|
|
- })
|
|
|
- }, { once: true })
|
|
|
+ if (!videoRef.value) return
|
|
|
+
|
|
|
+ // 注册到 manager:track meta + 支持 release
|
|
|
+ manager.register(videoRef.value)
|
|
|
+
|
|
|
+ if (bgCanvasRef.value) {
|
|
|
+ const ctx = bgCanvasRef.value.getContext('2d')
|
|
|
+ if (ctx) {
|
|
|
+ videoRef.value.addEventListener('loadedmetadata', () => {
|
|
|
+ videoRef.value!.requestVideoFrameCallback(() => {
|
|
|
+ ctx.drawImage(videoRef.value!, 0, 0, bgCanvasRef.value!.width, bgCanvasRef.value!.height)
|
|
|
+ })
|
|
|
+ }, { once: true })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // autoplay:走 play() 流程 → releaseOthers → 挂 src → play
|
|
|
+ // iOS 自动播放要求 muted,play() 里已经处理了
|
|
|
+ if (props.autoplay) {
|
|
|
+ play()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ if (videoRef.value) manager.unregister(videoRef.value)
|
|
|
})
|
|
|
|
|
|
useMSE(props.src, videoRef)
|