import e from "bare-rpc"
import { RPCCommands as t } from "./rpc-commands"
export class Call {
#e
#t
#o
#i
#r
#s
#a
#d
#n
#h
#c
#u
#m
#l
#p
#S
#T
#f
#k
#w
#C = 0
#v = 0
constructor({
workerLink: e,
spawn: t,
videoChromium: o,
onMetadata: i,
onStats: r,
onFrame: s,
onStop: a,
onState: d,
}) {
;((this.#e = e || "./node_modules/@holepunchto/broadcast-call/workers"),
(this.#t = t),
(this.#o = o),
(this.#r = new TextDecoder()),
(this.#l = new Set()),
(this.#p = new Set()),
(this.#S = i),
(this.#T = r),
(this.#f = s),
(this.#k = a),
(this.#w = d))
}
isCallStarted() {
return !!this.#s
}
async startCall(o, { chromium: i, live: r, codecs: s } = {}) {
if (((s = s || "av01.0.05M.08, opus"), this.isCallStarted())) return
if (!o) {
const t = await this.#t(`${this.#e}/write.js`)
return void (this.#s = new e(t.IPC))
}
if (i) {
const i = new MediaSource()
return (
(this.#o.src = URL.createObjectURL(i)),
void i.addEventListener("sourceopen", async () => {
if (i.sourceBuffers.length > 0) return
const a = `video/webm; codecs="${s}"`
if (!MediaSource.isTypeSupported(a))
throw new Error(`MIME type or codec not supported: ${a}`)
const d = i.addSourceBuffer(a),
n = []
;(this.#S?.({}),
this.#o.play().catch((e) => {
console.error(e)
}))
const h = () => {
if (this.#o.error) throw this.#o.error
if (d.updating) return
if (0 === n.length) return
const e = n.shift()
try {
d.appendBuffer(e)
} catch (t) {
if ("QuotaExceededError" !== t.name) throw t
{
n.unshift(e)
const t = d.buffered.start(0),
o = this.#o.currentTime - 10
if (o <= t) return
d.remove(t, o)
}
}
}
let c = !1
d.addEventListener("updateend", () => {
if ((h(), !c && d.buffered.length > 0)) {
const e = d.buffered.start(0)
;((this.#o.currentTime = e), (c = !0))
}
})
const u = await this.#t(`${this.#e}/chromium.js`)
this.#s = new e(u.IPC, async (e) => {
switch (e.command) {
case t.VIDEO_CHROMIUM:
;(n.push(e.data), h())
break
case t.STATS:
const o = this.#r.decode(e.data)
this.#T?.(o)
break
case t.STATE:
const i = this.#r.decode(e.data)
this.#w?.({ state: i })
break
default:
throw new Error(`Unexpected command: ${e.command}`)
}
})
const m = this.#s.request(t.START),
l = { key: o, live: r }
m.send(JSON.stringify(l))
})
)
}
let a, d
const n = await this.#t(`${this.#e}/read.js`)
this.#s = new e(n.IPC, (e) => {
switch (e.command) {
case t.METADATA:
const o = this.#r.decode(e.data),
i = JSON.parse(o),
{ width: r, height: s, sampleRate: n, videoTimeBase: h, audioTimeBase: c } = i
;((this.#a = n), (this.#u = h), (this.#m = c), this.#S?.({ width: r, height: s }))
break
case t.STATS:
const u = this.#r.decode(e.data)
this.#T?.(u)
break
case t.VIDEO_PTS:
const m = this.#r.decode(e.data)
a = Number(m)
break
case t.VIDEO:
;(this.#y(e.data, a), (a = void 0), e.reply())
break
case t.AUDIO_PTS:
const l = this.#r.decode(e.data)
d = Number(l)
break
case t.AUDIO:
;(this.#b(e.data, d), (d = void 0), e.reply())
break
default:
throw new Error(`Unexpected command: ${e.command}`)
}
})
const h = this.#s.request(t.START),
c = { key: o, live: r }
;(h.send(JSON.stringify(c)), (this.#n = new AudioContext()))
}
stopCall() {
if (this.isCallStarted()) {
if ((this.#s.request(t.STOP).send(), (this.#s = null), this.#o))
(this.#o.pause(), (this.#o.currentTime = 0))
else {
for (const e of this.#l) clearTimeout(e)
this.#l.clear()
for (const e of this.#p) e.stop()
;(this.#p.clear(), (this.#n = null), (this.#d = null))
}
this.#k()
}
}
destroy() {
this.isCallStarted() && this.stopCall()
}
#x(e, t = 0) {
if (!this.#n) return
const o = new DataView(e.buffer, e.byteOffset, e.byteLength),
i = new Float32Array(e.length)
for (let e = 0; e < i.length; e++) i[e] = (2 * o.getUint8(e)) / 255 - 1
const r = i.length / 1,
s = this.#n.createBuffer(1, r, this.#a)
for (let e = 0; e < 1; e++) {
const t = s.getChannelData(e)
for (let o = 0; o < r; o++) t[o] = i[1 * o + e]
}
const a = this.#n.createBufferSource()
;((a.buffer = s),
a.connect(this.#n.destination),
a.start(this.#n.currentTime + t),
(a.onended = () => {
this.#p.delete(a)
}),
this.#p.add(a))
}
#y(e, t) {
if (!this.#n) return
const o = (t * this.#u.numerator) / this.#u.denominator,
i = this.#n.currentTime
void 0 === this.#c && (this.#c = i - o)
let r = this.#c - i + o
;(r < 0 && this.#C < -r && (this.#C = -r), (r += this.#C))
const s = setTimeout(() => {
;(this.#f(e), this.#l.delete(s))
}, 1e3 * r)
this.#l.add(s)
}
#b(e, t) {
if (!this.#n) return
const o = (t * this.#m.numerator) / this.#m.denominator,
i = this.#n.currentTime
void 0 === this.#h && (this.#h = i - o)
let r = this.#h - i + o
;(r < 0 && this.#v < -r && (this.#v = -r), (r += this.#v), this.#x(e, r))
}
}
export class Record {
#e
#A
#r
#s
#D
#k
#E
#t
#L
setStreamController(e) {
this.#L = e
}
constructor({ workerLink: e, demuxWorkerLink: t, onStart: o, onStop: i }) {
;((this.#e = e || "./node_modules/@holepunchto/broadcast-call/workers"),
(this.#A = t || "./workers/node_modules/@holepunchto/vcr-core/util/workers"),
(this.#r = new TextDecoder()),
(this.#D = o),
(this.#k = i))
}
isCallStarted() {
return !!this.#s
}
async start(o = { fragmentDuration: 1e3 }) {
if (this.isCallStarted()) return
const i = await this.#t(`${this.#e}/obs.js`)
this.#s = new e(i.IPC, async (e) => {
switch (e.command) {
case t.METADATA:
const o = this.#r.decode(e.data),
i = JSON.parse(o)
this.#D?.(i)
break
case t.STATE:
const r = this.#r.decode(e.data)
;(this.#k?.(r), this.#s.request(t.STOP).send())
break
case t.VIDEO_CHROMIUM:
this.#L && this.#L.enqueue(e.data)
break
case t.EXPORT_COMPLETE:
this.#L && (this.#L.close(), (this.#L = null))
break
default:
throw new Error(`Unexpected command: ${e.command}`)
}
})
const r = this.#s.request(t.START),
s = { demuxWorkerLink: this.#A, fragmentDuration: o.fragmentDuration }
r.send(JSON.stringify(s))
}
async stop() {
this.isCallStarted() && (this.#s.request(t.STOP).send(), this.#k?.())
}
async saveRecording(e) {
;((this.#E = e), this.#s.request(t.VIDEO).send())
}
async destroy() {
this.isCallStarted() && (this.#s.request(t.CLEANUP).send(), (this.#s = null))
}
}