fastrtc 1.0.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 +233 -0
- package/dist/consts.d.ts +9 -0
- package/dist/event-emitter.d.ts +7 -0
- package/dist/fast-data-channel.d.ts +24 -0
- package/dist/framing.d.ts +5 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/peer-node.d.ts +26 -0
- package/dist/rtc-manager.d.ts +44 -0
- package/dist/stream-source.d.ts +12 -0
- package/dist/transfer-limiter.d.ts +6 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NotZero
|
|
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
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# fastrtc
|
|
2
|
+
|
|
3
|
+
Browser WebRTC mesh library. Zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
You wire signaling (WebSocket, SSE, whatever). fastrtc handles peer connections, data channels, and media tracks.
|
|
6
|
+
|
|
7
|
+
## Mental model
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
You fastrtc Remote peer
|
|
11
|
+
│ │ │
|
|
12
|
+
│ signal(to, payload) ───────►│ forwards SDP / ICE │
|
|
13
|
+
│◄──── receive(from, payload) ──│ │
|
|
14
|
+
│ │◄──────── WebRTC ────────────────►│
|
|
15
|
+
│ │ │
|
|
16
|
+
│ channel("chat") │ opens labeled data channels │
|
|
17
|
+
│ connect("bob") │ negotiates connection │
|
|
18
|
+
│ on("ready") │ all channels open → safe to send │
|
|
19
|
+
│ send / sendTo │ text or streamed files │
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Three things to remember:**
|
|
23
|
+
|
|
24
|
+
1. **Signaling is yours** — forward `SignalPayload` objects between peers. fastrtc never touches your server.
|
|
25
|
+
2. **Declare channels before `connect()`** — `channel("files")` registers a label for every current and future peer.
|
|
26
|
+
3. **`ready` means go** — wait for `ready` (or `whenOpen`) before sending on a channel.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add fastrtc
|
|
32
|
+
# or
|
|
33
|
+
npm install fastrtc
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { RTCManager } from "fastrtc";
|
|
40
|
+
import type { SignalPayload } from "fastrtc";
|
|
41
|
+
|
|
42
|
+
const rtc = new RTCManager({
|
|
43
|
+
id: "alice",
|
|
44
|
+
signal: (to, payload) => {
|
|
45
|
+
ws.send(JSON.stringify({ to, from: "alice", payload }));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
ws.onmessage = (e) => {
|
|
50
|
+
const { from, payload } = JSON.parse(e.data);
|
|
51
|
+
rtc.receive(from, payload as SignalPayload);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
rtc.channel("chat", { ordered: true });
|
|
55
|
+
rtc.connect("bob");
|
|
56
|
+
|
|
57
|
+
rtc.on("channel", (_id, ch) => {
|
|
58
|
+
ch.on("message", (data) => console.log(data));
|
|
59
|
+
ch.on("open", () => void ch.send("hello"));
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Signaling
|
|
64
|
+
|
|
65
|
+
Each outbound signal is a `SignalPayload`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
type SignalPayload =
|
|
69
|
+
| { type: "description"; description: RTCSessionDescriptionInit }
|
|
70
|
+
| { type: "candidate"; candidate: RTCIceCandidateInit };
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `signal(to, payload)` — fastrtc needs to send SDP or ICE to `to`
|
|
74
|
+
- `receive(from, payload)` — apply a remote signal (auto-connects if needed)
|
|
75
|
+
|
|
76
|
+
**Offer glare:** peer with the lexicographically larger ID is *polite* and yields on collision. Only the *impolite* peer creates labeled data channels; the polite peer waits for `ondatachannel`.
|
|
77
|
+
|
|
78
|
+
## Data channels
|
|
79
|
+
|
|
80
|
+
### Setup
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
rtc.channel("chat", { ordered: true });
|
|
84
|
+
rtc.channel("files", { ordered: true });
|
|
85
|
+
|
|
86
|
+
rtc.on("channel", (peerId, ch) => {
|
|
87
|
+
ch.on("open", () => { /* channel is open for this peer */ });
|
|
88
|
+
ch.on("message", (data) => { /* string message */ });
|
|
89
|
+
ch.on("stream", async (stream, meta) => {
|
|
90
|
+
const blob = await new Response(stream).blob();
|
|
91
|
+
console.log(peerId, meta, blob.size);
|
|
92
|
+
});
|
|
93
|
+
ch.on("progress", ({ direction, bytes, total }) => {
|
|
94
|
+
console.log(direction, bytes, total);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
rtc.on("ready", (peerId) => {
|
|
99
|
+
// every declared channel is open for this peer
|
|
100
|
+
void rtc.sendTo(peerId, "files", file);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Event | When |
|
|
105
|
+
| ------- | ---- |
|
|
106
|
+
| `join` | Peer connection created |
|
|
107
|
+
| `ready` | All declared channels open for that peer (fires once) |
|
|
108
|
+
| `leave` | Peer disconnected |
|
|
109
|
+
|
|
110
|
+
Get a channel directly: `rtc.getChannel(peerId, "files")` or `await rtc.whenOpen(peerId, "files")`.
|
|
111
|
+
|
|
112
|
+
### Sending
|
|
113
|
+
|
|
114
|
+
| Method | Target |
|
|
115
|
+
| ------ | ------ |
|
|
116
|
+
| `send(label, data)` | Every peer with an open channel on `label` |
|
|
117
|
+
| `sendTo(peerId, label, data)` | One peer |
|
|
118
|
+
| `ch.send(data)` | When you already have the `FastDataChannel` |
|
|
119
|
+
|
|
120
|
+
**Data types:**
|
|
121
|
+
|
|
122
|
+
- **String** → single SCTP message → `message` event
|
|
123
|
+
- **Blob / File / BufferSource / ReadableStream / async iterable** → framed stream → `stream` event
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await rtc.send("files", file); // all peers
|
|
127
|
+
await rtc.sendTo("bob", "files", file); // one peer
|
|
128
|
+
await rtc.sendTo("bob", "files", file, {
|
|
129
|
+
meta: { name: file.name }, // JSON on first frame
|
|
130
|
+
offset: 1024, // Blob/File slice start
|
|
131
|
+
onProgress: (bytes, total) => { /* ... */ },
|
|
132
|
+
signal: abortController.signal,
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Multi-peer file sends:** pass the `Blob`/`File`, not `blob.stream()`. Each peer gets its own `blob.stream()`, so a slow peer does not block others. A late joiner gets a fresh stream from byte 0.
|
|
137
|
+
|
|
138
|
+
**ReadableStream caveat:** one-shot streams work with `sendTo` only. `send()` to multiple peers throws — use a `Blob`/`File` instead.
|
|
139
|
+
|
|
140
|
+
Strings are drained before queued file frames, so control messages are not stuck behind a large send.
|
|
141
|
+
|
|
142
|
+
### Receiving
|
|
143
|
+
|
|
144
|
+
Attach a `stream` listener before data arrives — streams without a listener are dropped. A slow consumer that lets the receive queue grow past the backlog limit gets an error instead of unbounded RAM.
|
|
145
|
+
|
|
146
|
+
## Media
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
150
|
+
rtc.add(stream); // tracks added before connect() attach to new peers automatically
|
|
151
|
+
|
|
152
|
+
rtc.on("track", (peerId, track, stream) => {
|
|
153
|
+
videoElement.srcObject = stream;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
rtc.on("track-ended", (peerId, track) => { /* remote track stopped */ });
|
|
157
|
+
|
|
158
|
+
rtc.remove(stream);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Transfer limiter
|
|
162
|
+
|
|
163
|
+
Large file sends use memory. `maxTransfers` caps how many run at once (default `4`); extras wait in FIFO.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const rtc = new RTCManager({ id: "alice", signal, maxTransfers: 4 });
|
|
167
|
+
// Infinity = unlimited
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each outgoing framed send holds one slot until complete. Same-channel sends are serialized and do not hold a slot while waiting.
|
|
171
|
+
|
|
172
|
+
## API
|
|
173
|
+
|
|
174
|
+
### `RTCManager`
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
new RTCManager({
|
|
178
|
+
id: string;
|
|
179
|
+
signal: (to: string, payload: SignalPayload) => void;
|
|
180
|
+
rtcConfig?: RTCConfiguration; // default: Google STUN
|
|
181
|
+
maxTransfers?: number; // default: 4
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Method | Description |
|
|
186
|
+
| ------ | ----------- |
|
|
187
|
+
| `connect(remoteId)` | Open connection to a peer |
|
|
188
|
+
| `receive(remoteId, payload)` | Apply remote SDP/ICE |
|
|
189
|
+
| `channel(label, options?)` | Register a labeled channel for all peers |
|
|
190
|
+
| `getChannel(peerId, label)` | Existing channel, or `undefined` |
|
|
191
|
+
| `whenOpen(peerId, label, { signal? })` | Promise for an open channel |
|
|
192
|
+
| `send(label, data, options?)` | Send to every open peer on `label` |
|
|
193
|
+
| `sendTo(peerId, label, data, options?)` | Send to one peer |
|
|
194
|
+
| `add(stream)` / `remove(stream)` | Manage outgoing media tracks |
|
|
195
|
+
| `close(remoteId)` | Tear down one peer |
|
|
196
|
+
| `destroy()` | Close everything, reject queued transfers |
|
|
197
|
+
|
|
198
|
+
| Property | Description |
|
|
199
|
+
| -------- | ----------- |
|
|
200
|
+
| `id` | Local peer ID |
|
|
201
|
+
| `rtcConfig` | Active RTC configuration |
|
|
202
|
+
|
|
203
|
+
**Events:** `join`, `ready`, `leave`, `channel`, `track`, `track-ended`. `.on(event, handler)` returns unsubscribe.
|
|
204
|
+
|
|
205
|
+
### `FastDataChannel`
|
|
206
|
+
|
|
207
|
+
Wraps `RTCDataChannel` with streaming, backpressure, and typed events.
|
|
208
|
+
|
|
209
|
+
| Method / property | Description |
|
|
210
|
+
| ----------------- | ----------- |
|
|
211
|
+
| `channel` | Underlying `RTCDataChannel` |
|
|
212
|
+
| `send(data, options?)` | String → message; else → framed stream |
|
|
213
|
+
| `destroy()` | Close and detach |
|
|
214
|
+
|
|
215
|
+
**Events:** `open`, `close`, `message`, `error`, `stream`, `progress`.
|
|
216
|
+
|
|
217
|
+
### Exports
|
|
218
|
+
|
|
219
|
+
`RTCManager`, `FastDataChannel`, `SignalPayload`, `SendData`, `SendOptions`
|
|
220
|
+
|
|
221
|
+
## Build
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
pnpm build # output in dist/
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Requirements
|
|
228
|
+
|
|
229
|
+
- Browser with `RTCPeerConnection` and `RTCDataChannel`
|
|
230
|
+
- A signaling transport you control
|
|
231
|
+
|
|
232
|
+
## Source Code
|
|
233
|
+
Since this plugin is MIT licensed, you can also contribute to it at it's repo on [GitHub](https://github.com/YSpoof/fastrtc)
|
package/dist/consts.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const FRAME_MAGIC = 252;
|
|
2
|
+
export declare const FLAG_FIRST = 1;
|
|
3
|
+
export declare const FLAG_LAST = 2;
|
|
4
|
+
export declare const FLAG_META = 4;
|
|
5
|
+
export declare const FRAME_HEADER_SIZE = 2;
|
|
6
|
+
export declare const RECEIVE_BACKLOG = 32;
|
|
7
|
+
export declare const SCTP_DEFAULT_MESSAGE_SIZE = 65536;
|
|
8
|
+
export declare const DEFAULT_RTC_CONFIG: RTCConfiguration;
|
|
9
|
+
export declare const DEFAULT_MAX_TRANSFERS = 4;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class EventEmitter<TEvents extends object = Record<string, (...args: unknown[]) => void>> {
|
|
2
|
+
#private;
|
|
3
|
+
on<K extends keyof TEvents & string>(name: K, fn: TEvents[K]): () => void;
|
|
4
|
+
has<K extends keyof TEvents & string>(name: K): boolean;
|
|
5
|
+
emit<K extends keyof TEvents & string>(name: K, ...args: Parameters<TEvents[K] extends (...args: infer A) => unknown ? TEvents[K] : never>): void;
|
|
6
|
+
destroy(): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventEmitter } from "./event-emitter";
|
|
2
|
+
import { type SendData, type SendOptions } from "./stream-source";
|
|
3
|
+
import { TransferLimiter } from "./transfer-limiter";
|
|
4
|
+
export type ChannelProgress = {
|
|
5
|
+
direction: "send" | "receive";
|
|
6
|
+
bytes: number;
|
|
7
|
+
total?: number;
|
|
8
|
+
};
|
|
9
|
+
interface FastDataChannelEvents {
|
|
10
|
+
open: () => void;
|
|
11
|
+
close: () => void;
|
|
12
|
+
message: (data: unknown) => void;
|
|
13
|
+
error: (error: unknown) => void;
|
|
14
|
+
stream: (stream: ReadableStream<Uint8Array>, meta?: unknown) => void;
|
|
15
|
+
progress: (info: ChannelProgress) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare class FastDataChannel extends EventEmitter<FastDataChannelEvents> {
|
|
18
|
+
#private;
|
|
19
|
+
readonly channel: RTCDataChannel;
|
|
20
|
+
constructor(channel: RTCDataChannel, transport: RTCSctpTransport | RTCPeerConnection | null, limiter?: TransferLimiter);
|
|
21
|
+
send(data: SendData, options?: SendOptions): Promise<void>;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var R=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var K=(i,e)=>{for(var t in e)R(i,t,{get:e[t],enumerable:!0})},q=(i,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of j(e))!I.call(i,r)&&r!==t&&R(i,r,{get:()=>e[r],enumerable:!(n=N(e,r))||n.enumerable});return i};var V=i=>q(R({},"__esModule",{value:!0}),i);var H={};K(H,{FastDataChannel:()=>w,RTCManager:()=>k});module.exports=V(H);var F={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},L=4;var p=class{#t=new Map;on(e,t){let n=t,r=this.#t.get(e)??[];return r.push(n),this.#t.set(e,r),()=>{let s=this.#t.get(e);if(!s)return;let a=s.indexOf(n);a>=0&&s.splice(a,1),s.length===0&&this.#t.delete(e)}}has(e){return(this.#t.get(e)?.length??0)>0}emit(e,...t){let n=this.#t.get(e);if(n)for(let r of n.slice())r(...t)}destroy(){this.#t.clear()}};function B(i,e){let t=new ArrayBuffer(2+e.byteLength),n=new DataView(t);return n.setUint8(0,252),n.setUint8(1,i),new Uint8Array(t,2).set(e),t}function U(i){if(i.byteLength<2)return null;let e=new DataView(i);return e.getUint8(0)!==252?null:{flags:e.getUint8(1),payload:new Uint8Array(i,2)}}function _(i){return i instanceof Blob||i instanceof ArrayBuffer||ArrayBuffer.isView(i)}function O(i){if(i instanceof Blob)return i.stream();if(i instanceof ArrayBuffer||ArrayBuffer.isView(i)){let t=T(i);return new ReadableStream({start(n){n.enqueue(t),n.close()}})}if(i instanceof ReadableStream)return i;let e=i[Symbol.asyncIterator]();return new ReadableStream({async pull(t){let{done:n,value:r}=await e.next();if(n){t.close();return}t.enqueue(T(r))},cancel:()=>{e.return?.()}})}function T(i){if(i instanceof Uint8Array)return i;if(i instanceof ArrayBuffer)return new Uint8Array(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(typeof i=="string")return new TextEncoder().encode(i);throw i instanceof Blob?new Error("Pass the Blob/File to send, not blob chunks"):new Error("Unsupported stream chunk")}var S=class{#t;#e=0;#s=[];#i=!1;constructor(e){this.#t=e}async acquire(){if(this.#i)throw new Error("Transfer limiter destroyed");if(this.#e<this.#t)this.#e++;else{let t=Promise.withResolvers();if(this.#s.push(t),await t.promise,this.#i)throw this.#e--,new Error("Transfer limiter destroyed")}let e=!1;return()=>{e||(e=!0,this.#n())}}destroy(){this.#i=!0;let e=new Error("Transfer limiter destroyed");for(let t of this.#s.splice(0))t.reject(e)}#n(){if(this.#i){this.#e--;return}let e=this.#s.shift();if(e){e.resolve();return}this.#e--}};function Z(i){let e=i?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var w=class extends p{channel;#t=0;#e=null;#s=null;#i;#n=[];#r=null;#a=null;#o=!1;#l=!1;#u=Promise.resolve();#d=0;constructor(e,t,n=new S(1/0)){super(),this.channel=e,this.#i=n,t instanceof RTCPeerConnection?this.#e=t:this.#s=t,this.#f(),this.channel.binaryType="arraybuffer",this.channel.onopen=()=>this.#m(),this.channel.onclose=()=>this.destroy(),this.channel.onmessage=r=>this.#C(r.data),this.channel.onerror=r=>this.emit("error",r),this.channel.onbufferedamountlow=()=>{this.#c(),this.#p()},this.channel.readyState==="open"&&queueMicrotask(()=>this.#m())}async send(e,t={}){if(this.#l)throw new Error("Data channel closed");if(typeof e=="string"){this.#n.push(e),this.#c();return}await this.#v(async()=>{let n=await this.#i.acquire();try{await this.#b(e,t)}finally{n()}})}destroy(){if(this.#l)return;this.#l=!0,this.#n.length=0,this.#o=!1;let e=new Error("Data channel closed");this.#r?.reject(e),this.#r=null,this.#a?.error(e),this.#a=null,this.emit("close"),this.channel.onopen=null,this.channel.onclose=null,this.channel.onmessage=null,this.channel.onerror=null,this.channel.onbufferedamountlow=null,super.destroy();try{this.channel.close()}catch{}}#m(){this.#l||(this.#f(),this.#c(),this.emit("open"))}#f(){let e=Z(this.#s??this.#e?.sctp??null);this.#t=Math.max(1,e-2),this.channel.bufferedAmountLowThreshold=e*2}async#v(e){let t=this.#u,{promise:n,resolve:r}=Promise.withResolvers();this.#u=n,await t;try{await e()}finally{r()}}#S(e,t){if(e instanceof Blob)return e.size-t;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength-t}#w(e,t,n){n?.(e,t),this.emit("progress",{direction:"send",bytes:e,total:t})}async#b(e,t){this.#f();let n=t.offset??0;if(n!==0&&!(e instanceof Blob))throw new Error("offset requires a Blob/File");let r=e;n!==0&&e instanceof Blob&&(r=e.slice(n));let s=this.#S(e,n),a=0,h=!1;if(t.meta!==void 0){let l=new TextEncoder().encode(JSON.stringify(t.meta));this.#h(5,l),h=!0}let m=O(r),u=m.getReader(),c=!h,d=null,b=()=>{if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(this.#l)throw new Error("Data channel closed")},y=l=>{if(!d)return;let g=d.payload;this.#h(d.flags|(l?2:0),g),d=null,a+=g.byteLength,this.#w(a,s,t.onProgress)},o=()=>{u.cancel(t.signal?.reason??new Error("Aborted"))};t.signal?.addEventListener("abort",o,{once:!0});try{for(;;){b(),await this.#g(t.signal);let{done:l,value:g}=await u.read();if(l){d?y(!0):c?this.#h(3,new Uint8Array(0)):h&&this.#h(2,new Uint8Array(0));break}let D=T(g),C=0;for(;C<D.byteLength;){b(),await this.#g(t.signal);let P=Math.min(C+this.#t,D.byteLength);y(!1);let G=c?1:0;c=!1,d={flags:G,payload:D.subarray(C,P)},C=P}}}catch(l){try{d?y(!0):c||this.#h(2,new Uint8Array(0))}catch{}try{await u.cancel(l)}catch{}throw l}finally{t.signal?.removeEventListener("abort",o),m.locked&&u.releaseLock()}}#C(e){let t=e instanceof ArrayBuffer?U(e):null;if(!t){this.emit("message",e);return}if(this.#o){t.flags&2&&(this.#o=!1);return}if(!this.#a){if(!(t.flags&1))return;if(!this.has("stream")){this.#o=!(t.flags&2);return}let r;if(t.flags&4)try{r=JSON.parse(new TextDecoder().decode(t.payload))}catch(h){this.emit("error",h),this.#o=!(t.flags&2);return}let s,a=new ReadableStream({start:h=>{s=h,this.#a=h},cancel:()=>{this.#a=null}});if(this.#d=0,this.emit("stream",a,r),t.flags&4){t.flags&2&&(s?.close(),this.#a=null);return}}let n=this.#a;if(n){if(n.enqueue(t.payload),this.#d+=t.payload.byteLength,this.emit("progress",{direction:"receive",bytes:this.#d}),(n.desiredSize??0)<-32){n.error(new Error("Receive stream backlog exceeded")),this.#a=null,this.#o=!(t.flags&2);return}t.flags&2&&(n.close(),this.#a=null)}}#h(e,t){this.#l||(this.#n.push(B(e,t)),this.#c())}#y(){return!this.#l&&this.channel.readyState==="open"&&this.#n.length===0&&this.channel.bufferedAmount<=this.channel.bufferedAmountLowThreshold}#T(){let e=this.#n.findIndex(t=>typeof t=="string");return e>=0?this.#n.splice(e,1)[0]:this.#n.shift()}#c(){if(!(this.#l||this.channel.readyState!=="open")){for(;this.#n.length>0&&!(this.channel.bufferedAmount>this.channel.bufferedAmountLowThreshold);){let e=this.#T();if(e===void 0)break;try{typeof e=="string"?this.channel.send(e):this.channel.send(e)}catch(t){this.emit("error",t),this.#n.unshift(e);break}}this.#p()}}#g(e){if(this.#l)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#y())return Promise.resolve();this.#r||(this.#r=Promise.withResolvers());let{promise:t}=this.#r,n=()=>{this.#r?.reject(e?.reason??new Error("Aborted")),this.#r=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#p(){this.#y()&&(this.#r?.resolve(),this.#r=null)}};var E=class{remoteId;channels=new Map;#t;#e;#s;#i=[];#n=!1;#r=!1;constructor(e){this.remoteId=e.remoteId,this.#t=e,this.#s=e.localId>e.remoteId,this.#e=new RTCPeerConnection(e.rtcConfig),this.#e.onicecandidate=t=>{t.candidate&&this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.#e.onnegotiationneeded=async()=>{try{this.#n=!0,await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription})}catch{}finally{this.#n=!1}},this.#e.onconnectionstatechange=()=>{["disconnected","failed","closed"].includes(this.#e.connectionState)&&this.#t.onDisconnect()},this.#e.ontrack=t=>{let n=t.streams[0];t.track.onended=()=>this.#t.onTrackEnded(t.track,n),this.#t.onTrack(t.track,n)},this.#e.ondatachannel=t=>this.#a(t.channel)}addTrack(e,t){this.#e.addTrack(e,t)}removeTrack(e){let t=this.#e.getSenders().find(n=>n.track===e);if(t)try{this.#e.removeTrack(t)}catch{}}async receiveSignal(e){try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#n||this.#e.signalingState!=="stable");if(this.#r=!this.#s&&t,this.#r)return;await this.#e.setRemoteDescription(e.description);for(let n of this.#i)await this.#e.addIceCandidate(n);this.#i=[],e.description.type==="offer"&&(await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription}));return}this.#e.remoteDescription?await this.#e.addIceCandidate(e.candidate):this.#i.push(e.candidate)}catch{}}createDataChannel(e,t){this.#s||this.#a(this.#e.createDataChannel(e,t))}#a(e){this.channels.get(e.label)?.destroy();let n=new w(e,this.#e,this.#t.limiter);this.channels.set(e.label,n),this.#t.onChannel(n)}destroy(){this.#e.onicecandidate=null,this.#e.onnegotiationneeded=null,this.#e.onconnectionstatechange=null,this.#e.ontrack=null,this.#e.ondatachannel=null;for(let e of this.#e.getReceivers())e.track.onended=null;for(let e of this.channels.values())e.destroy();this.channels.clear();try{this.#e.close()}catch{}}};var k=class extends p{id;rtcConfig;#t=new Map;#e;#s=new Set;#i=new Map;#n;#r=new Set;constructor({id:e,signal:t,rtcConfig:n=F,maxTransfers:r=L}){super(),this.id=e,this.#e=t,this.rtcConfig=n,this.#n=new S(r)}add(e){this.#s.add(e);for(let t of e.getTracks())for(let n of this.#t.values())n.addTrack(t,e)}remove(e){this.#s.delete(e);for(let t of e.getTracks())for(let n of this.#t.values())n.removeTrack(t)}channel(e,t={}){this.#i.set(e,t);for(let n of this.#t.values())n.createDataChannel(e,t)}getChannel(e,t){return this.#t.get(e)?.channels.get(t)}whenOpen(e,t,n={}){let r=this.getChannel(e,t);if(r?.channel.readyState==="open")return Promise.resolve(r);let{promise:s,resolve:a,reject:h}=Promise.withResolvers(),m=[],u=!1,c=(o,l)=>{if(!u){u=!0;for(let g of m)g();n.signal?.removeEventListener("abort",d),l?a(l):h(o??new Error("Channel open failed"))}},d=()=>{c(n.signal?.reason??new Error("Aborted"))};if(n.signal?.aborted)return Promise.reject(n.signal.reason??new Error("Aborted"));n.signal?.addEventListener("abort",d,{once:!0});let b=o=>{if(o.channel.readyState==="open"){c(void 0,o);return}m.push(o.on("open",()=>c(void 0,o)),o.on("close",()=>c(new Error("Data channel closed"))))},y=this.getChannel(e,t);return y&&b(y),m.push(this.on("channel",(o,l)=>{o!==e||l.channel.label!==t||b(l)}),this.on("leave",o=>{o===e&&c(new Error(`Peer left: ${e}`))})),s}connect(e){if(this.#t.has(e))return;let t=new E({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,limiter:this.#n,onSignal:n=>this.#e(e,n),onTrack:(n,r)=>this.emit("track",e,n,r),onTrackEnded:(n,r)=>this.emit("track-ended",e,n,r),onChannel:n=>{this.emit("channel",e,n);let r=()=>this.#o(e);n.on("open",r),n.channel.readyState==="open"&&r()},onDisconnect:()=>{this.#t.delete(e)&&(this.#r.delete(e),t.destroy(),this.emit("leave",e))}});this.#i.forEach((n,r)=>t.createDataChannel(r,n));for(let n of this.#s)for(let r of n.getTracks())t.addTrack(r,n);this.#t.set(e,t),this.emit("join",e),this.#o(e)}receive(e,t){this.connect(e),this.#t.get(e).receiveSignal(t)}async sendTo(e,t,n,r={}){let s=this.#t.get(e);if(!s)throw new Error(`Unknown peer: ${e}`);let a=s.channels.get(t);if(!a)throw new Error(`No channel "${t}" for peer ${e}`);if(a.channel.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await a.send(n,r)}async send(e,t,n={}){let r=[];for(let s of this.#t.values()){let a=s.channels.get(e);a?.channel.readyState==="open"&&r.push(a)}if(r.length!==0){if(r.length>1&&typeof t!="string"&&!_(t))throw new Error("Pass a Blob/File/BufferSource to send to multiple peers");await Promise.all(r.map(s=>s.send(t,n)))}}close(e){let t=this.#t.get(e);t&&(this.#t.delete(e),this.#r.delete(e),t.destroy(),this.emit("leave",e))}destroy(){this.#n.destroy();for(let e of[...this.#t.keys()])this.close(e);this.#s.clear(),this.#i.clear(),this.#r.clear(),super.destroy()}#a(e){let t=this.#t.get(e);if(!t)return!1;for(let n of this.#i.keys()){let r=t.channels.get(n);if(!r||r.channel.readyState!=="open")return!1}return!0}#o(e){this.#r.has(e)||this.#a(e)&&(this.#r.add(e),this.emit("ready",e))}};0&&(module.exports={FastDataChannel,RTCManager});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var P={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},F=4;var p=class{#t=new Map;on(e,t){let n=t,r=this.#t.get(e)??[];return r.push(n),this.#t.set(e,r),()=>{let s=this.#t.get(e);if(!s)return;let a=s.indexOf(n);a>=0&&s.splice(a,1),s.length===0&&this.#t.delete(e)}}has(e){return(this.#t.get(e)?.length??0)>0}emit(e,...t){let n=this.#t.get(e);if(n)for(let r of n.slice())r(...t)}destroy(){this.#t.clear()}};function M(i,e){let t=new ArrayBuffer(2+e.byteLength),n=new DataView(t);return n.setUint8(0,252),n.setUint8(1,i),new Uint8Array(t,2).set(e),t}function B(i){if(i.byteLength<2)return null;let e=new DataView(i);return e.getUint8(0)!==252?null:{flags:e.getUint8(1),payload:new Uint8Array(i,2)}}function U(i){return i instanceof Blob||i instanceof ArrayBuffer||ArrayBuffer.isView(i)}function _(i){if(i instanceof Blob)return i.stream();if(i instanceof ArrayBuffer||ArrayBuffer.isView(i)){let t=T(i);return new ReadableStream({start(n){n.enqueue(t),n.close()}})}if(i instanceof ReadableStream)return i;let e=i[Symbol.asyncIterator]();return new ReadableStream({async pull(t){let{done:n,value:r}=await e.next();if(n){t.close();return}t.enqueue(T(r))},cancel:()=>{e.return?.()}})}function T(i){if(i instanceof Uint8Array)return i;if(i instanceof ArrayBuffer)return new Uint8Array(i);if(ArrayBuffer.isView(i))return new Uint8Array(i.buffer,i.byteOffset,i.byteLength);if(typeof i=="string")return new TextEncoder().encode(i);throw i instanceof Blob?new Error("Pass the Blob/File to send, not blob chunks"):new Error("Unsupported stream chunk")}var S=class{#t;#e=0;#s=[];#i=!1;constructor(e){this.#t=e}async acquire(){if(this.#i)throw new Error("Transfer limiter destroyed");if(this.#e<this.#t)this.#e++;else{let t=Promise.withResolvers();if(this.#s.push(t),await t.promise,this.#i)throw this.#e--,new Error("Transfer limiter destroyed")}let e=!1;return()=>{e||(e=!0,this.#n())}}destroy(){this.#i=!0;let e=new Error("Transfer limiter destroyed");for(let t of this.#s.splice(0))t.reject(e)}#n(){if(this.#i){this.#e--;return}let e=this.#s.shift();if(e){e.resolve();return}this.#e--}};function j(i){let e=i?.maxMessageSize;return!e||!Number.isFinite(e)?65536:e}var b=class extends p{channel;#t=0;#e=null;#s=null;#i;#n=[];#r=null;#a=null;#o=!1;#l=!1;#u=Promise.resolve();#d=0;constructor(e,t,n=new S(1/0)){super(),this.channel=e,this.#i=n,t instanceof RTCPeerConnection?this.#e=t:this.#s=t,this.#f(),this.channel.binaryType="arraybuffer",this.channel.onopen=()=>this.#m(),this.channel.onclose=()=>this.destroy(),this.channel.onmessage=r=>this.#C(r.data),this.channel.onerror=r=>this.emit("error",r),this.channel.onbufferedamountlow=()=>{this.#c(),this.#p()},this.channel.readyState==="open"&&queueMicrotask(()=>this.#m())}async send(e,t={}){if(this.#l)throw new Error("Data channel closed");if(typeof e=="string"){this.#n.push(e),this.#c();return}await this.#v(async()=>{let n=await this.#i.acquire();try{await this.#b(e,t)}finally{n()}})}destroy(){if(this.#l)return;this.#l=!0,this.#n.length=0,this.#o=!1;let e=new Error("Data channel closed");this.#r?.reject(e),this.#r=null,this.#a?.error(e),this.#a=null,this.emit("close"),this.channel.onopen=null,this.channel.onclose=null,this.channel.onmessage=null,this.channel.onerror=null,this.channel.onbufferedamountlow=null,super.destroy();try{this.channel.close()}catch{}}#m(){this.#l||(this.#f(),this.#c(),this.emit("open"))}#f(){let e=j(this.#s??this.#e?.sctp??null);this.#t=Math.max(1,e-2),this.channel.bufferedAmountLowThreshold=e*2}async#v(e){let t=this.#u,{promise:n,resolve:r}=Promise.withResolvers();this.#u=n,await t;try{await e()}finally{r()}}#S(e,t){if(e instanceof Blob)return e.size-t;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e.byteLength-t}#w(e,t,n){n?.(e,t),this.emit("progress",{direction:"send",bytes:e,total:t})}async#b(e,t){this.#f();let n=t.offset??0;if(n!==0&&!(e instanceof Blob))throw new Error("offset requires a Blob/File");let r=e;n!==0&&e instanceof Blob&&(r=e.slice(n));let s=this.#S(e,n),a=0,h=!1;if(t.meta!==void 0){let l=new TextEncoder().encode(JSON.stringify(t.meta));this.#h(5,l),h=!0}let m=_(r),u=m.getReader(),c=!h,d=null,w=()=>{if(t.signal?.aborted)throw t.signal.reason??new Error("Aborted");if(this.#l)throw new Error("Data channel closed")},y=l=>{if(!d)return;let g=d.payload;this.#h(d.flags|(l?2:0),g),d=null,a+=g.byteLength,this.#w(a,s,t.onProgress)},o=()=>{u.cancel(t.signal?.reason??new Error("Aborted"))};t.signal?.addEventListener("abort",o,{once:!0});try{for(;;){w(),await this.#g(t.signal);let{done:l,value:g}=await u.read();if(l){d?y(!0):c?this.#h(3,new Uint8Array(0)):h&&this.#h(2,new Uint8Array(0));break}let k=T(g),C=0;for(;C<k.byteLength;){w(),await this.#g(t.signal);let x=Math.min(C+this.#t,k.byteLength);y(!1);let O=c?1:0;c=!1,d={flags:O,payload:k.subarray(C,x)},C=x}}}catch(l){try{d?y(!0):c||this.#h(2,new Uint8Array(0))}catch{}try{await u.cancel(l)}catch{}throw l}finally{t.signal?.removeEventListener("abort",o),m.locked&&u.releaseLock()}}#C(e){let t=e instanceof ArrayBuffer?B(e):null;if(!t){this.emit("message",e);return}if(this.#o){t.flags&2&&(this.#o=!1);return}if(!this.#a){if(!(t.flags&1))return;if(!this.has("stream")){this.#o=!(t.flags&2);return}let r;if(t.flags&4)try{r=JSON.parse(new TextDecoder().decode(t.payload))}catch(h){this.emit("error",h),this.#o=!(t.flags&2);return}let s,a=new ReadableStream({start:h=>{s=h,this.#a=h},cancel:()=>{this.#a=null}});if(this.#d=0,this.emit("stream",a,r),t.flags&4){t.flags&2&&(s?.close(),this.#a=null);return}}let n=this.#a;if(n){if(n.enqueue(t.payload),this.#d+=t.payload.byteLength,this.emit("progress",{direction:"receive",bytes:this.#d}),(n.desiredSize??0)<-32){n.error(new Error("Receive stream backlog exceeded")),this.#a=null,this.#o=!(t.flags&2);return}t.flags&2&&(n.close(),this.#a=null)}}#h(e,t){this.#l||(this.#n.push(M(e,t)),this.#c())}#y(){return!this.#l&&this.channel.readyState==="open"&&this.#n.length===0&&this.channel.bufferedAmount<=this.channel.bufferedAmountLowThreshold}#T(){let e=this.#n.findIndex(t=>typeof t=="string");return e>=0?this.#n.splice(e,1)[0]:this.#n.shift()}#c(){if(!(this.#l||this.channel.readyState!=="open")){for(;this.#n.length>0&&!(this.channel.bufferedAmount>this.channel.bufferedAmountLowThreshold);){let e=this.#T();if(e===void 0)break;try{typeof e=="string"?this.channel.send(e):this.channel.send(e)}catch(t){this.emit("error",t),this.#n.unshift(e);break}}this.#p()}}#g(e){if(this.#l)return Promise.reject(new Error("Data channel closed"));if(e?.aborted)return Promise.reject(e.reason??new Error("Aborted"));if(this.#y())return Promise.resolve();this.#r||(this.#r=Promise.withResolvers());let{promise:t}=this.#r,n=()=>{this.#r?.reject(e?.reason??new Error("Aborted")),this.#r=null};return e?.addEventListener("abort",n,{once:!0}),t.finally(()=>{e?.removeEventListener("abort",n)})}#p(){this.#y()&&(this.#r?.resolve(),this.#r=null)}};var E=class{remoteId;channels=new Map;#t;#e;#s;#i=[];#n=!1;#r=!1;constructor(e){this.remoteId=e.remoteId,this.#t=e,this.#s=e.localId>e.remoteId,this.#e=new RTCPeerConnection(e.rtcConfig),this.#e.onicecandidate=t=>{t.candidate&&this.#t.onSignal({type:"candidate",candidate:t.candidate})},this.#e.onnegotiationneeded=async()=>{try{this.#n=!0,await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription})}catch{}finally{this.#n=!1}},this.#e.onconnectionstatechange=()=>{["disconnected","failed","closed"].includes(this.#e.connectionState)&&this.#t.onDisconnect()},this.#e.ontrack=t=>{let n=t.streams[0];t.track.onended=()=>this.#t.onTrackEnded(t.track,n),this.#t.onTrack(t.track,n)},this.#e.ondatachannel=t=>this.#a(t.channel)}addTrack(e,t){this.#e.addTrack(e,t)}removeTrack(e){let t=this.#e.getSenders().find(n=>n.track===e);if(t)try{this.#e.removeTrack(t)}catch{}}async receiveSignal(e){try{if(e.type==="description"){let t=e.description.type==="offer"&&(this.#n||this.#e.signalingState!=="stable");if(this.#r=!this.#s&&t,this.#r)return;await this.#e.setRemoteDescription(e.description);for(let n of this.#i)await this.#e.addIceCandidate(n);this.#i=[],e.description.type==="offer"&&(await this.#e.setLocalDescription(),this.#t.onSignal({type:"description",description:this.#e.localDescription}));return}this.#e.remoteDescription?await this.#e.addIceCandidate(e.candidate):this.#i.push(e.candidate)}catch{}}createDataChannel(e,t){this.#s||this.#a(this.#e.createDataChannel(e,t))}#a(e){this.channels.get(e.label)?.destroy();let n=new b(e,this.#e,this.#t.limiter);this.channels.set(e.label,n),this.#t.onChannel(n)}destroy(){this.#e.onicecandidate=null,this.#e.onnegotiationneeded=null,this.#e.onconnectionstatechange=null,this.#e.ontrack=null,this.#e.ondatachannel=null;for(let e of this.#e.getReceivers())e.track.onended=null;for(let e of this.channels.values())e.destroy();this.channels.clear();try{this.#e.close()}catch{}}};var R=class extends p{id;rtcConfig;#t=new Map;#e;#s=new Set;#i=new Map;#n;#r=new Set;constructor({id:e,signal:t,rtcConfig:n=P,maxTransfers:r=F}){super(),this.id=e,this.#e=t,this.rtcConfig=n,this.#n=new S(r)}add(e){this.#s.add(e);for(let t of e.getTracks())for(let n of this.#t.values())n.addTrack(t,e)}remove(e){this.#s.delete(e);for(let t of e.getTracks())for(let n of this.#t.values())n.removeTrack(t)}channel(e,t={}){this.#i.set(e,t);for(let n of this.#t.values())n.createDataChannel(e,t)}getChannel(e,t){return this.#t.get(e)?.channels.get(t)}whenOpen(e,t,n={}){let r=this.getChannel(e,t);if(r?.channel.readyState==="open")return Promise.resolve(r);let{promise:s,resolve:a,reject:h}=Promise.withResolvers(),m=[],u=!1,c=(o,l)=>{if(!u){u=!0;for(let g of m)g();n.signal?.removeEventListener("abort",d),l?a(l):h(o??new Error("Channel open failed"))}},d=()=>{c(n.signal?.reason??new Error("Aborted"))};if(n.signal?.aborted)return Promise.reject(n.signal.reason??new Error("Aborted"));n.signal?.addEventListener("abort",d,{once:!0});let w=o=>{if(o.channel.readyState==="open"){c(void 0,o);return}m.push(o.on("open",()=>c(void 0,o)),o.on("close",()=>c(new Error("Data channel closed"))))},y=this.getChannel(e,t);return y&&w(y),m.push(this.on("channel",(o,l)=>{o!==e||l.channel.label!==t||w(l)}),this.on("leave",o=>{o===e&&c(new Error(`Peer left: ${e}`))})),s}connect(e){if(this.#t.has(e))return;let t=new E({localId:this.id,remoteId:e,rtcConfig:this.rtcConfig,limiter:this.#n,onSignal:n=>this.#e(e,n),onTrack:(n,r)=>this.emit("track",e,n,r),onTrackEnded:(n,r)=>this.emit("track-ended",e,n,r),onChannel:n=>{this.emit("channel",e,n);let r=()=>this.#o(e);n.on("open",r),n.channel.readyState==="open"&&r()},onDisconnect:()=>{this.#t.delete(e)&&(this.#r.delete(e),t.destroy(),this.emit("leave",e))}});this.#i.forEach((n,r)=>t.createDataChannel(r,n));for(let n of this.#s)for(let r of n.getTracks())t.addTrack(r,n);this.#t.set(e,t),this.emit("join",e),this.#o(e)}receive(e,t){this.connect(e),this.#t.get(e).receiveSignal(t)}async sendTo(e,t,n,r={}){let s=this.#t.get(e);if(!s)throw new Error(`Unknown peer: ${e}`);let a=s.channels.get(t);if(!a)throw new Error(`No channel "${t}" for peer ${e}`);if(a.channel.readyState!=="open")throw new Error(`Channel "${t}" with ${e} is not open`);await a.send(n,r)}async send(e,t,n={}){let r=[];for(let s of this.#t.values()){let a=s.channels.get(e);a?.channel.readyState==="open"&&r.push(a)}if(r.length!==0){if(r.length>1&&typeof t!="string"&&!U(t))throw new Error("Pass a Blob/File/BufferSource to send to multiple peers");await Promise.all(r.map(s=>s.send(t,n)))}}close(e){let t=this.#t.get(e);t&&(this.#t.delete(e),this.#r.delete(e),t.destroy(),this.emit("leave",e))}destroy(){this.#n.destroy();for(let e of[...this.#t.keys()])this.close(e);this.#s.clear(),this.#i.clear(),this.#r.clear(),super.destroy()}#a(e){let t=this.#t.get(e);if(!t)return!1;for(let n of this.#i.keys()){let r=t.channels.get(n);if(!r||r.channel.readyState!=="open")return!1}return!0}#o(e){this.#r.has(e)||this.#a(e)&&(this.#r.add(e),this.emit("ready",e))}};export{b as FastDataChannel,R as RTCManager};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FastDataChannel } from "./fast-data-channel";
|
|
2
|
+
import type { SignalPayload } from "./rtc-manager";
|
|
3
|
+
import type { TransferLimiter } from "./transfer-limiter";
|
|
4
|
+
type PeerNodeOptions = {
|
|
5
|
+
localId: string;
|
|
6
|
+
remoteId: string;
|
|
7
|
+
rtcConfig: RTCConfiguration;
|
|
8
|
+
limiter: TransferLimiter;
|
|
9
|
+
onSignal: (payload: SignalPayload) => void;
|
|
10
|
+
onTrack: (track: MediaStreamTrack, stream: MediaStream) => void;
|
|
11
|
+
onTrackEnded: (track: MediaStreamTrack, stream: MediaStream) => void;
|
|
12
|
+
onChannel: (channel: FastDataChannel) => void;
|
|
13
|
+
onDisconnect: () => void;
|
|
14
|
+
};
|
|
15
|
+
export declare class PeerNode {
|
|
16
|
+
#private;
|
|
17
|
+
readonly remoteId: string;
|
|
18
|
+
readonly channels: Map<string, FastDataChannel>;
|
|
19
|
+
constructor(opts: PeerNodeOptions);
|
|
20
|
+
addTrack(track: MediaStreamTrack, stream: MediaStream): void;
|
|
21
|
+
removeTrack(track: MediaStreamTrack): void;
|
|
22
|
+
receiveSignal(signal: SignalPayload): Promise<void>;
|
|
23
|
+
createDataChannel(label: string, options?: RTCDataChannelInit): void;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventEmitter } from "./event-emitter";
|
|
2
|
+
import { FastDataChannel } from "./fast-data-channel";
|
|
3
|
+
import { type SendData, type SendOptions } from "./stream-source";
|
|
4
|
+
export type SignalPayload = {
|
|
5
|
+
type: "description";
|
|
6
|
+
description: RTCSessionDescriptionInit;
|
|
7
|
+
} | {
|
|
8
|
+
type: "candidate";
|
|
9
|
+
candidate: RTCIceCandidateInit;
|
|
10
|
+
};
|
|
11
|
+
interface RTCManagerOptions {
|
|
12
|
+
id: string;
|
|
13
|
+
signal: (to: string, payload: SignalPayload) => void;
|
|
14
|
+
rtcConfig?: RTCConfiguration;
|
|
15
|
+
maxTransfers?: number;
|
|
16
|
+
}
|
|
17
|
+
interface RTCManagerEvents {
|
|
18
|
+
join: (id: string) => void;
|
|
19
|
+
leave: (id: string) => void;
|
|
20
|
+
ready: (id: string) => void;
|
|
21
|
+
channel: (id: string, channel: FastDataChannel) => void;
|
|
22
|
+
track: (id: string, track: MediaStreamTrack, stream: MediaStream) => void;
|
|
23
|
+
"track-ended": (id: string, track: MediaStreamTrack, stream: MediaStream) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class RTCManager extends EventEmitter<RTCManagerEvents> {
|
|
26
|
+
#private;
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly rtcConfig: RTCConfiguration;
|
|
29
|
+
constructor({ id, signal, rtcConfig, maxTransfers, }: RTCManagerOptions);
|
|
30
|
+
add(stream: MediaStream): void;
|
|
31
|
+
remove(stream: MediaStream): void;
|
|
32
|
+
channel(label: string, options?: RTCDataChannelInit): void;
|
|
33
|
+
getChannel(peerId: string, label: string): FastDataChannel | undefined;
|
|
34
|
+
whenOpen(peerId: string, label: string, options?: {
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}): Promise<FastDataChannel>;
|
|
37
|
+
connect(remoteId: string): void;
|
|
38
|
+
receive(remoteId: string, payload: SignalPayload): void;
|
|
39
|
+
sendTo(peerId: string, label: string, data: SendData, options?: SendOptions): Promise<void>;
|
|
40
|
+
send(label: string, data: SendData, options?: SendOptions): Promise<void>;
|
|
41
|
+
close(remoteId: string): void;
|
|
42
|
+
destroy(): void;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SendableData = string | Blob | ArrayBuffer | ArrayBufferView;
|
|
2
|
+
export type SendData = string | Blob | BufferSource | ReadableStream<SendableData> | AsyncIterable<SendableData>;
|
|
3
|
+
export type SendOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
meta?: unknown;
|
|
6
|
+
/** Blob/File only. Slice from this byte before streaming. */
|
|
7
|
+
offset?: number;
|
|
8
|
+
onProgress?: (bytes: number, total?: number) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function canCloneSource(data: SendData): boolean;
|
|
11
|
+
export declare function toReadableStream(source: Exclude<SendData, string>): ReadableStream<SendableData>;
|
|
12
|
+
export declare function normalizeChunk(value: SendableData): Uint8Array;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastrtc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Easy to use, zero-dependency WebRTC mesh library for browsers",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"datachannel",
|
|
7
|
+
"fast",
|
|
8
|
+
"mesh",
|
|
9
|
+
"p2p",
|
|
10
|
+
"rtc",
|
|
11
|
+
"webrtc"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "n0tz3r0",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "dist/index.cjs",
|
|
20
|
+
"module": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^7.0.2",
|
|
30
|
+
"tsup": "^8.5.1"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"clean": "rm -rf dist",
|
|
34
|
+
"build": "pnpm clean && tsup && tsc",
|
|
35
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
36
|
+
}
|
|
37
|
+
}
|