@streamplace/components 0.7.3 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/components/chat/chat-box.js +210 -25
  2. package/dist/components/chat/chat-message.js +5 -5
  3. package/dist/components/chat/chat.js +83 -5
  4. package/dist/components/chat/emoji-suggestions.js +35 -0
  5. package/dist/components/chat/mod-view.js +59 -8
  6. package/dist/components/chat/system-message.js +19 -0
  7. package/dist/components/icons/bluesky-icon.js +9 -0
  8. package/dist/components/keep-awake.js +7 -0
  9. package/dist/components/keep-awake.native.js +16 -0
  10. package/dist/components/mobile-player/fullscreen.js +2 -1
  11. package/dist/components/mobile-player/fullscreen.native.js +3 -3
  12. package/dist/components/mobile-player/player.js +15 -30
  13. package/dist/components/mobile-player/ui/index.js +2 -1
  14. package/dist/components/mobile-player/ui/report-modal.js +90 -0
  15. package/dist/components/mobile-player/ui/{loading.js → streamer-loading-overlay.js} +1 -1
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
  17. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
  18. package/dist/components/mobile-player/use-webrtc.js +7 -1
  19. package/dist/components/mobile-player/video-retry.js +29 -0
  20. package/dist/components/mobile-player/video.js +84 -9
  21. package/dist/components/mobile-player/video.native.js +24 -10
  22. package/dist/components/share/sharesheet.js +91 -0
  23. package/dist/components/ui/dialog.js +1 -1
  24. package/dist/components/ui/dropdown.js +6 -6
  25. package/dist/components/ui/index.js +2 -0
  26. package/dist/components/ui/primitives/modal.js +0 -1
  27. package/dist/components/ui/slider.js +5 -0
  28. package/dist/hooks/index.js +1 -0
  29. package/dist/hooks/usePointerDevice.js +71 -0
  30. package/dist/index.js +10 -3
  31. package/dist/lib/system-messages.js +101 -0
  32. package/dist/livestream-store/chat.js +111 -18
  33. package/dist/livestream-store/livestream-store.js +3 -0
  34. package/dist/livestream-store/problems.js +76 -0
  35. package/dist/livestream-store/websocket-consumer.js +39 -4
  36. package/dist/player-store/player-store.js +30 -4
  37. package/dist/streamplace-store/block.js +51 -12
  38. package/dist/ui/index.js +79 -0
  39. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  40. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  41. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  42. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  43. package/package.json +6 -2
  44. package/src/components/chat/chat-box.tsx +293 -25
  45. package/src/components/chat/chat-message.tsx +6 -7
  46. package/src/components/chat/chat.tsx +192 -41
  47. package/src/components/chat/emoji-suggestions.tsx +94 -0
  48. package/src/components/chat/mod-view.tsx +119 -40
  49. package/src/components/chat/system-message.tsx +38 -0
  50. package/src/components/icons/bluesky-icon.tsx +9 -0
  51. package/src/components/keep-awake.native.tsx +13 -0
  52. package/src/components/keep-awake.tsx +3 -0
  53. package/src/components/mobile-player/fullscreen.native.tsx +12 -3
  54. package/src/components/mobile-player/fullscreen.tsx +10 -3
  55. package/src/components/mobile-player/player.tsx +28 -36
  56. package/src/components/mobile-player/props.tsx +1 -0
  57. package/src/components/mobile-player/ui/index.ts +2 -1
  58. package/src/components/mobile-player/ui/report-modal.tsx +195 -0
  59. package/src/components/mobile-player/ui/{loading.tsx → streamer-loading-overlay.tsx} +1 -1
  60. package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
  61. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
  62. package/src/components/mobile-player/use-webrtc.tsx +10 -2
  63. package/src/components/mobile-player/video-retry.tsx +28 -0
  64. package/src/components/mobile-player/video.native.tsx +24 -10
  65. package/src/components/mobile-player/video.tsx +100 -21
  66. package/src/components/share/sharesheet.tsx +185 -0
  67. package/src/components/ui/dialog.tsx +1 -1
  68. package/src/components/ui/dropdown.tsx +13 -13
  69. package/src/components/ui/index.ts +2 -0
  70. package/src/components/ui/primitives/modal.tsx +0 -1
  71. package/src/components/ui/slider.tsx +1 -0
  72. package/src/hooks/index.ts +1 -0
  73. package/src/hooks/usePointerDevice.ts +89 -0
  74. package/src/index.tsx +11 -2
  75. package/src/lib/system-messages.ts +135 -0
  76. package/src/livestream-store/chat.tsx +145 -17
  77. package/src/livestream-store/livestream-state.tsx +10 -0
  78. package/src/livestream-store/livestream-store.tsx +3 -0
  79. package/src/livestream-store/problems.tsx +96 -0
  80. package/src/livestream-store/websocket-consumer.tsx +44 -4
  81. package/src/player-store/player-state.tsx +21 -4
  82. package/src/player-store/player-store.tsx +38 -5
  83. package/src/streamplace-store/block.tsx +55 -13
  84. package/src/ui/index.ts +86 -0
  85. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeepAwake = KeepAwake;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const expo_keep_awake_1 = require("expo-keep-awake");
