File size: 3,872 Bytes
7312c42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | import { useEffect, useRef, useState } from 'react'
export interface SamplingPreviewFrame {
bitmap: ImageBitmap
durationMs: number
}
export function useSamplingPreviewFrames(
blob: Blob | null,
revision: string | undefined,
enabled = true,
) {
const [frames, setFrames] = useState<SamplingPreviewFrame[]>([])
const activeFrames = useRef<SamplingPreviewFrame[]>([])
useEffect(() => {
let disposed = false
let committed = false
let decoder: ImageDecoder | null = null
const decodedFrames: SamplingPreviewFrame[] = []
const replaceFrames = (nextFrames: SamplingPreviewFrame[]) => {
const previousFrames = activeFrames.current
activeFrames.current = nextFrames
setFrames(nextFrames)
previousFrames.forEach(({ bitmap }) => bitmap.close())
}
async function decodeFrames() {
if (!enabled || !blob || blob.type !== 'image/webp' || typeof ImageDecoder === 'undefined') {
if (!disposed) replaceFrames([])
return
}
const supported = await ImageDecoder.isTypeSupported(blob.type)
if (disposed) return
if (!supported) {
replaceFrames([])
return
}
const data = await blob.arrayBuffer()
if (disposed) return
decoder = new ImageDecoder({ data, type: blob.type, preferAnimation: true })
await decoder.tracks.ready
const frameCount = decoder.tracks.selectedTrack?.frameCount ?? 0
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
const result = await decoder.decode({ frameIndex, completeFramesOnly: true })
const durationMs = Math.max(1, (result.image.duration ?? 0) / 1000)
let bitmap: ImageBitmap
try {
bitmap = await createImageBitmap(result.image)
} finally {
result.image.close()
}
if (disposed) {
bitmap.close()
return
}
decodedFrames.push({ bitmap, durationMs })
}
if (!disposed) {
committed = true
replaceFrames([...decodedFrames])
decoder.close()
decoder = null
}
}
void decodeFrames().catch((error: unknown) => {
if (!disposed) console.error('[SamplingPreviewWidget] failed to decode preview:', error)
})
return () => {
disposed = true
decoder?.close()
if (!committed) decodedFrames.forEach(({ bitmap }) => bitmap.close())
}
}, [blob, enabled, revision])
useEffect(() => () => {
activeFrames.current.forEach(({ bitmap }) => bitmap.close())
activeFrames.current = []
}, [])
return frames
}
export function useSamplingPreviewPlayback(
revision: string | undefined,
frameCount: number,
fps: number | null,
isPlaying: boolean,
) {
const [progress, setProgress] = useState(0)
const [frameIndex, setFrameIndex] = useState(0)
const elapsedMs = useRef(0)
useEffect(() => {
elapsedMs.current = 0
setProgress(0)
setFrameIndex(0)
}, [revision])
useEffect(() => {
const durationMs = fps && frameCount > 1 ? frameCount / fps * 1000 : 0
if (!revision || durationMs <= 0) {
setProgress(revision ? 100 : 0)
setFrameIndex(0)
return
}
if (!isPlaying) return
let animationFrame = 0
let previousTime = performance.now()
const update = (now: number) => {
elapsedMs.current = (elapsedMs.current + now - previousTime) % durationMs
previousTime = now
const nextProgress = elapsedMs.current / durationMs * 100
setProgress(nextProgress)
setFrameIndex(Math.min(frameCount - 1, Math.floor(nextProgress / 100 * frameCount)))
animationFrame = requestAnimationFrame(update)
}
animationFrame = requestAnimationFrame(update)
return () => cancelAnimationFrame(animationFrame)
}, [fps, frameCount, isPlaying, revision])
return { frameIndex, progress }
}
|