@streamplace/components 0.7.14 → 0.7.17

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 (123) hide show
  1. package/package.json +13 -15
  2. package/src/components/mobile-player/fullscreen.native.tsx +14 -3
  3. package/src/components/mobile-player/fullscreen.tsx +10 -2
  4. package/src/components/mobile-player/player.tsx +7 -1
  5. package/src/components/mobile-player/props.tsx +7 -0
  6. package/src/components/mobile-player/video-async.native.tsx +436 -0
  7. package/src/components/mobile-player/video.native.tsx +16 -406
  8. package/src/components/mobile-player/video.tsx +14 -3
  9. package/src/hooks/useLivestreamInfo.ts +6 -2
  10. package/src/lib/browser.ts +27 -0
  11. package/src/livestream-store/stream-key.tsx +1 -28
  12. package/src/streamplace-store/stream.tsx +52 -13
  13. package/dist/assets/emoji-data.json +0 -19371
  14. package/dist/components/chat/chat-box.js +0 -314
  15. package/dist/components/chat/chat-message.js +0 -87
  16. package/dist/components/chat/chat.js +0 -149
  17. package/dist/components/chat/emoji-suggestions.js +0 -35
  18. package/dist/components/chat/mention-suggestions.js +0 -42
  19. package/dist/components/chat/mod-view.js +0 -94
  20. package/dist/components/chat/system-message.js +0 -19
  21. package/dist/components/dashboard/chat-panel.js +0 -38
  22. package/dist/components/dashboard/header.js +0 -80
  23. package/dist/components/dashboard/index.js +0 -14
  24. package/dist/components/dashboard/information-widget.js +0 -234
  25. package/dist/components/dashboard/mod-actions.js +0 -71
  26. package/dist/components/dashboard/problems.js +0 -74
  27. package/dist/components/icons/bluesky-icon.js +0 -9
  28. package/dist/components/keep-awake.js +0 -7
  29. package/dist/components/keep-awake.native.js +0 -16
  30. package/dist/components/mobile-player/fullscreen.js +0 -74
  31. package/dist/components/mobile-player/fullscreen.native.js +0 -141
  32. package/dist/components/mobile-player/player.js +0 -94
  33. package/dist/components/mobile-player/props.js +0 -2
  34. package/dist/components/mobile-player/shared.js +0 -54
  35. package/dist/components/mobile-player/ui/countdown.js +0 -83
  36. package/dist/components/mobile-player/ui/index.js +0 -11
  37. package/dist/components/mobile-player/ui/input.js +0 -42
  38. package/dist/components/mobile-player/ui/metrics.js +0 -44
  39. package/dist/components/mobile-player/ui/report-modal.js +0 -90
  40. package/dist/components/mobile-player/ui/streamer-context-menu.js +0 -7
  41. package/dist/components/mobile-player/ui/streamer-loading-overlay.js +0 -104
  42. package/dist/components/mobile-player/ui/viewer-context-menu.js +0 -51
  43. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +0 -49
  44. package/dist/components/mobile-player/ui/viewers.js +0 -23
  45. package/dist/components/mobile-player/use-webrtc.js +0 -243
  46. package/dist/components/mobile-player/video-retry.js +0 -29
  47. package/dist/components/mobile-player/video.js +0 -460
  48. package/dist/components/mobile-player/video.native.js +0 -276
  49. package/dist/components/mobile-player/webrtc-diagnostics.js +0 -110
  50. package/dist/components/mobile-player/webrtc-primitives.js +0 -27
  51. package/dist/components/mobile-player/webrtc-primitives.native.js +0 -8
  52. package/dist/components/share/sharesheet.js +0 -91
  53. package/dist/components/ui/button.js +0 -223
  54. package/dist/components/ui/dialog.js +0 -206
  55. package/dist/components/ui/dropdown.js +0 -172
  56. package/dist/components/ui/icons.js +0 -25
  57. package/dist/components/ui/index.js +0 -34
  58. package/dist/components/ui/info-box.js +0 -31
  59. package/dist/components/ui/info-row.js +0 -23
  60. package/dist/components/ui/input.js +0 -205
  61. package/dist/components/ui/loader.js +0 -10
  62. package/dist/components/ui/primitives/button.js +0 -125
  63. package/dist/components/ui/primitives/input.js +0 -206
  64. package/dist/components/ui/primitives/modal.js +0 -206
  65. package/dist/components/ui/primitives/text.js +0 -292
  66. package/dist/components/ui/resizeable.js +0 -121
  67. package/dist/components/ui/slider.js +0 -5
  68. package/dist/components/ui/text.js +0 -177
  69. package/dist/components/ui/textarea.js +0 -19
  70. package/dist/components/ui/toast.js +0 -175
  71. package/dist/components/ui/view.js +0 -252
  72. package/dist/hooks/index.js +0 -14
  73. package/dist/hooks/useAvatars.js +0 -35
  74. package/dist/hooks/useCameraToggle.js +0 -12
  75. package/dist/hooks/useKeyboard.js +0 -36
  76. package/dist/hooks/useKeyboardSlide.js +0 -14
  77. package/dist/hooks/useLivestreamInfo.js +0 -65
  78. package/dist/hooks/useOuterAndInnerDimensions.js +0 -30
  79. package/dist/hooks/usePlayerDimensions.js +0 -22
  80. package/dist/hooks/usePointerDevice.js +0 -71
  81. package/dist/hooks/useSegmentDimensions.js +0 -17
  82. package/dist/hooks/useSegmentTiming.js +0 -65
  83. package/dist/index.js +0 -34
  84. package/dist/lib/facet.js +0 -92
  85. package/dist/lib/system-messages.js +0 -101
  86. package/dist/lib/theme/atoms.js +0 -646
  87. package/dist/lib/theme/atoms.types.js +0 -6
  88. package/dist/lib/theme/index.js +0 -35
  89. package/dist/lib/theme/theme.js +0 -256
  90. package/dist/lib/theme/tokens.js +0 -659
  91. package/dist/lib/utils.js +0 -105
  92. package/dist/livestream-provider/index.js +0 -30
  93. package/dist/livestream-provider/websocket.js +0 -45
  94. package/dist/livestream-store/chat.js +0 -286
  95. package/dist/livestream-store/context.js +0 -5
  96. package/dist/livestream-store/index.js +0 -7
  97. package/dist/livestream-store/livestream-state.js +0 -2
  98. package/dist/livestream-store/livestream-store.js +0 -58
  99. package/dist/livestream-store/problems.js +0 -76
  100. package/dist/livestream-store/stream-key.js +0 -119
  101. package/dist/livestream-store/websocket-consumer.js +0 -94
  102. package/dist/player-store/context.js +0 -5
  103. package/dist/player-store/index.js +0 -9
  104. package/dist/player-store/player-provider.js +0 -57
  105. package/dist/player-store/player-state.js +0 -25
  106. package/dist/player-store/player-store.js +0 -199
  107. package/dist/player-store/single-player-provider.js +0 -121
  108. package/dist/streamplace-provider/context.js +0 -5
  109. package/dist/streamplace-provider/index.js +0 -20
  110. package/dist/streamplace-provider/poller.js +0 -49
  111. package/dist/streamplace-provider/xrpc.js +0 -0
  112. package/dist/streamplace-store/block.js +0 -65
  113. package/dist/streamplace-store/index.js +0 -6
  114. package/dist/streamplace-store/stream.js +0 -218
  115. package/dist/streamplace-store/streamplace-store.js +0 -47
  116. package/dist/streamplace-store/user.js +0 -52
  117. package/dist/streamplace-store/xrpc.js +0 -15
  118. package/dist/ui/index.js +0 -79
  119. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  120. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  121. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  122. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  123. package/tsconfig.tsbuildinfo +0 -1
