Skip to content
This repository was archived by the owner on Jul 17, 2024. It is now read-only.

Commit 62539bf

Browse files
committed
Reconnect on network change
1 parent 0cfd711 commit 62539bf

File tree

9 files changed

+159
-54
lines changed

9 files changed

+159
-54
lines changed

assets/package-lock.json

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"preview": "vite preview"
1212
},
1313
"dependencies": {
14-
"@fishjam-dev/react-client": "github:fishjam-dev/react-client-sdk#turnconfig",
14+
"@fishjam-dev/react-client": "github:fishjam-dev/react-client-sdk#reconnet-on-network-change",
1515
"@mediapipe/tasks-vision": "^0.10.12",
1616
"axios": "^1.6.8",
1717
"chartist": "^1.3.0",

assets/src/features/devices/LocalPeerMediaContext.tsx

+25-13
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ import {
44
SCREENSHARING_TRACK_CONSTRAINTS,
55
VIDEO_TRACK_CONSTRAINTS
66
} from "../../pages/room/consts";
7-
import { PeerMetadata, TrackMetadata, useCamera, useClient, useMicrophone, useSetupMedia } from "../../fishjam";
8-
import { ClientEvents, UseCameraResult, SimulcastConfig } from "@fishjam-dev/react-client";
7+
import {
8+
PeerMetadata,
9+
TrackMetadata,
10+
useCamera,
11+
useClient,
12+
useMicrophone,
13+
useSetupMedia
14+
} from "../../fishjam";
15+
import { ClientEvents, CameraAPI, SimulcastConfig } from "@fishjam-dev/react-client";
916
import { BlurProcessor } from "./BlurProcessor";
1017
import { selectBandwidthLimit } from "../../pages/room/bandwidth.tsx";
1118
import { useDeveloperInfo } from "../../contexts/DeveloperInfoContext.tsx";
1219
import { useUser } from "../../contexts/UserContext.tsx";
1320

1421
export type LocalPeerContext = {
15-
video: UseCameraResult<TrackMetadata>;
22+
video: CameraAPI<TrackMetadata>;
1623
init: () => void;
1724
blur: boolean;
1825
setBlur: (status: boolean, restart: boolean) => void;
@@ -67,6 +74,7 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
6774
});
6875

6976
const video = useCamera();
77+
const microphone = useMicrophone();
7078

7179
const [blur, setBlur] = useState(false);
7280
const processor = useRef<BlurProcessor | null>(null);
@@ -127,6 +135,9 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
127135
.some((track) => track?.metadata?.type === "camera");
128136

129137
const newTrack = trackRef.current;
138+
139+
const newMetadata = { active: metadataActive, type: "camera", displayName } as const;
140+
130141
if (!lastCameraTrack && streamRef.current && newTrack) {
131142
const mediaStream = new MediaStream();
132143
mediaStream.addTrack(newTrack);
@@ -140,8 +151,7 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
140151

141152
remoteTrackIdRef.current = await client.addTrack(
142153
newTrack,
143-
mediaStream,
144-
{ active: metadataActive, type: "camera", displayName },
154+
newMetadata,
145155
simulcastConfig,
146156
selectBandwidthLimit("camera", simulcastEnabled)
147157
);
@@ -152,7 +162,6 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
152162
// todo
153163
// When you replaceTrack, this does not affect the stream, so the local peer doesn't know that something has changed.
154164
// add localTrackReplaced event
155-
const newMetadata: TrackMetadata = { active: metadataActive, type: "camera", displayName };
156165
await client.replaceTrack(remoteTrackIdRef.current, trackRef.current, newMetadata);
157166
} else if (remoteTrackIdRef.current && !stream) {
158167
await client.removeTrack(remoteTrackIdRef.current);
@@ -240,12 +249,14 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
240249
}
241250
};
242251

