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 }
}