sabha-meetings-react 0.1.7 → 0.1.9
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/dist/index.d.mts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +256 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +256 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -146,10 +146,13 @@ type UpdateMeetingInput = {
|
|
|
146
146
|
[key: string]: unknown;
|
|
147
147
|
};
|
|
148
148
|
type MeetingConnectionState = "disconnected" | "connecting" | "connected" | "joined" | "error";
|
|
149
|
+
/** Host publishes A/V; participants join to view the host only. */
|
|
150
|
+
type MeetingRole = "host" | "participant";
|
|
149
151
|
type MeetingParticipant = {
|
|
150
152
|
id: string;
|
|
151
153
|
displayName?: string;
|
|
152
154
|
username?: string;
|
|
155
|
+
role?: MeetingRole;
|
|
153
156
|
audioEnabled?: boolean;
|
|
154
157
|
videoEnabled?: boolean;
|
|
155
158
|
screenSharing?: boolean;
|
|
@@ -188,6 +191,8 @@ type MeetingRoomEventMap = {
|
|
|
188
191
|
reason?: string;
|
|
189
192
|
};
|
|
190
193
|
mediaState: LocalMediaState;
|
|
194
|
+
/** Host media received by a participant (one-way). */
|
|
195
|
+
remoteStream: MediaStream | null;
|
|
191
196
|
captureAttempt: {
|
|
192
197
|
type: "screenshot" | "recording" | "visibility";
|
|
193
198
|
at: string;
|
|
@@ -269,6 +274,10 @@ type MeetingConnectionOptions = {
|
|
|
269
274
|
socketPath?: string;
|
|
270
275
|
events?: Partial<SabhaSocketEvents>;
|
|
271
276
|
mediaPolicy?: Partial<MediaPolicy>;
|
|
277
|
+
/** Host publishes A/V; participant receives host stream only. Default host. */
|
|
278
|
+
role?: MeetingRole;
|
|
279
|
+
/** Stable id for WebRTC signaling (usually Sabha userId). */
|
|
280
|
+
selfId?: string;
|
|
272
281
|
};
|
|
273
282
|
declare class MeetingConnection {
|
|
274
283
|
private readonly options;
|
|
@@ -276,15 +285,21 @@ declare class MeetingConnection {
|
|
|
276
285
|
private state;
|
|
277
286
|
private readonly events;
|
|
278
287
|
private readonly mediaPolicy;
|
|
288
|
+
private readonly role;
|
|
289
|
+
private readonly selfId;
|
|
279
290
|
readonly media: MediaController;
|
|
280
291
|
readonly protection: ContentProtection;
|
|
281
292
|
private readonly listeners;
|
|
282
293
|
private mediaUnsub;
|
|
283
294
|
private captureUnsub;
|
|
295
|
+
private readonly peers;
|
|
296
|
+
private remoteStream;
|
|
284
297
|
constructor(options: MeetingConnectionOptions);
|
|
285
298
|
get meetingId(): string;
|
|
299
|
+
get meetingRole(): MeetingRole;
|
|
286
300
|
get connectionState(): MeetingConnectionState;
|
|
287
301
|
getMediaState(): LocalMediaState;
|
|
302
|
+
getRemoteStream(): MediaStream | null;
|
|
288
303
|
on<K extends keyof MeetingRoomEventMap>(event: K, listener: Listener<MeetingRoomEventMap[K]>): () => void;
|
|
289
304
|
connect(): Promise<void>;
|
|
290
305
|
setAudioEnabled(enabled: boolean): Promise<LocalMediaState>;
|
|
@@ -298,6 +313,13 @@ declare class MeetingConnection {
|
|
|
298
313
|
private teardownMedia;
|
|
299
314
|
private join;
|
|
300
315
|
private bindServerEvents;
|
|
316
|
+
private sendSignal;
|
|
317
|
+
private handleSignal;
|
|
318
|
+
private createHostOffer;
|
|
319
|
+
private getOrCreatePeer;
|
|
320
|
+
private attachLocalTracks;
|
|
321
|
+
private refreshHostTracks;
|
|
322
|
+
private closePeer;
|
|
301
323
|
private setState;
|
|
302
324
|
private emit;
|
|
303
325
|
}
|
|
@@ -344,7 +366,9 @@ declare class SabhaClient {
|
|
|
344
366
|
meeting: Meeting;
|
|
345
367
|
connection: MeetingConnection;
|
|
346
368
|
}>;
|
|
347
|
-
connect(meetingId: string
|
|
369
|
+
connect(meetingId: string, options?: {
|
|
370
|
+
role?: MeetingRole;
|
|
371
|
+
}): Promise<MeetingConnection>;
|
|
348
372
|
getConnection(): MeetingConnection | null;
|
|
349
373
|
leaveMeeting(): Promise<void>;
|
|
350
374
|
private applySession;
|
|
@@ -407,7 +431,9 @@ declare function useSabhaMeetings(): {
|
|
|
407
431
|
meeting: Meeting;
|
|
408
432
|
connection: MeetingConnection;
|
|
409
433
|
}>;
|
|
410
|
-
connect: (meetingId: string
|
|
434
|
+
connect: (meetingId: string, options?: {
|
|
435
|
+
role?: MeetingRole;
|
|
436
|
+
}) => Promise<MeetingConnection>;
|
|
411
437
|
leaveMeeting: () => Promise<void>;
|
|
412
438
|
connection: MeetingConnection | null;
|
|
413
439
|
connectionState: MeetingConnectionState;
|
|
@@ -447,4 +473,4 @@ declare function readStoredSessionCookie(): string | null;
|
|
|
447
473
|
declare function writeStoredSessionCookie(value: string | null): void;
|
|
448
474
|
|
|
449
475
|
export { ContentProtection, DEFAULT_BASE_URL, DEFAULT_MEDIA_POLICY, MediaController, MeetingConnection, SABHA_SESSION_HEADER, SABHA_SESSION_STORAGE_KEY, SABHA_TOKENS_STORAGE_KEY, SabhaClient, SabhaError, SabhaProvider, createSabhaClient, normalizeMeeting, readStoredSessionCookie, readStoredTokens, toSabhaError, useSabha, useSabhaMeetings, writeStoredSessionCookie, writeStoredTokens };
|
|
450
|
-
export type { AuthSession, AuthUser, CaptureAttemptType, CreateMeetingInput, LocalMediaState, LoginInput, MediaPolicy, MediaTrackKind, Meeting, MeetingConnectionOptions, MeetingConnectionState, MeetingInvitee, MeetingOrganizer, MeetingParticipant, MeetingRoomEventMap, RegisterInput, SabhaClientStatus, SabhaConfig, SabhaContextValue, SabhaOrg, SabhaProviderProps, SabhaSocketEvents, SabhaTokens, UpdateMeetingInput, VerifyEmailInput };
|
|
476
|
+
export type { AuthSession, AuthUser, CaptureAttemptType, CreateMeetingInput, LocalMediaState, LoginInput, MediaPolicy, MediaTrackKind, Meeting, MeetingConnectionOptions, MeetingConnectionState, MeetingInvitee, MeetingOrganizer, MeetingParticipant, MeetingRole, MeetingRoomEventMap, RegisterInput, SabhaClientStatus, SabhaConfig, SabhaContextValue, SabhaOrg, SabhaProviderProps, SabhaSocketEvents, SabhaTokens, UpdateMeetingInput, VerifyEmailInput };
|
package/dist/index.d.ts
CHANGED
|
@@ -146,10 +146,13 @@ type UpdateMeetingInput = {
|
|
|
146
146
|
[key: string]: unknown;
|
|
147
147
|
};
|
|
148
148
|
type MeetingConnectionState = "disconnected" | "connecting" | "connected" | "joined" | "error";
|
|
149
|
+
/** Host publishes A/V; participants join to view the host only. */
|
|
150
|
+
type MeetingRole = "host" | "participant";
|
|
149
151
|
type MeetingParticipant = {
|
|
150
152
|
id: string;
|
|
151
153
|
displayName?: string;
|
|
152
154
|
username?: string;
|
|
155
|
+
role?: MeetingRole;
|
|
153
156
|
audioEnabled?: boolean;
|
|
154
157
|
videoEnabled?: boolean;
|
|
155
158
|
screenSharing?: boolean;
|
|
@@ -188,6 +191,8 @@ type MeetingRoomEventMap = {
|
|
|
188
191
|
reason?: string;
|
|
189
192
|
};
|
|
190
193
|
mediaState: LocalMediaState;
|
|
194
|
+
/** Host media received by a participant (one-way). */
|
|
195
|
+
remoteStream: MediaStream | null;
|
|
191
196
|
captureAttempt: {
|
|
192
197
|
type: "screenshot" | "recording" | "visibility";
|
|
193
198
|
at: string;
|
|
@@ -269,6 +274,10 @@ type MeetingConnectionOptions = {
|
|
|
269
274
|
socketPath?: string;
|
|
270
275
|
events?: Partial<SabhaSocketEvents>;
|
|
271
276
|
mediaPolicy?: Partial<MediaPolicy>;
|
|
277
|
+
/** Host publishes A/V; participant receives host stream only. Default host. */
|
|
278
|
+
role?: MeetingRole;
|
|
279
|
+
/** Stable id for WebRTC signaling (usually Sabha userId). */
|
|
280
|
+
selfId?: string;
|
|
272
281
|
};
|
|
273
282
|
declare class MeetingConnection {
|
|
274
283
|
private readonly options;
|
|
@@ -276,15 +285,21 @@ declare class MeetingConnection {
|
|
|
276
285
|
private state;
|
|
277
286
|
private readonly events;
|
|
278
287
|
private readonly mediaPolicy;
|
|
288
|
+
private readonly role;
|
|
289
|
+
private readonly selfId;
|
|
279
290
|
readonly media: MediaController;
|
|
280
291
|
readonly protection: ContentProtection;
|
|
281
292
|
private readonly listeners;
|
|
282
293
|
private mediaUnsub;
|
|
283
294
|
private captureUnsub;
|
|
295
|
+
private readonly peers;
|
|
296
|
+
private remoteStream;
|
|
284
297
|
constructor(options: MeetingConnectionOptions);
|
|
285
298
|
get meetingId(): string;
|
|
299
|
+
get meetingRole(): MeetingRole;
|
|
286
300
|
get connectionState(): MeetingConnectionState;
|
|
287
301
|
getMediaState(): LocalMediaState;
|
|
302
|
+
getRemoteStream(): MediaStream | null;
|
|
288
303
|
on<K extends keyof MeetingRoomEventMap>(event: K, listener: Listener<MeetingRoomEventMap[K]>): () => void;
|
|
289
304
|
connect(): Promise<void>;
|
|
290
305
|
setAudioEnabled(enabled: boolean): Promise<LocalMediaState>;
|
|
@@ -298,6 +313,13 @@ declare class MeetingConnection {
|
|
|
298
313
|
private teardownMedia;
|
|
299
314
|
private join;
|
|
300
315
|
private bindServerEvents;
|
|
316
|
+
private sendSignal;
|
|
317
|
+
private handleSignal;
|
|
318
|
+
private createHostOffer;
|
|
319
|
+
private getOrCreatePeer;
|
|
320
|
+
private attachLocalTracks;
|
|
321
|
+
private refreshHostTracks;
|
|
322
|
+
private closePeer;
|
|
301
323
|
private setState;
|
|
302
324
|
private emit;
|
|
303
325
|
}
|
|
@@ -344,7 +366,9 @@ declare class SabhaClient {
|
|
|
344
366
|
meeting: Meeting;
|
|
345
367
|
connection: MeetingConnection;
|
|
346
368
|
}>;
|
|
347
|
-
connect(meetingId: string
|
|
369
|
+
connect(meetingId: string, options?: {
|
|
370
|
+
role?: MeetingRole;
|
|
371
|
+
}): Promise<MeetingConnection>;
|
|
348
372
|
getConnection(): MeetingConnection | null;
|
|
349
373
|
leaveMeeting(): Promise<void>;
|
|
350
374
|
private applySession;
|
|
@@ -407,7 +431,9 @@ declare function useSabhaMeetings(): {
|
|
|
407
431
|
meeting: Meeting;
|
|
408
432
|
connection: MeetingConnection;
|
|
409
433
|
}>;
|
|
410
|
-
connect: (meetingId: string
|
|
434
|
+
connect: (meetingId: string, options?: {
|
|
435
|
+
role?: MeetingRole;
|
|
436
|
+
}) => Promise<MeetingConnection>;
|
|
411
437
|
leaveMeeting: () => Promise<void>;
|
|
412
438
|
connection: MeetingConnection | null;
|
|
413
439
|
connectionState: MeetingConnectionState;
|
|
@@ -447,4 +473,4 @@ declare function readStoredSessionCookie(): string | null;
|
|
|
447
473
|
declare function writeStoredSessionCookie(value: string | null): void;
|
|
448
474
|
|
|
449
475
|
export { ContentProtection, DEFAULT_BASE_URL, DEFAULT_MEDIA_POLICY, MediaController, MeetingConnection, SABHA_SESSION_HEADER, SABHA_SESSION_STORAGE_KEY, SABHA_TOKENS_STORAGE_KEY, SabhaClient, SabhaError, SabhaProvider, createSabhaClient, normalizeMeeting, readStoredSessionCookie, readStoredTokens, toSabhaError, useSabha, useSabhaMeetings, writeStoredSessionCookie, writeStoredTokens };
|
|
450
|
-
export type { AuthSession, AuthUser, CaptureAttemptType, CreateMeetingInput, LocalMediaState, LoginInput, MediaPolicy, MediaTrackKind, Meeting, MeetingConnectionOptions, MeetingConnectionState, MeetingInvitee, MeetingOrganizer, MeetingParticipant, MeetingRoomEventMap, RegisterInput, SabhaClientStatus, SabhaConfig, SabhaContextValue, SabhaOrg, SabhaProviderProps, SabhaSocketEvents, SabhaTokens, UpdateMeetingInput, VerifyEmailInput };
|
|
476
|
+
export type { AuthSession, AuthUser, CaptureAttemptType, CreateMeetingInput, LocalMediaState, LoginInput, MediaPolicy, MediaTrackKind, Meeting, MeetingConnectionOptions, MeetingConnectionState, MeetingInvitee, MeetingOrganizer, MeetingParticipant, MeetingRole, MeetingRoomEventMap, RegisterInput, SabhaClientStatus, SabhaConfig, SabhaContextValue, SabhaOrg, SabhaProviderProps, SabhaSocketEvents, SabhaTokens, UpdateMeetingInput, VerifyEmailInput };
|
package/dist/index.js
CHANGED
|
@@ -681,6 +681,11 @@ var DEFAULT_EVENTS = {
|
|
|
681
681
|
error: "meeting:error",
|
|
682
682
|
mediaState: "meeting:media-state"
|
|
683
683
|
};
|
|
684
|
+
var SIGNAL_EVENT = "meeting:signal";
|
|
685
|
+
var ICE_SERVERS = [
|
|
686
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
687
|
+
{ urls: "stun:stun1.l.google.com:19302" }
|
|
688
|
+
];
|
|
684
689
|
var MeetingConnection = class {
|
|
685
690
|
constructor(options) {
|
|
686
691
|
this.options = options;
|
|
@@ -689,20 +694,37 @@ var MeetingConnection = class {
|
|
|
689
694
|
this.listeners = /* @__PURE__ */ new Map();
|
|
690
695
|
this.mediaUnsub = null;
|
|
691
696
|
this.captureUnsub = null;
|
|
697
|
+
this.peers = /* @__PURE__ */ new Map();
|
|
698
|
+
this.remoteStream = null;
|
|
692
699
|
this.events = { ...DEFAULT_EVENTS, ...options.events };
|
|
693
|
-
this.
|
|
700
|
+
this.role = options.role ?? "host";
|
|
701
|
+
this.selfId = options.selfId || `anon-${Math.random().toString(36).slice(2, 10)}`;
|
|
702
|
+
this.mediaPolicy = this.role === "participant" ? {
|
|
703
|
+
...DEFAULT_MEDIA_POLICY,
|
|
704
|
+
...options.mediaPolicy,
|
|
705
|
+
audio: false,
|
|
706
|
+
video: false,
|
|
707
|
+
allowScreenShare: false,
|
|
708
|
+
screenOff: true
|
|
709
|
+
} : { ...DEFAULT_MEDIA_POLICY, ...options.mediaPolicy };
|
|
694
710
|
this.media = new MediaController(this.mediaPolicy);
|
|
695
711
|
this.protection = new ContentProtection(this.mediaPolicy);
|
|
696
712
|
}
|
|
697
713
|
get meetingId() {
|
|
698
714
|
return this.options.meetingId;
|
|
699
715
|
}
|
|
716
|
+
get meetingRole() {
|
|
717
|
+
return this.role;
|
|
718
|
+
}
|
|
700
719
|
get connectionState() {
|
|
701
720
|
return this.state;
|
|
702
721
|
}
|
|
703
722
|
getMediaState() {
|
|
704
723
|
return this.media.getMediaState();
|
|
705
724
|
}
|
|
725
|
+
getRemoteStream() {
|
|
726
|
+
return this.remoteStream;
|
|
727
|
+
}
|
|
706
728
|
on(event, listener) {
|
|
707
729
|
const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
708
730
|
set.add(listener);
|
|
@@ -734,7 +756,9 @@ var MeetingConnection = class {
|
|
|
734
756
|
withCredentials: this.options.withCredentials !== false,
|
|
735
757
|
auth,
|
|
736
758
|
extraHeaders,
|
|
737
|
-
autoConnect: true
|
|
759
|
+
autoConnect: true,
|
|
760
|
+
// Leave must stay left — do not let socket.io reconnect the room.
|
|
761
|
+
reconnection: false
|
|
738
762
|
});
|
|
739
763
|
this.socket = socket;
|
|
740
764
|
const onConnect = () => {
|
|
@@ -749,11 +773,22 @@ var MeetingConnection = class {
|
|
|
749
773
|
meetingId: this.options.meetingId,
|
|
750
774
|
...mediaState
|
|
751
775
|
});
|
|
776
|
+
if (this.role === "host") {
|
|
777
|
+
void this.refreshHostTracks();
|
|
778
|
+
}
|
|
752
779
|
});
|
|
753
780
|
this.captureUnsub = this.protection.onCaptureAttempt((event) => {
|
|
754
781
|
this.emit("captureAttempt", event);
|
|
755
782
|
});
|
|
756
|
-
|
|
783
|
+
if (this.role === "host") {
|
|
784
|
+
await this.media.start(this.mediaPolicy);
|
|
785
|
+
} else {
|
|
786
|
+
this.sendSignal({
|
|
787
|
+
type: "request-offer",
|
|
788
|
+
from: this.selfId,
|
|
789
|
+
role: "participant"
|
|
790
|
+
});
|
|
791
|
+
}
|
|
757
792
|
resolve();
|
|
758
793
|
}).catch(reject);
|
|
759
794
|
};
|
|
@@ -777,24 +812,56 @@ var MeetingConnection = class {
|
|
|
777
812
|
});
|
|
778
813
|
}
|
|
779
814
|
async setAudioEnabled(enabled) {
|
|
815
|
+
if (this.role !== "host") {
|
|
816
|
+
throw new SabhaError("Only the host can control the microphone", {
|
|
817
|
+
code: "HOST_ONLY"
|
|
818
|
+
});
|
|
819
|
+
}
|
|
780
820
|
return this.media.setAudioEnabled(enabled);
|
|
781
821
|
}
|
|
782
822
|
async setVideoEnabled(enabled) {
|
|
823
|
+
if (this.role !== "host") {
|
|
824
|
+
throw new SabhaError("Only the host can control the camera", {
|
|
825
|
+
code: "HOST_ONLY"
|
|
826
|
+
});
|
|
827
|
+
}
|
|
783
828
|
return this.media.setVideoEnabled(enabled);
|
|
784
829
|
}
|
|
785
830
|
async setScreenOff(off) {
|
|
831
|
+
if (this.role !== "host") {
|
|
832
|
+
throw new SabhaError("Only the host can change screen settings", {
|
|
833
|
+
code: "HOST_ONLY"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
786
836
|
return this.media.setScreenOff(off);
|
|
787
837
|
}
|
|
788
838
|
async startScreenShare() {
|
|
839
|
+
if (this.role !== "host") {
|
|
840
|
+
throw new SabhaError("Only the host can share screen", {
|
|
841
|
+
code: "HOST_ONLY"
|
|
842
|
+
});
|
|
843
|
+
}
|
|
789
844
|
return this.media.startScreenShare();
|
|
790
845
|
}
|
|
791
846
|
async stopScreenShare() {
|
|
847
|
+
if (this.role !== "host") {
|
|
848
|
+
throw new SabhaError("Only the host can stop screen share", {
|
|
849
|
+
code: "HOST_ONLY"
|
|
850
|
+
});
|
|
851
|
+
}
|
|
792
852
|
return this.media.stopScreenShare();
|
|
793
853
|
}
|
|
794
854
|
async leave() {
|
|
795
855
|
const socket = this.socket;
|
|
796
856
|
this.teardownMedia();
|
|
797
|
-
if (!socket)
|
|
857
|
+
if (!socket) {
|
|
858
|
+
this.setState("disconnected");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
socket.io.reconnection(false);
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
798
865
|
await new Promise((resolve) => {
|
|
799
866
|
socket.timeout(5e3).emit(
|
|
800
867
|
this.events.leave,
|
|
@@ -803,13 +870,22 @@ var MeetingConnection = class {
|
|
|
803
870
|
);
|
|
804
871
|
setTimeout(() => resolve(), 500);
|
|
805
872
|
});
|
|
873
|
+
socket.removeAllListeners();
|
|
806
874
|
socket.disconnect();
|
|
807
875
|
this.socket = null;
|
|
808
876
|
this.setState("disconnected");
|
|
809
877
|
}
|
|
810
878
|
disconnect() {
|
|
811
879
|
this.teardownMedia();
|
|
812
|
-
this.socket
|
|
880
|
+
const socket = this.socket;
|
|
881
|
+
if (socket) {
|
|
882
|
+
try {
|
|
883
|
+
socket.io.reconnection(false);
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
socket.removeAllListeners();
|
|
887
|
+
socket.disconnect();
|
|
888
|
+
}
|
|
813
889
|
this.socket = null;
|
|
814
890
|
this.setState("disconnected");
|
|
815
891
|
}
|
|
@@ -826,6 +902,10 @@ var MeetingConnection = class {
|
|
|
826
902
|
this.captureUnsub?.();
|
|
827
903
|
this.mediaUnsub = null;
|
|
828
904
|
this.captureUnsub = null;
|
|
905
|
+
for (const peer of this.peers.values()) peer.close();
|
|
906
|
+
this.peers.clear();
|
|
907
|
+
this.remoteStream = null;
|
|
908
|
+
this.emit("remoteStream", null);
|
|
829
909
|
this.media.stopAll();
|
|
830
910
|
this.protection.disable();
|
|
831
911
|
}
|
|
@@ -846,7 +926,11 @@ var MeetingConnection = class {
|
|
|
846
926
|
};
|
|
847
927
|
socket.timeout(5e3).emit(
|
|
848
928
|
this.events.join,
|
|
849
|
-
{
|
|
929
|
+
{
|
|
930
|
+
meetingId: this.options.meetingId,
|
|
931
|
+
role: this.role,
|
|
932
|
+
peerId: this.selfId
|
|
933
|
+
},
|
|
850
934
|
() => done()
|
|
851
935
|
);
|
|
852
936
|
setTimeout(done, 600);
|
|
@@ -856,10 +940,19 @@ var MeetingConnection = class {
|
|
|
856
940
|
socket.on(this.events.participantJoined, (payload) => {
|
|
857
941
|
this.emit("participantJoined", payload);
|
|
858
942
|
this.emit("raw", { event: this.events.participantJoined, payload });
|
|
943
|
+
if (this.role === "host") {
|
|
944
|
+
const peerId = payload.id;
|
|
945
|
+
if (peerId && peerId !== this.selfId) {
|
|
946
|
+
void this.createHostOffer(peerId).catch((error) => {
|
|
947
|
+
this.emit("error", toError(error, "Failed to create host offer"));
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
859
951
|
});
|
|
860
952
|
socket.on(this.events.participantLeft, (payload) => {
|
|
861
953
|
this.emit("participantLeft", payload);
|
|
862
954
|
this.emit("raw", { event: this.events.participantLeft, payload });
|
|
955
|
+
this.closePeer(payload.id);
|
|
863
956
|
});
|
|
864
957
|
socket.on(
|
|
865
958
|
this.events.meetingEnded,
|
|
@@ -881,10 +974,135 @@ var MeetingConnection = class {
|
|
|
881
974
|
this.emit("error", error);
|
|
882
975
|
this.emit("raw", { event: this.events.error, payload });
|
|
883
976
|
});
|
|
977
|
+
socket.on(SIGNAL_EVENT, (payload) => {
|
|
978
|
+
void this.handleSignal(payload).catch((error) => {
|
|
979
|
+
this.emit("error", toError(error, "WebRTC signal failed"));
|
|
980
|
+
});
|
|
981
|
+
});
|
|
884
982
|
socket.on("disconnect", () => {
|
|
885
983
|
this.setState("disconnected");
|
|
886
984
|
});
|
|
887
985
|
}
|
|
986
|
+
sendSignal(partial) {
|
|
987
|
+
if (!this.socket?.connected) return;
|
|
988
|
+
this.socket.emit(SIGNAL_EVENT, {
|
|
989
|
+
meetingId: this.options.meetingId,
|
|
990
|
+
...partial
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
async handleSignal(payload) {
|
|
994
|
+
if (!payload || payload.meetingId !== this.options.meetingId) return;
|
|
995
|
+
if (payload.from === this.selfId) return;
|
|
996
|
+
if (payload.to && payload.to !== this.selfId) return;
|
|
997
|
+
if (payload.type === "request-offer" && this.role === "host") {
|
|
998
|
+
await this.createHostOffer(payload.from);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (payload.type === "offer" && this.role === "participant") {
|
|
1002
|
+
const peer = this.getOrCreatePeer(payload.from, false);
|
|
1003
|
+
await peer.setRemoteDescription(payload.sdp);
|
|
1004
|
+
const answer = await peer.createAnswer();
|
|
1005
|
+
await peer.setLocalDescription(answer);
|
|
1006
|
+
this.sendSignal({
|
|
1007
|
+
type: "answer",
|
|
1008
|
+
from: this.selfId,
|
|
1009
|
+
to: payload.from,
|
|
1010
|
+
sdp: answer,
|
|
1011
|
+
role: "participant"
|
|
1012
|
+
});
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (payload.type === "answer" && this.role === "host") {
|
|
1016
|
+
const peer = this.peers.get(payload.from);
|
|
1017
|
+
if (!peer) return;
|
|
1018
|
+
await peer.setRemoteDescription(payload.sdp);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (payload.type === "ice") {
|
|
1022
|
+
const peer = this.peers.get(payload.from);
|
|
1023
|
+
if (!peer || !payload.candidate) return;
|
|
1024
|
+
await peer.addIceCandidate(payload.candidate);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
async createHostOffer(peerId) {
|
|
1028
|
+
const peer = this.getOrCreatePeer(peerId, true);
|
|
1029
|
+
await this.attachLocalTracks(peer);
|
|
1030
|
+
const offer = await peer.createOffer({
|
|
1031
|
+
offerToReceiveAudio: false,
|
|
1032
|
+
offerToReceiveVideo: false
|
|
1033
|
+
});
|
|
1034
|
+
await peer.setLocalDescription(offer);
|
|
1035
|
+
this.sendSignal({
|
|
1036
|
+
type: "offer",
|
|
1037
|
+
from: this.selfId,
|
|
1038
|
+
to: peerId,
|
|
1039
|
+
sdp: offer,
|
|
1040
|
+
role: "host"
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
getOrCreatePeer(peerId, isHost) {
|
|
1044
|
+
const existing = this.peers.get(peerId);
|
|
1045
|
+
if (existing) return existing;
|
|
1046
|
+
const peer = new RTCPeerConnection({ iceServers: ICE_SERVERS });
|
|
1047
|
+
this.peers.set(peerId, peer);
|
|
1048
|
+
peer.onicecandidate = (event) => {
|
|
1049
|
+
this.sendSignal({
|
|
1050
|
+
type: "ice",
|
|
1051
|
+
from: this.selfId,
|
|
1052
|
+
to: peerId,
|
|
1053
|
+
candidate: event.candidate ? event.candidate.toJSON() : null,
|
|
1054
|
+
role: this.role
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
peer.onconnectionstatechange = () => {
|
|
1058
|
+
if (peer.connectionState === "failed" || peer.connectionState === "closed" || peer.connectionState === "disconnected") ;
|
|
1059
|
+
};
|
|
1060
|
+
if (!isHost) {
|
|
1061
|
+
peer.ontrack = (event) => {
|
|
1062
|
+
const stream = event.streams[0] ?? new MediaStream([event.track]);
|
|
1063
|
+
this.remoteStream = stream;
|
|
1064
|
+
this.emit("remoteStream", stream);
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
return peer;
|
|
1068
|
+
}
|
|
1069
|
+
async attachLocalTracks(peer) {
|
|
1070
|
+
const local = new MediaStream();
|
|
1071
|
+
const cam = this.media.getCameraStream();
|
|
1072
|
+
const mic = this.media.getMicrophoneStream();
|
|
1073
|
+
const screen = this.media.getScreenStream();
|
|
1074
|
+
mic?.getAudioTracks().forEach((track) => local.addTrack(track));
|
|
1075
|
+
const videoSource = screen ?? cam;
|
|
1076
|
+
videoSource?.getVideoTracks().forEach((track) => local.addTrack(track));
|
|
1077
|
+
const senders = peer.getSenders();
|
|
1078
|
+
for (const track of local.getTracks()) {
|
|
1079
|
+
const already = senders.some((s) => s.track?.id === track.id);
|
|
1080
|
+
if (!already) peer.addTrack(track, local);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async refreshHostTracks() {
|
|
1084
|
+
for (const [peerId, peer] of this.peers) {
|
|
1085
|
+
try {
|
|
1086
|
+
await this.attachLocalTracks(peer);
|
|
1087
|
+
const offer = await peer.createOffer();
|
|
1088
|
+
await peer.setLocalDescription(offer);
|
|
1089
|
+
this.sendSignal({
|
|
1090
|
+
type: "offer",
|
|
1091
|
+
from: this.selfId,
|
|
1092
|
+
to: peerId,
|
|
1093
|
+
sdp: offer,
|
|
1094
|
+
role: "host"
|
|
1095
|
+
});
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
closePeer(peerId) {
|
|
1101
|
+
const peer = this.peers.get(peerId);
|
|
1102
|
+
if (!peer) return;
|
|
1103
|
+
peer.close();
|
|
1104
|
+
this.peers.delete(peerId);
|
|
1105
|
+
}
|
|
888
1106
|
setState(state) {
|
|
889
1107
|
this.state = state;
|
|
890
1108
|
this.emit("state", state);
|
|
@@ -895,6 +1113,10 @@ var MeetingConnection = class {
|
|
|
895
1113
|
for (const listener of set) listener(payload);
|
|
896
1114
|
}
|
|
897
1115
|
};
|
|
1116
|
+
function toError(error, fallback) {
|
|
1117
|
+
if (error instanceof Error) return error;
|
|
1118
|
+
return new Error(fallback);
|
|
1119
|
+
}
|
|
898
1120
|
|
|
899
1121
|
// src/SabhaClient.ts
|
|
900
1122
|
var SabhaClient = class {
|
|
@@ -1110,15 +1332,16 @@ var SabhaClient = class {
|
|
|
1110
1332
|
groupId: input?.groupId
|
|
1111
1333
|
});
|
|
1112
1334
|
}
|
|
1113
|
-
const connection = await this.connect(meeting.id);
|
|
1335
|
+
const connection = await this.connect(meeting.id, { role: "host" });
|
|
1114
1336
|
return { meeting, connection };
|
|
1115
1337
|
}
|
|
1116
|
-
async connect(meetingId) {
|
|
1338
|
+
async connect(meetingId, options) {
|
|
1117
1339
|
this.ensureConfigured();
|
|
1118
1340
|
if (this.activeConnection) {
|
|
1119
1341
|
this.activeConnection.disconnect();
|
|
1120
1342
|
this.activeConnection = null;
|
|
1121
1343
|
}
|
|
1344
|
+
const role = options?.role ?? "host";
|
|
1122
1345
|
const connection = new MeetingConnection({
|
|
1123
1346
|
baseUrl: this.config.baseUrl ?? DEFAULT_BASE_URL,
|
|
1124
1347
|
meetingId,
|
|
@@ -1127,7 +1350,10 @@ var SabhaClient = class {
|
|
|
1127
1350
|
withCredentials: this.config.withCredentials !== false,
|
|
1128
1351
|
socketPath: this.config.socketPath,
|
|
1129
1352
|
events: this.config.socketEvents,
|
|
1130
|
-
mediaPolicy: this.mediaPolicy
|
|
1353
|
+
mediaPolicy: this.mediaPolicy,
|
|
1354
|
+
role,
|
|
1355
|
+
// Per-browser id so same Sabha account can host on one device and view on another.
|
|
1356
|
+
selfId: getDevicePeerId(this.user?.userId)
|
|
1131
1357
|
});
|
|
1132
1358
|
try {
|
|
1133
1359
|
await connection.connect();
|
|
@@ -1222,6 +1448,18 @@ function createSabhaClient(config) {
|
|
|
1222
1448
|
client.configure(config ?? { baseUrl: DEFAULT_BASE_URL });
|
|
1223
1449
|
return client;
|
|
1224
1450
|
}
|
|
1451
|
+
function getDevicePeerId(userId) {
|
|
1452
|
+
const key = "sabha_device_peer_id";
|
|
1453
|
+
try {
|
|
1454
|
+
const existing = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
|
|
1455
|
+
if (existing) return existing;
|
|
1456
|
+
const next = `${userId || "user"}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1457
|
+
if (typeof sessionStorage !== "undefined") sessionStorage.setItem(key, next);
|
|
1458
|
+
return next;
|
|
1459
|
+
} catch {
|
|
1460
|
+
return `${userId || "user"}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1225
1463
|
var SabhaContext = react.createContext(null);
|
|
1226
1464
|
var idleMedia = {
|
|
1227
1465
|
audioEnabled: false,
|
|
@@ -1284,7 +1522,13 @@ function SabhaProvider({
|
|
|
1284
1522
|
}
|
|
1285
1523
|
setConnectionState(connection.connectionState);
|
|
1286
1524
|
setMediaState(connection.getMediaState());
|
|
1287
|
-
const offState = connection.on("state",
|
|
1525
|
+
const offState = connection.on("state", (state) => {
|
|
1526
|
+
setConnectionState(state);
|
|
1527
|
+
if (state === "disconnected" || state === "error") {
|
|
1528
|
+
setConnection(null);
|
|
1529
|
+
setMediaState(idleMedia);
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1288
1532
|
const offMedia = connection.on("mediaState", setMediaState);
|
|
1289
1533
|
return () => {
|
|
1290
1534
|
offState();
|
|
@@ -1378,10 +1622,10 @@ function SabhaProvider({
|
|
|
1378
1622
|
[client, connection]
|
|
1379
1623
|
);
|
|
1380
1624
|
const connect = react.useCallback(
|
|
1381
|
-
async (meetingId) => {
|
|
1625
|
+
async (meetingId, options) => {
|
|
1382
1626
|
setError(null);
|
|
1383
1627
|
try {
|
|
1384
|
-
const next = await client.connect(meetingId);
|
|
1628
|
+
const next = await client.connect(meetingId, options);
|
|
1385
1629
|
setConnection(next);
|
|
1386
1630
|
return next;
|
|
1387
1631
|
} catch (err) {
|