@hyperttp/transport-webrtc 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,14 @@ implementation. It requires an installed FFmpeg executable with `libopus` suppor
48
48
  (`ffmpegPath` can override PATH). It encodes one shared PCM input to 20 ms Opus
49
49
  frames and delivers RTP to independent Werift tracks over IPv4 loopback UDP.
50
50
  It starts consuming immediately; late subscribers join the current position.
51
+ By default FFmpeg's `-re` paces input at its nominal rate. For a live input that
52
+ is already paced, use `createPcmAudioSource(pcmStream, { realtime: false })` to
53
+ avoid this second limiter. RTP is paced using its 48 kHz timestamps, with a
54
+ bounded queue of 100 packets. An unpaced producer can overflow that queue, in
55
+ which case `audio.done` rejects; UDP itself can also drop packets before they
56
+ reach the queue. Producers remain responsible for pacing and bounded buffering.
57
+ After an input or event-loop pause, delivery resumes without a catch-up burst.
58
+ At EOF, `done` waits for queued packets to be delivered; explicit close discards them.
51
59
  Observe `audio.done` for EOF/encoder/input errors. Close the adapter when its
52
60
  playback session ends. Closing a listener stops only its track, not the source.
53
61
  It does not implement seeking, playback pause, or audio mixing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperttp/transport-webrtc",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "WebRTC transport for Hyperttp powered by Werift",
