@streamplace/components 0.0.1 → 0.7.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.
Files changed (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,557 @@
1
+ import Hls from "hls.js";
2
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
3
+ import {
4
+ IngestMediaSource,
5
+ PlayerProtocol,
6
+ PlayerStatus,
7
+ usePlayerStore,
8
+ useStreamplaceStore,
9
+ } from "../..";
10
+ import { borderRadius, colors, mt, p } from "../../lib/theme/atoms";
11
+ import { Text, View } from "../ui/index";
12
+ import { srcToUrl } from "./shared";
13
+ import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
14
+ import {
15
+ logWebRTCDiagnostics,
16
+ useWebRTCDiagnostics,
17
+ } from "./webrtc-diagnostics";
18
+ import { checkWebRTCSupport } from "./webrtc-primitives";
19
+
20
+ function assignVideoRef(
21
+ ref:
22
+ | React.MutableRefObject<HTMLVideoElement | null>
23
+ | ((instance: HTMLVideoElement | null) => void)
24
+ | null
25
+ | undefined,
26
+ instance: HTMLVideoElement | null,
27
+ ) {
28
+ if (!ref) return;
29
+ if (typeof ref === "function") ref(instance);
30
+ else ref.current = instance;
31
+ }
32
+
33
+ type VideoProps = {
34
+ url: string;
35
+ videoRef?: React.RefObject<HTMLVideoElement>;
36
+ };
37
+
38
+ function useVideoDimensions(videoRef: React.RefObject<HTMLVideoElement>) {
39
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
40
+
41
+ useEffect(() => {
42
+ if (!videoRef.current) return;
43
+
44
+ function updateSize() {
45
+ setDimensions({
46
+ width: videoRef.current?.videoWidth || 0,
47
+ height: videoRef.current?.videoHeight || 0,
48
+ });
49
+ }
50
+
51
+ updateSize();
52
+
53
+ const observer = new window.ResizeObserver(updateSize);
54
+ observer.observe(videoRef.current);
55
+
56
+ videoRef.current.addEventListener("loadedmetadata", updateSize);
57
+ videoRef.current.addEventListener("resize", updateSize);
58
+
59
+ return () => {
60
+ observer.disconnect();
61
+ videoRef.current?.removeEventListener("loadedmetadata", updateSize);
62
+ videoRef.current?.removeEventListener("resize", updateSize);
63
+ };
64
+ }, [videoRef, videoRef.current]);
65
+
66
+ return dimensions;
67
+ }
68
+
69
+ export default function WebVideo() {
70
+ const inProto = usePlayerStore((x) => x.protocol);
71
+ const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null);
72
+ const selectedRendition = usePlayerStore((x) => x.selectedRendition);
73
+ const src = usePlayerStore((x) => x.src);
74
+ const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
75
+ const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
76
+ const { url, protocol } = srcToUrl({ src: src, selectedRendition }, inProto);
77
+
78
+ const videoRef = useRef<HTMLVideoElement | null>(null);
79
+ const dimensions = useVideoDimensions(videoRef);
80
+
81
+ useEffect(() => {
82
+ if (videoRef.current) {
83
+ setPlayerWidth(dimensions.width);
84
+ setPlayerHeight(dimensions.height);
85
+ }
86
+ }, [dimensions, setPlayerWidth, setPlayerHeight]);
87
+
88
+ const playerProps = { url, videoRef };
89
+
90
+ return (
91
+ <>
92
+ {isIngesting ? (
93
+ <WebcamIngestPlayer {...playerProps} />
94
+ ) : protocol === PlayerProtocol.PROGRESSIVE_MP4 ? (
95
+ <ProgressiveMP4Player {...playerProps} />
96
+ ) : protocol === PlayerProtocol.PROGRESSIVE_WEBM ? (
97
+ <ProgressiveWebMPlayer {...playerProps} />
98
+ ) : protocol === PlayerProtocol.HLS ? (
99
+ <HLSPlayer {...playerProps} />
100
+ ) : protocol === PlayerProtocol.WEBRTC ? (
101
+ <WebRTCPlayer {...playerProps} />
102
+ ) : (
103
+ (() => {
104
+ throw new Error(`unknown playback protocol ${inProto}`);
105
+ })()
106
+ )}
107
+ </>
108
+ );
109
+ }
110
+
111
+ const updateEvents = {
112
+ playing: true,
113
+ waiting: true,
114
+ stalled: true,
115
+ pause: true,
116
+ suspend: true,
117
+ mute: true,
118
+ };
119
+
120
+ const VideoElement = forwardRef<
121
+ HTMLVideoElement,
122
+ VideoProps & { videoRef?: React.RefObject<HTMLVideoElement> }
123
+ >((props, ref) => {
124
+ const x = usePlayerStore((x) => x);
125
+ const url = useStreamplaceStore((x) => x.url);
126
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
127
+ const setMuted = usePlayerStore((x) => x.setMuted);
128
+ const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
129
+ const muted = usePlayerStore((x) => x.muted);
130
+ const ingest = usePlayerStore((x) => x.ingestConnectionState !== null);
131
+ const volume = usePlayerStore((x) => x.volume);
132
+ const setStatus = usePlayerStore((x) => x.setStatus);
133
+ const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
134
+ const setVideoRef = usePlayerStore((x) => x.setVideoRef);
135
+
136
+ const event = (evType) => (e) => {
137
+ console.log(evType);
138
+ const now = new Date();
139
+ if (updateEvents[evType]) {
140
+ x.setStatus(evType);
141
+ }
142
+ console.log("Sending", evType, "status to", url);
143
+ playerEvent(url, now.toISOString(), evType, {});
144
+ };
145
+ const [firstAttempt, setFirstAttempt] = useState(true);
146
+
147
+ const localVideoRef = props.videoRef ?? useRef<HTMLVideoElement | null>(null);
148
+
149
+ const canPlayThrough = (e) => {
150
+ event("canplaythrough")(e);
151
+ if (firstAttempt && localVideoRef.current) {
152
+ setFirstAttempt(false);
153
+ localVideoRef.current.play().catch((err) => {
154
+ if (err.name === "NotAllowedError") {
155
+ if (localVideoRef.current) {
156
+ setMuted(true);
157
+ localVideoRef.current.muted = true;
158
+ localVideoRef.current
159
+ .play()
160
+ .then(() => {
161
+ console.warn("Browser forced video to start muted");
162
+ setMuteWasForced(true);
163
+ })
164
+ .catch((err) => {
165
+ console.error("error playing video", err);
166
+ });
167
+ }
168
+ }
169
+ });
170
+ }
171
+ };
172
+
173
+ useEffect(() => {
174
+ return () => {
175
+ setStatus(PlayerStatus.START);
176
+ };
177
+ }, []);
178
+
179
+ useEffect(() => {
180
+ if (localVideoRef.current) {
181
+ localVideoRef.current.volume = volume;
182
+ console.log("Setting volume to", volume);
183
+ }
184
+ }, [volume]);
185
+
186
+ useEffect(() => {
187
+ console.log(localVideoRef.current?.width, localVideoRef.current?.height);
188
+ setVideoRef(localVideoRef);
189
+ }, [setVideoRef, localVideoRef]);
190
+
191
+ const handleVideoRef = (videoElement: HTMLVideoElement | null) => {
192
+ if (typeof ref === "function") {
193
+ ref(videoElement);
194
+ } else if (ref) {
195
+ (ref as React.MutableRefObject<HTMLVideoElement | null>).current =
196
+ videoElement;
197
+ }
198
+ // if (localVideoRef && typeof localVideoRef !== "function") {
199
+ // localVideoRef.current = videoElement;
200
+ // }
201
+ };
202
+
203
+ return (
204
+ <video
205
+ autoPlay={true}
206
+ playsInline={true}
207
+ ref={handleVideoRef}
208
+ controls={false}
209
+ src={ingest ? undefined : props.url}
210
+ muted={muted}
211
+ crossOrigin="anonymous"
212
+ onMouseMove={setUserInteraction}
213
+ onClick={setUserInteraction}
214
+ onAbort={event("abort")}
215
+ onCanPlay={event("canplay")}
216
+ onCanPlayThrough={canPlayThrough}
217
+ onEmptied={event("emptied")}
218
+ onEncrypted={event("encrypted")}
219
+ onEnded={event("ended")}
220
+ onError={event("error")}
221
+ onLoadedData={event("loadeddata")}
222
+ onLoadedMetadata={event("loadedmetadata")}
223
+ onLoadStart={event("loadstart")}
224
+ onPause={event("pause")}
225
+ onPlay={event("play")}
226
+ onPlaying={event("playing")}
227
+ onRateChange={event("ratechange")}
228
+ onSeeked={event("seeked")}
229
+ onSeeking={event("seeking")}
230
+ onStalled={event("stalled")}
231
+ onSuspend={event("suspend")}
232
+ onVolumeChange={event("volumechange")}
233
+ onWaiting={event("waiting")}
234
+ style={{
235
+ objectFit: "contain",
236
+ backgroundColor: "transparent",
237
+ width: "100%",
238
+ height: "100%",
239
+ transform: ingest ? "scaleX(-1)" : undefined,
240
+ }}
241
+ />
242
+ );
243
+ });
244
+
245
+ export function ProgressiveMP4Player(props: VideoProps) {
246
+ return <VideoElement {...props} />;
247
+ }
248
+
249
+ export function ProgressiveWebMPlayer(props: VideoProps) {
250
+ return <VideoElement {...props} />;
251
+ }
252
+
253
+ export function HLSPlayer(props: VideoProps) {
254
+ const localRef = useRef<HTMLVideoElement | null>(null);
255
+
256
+ useEffect(() => {
257
+ if (!localRef.current) {
258
+ return;
259
+ }
260
+ if (Hls.isSupported()) {
261
+ var hls = new Hls({ maxAudioFramesDrift: 20 });
262
+ hls.loadSource(props.url);
263
+ try {
264
+ hls.attachMedia(localRef.current);
265
+ } catch (e) {
266
+ console.error("error on attachMedia");
267
+ hls.stopLoad();
268
+ return;
269
+ }
270
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
271
+ if (!localRef.current) {
272
+ return;
273
+ }
274
+ localRef.current.play();
275
+ });
276
+ return () => {
277
+ hls.stopLoad();
278
+ };
279
+ } else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) {
280
+ localRef.current.src = props.url;
281
+ localRef.current.addEventListener("canplay", () => {
282
+ if (!localRef.current) {
283
+ return;
284
+ }
285
+ localRef.current.play();
286
+ });
287
+ }
288
+ }, [props.url]);
289
+ return <VideoElement {...props} ref={localRef} />;
290
+ }
291
+
292
+ export function WebRTCPlayer(
293
+ props: VideoProps & { videoRef?: React.RefObject<HTMLVideoElement> },
294
+ ) {
295
+ const [webrtcError, setWebrtcError] = useState<string | null>(null);
296
+ const setStatus = usePlayerStore((x) => x.setStatus);
297
+ const setProtocol = usePlayerStore((x) => x.setProtocol);
298
+ const diagnostics = useWebRTCDiagnostics();
299
+ // Check WebRTC compatibility on component mount
300
+ useEffect(() => {
301
+ try {
302
+ checkWebRTCSupport();
303
+ console.log("WebRTC Player - Browser compatibility check passed");
304
+ logWebRTCDiagnostics();
305
+ } catch (error) {
306
+ console.error("WebRTC Player - Compatibility error:", error.message);
307
+ setWebrtcError(error.message);
308
+ setStatus(PlayerStatus.START);
309
+ return;
310
+ }
311
+ }, []);
312
+
313
+ // Monitor diagnostics for errors
314
+ useEffect(() => {
315
+ if (!diagnostics.browserSupport && diagnostics.errors.length > 0) {
316
+ setWebrtcError(diagnostics.errors.join(", "));
317
+ }
318
+ }, [diagnostics]);
319
+
320
+ if (!diagnostics.done) return <></>;
321
+
322
+ if (webrtcError) {
323
+ setProtocol(PlayerProtocol.HLS);
324
+ return (
325
+ <View backgroundColor="#111">
326
+ <View>
327
+ <View>
328
+ <Text>WebRTC Not Supported</Text>
329
+ </View>
330
+ <Text>{webrtcError}</Text>
331
+ {diagnostics.errors.length > 0 && (
332
+ <View>
333
+ <Text>Technical Details:</Text>
334
+ {diagnostics.errors.map((error, index) => (
335
+ <Text key={index}>• {error}</Text>
336
+ ))}
337
+ </View>
338
+ )}
339
+ <Text>
340
+ • To use WebRTC, you may need to disable any blocking extensions or
341
+ update your browser.
342
+ </Text>
343
+ <Text style={[mt[2]]}>Switching to HLS...</Text>
344
+ </View>
345
+ </View>
346
+ );
347
+ }
348
+ return <WebRTCPlayerInner url={props.url} videoRef={props.videoRef} />;
349
+ }
350
+
351
+ export function WebRTCPlayerInner({
352
+ videoRef,
353
+ url,
354
+ width,
355
+ height,
356
+ }: {
357
+ videoRef?: React.RefObject<HTMLVideoElement>;
358
+ url: string;
359
+ width?: string | number;
360
+ height?: string | number;
361
+ }) {
362
+ const [connectionStatus, setConnectionStatus] =
363
+ useState<string>("initializing");
364
+
365
+ const status = usePlayerStore((x) => x.status);
366
+ const setStatus = usePlayerStore((x) => x.setStatus);
367
+
368
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
369
+ const spurl = useStreamplaceStore((x) => x.url);
370
+
371
+ const [mediaStream, stuck] = useWebRTC(url);
372
+
373
+ useEffect(() => {
374
+ if (stuck) {
375
+ setConnectionStatus("connection-failed");
376
+ } else if (mediaStream) {
377
+ setConnectionStatus("connected");
378
+ } else {
379
+ setConnectionStatus("connecting");
380
+ }
381
+ }, [url, mediaStream, stuck, status]);
382
+
383
+ useEffect(() => {
384
+ if (stuck && status === PlayerStatus.PLAYING) {
385
+ setStatus(PlayerStatus.STALLED);
386
+ }
387
+ if (!stuck && mediaStream) {
388
+ setStatus(PlayerStatus.PLAYING);
389
+ }
390
+ }, [stuck, status, mediaStream]);
391
+
392
+ useEffect(() => {
393
+ if (!mediaStream) {
394
+ return;
395
+ }
396
+ const evt = (evType) => (e) => {
397
+ console.log("webrtc event", evType);
398
+ playerEvent(spurl, new Date().toISOString(), evType, {});
399
+ };
400
+ const active = evt("active");
401
+ const inactive = evt("inactive");
402
+ const ended = evt("ended");
403
+ const mute = evt("mute");
404
+ const unmute = evt("playing");
405
+
406
+ mediaStream.addEventListener("active", active);
407
+ mediaStream.addEventListener("inactive", inactive);
408
+ mediaStream.addEventListener("ended", ended);
409
+ for (const track of mediaStream.getTracks()) {
410
+ track.addEventListener("ended", ended);
411
+ track.addEventListener("mute", mute);
412
+ track.addEventListener("unmute", unmute);
413
+ }
414
+ return () => {
415
+ for (const track of mediaStream.getTracks()) {
416
+ track.removeEventListener("ended", ended);
417
+ track.removeEventListener("mute", mute);
418
+ track.removeEventListener("unmute", unmute);
419
+ }
420
+ mediaStream.removeEventListener("active", active);
421
+ mediaStream.removeEventListener("inactive", inactive);
422
+ mediaStream.removeEventListener("ended", ended);
423
+ };
424
+ }, [mediaStream]);
425
+
426
+ useEffect(() => {
427
+ if (!videoRef || !videoRef.current) {
428
+ return;
429
+ }
430
+ videoRef.current.srcObject = mediaStream;
431
+ }, [mediaStream]);
432
+
433
+ if (!mediaStream) {
434
+ return (
435
+ <View
436
+ backgroundColor="#111"
437
+ style={{ minWidth: "100%", minHeight: "100%" }}
438
+ >
439
+ <View
440
+ backgroundColor={colors.primary[800]}
441
+ style={{ borderRadius: borderRadius.md }}
442
+ >
443
+ <View>
444
+ <Text>Connecting...</Text>
445
+ </View>
446
+ <Text>Establishing WebRTC connection ({connectionStatus})</Text>
447
+ </View>
448
+ </View>
449
+ );
450
+ }
451
+ return <VideoElement url={url} ref={videoRef} />;
452
+ }
453
+
454
+ export function WebcamIngestPlayer(props: VideoProps) {
455
+ const ingestStarting = usePlayerStore((x) => x.ingestStarting);
456
+ const ingestMediaSource = usePlayerStore((x) => x.ingestMediaSource);
457
+ const ingestAutoStart = usePlayerStore((x) => x.ingestAutoStart);
458
+
459
+ const [error, setError] = useState<Error | null>(null);
460
+
461
+ let streamKey = null;
462
+
463
+ const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
464
+ null,
465
+ );
466
+ const handleRef = useCallback((node: HTMLVideoElement | null) => {
467
+ if (node) {
468
+ setVideoElement(node);
469
+ }
470
+ }, []);
471
+
472
+ const url = useStreamplaceStore((x) => x.url);
473
+ const [localMediaStream, setLocalMediaStream] = useState<MediaStream | null>(
474
+ null,
475
+ );
476
+ // we assign a stream key in the webrtcingest hook
477
+ const [remoteMediaStream, setRemoteMediaStream] = useWebRTCIngest({
478
+ endpoint: `${url}/api/ingest/webrtc`,
479
+ });
480
+
481
+ useEffect(() => {
482
+ if (ingestMediaSource === IngestMediaSource.DISPLAY) {
483
+ navigator.mediaDevices
484
+ .getDisplayMedia({
485
+ audio: true,
486
+ video: true,
487
+ })
488
+ .then((stream) => {
489
+ setLocalMediaStream(stream);
490
+ })
491
+ .catch((e) => {
492
+ console.error("error getting display media", e);
493
+ });
494
+ } else {
495
+ navigator.mediaDevices
496
+ .getUserMedia({
497
+ audio: true,
498
+ video: {
499
+ width: { min: 200, ideal: 1080, max: 2160 },
500
+ height: { min: 200, ideal: 1920, max: 3840 },
501
+ },
502
+ })
503
+ .then((stream) => {
504
+ setLocalMediaStream(stream);
505
+ })
506
+ .catch((e) => {
507
+ console.error("error getting user media", e.name);
508
+ if (e.name == "NotAllowedError") {
509
+ setError(
510
+ new Error(
511
+ "Unable to access video! Please allow it in your browser settings.",
512
+ ),
513
+ );
514
+ }
515
+ });
516
+ }
517
+ }, [ingestMediaSource]);
518
+
519
+ useEffect(() => {
520
+ if (!ingestStarting && !ingestAutoStart) {
521
+ setRemoteMediaStream(null);
522
+ return;
523
+ }
524
+ if (!localMediaStream) {
525
+ return;
526
+ }
527
+ setRemoteMediaStream(localMediaStream);
528
+ }, [localMediaStream, ingestStarting, ingestAutoStart]);
529
+
530
+ useEffect(() => {
531
+ if (!videoElement) {
532
+ return;
533
+ }
534
+ if (!localMediaStream) {
535
+ return;
536
+ }
537
+ videoElement.srcObject = localMediaStream;
538
+ }, [videoElement, localMediaStream]);
539
+
540
+ if (error) {
541
+ return (
542
+ <View
543
+ backgroundColor={colors.destructive[900]}
544
+ style={[p[4], { borderRadius: borderRadius.md }]}
545
+ >
546
+ <View>
547
+ <Text size="xl" weight="extrabold">
548
+ Error encountered!
549
+ </Text>
550
+ </View>
551
+ <Text>{error.message}</Text>
552
+ </View>
553
+ );
554
+ }
555
+
556
+ return <VideoElement {...props} ref={handleRef} />;
557
+ }
@@ -0,0 +1,149 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export interface WebRTCDiagnostics {
4
+ done: boolean;
5
+ browserSupport: boolean;
6
+ rtcPeerConnection: boolean;
7
+ rtcSessionDescription: boolean;
8
+ getUserMedia: boolean;
9
+ getDisplayMedia: boolean;
10
+ errors: string[];
11
+ warnings: string[];
12
+ }
13
+
14
+ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
15
+ const [diagnostics, setDiagnostics] = useState<WebRTCDiagnostics>({
16
+ done: false,
17
+ browserSupport: false,
18
+ rtcPeerConnection: false,
19
+ rtcSessionDescription: false,
20
+ getUserMedia: false,
21
+ getDisplayMedia: false,
22
+ errors: [],
23
+ warnings: [],
24
+ });
25
+
26
+ useEffect(() => {
27
+ const errors: string[] = [];
28
+ const warnings: string[] = [];
29
+
30
+ // Check if we're in a browser environment
31
+ if (typeof window === "undefined") {
32
+ errors.push("Running in non-browser environment");
33
+ setDiagnostics({
34
+ done: false,
35
+ browserSupport: false,
36
+ rtcPeerConnection: false,
37
+ rtcSessionDescription: false,
38
+ getUserMedia: false,
39
+ getDisplayMedia: false,
40
+ errors,
41
+ warnings,
42
+ });
43
+ return;
44
+ }
45
+
46
+ // Check RTCPeerConnection support
47
+ const rtcPeerConnection = !!(
48
+ window.RTCPeerConnection ||
49
+ (window as any).webkitRTCPeerConnection ||
50
+ (window as any).mozRTCPeerConnection
51
+ );
52
+
53
+ if (!rtcPeerConnection) {
54
+ errors.push("RTCPeerConnection is not supported");
55
+ }
56
+
57
+ // Check RTCSessionDescription support
58
+ const rtcSessionDescription = !!(
59
+ window.RTCSessionDescription ||
60
+ (window as any).webkitRTCSessionDescription ||
61
+ (window as any).mozRTCSessionDescription
62
+ );
63
+
64
+ if (!rtcSessionDescription) {
65
+ errors.push("RTCSessionDescription is not supported");
66
+ }
67
+
68
+ // Check getUserMedia support
69
+ const getUserMedia = !!(
70
+ navigator.mediaDevices?.getUserMedia ||
71
+ (navigator as any).getUserMedia ||
72
+ (navigator as any).webkitGetUserMedia ||
73
+ (navigator as any).mozGetUserMedia
74
+ );
75
+
76
+ if (!getUserMedia) {
77
+ warnings.push(
78
+ "getUserMedia is not supported - webcam features unavailable",
79
+ );
80
+ }
81
+
82
+ // Check getDisplayMedia support
83
+ const getDisplayMedia = !!navigator.mediaDevices?.getDisplayMedia;
84
+
85
+ if (!getDisplayMedia) {
86
+ warnings.push(
87
+ "getDisplayMedia is not supported - screen sharing unavailable",
88
+ );
89
+ }
90
+
91
+ // Check if running over HTTPS (required for some WebRTC features)
92
+ if (
93
+ location.protocol !== "https:" &&
94
+ location.hostname !== "localhost" &&
95
+ location.hostname !== "127.0.0.1"
96
+ ) {
97
+ warnings.push("WebRTC features may be limited over HTTP connections");
98
+ }
99
+
100
+ // Check browser-specific issues
101
+ const userAgent = navigator.userAgent.toLowerCase();
102
+ if (userAgent.includes("safari") && !userAgent.includes("chrome")) {
103
+ warnings.push("Safari may have limited WebRTC codec support");
104
+ }
105
+
106
+ const browserSupport = rtcPeerConnection && rtcSessionDescription;
107
+
108
+ setDiagnostics({
109
+ done: true,
110
+ browserSupport,
111
+ rtcPeerConnection,
112
+ rtcSessionDescription,
113
+ getUserMedia,
114
+ getDisplayMedia,
115
+ errors,
116
+ warnings,
117
+ });
118
+ }, []);
119
+
120
+ return diagnostics;
121
+ }
122
+
123
+ export function logWebRTCDiagnostics() {
124
+ console.group("WebRTC Diagnostics");
125
+
126
+ // Log browser support
127
+ console.log("RTCPeerConnection:", !!window.RTCPeerConnection);
128
+ console.log("RTCSessionDescription:", !!window.RTCSessionDescription);
129
+ console.log("getUserMedia:", !!navigator.mediaDevices?.getUserMedia);
130
+ console.log("getDisplayMedia:", !!navigator.mediaDevices?.getDisplayMedia);
131
+
132
+ // Log browser info
133
+ console.log("User Agent:", navigator.userAgent);
134
+ console.log("Protocol:", location.protocol);
135
+ console.log("Host:", location.hostname);
136
+
137
+ // Test basic WebRTC functionality
138
+ if (window.RTCPeerConnection) {
139
+ try {
140
+ const pc = new RTCPeerConnection();
141
+ console.log("RTCPeerConnection creation: ✓ Success");
142
+ pc.close();
143
+ } catch (error) {
144
+ console.error("RTCPeerConnection creation: ✗ Failed", error);
145
+ }
146
+ }
147
+
148
+ console.groupEnd();
149
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ RTCPeerConnection,
3
+ RTCSessionDescription,
4
+ MediaStream as WebRTCMediaStream,
5
+ mediaDevices,
6
+ } from "react-native-webrtc";