6
+ const react_1 = require("react");
7
+ function KeepAwake() {
8
+ // useKeepAwake();
9
+ (0, react_1.useEffect)(() => {
10
+ (0, expo_keep_awake_1.activateKeepAwakeAsync)();
11
+ return () => {
12
+ (0, expo_keep_awake_1.deactivateKeepAwake)();
13
+ };
14
+ }, []);
15
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
16
+ }
@@ -7,6 +7,7 @@ const react_1 = require("react");
7
7
  const __1 = require("../..");
8
8
  const ui_1 = require("../../components/ui");
9
9
  const video_1 = tslib_1.__importDefault(require("./video"));
10
+ const video_retry_1 = tslib_1.__importDefault(require("./video-retry"));
10
11
  function Fullscreen(props) {
11
12
  const playerId = (0, __1.getFirstPlayerID)();
12
13
  const protocol = (0, __1.usePlayerStore)((x) => x.protocol, playerId);
@@ -69,5 +70,5 @@ function Fullscreen(props) {
69
70
  document.body.removeEventListener("webkitfullscreenchange", listener);
70
71
  };
71
72
  }, []);
72
- return ((0, jsx_runtime_1.jsx)(ui_1.View, { ref: divRef, children: (0, jsx_runtime_1.jsx)(video_1.default, {}) }));
73
+ return ((0, jsx_runtime_1.jsxs)(ui_1.View, { ref: divRef, style: { width: "100%", height: "100%", overflow: "hidden" }, children: [(0, jsx_runtime_1.jsx)(video_retry_1.default, { children: (0, jsx_runtime_1.jsx)(video_1.default, {}) }), props.children] }));
73
74
  }
@@ -119,7 +119,7 @@ function Fullscreen(props) {
119
119
  width: isLandscape ? dimensions.width + 40 : dimensions.width,
120
120
  height: dimensions.height,
121
121
  },
122
- ], children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
122
+ ], children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
123
123
  styles.videoContainer,
124
124
  {
125
125
  width: isLandscape ? videoWidth + 40 : videoWidth,
@@ -127,10 +127,10 @@ function Fullscreen(props) {
127
127
  left: leftPosition,
128
128
  top: topPosition,
129
129
  },
130
- ], children: (0, jsx_runtime_1.jsx)(video_native_1.default, {}) }) }));
130
+ ], children: [(0, jsx_runtime_1.jsx)(video_native_1.default, {}), props.children] }) }));
131
131
  }
132
132
  // Normal non-fullscreen mode
133
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(video_native_1.default, {}) }));
133
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(__1.VideoRetry, { children: (0, jsx_runtime_1.jsx)(video_native_1.default, {}) }), props.children] }));
134
134
  }