243-
const disconnected: ClientEvents<PeerMetadata, TrackMetadata>["disconnected"] = async (client) => {
252+
const disconnected: ClientEvents<PeerMetadata, TrackMetadata>["disconnected"] = async (clientApi) => {
244253
remoteTrackIdRef.current = null;
245254

246-
if (client.devices.microphone.stream) client.devices.microphone.stop();
247-
if (client.devices.camera.stream) client.devices.camera.stop();
248-
if (client.devices.screenShare.stream) client.devices.screenShare.stop();
255+
if (!client.isReconnecting()) {
256+
if (clientApi.devices.microphone.stream) clientApi.devices.microphone.stop();
257+
if (clientApi.devices.camera.stream) clientApi.devices.camera.stop();
258+
if (clientApi.devices.screenShare.stream) clientApi.devices.screenShare.stop();
259+
}
249260
};
250261

251262

@@ -276,12 +287,15 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
276287

277288
const noop: () => Promise<any> = useCallback((..._args) => Promise.resolve(), []);
278289

279-
const newVideo: UseCameraResult<TrackMetadata> = useMemo(() => ({
290+
const newVideo: CameraAPI<TrackMetadata> = useMemo(() => ({
280291
stream: stream || null,
281292
track: track || null,
282293
addTrack: noop,
283294
removeTrack: noop,
284295
replaceTrack: noop,
296+
updateTrackMetadata: noop,
297+
muteTrack: noop,
298+
unmuteTrack: noop,
285299
broadcast: video.broadcast,
286300
deviceInfo: video.deviceInfo,
287301
devices: video.devices,
@@ -295,8 +309,6 @@ export const LocalPeerMediaProvider = ({ children }: Props) => {
295309
mediaStatus: video.mediaStatus
296310
}), [stream, track]);
297311

298-
const microphone = useMicrophone();
299-
300312
const setDevice = useCallback(async (cameraId: string | null, microphoneId: string | null, blur: boolean) => {
301313
if (microphoneId && microphoneIntentionRef.current) {
302314
microphone.start(microphoneId);

assets/src/features/shared/context/ToastContext.tsx

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { createContext, ReactNode, useCallback, useMemo, useState } from "react";
22
import Toast from "../components/Toast";
33

4-
const DEFAULT_TOAST_TIMEOUT = 2500;
4+
const DEFAULT_TOAST_TIMEOUT = 2500; // milliseconds
55

6-
export type ToastType = { id: string; message?: string; timeout?: number | "INFINITY"; type?: "information" | "error" };
6+
export type ToastType = {
7+
id: string;
8+
message?: string;
9+
timeout?: number | "INFINITY"; // milliseconds
10+
type?: "information" | "error"
11+
};
712

813
export const ToastContext = createContext({
9-
addToast: (newToast: ToastType) => console.error(`Unknown error while adding toast: ${newToast}`)
14+
addToast: (newToast: ToastType) => console.error(`Unknown error while adding toast: ${newToast}`),
15+
removeToast: (toastId: string) => console.error(`Unknown error while removing toast: ${toastId}`)
1016
});
1117

1218
export const ToastProvider = ({ children }: { children?: ReactNode }) => {
@@ -28,14 +34,14 @@ export const ToastProvider = ({ children }: { children?: ReactNode }) => {
2834
[toasts]
2935
);
3036

31-
const removeToast = (toastId: string) => {
37+
const removeToast = useCallback((toastId: string) => {
3238
document.getElementById(toastId)?.classList.add("fadeOut");
3339
setTimeout(() => {
3440
setToasts((prev) => prev.filter((t) => t.id != toastId));
3541
}, 2000);
36-
};
42+
}, []);
3743

38-
const value = useMemo(() => ({ addToast }), [addToast]);
44+
const value = useMemo(() => ({ addToast, removeToast }), [removeToast, addToast]);
3945

4046
return (
4147
<ToastContext.Provider value={value}>

assets/src/features/streaming/StreamingErrorBoundary.tsx

+83-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { FC, PropsWithChildren, useCallback, useEffect, useState } from "react";
22
import useToast from "../shared/hooks/useToast";
33
import { ErrorMessage, messageComparator } from "../../pages/room/errorMessage";
4-
import { useClient } from "../../fishjam";
4+
import { PeerMetadata, TrackMetadata, useClient } from "../../fishjam";
55
import useEffectOnChange from "../shared/hooks/useEffectOnChange";
6+
import { ClientEvents } from "@fishjam-dev/react-client";
7+
8+
const RECONNECTING_TOAST_ID = "RECONNECTING";
69

710
export const StreamingErrorBoundary: FC<PropsWithChildren> = ({ children }) => {
8-
const { addToast } = useToast();
11+
const { addToast, removeToast } = useToast();
912

1013
// todo remove state, refactor to function invocation
1114
const [errorMessage, setErrorMessage] = useState<ErrorMessage | undefined>();
@@ -20,34 +23,104 @@ export const StreamingErrorBoundary: FC<PropsWithChildren> = ({ children }) => {
2023
[setErrorMessage]
2124
);
2225

26+
const displayInfo = useCallback(
27+
(text: string, id?: string) => {
28+
addToast({
29+
id: id || crypto.randomUUID(),
30+
message: text,
31+
timeout: 5000,
32+
type: "information"
33+
});
34+
},
35+
[]
36+
);
37+
2338
useEffect(() => {
2439
if (!client) return;
2540

26-
const onSocketError = (_: Event) => {
41+
const onReconnectionStarted: ClientEvents<PeerMetadata, TrackMetadata>["reconnectionStarted"] = () => {
42+
console.log("%c" + "reconnectionStarted", "color:green");
43+
44+
addToast({
45+
id: RECONNECTING_TOAST_ID,
46+
message: "Reconnecting",
47+
timeout: "INFINITY",
48+
type: "information"
49+
});
50+
};
51+
52+
const onReconnected: ClientEvents<PeerMetadata, TrackMetadata>["reconnected"] = () => {
53+
console.log("%cReconnected", "color:green");
54+
removeToast(RECONNECTING_TOAST_ID);
55+
56+
setTimeout(() => {
57+
displayInfo("Reconnected");
58+
}, 2000);
59+
};
60+
61+
const onReconnectionFailed: ClientEvents<PeerMetadata, TrackMetadata>["reconnectionFailed"] = () => {
62+
console.log("%cReconnectionFailed", "color:red");
63+
removeToast(RECONNECTING_TOAST_ID);
64+
65+
setTimeout(() => {
66+
handleError(`Reconnection failed`);
67+
}, 2000);
68+
};
69+
70+
const onSocketError: ClientEvents<PeerMetadata, TrackMetadata>["socketError"] = (error: Event) => {
71+
console.warn(error);
2772
handleError(`Socket error occurred.`, "onSocketError");
2873
};
2974

30-
const onConnectionError = (message: string) => {
31-
handleError(`Connection error occurred. ${message ?? ""}`);
75+
const onConnectionError: ClientEvents<PeerMetadata, TrackMetadata>["connectionError"] = (error, client) => {
76+
if (client.isReconnecting()) {
77+
console.log("%c" + "During reconnection: connectionError %o", "color:gray", {
78+
error,
79+
iceConnectionState: error?.event?.target?.["iceConnectionState"]
80+
});
81+
} else {
82+
console.warn({ error, state: error?.event?.target?.["iceConnectionState"] });
83+
handleError(`Connection error occurred. ${error?.message ?? ""}`);
84+
}
3285
};
33-
const onJoinError = (_: unknown) => {
86+
87+
const onJoinError: ClientEvents<PeerMetadata, TrackMetadata>["joinError"] = (event) => {
88+
console.log(event);
3489
handleError(`Failed to join the room`);
3590
};
36-
const onAuthError = () => {
37-
handleError(`Socket error occurred.`, "onAuthError");
91+
92+
const onAuthError: ClientEvents<PeerMetadata, TrackMetadata>["authError"] = (reason) => {
93+
if (client.isReconnecting()) {
94+
console.log("%c" + "During reconnection: authError: " + reason, "color:gray");
95+
} else {
96+
handleError(`Socket error occurred.`, "onAuthError");
97+
}
3898
};
3999

40-
const onSocketClose = (_: Event) => {
41-
handleError(`Signaling socket closed.`, "onSocketClose");
100+
const onSocketClose: ClientEvents<PeerMetadata, TrackMetadata>["socketClose"] = (event) => {
101+
if (client.isReconnecting()) {
102+
console.log("%c" + "During reconnection: Signaling socket closed", "color:gray");
103+
} else {
104+
console.warn(event);
105+
handleError(`Signaling socket closed.`, "onSocketClose");
106+
}
42107
};
43108

109+
client.on("reconnectionStarted", onReconnectionStarted);
110+
client.on("reconnected", onReconnected);
111+
client.on("reconnectionFailed", onReconnectionFailed);
112+
44113
client.on("socketError", onSocketError);
45114
client.on("connectionError", onConnectionError);
46115
client.on("joinError", onJoinError);
47116
client.on("authError", onAuthError);
48117
client.on("socketClose", onSocketClose);
49118

50119
return () => {
120+
client.off("reconnectionStarted", onReconnectionStarted);
121+
client.off("reconnected", onReconnected);
122+
client.off("reconnectionFailed", onReconnectionFailed);
123+
51124
client.off("socketError", onSocketError);
52125
client.off("connectionError", onConnectionError);
53126
client.off("joinError", onJoinError);

assets/src/fishjam.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export const {
3838
peerMetadataParser: (obj) => peerMetadataSchema.parse(obj),
3939
trackMetadataParser: (obj) => trackMetadataSchema.parse(obj),
4040
reconnect: {
41-
initialDelay: 10, // ms
42-
delay: 200, // ms
43-
maxAttempts: 1,
41+
initialDelay: 1000, // ms
42+
delay: 1000, // ms
43+
maxAttempts: 1000,
4444
}
4545
}, { storage: true });
4646

assets/src/pages/room/VideochatSection.tsx

+18-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import UnpinnedTilesSection from "./components/StreamPlayer/UnpinnedTilesSection
1010
import PinnedTilesSection from "./components/StreamPlayer/PinnedTilesSection";
1111
import useTilePinning from "./hooks/useTilePinning";
1212
import { toLocalTrackSelector, toRemotePeerSelector, TrackType, useSelector } from "../../fishjam";
13+
import { useLocalPeer } from "../../features/devices/LocalPeerMediaContext.tsx";
1314

1415
type Props = {
1516
showSimulcast: boolean;
@@ -36,7 +37,7 @@ const mapRemotePeersToMediaPlayerConfig = (peers: RemotePeer[]): PeerTileConfig[
3637
const audioTrack: TrackWithId | null = getTrack(peer.tracks, "audio");
3738

3839
return {
39-
mediaPlayerId: videoTrack?.remoteTrackId || peer.id,
40+
mediaPlayerId: peer.id,
4041
typeName: "remote",
4142
peerId: peer.id,
4243
displayName: peer.displayName || "Unknown",
@@ -108,17 +109,30 @@ export const VideochatSection: FC<Props> = ({ showSimulcast, unpinnedTilesHorizo
108109
initials: computeInitials(state?.local?.metadata?.name || "")
109110
}));
110111

111-
const localUser: PeerTileConfig = {
112+
const localPeer = useLocalPeer();
113+
114+
const localVideoWithId: TrackWithId = useMemo(() =>
115+
({
116+
stream: localPeer?.video?.stream ?? undefined,
117+
track: localPeer?.video?.track ?? undefined,
118+
isSpeaking: false,
119+
enabled: localPeer.video.enabled,
120+
encodingQuality: null,
121+
remoteTrackId: null,
122+
metadata: null
123+
}), [localPeer]);
124+
125+
const localUser: PeerTileConfig = useMemo(() => ({
112126
typeName: "local",
113127
peerId,
114128
displayName: LOCAL_PEER_NAME,
115129
initials,
116-
video: video,
130+
video: video ?? localVideoWithId,
117131
audio: audio,
118132
streamSource: "local",
119133
mediaPlayerId: LOCAL_VIDEO_ID,
120134
isSpeaking: false
121-
};
135+
}), [peerId, initials, video, localVideoWithId, audio]);
122136

123137
const peers: RemotePeer[] = useSelector((state) => toRemotePeerSelector(state));
124138
const screenSharingStreams = prepareScreenSharingStreams(peers, screenSharing);

0 commit comments

Comments
 (0)