@@ -1,419 +1,29 @@
1
- import { useVideoPlayer, VideoPlayerEvents, VideoView } from "expo-video";
2
- import { ArrowRight } from "lucide-react-native";
3
- import { useCallback, useEffect, useRef, useState } from "react";
4
- import { LayoutChangeEvent, Linking } from "react-native";
5
- import {
6
- MediaStream,
7
- RTCView,
8
- RTCView as RTCViewIngest,
9
- } from "react-native-webrtc";
10
- import {
11
- Button,
12
- IngestMediaSource,
13
- PlayerStatus as IngestPlayerStatus,
14
- PlayerProtocol,
15
- PlayerStatus,
16
- Text,
17
- usePlayerStore as useIngestPlayerStore,
18
- usePlayerStore,
19
- useStreamplaceStore,
20
- View,
21
- } from "../..";
22
- import {
23
- borderRadius,
24
- colors,
25
- fontWeight,
26
- gap,
27
- h,
28
- layout,
29
- m,
30
- p,
31
- } from "../../lib/theme/atoms";
32
- import { srcToUrl } from "./shared";
33
- import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
34
- import { mediaDevices, WebRTCMediaStream } from "./webrtc-primitives.native";
1
+ import { useEffect, useState } from "react";
2
+ import { View } from "react-native";
3
+ import { VideoNativeProps } from "./props";
35
4
 