135
135
  const styles = react_native_1.StyleSheet.create({
136
136
  fullscreenContainer: {
@@ -7,18 +7,23 @@ const tslib_1 = require("tslib");
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const react_1 = require("react");
9
9
  const atoms_1 = require("../../lib/theme/atoms");
10
- const livestream_store_1 = require("../../livestream-store");
11
10
  const player_store_1 = require("../../player-store");
12
11
  const streamplace_store_1 = require("../../streamplace-store");
13
12
  const ui_1 = require("../ui");
14
13
  const fullscreen_1 = require("./fullscreen");
14
+ const report_modal_1 = tslib_1.__importDefault(require("./ui/report-modal"));
15
15
  const OFFLINE_THRESHOLD = 10000;
16
16
  exports.PlayerUI = tslib_1.__importStar(require("./ui"));
17
17
  function Player(props) {
18
- const playing = (0, player_store_1.usePlayerStore)((x) => x.status === player_store_1.PlayerStatus.PLAYING);
19
- const setOffline = (0, player_store_1.usePlayerStore)((x) => x.setOffline);
20
18
  const setIngest = (0, player_store_1.usePlayerStore)((x) => x.setIngestConnectionState);
21
19
  const clearControlsTimeout = (0, player_store_1.usePlayerStore)((x) => x.clearControlsTimeout);
20
+ const setReportingURL = (0, player_store_1.usePlayerStore)((x) => x.setReportingURL);
21
+ const reportModalOpen = (0, player_store_1.usePlayerStore)((x) => x.reportModalOpen);
22
+ const setReportModalOpen = (0, player_store_1.usePlayerStore)((x) => x.setReportModalOpen);
23
+ const reportSubject = (0, player_store_1.usePlayerStore)((x) => x.reportSubject);
24
+ (0, react_1.useEffect)(() => {
25
+ setReportingURL(props.reportingURL ?? null);
26
+ }, [props.reportingURL]);
22
27
  // Will call back every few seconds to send health updates
23
28
  usePlayerStatus();
24
29
  (0, react_1.useEffect)(() => {
@@ -32,33 +37,13 @@ function Player(props) {
32
37
  clearControlsTimeout();
33
38
  };
34
39
  }, []);
35
- const segment = (0, livestream_store_1.useSegment)();
36
- const [lastCheck, setLastCheck] = (0, react_1.useState)(0);
37
- (0, react_1.useEffect)(() => {
38
- if (playing) {
39
- setOffline(false);
40
- return;
41
- }
42
- if (!segment) {
43
- setOffline(false);
44
- return;
45
- }
46
- const startTime = Date.parse(segment.startTime);
47
- if (!startTime) {
48
- console.error("startTime is not a number", segment.startTime);
49
- return;
50
- }
51
- const timeSinceStart = Date.now() - startTime;
52
- if (timeSinceStart > OFFLINE_THRESHOLD) {
53
- setOffline(true);
54
- return;
55
- }
56
- const handle = setTimeout(() => {
57
- setLastCheck(Date.now());
58
- }, 1000);
59
- return () => clearTimeout(handle);
60
- }, [segment, playing, lastCheck]);
61
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(ui_1.View, { style: [atoms_1.zIndex[0], atoms_1.flex.values[1], atoms_1.w.percent[100], atoms_1.layout.flex.center], children: (0, jsx_runtime_1.jsx)(fullscreen_1.Fullscreen, { src: props.src }) }) }));
40
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)(ui_1.View, { style: [
41
+ atoms_1.zIndex[0],
42
+ atoms_1.w.percent[100],
43
+ atoms_1.h.percent[100],
44
+ atoms_1.flex.shrink[1],
45
+ atoms_1.layout.flex.center,
46
+ ], children: [(0, jsx_runtime_1.jsx)(report_modal_1.default, { open: reportModalOpen, onOpenChange: setReportModalOpen, subject: reportSubject }), (0, jsx_runtime_1.jsx)(fullscreen_1.Fullscreen, { src: props.src, children: props.children })] }) }));
62
47
  }
63
48
  const POLL_INTERVAL = 5000;
