@streamplace/components 0.7.0 → 0.7.1

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.
@@ -3,3 +3,4 @@ export * from "./input";
3
3
  export * from "./metrics";
4
4
  export * from "./streamer-context-menu";
5
5
  export * from "./viewer-context-menu";
6
+ export * from "./viewers";
@@ -16,5 +16,5 @@ export function ContextMenu() {
16
16
  const setLowLatency = (value) => {
17
17
  setProtocol(value ? PlayerProtocol.WEBRTC : PlayerProtocol.HLS);
18
18
  };
19
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { children: _jsx(Menu, { size: 32, color: colors.gray[200] }) }), _jsxs(ResponsiveDropdownMenuContent, { children: [_jsx(DropdownMenuGroup, { title: "Resolution", children: _jsxs(DropdownMenuRadioGroup, { value: quality, onValueChange: setQuality, children: [_jsx(DropdownMenuRadioItem, { value: "source", children: _jsx(Text, { children: "Source" }) }), qualities.map((r) => (_jsx(DropdownMenuRadioItem, { value: r.name, children: _jsx(Text, { children: r.name }) })))] }) }), _jsxs(DropdownMenuGroup, { title: "Advanced", children: [_jsx(DropdownMenuCheckboxItem, { checked: lowLatency, onCheckedChange: () => setLowLatency(!lowLatency), children: _jsx(Text, { children: "Low Latency" }) }), _jsx(DropdownMenuInfo, { description: "Lowers the delay between video and chat messages." }), _jsx(DropdownMenuCheckboxItem, { checked: debugInfo, onCheckedChange: () => setShowDebugInfo(!debugInfo), children: _jsx(Text, { children: "Segment Debug Info" }) })] }), _jsx(DropdownMenuInfo, { description: "Lowers the delay between video and chat messages." })] })] }));
19
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { children: _jsx(Menu, { size: 32, color: colors.gray[200] }) }), _jsxs(ResponsiveDropdownMenuContent, { children: [_jsx(DropdownMenuGroup, { title: "Resolution", children: _jsxs(DropdownMenuRadioGroup, { value: quality, onValueChange: setQuality, children: [_jsx(DropdownMenuRadioItem, { value: "source", children: _jsx(Text, { children: "Source (Original Quality)" }) }), qualities.map((r) => (_jsx(DropdownMenuRadioItem, { value: r.name, children: _jsx(Text, { children: r.name }) })))] }) }), _jsx(DropdownMenuGroup, { title: "Advanced", children: _jsx(DropdownMenuCheckboxItem, { checked: lowLatency, onCheckedChange: () => setLowLatency(!lowLatency), children: _jsx(Text, { children: "Low Latency" }) }) }), _jsx(DropdownMenuInfo, { description: "Reduces the delay between video and chat for a more real-time experience." }), _jsx(DropdownMenuGroup, { children: _jsx(DropdownMenuCheckboxItem, { checked: debugInfo, onCheckedChange: () => setShowDebugInfo(!debugInfo), children: _jsx(Text, { children: "Show Debug Info" }) }) })] })] }));
20
20
  }
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Eye } from "lucide-react-native";
3
+ import * as atoms from "../../../lib/theme/atoms";
4
+ import { useViewers } from "../../../livestream-store";
5
+ import { Text, View } from "../../ui";
6
+ export function Viewers() {
7
+ const viewers = useViewers();
8
+ return (_jsxs(View, { style: [
9
+ atoms.layout.flex.center,
10
+ atoms.layout.flex.row,
11
+ atoms.gap.all[2],
12
+ ], children: [_jsx(Eye, { color: "#fd5050" }), _jsx(Text, { style: {
13
+ color: "#fd5050",
14
+ textShadowColor: "black",
15
+ textShadowOffset: { width: -1, height: 1 },
16
+ textShadowRadius: 3,
17
+ fontSize: 16,
18
+ }, children: new Intl.NumberFormat(undefined, { notation: "compact" }).format(viewers || 0) })] }));
19
+ }
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useVideoPlayer, VideoView } from "expo-video";
3
+ import { ArrowRight } from "lucide-react-native";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { Linking } from "react-native";
4
6
  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 { Button, IngestMediaSource, PlayerStatus as IngestPlayerStatus, PlayerProtocol, PlayerStatus, Text, usePlayerStore as useIngestPlayerStore, usePlayerStore, useStreamplaceStore, View, } from "../..";
