@conference-kit/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -0
- package/src/Peer.ts +277 -0
- package/src/index.ts +2 -0
- package/src/types.ts +52 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@conference-kit/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/Peer.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PeerConfig,
|
|
3
|
+
PeerControls,
|
|
4
|
+
PeerEventHandler,
|
|
5
|
+
PeerEvents,
|
|
6
|
+
PeerSide,
|
|
7
|
+
SignalData,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
// Minimal typed event emitter to avoid external deps
|
|
11
|
+
class Emitter {
|
|
12
|
+
private listeners = new Map<keyof PeerEvents, Set<PeerEventHandler<any>>>();
|
|
13
|
+
|
|
14
|
+
on<K extends keyof PeerEvents>(event: K, handler: PeerEventHandler<K>) {
|
|
15
|
+
const set = this.listeners.get(event) ?? new Set();
|
|
16
|
+
set.add(handler as PeerEventHandler<any>);
|
|
17
|
+
this.listeners.set(event, set);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
off<K extends keyof PeerEvents>(event: K, handler: PeerEventHandler<K>) {
|
|
21
|
+
const set = this.listeners.get(event);
|
|
22
|
+
if (!set) return;
|
|
23
|
+
set.delete(handler as PeerEventHandler<any>);
|
|
24
|
+
if (set.size === 0) this.listeners.delete(event);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
once<K extends keyof PeerEvents>(event: K, handler: PeerEventHandler<K>) {
|
|
28
|
+
const wrapped = (payload: PeerEvents[K]) => {
|
|
29
|
+
this.off(event, wrapped as PeerEventHandler<K>);
|
|
30
|
+
handler(payload);
|
|
31
|
+
};
|
|
32
|
+
this.on(event, wrapped as PeerEventHandler<K>);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit<K extends keyof PeerEvents>(event: K, payload: PeerEvents[K]) {
|
|
36
|
+
const set = this.listeners.get(event);
|
|
37
|
+
if (!set) return;
|
|
38
|
+
for (const handler of Array.from(set)) {
|
|
39
|
+
handler(payload);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class Peer implements PeerControls {
|
|
45
|
+
private pc: RTCPeerConnection;
|
|
46
|
+
private dataChannel?: RTCDataChannel;
|
|
47
|
+
private emitter = new Emitter();
|
|
48
|
+
private destroyed = false;
|
|
49
|
+
private side: PeerSide;
|
|
50
|
+
private trickle: boolean;
|
|
51
|
+
private makingOffer = false;
|
|
52
|
+
private negotiationPending = false;
|
|
53
|
+
|
|
54
|
+
constructor({
|
|
55
|
+
side,
|
|
56
|
+
stream,
|
|
57
|
+
config,
|
|
58
|
+
channelLabel = "data",
|
|
59
|
+
trickle = true,
|
|
60
|
+
enableDataChannel = true,
|
|
61
|
+
}: PeerConfig) {
|
|
62
|
+
this.side = side;
|
|
63
|
+
this.trickle = trickle;
|
|
64
|
+
this.pc = new RTCPeerConnection(config);
|
|
65
|
+
|
|
66
|
+
if (stream) {
|
|
67
|
+
for (const track of stream.getTracks()) {
|
|
68
|
+
this.pc.addTrack(track, stream);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (side === "initiator" && enableDataChannel) {
|
|
73
|
+
this.dataChannel = this.pc.createDataChannel(channelLabel);
|
|
74
|
+
this.wireDataChannel(this.dataChannel);
|
|
75
|
+
} else {
|
|
76
|
+
this.pc.addEventListener("datachannel", (event) => {
|
|
77
|
+
this.dataChannel = event.channel;
|
|
78
|
+
this.wireDataChannel(this.dataChannel);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.pc.addEventListener("track", (event) => {
|
|
83
|
+
const [remoteStream] = event.streams;
|
|
84
|
+
if (remoteStream) this.emitter.emit("stream", remoteStream);
|
|
85
|
+
this.emitter.emit("track", event.track);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.pc.addEventListener("icecandidate", ({ candidate }) => {
|
|
89
|
+
if (candidate && this.trickle) {
|
|
90
|
+
this.emitter.emit("signal", candidate.toJSON());
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.pc.addEventListener("iceconnectionstatechange", () => {
|
|
95
|
+
this.emitter.emit("iceStateChange", this.pc.iceConnectionState);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.pc.addEventListener("connectionstatechange", () => {
|
|
99
|
+
this.emitter.emit("connectionStateChange", this.pc.connectionState);
|
|
100
|
+
if (this.pc.connectionState === "connected")
|
|
101
|
+
this.emitter.emit("connect", undefined);
|
|
102
|
+
if (
|
|
103
|
+
this.pc.connectionState === "failed" ||
|
|
104
|
+
this.pc.connectionState === "disconnected" ||
|
|
105
|
+
this.pc.connectionState === "closed"
|
|
106
|
+
) {
|
|
107
|
+
this.emitter.emit("close", undefined);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.pc.addEventListener("signalingstatechange", () => {
|
|
112
|
+
if (
|
|
113
|
+
this.side === "initiator" &&
|
|
114
|
+
this.pc.signalingState === "stable" &&
|
|
115
|
+
this.negotiationPending
|
|
116
|
+
) {
|
|
117
|
+
this.negotiationPending = false;
|
|
118
|
+
void this.createOffer();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.pc.addEventListener("negotiationneeded", () => {
|
|
123
|
+
if (this.side !== "initiator") return;
|
|
124
|
+
if (this.destroyed) return;
|
|
125
|
+
if (this.makingOffer) {
|
|
126
|
+
this.negotiationPending = true;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this.pc.signalingState !== "stable") {
|
|
130
|
+
this.negotiationPending = true;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
void this.createOffer();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (this.side === "initiator") {
|
|
137
|
+
void this.createOffer();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
on = this.emitter.on.bind(this.emitter);
|
|
142
|
+
off = this.emitter.off.bind(this.emitter);
|
|
143
|
+
once = this.emitter.once.bind(this.emitter);
|
|
144
|
+
|
|
145
|
+
addStream(stream: MediaStream) {
|
|
146
|
+
stream.getTracks().forEach((track) => this.pc.addTrack(track, stream));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
removeStream(stream: MediaStream) {
|
|
150
|
+
for (const sender of this.pc.getSenders()) {
|
|
151
|
+
if (sender.track && stream.getTracks().includes(sender.track)) {
|
|
152
|
+
this.pc.removeTrack(sender);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
addTrack(track: MediaStreamTrack, stream: MediaStream) {
|
|
158
|
+
this.pc.addTrack(track, stream);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
removeTrack(sender: RTCRtpSender) {
|
|
162
|
+
this.pc.removeTrack(sender);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
replaceTrack(
|
|
166
|
+
oldTrack: MediaStreamTrack,
|
|
167
|
+
newTrack: MediaStreamTrack,
|
|
168
|
+
stream: MediaStream
|
|
169
|
+
) {
|
|
170
|
+
const sender = this.pc.getSenders().find((s) => s.track === oldTrack);
|
|
171
|
+
if (sender) {
|
|
172
|
+
sender.replaceTrack(newTrack);
|
|
173
|
+
} else {
|
|
174
|
+
this.pc.addTrack(newTrack, stream);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async signal(data: SignalData) {
|
|
179
|
+
if (this.destroyed) return;
|
|
180
|
+
try {
|
|
181
|
+
if ("type" in data) {
|
|
182
|
+
const description = new RTCSessionDescription(data);
|
|
183
|
+
await this.pc.setRemoteDescription(description);
|
|
184
|
+
|
|
185
|
+
if (description.type === "offer") {
|
|
186
|
+
await this.createAnswer();
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const candidate = new RTCIceCandidate(data);
|
|
190
|
+
await this.pc.addIceCandidate(candidate);
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.emitter.emit("error", error as Error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
send(data: string | ArrayBufferView | ArrayBuffer | Blob) {
|
|
198
|
+
if (!this.dataChannel || this.dataChannel.readyState !== "open") {
|
|
199
|
+
throw new Error("Data channel is not open");
|
|
200
|
+
}
|
|
201
|
+
this.dataChannel.send(data as any);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
destroy() {
|
|
205
|
+
if (this.destroyed) return;
|
|
206
|
+
this.destroyed = true;
|
|
207
|
+
this.dataChannel?.close();
|
|
208
|
+
this.pc.getSenders().forEach((sender) => {
|
|
209
|
+
try {
|
|
210
|
+
sender.track?.stop();
|
|
211
|
+
} catch {}
|
|
212
|
+
});
|
|
213
|
+
this.pc.getReceivers().forEach((receiver) => {
|
|
214
|
+
try {
|
|
215
|
+
receiver.track.stop();
|
|
216
|
+
} catch {}
|
|
217
|
+
});
|
|
218
|
+
this.pc.close();
|
|
219
|
+
this.emitter.emit("close", undefined);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getConnectionState(): RTCPeerConnectionState {
|
|
223
|
+
return this.pc.connectionState;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private wireDataChannel(channel: RTCDataChannel) {
|
|
227
|
+
channel.addEventListener("open", () => {
|
|
228
|
+
// Mark data channel ready regardless of current connectionState to ensure downstream hooks know it's usable.
|
|
229
|
+
this.emitter.emit("connect", undefined);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
channel.addEventListener("message", (event) => {
|
|
233
|
+
this.emitter.emit("data", event.data);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
channel.addEventListener("close", () => {
|
|
237
|
+
this.emitter.emit("close", undefined);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
channel.addEventListener("error", (event) => {
|
|
241
|
+
const err =
|
|
242
|
+
event instanceof Error ? event : new Error("Data channel error");
|
|
243
|
+
this.emitter.emit("error", err);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async createOffer() {
|
|
248
|
+
if (this.destroyed) return;
|
|
249
|
+
try {
|
|
250
|
+
this.makingOffer = true;
|
|
251
|
+
const offer = await this.pc.createOffer();
|
|
252
|
+
await this.pc.setLocalDescription(offer);
|
|
253
|
+
if (this.pc.localDescription) {
|
|
254
|
+
this.emitter.emit("signal", this.pc.localDescription.toJSON());
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.emitter.emit("error", error as Error);
|
|
258
|
+
} finally {
|
|
259
|
+
this.makingOffer = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async createAnswer() {
|
|
264
|
+
if (this.destroyed) return;
|
|
265
|
+
try {
|
|
266
|
+
const answer = await this.pc.createAnswer();
|
|
267
|
+
await this.pc.setLocalDescription(answer);
|
|
268
|
+
if (this.pc.localDescription) {
|
|
269
|
+
this.emitter.emit("signal", this.pc.localDescription.toJSON());
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this.emitter.emit("error", error as Error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default Peer;
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type PeerSide = "initiator" | "responder";
|
|
2
|
+
|
|
3
|
+
export type SignalData = RTCSessionDescriptionInit | RTCIceCandidateInit;
|
|
4
|
+
|
|
5
|
+
export interface PeerConfig {
|
|
6
|
+
side: PeerSide;
|
|
7
|
+
stream?: MediaStream;
|
|
8
|
+
config?: RTCConfiguration;
|
|
9
|
+
channelLabel?: string;
|
|
10
|
+
trickle?: boolean;
|
|
11
|
+
enableDataChannel?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PeerEvents {
|
|
15
|
+
signal: SignalData;
|
|
16
|
+
connect: void;
|
|
17
|
+
close: void;
|
|
18
|
+
error: Error;
|
|
19
|
+
stream: MediaStream;
|
|
20
|
+
data: ArrayBuffer | string;
|
|
21
|
+
track: MediaStreamTrack;
|
|
22
|
+
iceStateChange: RTCIceConnectionState;
|
|
23
|
+
connectionStateChange: RTCPeerConnectionState;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type EventKey = keyof PeerEvents;
|
|
27
|
+
|
|
28
|
+
export type PeerEventHandler<K extends EventKey> = (
|
|
29
|
+
payload: PeerEvents[K]
|
|
30
|
+
) => void;
|
|
31
|
+
|
|
32
|
+
export interface TypedEventEmitter {
|
|
33
|
+
on<K extends EventKey>(event: K, handler: PeerEventHandler<K>): void;
|
|
34
|
+
off<K extends EventKey>(event: K, handler: PeerEventHandler<K>): void;
|
|
35
|
+
once<K extends EventKey>(event: K, handler: PeerEventHandler<K>): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PeerControls extends TypedEventEmitter {
|
|
39
|
+
addStream(stream: MediaStream): void;
|
|
40
|
+
removeStream(stream: MediaStream): void;
|
|
41
|
+
addTrack(track: MediaStreamTrack, stream: MediaStream): void;
|
|
42
|
+
removeTrack(sender: RTCRtpSender): void;
|
|
43
|
+
replaceTrack(
|
|
44
|
+
oldTrack: MediaStreamTrack,
|
|
45
|
+
newTrack: MediaStreamTrack,
|
|
46
|
+
stream: MediaStream
|
|
47
|
+
): void;
|
|
48
|
+
signal(data: SignalData): Promise<void>;
|
|
49
|
+
send(data: string | ArrayBufferView | ArrayBuffer | Blob): void;
|
|
50
|
+
destroy(): void;
|
|
51
|
+
getConnectionState(): RTCPeerConnectionState;
|
|
52
|
+
}
|