64
49
  function usePlayerStatus() {
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./countdown"), exports);
5
5
  tslib_1.__exportStar(require("./input"), exports);
6
- tslib_1.__exportStar(require("./loading"), exports);
7
6
  tslib_1.__exportStar(require("./metrics"), exports);
8
7
  tslib_1.__exportStar(require("./streamer-context-menu"), exports);
8
+ tslib_1.__exportStar(require("./streamer-loading-overlay"), exports);
9
9
  tslib_1.__exportStar(require("./viewer-context-menu"), exports);
10
+ tslib_1.__exportStar(require("./viewer-loading-overlay"), exports);
10
11
  tslib_1.__exportStar(require("./viewers"), exports);
@@ -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;
@@ -7,7 +7,7 @@ const react_1 = require("react");
7
7
  const react_native_reanimated_1 = tslib_1.__importStar(require("react-native-reanimated"));
8
8
  const atoms_1 = require("../../../lib/theme/atoms");
9
9
  const defaultMessages = [
10
- "Creating your stream",
10
+ "Creating the stream",
11
11
  "Uploading thumbnails",
12
12
  "Getting things ready",
13
13
  "Doing some magic",
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContextMenu = ContextMenu;
4
+ exports.ReportButton = ReportButton;
4
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const dropdown_menu_1 = require("@rn-primitives/dropdown-menu");
5
7
  const lucide_react_native_1 = require("lucide-react-native");
6
8
  const theme_1 = require("../../../lib/theme");
7
9
  const livestream_store_1 = require("../../../livestream-store");
@@ -19,5 +21,22 @@ function ContextMenu() {
19
21
  const setLowLatency = (value) => {
20
22
  setProtocol(value ? player_store_1.PlayerProtocol.WEBRTC : player_store_1.PlayerProtocol.HLS);
21
23
  };
22
- 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.Menu, { size: 32, color: theme_1.colors.gray[200] }) }), (0, jsx_runtime_1.jsxs)(ui_1.ResponsiveDropdownMenuContent, { 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" }) }) })] })] }));
24
+ 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.jsxs)(ui_1.ResponsiveDropdownMenuContent, { 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, {}) })] })] }));
25
+ }
26
+ function ReportButton() {
27
+ const livestream = (0, livestream_store_1.useLivestreamStore)((x) => x.livestream);
28
+ const setReportModalOpen = (0, player_store_1.usePlayerStore)((x) => x.setReportModalOpen);
29
+ const setReportSubject = (0, player_store_1.usePlayerStore)((x) => x.setReportSubject);
30
+ const { onOpenChange } = (0, dropdown_menu_1.useRootContext)();
31
+ return ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
32
+ if (!livestream)
33
+ return;
34
+ onOpenChange?.(false);
35
+ setReportModalOpen(true);
36
+ setReportSubject({
37
+ $type: "com.atproto.repo.strongRef",
38
+ uri: livestream.uri,
39
+ cid: livestream.cid,
40
+ });
41
+ }, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Report Livestream..." }) }));
23
42
  }
