OverviewFilesCommitsIssuesDocsReleasesPeers

keet-deobfuscated / @holepunchto / broadcast-call / index.js

275 lines · javascript ref:
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))
  }
}