@streamplace/components 0.7.18 → 0.7.21
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/assets/emoji-data.json +19371 -0
- package/dist/components/chat/chat-box.js +319 -0
- package/dist/components/chat/chat-message.js +87 -0
- package/dist/components/chat/chat.js +150 -0
- package/dist/components/chat/emoji-suggestions.js +35 -0
- package/dist/components/chat/mention-suggestions.js +42 -0
- package/dist/components/chat/mod-view.js +112 -0
- package/dist/components/chat/system-message.js +19 -0
- package/dist/components/dashboard/chat-panel.js +38 -0
- package/dist/components/dashboard/header.js +80 -0
- package/dist/components/dashboard/index.js +14 -0
- package/dist/components/dashboard/information-widget.js +234 -0
- package/dist/components/dashboard/mod-actions.js +71 -0
- package/dist/components/dashboard/problems.js +74 -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 +76 -0
- package/dist/components/mobile-player/fullscreen.native.js +141 -0
- package/dist/components/mobile-player/player.js +94 -0
- package/dist/components/mobile-player/props.js +2 -0
- package/dist/components/mobile-player/shared.js +54 -0
- package/dist/components/mobile-player/ui/autoplay-button.js +68 -0
- package/dist/components/mobile-player/ui/countdown.js +83 -0
- package/dist/components/mobile-player/ui/index.js +12 -0
- package/dist/components/mobile-player/ui/input.js +42 -0
- package/dist/components/mobile-player/ui/metrics.js +44 -0
- package/dist/components/mobile-player/ui/report-modal.js +90 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +7 -0
- package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +51 -0
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
- package/dist/components/mobile-player/ui/viewers.js +23 -0
- package/dist/components/mobile-player/use-webrtc.js +243 -0
- package/dist/components/mobile-player/video-async.native.js +276 -0
- package/dist/components/mobile-player/video-retry.js +29 -0
- package/dist/components/mobile-player/video.js +475 -0
- package/dist/components/mobile-player/video.native.js +56 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +110 -0
- package/dist/components/mobile-player/webrtc-primitives.js +27 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +8 -0
- package/dist/components/share/sharesheet.js +91 -0
- package/dist/components/ui/button.js +223 -0
- package/dist/components/ui/dialog.js +206 -0
- package/dist/components/ui/dropdown.js +172 -0
- package/dist/components/ui/icons.js +25 -0
- package/dist/components/ui/index.js +34 -0
- package/dist/components/ui/info-box.js +31 -0
- package/dist/components/ui/info-row.js +23 -0
- package/dist/components/ui/input.js +205 -0
- package/dist/components/ui/loader.js +10 -0
- package/dist/components/ui/primitives/button.js +125 -0
- package/dist/components/ui/primitives/input.js +206 -0
- package/dist/components/ui/primitives/modal.js +206 -0
- package/dist/components/ui/primitives/text.js +292 -0
- package/dist/components/ui/resizeable.js +121 -0
- package/dist/components/ui/slider.js +5 -0
- package/dist/components/ui/text.js +177 -0
- package/dist/components/ui/textarea.js +19 -0
- package/dist/components/ui/toast.js +175 -0
- package/dist/components/ui/view.js +252 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/useAvatars.js +35 -0
- package/dist/hooks/useCameraToggle.js +12 -0
- package/dist/hooks/useKeyboard.js +36 -0
- package/dist/hooks/useKeyboardSlide.js +14 -0
- package/dist/hooks/useLivestreamInfo.js +69 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +30 -0
- package/dist/hooks/usePlayerDimensions.js +22 -0
- package/dist/hooks/usePointerDevice.js +71 -0
- package/dist/hooks/useSegmentDimensions.js +17 -0
- package/dist/hooks/useSegmentTiming.js +65 -0
- package/dist/index.js +34 -0
- package/dist/lib/browser.js +35 -0
- package/dist/lib/facet.js +92 -0
- package/dist/lib/system-messages.js +101 -0
- package/dist/lib/theme/atoms.js +646 -0
- package/dist/lib/theme/atoms.types.js +6 -0
- package/dist/lib/theme/index.js +35 -0
- package/dist/lib/theme/theme.js +256 -0
- package/dist/lib/theme/tokens.js +659 -0
- package/dist/lib/utils.js +105 -0
- package/dist/livestream-provider/index.js +30 -0
- package/dist/livestream-provider/websocket.js +45 -0
- package/dist/livestream-store/chat.js +308 -0
- package/dist/livestream-store/context.js +5 -0
- package/dist/livestream-store/index.js +7 -0
- package/dist/livestream-store/livestream-state.js +2 -0
- package/dist/livestream-store/livestream-store.js +58 -0
- package/dist/livestream-store/problems.js +76 -0
- package/dist/livestream-store/stream-key.js +88 -0
- package/dist/livestream-store/websocket-consumer.js +94 -0
- package/dist/player-store/context.js +5 -0
- package/dist/player-store/index.js +9 -0
- package/dist/player-store/player-provider.js +58 -0
- package/dist/player-store/player-state.js +25 -0
- package/dist/player-store/player-store.js +201 -0
- package/dist/player-store/single-player-provider.js +121 -0
- package/dist/streamplace-provider/context.js +5 -0
- package/dist/streamplace-provider/index.js +20 -0
- package/dist/streamplace-provider/poller.js +49 -0
- package/dist/streamplace-provider/xrpc.js +0 -0
- package/dist/streamplace-store/block.js +65 -0
- package/dist/streamplace-store/index.js +6 -0
- package/dist/streamplace-store/stream.js +247 -0
- package/dist/streamplace-store/streamplace-store.js +47 -0
- package/dist/streamplace-store/user.js +52 -0
- package/dist/streamplace-store/xrpc.js +15 -0
- package/dist/ui/index.js +79 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +5 -4
- package/src/components/chat/chat-box.tsx +3 -0
- package/src/components/chat/mod-view.tsx +39 -5
- package/src/components/mobile-player/fullscreen.tsx +2 -0
- package/src/components/mobile-player/ui/autoplay-button.tsx +86 -0
- package/src/components/mobile-player/ui/index.ts +1 -0
- package/src/components/mobile-player/video.tsx +11 -1
- package/src/livestream-store/chat.tsx +22 -0
- package/src/player-store/player-provider.tsx +2 -1
- package/src/player-store/player-state.tsx +6 -0
- package/src/player-store/player-store.tsx +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InputPanel = InputPanel;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_native_1 = require("react-native");
|
|
7
|
+
const hooks_1 = require("../../../hooks");
|
|
8
|
+
const atoms = tslib_1.__importStar(require("../../../lib/theme/atoms"));
|
|
9
|
+
const ui_1 = require("../../ui");
|
|
10
|
+
const { gap, h, layout, mt, p, position, px, py, sizes, w } = atoms;
|
|
11
|
+
function InputPanel({ title, setTitle, ingestStarting, toggleGoLive, }) {
|
|
12
|
+
const { slideKeyboard } = (0, hooks_1.useKeyboardSlide)();
|
|
13
|
+
return ((0, jsx_runtime_1.jsx)(ui_1.View, { style: [
|
|
14
|
+
layout.position.absolute,
|
|
15
|
+
h.percent[30],
|
|
16
|
+
position.bottom[0],
|
|
17
|
+
w.percent[100],
|
|
18
|
+
layout.flex.center,
|
|
19
|
+
{ transform: [{ translateY: slideKeyboard }] },
|
|
20
|
+
], children: (0, jsx_runtime_1.jsxs)(ui_1.View, { style: [
|
|
21
|
+
layout.flex.column,
|
|
22
|
+
gap.all[2],
|
|
23
|
+
sizes.maxWidth[80],
|
|
24
|
+
{ padding: 10 },
|
|
25
|
+
], children: [(0, jsx_runtime_1.jsx)(ui_1.View, { backgroundColor: "rgba(64,64,64,0.8)", borderRadius: 12, children: (0, jsx_runtime_1.jsx)(ui_1.Input, { value: title, onChange: setTitle, placeholder: "Enter stream title", onEndEditing: react_native_1.Keyboard.dismiss }) }), ingestStarting ? ((0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Starting your stream..." })) : ((0, jsx_runtime_1.jsxs)(ui_1.View, { style: [layout.flex.center], children: [(0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: toggleGoLive, style: [
|
|
26
|
+
px[4],
|
|
27
|
+
py[2],
|
|
28
|
+
layout.flex.row,
|
|
29
|
+
layout.flex.center,
|
|
30
|
+
gap.all[1],
|
|
31
|
+
{
|
|
32
|
+
backgroundColor: "rgba(64,64,64, 0.8)",
|
|
33
|
+
borderRadius: 12,
|
|
34
|
+
},
|
|
35
|
+
], children: [(0, jsx_runtime_1.jsx)(ui_1.View, { style: [
|
|
36
|
+
p[2],
|
|
37
|
+
{
|
|
38
|
+
backgroundColor: "rgba(256,0,0, 0.8)",
|
|
39
|
+
borderRadius: 12,
|
|
40
|
+
},
|
|
41
|
+
] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { center: true, children: "Go Live" })] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { color: "muted", size: "xs", style: [mt[2]], children: "We'll announce that you're live on Bluesky." })] }))] }) }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsPanel = MetricsPanel;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const lucide_react_native_1 = require("lucide-react-native");
|
|
7
|
+
const useSegmentTiming_1 = require("../../../hooks/useSegmentTiming");
|
|
8
|
+
const atoms = tslib_1.__importStar(require("../../../lib/theme/atoms"));
|
|
9
|
+
const ui_1 = require("../../ui");
|
|
10
|
+
function MetricsPanel({ showMetrics }) {
|
|
11
|
+
const { connectionQuality, segmentDeltas, mean, range } = (0, useSegmentTiming_1.useSegmentTiming)();
|
|
12
|
+
let icon = (0, jsx_runtime_1.jsx)(lucide_react_native_1.CircleX, { color: "#d44" });
|
|
13
|
+
let color = "#d44";
|
|
14
|
+
if (connectionQuality === "good") {
|
|
15
|
+
icon = (0, jsx_runtime_1.jsx)(lucide_react_native_1.CircleCheck, { color: "#4d4" });
|
|
16
|
+
color = "#4d4";
|
|
17
|
+
}
|
|
18
|
+
else if (connectionQuality === "degraded") {
|
|
19
|
+
icon = (0, jsx_runtime_1.jsx)(lucide_react_native_1.AlertCircle, { color: "#aa4" });
|
|
20
|
+
color = "#aa4";
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
icon = (0, jsx_runtime_1.jsx)(lucide_react_native_1.CircleX, { color: "#d44" });
|
|
24
|
+
color = "#d44";
|
|
25
|
+
}
|
|
26
|
+
return ((0, jsx_runtime_1.jsxs)(ui_1.View, { style: {
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
gap: 8,
|
|
29
|
+
}, children: [(0, jsx_runtime_1.jsxs)(ui_1.View, { style: {
|
|
30
|
+
flexDirection: "row",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
padding: 10,
|
|
33
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
34
|
+
borderRadius: 8,
|
|
35
|
+
gap: 4,
|
|
36
|
+
}, children: [icon, (0, jsx_runtime_1.jsx)(ui_1.Text, { style: [
|
|
37
|
+
atoms.pt[0],
|
|
38
|
+
{
|
|
39
|
+
color,
|
|
40
|
+
},
|
|
41
|
+
], children: connectionQuality.toUpperCase() })] }), showMetrics && ((0, jsx_runtime_1.jsxs)(ui_1.View, { children: [(0, jsx_runtime_1.jsxs)(ui_1.Text, { children: ["last \u0394:", " ", segmentDeltas.length > 0
|
|
42
|
+
? segmentDeltas[segmentDeltas.length - 1]
|
|
43
|
+
: "—"] }), (0, jsx_runtime_1.jsxs)(ui_1.Text, { children: ["mean: ", mean] }), (0, jsx_runtime_1.jsxs)(ui_1.Text, { children: ["range: ", range] })] }))] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReportModal = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const api_1 = require("@atproto/api");
|
|
6
|
+
const lucide_react_native_1 = require("lucide-react-native");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
const __1 = require("../../..");
|
|
10
|
+
const livestream_store_1 = require("../../../livestream-store");
|
|
11
|
+
const ui_1 = require("../../ui");
|
|
12
|
+
// AT Protocol moderation reason types with proper labels
|
|
13
|
+
const REPORT_REASONS = [
|
|
14
|
+
{
|
|
15
|
+
value: api_1.ComAtprotoModerationDefs.REASONSPAM,
|
|
16
|
+
label: "Spam",
|
|
17
|
+
description: "Excessive unwanted promotion, replies, mentions",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
value: api_1.ComAtprotoModerationDefs.REASONVIOLATION,
|
|
21
|
+
label: "Rule Violation",
|
|
22
|
+
description: "Direct, blatant violation of laws or terms of service",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: api_1.ComAtprotoModerationDefs.REASONMISLEADING,
|
|
26
|
+
label: "Misleading Content",
|
|
27
|
+
description: "Misleading identity, affiliation, or content",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: api_1.ComAtprotoModerationDefs.REASONSEXUAL,
|
|
31
|
+
label: "Sexual Content",
|
|
32
|
+
description: "Unwanted or mislabeled sexual content",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: api_1.ComAtprotoModerationDefs.REASONRUDE,
|
|
36
|
+
label: "Harassment",
|
|
37
|
+
description: "Rude, harassing, explicit, or otherwise unwelcoming behavior",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: api_1.ComAtprotoModerationDefs.REASONOTHER,
|
|
41
|
+
label: "Other",
|
|
42
|
+
description: "Reports not falling under another report category",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const ReportModal = ({ open, onOpenChange, onSubmit, subject, title = "Report", description = "Why are you submitting this report?", }) => {
|
|
46
|
+
const [selectedReason, setSelectedReason] = (0, react_1.useState)(null);
|
|
47
|
+
const [additionalComments, setAdditionalComments] = (0, react_1.useState)("");
|
|
48
|
+
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
49
|
+
const [submitError, setSubmitError] = (0, react_1.useState)(null);
|
|
50
|
+
const submitReport = (0, livestream_store_1.useSubmitReport)();
|
|
51
|
+
const handleCancel = () => {
|
|
52
|
+
setSelectedReason(null);
|
|
53
|
+
setAdditionalComments("");
|
|
54
|
+
setSubmitError(null);
|
|
55
|
+
onOpenChange(false);
|
|
56
|
+
};
|
|
57
|
+
const handleSubmit = async () => {
|
|
58
|
+
if (!selectedReason)
|
|
59
|
+
return;
|
|
60
|
+
setIsSubmitting(true);
|
|
61
|
+
setSubmitError(null);
|
|
62
|
+
try {
|
|
63
|
+
submitReport(subject, selectedReason, additionalComments.trim() || undefined);
|
|
64
|
+
// Reset form and close modal on success
|
|
65
|
+
setSelectedReason(null);
|
|
66
|
+
setAdditionalComments("");
|
|
67
|
+
onOpenChange(false);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error("Failed to submit report:", error);
|
|
71
|
+
setSubmitError("Failed to submit report. Please try again.");
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
setIsSubmitting(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return ((0, jsx_runtime_1.jsxs)(ui_1.Dialog, { open: open, onOpenChange: onOpenChange, title: title, description: description, showCloseButton: true, variant: "default", size: "md", dismissible: false, position: "center", children: [(0, jsx_runtime_1.jsxs)(ui_1.ModalContent, { style: [__1.zero.pb[2]], children: [REPORT_REASONS.map((reason) => ((0, jsx_runtime_1.jsxs)(react_native_1.TouchableOpacity, { onPress: () => setSelectedReason(reason.value), style: [
|
|
78
|
+
__1.zero.layout.flex.row,
|
|
79
|
+
__1.zero.gap.all[2],
|
|
80
|
+
__1.zero.py[3],
|
|
81
|
+
__1.zero.px[3],
|
|
82
|
+
__1.zero.borderRadius[8],
|
|
83
|
+
__1.zero.layout.flex.alignCenter,
|
|
84
|
+
selectedReason === reason.value && {
|
|
85
|
+
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
|
86
|
+
},
|
|
87
|
+
], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { children: selectedReason === reason.value ? (0, jsx_runtime_1.jsx)(lucide_react_native_1.CheckCircle, {}) : (0, jsx_runtime_1.jsx)(lucide_react_native_1.Circle, {}) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [__1.zero.layout.flex.column, __1.zero.gap.all[1], __1.zero.flex[1]], children: [(0, jsx_runtime_1.jsx)(ui_1.Text, { style: [{ fontWeight: "600" }], children: reason.label }), (0, jsx_runtime_1.jsx)(ui_1.Text, { style: [{ fontSize: 14, color: "rgba(255,255,255,0.7)" }], children: reason.description })] })] }, reason.value))), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [__1.zero.pb[4], __1.zero.mt[4], __1.zero.px[2]], children: [(0, jsx_runtime_1.jsx)(ui_1.Text, { style: [__1.zero.mb[2]], children: "Additional Comments (optional)" }), (0, jsx_runtime_1.jsx)(ui_1.Textarea, { maxLength: 500, numberOfLines: 3, value: additionalComments, onChangeText: setAdditionalComments, placeholder: "Provide additional context for this report..." }), submitError && ((0, jsx_runtime_1.jsx)(ui_1.Text, { style: [__1.zero.mt[2], { color: "red", fontSize: 14 }], children: submitError }))] })] }), (0, jsx_runtime_1.jsxs)(ui_1.DialogFooter, { children: [(0, jsx_runtime_1.jsx)(ui_1.Button, { variant: "secondary", onPress: handleCancel, disabled: isSubmitting, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Cancel" }) }), (0, jsx_runtime_1.jsx)(ui_1.Button, { variant: "primary", onPress: handleSubmit, disabled: !selectedReason || isSubmitting, children: isSubmitting ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Loader2, { style: [{ marginRight: 8 }] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Submitting..." })] })) : ((0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Submit" })) })] })] }));
|
|
88
|
+
};
|
|
89
|
+
exports.ReportModal = ReportModal;
|
|
90
|
+
exports.default = exports.ReportModal;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StreamContextMenu = StreamContextMenu;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
function StreamContextMenu() {
|
|
6
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
|
|
7
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoadingOverlay = LoadingOverlay;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_native_reanimated_1 = tslib_1.__importStar(require("react-native-reanimated"));
|
|
8
|
+
const atoms_1 = require("../../../lib/theme/atoms");
|
|
9
|
+
const defaultMessages = [
|
|
10
|
+
"Creating the stream",
|
|
11
|
+
"Uploading thumbnails",
|
|
12
|
+
"Getting things ready",
|
|
13
|
+
"Doing some magic",
|
|
14
|
+
"Preparing something special",
|
|
15
|
+
"Reticulating splines",
|
|
16
|
+
"Making it nice",
|
|
17
|
+
"Flipping some switches",
|
|
18
|
+
"Adding good vibes",
|
|
19
|
+
"Almost there",
|
|
20
|
+
"Summoning your Persona",
|
|
21
|
+
"Awakening our true selves",
|
|
22
|
+
"Fusion in progress",
|
|
23
|
+
"Equipping the right materia",
|
|
24
|
+
];
|
|
25
|
+
function LoadingOverlay({ visible, width, height, subtitle, messages = defaultMessages, interval = 3000, }) {
|
|
26
|
+
const [currentIndex, setCurrentIndex] = (0, react_1.useState)(0);
|
|
27
|
+
const [shouldRender, setShouldRender] = (0, react_1.useState)(visible);
|
|
28
|
+
// Animation values
|
|
29
|
+
const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
30
|
+
const opacity = (0, react_native_reanimated_1.useSharedValue)(1);
|
|
31
|
+
const wholeOpacity = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
32
|
+
// Handle fade-in and fade-out animations
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
if (visible) {
|
|
35
|
+
setShouldRender(true); // Ensure the component is mounted
|
|
36
|
+
wholeOpacity.value = (0, react_native_reanimated_1.withTiming)(1, { duration: 500 }); // Fade in
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
wholeOpacity.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 500 }, () => {
|
|
40
|
+
// Unmount after fade-out
|
|
41
|
+
(0, react_native_reanimated_1.runOnJS)(setShouldRender)(false);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}, [visible]);
|
|
45
|
+
// Cycle messages on a timer
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
if (!visible) {
|
|
48
|
+
setCurrentIndex(0);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const timeout = setTimeout(() => {
|
|
52
|
+
setCurrentIndex((prev) => (prev + 1) % messages.length);
|
|
53
|
+
}, interval);
|
|
54
|
+
return () => clearTimeout(timeout);
|
|
55
|
+
}, [visible, currentIndex, interval, messages.length]);
|
|
56
|
+
// Trigger animation on each message change
|
|
57
|
+
(0, react_1.useEffect)(() => {
|
|
58
|
+
if (!visible)
|
|
59
|
+
return;
|
|
60
|
+
const fadeDuration = Math.min(interval / 2, 250); // Simplified fade duration
|
|
61
|
+
// Reset animation values
|
|
62
|
+
translateY.value = 20;
|
|
63
|
+
opacity.value = 0;
|
|
64
|
+
// Sequential fade-in and fade-out
|
|
65
|
+
translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: fadeDuration });
|
|
66
|
+
opacity.value = (0, react_native_reanimated_1.withTiming)(1, { duration: fadeDuration }, () => {
|
|
67
|
+
// add a delay for interval - (fadeDuration*2)
|
|
68
|
+
translateY.value = (0, react_native_reanimated_1.withDelay)(interval - fadeDuration * 2, (0, react_native_reanimated_1.withTiming)(-10, { duration: fadeDuration }));
|
|
69
|
+
opacity.value = (0, react_native_reanimated_1.withDelay)(interval - fadeDuration * 2, (0, react_native_reanimated_1.withTiming)(0, { duration: fadeDuration }));
|
|
70
|
+
});
|
|
71
|
+
}, [currentIndex, visible]);
|
|
72
|
+
const animatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
|
|
73
|
+
return {
|
|
74
|
+
transform: [{ translateY: translateY.value }],
|
|
75
|
+
opacity: opacity.value,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const wholeAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => ({
|
|
79
|
+
opacity: wholeOpacity.value,
|
|
80
|
+
}));
|
|
81
|
+
if (!shouldRender)
|
|
82
|
+
return null;
|
|
83
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_reanimated_1.default.View, { style: [
|
|
84
|
+
{
|
|
85
|
+
position: "absolute",
|
|
86
|
+
top: 0,
|
|
87
|
+
left: 0,
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
backgroundColor: "rgba(0,0,0,0.7)",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
justifyContent: "center",
|
|
93
|
+
zIndex: 1000,
|
|
94
|
+
},
|
|
95
|
+
wholeAnimatedStyle,
|
|
96
|
+
], children: [(0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.Text, { style: [
|
|
97
|
+
{
|
|
98
|
+
color: "white",
|
|
99
|
+
fontSize: 24,
|
|
100
|
+
fontWeight: "bold",
|
|
101
|
+
},
|
|
102
|
+
animatedStyle,
|
|
103
|
+
], children: messages[currentIndex] }), (0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.Text, { style: [atoms_1.pt[5], { color: "#a0a0a0" }], children: subtitle })] }));
|
|
104
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContextMenu = ContextMenu;
|
|
4
|
+
exports.ReportButton = ReportButton;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const dropdown_menu_1 = require("@rn-primitives/dropdown-menu");
|
|
7
|
+
const lucide_react_native_1 = require("lucide-react-native");
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
const theme_1 = require("../../../lib/theme");
|
|
10
|
+
const livestream_store_1 = require("../../../livestream-store");
|
|
11
|
+
const player_store_1 = require("../../../player-store/");
|
|
12
|
+
const ui_1 = require("../../ui");
|
|
13
|
+
function ContextMenu({ dropdownPortalContainer, }) {
|
|
14
|
+
const quality = (0, player_store_1.usePlayerStore)((x) => x.selectedRendition);
|
|
15
|
+
const setQuality = (0, player_store_1.usePlayerStore)((x) => x.setSelectedRendition);
|
|
16
|
+
const qualities = (0, livestream_store_1.useLivestreamStore)((x) => x.renditions);
|
|
17
|
+
const protocol = (0, player_store_1.usePlayerStore)((x) => x.protocol);
|
|
18
|
+
const setProtocol = (0, player_store_1.usePlayerStore)((x) => x.setProtocol);
|
|
19
|
+
const debugInfo = (0, player_store_1.usePlayerStore)((x) => x.showDebugInfo);
|
|
20
|
+
const setShowDebugInfo = (0, player_store_1.usePlayerStore)((x) => x.setShowDebugInfo);
|
|
21
|
+
const livestream = (0, livestream_store_1.useLivestreamStore)((x) => x.livestream);
|
|
22
|
+
const setReportModalOpen = (0, player_store_1.usePlayerStore)((x) => x.setReportModalOpen);
|
|
23
|
+
const setReportSubject = (0, player_store_1.usePlayerStore)((x) => x.setReportSubject);
|
|
24
|
+
const lowLatency = protocol === "webrtc";
|
|
25
|
+
const setLowLatency = (value) => {
|
|
26
|
+
setProtocol(value ? player_store_1.PlayerProtocol.WEBRTC : player_store_1.PlayerProtocol.HLS);
|
|
27
|
+
};
|
|
28
|
+
// are we on mobile? then do dropdowns
|
|
29
|
+
const isMobile = react_native_1.Platform.OS === "ios" || react_native_1.Platform.OS === "android";
|
|
30
|
+
// dummy portal for mobile
|
|
31
|
+
const Portal = isMobile ? react_native_1.View : ui_1.DropdownMenuPortal;
|
|
32
|
+
// render the responsive version on mobile as we can't fullscreen there
|
|
33
|
+
const DropdownMenuContent = isMobile
|
|
34
|
+
? ui_1.ResponsiveDropdownMenuContent
|
|
35
|
+
: ui_1.DropdownMenuContentWithoutPortal;
|
|
36
|
+
return ((0, jsx_runtime_1.jsxs)(ui_1.DropdownMenu, { children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuTrigger, { children: (0, jsx_runtime_1.jsx)(lucide_react_native_1.Settings, { color: theme_1.colors.gray[200] }) }), (0, jsx_runtime_1.jsx)(Portal, { container: dropdownPortalContainer, children: (0, jsx_runtime_1.jsxs)(DropdownMenuContent, { side: "top", align: "end", children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuGroup, { title: "Resolution", children: (0, jsx_runtime_1.jsxs)(ui_1.DropdownMenuRadioGroup, { value: quality, onValueChange: setQuality, children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuRadioItem, { value: "source", children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Source (Original Quality)" }) }), qualities.map((r) => ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuRadioItem, { value: r.name, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: r.name }) })))] }) }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuGroup, { title: "Advanced", children: (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuCheckboxItem, { checked: lowLatency, onCheckedChange: () => setLowLatency(!lowLatency), children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Low Latency" }) }) }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuInfo, { description: "Reduces the delay between video and chat for a more real-time experience." }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuGroup, { children: (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuCheckboxItem, { checked: debugInfo, onCheckedChange: () => setShowDebugInfo(!debugInfo), children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Show Debug Info" }) }) }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuGroup, { title: "Report", children: (0, jsx_runtime_1.jsx)(ReportButton, { livestream: livestream, setReportModalOpen: setReportModalOpen, setReportSubject: setReportSubject }) })] }) })] }));
|
|
37
|
+
}
|
|
38
|
+
function ReportButton({ livestream, setReportModalOpen, setReportSubject, }) {
|
|
39
|
+
const { onOpenChange } = (0, dropdown_menu_1.useRootContext)();
|
|
40
|
+
return ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
|
|
41
|
+
if (!livestream)
|
|
42
|
+
return;
|
|
43
|
+
onOpenChange?.(false);
|
|
44
|
+
setReportModalOpen(true);
|
|
45
|
+
setReportSubject({
|
|
46
|
+
$type: "com.atproto.repo.strongRef",
|
|
47
|
+
uri: livestream.uri,
|
|
48
|
+
cid: livestream.cid,
|
|
49
|
+
});
|
|
50
|
+
}, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Report Livestream..." }) }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ViewerLoadingOverlay = ViewerLoadingOverlay;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const lucide_react_native_1 = require("lucide-react-native");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_native_reanimated_1 = tslib_1.__importStar(require("react-native-reanimated"));
|
|
9
|
+
const __1 = require("../../..");
|
|
10
|
+
function ViewerLoadingOverlay() {
|
|
11
|
+
const status = (0, __1.usePlayerStore)((x) => x.status);
|
|
12
|
+
const theme = (0, __1.useTheme)();
|
|
13
|
+
const opacity = (0, react_native_reanimated_1.useSharedValue)(0);
|
|
14
|
+
(0, react_1.useEffect)(() => {
|
|
15
|
+
if (status === __1.PlayerStatus.PLAYING || status === __1.PlayerStatus.SUSPEND) {
|
|
16
|
+
opacity.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 300 });
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
opacity.value = (0, react_native_reanimated_1.withTiming)(1, { duration: 300 });
|
|
20
|
+
}
|
|
21
|
+
}, [status, opacity]);
|
|
22
|
+
const animatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
|
|
23
|
+
return {
|
|
24
|
+
opacity: opacity.value,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
if (status === __1.PlayerStatus.PLAYING) {
|
|
28
|
+
return (0, jsx_runtime_1.jsx)(__1.KeepAwake, {});
|
|
29
|
+
}
|
|
30
|
+
if (status === __1.PlayerStatus.SUSPEND) {
|
|
31
|
+
return null; // No overlay when stopped
|
|
32
|
+
}
|
|
33
|
+
let spinner = (0, jsx_runtime_1.jsx)(__1.Loader, { size: "large" });
|
|
34
|
+
if (status === __1.PlayerStatus.PAUSE) {
|
|
35
|
+
spinner = (0, jsx_runtime_1.jsx)(lucide_react_native_1.Play, { size: "$12", color: theme.styles.text.primary["color"] });
|
|
36
|
+
}
|
|
37
|
+
return ((0, jsx_runtime_1.jsx)(react_native_reanimated_1.default.View, { style: [
|
|
38
|
+
{
|
|
39
|
+
position: "absolute",
|
|
40
|
+
width: "100%",
|
|
41
|
+
height: "100%",
|
|
42
|
+
zIndex: 998,
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
justifyContent: "center",
|
|
45
|
+
backgroundColor: "rgba(0,0,0,0.3)",
|
|
46
|
+
},
|
|
47
|
+
animatedStyle,
|
|
48
|
+
], children: spinner }));
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Viewers = Viewers;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const lucide_react_native_1 = require("lucide-react-native");
|
|
7
|
+
const atoms = tslib_1.__importStar(require("../../../lib/theme/atoms"));
|
|
8
|
+
const livestream_store_1 = require("../../../livestream-store");
|
|
9
|
+
const ui_1 = require("../../ui");
|
|
10
|
+
function Viewers() {
|
|
11
|
+
const viewers = (0, livestream_store_1.useViewers)();
|
|
12
|
+
return ((0, jsx_runtime_1.jsxs)(ui_1.View, { style: [
|
|
13
|
+
atoms.layout.flex.center,
|
|
14
|
+
atoms.layout.flex.row,
|
|
15
|
+
atoms.gap.all[2],
|
|
16
|
+
], children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Eye, { color: "#fd5050" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { style: {
|
|
17
|
+
color: "#fd5050",
|
|
18
|
+
textShadowColor: "black",
|
|
19
|
+
textShadowOffset: { width: -1, height: 1 },
|
|
20
|
+
textShadowRadius: 3,
|
|
21
|
+
fontSize: 16,
|
|
22
|
+
}, children: new Intl.NumberFormat(undefined, { notation: "compact" }).format(viewers || 0) })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = useWebRTC;
|
|
4
|
+
exports.negotiateConnectionWithClientOffer = negotiateConnectionWithClientOffer;
|
|
5
|
+
exports.useWebRTCIngest = useWebRTCIngest;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const __1 = require("../..");
|
|
8
|
+
const webrtc_primitives_1 = require("./webrtc-primitives");
|
|
9
|
+
function useWebRTC(endpoint) {
|
|
10
|
+
const [mediaStream, setMediaStream] = (0, react_1.useState)(null);
|
|
11
|
+
const [stuck, setStuck] = (0, react_1.useState)(false);
|
|
12
|
+
const setStatus = (0, __1.usePlayerStore)((x) => x.setStatus);
|
|
13
|
+
const lastChange = (0, react_1.useRef)(0);
|
|
14
|
+
(0, react_1.useEffect)(() => {
|
|
15
|
+
const peerConnection = new webrtc_primitives_1.RTCPeerConnection({
|
|
16
|
+
bundlePolicy: "max-bundle",
|
|
17
|
+
});
|
|
18
|
+
peerConnection.addTransceiver("video", {
|
|
19
|
+
direction: "recvonly",
|
|
20
|
+
});
|
|
21
|
+
peerConnection.addTransceiver("audio", {
|
|
22
|
+
direction: "recvonly",
|
|
23
|
+
});
|
|
24
|
+
peerConnection.addEventListener("track", (event) => {
|
|
25
|
+
const track = event.track;
|
|
26
|
+
if (!track) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setMediaStream(event.streams[0]);
|
|
30
|
+
});
|
|
31
|
+
peerConnection.addEventListener("connectionstatechange", () => {
|
|
32
|
+
console.log("connection state change", peerConnection.connectionState);
|
|
33
|
+
if (peerConnection.connectionState === "closed" ||
|
|
34
|
+
peerConnection.connectionState === "failed" ||
|
|
35
|
+
peerConnection.connectionState === "disconnected") {
|
|
36
|
+
console.log("setting stuck to true", peerConnection.connectionState);
|
|
37
|
+
setStuck(true);
|
|
38
|
+
}
|
|
39
|
+
if (peerConnection.connectionState !== "connected") {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
peerConnection.addEventListener("negotiationneeded", () => {
|
|
44
|
+
negotiateConnectionWithClientOffer(peerConnection, endpoint);
|
|
45
|
+
});
|
|
46
|
+
let lastFramesReceived = 0;
|
|
47
|
+
let lastAudioFramesReceived = 0;
|
|
48
|
+
const handle = setInterval(async () => {
|
|
49
|
+
const stats = await peerConnection.getStats();
|
|
50
|
+
stats.forEach((stat) => {
|
|
51
|
+
const mediaType = stat.mediaType /* web */ ?? stat.kind; /* native */
|
|
52
|
+
if (stat.type === "inbound-rtp" && mediaType === "audio") {
|
|
53
|
+
const audioFramesReceived = stat.lastPacketReceivedTimestamp;
|
|
54
|
+
if (lastAudioFramesReceived !== audioFramesReceived) {
|
|
55
|
+
lastAudioFramesReceived = audioFramesReceived;
|
|
56
|
+
lastChange.current = Date.now();
|
|
57
|
+
setStatus(__1.PlayerStatus.PLAYING);
|
|
58
|
+
setStuck(false);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (stat.type === "inbound-rtp" && mediaType === "video") {
|
|
62
|
+
const framesReceived = stat.framesReceived;
|
|
63
|
+
if (lastFramesReceived !== framesReceived) {
|
|
64
|
+
lastFramesReceived = framesReceived;
|
|
65
|
+
lastChange.current = Date.now();
|
|
66
|
+
setStatus(__1.PlayerStatus.PLAYING);
|
|
67
|
+
setStuck(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (Date.now() - lastChange.current > 2000) {
|
|
72
|
+
setStuck(true);
|
|
73
|
+
}
|
|
74
|
+
}, 200);
|
|
75
|
+
return () => {
|
|
76
|
+
clearInterval(handle);
|
|
77
|
+
peerConnection.close();
|
|
78
|
+
};
|
|
79
|
+
}, [endpoint]);
|
|
80
|
+
return [mediaStream, stuck];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Performs the actual SDP exchange.
|
|
84
|
+
*
|
|
85
|
+
* 1. Constructs the client's SDP offer
|
|
86
|
+
* 2. Sends the SDP offer to the server,
|
|
87
|
+
* 3. Awaits the server's offer.
|
|
88
|
+
*
|
|
89
|
+
* SDP describes what kind of media we can send and how the server and client communicate.
|
|
90
|
+
*
|
|
91
|
+
* https://developer.mozilla.org/en-US/docs/Glossary/SDP
|
|
92
|
+
* https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html#name-protocol-operation
|
|
93
|
+
*/
|
|
94
|
+
async function negotiateConnectionWithClientOffer(peerConnection, endpoint, bearerToken) {
|
|
95
|
+
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */
|
|
96
|
+
const offer = await peerConnection.createOffer({
|
|
97
|
+
offerToReceiveAudio: true,
|
|
98
|
+
offerToReceiveVideo: true,
|
|
99
|
+
});
|
|
100
|
+
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */
|
|
101
|
+
await peerConnection.setLocalDescription(offer);
|
|
102
|
+
/** Wait for ICE gathering to complete */
|
|
103
|
+
let ofr = await waitToCompleteICEGathering(peerConnection);
|
|
104
|
+
if (!ofr) {
|
|
105
|
+
throw Error("failed to gather ICE candidates for offer");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* As long as the connection is open, attempt to...
|
|
109
|
+
*/
|
|
110
|
+
while (peerConnection.connectionState !== "closed") {
|
|
111
|
+
try {
|
|
112
|
+
/**
|
|
113
|
+
* This response contains the server's SDP offer.
|
|
114
|
+
* This specifies how the client should communicate,
|
|
115
|
+
* and what kind of media client and server have negotiated to exchange.
|
|
116
|
+
*/
|
|
117
|
+
let response = await postSDPOffer(`${endpoint}`, ofr.sdp, bearerToken);
|
|
118
|
+
if (response.status === 201) {
|
|
119
|
+
let answerSDP = await response.text();
|
|
120
|
+
if (peerConnection.connectionState === "closed") {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await peerConnection.setRemoteDescription(new webrtc_primitives_1.RTCSessionDescription({ type: "answer", sdp: answerSDP }));
|
|
124
|
+
return response.headers.get("Location");
|
|
125
|
+
}
|
|
126
|
+
else if (response.status === 405) {
|
|
127
|
+
console.log("Remember to update the URL passed into the WHIP or WHEP client");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const errorMessage = await response.text();
|
|
131
|
+
console.error(errorMessage);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.error(`posting sdp offer failed: ${e}`);
|
|
136
|
+
}
|
|
137
|
+
/** Limit reconnection attempts to at-most once every 5 seconds */
|
|
138
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function postSDPOffer(endpoint, data, bearerToken) {
|
|
142
|
+
return await fetch(endpoint, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
mode: "cors",
|
|
145
|
+
headers: {
|
|
146
|
+
"content-type": "application/sdp",
|
|
147
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
148
|
+
},
|
|
149
|
+
body: data,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Receives an RTCPeerConnection and waits until
|
|
154
|
+
* the connection is initialized or a timeout passes.
|
|
155
|
+
*
|
|
156
|
+
* https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html#section-4.1
|
|
157
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceGatheringState
|
|
158
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/icegatheringstatechange_event
|
|
159
|
+
*/
|
|
160
|
+
async function waitToCompleteICEGathering(peerConnection) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
/** Wait at most 1 second for ICE gathering. */
|
|
163
|
+
setTimeout(function () {
|
|
164
|
+
if (peerConnection.connectionState === "closed") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
resolve(peerConnection.localDescription);
|
|
168
|
+
}, 1000);
|
|
169
|
+
peerConnection.addEventListener("icegatheringstatechange", (ev) => {
|
|
170
|
+
if (peerConnection.iceGatheringState === "complete") {
|
|
171
|
+
resolve(peerConnection.localDescription);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function useWebRTCIngest({ endpoint, }) {
|
|
177
|
+
const [mediaStream, setMediaStream] = (0, react_1.useState)(null);
|
|
178
|
+
const ingestConnectionState = (0, __1.usePlayerStore)((x) => x.ingestConnectionState);
|
|
179
|
+
const setIngestConnectionState = (0, __1.usePlayerStore)((x) => x.setIngestConnectionState);
|
|
180
|
+
const storedKey = (0, __1.useStreamKey)();
|
|
181
|
+
const [peerConnection, setPeerConnection] = (0, react_1.useState)(null);
|
|
182
|
+
const videoTransceiver = (0, react_1.useRef)(null);
|
|
183
|
+
const audioTransceiver = (0, react_1.useRef)(null);
|
|
184
|
+
const [retryTime, setRetryTime] = (0, react_1.useState)(0);
|
|
185
|
+
const ingestLive = (0, __1.usePlayerStore)((x) => x.ingestLive);
|
|
186
|
+
// "Outer loop": when we need a new peer connection, this sets that up
|
|
187
|
+
(0, react_1.useEffect)(() => {
|
|
188
|
+
if (!storedKey) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!ingestLive) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const peerConnection = new webrtc_primitives_1.RTCPeerConnection({
|
|
195
|
+
bundlePolicy: "max-bundle",
|
|
196
|
+
});
|
|
197
|
+
videoTransceiver.current = peerConnection.addTransceiver("video", {
|
|
198
|
+
direction: "sendonly",
|
|
199
|
+
});
|
|
200
|
+
audioTransceiver.current = peerConnection.addTransceiver("audio", {
|
|
201
|
+
direction: "sendonly",
|
|
202
|
+
});
|
|
203
|
+
peerConnection.addEventListener("connectionstatechange", (ev) => {
|
|
204
|
+
setIngestConnectionState(peerConnection.connectionState);
|
|
205
|
+
console.log("connection state change", peerConnection.connectionState);
|
|
206
|
+
if (peerConnection.connectionState === "failed") {
|
|
207
|
+
setRetryTime(Date.now());
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
peerConnection.addEventListener("negotiationneeded", (ev) => {
|
|
211
|
+
negotiateConnectionWithClientOffer(peerConnection, endpoint, storedKey.streamKey?.privateKey);
|
|
212
|
+
});
|
|
213
|
+
peerConnection.addEventListener("track", (ev) => {
|
|
214
|
+
console.log(ev);
|
|
215
|
+
});
|
|
216
|
+
setPeerConnection(peerConnection);
|
|
217
|
+
return () => {
|
|
218
|
+
peerConnection.close();
|
|
219
|
+
};
|
|
220
|
+
}, [endpoint, storedKey.streamKey?.privateKey, retryTime, ingestLive]);
|
|
221
|
+
// "Inner loop": when our tracks change, we update the transceivers
|
|
222
|
+
(0, react_1.useEffect)(() => {
|
|
223
|
+
if (!mediaStream) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!peerConnection) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!ingestLive) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
for (const track of mediaStream.getTracks()) {
|
|
233
|
+
console.log("adding track", track.kind, track.label, track.enabled, track.readyState);
|
|
234
|
+
if (track.kind === "video") {
|
|
235
|
+
videoTransceiver.current?.sender?.replaceTrack(track);
|
|
236
|
+
}
|
|
237
|
+
else if (track.kind === "audio") {
|
|
238
|
+
audioTransceiver.current?.sender?.replaceTrack(track);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}, [peerConnection, mediaStream, ingestLive]);
|
|
242
|
+
return [mediaStream, setMediaStream];
|
|
243
|
+
}
|