@streamplace/components 0.7.3 → 0.7.7
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 +212 -24
- package/dist/components/chat/chat-message.js +5 -5
- package/dist/components/chat/chat.js +83 -5
- package/dist/components/chat/emoji-suggestions.js +35 -0
- package/dist/components/chat/mod-view.js +59 -8
- package/dist/components/chat/system-message.js +19 -0
- package/dist/components/icons/bluesky-icon.js +9 -0
- package/dist/components/keep-awake.js +7 -0
- package/dist/components/keep-awake.native.js +16 -0
- package/dist/components/mobile-player/fullscreen.js +2 -1
- package/dist/components/mobile-player/fullscreen.native.js +3 -3
- package/dist/components/mobile-player/player.js +15 -30
- package/dist/components/mobile-player/ui/index.js +2 -1
- package/dist/components/mobile-player/ui/report-modal.js +90 -0
- package/dist/components/mobile-player/ui/{loading.js → streamer-loading-overlay.js} +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
- package/dist/components/mobile-player/use-webrtc.js +7 -1
- package/dist/components/mobile-player/video-retry.js +29 -0
- package/dist/components/mobile-player/video.js +84 -9
- package/dist/components/mobile-player/video.native.js +24 -10
- package/dist/components/share/sharesheet.js +91 -0
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/dropdown.js +6 -6
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/primitives/modal.js +0 -1
- package/dist/components/ui/slider.js +5 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/usePointerDevice.js +71 -0
- package/dist/index.js +10 -3
- package/dist/lib/system-messages.js +101 -0
- package/dist/livestream-store/chat.js +111 -18
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/problems.js +76 -0
- package/dist/livestream-store/websocket-consumer.js +39 -4
- package/dist/player-store/player-store.js +30 -4
- package/dist/streamplace-store/block.js +51 -12
- package/dist/ui/index.js +79 -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 +6 -2
- package/src/components/chat/chat-box.tsx +295 -25
- package/src/components/chat/chat-message.tsx +6 -7
- package/src/components/chat/chat.tsx +192 -41
- package/src/components/chat/emoji-suggestions.tsx +94 -0
- package/src/components/chat/mod-view.tsx +119 -40
- package/src/components/chat/system-message.tsx +38 -0
- package/src/components/icons/bluesky-icon.tsx +9 -0
- package/src/components/keep-awake.native.tsx +13 -0
- package/src/components/keep-awake.tsx +3 -0
- package/src/components/mobile-player/fullscreen.native.tsx +12 -3
- package/src/components/mobile-player/fullscreen.tsx +10 -3
- package/src/components/mobile-player/player.tsx +28 -36
- package/src/components/mobile-player/props.tsx +1 -0
- package/src/components/mobile-player/ui/index.ts +2 -1
- package/src/components/mobile-player/ui/report-modal.tsx +195 -0
- package/src/components/mobile-player/ui/{loading.tsx → streamer-loading-overlay.tsx} +1 -1
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
- package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
- package/src/components/mobile-player/use-webrtc.tsx +10 -2
- package/src/components/mobile-player/video-retry.tsx +28 -0
- package/src/components/mobile-player/video.native.tsx +24 -10
- package/src/components/mobile-player/video.tsx +100 -21
- package/src/components/share/sharesheet.tsx +185 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/dropdown.tsx +13 -13
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/primitives/modal.tsx +0 -1
- package/src/components/ui/slider.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePointerDevice.ts +89 -0
- package/src/index.tsx +11 -2
- package/src/lib/system-messages.ts +135 -0
- package/src/livestream-store/chat.tsx +145 -17
- package/src/livestream-store/livestream-state.tsx +10 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/problems.tsx +96 -0
- package/src/livestream-store/websocket-consumer.tsx +44 -4
- package/src/player-store/player-state.tsx +21 -4
- package/src/player-store/player-store.tsx +38 -5
- package/src/streamplace-store/block.tsx +55 -13
- package/src/ui/index.ts +86 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -4,13 +4,18 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { BackHandler, Dimensions, StyleSheet, View } from "react-native";
|
|
5
5
|
import { SystemBars } from "react-native-edge-to-edge";
|
|
6
6
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
PlayerProtocol,
|
|
9
|
+
useLivestreamStore,
|
|
10
|
+
usePlayerStore,
|
|
11
|
+
VideoRetry,
|
|
12
|
+
} from "../..";
|
|
8
13
|
import Video from "./video.native";
|
|
9
14
|
|
|
10
15
|
// Standard 16:9 video aspect ratio
|
|
11
16
|
const VIDEO_ASPECT_RATIO = 16 / 9;
|
|
12
17
|
|
|
13
|
-
export function Fullscreen(props: { src: string }) {
|
|
18
|
+
export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
|
|
14
19
|
const ref = useRef<VideoView>(null);
|
|
15
20
|
const insets = useSafeAreaInsets();
|
|
16
21
|
const navigation = useNavigation();
|
|
@@ -157,6 +162,7 @@ export function Fullscreen(props: { src: string }) {
|
|
|
157
162
|
]}
|
|
158
163
|
>
|
|
159
164
|
<Video />
|
|
165
|
+
{props.children}
|
|
160
166
|
</View>
|
|
161
167
|
</View>
|
|
162
168
|
);
|
|
@@ -165,7 +171,10 @@ export function Fullscreen(props: { src: string }) {
|
|
|
165
171
|
// Normal non-fullscreen mode
|
|
166
172
|
return (
|
|
167
173
|
<>
|
|
168
|
-
<
|
|
174
|
+
<VideoRetry>
|
|
175
|
+
<Video />
|
|
176
|
+
</VideoRetry>
|
|
177
|
+
{props.children}
|
|
169
178
|
</>
|
|
170
179
|
);
|
|
171
180
|
}
|
|
@@ -3,8 +3,9 @@ import { View as RNView } from "react-native";
|
|
|
3
3
|
import { getFirstPlayerID, usePlayerStore } from "../..";
|
|
4
4
|
import { View } from "../../components/ui";
|
|
5
5
|
import Video from "./video";
|
|
6
|
+
import VideoRetry from "./video-retry";
|
|
6
7
|
|
|
7
|
-
export function Fullscreen(props: { src: string }) {
|
|
8
|
+
export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
|
|
8
9
|
const playerId = getFirstPlayerID();
|
|
9
10
|
const protocol = usePlayerStore((x) => x.protocol, playerId);
|
|
10
11
|
const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
|
|
@@ -72,8 +73,14 @@ export function Fullscreen(props: { src: string }) {
|
|
|
72
73
|
}, []);
|
|
73
74
|
|
|
74
75
|
return (
|
|
75
|
-
<View
|
|
76
|
-
|
|
76
|
+
<View
|
|
77
|
+
ref={divRef}
|
|
78
|
+
style={{ width: "100%", height: "100%", overflow: "hidden" }}
|
|
79
|
+
>
|
|
80
|
+
<VideoRetry>
|
|
81
|
+
<Video />
|
|
82
|
+
</VideoRetry>
|
|
83
|
+
{props.children}
|
|
77
84
|
</View>
|
|
78
85
|
);
|
|
79
86
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { flex, layout, w, zIndex } from "../../lib/theme/atoms";
|
|
3
|
-
import { useSegment } from "../../livestream-store";
|
|
2
|
+
import { flex, h, layout, w, zIndex } from "../../lib/theme/atoms";
|
|
4
3
|
import {
|
|
5
4
|
PlayerStatus,
|
|
6
5
|
PlayerStatusTracker,
|
|
@@ -10,19 +9,29 @@ import { useStreamplaceStore } from "../../streamplace-store";
|
|
|
10
9
|
import { Text, View } from "../ui";
|
|
11
10
|
import { Fullscreen } from "./fullscreen";
|
|
12
11
|
import { PlayerProps } from "./props";
|
|
12
|
+
import ReportModal from "./ui/report-modal";
|
|
13
13
|
|
|
14
14
|
const OFFLINE_THRESHOLD = 10000;
|
|
15
15
|
|
|
16
16
|
export * as PlayerUI from "./ui";
|
|
17
17
|
|
|
18
|
-
export function Player(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const setOffline = usePlayerStore((x) => x.setOffline);
|
|
18
|
+
export function Player(
|
|
19
|
+
props: Partial<PlayerProps> & { children?: React.ReactNode },
|
|
20
|
+
) {
|
|
22
21
|
const setIngest = usePlayerStore((x) => x.setIngestConnectionState);
|
|
23
22
|
|
|
24
23
|
const clearControlsTimeout = usePlayerStore((x) => x.clearControlsTimeout);
|
|
25
24
|
|
|
25
|
+
const setReportingURL = usePlayerStore((x) => x.setReportingURL);
|
|
26
|
+
|
|
27
|
+
const reportModalOpen = usePlayerStore((x) => x.reportModalOpen);
|
|
28
|
+
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
29
|
+
const reportSubject = usePlayerStore((x) => x.reportSubject);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setReportingURL(props.reportingURL ?? null);
|
|
33
|
+
}, [props.reportingURL]);
|
|
34
|
+
|
|
26
35
|
// Will call back every few seconds to send health updates
|
|
27
36
|
usePlayerStatus();
|
|
28
37
|
|
|
@@ -44,40 +53,23 @@ export function Player(props: Partial<PlayerProps>) {
|
|
|
44
53
|
};
|
|
45
54
|
}, []);
|
|
46
55
|
|
|
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
56
|
return (
|
|
76
57
|
<>
|
|
77
58
|
<View
|
|
78
|
-
style={[
|
|
59
|
+
style={[
|
|
60
|
+
zIndex[0],
|
|
61
|
+
w.percent[100],
|
|
62
|
+
h.percent[100],
|
|
63
|
+
flex.shrink[1],
|
|
64
|
+
layout.flex.center,
|
|
65
|
+
]}
|
|
79
66
|
>
|
|
80
|
-
<
|
|
67
|
+
<ReportModal
|
|
68
|
+
open={reportModalOpen}
|
|
69
|
+
onOpenChange={setReportModalOpen}
|
|
70
|
+
subject={reportSubject!}
|
|
71
|
+
/>
|
|
72
|
+
<Fullscreen src={props.src}>{props.children}</Fullscreen>
|
|
81
73
|
</View>
|
|
82
74
|
</>
|
|
83
75
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from "./countdown";
|
|
2
2
|
export * from "./input";
|
|
3
|
-
export * from "./loading";
|
|
4
3
|
export * from "./metrics";
|
|
5
4
|
export * from "./streamer-context-menu";
|
|
5
|
+
export * from "./streamer-loading-overlay";
|
|
6
6
|
export * from "./viewer-context-menu";
|
|
7
|
+
export * from "./viewer-loading-overlay";
|
|
7
8
|
export * from "./viewers";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComAtprotoModerationCreateReport,
|
|
3
|
+
ComAtprotoModerationDefs,
|
|
4
|
+
} from "@atproto/api";
|
|
5
|
+
import { CheckCircle, Circle, Loader2 } from "lucide-react-native";
|
|
6
|
+
import React, { useState } from "react";
|
|
7
|
+
import { TouchableOpacity, View } from "react-native";
|
|
8
|
+
import { zero } from "../../..";
|
|
9
|
+
import { useSubmitReport } from "../../../livestream-store";
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
ModalContent,
|
|
15
|
+
Text,
|
|
16
|
+
Textarea,
|
|
17
|
+
} from "../../ui";
|
|
18
|
+
|
|
19
|
+
// AT Protocol moderation reason types with proper labels
|
|
20
|
+
const REPORT_REASONS = [
|
|
21
|
+
{
|
|
22
|
+
value: ComAtprotoModerationDefs.REASONSPAM,
|
|
23
|
+
label: "Spam",
|
|
24
|
+
description: "Excessive unwanted promotion, replies, mentions",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: ComAtprotoModerationDefs.REASONVIOLATION,
|
|
28
|
+
label: "Rule Violation",
|
|
29
|
+
description: "Direct, blatant violation of laws or terms of service",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: ComAtprotoModerationDefs.REASONMISLEADING,
|
|
33
|
+
label: "Misleading Content",
|
|
34
|
+
description: "Misleading identity, affiliation, or content",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
value: ComAtprotoModerationDefs.REASONSEXUAL,
|
|
38
|
+
label: "Sexual Content",
|
|
39
|
+
description: "Unwanted or mislabeled sexual content",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: ComAtprotoModerationDefs.REASONRUDE,
|
|
43
|
+
label: "Harassment",
|
|
44
|
+
description: "Rude, harassing, explicit, or otherwise unwelcoming behavior",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: ComAtprotoModerationDefs.REASONOTHER,
|
|
48
|
+
label: "Other",
|
|
49
|
+
description: "Reports not falling under another report category",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
interface ReportModalProps {
|
|
54
|
+
open: boolean;
|
|
55
|
+
onOpenChange: (open: boolean) => void;
|
|
56
|
+
onSubmit?: (reason: string, additionalComments?: string) => void;
|
|
57
|
+
subject: ComAtprotoModerationCreateReport.InputSchema["subject"];
|
|
58
|
+
title?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const ReportModal: React.FC<ReportModalProps> = ({
|
|
63
|
+
open,
|
|
64
|
+
onOpenChange,
|
|
65
|
+
onSubmit,
|
|
66
|
+
subject,
|
|
67
|
+
title = "Report",
|
|
68
|
+
description = "Why are you submitting this report?",
|
|
69
|
+
}) => {
|
|
70
|
+
const [selectedReason, setSelectedReason] = useState<string | null>(null);
|
|
71
|
+
const [additionalComments, setAdditionalComments] = useState<string>("");
|
|
72
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
73
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
74
|
+
|
|
75
|
+
const submitReport = useSubmitReport();
|
|
76
|
+
|
|
77
|
+
const handleCancel = () => {
|
|
78
|
+
setSelectedReason(null);
|
|
79
|
+
setAdditionalComments("");
|
|
80
|
+
setSubmitError(null);
|
|
81
|
+
onOpenChange(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleSubmit = async () => {
|
|
85
|
+
if (!selectedReason) return;
|
|
86
|
+
|
|
87
|
+
setIsSubmitting(true);
|
|
88
|
+
setSubmitError(null);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
submitReport(
|
|
92
|
+
subject,
|
|
93
|
+
selectedReason,
|
|
94
|
+
additionalComments.trim() || undefined,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Reset form and close modal on success
|
|
98
|
+
setSelectedReason(null);
|
|
99
|
+
setAdditionalComments("");
|
|
100
|
+
onOpenChange(false);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Failed to submit report:", error);
|
|
103
|
+
setSubmitError("Failed to submit report. Please try again.");
|
|
104
|
+
} finally {
|
|
105
|
+
setIsSubmitting(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Dialog
|
|
111
|
+
open={open}
|
|
112
|
+
onOpenChange={onOpenChange}
|
|
113
|
+
title={title}
|
|
114
|
+
description={description}
|
|
115
|
+
showCloseButton
|
|
116
|
+
variant="default"
|
|
117
|
+
size="md"
|
|
118
|
+
dismissible={false}
|
|
119
|
+
position="center"
|
|
120
|
+
>
|
|
121
|
+
<ModalContent style={[zero.pb[2]]}>
|
|
122
|
+
{REPORT_REASONS.map((reason) => (
|
|
123
|
+
<TouchableOpacity
|
|
124
|
+
key={reason.value}
|
|
125
|
+
onPress={() => setSelectedReason(reason.value)}
|
|
126
|
+
style={[
|
|
127
|
+
zero.layout.flex.row,
|
|
128
|
+
zero.gap.all[2],
|
|
129
|
+
zero.py[3],
|
|
130
|
+
zero.px[3],
|
|
131
|
+
zero.borderRadius[8],
|
|
132
|
+
zero.layout.flex.alignCenter,
|
|
133
|
+
selectedReason === reason.value && {
|
|
134
|
+
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
|
135
|
+
},
|
|
136
|
+
]}
|
|
137
|
+
>
|
|
138
|
+
<View>
|
|
139
|
+
{selectedReason === reason.value ? <CheckCircle /> : <Circle />}
|
|
140
|
+
</View>
|
|
141
|
+
<View
|
|
142
|
+
style={[zero.layout.flex.column, zero.gap.all[1], zero.flex[1]]}
|
|
143
|
+
>
|
|
144
|
+
<Text style={[{ fontWeight: "600" }]}>{reason.label}</Text>
|
|
145
|
+
<Text style={[{ fontSize: 14, color: "rgba(255,255,255,0.7)" }]}>
|
|
146
|
+
{reason.description}
|
|
147
|
+
</Text>
|
|
148
|
+
</View>
|
|
149
|
+
</TouchableOpacity>
|
|
150
|
+
))}
|
|
151
|
+
|
|
152
|
+
<View style={[zero.pb[4], zero.mt[4], zero.px[2]]}>
|
|
153
|
+
<Text style={[zero.mb[2]]}>Additional Comments (optional)</Text>
|
|
154
|
+
<Textarea
|
|
155
|
+
maxLength={500}
|
|
156
|
+
numberOfLines={3}
|
|
157
|
+
value={additionalComments}
|
|
158
|
+
onChangeText={setAdditionalComments}
|
|
159
|
+
placeholder="Provide additional context for this report..."
|
|
160
|
+
/>
|
|
161
|
+
{submitError && (
|
|
162
|
+
<Text style={[zero.mt[2], { color: "red", fontSize: 14 }]}>
|
|
163
|
+
{submitError}
|
|
164
|
+
</Text>
|
|
165
|
+
)}
|
|
166
|
+
</View>
|
|
167
|
+
</ModalContent>
|
|
168
|
+
<DialogFooter>
|
|
169
|
+
<Button
|
|
170
|
+
variant="secondary"
|
|
171
|
+
onPress={handleCancel}
|
|
172
|
+
disabled={isSubmitting}
|
|
173
|
+
>
|
|
174
|
+
<Text>Cancel</Text>
|
|
175
|
+
</Button>
|
|
176
|
+
<Button
|
|
177
|
+
variant="primary"
|
|
178
|
+
onPress={handleSubmit}
|
|
179
|
+
disabled={!selectedReason || isSubmitting}
|
|
180
|
+
>
|
|
181
|
+
{isSubmitting ? (
|
|
182
|
+
<>
|
|
183
|
+
<Loader2 style={[{ marginRight: 8 }]} />
|
|
184
|
+
<Text>Submitting...</Text>
|
|
185
|
+
</>
|
|
186
|
+
) : (
|
|
187
|
+
<Text>Submit</Text>
|
|
188
|
+
)}
|
|
189
|
+
</Button>
|
|
190
|
+
</DialogFooter>
|
|
191
|
+
</Dialog>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export default ReportModal;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRootContext } from "@rn-primitives/dropdown-menu";
|
|
2
|
+
import { Settings } from "lucide-react-native";
|
|
2
3
|
import { colors } from "../../../lib/theme";
|
|
3
4
|
import { useLivestreamStore } from "../../../livestream-store";
|
|
4
5
|
import { PlayerProtocol, usePlayerStore } from "../../../player-store/";
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
DropdownMenuCheckboxItem,
|
|
8
9
|
DropdownMenuGroup,
|
|
9
10
|
DropdownMenuInfo,
|
|
11
|
+
DropdownMenuItem,
|
|
10
12
|
DropdownMenuRadioGroup,
|
|
11
13
|
DropdownMenuRadioItem,
|
|
12
14
|
DropdownMenuTrigger,
|
|
@@ -33,9 +35,9 @@ export function ContextMenu() {
|
|
|
33
35
|
return (
|
|
34
36
|
<DropdownMenu>
|
|
35
37
|
<DropdownMenuTrigger>
|
|
36
|
-
<
|
|
38
|
+
<Settings color={colors.gray[200]} />
|
|
37
39
|
</DropdownMenuTrigger>
|
|
38
|
-
<ResponsiveDropdownMenuContent>
|
|
40
|
+
<ResponsiveDropdownMenuContent side="top" align="end">
|
|
39
41
|
<DropdownMenuGroup title="Resolution">
|
|
40
42
|
<DropdownMenuRadioGroup value={quality} onValueChange={setQuality}>
|
|
41
43
|
<DropdownMenuRadioItem value="source">
|
|
@@ -65,7 +67,33 @@ export function ContextMenu() {
|
|
|
65
67
|
<Text>Show Debug Info</Text>
|
|
66
68
|
</DropdownMenuCheckboxItem>
|
|
67
69
|
</DropdownMenuGroup>
|
|
70
|
+
<DropdownMenuGroup title="Report">
|
|
71
|
+
<ReportButton />
|
|
72
|
+
</DropdownMenuGroup>
|
|
68
73
|
</ResponsiveDropdownMenuContent>
|
|
69
74
|
</DropdownMenu>
|
|
70
75
|
);
|
|
71
76
|
}
|
|
77
|
+
|
|
78
|
+
export function ReportButton() {
|
|
79
|
+
const livestream = useLivestreamStore((x) => x.livestream);
|
|
80
|
+
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
81
|
+
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
82
|
+
const { onOpenChange } = useRootContext();
|
|
83
|
+
return (
|
|
84
|
+
<DropdownMenuItem
|
|
85
|
+
onPress={() => {
|
|
86
|
+
if (!livestream) return;
|
|
87
|
+
onOpenChange?.(false);
|
|
88
|
+
setReportModalOpen(true);
|
|
89
|
+
setReportSubject({
|
|
90
|
+
$type: "com.atproto.repo.strongRef",
|
|
91
|
+
uri: livestream.uri,
|
|
92
|
+
cid: livestream.cid,
|
|
93
|
+
});
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Text>Report Livestream...</Text>
|
|
97
|
+
</DropdownMenuItem>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Play } from "lucide-react-native";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import {
|
|
9
|
+
KeepAwake,
|
|
10
|
+
Loader,
|
|
11
|
+
PlayerStatus,
|
|
12
|
+
usePlayerStore,
|
|
13
|
+
useTheme,
|
|
14
|
+
} from "../../..";
|
|
15
|
+
|
|
16
|
+
export function ViewerLoadingOverlay() {
|
|
17
|
+
const status = usePlayerStore((x) => x.status);
|
|
18
|
+
const theme = useTheme();
|
|
19
|
+
const opacity = useSharedValue(0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (status === PlayerStatus.PLAYING || status === PlayerStatus.SUSPEND) {
|
|
23
|
+
opacity.value = withTiming(0, { duration: 300 });
|
|
24
|
+
} else {
|
|
25
|
+
opacity.value = withTiming(1, { duration: 300 });
|
|
26
|
+
}
|
|
27
|
+
}, [status, opacity]);
|
|
28
|
+
|
|
29
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
30
|
+
return {
|
|
31
|
+
opacity: opacity.value,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (status === PlayerStatus.PLAYING) {
|
|
36
|
+
return <KeepAwake />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (status === PlayerStatus.SUSPEND) {
|
|
40
|
+
return null; // No overlay when stopped
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let spinner = <Loader size="large" />;
|
|
44
|
+
if (status === PlayerStatus.PAUSE) {
|
|
45
|
+
spinner = <Play size="$12" color={theme.styles.text.primary["color"]} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Animated.View
|
|
50
|
+
style={[
|
|
51
|
+
{
|
|
52
|
+
position: "absolute",
|
|
53
|
+
width: "100%",
|
|
54
|
+
height: "100%",
|
|
55
|
+
zIndex: 998,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
justifyContent: "center",
|
|
58
|
+
backgroundColor: "rgba(0,0,0,0.3)",
|
|
59
|
+
},
|
|
60
|
+
animatedStyle,
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{spinner}
|
|
64
|
+
</Animated.View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { usePlayerStore, useStreamKey } from "../..";
|
|
2
|
+
import { PlayerStatus, usePlayerStore, useStreamKey } from "../..";
|
|
3
3
|
import { RTCPeerConnection, RTCSessionDescription } from "./webrtc-primitives";
|
|
4
4
|
|
|
5
5
|
export default function useWebRTC(
|
|
@@ -7,6 +7,7 @@ export default function useWebRTC(
|
|
|
7
7
|
): [MediaStream | null, boolean] {
|
|
8
8
|
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
|
|
9
9
|
const [stuck, setStuck] = useState<boolean>(false);
|
|
10
|
+
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
10
11
|
|
|
11
12
|
const lastChange = useRef<number>(0);
|
|
12
13
|
|
|
@@ -29,7 +30,12 @@ export default function useWebRTC(
|
|
|
29
30
|
});
|
|
30
31
|
peerConnection.addEventListener("connectionstatechange", () => {
|
|
31
32
|
console.log("connection state change", peerConnection.connectionState);
|
|
32
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
peerConnection.connectionState === "closed" ||
|
|
35
|
+
peerConnection.connectionState === "failed" ||
|
|
36
|
+
peerConnection.connectionState === "disconnected"
|
|
37
|
+
) {
|
|
38
|
+
console.log("setting stuck to true", peerConnection.connectionState);
|
|
33
39
|
setStuck(true);
|
|
34
40
|
}
|
|
35
41
|
if (peerConnection.connectionState !== "connected") {
|
|
@@ -52,6 +58,7 @@ export default function useWebRTC(
|
|
|
52
58
|
if (lastAudioFramesReceived !== audioFramesReceived) {
|
|
53
59
|
lastAudioFramesReceived = audioFramesReceived;
|
|
54
60
|
lastChange.current = Date.now();
|
|
61
|
+
setStatus(PlayerStatus.PLAYING);
|
|
55
62
|
setStuck(false);
|
|
56
63
|
}
|
|
57
64
|
}
|
|
@@ -60,6 +67,7 @@ export default function useWebRTC(
|
|
|
60
67
|
if (lastFramesReceived !== framesReceived) {
|
|
61
68
|
lastFramesReceived = framesReceived;
|
|
62
69
|
lastChange.current = Date.now();
|
|
70
|
+
setStatus(PlayerStatus.PLAYING);
|
|
63
71
|
setStuck(false);
|
|
64
72
|
}
|
|
65
73
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { PlayerStatus, usePlayerStore } from "../..";
|
|
3
|
+
|
|
4
|
+
export default function VideoRetry(props: { children: React.ReactNode }) {
|
|
5
|
+
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
6
|
+
const [retries, setRetries] = useState(0);
|
|
7
|
+
const playing = usePlayerStore((x) => x.status === PlayerStatus.PLAYING);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!playing) {
|
|
11
|
+
const jitter = 500 + Math.random() * 1500;
|
|
12
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
13
|
+
console.log("Retrying video playback...");
|
|
14
|
+
setRetries((prevRetries) => prevRetries + 1);
|
|
15
|
+
}, jitter);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
if (retryTimeoutRef.current) {
|
|
20
|
+
console.log("Clearing retry timeout");
|
|
21
|
+
clearTimeout(retryTimeoutRef.current);
|
|
22
|
+
retryTimeoutRef.current = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}, [!playing]);
|
|
26
|
+
|
|
27
|
+
return <React.Fragment key={retries}>{props.children}</React.Fragment>;
|
|
28
|
+
}
|
|
@@ -89,6 +89,14 @@ export function NativeVideo() {
|
|
|
89
89
|
}, [setStatus]);
|
|
90
90
|
|
|
91
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
|
+
}
|
|
99
|
+
});
|
|
92
100
|
player.loop = true;
|
|
93
101
|
player.muted = muted;
|
|
94
102
|
player.play();
|
|
@@ -115,12 +123,14 @@ export function NativeVideo() {
|
|
|
115
123
|
).map((evType) => {
|
|
116
124
|
return player.addListener(evType, (...args) => {
|
|
117
125
|
const now = new Date();
|
|
126
|
+
console.log("video native event", evType);
|
|
118
127
|
playerEvent(spurl, now.toISOString(), evType, { args: args });
|
|
119
128
|
});
|
|
120
129
|
});
|
|
121
130
|
|
|
122
131
|
subs.push(
|
|
123
132
|
player.addListener("playingChange", (newIsPlaying) => {
|
|
133
|
+
console.log("playingChange", newIsPlaying);
|
|
124
134
|
if (newIsPlaying) {
|
|
125
135
|
setStatus(PlayerStatus.PLAYING);
|
|
126
136
|
} else {
|
|
@@ -164,6 +174,7 @@ export function NativeWHEP() {
|
|
|
164
174
|
PlayerProtocol.WEBRTC,
|
|
165
175
|
);
|
|
166
176
|
const [stream, stuck] = useWebRTC(url);
|
|
177
|
+
const status = usePlayerStore((x) => x.status);
|
|
167
178
|
|
|
168
179
|
const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
|
|
169
180
|
const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
|
|
@@ -189,22 +200,25 @@ export function NativeWHEP() {
|
|
|
189
200
|
const volume = usePlayerStore((x) => x.volume);
|
|
190
201
|
|
|
191
202
|
useEffect(() => {
|
|
192
|
-
if (stuck) {
|
|
203
|
+
if (stuck && status === PlayerStatus.PLAYING) {
|
|
204
|
+
console.log("setting status to stalled", status);
|
|
193
205
|
setStatus(PlayerStatus.STALLED);
|
|
194
|
-
}
|
|
206
|
+
}
|
|
207
|
+
if (!stuck && status === PlayerStatus.STALLED) {
|
|
208
|
+
console.log("setting status to playing", status);
|
|
195
209
|
setStatus(PlayerStatus.PLAYING);
|
|
196
210
|
}
|
|
197
|
-
}, [stuck,
|
|
211
|
+
}, [stuck, status]);
|
|
198
212
|
|
|
199
213
|
const mediaStream = stream as unknown as MediaStream;
|
|
200
214
|
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}, [mediaStream, setStatus]);
|
|
215
|
+
// useEffect(() => {
|
|
216
|
+
// if (!mediaStream) {
|
|
217
|
+
// setStatus(PlayerStatus.WAITING);
|
|
218
|
+
// return;
|
|
219
|
+
// }
|
|
220
|
+
// setStatus(PlayerStatus.PLAYING);
|
|
221
|
+
// }, [mediaStream, setStatus]);
|
|
208
222
|
|
|
209
223
|
useEffect(() => {
|
|
210
224
|
if (!mediaStream) {
|