@streamplace/components 0.9.7 → 0.9.10
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/assets/badges/live.png +0 -0
- package/assets/badges/live_2x.png +0 -0
- package/assets/badges/mod.png +0 -0
- package/assets/badges/mod_2x.png +0 -0
- package/assets/badges/vip.png +0 -0
- package/assets/badges/vip_2x.png +0 -0
- package/dist/components/chat/badge.d.ts +10 -0
- package/dist/components/chat/badge.d.ts.map +1 -0
- package/dist/components/chat/badge.js +29 -0
- package/dist/components/chat/badge.js.map +1 -0
- package/dist/components/chat/chat-box.d.ts +5 -1
- package/dist/components/chat/chat-box.d.ts.map +1 -1
- package/dist/components/chat/chat-box.js +55 -50
- package/dist/components/chat/chat-box.js.map +1 -1
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +9 -11
- package/dist/components/chat/chat-message.js.map +1 -1
- package/dist/components/chat/chat.d.ts.map +1 -1
- package/dist/components/chat/chat.js +37 -43
- package/dist/components/chat/chat.js.map +1 -1
- package/dist/components/chat/emoji-suggestions.d.ts +7 -18
- package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
- package/dist/components/chat/emoji-suggestions.js +6 -2
- package/dist/components/chat/emoji-suggestions.js.map +1 -1
- package/dist/components/chat/system-message.d.ts.map +1 -1
- package/dist/components/chat/system-message.js +9 -1
- package/dist/components/chat/system-message.js.map +1 -1
- package/dist/components/chat/teleport-modal.d.ts +9 -0
- package/dist/components/chat/teleport-modal.d.ts.map +1 -0
- package/dist/components/chat/teleport-modal.js +148 -0
- package/dist/components/chat/teleport-modal.js.map +1 -0
- package/dist/components/chat/user-profile-card.d.ts +12 -0
- package/dist/components/chat/user-profile-card.d.ts.map +1 -0
- package/dist/components/chat/user-profile-card.js +135 -0
- package/dist/components/chat/user-profile-card.js.map +1 -0
- package/dist/components/dashboard/chat-panel.d.ts +3 -1
- package/dist/components/dashboard/chat-panel.d.ts.map +1 -1
- package/dist/components/dashboard/chat-panel.js +2 -2
- package/dist/components/dashboard/chat-panel.js.map +1 -1
- package/dist/components/dashboard/header.d.ts +2 -3
- package/dist/components/dashboard/header.d.ts.map +1 -1
- package/dist/components/dashboard/header.js +6 -2
- package/dist/components/dashboard/header.js.map +1 -1
- package/dist/components/dashboard/information-widget.d.ts.map +1 -1
- package/dist/components/dashboard/information-widget.js +15 -12
- package/dist/components/dashboard/information-widget.js.map +1 -1
- package/dist/components/mobile-player/fullscreen.d.ts.map +1 -1
- package/dist/components/mobile-player/fullscreen.js +2 -1
- package/dist/components/mobile-player/fullscreen.js.map +1 -1
- package/dist/components/mobile-player/fullscreen.native.d.ts.map +1 -1
- package/dist/components/mobile-player/fullscreen.native.js +3 -2
- package/dist/components/mobile-player/fullscreen.native.js.map +1 -1
- package/dist/components/mobile-player/player.d.ts.map +1 -1
- package/dist/components/mobile-player/player.js +15 -0
- package/dist/components/mobile-player/player.js.map +1 -1
- package/dist/components/mobile-player/ui/audio-only-overlay.d.ts +2 -0
- package/dist/components/mobile-player/ui/audio-only-overlay.d.ts.map +1 -0
- package/dist/components/mobile-player/ui/audio-only-overlay.js +29 -0
- package/dist/components/mobile-player/ui/audio-only-overlay.js.map +1 -0
- package/dist/components/mobile-player/ui/index.d.ts +1 -0
- package/dist/components/mobile-player/ui/index.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/index.js +1 -0
- package/dist/components/mobile-player/ui/index.js.map +1 -1
- package/dist/components/mobile-player/ui/input.d.ts +3 -2
- package/dist/components/mobile-player/ui/input.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/input.js +18 -2
- package/dist/components/mobile-player/ui/input.js.map +1 -1
- package/dist/components/mobile-player/ui/metrics.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/metrics.js +20 -2
- package/dist/components/mobile-player/ui/metrics.js.map +1 -1
- package/dist/components/mobile-player/ui/streamer-context-menu.d.ts +3 -1
- package/dist/components/mobile-player/ui/streamer-context-menu.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/streamer-context-menu.js +64 -2
- package/dist/components/mobile-player/ui/streamer-context-menu.js.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js +29 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
- package/dist/components/mobile-player/use-webrtc.d.ts +4 -2
- package/dist/components/mobile-player/use-webrtc.d.ts.map +1 -1
- package/dist/components/mobile-player/use-webrtc.js +89 -15
- package/dist/components/mobile-player/use-webrtc.js.map +1 -1
- package/dist/components/mobile-player/video-async.native.d.ts.map +1 -1
- package/dist/components/mobile-player/video-async.native.js +15 -5
- package/dist/components/mobile-player/video-async.native.js.map +1 -1
- package/dist/components/mobile-player/video.d.ts.map +1 -1
- package/dist/components/mobile-player/video.js +10 -7
- package/dist/components/mobile-player/video.js.map +1 -1
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +8 -0
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useAQState.d.ts +2 -0
- package/dist/hooks/useAQState.d.ts.map +1 -0
- package/dist/hooks/useAQState.js +37 -0
- package/dist/hooks/useAQState.js.map +1 -0
- package/dist/hooks/useLivestreamInfo.d.ts +1 -2
- package/dist/hooks/useLivestreamInfo.d.ts.map +1 -1
- package/dist/hooks/useLivestreamInfo.js +18 -22
- package/dist/hooks/useLivestreamInfo.js.map +1 -1
- package/dist/hooks/useSegmentTiming.d.ts +1 -1
- package/dist/hooks/useSegmentTiming.d.ts.map +1 -1
- package/dist/hooks/useSegmentTiming.js +4 -0
- package/dist/hooks/useSegmentTiming.js.map +1 -1
- package/dist/i18n/i18n-loader.native.d.ts.map +1 -1
- package/dist/i18n/i18n-loader.native.js +13 -4
- package/dist/i18n/i18n-loader.native.js.map +1 -1
- package/dist/lib/slash-commands/teleport.d.ts +5 -1
- package/dist/lib/slash-commands/teleport.d.ts.map +1 -1
- package/dist/lib/slash-commands/teleport.js +57 -1
- package/dist/lib/slash-commands/teleport.js.map +1 -1
- package/dist/lib/theme/atoms.d.ts +125 -125
- package/dist/livestream-store/chat.d.ts +1 -0
- package/dist/livestream-store/chat.d.ts.map +1 -1
- package/dist/livestream-store/chat.js +10 -1
- package/dist/livestream-store/chat.js.map +1 -1
- package/dist/livestream-store/livestream-state.d.ts +2 -0
- package/dist/livestream-store/livestream-state.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.d.ts +1 -1
- package/dist/livestream-store/livestream-store.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.js +10 -1
- package/dist/livestream-store/livestream-store.js.map +1 -1
- package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
- package/dist/livestream-store/websocket-consumer.js +1 -0
- package/dist/livestream-store/websocket-consumer.js.map +1 -1
- package/dist/player-store/player-state.d.ts +3 -5
- package/dist/player-store/player-state.d.ts.map +1 -1
- package/dist/player-store/player-store.d.ts.map +1 -1
- package/dist/player-store/player-store.js +28 -5
- package/dist/player-store/player-store.js.map +1 -1
- package/dist/player-store/single-player-provider.d.ts +0 -2
- package/dist/player-store/single-player-provider.d.ts.map +1 -1
- package/dist/player-store/single-player-provider.js +0 -2
- package/dist/player-store/single-player-provider.js.map +1 -1
- package/dist/streamplace-store/branding.d.ts.map +1 -1
- package/dist/streamplace-store/branding.js +52 -1
- package/dist/streamplace-store/branding.js.map +1 -1
- package/dist/streamplace-store/stream.d.ts +4 -2
- package/dist/streamplace-store/stream.d.ts.map +1 -1
- package/dist/streamplace-store/stream.js +36 -74
- package/dist/streamplace-store/stream.js.map +1 -1
- package/locales/en-US/common.ftl +13 -1
- package/locales/manifest.json +21 -1
- package/locales/ro-RO/common.ftl +74 -0
- package/locales/ro-RO/settings.ftl +233 -0
- package/locales/zh-Hans/common.ftl +57 -0
- package/locales/zh-Hans/settings.ftl +222 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +2 -2
- package/src/components/chat/badge.tsx +45 -0
- package/src/components/chat/chat-box.tsx +84 -54
- package/src/components/chat/chat-message.tsx +25 -21
- package/src/components/chat/chat.tsx +107 -90
- package/src/components/chat/emoji-suggestions.tsx +12 -21
- package/src/components/chat/system-message.tsx +12 -2
- package/src/components/chat/teleport-modal.tsx +310 -0
- package/src/components/chat/user-profile-card.tsx +275 -0
- package/src/components/dashboard/chat-panel.tsx +8 -0
- package/src/components/dashboard/header.tsx +8 -17
- package/src/components/dashboard/information-widget.tsx +17 -10
- package/src/components/mobile-player/fullscreen.native.tsx +3 -0
- package/src/components/mobile-player/fullscreen.tsx +2 -0
- package/src/components/mobile-player/player.tsx +22 -1
- package/src/components/mobile-player/ui/audio-only-overlay.tsx +48 -0
- package/src/components/mobile-player/ui/index.ts +1 -0
- package/src/components/mobile-player/ui/input.tsx +42 -12
- package/src/components/mobile-player/ui/metrics.tsx +17 -2
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +138 -2
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +40 -3
- package/src/components/mobile-player/use-webrtc.tsx +118 -17
- package/src/components/mobile-player/video-async.native.tsx +18 -5
- package/src/components/mobile-player/video.tsx +10 -7
- package/src/components/ui/dialog.tsx +8 -0
- package/src/components/ui/textarea.tsx +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAQState.ts +37 -0
- package/src/hooks/useLivestreamInfo.ts +21 -22
- package/src/hooks/useSegmentTiming.tsx +7 -2
- package/src/i18n/i18n-loader.native.ts +9 -0
- package/src/lib/slash-commands/teleport.ts +68 -0
- package/src/livestream-store/chat.tsx +12 -0
- package/src/livestream-store/livestream-state.tsx +2 -0
- package/src/livestream-store/livestream-store.tsx +9 -1
- package/src/livestream-store/websocket-consumer.tsx +1 -0
- package/src/player-store/player-state.tsx +4 -7
- package/src/player-store/player-store.tsx +33 -7
- package/src/player-store/single-player-provider.tsx +0 -4
- package/src/streamplace-store/branding.tsx +60 -1
- package/src/streamplace-store/stream.tsx +42 -99
- package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
|
@@ -12,7 +12,10 @@ export function MetricsPanel({ showMetrics }: MetricsPanelProps) {
|
|
|
12
12
|
|
|
13
13
|
let icon = <CircleX color="#d44" />;
|
|
14
14
|
let color = "#d44";
|
|
15
|
-
if (connectionQuality === "
|
|
15
|
+
if (connectionQuality === "pre-live") {
|
|
16
|
+
icon = <CircleCheck color={atoms.colors.blue[500]} />;
|
|
17
|
+
color = atoms.colors.blue[500];
|
|
18
|
+
} else if (connectionQuality === "good") {
|
|
16
19
|
icon = <CircleCheck color="#4d4" />;
|
|
17
20
|
color = "#4d4";
|
|
18
21
|
} else if (connectionQuality === "degraded") {
|
|
@@ -23,6 +26,18 @@ export function MetricsPanel({ showMetrics }: MetricsPanelProps) {
|
|
|
23
26
|
color = "#d44";
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
const connectionText = () => {
|
|
30
|
+
if (connectionQuality === "pre-live") {
|
|
31
|
+
return "READY TO STREAM";
|
|
32
|
+
} else if (connectionQuality === "good") {
|
|
33
|
+
return "GOOD";
|
|
34
|
+
} else if (connectionQuality === "degraded") {
|
|
35
|
+
return "DEGRADED";
|
|
36
|
+
} else {
|
|
37
|
+
return "POOR";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
26
41
|
return (
|
|
27
42
|
<View
|
|
28
43
|
style={{
|
|
@@ -49,7 +64,7 @@ export function MetricsPanel({ showMetrics }: MetricsPanelProps) {
|
|
|
49
64
|
},
|
|
50
65
|
]}
|
|
51
66
|
>
|
|
52
|
-
{
|
|
67
|
+
{connectionText()}
|
|
53
68
|
</Text>
|
|
54
69
|
</View>
|
|
55
70
|
{showMetrics && (
|
|
@@ -1,3 +1,139 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { ChevronRight, Cog } from "lucide-react-native";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withSequence,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import { useLivestreamInfo, zero } from "../../..";
|
|
12
|
+
import { usePlayerStore } from "../../../player-store";
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuCheckboxItem,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
ResponsiveDropdownMenuContent,
|
|
20
|
+
Text,
|
|
21
|
+
useTheme,
|
|
22
|
+
} from "../../ui";
|
|
23
|
+
|
|
24
|
+
export function StreamContextMenu({
|
|
25
|
+
dropdownPortalContainer,
|
|
26
|
+
}: {
|
|
27
|
+
dropdownPortalContainer?: string;
|
|
28
|
+
}) {
|
|
29
|
+
const th = useTheme();
|
|
30
|
+
const debugInfo = usePlayerStore((x) => x.showDebugInfo);
|
|
31
|
+
const setShowDebugInfo = usePlayerStore((x) => x.setShowDebugInfo);
|
|
32
|
+
const { toggleStopStream } = useLivestreamInfo();
|
|
33
|
+
const ingest = usePlayerStore((x) => x.ingestConnectionState);
|
|
34
|
+
const isLive = ingest !== null && ingest !== "new";
|
|
35
|
+
|
|
36
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
37
|
+
const [hasShownTooltip, setHasShownTooltip] = useState(false);
|
|
38
|
+
|
|
39
|
+
const tooltipOpacity = useSharedValue(0);
|
|
40
|
+
const tooltipTranslateX = useSharedValue(20);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isLive && !hasShownTooltip) {
|
|
44
|
+
tooltipOpacity.value = withDelay(
|
|
45
|
+
500,
|
|
46
|
+
withSequence(
|
|
47
|
+
withTiming(1, { duration: 300 }),
|
|
48
|
+
withDelay(10000, withTiming(0, { duration: 300 })),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
tooltipTranslateX.value = withDelay(
|
|
52
|
+
500,
|
|
53
|
+
withSequence(
|
|
54
|
+
withTiming(0, { duration: 300 }),
|
|
55
|
+
withDelay(10000, withTiming(20, { duration: 300 })),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
setHasShownTooltip(true);
|
|
59
|
+
}
|
|
60
|
+
}, [isLive, hasShownTooltip]);
|
|
61
|
+
|
|
62
|
+
const iconRotate = useAnimatedStyle(() => {
|
|
63
|
+
return {
|
|
64
|
+
transform: [
|
|
65
|
+
{
|
|
66
|
+
rotateZ: withTiming(isOpen ? "240deg" : "0deg", {
|
|
67
|
+
duration: 650,
|
|
68
|
+
easing: Easing.out(Easing.ease),
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const tooltipStyle = useAnimatedStyle(() => {
|
|
76
|
+
return {
|
|
77
|
+
opacity: tooltipOpacity.value,
|
|
78
|
+
transform: [{ translateX: tooltipTranslateX.value }],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<DropdownMenu onOpenChange={setIsOpen} key={dropdownPortalContainer}>
|
|
84
|
+
<DropdownMenuTrigger>
|
|
85
|
+
<Animated.View style={[iconRotate]}>
|
|
86
|
+
<Cog color={th.theme.colors.foreground} />
|
|
87
|
+
</Animated.View>
|
|
88
|
+
<Animated.View
|
|
89
|
+
style={[
|
|
90
|
+
tooltipStyle,
|
|
91
|
+
{
|
|
92
|
+
position: "absolute",
|
|
93
|
+
right: 30,
|
|
94
|
+
top: 0,
|
|
95
|
+
backgroundColor: "rgba(64,64,64,0.95)",
|
|
96
|
+
borderRadius: 8,
|
|
97
|
+
paddingHorizontal: 8,
|
|
98
|
+
paddingRight: 12,
|
|
99
|
+
paddingVertical: 4,
|
|
100
|
+
flexDirection: "row",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
gap: 6,
|
|
103
|
+
zIndex: 9999999,
|
|
104
|
+
pointerEvents: "box-none",
|
|
105
|
+
width: 120,
|
|
106
|
+
},
|
|
107
|
+
]}
|
|
108
|
+
>
|
|
109
|
+
<Text size="sm" color="white">
|
|
110
|
+
End stream here
|
|
111
|
+
</Text>
|
|
112
|
+
<ChevronRight color="white" size={16} style={[zero.mr[4]]} />
|
|
113
|
+
</Animated.View>
|
|
114
|
+
</DropdownMenuTrigger>
|
|
115
|
+
<ResponsiveDropdownMenuContent side="top" align="end">
|
|
116
|
+
{isLive && (
|
|
117
|
+
<DropdownMenuGroup title="Stream">
|
|
118
|
+
<DropdownMenuItem
|
|
119
|
+
closeOnPress={true}
|
|
120
|
+
onPress={() => {
|
|
121
|
+
toggleStopStream();
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<Text color="destructive">Stop Stream</Text>
|
|
125
|
+
</DropdownMenuItem>
|
|
126
|
+
</DropdownMenuGroup>
|
|
127
|
+
)}
|
|
128
|
+
<DropdownMenuGroup title="Advanced">
|
|
129
|
+
<DropdownMenuCheckboxItem
|
|
130
|
+
checked={debugInfo}
|
|
131
|
+
onCheckedChange={() => setShowDebugInfo(!debugInfo)}
|
|
132
|
+
>
|
|
133
|
+
<Text>Show Debug Info</Text>
|
|
134
|
+
</DropdownMenuCheckboxItem>
|
|
135
|
+
</DropdownMenuGroup>
|
|
136
|
+
</ResponsiveDropdownMenuContent>
|
|
137
|
+
</DropdownMenu>
|
|
138
|
+
);
|
|
3
139
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
formatHandleWithAt,
|
|
15
15
|
useAvatars,
|
|
16
16
|
useLivestreamInfo,
|
|
17
|
+
useStreamplaceStore,
|
|
17
18
|
zero,
|
|
18
19
|
} from "../../..";
|
|
19
20
|
import { useLivestreamStore } from "../../../livestream-store";
|
|
@@ -58,6 +59,38 @@ export function ContextMenu({
|
|
|
58
59
|
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
59
60
|
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
60
61
|
|
|
62
|
+
const isDevModeOn = useStreamplaceStore((x) => x.danmuUnlocked);
|
|
63
|
+
|
|
64
|
+
const latestSegment = useLivestreamStore((x) => x.segment);
|
|
65
|
+
// get highest height x width rendition for video
|
|
66
|
+
const videoRendition = latestSegment?.video?.reduce((prev, current) => {
|
|
67
|
+
const prevPixels = prev.width * prev.height;
|
|
68
|
+
const currentPixels = current.width * current.height;
|
|
69
|
+
return currentPixels > prevPixels ? current : prev;
|
|
70
|
+
}, latestSegment?.video?.[0]);
|
|
71
|
+
const highestLength = videoRendition
|
|
72
|
+
? videoRendition.height < videoRendition.width
|
|
73
|
+
? videoRendition.height
|
|
74
|
+
: videoRendition?.width
|
|
75
|
+
: 0;
|
|
76
|
+
|
|
77
|
+
// ugh i hate this
|
|
78
|
+
const frames = videoRendition?.framerate as
|
|
79
|
+
| { num: number; den: number }
|
|
80
|
+
| undefined;
|
|
81
|
+
let fps =
|
|
82
|
+
frames?.num && frames?.den
|
|
83
|
+
? Math.round((frames.num / frames.den) * 100) / 100
|
|
84
|
+
: 0;
|
|
85
|
+
|
|
86
|
+
if (!isDevModeOn && latestSegment?.video?.length) {
|
|
87
|
+
fps = Math.round(fps);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const resolutionDisplay = highestLength
|
|
91
|
+
? `(${highestLength}p${fps > 0 ? fps : ""})`
|
|
92
|
+
: "(Original Quality)";
|
|
93
|
+
|
|
61
94
|
const { profile } = useLivestreamInfo();
|
|
62
95
|
|
|
63
96
|
const avatars = useAvatars(profile?.did ? [profile?.did] : []);
|
|
@@ -215,7 +248,11 @@ export function ContextMenu({
|
|
|
215
248
|
>
|
|
216
249
|
<Text>Quality</Text>
|
|
217
250
|
<Text muted size={isMobile ? "base" : "sm"}>
|
|
218
|
-
{quality === "source"
|
|
251
|
+
{quality === "source"
|
|
252
|
+
? `Source${resolutionDisplay ? " " + resolutionDisplay + "\n" : ", "}`
|
|
253
|
+
: quality === "audio"
|
|
254
|
+
? `Audio Only\n`
|
|
255
|
+
: quality}
|
|
219
256
|
{lowLatency ? "Low Latency" : ""}
|
|
220
257
|
</Text>
|
|
221
258
|
</View>
|
|
@@ -227,11 +264,11 @@ export function ContextMenu({
|
|
|
227
264
|
onValueChange={setQuality}
|
|
228
265
|
>
|
|
229
266
|
<DropdownMenuRadioItem value="source">
|
|
230
|
-
<Text>Source
|
|
267
|
+
<Text>Source {resolutionDisplay}</Text>
|
|
231
268
|
</DropdownMenuRadioItem>
|
|
232
269
|
{qualities.map((r) => (
|
|
233
270
|
<DropdownMenuRadioItem key={r.name} value={r.name}>
|
|
234
|
-
<Text>{r.name}</Text>
|
|
271
|
+
<Text>{r.name === "audio" ? "Audio Only" : r.name}</Text>
|
|
235
272
|
</DropdownMenuRadioItem>
|
|
236
273
|
))}
|
|
237
274
|
</DropdownMenuRadioGroup>
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import * as sdpTransform from "sdp-transform";
|
|
3
|
-
import {
|
|
3
|
+
import { StreamplaceAgent } from "streamplace";
|
|
4
|
+
import {
|
|
5
|
+
PlayerStatus,
|
|
6
|
+
usePlayerStore,
|
|
7
|
+
usePossiblyUnauthedPDSAgent,
|
|
8
|
+
useStreamKey,
|
|
9
|
+
} from "../..";
|
|
4
10
|
import { RTCPeerConnection, RTCSessionDescription } from "./webrtc-primitives";
|
|
5
11
|
|
|
6
12
|
export default function useWebRTC(
|
|
7
|
-
|
|
13
|
+
streamer: string,
|
|
8
14
|
): [MediaStream | null, boolean] {
|
|
9
15
|
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
|
|
10
16
|
const [stuck, setStuck] = useState<boolean>(false);
|
|
11
17
|
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
18
|
+
let agent = usePossiblyUnauthedPDSAgent();
|
|
12
19
|
|
|
13
20
|
const lastChange = useRef<number>(0);
|
|
14
21
|
|
|
15
22
|
useEffect(() => {
|
|
23
|
+
if (!agent) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
16
26
|
const peerConnection = new RTCPeerConnection({
|
|
17
27
|
bundlePolicy: "max-bundle",
|
|
18
28
|
});
|
|
@@ -44,7 +54,12 @@ export default function useWebRTC(
|
|
|
44
54
|
}
|
|
45
55
|
});
|
|
46
56
|
peerConnection.addEventListener("negotiationneeded", () => {
|
|
47
|
-
negotiateConnectionWithClientOffer(
|
|
57
|
+
negotiateConnectionWithClientOffer(
|
|
58
|
+
peerConnection,
|
|
59
|
+
streamer,
|
|
60
|
+
undefined,
|
|
61
|
+
agent,
|
|
62
|
+
);
|
|
48
63
|
});
|
|
49
64
|
|
|
50
65
|
let lastFramesReceived = 0;
|
|
@@ -82,7 +97,7 @@ export default function useWebRTC(
|
|
|
82
97
|
clearInterval(handle);
|
|
83
98
|
peerConnection.close();
|
|
84
99
|
};
|
|
85
|
-
}, [
|
|
100
|
+
}, [streamer, agent]);
|
|
86
101
|
return [mediaStream, stuck];
|
|
87
102
|
}
|
|
88
103
|
|
|
@@ -100,8 +115,9 @@ export default function useWebRTC(
|
|
|
100
115
|
*/
|
|
101
116
|
export async function negotiateConnectionWithClientOffer(
|
|
102
117
|
peerConnection: RTCPeerConnection,
|
|
103
|
-
|
|
118
|
+
streamer: string,
|
|
104
119
|
bearerToken?: string,
|
|
120
|
+
agent?: StreamplaceAgent,
|
|
105
121
|
) {
|
|
106
122
|
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */
|
|
107
123
|
const offer = await peerConnection.createOffer({
|
|
@@ -134,23 +150,79 @@ export async function negotiateConnectionWithClientOffer(
|
|
|
134
150
|
* This specifies how the client should communicate,
|
|
135
151
|
* and what kind of media client and server have negotiated to exchange.
|
|
136
152
|
*/
|
|
137
|
-
let response = await postSDPOffer(
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
let response = await postSDPOffer(streamer, ofr.sdp, bearerToken, agent);
|
|
154
|
+
let text = new TextDecoder().decode(response.data);
|
|
155
|
+
if (response.success) {
|
|
140
156
|
if ((peerConnection.connectionState as string) === "closed") {
|
|
141
157
|
return;
|
|
142
158
|
}
|
|
143
159
|
await peerConnection.setRemoteDescription(
|
|
144
|
-
new RTCSessionDescription({ type: "answer", sdp:
|
|
160
|
+
new RTCSessionDescription({ type: "answer", sdp: text }),
|
|
145
161
|
);
|
|
146
|
-
return
|
|
147
|
-
} else
|
|
148
|
-
console.
|
|
149
|
-
|
|
162
|
+
return "https://stream.place/example";
|
|
163
|
+
} else {
|
|
164
|
+
console.error(text);
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error(`posting sdp offer failed: ${e}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Limit reconnection attempts to at-most once every 5 seconds */
|
|
171
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function negotiateIngestConnectionWithClientOffer(
|
|
176
|
+
peerConnection: RTCPeerConnection,
|
|
177
|
+
endpoint: string,
|
|
178
|
+
bearerToken: string,
|
|
179
|
+
) {
|
|
180
|
+
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */
|
|
181
|
+
const offer = await peerConnection.createOffer({
|
|
182
|
+
offerToReceiveAudio: true,
|
|
183
|
+
offerToReceiveVideo: true,
|
|
184
|
+
});
|
|
185
|
+
if (!offer.sdp) {
|
|
186
|
+
throw Error("no SDP in offer");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const newSDP = forceStereoAudio(offer.sdp);
|
|
190
|
+
|
|
191
|
+
offer.sdp = newSDP;
|
|
192
|
+
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */
|
|
193
|
+
await peerConnection.setLocalDescription(offer);
|
|
194
|
+
|
|
195
|
+
/** Wait for ICE gathering to complete */
|
|
196
|
+
let ofr = await waitToCompleteICEGathering(peerConnection);
|
|
197
|
+
if (!ofr) {
|
|
198
|
+
throw Error("failed to gather ICE candidates for offer");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* As long as the connection is open, attempt to...
|
|
203
|
+
*/
|
|
204
|
+
while (peerConnection.connectionState !== "closed") {
|
|
205
|
+
try {
|
|
206
|
+
/**
|
|
207
|
+
* This response contains the server's SDP offer.
|
|
208
|
+
* This specifies how the client should communicate,
|
|
209
|
+
* and what kind of media client and server have negotiated to exchange.
|
|
210
|
+
*/
|
|
211
|
+
let response = await postSDPIngestOffer(endpoint, ofr.sdp, bearerToken);
|
|
212
|
+
|
|
213
|
+
if (response.status === 201) {
|
|
214
|
+
if ((peerConnection.connectionState as string) === "closed") {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await peerConnection.setRemoteDescription(
|
|
218
|
+
new RTCSessionDescription({
|
|
219
|
+
type: "answer",
|
|
220
|
+
sdp: await response.text(),
|
|
221
|
+
}),
|
|
150
222
|
);
|
|
223
|
+
return "https://stream.place/example";
|
|
151
224
|
} else {
|
|
152
|
-
|
|
153
|
-
console.error(errorMessage);
|
|
225
|
+
console.error(await response.text());
|
|
154
226
|
}
|
|
155
227
|
} catch (e) {
|
|
156
228
|
console.error(`posting sdp offer failed: ${e}`);
|
|
@@ -162,9 +234,35 @@ export async function negotiateConnectionWithClientOffer(
|
|
|
162
234
|
}
|
|
163
235
|
|
|
164
236
|
async function postSDPOffer(
|
|
165
|
-
|
|
237
|
+
streamer: string,
|
|
166
238
|
data: string,
|
|
167
239
|
bearerToken?: string,
|
|
240
|
+
agent?: StreamplaceAgent,
|
|
241
|
+
) {
|
|
242
|
+
if (!agent) {
|
|
243
|
+
throw new Error("No agent found");
|
|
244
|
+
}
|
|
245
|
+
return await agent.place.stream.playback.whep(data, {
|
|
246
|
+
qp: {
|
|
247
|
+
rendition: "source",
|
|
248
|
+
streamer: streamer,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
// return await fetch(endpoint, {
|
|
252
|
+
// method: "POST",
|
|
253
|
+
// mode: "cors",
|
|
254
|
+
// headers: {
|
|
255
|
+
// "content-type": "application/sdp",
|
|
256
|
+
// ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
257
|
+
// },
|
|
258
|
+
// body: data,
|
|
259
|
+
// });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function postSDPIngestOffer(
|
|
263
|
+
endpoint: string,
|
|
264
|
+
data: string,
|
|
265
|
+
bearerToken: string,
|
|
168
266
|
) {
|
|
169
267
|
return await fetch(endpoint, {
|
|
170
268
|
method: "POST",
|
|
@@ -254,7 +352,10 @@ export function useWebRTCIngest({
|
|
|
254
352
|
}
|
|
255
353
|
});
|
|
256
354
|
peerConnection.addEventListener("negotiationneeded", (ev) => {
|
|
257
|
-
|
|
355
|
+
if (!storedKey?.streamKey?.privateKey) {
|
|
356
|
+
throw new Error("no private key found");
|
|
357
|
+
}
|
|
358
|
+
negotiateIngestConnectionWithClientOffer(
|
|
258
359
|
peerConnection,
|
|
259
360
|
endpoint,
|
|
260
361
|
storedKey.streamKey?.privateKey,
|
|
@@ -285,9 +285,9 @@ export function NativeWHEP(props?: {
|
|
|
285
285
|
export function NativeIngestPlayer(props?: {
|
|
286
286
|
objectFit?: "contain" | "cover";
|
|
287
287
|
}) {
|
|
288
|
-
const ingestStarting = useIngestPlayerStore((x) => x.ingestStarting);
|
|
289
288
|
const ingestMediaSource = useIngestPlayerStore((x) => x.ingestMediaSource);
|
|
290
289
|
const ingestAutoStart = useIngestPlayerStore((x) => x.ingestAutoStart);
|
|
290
|
+
const setIngestLive = useIngestPlayerStore((x) => x.setIngestLive);
|
|
291
291
|
const setStatus = useIngestPlayerStore((x) => x.setStatus);
|
|
292
292
|
const setVideoRef = usePlayerStore((x) => x.setVideoRef);
|
|
293
293
|
|
|
@@ -315,10 +315,13 @@ export function NativeIngestPlayer(props?: {
|
|
|
315
315
|
const localMediaStream = lms;
|
|
316
316
|
|
|
317
317
|
useEffect(() => {
|
|
318
|
+
let acquiredStream: WebRTCMediaStream | null = null;
|
|
319
|
+
|
|
318
320
|
if (ingestMediaSource === IngestMediaSource.DISPLAY) {
|
|
319
321
|
mediaDevices
|
|
320
322
|
.getDisplayMedia()
|
|
321
323
|
.then((stream: WebRTCMediaStream) => {
|
|
324
|
+
acquiredStream = stream;
|
|
322
325
|
console.log("display media", stream);
|
|
323
326
|
setLocalMediaStream(stream);
|
|
324
327
|
})
|
|
@@ -344,6 +347,7 @@ export function NativeIngestPlayer(props?: {
|
|
|
344
347
|
},
|
|
345
348
|
})
|
|
346
349
|
.then((stream: WebRTCMediaStream) => {
|
|
350
|
+
acquiredStream = stream;
|
|
347
351
|
setLocalMediaStream(stream);
|
|
348
352
|
|
|
349
353
|
let errs: string[] = [];
|
|
@@ -374,20 +378,29 @@ export function NativeIngestPlayer(props?: {
|
|
|
374
378
|
);
|
|
375
379
|
});
|
|
376
380
|
}
|
|
381
|
+
|
|
382
|
+
return () => {
|
|
383
|
+
if (acquiredStream) {
|
|
384
|
+
acquiredStream.getTracks().forEach((track) => track.stop());
|
|
385
|
+
}
|
|
386
|
+
setLocalMediaStream(null);
|
|
387
|
+
};
|
|
377
388
|
}, [ingestMediaSource, ingestCamera]);
|
|
378
389
|
|
|
379
390
|
useEffect(() => {
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
return;
|
|
391
|
+
if (localMediaStream) {
|
|
392
|
+
setIngestLive(true);
|
|
383
393
|
}
|
|
394
|
+
}, [localMediaStream]);
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
384
397
|
if (!localMediaStream) {
|
|
385
398
|
return;
|
|
386
399
|
}
|
|
387
400
|
console.log("setting remote media stream", localMediaStream);
|
|
388
401
|
// @ts-expect-error: WebRTCMediaStream may not have all MediaStream properties, but is compatible for our use
|
|
389
402
|
setRemoteMediaStream(localMediaStream);
|
|
390
|
-
}, [localMediaStream,
|
|
403
|
+
}, [localMediaStream, ingestAutoStart, setRemoteMediaStream]);
|
|
391
404
|
|
|
392
405
|
if (!localMediaStream) {
|
|
393
406
|
return null;
|
|
@@ -442,11 +442,12 @@ export function WebRTCPlayerInner({
|
|
|
442
442
|
|
|
443
443
|
const status = usePlayerStore((x) => x.status);
|
|
444
444
|
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
445
|
+
const src = usePlayerStore((x) => x.src);
|
|
445
446
|
|
|
446
447
|
const playerEvent = usePlayerStore((x) => x.playerEvent);
|
|
447
448
|
const spurl = useStreamplaceStore((x) => x.url);
|
|
448
449
|
|
|
449
|
-
const [mediaStream, stuck] = useWebRTC(
|
|
450
|
+
const [mediaStream, stuck] = useWebRTC(src);
|
|
450
451
|
|
|
451
452
|
useEffect(() => {
|
|
452
453
|
if (stuck) {
|
|
@@ -541,9 +542,9 @@ export function WebRTCPlayerInner({
|
|
|
541
542
|
}
|
|
542
543
|
|
|
543
544
|
export function WebcamIngestPlayer(props: VideoProps) {
|
|
544
|
-
const ingestStarting = usePlayerStore((x) => x.ingestStarting);
|
|
545
545
|
const ingestMediaSource = usePlayerStore((x) => x.ingestMediaSource);
|
|
546
546
|
const ingestAutoStart = usePlayerStore((x) => x.ingestAutoStart);
|
|
547
|
+
const setIngestLive = usePlayerStore((x) => x.setIngestLive);
|
|
547
548
|
|
|
548
549
|
const [error, setError] = useState<Error | null>(null);
|
|
549
550
|
|
|
@@ -606,15 +607,17 @@ export function WebcamIngestPlayer(props: VideoProps) {
|
|
|
606
607
|
}, [ingestMediaSource]);
|
|
607
608
|
|
|
608
609
|
useEffect(() => {
|
|
609
|
-
if (!
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
610
|
+
// if (!ingestAutoStart) {
|
|
611
|
+
// setRemoteMediaStream(null);
|
|
612
|
+
// return;
|
|
613
|
+
// }
|
|
613
614
|
if (!localMediaStream) {
|
|
614
615
|
return;
|
|
615
616
|
}
|
|
617
|
+
console.log("setting remote media stream", localMediaStream);
|
|
618
|
+
setIngestLive(true);
|
|
616
619
|
setRemoteMediaStream(localMediaStream);
|
|
617
|
-
}, [localMediaStream,
|
|
620
|
+
}, [localMediaStream, setIngestLive, setRemoteMediaStream]);
|
|
618
621
|
|
|
619
622
|
useEffect(() => {
|
|
620
623
|
if (!videoElement) {
|
|
@@ -477,22 +477,30 @@ function createStyles(theme: any) {
|
|
|
477
477
|
|
|
478
478
|
// Size styles
|
|
479
479
|
smContent: {
|
|
480
|
+
width: 400,
|
|
480
481
|
minWidth: 300,
|
|
482
|
+
maxWidth: 500,
|
|
481
483
|
minHeight: 200,
|
|
482
484
|
},
|
|
483
485
|
|
|
484
486
|
mdContent: {
|
|
487
|
+
width: 500,
|
|
485
488
|
minWidth: 400,
|
|
489
|
+
maxWidth: 600,
|
|
486
490
|
minHeight: 300,
|
|
487
491
|
},
|
|
488
492
|
|
|
489
493
|
lgContent: {
|
|
494
|
+
width: 600,
|
|
490
495
|
minWidth: 500,
|
|
496
|
+
maxWidth: 800,
|
|
491
497
|
minHeight: 400,
|
|
492
498
|
},
|
|
493
499
|
|
|
494
500
|
xlContent: {
|
|
501
|
+
width: 800,
|
|
495
502
|
minWidth: 600,
|
|
503
|
+
maxWidth: 1000,
|
|
496
504
|
minHeight: 500,
|
|
497
505
|
},
|
|
498
506
|
|
|
@@ -40,6 +40,8 @@ const Textarea = React.forwardRef<TextInput, TextInputProps>(
|
|
|
40
40
|
{ borderRadius: 10 },
|
|
41
41
|
style,
|
|
42
42
|
]}
|
|
43
|
+
autoComplete={props.autoComplete || "off"}
|
|
44
|
+
textContentType={props.textContentType || "none"}
|
|
43
45
|
multiline={multiline}
|
|
44
46
|
numberOfLines={numberOfLines}
|
|
45
47
|
textAlignVertical="top"
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import storage from "../storage";
|
|
3
|
+
|
|
4
|
+
export function useAQState<T>(
|
|
5
|
+
key: string,
|
|
6
|
+
defaultValue: T,
|
|
7
|
+
): [T, (value: T) => void] {
|
|
8
|
+
const [state, setState] = useState<T>(defaultValue);
|
|
9
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const loadFromStorage = async () => {
|
|
13
|
+
try {
|
|
14
|
+
const stored = await storage.getItem(key);
|
|
15
|
+
if (stored !== null) {
|
|
16
|
+
setState(JSON.parse(stored));
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error(`Failed to load ${key} from storage:`, error);
|
|
20
|
+
} finally {
|
|
21
|
+
setIsLoaded(true);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
loadFromStorage();
|
|
25
|
+
}, [key]);
|
|
26
|
+
|
|
27
|
+
const setStoredState = (value: T) => {
|
|
28
|
+
setState(value);
|
|
29
|
+
if (isLoaded) {
|
|
30
|
+
storage.setItem(key, JSON.stringify(value)).catch((error) => {
|
|
31
|
+
console.error(`Failed to save ${key} to storage:`, error);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return [state, setStoredState];
|
|
37
|
+
}
|