openrtc 0.1.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.
Files changed (122) hide show
  1. package/README.md +106 -0
  2. package/dist/OpenRTC.d.ts +71 -0
  3. package/dist/OpenRTC.js +130 -0
  4. package/dist/api/MediaTransport.d.ts +7 -0
  5. package/dist/api/MediaTransport.js +262 -0
  6. package/dist/api/PlutoPeerConnection.d.ts +50 -0
  7. package/dist/api/PlutoPeerConnection.js +434 -0
  8. package/dist/api/PlutoWebSocket.d.ts +24 -0
  9. package/dist/api/PlutoWebSocket.js +113 -0
  10. package/dist/api/PlutoWebTransport.d.ts +29 -0
  11. package/dist/api/PlutoWebTransport.js +105 -0
  12. package/dist/auth/host.d.ts +73 -0
  13. package/dist/auth/host.js +513 -0
  14. package/dist/auth/index.d.ts +1 -0
  15. package/dist/auth/index.js +1 -0
  16. package/dist/auth/internal.d.ts +1 -0
  17. package/dist/auth/internal.js +1 -0
  18. package/dist/compat/index.d.ts +20 -0
  19. package/dist/compat/index.js +20 -0
  20. package/dist/core/Client.d.ts +352 -0
  21. package/dist/core/Client.js +3689 -0
  22. package/dist/core/Connection.d.ts +67 -0
  23. package/dist/core/Connection.js +404 -0
  24. package/dist/core/IEngineBridge.d.ts +18 -0
  25. package/dist/core/IEngineBridge.js +1 -0
  26. package/dist/core/ISignalingTransport.d.ts +13 -0
  27. package/dist/core/ISignalingTransport.js +1 -0
  28. package/dist/core/PeerLifecycleManager.d.ts +43 -0
  29. package/dist/core/PeerLifecycleManager.js +330 -0
  30. package/dist/core/PeerProjectionStore.d.ts +43 -0
  31. package/dist/core/PeerProjectionStore.js +327 -0
  32. package/dist/core/PlutoRTC.d.ts +13 -0
  33. package/dist/core/PlutoRTC.js +26 -0
  34. package/dist/core/Room.d.ts +47 -0
  35. package/dist/core/Room.js +376 -0
  36. package/dist/core/Signaling.d.ts +39 -0
  37. package/dist/core/Signaling.js +147 -0
  38. package/dist/core/bridges/BridgeCodecs.d.ts +19 -0
  39. package/dist/core/bridges/BridgeCodecs.js +317 -0
  40. package/dist/core/bridges/BrowserFirestoreRooms.d.ts +36 -0
  41. package/dist/core/bridges/BrowserFirestoreRooms.js +169 -0
  42. package/dist/core/bridges/BrowserFirestoreSignaling.d.ts +111 -0
  43. package/dist/core/bridges/BrowserFirestoreSignaling.js +1010 -0
  44. package/dist/core/bridges/NativeIpcBridge.d.ts +156 -0
  45. package/dist/core/bridges/NativeIpcBridge.js +1472 -0
  46. package/dist/core/bridges/RtdbPresence.d.ts +122 -0
  47. package/dist/core/bridges/RtdbPresence.js +201 -0
  48. package/dist/core/bridges/TauriBridge.d.ts +8 -0
  49. package/dist/core/bridges/TauriBridge.js +7 -0
  50. package/dist/core/bridges/WasmBridge.d.ts +233 -0
  51. package/dist/core/bridges/WasmBridge.js +1811 -0
  52. package/dist/core/config.d.ts +31 -0
  53. package/dist/core/config.js +138 -0
  54. package/dist/core/transports/ITransport.d.ts +12 -0
  55. package/dist/core/transports/ITransport.js +1 -0
  56. package/dist/core/transports/LocalDaemonBridge.d.ts +41 -0
  57. package/dist/core/transports/LocalDaemonBridge.js +227 -0
  58. package/dist/core/transports/TauriSignaling.d.ts +28 -0
  59. package/dist/core/transports/TauriSignaling.js +373 -0
  60. package/dist/core/transports/TransportFactory.d.ts +9 -0
  61. package/dist/core/transports/TransportFactory.js +59 -0
  62. package/dist/core/transports/WasmSignaling.d.ts +78 -0
  63. package/dist/core/transports/WasmSignaling.js +522 -0
  64. package/dist/core/transports/WebRTCTransport.d.ts +23 -0
  65. package/dist/core/transports/WebRTCTransport.js +132 -0
  66. package/dist/core/trust.d.ts +23 -0
  67. package/dist/core/trust.js +99 -0
  68. package/dist/core/types.d.ts +559 -0
  69. package/dist/core/types.js +19 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +3 -0
  72. package/dist/moq/MoQMediaTransport.d.ts +6 -0
  73. package/dist/moq/MoQMediaTransport.js +106 -0
  74. package/dist/moq/MoQPublisher.d.ts +22 -0
  75. package/dist/moq/MoQPublisher.js +179 -0
  76. package/dist/moq/MoQSession.d.ts +35 -0
  77. package/dist/moq/MoQSession.js +166 -0
  78. package/dist/moq/MoQSubscriber.d.ts +19 -0
  79. package/dist/moq/MoQSubscriber.js +235 -0
  80. package/dist/moq/common.d.ts +47 -0
  81. package/dist/moq/common.js +304 -0
  82. package/dist/moq/messages.d.ts +105 -0
  83. package/dist/moq/messages.js +367 -0
  84. package/dist/presets/plutoHosted.d.ts +13 -0
  85. package/dist/presets/plutoHosted.js +18 -0
  86. package/dist/runtime/DelegatingRuntimeAdapter.d.ts +68 -0
  87. package/dist/runtime/DelegatingRuntimeAdapter.js +250 -0
  88. package/dist/runtime/IpcRuntimeAdapter.d.ts +9 -0
  89. package/dist/runtime/IpcRuntimeAdapter.js +13 -0
  90. package/dist/runtime/RuntimeClient.d.ts +257 -0
  91. package/dist/runtime/RuntimeClient.js +1484 -0
  92. package/dist/runtime/TauriRuntimeAdapter.d.ts +1 -0
  93. package/dist/runtime/TauriRuntimeAdapter.js +1 -0
  94. package/dist/runtime/WasmRuntimeAdapter.d.ts +6 -0
  95. package/dist/runtime/WasmRuntimeAdapter.js +11 -0
  96. package/dist/runtime/factories.d.ts +24 -0
  97. package/dist/runtime/factories.js +66 -0
  98. package/dist/runtime/index.d.ts +6 -0
  99. package/dist/runtime/index.js +5 -0
  100. package/dist/runtime/ipc.d.ts +8 -0
  101. package/dist/runtime/ipc.js +65 -0
  102. package/dist/runtime/ipcContract.d.ts +46 -0
  103. package/dist/runtime/ipcContract.js +1 -0
  104. package/dist/runtime/tauri.d.ts +16 -0
  105. package/dist/runtime/tauri.js +22 -0
  106. package/dist/runtime/types.d.ts +110 -0
  107. package/dist/runtime/types.js +1 -0
  108. package/dist/transports/TauriTransport.d.ts +17 -0
  109. package/dist/transports/TauriTransport.js +49 -0
  110. package/dist/transports/WebTransport.d.ts +23 -0
  111. package/dist/transports/WebTransport.js +71 -0
  112. package/dist/utils/debug.d.ts +4 -0
  113. package/dist/utils/debug.js +33 -0
  114. package/dist/utils/runtime.d.ts +1 -0
  115. package/dist/utils/runtime.js +10 -0
  116. package/package.json +97 -0
  117. package/wasm/README.md +64 -0
  118. package/wasm/openrtc.d.ts +284 -0
  119. package/wasm/openrtc.js +2132 -0
  120. package/wasm/openrtc_bg.wasm +0 -0
  121. package/wasm/openrtc_bg.wasm.d.ts +110 -0
  122. package/wasm/package.json +16 -0