8
+ import { borderRadius, colors, fontWeight, gap, h, layout, m, p, } from "../../lib/theme/atoms";
7
9
  import { srcToUrl } from "./shared";
8
10
  import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
9
11
  import { mediaDevices } from "./webrtc-primitives.native";
@@ -205,10 +207,25 @@ export function NativeIngestPlayer() {
205
207
  })
206
208
  .then((stream) => {
207
209
  setLocalMediaStream(stream);
210
+ let errs = [];
211
+ if (stream.getAudioTracks().length === 0) {
212
+ console.warn("No audio tracks found in user media stream");
213
+ errs.push("microphone");
214
+ }
215
+ if (stream.getVideoTracks().length === 0) {
216
+ console.warn("No video tracks found in user media stream");
217
+ errs.push("camera");
218
+ }
219
+ if (errs.length > 0) {
220
+ setError(new Error(`We could not access your ${errs.join(" and ")}. To stream, you need to give us permission to access these.`));
221
+ }
222
+ else {
223
+ setError(null);
224
+ }
208
225
  })
209
226
  .catch((e) => {
210
227
  console.error("error getting user media", e);
211
- setError(new Error("We could not access your camera or microphone. Please check your permissions."));
228
+ setError(new Error("We could not access your camera or microphone. To stream, you need to give us permission to access these."));
212
229
  });
213
230
  }
214
231
  }, [ingestMediaSource, ingestCamera]);
@@ -228,7 +245,7 @@ export function NativeIngestPlayer() {
228
245
  return null;
229
246
  }
230
247
  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 })] }));
248
+ return (_jsxs(View, { backgroundColor: colors.destructive[900], style: [p[4], m[4], gap.all[2], { borderRadius: borderRadius.md }], children: [_jsx(View, { children: _jsx(Text, { style: [fontWeight.semibold], size: "2xl", children: "Error encountered!" }) }), _jsx(Text, { children: error.message }), error.message.includes("To stream, you need to give us permission to access these.") && (_jsx(Button, { onPress: Linking.openSettings, style: [h[10]], variant: "secondary", children: _jsxs(View, { style: [layout.flex.row, gap.all[1]], children: [_jsx(Text, { children: "Open Settings" }), " ", _jsx(ArrowRight, { color: "white", size: "18" })] }) }))] }));
232
249
  }
