@streamplace/components 0.6.37 → 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 (138) hide show
  1. package/dist/components/chat/chat-box.js +109 -0
  2. package/dist/components/chat/chat-message.js +76 -0
  3. package/dist/components/chat/chat.js +56 -0
  4. package/dist/components/chat/mention-suggestions.js +39 -0
  5. package/dist/components/chat/mod-view.js +33 -0
  6. package/dist/components/mobile-player/fullscreen.js +69 -0
  7. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  8. package/dist/components/mobile-player/player.js +103 -0
  9. package/dist/components/mobile-player/props.js +1 -0
  10. package/dist/components/mobile-player/shared.js +51 -0
  11. package/dist/components/mobile-player/ui/countdown.js +79 -0
  12. package/dist/components/mobile-player/ui/index.js +5 -0
  13. package/dist/components/mobile-player/ui/input.js +38 -0
  14. package/dist/components/mobile-player/ui/metrics.js +40 -0
  15. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  17. package/dist/components/mobile-player/use-webrtc.js +232 -0
  18. package/dist/components/mobile-player/video.js +375 -0
  19. package/dist/components/mobile-player/video.native.js +238 -0
  20. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  21. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  22. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  23. package/dist/components/ui/button.js +220 -0
  24. package/dist/components/ui/dialog.js +203 -0
  25. package/dist/components/ui/dropdown.js +148 -0
  26. package/dist/components/ui/icons.js +22 -0
  27. package/dist/components/ui/index.js +22 -0
  28. package/dist/components/ui/input.js +202 -0
  29. package/dist/components/ui/loader.js +7 -0
  30. package/dist/components/ui/primitives/button.js +121 -0
  31. package/dist/components/ui/primitives/input.js +202 -0
  32. package/dist/components/ui/primitives/modal.js +203 -0
  33. package/dist/components/ui/primitives/text.js +286 -0
  34. package/dist/components/ui/resizeable.js +101 -0
  35. package/dist/components/ui/text.js +175 -0
  36. package/dist/components/ui/textarea.js +17 -0
  37. package/dist/components/ui/toast.js +129 -0
  38. package/dist/components/ui/view.js +250 -0
  39. package/dist/hooks/index.js +9 -0
  40. package/dist/hooks/useAvatars.js +32 -0
  41. package/dist/hooks/useCameraToggle.js +9 -0
  42. package/dist/hooks/useKeyboard.js +33 -0
  43. package/dist/hooks/useKeyboardSlide.js +11 -0
  44. package/dist/hooks/useLivestreamInfo.js +62 -0
  45. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  46. package/dist/hooks/usePlayerDimensions.js +19 -0
  47. package/dist/hooks/useSegmentTiming.js +62 -0
  48. package/dist/index.js +10 -0
  49. package/dist/lib/facet.js +88 -0
  50. package/dist/lib/theme/atoms.js +620 -0
  51. package/dist/lib/theme/atoms.types.js +5 -0
  52. package/dist/lib/theme/index.js +9 -0
  53. package/dist/lib/theme/theme.js +248 -0
  54. package/dist/lib/theme/tokens.js +383 -0
  55. package/dist/lib/utils.js +94 -0
  56. package/dist/livestream-provider/index.js +8 -3
  57. package/dist/livestream-store/chat.js +89 -65
  58. package/dist/livestream-store/index.js +1 -0
  59. package/dist/livestream-store/livestream-store.js +3 -0
  60. package/dist/livestream-store/stream-key.js +115 -0
  61. package/dist/player-store/player-provider.js +0 -1
  62. package/dist/player-store/player-store.js +13 -0
  63. package/dist/streamplace-store/block.js +23 -0
  64. package/dist/streamplace-store/index.js +1 -0
  65. package/dist/streamplace-store/stream.js +193 -0
  66. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  67. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  68. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  69. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  70. package/package.json +20 -4
  71. package/src/components/chat/chat-box.tsx +195 -0
  72. package/src/components/chat/chat-message.tsx +192 -0
  73. package/src/components/chat/chat.tsx +128 -0
  74. package/src/components/chat/mention-suggestions.tsx +71 -0
  75. package/src/components/chat/mod-view.tsx +118 -0
  76. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  77. package/src/components/mobile-player/fullscreen.tsx +79 -0
  78. package/src/components/mobile-player/player.tsx +134 -0
  79. package/src/components/mobile-player/props.tsx +11 -0
  80. package/src/components/mobile-player/shared.tsx +56 -0
  81. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  82. package/src/components/mobile-player/ui/index.ts +5 -0
  83. package/src/components/mobile-player/ui/input.tsx +85 -0
  84. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  85. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  86. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  87. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  88. package/src/components/mobile-player/video.native.tsx +360 -0
  89. package/src/components/mobile-player/video.tsx +557 -0
  90. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  91. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  92. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  93. package/src/components/ui/button.tsx +309 -0
  94. package/src/components/ui/dialog.tsx +376 -0
  95. package/src/components/ui/dropdown.tsx +399 -0
  96. package/src/components/ui/icons.tsx +50 -0
  97. package/src/components/ui/index.ts +33 -0
  98. package/src/components/ui/input.tsx +350 -0
  99. package/src/components/ui/loader.tsx +9 -0
  100. package/src/components/ui/primitives/button.tsx +292 -0
  101. package/src/components/ui/primitives/input.tsx +422 -0
  102. package/src/components/ui/primitives/modal.tsx +421 -0
  103. package/src/components/ui/primitives/text.tsx +499 -0
  104. package/src/components/ui/resizeable.tsx +169 -0
  105. package/src/components/ui/text.tsx +330 -0
  106. package/src/components/ui/textarea.tsx +34 -0
  107. package/src/components/ui/toast.tsx +203 -0
  108. package/src/components/ui/view.tsx +344 -0
  109. package/src/hooks/index.ts +9 -0
  110. package/src/hooks/useAvatars.tsx +44 -0
  111. package/src/hooks/useCameraToggle.ts +12 -0
  112. package/src/hooks/useKeyboard.tsx +41 -0
  113. package/src/hooks/useKeyboardSlide.ts +12 -0
  114. package/src/hooks/useLivestreamInfo.ts +67 -0
  115. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  116. package/src/hooks/usePlayerDimensions.ts +23 -0
  117. package/src/hooks/useSegmentTiming.tsx +88 -0
  118. package/src/index.tsx +21 -0
  119. package/src/lib/facet.ts +131 -0
  120. package/src/lib/theme/atoms.ts +760 -0
  121. package/src/lib/theme/atoms.types.ts +258 -0
  122. package/src/lib/theme/index.ts +48 -0
  123. package/src/lib/theme/theme.tsx +436 -0
  124. package/src/lib/theme/tokens.ts +409 -0
  125. package/src/lib/utils.ts +132 -0
  126. package/src/livestream-provider/index.tsx +13 -2
  127. package/src/livestream-store/chat.tsx +115 -78
  128. package/src/livestream-store/index.tsx +1 -0
  129. package/src/livestream-store/livestream-state.tsx +3 -0
  130. package/src/livestream-store/livestream-store.tsx +3 -0
  131. package/src/livestream-store/stream-key.tsx +124 -0
  132. package/src/player-store/player-provider.tsx +0 -1
  133. package/src/player-store/player-state.tsx +28 -0
  134. package/src/player-store/player-store.tsx +22 -0
  135. package/src/streamplace-store/block.tsx +29 -0
  136. package/src/streamplace-store/index.tsx +1 -0
  137. package/src/streamplace-store/stream.tsx +262 -0
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,118 @@
1
+ import { TriggerRef } from "@rn-primitives/dropdown-menu";
2
+ import { forwardRef, useEffect, useRef } from "react";
3
+ import { gap, mr } from "../../lib/theme/atoms";
4
+ import { usePlayerStore } from "../../player-store";
5
+ import { useCreateBlockRecord } from "../../streamplace-store/block";
6
+ import { usePDSAgent } from "../../streamplace-store/xrpc";
7
+
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuGroup,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ layout,
14
+ ResponsiveDropdownMenuContent,
15
+ Text,
16
+ View,
17
+ } from "../ui";
18
+ import { RenderChatMessage } from "./chat-message";
19
+
20
+ type ModViewProps = {
21
+ onClose?: () => void;
22
+ // onDeleteMessage?: (msg: ChatMessageViewHydrated) => void;
23
+ // onBanUser?: (userHandle: string) => void;
24
+ };
25
+
26
+ export type ModViewRef = {
27
+ open: () => void;
28
+ close: () => void;
29
+ };
30
+
31
+ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
32
+ const triggerRef = useRef<TriggerRef>(null);
33
+ const message = usePlayerStore((state) => state.modMessage);
34
+
35
+ let agent = usePDSAgent();
36
+ let createBlockRecord = useCreateBlockRecord();
37
+
38
+ if (!agent?.did) {
39
+ <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
40
+ <Text>Log in to submit mod actions</Text>
41
+ </View>;
42
+ }
43
+
44
+ useEffect(() => {
45
+ if (message) {
46
+ console.log("opening mod view");
47
+ triggerRef.current?.open();
48
+ } else {
49
+ console.log("closing mod view");
50
+ triggerRef.current?.close();
51
+ }
52
+ }, [message]);
53
+
54
+ return (
55
+ <DropdownMenu>
56
+ <DropdownMenuTrigger ref={triggerRef}>
57
+ {/* Hidden trigger */}
58
+ <View />
59
+ </DropdownMenuTrigger>
60
+ <ResponsiveDropdownMenuContent>
61
+ {message && (
62
+ <>
63
+ <DropdownMenuGroup>
64
+ <DropdownMenuItem>
65
+ <View style={[layout.flex.column, mr[5], { gap: 6 }]}>
66
+ <RenderChatMessage item={message} />
67
+ </View>
68
+ </DropdownMenuItem>
69
+ </DropdownMenuGroup>
70
+
71
+ <DropdownMenuGroup title={`Moderation actions`}>
72
+ {/* <DropdownMenuItem
73
+ onPress={
74
+ onDeleteMessage
75
+ ? () => onDeleteMessage(modMessage)
76
+ : undefined
77
+ }
78
+ >
79
+ <Text customColor={colors.ios.systemTeal}>
80
+ Delete message
81
+ </Text>
82
+ </DropdownMenuItem>
83
+ <DropdownMenuSeparator />
84
+ <DropdownMenuItem
85
+ onPress={
86
+ onBanUser
87
+ ? () => onBanUser(modMessage.author.handle)
88
+ : undefined
89
+ }
90
+ >
91
+ <Text color="destructive">
92
+ Ban user @{modMessage.author.handle}
93
+ </Text>
94
+ </DropdownMenuItem> */}
95
+ <DropdownMenuItem
96
+ disabled={message.author.did === agent?.did}
97
+ onPress={() => {
98
+ console.log("Creating block record");
99
+ createBlockRecord(message.author.did)
100
+ .then((r) => console.log(r))
101
+ .catch((e) => console.error(e));
102
+ }}
103
+ >
104
+ <Text color="destructive">
105
+ {message.author.did === agent?.did ? (
106
+ <>Block yourself (you can't block yourself)</>
107
+ ) : (
108
+ <>Block user @{message.author.handle} from this channel</>
109
+ )}
110
+ </Text>
111
+ </DropdownMenuItem>
112
+ </DropdownMenuGroup>
113
+ </>
114
+ )}
115
+ </ResponsiveDropdownMenuContent>
116
+ </DropdownMenu>
117
+ );
118
+ });
@@ -0,0 +1,193 @@
1
+ import { useNavigation } from "@react-navigation/native";
2
+ import { VideoView } from "expo-video";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { BackHandler, Dimensions, StyleSheet, View } from "react-native";
5
+ import { SystemBars } from "react-native-edge-to-edge";
6
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
7
+ import { PlayerProtocol, useLivestreamStore, usePlayerStore } from "../..";
8
+ import Video from "./video.native";
9
+
10
+ // Standard 16:9 video aspect ratio
11
+ const VIDEO_ASPECT_RATIO = 16 / 9;
12
+
13
+ export function Fullscreen(props: { src: string }) {
14
+ const ref = useRef<VideoView>(null);
15
+ const insets = useSafeAreaInsets();
16
+ const navigation = useNavigation();
17
+ const [dimensions, setDimensions] = useState(Dimensions.get("window"));
18
+
19
+ // Get state from player store
20
+ const protocol = usePlayerStore((x) => x.protocol);
21
+ const fullscreen = usePlayerStore((x) => x.fullscreen);
22
+ const setFullscreen = usePlayerStore((x) => x.setFullscreen);
23
+ const handle = useLivestreamStore((x) => x.profile?.handle);
24
+
25
+ const setSrc = usePlayerStore((x) => x.setSrc);
26
+
27
+ useEffect(() => {
28
+ setSrc(props.src);
29
+ }, [props.src]);
30
+
31
+ // Re-calculate dimensions on orientation change
32
+ useEffect(() => {
33
+ const updateDimensions = () => {
34
+ setDimensions(Dimensions.get("window"));
35
+ };
36
+
37
+ const subscription = Dimensions.addEventListener(
38
+ "change",
39
+ updateDimensions,
40
+ );
41
+
42
+ return () => {
43
+ subscription.remove();
44
+ };
45
+ }, []);
46
+
47
+ // Hide status bar when in fullscreen mode
48
+ useEffect(() => {
49
+ if (fullscreen) {
50
+ SystemBars.setHidden(true);
51
+ console.log("setting sidebar hidden");
52
+
53
+ // Hide the navigation header
54
+ navigation.setOptions({
55
+ headerShown: false,
56
+ });
57
+
58
+ // Handle hardware back button
59
+ const backHandler = BackHandler.addEventListener(
60
+ "hardwareBackPress",
61
+ () => {
62
+ setFullscreen(false);
63
+ return true;
64
+ },
65
+ );
66
+
67
+ return () => {
68
+ backHandler.remove();
69
+ };
70
+ } else {
71
+ SystemBars.setHidden(false);
72
+
73
+ // Restore the navigation header
74
+ navigation.setOptions({
75
+ headerShown: true,
76
+ });
77
+ }
78
+
79
+ return () => {
80
+ SystemBars.setHidden(false);
81
+ // Ensure header is restored if component unmounts
82
+ navigation.setOptions({
83
+ headerShown: true,
84
+ });
85
+ };
86
+ }, [fullscreen, navigation, setFullscreen]);
87
+
88
+ // Handle fullscreen state changes for native video players
89
+ useEffect(() => {
90
+ // For WebRTC, we handle fullscreen manually via the custom implementation
91
+ if (protocol === PlayerProtocol.WEBRTC) {
92
+ return;
93
+ }
94
+
95
+ // For HLS and other protocols, sync with native fullscreen
96
+ if (ref.current) {
97
+ if (fullscreen) {
98
+ ref.current.enterFullscreen();
99
+ } else {
100
+ ref.current.exitFullscreen();
101
+ }
102
+ }
103
+ }, [fullscreen, protocol]);
104
+
105
+ if (fullscreen && protocol === PlayerProtocol.WEBRTC) {
106
+ // Determine if we're in landscape mode
107
+ const isLandscape = dimensions.width > dimensions.height;
108
+
109
+ // Calculate video container dimensions based on screen size and orientation
110
+ let videoWidth: number;
111
+ let videoHeight: number;
112
+
113
+ if (isLandscape) {
114
+ // In landscape, account for safe areas and use available height
115
+ const availableHeight = dimensions.height - (insets.top + insets.bottom);
116
+ const availableWidth = dimensions.width - (insets.left + insets.right);
117
+
118
+ videoHeight = availableHeight;
119
+ videoWidth = videoHeight * VIDEO_ASPECT_RATIO;
120
+
121
+ // If calculated width exceeds available width, constrain and maintain aspect ratio
122
+ if (videoWidth > availableWidth) {
123
+ videoWidth = availableWidth;
124
+ videoHeight = videoWidth / VIDEO_ASPECT_RATIO;
125
+ }
126
+ } else {
127
+ // In portrait, account for safe areas
128
+ const availableWidth = dimensions.width - (insets.left + insets.right);
129
+ videoWidth = availableWidth;
130
+ videoHeight = videoWidth / VIDEO_ASPECT_RATIO;
131
+ }
132
+
133
+ // Calculate position to center the video, accounting for safe areas
134
+ const leftPosition = (dimensions.width - videoWidth) / 2;
135
+ const topPosition = (dimensions.height - videoHeight) / 2;
136
+
137
+ // When in custom fullscreen mode
138
+ return (
139
+ <View
140
+ style={[
141
+ styles.fullscreenContainer,
142
+ {
143
+ width: isLandscape ? dimensions.width + 40 : dimensions.width,
144
+ height: dimensions.height,
145
+ },
146
+ ]}
147
+ >
148
+ <View
149
+ style={[
150
+ styles.videoContainer,
151
+ {
152
+ width: isLandscape ? videoWidth + 40 : videoWidth,
153
+ height: videoHeight,
154
+ left: leftPosition,
155
+ top: topPosition,
156
+ },
157
+ ]}
158
+ >
159
+ <Video />
160
+ </View>
161
+ </View>
162
+ );
163
+ }
164
+
165
+ // Normal non-fullscreen mode
166
+ return (
167
+ <>
168
+ <Video />
169
+ </>
170
+ );
171
+ }
172
+
173
+ const styles = StyleSheet.create({
174
+ fullscreenContainer: {
175
+ position: "absolute",
176
+ top: 0,
177
+ left: 0,
178
+ right: 0,
179
+ bottom: 0,
180
+ backgroundColor: "#000",
181
+ zIndex: 9999,
182
+ elevation: 9999,
183
+ margin: 0,
184
+ padding: 0,
185
+ justifyContent: "center",
186
+ alignItems: "center",
187
+ },
188
+ videoContainer: {
189
+ position: "absolute",
190
+ backgroundColor: "#111",
191
+ overflow: "hidden",
192
+ },
193
+ });
@@ -0,0 +1,79 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { View as RNView } from "react-native";
3
+ import { getFirstPlayerID, usePlayerStore } from "../..";
4
+ import { View } from "../../components/ui";
5
+ import Video from "./video";
6
+
7
+ export function Fullscreen(props: { src: string }) {
8
+ const playerId = getFirstPlayerID();
9
+ const protocol = usePlayerStore((x) => x.protocol, playerId);
10
+ const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
11
+ const setFullscreen = usePlayerStore((x) => x.setFullscreen, playerId);
12
+ const setSrc = usePlayerStore((x) => x.setSrc);
13
+
14
+ const divRef = useRef<RNView>(null);
15
+ const videoRef = useRef<HTMLVideoElement | null>(null);
16
+
17
+ useEffect(() => {
18
+ setSrc(props.src);
19
+ }, [props.src]);
20
+
21
+ useEffect(() => {
22
+ if (!divRef.current) {
23
+ return;
24
+ }
25
+ (async () => {
26
+ if (fullscreen && !document.fullscreenElement) {
27
+ try {
28
+ const div = divRef.current as unknown as HTMLDivElement;
29
+ if (typeof div.requestFullscreen === "function") {
30
+ await div.requestFullscreen();
31
+ } else if (videoRef.current) {
32
+ if (
33
+ typeof (videoRef.current as any).webkitEnterFullscreen ===
34
+ "function"
35
+ ) {
36
+ await (videoRef.current as any).webkitEnterFullscreen();
37
+ } else if (
38
+ typeof videoRef.current.requestFullscreen === "function"
39
+ ) {
40
+ await videoRef.current.requestFullscreen();
41
+ }
42
+ }
43
+ setFullscreen(true);
44
+ } catch (e) {
45
+ console.error("fullscreen failed", e.message);
46
+ }
47
+ }
48
+ if (!fullscreen) {
49
+ if (document.fullscreenElement) {
50
+ try {
51
+ await document.exitFullscreen();
52
+ } catch (e) {
53
+ console.error("fullscreen exit failed", e.message);
54
+ }
55
+ }
56
+ setFullscreen(false);
57
+ }
58
+ })();
59
+ }, [fullscreen, protocol]);
60
+
61
+ useEffect(() => {
62
+ const listener = () => {
63
+ console.log("fullscreenchange", document.fullscreenElement);
64
+ setFullscreen(!!document.fullscreenElement);
65
+ };
66
+ document.body.addEventListener("fullscreenchange", listener);
67
+ document.body.addEventListener("webkitfullscreenchange", listener);
68
+ return () => {
69
+ document.body.removeEventListener("fullscreenchange", listener);
70
+ document.body.removeEventListener("webkitfullscreenchange", listener);
71
+ };
72
+ }, []);
73
+
74
+ return (
75
+ <View ref={divRef}>
76
+ <Video />
77
+ </View>
78
+ );
79
+ }
@@ -0,0 +1,134 @@
1
+ import { useEffect, useState } from "react";
2
+ import { flex, layout, w, zIndex } from "../../lib/theme/atoms";
3
+ import { useSegment } from "../../livestream-store";
4
+ import {
5
+ PlayerStatus,
6
+ PlayerStatusTracker,
7
+ usePlayerStore,
8
+ } from "../../player-store";
9
+ import { useStreamplaceStore } from "../../streamplace-store";
10
+ import { Text, View } from "../ui";
11
+ import { Fullscreen } from "./fullscreen";
12
+ import { PlayerProps } from "./props";
13
+
14
+ const OFFLINE_THRESHOLD = 10000;
15
+
16
+ export * as PlayerUI from "./ui";
17
+
18
+ export function Player(props: Partial<PlayerProps>) {
19
+ const playing = usePlayerStore((x) => x.status === PlayerStatus.PLAYING);
20
+
21
+ const setOffline = usePlayerStore((x) => x.setOffline);
22
+ const setIngest = usePlayerStore((x) => x.setIngestConnectionState);
23
+
24
+ const clearControlsTimeout = usePlayerStore((x) => x.clearControlsTimeout);
25
+
26
+ // Will call back every few seconds to send health updates
27
+ usePlayerStatus();
28
+
29
+ useEffect(() => {
30
+ setIngest(props.ingest ? "new" : null);
31
+ }, []);
32
+
33
+ if (typeof props.src !== "string") {
34
+ return (
35
+ <View>
36
+ <Text>No source provided 🤷</Text>
37
+ </View>
38
+ );
39
+ }
40
+
41
+ useEffect(() => {
42
+ return () => {
43
+ clearControlsTimeout();
44
+ };
45
+ }, []);
46
+
47
+ const segment = useSegment();
48
+ const [lastCheck, setLastCheck] = useState(0);
49
+
50
+ useEffect(() => {
51
+ if (playing) {
52
+ setOffline(false);
53
+ return;
54
+ }
55
+ if (!segment) {
56
+ setOffline(false);
57
+ return;
58
+ }
59
+ const startTime = Date.parse(segment.startTime);
60
+ if (!startTime) {
61
+ console.error("startTime is not a number", segment.startTime);
62
+ return;
63
+ }
64
+ const timeSinceStart = Date.now() - startTime;
65
+ if (timeSinceStart > OFFLINE_THRESHOLD) {
66
+ setOffline(true);
67
+ return;
68
+ }
69
+ const handle = setTimeout(() => {
70
+ setLastCheck(Date.now());
71
+ }, 1000);
72
+ return () => clearTimeout(handle);
73
+ }, [segment, playing, lastCheck]);
74
+
75
+ return (
76
+ <>
77
+ <View
78
+ style={[zIndex[0], flex.values[1], w.percent[100], layout.flex.center]}
79
+ >
80
+ <Fullscreen src={props.src}></Fullscreen>
81
+ </View>
82
+ </>
83
+ );
84
+ }
85
+
86
+ const POLL_INTERVAL = 5000;
87
+ export function usePlayerStatus(): [PlayerStatus] {
88
+ const playerStatus = usePlayerStore((x) => x.status);
89
+ const url = useStreamplaceStore((x) => x.url);
90
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
91
+ const [whatDoing, setWhatDoing] = useState<PlayerStatus>(PlayerStatus.START);
92
+ const [whatDid, setWhatDid] = useState<PlayerStatusTracker>({});
93
+ const [doingSince, setDoingSince] = useState(Date.now());
94
+ const [lastUpdated, setLastUpdated] = useState(0);
95
+ const updateWhatDid = (now: Date): PlayerStatusTracker => {
96
+ const prev = whatDid[whatDoing] ?? 0;
97
+ const duration = now.getTime() - doingSince;
98
+ const ret = {
99
+ ...whatDid,
100
+ [whatDoing]: prev + duration,
101
+ };
102
+ return ret;
103
+ };
104
+ // callback to update the status
105
+ useEffect(() => {
106
+ const now = new Date();
107
+ if (playerStatus !== whatDoing) {
108
+ setWhatDid(updateWhatDid(now));
109
+ setWhatDoing(playerStatus);
110
+ setDoingSince(now.getTime());
111
+ }
112
+ }, [playerStatus]);
113
+
114
+ useEffect(() => {
115
+ if (lastUpdated === 0) {
116
+ return;
117
+ }
118
+ const now = new Date();
119
+ const fullWhatDid = updateWhatDid(now);
120
+ setWhatDid({} as PlayerStatusTracker);
121
+ setDoingSince(now.getTime());
122
+ playerEvent(url, now.toISOString(), "aq-played", {
123
+ whatHappened: fullWhatDid,
124
+ });
125
+ }, [lastUpdated]);
126
+
127
+ useEffect(() => {
128
+ const interval = setInterval((_) => {
129
+ setLastUpdated(Date.now());
130
+ }, POLL_INTERVAL);
131
+ return () => clearInterval(interval);
132
+ }, []);
133
+ return [whatDoing];
134
+ }
@@ -0,0 +1,11 @@
1
+ export type PlayerProps = {
2
+ name: string;
3
+ playerId?: string;
4
+ src: string;
5
+ muted: boolean;
6
+ telemetry: boolean;
7
+ fullscreen: boolean;
8
+ setFullscreen: (isFullscreen: boolean) => void;
9
+ ingest?: boolean;
10
+ embedded?: boolean;
11
+ };
@@ -0,0 +1,56 @@
1
+ import { useMemo } from "react";
2
+ import { PlayerProtocol, useStreamplaceStore } from "../..";
3
+
4
+ const protocolSuffixes = {
5
+ m3u8: PlayerProtocol.HLS,
6
+ mp4: PlayerProtocol.PROGRESSIVE_MP4,
7
+ webm: PlayerProtocol.PROGRESSIVE_WEBM,
8
+ webrtc: PlayerProtocol.WEBRTC,
9
+ };
10
+
11
+ export function srcToUrl(
12
+ props: {
13
+ src: string;
14
+ selectedRendition?: string;
15
+ },
16
+ protocol: PlayerProtocol,
17
+ ): {
18
+ url: string;
19
+ protocol: string;
20
+ } {
21
+ const url = useStreamplaceStore((x) => x.url);
22
+ return useMemo(() => {
23
+ if (props.src.startsWith("http://") || props.src.startsWith("https://")) {
24
+ const segments = props.src.split(/[./]/);
25
+ const suffix = segments[segments.length - 1];
26
+ if (protocolSuffixes[suffix]) {
27
+ return {
28
+ url: props.src,
29
+ protocol: protocolSuffixes[suffix],
30
+ };
31
+ } else {
32
+ throw new Error(`unknown playback protocol: ${suffix}`);
33
+ }
34
+ }
35
+ let outUrl: string;
36
+ if (protocol === PlayerProtocol.HLS) {
37
+ if (props.selectedRendition === "auto") {
38
+ outUrl = `${url}/api/playback/${props.src}/hls/index.m3u8`;
39
+ } else {
40
+ outUrl = `${url}/api/playback/${props.src}/hls/index.m3u8?rendition=${props.selectedRendition || "source"}`;
41
+ }
42
+ } else if (protocol === PlayerProtocol.PROGRESSIVE_MP4) {
43
+ outUrl = `${url}/api/playback/${props.src}/stream.mp4`;
44
+ } else if (protocol === PlayerProtocol.PROGRESSIVE_WEBM) {
45
+ outUrl = `${url}/api/playback/${props.src}/stream.webm`;
46
+ } else if (protocol === PlayerProtocol.WEBRTC) {
47
+ outUrl = `${url}/api/playback/${props.src}/webrtc?rendition=${props.selectedRendition || "source"}`;
48
+ } else {
49
+ throw new Error(`unknown playback protocol: ${protocol}`);
50
+ }
51
+ return {
52
+ protocol: protocol,
53
+ url: outUrl,
54
+ };
55
+ }, [props.src, props.selectedRendition, protocol, url]);
56
+ }