{"version":3,"sources":["../src/audio_source.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type {\n  AudioSourceInfo,\n  CaptureAudioFrameCallback,\n  CaptureAudioFrameResponse,\n  ClearAudioBufferResponse,\n  NewAudioSourceResponse,\n} from '@livekit/rtc-ffi-bindings';\nimport {\n  AudioSourceType,\n  CaptureAudioFrameRequest,\n  ClearAudioBufferRequest,\n  FfiHandle,\n  NewAudioSourceRequest,\n} from '@livekit/rtc-ffi-bindings';\nimport type { AudioFrame } from './audio_frame.js';\nimport { FfiClient } from './ffi_client.js';\n\nexport class AudioSource {\n  /** @internal */\n  info: AudioSourceInfo;\n  /** @internal */\n  ffiHandle: FfiHandle;\n  /** @internal */\n  lastCapture: number;\n  /** @internal */\n  currentQueueSize: number;\n  /** @internal */\n  promise?: Promise<void> = undefined;\n  /** @internal */\n  resolvePromise?: () => void = undefined;\n  /** @internal */\n  timeout?: ReturnType<typeof setTimeout> = undefined;\n  /** @internal */\n  closed = false;\n\n  sampleRate: number;\n  numChannels: number;\n  queueSize: number;\n\n  constructor(sampleRate: number, numChannels: number, queueSize = 1000) {\n    this.sampleRate = sampleRate;\n    this.numChannels = numChannels;\n    this.queueSize = queueSize;\n\n    this.lastCapture = 0;\n    this.currentQueueSize = 0;\n\n    const req = new NewAudioSourceRequest({\n      type: AudioSourceType.AUDIO_SOURCE_NATIVE,\n      sampleRate: sampleRate,\n      numChannels: numChannels,\n      queueSizeMs: queueSize,\n    });\n\n    const res = FfiClient.instance.request<NewAudioSourceResponse>({\n      message: {\n        case: 'newAudioSource',\n        value: req,\n      },\n    });\n\n    this.info = res.source!.info!;\n    this.ffiHandle = new FfiHandle(res.source!.handle!.id!);\n  }\n\n  get queuedDuration(): number {\n    return Math.max(\n      this.currentQueueSize - Number(process.hrtime.bigint() / BigInt(1000000)) + this.lastCapture,\n      0,\n    );\n  }\n\n  clearQueue() {\n    const req = new ClearAudioBufferRequest({\n      sourceHandle: this.ffiHandle.handle,\n    });\n\n    FfiClient.instance.request<ClearAudioBufferResponse>({\n      message: {\n        case: 'clearAudioBuffer',\n        value: req,\n      },\n    });\n\n    this.releaseWaiter();\n  }\n\n  /**\n   * Resolve the pending waitForPlayout() promise (if any) and reset the queue\n   * bookkeeping. Mirrors python-sdks' AudioSource._release_waiter: the promise is\n   * discarded here and lazily re-created by the next captureFrame, so a later\n   * waitForPlayout() can never consume a stale resolution and report playout\n   * complete while audio is still queued.\n   * @internal\n   */\n  releaseWaiter = () => {\n    if (!this.promise) {\n      return;\n    }\n\n    this.resolvePromise?.();\n    this.lastCapture = 0;\n    this.currentQueueSize = 0;\n    this.promise = undefined;\n    this.resolvePromise = undefined;\n    // cancel the drain timer (e.g. when released early by clearQueue), otherwise\n    // it would fire later and release the waiter of a subsequent segment\n    if (this.timeout) {\n      clearTimeout(this.timeout);\n      this.timeout = undefined;\n    }\n  };\n\n  async waitForPlayout() {\n    if (!this.promise) {\n      return;\n    }\n\n    await this.promise;\n  }\n\n  async captureFrame(frame: AudioFrame) {\n    if (this.closed) {\n      throw new Error('AudioSource is closed');\n    }\n\n    if (frame.samplesPerChannel === 0) {\n      return;\n    }\n\n    const now = Number(process.hrtime.bigint() / BigInt(1000000));\n    const elapsed = this.lastCapture === 0 ? 0 : now - this.lastCapture;\n    const frameDurationMs = (frame.samplesPerChannel / frame.sampleRate) * 1000;\n    this.currentQueueSize = Math.max(this.currentQueueSize - elapsed, 0) + frameDurationMs;\n\n    this.lastCapture = now;\n\n    if (this.timeout) {\n      clearTimeout(this.timeout);\n    }\n\n    if (!this.promise) {\n      this.promise = new Promise<void>((resolve) => {\n        this.resolvePromise = resolve;\n      });\n    }\n\n    this.timeout = setTimeout(this.releaseWaiter, this.currentQueueSize);\n\n    const req = new CaptureAudioFrameRequest({\n      sourceHandle: this.ffiHandle.handle,\n      buffer: frame.protoInfo(),\n    });\n\n    const res = FfiClient.instance.request<CaptureAudioFrameResponse>({\n      message: { case: 'captureAudioFrame', value: req },\n    });\n\n    const cb = await FfiClient.instance.waitFor<CaptureAudioFrameCallback>((ev) => {\n      return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId;\n    });\n\n    if (cb.error) {\n      throw new Error(cb.error);\n    }\n  }\n\n  async close() {\n    // Clear any pending playout timeout so its callback doesn't fire after\n    // the handle is disposed, which would reference freed native state.\n    if (this.timeout) {\n      clearTimeout(this.timeout);\n      this.timeout = undefined;\n    }\n    // Resolve any pending waitForPlayout() promise so callers don't hang.\n    this.releaseWaiter();\n    this.ffiHandle.dispose();\n    this.closed = true;\n  }\n}\n"],"mappings":"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","names":[]}