fix(website): harden LottieVideoPlayer load errors and gate videos by shouldPlay

- Wrap dynamic import('lottie-web') and loadAnimation in try/catch:
  cleanup partial anim, keep assetsReady false, log src + error,
  and emit('ready') so the fallback path completes.
- Stop autoplaying injected <video> elements on creation. Track them
  in a module-level array and play/pause from the existing shouldPlay
  watcher (currentTime=0 on pause for clean re-entry). Pause and
  reset videos when src/assetsPath changes and on unmount so inactive
  scenes stop decoding.

Amp-Thread-ID: https://ampcode.com/threads/T-019e0ce9-0296-730f-9c91-162432c4b8e6
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Yourz
2026-05-09 22:35:00 +08:00
parent b4dae62e8a
commit 54cac15610

View File

@@ -28,6 +28,7 @@ const lottieContainer = useTemplateRef<HTMLDivElement>('lottieContainer')
const assetsReady = ref(false)
const posterFaded = ref(!poster)
let anim: AnimationItem | null = null
let videos: HTMLVideoElement[] = []
let loadGen = 0
let fadeTimer: ReturnType<typeof setTimeout> | null = null
@@ -63,7 +64,8 @@ function swapImageForVideo(
fo.setAttribute('height', height)
const v = document.createElement('video')
v.src = href
v.autoplay = true
v.autoplay = false
v.preload = 'auto'
v.loop = true
v.muted = true
v.playsInline = true
@@ -93,7 +95,9 @@ function prepareAssets(container: HTMLElement): Promise<void> {
image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href') ?? ''
if (!href) continue
if (/\.(webm|mp4)$/i.test(href)) {
pending.push(whenLoaded(swapImageForVideo(image, href)))
const v = swapImageForVideo(image, href)
videos.push(v)
pending.push(whenLoaded(v))
} else {
const img = new Image()
img.src = href
@@ -107,36 +111,47 @@ watch(
[lottieContainer, () => src, () => assetsPath],
async ([container]) => {
const gen = ++loadGen
for (const v of videos) v.pause()
videos = []
anim?.destroy()
anim = null
assetsReady.value = false
if (!container) return
const { default: lottie } = await import('lottie-web')
if (gen !== loadGen) return
const created = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: false,
path: src,
assetsPath,
rendererSettings: { preserveAspectRatio: 'xMidYMid slice' }
})
anim = created
created.addEventListener('DOMLoaded', () => {
if (gen !== loadGen || anim !== created) return
created.goToAndStop(0, true)
void prepareAssets(container).then(() => {
try {
const { default: lottie } = await import('lottie-web')
if (gen !== loadGen) return
const created = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: false,
path: src,
assetsPath,
rendererSettings: { preserveAspectRatio: 'xMidYMid slice' }
})
anim = created
created.addEventListener('DOMLoaded', () => {
if (gen !== loadGen || anim !== created) return
assetsReady.value = true
created.goToAndStop(0, true)
void prepareAssets(container).then(() => {
if (gen !== loadGen || anim !== created) return
assetsReady.value = true
emit('ready')
})
})
created.addEventListener('data_failed', () => {
if (gen !== loadGen || anim !== created) return
console.error('[LottieVideoPlayer] Lottie data failed to load:', src)
emit('ready')
})
})
created.addEventListener('data_failed', () => {
if (gen !== loadGen || anim !== created) return
console.error('[LottieVideoPlayer] Lottie data failed to load:', src)
} catch (err) {
if (gen !== loadGen) return
console.error('[LottieVideoPlayer] failed to initialize:', src, err)
anim?.destroy()
anim = null
assetsReady.value = false
emit('ready')
})
}
},
{ immediate: true }
)
@@ -144,14 +159,24 @@ watch(
watch(
() => assetsReady.value && posterFaded.value && playing,
(shouldPlay) => {
if (!anim) return
if (shouldPlay) anim.goToAndPlay(0, true)
else anim.pause()
if (shouldPlay) {
anim?.goToAndPlay(0, true)
for (const v of videos) {
void v.play().catch(() => {})
}
} else {
anim?.pause()
for (const v of videos) {
v.pause()
v.currentTime = 0
}
}
}
)
onBeforeUnmount(() => {
if (fadeTimer) clearTimeout(fadeTimer)
for (const v of videos) v.pause()
anim?.destroy()
})
</script>