@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.
Files changed (195) hide show
  1. package/assets/badges/live.png +0 -0
  2. package/assets/badges/live_2x.png +0 -0
  3. package/assets/badges/mod.png +0 -0
  4. package/assets/badges/mod_2x.png +0 -0
  5. package/assets/badges/vip.png +0 -0
  6. package/assets/badges/vip_2x.png +0 -0
  7. package/dist/components/chat/badge.d.ts +10 -0
  8. package/dist/components/chat/badge.d.ts.map +1 -0
  9. package/dist/components/chat/badge.js +29 -0
  10. package/dist/components/chat/badge.js.map +1 -0
  11. package/dist/components/chat/chat-box.d.ts +5 -1
  12. package/dist/components/chat/chat-box.d.ts.map +1 -1
  13. package/dist/components/chat/chat-box.js +55 -50
  14. package/dist/components/chat/chat-box.js.map +1 -1
  15. package/dist/components/chat/chat-message.d.ts.map +1 -1
  16. package/dist/components/chat/chat-message.js +9 -11
  17. package/dist/components/chat/chat-message.js.map +1 -1
  18. package/dist/components/chat/chat.d.ts.map +1 -1
  19. package/dist/components/chat/chat.js +37 -43
  20. package/dist/components/chat/chat.js.map +1 -1
  21. package/dist/components/chat/emoji-suggestions.d.ts +7 -18
  22. package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
  23. package/dist/components/chat/emoji-suggestions.js +6 -2
  24. package/dist/components/chat/emoji-suggestions.js.map +1 -1
  25. package/dist/components/chat/system-message.d.ts.map +1 -1
  26. package/dist/components/chat/system-message.js +9 -1
  27. package/dist/components/chat/system-message.js.map +1 -1
  28. package/dist/components/chat/teleport-modal.d.ts +9 -0
  29. package/dist/components/chat/teleport-modal.d.ts.map +1 -0
  30. package/dist/components/chat/teleport-modal.js +148 -0
  31. package/dist/components/chat/teleport-modal.js.map +1 -0
  32. package/dist/components/chat/user-profile-card.d.ts +12 -0
  33. package/dist/components/chat/user-profile-card.d.ts.map +1 -0
  34. package/dist/components/chat/user-profile-card.js +135 -0
  35. package/dist/components/chat/user-profile-card.js.map +1 -0
  36. package/dist/components/dashboard/chat-panel.d.ts +3 -1
  37. package/dist/components/dashboard/chat-panel.d.ts.map +1 -1
  38. package/dist/components/dashboard/chat-panel.js +2 -2
  39. package/dist/components/dashboard/chat-panel.js.map +1 -1
  40. package/dist/components/dashboard/header.d.ts +2 -3
  41. package/dist/components/dashboard/header.d.ts.map +1 -1
  42. package/dist/components/dashboard/header.js +6 -2
  43. package/dist/components/dashboard/header.js.map +1 -1
  44. package/dist/components/dashboard/information-widget.d.ts.map +1 -1
  45. package/dist/components/dashboard/information-widget.js +15 -12
  46. package/dist/components/dashboard/information-widget.js.map +1 -1
  47. package/dist/components/mobile-player/fullscreen.d.ts.map +1 -1
  48. package/dist/components/mobile-player/fullscreen.js +2 -1
  49. package/dist/components/mobile-player/fullscreen.js.map +1 -1
  50. package/dist/components/mobile-player/fullscreen.native.d.ts.map +1 -1
  51. package/dist/components/mobile-player/fullscreen.native.js +3 -2
  52. package/dist/components/mobile-player/fullscreen.native.js.map +1 -1
  53. package/dist/components/mobile-player/player.d.ts.map +1 -1
  54. package/dist/components/mobile-player/player.js +15 -0
  55. package/dist/components/mobile-player/player.js.map +1 -1
  56. package/dist/components/mobile-player/ui/audio-only-overlay.d.ts +2 -0
  57. package/dist/components/mobile-player/ui/audio-only-overlay.d.ts.map +1 -0
  58. package/dist/components/mobile-player/ui/audio-only-overlay.js +29 -0
  59. package/dist/components/mobile-player/ui/audio-only-overlay.js.map +1 -0
  60. package/dist/components/mobile-player/ui/index.d.ts +1 -0
  61. package/dist/components/mobile-player/ui/index.d.ts.map +1 -1
  62. package/dist/components/mobile-player/ui/index.js +1 -0
  63. package/dist/components/mobile-player/ui/index.js.map +1 -1
  64. package/dist/components/mobile-player/ui/input.d.ts +3 -2
  65. package/dist/components/mobile-player/ui/input.d.ts.map +1 -1
  66. package/dist/components/mobile-player/ui/input.js +18 -2
  67. package/dist/components/mobile-player/ui/input.js.map +1 -1
  68. package/dist/components/mobile-player/ui/metrics.d.ts.map +1 -1
  69. package/dist/components/mobile-player/ui/metrics.js +20 -2
  70. package/dist/components/mobile-player/ui/metrics.js.map +1 -1
  71. package/dist/components/mobile-player/ui/streamer-context-menu.d.ts +3 -1
  72. package/dist/components/mobile-player/ui/streamer-context-menu.d.ts.map +1 -1
  73. package/dist/components/mobile-player/ui/streamer-context-menu.js +64 -2
  74. package/dist/components/mobile-player/ui/streamer-context-menu.js.map +1 -1
  75. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
  76. package/dist/components/mobile-player/ui/viewer-context-menu.js +29 -1
  77. package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
  78. package/dist/components/mobile-player/use-webrtc.d.ts +4 -2
  79. package/dist/components/mobile-player/use-webrtc.d.ts.map +1 -1
  80. package/dist/components/mobile-player/use-webrtc.js +89 -15
  81. package/dist/components/mobile-player/use-webrtc.js.map +1 -1
  82. package/dist/components/mobile-player/video-async.native.d.ts.map +1 -1
  83. package/dist/components/mobile-player/video-async.native.js +15 -5
  84. package/dist/components/mobile-player/video-async.native.js.map +1 -1
  85. package/dist/components/mobile-player/video.d.ts.map +1 -1
  86. package/dist/components/mobile-player/video.js +10 -7
  87. package/dist/components/mobile-player/video.js.map +1 -1
  88. package/dist/components/ui/dialog.d.ts.map +1 -1
  89. package/dist/components/ui/dialog.js +8 -0
  90. package/dist/components/ui/dialog.js.map +1 -1
  91. package/dist/components/ui/textarea.d.ts.map +1 -1
  92. package/dist/components/ui/textarea.js +1 -1
  93. package/dist/components/ui/textarea.js.map +1 -1
  94. package/dist/hooks/index.d.ts +1 -0
  95. package/dist/hooks/index.d.ts.map +1 -1
  96. package/dist/hooks/index.js +1 -0
  97. package/dist/hooks/index.js.map +1 -1
  98. package/dist/hooks/useAQState.d.ts +2 -0
  99. package/dist/hooks/useAQState.d.ts.map +1 -0
  100. package/dist/hooks/useAQState.js +37 -0
  101. package/dist/hooks/useAQState.js.map +1 -0
  102. package/dist/hooks/useLivestreamInfo.d.ts +1 -2
  103. package/dist/hooks/useLivestreamInfo.d.ts.map +1 -1
  104. package/dist/hooks/useLivestreamInfo.js +18 -22
  105. package/dist/hooks/useLivestreamInfo.js.map +1 -1
  106. package/dist/hooks/useSegmentTiming.d.ts +1 -1
  107. package/dist/hooks/useSegmentTiming.d.ts.map +1 -1
  108. package/dist/hooks/useSegmentTiming.js +4 -0
  109. package/dist/hooks/useSegmentTiming.js.map +1 -1
  110. package/dist/i18n/i18n-loader.native.d.ts.map +1 -1
  111. package/dist/i18n/i18n-loader.native.js +13 -4
  112. package/dist/i18n/i18n-loader.native.js.map +1 -1
  113. package/dist/lib/slash-commands/teleport.d.ts +5 -1
  114. package/dist/lib/slash-commands/teleport.d.ts.map +1 -1
  115. package/dist/lib/slash-commands/teleport.js +57 -1
  116. package/dist/lib/slash-commands/teleport.js.map +1 -1
  117. package/dist/lib/theme/atoms.d.ts +125 -125
  118. package/dist/livestream-store/chat.d.ts +1 -0
  119. package/dist/livestream-store/chat.d.ts.map +1 -1
  120. package/dist/livestream-store/chat.js +10 -1
  121. package/dist/livestream-store/chat.js.map +1 -1
  122. package/dist/livestream-store/livestream-state.d.ts +2 -0
  123. package/dist/livestream-store/livestream-state.d.ts.map +1 -1
  124. package/dist/livestream-store/livestream-store.d.ts +1 -1
  125. package/dist/livestream-store/livestream-store.d.ts.map +1 -1
  126. package/dist/livestream-store/livestream-store.js +10 -1
  127. package/dist/livestream-store/livestream-store.js.map +1 -1
  128. package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
  129. package/dist/livestream-store/websocket-consumer.js +1 -0
  130. package/dist/livestream-store/websocket-consumer.js.map +1 -1
  131. package/dist/player-store/player-state.d.ts +3 -5
  132. package/dist/player-store/player-state.d.ts.map +1 -1
  133. package/dist/player-store/player-store.d.ts.map +1 -1
  134. package/dist/player-store/player-store.js +28 -5
  135. package/dist/player-store/player-store.js.map +1 -1
  136. package/dist/player-store/single-player-provider.d.ts +0 -2
  137. package/dist/player-store/single-player-provider.d.ts.map +1 -1
  138. package/dist/player-store/single-player-provider.js +0 -2
  139. package/dist/player-store/single-player-provider.js.map +1 -1
  140. package/dist/streamplace-store/branding.d.ts.map +1 -1
  141. package/dist/streamplace-store/branding.js +52 -1
  142. package/dist/streamplace-store/branding.js.map +1 -1
  143. package/dist/streamplace-store/stream.d.ts +4 -2
  144. package/dist/streamplace-store/stream.d.ts.map +1 -1
  145. package/dist/streamplace-store/stream.js +36 -74
  146. package/dist/streamplace-store/stream.js.map +1 -1
  147. package/locales/en-US/common.ftl +13 -1
  148. package/locales/manifest.json +21 -1
  149. package/locales/ro-RO/common.ftl +74 -0
  150. package/locales/ro-RO/settings.ftl +233 -0
  151. package/locales/zh-Hans/common.ftl +57 -0
  152. package/locales/zh-Hans/settings.ftl +222 -0
  153. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  154. package/package.json +2 -2
  155. package/src/components/chat/badge.tsx +45 -0
  156. package/src/components/chat/chat-box.tsx +84 -54
  157. package/src/components/chat/chat-message.tsx +25 -21
  158. package/src/components/chat/chat.tsx +107 -90
  159. package/src/components/chat/emoji-suggestions.tsx +12 -21
  160. package/src/components/chat/system-message.tsx +12 -2
  161. package/src/components/chat/teleport-modal.tsx +310 -0
  162. package/src/components/chat/user-profile-card.tsx +275 -0
  163. package/src/components/dashboard/chat-panel.tsx +8 -0
  164. package/src/components/dashboard/header.tsx +8 -17
  165. package/src/components/dashboard/information-widget.tsx +17 -10
  166. package/src/components/mobile-player/fullscreen.native.tsx +3 -0
  167. package/src/components/mobile-player/fullscreen.tsx +2 -0
  168. package/src/components/mobile-player/player.tsx +22 -1
  169. package/src/components/mobile-player/ui/audio-only-overlay.tsx +48 -0
  170. package/src/components/mobile-player/ui/index.ts +1 -0
  171. package/src/components/mobile-player/ui/input.tsx +42 -12
  172. package/src/components/mobile-player/ui/metrics.tsx +17 -2
  173. package/src/components/mobile-player/ui/streamer-context-menu.tsx +138 -2
  174. package/src/components/mobile-player/ui/viewer-context-menu.tsx +40 -3
  175. package/src/components/mobile-player/use-webrtc.tsx +118 -17
  176. package/src/components/mobile-player/video-async.native.tsx +18 -5
  177. package/src/components/mobile-player/video.tsx +10 -7
  178. package/src/components/ui/dialog.tsx +8 -0
  179. package/src/components/ui/textarea.tsx +2 -0
  180. package/src/hooks/index.ts +1 -0
  181. package/src/hooks/useAQState.ts +37 -0
  182. package/src/hooks/useLivestreamInfo.ts +21 -22
  183. package/src/hooks/useSegmentTiming.tsx +7 -2
  184. package/src/i18n/i18n-loader.native.ts +9 -0
  185. package/src/lib/slash-commands/teleport.ts +68 -0
  186. package/src/livestream-store/chat.tsx +12 -0
  187. package/src/livestream-store/livestream-state.tsx +2 -0
  188. package/src/livestream-store/livestream-store.tsx +9 -1
  189. package/src/livestream-store/websocket-consumer.tsx +1 -0
  190. package/src/player-store/player-state.tsx +4 -7
  191. package/src/player-store/player-store.tsx +33 -7
  192. package/src/player-store/single-player-provider.tsx +0 -4
  193. package/src/streamplace-store/branding.tsx +60 -1
  194. package/src/streamplace-store/stream.tsx +42 -99
  195. 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 === "good") {
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
- {connectionQuality.toUpperCase()}
67
+ {connectionText()}
53
68
  </Text>
54
69
  </View>
55
70
  {showMetrics && (
@@ -1,3 +1,139 @@
1
- export function StreamContextMenu() {
2
- return <></>;
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" ? "Source" : quality},{" "}
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 (Original Quality)</Text>
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 { PlayerStatus, usePlayerStore, useStreamKey } from "../..";
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
- endpoint: string,
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(peerConnection, endpoint);
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
- }, [endpoint]);
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
- endpoint: string,
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(`${endpoint}`, ofr.sdp, bearerToken);
138
- if (response.status === 201) {
139
- let answerSDP = await response.text();
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: answerSDP }),
160
+ new RTCSessionDescription({ type: "answer", sdp: text }),
145
161
  );
146
- return response.headers.get("Location");
147
- } else if (response.status === 405) {
148
- console.log(
149
- "Remember to update the URL passed into the WHIP or WHEP client",
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
- const errorMessage = await response.text();
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
- endpoint: string,
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
- negotiateConnectionWithClientOffer(
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 (!ingestStarting && !ingestAutoStart) {
381
- setRemoteMediaStream(null);
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, ingestStarting, ingestAutoStart, setRemoteMediaStream]);
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(url);
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 (!ingestStarting && !ingestAutoStart) {
610
- setRemoteMediaStream(null);
611
- return;
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, ingestStarting, ingestAutoStart]);
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"
@@ -1,4 +1,5 @@
1
1
  // barrel file :)
2
+ export * from "./useAQState";
2
3
  export * from "./useAvatars";
3
4
  export * from "./useCameraToggle";
4
5
  export * from "./useDocumentTitle";
@@ -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
+ }