@@ -0,0 +1,434 @@
1
+ import { MediaTransport } from './MediaTransport';
2
+ import { MoQMediaTransport } from '../moq/MoQMediaTransport';
3
+ import { TransportFactory } from '../core/transports/TransportFactory';
4
+ // Tiny SDP helpers to make the ticket look like an SDP so generic parsers don't choke immediately
5
+ // although we usually just pass the string. The demo code uses desc.sdp.
6
+ function toSDP(ticket) {
7
+ return `v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=pluto-ticket:${ticket}\r\n`;
8
+ }
9
+ function fromSDP(sdp) {
10
+ const match = sdp.match(/a=pluto-ticket:(.+)(\r\n|$)/);
11
+ return match ? match[1] : null;
12
+ }
13
+ export class PlutoPeerConnection extends EventTarget {
14
+ static setDefaultClient(client) {
15
+ this.defaultClient = client;
16
+ }
17
+ // MoQ State helpers using Client
18
+ get useMoq() {
19
+ return this.client.moqState === 'ready';
20
+ }
21
+ get transportInfo() {
22
+ return {
23
+ signaling: this.connection?.transportType || 'disconnected',
24
+ media: this.useMoq ? 'moq' : (this.connection?.transportType || 'disconnected')
25
+ };
26
+ }
27
+ constructor(configuration, clientInstance) {
28
+ super();
29
+ this.onicecandidate = null;
30
+ this.oniceconnectionstatechange = null;
31
+ this.ontrack = null;
32
+ this.onsignalingstatechange = null;
33
+ this.connectionState = 'new';
34
+ this.iceConnectionState = 'new';
35
+ this.signalingState = 'stable';
36
+ this.localDescription = null;
37
+ this.remoteDescription = null;
38
+ this.connection = null;
39
+ /**
40
+ * Manually provide an existing connection (e.g. from an incoming connection listener)
41
+ */
42
+ this.bufferTracks = [];
43
+ this.receivers = {};
44
+ // MoQ receivers: direct WebCodecs decode (bypass MediaTransport framing)
45
+ this.moqDecoders = {};
46
+ this.moqGenerators = {};
47
+ this.mediaSenders = {};
48
+ TransportFactory.assertProtocolAvailable('webrtc');
49
+ // Try to resolve client: constructor arg -> config arg (custom) -> static default
50
+ const client = clientInstance || configuration?.client || PlutoPeerConnection.defaultClient;
51
+ if (!client) {
52
+ throw new Error("PlutoPeerConnection requires a 'Client' instance. Pass it in constructor or use PlutoPeerConnection.setDefaultClient().");
53
+ }
54
+ this.client = client;
55
+ // MoQ is configured at Client level via experimental.moq
56
+ }
57
+ async createOffer(options) {
58
+ // Get our ticket
59
+ let ticket;
60
+ try {
61
+ ticket = await this.client.getTicket();
62
+ }
63
+ catch (e) {
64
+ console.error("Failed to get ticket for offer:", e);
65
+ ticket = "error-no-ticket";
66
+ }
67
+ const sdp = toSDP(ticket);
68
+ return { type: 'offer', sdp };
69
+ }
70
+ async createAnswer(options) {
71
+ // Return dummy answer.
72
+ return { type: 'answer', sdp: toSDP('answer-dummy') };
73
+ }
74
+ async setLocalDescription(desc) {
75
+ this.localDescription = desc;
76
+ this.signalingState = (desc.type === 'offer') ? 'have-local-offer' : 'stable';
77
+ // If we just set a local offer, we should simulate gathering candidates (immediate finish)
78
+ if (desc.type === 'offer') {
79
+ setTimeout(() => {
80
+ // Emit a null candidate to signal end of candidates
81
+ const event = new Event('icecandidate');
82
+ event.candidate = null;
83
+ if (this.onicecandidate)
84
+ this.onicecandidate(event);
85
+ this.dispatchEvent(event);
86
+ }, 10);
87
+ }
88
+ }
89
+ async setRemoteDescription(desc) {
90
+ this.remoteDescription = desc;
91
+ this.signalingState = (desc.type === 'offer') ? 'have-remote-offer' : 'stable';
92
+ // Check if description has a ticket
93
+ const ticket = fromSDP(desc.sdp || '');
94
+ if (ticket && ticket !== 'answer-dummy' && ticket !== 'error-no-ticket') {
95
+ // We received a ticket! Connect to it.
96
+ this.connect(ticket);
97
+ }
98
+ }
99
+ async addIceCandidate(candidate) {
100
+ // No-op for Iroh/Pluto
101
+ return;
102
+ }
103
+ async handleIncomingConnection(connection) {
104
+ this.connection = connection;
105
+ this.setupHooks();
106
+ // Flush buffered tracks
107
+ this.bufferTracks.forEach(t => this.addTrack(t.track));
108
+ this.bufferTracks = [];
109
+ }
110
+ setupHooks() {
111
+ if (!this.connection)
112
+ return;
113
+ this.connection.onDisconnect(() => this.close());
114
+ this.connection.onTrack((ev) => {
115
+ // Native WebRTC Track
116
+ if (this.ontrack)
117
+ this.ontrack(ev);
118
+ const newEvent = new RTCTrackEvent('track', {
119
+ track: ev.track,
120
+ streams: [...ev.streams],
121
+ receiver: ev.receiver,
122
+ transceiver: ev.transceiver
123
+ });
124
+ this.dispatchEvent(newEvent);
125
+ });
126
+ // Use Client-level MoQ listeners
127
+ // Only process objects where namespace == remote deviceId
128
+ const moqObjectHandler = (ns, name, obj, meta) => {
129
+ if (this.connection && ns === this.connection.remoteNodeId) {
130
+ if (name.startsWith('data/')) {
131
+ this.handleMoQData(obj);
132
+ }
133
+ else {
134
+ this.handleMoQMedia(ns, name, obj, meta);
135
+ }
136
+ }
137
+ };
138
+ this.client.onMoQObject(moqObjectHandler);
139
+ // Iroh media fallback (used when MoQ is not available)
140
+ this.connection.onMedia((type, data) => {
141
+ if (!this.useMoq) {
142
+ this.handleLegacyMedia(type, data);
143
+ }
144
+ });
145
+ // MoQ Subscription Logic
146
+ const subscribeMoQ = () => {
147
+ if (this.client.moq && this.connection) {
148
+ console.log(`[PPC] MoQ Ready — subscribing to ${this.connection.remoteNodeId} media`);
149
+ this.client.moq.subscriber.subscribe(this.connection.remoteNodeId, "audio");
150
+ this.client.moq.subscriber.subscribe(this.connection.remoteNodeId, "video");
151
+ // Data Tracks
152
+ this.client.moq.subscriber.subscribe(this.connection.remoteNodeId, "data/broadcast");
153
+ this.client.getNodeId().then(myId => {
154
+ if (this.connection)
155
+ this.client.moq?.subscriber.subscribe(this.connection.remoteNodeId, `data/${myId}`);
156
+ });
157
+ }
158
+ };
159
+ if (this.client.moqState === 'ready') {
160
+ subscribeMoQ();
161
+ }
162
+ else if (this.client.moqState === 'connecting') {
163
+ this.client.onMoQReady(subscribeMoQ);
164
+ }
165
+ }
166
+ handleLegacyMedia(type, data) {
167
+ let track = this.receivers[type];
168
+ if (!track) {
169
+ // New track received!
170
+ track = MediaTransport.receiveTrack(type, () => {
171
+ this.connection?.send({ type: 'PLI', kind: type }).catch(console.error);
172
+ });
173
+ if (track) {
174
+ this.receivers[type] = track;
175
+ this.fireTrackEvent(track);
176
+ }
177
+ }
178
+ if (track)
179
+ (track.feed(data));
180
+ }
181
+ handleMoQMedia(namespace, name, object, meta) {
182
+ const trackId = `${namespace}/${name}`;
183
+ const kind = name.includes('video') ? 'video' : 'audio';
184
+ // Create receiver track + decoder on first object
185
+ if (!this.receivers[trackId]) {
186
+ const generator = new window.MediaStreamTrackGenerator({ kind });
187
+ const writer = generator.writable.getWriter();
188
+ this.moqGenerators[trackId] = generator;
189
+ if (kind === 'video') {
190
+ const decoder = new window.VideoDecoder({
191
+ output: (frame) => {
192
+ try {
193
+ writer.write(frame);
194
+ }
195
+ finally {
196
+ frame.close();
197
+ }
198
+ },
199
+ error: (e) => console.error("MoQ VideoDecoder error", e)
200
+ });
201
+ decoder.configure({ codec: 'vp09.00.10.08' });
202
+ decoder.waitingForKeyFrame = true;
203
+ this.moqDecoders[trackId] = decoder;
204
+ }
205
+ else {
206
+ const decoder = new window.AudioDecoder({
207
+ output: (data) => { writer.write(data); },
208
+ error: (e) => console.error("MoQ AudioDecoder error", e)
209
+ });
210
+ decoder.configure({ codec: 'opus', sampleRate: 48000, numberOfChannels: 1 });
211
+ this.moqDecoders[trackId] = decoder;
212
+ }
213
+ this.receivers[trackId] = generator;
214
+ this.fireTrackEvent(generator);
215
+ }
216
+ const decoder = this.moqDecoders[trackId];
217
+ if (!decoder || decoder.state === 'closed')
218
+ return;
219
+ if (kind === 'video') {
220
+ // First object in a group (objectId=0) is assumed to be the keyframe
221
+ const isKey = meta.objectId === 0;
222
+ if (!isKey && decoder.waitingForKeyFrame) {
223
+ return; // Drop delta frames until we get a keyframe
224
+ }
225
+ try {
226
+ // Parse [timestamp 8B big-endian][encoded data]
227
+ const view = new DataView(object.buffer, object.byteOffset, object.byteLength);
228
+ const timestamp = Number(view.getBigInt64(0, false));
229
+ const encodedData = object.slice(8);
230
+ const chunk = new window.EncodedVideoChunk({
231
+ type: isKey ? 'key' : 'delta',
232
+ timestamp,
233
+ data: encodedData
234
+ });
235
+ decoder.decode(chunk);
236
+ // If we got here without error, this keyframe was valid
237
+ if (isKey) {
238
+ decoder.waitingForKeyFrame = false;
239
+ }
240
+ }
241
+ catch (e) {
242
+ // VP8 realtime encoder can report type='key' but produce delta bitstream.
243
+ // Reset decoder and wait for next group's real keyframe.
244
+ if (isKey) {
245
+ console.warn("[MoQ] Video keyframe rejected by decoder — waiting for next group");
246
+ }
247
+ decoder.waitingForKeyFrame = true;
248
+ // Reset decoder to clear any corrupted state
249
+ try {
250
+ decoder.reset();
251
+ decoder.configure({ codec: 'vp09.00.10.08' });
252
+ }
253
+ catch (_) { /* decoder may already be closed */ }
254
+ }
255
+ }
256
+ else {
257
+ // Audio: all Opus frames are independently decodable
258
+ try {
259
+ // Parse [timestamp 8B big-endian][encoded data]
260
+ const view = new DataView(object.buffer, object.byteOffset, object.byteLength);
261
+ const timestamp = Number(view.getBigInt64(0, false));
262
+ const encodedData = object.slice(8);
263
+ const chunk = new window.EncodedAudioChunk({
264
+ type: 'key',
265
+ timestamp,
266
+ data: encodedData
267
+ });
268
+ decoder.decode(chunk);
269
+ }
270
+ catch (e) {
271
+ console.error("[MoQ] Audio decode error:", e);
272
+ }
273
+ }
274
+ }
275
+ handleMoQData(object) {
276
+ // MoQ Data Payload: [Len][Type][Payload] ??
277
+ // In Connection.sendTyped, we framed it: [Len 4][Type 1][Payload].
278
+ // So `object` here IS the frame.
279
+ // We can pass it directly to `connection.handleData`?
280
+ // `connection.handleData` expects streamed chunks and buffers them.
281
+ // `object` is a discrete unit.
282
+ // We can synthesize a chunk push to Connection?
283
+ // Or unpack here?
284
+ // If we pass to `connection.handleData`, it will parse the length header.
285
+ // Since `object` IS the frame (as sent by sendTyped), this works!
286
+ // We just need to expose `handleData` or similar on Connection?
287
+ // `Connection.handleData` is private.
288
+ // But `Connection` reads from `reader`.
289
+ // We can push to `reader` stream?
290
+ // `Connection` constructor takes `reader`.
291
+ // We can't access `reader` easily from outside.
292
+ // Alternative: Add `injectData(chunk: Uint8Array)` to Connection.
293
+ if (this.connection) {
294
+ // @ts-ignore - Accessing private for now or add public method
295
+ if (typeof this.connection['handleData'] === 'function') {
296
+ // @ts-ignore
297
+ this.connection['handleData'](object);
298
+ }
299
+ }
300
+ }
301
+ fireTrackEvent(track) {
302
+ const event = new Event('track');
303
+ event.track = track;
304
+ event.streams = [new MediaStream([track])];
305
+ event.receiver = { track };
306
+ event.transceiver = { receiver: event.receiver, sender: {}, direction: 'receiveonly' };
307
+ if (this.ontrack)
308
+ this.ontrack(event);
309
+ this.dispatchEvent(event);
310
+ }
311
+ addTrack(track, ...streams) {
312
+ const sender = {
313
+ track,
314
+ replaceTrack: async () => { },
315
+ getStats: async () => new Map()
316
+ };
317
+ if (this.connection) {
318
+ // Transport Choice: MoQ (via Client) > Iroh
319
+ if (this.client.moqState === 'ready') {
320
+ this.startMoQStream(track);
321
+ }
322
+ else if (this.client.moqState === 'connecting') {
323
+ console.log("[PPC] addTrack: MoQ connecting, waiting...");
324
+ const onReady = () => {
325
+ console.log("[PPC] MoQ ready, starting stream");
326
+ this.startMoQStream(track);
327
+ };
328
+ const onFailed = () => {
329
+ console.warn("[PPC] MoQ failed, falling back to Iroh");
330
+ this.startIrohStream(track);
331
+ };
332
+ this.client.onMoQReady(onReady);
333
+ this.client.onMoQFailed(onFailed);
334
+ }
335
+ else {
336
+ // Legacy Iroh
337
+ this.startIrohStream(track);
338
+ }
339
+ }
340
+ else {
341
+ this.bufferTracks.push({ track });
342
+ }
343
+ return sender;
344
+ }
345
+ async startIrohStream(track) {
346
+ if (!this.connection)
347
+ return;
348
+ const conn = this.connection;
349
+ try {
350
+ const sender = await MediaTransport.sendTrack(track, async (data) => {
351
+ if (!this.connection || this.connection.isClosed)
352
+ throw new Error("Connection closed");
353
+ try {
354
+ await conn.sendMedia(track.kind, data);
355
+ }
356
+ catch (e) {
357
+ if (e.message && e.message.includes('Connection is closed'))
358
+ return;
359
+ console.error("Iroh send failed", e);
360
+ }
361
+ });
362
+ if (sender) {
363
+ this.mediaSenders[track.kind] = sender;
364
+ }
365
+ }
366
+ catch (e) {
367
+ console.error("Failed to start Iroh stream", e);
368
+ }
369
+ }
370
+ async startMoQStream(track) {
371
+ if (this.client.moqState !== 'ready' || !this.client.moq)
372
+ return;
373
+ console.log(`[PPC] Starting MoQ Stream for ${track.kind}`);
374
+ try {
375
+ // Use shared transport
376
+ const transport = new MoQMediaTransport(this.client.moq.publisher);
377
+ // Namespace is LOCAL node ID (client knows it)
378
+ const namespace = await this.client.getNodeId();
379
+ await transport.sendTrack(track, namespace, track.kind);
380
+ console.log(`[MoQ] Started publishing track: ${track.kind} to ${namespace}`);
381
+ }
382
+ catch (e) {
383
+ console.error("Failed to start MoQ stream", e);
384
+ }
385
+ }
386
+ createDataChannel(label) {
387
+ // We return a proxy that calls connection.send
388
+ return new PlutoDataChannel(label, this);
389
+ }
390
+ sendData(data) {
391
+ this.connection?.send(data);
392
+ }
393
+ async connect(ticket) {
394
+ this.connection = await this.client.connect(ticket);
395
+ this.setupHooks();
396
+ // Flush any buffered tracks now that we are connected
397
+ this.bufferTracks.forEach(t => this.addTrack(t.track));
398
+ this.bufferTracks = [];
399
+ this.updateState('connected');
400
+ }
401
+ close() {
402
+ this.connection?.disconnect();
403
+ // Stop all legacy senders
404
+ Object.values(this.mediaSenders).forEach(sender => {
405
+ try {
406
+ sender.stop();
407
+ }
408
+ catch (e) {
409
+ console.warn("Error stopping sender", e);
410
+ }
411
+ });
412
+ this.mediaSenders = {};
413
+ this.updateState('closed');
414
+ }
415
+ updateState(state) {
416
+ this.connectionState = state;
417
+ this.iceConnectionState = (state === 'closed') ? 'closed' : 'connected'; // aprox
418
+ this.dispatchEvent(new Event('connectionstatechange'));
419
+ this.dispatchEvent(new Event('iceconnectionstatechange'));
420
+ }
421
+ }
422
+ PlutoPeerConnection.defaultClient = null;
423
+ // ... Simple Data Channel Proxy ...
424
+ class PlutoDataChannel extends EventTarget {
425
+ constructor(label, pc) {
426
+ super();
427
+ this.label = label;
428
+ this.pc = pc;
429
+ setTimeout(() => this.emitOpen(), 0);
430
+ }
431
+ send(data) { this.pc.sendData(data); }
432
+ emitOpen() { this.dispatchEvent(new Event('open')); if (this.onopen)
433
+ this.onopen(new Event('open')); }
434
+ }
@@ -0,0 +1,24 @@
1
+ import { Client } from '../core/Client';
2
+ export declare class PlutoWebSocket extends EventTarget {
3
+ static readonly CONNECTING = 0;
4
+ static readonly OPEN = 1;
5
+ static readonly CLOSING = 2;
6
+ static readonly CLOSED = 3;
7
+ readonly url: string;
8
+ readyState: number;
9
+ bufferedAmount: number;
10
+ extensions: string;
11
+ protocol: string;
12
+ binaryType: "blob" | "arraybuffer";
13
+ onopen: ((this: PlutoWebSocket, ev: Event) => any) | null;
14
+ onmessage: ((this: PlutoWebSocket, ev: MessageEvent) => any) | null;
15
+ onclose: ((this: PlutoWebSocket, ev: CloseEvent) => any) | null;
16
+ onerror: ((this: PlutoWebSocket, ev: Event) => any) | null;
17
+ private client;
18
+ private connection;
19
+ constructor(url: string, protocols?: string | string[], clientInstance?: Client);
20
+ private connect;
21
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
22
+ close(code?: number, reason?: string): void;
23
+ private handleClose;
24
+ }
@@ -0,0 +1,113 @@
1
+ import { TransportFactory } from '../core/transports/TransportFactory';
2
+ export class PlutoWebSocket extends EventTarget {
3
+ constructor(url, protocols, clientInstance) {
4
+ super();
5
+ this.readyState = PlutoWebSocket.CONNECTING;
6
+ this.bufferedAmount = 0;
7
+ this.extensions = "";
8
+ this.protocol = "";
9
+ this.binaryType = "blob";
10
+ this.onopen = null;
11
+ this.onmessage = null;
12
+ this.onclose = null;
13
+ this.onerror = null;
14
+ this.connection = null;
15
+ this.url = url;
16
+ TransportFactory.assertProtocolAvailable('websocket');
17
+ // We need a Client instance. If not provided, we have to throw or use a singleton?
18
+ // Since Pluto requires setup (tag, auth), it's hard to make a 100% drop-in without configuration.
19
+ // We will assume the user has a Client initialized or we provide one via a static method,
20
+ // BUT for a true new WebSocket replacement, we might rely on a global or strictly require a passed client if not configuring here.
21
+ // However, the standard WebSocket(url) doesn't take a client.
22
+ // To make it easy, we will support passing the client in the 2nd arg if protocols is ignored/abused,
23
+ // or just expect the developer to instantiate this class with the client.
24
+ if (clientInstance) {
25
+ this.client = clientInstance;
26
+ }
27
+ else {
28
+ throw new Error("PlutoWebSocket requires a 'Client' instance to be passed in constructor currently.");
29
+ }
30
+ this.connect();
31
+ }
32
+ async connect() {
33
+ try {
34
+ // URL format: pluto://<ticket>
35
+ let ticket = this.url;
36
+ if (ticket.startsWith('pluto://')) {
37
+ ticket = ticket.replace('pluto://', '');
38
+ }
39
+ else if (ticket.startsWith('wss://') || ticket.startsWith('ws://')) {
40
+ // Ignore standard WS urls or treat as ticket?
41
+ // Assume ticket only for now.
42
+ }
43
+ this.connection = await this.client.connect(ticket);
44
+ this.readyState = PlutoWebSocket.OPEN;
45
+ const event = new Event('open');
46
+ if (this.onopen)
47
+ this.onopen(event);
48
+ this.dispatchEvent(event);
49
+ this.connection.onMessage((msg) => {
50
+ let data = msg;
51
+ // If msg is buffer/uint8array, respect binaryType
52
+ if (this.binaryType === 'arraybuffer' && (msg instanceof Uint8Array)) {
53
+ data = msg.buffer;
54
+ }
55
+ const msgEvent = new MessageEvent('message', {
56
+ data,
57
+ origin: this.url,
58
+ });
59
+ if (this.onmessage)
60
+ this.onmessage(msgEvent);
61
+ this.dispatchEvent(msgEvent);
62
+ });
63
+ this.connection.onDisconnect(() => {
64
+ this.handleClose();
65
+ });
66
+ }
67
+ catch (e) {
68
+ console.error("WebSocket connection failure:", e);
69
+ const event = new Event('error');
70
+ if (this.onerror)
71
+ this.onerror(event);
72
+ this.dispatchEvent(event);
73
+ this.handleClose(1006, "Connection failed");
74
+ }
75
+ }
76
+ send(data) {
77
+ if (this.readyState !== PlutoWebSocket.OPEN || !this.connection) {
78
+ throw new Error("WebSocket is not open");
79
+ }
80
+ if (data instanceof Blob) {
81
+ data.arrayBuffer().then(buf => {
82
+ this.connection?.send(buf);
83
+ });
84
+ }
85
+ else {
86
+ // @ts-ignore - Connection.send handles these
87
+ this.connection.send(data);
88
+ }
89
+ }
90
+ close(code, reason) {
91
+ if (this.readyState === PlutoWebSocket.CLOSED)
92
+ return;
93
+ this.readyState = PlutoWebSocket.CLOSING;
94
+ if (this.connection) {
95
+ this.connection.disconnect();
96
+ }
97
+ // Force close if it takes too long?
98
+ this.handleClose(code, reason);
99
+ }
100
+ handleClose(code = 1000, reason = "") {
101
+ if (this.readyState === PlutoWebSocket.CLOSED)
102
+ return;
103
+ this.readyState = PlutoWebSocket.CLOSED;
104
+ const event = new CloseEvent('close', { code, reason, wasClean: true });
105
+ if (this.onclose)
106
+ this.onclose(event);
107
+ this.dispatchEvent(event);
108
+ }
109
+ }
110
+ PlutoWebSocket.CONNECTING = 0;
111
+ PlutoWebSocket.OPEN = 1;
112
+ PlutoWebSocket.CLOSING = 2;
113
+ PlutoWebSocket.CLOSED = 3;
@@ -0,0 +1,29 @@
1
+ import { Client } from '../core/Client';
2
+ export declare class PlutoWebTransport {
3
+ readonly ready: Promise<void>;
4
+ closed: Promise<void>;
5
+ datagrams: any;
6
+ incomingBidirectionalStreams: ReadableStream<WebTransportBidirectionalStream>;
7
+ incomingUnidirectionalStreams: ReadableStream<ReadableStream<Uint8Array>>;
8
+ private client;
9
+ private connection;
10
+ private url;
11
+ private _readyResolve;
12
+ private _readyReject;
13
+ private _closedResolve;
14
+ private _closedReject;
15
+ private incomingBiController;
16
+ private incomingUniController;
17
+ private remoteNodeId;
18
+ private offIncomingStreams;
19
+ constructor(url: string, options?: any, clientInstance?: Client);
20
+ private setupListeners;
21
+ private connect;
22
+ createBidirectionalStream(): Promise<WebTransportBidirectionalStream>;
23
+ createUnidirectionalStream(): Promise<WritableStream<Uint8Array>>;
24
+ close(): void;
25
+ }
26
+ export interface WebTransportBidirectionalStream {
27
+ readable: ReadableStream<Uint8Array>;
28
+ writable: WritableStream<Uint8Array>;
29
+ }