@fishjam-cloud/react-client 0.5.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/dist/Client.d.ts +210 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +589 -0
- package/dist/DeviceManager.d.ts +49 -0
- package/dist/DeviceManager.d.ts.map +1 -0
- package/dist/DeviceManager.js +157 -0
- package/dist/ScreenShareManager.d.ts +57 -0
- package/dist/ScreenShareManager.d.ts.map +1 -0
- package/dist/ScreenShareManager.js +135 -0
- package/dist/constraints.d.ts +26 -0
- package/dist/constraints.d.ts.map +1 -0
- package/dist/constraints.js +47 -0
- package/dist/create.d.ts +11 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +228 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/mediaInitializer.d.ts +11 -0
- package/dist/mediaInitializer.d.ts.map +1 -0
- package/dist/mediaInitializer.js +48 -0
- package/dist/state.types.d.ts +60 -0
- package/dist/state.types.d.ts.map +1 -0
- package/dist/state.types.js +1 -0
- package/dist/trackManager.d.ts +24 -0
- package/dist/trackManager.d.ts.map +1 -0
- package/dist/trackManager.js +99 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/useSetupMedia.d.ts +3 -0
- package/dist/useSetupMedia.d.ts.map +1 -0
- package/dist/useSetupMedia.js +262 -0
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +22 -0
- package/dist/utils/localStorage.d.ts +5 -0
- package/dist/utils/localStorage.d.ts.map +1 -0
- package/dist/utils/localStorage.js +21 -0
- package/dist/utils/media.d.ts +12 -0
- package/dist/utils/media.d.ts.map +1 -0
- package/dist/utils/media.js +64 -0
- package/package.json +65 -0
- package/readme.md +151 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
export const createUseSetupMediaHook = (useFishjamContext) => {
|
|
3
|
+
const isBroadcastedTrackChanged = (client, pending) => client.status === "joined" && !pending && !client.isReconnecting();
|
|
4
|
+
const isBroadcastedTrackStopped = (status, stream) => status === "joined" && stream;
|
|
5
|
+
return (config) => {
|
|
6
|
+
const { state } = useFishjamContext();
|
|
7
|
+
const configRef = useRef(config);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
configRef.current = config;
|
|
10
|
+
if (config.screenShare.streamConfig) {
|
|
11
|
+
state.client.setScreenManagerConfig(config.screenShare.streamConfig);
|
|
12
|
+
}
|
|
13
|
+
state.client.setDeviceManagerConfig({
|
|
14
|
+
storage: config.storage,
|
|
15
|
+
});
|
|
16
|
+
}, [config, state.client]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!configRef.current.startOnMount)
|
|
19
|
+
return;
|
|
20
|
+
if (state.client.audioDeviceManager.getStatus() === "uninitialized" ||
|
|
21
|
+
state.client.videoDeviceManager.getStatus() === "uninitialized") {
|
|
22
|
+
state.client.initializeDevices({
|
|
23
|
+
audioTrackConstraints: configRef?.current.microphone.trackConstraints,
|
|
24
|
+
videoTrackConstraints: configRef?.current.camera.trackConstraints,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// eslint-disable-next-line
|
|
28
|
+
}, []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let pending = false;
|
|
31
|
+
const broadcastOnCameraStart = async (client) => {
|
|
32
|
+
const config = configRef.current.camera;
|
|
33
|
+
const videoTrackManager = client.videoTrackManager;
|
|
34
|
+
const onDeviceChange = config.onDeviceChange ?? "replace";
|
|
35
|
+
const camera = client.devices.camera;
|
|
36
|
+
const stream = camera.broadcast?.stream;
|
|
37
|
+
if (isBroadcastedTrackChanged(client, pending)) {
|
|
38
|
+
if (!stream && config.broadcastOnDeviceStart) {
|
|
39
|
+
pending = true;
|
|
40
|
+
await videoTrackManager
|
|
41
|
+
.startStreaming(config.defaultTrackMetadata, config.defaultSimulcastConfig, config.defaultMaxBandwidth)
|
|
42
|
+
.finally(() => {
|
|
43
|
+
pending = false;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (stream && onDeviceChange === "replace") {
|
|
47
|
+
pending = true;
|
|
48
|
+
await videoTrackManager.refreshStreamedTrack().finally(() => {
|
|
49
|
+
pending = false;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (stream && onDeviceChange === "remove") {
|
|
53
|
+
pending = true;
|
|
54
|
+
await videoTrackManager.stopStreaming().finally(() => {
|
|
55
|
+
pending = false;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const managerInitialized = async (event, client) => {
|
|
61
|
+
if (event.video?.media?.stream) {
|
|
62
|
+
await broadcastOnCameraStart(client);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const devicesReady = async (event, client) => {
|
|
66
|
+
if (event.trackType === "video" && event.restarted && event?.media?.stream) {
|
|
67
|
+
await broadcastOnCameraStart(client);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const deviceReady = async (event, client) => {
|
|
71
|
+
if (event.trackType === "video") {
|
|
72
|
+
await broadcastOnCameraStart(client);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
state.client.on("managerInitialized", managerInitialized);
|
|
76
|
+
state.client.on("devicesReady", devicesReady);
|
|
77
|
+
state.client.on("deviceReady", deviceReady);
|
|
78
|
+
return () => {
|
|
79
|
+
state.client.removeListener("managerInitialized", managerInitialized);
|
|
80
|
+
state.client.removeListener("devicesReady", devicesReady);
|
|
81
|
+
state.client.removeListener("deviceReady", deviceReady);
|
|
82
|
+
};
|
|
83
|
+
}, [state.client]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const removeOnCameraStopped = async (event, client) => {
|
|
86
|
+
const camera = client.devices.camera;
|
|
87
|
+
const videoTrackManager = client.videoTrackManager;
|
|
88
|
+
const stream = camera.broadcast?.stream;
|
|
89
|
+
const onDeviceStop = configRef.current.camera.onDeviceStop ?? "mute";
|
|
90
|
+
if (event.mediaDeviceType === "userMedia" &&
|
|
91
|
+
event.trackType === "video" &&
|
|
92
|
+
isBroadcastedTrackStopped(client.status, stream)) {
|
|
93
|
+
if (onDeviceStop === "mute") {
|
|
94
|
+
await videoTrackManager.pauseStreaming();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
await videoTrackManager.stopStreaming();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
state.client.on("deviceStopped", removeOnCameraStopped);
|
|
102
|
+
return () => {
|
|
103
|
+
state.client.removeListener("deviceStopped", removeOnCameraStopped);
|
|
104
|
+
};
|
|
105
|
+
}, [state.client]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const broadcastCameraOnConnect = async (_, client) => {
|
|
108
|
+
const camera = client.devices.camera;
|
|
109
|
+
const stream = camera.stream;
|
|
110
|
+
const config = configRef.current.camera;
|
|
111
|
+
if (stream && config.broadcastOnConnect) {
|
|
112
|
+
await client.videoTrackManager.startStreaming(config.defaultTrackMetadata, config.defaultSimulcastConfig, config.defaultMaxBandwidth);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
state.client.on("joined", broadcastCameraOnConnect);
|
|
116
|
+
return () => {
|
|
117
|
+
state.client.removeListener("joined", broadcastCameraOnConnect);
|
|
118
|
+
};
|
|
119
|
+
}, [state.client]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
let pending = false;
|
|
122
|
+
const broadcastOnMicrophoneStart = async (client) => {
|
|
123
|
+
const microphone = client.devices.microphone;
|
|
124
|
+
const audioTrackManager = client.audioTrackManager;
|
|
125
|
+
const stream = microphone.broadcast?.stream;
|
|
126
|
+
const config = configRef.current.microphone;
|
|
127
|
+
const onDeviceChange = config.onDeviceChange ?? "replace";
|
|
128
|
+
if (isBroadcastedTrackChanged(client, pending)) {
|
|
129
|
+
if (!stream && config.broadcastOnDeviceStart) {
|
|
130
|
+
pending = true;
|
|
131
|
+
await audioTrackManager
|
|
132
|
+
.startStreaming(config.defaultTrackMetadata, undefined, config.defaultMaxBandwidth)
|
|
133
|
+
.finally(() => {
|
|
134
|
+
pending = false;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else if (stream && onDeviceChange === "replace") {
|
|
138
|
+
pending = true;
|
|
139
|
+
await audioTrackManager.refreshStreamedTrack().finally(() => {
|
|
140
|
+
pending = false;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else if (stream && onDeviceChange === "remove") {
|
|
144
|
+
pending = true;
|
|
145
|
+
await audioTrackManager.stopStreaming().finally(() => {
|
|
146
|
+
pending = false;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const managerInitialized = async (event, client) => {
|
|
152
|
+
if (event.audio?.media?.stream) {
|
|
153
|
+
await broadcastOnMicrophoneStart(client);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const devicesReady = async (event, client) => {
|
|
157
|
+
if (event.trackType === "audio" && event.restarted && event?.media?.stream) {
|
|
158
|
+
await broadcastOnMicrophoneStart(client);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const deviceReady = async (event, client) => {
|
|
162
|
+
if (event.trackType === "audio") {
|
|
163
|
+
await broadcastOnMicrophoneStart(client);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
state.client.on("managerInitialized", managerInitialized);
|
|
167
|
+
state.client.on("deviceReady", deviceReady);
|
|
168
|
+
state.client.on("devicesReady", devicesReady);
|
|
169
|
+
return () => {
|
|
170
|
+
state.client.removeListener("managerInitialized", managerInitialized);
|
|
171
|
+
state.client.removeListener("deviceReady", deviceReady);
|
|
172
|
+
state.client.removeListener("devicesReady", devicesReady);
|
|
173
|
+
};
|
|
174
|
+
}, [state.client]);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const onMicrophoneStopped = async (event, client) => {
|
|
177
|
+
const audioTrackManager = client.audioTrackManager;
|
|
178
|
+
const stream = client.devices.microphone.broadcast?.stream;
|
|
179
|
+
const onDeviceStop = configRef.current.microphone.onDeviceStop ?? "mute";
|
|
180
|
+
const isRightDeviceType = event.mediaDeviceType === "userMedia";
|
|
181
|
+
const isRightTrackType = event.trackType === "audio";
|
|
182
|
+
if (isRightDeviceType && isRightTrackType && isBroadcastedTrackStopped(client.status, stream)) {
|
|
183
|
+
if (onDeviceStop === "mute") {
|
|
184
|
+
await audioTrackManager.pauseStreaming();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await audioTrackManager.stopStreaming();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
state.client.on("deviceStopped", onMicrophoneStopped);
|
|
192
|
+
return () => {
|
|
193
|
+
state.client.removeListener("deviceStopped", onMicrophoneStopped);
|
|
194
|
+
};
|
|
195
|
+
}, [state.client]);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
const broadcastMicrophoneOnConnect = async (_, client) => {
|
|
198
|
+
const config = configRef.current.microphone;
|
|
199
|
+
const microphone = client.devices.microphone;
|
|
200
|
+
if (microphone.stream && config.broadcastOnConnect) {
|
|
201
|
+
await client.audioTrackManager.startStreaming(config.defaultTrackMetadata, undefined, config.defaultMaxBandwidth);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
state.client.on("joined", broadcastMicrophoneOnConnect);
|
|
205
|
+
return () => {
|
|
206
|
+
state.client.removeListener("joined", broadcastMicrophoneOnConnect);
|
|
207
|
+
};
|
|
208
|
+
}, [state.client]);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
let pending = false;
|
|
211
|
+
const broadcastOnScreenShareStart = async (event, client) => {
|
|
212
|
+
const screenShare = client.devices.screenShare;
|
|
213
|
+
const stream = screenShare.broadcast?.stream;
|
|
214
|
+
const { broadcastOnDeviceStart, defaultTrackMetadata, defaultMaxBandwidth } = configRef.current.screenShare;
|
|
215
|
+
if (event.mediaDeviceType === "displayMedia" &&
|
|
216
|
+
isBroadcastedTrackChanged(client, pending) &&
|
|
217
|
+
!stream &&
|
|
218
|
+
broadcastOnDeviceStart) {
|
|
219
|
+
pending = true;
|
|
220
|
+
await screenShare.startStreaming(defaultTrackMetadata, defaultMaxBandwidth).finally(() => {
|
|
221
|
+
pending = false;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
state.client.on("deviceReady", broadcastOnScreenShareStart);
|
|
226
|
+
return () => {
|
|
227
|
+
state.client.removeListener("deviceReady", broadcastOnScreenShareStart);
|
|
228
|
+
};
|
|
229
|
+
}, [state.client]);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const onScreenShareStop = async (event, client) => {
|
|
232
|
+
const stream = client.devices.screenShare.broadcast?.stream;
|
|
233
|
+
const isRightDeviceType = event.mediaDeviceType === "displayMedia";
|
|
234
|
+
const isRightTrackType = event.trackType === "video";
|
|
235
|
+
if (isRightDeviceType && isRightTrackType && isBroadcastedTrackStopped(client.status, stream)) {
|
|
236
|
+
await client.devices.screenShare.stopStreaming();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
state.client.on("deviceStopped", onScreenShareStop);
|
|
240
|
+
return () => {
|
|
241
|
+
state.client.removeListener("deviceStopped", onScreenShareStop);
|
|
242
|
+
};
|
|
243
|
+
}, [state.client]);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const broadcastScreenShareOnConnect = async (_, client) => {
|
|
246
|
+
if (client.devices.screenShare.stream && configRef.current.screenShare.broadcastOnConnect) {
|
|
247
|
+
await client.devices.screenShare.startStreaming(configRef.current.screenShare.defaultTrackMetadata, configRef.current.screenShare.defaultMaxBandwidth);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
state.client.on("joined", broadcastScreenShareOnConnect);
|
|
251
|
+
return () => {
|
|
252
|
+
state.client.removeListener("joined", broadcastScreenShareOnConnect);
|
|
253
|
+
};
|
|
254
|
+
}, [state.client]);
|
|
255
|
+
return useMemo(() => ({
|
|
256
|
+
init: () => state.client.initializeDevices({
|
|
257
|
+
audioTrackConstraints: configRef.current?.microphone?.trackConstraints,
|
|
258
|
+
videoTrackConstraints: configRef.current?.camera?.trackConstraints,
|
|
259
|
+
}),
|
|
260
|
+
}), [state.client]);
|
|
261
|
+
};
|
|
262
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeviceError } from "../types";
|
|
2
|
+
export declare const PERMISSION_DENIED: DeviceError;
|
|
3
|
+
export declare const OVERCONSTRAINED_ERROR: DeviceError;
|
|
4
|
+
export declare const NOT_FOUND_ERROR: DeviceError;
|
|
5
|
+
export declare const UNHANDLED_ERROR: DeviceError;
|
|
6
|
+
export declare const parseUserMediaError: (error: unknown) => DeviceError | null;
|
|
7
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,eAAO,MAAM,iBAAiB,EAAE,WAAyC,CAAC;AAC1E,eAAO,MAAM,qBAAqB,EAAE,WAA8C,CAAC;AACnF,eAAO,MAAM,eAAe,EAAE,WAAuC,CAAC;AACtE,eAAO,MAAM,eAAe,EAAE,WAAyC,CAAC;AAIxE,eAAO,MAAM,mBAAmB,UAAW,OAAO,KAAG,WAAW,GAAG,IAelE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const PERMISSION_DENIED = { name: "NotAllowedError" };
|
|
2
|
+
export const OVERCONSTRAINED_ERROR = { name: "OverconstrainedError" };
|
|
3
|
+
export const NOT_FOUND_ERROR = { name: "NotFoundError" };
|
|
4
|
+
export const UNHANDLED_ERROR = { name: "UNHANDLED_ERROR" };
|
|
5
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions
|
|
6
|
+
// OverconstrainedError has higher priority than NotAllowedError
|
|
7
|
+
export const parseUserMediaError = (error) => {
|
|
8
|
+
if (!(error instanceof DOMException)) {
|
|
9
|
+
console.warn({ name: "Unhandled getUserMedia error", error });
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (error.name === "NotAllowedError") {
|
|
13
|
+
return PERMISSION_DENIED;
|
|
14
|
+
}
|
|
15
|
+
else if (error.name === "OverconstrainedError") {
|
|
16
|
+
return OVERCONSTRAINED_ERROR;
|
|
17
|
+
}
|
|
18
|
+
else if (error.name === "NotFoundError") {
|
|
19
|
+
return NOT_FOUND_ERROR;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const loadObject: <T>(key: string, defaultValue: T) => T;
|
|
2
|
+
export declare const loadString: (key: string, defaultValue?: string) => string;
|
|
3
|
+
export declare const saveObject: <T>(key: string, value: T) => void;
|
|
4
|
+
export declare const saveString: (key: string, value: string) => void;
|
|
5
|
+
//# sourceMappingURL=localStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localStorage.d.ts","sourceRoot":"","sources":["../../src/utils/localStorage.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,GAAI,CAAC,OAAO,MAAM,gBAAgB,CAAC,KAAG,CAM5D,CAAC;AAEF,eAAO,MAAM,UAAU,QAAS,MAAM,kCAMrC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,CAAC,OAAO,MAAM,SAAS,CAAC,SAGlD,CAAC;AAEF,eAAO,MAAM,UAAU,QAAS,MAAM,SAAS,MAAM,SAEpD,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const loadObject = (key, defaultValue) => {
|
|
2
|
+
const stringValue = loadString(key, "");
|
|
3
|
+
if (stringValue === "") {
|
|
4
|
+
return defaultValue;
|
|
5
|
+
}
|
|
6
|
+
return JSON.parse(stringValue);
|
|
7
|
+
};
|
|
8
|
+
export const loadString = (key, defaultValue = "") => {
|
|
9
|
+
const value = localStorage.getItem(key);
|
|
10
|
+
if (value === null || value === undefined) {
|
|
11
|
+
return defaultValue;
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
export const saveObject = (key, value) => {
|
|
16
|
+
const stringValue = JSON.stringify(value);
|
|
17
|
+
saveString(key, stringValue);
|
|
18
|
+
};
|
|
19
|
+
export const saveString = (key, value) => {
|
|
20
|
+
localStorage.setItem(key, value);
|
|
21
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CurrentDevices, DeviceError, DeviceState, StorageConfig } from "../types";
|
|
2
|
+
export declare const removeSpecifiedDeviceFromConstraints: (trackConstraints?: boolean | MediaTrackConstraints) => boolean | MediaTrackConstraints | undefined;
|
|
3
|
+
export declare const REQUESTING = "Requesting";
|
|
4
|
+
export declare const NOT_REQUESTED = "Not requested";
|
|
5
|
+
export declare const getDeviceInfo: (trackDeviceId: string | null, devices: MediaDeviceInfo[]) => MediaDeviceInfo | null;
|
|
6
|
+
export declare const getCurrentDevicesSettings: (requestedDevices: MediaStream, mediaDeviceInfos: MediaDeviceInfo[]) => CurrentDevices;
|
|
7
|
+
export declare const isDeviceDifferentFromLastSession: (lastDevice: MediaDeviceInfo | null, currentDevice: MediaDeviceInfo | null) => boolean | null;
|
|
8
|
+
export declare const isAnyDeviceDifferentFromLastSession: (lastVideoDevice: MediaDeviceInfo | null, lastAudioDevice: MediaDeviceInfo | null, currentDevices: CurrentDevices | null) => boolean;
|
|
9
|
+
export declare const stopTracks: (requestedDevices: MediaStream) => void;
|
|
10
|
+
export declare const prepareDeviceState: (stream: MediaStream | null, track: MediaStreamTrack | null, devices: MediaDeviceInfo[], error: DeviceError | null, shouldAsk: boolean) => DeviceState;
|
|
11
|
+
export declare const getLocalStorageConfig: (deviceType: "audio" | "video") => StorageConfig;
|
|
12
|
+
//# sourceMappingURL=media.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/utils/media.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAiB,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIvG,eAAO,MAAM,oCAAoC,sBAC5B,OAAO,GAAG,qBAAqB,KACjD,OAAO,GAAG,qBAAqB,GAAG,SAKpC,CAAC;AAEF,eAAO,MAAM,UAAU,eAAe,CAAC;AACvC,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,eAAO,MAAM,aAAa,kBAAmB,MAAM,GAAG,IAAI,WAAW,eAAe,EAAE,KAAG,eAAe,GAAG,IACpB,CAAC;AAExF,eAAO,MAAM,yBAAyB,qBAClB,WAAW,oBACX,eAAe,EAAE,KAClC,cAcF,CAAC;AAEF,eAAO,MAAM,gCAAgC,eAC/B,eAAe,GAAG,IAAI,iBACnB,eAAe,GAAG,IAAI,mBAC2E,CAAC;AAEnH,eAAO,MAAM,mCAAmC,oBAC7B,eAAe,GAAG,IAAI,mBACtB,eAAe,GAAG,IAAI,kBACvB,cAAc,GAAG,IAAI,KACpC,OAMA,CAAC;AAEJ,eAAO,MAAM,UAAU,qBAAsB,WAAW,SAIvD,CAAC;AAQF,eAAO,MAAM,kBAAkB,WACrB,WAAW,GAAG,IAAI,SACnB,gBAAgB,GAAG,IAAI,WACrB,eAAe,EAAE,SACnB,WAAW,GAAG,IAAI,aACd,OAAO,KACjB,WAgBF,CAAC;AAEF,eAAO,MAAM,qBAAqB,eAAgB,OAAO,GAAG,OAAO,KAAG,aAMrE,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { loadObject, saveObject } from "./localStorage";
|
|
2
|
+
export const removeSpecifiedDeviceFromConstraints = (trackConstraints) => {
|
|
3
|
+
if (typeof trackConstraints === "object") {
|
|
4
|
+
return { ...trackConstraints, deviceId: undefined };
|
|
5
|
+
}
|
|
6
|
+
return trackConstraints;
|
|
7
|
+
};
|
|
8
|
+
export const REQUESTING = "Requesting";
|
|
9
|
+
export const NOT_REQUESTED = "Not requested";
|
|
10
|
+
export const getDeviceInfo = (trackDeviceId, devices) => (trackDeviceId && devices.find(({ deviceId }) => trackDeviceId === deviceId)) || null;
|
|
11
|
+
export const getCurrentDevicesSettings = (requestedDevices, mediaDeviceInfos) => {
|
|
12
|
+
const currentDevices = { videoinput: null, audioinput: null };
|
|
13
|
+
for (const track of requestedDevices.getTracks()) {
|
|
14
|
+
const settings = track.getSettings();
|
|
15
|
+
if (settings.deviceId) {
|
|
16
|
+
const currentDevice = mediaDeviceInfos.find((device) => device.deviceId == settings.deviceId);
|
|
17
|
+
const kind = currentDevice?.kind ?? null;
|
|
18
|
+
if ((currentDevice && kind === "videoinput") || kind === "audioinput") {
|
|
19
|
+
currentDevices[kind] = currentDevice ?? null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return currentDevices;
|
|
24
|
+
};
|
|
25
|
+
export const isDeviceDifferentFromLastSession = (lastDevice, currentDevice) => lastDevice && (currentDevice?.deviceId !== lastDevice.deviceId || currentDevice?.label !== lastDevice?.label);
|
|
26
|
+
export const isAnyDeviceDifferentFromLastSession = (lastVideoDevice, lastAudioDevice, currentDevices) => !!((currentDevices?.videoinput &&
|
|
27
|
+
isDeviceDifferentFromLastSession(lastVideoDevice, currentDevices?.videoinput || null)) ||
|
|
28
|
+
(currentDevices?.audioinput &&
|
|
29
|
+
isDeviceDifferentFromLastSession(lastAudioDevice, currentDevices?.audioinput || null)));
|
|
30
|
+
export const stopTracks = (requestedDevices) => {
|
|
31
|
+
for (const track of requestedDevices.getTracks()) {
|
|
32
|
+
track.stop();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const prepareStatus = (requested, track) => {
|
|
36
|
+
if (!requested)
|
|
37
|
+
return ["Not requested", null];
|
|
38
|
+
if (track)
|
|
39
|
+
return ["OK", null];
|
|
40
|
+
return ["Error", null];
|
|
41
|
+
};
|
|
42
|
+
export const prepareDeviceState = (stream, track, devices, error, shouldAsk) => {
|
|
43
|
+
const deviceInfo = getDeviceInfo(track?.getSettings()?.deviceId || null, devices);
|
|
44
|
+
const [devicesStatus, newError] = prepareStatus(shouldAsk, track);
|
|
45
|
+
return {
|
|
46
|
+
devices,
|
|
47
|
+
devicesStatus,
|
|
48
|
+
media: {
|
|
49
|
+
stream: track ? stream : null,
|
|
50
|
+
track: track,
|
|
51
|
+
deviceInfo,
|
|
52
|
+
enabled: !!track,
|
|
53
|
+
},
|
|
54
|
+
mediaStatus: devicesStatus,
|
|
55
|
+
error: newError ?? error,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const getLocalStorageConfig = (deviceType) => {
|
|
59
|
+
const key = `last-selected-${deviceType}-device`;
|
|
60
|
+
return {
|
|
61
|
+
getLastDevice: () => loadObject(key, null),
|
|
62
|
+
saveLastDevice: (info) => saveObject(key, info),
|
|
63
|
+
};
|
|
64
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fishjam-cloud/react-client",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "React client library for Fishjam Cloud",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Fishjam Cloud Team",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**"
|
|
12
|
+
],
|
|
13
|
+
"repository": "github:fishjam-cloud/web-client-sdk",
|
|
14
|
+
"homepage": "https://github.com/fishjam-cloud/web-client-sdk#readme",
|
|
15
|
+
"bugs": "https://github.com/fishjam-cloud/web-client-sdk/issues",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"webrtc",
|
|
18
|
+
"fishjam"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"typesVersions": {
|
|
27
|
+
"*": {
|
|
28
|
+
".": [
|
|
29
|
+
"./dist/index.d.ts"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"e2e": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test",
|
|
36
|
+
"docs": "typedoc src src/experimental",
|
|
37
|
+
"format": "prettier --write . --ignore-path ./.eslintignore",
|
|
38
|
+
"format:check": "prettier --check . --ignore-path ./.eslintignore",
|
|
39
|
+
"lint": "eslint . --ext .ts,.tsx --fix",
|
|
40
|
+
"lint:check": "eslint . --ext .ts,.tsx",
|
|
41
|
+
"prepack": "yarn workspace @fishjam-cloud/ts-client build && yarn build",
|
|
42
|
+
"typecheck": "tsc"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@playwright/test": "^1.45.3",
|
|
46
|
+
"@types/events": "^3.0.3",
|
|
47
|
+
"@types/lodash.isequal": "^4.5.8",
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"@types/react": "^18.2.55",
|
|
50
|
+
"react": "^18.2.0",
|
|
51
|
+
"typed-emitter": "^2.1.0",
|
|
52
|
+
"typedoc": "^0.26.5",
|
|
53
|
+
"typedoc-plugin-mdn-links": "^3.2.6",
|
|
54
|
+
"typescript": "^5.5.4"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@fishjam-cloud/ts-client": "0.5.0",
|
|
58
|
+
"events": "3.3.0",
|
|
59
|
+
"lodash.isequal": "4.5.0"
|
|
60
|
+
},
|
|
61
|
+
"directories": {
|
|
62
|
+
"example": "examples"
|
|
63
|
+
},
|
|
64
|
+
"packageManager": "yarn@4.3.0"
|
|
65
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Fishjam React Client
|
|
2
|
+
|
|
3
|
+
React client library for [Fishjam Cloud](https://cloud.fishjam.stream).
|
|
4
|
+
It is a wrapper around
|
|
5
|
+
the [TS client](../ts-client/README.md).
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
Documentation is available [here](https://fishjam-cloud.github.io/web-client-sdk/modules/_fishjam_dev_react_client.html or you can generate it locally:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn run docs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
You can install the library using `npm` or `yarn:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fishjam-cloud/react-client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
yarn add @fishjam-cloud/react-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It was tested with `nodejs` version mentioned in [`.tool-versions`](./.tool-versions) file.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
For pure TypeScript usage,
|
|
32
|
+
see [TS client](../ts-client/README.md).
|
|
33
|
+
|
|
34
|
+
Prerequisites:
|
|
35
|
+
|
|
36
|
+
- Account on Fishjam Cloud with App configured.
|
|
37
|
+
- Created room and token of peer in that room.
|
|
38
|
+
You can use Room Manager to create room and peer token.
|
|
39
|
+
|
|
40
|
+
This snippet is based
|
|
41
|
+
on [minimal-react](../../examples/react-client/minimal-react/) example.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// main.tsx
|
|
45
|
+
import React from "react";
|
|
46
|
+
import ReactDOM from "react-dom/client";
|
|
47
|
+
import { App, FishjamContextProvider } from "./components/App";
|
|
48
|
+
|
|
49
|
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
50
|
+
<React.StrictMode>
|
|
51
|
+
<FishjamContextProvider>
|
|
52
|
+
<App />
|
|
53
|
+
</FishjamContextProvider>
|
|
54
|
+
</React.StrictMode>,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// components/App.tsx
|
|
58
|
+
import VideoPlayer from "./VideoPlayer";
|
|
59
|
+
import { SCREEN_SHARING_MEDIA_CONSTRAINTS } from "@fishjam-cloud/react-client";
|
|
60
|
+
import { create } from "@fishjam-cloud/react-client";
|
|
61
|
+
import { useState } from "react";
|
|
62
|
+
|
|
63
|
+
// Example metadata types for peer and track
|
|
64
|
+
// You can define your own metadata types just make sure they are serializable
|
|
65
|
+
export type PeerMetadata = {
|
|
66
|
+
name: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type TrackMetadata = {
|
|
70
|
+
type: "camera" | "screen";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Create a Fishjam client instance
|
|
74
|
+
// remember to use FishjamContextProvider
|
|
75
|
+
export const { useApi, useTracks, useStatus, useConnect, useDisconnect, FishjamContextProvider } = create<
|
|
76
|
+
PeerMetadata,
|
|
77
|
+
TrackMetadata
|
|
78
|
+
>();
|
|
79
|
+
|
|
80
|
+
export const App = () => {
|
|
81
|
+
const [token, setToken] = useState("");
|
|
82
|
+
|
|
83
|
+
const connect = useConnect();
|
|
84
|
+
const disconnect = useDisconnect();
|
|
85
|
+
const api = useApi();
|
|
86
|
+
const status = useStatus();
|
|
87
|
+
const tracks = useTracks();
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<input value={token} onChange={(e) => setToken(() => e?.target?.value)} placeholder="token" />
|
|
92
|
+
<div>
|
|
93
|
+
<button
|
|
94
|
+
disabled={token === "" || status === "joined"}
|
|
95
|
+
onClick={() => {
|
|
96
|
+
if (!token || token === "") throw Error("Token is empty");
|
|
97
|
+
connect({
|
|
98
|
+
peerMetadata: { name: "John Doe" }, // example metadata
|
|
99
|
+
token: token,
|
|
100
|
+
});
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
Connect
|
|
104
|
+
</button>
|
|
105
|
+
<button
|
|
106
|
+
disabled={status !== "joined"}
|
|
107
|
+
onClick={() => {
|
|
108
|
+
disconnect();
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
Disconnect
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
disabled={status !== "joined"}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
// Get screen sharing MediaStream
|
|
117
|
+
navigator.mediaDevices.getDisplayMedia(SCREEN_SHARING_MEDIA_CONSTRAINTS).then((screenStream) => {
|
|
118
|
+
// Add local MediaStream to webrtc
|
|
119
|
+
screenStream.getTracks().forEach((track) => api.addTrack(track, { type: "screen" }));
|
|
120
|
+
});
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Start screen share
|
|
124
|
+
</button>
|
|
125
|
+
<span>Status: {status}</span>
|
|
126
|
+
</div>
|
|
127
|
+
{/* Render the remote tracks from other peers*/}
|
|
128
|
+
{Object.values(tracks).map(({ stream, trackId }) => (
|
|
129
|
+
<VideoPlayer key={trackId} stream={stream} /> // Simple component to render a video element
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Releasing new versions
|
|
137
|
+
|
|
138
|
+
To release a new version of the package, go to `Actions` > `Release package` workflow and trigger it with the chosen release type.
|
|
139
|
+
The workflow will bump the package version in `package.json`, release the package to NPM, create a new git tag and a GitHub release.
|
|
140
|
+
|
|
141
|
+
## Examples
|
|
142
|
+
|
|
143
|
+
For examples, see [examples](../../examples/react-client/) folder.
|
|
144
|
+
|
|
145
|
+
## Copyright and License
|
|
146
|
+
|
|
147
|
+
Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=react-client)
|
|
148
|
+
|
|
149
|
+
[](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=react-client)
|
|
150
|
+
|
|
151
|
+
Licensed under the [Apache License, Version 2.0](LICENSE)
|