36
- // Add NativeIngestPlayer to the switch below!
37
- export default function VideoNative() {
38
- const protocol = usePlayerStore((x) => x.protocol);
39
- const ingest = usePlayerStore((x) => x.ingestConnectionState) != null;
5
+ let importPromise: Promise<typeof import("./video-async.native")> | null = null;
40
6
 
41
- return (
42
- <View>
43
- {ingest ? (
44
- <NativeIngestPlayer />
45
- ) : protocol === PlayerProtocol.WEBRTC ? (
46
- <NativeWHEP />
47
- ) : (
48
- <NativeVideo />
49
- )}
50
- </View>
51
- );
52
- }
53
-
54
- export function NativeVideo() {
55
- const videoRef = useRef<VideoView | null>(null);
56
- const protocol = usePlayerStore((x) => x.protocol);
57
-
58
- const selectedRendition = usePlayerStore((x) => x.selectedRendition);
59
- const src = usePlayerStore((x) => x.src);
60
- const { url } = srcToUrl({ src: src, selectedRendition }, protocol);
61
- const setStatus = usePlayerStore((x) => x.setStatus);
62
- const muted = usePlayerStore((x) => x.muted);
63
- const volume = usePlayerStore((x) => x.volume);
64
- const setFullscreen = usePlayerStore((x) => x.setFullscreen);
65
- const fullscreen = usePlayerStore((x) => x.fullscreen);
66
- const playerEvent = usePlayerStore((x) => x.playerEvent);
67
- const spurl = useStreamplaceStore((x) => x.url);
68
-
69
- const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
70
- const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
71
-
72
- // State for live dimensions
73
- const [dimensions, setDimensions] = useState<{
74
- width: number;
75
- height: number;
76
- }>({ width: 0, height: 0 });
7
+ export default function VideoNative(props: VideoNativeProps) {
8
+ if (!importPromise) {
9
+ importPromise = import("./video-async.native");
10
+ }
77
11
 
78
- const handleLayout = useCallback((event: LayoutChangeEvent) => {
79
- const { width, height } = event.nativeEvent.layout;
80
- setDimensions({ width, height });
81
- setPlayerWidth(width);
82
- setPlayerHeight(height);
83
- }, []);
12
+ const [videoNativeModule, setVideoNativeModule] = useState<
13
+ typeof import("./video-async.native") | null
14
+ >(null);
84
15
 
85
16
  useEffect(() => {
86
- return () => {
87
- setStatus(PlayerStatus.START);
88
- };
89
- }, [setStatus]);
90
-
91
- const player = useVideoPlayer(url, (player) => {
92
- player.addListener("playingChange", (newIsPlaying) => {
93
- console.log("playingChange", newIsPlaying);
94
- if (newIsPlaying) {
95
- setStatus(PlayerStatus.PLAYING);
96
- } else {
97
- setStatus(PlayerStatus.WAITING);
98
- }
17
+ importPromise?.then((module) => {
18
+ setVideoNativeModule(module);
99
19
  });
100
- player.loop = true;
101
- player.muted = muted;
102
- player.play();
103
- });
104
-
105
- useEffect(() => {
106
- player.muted = muted;
107
- }, [muted, player]);
108
-
109
- useEffect(() => {
110
- player.volume = volume;
111
- }, [volume, player]);
112
-
113
- useEffect(() => {
114
- const subs = (
115
- [
116
- "playToEnd",
117
- "playbackRateChange",
118
- "playingChange",
119
- "sourceChange",
120
- "statusChange",
121
- "volumeChange",
122
- ] as (keyof VideoPlayerEvents)[]
123
- ).map((evType) => {
124
- return player.addListener(evType, (...args) => {
125
- const now = new Date();
126
- console.log("video native event", evType);
127
- playerEvent(spurl, now.toISOString(), evType, { args: args });
128
- });
129
- });
130
-
131
- subs.push(
132
- player.addListener("playingChange", (newIsPlaying) => {
133
- console.log("playingChange", newIsPlaying);
134
- if (newIsPlaying) {
135
- setStatus(PlayerStatus.PLAYING);
136
- } else {
137
- setStatus(PlayerStatus.WAITING);
138
- }
139
- }),
140
- );
141
-
142
- return () => {
143
- for (const sub of subs) {
144
- sub.remove();
145
- }
146
- };
147
- }, [player, playerEvent, setStatus, spurl]);
148
-
149
- return (
150
- <>
151
- <VideoView
152
- ref={videoRef}
153
- player={player}
154
- allowsFullscreen
155
- nativeControls={fullscreen}
156
- onFullscreenEnter={() => {
157
- setFullscreen(true);
158
- }}
159
- onFullscreenExit={() => {
160
- setFullscreen(false);
161
- }}
162
- allowsPictureInPicture
163
- onLayout={handleLayout}
164
- />
165
- </>
166
- );
167
- }
168
-
169
- export function NativeWHEP() {
170
- const selectedRendition = usePlayerStore((x) => x.selectedRendition);
171
- const src = usePlayerStore((x) => x.src);
172
- const { url } = srcToUrl(
173
- { src: src, selectedRendition },
174
- PlayerProtocol.WEBRTC,
175
- );
176
- const [stream, stuck] = useWebRTC(url);
177
- const status = usePlayerStore((x) => x.status);
178
-
179
- const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
180
- const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
181
-
182
- // PiP support: wire up videoRef (no direct ref for RTCView)
183
- const setVideoRef = usePlayerStore((x) => x.setVideoRef);
184
-
185
- // State for live dimensions
186
- const [dimensions, setDimensions] = useState<{
187
- width: number;
188
- height: number;
189
- }>({ width: 0, height: 0 });
190
-
191
- const handleLayout = useCallback((event: LayoutChangeEvent) => {
192
- const { width, height } = event.nativeEvent.layout;
193
- setDimensions({ width, height });
194
- setPlayerWidth(width);
195
- setPlayerHeight(height);
196
20
  }, []);
197
21
 
198
- const setStatus = usePlayerStore((x) => x.setStatus);
199
- const muted = usePlayerStore((x) => x.muted);
200
- const volume = usePlayerStore((x) => x.volume);
201
-
202
- useEffect(() => {
203
- if (stuck && status === PlayerStatus.PLAYING) {
204
- console.log("setting status to stalled", status);
205
- setStatus(PlayerStatus.STALLED);
206
- }
207
- if (!stuck && status === PlayerStatus.STALLED) {
208
- console.log("setting status to playing", status);
209
- setStatus(PlayerStatus.PLAYING);
210
- }
211
- }, [stuck, status]);
212
-
213
- const mediaStream = stream as unknown as MediaStream;
214
-
215
- // useEffect(() => {
216
- // if (!mediaStream) {
217
- // setStatus(PlayerStatus.WAITING);
218
- // return;
219
- // }
220
- // setStatus(PlayerStatus.PLAYING);
221
- // }, [mediaStream, setStatus]);
222
-
223
- useEffect(() => {
224
- if (!mediaStream) {
225
- return;
226
- }
227
- mediaStream.getTracks().forEach((track) => {
228
- if (track.kind === "audio") {
229
- track._setVolume(muted ? 0 : volume);
230
- }
231
- });
232
- }, [mediaStream, muted, volume]);
233
-
234
- // Keep the playerStore videoRef in sync for PiP (if possible)
235
- useEffect(() => {
236
- if (typeof setVideoRef === "function") {
237
- setVideoRef(null); // No direct ref for RTCView, but keep API consistent
238
- }
239
- }, [setVideoRef]);
240
-
241
- if (!mediaStream) {
22
+ if (!videoNativeModule) {
242
23
  return <View></View>;
243
24
  }
244
25
 
245
- return (
246
- <>
247
- <RTCView
248
- mirror={false}
249
- objectFit={"contain"}
250
- streamURL={mediaStream.toURL()}
251
- onLayout={handleLayout}
252
- pictureInPictureEnabled={true}
253
- autoStartPictureInPicture={true}
254
- pictureInPicturePreferredSize={{
255
- width: 160,
256
- height: 90,
257
- }}
258
- style={{
259
- minWidth: "100%",
260
- minHeight: "100%",
261
- flex: 1,
262
- }}
263
- />
264
- </>
265
- );
266
- }
267
-
268
- export function NativeIngestPlayer() {
269
- const ingestStarting = useIngestPlayerStore((x) => x.ingestStarting);
270
- const ingestMediaSource = useIngestPlayerStore((x) => x.ingestMediaSource);
271
- const ingestAutoStart = useIngestPlayerStore((x) => x.ingestAutoStart);
272
- const setStatus = useIngestPlayerStore((x) => x.setStatus);
273
- const setVideoRef = usePlayerStore((x) => x.setVideoRef);
274
-
275
- const [error, setError] = useState<Error | null>(null);
276
-
277
- const ingestCamera = useIngestPlayerStore((x) => x.ingestCamera);
278
-
279
- useEffect(() => {
280
- setStatus(IngestPlayerStatus.PLAYING);
281
- }, [setStatus]);
282
-
283
- useEffect(() => {
284
- if (typeof setVideoRef === "function") {
285
- setVideoRef(null);
286
- }
287
- }, [setVideoRef]);
288
-
289
- const url = useStreamplaceStore((x) => x.url);
290
- const [lms, setLocalMediaStream] = useState<WebRTCMediaStream | null>(null);
291
- const [, setRemoteMediaStream] = useWebRTCIngest({
292
- endpoint: `${url}/api/ingest/webrtc`,
293
- });
294
-
295
- // Use lms directly as localMediaStream
296
- const localMediaStream = lms;
297
-
298
- useEffect(() => {
299
- if (ingestMediaSource === IngestMediaSource.DISPLAY) {
300
- mediaDevices
301
- .getDisplayMedia()
302
- .then((stream: WebRTCMediaStream) => {
303
- console.log("display media", stream);
304
- setLocalMediaStream(stream);
305
- })
306
- .catch((e: any) => {
307
- console.log("error getting display media", e);
308
- console.error("error getting display media", e);
309
- });
310
- } else {
311
- mediaDevices
312
- .getUserMedia({
313
- audio: {
314
- // deviceId: "audio-1",
315
- // echoCancellation: true,
316
- // autoGainControl: true,
317
- // noiseSuppression: true,
318
- // latency: false,
319
- // channelCount: false,
320
- },
321
- video: {
322
- facingMode: ingestCamera,
323
- width: { min: 200, ideal: 1080, max: 2160 },
324
- height: { min: 200, ideal: 1920, max: 3840 },
325
- },
326
- })
327
- .then((stream: WebRTCMediaStream) => {
328
- setLocalMediaStream(stream);
329
-
330
- let errs: string[] = [];
331
- if (stream.getAudioTracks().length === 0) {
332
- console.warn("No audio tracks found in user media stream");
333
- errs.push("microphone");
334
- }
335
- if (stream.getVideoTracks().length === 0) {
336
- console.warn("No video tracks found in user media stream");
337
- errs.push("camera");
338
- }
339
- if (errs.length > 0) {
340
- setError(
341
- new Error(
342
- `We could not access your ${errs.join(" and ")}. To stream, you need to give us permission to access these.`,
343
- ),
344
- );
345
- } else {
346
- setError(null);
347
- }
348
- })
349
- .catch((e: any) => {
350
- console.error("error getting user media", e);
351
- setError(
352
- new Error(
353
- "We could not access your camera or microphone. To stream, you need to give us permission to access these.",
354
- ),
355
- );
356
- });
357
- }
358
- }, [ingestMediaSource, ingestCamera]);
359
-
360
- useEffect(() => {
361
- if (!ingestStarting && !ingestAutoStart) {
362
- setRemoteMediaStream(null);
363
- return;
364
- }
365
- if (!localMediaStream) {
366
- return;
367
- }
368
- console.log("setting remote media stream", localMediaStream);
369
- // @ts-expect-error: WebRTCMediaStream may not have all MediaStream properties, but is compatible for our use
370
- setRemoteMediaStream(localMediaStream);
371
- }, [localMediaStream, ingestStarting, ingestAutoStart, setRemoteMediaStream]);
372
-
373
- if (!localMediaStream) {
374
- return null;
375
- }
376
-
377
- if (error) {
378
- return (
379
- <View
380
- backgroundColor={colors.destructive[900]}
381
- style={[p[4], m[4], gap.all[2], { borderRadius: borderRadius.md }]}
382
- >
383
- <View>
384
- <Text style={[fontWeight.semibold]} size="2xl">
385
- Error encountered!
386
- </Text>
387
- </View>
388
- <Text>{error.message}</Text>
389
- {error.message.includes(
390
- "To stream, you need to give us permission to access these.",
391
- ) && (
392
- <Button
393
- onPress={Linking.openSettings}
394
- style={[h[10]]}
395
- variant="secondary"
396
- >
397
- <View style={[layout.flex.row, gap.all[1]]}>
398
- <Text>Open Settings</Text> <ArrowRight color="white" size="18" />
399
- </View>
400
- </Button>
401
- )}
402
- </View>
403
- );
404
- }
26
+ const VideoNative = videoNativeModule.default;
405
27
 
406
- return (
407
- <RTCViewIngest
408
- mirror={ingestCamera !== "environment"}
409
- objectFit={"contain"}
410
- streamURL={localMediaStream.toURL()}
411
- zOrder={0}
412
- style={{
413
- minWidth: "100%",
414
- minHeight: "100%",
415
- flex: 1,
416
- }}
417
- />
418
- );
28
+ return <VideoNative {...props} />;
419
29
  }
@@ -34,6 +34,8 @@ function assignVideoRef(
34
34
  type VideoProps = {
35
35
  url: string;
36
36
  videoRef?: React.RefObject<HTMLVideoElement>;
37
+ objectFit?: "contain" | "cover";
38
+ pictureInPictureEnabled?: boolean;
37
39
  };
38
40
 
39
41
  function useVideoDimensions(videoRef: React.RefObject<HTMLVideoElement>) {
@@ -67,7 +69,10 @@ function useVideoDimensions(videoRef: React.RefObject<HTMLVideoElement>) {
67
69
  return dimensions;
68
70
  }
69
71
 
70
- export default function WebVideo() {
72
+ export default function WebVideo(props?: {
73
+ objectFit?: "contain" | "cover";
74
+ pictureInPictureEnabled?: boolean;
75
+ }) {
71
76
  const inProto = usePlayerStore((x) => x.protocol);
72
77
  const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null);
73
78
  const selectedRendition = usePlayerStore((x) => x.selectedRendition);
@@ -86,7 +91,12 @@ export default function WebVideo() {
86
91
  }
87
92
  }, [dimensions, setPlayerWidth, setPlayerHeight]);
88
93
 
89
- const playerProps = { url, videoRef };
94
+ const playerProps = {
95
+ url,
96
+ videoRef,
97
+ objectFit: props?.objectFit,
98
+ pictureInPictureEnabled: props?.pictureInPictureEnabled,
99
+ };
90
100
 
91
101
  return (
92
102
  <>
@@ -274,7 +284,7 @@ const VideoElement = forwardRef<
274
284
  onVolumeChange={event("volumechange")}
275
285
  onWaiting={event("waiting")}
276
286
  style={{
277
- objectFit: "contain",
287
+ objectFit: props.objectFit || "contain",
278
288
  backgroundColor: "transparent",
279
289
  width: "100%",
280
290
  height: "100%",
@@ -282,6 +292,7 @@ const VideoElement = forwardRef<
282
292
  maxHeight: "100%",
283
293
  transform: ingest ? "scaleX(-1)" : undefined,
284
294
  }}
295
+ disablePictureInPicture={props.pictureInPictureEnabled === false}
285
296
  />
286
297
  );
287
298
  });
@@ -3,7 +3,7 @@ import { useLivestreamStore } from "../livestream-store";
3
3
  import { usePlayerStore } from "../player-store";
4
4
  import { useCreateStreamRecord } from "../streamplace-store";
5
5
 
6
- export function useLivestreamInfo() {
6
+ export function useLivestreamInfo(url?: string) {
7
7
  const ingest = usePlayerStore((x) => x.ingestConnectionState);
8
8
  const profile = useLivestreamStore((x) => x.profile);
9
9
  const ingestStarting = usePlayerStore((x) => x.ingestStarting);
@@ -20,7 +20,11 @@ export function useLivestreamInfo() {
20
20
  try {
21
21
  if (title !== "") {
22
22
  setRecordSubmitted(true);
23
- await createStreamRecord(title);
23
+ // Create the livestream record with title and custom url if available
24
+ await createStreamRecord({
25
+ title,
26
+ customUrl: url || null,
27
+ });
24
28
  }
25
29
  } catch (error) {
26
30
  console.error("Error creating livestream:", error);
@@ -0,0 +1,27 @@
1
+ export function getBrowserName(userAgent: string) {
2
+ // The order matters here, and this may report false positives for unlisted browsers.
3
+
4
+ if (userAgent.includes("Firefox")) {
5
+ // "Mozilla/5.0 (X11; Linux i686; rv:104.0) Gecko/20100101 Firefox/104.0"
6
+ return "Firefox";
7
+ } else if (userAgent.includes("SamsungBrowser")) {
8
+ // "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G955F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.4 Chrome/67.0.3396.87 Mobile Safari/537.36"
9
+ return "Samsung Internet";
10
+ } else if (userAgent.includes("Opera") || userAgent.includes("OPR")) {
11
+ // "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 OPR/90.0.4480.54"
12
+ return "Opera";
13
+ } else if (userAgent.includes("Edge")) {
14
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
15
+ return "Edge (Legacy)";
16
+ } else if (userAgent.includes("Edg")) {
17
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/104.0.1293.70"
18
+ return "Edge (Chromium)";
19
+ } else if (userAgent.includes("Chrome")) {
20
+ // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
21
+ return "Chrome";
22
+ } else if (userAgent.includes("Safari")) {
23
+ // "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1"
24
+ return "Safari";
25
+ }
26
+ return "unknown";
27
+ }
@@ -3,37 +3,10 @@ import { useEffect, useState } from "react";
3
3
  import { Platform } from "react-native";
4
4
  import { PlaceStreamKey } from "streamplace";
5
5
  import { privateKeyToAccount } from "viem/accounts";
6
+ import { getBrowserName } from "../lib/browser";
6
7
  import { usePDSAgent } from "../streamplace-store/xrpc";
7
8
  import { useLivestreamStore } from "./livestream-store";
8
9
 
9
- function getBrowserName(userAgent: string) {
10
- // The order matters here, and this may report false positives for unlisted browsers.
11
-
12
- if (userAgent.includes("Firefox")) {
13
- // "Mozilla/5.0 (X11; Linux i686; rv:104.0) Gecko/20100101 Firefox/104.0"
14
- return "Mozilla Firefox";
15
- } else if (userAgent.includes("SamsungBrowser")) {
16
- // "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G955F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.4 Chrome/67.0.3396.87 Mobile Safari/537.36"
17
- return "Samsung Internet";
18
- } else if (userAgent.includes("Opera") || userAgent.includes("OPR")) {
19
- // "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 OPR/90.0.4480.54"
20
- return "Opera";
21
- } else if (userAgent.includes("Edge")) {
22
- // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
23
- return "Microsoft Edge (Legacy)";
24
- } else if (userAgent.includes("Edg")) {
25
- // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/104.0.1293.70"
26
- return "Microsoft Edge (Chromium)";
27
- } else if (userAgent.includes("Chrome")) {
28
- // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
29
- return "Google Chrome or Chromium";
30
- } else if (userAgent.includes("Safari")) {
31
- // "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1"
32
- return "Apple Safari";
33
- }
34
- return "unknown";
35
- }
36
-
37
10
  export const useStreamKey = (): {
38
11
  streamKey: {
39
12
  privateKey: string;