@hyperttp/transport-webrtc 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +169 -2
- package/dist/browser.d.ts +63 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +2 -0
- package/dist/browser.js.map +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/pcm-audio.d.ts +29 -0
- package/dist/pcm-audio.d.ts.map +1 -0
- package/dist/pcm-audio.js +2 -0
- package/dist/pcm-audio.js.map +7 -0
- package/dist/types/types.d.ts +106 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/types.js +2 -0
- package/dist/types/types.js.map +7 -0
- package/dist/utils/helper.d.ts +8 -0
- package/dist/utils/helper.d.ts.map +1 -0
- package/dist/utils/helper.js +2 -0
- package/dist/utils/helper.js.map +7 -0
- package/dist/webrtc.d.ts +74 -0
- package/dist/webrtc.d.ts.map +1 -0
- package/dist/webrtc.js +2 -0
- package/dist/webrtc.js.map +7 -0
- package/package.json +68 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IT IF OR
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,170 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hyperttp/transport-webrtc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WebRTC transport for Hyperttp: Werift on the server, native WebRTC in the browser.
|
|
4
|
+
Requests/responses use a reliable ordered DataChannel; audio/video use WebRTC media tracks.
|
|
5
|
+
No `wrtc` dependency is required.
|
|
6
|
+
|
|
7
|
+
## Server: receive-only listeners
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { WebRtcTransport } from "@hyperttp/transport-webrtc";
|
|
11
|
+
import { createPcmAudioSource } from "@hyperttp/transport-webrtc/pcm-audio";
|
|
12
|
+
|
|
13
|
+
// Raw PCM: signed 16-bit little-endian, 48 kHz, stereo.
|
|
14
|
+
// Pass an independently subscribed branch of your audio source, not a stream
|
|
15
|
+
// that is already locked/consumed by the Discord player.
|
|
16
|
+
const audio = await createPcmAudioSource(pcmStream);
|
|
17
|
+
const subscriptions = new Map();
|
|
18
|
+
const transport = new WebRtcTransport({
|
|
19
|
+
iceServers: [], // Local tests only. Configure STUN/TURN for internet clients.
|
|
20
|
+
localStream(sessionId) {
|
|
21
|
+
const subscription = audio.subscribe();
|
|
22
|
+
subscriptions.set(sessionId, subscription);
|
|
23
|
+
return subscription.stream;
|
|
24
|
+
},
|
|
25
|
+
disposeLocalStream(sessionId) {
|
|
26
|
+
subscriptions.get(sessionId)?.close();
|
|
27
|
+
subscriptions.delete(sessionId);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const server = await transport.listen({
|
|
32
|
+
host: "127.0.0.1",
|
|
33
|
+
port: 3001,
|
|
34
|
+
onRequest: (request) => ({
|
|
35
|
+
status: 200,
|
|
36
|
+
headers: {},
|
|
37
|
+
body: { listening: true },
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// On application shutdown:
|
|
42
|
+
// await transport.close();
|
|
43
|
+
// await audio.close();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`createPcmAudioSource` is an **optional server-only adapter**, not a WebRTC codec
|
|
47
|
+
implementation. It requires an installed FFmpeg executable with `libopus` support
|
|
48
|
+
(`ffmpegPath` can override PATH). It encodes one shared PCM input to 20 ms Opus
|
|
49
|
+
frames and delivers RTP to independent Werift tracks over IPv4 loopback UDP.
|
|
50
|
+
It starts consuming immediately; late subscribers join the current position.
|
|
51
|
+
Observe `audio.done` for EOF/encoder/input errors. Close the adapter when its
|
|
52
|
+
playback session ends. Closing a listener stops only its track, not the source.
|
|
53
|
+
It does not implement seeking, playback pause, or audio mixing.
|
|
54
|
+
|
|
55
|
+
## Browser
|
|
56
|
+
|
|
57
|
+
Import the explicit browser entry point so Node/Werift code is not bundled:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { WebRtcTransport } from "@hyperttp/transport-webrtc/browser";
|
|
61
|
+
|
|
62
|
+
const transport = new WebRtcTransport();
|
|
63
|
+
const audioElement = document.querySelector("audio")!;
|
|
64
|
+
transport.onTrack(({ stream }) => {
|
|
65
|
+
audioElement.srcObject = stream;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Run from a user gesture; browser autoplay policy may require another click
|
|
69
|
+
// once the remote track is available.
|
|
70
|
+
await transport.execute({
|
|
71
|
+
method: "POST",
|
|
72
|
+
protocol: "webrtc",
|
|
73
|
+
url: "webrtc://127.0.0.1:3001/signal",
|
|
74
|
+
headers: {},
|
|
75
|
+
body: { action: "stream" },
|
|
76
|
+
receiveAudio: true,
|
|
77
|
+
});
|
|
78
|
+
await audioElement.play();
|
|
79
|
+
|
|
80
|
+
// await transport.close();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For Hyperttp use this browser transport as `customTransport` together with
|
|
84
|
+
`WebRtcProtocol`. The protocol's `receive(peerUrl, options?, signal?)` convenience
|
|
85
|
+
method selects `action: "stream"` and defaults `receiveAudio` to `true`; it does
|
|
86
|
+
not ask for a microphone. Rebuild both linked packages after source changes.
|
|
87
|
+
Existing `connect`, `stream`, and `dataChannel` protocol methods remain available.
|
|
88
|
+
|
|
89
|
+
The server implementation exports `createMediaStream`, `getSession(peerUrl)`,
|
|
90
|
+
`getServerSession(sessionId)` and `onTrack`. Server requests include an
|
|
91
|
+
`x-webrtc-session-id` header inserted by the transport (overriding the wire value).
|
|
92
|
+
This identifies a transport session, **not an authenticated user**.
|
|
93
|
+
|
|
94
|
+
## Discord and web playback
|
|
95
|
+
|
|
96
|
+
Keep the shared playback source independent of both outputs:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
AudioService / shared playback clock
|
|
100
|
+
├─ bounded PCM subscription → DiscordAudioOutput
|
|
101
|
+
└─ bounded PCM subscription → createPcmAudioSource → Werift → browsers
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Do not give the same `ReadableStream` to two readers or use unbounded `tee()`
|
|
105
|
+
queues for a long-running broadcast. The application's distributor must handle
|
|
106
|
+
bounded per-output queues, backpressure and detached consumers. Discord leaving
|
|
107
|
+
voice must detach its output rather than reset the shared playback session.
|
|
108
|
+
This package does not change a Discord bot's player lifecycle automatically.
|
|
109
|
+
Local browser mute/volume should not change the common source's volume.
|
|
110
|
+
|
|
111
|
+
## Deployment constraints
|
|
112
|
+
|
|
113
|
+
- Signaling is non-trickle SDP over HTTP POST `/signal`; DELETE releases a session.
|
|
114
|
+
- `webrtc://` maps to HTTP, `webrtcs://` to HTTPS. Terminate TLS at a reverse proxy.
|
|
115
|
+
- The built-in signaling server has no authentication, CORS policy, or session
|
|
116
|
+
quotas. Use a same-origin authenticated gateway before exposing it publicly;
|
|
117
|
+
authenticate the initial SDP request before attaching a private audio source.
|
|
118
|
+
- STUN alone is insufficient for all NATs; configure TURN for production.
|
|
119
|
+
- Request bodies are buffered JSON (binary bodies are base64), not streaming file
|
|
120
|
+
transfers. Large messages and high-rate bulk traffic need additional limits and
|
|
121
|
+
DataChannel backpressure handling.
|
|
122
|
+
- `close()` is terminal; create a new transport to reconnect after closing it.
|
|
123
|
+
- The browser entry point is integration-tested with injected Werift peers. Real
|
|
124
|
+
browser playback and Discord continuity still require application-level tests.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npm run typecheck
|
|
130
|
+
npm run lint
|
|
131
|
+
npm test
|
|
132
|
+
npm run build
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Network integration tests use loopback and no public STUN servers. PCM adapter
|
|
136
|
+
integration tests require FFmpeg with libopus.
|
|
137
|
+
|
|
138
|
+
## Integration boundaries
|
|
139
|
+
|
|
140
|
+
The root entry has a `browser` export condition for bundlers that support it;
|
|
141
|
+
`/browser` remains the explicit, portable browser entry. The PCM adapter remains
|
|
142
|
+
server-only. Node and browser transports can also tunnel other protocol requests
|
|
143
|
+
through the DataChannel; they intentionally do not restrict `protocol` to WebRTC.
|
|
144
|
+
|
|
145
|
+
The protocol's `sessionId` is application metadata. The server transport inserts
|
|
146
|
+
its own `x-webrtc-session-id`, exposed by `protocol-webrtc` as `transportSessionId`.
|
|
147
|
+
It is suitable for `getServerSession(id)`, not authentication.
|
|
148
|
+
|
|
149
|
+
Configure an application ACK-handler on protocol servers even for media-only
|
|
150
|
+
requests (for example `webRtcControlHandler` from `protocol-webrtc`). Media can
|
|
151
|
+
start during negotiation before that handler is invoked. Core request hooks and
|
|
152
|
+
DataChannel handlers therefore do **not** authorize HTTP signaling or media access.
|
|
153
|
+
For private streams place the signaling endpoint behind an authenticated gateway
|
|
154
|
+
that also enforces session ownership and resource limits. Treat `sessionId`,
|
|
155
|
+
`x-peer-id`, and `x-webrtc-session-id` as metadata, never user identity.
|
|
156
|
+
|
|
157
|
+
Protocol `offer`/`answer`/`candidate` actions are application messages; the built-in
|
|
158
|
+
transport uses its own HTTP SDP negotiation rather than these messages.
|
|
159
|
+
|
|
160
|
+
## Custom signaling path and early authorization
|
|
161
|
+
|
|
162
|
+
Low-level `transport.listen()` and `core.listen()` accept
|
|
163
|
+
`signaling: { path: "/calls/signal", authorize: async request => boolean }`.
|
|
164
|
+
Authorization runs for POST and DELETE before SDP parsing, session lookup or peer
|
|
165
|
+
creation. Returning false or throwing denies access with 403. The raw request has
|
|
166
|
+
headers/method/URL and an AbortSignal, no body. On disconnect or listener shutdown,
|
|
167
|
+
the signal aborts; a late authorization result cannot create a new session.
|
|
168
|
+
Core forwards these options unchanged. The high-level `server.webrtc.signal()`
|
|
169
|
+
facade additionally injects typed application services into both callbacks.
|
|
170
|
+
The default remains `/signal` without authorization for backward compatibility.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { HyperTransport, TransportRequest, TransportResponse } from "@hyperttp/types";
|
|
2
|
+
export type WebRtcTransportRequest = TransportRequest & {
|
|
3
|
+
peerUrl?: string;
|
|
4
|
+
iceServers?: RTCIceServer[];
|
|
5
|
+
localStream?: MediaStream;
|
|
6
|
+
receiveAudio?: boolean;
|
|
7
|
+
receiveVideo?: boolean;
|
|
8
|
+
channelLabel?: string;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
};
|
|
11
|
+
export interface WebRtcTransportOptions {
|
|
12
|
+
readonly iceServers?: RTCIceServer[];
|
|
13
|
+
readonly iceGatheringTimeoutMs?: number;
|
|
14
|
+
readonly signalingTimeoutMs?: number;
|
|
15
|
+
readonly connectionTimeoutMs?: number;
|
|
16
|
+
readonly requestTimeoutMs?: number;
|
|
17
|
+
readonly createPeerConnection?: (configuration: RTCConfiguration) => RTCPeerConnection;
|
|
18
|
+
readonly createMediaStream?: () => MediaStream;
|
|
19
|
+
}
|
|
20
|
+
export interface WebRtcTrackEvent {
|
|
21
|
+
readonly side: "client";
|
|
22
|
+
readonly sessionId: string;
|
|
23
|
+
readonly peerUrl: string;
|
|
24
|
+
readonly peerConnection: RTCPeerConnection;
|
|
25
|
+
readonly track: MediaStreamTrack;
|
|
26
|
+
readonly stream: MediaStream;
|
|
27
|
+
readonly streams: readonly MediaStream[];
|
|
28
|
+
}
|
|
29
|
+
export interface WebRtcSessionView {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly peerUrl: string;
|
|
32
|
+
readonly peerConnection: RTCPeerConnection;
|
|
33
|
+
readonly dataChannel: RTCDataChannel;
|
|
34
|
+
readonly remoteStream: MediaStream;
|
|
35
|
+
readonly localStream?: MediaStream;
|
|
36
|
+
}
|
|
37
|
+
/** Browser client transport. Caller-owned local media tracks are never stopped. */
|
|
38
|
+
export declare class WebRtcTransport implements HyperTransport {
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly sessions;
|
|
41
|
+
private readonly queues;
|
|
42
|
+
private readonly trackListeners;
|
|
43
|
+
private readonly disposals;
|
|
44
|
+
private closed;
|
|
45
|
+
private closing;
|
|
46
|
+
private sequence;
|
|
47
|
+
constructor(options?: WebRtcTransportOptions);
|
|
48
|
+
onTrack(listener: (event: WebRtcTrackEvent) => void): () => void;
|
|
49
|
+
getSession(peerUrl: string): WebRtcSessionView | undefined;
|
|
50
|
+
execute(request: WebRtcTransportRequest): Promise<TransportResponse>;
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
destroy(): Promise<void>;
|
|
53
|
+
private createSession;
|
|
54
|
+
private configureMedia;
|
|
55
|
+
private negotiate;
|
|
56
|
+
private receive;
|
|
57
|
+
private sendRequest;
|
|
58
|
+
private dispose;
|
|
59
|
+
private run;
|
|
60
|
+
private abortable;
|
|
61
|
+
private waitFor;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{abortError as h,decodeWireBody as f,encodeWireBody as u}from"./utils/helper.js";class w{options;sessions=new Map;queues=new Map;trackListeners=new Set;disposals=new Set;closed=!1;closing;sequence=0;constructor(e={}){this.options=e}onTrack(e){return this.trackListeners.add(e),()=>{this.trackListeners.delete(e)}}getSession(e){const t=this.sessions.get(e);if(!(!t||t.controller.signal.aborted))return{id:t.id,peerUrl:t.peerUrl,peerConnection:t.peerConnection,dataChannel:t.dataChannel,remoteStream:t.remoteStream,...t.localStream?{localStream:t.localStream}:{}}}async execute(e){if(this.closed)throw new Error("WebRTC transport is closed");if(e.signal?.aborted)throw h(e.signal);const t=e.peerUrl??e.url,o=(this.queues.get(t)??Promise.resolve()).catch(()=>{}).then(async()=>{if(this.closed)throw new Error("WebRTC transport is closed");if(e.signal?.aborted)throw h(e.signal);let a=this.sessions.get(t);if(a&&(a.controller.signal.aborted||a.dataChannel.readyState!=="open"||["failed","closed"].includes(a.peerConnection.connectionState))&&(await this.dispose(a),a=void 0),this.closed)throw new Error("WebRTC transport is closed");a??=this.createSession(t,e);try{const c=this.configureMedia(a,e);return(!a.signaled||c)&&await this.negotiate(a,e.signal),await this.run(a,e.signal,this.options.connectionTimeoutMs??15e3,"DataChannel open",l=>this.waitFor(a.dataChannel,["open","close","error"],l,()=>a.dataChannel.readyState==="open",()=>["closing","closed"].includes(a.dataChannel.readyState))),a}catch(c){throw this.dispose(a),c}});this.queues.set(t,o);const n=()=>{this.queues.get(t)===o&&this.queues.delete(t)};o.then(n,n);const s=await this.abortable(o,e.signal);return this.sendRequest(s,e)}close(){if(this.closing)return this.closing;this.closed=!0;const e=[...this.queues.values()],t=[...this.sessions.values()];return this.closing=(async()=>{await Promise.all(t.map(r=>this.dispose(r))),await Promise.allSettled(e),await Promise.all(this.disposals),this.trackListeners.clear()})(),this.closing}destroy(){return this.close()}createSession(e,t){const r=new URL(e.replace(/^webrtc:/i,"http:").replace(/^webrtcs:/i,"https:"));if(r.protocol!=="http:"&&r.protocol!=="https:")throw new Error(`Unsupported WebRTC signaling protocol: ${r.protocol}`);r.pathname==="/"&&(r.pathname="/signal"),r.search="",r.hash="";const o=(this.options.createPeerConnection??(i=>new RTCPeerConnection(i)))({iceServers:t.iceServers??this.options.iceServers??[{urls:"stun:stun.l.google.com:19302"}]});let n;try{n={id:`session-${crypto.randomUUID()}`,peerUrl:e,signalingUrl:r.toString(),peerConnection:o,dataChannel:o.createDataChannel(t.channelLabel??"hyperttp-webrtc-transport",{ordered:!0}),remoteStream:(this.options.createMediaStream??(()=>new MediaStream))(),controller:new AbortController,pending:new Map,cleanup:[],signaled:!1}}catch(i){throw o.close(),i}this.sessions.set(e,n);const s=i=>{n.remoteStream.getTracks().includes(i.track)||n.remoteStream.addTrack(i.track);const d={side:"client",sessionId:n.id,peerUrl:e,peerConnection:o,track:i.track,stream:n.remoteStream,streams:i.streams};for(const p of this.trackListeners)try{p(d)}catch{}},a=()=>{(o.connectionState==="failed"||o.connectionState==="closed")&&this.dispose(n)},c=()=>{this.dispose(n)},l=i=>this.receive(n,i);return o.addEventListener("track",s),o.addEventListener("connectionstatechange",a),n.dataChannel.addEventListener("message",l),n.dataChannel.addEventListener("close",c),n.dataChannel.addEventListener("error",c),n.cleanup.push(()=>{o.removeEventListener("track",s),o.removeEventListener("connectionstatechange",a),n.dataChannel.removeEventListener("message",l),n.dataChannel.removeEventListener("close",c),n.dataChannel.removeEventListener("error",c)}),n}configureMedia(e,t){const r=e.peerConnection;let o=!1;if(t.localStream){for(const n of t.localStream.getTracks())n.readyState==="live"&&!r.getSenders().some(s=>s.track===n)&&(r.addTrack(n,t.localStream),o=!0);e.localStream=t.localStream}for(const n of["audio","video"]){if(!(n==="audio"?t.receiveAudio:t.receiveVideo))continue;const s=r.getTransceivers().find(a=>a.receiver.track.kind===n);s?(s.direction==="sendonly"||s.direction==="inactive")&&(s.direction=s.direction==="sendonly"?"sendrecv":"recvonly",o=!0):(r.addTransceiver(n,{direction:"recvonly"}),o=!0)}return o}async negotiate(e,t){const r=e.peerConnection;await this.run(e,t,this.options.signalingTimeoutMs??1e4,"SDP offer",async o=>{const n=await r.createOffer();if(o.aborted)throw h(o);await r.setLocalDescription(n)}),await this.run(e,t,this.options.iceGatheringTimeoutMs??1e4,"ICE gathering",o=>this.waitFor(r,["icegatheringstatechange"],o,()=>r.iceGatheringState==="complete")),await this.run(e,t,this.options.signalingTimeoutMs??1e4,"Signaling",async o=>{if(!r.localDescription?.sdp)throw new Error("Local SDP offer was not generated");e.signaled=!0;const n=await fetch(e.signalingUrl,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({sessionId:e.id,sdp:r.localDescription}),signal:o});if(!n.ok)throw new Error(`Signaling HTTP error: ${n.status} ${n.statusText}`);const s=await n.json();if(!s||typeof s!="object"||!("type"in s)||s.type!=="answer"||!("sdp"in s)||typeof s.sdp!="string")throw new Error("Signaling server returned an invalid SDP answer");if(o.aborted)throw h(o);await r.setRemoteDescription({type:"answer",sdp:s.sdp})})}receive(e,t){if(typeof t.data!="string")return;let r;try{const n=JSON.parse(t.data);if(!n||typeof n!="object")return;r=n}catch{return}if(typeof r.requestId!="string")return;const o=e.pending.get(r.requestId);if(o)try{o(void 0,{status:typeof r.status=="number"?r.status:200,statusText:typeof r.statusText=="string"?r.statusText:"OK",headers:r.headers&&typeof r.headers=="object"?r.headers:{},body:f(r.body),url:e.peerUrl})}catch(n){o(n)}}sendRequest(e,t){const r=t.timeout&&t.timeout>0?t.timeout:this.options.requestTimeoutMs??15e3;return this.run(e,t.signal,r,"WebRTC request",o=>new Promise((n,s)=>{if(e.dataChannel.readyState!=="open"){s(new Error("WebRTC DataChannel is not open"));return}const a=`request-${++this.sequence}`,c=(i,d)=>{e.pending.delete(a),o.removeEventListener("abort",l),d?n(d):s(i??new Error("WebRTC session closed"))},l=()=>c(h(o));if(e.pending.set(a,c),o.addEventListener("abort",l,{once:!0}),o.aborted){l();return}try{e.dataChannel.send(JSON.stringify({requestId:a,method:t.method,url:t.url,headers:t.headers,body:u(t.body),protocol:t.protocol}))}catch(i){c(i)}}))}dispose(e){if(e.disposal)return e.disposal;const t=Promise.resolve().then(async()=>{e.controller.abort(new Error("WebRTC session closed")),this.sessions.get(e.peerUrl)===e&&this.sessions.delete(e.peerUrl);for(const s of e.cleanup)s();e.cleanup.length=0;for(const s of e.pending.values())s(new Error("WebRTC session closed"));try{e.dataChannel.close()}catch{}try{e.peerConnection.close()}catch{}if(!e.signaled)return;const r=new URL(e.signalingUrl);r.searchParams.set("sessionId",e.id);const o=new AbortController,n=setTimeout(()=>o.abort(),2e3);try{await this.abortable(fetch(r,{method:"DELETE",signal:o.signal}),o.signal)}catch{}finally{clearTimeout(n)}});return e.disposal=t,this.disposals.add(t),t.then(()=>this.disposals.delete(t)),t}async run(e,t,r,o,n){const s=new AbortController,a=[e.controller.signal,...t?[t]:[]],c=()=>{const i=a.find(d=>d.aborted);i&&s.abort(h(i))};for(const i of a)i.addEventListener("abort",c,{once:!0});const l=setTimeout(()=>s.abort(new Error(`${o} timed out after ${r} ms`)),r);try{if(c(),s.signal.aborted)throw h(s.signal);return await this.abortable(n(s.signal),s.signal)}finally{clearTimeout(l);for(const i of a)i.removeEventListener("abort",c)}}abortable(e,t){return t?new Promise((r,o)=>{const n=()=>t.removeEventListener("abort",s),s=()=>{n(),o(h(t))};t.addEventListener("abort",s,{once:!0}),t.aborted&&s(),e.then(a=>{n(),r(a)},a=>{n(),o(a)})}):e}waitFor(e,t,r,o,n=()=>!1){return new Promise((s,a)=>{const c=d=>{for(const p of t)e.removeEventListener(p,i);r.removeEventListener("abort",l),d?a(d):s()},l=()=>c(h(r)),i=d=>{r.aborted?l():n()||d?.type==="error"?c(new Error("WebRTC connection failed")):o()&&c()};for(const d of t)e.addEventListener(d,i);r.addEventListener("abort",l,{once:!0}),i()})}}export{w as WebRtcTransport};
|
|
2
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/browser.ts"],
|
|
4
|
+
"sourcesContent": ["import type { HyperTransport, TransportRequest, TransportResponse } from \"@hyperttp/types\";\nimport { abortError, decodeWireBody, encodeWireBody } from \"./utils/helper.js\";\n\nexport type WebRtcTransportRequest = TransportRequest & {\n peerUrl?: string;\n iceServers?: RTCIceServer[];\n localStream?: MediaStream;\n receiveAudio?: boolean;\n receiveVideo?: boolean;\n channelLabel?: string;\n timeout?: number;\n};\n\nexport interface WebRtcTransportOptions {\n readonly iceServers?: RTCIceServer[];\n readonly iceGatheringTimeoutMs?: number;\n readonly signalingTimeoutMs?: number;\n readonly connectionTimeoutMs?: number;\n readonly requestTimeoutMs?: number;\n readonly createPeerConnection?: (configuration: RTCConfiguration) => RTCPeerConnection;\n readonly createMediaStream?: () => MediaStream;\n}\n\nexport interface WebRtcTrackEvent {\n readonly side: \"client\";\n readonly sessionId: string;\n readonly peerUrl: string;\n readonly peerConnection: RTCPeerConnection;\n readonly track: MediaStreamTrack;\n readonly stream: MediaStream;\n readonly streams: readonly MediaStream[];\n}\n\nexport interface WebRtcSessionView {\n readonly id: string;\n readonly peerUrl: string;\n readonly peerConnection: RTCPeerConnection;\n readonly dataChannel: RTCDataChannel;\n readonly remoteStream: MediaStream;\n readonly localStream?: MediaStream;\n}\n\ninterface Session extends WebRtcSessionView {\n localStream?: MediaStream;\n signalingUrl: string;\n controller: AbortController;\n pending: Map<string, (error?: unknown, response?: TransportResponse) => void>;\n cleanup: (() => void)[];\n signaled: boolean;\n disposal?: Promise<void>;\n}\n\n/** Browser client transport. Caller-owned local media tracks are never stopped. */\nexport class WebRtcTransport implements HyperTransport {\n private readonly sessions = new Map<string, Session>();\n private readonly queues = new Map<string, Promise<Session>>();\n private readonly trackListeners = new Set<(event: WebRtcTrackEvent) => void>();\n private readonly disposals = new Set<Promise<void>>();\n private closed = false;\n private closing: Promise<void> | undefined;\n private sequence = 0;\n\n constructor(private readonly options: WebRtcTransportOptions = {}) {}\n\n onTrack(listener: (event: WebRtcTrackEvent) => void): () => void {\n this.trackListeners.add(listener);\n return () => {\n this.trackListeners.delete(listener);\n };\n }\n\n getSession(peerUrl: string): WebRtcSessionView | undefined {\n const session = this.sessions.get(peerUrl);\n if (!session || session.controller.signal.aborted) return undefined;\n return {\n id: session.id,\n peerUrl: session.peerUrl,\n peerConnection: session.peerConnection,\n dataChannel: session.dataChannel,\n remoteStream: session.remoteStream,\n ...(session.localStream ? { localStream: session.localStream } : {}),\n };\n }\n\n async execute(request: WebRtcTransportRequest): Promise<TransportResponse> {\n if (this.closed) throw new Error(\"WebRTC transport is closed\");\n if (request.signal?.aborted) throw abortError(request.signal);\n const peerUrl = request.peerUrl ?? request.url;\n // Queue before awaiting anything, including disposal of an unusable session.\n const previous = this.queues.get(peerUrl);\n const operation = (previous ?? Promise.resolve())\n .catch(() => undefined)\n .then(async () => {\n if (this.closed) throw new Error(\"WebRTC transport is closed\");\n if (request.signal?.aborted) throw abortError(request.signal);\n let session = this.sessions.get(peerUrl);\n if (\n session &&\n (session.controller.signal.aborted ||\n session.dataChannel.readyState !== \"open\" ||\n [\"failed\", \"closed\"].includes(session.peerConnection.connectionState))\n ) {\n await this.dispose(session);\n session = undefined;\n }\n if (this.closed) throw new Error(\"WebRTC transport is closed\");\n session ??= this.createSession(peerUrl, request);\n try {\n const changed = this.configureMedia(session, request);\n if (!session.signaled || changed) await this.negotiate(session, request.signal);\n await this.run(\n session,\n request.signal,\n this.options.connectionTimeoutMs ?? 15_000,\n \"DataChannel open\",\n (signal) =>\n this.waitFor(\n session!.dataChannel,\n [\"open\", \"close\", \"error\"],\n signal,\n () => session!.dataChannel.readyState === \"open\",\n () => [\"closing\", \"closed\"].includes(session!.dataChannel.readyState),\n ),\n );\n return session;\n } catch (error) {\n void this.dispose(session);\n throw error;\n }\n });\n this.queues.set(peerUrl, operation);\n const remove = () => {\n if (this.queues.get(peerUrl) === operation) this.queues.delete(peerUrl);\n };\n void operation.then(remove, remove);\n // A queued caller can abort immediately without interrupting another caller's setup.\n const session = await this.abortable(operation, request.signal);\n return this.sendRequest(session, request);\n }\n\n close(): Promise<void> {\n if (this.closing) return this.closing;\n this.closed = true;\n const connections = [...this.queues.values()];\n const sessions = [...this.sessions.values()];\n this.closing = (async () => {\n await Promise.all(sessions.map((session) => this.dispose(session)));\n await Promise.allSettled(connections);\n await Promise.all(this.disposals);\n this.trackListeners.clear();\n })();\n return this.closing;\n }\n\n destroy(): Promise<void> {\n return this.close();\n }\n\n private createSession(peerUrl: string, request: WebRtcTransportRequest): Session {\n const url = new URL(peerUrl.replace(/^webrtc:/i, \"http:\").replace(/^webrtcs:/i, \"https:\"));\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new Error(`Unsupported WebRTC signaling protocol: ${url.protocol}`);\n }\n if (url.pathname === \"/\") url.pathname = \"/signal\";\n url.search = \"\";\n url.hash = \"\";\n const pc = (this.options.createPeerConnection ?? ((config) => new RTCPeerConnection(config)))({\n iceServers: request.iceServers ??\n this.options.iceServers ?? [{ urls: \"stun:stun.l.google.com:19302\" }],\n });\n let session: Session;\n try {\n session = {\n id: `session-${crypto.randomUUID()}`,\n peerUrl,\n signalingUrl: url.toString(),\n peerConnection: pc,\n dataChannel: pc.createDataChannel(request.channelLabel ?? \"hyperttp-webrtc-transport\", {\n ordered: true,\n }),\n remoteStream: (this.options.createMediaStream ?? (() => new MediaStream()))(),\n controller: new AbortController(),\n pending: new Map(),\n cleanup: [],\n signaled: false,\n };\n } catch (error) {\n pc.close();\n throw error;\n }\n this.sessions.set(peerUrl, session);\n const onTrack = (event: RTCTrackEvent) => {\n if (!session.remoteStream.getTracks().includes(event.track))\n session.remoteStream.addTrack(event.track);\n const trackEvent: WebRtcTrackEvent = {\n side: \"client\",\n sessionId: session.id,\n peerUrl,\n peerConnection: pc,\n track: event.track,\n stream: session.remoteStream,\n streams: event.streams,\n };\n for (const listener of this.trackListeners) {\n try {\n listener(trackEvent);\n } catch {\n /* Consumers must not interrupt transport events. */\n }\n }\n };\n const onState = () => {\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\")\n void this.dispose(session);\n };\n const onClose = () => {\n void this.dispose(session);\n };\n const onMessage = (event: MessageEvent) => this.receive(session, event);\n pc.addEventListener(\"track\", onTrack);\n pc.addEventListener(\"connectionstatechange\", onState);\n session.dataChannel.addEventListener(\"message\", onMessage);\n session.dataChannel.addEventListener(\"close\", onClose);\n session.dataChannel.addEventListener(\"error\", onClose);\n session.cleanup.push(() => {\n pc.removeEventListener(\"track\", onTrack);\n pc.removeEventListener(\"connectionstatechange\", onState);\n session.dataChannel.removeEventListener(\"message\", onMessage);\n session.dataChannel.removeEventListener(\"close\", onClose);\n session.dataChannel.removeEventListener(\"error\", onClose);\n });\n return session;\n }\n\n private configureMedia(session: Session, request: WebRtcTransportRequest): boolean {\n const pc = session.peerConnection;\n let changed = false;\n if (request.localStream) {\n for (const track of request.localStream.getTracks()) {\n if (\n track.readyState === \"live\" &&\n !pc.getSenders().some((sender) => sender.track === track)\n ) {\n pc.addTrack(track, request.localStream);\n changed = true;\n }\n }\n session.localStream = request.localStream;\n }\n for (const kind of [\"audio\", \"video\"] as const) {\n if (!(kind === \"audio\" ? request.receiveAudio : request.receiveVideo)) continue;\n const transceiver = pc.getTransceivers().find((item) => item.receiver.track.kind === kind);\n if (!transceiver) {\n pc.addTransceiver(kind, { direction: \"recvonly\" });\n changed = true;\n } else if (transceiver.direction === \"sendonly\" || transceiver.direction === \"inactive\") {\n transceiver.direction = transceiver.direction === \"sendonly\" ? \"sendrecv\" : \"recvonly\";\n changed = true;\n }\n }\n return changed;\n }\n\n private async negotiate(session: Session, signal?: AbortSignal): Promise<void> {\n const pc = session.peerConnection;\n await this.run(\n session,\n signal,\n this.options.signalingTimeoutMs ?? 10_000,\n \"SDP offer\",\n async (active) => {\n const offer = await pc.createOffer();\n if (active.aborted) throw abortError(active);\n await pc.setLocalDescription(offer);\n },\n );\n await this.run(\n session,\n signal,\n this.options.iceGatheringTimeoutMs ?? 10_000,\n \"ICE gathering\",\n (active) =>\n this.waitFor(\n pc,\n [\"icegatheringstatechange\"],\n active,\n () => pc.iceGatheringState === \"complete\",\n ),\n );\n await this.run(\n session,\n signal,\n this.options.signalingTimeoutMs ?? 10_000,\n \"Signaling\",\n async (active) => {\n if (!pc.localDescription?.sdp) throw new Error(\"Local SDP offer was not generated\");\n session.signaled = true;\n const response = await fetch(session.signalingUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ sessionId: session.id, sdp: pc.localDescription }),\n signal: active,\n });\n if (!response.ok)\n throw new Error(`Signaling HTTP error: ${response.status} ${response.statusText}`);\n const answer: unknown = await response.json();\n if (\n !answer ||\n typeof answer !== \"object\" ||\n !(\"type\" in answer) ||\n answer.type !== \"answer\" ||\n !(\"sdp\" in answer) ||\n typeof answer.sdp !== \"string\"\n ) {\n throw new Error(\"Signaling server returned an invalid SDP answer\");\n }\n if (active.aborted) throw abortError(active);\n await pc.setRemoteDescription({ type: \"answer\", sdp: answer.sdp });\n },\n );\n }\n\n private receive(session: Session, event: MessageEvent): void {\n if (typeof event.data !== \"string\") return;\n let message: Record<string, unknown>;\n try {\n const parsed: unknown = JSON.parse(event.data);\n if (!parsed || typeof parsed !== \"object\") return;\n message = parsed as Record<string, unknown>;\n } catch {\n return;\n }\n if (typeof message.requestId !== \"string\") return;\n const finish = session.pending.get(message.requestId);\n if (!finish) return;\n try {\n finish(undefined, {\n status: typeof message.status === \"number\" ? message.status : 200,\n statusText: typeof message.statusText === \"string\" ? message.statusText : \"OK\",\n headers:\n message.headers && typeof message.headers === \"object\"\n ? (message.headers as TransportResponse[\"headers\"])\n : {},\n body: decodeWireBody(message.body),\n url: session.peerUrl,\n });\n } catch (error) {\n finish(error);\n }\n }\n\n private sendRequest(\n session: Session,\n request: WebRtcTransportRequest,\n ): Promise<TransportResponse> {\n const timeout =\n request.timeout && request.timeout > 0\n ? request.timeout\n : (this.options.requestTimeoutMs ?? 15_000);\n return this.run(\n session,\n request.signal,\n timeout,\n \"WebRTC request\",\n (signal) =>\n new Promise<TransportResponse>((resolve, reject) => {\n if (session.dataChannel.readyState !== \"open\") {\n reject(new Error(\"WebRTC DataChannel is not open\"));\n return;\n }\n const requestId = `request-${++this.sequence}`;\n const finish = (error?: unknown, response?: TransportResponse) => {\n session.pending.delete(requestId);\n signal.removeEventListener(\"abort\", onAbort);\n if (response) resolve(response);\n else reject(error ?? new Error(\"WebRTC session closed\"));\n };\n const onAbort = () => finish(abortError(signal));\n session.pending.set(requestId, finish);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n if (signal.aborted) {\n onAbort();\n return;\n }\n try {\n session.dataChannel.send(\n JSON.stringify({\n requestId,\n method: request.method,\n url: request.url,\n headers: request.headers,\n body: encodeWireBody(request.body),\n protocol: request.protocol,\n }),\n );\n } catch (error) {\n finish(error);\n }\n }),\n );\n }\n\n private dispose(session: Session): Promise<void> {\n if (session.disposal) return session.disposal;\n const disposal = Promise.resolve().then(async () => {\n session.controller.abort(new Error(\"WebRTC session closed\"));\n if (this.sessions.get(session.peerUrl) === session) this.sessions.delete(session.peerUrl);\n for (const cleanup of session.cleanup) cleanup();\n session.cleanup.length = 0;\n for (const finish of session.pending.values()) finish(new Error(\"WebRTC session closed\"));\n try {\n session.dataChannel.close();\n } catch {}\n try {\n session.peerConnection.close();\n } catch {}\n if (!session.signaled) return;\n const url = new URL(session.signalingUrl);\n url.searchParams.set(\"sessionId\", session.id);\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 2_000);\n try {\n await this.abortable(\n fetch(url, { method: \"DELETE\", signal: controller.signal }),\n controller.signal,\n );\n } catch {\n /* Remote cleanup is best effort. */\n } finally {\n clearTimeout(timer);\n }\n });\n session.disposal = disposal;\n this.disposals.add(disposal);\n void disposal.then(() => this.disposals.delete(disposal));\n return disposal;\n }\n\n private async run<T>(\n session: Session,\n signal: AbortSignal | undefined,\n timeout: number,\n label: string,\n task: (signal: AbortSignal) => Promise<T>,\n ): Promise<T> {\n const controller = new AbortController();\n const sources = [session.controller.signal, ...(signal ? [signal] : [])];\n const onAbort = () => {\n const source = sources.find((item) => item.aborted);\n if (source) controller.abort(abortError(source));\n };\n for (const source of sources) source.addEventListener(\"abort\", onAbort, { once: true });\n const timer = setTimeout(\n () => controller.abort(new Error(`${label} timed out after ${timeout} ms`)),\n timeout,\n );\n try {\n onAbort();\n if (controller.signal.aborted) throw abortError(controller.signal);\n return await this.abortable(task(controller.signal), controller.signal);\n } finally {\n clearTimeout(timer);\n for (const source of sources) source.removeEventListener(\"abort\", onAbort);\n }\n }\n\n private abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {\n if (!signal) return promise;\n return new Promise<T>((resolve, reject) => {\n const cleanup = () => signal.removeEventListener(\"abort\", onAbort);\n const onAbort = () => {\n cleanup();\n reject(abortError(signal));\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n if (signal.aborted) onAbort();\n void promise.then(\n (value) => {\n cleanup();\n resolve(value);\n },\n (error: unknown) => {\n cleanup();\n reject(error);\n },\n );\n });\n }\n\n private waitFor(\n target: EventTarget,\n events: string[],\n signal: AbortSignal,\n ready: () => boolean,\n failed: () => boolean = () => false,\n ): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const finish = (error?: Error) => {\n for (const event of events) target.removeEventListener(event, check);\n signal.removeEventListener(\"abort\", onAbort);\n if (error) reject(error);\n else resolve();\n };\n const onAbort = () => finish(abortError(signal));\n const check = (event?: Event) => {\n if (signal.aborted) onAbort();\n else if (failed() || event?.type === \"error\") finish(new Error(\"WebRTC connection failed\"));\n else if (ready()) finish();\n };\n for (const event of events) target.addEventListener(event, check);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n check();\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["abortError", "decodeWireBody", "encodeWireBody", "WebRtcTransport", "options", "listener", "peerUrl", "session", "request", "operation", "changed", "signal", "error", "remove", "connections", "sessions", "url", "pc", "config", "onTrack", "event", "trackEvent", "onState", "onClose", "onMessage", "track", "sender", "kind", "transceiver", "item", "active", "offer", "response", "answer", "message", "parsed", "finish", "timeout", "resolve", "reject", "requestId", "onAbort", "disposal", "cleanup", "controller", "timer", "label", "task", "sources", "source", "promise", "value", "target", "events", "ready", "failed", "check"]
|
|
7
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { WebRtcTransport } from "./webrtc.js";
|
|
2
|
+
export type { WebRtcTransportOptions, WebRtcTransportServer, WebRtcServerSessionView, } from "./webrtc.js";
|
|
3
|
+
export type { WebRtcSessionView, WebRtcTrackEvent, WebRtcMediaController, WebRtcTransportRequest, } from "./types/types.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["export { WebRtcTransport } from \"./webrtc.js\";\nexport type {\n WebRtcTransportOptions,\n WebRtcTransportServer,\n WebRtcServerSessionView,\n} from \"./webrtc.js\";\nexport type {\n WebRtcSessionView,\n WebRtcTrackEvent,\n WebRtcMediaController,\n WebRtcTransportRequest,\n} from \"./types/types.js\";\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,mBAAAA,MAAuB",
|
|
6
|
+
"names": ["WebRtcTransport"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MediaStream } from "werift";
|
|
2
|
+
export interface PcmAudioSubscription {
|
|
3
|
+
stream: MediaStream;
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
export interface PcmAudioSource {
|
|
7
|
+
subscribe(): PcmAudioSubscription;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
/** Resolves on EOF or explicit close; rejects on input, socket, or encoder failure. */
|
|
10
|
+
done: Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface PcmAudioOptions {
|
|
13
|
+
ffmpegPath?: string;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Optional server-only helper. Requires an external FFmpeg executable with libopus
|
|
18
|
+
* on PATH (or ffmpegPath). Input is raw 48 kHz, stereo, signed 16-bit little-endian
|
|
19
|
+
* 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
|
|
21
|
+
* IPv4 loopback. No peer connection or native wrtc dependency is created.
|
|
22
|
+
*
|
|
23
|
+
* Writes are bounded to 16 KiB, with one outstanding write and one input chunk.
|
|
24
|
+
* Producers must bound their own chunks/queues. Closing requests iterator.return()
|
|
25
|
+
* or reader.cancel(); arbitrary iterators must cooperate to release their resources
|
|
26
|
+
* (a stuck next()/return() does not prevent this helper from closing).
|
|
27
|
+
*/
|
|
28
|
+
export declare function createPcmAudioSource(input: AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>, options?: PcmAudioOptions): Promise<PcmAudioSource>;
|
|
29
|
+
//# sourceMappingURL=pcm-audio.d.ts.map
|
|
@@ -0,0 +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,CAuPzB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
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";async function F(w,s={}){s.signal?.throwIfAborted();const c=I("udp4");try{await new Promise((e,r)=>{c.once("error",r),c.bind(0,"127.0.0.1",()=>{c.removeListener("error",r),e()})}),s.signal?.throwIfAborted()}catch(e){try{c.close()}catch{}throw e}const g=c.address().port,o=_(s.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"]}),f=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(l=>{d.delete(t),r(l)},l=>{d.delete(t),i(l)}),n&&(d.delete(t),t())})}const C=new Promise(e=>o.once("close",()=>e()));let u;function a(e){if(u)return u;n=!0;for(const r of d)r();d.clear(),s.signal?.removeEventListener("abort",b);try{Promise.resolve(h?.()).catch(()=>{})}catch{}for(const r of f)r.stop();return f.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([C,new Promise(i=>c.close(()=>i()))])}finally{clearTimeout(r)}})(),u.then(()=>e===void 0?P():y(e),y),u}function b(){a(s.signal?.reason??new Error("PCM audio source aborted"))}c.on("error",e=>{a(e)}),o.stdin.on("error",e=>{n||a(e)}),o.stderr.on("data",e=>{m=(m+e.toString()).slice(-8192)}),o.on("error",e=>{a(e)}),o.on("close",(e,r)=>{n||(e===0&&k?a():a(new Error(`FFmpeg exited (${r??e}): ${m.trim()}`)))}),c.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 f)t.stopped||t.writeRtp(i)}),s.signal?.addEventListener("abort",b,{once:!0}),s.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 a(e),e}async function L(){let e;try{let r;if("getReader"in w){const t=w.getReader();r={next:()=>t.read()},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 l=t.value;i=(i+l.byteLength)%4;for(let p=0;p<l.byteLength&&!n;p+=16384)await S(new Promise((M,T)=>{o.stdin.write(l.subarray(p,p+16384),E=>E?T(E):M())}))}}catch(r){n||a(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),f.add(e),{stream:r,close(){f.delete(e),e.stop()}}},close:()=>a()}}export{F as createPcmAudioSource};
|
|
2
|
+
//# sourceMappingURL=pcm-audio.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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 = { next: () => reader.read() };\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", "createPcmAudioSource", "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", "bytes", "result", "offset", "stream"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { LogLevel, TransportRequest, TransportResponse } from "@hyperttp/types";
|
|
2
|
+
import type { MediaStream as WeriftMediaStream, RTCPeerConnection as WeriftPeerConnection, RTCDataChannel as WeriftDataChannel, MediaStreamTrack as WeriftMediaStreamTrack } from "werift";
|
|
3
|
+
export declare const WEBRTC_BYTES_MARKER = "__hyperttp_webrtc_bytes_v1__";
|
|
4
|
+
export declare const MAX_SIGNALING_BODY_BYTES = 1000000;
|
|
5
|
+
export type EncodedWebRtcBytes = {
|
|
6
|
+
[WEBRTC_BYTES_MARKER]: true;
|
|
7
|
+
base64: string;
|
|
8
|
+
};
|
|
9
|
+
export type WebRtcAction = "connect" | "stream" | "datachannel";
|
|
10
|
+
export type WebRtcTransportRequest = TransportRequest & {
|
|
11
|
+
peerUrl?: string;
|
|
12
|
+
action?: WebRtcAction;
|
|
13
|
+
iceServers?: RTCIceServer[];
|
|
14
|
+
localStream?: WeriftMediaStream;
|
|
15
|
+
receiveAudio?: boolean;
|
|
16
|
+
receiveVideo?: boolean;
|
|
17
|
+
channelLabel?: string;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
};
|
|
20
|
+
export interface WebRtcTrackEvent {
|
|
21
|
+
readonly side: "client" | "server";
|
|
22
|
+
readonly sessionId: string;
|
|
23
|
+
readonly peerUrl?: string;
|
|
24
|
+
readonly peerConnection: WeriftPeerConnection;
|
|
25
|
+
readonly track: WeriftMediaStreamTrack;
|
|
26
|
+
readonly stream: WeriftMediaStream;
|
|
27
|
+
readonly streams: readonly WeriftMediaStream[];
|
|
28
|
+
}
|
|
29
|
+
export interface WebRtcMediaController {
|
|
30
|
+
readonly localStream: WeriftMediaStream | undefined;
|
|
31
|
+
readonly remoteStream: WeriftMediaStream;
|
|
32
|
+
addTrack(track: WeriftMediaStreamTrack, stream?: WeriftMediaStream): Promise<void>;
|
|
33
|
+
replaceTrack(kind: "audio" | "video", track: WeriftMediaStreamTrack | null): Promise<void>;
|
|
34
|
+
removeTrack(track: WeriftMediaStreamTrack): Promise<void>;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
on(event: "track", listener: (e: {
|
|
37
|
+
track: WeriftMediaStreamTrack;
|
|
38
|
+
stream: WeriftMediaStream;
|
|
39
|
+
}) => void): () => void;
|
|
40
|
+
on(event: "trackEnded", listener: (e: {
|
|
41
|
+
track: WeriftMediaStreamTrack;
|
|
42
|
+
}) => void): () => void;
|
|
43
|
+
on(event: "stream", listener: (e: {
|
|
44
|
+
stream: WeriftMediaStream;
|
|
45
|
+
}) => void): () => void;
|
|
46
|
+
}
|
|
47
|
+
export interface WebRtcSessionView {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly peerUrl: string;
|
|
50
|
+
readonly peerConnection: WeriftPeerConnection;
|
|
51
|
+
readonly dataChannel: WeriftDataChannel;
|
|
52
|
+
readonly remoteStream: WeriftMediaStream;
|
|
53
|
+
readonly localStream?: WeriftMediaStream;
|
|
54
|
+
readonly media: WebRtcMediaController;
|
|
55
|
+
}
|
|
56
|
+
export interface WebRtcTransportOptions {
|
|
57
|
+
readonly iceServers?: RTCIceServer[];
|
|
58
|
+
readonly iceGatheringTimeoutMs?: number;
|
|
59
|
+
readonly signalingTimeoutMs?: number;
|
|
60
|
+
readonly connectionTimeoutMs?: number;
|
|
61
|
+
readonly requestTimeoutMs?: number;
|
|
62
|
+
readonly logger?: (level: LogLevel, message: string, meta?: unknown) => void;
|
|
63
|
+
readonly verbose?: boolean;
|
|
64
|
+
readonly createPeerConnection?: (configuration: RTCConfiguration) => WeriftPeerConnection;
|
|
65
|
+
readonly createMediaStream?: () => WeriftMediaStream;
|
|
66
|
+
readonly disposeLocalStream?: (sessionId: string, stream: WeriftMediaStream) => void;
|
|
67
|
+
readonly localStream?: WeriftMediaStream | ((sessionId: string) => WeriftMediaStream | undefined);
|
|
68
|
+
}
|
|
69
|
+
export interface PendingResponse {
|
|
70
|
+
resolve: (response: TransportResponse) => void;
|
|
71
|
+
reject: (error: Error) => void;
|
|
72
|
+
timer: ReturnType<typeof setTimeout>;
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
abortListener?: () => void;
|
|
75
|
+
}
|
|
76
|
+
export interface ClientSession {
|
|
77
|
+
id: string;
|
|
78
|
+
peerUrl: string;
|
|
79
|
+
signalingUrl: string;
|
|
80
|
+
peerConnection: WeriftPeerConnection;
|
|
81
|
+
dataChannel: WeriftDataChannel;
|
|
82
|
+
remoteStream: WeriftMediaStream;
|
|
83
|
+
localStream?: WeriftMediaStream;
|
|
84
|
+
localTrackIds: Set<string>;
|
|
85
|
+
pending: Map<string, PendingResponse>;
|
|
86
|
+
negotiation: Promise<void>;
|
|
87
|
+
closed: boolean;
|
|
88
|
+
mediaController: WebRtcMediaController;
|
|
89
|
+
}
|
|
90
|
+
export interface ServerSession {
|
|
91
|
+
negotiating?: boolean;
|
|
92
|
+
id: string;
|
|
93
|
+
peerConnection: WeriftPeerConnection;
|
|
94
|
+
remoteStream: WeriftMediaStream;
|
|
95
|
+
localTrackIds: Set<string>;
|
|
96
|
+
localStream?: WeriftMediaStream;
|
|
97
|
+
localStreamResolved: boolean;
|
|
98
|
+
ownsLocalStream: boolean;
|
|
99
|
+
closed: boolean;
|
|
100
|
+
}
|
|
101
|
+
export interface RTCIceServer {
|
|
102
|
+
credential?: string;
|
|
103
|
+
urls: string | string[];
|
|
104
|
+
username?: string;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,KAAK,EACV,WAAW,IAAI,iBAAiB,EAChC,iBAAiB,IAAI,oBAAoB,EACzC,cAAc,IAAI,iBAAiB,EACnC,gBAAgB,IAAI,sBAAsB,EAC3C,MAAM,QAAQ,CAAC;AAEhB,eAAO,MAAM,mBAAmB,iCAAiC,CAAC;AAClE,eAAO,MAAM,wBAAwB,UAAY,CAAC;AAElD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEhE,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAChD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IAEzC,QAAQ,CAAC,KAAK,EAAE,sBAAsB,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,KAAK,EAAE,sBAAsB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3F,WAAW,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,EAAE,CACA,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,sBAAsB,CAAC;QAAC,MAAM,EAAE,iBAAiB,CAAA;KAAE,KAAK,IAAI,GAClF,MAAM,IAAI,CAAC;IACd,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,sBAAsB,CAAA;KAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC9F,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE;QAAE,MAAM,EAAE,iBAAiB,CAAA;KAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACvF;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IACrC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,aAAa,EAAE,gBAAgB,KAAK,oBAAoB,CAAC;IAC1F,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,iBAAiB,CAAC;IACrD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrF,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC,CAAC;CACnG;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC/C,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,oBAAoB,CAAC;IACrC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,YAAY,EAAE,iBAAiB,CAAC;IAChC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACtC,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,qBAAqB,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,oBAAoB,CAAC;IACrC,YAAY,EAAE,iBAAiB,CAAC;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/types/types.ts"],
|
|
4
|
+
"sourcesContent": ["import type { LogLevel, TransportRequest, TransportResponse } from \"@hyperttp/types\";\nimport type {\n MediaStream as WeriftMediaStream,\n RTCPeerConnection as WeriftPeerConnection,\n RTCDataChannel as WeriftDataChannel,\n MediaStreamTrack as WeriftMediaStreamTrack,\n} from \"werift\";\n\nexport const WEBRTC_BYTES_MARKER = \"__hyperttp_webrtc_bytes_v1__\";\nexport const MAX_SIGNALING_BODY_BYTES = 1_000_000;\n\nexport type EncodedWebRtcBytes = {\n [WEBRTC_BYTES_MARKER]: true;\n base64: string;\n};\n\nexport type WebRtcAction = \"connect\" | \"stream\" | \"datachannel\";\n\nexport type WebRtcTransportRequest = TransportRequest & {\n peerUrl?: string;\n action?: WebRtcAction;\n iceServers?: RTCIceServer[];\n localStream?: WeriftMediaStream;\n receiveAudio?: boolean;\n receiveVideo?: boolean;\n channelLabel?: string;\n timeout?: number;\n};\n\nexport interface WebRtcTrackEvent {\n readonly side: \"client\" | \"server\";\n readonly sessionId: string;\n readonly peerUrl?: string;\n readonly peerConnection: WeriftPeerConnection;\n readonly track: WeriftMediaStreamTrack;\n readonly stream: WeriftMediaStream;\n readonly streams: readonly WeriftMediaStream[];\n}\n\nexport interface WebRtcMediaController {\n readonly localStream: WeriftMediaStream | undefined;\n readonly remoteStream: WeriftMediaStream;\n\n addTrack(track: WeriftMediaStreamTrack, stream?: WeriftMediaStream): Promise<void>;\n replaceTrack(kind: \"audio\" | \"video\", track: WeriftMediaStreamTrack | null): Promise<void>;\n removeTrack(track: WeriftMediaStreamTrack): Promise<void>;\n close(): Promise<void>;\n\n on(\n event: \"track\",\n listener: (e: { track: WeriftMediaStreamTrack; stream: WeriftMediaStream }) => void,\n ): () => void;\n on(event: \"trackEnded\", listener: (e: { track: WeriftMediaStreamTrack }) => void): () => void;\n on(event: \"stream\", listener: (e: { stream: WeriftMediaStream }) => void): () => void;\n}\n\nexport interface WebRtcSessionView {\n readonly id: string;\n readonly peerUrl: string;\n readonly peerConnection: WeriftPeerConnection;\n readonly dataChannel: WeriftDataChannel;\n readonly remoteStream: WeriftMediaStream;\n readonly localStream?: WeriftMediaStream;\n readonly media: WebRtcMediaController;\n}\n\nexport interface WebRtcTransportOptions {\n readonly iceServers?: RTCIceServer[];\n readonly iceGatheringTimeoutMs?: number;\n readonly signalingTimeoutMs?: number;\n readonly connectionTimeoutMs?: number;\n readonly requestTimeoutMs?: number;\n readonly logger?: (level: LogLevel, message: string, meta?: unknown) => void;\n readonly verbose?: boolean;\n readonly createPeerConnection?: (configuration: RTCConfiguration) => WeriftPeerConnection;\n readonly createMediaStream?: () => WeriftMediaStream;\n readonly disposeLocalStream?: (sessionId: string, stream: WeriftMediaStream) => void;\n readonly localStream?: WeriftMediaStream | ((sessionId: string) => WeriftMediaStream | undefined);\n}\n\nexport interface PendingResponse {\n resolve: (response: TransportResponse) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n signal?: AbortSignal;\n abortListener?: () => void;\n}\n\nexport interface ClientSession {\n id: string;\n peerUrl: string;\n signalingUrl: string;\n peerConnection: WeriftPeerConnection;\n dataChannel: WeriftDataChannel;\n remoteStream: WeriftMediaStream;\n localStream?: WeriftMediaStream;\n localTrackIds: Set<string>;\n pending: Map<string, PendingResponse>;\n negotiation: Promise<void>;\n closed: boolean;\n mediaController: WebRtcMediaController;\n}\n\nexport interface ServerSession {\n negotiating?: boolean;\n id: string;\n peerConnection: WeriftPeerConnection;\n remoteStream: WeriftMediaStream;\n localTrackIds: Set<string>;\n localStream?: WeriftMediaStream;\n localStreamResolved: boolean;\n ownsLocalStream: boolean;\n closed: boolean;\n}\n\nexport interface RTCIceServer {\n credential?: string;\n urls: string | string[];\n username?: string;\n}\n"],
|
|
5
|
+
"mappings": "AAQO,MAAMA,EAAsB,+BACtBC,EAA2B",
|
|
6
|
+
"names": ["WEBRTC_BYTES_MARKER", "MAX_SIGNALING_BODY_BYTES"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
2
|
+
export declare function base64ToBytes(base64: string): Uint8Array;
|
|
3
|
+
export declare function encodeWireBody(body: unknown): unknown;
|
|
4
|
+
export declare function decodeWireBody(body: unknown): unknown;
|
|
5
|
+
export declare function errorFromUnknown(error: unknown, fallback: string): Error;
|
|
6
|
+
export declare function abortError(signal?: AbortSignal): Error;
|
|
7
|
+
export declare function countIceCandidates(sdp: string | undefined): number;
|
|
8
|
+
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../../src/utils/helper.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CASvD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMxD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAmBrD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAWrD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAExE;AAED,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,KAAK,CAGtD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAElE"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{WEBRTC_BYTES_MARKER as o}from"../types/types.js";function i(e){let r="";for(let n=0;n<e.length;n+=32768)r+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(r)}function f(e){const r=atob(e),t=new Uint8Array(r.length);for(let n=0;n<r.length;n++)t[n]=r.charCodeAt(n);return t}function c(e){let r;return e instanceof Uint8Array?r=e:e instanceof ArrayBuffer?r=new Uint8Array(e):ArrayBuffer.isView(e)&&(r=new Uint8Array(e.buffer,e.byteOffset,e.byteLength)),r?{[o]:!0,base64:i(r)}:e}function u(e){if(typeof e!="object"||e===null||!(o in e))return e;const r=e;return r[o]!==!0||typeof r.base64!="string"?e:f(r.base64)}function s(e,r){return e instanceof Error?e:new Error(r)}function h(e){return e?.reason instanceof Error?e.reason:new Error("WebRTC operation aborted")}function l(e){return e?(e.match(/^a=candidate:/gm)??[]).length:0}export{h as abortError,f as base64ToBytes,i as bytesToBase64,l as countIceCandidates,u as decodeWireBody,c as encodeWireBody,s as errorFromUnknown};
|
|
2
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/helper.ts"],
|
|
4
|
+
"sourcesContent": ["import { WEBRTC_BYTES_MARKER, type EncodedWebRtcBytes } from \"../types/types.js\";\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\";\n const chunkSize = 0x8000;\n\n for (let offset = 0; offset < bytes.length; offset += chunkSize) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));\n }\n\n return btoa(binary);\n}\n\nexport function base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\nexport function encodeWireBody(body: unknown): unknown {\n let bytes: Uint8Array | undefined;\n\n if (body instanceof Uint8Array) {\n bytes = body;\n } else if (body instanceof ArrayBuffer) {\n bytes = new Uint8Array(body);\n } else if (ArrayBuffer.isView(body)) {\n bytes = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);\n }\n\n if (!bytes) return body;\n\n const encoded: EncodedWebRtcBytes = {\n [WEBRTC_BYTES_MARKER]: true,\n base64: bytesToBase64(bytes),\n };\n\n return encoded;\n}\n\nexport function decodeWireBody(body: unknown): unknown {\n if (typeof body !== \"object\" || body === null || !(WEBRTC_BYTES_MARKER in body)) {\n return body;\n }\n\n const encoded = body as Partial<EncodedWebRtcBytes>;\n if (encoded[WEBRTC_BYTES_MARKER] !== true || typeof encoded.base64 !== \"string\") {\n return body;\n }\n\n return base64ToBytes(encoded.base64);\n}\n\nexport function errorFromUnknown(error: unknown, fallback: string): Error {\n return error instanceof Error ? error : new Error(fallback);\n}\n\nexport function abortError(signal?: AbortSignal): Error {\n if (signal?.reason instanceof Error) return signal.reason;\n return new Error(\"WebRTC operation aborted\");\n}\n\nexport function countIceCandidates(sdp: string | undefined): number {\n return sdp ? (sdp.match(/^a=candidate:/gm) ?? []).length : 0;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,uBAAAA,MAAoD,oBAEvD,SAAUC,EAAcC,EAAiB,CAC7C,IAAIC,EAAS,GAGb,QAASC,EAAS,EAAGA,EAASF,EAAM,OAAQE,GAAU,MACpDD,GAAU,OAAO,aAAa,GAAGD,EAAM,SAASE,EAAQA,EAAS,KAAS,CAAC,EAG7E,OAAO,KAAKD,CAAM,CACpB,CAEM,SAAUE,EAAcC,EAAc,CAC1C,MAAMH,EAAS,KAAKG,CAAM,EACpBJ,EAAQ,IAAI,WAAWC,EAAO,MAAM,EAE1C,QAASI,EAAI,EAAGA,EAAIJ,EAAO,OAAQI,IAAKL,EAAMK,CAAC,EAAIJ,EAAO,WAAWI,CAAC,EACtE,OAAOL,CACT,CAEM,SAAUM,EAAeC,EAAa,CAC1C,IAAIP,EAUJ,OARIO,aAAgB,WAClBP,EAAQO,EACCA,aAAgB,YACzBP,EAAQ,IAAI,WAAWO,CAAI,EAClB,YAAY,OAAOA,CAAI,IAChCP,EAAQ,IAAI,WAAWO,EAAK,OAAQA,EAAK,WAAYA,EAAK,UAAU,GAGjEP,EAE+B,CAClC,CAACF,CAAmB,EAAG,GACvB,OAAQC,EAAcC,CAAK,GAJVO,CAQrB,CAEM,SAAUC,EAAeD,EAAa,CAC1C,GAAI,OAAOA,GAAS,UAAYA,IAAS,MAAQ,EAAET,KAAuBS,GACxE,OAAOA,EAGT,MAAME,EAAUF,EAChB,OAAIE,EAAQX,CAAmB,IAAM,IAAQ,OAAOW,EAAQ,QAAW,SAC9DF,EAGFJ,EAAcM,EAAQ,MAAM,CACrC,CAEM,SAAUC,EAAiBC,EAAgBC,EAAgB,CAC/D,OAAOD,aAAiB,MAAQA,EAAQ,IAAI,MAAMC,CAAQ,CAC5D,CAEM,SAAUC,EAAWC,EAAoB,CAC7C,OAAIA,GAAQ,kBAAkB,MAAcA,EAAO,OAC5C,IAAI,MAAM,0BAA0B,CAC7C,CAEM,SAAUC,EAAmBC,EAAuB,CACxD,OAAOA,GAAOA,EAAI,MAAM,iBAAiB,GAAK,CAAA,GAAI,OAAS,CAC7D",
|
|
6
|
+
"names": ["WEBRTC_BYTES_MARKER", "bytesToBase64", "bytes", "binary", "offset", "base64ToBytes", "base64", "i", "encodeWireBody", "body", "decodeWireBody", "encoded", "errorFromUnknown", "error", "fallback", "abortError", "signal", "countIceCandidates", "sdp"]
|
|
7
|
+
}
|
package/dist/webrtc.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { HyperTransport, LogLevel, TransportListenOptions, TransportRequest, TransportResponse, TransportServer } from "@hyperttp/types";
|
|
2
|
+
import { type WebRtcSessionView, type WebRtcTrackEvent, type WebRtcTransportOptions as InternalWebRtcTransportOptions } from "./types/types.js";
|
|
3
|
+
import { MediaStream as WeriftMediaStream, RTCPeerConnection as WeriftPeerConnection } from "werift";
|
|
4
|
+
declare module "@hyperttp/types" {
|
|
5
|
+
interface TransportServer {
|
|
6
|
+
readonly webrtc?: Pick<WebRtcTransport, "getSession" | "getServerSession" | "onTrack">;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export type WebRtcTransportOptions = Omit<InternalWebRtcTransportOptions, "localStream" | "disposeLocalStream" | "createMediaStream"> & {
|
|
10
|
+
readonly localStream?: WeriftMediaStream | ((sessionId: string) => WeriftMediaStream | undefined);
|
|
11
|
+
readonly disposeLocalStream?: (sessionId: string, stream: WeriftMediaStream) => void;
|
|
12
|
+
readonly createMediaStream?: () => WeriftMediaStream;
|
|
13
|
+
readonly logger?: (level: LogLevel, message: string, meta?: unknown) => void;
|
|
14
|
+
};
|
|
15
|
+
export interface WebRtcServerSessionView {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly peerConnection: WeriftPeerConnection;
|
|
18
|
+
readonly remoteStream: WeriftMediaStream;
|
|
19
|
+
readonly localStream?: WeriftMediaStream;
|
|
20
|
+
}
|
|
21
|
+
export interface WebRtcTransportServer extends TransportServer {
|
|
22
|
+
readonly webrtc: Pick<WebRtcTransport, "getSession" | "getServerSession" | "onTrack">;
|
|
23
|
+
}
|
|
24
|
+
export declare class WebRtcTransport implements HyperTransport {
|
|
25
|
+
private readonly sessions;
|
|
26
|
+
private readonly connecting;
|
|
27
|
+
private readonly serverSessions;
|
|
28
|
+
private readonly trackListeners;
|
|
29
|
+
private readonly serverClosers;
|
|
30
|
+
private requestSequence;
|
|
31
|
+
private closing;
|
|
32
|
+
private readonly pendingPeers;
|
|
33
|
+
private readonly iceServers;
|
|
34
|
+
private readonly iceGatheringTimeoutMs;
|
|
35
|
+
private readonly signalingTimeoutMs;
|
|
36
|
+
private readonly connectionTimeoutMs;
|
|
37
|
+
private readonly requestTimeoutMs;
|
|
38
|
+
readonly createMediaStream: () => WeriftMediaStream;
|
|
39
|
+
private readonly createPeerConnection;
|
|
40
|
+
private readonly options;
|
|
41
|
+
constructor(options?: WebRtcTransportOptions);
|
|
42
|
+
private log;
|
|
43
|
+
onTrack(listener: (event: WebRtcTrackEvent) => void): () => void;
|
|
44
|
+
getServerSession(sessionId: string): WebRtcServerSessionView | undefined;
|
|
45
|
+
getSession(peerUrl: string): WebRtcSessionView | undefined;
|
|
46
|
+
execute(request: TransportRequest): Promise<TransportResponse>;
|
|
47
|
+
listen(options: TransportListenOptions): Promise<WebRtcTransportServer>;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
destroy(): Promise<void>;
|
|
50
|
+
private getOrCreateSession;
|
|
51
|
+
private createClientSession;
|
|
52
|
+
private attachClientPeerEvents;
|
|
53
|
+
private queueNegotiation;
|
|
54
|
+
private ensureMedia;
|
|
55
|
+
private addLocalTracks;
|
|
56
|
+
private addReceiveTransceivers;
|
|
57
|
+
private negotiate;
|
|
58
|
+
private setRemoteDescription;
|
|
59
|
+
private withDeadline;
|
|
60
|
+
private waitForIceGatheringComplete;
|
|
61
|
+
private waitForChannelOpen;
|
|
62
|
+
private sendRequest;
|
|
63
|
+
private finishPending;
|
|
64
|
+
private rejectAllPending;
|
|
65
|
+
private disposeClientSession;
|
|
66
|
+
private attachServerPeerEvents;
|
|
67
|
+
private attachConfiguredServerStream;
|
|
68
|
+
private disposeServerLocalStream;
|
|
69
|
+
private closeServerSession;
|
|
70
|
+
private emitTrack;
|
|
71
|
+
private toSignalingUrl;
|
|
72
|
+
private createId;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=webrtc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webrtc.d.ts","sourceRoot":"","sources":["../src/webrtc.ts"],"names":[],"mappings":"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"}
|
package/dist/webrtc.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{MAX_SIGNALING_BODY_BYTES as W}from"./types/types.js";import{abortError as y,decodeWireBody as P,encodeWireBody as k,errorFromUnknown as b}from"./utils/helper.js";import{createServer as M}from"node:http";import{MediaStream as D,RTCPeerConnection as A}from"werift";function O(v){return(v??[]).flatMap(t=>{const r=Array.isArray(t.urls)?t.urls:[t.urls];if(t.credential!==void 0&&typeof t.credential!="string")throw new TypeError("Werift currently requires ICE server credentials to be strings.");return r.map(o=>({urls:o,...t.username!==void 0?{username:t.username}:{},...t.credential!==void 0?{credential:t.credential}:{}}))})}function x(v){const e={iceServers:O(v.iceServers),...v.iceTransportPolicy!==void 0?{iceTransportPolicy:v.iceTransportPolicy}:{}};return new A(e)}function $(){return new D}class F{sessions=new Map;connecting=new Map;serverSessions=new Map;trackListeners=new Set;serverClosers=new Set;requestSequence=0;closing=!1;pendingPeers=new Set;iceServers;iceGatheringTimeoutMs;signalingTimeoutMs;connectionTimeoutMs;requestTimeoutMs;createMediaStream;createPeerConnection;options;constructor(e={}){this.options=e,this.createPeerConnection=e.createPeerConnection??x,this.createMediaStream=e.createMediaStream??$,this.iceServers=e.iceServers??[{urls:"stun:stun.l.google.com:19302"}],this.iceGatheringTimeoutMs=e.iceGatheringTimeoutMs??1e4,this.signalingTimeoutMs=e.signalingTimeoutMs??1e4,this.connectionTimeoutMs=e.connectionTimeoutMs??15e3,this.requestTimeoutMs=e.requestTimeoutMs??15e3}log(e,t,r){if(this.options.logger){this.options.logger(e,t,r);return}if(!this.options.verbose&&e!=="error")return;const o=e==="debug"?console.debug:console[e];r===void 0?o(t):o(t,r)}onTrack(e){return this.trackListeners.add(e),()=>this.trackListeners.delete(e)}getServerSession(e){const t=this.serverSessions.get(e);if(!(!t||t.closed))return{id:t.id,peerConnection:t.peerConnection,remoteStream:t.remoteStream,...t.localStream?{localStream:t.localStream}:{}}}getSession(e){const t=this.sessions.get(e);if(!(!t||t.closed))return{id:t.id,peerUrl:t.peerUrl,peerConnection:t.peerConnection,dataChannel:t.dataChannel,remoteStream:t.remoteStream,media:t.mediaController,...t.localStream?{localStream:t.localStream}:{}}}async execute(e){if(this.closing)throw new Error("WebRTC transport is closed");const t=e,r=t.peerUrl??e.url;if(e.signal?.aborted)throw y(e.signal);const o=await this.getOrCreateSession(r,t);if(await this.ensureMedia(o,t),e.signal?.aborted)throw y(e.signal);return this.sendRequest(o,e,t.timeout)}async listen(e){if(this.closing)throw new Error("WebRTC transport is closed");const t=new Set,r=e.port??3e3,o=e.host??"127.0.0.1",n=e.signaling?.path??"/signal";if(!n.startsWith("/")||n.startsWith("//")||/[?#\\\s]/u.test(n)||Array.from(n).some(a=>{const l=a.codePointAt(0);return l<=31||l>=127&&l<=159})||new URL(n,"http://localhost").pathname!==n)throw new TypeError("WebRTC signaling path must be an absolute literal pathname without query or fragment");if(e.signal?.aborted)throw new Error("WebRtcTransport: server startup aborted");let s=!1,i;const c=new Set,d=M((a,l)=>{(async()=>{const m=(h,S)=>{l.headersSent||l.destroyed||(l.writeHead(h,{"content-type":"application/json"}),l.end(JSON.stringify(S)))};if(s||this.closing){m(503,{error:"WebRTC server is closing"});return}let w;try{w=new URL(a.url??"/","http://localhost")}catch{m(400,{error:"Invalid request URL"});return}if(w.pathname!==n){m(404,{error:"Signaling endpoint not found"});return}if((a.method==="POST"||a.method==="DELETE")&&e.signaling?.authorize){const h=new AbortController,S=()=>h.abort(),E=()=>{m(503,{error:"WebRTC signaling request aborted"})};a.once("aborted",S),l.once("close",S),h.signal.addEventListener("abort",E,{once:!0}),c.add(h);try{const T={};for(const[I,L]of Object.entries(a.headers))L!==void 0&&(T[I]=L);const R=await e.signaling.authorize({protocol:"webrtc",method:a.method,url:a.url??"/",headers:T,signal:h.signal});if(h.signal.aborted||a.aborted||l.destroyed)return;if(s||this.closing||e.signal?.aborted){m(503,{error:"WebRTC server is closing"});return}if(!R){m(403,{error:"Signaling forbidden"});return}}catch{m(403,{error:"Signaling forbidden"});return}finally{c.delete(h),a.off("aborted",S),l.off("close",S),h.signal.removeEventListener("abort",E)}}if(a.method==="DELETE"){const h=w.searchParams.get("sessionId");h&&t.has(h)&&(await this.closeServerSession(h),t.delete(h)),l.writeHead(204).end();return}if(a.method!=="POST"){m(405,{error:"Method not allowed"});return}let C="";try{for await(const h of a)if(C+=Buffer.isBuffer(h)?h.toString("utf8"):String(h),Buffer.byteLength(C,"utf8")>W){m(413,{error:"Signaling payload too large"});return}}catch(h){m(400,{error:b(h,"Failed to read body").message});return}let p,u;try{const h=JSON.parse(C);if(typeof h.sessionId!="string"||h.sessionId.length<1||h.sessionId.length>200||h.sdp?.type!=="offer"||typeof h.sdp.sdp!="string"){m(400,{error:"Expected sessionId and a valid SDP offer"});return}if(s||this.closing||e.signal?.aborted||a.aborted||l.destroyed)throw new Error("WebRTC signaling request aborted");const S=h.sessionId;if(u=this.serverSessions.get(S),u&&!t.has(S)){m(409,{error:"Session belongs to another listener"});return}if(u?.negotiating){m(409,{error:"Session negotiation already in progress"});return}!u||u.closed?(p=this.createPeerConnection({iceServers:this.iceServers}),u={id:S,peerConnection:p,remoteStream:this.createMediaStream(),localTrackIds:new Set,localStreamResolved:!1,ownsLocalStream:!1,closed:!1},t.add(S),this.serverSessions.set(S,u),this.attachServerPeerEvents(u,e.onRequest)):p=u.peerConnection,u.negotiating=!0,await this.setRemoteDescription(p,h.sdp),await this.attachConfiguredServerStream(u);const E=await p.createAnswer();await this.withDeadline(p.setLocalDescription(E),this.iceGatheringTimeoutMs),await this.waitForIceGatheringComplete(p,this.iceGatheringTimeoutMs);const T=p.localDescription;if(!T?.sdp||T.type!=="answer")throw new Error("Local SDP answer was not generated");if(s||u.closed)throw new Error("WebRTC session closed");u.negotiating=!1,m(200,T)}catch(h){if(this.log("error","[WebRtcTransport Server] \u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0438\u0433\u043D\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438:",h),u)await this.closeServerSession(u.id);else if(p)try{await p.close()}catch{}m(400,{error:b(h,"Signaling failed").message})}})()});await new Promise((a,l)=>{const m=C=>{d.off("listening",w),l(C)},w=()=>{d.off("error",m),a()};d.once("error",m),d.once("listening",w),d.listen(r,o)}),this.log("info",`[WebRtcTransport] Listening on http://${o}:${r}${n}`);const g=()=>{f().catch(a=>{this.log("error","[WebRtcTransport] Server shutdown failed:",a)})},f=()=>{if(i)return i;s=!0;for(const a of c)a.abort();return e.signal?.removeEventListener("abort",g),i=(async()=>{const a=d.listening?new Promise((l,m)=>{d.close(w=>w?m(w):l())}):Promise.resolve();await Promise.all([...t].map(l=>this.closeServerSession(l))),t.clear(),await a,this.serverClosers.delete(f)})(),i};return e.signal?.addEventListener("abort",g,{once:!0}),this.serverClosers.add(f),e.signal?.aborted&&!s&&await f(),{close:f,webrtc:{getSession:this.getSession.bind(this),getServerSession:this.getServerSession.bind(this),onTrack:this.onTrack.bind(this)}}}async close(){this.closing=!0,await Promise.all([...this.pendingPeers].map(e=>e.close().catch(()=>{}))),await Promise.allSettled(this.connecting.values()),await Promise.all([...this.sessions.values()].map(e=>this.disposeClientSession(e,!0))),await Promise.all([...this.serverClosers].map(e=>e()))}async destroy(){await this.close()}async getOrCreateSession(e,t){const r=this.sessions.get(e);if(r&&!r.closed&&r.dataChannel.readyState==="open"&&r.peerConnection.connectionState!=="failed"&&r.peerConnection.connectionState!=="closed")return r;r&&await this.disposeClientSession(r,!0);const o=this.connecting.get(e);if(o)return o;const n=this.createClientSession(e,t);this.connecting.set(e,n);try{const s=await n;if(this.closing||s.closed)throw await this.disposeClientSession(s,!0),new Error("WebRTC transport is closed");return this.sessions.set(e,s),s}finally{this.connecting.get(e)===n&&this.connecting.delete(e)}}async createClientSession(e,t){const r=this.toSignalingUrl(e),o=this.createId("session"),n=this.createPeerConnection({iceServers:t.iceServers??this.iceServers});this.pendingPeers.add(n);const s=n.createDataChannel(t.channelLabel??"hyperttp-webrtc-transport",{ordered:!0}),i={id:o,peerUrl:e,signalingUrl:r,peerConnection:n,dataChannel:s,remoteStream:this.createMediaStream(),...t.localStream?{localStream:t.localStream}:{},localTrackIds:new Set,pending:new Map,negotiation:Promise.resolve(),closed:!1,mediaController:{}},c=new Set,d=new Set,g=new Set;n.addEventListener("track",f=>{const{track:a}=f;i.remoteStream.getTracks().includes(a)||i.remoteStream.addTrack(a);for(const l of c)l({track:a,stream:i.remoteStream});for(const l of g)l({stream:i.remoteStream});a.addEventListener("ended",()=>{for(const l of d)l({track:a})})}),i.mediaController={localStream:i.localStream,remoteStream:i.remoteStream,addTrack:(f,a)=>this.queueNegotiation(i,async()=>{n.addTrack(f,a??i.localStream??this.createMediaStream()),f.id&&i.localTrackIds.add(f.id),await this.negotiate(i)}),replaceTrack:async(f,a)=>{const l=n.getSenders().find(m=>m.track&&m.track.kind===f);l&&await l.replaceTrack(a)},removeTrack:f=>this.queueNegotiation(i,async()=>{const a=n.getSenders().find(l=>l.track===f);a&&(n.removeTrack(a),f.id&&i.localTrackIds.delete(f.id),await this.negotiate(i))}),close:async()=>{await this.disposeClientSession(i,!0)},on(f,a){return f==="track"&&c.add(a),f==="trackEnded"&&d.add(a),f==="stream"&&g.add(a),()=>{c.delete(a),d.delete(a),g.delete(a)}}};try{return this.attachClientPeerEvents(i),this.addLocalTracks(i,t.localStream),this.addReceiveTransceivers(i,t.receiveAudio,t.receiveVideo),await this.negotiate(i,t.signal),await this.waitForChannelOpen(s,this.connectionTimeoutMs,t.signal),i}catch(f){throw await this.disposeClientSession(i,!0),f}finally{this.pendingPeers.delete(n)}}attachClientPeerEvents(e){const{peerConnection:t,dataChannel:r}=e;t.addEventListener("track",o=>{const n=o.track;e.remoteStream.getTracks().some(s=>s.id===n.id)||e.remoteStream.addTrack(n),this.emitTrack({side:"client",sessionId:e.id,peerUrl:e.peerUrl,peerConnection:t,track:n,stream:e.remoteStream,streams:o.streams})}),t.addEventListener("connectionstatechange",()=>{(t.connectionState==="failed"||t.connectionState==="closed")&&this.disposeClientSession(e,!0)}),r.addEventListener("message",o=>{let n;try{n=JSON.parse(String(o.data))}catch{return}if(!n||typeof n!="object"||typeof n.requestId!="string")return;const s=e.pending.get(n.requestId);if(s)try{const i=P(n.body);this.finishPending(e,n.requestId),s.resolve({status:typeof n.status=="number"?n.status:200,statusText:typeof n.statusText=="string"?n.statusText:"OK",headers:n.headers&&typeof n.headers=="object"?n.headers:{},body:i,url:typeof n.url=="string"?n.url:e.peerUrl})}catch(i){this.finishPending(e,n.requestId),s.reject(b(i,"Invalid WebRTC response"))}}),r.addEventListener("close",()=>{this.rejectAllPending(e,new Error("WebRTC DataChannel closed")),this.disposeClientSession(e,!0)}),r.addEventListener("error",()=>{this.rejectAllPending(e,new Error("WebRTC DataChannel error"))})}queueNegotiation(e,t){const r=e.negotiation.catch(()=>{}).then(async()=>{if(e.closed)throw new Error("WebRTC session closed");try{await t()}catch(o){throw await this.disposeClientSession(e,!0),o}});return e.negotiation=r,r}async ensureMedia(e,t){!(t.localStream||t.receiveAudio||t.receiveVideo)||e.closed||await this.queueNegotiation(e,async()=>{let o=this.addLocalTracks(e,t.localStream);o=this.addReceiveTransceivers(e,t.receiveAudio,t.receiveVideo)||o,t.localStream&&(e.localStream=t.localStream,e.mediaController.localStream=t.localStream),o&&await this.negotiate(e,t.signal)})}addLocalTracks(e,t){if(!t)return!1;let r=!1;for(const o of t.getTracks())o.readyState!=="live"||e.localTrackIds.has(o.id)||(e.peerConnection.addTrack(o,t),e.localTrackIds.add(o.id),r=!0);return r}addReceiveTransceivers(e,t,r){const o=e.peerConnection;let n=!1;const s=(i,c)=>{if(!c)return;o.getTransceivers().find(g=>g.receiver.track.kind===i&&(g.direction==="recvonly"||g.direction==="sendrecv"))||(o.addTransceiver(i,{direction:"recvonly"}),n=!0)};return s("audio",t),s("video",r),n}async negotiate(e,t){if(t?.aborted)throw y(t);const r=e.peerConnection;if(r.signalingState!=="stable")throw new Error(`Cannot negotiate WebRTC session in state ${r.signalingState}`);const o=await r.createOffer();await this.withDeadline(r.setLocalDescription(o),this.iceGatheringTimeoutMs,t),await this.waitForIceGatheringComplete(r,this.iceGatheringTimeoutMs);const n=r.localDescription;if(!n?.sdp||n.type!=="offer")throw new Error("Local SDP offer was not generated");const s=new AbortController,i=setTimeout(()=>s.abort(),this.signalingTimeoutMs),c=()=>s.abort(t?.reason);t?.addEventListener("abort",c,{once:!0}),t?.aborted&&c();try{const d=await fetch(e.signalingUrl,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({sessionId:e.id,sdp:n}),signal:s.signal});if(!d.ok){const f=await d.text();throw new Error(`Signaling HTTP error: ${d.status} ${d.statusText}${f?` \u2014 ${f}`:""}`)}const g=await d.json();if(g.type!=="answer"||typeof g.sdp!="string")throw new Error("Signaling server returned an invalid SDP answer");await this.setRemoteDescription(r,g)}catch(d){throw t?.aborted?y(t):s.signal.aborted?new Error(`Signaling timed out after ${this.signalingTimeoutMs} ms`):b(d,"WebRTC signaling failed")}finally{clearTimeout(i),t?.removeEventListener("abort",c)}}async setRemoteDescription(e,t){const r=new Set(e.dtlsTransports);try{await e.setRemoteDescription(t)}finally{const o=new Set(e.dtlsTransports);await Promise.all([...r].filter(n=>!o.has(n)).map(n=>n.stop()))}}async withDeadline(e,t,r){let o,n;try{return await Promise.race([e,new Promise((s,i)=>{o=setTimeout(()=>i(new Error(`WebRTC operation timed out after ${t} ms`)),t),n=()=>i(y(r)),r?.addEventListener("abort",n,{once:!0}),r?.aborted&&n()})])}finally{clearTimeout(o),n&&r?.removeEventListener("abort",n)}}async waitForIceGatheringComplete(e,t){if(e.iceGatheringState==="complete")return!0;if(!await new Promise(o=>{let n=!1,s;const i=()=>{clearTimeout(s),e.removeEventListener("icegatheringstatechange",d)},c=g=>{n||(n=!0,i(),o(g))},d=()=>{e.iceGatheringState==="complete"&&c(!0)};s=setTimeout(()=>c(!1),t),e.addEventListener("icegatheringstatechange",d),d()}))throw new Error(`ICE gathering timed out after ${t} ms`);return!0}async waitForChannelOpen(e,t,r){if(e.readyState!=="open"){if(r?.aborted)throw y(r);await new Promise((o,n)=>{let s=!1;const i=()=>{clearTimeout(l),e.removeEventListener("open",d),e.removeEventListener("error",g),e.removeEventListener("close",f),r?.removeEventListener("abort",a)},c=m=>{s||(s=!0,i(),m?n(m):o())},d=()=>c(),g=()=>c(new Error("WebRTC DataChannel failed to open")),f=()=>c(new Error("WebRTC DataChannel closed before opening")),a=()=>c(y(r)),l=setTimeout(()=>c(new Error(`DataChannel open timeout after ${t} ms`)),t);e.addEventListener("open",d),e.addEventListener("error",g),e.addEventListener("close",f),r?.addEventListener("abort",a,{once:!0}),e.readyState==="open"?c():e.readyState==="closed"&&c(new Error("WebRTC DataChannel is closed"))})}}sendRequest(e,t,r){if(t.signal?.aborted)return Promise.reject(y(t.signal));if(e.dataChannel.readyState!=="open")return Promise.reject(new Error("WebRTC DataChannel is not open"));const o=this.createId(`request${++this.requestSequence}`),n=r&&r>0?r:this.requestTimeoutMs;return new Promise((s,i)=>{const c={resolve:s,reject:i,timer:setTimeout(()=>{this.finishPending(e,o),i(new Error(`WebRTC request timed out after ${n} ms`))},n),...t.signal?{signal:t.signal}:{}};t.signal&&(c.abortListener=()=>{this.finishPending(e,o),i(y(t.signal))},t.signal.addEventListener("abort",c.abortListener,{once:!0})),e.pending.set(o,c);try{e.dataChannel.send(JSON.stringify({requestId:o,method:t.method,url:t.url,headers:t.headers,body:k(t.body),protocol:t.protocol}))}catch(d){this.finishPending(e,o),i(b(d,"Failed to send WebRTC request"))}})}finishPending(e,t){const r=e.pending.get(t);r&&(clearTimeout(r.timer),r.signal&&r.abortListener&&r.signal.removeEventListener("abort",r.abortListener),e.pending.delete(t))}rejectAllPending(e,t){for(const[r,o]of e.pending)this.finishPending(e,r),o.reject(t)}async disposeClientSession(e,t){if(!e.closed){e.closed=!0,this.sessions.get(e.peerUrl)===e&&this.sessions.delete(e.peerUrl),this.rejectAllPending(e,new Error("WebRTC session closed"));try{e.dataChannel.close()}catch{}try{await e.peerConnection.close()}catch{}if(t)try{const r=new URL(e.signalingUrl);r.searchParams.set("sessionId",e.id);const o=new AbortController,n=setTimeout(()=>o.abort(),2e3);try{await fetch(r,{method:"DELETE",signal:o.signal})}finally{clearTimeout(n)}}catch{}}}attachServerPeerEvents(e,t){const r=e.peerConnection;r.addEventListener("datachannel",o=>{const n=o.channel;n.addEventListener("message",s=>{(async()=>{let i;try{const c=JSON.parse(String(s.data));if(i=typeof c.requestId=="string"?c.requestId:void 0,!i||typeof c.method!="string"||typeof c.url!="string")throw new Error("Invalid WebRTC transport request");const d={method:c.method,url:c.url,headers:c.headers&&typeof c.headers=="object"?{...c.headers,"x-webrtc-session-id":e.id}:{"x-webrtc-session-id":e.id},body:P(c.body),protocol:typeof c.protocol=="string"?c.protocol:"webrtc"};let g;if(t?g=await t(d):g={status:501,statusText:"Not Implemented",headers:{"content-type":"application/json"},body:{error:"No server request handler configured"},url:d.url},n.readyState!=="open")return;n.send(JSON.stringify({requestId:i,status:g.status,statusText:g.statusText??"",headers:g.headers,body:k(g.body),url:g.url??d.url}))}catch(c){if(this.log("error","[WebRtcTransport Server] Request failed:",c),i&&n.readyState==="open")try{n.send(JSON.stringify({requestId:i,status:500,statusText:"Internal Server Error",headers:{"content-type":"application/json"},body:{error:b(c,"Request handler failed").message}}))}catch{}}})()})}),r.addEventListener("track",o=>{const n=o.track;e.remoteStream.getTracks().some(s=>s.id===n.id)||e.remoteStream.addTrack(n),this.emitTrack({side:"server",sessionId:e.id,peerConnection:r,track:n,stream:e.remoteStream,streams:o.streams})}),r.addEventListener("connectionstatechange",()=>{(r.connectionState==="failed"||r.connectionState==="closed")&&this.closeServerSession(e.id)})}async attachConfiguredServerStream(e){if(e.closed)return;if(!e.localStreamResolved){const o=this.options.localStream,n=typeof o=="function",s=n?o(e.id):o;e.localStreamResolved=!0,s&&(e.localStream=s,e.ownsLocalStream=n)}const t=e.localStream;if(!t)return;const r=e.peerConnection;for(const o of t.getTracks())e.closed||o.readyState!=="live"||e.localTrackIds.has(o.id)||(r.addTrack(o,t),e.localTrackIds.add(o.id))}disposeServerLocalStream(e){const t=e.localStream,r=e.ownsLocalStream;if(delete e.localStream,e.ownsLocalStream=!1,!(!t||!r))try{this.options.disposeLocalStream?.(e.id,t)}catch{}finally{for(const o of t.getTracks())o.readyState!=="ended"&&o.stop()}}async closeServerSession(e){const t=this.serverSessions.get(e);if(!(!t||t.closed)){t.closed=!0,this.serverSessions.delete(e);try{await t.peerConnection.close()}catch{}finally{this.disposeServerLocalStream(t)}}}emitTrack(e){for(const t of this.trackListeners)try{t(e)}catch(r){this.log("error","[WebRtcTransport] onTrack listener failed:",r)}}toSignalingUrl(e){if(e.startsWith("webrtcs://"))return e.replace(/^webrtcs:\/\//,"https://");if(e.startsWith("webrtc://"))return e.replace(/^webrtc:\/\//,"http://");throw new Error(`Unsupported WebRTC signaling URL: ${e}`)}createId(e){return`${e}-${crypto.randomUUID()}`}}export{F as WebRtcTransport};
|
|
2
|
+
//# sourceMappingURL=webrtc.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/webrtc.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n HyperTransport,\n LogLevel,\n TransportListenOptions,\n TransportRequest,\n TransportResponse,\n TransportServer,\n} from \"@hyperttp/types\";\nimport {\n MAX_SIGNALING_BODY_BYTES,\n type ClientSession,\n type PendingResponse,\n type ServerSession,\n type WebRtcSessionView,\n type WebRtcTrackEvent,\n type WebRtcTransportOptions as InternalWebRtcTransportOptions,\n type WebRtcTransportRequest,\n} from \"./types/types.js\";\nimport { abortError, decodeWireBody, encodeWireBody, errorFromUnknown } from \"./utils/helper.js\";\nimport { createServer, type Server as HttpServer } from \"node:http\";\nimport {\n MediaStream as WeriftMediaStream,\n RTCPeerConnection as WeriftPeerConnection,\n RTCDataChannel as WeriftDataChannel,\n type MediaStreamTrack as WeriftMediaStreamTrack,\n} from \"werift\";\n\ndeclare module \"@hyperttp/types\" {\n interface TransportServer {\n readonly webrtc?: Pick<WebRtcTransport, \"getSession\" | \"getServerSession\" | \"onTrack\">;\n }\n}\n\ntype WeriftPeerConfig = NonNullable<ConstructorParameters<typeof WeriftPeerConnection>[0]>;\ntype WeriftIceServers = WeriftPeerConfig[\"iceServers\"];\n\nfunction normalizeIceServers(servers: RTCIceServer[] | undefined): WeriftIceServers {\n const normalized = (servers ?? []).flatMap((server) => {\n const urls = Array.isArray(server.urls) ? server.urls : [server.urls];\n if (server.credential !== undefined && typeof server.credential !== \"string\") {\n throw new TypeError(\"Werift currently requires ICE server credentials to be strings.\");\n }\n return urls.map((url) => ({\n urls: url,\n ...(server.username !== undefined ? { username: server.username } : {}),\n ...(server.credential !== undefined ? { credential: server.credential } : {}),\n }));\n });\n return normalized as unknown as WeriftIceServers;\n}\n\nfunction createWeriftPeerConnection(configuration: RTCConfiguration): WeriftPeerConnection {\n const config = {\n iceServers: normalizeIceServers(configuration.iceServers),\n ...(configuration.iceTransportPolicy !== undefined\n ? { iceTransportPolicy: configuration.iceTransportPolicy }\n : {}),\n } as unknown as WeriftPeerConfig;\n return new WeriftPeerConnection(config);\n}\n\nfunction createWeriftMediaStream(): WeriftMediaStream {\n return new WeriftMediaStream();\n}\n\nexport type WebRtcTransportOptions = Omit<\n InternalWebRtcTransportOptions,\n \"localStream\" | \"disposeLocalStream\" | \"createMediaStream\"\n> & {\n readonly localStream?: WeriftMediaStream | ((sessionId: string) => WeriftMediaStream | undefined);\n readonly disposeLocalStream?: (sessionId: string, stream: WeriftMediaStream) => void;\n readonly createMediaStream?: () => WeriftMediaStream;\n readonly logger?: (level: LogLevel, message: string, meta?: unknown) => void;\n};\n\nexport interface WebRtcServerSessionView {\n readonly id: string;\n readonly peerConnection: WeriftPeerConnection;\n readonly remoteStream: WeriftMediaStream;\n readonly localStream?: WeriftMediaStream;\n}\n\nexport interface WebRtcTransportServer extends TransportServer {\n readonly webrtc: Pick<WebRtcTransport, \"getSession\" | \"getServerSession\" | \"onTrack\">;\n}\n\nexport class WebRtcTransport implements HyperTransport {\n private readonly sessions = new Map<string, ClientSession>();\n private readonly connecting = new Map<string, Promise<ClientSession>>();\n private readonly serverSessions = new Map<string, ServerSession>();\n private readonly trackListeners = new Set<(event: WebRtcTrackEvent) => void>();\n private readonly serverClosers = new Set<() => Promise<void>>();\n private requestSequence = 0;\n private closing = false;\n private readonly pendingPeers = new Set<WeriftPeerConnection>();\n private readonly iceServers: RTCIceServer[];\n private readonly iceGatheringTimeoutMs: number;\n private readonly signalingTimeoutMs: number;\n private readonly connectionTimeoutMs: number;\n private readonly requestTimeoutMs: number;\n readonly createMediaStream: () => WeriftMediaStream;\n private readonly createPeerConnection: (configuration: RTCConfiguration) => WeriftPeerConnection;\n private readonly options: InternalWebRtcTransportOptions;\n\n constructor(options: WebRtcTransportOptions = {}) {\n this.options = options as unknown as InternalWebRtcTransportOptions;\n this.createPeerConnection = options.createPeerConnection ?? createWeriftPeerConnection;\n this.createMediaStream =\n (options.createMediaStream as unknown as (() => WeriftMediaStream) | undefined) ??\n createWeriftMediaStream;\n this.iceServers = options.iceServers ?? [{ urls: \"stun:stun.l.google.com:19302\" }];\n this.iceGatheringTimeoutMs = options.iceGatheringTimeoutMs ?? 10_000;\n this.signalingTimeoutMs = options.signalingTimeoutMs ?? 10_000;\n this.connectionTimeoutMs = options.connectionTimeoutMs ?? 15_000;\n this.requestTimeoutMs = options.requestTimeoutMs ?? 15_000;\n }\n\n private log(level: \"debug\" | \"info\" | \"warn\" | \"error\", message: string, meta?: unknown): void {\n if (this.options.logger) {\n this.options.logger(level, message, meta);\n return;\n }\n if (!this.options.verbose && level !== \"error\") return;\n const output = level === \"debug\" ? console.debug : console[level];\n if (meta === undefined) output(message);\n else output(message, meta);\n }\n\n onTrack(listener: (event: WebRtcTrackEvent) => void): () => void {\n this.trackListeners.add(listener);\n return () => this.trackListeners.delete(listener);\n }\n\n getServerSession(sessionId: string): WebRtcServerSessionView | undefined {\n const session = this.serverSessions.get(sessionId);\n if (!session || session.closed) return undefined;\n return {\n id: session.id,\n peerConnection: session.peerConnection,\n remoteStream: session.remoteStream,\n ...(session.localStream ? { localStream: session.localStream } : {}),\n };\n }\n\n getSession(peerUrl: string): WebRtcSessionView | undefined {\n const session = this.sessions.get(peerUrl);\n if (!session || session.closed) return undefined;\n\n return {\n id: session.id,\n peerUrl: session.peerUrl,\n peerConnection: session.peerConnection,\n dataChannel: session.dataChannel,\n remoteStream: session.remoteStream,\n media: session.mediaController,\n ...(session.localStream ? { localStream: session.localStream } : {}),\n };\n }\n\n async execute(request: TransportRequest): Promise<TransportResponse> {\n if (this.closing) throw new Error(\"WebRTC transport is closed\");\n const rtcRequest = request as WebRtcTransportRequest;\n const peerUrl = rtcRequest.peerUrl ?? request.url;\n\n if (request.signal?.aborted) throw abortError(request.signal);\n\n const session = await this.getOrCreateSession(peerUrl, rtcRequest);\n await this.ensureMedia(session, rtcRequest);\n\n if (request.signal?.aborted) throw abortError(request.signal);\n\n return this.sendRequest(session, request, rtcRequest.timeout);\n }\n\n public async listen(options: TransportListenOptions): Promise<WebRtcTransportServer> {\n if (this.closing) throw new Error(\"WebRTC transport is closed\");\n const ownedSessions = new Set<string>();\n const port = options.port ?? 3000;\n const host = options.host ?? \"127.0.0.1\";\n const signalingPath = options.signaling?.path ?? \"/signal\";\n if (\n !signalingPath.startsWith(\"/\") ||\n signalingPath.startsWith(\"//\") ||\n /[?#\\\\\\s]/u.test(signalingPath) ||\n Array.from(signalingPath).some((char) => {\n const code = char.codePointAt(0)!;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n }) ||\n new URL(signalingPath, \"http://localhost\").pathname !== signalingPath\n ) {\n throw new TypeError(\n \"WebRTC signaling path must be an absolute literal pathname without query or fragment\",\n );\n }\n\n if (options.signal?.aborted) {\n throw new Error(\"WebRtcTransport: server startup aborted\");\n }\n\n let closed = false;\n let closePromise: Promise<void> | undefined;\n const pendingAuthorizations = new Set<AbortController>();\n\n const httpServer: HttpServer = createServer((req, res) => {\n void (async () => {\n const sendJson = (status: number, body: unknown) => {\n if (res.headersSent || res.destroyed) return;\n res.writeHead(status, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(body));\n };\n\n if (closed || this.closing) {\n sendJson(503, { error: \"WebRTC server is closing\" });\n return;\n }\n\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(req.url ?? \"/\", \"http://localhost\");\n } catch {\n sendJson(400, { error: \"Invalid request URL\" });\n return;\n }\n\n if (parsedUrl.pathname !== signalingPath) {\n sendJson(404, { error: \"Signaling endpoint not found\" });\n return;\n }\n\n if ((req.method === \"POST\" || req.method === \"DELETE\") && options.signaling?.authorize) {\n const controller = new AbortController();\n const onDisconnect = () => controller.abort();\n const onAuthorizationAbort = () => {\n sendJson(503, { error: \"WebRTC signaling request aborted\" });\n };\n req.once(\"aborted\", onDisconnect);\n res.once(\"close\", onDisconnect);\n controller.signal.addEventListener(\"abort\", onAuthorizationAbort, { once: true });\n pendingAuthorizations.add(controller);\n try {\n const headers: Record<string, string | string[]> = {};\n for (const [name, value] of Object.entries(req.headers)) {\n if (value !== undefined) headers[name] = value;\n }\n const allowed = await options.signaling.authorize({\n protocol: \"webrtc\",\n method: req.method,\n url: req.url ?? \"/\",\n headers,\n signal: controller.signal,\n });\n if (controller.signal.aborted || req.aborted || res.destroyed) return;\n if (closed || this.closing || options.signal?.aborted) {\n sendJson(503, { error: \"WebRTC server is closing\" });\n return;\n }\n if (!allowed) {\n sendJson(403, { error: \"Signaling forbidden\" });\n return;\n }\n } catch {\n sendJson(403, { error: \"Signaling forbidden\" });\n return;\n } finally {\n pendingAuthorizations.delete(controller);\n req.off(\"aborted\", onDisconnect);\n res.off(\"close\", onDisconnect);\n controller.signal.removeEventListener(\"abort\", onAuthorizationAbort);\n }\n }\n\n if (req.method === \"DELETE\") {\n const sessionId = parsedUrl.searchParams.get(\"sessionId\");\n if (sessionId && ownedSessions.has(sessionId)) {\n await this.closeServerSession(sessionId);\n ownedSessions.delete(sessionId);\n }\n res.writeHead(204).end();\n return;\n }\n\n if (req.method !== \"POST\") {\n sendJson(405, { error: \"Method not allowed\" });\n return;\n }\n\n let raw = \"\";\n try {\n for await (const chunk of req) {\n raw += Buffer.isBuffer(chunk) ? chunk.toString(\"utf8\") : String(chunk);\n if (Buffer.byteLength(raw, \"utf8\") > MAX_SIGNALING_BODY_BYTES) {\n sendJson(413, { error: \"Signaling payload too large\" });\n return;\n }\n }\n } catch (error) {\n sendJson(400, { error: errorFromUnknown(error, \"Failed to read body\").message });\n return;\n }\n\n let pc: WeriftPeerConnection | undefined;\n let session: ServerSession | undefined;\n\n try {\n const payload = JSON.parse(raw) as {\n sessionId?: unknown;\n sdp?: RTCSessionDescriptionInit;\n };\n\n if (\n typeof payload.sessionId !== \"string\" ||\n payload.sessionId.length < 1 ||\n payload.sessionId.length > 200 ||\n payload.sdp?.type !== \"offer\" ||\n typeof payload.sdp.sdp !== \"string\"\n ) {\n sendJson(400, { error: \"Expected sessionId and a valid SDP offer\" });\n return;\n }\n\n if (closed || this.closing || options.signal?.aborted || req.aborted || res.destroyed) {\n throw new Error(\"WebRTC signaling request aborted\");\n }\n const sessionId = payload.sessionId;\n session = this.serverSessions.get(sessionId);\n if (session && !ownedSessions.has(sessionId)) {\n sendJson(409, { error: \"Session belongs to another listener\" });\n return;\n }\n if (session?.negotiating) {\n sendJson(409, { error: \"Session negotiation already in progress\" });\n return;\n }\n\n if (!session || session.closed) {\n pc = this.createPeerConnection({ iceServers: this.iceServers });\n session = {\n id: sessionId,\n peerConnection: pc,\n remoteStream: this.createMediaStream(),\n localTrackIds: new Set<string>(),\n localStreamResolved: false,\n ownsLocalStream: false,\n closed: false,\n };\n ownedSessions.add(sessionId);\n this.serverSessions.set(sessionId, session);\n this.attachServerPeerEvents(session, options.onRequest);\n } else {\n pc = session.peerConnection;\n }\n\n session.negotiating = true;\n await this.setRemoteDescription(pc, payload.sdp);\n await this.attachConfiguredServerStream(session);\n\n const answer = await pc.createAnswer();\n await this.withDeadline(pc.setLocalDescription(answer), this.iceGatheringTimeoutMs);\n await this.waitForIceGatheringComplete(pc, this.iceGatheringTimeoutMs);\n\n const localDescription = pc.localDescription;\n if (!localDescription?.sdp || localDescription.type !== \"answer\") {\n throw new Error(\"Local SDP answer was not generated\");\n }\n\n if (closed || session.closed) throw new Error(\"WebRTC session closed\");\n session.negotiating = false;\n sendJson(200, localDescription);\n } catch (error) {\n this.log(\"error\", \"[WebRtcTransport Server] \u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0438\u0433\u043D\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438:\", error);\n if (session) {\n await this.closeServerSession(session.id);\n } else if (pc) {\n try {\n await pc.close();\n } catch {}\n }\n sendJson(400, { error: errorFromUnknown(error, \"Signaling failed\").message });\n }\n })();\n });\n\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error) => {\n httpServer.off(\"listening\", onListening);\n reject(error);\n };\n const onListening = () => {\n httpServer.off(\"error\", onError);\n resolve();\n };\n httpServer.once(\"error\", onError);\n httpServer.once(\"listening\", onListening);\n httpServer.listen(port, host);\n });\n\n this.log(\"info\", `[WebRtcTransport] Listening on http://${host}:${port}${signalingPath}`);\n\n const onAbort = () => {\n void closeServer().catch((error) => {\n this.log(\"error\", \"[WebRtcTransport] Server shutdown failed:\", error);\n });\n };\n\n const closeServer = (): Promise<void> => {\n if (closePromise) return closePromise;\n closed = true;\n for (const controller of pendingAuthorizations) controller.abort();\n options.signal?.removeEventListener(\"abort\", onAbort);\n closePromise = (async () => {\n const stopped = httpServer.listening\n ? new Promise<void>((resolve, reject) => {\n httpServer.close((error) => (error ? reject(error) : resolve()));\n })\n : Promise.resolve();\n await Promise.all([...ownedSessions].map((id) => this.closeServerSession(id)));\n ownedSessions.clear();\n await stopped;\n this.serverClosers.delete(closeServer);\n })();\n return closePromise;\n };\n\n options.signal?.addEventListener(\"abort\", onAbort, { once: true });\n this.serverClosers.add(closeServer);\n if (options.signal?.aborted && !closed) await closeServer();\n\n return {\n close: closeServer,\n webrtc: {\n getSession: this.getSession.bind(this),\n getServerSession: this.getServerSession.bind(this),\n onTrack: this.onTrack.bind(this),\n },\n };\n }\n\n async close(): Promise<void> {\n this.closing = true;\n await Promise.all([...this.pendingPeers].map((pc) => pc.close().catch(() => {})));\n await Promise.allSettled(this.connecting.values());\n await Promise.all(\n [...this.sessions.values()].map((session) => this.disposeClientSession(session, true)),\n );\n await Promise.all([...this.serverClosers].map((closeServer) => closeServer()));\n }\n\n async destroy(): Promise<void> {\n await this.close();\n }\n\n private async getOrCreateSession(\n peerUrl: string,\n request: WebRtcTransportRequest,\n ): Promise<ClientSession> {\n const existing = this.sessions.get(peerUrl);\n if (\n existing &&\n !existing.closed &&\n existing.dataChannel.readyState === \"open\" &&\n existing.peerConnection.connectionState !== \"failed\" &&\n existing.peerConnection.connectionState !== \"closed\"\n ) {\n return existing;\n }\n\n if (existing) {\n await this.disposeClientSession(existing, true);\n }\n\n const inFlight = this.connecting.get(peerUrl);\n if (inFlight) return inFlight;\n\n const connectionPromise = this.createClientSession(peerUrl, request);\n this.connecting.set(peerUrl, connectionPromise);\n\n try {\n const session = await connectionPromise;\n if (this.closing || session.closed) {\n await this.disposeClientSession(session, true);\n throw new Error(\"WebRTC transport is closed\");\n }\n this.sessions.set(peerUrl, session);\n return session;\n } finally {\n if (this.connecting.get(peerUrl) === connectionPromise) {\n this.connecting.delete(peerUrl);\n }\n }\n }\n\n private async createClientSession(\n peerUrl: string,\n request: WebRtcTransportRequest,\n ): Promise<ClientSession> {\n const signalingUrl = this.toSignalingUrl(peerUrl);\n const id = this.createId(\"session\");\n const pc = this.createPeerConnection({\n iceServers: request.iceServers ?? this.iceServers,\n });\n this.pendingPeers.add(pc);\n const channel = pc.createDataChannel(request.channelLabel ?? \"hyperttp-webrtc-transport\", {\n ordered: true,\n });\n\n const session: ClientSession = {\n id,\n peerUrl,\n signalingUrl,\n peerConnection: pc,\n dataChannel: channel,\n remoteStream: this.createMediaStream(),\n ...(request.localStream ? { localStream: request.localStream } : {}),\n localTrackIds: new Set<string>(),\n pending: new Map<string, PendingResponse>(),\n negotiation: Promise.resolve(),\n closed: false,\n mediaController: {} as any,\n };\n\n const trackListeners = new Set<\n (e: { track: WeriftMediaStreamTrack; stream: WeriftMediaStream }) => void\n >();\n const trackEndedListeners = new Set<(e: { track: WeriftMediaStreamTrack }) => void>();\n const streamListeners = new Set<(e: { stream: WeriftMediaStream }) => void>();\n\n pc.addEventListener(\"track\", (event: WebRtcTrackEvent) => {\n const { track } = event;\n if (!session.remoteStream.getTracks().includes(track)) session.remoteStream.addTrack(track);\n for (const listener of trackListeners) listener({ track, stream: session.remoteStream });\n for (const listener of streamListeners) listener({ stream: session.remoteStream });\n track.addEventListener(\"ended\", () => {\n for (const listener of trackEndedListeners) listener({ track });\n });\n });\n\n session.mediaController = {\n localStream: session.localStream,\n remoteStream: session.remoteStream,\n addTrack: (track, stream) =>\n this.queueNegotiation(session, async () => {\n pc.addTrack(track, stream ?? session.localStream ?? this.createMediaStream());\n if (track.id) session.localTrackIds.add(track.id);\n await this.negotiate(session);\n }),\n replaceTrack: async (kind: \"audio\" | \"video\", track: any | null) => {\n const sender = pc.getSenders().find((s: any) => s.track && s.track.kind === kind);\n if (sender) {\n await sender.replaceTrack(track);\n }\n },\n removeTrack: (track) =>\n this.queueNegotiation(session, async () => {\n const sender = pc.getSenders().find((item) => item.track === track);\n if (sender) {\n pc.removeTrack(sender);\n if (track.id) session.localTrackIds.delete(track.id);\n await this.negotiate(session);\n }\n }),\n close: async () => {\n await this.disposeClientSession(session, true);\n },\n on(event: string, listener: any) {\n if (event === \"track\") trackListeners.add(listener);\n if (event === \"trackEnded\") trackEndedListeners.add(listener);\n if (event === \"stream\") streamListeners.add(listener);\n return () => {\n trackListeners.delete(listener);\n trackEndedListeners.delete(listener);\n streamListeners.delete(listener);\n };\n },\n };\n\n try {\n this.attachClientPeerEvents(session);\n this.addLocalTracks(session, request.localStream);\n this.addReceiveTransceivers(session, request.receiveAudio, request.receiveVideo);\n await this.negotiate(session, request.signal);\n await this.waitForChannelOpen(channel, this.connectionTimeoutMs, request.signal);\n return session;\n } catch (error) {\n await this.disposeClientSession(session, true);\n throw error;\n } finally {\n this.pendingPeers.delete(pc);\n }\n }\n\n private attachClientPeerEvents(session: ClientSession): void {\n const { peerConnection: pc, dataChannel: channel } = session;\n\n pc.addEventListener(\"track\", (event: any) => {\n const track = event.track;\n if (!session.remoteStream.getTracks().some((item: any) => item.id === track.id)) {\n session.remoteStream.addTrack(track);\n }\n\n this.emitTrack({\n side: \"client\",\n sessionId: session.id,\n peerUrl: session.peerUrl,\n peerConnection: pc,\n track,\n stream: session.remoteStream,\n streams: event.streams,\n });\n });\n\n pc.addEventListener(\"connectionstatechange\", () => {\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n void this.disposeClientSession(session, true);\n }\n });\n\n channel.addEventListener(\"message\", (event: MessageEvent) => {\n let message: Record<string, unknown>;\n try {\n message = JSON.parse(String(event.data)) as Record<string, unknown>;\n } catch {\n return;\n }\n\n if (!message || typeof message !== \"object\" || typeof message.requestId !== \"string\") return;\n const pending = session.pending.get(message.requestId);\n if (!pending) return;\n\n try {\n const body = decodeWireBody(message.body);\n this.finishPending(session, message.requestId);\n pending.resolve({\n status: typeof message.status === \"number\" ? message.status : 200,\n statusText: typeof message.statusText === \"string\" ? message.statusText : \"OK\",\n headers:\n message.headers && typeof message.headers === \"object\"\n ? (message.headers as TransportResponse[\"headers\"])\n : {},\n body,\n url: typeof message.url === \"string\" ? message.url : session.peerUrl,\n });\n } catch (error) {\n this.finishPending(session, message.requestId);\n pending.reject(errorFromUnknown(error, \"Invalid WebRTC response\"));\n }\n });\n\n channel.addEventListener(\"close\", () => {\n this.rejectAllPending(session, new Error(\"WebRTC DataChannel closed\"));\n void this.disposeClientSession(session, true);\n });\n\n channel.addEventListener(\"error\", () => {\n this.rejectAllPending(session, new Error(\"WebRTC DataChannel error\"));\n });\n }\n\n private queueNegotiation(session: ClientSession, operation: () => Promise<void>): Promise<void> {\n const pending = session.negotiation\n .catch(() => undefined)\n .then(async () => {\n if (session.closed) throw new Error(\"WebRTC session closed\");\n try {\n await operation();\n } catch (error) {\n await this.disposeClientSession(session, true);\n throw error;\n }\n });\n session.negotiation = pending;\n return pending;\n }\n\n private async ensureMedia(\n session: ClientSession,\n request: WebRtcTransportRequest,\n ): Promise<void> {\n const needsPotentialChanges =\n Boolean(request.localStream) ||\n Boolean(request.receiveAudio) ||\n Boolean(request.receiveVideo);\n if (!needsPotentialChanges || session.closed) return;\n\n await this.queueNegotiation(session, async () => {\n let changed = this.addLocalTracks(session, request.localStream);\n changed =\n this.addReceiveTransceivers(session, request.receiveAudio, request.receiveVideo) || changed;\n\n if (request.localStream) {\n session.localStream = request.localStream;\n (session.mediaController as { localStream?: WeriftMediaStream }).localStream =\n request.localStream;\n }\n if (changed) await this.negotiate(session, request.signal);\n });\n }\n\n private addLocalTracks(session: ClientSession, stream?: WeriftMediaStream): boolean {\n if (!stream) return false;\n let changed = false;\n\n for (const track of stream.getTracks()) {\n if (track.readyState !== \"live\" || session.localTrackIds.has(track.id!)) continue;\n session.peerConnection.addTrack(track, stream);\n session.localTrackIds.add(track.id!);\n changed = true;\n }\n\n return changed;\n }\n\n private addReceiveTransceivers(\n session: ClientSession,\n receiveAudio?: boolean,\n receiveVideo?: boolean,\n ): boolean {\n const pc = session.peerConnection;\n let changed = false;\n\n const ensureReceive = (kind: \"audio\" | \"video\", enabled: boolean | undefined) => {\n if (!enabled) return;\n const transceivers = pc\n .getTransceivers()\n .find(\n (item: any) =>\n item.receiver.track.kind === kind &&\n (item.direction === \"recvonly\" || item.direction === \"sendrecv\"),\n );\n if (!transceivers) {\n pc.addTransceiver(kind, { direction: \"recvonly\" });\n changed = true;\n }\n };\n\n ensureReceive(\"audio\", receiveAudio);\n ensureReceive(\"video\", receiveVideo);\n\n return changed;\n }\n\n private async negotiate(session: ClientSession, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) throw abortError(signal);\n\n const pc = session.peerConnection;\n if (pc.signalingState !== \"stable\") {\n throw new Error(`Cannot negotiate WebRTC session in state ${pc.signalingState}`);\n }\n\n const offer = await pc.createOffer();\n await this.withDeadline(pc.setLocalDescription(offer), this.iceGatheringTimeoutMs, signal);\n await this.waitForIceGatheringComplete(pc, this.iceGatheringTimeoutMs);\n\n const localDescription = pc.localDescription;\n if (!localDescription?.sdp || localDescription.type !== \"offer\") {\n throw new Error(\"Local SDP offer was not generated\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.signalingTimeoutMs);\n const abortListener = () => controller.abort(signal?.reason);\n signal?.addEventListener(\"abort\", abortListener, { once: true });\n if (signal?.aborted) abortListener();\n\n try {\n const response = await fetch(session.signalingUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ sessionId: session.id, sdp: localDescription }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(\n `Signaling HTTP error: ${response.status} ${response.statusText}${text ? ` \u2014 ${text}` : \"\"}`,\n );\n }\n\n const answer = (await response.json()) as RTCSessionDescriptionInit;\n if (answer.type !== \"answer\" || typeof answer.sdp !== \"string\") {\n throw new Error(\"Signaling server returned an invalid SDP answer\");\n }\n\n await this.setRemoteDescription(pc, answer);\n } catch (error) {\n if (signal?.aborted) throw abortError(signal);\n if (controller.signal.aborted) {\n throw new Error(`Signaling timed out after ${this.signalingTimeoutMs} ms`);\n }\n throw errorFromUnknown(error, \"WebRTC signaling failed\");\n } finally {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", abortListener);\n }\n }\n\n private async setRemoteDescription(\n pc: WeriftPeerConnection,\n description: RTCSessionDescriptionInit,\n ): Promise<void> {\n const previous = new Set(pc.dtlsTransports);\n try {\n await pc.setRemoteDescription(description);\n } finally {\n // Werift can replace transports while applying BUNDLE without stopping the\n // displaced ICE sockets. They are no longer reachable by pc.close().\n const active = new Set(pc.dtlsTransports);\n await Promise.all(\n [...previous]\n .filter((transport) => !active.has(transport))\n .map((transport) => transport.stop()),\n );\n }\n }\n\n private async withDeadline<T>(\n operation: Promise<T>,\n timeoutMs: number,\n signal?: AbortSignal,\n ): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let onAbort: (() => void) | undefined;\n try {\n return await Promise.race([\n operation,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error(`WebRTC operation timed out after ${timeoutMs} ms`)),\n timeoutMs,\n );\n onAbort = () => reject(abortError(signal));\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (signal?.aborted) onAbort();\n }),\n ]);\n } finally {\n clearTimeout(timer);\n if (onAbort) signal?.removeEventListener(\"abort\", onAbort);\n }\n }\n\n private async waitForIceGatheringComplete(\n pc: WeriftPeerConnection,\n timeoutMs: number,\n ): Promise<boolean> {\n if (pc.iceGatheringState === \"complete\") return true;\n\n const completed = await new Promise<boolean>((resolve) => {\n let settled = false;\n let timer: ReturnType<typeof setTimeout>;\n\n const cleanup = () => {\n clearTimeout(timer);\n pc.removeEventListener(\"icegatheringstatechange\", onStateChange);\n };\n\n const finish = (value: boolean) => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(value);\n };\n\n const onStateChange = () => {\n if (pc.iceGatheringState === \"complete\") finish(true);\n };\n\n timer = setTimeout(() => finish(false), timeoutMs);\n pc.addEventListener(\"icegatheringstatechange\", onStateChange);\n onStateChange();\n });\n\n if (!completed) throw new Error(`ICE gathering timed out after ${timeoutMs} ms`);\n return true;\n }\n\n private async waitForChannelOpen(\n channel: WeriftDataChannel,\n timeoutMs: number,\n signal?: AbortSignal,\n ): Promise<void> {\n if (channel.readyState === \"open\") return;\n if (signal?.aborted) throw abortError(signal);\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const cleanup = () => {\n clearTimeout(timer);\n channel.removeEventListener(\"open\", onOpen);\n channel.removeEventListener(\"error\", onError);\n channel.removeEventListener(\"close\", onClose);\n signal?.removeEventListener(\"abort\", onAbort);\n };\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n cleanup();\n if (error) reject(error);\n else resolve();\n };\n const onOpen = () => finish();\n const onError = () => finish(new Error(\"WebRTC DataChannel failed to open\"));\n const onClose = () => finish(new Error(\"WebRTC DataChannel closed before opening\"));\n const onAbort = () => finish(abortError(signal));\n const timer = setTimeout(\n () => finish(new Error(`DataChannel open timeout after ${timeoutMs} ms`)),\n timeoutMs,\n );\n\n channel.addEventListener(\"open\", onOpen);\n channel.addEventListener(\"error\", onError);\n channel.addEventListener(\"close\", onClose);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n if (channel.readyState === \"open\") finish();\n else if (channel.readyState === \"closed\") finish(new Error(\"WebRTC DataChannel is closed\"));\n });\n }\n\n private sendRequest(\n session: ClientSession,\n request: TransportRequest,\n requestedTimeout?: number,\n ): Promise<TransportResponse> {\n if (request.signal?.aborted) return Promise.reject(abortError(request.signal));\n if (session.dataChannel.readyState !== \"open\") {\n return Promise.reject(new Error(\"WebRTC DataChannel is not open\"));\n }\n\n const requestId = this.createId(`request${++this.requestSequence}`);\n const timeoutMs =\n requestedTimeout && requestedTimeout > 0 ? requestedTimeout : this.requestTimeoutMs;\n\n return new Promise<TransportResponse>((resolve, reject) => {\n const pending: PendingResponse = {\n resolve,\n reject,\n timer: setTimeout(() => {\n this.finishPending(session, requestId);\n reject(new Error(`WebRTC request timed out after ${timeoutMs} ms`));\n }, timeoutMs),\n ...(request.signal ? { signal: request.signal } : {}),\n };\n\n if (request.signal) {\n pending.abortListener = () => {\n this.finishPending(session, requestId);\n reject(abortError(request.signal));\n };\n request.signal.addEventListener(\"abort\", pending.abortListener, { once: true });\n }\n\n session.pending.set(requestId, pending);\n\n try {\n session.dataChannel.send(\n JSON.stringify({\n requestId,\n method: request.method,\n url: request.url,\n headers: request.headers,\n body: encodeWireBody(request.body),\n protocol: request.protocol,\n }),\n );\n } catch (error) {\n this.finishPending(session, requestId);\n reject(errorFromUnknown(error, \"Failed to send WebRTC request\"));\n }\n });\n }\n\n private finishPending(session: ClientSession, requestId: string): void {\n const pending = session.pending.get(requestId);\n if (!pending) return;\n clearTimeout(pending.timer);\n if (pending.signal && pending.abortListener) {\n pending.signal.removeEventListener(\"abort\", pending.abortListener);\n }\n session.pending.delete(requestId);\n }\n\n private rejectAllPending(session: ClientSession, error: Error): void {\n for (const [requestId, pending] of session.pending) {\n this.finishPending(session, requestId);\n pending.reject(error);\n }\n }\n\n private async disposeClientSession(session: ClientSession, notifyRemote: boolean): Promise<void> {\n if (session.closed) return;\n session.closed = true;\n\n if (this.sessions.get(session.peerUrl) === session) {\n this.sessions.delete(session.peerUrl);\n }\n this.rejectAllPending(session, new Error(\"WebRTC session closed\"));\n\n try {\n session.dataChannel.close();\n } catch {}\n try {\n await session.peerConnection.close();\n } catch {}\n\n if (notifyRemote) {\n try {\n const url = new URL(session.signalingUrl);\n url.searchParams.set(\"sessionId\", session.id);\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 2_000);\n try {\n await fetch(url, { method: \"DELETE\", signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n } catch {}\n }\n }\n\n private attachServerPeerEvents(\n session: ServerSession,\n onRequest: TransportListenOptions[\"onRequest\"],\n ): void {\n const pc = session.peerConnection;\n\n pc.addEventListener(\"datachannel\", (event: any) => {\n const channel = event.channel;\n channel.addEventListener(\"message\", (messageEvent: MessageEvent) => {\n void (async () => {\n let requestId: string | undefined;\n try {\n const message = JSON.parse(String(messageEvent.data)) as {\n requestId?: unknown;\n method?: unknown;\n url?: unknown;\n headers?: unknown;\n body?: unknown;\n protocol?: unknown;\n };\n\n requestId = typeof message.requestId === \"string\" ? message.requestId : undefined;\n if (\n !requestId ||\n typeof message.method !== \"string\" ||\n typeof message.url !== \"string\"\n ) {\n throw new Error(\"Invalid WebRTC transport request\");\n }\n\n const request: TransportRequest = {\n method: message.method,\n url: message.url,\n headers:\n message.headers && typeof message.headers === \"object\"\n ? {\n ...(message.headers as TransportRequest[\"headers\"]),\n \"x-webrtc-session-id\": session.id,\n }\n : { \"x-webrtc-session-id\": session.id },\n body: decodeWireBody(message.body),\n protocol:\n typeof message.protocol === \"string\"\n ? (message.protocol as TransportRequest[\"protocol\"])\n : \"webrtc\",\n };\n\n let response: TransportResponse;\n if (!onRequest) {\n response = {\n status: 501,\n statusText: \"Not Implemented\",\n headers: { \"content-type\": \"application/json\" },\n body: { error: \"No server request handler configured\" },\n url: request.url,\n };\n } else {\n response = await onRequest(request);\n }\n\n if (channel.readyState !== \"open\") return;\n channel.send(\n JSON.stringify({\n requestId,\n status: response.status,\n statusText: response.statusText ?? \"\",\n headers: response.headers,\n body: encodeWireBody(response.body),\n url: response.url ?? request.url,\n }),\n );\n } catch (error) {\n this.log(\"error\", \"[WebRtcTransport Server] Request failed:\", error);\n if (requestId && channel.readyState === \"open\") {\n try {\n channel.send(\n JSON.stringify({\n requestId,\n status: 500,\n statusText: \"Internal Server Error\",\n headers: { \"content-type\": \"application/json\" },\n body: { error: errorFromUnknown(error, \"Request handler failed\").message },\n }),\n );\n } catch {}\n }\n }\n })();\n });\n });\n\n pc.addEventListener(\"track\", (event: any) => {\n const track = event.track;\n if (!session.remoteStream.getTracks().some((item: any) => item.id === track.id)) {\n session.remoteStream.addTrack(track);\n }\n this.emitTrack({\n side: \"server\",\n sessionId: session.id,\n peerConnection: pc,\n track,\n stream: session.remoteStream,\n streams: event.streams,\n });\n });\n\n pc.addEventListener(\"connectionstatechange\", () => {\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n void this.closeServerSession(session.id);\n }\n });\n }\n\n private async attachConfiguredServerStream(session: ServerSession): Promise<void> {\n if (session.closed) return;\n\n if (!session.localStreamResolved) {\n const configured = this.options.localStream;\n const isFactory = typeof configured === \"function\";\n const stream = isFactory ? configured(session.id) : configured;\n session.localStreamResolved = true;\n\n if (stream) {\n session.localStream = stream;\n session.ownsLocalStream = isFactory;\n }\n }\n\n const stream = session.localStream;\n if (!stream) return;\n\n const pc = session.peerConnection;\n for (const track of stream.getTracks()) {\n if (session.closed || track.readyState !== \"live\" || session.localTrackIds.has(track.id!)) {\n continue;\n }\n pc.addTrack(track, stream);\n session.localTrackIds.add(track.id!);\n }\n }\n\n private disposeServerLocalStream(session: ServerSession): void {\n const stream = session.localStream;\n const ownsStream = session.ownsLocalStream;\n\n delete session.localStream;\n session.ownsLocalStream = false;\n\n if (!stream || !ownsStream) return;\n\n try {\n this.options.disposeLocalStream?.(session.id, stream);\n } catch {\n } finally {\n for (const track of stream.getTracks()) {\n if (track.readyState !== \"ended\") track.stop();\n }\n }\n }\n\n private async closeServerSession(sessionId: string): Promise<void> {\n const session = this.serverSessions.get(sessionId);\n if (!session || session.closed) return;\n\n session.closed = true;\n this.serverSessions.delete(sessionId);\n\n try {\n await session.peerConnection.close();\n } catch {\n } finally {\n this.disposeServerLocalStream(session);\n }\n }\n\n private emitTrack(event: WebRtcTrackEvent): void {\n for (const listener of this.trackListeners) {\n try {\n listener(event);\n } catch (error) {\n this.log(\"error\", \"[WebRtcTransport] onTrack listener failed:\", error);\n }\n }\n }\n\n private toSignalingUrl(peerUrl: string): string {\n if (peerUrl.startsWith(\"webrtcs://\")) return peerUrl.replace(/^webrtcs:\\/\\//, \"https://\");\n if (peerUrl.startsWith(\"webrtc://\")) return peerUrl.replace(/^webrtc:\\/\\//, \"http://\");\n throw new Error(`Unsupported WebRTC signaling URL: ${peerUrl}`);\n }\n\n private createId(prefix: string): string {\n return `${prefix}-${crypto.randomUUID()}`;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["MAX_SIGNALING_BODY_BYTES", "abortError", "decodeWireBody", "encodeWireBody", "errorFromUnknown", "createServer", "WeriftMediaStream", "WeriftPeerConnection", "normalizeIceServers", "servers", "server", "urls", "url", "createWeriftPeerConnection", "configuration", "config", "createWeriftMediaStream", "WebRtcTransport", "options", "level", "message", "meta", "output", "listener", "sessionId", "session", "peerUrl", "request", "rtcRequest", "ownedSessions", "port", "host", "signalingPath", "char", "code", "closed", "closePromise", "pendingAuthorizations", "httpServer", "req", "res", "sendJson", "status", "body", "parsedUrl", "controller", "onDisconnect", "onAuthorizationAbort", "headers", "name", "value", "allowed", "raw", "chunk", "error", "pc", "payload", "answer", "localDescription", "resolve", "reject", "onError", "onListening", "onAbort", "closeServer", "stopped", "id", "existing", "inFlight", "connectionPromise", "signalingUrl", "channel", "trackListeners", "trackEndedListeners", "streamListeners", "event", "track", "stream", "kind", "sender", "s", "item", "pending", "operation", "changed", "receiveAudio", "receiveVideo", "ensureReceive", "enabled", "signal", "offer", "timer", "abortListener", "response", "text", "description", "previous", "active", "transport", "timeoutMs", "_resolve", "settled", "cleanup", "onStateChange", "finish", "onOpen", "onClose", "requestedTimeout", "requestId", "notifyRemote", "onRequest", "messageEvent", "configured", "isFactory", "ownsStream", "prefix"]
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperttp/transport-webrtc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "WebRTC transport for Hyperttp powered by Werift",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"fetch",
|
|
8
|
+
"high-performance",
|
|
9
|
+
"http",
|
|
10
|
+
"hyperttp",
|
|
11
|
+
"transport"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "dirold2",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/IT-IF-OR/hyperttp-transport-webrtc.git"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"browser": {
|
|
29
|
+
"types": "./dist/browser.d.ts",
|
|
30
|
+
"import": "./dist/browser.js"
|
|
31
|
+
},
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./pcm-audio": {
|
|
36
|
+
"types": "./dist/pcm-audio.d.ts",
|
|
37
|
+
"import": "./dist/pcm-audio.js"
|
|
38
|
+
},
|
|
39
|
+
"./browser": {
|
|
40
|
+
"types": "./dist/browser.d.ts",
|
|
41
|
+
"import": "./dist/browser.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "oxlint src",
|
|
47
|
+
"lint:fix": "oxlint src --fix",
|
|
48
|
+
"format": "oxfmt src",
|
|
49
|
+
"format:check": "oxfmt src --check",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"dev": "tsx src/index.ts",
|
|
52
|
+
"build": "node ../../scripts/build-package.ts",
|
|
53
|
+
"clean": "node --input-type=module -e 'import { rmSync } from \"node:fs\"; rmSync(\"dist\", { recursive: true, force: true });'"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"werift": "^0.26.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/bun": "^1.4.4",
|
|
60
|
+
"@types/node": "^26.6.6",
|
|
61
|
+
"esbuild": "^0.28.2",
|
|
62
|
+
"oxfmt": "^0.75.0",
|
|
63
|
+
"oxlint": "^1.90.0",
|
|
64
|
+
"typescript": "^7.0.2",
|
|
65
|
+
"vitest": "^5.1.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@hyperttp/types": "^0.5.1"
|
|
69
|
+
}
|
|
70
|
+
}
|