Skip to content

Commit 9577030

Browse files
committed
simplifiy by preallocate in constructor
1 parent 75d45ac commit 9577030

File tree

1 file changed

+117
-181
lines changed

1 file changed

+117
-181
lines changed

client/src/lib/index.ts

Lines changed: 117 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
TrackUnpublishedPayload,
1212
} from "./sfu";
1313

14+
const MAX_DOWNSTREAMS = 9;
15+
1416
export type ClientStatus =
1517
| "new"
1618
| "connecting"
@@ -40,16 +42,15 @@ export interface AppDataChannelConfig {
4042
}
4143

4244
export class PulsebeamClient {
43-
#pc: RTCPeerConnection | null = null;
44-
#sfuRpcCh: RTCDataChannel | null = null;
45-
#appDataCh: RTCDataChannel | null = null;
46-
4745
readonly #sfuUrl: string;
48-
readonly #maxDownstreams: number;
49-
readonly #appDataConfig?: AppDataChannelConfig;
5046

51-
#videoSender: RTCRtpSender | null = null;
52-
#audioSender: RTCRtpSender | null = null;
47+
#pc: RTCPeerConnection;
48+
#sfuRpcCh: RTCDataChannel;
49+
#appDataCh: RTCDataChannel;
50+
51+
#videoSender: RTCRtpSender;
52+
#audioSender: RTCRtpSender;
53+
5354
#videoRecvMids: string[] = [];
5455
#audioRecvMids: string[] = [];
5556
#usedMids = new Set<string>();
@@ -75,139 +76,13 @@ export class PulsebeamClient {
7576
appDataConfig?: AppDataChannelConfig,
7677
) {
7778
this.#sfuUrl = sfuUrl;
78-
this.#maxDownstreams = maxDownstreams;
79-
this.#appDataConfig = appDataConfig;
80-
}
81-
82-
#terminateInstance(
83-
finalStatus: "disconnected" | "failed",
84-
message?: string,
85-
): void {
86-
if (this.#instanceTerminated) {
87-
return;
88-
}
89-
this.#instanceTerminated = true;
90-
console.debug(
91-
`PulsebeamClient: Terminating instance with status: ${finalStatus}, message: ${message || "N/A"
92-
}`,
93-
);
94-
95-
this.localVideo.get()?.stop();
96-
this.localAudio.get()?.stop();
97-
this.localVideo.set(null);
98-
this.localAudio.set(null);
99-
100-
Object.values(this.remoteTracks.get()).forEach((remoteTrackInfo) => {
101-
remoteTrackInfo?.track?.stop();
102-
});
103-
this.remoteTracks.set({});
104-
this.availableTracks.set({});
105-
106-
const cleanupChannel = (channel: RTCDataChannel | null): void => {
107-
if (channel) {
108-
channel.onopen = null;
109-
channel.onmessage = null;
110-
channel.onclose = null;
111-
channel.onerror = null;
112-
if (
113-
channel.readyState === "open" || channel.readyState === "connecting"
114-
) {
115-
try {
116-
channel.close();
117-
} catch (e) {
118-
console.warn("Error closing data channel:", e);
119-
}
120-
}
121-
}
122-
};
123-
124-
cleanupChannel(this.#sfuRpcCh);
125-
this.#sfuRpcCh = null;
126-
cleanupChannel(this.#appDataCh);
127-
this.#appDataCh = null;
128-
129-
if (this.#pc) {
130-
this.#pc.onconnectionstatechange = null;
131-
this.#pc.ontrack = null;
132-
this.#pc.onicecandidate = null;
133-
134-
this.#pc.getSenders().forEach((sender) => {
135-
sender.track?.stop();
136-
});
137-
// No need to explicitly stop receiver tracks here, as they are managed by remoteTracks cleanup
138-
139-
if (this.#pc.signalingState !== "closed") {
140-
try {
141-
this.#pc.close();
142-
} catch (e) {
143-
console.warn("Error closing PeerConnection:", e);
144-
}
145-
}
146-
this.#pc = null;
147-
}
148-
149-
this.#activeSubscriptions.clear();
150-
this.#usedMids.clear();
151-
this.#videoRecvMids = [];
152-
this.#audioRecvMids = [];
153-
this.#videoSender = null;
154-
this.#audioSender = null;
155-
156-
if (message) {
157-
this.errorMsg.set(message);
158-
}
159-
this.status.set(finalStatus);
160-
console.warn(
161-
"PulsebeamClient instance has been terminated and is no longer usable.",
79+
maxDownstreams = Math.max(
80+
Math.min(maxDownstreams, MAX_DOWNSTREAMS),
81+
0,
16282
);
163-
}
164-
165-
#updateConnectedStatus(): void {
166-
if (this.#instanceTerminated || this.status.get() !== "connecting") {
167-
return;
168-
}
169-
170-
const pcConnected = this.#pc?.connectionState === "connected";
171-
const rpcReady = this.#sfuRpcCh?.readyState === "open";
172-
const appDcReady = !this.#appDataConfig ||
173-
this.#appDataCh?.readyState === "open";
174-
175-
if (pcConnected && rpcReady && appDcReady) {
176-
this.status.set("connected");
177-
this.errorMsg.set(null); // Clear any transient errors from connecting phase
178-
}
179-
}
180-
181-
async connect(room: string, participantId: string): Promise<void> {
182-
if (this.#instanceTerminated) {
183-
const errorMessage =
184-
"This client instance has been terminated and cannot be reused.";
185-
this.errorMsg.set(errorMessage);
186-
console.error(errorMessage);
187-
throw new Error(errorMessage); // More direct feedback to developer
188-
}
189-
190-
if (this.status.get() !== "new") {
191-
const errorMessage =
192-
`Client can only connect when in "new" state. Current status: ${this.status.get()}. Create a new instance to reconnect.`;
193-
// Only set error if it's not already a terminal state from a previous attempt on this (now invalid) instance
194-
if (
195-
this.status.get() !== "failed" && this.status.get() !== "disconnected"
196-
) {
197-
this.errorMsg.set(errorMessage);
198-
}
199-
console.warn(errorMessage);
200-
return; // Do not proceed
201-
}
202-
203-
this.status.set("connecting");
204-
this.errorMsg.set(null);
20583

20684
this.#pc = new RTCPeerConnection();
207-
const peerConnection = this.#pc; // Use a more descriptive local variable
208-
peerConnection.onicecandidate = null; // No ICE trickling
209-
210-
peerConnection.onconnectionstatechange = () => {
85+
this.#pc.onconnectionstatechange = () => {
21186
if (this.#instanceTerminated || !this.#pc) return; // Guard
21287
const connectionState = this.#pc.connectionState;
21388
console.debug(`PeerConnection state changed: ${connectionState}`);
@@ -225,7 +100,7 @@ export class PulsebeamClient {
225100
}
226101
};
227102

228-
peerConnection.ontrack = (event: RTCTrackEvent) => {
103+
this.#pc.ontrack = (event: RTCTrackEvent) => {
229104
if (this.#instanceTerminated) return;
230105
const mid = event.transceiver?.mid;
231106
const track = event.track;
@@ -269,7 +144,7 @@ export class PulsebeamClient {
269144
};
270145

271146
// SFU RPC DataChannel
272-
this.#sfuRpcCh = peerConnection.createDataChannel("pulsebeam::rpc");
147+
this.#sfuRpcCh = this.#pc.createDataChannel("pulsebeam::rpc");
273148
this.#sfuRpcCh.binaryType = "arraybuffer";
274149
this.#sfuRpcCh.onopen = () => {
275150
if (!this.#instanceTerminated) this.#updateConnectedStatus();
@@ -349,62 +224,123 @@ export class PulsebeamClient {
349224
this.#sfuRpcCh.onclose = createFatalRpcHandler("closed");
350225
this.#sfuRpcCh.onerror = createFatalRpcHandler("error");
351226

352-
// Optional Application DataChannel
353-
if (this.#appDataConfig) {
354-
this.#appDataCh = peerConnection.createDataChannel(
355-
"app-data",
356-
this.#appDataConfig.options,
357-
);
358-
this.#appDataCh.onmessage = (event: MessageEvent) => {
359-
if (this.#instanceTerminated || !this.#appDataConfig) return;
360-
if (typeof event.data === "string") {
361-
this.#appDataConfig.onMessage(event.data);
362-
} else {
363-
console.warn(
364-
"Received non-string message on app data channel, ignoring.",
365-
);
366-
}
367-
};
368-
const appDcOpenHandler = (event: Event) => {
369-
if (!this.#instanceTerminated) {
370-
this.#appDataConfig?.onOpen?.(event);
371-
this.#updateConnectedStatus();
372-
}
373-
};
374-
this.#appDataCh.onopen = appDcOpenHandler;
375-
376-
const createFatalAppDcHandler = (type: string) => (event?: Event) => { // onerror might not pass event
377-
if (!this.#instanceTerminated) {
378-
if (type === "close" && this.#appDataConfig?.onClose && event) {
379-
this.#appDataConfig.onClose(event);
380-
}
381-
this.#terminateInstance("failed", `Application DataChannel ${type}`);
382-
}
227+
if (!appDataConfig) {
228+
appDataConfig = {
229+
onMessage: () => { },
383230
};
384-
this.#appDataCh.onclose = createFatalAppDcHandler("closed");
385-
this.#appDataCh.onerror = createFatalAppDcHandler("error");
386231
}
387232

233+
this.#appDataCh = this.#pc.createDataChannel(
234+
"app::data",
235+
appDataConfig.options,
236+
);
237+
this.#appDataCh.onopen = appDataConfig.onOpen || null;
238+
this.#appDataCh.onclose = appDataConfig.onClose || null;
239+
388240
// Transceivers
389241
this.#videoSender =
390-
peerConnection.addTransceiver("video", { direction: "sendonly" }).sender;
242+
this.#pc.addTransceiver("video", { direction: "sendonly" }).sender;
391243
this.#audioSender =
392-
peerConnection.addTransceiver("audio", { direction: "sendonly" }).sender;
393-
for (let i = 0; i < this.#maxDownstreams; i++) {
394-
const videoTransceiver = peerConnection.addTransceiver("video", {
244+
this.#pc.addTransceiver("audio", { direction: "sendonly" }).sender;
245+
for (let i = 0; i < maxDownstreams; i++) {
246+
const videoTransceiver = this.#pc.addTransceiver("video", {
395247
direction: "recvonly",
396248
});
397249
if (videoTransceiver.mid) this.#videoRecvMids.push(videoTransceiver.mid);
398-
const audioTransceiver = peerConnection.addTransceiver("audio", {
250+
const audioTransceiver = this.#pc.addTransceiver("audio", {
399251
direction: "recvonly",
400252
});
401253
if (audioTransceiver.mid) this.#audioRecvMids.push(audioTransceiver.mid);
402254
}
255+
}
256+
257+
#terminateInstance(
258+
finalStatus: "disconnected" | "failed",
259+
message?: string,
260+
): void {
261+
if (this.#instanceTerminated) {
262+
return;
263+
}
264+
this.#instanceTerminated = true;
265+
console.debug(
266+
`PulsebeamClient: Terminating instance with status: ${finalStatus}, message: ${message || "N/A"
267+
}`,
268+
);
269+
270+
this.localVideo.get()?.stop();
271+
this.localAudio.get()?.stop();
272+
this.localVideo.set(null);
273+
this.localAudio.set(null);
274+
275+
Object.values(this.remoteTracks.get()).forEach((remoteTrackInfo) => {
276+
remoteTrackInfo?.track?.stop();
277+
});
278+
this.remoteTracks.set({});
279+
this.availableTracks.set({});
280+
281+
this.#pc.getSenders().forEach((sender) => {
282+
sender.track?.stop();
283+
});
284+
// No need to explicitly stop receiver tracks here, as they are managed by remoteTracks cleanup
285+
this.#pc.close();
286+
287+
this.#activeSubscriptions.clear();
288+
this.#usedMids.clear();
289+
this.#videoRecvMids = [];
290+
this.#audioRecvMids = [];
291+
292+
if (message) {
293+
this.errorMsg.set(message);
294+
}
295+
this.status.set(finalStatus);
296+
console.warn(
297+
"PulsebeamClient instance has been terminated and is no longer usable.",
298+
);
299+
}
300+
301+
#updateConnectedStatus(): void {
302+
if (this.#instanceTerminated || this.status.get() !== "connecting") {
303+
return;
304+
}
305+
306+
const pcConnected = this.#pc?.connectionState === "connected";
307+
const rpcReady = this.#sfuRpcCh?.readyState === "open";
308+
309+
if (pcConnected && rpcReady) {
310+
this.status.set("connected");
311+
this.errorMsg.set(null); // Clear any transient errors from connecting phase
312+
}
313+
}
314+
315+
async connect(room: string, participantId: string): Promise<void> {
316+
if (this.#instanceTerminated) {
317+
const errorMessage =
318+
"This client instance has been terminated and cannot be reused.";
319+
this.errorMsg.set(errorMessage);
320+
console.error(errorMessage);
321+
throw new Error(errorMessage); // More direct feedback to developer
322+
}
323+
324+
if (this.status.get() !== "new") {
325+
const errorMessage =
326+
`Client can only connect when in "new" state. Current status: ${this.status.get()}. Create a new instance to reconnect.`;
327+
// Only set error if it's not already a terminal state from a previous attempt on this (now invalid) instance
328+
if (
329+
this.status.get() !== "failed" && this.status.get() !== "disconnected"
330+
) {
331+
this.errorMsg.set(errorMessage);
332+
}
333+
console.warn(errorMessage);
334+
return; // Do not proceed
335+
}
336+
337+
this.status.set("connecting");
338+
this.errorMsg.set(null);
403339

404340
// Signaling
405341
try {
406-
const offer = await peerConnection.createOffer();
407-
await peerConnection.setLocalDescription(offer);
342+
const offer = await this.#pc.createOffer();
343+
await this.#pc.setLocalDescription(offer);
408344
const response = await fetch(
409345
`${this.#sfuUrl}?room=${room}&participant=${participantId}`,
410346
{
@@ -419,7 +355,7 @@ export class PulsebeamClient {
419355
.text()}`,
420356
);
421357
}
422-
await peerConnection.setRemoteDescription({
358+
await this.#pc.setRemoteDescription({
423359
type: "answer",
424360
sdp: await response.text(),
425361
});

0 commit comments

Comments
 (0)