5
5
  "keywords": [
6
6
  "bun",
package/pcm-audio.d.ts CHANGED
@@ -11,13 +11,15 @@ export interface PcmAudioSource {
11
11
  }
12
12
  export interface PcmAudioOptions {
13
13
  ffmpegPath?: string;
14
+ /** Pace input with FFmpeg's -re (default: true). RTP is additionally paced when false. */
15
+ realtime?: boolean;
14
16
  signal?: AbortSignal;
15
17
  }
16
18
  /**
17
19
  * Optional server-only helper. Requires an external FFmpeg executable with libopus
18
20
  * on PATH (or ffmpegPath). Input is raw 48 kHz, stereo, signed 16-bit little-endian
19
21
  * PCM; chunk boundaries need not align to samples. One input/encoder is shared,
20
- * paced by FFmpeg's -re, with no replay for late subscribers. RTP/RTCP stays on
22
+ * paced by FFmpeg's -re by default, with no replay for late subscribers. RTP/RTCP stays on
21
23
  * IPv4 loopback. No peer connection or native wrtc dependency is created.
22
24
  *
23
25
  * Writes are bounded to 16 KiB, with one outstanding write and one input chunk.
@@ -1 +1 @@
1
- {"version":3,"file":"pcm-audio.d.ts","sourceRoot":"","sources":["../src/pcm-audio.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAA+B,MAAM,QAAQ,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,IAAI,oBAAoB,CAAC;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,uFAAuF;IACvF,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,EAC7D,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA4PzB"}
1
+ {"version":3,"file":"pcm-audio.d.ts","sourceRoot":"","sources":["../src/pcm-audio.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAA+B,MAAM,QAAQ,CAAC;AAGlE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,IAAI,oBAAoB,CAAC;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,uFAAuF;IACvF,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,EAC7D,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA2RzB"}
package/pcm-audio.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";import{spawn as _}from"node:child_process";import{createSocket as I}from"node:dgram";import{MediaStream as x,MediaStreamTrack as A,RtpPacket as R}from"werift";export async function createPcmAudioSource(w,l={}){l.signal?.throwIfAborted();const s=I("udp4");try{await new Promise((e,r)=>{s.once("error",r),s.bind(0,"127.0.0.1",()=>{s.removeListener("error",r),e()})}),l.signal?.throwIfAborted()}catch(e){try{s.close()}catch{}throw e}const g=s.address().port,o=_(l.ffmpegPath??"ffmpeg",["-hide_banner","-loglevel","error","-nostdin","-re","-f","s16le","-ar","48000","-ac","2","-i","pipe:0","-c:a","libopus","-frame_duration","20","-ar","48000","-ac","2","-payload_type","111","-f","rtp",`rtp://127.0.0.1:${g}?rtcpport=${g}&pkt_size=1200`],{stdio:["pipe","ignore","pipe"]}),u=new Set;let n=!1,k=!1,m="",h,P,y;const v=new Promise((e,r)=>{P=e,y=r});v.catch(()=>{});const d=new Set;function S(e){return new Promise((r,i)=>{const t=()=>r(void 0);d.add(t),Promise.resolve(e).then(a=>{d.delete(t),r(a)},a=>{d.delete(t),i(a)}),n&&(d.delete(t),t())})}const C=new Promise(e=>o.once("close",()=>e()));let f;function c(e){if(f)return f;n=!0;for(const r of d)r();d.clear(),l.signal?.removeEventListener("abort",b);try{Promise.resolve(h?.()).catch(()=>{})}catch{}for(const r of u)r.stop();return u.clear(),o.stdin.destroy(),o.exitCode===null&&o.signalCode===null&&o.kill("SIGTERM"),f=(async()=>{const r=setTimeout(()=>{o.kill("SIGKILL")},1e3);try{await Promise.all([C,new Promise(i=>s.close(()=>i()))])}finally{clearTimeout(r)}})(),f.then(()=>e===void 0?P():y(e),y),f}function b(){c(l.signal?.reason??new Error("PCM audio source aborted"))}s.on("error",e=>{c(e)}),o.stdin.on("error",e=>{n||c(e)}),o.stderr.on("data",e=>{m=(m+e.toString()).slice(-8192)}),o.on("error",e=>{c(e)}),o.on("close",(e,r)=>{n||(e===0&&k?c():c(new Error(`FFmpeg exited (${r??e}): ${m.trim()}`)))}),s.on("message",(e,r)=>{if(n||r.address!=="127.0.0.1"||e.length<12||e[0]>>>6!==2||(e[1]&127)!==111)return;let i;try{i=R.deSerialize(e)}catch{return}if(i.header.payloadType===111)for(const t of u)t.stopped||t.writeRtp(i)}),l.signal?.addEventListener("abort",b,{once:!0}),l.signal?.aborted&&b();try{if(await new Promise((e,r)=>{o.once("spawn",e),o.once("error",r)}),n)throw await v,new Error("PCM audio source closed during startup")}catch(e){throw await c(e),e}async function L(){let e;try{let r;if("getReader"in w){const t=w.getReader();r={next:async()=>{const a=await t.read();return a.done?{done:!0,value:void 0}:{done:!1,value:a.value}}},h=()=>t.cancel(),e=()=>t.releaseLock()}else r=w[Symbol.asyncIterator](),h=()=>r.return?.();let i=0;for(;!n;){const t=await S(r.next());if(!t||n)return;if(t.done){if(i%4!==0)throw new Error("PCM input ends with an incomplete stereo s16le sample");k=!0,o.stdin.end();return}if(!(t.value instanceof Uint8Array))throw new TypeError("PCM chunks must be Uint8Array");const a=t.value;i=(i+a.byteLength)%4;for(let p=0;p<a.byteLength&&!n;p+=16384)await S(new Promise((M,T)=>{o.stdin.write(a.subarray(p,p+16384),E=>E?T(E):M())}))}}catch(r){n||c(r)}finally{try{e?.()}catch{}}}return L(),{done:v,subscribe(){if(n)throw new Error("PCM audio source is closed");const e=new A({kind:"audio"}),r=new x;return r.addTrack(e),u.add(e),{stream:r,close(){u.delete(e),e.stop()}}},close:()=>c()}}
1
+ "use strict";import{spawn as x}from"node:child_process";import{createSocket as A}from"node:dgram";import{MediaStream as $,MediaStreamTrack as z,RtpPacket as D}from"werift";import{RtpPacer as F}from"./rtp-pacer.js";export async function createPcmAudioSource(h,c={}){c.signal?.throwIfAborted();const d=A("udp4");try{await new Promise((e,r)=>{d.once("error",r),d.bind(0,"127.0.0.1",()=>{d.removeListener("error",r),e()})}),c.signal?.throwIfAborted()}catch(e){try{d.close()}catch{}throw e}const g=d.address().port,o=x(c.ffmpegPath??"ffmpeg",["-hide_banner","-loglevel","error","-nostdin",...c.realtime===!1?[]:["-re"],"-f","s16le","-ar","48000","-ac","2","-i","pipe:0","-c:a","libopus","-frame_duration","20","-ar","48000","-ac","2","-payload_type","111","-f","rtp",`rtp://127.0.0.1:${g}?rtcpport=${g}&pkt_size=1200`],{stdio:["pipe","ignore","pipe"]}),l=new Set;let a=!1,E=!1,y="",v,S,b;const k=new Promise((e,r)=>{S=e,b=r});k.catch(()=>{});const f=new Set;function T(e){return new Promise((r,n)=>{const t=()=>r(void 0);f.add(t),Promise.resolve(e).then(s=>{f.delete(t),r(s)},s=>{f.delete(t),n(s)}),a&&(f.delete(t),t())})}const M=new Promise(e=>o.once("close",()=>e()));let u,C=!1,p;const m=c.realtime===!1?new F(e=>{for(const r of l)r.stopped||r.writeRtp(e)},e=>{i(e)},()=>{C&&p===void 0&&i()}):void 0;function i(e){if(u)return u;a=!0,p!==void 0&&clearTimeout(p),m?.stop();for(const r of f)r();f.clear(),c.signal?.removeEventListener("abort",P);try{Promise.resolve(v?.()).catch(()=>{})}catch{}for(const r of l)r.stop();return l.clear(),o.stdin.destroy(),o.exitCode===null&&o.signalCode===null&&o.kill("SIGTERM"),u=(async()=>{const r=setTimeout(()=>{o.kill("SIGKILL")},1e3);try{await Promise.all([M,new Promise(n=>d.close(()=>n()))])}finally{clearTimeout(r)}})(),u.then(()=>e===void 0?S():b(e),b),u}function P(){i(c.signal?.reason??new Error("PCM audio source aborted"))}d.on("error",e=>{i(e)}),o.stdin.on("error",e=>{a||i(e)}),o.stderr.on("data",e=>{y=(y+e.toString()).slice(-8192)}),o.on("error",e=>{i(e)}),o.on("close",(e,r)=>{a||(e===0&&E?m?(C=!0,p=setTimeout(()=>{p=void 0,m.empty&&i()},50)):i():i(new Error(`FFmpeg exited (${r??e}): ${y.trim()}`)))}),d.on("message",(e,r)=>{if(a||r.address!=="127.0.0.1"||e.length<12||e[0]>>>6!==2||(e[1]&127)!==111)return;let n;try{n=D.deSerialize(e)}catch{return}if(n.header.payloadType===111)if(m)m.push(n,n.header.timestamp);else for(const t of l)t.stopped||t.writeRtp(n)}),c.signal?.addEventListener("abort",P,{once:!0}),c.signal?.aborted&&P();try{if(await new Promise((e,r)=>{o.once("spawn",e),o.once("error",r)}),a)throw await k,new Error("PCM audio source closed during startup")}catch(e){throw await i(e),e}async function _(){let e;try{let r;if("getReader"in h){const t=h.getReader();r={next:async()=>{const s=await t.read();return s.done?{done:!0,value:void 0}:{done:!1,value:s.value}}},v=()=>t.cancel(),e=()=>t.releaseLock()}else r=h[Symbol.asyncIterator](),v=()=>r.return?.();let n=0;for(;!a;){const t=await T(r.next());if(!t||a)return;if(t.done){if(n%4!==0)throw new Error("PCM input ends with an incomplete stereo s16le sample");E=!0,o.stdin.end();return}if(!(t.value instanceof Uint8Array))throw new TypeError("PCM chunks must be Uint8Array");const s=t.value;n=(n+s.byteLength)%4;for(let w=0;w<s.byteLength&&!a;w+=16384)await T(new Promise((I,R)=>{o.stdin.write(s.subarray(w,w+16384),L=>L?R(L):I())}))}}catch(r){a||i(r)}finally{try{e?.()}catch{}}}return _(),{done:k,subscribe(){if(a)throw new Error("PCM audio source is closed");const e=new z({kind:"audio"}),r=new $;return r.addTrack(e),l.add(e),{stream:r,close(){l.delete(e),e.stop()}}},close:()=>i()}}
2
2
  //# sourceMappingURL=pcm-audio.js.map
package/pcm-audio.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/pcm-audio.ts"],
4
- "sourcesContent": ["import { spawn } from \"node:child_process\";\nimport { createSocket } from \"node:dgram\";\nimport { MediaStream, MediaStreamTrack, RtpPacket } from \"werift\";\n\nexport interface PcmAudioSubscription {\n stream: MediaStream;\n close(): void;\n}\n\nexport interface PcmAudioSource {\n subscribe(): PcmAudioSubscription;\n close(): Promise<void>;\n /** Resolves on EOF or explicit close; rejects on input, socket, or encoder failure. */\n done: Promise<void>;\n}\n\nexport interface PcmAudioOptions {\n ffmpegPath?: string;\n signal?: AbortSignal;\n}\n\n/**\n * Optional server-only helper. Requires an external FFmpeg executable with libopus\n * on PATH (or ffmpegPath). Input is raw 48 kHz, stereo, signed 16-bit little-endian\n * PCM; chunk boundaries need not align to samples. One input/encoder is shared,\n * paced by FFmpeg's -re, with no replay for late subscribers. RTP/RTCP stays on\n * IPv4 loopback. No peer connection or native wrtc dependency is created.\n *\n * Writes are bounded to 16 KiB, with one outstanding write and one input chunk.\n * Producers must bound their own chunks/queues. Closing requests iterator.return()\n * or reader.cancel(); arbitrary iterators must cooperate to release their resources\n * (a stuck next()/return() does not prevent this helper from closing).\n */\nexport async function createPcmAudioSource(\n input: AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>,\n options: PcmAudioOptions = {},\n): Promise<PcmAudioSource> {\n options.signal?.throwIfAborted();\n const socket = createSocket(\"udp4\");\n try {\n await new Promise<void>((resolve, reject) => {\n socket.once(\"error\", reject);\n socket.bind(0, \"127.0.0.1\", () => {\n socket.removeListener(\"error\", reject);\n resolve();\n });\n });\n options.signal?.throwIfAborted();\n } catch (error) {\n try {\n socket.close();\n } catch {\n /* Bind may not have succeeded. */\n }\n throw error;\n }\n\n const port = socket.address().port;\n const child = spawn(\n options.ffmpegPath ?? \"ffmpeg\",\n [\n \"-hide_banner\",\n \"-loglevel\",\n \"error\",\n \"-nostdin\",\n \"-re\",\n \"-f\",\n \"s16le\",\n \"-ar\",\n \"48000\",\n \"-ac\",\n \"2\",\n \"-i\",\n \"pipe:0\",\n \"-c:a\",\n \"libopus\",\n \"-frame_duration\",\n \"20\",\n \"-ar\",\n \"48000\",\n \"-ac\",\n \"2\",\n \"-payload_type\",\n \"111\",\n \"-f\",\n \"rtp\",\n `rtp://127.0.0.1:${port}?rtcpport=${port}&pkt_size=1200`,\n ],\n { stdio: [\"pipe\", \"ignore\", \"pipe\"] },\n );\n\n const tracks = new Set<MediaStreamTrack>();\n let stopped = false;\n let inputEnded = false;\n let stderr = \"\";\n let cancelInput: (() => unknown) | undefined;\n let resolveDone!: () => void;\n let rejectDone!: (error: unknown) => void;\n const done = new Promise<void>((resolve, reject) => {\n resolveDone = resolve;\n rejectDone = reject;\n });\n // Callers may observe done later, or only call close().\n void done.catch(() => {});\n const pending = new Set<() => void>();\n function interruptible<T>(operation: PromiseLike<T>): Promise<T | undefined> {\n return new Promise((resolve, reject) => {\n const cancel = () => resolve(undefined);\n pending.add(cancel);\n void Promise.resolve(operation).then(\n (value) => {\n pending.delete(cancel);\n resolve(value);\n },\n (error) => {\n pending.delete(cancel);\n reject(error);\n },\n );\n if (stopped) {\n pending.delete(cancel);\n cancel();\n }\n });\n }\n const childClosed = new Promise<void>((resolve) => child.once(\"close\", () => resolve()));\n let cleanup: Promise<void> | undefined;\n\n function finish(error?: unknown): Promise<void> {\n if (cleanup) return cleanup;\n stopped = true;\n for (const cancel of pending) cancel();\n pending.clear();\n options.signal?.removeEventListener(\"abort\", onAbort);\n // Do not wait for a user iterator that ignores cancellation.\n try {\n void Promise.resolve(cancelInput?.()).catch(() => {});\n } catch {\n /* Best effort. */\n }\n for (const track of tracks) track.stop();\n tracks.clear();\n child.stdin.destroy();\n if (child.exitCode === null && child.signalCode === null) child.kill(\"SIGTERM\");\n cleanup = (async () => {\n const killTimer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n }, 1_000);\n try {\n await Promise.all([\n childClosed,\n new Promise<void>((resolve) => socket.close(() => resolve())),\n ]);\n } finally {\n clearTimeout(killTimer);\n }\n })();\n void cleanup.then(() => (error === undefined ? resolveDone() : rejectDone(error)), rejectDone);\n return cleanup;\n }\n\n function onAbort() {\n void finish(options.signal?.reason ?? new Error(\"PCM audio source aborted\"));\n }\n socket.on(\"error\", (error) => {\n void finish(error);\n });\n child.stdin.on(\"error\", (error) => {\n if (!stopped) void finish(error);\n });\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr = (stderr + chunk.toString()).slice(-8_192);\n });\n child.on(\"error\", (error) => {\n void finish(error);\n });\n child.on(\"close\", (code, signal) => {\n if (stopped) return;\n if (code === 0 && inputEnded) void finish();\n else void finish(new Error(`FFmpeg exited (${signal ?? code}): ${stderr.trim()}`));\n });\n socket.on(\"message\", (message, remote) => {\n if (stopped || remote.address !== \"127.0.0.1\" || message.length < 12) return;\n // Reject RTCP and malformed/non-RTP datagrams before parsing.\n if (message[0]! >>> 6 !== 2 || (message[1]! & 0x7f) !== 111) return;\n let packet: RtpPacket;\n try {\n packet = RtpPacket.deSerialize(message);\n } catch {\n return;\n }\n if (packet.header.payloadType !== 111) return;\n for (const track of tracks) {\n if (!track.stopped) track.writeRtp(packet);\n }\n });\n options.signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (options.signal?.aborted) onAbort();\n\n // Surface executable startup failures from the factory, not just done.\n try {\n await new Promise<void>((resolve, reject) => {\n child.once(\"spawn\", resolve);\n child.once(\"error\", reject);\n });\n if (stopped) {\n await done;\n throw new Error(\"PCM audio source closed during startup\");\n }\n } catch (error) {\n await finish(error);\n throw error;\n }\n\n async function pump() {\n let release: (() => void) | undefined;\n try {\n let iterator: AsyncIterator<Uint8Array>;\n if (\"getReader\" in input) {\n const reader = input.getReader();\n iterator = {\n next: async () => {\n const result = await reader.read();\n return result.done ? { done: true, value: undefined } : { done: false, value: result.value };\n },\n };\n cancelInput = () => reader.cancel();\n release = () => reader.releaseLock();\n } else {\n iterator = input[Symbol.asyncIterator]();\n cancelInput = () => iterator.return?.();\n }\n let bytes = 0;\n while (!stopped) {\n const result = await interruptible(iterator.next());\n if (!result || stopped) return;\n if (result.done) {\n if (bytes % 4 !== 0)\n throw new Error(\"PCM input ends with an incomplete stereo s16le sample\");\n inputEnded = true;\n child.stdin.end();\n return;\n }\n if (!(result.value instanceof Uint8Array))\n throw new TypeError(\"PCM chunks must be Uint8Array\");\n const chunk = result.value;\n bytes = (bytes + chunk.byteLength) % 4;\n for (let offset = 0; offset < chunk.byteLength && !stopped; offset += 16_384) {\n await interruptible(\n new Promise<void>((resolve, reject) => {\n child.stdin.write(chunk.subarray(offset, offset + 16_384), (error) =>\n error ? reject(error) : resolve(),\n );\n }),\n );\n }\n }\n } catch (error) {\n if (!stopped) void finish(error);\n } finally {\n // A cancelled read may still be pending on a non-cooperative stream.\n try {\n release?.();\n } catch {\n /* Cancellation owns the pending read. */\n }\n }\n }\n void pump();\n\n return {\n done,\n subscribe() {\n if (stopped) throw new Error(\"PCM audio source is closed\");\n const track = new MediaStreamTrack({ kind: \"audio\" });\n const stream = new MediaStream();\n stream.addTrack(track);\n tracks.add(track);\n return {\n stream,\n close() {\n tracks.delete(track);\n track.stop();\n },\n };\n },\n close: () => finish(),\n };\n}\n"],
5
- "mappings": "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",
6
- "names": ["spawn", "createSocket", "MediaStream", "MediaStreamTrack", "RtpPacket", "input", "options", "socket", "resolve", "reject", "error", "port", "child", "tracks", "stopped", "inputEnded", "stderr", "cancelInput", "resolveDone", "rejectDone", "done", "pending", "interruptible", "operation", "cancel", "value", "childClosed", "cleanup", "finish", "onAbort", "track", "killTimer", "chunk", "code", "signal", "message", "remote", "packet", "pump", "release", "iterator", "reader", "result", "bytes", "offset", "stream"]
4
+ "sourcesContent": ["import { spawn } from \"node:child_process\";\nimport { createSocket } from \"node:dgram\";\nimport { MediaStream, MediaStreamTrack, RtpPacket } from \"werift\";\nimport { RtpPacer } from \"./rtp-pacer.js\";\n\nexport interface PcmAudioSubscription {\n stream: MediaStream;\n close(): void;\n}\n\nexport interface PcmAudioSource {\n subscribe(): PcmAudioSubscription;\n close(): Promise<void>;\n /** Resolves on EOF or explicit close; rejects on input, socket, or encoder failure. */\n done: Promise<void>;\n}\n\nexport interface PcmAudioOptions {\n ffmpegPath?: string;\n /** Pace input with FFmpeg's -re (default: true). RTP is additionally paced when false. */\n realtime?: boolean;\n signal?: AbortSignal;\n}\n\n/**\n * Optional server-only helper. Requires an external FFmpeg executable with libopus\n * on PATH (or ffmpegPath). Input is raw 48 kHz, stereo, signed 16-bit little-endian\n * PCM; chunk boundaries need not align to samples. One input/encoder is shared,\n * paced by FFmpeg's -re by default, with no replay for late subscribers. RTP/RTCP stays on\n * IPv4 loopback. No peer connection or native wrtc dependency is created.\n *\n * Writes are bounded to 16 KiB, with one outstanding write and one input chunk.\n * Producers must bound their own chunks/queues. Closing requests iterator.return()\n * or reader.cancel(); arbitrary iterators must cooperate to release their resources\n * (a stuck next()/return() does not prevent this helper from closing).\n */\nexport async function createPcmAudioSource(\n input: AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>,\n options: PcmAudioOptions = {},\n): Promise<PcmAudioSource> {\n options.signal?.throwIfAborted();\n const socket = createSocket(\"udp4\");\n try {\n await new Promise<void>((resolve, reject) => {\n socket.once(\"error\", reject);\n socket.bind(0, \"127.0.0.1\", () => {\n socket.removeListener(\"error\", reject);\n resolve();\n });\n });\n options.signal?.throwIfAborted();\n } catch (error) {\n try {\n socket.close();\n } catch {\n /* Bind may not have succeeded. */\n }\n throw error;\n }\n\n const port = socket.address().port;\n const child = spawn(\n options.ffmpegPath ?? \"ffmpeg\",\n [\n \"-hide_banner\",\n \"-loglevel\",\n \"error\",\n \"-nostdin\",\n ...(options.realtime === false ? [] : [\"-re\"]),\n \"-f\",\n \"s16le\",\n \"-ar\",\n \"48000\",\n \"-ac\",\n \"2\",\n \"-i\",\n \"pipe:0\",\n \"-c:a\",\n \"libopus\",\n \"-frame_duration\",\n \"20\",\n \"-ar\",\n \"48000\",\n \"-ac\",\n \"2\",\n \"-payload_type\",\n \"111\",\n \"-f\",\n \"rtp\",\n `rtp://127.0.0.1:${port}?rtcpport=${port}&pkt_size=1200`,\n ],\n { stdio: [\"pipe\", \"ignore\", \"pipe\"] },\n );\n\n const tracks = new Set<MediaStreamTrack>();\n let stopped = false;\n let inputEnded = false;\n let stderr = \"\";\n let cancelInput: (() => unknown) | undefined;\n let resolveDone!: () => void;\n let rejectDone!: (error: unknown) => void;\n const done = new Promise<void>((resolve, reject) => {\n resolveDone = resolve;\n rejectDone = reject;\n });\n // Callers may observe done later, or only call close().\n void done.catch(() => {});\n const pending = new Set<() => void>();\n function interruptible<T>(operation: PromiseLike<T>): Promise<T | undefined> {\n return new Promise((resolve, reject) => {\n const cancel = () => resolve(undefined);\n pending.add(cancel);\n void Promise.resolve(operation).then(\n (value) => {\n pending.delete(cancel);\n resolve(value);\n },\n (error) => {\n pending.delete(cancel);\n reject(error);\n },\n );\n if (stopped) {\n pending.delete(cancel);\n cancel();\n }\n });\n }\n const childClosed = new Promise<void>((resolve) => child.once(\"close\", () => resolve()));\n let cleanup: Promise<void> | undefined;\n let encoderEnded = false;\n let settleTimer: ReturnType<typeof setTimeout> | undefined;\n const pacer =\n options.realtime === false\n ? new RtpPacer<RtpPacket>(\n (packet) => {\n for (const track of tracks) if (!track.stopped) track.writeRtp(packet);\n },\n (error) => {\n void finish(error);\n },\n () => {\n if (encoderEnded && settleTimer === undefined) void finish();\n },\n )\n : undefined;\n\n function finish(error?: unknown): Promise<void> {\n if (cleanup) return cleanup;\n stopped = true;\n if (settleTimer !== undefined) clearTimeout(settleTimer);\n pacer?.stop();\n for (const cancel of pending) cancel();\n pending.clear();\n options.signal?.removeEventListener(\"abort\", onAbort);\n // Do not wait for a user iterator that ignores cancellation.\n try {\n void Promise.resolve(cancelInput?.()).catch(() => {});\n } catch {\n /* Best effort. */\n }\n for (const track of tracks) track.stop();\n tracks.clear();\n child.stdin.destroy();\n if (child.exitCode === null && child.signalCode === null) child.kill(\"SIGTERM\");\n cleanup = (async () => {\n const killTimer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n }, 1_000);\n try {\n await Promise.all([\n childClosed,\n new Promise<void>((resolve) => socket.close(() => resolve())),\n ]);\n } finally {\n clearTimeout(killTimer);\n }\n })();\n void cleanup.then(() => (error === undefined ? resolveDone() : rejectDone(error)), rejectDone);\n return cleanup;\n }\n\n function onAbort() {\n void finish(options.signal?.reason ?? new Error(\"PCM audio source aborted\"));\n }\n socket.on(\"error\", (error) => {\n void finish(error);\n });\n child.stdin.on(\"error\", (error) => {\n if (!stopped) void finish(error);\n });\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr = (stderr + chunk.toString()).slice(-8_192);\n });\n child.on(\"error\", (error) => {\n void finish(error);\n });\n child.on(\"close\", (code, signal) => {\n if (stopped) return;\n if (code === 0 && inputEnded) {\n if (!pacer) void finish();\n else {\n encoderEnded = true;\n // UDP delivery can lag behind the child process close event.\n settleTimer = setTimeout(() => {\n settleTimer = undefined;\n if (pacer.empty) void finish();\n }, 50);\n }\n } else void finish(new Error(`FFmpeg exited (${signal ?? code}): ${stderr.trim()}`));\n });\n socket.on(\"message\", (message, remote) => {\n if (stopped || remote.address !== \"127.0.0.1\" || message.length < 12) return;\n // Reject RTCP and malformed/non-RTP datagrams before parsing.\n if (message[0]! >>> 6 !== 2 || (message[1]! & 0x7f) !== 111) return;\n let packet: RtpPacket;\n try {\n packet = RtpPacket.deSerialize(message);\n } catch {\n return;\n }\n if (packet.header.payloadType !== 111) return;\n if (pacer) pacer.push(packet, packet.header.timestamp);\n else\n for (const track of tracks) {\n if (!track.stopped) track.writeRtp(packet);\n }\n });\n options.signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (options.signal?.aborted) onAbort();\n\n // Surface executable startup failures from the factory, not just done.\n try {\n await new Promise<void>((resolve, reject) => {\n child.once(\"spawn\", resolve);\n child.once(\"error\", reject);\n });\n if (stopped) {\n await done;\n throw new Error(\"PCM audio source closed during startup\");\n }\n } catch (error) {\n await finish(error);\n throw error;\n }\n\n async function pump() {\n let release: (() => void) | undefined;\n try {\n let iterator: AsyncIterator<Uint8Array>;\n if (\"getReader\" in input) {\n const reader = input.getReader();\n iterator = {\n next: async () => {\n const result = await reader.read();\n return result.done\n ? { done: true, value: undefined }\n : { done: false, value: result.value };\n },\n };\n cancelInput = () => reader.cancel();\n release = () => reader.releaseLock();\n } else {\n iterator = input[Symbol.asyncIterator]();\n cancelInput = () => iterator.return?.();\n }\n let bytes = 0;\n while (!stopped) {\n const result = await interruptible(iterator.next());\n if (!result || stopped) return;\n if (result.done) {\n if (bytes % 4 !== 0)\n throw new Error(\"PCM input ends with an incomplete stereo s16le sample\");\n inputEnded = true;\n child.stdin.end();\n return;\n }\n if (!(result.value instanceof Uint8Array))\n throw new TypeError(\"PCM chunks must be Uint8Array\");\n const chunk = result.value;\n bytes = (bytes + chunk.byteLength) % 4;\n for (let offset = 0; offset < chunk.byteLength && !stopped; offset += 16_384) {\n await interruptible(\n new Promise<void>((resolve, reject) => {\n child.stdin.write(chunk.subarray(offset, offset + 16_384), (error) =>\n error ? reject(error) : resolve(),\n );\n }),\n );\n }\n }\n } catch (error) {\n if (!stopped) void finish(error);\n } finally {\n // A cancelled read may still be pending on a non-cooperative stream.\n try {\n release?.();\n } catch {\n /* Cancellation owns the pending read. */\n }\n }\n }\n void pump();\n\n return {\n done,\n subscribe() {\n if (stopped) throw new Error(\"PCM audio source is closed\");\n const track = new MediaStreamTrack({ kind: \"audio\" });\n const stream = new MediaStream();\n stream.addTrack(track);\n tracks.add(track);\n return {\n stream,\n close() {\n tracks.delete(track);\n track.stop();\n },\n };\n },\n close: () => finish(),\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["spawn", "createSocket", "MediaStream", "MediaStreamTrack", "RtpPacket", "RtpPacer", "input", "options", "socket", "resolve", "reject", "error", "port", "child", "tracks", "stopped", "inputEnded", "stderr", "cancelInput", "resolveDone", "rejectDone", "done", "pending", "interruptible", "operation", "cancel", "value", "childClosed", "cleanup", "encoderEnded", "settleTimer", "pacer", "packet", "track", "finish", "onAbort", "killTimer", "chunk", "code", "signal", "message", "remote", "pump", "release", "iterator", "reader", "result", "bytes", "offset", "stream"]
7
7
  }
package/rtp-pacer.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** Bounded, monotonic RTP playout scheduler for a single 48 kHz stream. */
2
+ export declare class RtpPacer<T> {
3
+ private readonly send;
4
+ private readonly fail;
5
+ private readonly drained;
6
+ private readonly maxPackets;
7
+ private queue;
8
+ private timer;
9
+ private lastTimestamp;
10
+ private lastDeadline;
11
+ private lastSentAt;
12
+ private closed;
13
+ private wasDrained;
14
+ constructor(send: (packet: T) => void, fail: (error: Error) => void, drained: () => void, maxPackets?: number);
15
+ push(packet: T, timestamp: number): void;
16
+ stop(): void;
17
+ get empty(): boolean;
18
+ private schedule;
19
+ }
20
+ //# sourceMappingURL=rtp-pacer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtp-pacer.d.ts","sourceRoot":"","sources":["../src/rtp-pacer.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,qBAAa,QAAQ,CAAC,CAAC;IAUnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAZ7B,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAE3B,YACmB,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,EACzB,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAC5B,OAAO,EAAE,MAAM,IAAI,EACnB,UAAU,SAAM,EAC/B;IAEJ,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAQvC;IAED,IAAI,IAAI,IAAI,CAKX;IAED,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,OAAO,CAAC,QAAQ;CAsCjB"}
package/rtp-pacer.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";export class RtpPacer{send;fail;drained;maxPackets;queue=[];timer;lastTimestamp;lastDeadline=0;lastSentAt=0;closed=!1;wasDrained=!1;constructor(i,e,t,s=100){this.send=i,this.fail=e,this.drained=t,this.maxPackets=s}push(i,e){if(!this.closed){if(this.queue.length>=this.maxPackets){this.fail(new Error("PCM RTP pacing queue overflow"));return}this.queue.push({packet:i,timestamp:e}),this.schedule()}}stop(){this.closed=!0,this.timer!==void 0&&clearTimeout(this.timer),this.timer=void 0,this.queue=[]}get empty(){return this.queue.length===0}schedule(){if(this.closed||this.timer!==void 0||this.queue.length===0)return;const i=this.queue[0],e=this.lastTimestamp===void 0?0:i.timestamp-this.lastTimestamp|0;if(e<0||e>48e4){this.fail(new Error("Invalid PCM RTP timestamp discontinuity"));return}const t=performance.now(),s=e/48;let a=this.lastTimestamp===void 0?t:this.lastDeadline+s;this.queue.length===1&&this.timer===void 0&&this.wasDrained&&a<t&&(a=t),this.wasDrained=!1;const h=this.lastTimestamp===void 0?t:this.lastSentAt+s*.9,r=Math.max(0,Math.ceil(Math.max(a,h)-t));this.timer=setTimeout(()=>{if(this.timer=void 0,this.closed)return;const n=this.queue.shift();this.lastTimestamp=n.timestamp,this.lastDeadline=a,this.lastSentAt=performance.now(),this.send(n.packet),!this.closed&&(this.queue.length===0?(this.wasDrained=!0,this.drained()):this.schedule())},r)}}
2
+ //# sourceMappingURL=rtp-pacer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/rtp-pacer.ts"],
4
+ "sourcesContent": ["/** Bounded, monotonic RTP playout scheduler for a single 48 kHz stream. */\nexport class RtpPacer<T> {\n private queue: { packet: T; timestamp: number }[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private lastTimestamp: number | undefined;\n private lastDeadline = 0;\n private lastSentAt = 0;\n private closed = false;\n private wasDrained = false;\n\n constructor(\n private readonly send: (packet: T) => void,\n private readonly fail: (error: Error) => void,\n private readonly drained: () => void,\n private readonly maxPackets = 100,\n ) {}\n\n push(packet: T, timestamp: number): void {\n if (this.closed) return;\n if (this.queue.length >= this.maxPackets) {\n this.fail(new Error(\"PCM RTP pacing queue overflow\"));\n return;\n }\n this.queue.push({ packet, timestamp });\n this.schedule();\n }\n\n stop(): void {\n this.closed = true;\n if (this.timer !== undefined) clearTimeout(this.timer);\n this.timer = undefined;\n this.queue = [];\n }\n\n get empty(): boolean {\n return this.queue.length === 0;\n }\n\n private schedule(): void {\n if (this.closed || this.timer !== undefined || this.queue.length === 0) return;\n const next = this.queue[0]!;\n // Signed modular difference accepts the normal 32-bit wrap, but rejects\n // backwards timestamps and discontinuities too large to schedule safely.\n const delta = this.lastTimestamp === undefined ? 0 : (next.timestamp - this.lastTimestamp) | 0;\n if (delta < 0 || delta > 480_000) {\n this.fail(new Error(\"Invalid PCM RTP timestamp discontinuity\"));\n return;\n }\n const now = performance.now();\n const interval = delta / 48;\n let deadline = this.lastTimestamp === undefined ? now : this.lastDeadline + interval;\n // An idle stream has no backlog to recover. Reset its phase, but keep the\n // minimum spacing from the previous send when packets resume quickly.\n if (this.queue.length === 1 && this.timer === undefined && this.wasDrained && deadline < now) {\n deadline = now;\n }\n this.wasDrained = false;\n // Recover lost time gradually, at no more than 1/0.9 of the media rate.\n // Keep the original media deadline so repeated stalls do not accumulate.\n const earliest = this.lastTimestamp === undefined ? now : this.lastSentAt + interval * 0.9;\n const delay = Math.max(0, Math.ceil(Math.max(deadline, earliest) - now));\n this.timer = setTimeout(() => {\n this.timer = undefined;\n if (this.closed) return;\n const item = this.queue.shift()!;\n this.lastTimestamp = item.timestamp;\n this.lastDeadline = deadline;\n this.lastSentAt = performance.now();\n this.send(item.packet);\n if (this.closed) return;\n if (this.queue.length === 0) {\n this.wasDrained = true;\n this.drained();\n } else this.schedule();\n }, delay);\n }\n}\n"],
5
+ "mappings": "aACM,aAAO,QAAQ,CAUA,KACA,KACA,QACA,WAZX,MAA4C,CAAA,EAC5C,MACA,cACA,aAAe,EACf,WAAa,EACb,OAAS,GACT,WAAa,GAErB,YACmBA,EACAC,EACAC,EACAC,EAAa,IAAG,WAHhBH,YACAC,eACAC,kBACAC,CAChB,CAEH,KAAKC,EAAWC,EAAiB,CAC/B,GAAI,MAAK,OACT,IAAI,KAAK,MAAM,QAAU,KAAK,WAAY,CACxC,KAAK,KAAK,IAAI,MAAM,+BAA+B,CAAC,EACpD,MACF,CACA,KAAK,MAAM,KAAK,CAAE,OAAAD,EAAQ,UAAAC,CAAS,CAAE,EACrC,KAAK,SAAQ,EACf,CAEA,MAAI,CACF,KAAK,OAAS,GACV,KAAK,QAAU,QAAW,aAAa,KAAK,KAAK,EACrD,KAAK,MAAQ,OACb,KAAK,MAAQ,CAAA,CACf,CAEA,IAAI,OAAK,CACP,OAAO,KAAK,MAAM,SAAW,CAC/B,CAEQ,UAAQ,CACd,GAAI,KAAK,QAAU,KAAK,QAAU,QAAa,KAAK,MAAM,SAAW,EAAG,OACxE,MAAMC,EAAO,KAAK,MAAM,CAAC,EAGnBC,EAAQ,KAAK,gBAAkB,OAAY,EAAKD,EAAK,UAAY,KAAK,cAAiB,EAC7F,GAAIC,EAAQ,GAAKA,EAAQ,KAAS,CAChC,KAAK,KAAK,IAAI,MAAM,yCAAyC,CAAC,EAC9D,MACF,CACA,MAAMC,EAAM,YAAY,IAAG,EACrBC,EAAWF,EAAQ,GACzB,IAAIG,EAAW,KAAK,gBAAkB,OAAYF,EAAM,KAAK,aAAeC,EAGxE,KAAK,MAAM,SAAW,GAAK,KAAK,QAAU,QAAa,KAAK,YAAcC,EAAWF,IACvFE,EAAWF,GAEb,KAAK,WAAa,GAGlB,MAAMG,EAAW,KAAK,gBAAkB,OAAYH,EAAM,KAAK,WAAaC,EAAW,GACjFG,EAAQ,KAAK,IAAI,EAAG,KAAK,KAAK,KAAK,IAAIF,EAAUC,CAAQ,EAAIH,CAAG,CAAC,EACvE,KAAK,MAAQ,WAAW,IAAK,CAE3B,GADA,KAAK,MAAQ,OACT,KAAK,OAAQ,OACjB,MAAMK,EAAO,KAAK,MAAM,MAAK,EAC7B,KAAK,cAAgBA,EAAK,UAC1B,KAAK,aAAeH,EACpB,KAAK,WAAa,YAAY,IAAG,EACjC,KAAK,KAAKG,EAAK,MAAM,EACjB,MAAK,SACL,KAAK,MAAM,SAAW,GACxB,KAAK,WAAa,GAClB,KAAK,QAAO,GACP,KAAK,SAAQ,EACtB,EAAGD,CAAK,CACV",
6
+ "names": ["send", "fail", "drained", "maxPackets", "packet", "timestamp", "next", "delta", "now", "interval", "deadline", "earliest", "delay", "item"]
7
+ }