@@ -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
+ }
@@ -9,6 +9,7 @@ const webrtc_primitives_1 = require("./webrtc-primitives");
9
9
  function useWebRTC(endpoint) {
10
10
  const [mediaStream, setMediaStream] = (0, react_1.useState)(null);
11
11
  const [stuck, setStuck] = (0, react_1.useState)(false);
12
+ const setStatus = (0, __1.usePlayerStore)((x) => x.setStatus);
12
13
  const lastChange = (0, react_1.useRef)(0);
13
14
  (0, react_1.useEffect)(() => {
14
15
  const peerConnection = new webrtc_primitives_1.RTCPeerConnection({
@@ -29,7 +30,10 @@ function useWebRTC(endpoint) {
29
30
  });
30
31
  peerConnection.addEventListener("connectionstatechange", () => {
31
32
  console.log("connection state change", peerConnection.connectionState);
32
- if (peerConnection.connectionState === "closed") {
33
+ if (peerConnection.connectionState === "closed" ||
34
+ peerConnection.connectionState === "failed" ||
35
+ peerConnection.connectionState === "disconnected") {
36
+ console.log("setting stuck to true", peerConnection.connectionState);
33
37
  setStuck(true);
34
38
  }
35
39
  if (peerConnection.connectionState !== "connected") {
@@ -50,6 +54,7 @@ function useWebRTC(endpoint) {
50
54
  if (lastAudioFramesReceived !== audioFramesReceived) {
51
55
  lastAudioFramesReceived = audioFramesReceived;
52
56
  lastChange.current = Date.now();
57
+ setStatus(__1.PlayerStatus.PLAYING);
53
58
  setStuck(false);
54
59
  }
55
60
  }
@@ -58,6 +63,7 @@ function useWebRTC(endpoint) {
58
63
  if (lastFramesReceived !== framesReceived) {
59
64
  lastFramesReceived = framesReceived;
60
65
  lastChange.current = Date.now();
66
+ setStatus(__1.PlayerStatus.PLAYING);
61
67
  setStuck(false);
62
68
  }
63
69
  }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = VideoRetry;
4
+ const tslib_1 = require("tslib");
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = tslib_1.__importStar(require("react"));
7
+ const __1 = require("../..");
8
+ function VideoRetry(props) {
9
+ const retryTimeoutRef = (0, react_1.useRef)(null);
10
+ const [retries, setRetries] = (0, react_1.useState)(0);
11
+ const playing = (0, __1.usePlayerStore)((x) => x.status === __1.PlayerStatus.PLAYING);
12
+ (0, react_1.useEffect)(() => {
13
+ if (!playing) {
14
+ const jitter = 500 + Math.random() * 1500;
15
+ retryTimeoutRef.current = setTimeout(() => {
16
+ console.log("Retrying video playback...");
17
+ setRetries((prevRetries) => prevRetries + 1);
18
+ }, jitter);
19
+ }
20
+ return () => {
21
+ if (retryTimeoutRef.current) {
22
+ console.log("Clearing retry timeout");
23
+ clearTimeout(retryTimeoutRef.current);
24
+ retryTimeoutRef.current = null;
25
+ }
26
+ };
27
+ }, [!playing]);
28
+ return (0, jsx_runtime_1.jsx)(react_1.default.Fragment, { children: props.children }, retries);
29
+ }
@@ -14,6 +14,7 @@ const react_1 = require("react");
14
14
  const __1 = require("../..");
15
15
  const atoms_1 = require("../../lib/theme/atoms");
16
16
  const index_1 = require("../ui/index");
17
+ const loader_1 = require("../ui/loader");
17
18
  const shared_1 = require("./shared");
18
19
  const use_webrtc_1 = tslib_1.__importStar(require("./use-webrtc"));
19
20
  const webrtc_diagnostics_1 = require("./webrtc-diagnostics");
@@ -102,23 +103,56 @@ const VideoElement = (0, react_1.forwardRef)((props, ref) => {
102
103
  };
103
104
  const [firstAttempt, setFirstAttempt] = (0, react_1.useState)(true);
104
105
  const localVideoRef = props.videoRef ?? (0, react_1.useRef)(null);
106
+ // setPipAction comes from Zustand store
107
+ (0, react_1.useEffect)(() => {
108
+ if (typeof x.setPipAction === "function") {
109
+ const fn = () => {
110
+ if (localVideoRef.current) {
111
+ try {
112
+ localVideoRef.current.requestPictureInPicture?.();
113
+ }
114
+ catch (err) {
115
+ console.error("Error requesting Picture-in-Picture:", err);
116
+ }
117
+ }
118
+ else {
119
+ console.log("No video ref available for PiP");
120
+ }
121
+ };
122
+ x.setPipAction(fn);
123
+ }
124
+ // Cleanup on unmount
125
+ return () => {
126
+ if (typeof x.setPipAction === "function") {
127
+ x.setPipAction(undefined);
128
+ }
129
+ };
130
+ }, []);
105
131
  const canPlayThrough = (e) => {
132
+ console.log("canPlayThrough called", {
133
+ firstAttempt,
134
+ videoRef: !!localVideoRef.current,
135
+ });
136
+ setStatus(__1.PlayerStatus.PLAYING);
106
137
  event("canplaythrough")(e);
107
138
  if (firstAttempt && localVideoRef.current) {
108
139
  setFirstAttempt(false);
140
+ console.log("Attempting to play video");
109
141
  localVideoRef.current.play().catch((err) => {
142
+ console.log("error playing video", err.name);
110
143
  if (err.name === "NotAllowedError") {
111
144
  if (localVideoRef.current) {
145
+ console.log("Setting muted and retrying");
112
146
  setMuted(true);
113
147
  localVideoRef.current.muted = true;
114
148
  localVideoRef.current
115
149
  .play()
116
150
  .then(() => {
117
- console.warn("Browser forced video to start muted");
151
+ console.log("Muted play succeeded");
118
152
  setMuteWasForced(true);
119
153
  })
120
154
  .catch((err) => {
121
- console.error("error playing video", err);
155
+ console.error("Muted play also failed", err);
122
156
  });
123
157
  }
124
158
  }
@@ -148,15 +182,24 @@ const VideoElement = (0, react_1.forwardRef)((props, ref) => {
148
182
  ref.current =
149
183
  videoElement;
150
184
  }
151
- // if (localVideoRef && typeof localVideoRef !== "function") {
152
- // localVideoRef.current = videoElement;
153
- // }
185
+ localVideoRef.current = videoElement;
154
186
  };
155
- return ((0, jsx_runtime_1.jsx)("video", { autoPlay: true, playsInline: true, ref: handleVideoRef, controls: false, src: ingest ? undefined : props.url, muted: muted, crossOrigin: "anonymous", onMouseMove: setUserInteraction, onClick: setUserInteraction, onAbort: event("abort"), onCanPlay: event("canplay"), onCanPlayThrough: canPlayThrough, onEmptied: event("emptied"), onEncrypted: event("encrypted"), onEnded: event("ended"), onError: event("error"), onLoadedData: event("loadeddata"), onLoadedMetadata: event("loadedmetadata"), onLoadStart: event("loadstart"), onPause: event("pause"), onPlay: event("play"), onPlaying: event("playing"), onRateChange: event("ratechange"), onSeeked: event("seeked"), onSeeking: event("seeking"), onStalled: event("stalled"), onSuspend: event("suspend"), onVolumeChange: event("volumechange"), onWaiting: event("waiting"), style: {
187
+ const eventLogger = (evType) => (e) => {
188
+ console.log("📺 Video event:", evType);
189
+ const now = new Date();
190
+ if (updateEvents[evType]) {
191
+ x.setStatus(evType);
192
+ }
193
+ console.log("Sending", evType, "status to", url);
194
+ playerEvent(url, now.toISOString(), evType, {});
195
+ };
196
+ return ((0, jsx_runtime_1.jsx)("video", { autoPlay: true, playsInline: true, ref: handleVideoRef, controls: false, src: ingest ? undefined : props.url, muted: muted, crossOrigin: "anonymous", onMouseMove: setUserInteraction, onClick: setUserInteraction, onAbort: event("abort"), onCanPlay: eventLogger, onCanPlayThroughCapture: eventLogger, onCanPlayThrough: canPlayThrough, onEmptied: event("emptied"), onEncrypted: event("encrypted"), onEnded: event("ended"), onError: event("error"), onLoadedData: event("loadeddata"), onLoadedMetadata: event("loadedmetadata"), onLoadStart: event("loadstart"), onPause: event("pause"), onPlay: event("play"), onPlaying: event("playing"), onRateChange: event("ratechange"), onSeeked: event("seeked"), onSeeking: event("seeking"), onStalled: event("stalled"), onSuspend: event("suspend"), onVolumeChange: event("volumechange"), onWaiting: event("waiting"), style: {
156
197
  objectFit: "contain",
157
198
  backgroundColor: "transparent",
158
199
  width: "100%",
159
200
  height: "100%",
201
+ maxWidth: "100%",
202
+ maxHeight: "100%",
160
203
  transform: ingest ? "scaleX(-1)" : undefined,
161
204
  } }));
162
205
  });
@@ -238,6 +281,14 @@ function WebRTCPlayer(props) {
238
281
  }
239
282
  return (0, jsx_runtime_1.jsx)(WebRTCPlayerInner, { url: props.url, videoRef: props.videoRef });
240
283
  }
284
+ const connectionStatusMessages = {
285
+ initializing: "Starting up...",
286
+ connecting: "Connecting...",
287
+ "connection-failed": "Connecting...",
288
+ connected: "Connected",
289
+ reconnecting: "Reconnecting...",
290
+ checking: "Checking connection...",
291
+ };
241
292
  function WebRTCPlayerInner({ videoRef, url, width, height, }) {
242
293
  const [connectionStatus, setConnectionStatus] = (0, react_1.useState)("initializing");
243
294
  const status = (0, __1.usePlayerStore)((x) => x.status);
@@ -260,7 +311,7 @@ function WebRTCPlayerInner({ videoRef, url, width, height, }) {
260
311
  if (stuck && status === __1.PlayerStatus.PLAYING) {
261
312
  setStatus(__1.PlayerStatus.STALLED);
262
313
  }
263
- if (!stuck && mediaStream) {
314
+ if (!stuck && status === __1.PlayerStatus.STALLED) {
264
315
  setStatus(__1.PlayerStatus.PLAYING);
265
316
  }
266
317
  }, [stuck, status, mediaStream]);
@@ -303,7 +354,19 @@ function WebRTCPlayerInner({ videoRef, url, width, height, }) {
303
354
  videoRef.current.srcObject = mediaStream;
304
355
  }, [mediaStream]);
305
356
  if (!mediaStream) {
306
- return ((0, jsx_runtime_1.jsx)(index_1.View, { backgroundColor: "#111", style: { minWidth: "100%", minHeight: "100%" }, children: (0, jsx_runtime_1.jsxs)(index_1.View, { backgroundColor: atoms_1.colors.primary[800], style: { borderRadius: atoms_1.borderRadius.md }, children: [(0, jsx_runtime_1.jsx)(index_1.View, { children: (0, jsx_runtime_1.jsx)(index_1.Text, { children: "Connecting..." }) }), (0, jsx_runtime_1.jsxs)(index_1.Text, { children: ["Establishing WebRTC connection (", connectionStatus, ")"] })] }) }));
357
+ const isError = connectionStatus === "connection-failed";
358
+ return ((0, jsx_runtime_1.jsx)(index_1.View, { backgroundColor: "#111", style: {
359
+ minWidth: "100%",
360
+ minHeight: "100%",
361
+ display: "flex",
362
+ alignItems: "center",
363
+ justifyContent: "center",
364
+ }, children: (0, jsx_runtime_1.jsxs)(index_1.View, { style: {
365
+ borderRadius: atoms_1.borderRadius.md,
366
+ padding: 24,
367
+ alignItems: "center",
368
+ gap: 16,
369
+ }, children: [!isError && (0, jsx_runtime_1.jsx)(loader_1.Loader, { size: "large" }), (0, jsx_runtime_1.jsx)(index_1.Text, { size: "lg", weight: "semibold", children: connectionStatusMessages[connectionStatus] || "Connecting..." })] }) }));
307
370
  }
308
371
  return (0, jsx_runtime_1.jsx)(VideoElement, { url: url, ref: videoRef });
309
372
  }
@@ -379,7 +442,19 @@ function WebcamIngestPlayer(props) {
379
442
  videoElement.srcObject = localMediaStream;
380
443
  }, [videoElement, localMediaStream]);
381
444
  if (error) {
382
- return ((0, jsx_runtime_1.jsxs)(index_1.View, { backgroundColor: atoms_1.colors.destructive[900], style: [atoms_1.p[4], { borderRadius: atoms_1.borderRadius.md }], children: [(0, jsx_runtime_1.jsx)(index_1.View, { children: (0, jsx_runtime_1.jsx)(index_1.Text, { size: "xl", weight: "extrabold", children: "Error encountered!" }) }), (0, jsx_runtime_1.jsx)(index_1.Text, { children: error.message })] }));
445
+ return ((0, jsx_runtime_1.jsx)(index_1.View, { backgroundColor: "#111", style: {
446
+ minWidth: "100%",
447
+ minHeight: "100%",
448
+ display: "flex",
449
+ alignItems: "center",
450
+ justifyContent: "center",
451
+ }, children: (0, jsx_runtime_1.jsx)(index_1.View, { backgroundColor: atoms_1.colors.destructive[900], style: {
452
+ borderRadius: atoms_1.borderRadius.md,
453
+ padding: 24,
454
+ alignItems: "center",
455
+ gap: 16,
456
+ maxWidth: 400,
457
+ }, children: (0, jsx_runtime_1.jsx)(index_1.Text, { size: "xl", weight: "extrabold", color: "default", children: error.message }) }) }));
383
458
  }
384
459
  return (0, jsx_runtime_1.jsx)(VideoElement, { ...props, ref: handleRef });
385
460
  }
@@ -51,6 +51,15 @@ function NativeVideo() {
51
51
  };
52
52
  }, [setStatus]);
53
53
  const player = (0, expo_video_1.useVideoPlayer)(url, (player) => {
54
+ player.addListener("playingChange", (newIsPlaying) => {
55
+ console.log("playingChange", newIsPlaying);
56
+ if (newIsPlaying) {
57
+ setStatus(__1.PlayerStatus.PLAYING);
58
+ }
59
+ else {
60
+ setStatus(__1.PlayerStatus.WAITING);
61
+ }
62
+ });
54
63
  player.loop = true;
55
64
  player.muted = muted;
56
65
  player.play();
@@ -72,10 +81,12 @@ function NativeVideo() {
72
81
  ].map((evType) => {
73
82
  return player.addListener(evType, (...args) => {
74
83
  const now = new Date();
84
+ console.log("video native event", evType);
75
85
  playerEvent(spurl, now.toISOString(), evType, { args: args });
76
86
  });
77
87
  });
78
88
  subs.push(player.addListener("playingChange", (newIsPlaying) => {
89
+ console.log("playingChange", newIsPlaying);
79
90
  if (newIsPlaying) {
80
91
  setStatus(__1.PlayerStatus.PLAYING);
81
92
  }
@@ -100,6 +111,7 @@ function NativeWHEP() {
100
111
  const src = (0, __1.usePlayerStore)((x) => x.src);
101
112
  const { url } = (0, shared_1.srcToUrl)({ src: src, selectedRendition }, __1.PlayerProtocol.WEBRTC);
102
113
  const [stream, stuck] = (0, use_webrtc_1.default)(url);
114
+ const status = (0, __1.usePlayerStore)((x) => x.status);
103
115
  const setPlayerWidth = (0, __1.usePlayerStore)((x) => x.setPlayerWidth);
104
116
  const setPlayerHeight = (0, __1.usePlayerStore)((x) => x.setPlayerHeight);
105
117
  // PiP support: wire up videoRef (no direct ref for RTCView)
@@ -116,21 +128,23 @@ function NativeWHEP() {
116
128
  const muted = (0, __1.usePlayerStore)((x) => x.muted);
117
129
  const volume = (0, __1.usePlayerStore)((x) => x.volume);
118
130
  (0, react_1.useEffect)(() => {
119
- if (stuck) {
131
+ if (stuck && status === __1.PlayerStatus.PLAYING) {
132
+ console.log("setting status to stalled", status);
120
133
  setStatus(__1.PlayerStatus.STALLED);
121
134
  }
122
- else {
135
+ if (!stuck && status === __1.PlayerStatus.STALLED) {
136
+ console.log("setting status to playing", status);
123
137
  setStatus(__1.PlayerStatus.PLAYING);
124
138
  }
125
- }, [stuck, setStatus]);
139
+ }, [stuck, status]);
126
140
  const mediaStream = stream;
127
- (0, react_1.useEffect)(() => {
128
- if (!mediaStream) {
129
- setStatus(__1.PlayerStatus.WAITING);
130
- return;
131
- }
132
- setStatus(__1.PlayerStatus.PLAYING);
133
- }, [mediaStream, setStatus]);
141
+ // useEffect(() => {
142
+ // if (!mediaStream) {
143
+ // setStatus(PlayerStatus.WAITING);
144
+ // return;
145
+ // }
146
+ // setStatus(PlayerStatus.PLAYING);
147
+ // }, [mediaStream, setStatus]);
134
148
  (0, react_1.useEffect)(() => {
135
149
  if (!mediaStream) {
136
150
  return;