-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreact.ts
More file actions
139 lines (131 loc) · 4.1 KB
/
Copy pathreact.ts
File metadata and controls
139 lines (131 loc) · 4.1 KB
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
132
133
134
135
136
137
138
139
/**
* react.ts - copyable React hooks over the ac3go wasm module. Zero
* dependencies beyond react and ./ac3go. See docs/wasm.md.
*/
import { useEffect, useRef, useState } from 'react'
import {
loadAc3Go,
decodeToAudioBuffer,
type Ac3GoApi,
type Ac3Input,
type DecodeOptions,
type LoadOptions,
type ProbeResult,
} from './ac3go'
/** Load the wasm module once; null until ready. */
export function useAc3Go(options: LoadOptions): Ac3GoApi | null {
const [api, setApi] = useState<Ac3GoApi | null>(null)
const opts = useRef(options)
useEffect(() => {
let live = true
loadAc3Go(opts.current).then((m) => live && setApi(m))
return () => {
live = false
}
}, [])
return api
}
/** Probe an input's first frame; re-probes when the input changes. */
export function useProbe(ac3: Ac3GoApi | null, input: Ac3Input | null) {
const [probe, setProbe] = useState<ProbeResult | null>(null)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setProbe(null)
setError(null)
if (!ac3 || !input) return
let live = true
ac3
.probe(input)
.then((p) => live && setProbe(p))
.catch((e) => live && setError(e))
return () => {
live = false
}
}, [ac3, input])
return { probe, error }
}
/**
* Decode an input to a Web Audio AudioBuffer, ready to play. Re-decodes when
* the input or the options change; an AudioContext is created lazily and
* reused. Pass options to downmix (`{ downmix: 'stereo' }`) or to switch off
* the decoder's noise (`{ dither: false }`).
*/
export function useAc3Buffer(ac3: Ac3GoApi | null, input: Ac3Input | null, options?: DecodeOptions) {
const [buffer, setBuffer] = useState<AudioBuffer | null>(null)
const [error, setError] = useState<Error | null>(null)
const ctxRef = useRef<AudioContext | null>(null)
const optsKey = JSON.stringify(options ?? {})
useEffect(() => {
setBuffer(null)
setError(null)
if (!ac3 || !input) return
let live = true
const ctx = (ctxRef.current ??= new AudioContext())
ac3
.decode(input, options)
.then((pcm) => live && setBuffer(decodeToAudioBuffer(pcm, ctx)))
.catch((e) => live && setError(e))
return () => {
live = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ac3, input, optsKey])
return { buffer, error }
}
/**
* Play a decoded input through Web Audio, optionally holding it in sync with a
* <video> element's clock (the video carries the picture, ac3go the sound).
* Returns `play`/`stop` and the current drift in milliseconds so a UI can show
* how tight the sync is. Cleans up its audio graph on unmount.
*/
export function useAc3Player(
ac3: Ac3GoApi | null,
input: Ac3Input | null,
video?: React.RefObject<HTMLVideoElement | null>,
options?: DecodeOptions,
) {
const { buffer, error } = useAc3Buffer(ac3, input, options)
const [drift, setDrift] = useState(0)
const ctxRef = useRef<AudioContext | null>(null)
const srcRef = useRef<AudioBufferSourceNode | null>(null)
const rafRef = useRef(0)
useEffect(
() => () => {
cancelAnimationFrame(rafRef.current)
srcRef.current?.stop()
srcRef.current?.disconnect()
},
[],
)
function stop() {
cancelAnimationFrame(rafRef.current)
srcRef.current?.stop()
srcRef.current = null
}
async function play() {
if (!buffer) return
stop()
const ctx = (ctxRef.current ??= new AudioContext())
await ctx.resume()
const src = ctx.createBufferSource()
src.buffer = buffer
src.connect(ctx.destination)
const startCtx = ctx.currentTime + 0.1
const el = video?.current
if (el) {
el.currentTime = 0
await el.play().catch(() => {})
}
src.start(startCtx)
srcRef.current = src
const tick = () => {
if (!srcRef.current) return
const audioTime = ctx.currentTime - startCtx
const videoTime = el ? el.currentTime : audioTime
setDrift((audioTime - videoTime) * 1000)
rafRef.current = requestAnimationFrame(tick)
}
rafRef.current = requestAnimationFrame(tick)
}
return { play, stop, drift, ready: buffer != null, error }
}