@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.
- package/dist/components/chat/chat-box.js +109 -0
- package/dist/components/chat/chat-message.js +76 -0
- package/dist/components/chat/chat.js +56 -0
- package/dist/components/chat/mention-suggestions.js +39 -0
- package/dist/components/chat/mod-view.js +33 -0
- package/dist/components/mobile-player/fullscreen.js +69 -0
- package/dist/components/mobile-player/fullscreen.native.js +151 -0
- package/dist/components/mobile-player/player.js +103 -0
- package/dist/components/mobile-player/props.js +1 -0
- package/dist/components/mobile-player/shared.js +51 -0
- package/dist/components/mobile-player/ui/countdown.js +79 -0
- package/dist/components/mobile-player/ui/index.js +5 -0
- package/dist/components/mobile-player/ui/input.js +38 -0
- package/dist/components/mobile-player/ui/metrics.js +40 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
- package/dist/components/mobile-player/use-webrtc.js +232 -0
- package/dist/components/mobile-player/video.js +375 -0
- package/dist/components/mobile-player/video.native.js +238 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
- package/dist/components/mobile-player/webrtc-primitives.js +25 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
- package/dist/components/ui/button.js +220 -0
- package/dist/components/ui/dialog.js +203 -0
- package/dist/components/ui/dropdown.js +148 -0
- package/dist/components/ui/icons.js +22 -0
- package/dist/components/ui/index.js +22 -0
- package/dist/components/ui/input.js +202 -0
- package/dist/components/ui/loader.js +7 -0
- package/dist/components/ui/primitives/button.js +121 -0
- package/dist/components/ui/primitives/input.js +202 -0
- package/dist/components/ui/primitives/modal.js +203 -0
- package/dist/components/ui/primitives/text.js +286 -0
- package/dist/components/ui/resizeable.js +101 -0
- package/dist/components/ui/text.js +175 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/toast.js +129 -0
- package/dist/components/ui/view.js +250 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useAvatars.js +32 -0
- package/dist/hooks/useCameraToggle.js +9 -0
- package/dist/hooks/useKeyboard.js +33 -0
- package/dist/hooks/useKeyboardSlide.js +11 -0
- package/dist/hooks/useLivestreamInfo.js +62 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
- package/dist/hooks/usePlayerDimensions.js +19 -0
- package/dist/hooks/useSegmentTiming.js +62 -0
- package/dist/index.js +10 -0
- package/dist/lib/facet.js +88 -0
- package/dist/lib/theme/atoms.js +620 -0
- package/dist/lib/theme/atoms.types.js +5 -0
- package/dist/lib/theme/index.js +9 -0
- package/dist/lib/theme/theme.js +248 -0
- package/dist/lib/theme/tokens.js +383 -0
- package/dist/lib/utils.js +94 -0
- package/dist/livestream-provider/index.js +8 -3
- package/dist/livestream-store/chat.js +89 -65
- package/dist/livestream-store/index.js +1 -0
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/stream-key.js +115 -0
- package/dist/player-store/player-provider.js +0 -1
- package/dist/player-store/player-store.js +13 -0
- package/dist/streamplace-store/block.js +23 -0
- package/dist/streamplace-store/index.js +1 -0
- package/dist/streamplace-store/stream.js +193 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +20 -4
- package/src/components/chat/chat-box.tsx +195 -0
- package/src/components/chat/chat-message.tsx +192 -0
- package/src/components/chat/chat.tsx +128 -0
- package/src/components/chat/mention-suggestions.tsx +71 -0
- package/src/components/chat/mod-view.tsx +118 -0
- package/src/components/mobile-player/fullscreen.native.tsx +193 -0
- package/src/components/mobile-player/fullscreen.tsx +79 -0
- package/src/components/mobile-player/player.tsx +134 -0
- package/src/components/mobile-player/props.tsx +11 -0
- package/src/components/mobile-player/shared.tsx +56 -0
- package/src/components/mobile-player/ui/countdown.tsx +119 -0
- package/src/components/mobile-player/ui/index.ts +5 -0
- package/src/components/mobile-player/ui/input.tsx +85 -0
- package/src/components/mobile-player/ui/metrics.tsx +69 -0
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
- package/src/components/mobile-player/use-webrtc.tsx +282 -0
- package/src/components/mobile-player/video.native.tsx +360 -0
- package/src/components/mobile-player/video.tsx +557 -0
- package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
- package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
- package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
- package/src/components/ui/button.tsx +309 -0
- package/src/components/ui/dialog.tsx +376 -0
- package/src/components/ui/dropdown.tsx +399 -0
- package/src/components/ui/icons.tsx +50 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +350 -0
- package/src/components/ui/loader.tsx +9 -0
- package/src/components/ui/primitives/button.tsx +292 -0
- package/src/components/ui/primitives/input.tsx +422 -0
- package/src/components/ui/primitives/modal.tsx +421 -0
- package/src/components/ui/primitives/text.tsx +499 -0
- package/src/components/ui/resizeable.tsx +169 -0
- package/src/components/ui/text.tsx +330 -0
- package/src/components/ui/textarea.tsx +34 -0
- package/src/components/ui/toast.tsx +203 -0
- package/src/components/ui/view.tsx +344 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAvatars.tsx +44 -0
- package/src/hooks/useCameraToggle.ts +12 -0
- package/src/hooks/useKeyboard.tsx +41 -0
- package/src/hooks/useKeyboardSlide.ts +12 -0
- package/src/hooks/useLivestreamInfo.ts +67 -0
- package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
- package/src/hooks/usePlayerDimensions.ts +23 -0
- package/src/hooks/useSegmentTiming.tsx +88 -0
- package/src/index.tsx +21 -0
- package/src/lib/facet.ts +131 -0
- package/src/lib/theme/atoms.ts +760 -0
- package/src/lib/theme/atoms.types.ts +258 -0
- package/src/lib/theme/index.ts +48 -0
- package/src/lib/theme/theme.tsx +436 -0
- package/src/lib/theme/tokens.ts +409 -0
- package/src/lib/utils.ts +132 -0
- package/src/livestream-provider/index.tsx +13 -2
- package/src/livestream-store/chat.tsx +115 -78
- package/src/livestream-store/index.tsx +1 -0
- package/src/livestream-store/livestream-state.tsx +3 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/stream-key.tsx +124 -0
- package/src/player-store/player-provider.tsx +0 -1
- package/src/player-store/player-state.tsx +28 -0
- package/src/player-store/player-store.tsx +22 -0
- package/src/streamplace-store/block.tsx +29 -0
- package/src/streamplace-store/index.tsx +1 -0
- package/src/streamplace-store/stream.tsx +262 -0
- 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,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
|
+
}
|