@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,375 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Hls from "hls.js";
3
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
4
+ import { IngestMediaSource, PlayerProtocol, PlayerStatus, usePlayerStore, useStreamplaceStore, } from "../..";
5
+ import { borderRadius, colors, mt, p } from "../../lib/theme/atoms";
6
+ import { Text, View } from "../ui/index";
7
+ import { srcToUrl } from "./shared";
8
+ import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
9
+ import { logWebRTCDiagnostics, useWebRTCDiagnostics, } from "./webrtc-diagnostics";
10
+ import { checkWebRTCSupport } from "./webrtc-primitives";
11
+ function assignVideoRef(ref, instance) {
12
+ if (!ref)
13
+ return;
14
+ if (typeof ref === "function")
15
+ ref(instance);
16
+ else
17
+ ref.current = instance;
18
+ }
19
+ function useVideoDimensions(videoRef) {
20
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
21
+ useEffect(() => {
22
+ if (!videoRef.current)
23
+ return;
24
+ function updateSize() {
25
+ setDimensions({
26
+ width: videoRef.current?.videoWidth || 0,
27
+ height: videoRef.current?.videoHeight || 0,
28
+ });
29
+ }
30
+ updateSize();
31
+ const observer = new window.ResizeObserver(updateSize);
32
+ observer.observe(videoRef.current);
33
+ videoRef.current.addEventListener("loadedmetadata", updateSize);
34
+ videoRef.current.addEventListener("resize", updateSize);
35
+ return () => {
36
+ observer.disconnect();
37
+ videoRef.current?.removeEventListener("loadedmetadata", updateSize);
38
+ videoRef.current?.removeEventListener("resize", updateSize);
39
+ };
40
+ }, [videoRef, videoRef.current]);
41
+ return dimensions;
42
+ }
43
+ export default function WebVideo() {
44
+ const inProto = usePlayerStore((x) => x.protocol);
45
+ const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null);
46
+ const selectedRendition = usePlayerStore((x) => x.selectedRendition);
47
+ const src = usePlayerStore((x) => x.src);
48
+ const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
49
+ const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
50
+ const { url, protocol } = srcToUrl({ src: src, selectedRendition }, inProto);
51
+ const videoRef = useRef(null);
52
+ const dimensions = useVideoDimensions(videoRef);
53
+ useEffect(() => {
54
+ if (videoRef.current) {
55
+ setPlayerWidth(dimensions.width);
56
+ setPlayerHeight(dimensions.height);
57
+ }
58
+ }, [dimensions, setPlayerWidth, setPlayerHeight]);
59
+ const playerProps = { url, videoRef };
60
+ return (_jsx(_Fragment, { children: isIngesting ? (_jsx(WebcamIngestPlayer, { ...playerProps })) : protocol === PlayerProtocol.PROGRESSIVE_MP4 ? (_jsx(ProgressiveMP4Player, { ...playerProps })) : protocol === PlayerProtocol.PROGRESSIVE_WEBM ? (_jsx(ProgressiveWebMPlayer, { ...playerProps })) : protocol === PlayerProtocol.HLS ? (_jsx(HLSPlayer, { ...playerProps })) : protocol === PlayerProtocol.WEBRTC ? (_jsx(WebRTCPlayer, { ...playerProps })) : ((() => {
61
+ throw new Error(`unknown playback protocol ${inProto}`);
62
+ })()) }));
63
+ }
64
+ const updateEvents = {
65
+ playing: true,
66
+ waiting: true,
67
+ stalled: true,
68
+ pause: true,
69
+ suspend: true,
70
+ mute: true,
71
+ };
72
+ const VideoElement = forwardRef((props, ref) => {
73
+ const x = usePlayerStore((x) => x);
74
+ const url = useStreamplaceStore((x) => x.url);
75
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
76
+ const setMuted = usePlayerStore((x) => x.setMuted);
77
+ const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
78
+ const muted = usePlayerStore((x) => x.muted);
79
+ const ingest = usePlayerStore((x) => x.ingestConnectionState !== null);
80
+ const volume = usePlayerStore((x) => x.volume);
81
+ const setStatus = usePlayerStore((x) => x.setStatus);
82
+ const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
83
+ const setVideoRef = usePlayerStore((x) => x.setVideoRef);
84
+ const event = (evType) => (e) => {
85
+ console.log(evType);
86
+ const now = new Date();
87
+ if (updateEvents[evType]) {
88
+ x.setStatus(evType);
89
+ }
90
+ console.log("Sending", evType, "status to", url);
91
+ playerEvent(url, now.toISOString(), evType, {});
92
+ };
93
+ const [firstAttempt, setFirstAttempt] = useState(true);
94
+ const localVideoRef = props.videoRef ?? useRef(null);
95
+ const canPlayThrough = (e) => {
96
+ event("canplaythrough")(e);
97
+ if (firstAttempt && localVideoRef.current) {
98
+ setFirstAttempt(false);
99
+ localVideoRef.current.play().catch((err) => {
100
+ if (err.name === "NotAllowedError") {
101
+ if (localVideoRef.current) {
102
+ setMuted(true);
103
+ localVideoRef.current.muted = true;
104
+ localVideoRef.current
105
+ .play()
106
+ .then(() => {
107
+ console.warn("Browser forced video to start muted");
108
+ setMuteWasForced(true);
109
+ })
110
+ .catch((err) => {
111
+ console.error("error playing video", err);
112
+ });
113
+ }
114
+ }
115
+ });
116
+ }
117
+ };
118
+ useEffect(() => {
119
+ return () => {
120
+ setStatus(PlayerStatus.START);
121
+ };
122
+ }, []);
123
+ useEffect(() => {
124
+ if (localVideoRef.current) {
125
+ localVideoRef.current.volume = volume;
126
+ console.log("Setting volume to", volume);
127
+ }
128
+ }, [volume]);
129
+ useEffect(() => {
130
+ console.log(localVideoRef.current?.width, localVideoRef.current?.height);
131
+ setVideoRef(localVideoRef);
132
+ }, [setVideoRef, localVideoRef]);
133
+ const handleVideoRef = (videoElement) => {
134
+ if (typeof ref === "function") {
135
+ ref(videoElement);
136
+ }
137
+ else if (ref) {
138
+ ref.current =
139
+ videoElement;
140
+ }
141
+ // if (localVideoRef && typeof localVideoRef !== "function") {
142
+ // localVideoRef.current = videoElement;
143
+ // }
144
+ };
145
+ return (_jsx("video", { autoPlay: true, playsInline: true, ref: handleVideoRef, controls: false, src: ingest ? undefined : props.url, muted: muted, crossOrigin: "anonymous", onMouseMove: setUserInteraction, onClick: setUserInteraction, onAbort: event("abort"), onCanPlay: event("canplay"), onCanPlayThrough: canPlayThrough, onEmptied: event("emptied"), onEncrypted: event("encrypted"), onEnded: event("ended"), onError: event("error"), onLoadedData: event("loadeddata"), onLoadedMetadata: event("loadedmetadata"), onLoadStart: event("loadstart"), onPause: event("pause"), onPlay: event("play"), onPlaying: event("playing"), onRateChange: event("ratechange"), onSeeked: event("seeked"), onSeeking: event("seeking"), onStalled: event("stalled"), onSuspend: event("suspend"), onVolumeChange: event("volumechange"), onWaiting: event("waiting"), style: {
146
+ objectFit: "contain",
147
+ backgroundColor: "transparent",
148
+ width: "100%",
149
+ height: "100%",
150
+ transform: ingest ? "scaleX(-1)" : undefined,
151
+ } }));
152
+ });
153
+ export function ProgressiveMP4Player(props) {
154
+ return _jsx(VideoElement, { ...props });
155
+ }
156
+ export function ProgressiveWebMPlayer(props) {
157
+ return _jsx(VideoElement, { ...props });
158
+ }
159
+ export function HLSPlayer(props) {
160
+ const localRef = useRef(null);
161
+ useEffect(() => {
162
+ if (!localRef.current) {
163
+ return;
164
+ }
165
+ if (Hls.isSupported()) {
166
+ var hls = new Hls({ maxAudioFramesDrift: 20 });
167
+ hls.loadSource(props.url);
168
+ try {
169
+ hls.attachMedia(localRef.current);
170
+ }
171
+ catch (e) {
172
+ console.error("error on attachMedia");
173
+ hls.stopLoad();
174
+ return;
175
+ }
176
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
177
+ if (!localRef.current) {
178
+ return;
179
+ }
180
+ localRef.current.play();
181
+ });
182
+ return () => {
183
+ hls.stopLoad();
184
+ };
185
+ }
186
+ else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) {
187
+ localRef.current.src = props.url;
188
+ localRef.current.addEventListener("canplay", () => {
189
+ if (!localRef.current) {
190
+ return;
191
+ }
192
+ localRef.current.play();
193
+ });
194
+ }
195
+ }, [props.url]);
196
+ return _jsx(VideoElement, { ...props, ref: localRef });
197
+ }
198
+ export function WebRTCPlayer(props) {
199
+ const [webrtcError, setWebrtcError] = useState(null);
200
+ const setStatus = usePlayerStore((x) => x.setStatus);
201
+ const setProtocol = usePlayerStore((x) => x.setProtocol);
202
+ const diagnostics = useWebRTCDiagnostics();
203
+ // Check WebRTC compatibility on component mount
204
+ useEffect(() => {
205
+ try {
206
+ checkWebRTCSupport();
207
+ console.log("WebRTC Player - Browser compatibility check passed");
208
+ logWebRTCDiagnostics();
209
+ }
210
+ catch (error) {
211
+ console.error("WebRTC Player - Compatibility error:", error.message);
212
+ setWebrtcError(error.message);
213
+ setStatus(PlayerStatus.START);
214
+ return;
215
+ }
216
+ }, []);
217
+ // Monitor diagnostics for errors
218
+ useEffect(() => {
219
+ if (!diagnostics.browserSupport && diagnostics.errors.length > 0) {
220
+ setWebrtcError(diagnostics.errors.join(", "));
221
+ }
222
+ }, [diagnostics]);
223
+ if (!diagnostics.done)
224
+ return _jsx(_Fragment, {});
225
+ if (webrtcError) {
226
+ setProtocol(PlayerProtocol.HLS);
227
+ return (_jsx(View, { backgroundColor: "#111", children: _jsxs(View, { children: [_jsx(View, { children: _jsx(Text, { children: "WebRTC Not Supported" }) }), _jsx(Text, { children: webrtcError }), diagnostics.errors.length > 0 && (_jsxs(View, { children: [_jsx(Text, { children: "Technical Details:" }), diagnostics.errors.map((error, index) => (_jsxs(Text, { children: ["\u2022 ", error] }, index)))] })), _jsx(Text, { children: "\u2022 To use WebRTC, you may need to disable any blocking extensions or update your browser." }), _jsx(Text, { style: [mt[2]], children: "Switching to HLS..." })] }) }));
228
+ }
229
+ return _jsx(WebRTCPlayerInner, { url: props.url, videoRef: props.videoRef });
230
+ }
231
+ export function WebRTCPlayerInner({ videoRef, url, width, height, }) {
232
+ const [connectionStatus, setConnectionStatus] = useState("initializing");
233
+ const status = usePlayerStore((x) => x.status);
234
+ const setStatus = usePlayerStore((x) => x.setStatus);
235
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
236
+ const spurl = useStreamplaceStore((x) => x.url);
237
+ const [mediaStream, stuck] = useWebRTC(url);
238
+ useEffect(() => {
239
+ if (stuck) {
240
+ setConnectionStatus("connection-failed");
241
+ }
242
+ else if (mediaStream) {
243
+ setConnectionStatus("connected");
244
+ }
245
+ else {
246
+ setConnectionStatus("connecting");
247
+ }
248
+ }, [url, mediaStream, stuck, status]);
249
+ useEffect(() => {
250
+ if (stuck && status === PlayerStatus.PLAYING) {
251
+ setStatus(PlayerStatus.STALLED);
252
+ }
253
+ if (!stuck && mediaStream) {
254
+ setStatus(PlayerStatus.PLAYING);
255
+ }
256
+ }, [stuck, status, mediaStream]);
257
+ useEffect(() => {
258
+ if (!mediaStream) {
259
+ return;
260
+ }
261
+ const evt = (evType) => (e) => {
262
+ console.log("webrtc event", evType);
263
+ playerEvent(spurl, new Date().toISOString(), evType, {});
264
+ };
265
+ const active = evt("active");
266
+ const inactive = evt("inactive");
267
+ const ended = evt("ended");
268
+ const mute = evt("mute");
269
+ const unmute = evt("playing");
270
+ mediaStream.addEventListener("active", active);
271
+ mediaStream.addEventListener("inactive", inactive);
272
+ mediaStream.addEventListener("ended", ended);
273
+ for (const track of mediaStream.getTracks()) {
274
+ track.addEventListener("ended", ended);
275
+ track.addEventListener("mute", mute);
276
+ track.addEventListener("unmute", unmute);
277
+ }
278
+ return () => {
279
+ for (const track of mediaStream.getTracks()) {
280
+ track.removeEventListener("ended", ended);
281
+ track.removeEventListener("mute", mute);
282
+ track.removeEventListener("unmute", unmute);
283
+ }
284
+ mediaStream.removeEventListener("active", active);
285
+ mediaStream.removeEventListener("inactive", inactive);
286
+ mediaStream.removeEventListener("ended", ended);
287
+ };
288
+ }, [mediaStream]);
289
+ useEffect(() => {
290
+ if (!videoRef || !videoRef.current) {
291
+ return;
292
+ }
293
+ videoRef.current.srcObject = mediaStream;
294
+ }, [mediaStream]);
295
+ if (!mediaStream) {
296
+ return (_jsx(View, { backgroundColor: "#111", style: { minWidth: "100%", minHeight: "100%" }, children: _jsxs(View, { backgroundColor: colors.primary[800], style: { borderRadius: borderRadius.md }, children: [_jsx(View, { children: _jsx(Text, { children: "Connecting..." }) }), _jsxs(Text, { children: ["Establishing WebRTC connection (", connectionStatus, ")"] })] }) }));
297
+ }
298
+ return _jsx(VideoElement, { url: url, ref: videoRef });
299
+ }
300
+ export function WebcamIngestPlayer(props) {
301
+ const ingestStarting = usePlayerStore((x) => x.ingestStarting);
302
+ const ingestMediaSource = usePlayerStore((x) => x.ingestMediaSource);
303
+ const ingestAutoStart = usePlayerStore((x) => x.ingestAutoStart);
304
+ const [error, setError] = useState(null);
305
+ let streamKey = null;
306
+ const [videoElement, setVideoElement] = useState(null);
307
+ const handleRef = useCallback((node) => {
308
+ if (node) {
309
+ setVideoElement(node);
310
+ }
311
+ }, []);
312
+ const url = useStreamplaceStore((x) => x.url);
313
+ const [localMediaStream, setLocalMediaStream] = useState(null);
314
+ // we assign a stream key in the webrtcingest hook
315
+ const [remoteMediaStream, setRemoteMediaStream] = useWebRTCIngest({
316
+ endpoint: `${url}/api/ingest/webrtc`,
317
+ });
318
+ useEffect(() => {
319
+ if (ingestMediaSource === IngestMediaSource.DISPLAY) {
320
+ navigator.mediaDevices
321
+ .getDisplayMedia({
322
+ audio: true,
323
+ video: true,
324
+ })
325
+ .then((stream) => {
326
+ setLocalMediaStream(stream);
327
+ })
328
+ .catch((e) => {
329
+ console.error("error getting display media", e);
330
+ });
331
+ }
332
+ else {
333
+ navigator.mediaDevices
334
+ .getUserMedia({
335
+ audio: true,
336
+ video: {
337
+ width: { min: 200, ideal: 1080, max: 2160 },
338
+ height: { min: 200, ideal: 1920, max: 3840 },
339
+ },
340
+ })
341
+ .then((stream) => {
342
+ setLocalMediaStream(stream);
343
+ })
344
+ .catch((e) => {
345
+ console.error("error getting user media", e.name);
346
+ if (e.name == "NotAllowedError") {
347
+ setError(new Error("Unable to access video! Please allow it in your browser settings."));
348
+ }
349
+ });
350
+ }
351
+ }, [ingestMediaSource]);
352
+ useEffect(() => {
353
+ if (!ingestStarting && !ingestAutoStart) {
354
+ setRemoteMediaStream(null);
355
+ return;
356
+ }
357
+ if (!localMediaStream) {
358
+ return;
359
+ }
360
+ setRemoteMediaStream(localMediaStream);
361
+ }, [localMediaStream, ingestStarting, ingestAutoStart]);
362
+ useEffect(() => {
363
+ if (!videoElement) {
364
+ return;
365
+ }
366
+ if (!localMediaStream) {
367
+ return;
368
+ }
369
+ videoElement.srcObject = localMediaStream;
370
+ }, [videoElement, localMediaStream]);
371
+ if (error) {
372
+ return (_jsxs(View, { backgroundColor: colors.destructive[900], style: [p[4], { borderRadius: borderRadius.md }], children: [_jsx(View, { children: _jsx(Text, { size: "xl", weight: "extrabold", children: "Error encountered!" }) }), _jsx(Text, { children: error.message })] }));
373
+ }
374
+ return _jsx(VideoElement, { ...props, ref: handleRef });
375
+ }
@@ -0,0 +1,238 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useVideoPlayer, VideoView } from "expo-video";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { RTCView, RTCView as RTCViewIngest, } from "react-native-webrtc";
5
+ import { IngestMediaSource, PlayerStatus as IngestPlayerStatus, PlayerProtocol, PlayerStatus, Text, usePlayerStore as useIngestPlayerStore, usePlayerStore, useStreamplaceStore, View, } from "../..";
6
+ import { borderRadius, colors, p } from "../../lib/theme/atoms";
7
+ import { srcToUrl } from "./shared";
8
+ import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
9
+ import { mediaDevices } from "./webrtc-primitives.native";
10
+ // Add NativeIngestPlayer to the switch below!
11
+ export default function VideoNative() {
12
+ const protocol = usePlayerStore((x) => x.protocol);
13
+ const ingest = usePlayerStore((x) => x.ingestConnectionState) != null;
14
+ return (_jsx(View, { children: ingest ? (_jsx(NativeIngestPlayer, {})) : protocol === PlayerProtocol.WEBRTC ? (_jsx(NativeWHEP, {})) : (_jsx(NativeVideo, {})) }));
15
+ }
16
+ export function NativeVideo() {
17
+ const videoRef = useRef(null);
18
+ const protocol = usePlayerStore((x) => x.protocol);
19
+ const selectedRendition = usePlayerStore((x) => x.selectedRendition);
20
+ const src = usePlayerStore((x) => x.src);
21
+ const { url } = srcToUrl({ src: src, selectedRendition }, protocol);
22
+ const setStatus = usePlayerStore((x) => x.setStatus);
23
+ const muted = usePlayerStore((x) => x.muted);
24
+ const volume = usePlayerStore((x) => x.volume);
25
+ const setFullscreen = usePlayerStore((x) => x.setFullscreen);
26
+ const fullscreen = usePlayerStore((x) => x.fullscreen);
27
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
28
+ const spurl = useStreamplaceStore((x) => x.url);
29
+ const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
30
+ const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
31
+ // State for live dimensions
32
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
33
+ const handleLayout = useCallback((event) => {
34
+ const { width, height } = event.nativeEvent.layout;
35
+ setDimensions({ width, height });
36
+ setPlayerWidth(width);
37
+ setPlayerHeight(height);
38
+ }, []);
39
+ useEffect(() => {
40
+ return () => {
41
+ setStatus(PlayerStatus.START);
42
+ };
43
+ }, [setStatus]);
44
+ const player = useVideoPlayer(url, (player) => {
45
+ player.loop = true;
46
+ player.muted = muted;
47
+ player.play();
48
+ });
49
+ useEffect(() => {
50
+ player.muted = muted;
51
+ }, [muted, player]);
52
+ useEffect(() => {
53
+ player.volume = volume;
54
+ }, [volume, player]);
55
+ useEffect(() => {
56
+ const subs = [
57
+ "playToEnd",
58
+ "playbackRateChange",
59
+ "playingChange",
60
+ "sourceChange",
61
+ "statusChange",
62
+ "volumeChange",
63
+ ].map((evType) => {
64
+ return player.addListener(evType, (...args) => {
65
+ const now = new Date();
66
+ playerEvent(spurl, now.toISOString(), evType, { args: args });
67
+ });
68
+ });
69
+ subs.push(player.addListener("playingChange", (newIsPlaying) => {
70
+ if (newIsPlaying) {
71
+ setStatus(PlayerStatus.PLAYING);
72
+ }
73
+ else {
74
+ setStatus(PlayerStatus.WAITING);
75
+ }
76
+ }));
77
+ return () => {
78
+ for (const sub of subs) {
79
+ sub.remove();
80
+ }
81
+ };
82
+ }, [player, playerEvent, setStatus, spurl]);
83
+ return (_jsx(_Fragment, { children: _jsx(VideoView, { ref: videoRef, player: player, allowsFullscreen: true, nativeControls: fullscreen, onFullscreenEnter: () => {
84
+ setFullscreen(true);
85
+ }, onFullscreenExit: () => {
86
+ setFullscreen(false);
87
+ }, allowsPictureInPicture: true, onLayout: handleLayout }) }));
88
+ }
89
+ export function NativeWHEP() {
90
+ const selectedRendition = usePlayerStore((x) => x.selectedRendition);
91
+ const src = usePlayerStore((x) => x.src);
92
+ const { url } = srcToUrl({ src: src, selectedRendition }, PlayerProtocol.WEBRTC);
93
+ const [stream, stuck] = useWebRTC(url);
94
+ const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
95
+ const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
96
+ // PiP support: wire up videoRef (no direct ref for RTCView)
97
+ const setVideoRef = usePlayerStore((x) => x.setVideoRef);
98
+ // State for live dimensions
99
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
100
+ const handleLayout = useCallback((event) => {
101
+ const { width, height } = event.nativeEvent.layout;
102
+ setDimensions({ width, height });
103
+ setPlayerWidth(width);
104
+ setPlayerHeight(height);
105
+ }, []);
106
+ const setStatus = usePlayerStore((x) => x.setStatus);
107
+ const muted = usePlayerStore((x) => x.muted);
108
+ const volume = usePlayerStore((x) => x.volume);
109
+ useEffect(() => {
110
+ if (stuck) {
111
+ setStatus(PlayerStatus.STALLED);
112
+ }
113
+ else {
114
+ setStatus(PlayerStatus.PLAYING);
115
+ }
116
+ }, [stuck, setStatus]);
117
+ const mediaStream = stream;
118
+ useEffect(() => {
119
+ if (!mediaStream) {
120
+ setStatus(PlayerStatus.WAITING);
121
+ return;
122
+ }
123
+ setStatus(PlayerStatus.PLAYING);
124
+ }, [mediaStream, setStatus]);
125
+ useEffect(() => {
126
+ if (!mediaStream) {
127
+ return;
128
+ }
129
+ mediaStream.getTracks().forEach((track) => {
130
+ if (track.kind === "audio") {
131
+ track._setVolume(muted ? 0 : volume);
132
+ }
133
+ });
134
+ }, [mediaStream, muted, volume]);
135
+ // Keep the playerStore videoRef in sync for PiP (if possible)
136
+ useEffect(() => {
137
+ if (typeof setVideoRef === "function") {
138
+ setVideoRef(null); // No direct ref for RTCView, but keep API consistent
139
+ }
140
+ }, [setVideoRef]);
141
+ if (!mediaStream) {
142
+ return _jsx(View, {});
143
+ }
144
+ return (_jsx(_Fragment, { children: _jsx(RTCView, { mirror: false, objectFit: "contain", streamURL: mediaStream.toURL(), onLayout: handleLayout, pictureInPictureEnabled: true, autoStartPictureInPicture: true, pictureInPicturePreferredSize: {
145
+ width: 160,
146
+ height: 90,
147
+ }, style: {
148
+ minWidth: "100%",
149
+ minHeight: "100%",
150
+ flex: 1,
151
+ } }) }));
152
+ }
153
+ export function NativeIngestPlayer() {
154
+ const ingestStarting = useIngestPlayerStore((x) => x.ingestStarting);
155
+ const ingestMediaSource = useIngestPlayerStore((x) => x.ingestMediaSource);
156
+ const ingestAutoStart = useIngestPlayerStore((x) => x.ingestAutoStart);
157
+ const setStatus = useIngestPlayerStore((x) => x.setStatus);
158
+ const setVideoRef = usePlayerStore((x) => x.setVideoRef);
159
+ const [error, setError] = useState(null);
160
+ const ingestCamera = useIngestPlayerStore((x) => x.ingestCamera);
161
+ useEffect(() => {
162
+ setStatus(IngestPlayerStatus.PLAYING);
163
+ }, [setStatus]);
164
+ useEffect(() => {
165
+ if (typeof setVideoRef === "function") {
166
+ setVideoRef(null);
167
+ }
168
+ }, [setVideoRef]);
169
+ const url = useStreamplaceStore((x) => x.url);
170
+ const [lms, setLocalMediaStream] = useState(null);
171
+ const [, setRemoteMediaStream] = useWebRTCIngest({
172
+ endpoint: `${url}/api/ingest/webrtc`,
173
+ });
174
+ // Use lms directly as localMediaStream
175
+ const localMediaStream = lms;
176
+ useEffect(() => {
177
+ if (ingestMediaSource === IngestMediaSource.DISPLAY) {
178
+ mediaDevices
179
+ .getDisplayMedia()
180
+ .then((stream) => {
181
+ console.log("display media", stream);
182
+ setLocalMediaStream(stream);
183
+ })
184
+ .catch((e) => {
185
+ console.log("error getting display media", e);
186
+ console.error("error getting display media", e);
187
+ });
188
+ }
189
+ else {
190
+ mediaDevices
191
+ .getUserMedia({
192
+ audio: {
193
+ // deviceId: "audio-1",
194
+ // echoCancellation: true,
195
+ // autoGainControl: true,
196
+ // noiseSuppression: true,
197
+ // latency: false,
198
+ // channelCount: false,
199
+ },
200
+ video: {
201
+ facingMode: ingestCamera,
202
+ width: { min: 200, ideal: 1080, max: 2160 },
203
+ height: { min: 200, ideal: 1920, max: 3840 },
204
+ },
205
+ })
206
+ .then((stream) => {
207
+ setLocalMediaStream(stream);
208
+ })
209
+ .catch((e) => {
210
+ console.error("error getting user media", e);
211
+ setError(new Error("We could not access your camera or microphone. Please check your permissions."));
212
+ });
213
+ }
214
+ }, [ingestMediaSource, ingestCamera]);
215
+ useEffect(() => {
216
+ if (!ingestStarting && !ingestAutoStart) {
217
+ setRemoteMediaStream(null);
218
+ return;
219
+ }
220
+ if (!localMediaStream) {
221
+ return;
222
+ }
223
+ console.log("setting remote media stream", localMediaStream);
224
+ // @ts-expect-error: WebRTCMediaStream may not have all MediaStream properties, but is compatible for our use
225
+ setRemoteMediaStream(localMediaStream);
226
+ }, [localMediaStream, ingestStarting, ingestAutoStart, setRemoteMediaStream]);
227
+ if (!localMediaStream) {
228
+ return null;
229
+ }
230
+ if (error) {
231
+ return (_jsxs(View, { backgroundColor: colors.destructive[900], style: [p[4], { borderRadius: borderRadius.md }], children: [_jsx(View, { children: _jsx(Text, { children: "Error encountered!" }) }), _jsx(Text, { children: error.message })] }));
232
+ }
233
+ return (_jsx(RTCViewIngest, { mirror: ingestCamera !== "environment", objectFit: "contain", streamURL: localMediaStream.toURL(), zOrder: 0, style: {
234
+ minWidth: "100%",
235
+ minHeight: "100%",
236
+ flex: 1,
237
+ } }));
238
+ }