233
250
  return (_jsx(RTCViewIngest, { mirror: ingestCamera !== "environment", objectFit: "contain", streamURL: localMediaStream.toURL(), zOrder: 0, style: {
234
251
  minWidth: "100%",
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ChevronUp } from "lucide-react-native";
3
+ import { useEffect } from "react";
3
4
  import { Dimensions } from "react-native";
4
5
  import { Gesture, GestureDetector, Pressable, } from "react-native-gesture-handler";
5
6
  import Animated, { Extrapolation, interpolate, useAnimatedStyle, useSharedValue, withSpring, } from "react-native-reanimated";
@@ -10,7 +11,7 @@ import { View } from "./view";
10
11
  const AnimatedView = Animated.createAnimatedComponent(View);
11
12
  const { height: SCREEN_HEIGHT } = Dimensions.get("window");
12
13
  const SPRING_CONFIG = { damping: 20, stiffness: 100 };
13
- export function Resizable({ isPlayerRatioGreater, style = {}, children, }) {
14
+ export function Resizable({ startingPercentage, isPlayerRatioGreater, style = {}, children, }) {
14
15
  const { slideKeyboard } = useKeyboardSlide();
15
16
  const { bottom: safeBottom } = useSafeAreaInsets();
16
17
  const MAX_HEIGHT = (SCREEN_HEIGHT - safeBottom) * 0.5;
@@ -18,6 +19,11 @@ export function Resizable({ isPlayerRatioGreater, style = {}, children, }) {
18
19
  const COLLAPSE_HEIGHT = (SCREEN_HEIGHT - safeBottom) * 0.1;
19
20
  const sheetHeight = useSharedValue(MIN_HEIGHT);
20
21
  const startHeight = useSharedValue(MIN_HEIGHT);
22
+ useEffect(() => {
23
+ setTimeout(() => {
24
+ sheetHeight.value = withSpring(startingPercentage ? startingPercentage * SCREEN_HEIGHT : MIN_HEIGHT, SPRING_CONFIG);
25
+ }, 1000);
26
+ }, []);
21
27
  const panGesture = Gesture.Pan()
22
28
  .onStart(() => {
23
29
  startHeight.value = sheetHeight.value;
@@ -38,7 +44,9 @@ export function Resizable({ isPlayerRatioGreater, style = {}, children, }) {
38
44
  opacity: interpolate(sheetHeight.value, [MIN_HEIGHT, COLLAPSE_HEIGHT], [0, 1], Extrapolation.CLAMP),
39
45
  transform: [
40
46
  {
41
- translateY: slideKeyboard - safeBottom + Math.max(0, -sheetHeight.value),
47
+ translateY: slideKeyboard +
48
+ Math.max(0, -sheetHeight.value) +
49
+ (slideKeyboard < 0 ? 0 : -safeBottom),
42
50
  },
43
51
  ],
44
52
  }));
@@ -57,7 +65,6 @@ export function Resizable({ isPlayerRatioGreater, style = {}, children, }) {
57
65
  w.percent[100],
58
66
  layout.flex.center,
59
67
  zIndex[1],
60
- { marginBottom: safeBottom },
61
68
  ], children: _jsx(Pressable, { onPress: () => {
62
69
  sheetHeight.value =
63
70
  sheetHeight.value === MIN_HEIGHT
@@ -6,4 +6,5 @@ export * from "./useKeyboardSlide";
6
6
  export * from "./useLivestreamInfo";
7
7
  export * from "./useOuterAndInnerDimensions";
8
8
  export * from "./usePlayerDimensions";
9
+ export * from "./useSegmentDimensions";
9
10
  export * from "./useSegmentTiming";
@@ -0,0 +1,14 @@
1
+ import { useLivestreamStore } from "../livestream-store";
2
+ export function useSegmentDimensions() {
3
+ const latestSegment = useLivestreamStore((x) => x.segment);
4
+ let seg = latestSegment?.video && latestSegment.video[0];
5
+ let ratio = {
6
+ height: seg?.height || 0,
7
+ width: seg?.width || 0,
8
+ };
9
+ return {
10
+ isPlayerRatioGreater: ratio.width > ratio.height,
11
+ height: ratio.height,
12
+ width: ratio.width,
13
+ };
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsc",
17
- "postinstall": "pnpm run build",
17
+ "prepare": "pnpm run build",
18
18
  "start": "tsc --watch --preserveWatchOutput"
19
19
  },
20
20
  "keywords": [
@@ -44,12 +44,12 @@
44
44
  "react-native-safe-area-context": "5.4.1",
45
45
  "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
46
46
  "react-use-websocket": "^4.13.0",
47
- "streamplace": "0.7.0",
47
+ "streamplace": "0.7.1",
48
48
  "viem": "^2.21.44",
49
49
  "zustand": "^5.0.5"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "*"
53
53
  },
54
- "gitHead": "c0b9266fbc2cb2a643203e8c0450980c1bd29635"
54
+ "gitHead": "e092ac26f32426cfb14ec3cbda96265ad2fbae12"
55
55
  }
@@ -3,3 +3,4 @@ export * from "./input";
3
3
  export * from "./metrics";
4
4
  export * from "./streamer-context-menu";
5
5
  export * from "./viewer-context-menu";
6
+ export * from "./viewers";
@@ -39,7 +39,7 @@ export function ContextMenu() {
39
39
  <DropdownMenuGroup title="Resolution">
40
40
  <DropdownMenuRadioGroup value={quality} onValueChange={setQuality}>
41
41
  <DropdownMenuRadioItem value="source">
42
- <Text>Source</Text>
42
+ <Text>Source (Original Quality)</Text>
43
43
  </DropdownMenuRadioItem>
44
44
  {qualities.map((r) => (
45
45
  <DropdownMenuRadioItem value={r.name}>
@@ -55,15 +55,16 @@ export function ContextMenu() {
55
55
  >
56
56
  <Text>Low Latency</Text>
57
57
  </DropdownMenuCheckboxItem>
58
- <DropdownMenuInfo description="Lowers the delay between video and chat messages." />
58
+ </DropdownMenuGroup>
59
+ <DropdownMenuInfo description="Reduces the delay between video and chat for a more real-time experience." />
60
+ <DropdownMenuGroup>
59
61
  <DropdownMenuCheckboxItem
60
62
  checked={debugInfo}
61
63
  onCheckedChange={() => setShowDebugInfo(!debugInfo)}
62
64
  >
63
- <Text>Segment Debug Info</Text>
65
+ <Text>Show Debug Info</Text>
64
66
  </DropdownMenuCheckboxItem>
65
67
  </DropdownMenuGroup>
66
- <DropdownMenuInfo description="Lowers the delay between video and chat messages." />
67
68
  </ResponsiveDropdownMenuContent>
68
69
  </DropdownMenu>
69
70
  );
@@ -0,0 +1,32 @@
1
+ import { Eye } from "lucide-react-native";
2
+ import * as atoms from "../../../lib/theme/atoms";
3
+ import { useViewers } from "../../../livestream-store";
4
+ import { Text, View } from "../../ui";
5
+
6
+ export function Viewers() {
7
+ const viewers = useViewers();
8
+ return (
9
+ <View
10
+ style={[
11
+ atoms.layout.flex.center,
12
+ atoms.layout.flex.row,
13
+ atoms.gap.all[2],
14
+ ]}
15
+ >
16
+ <Eye color="#fd5050" />
17
+ <Text
18
+ style={{
19
+ color: "#fd5050",
20
+ textShadowColor: "black",
21
+ textShadowOffset: { width: -1, height: 1 },
22
+ textShadowRadius: 3,
23
+ fontSize: 16,
24
+ }}
25
+ >
26
+ {new Intl.NumberFormat(undefined, { notation: "compact" }).format(
27
+ viewers || 0,
28
+ )}
29
+ </Text>
30
+ </View>
31
+ );
32
+ }
@@ -1,12 +1,14 @@
1
1
  import { useVideoPlayer, VideoPlayerEvents, VideoView } from "expo-video";
2
+ import { ArrowRight } from "lucide-react-native";
2
3
  import { useCallback, useEffect, useRef, useState } from "react";
3
- import { LayoutChangeEvent } from "react-native";
4
+ import { LayoutChangeEvent, Linking } from "react-native";
4
5
  import {
5
6
  MediaStream,
6
7
  RTCView,
7
8
  RTCView as RTCViewIngest,
8
9
  } from "react-native-webrtc";
9
10
  import {
11
+ Button,
10
12
  IngestMediaSource,
11
13
  PlayerStatus as IngestPlayerStatus,
12
14
  PlayerProtocol,
@@ -17,7 +19,16 @@ import {
17
19
  useStreamplaceStore,
18
20
  View,
19
21
  } from "../..";
20
- import { borderRadius, colors, p } from "../../lib/theme/atoms";
22
+ import {
23
+ borderRadius,
24
+ colors,
25
+ fontWeight,
26
+ gap,
27
+ h,
28
+ layout,
29
+ m,
30
+ p,
31
+ } from "../../lib/theme/atoms";
21
32
  import { srcToUrl } from "./shared";
22
33
  import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
23
34
  import { mediaDevices, WebRTCMediaStream } from "./webrtc-primitives.native";
@@ -301,12 +312,31 @@ export function NativeIngestPlayer() {
301
312
  })
302
313
  .then((stream: WebRTCMediaStream) => {
303
314
  setLocalMediaStream(stream);
315
+
316
+ let errs: string[] = [];
317
+ if (stream.getAudioTracks().length === 0) {
318
+ console.warn("No audio tracks found in user media stream");
319
+ errs.push("microphone");
320
+ }
321
+ if (stream.getVideoTracks().length === 0) {
322
+ console.warn("No video tracks found in user media stream");
323
+ errs.push("camera");
324
+ }
325
+ if (errs.length > 0) {
326
+ setError(
327
+ new Error(
328
+ `We could not access your ${errs.join(" and ")}. To stream, you need to give us permission to access these.`,
329
+ ),
330
+ );
331
+ } else {
332
+ setError(null);
333
+ }
304
334
  })
305
335
  .catch((e: any) => {
306
336
  console.error("error getting user media", e);
307
337
  setError(
308
338
  new Error(
309
- "We could not access your camera or microphone. Please check your permissions.",
339
+ "We could not access your camera or microphone. To stream, you need to give us permission to access these.",
310
340
  ),
311
341
  );
312
342
  });
@@ -334,12 +364,27 @@ export function NativeIngestPlayer() {
334
364
  return (
335
365
  <View
336
366
  backgroundColor={colors.destructive[900]}
337
- style={[p[4], { borderRadius: borderRadius.md }]}
367
+ style={[p[4], m[4], gap.all[2], { borderRadius: borderRadius.md }]}
338
368
  >
339
369
  <View>
340
- <Text>Error encountered!</Text>
370
+ <Text style={[fontWeight.semibold]} size="2xl">
371
+ Error encountered!
372
+ </Text>
341
373
  </View>
342
374
  <Text>{error.message}</Text>
375
+ {error.message.includes(
376
+ "To stream, you need to give us permission to access these.",
377
+ ) && (
378
+ <Button
379
+ onPress={Linking.openSettings}
380
+ style={[h[10]]}
381
+ variant="secondary"
382
+ >
383
+ <View style={[layout.flex.row, gap.all[1]]}>
384
+ <Text>Open Settings</Text> <ArrowRight color="white" size="18" />
385
+ </View>
386
+ </Button>
387
+ )}
343
388
  </View>
344
389
  );
345
390
  }
@@ -1,5 +1,5 @@
1
1
  import { ChevronUp } from "lucide-react-native";
2
- import { ComponentProps } from "react";
2
+ import { ComponentProps, useEffect } from "react";
3
3
  import { Dimensions } from "react-native";
4
4
  import {
5
5
  Gesture,
@@ -23,6 +23,7 @@ const AnimatedView = Animated.createAnimatedComponent(View);
23
23
  const { height: SCREEN_HEIGHT } = Dimensions.get("window");
24
24
 
25
25
  type ResizableChatSheetProps = {
26
+ startingPercentage?: number;
26
27
  isPlayerRatioGreater: boolean;
27
28
  style?: ComponentProps<typeof AnimatedView>["style"];
28
29
  children?: React.ReactNode;
@@ -31,6 +32,7 @@ type ResizableChatSheetProps = {
31
32
  const SPRING_CONFIG = { damping: 20, stiffness: 100 };
32
33
 
33
34
  export function Resizable({
35
+ startingPercentage,
34
36
  isPlayerRatioGreater,
35
37
  style = {},
36
38
  children,
@@ -44,6 +46,15 @@ export function Resizable({
44
46
  const sheetHeight = useSharedValue(MIN_HEIGHT);
45
47
  const startHeight = useSharedValue(MIN_HEIGHT);
46
48
 
49
+ useEffect(() => {
50
+ setTimeout(() => {
51
+ sheetHeight.value = withSpring(
52
+ startingPercentage ? startingPercentage * SCREEN_HEIGHT : MIN_HEIGHT,
53
+ SPRING_CONFIG,
54
+ );
55
+ }, 1000);
56
+ }, []);
57
+
47
58
  const panGesture = Gesture.Pan()
48
59
  .onStart(() => {
49
60
  startHeight.value = sheetHeight.value;
@@ -70,7 +81,9 @@ export function Resizable({
70
81
  transform: [
71
82
  {
72
83
  translateY:
73
- slideKeyboard - safeBottom + Math.max(0, -sheetHeight.value),
84
+ slideKeyboard +
85
+ Math.max(0, -sheetHeight.value) +
86
+ (slideKeyboard < 0 ? 0 : -safeBottom),
74
87
  },
75
88
  ],
76
89
  }));
@@ -94,7 +107,6 @@ export function Resizable({
94
107
  w.percent[100],
95
108
  layout.flex.center,
96
109
  zIndex[1],
97
- { marginBottom: safeBottom },
98
110
  ]}
99
111
  >
100
112
  <Pressable
@@ -6,4 +6,5 @@ export * from "./useKeyboardSlide";
6
6
  export * from "./useLivestreamInfo";
7
7
  export * from "./useOuterAndInnerDimensions";
8
8
  export * from "./usePlayerDimensions";
9
+ export * from "./useSegmentDimensions";
9
10
  export * from "./useSegmentTiming";
@@ -0,0 +1,18 @@
1
+ import { useLivestreamStore } from "../livestream-store";
2
+
3
+ export function useSegmentDimensions() {
4
+ const latestSegment = useLivestreamStore((x) => x.segment);
5
+
6
+ let seg = latestSegment?.video && latestSegment.video[0];
7
+
8
+ let ratio = {
9
+ height: seg?.height || 0,
10
+ width: seg?.width || 0,
11
+ };
12
+
13
+ return {
14
+ isPlayerRatioGreater: ratio.width > ratio.height,
15
+ height: ratio.height,
16
+ width: ratio.width,
17
+ };
18
+ }