@streamplace/components 0.9.9 → 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 (166) 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 +35 -41
  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 +1 -1
  41. package/dist/components/dashboard/header.d.ts.map +1 -1
  42. package/dist/components/dashboard/header.js +4 -0
  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 +13 -11
  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/ui/audio-only-overlay.d.ts +2 -0
  54. package/dist/components/mobile-player/ui/audio-only-overlay.d.ts.map +1 -0
  55. package/dist/components/mobile-player/ui/audio-only-overlay.js +29 -0
  56. package/dist/components/mobile-player/ui/audio-only-overlay.js.map +1 -0
  57. package/dist/components/mobile-player/ui/index.d.ts +1 -0
  58. package/dist/components/mobile-player/ui/index.d.ts.map +1 -1
  59. package/dist/components/mobile-player/ui/index.js +1 -0
  60. package/dist/components/mobile-player/ui/index.js.map +1 -1
  61. package/dist/components/mobile-player/ui/input.d.ts +1 -2
  62. package/dist/components/mobile-player/ui/input.d.ts.map +1 -1
  63. package/dist/components/mobile-player/ui/input.js +2 -2
  64. package/dist/components/mobile-player/ui/input.js.map +1 -1
  65. package/dist/components/mobile-player/ui/metrics.d.ts.map +1 -1
  66. package/dist/components/mobile-player/ui/metrics.js +20 -2
  67. package/dist/components/mobile-player/ui/metrics.js.map +1 -1
  68. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
  69. package/dist/components/mobile-player/ui/viewer-context-menu.js +29 -1
  70. package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
  71. package/dist/components/mobile-player/use-webrtc.d.ts +4 -2
  72. package/dist/components/mobile-player/use-webrtc.d.ts.map +1 -1
  73. package/dist/components/mobile-player/use-webrtc.js +89 -15
  74. package/dist/components/mobile-player/use-webrtc.js.map +1 -1
  75. package/dist/components/mobile-player/video-async.native.d.ts.map +1 -1
  76. package/dist/components/mobile-player/video-async.native.js +15 -5
  77. package/dist/components/mobile-player/video-async.native.js.map +1 -1
  78. package/dist/components/mobile-player/video.d.ts.map +1 -1
  79. package/dist/components/mobile-player/video.js +10 -7
  80. package/dist/components/mobile-player/video.js.map +1 -1
  81. package/dist/components/ui/dialog.d.ts.map +1 -1
  82. package/dist/components/ui/dialog.js +8 -0
  83. package/dist/components/ui/dialog.js.map +1 -1
  84. package/dist/hooks/useLivestreamInfo.d.ts +0 -2
  85. package/dist/hooks/useLivestreamInfo.d.ts.map +1 -1
  86. package/dist/hooks/useLivestreamInfo.js +13 -24
  87. package/dist/hooks/useLivestreamInfo.js.map +1 -1
  88. package/dist/hooks/useSegmentTiming.d.ts +1 -1
  89. package/dist/hooks/useSegmentTiming.d.ts.map +1 -1
  90. package/dist/hooks/useSegmentTiming.js +4 -0
  91. package/dist/hooks/useSegmentTiming.js.map +1 -1
  92. package/dist/i18n/i18n-loader.native.d.ts.map +1 -1
  93. package/dist/i18n/i18n-loader.native.js +13 -4
  94. package/dist/i18n/i18n-loader.native.js.map +1 -1
  95. package/dist/lib/slash-commands/teleport.d.ts +5 -1
  96. package/dist/lib/slash-commands/teleport.d.ts.map +1 -1
  97. package/dist/lib/slash-commands/teleport.js +57 -1
  98. package/dist/lib/slash-commands/teleport.js.map +1 -1
  99. package/dist/livestream-store/chat.d.ts +1 -0
  100. package/dist/livestream-store/chat.d.ts.map +1 -1
  101. package/dist/livestream-store/chat.js +10 -1
  102. package/dist/livestream-store/chat.js.map +1 -1
  103. package/dist/livestream-store/livestream-state.d.ts +2 -0
  104. package/dist/livestream-store/livestream-state.d.ts.map +1 -1
  105. package/dist/livestream-store/livestream-store.d.ts +1 -1
  106. package/dist/livestream-store/livestream-store.d.ts.map +1 -1
  107. package/dist/livestream-store/livestream-store.js +10 -1
  108. package/dist/livestream-store/livestream-store.js.map +1 -1
  109. package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
  110. package/dist/livestream-store/websocket-consumer.js +1 -0
  111. package/dist/livestream-store/websocket-consumer.js.map +1 -1
  112. package/dist/player-store/player-state.d.ts +1 -5
  113. package/dist/player-store/player-state.d.ts.map +1 -1
  114. package/dist/player-store/player-store.d.ts.map +1 -1
  115. package/dist/player-store/player-store.js +16 -5
  116. package/dist/player-store/player-store.js.map +1 -1
  117. package/dist/player-store/single-player-provider.d.ts +0 -2
  118. package/dist/player-store/single-player-provider.d.ts.map +1 -1
  119. package/dist/player-store/single-player-provider.js +0 -2
  120. package/dist/player-store/single-player-provider.js.map +1 -1
  121. package/dist/streamplace-store/stream.d.ts +4 -2
  122. package/dist/streamplace-store/stream.d.ts.map +1 -1
  123. package/dist/streamplace-store/stream.js +36 -74
  124. package/dist/streamplace-store/stream.js.map +1 -1
  125. package/locales/manifest.json +21 -1
  126. package/locales/ro-RO/common.ftl +74 -0
  127. package/locales/ro-RO/settings.ftl +233 -0
  128. package/locales/zh-Hans/common.ftl +57 -0
  129. package/locales/zh-Hans/settings.ftl +222 -0
  130. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  131. package/package.json +2 -2
  132. package/src/components/chat/badge.tsx +45 -0
  133. package/src/components/chat/chat-box.tsx +84 -54
  134. package/src/components/chat/chat-message.tsx +25 -21
  135. package/src/components/chat/chat.tsx +105 -88
  136. package/src/components/chat/emoji-suggestions.tsx +12 -21
  137. package/src/components/chat/system-message.tsx +12 -2
  138. package/src/components/chat/teleport-modal.tsx +310 -0
  139. package/src/components/chat/user-profile-card.tsx +275 -0
  140. package/src/components/dashboard/chat-panel.tsx +8 -0
  141. package/src/components/dashboard/header.tsx +7 -3
  142. package/src/components/dashboard/information-widget.tsx +15 -9
  143. package/src/components/mobile-player/fullscreen.native.tsx +3 -0
  144. package/src/components/mobile-player/fullscreen.tsx +2 -0
  145. package/src/components/mobile-player/ui/audio-only-overlay.tsx +48 -0
  146. package/src/components/mobile-player/ui/index.ts +1 -0
  147. package/src/components/mobile-player/ui/input.tsx +1 -5
  148. package/src/components/mobile-player/ui/metrics.tsx +17 -2
  149. package/src/components/mobile-player/ui/viewer-context-menu.tsx +40 -3
  150. package/src/components/mobile-player/use-webrtc.tsx +118 -17
  151. package/src/components/mobile-player/video-async.native.tsx +18 -5
  152. package/src/components/mobile-player/video.tsx +10 -7
  153. package/src/components/ui/dialog.tsx +8 -0
  154. package/src/hooks/useLivestreamInfo.ts +15 -24
  155. package/src/hooks/useSegmentTiming.tsx +7 -2
  156. package/src/i18n/i18n-loader.native.ts +9 -0
  157. package/src/lib/slash-commands/teleport.ts +68 -0
  158. package/src/livestream-store/chat.tsx +12 -0
  159. package/src/livestream-store/livestream-state.tsx +2 -0
  160. package/src/livestream-store/livestream-store.tsx +9 -1
  161. package/src/livestream-store/websocket-consumer.tsx +1 -0
  162. package/src/player-store/player-state.tsx +1 -7
  163. package/src/player-store/player-store.tsx +16 -7
  164. package/src/player-store/single-player-provider.tsx +0 -4
  165. package/src/streamplace-store/stream.tsx +42 -99
  166. package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
@@ -13,11 +13,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
13
13
  import { LayoutChangeEvent, Text, TouchableOpacity, View } from "react-native";
14
14
  import Svg, { Path, Line as SvgLine, Text as SvgText } from "react-native-svg";
15
15
  import { useAQState } from "../../hooks";
16
- import {
17
- useLivestreamStore,
18
- useSegment,
19
- useViewers,
20
- } from "../../livestream-store";
16
+ import { useLivestream, useSegment, useViewers } from "../../livestream-store";
21
17
  import * as zero from "../../ui";
22
18
  import { InfoBox, InfoRow } from "../ui";
23
19
 
@@ -50,7 +46,7 @@ export default function InformationWidget({
50
46
  const isCompactHeight = layoutMeasured && componentHeight < 350;
51
47
 
52
48
  const seg = useSegment();
53
- const livestream = useLivestreamStore((x) => x.livestream);
49
+ const livestream = useLivestream();
54
50
  const viewers = useViewers();
55
51
 
56
52
  const getBitrate = useCallback((): number => {
@@ -173,8 +169,9 @@ export default function InformationWidget({
173
169
  width: 8,
174
170
  height: 8,
175
171
  borderRadius: 4,
176
- backgroundColor:
177
- getConnectionStatus() === "good"
172
+ backgroundColor: !livestream
173
+ ? "#3b82f6"
174
+ : getConnectionStatus() === "good"
178
175
  ? "#22c55e"
179
176
  : getConnectionStatus() === "warning"
180
177
  ? "#f59e0b"
@@ -182,6 +179,11 @@ export default function InformationWidget({
182
179
  },
183
180
  ]}
184
181
  />
182
+ {!livestream && (
183
+ <Text style={[text.blue[400], { fontSize: 13, fontWeight: "600" }]}>
184
+ (not live)
185
+ </Text>
186
+ )}
185
187
  </View>
186
188
  <TouchableOpacity
187
189
  onPress={() => setShowViewers(!showViewers)}
@@ -315,6 +317,7 @@ export default function InformationWidget({
315
317
  data={bitrateHistory}
316
318
  width={componentWidth - 40}
317
319
  height={120}
320
+ color={livestream ? "#22c55e" : "#3b82f6"}
318
321
  />
319
322
  </View>
320
323
  )}
@@ -395,6 +398,7 @@ export default function InformationWidget({
395
398
  data={bitrateHistory}
396
399
  width={componentWidth - 40}
397
400
  height={isCompactHeight ? 80 : 120}
401
+ color={livestream ? "#22c55e" : "#3b82f6"}
398
402
  />
399
403
  </View>
400
404
  )}
@@ -432,10 +436,12 @@ function BitrateChart({
432
436
  data,
433
437
  width,
434
438
  height,
439
+ color = "#22c55e",
435
440
  }: {
436
441
  data: number[];
437
442
  width: number;
438
443
  height: number;
444
+ color?: string;
439
445
  }) {
440
446
  const maxDataValue = Math.max(...data, 1);
441
447
  const minDataValue = Math.min(...data);
@@ -515,7 +521,7 @@ function BitrateChart({
515
521
  </SvgText>
516
522
  <Path
517
523
  d={pathData}
518
- stroke="#22c55e"
524
+ stroke={color}
519
525
  strokeWidth="2"
520
526
  fill="none"
521
527
  strokeLinecap="round"
@@ -15,6 +15,7 @@ import {
15
15
  usePlayerStore,
16
16
  VideoRetry,
17
17
  } from "../..";
18
+ import { AudioOnlyOverlay } from "./ui/audio-only-overlay";
18
19
  import Video from "./video.native";
19
20
 
20
21
  // Standard 16:9 video aspect ratio
@@ -166,6 +167,7 @@ export function Fullscreen(props: {
166
167
  objectFit={props.objectFit}
167
168
  pictureInPictureEnabled={props.pictureInPictureEnabled}
168
169
  />
170
+ <AudioOnlyOverlay />
169
171
  <DanmuOverlay
170
172
  enabled={danmuEnabled}
171
173
  opacity={danmuOpacity}
@@ -188,6 +190,7 @@ export function Fullscreen(props: {
188
190
  pictureInPictureEnabled={props.pictureInPictureEnabled}
189
191
  />
190
192
  </VideoRetry>
193
+ <AudioOnlyOverlay />
191
194
  <DanmuOverlay
192
195
  enabled={danmuEnabled}
193
196
  opacity={danmuOpacity}
@@ -11,6 +11,7 @@ import {
11
11
  usePlayerStore,
12
12
  } from "../..";
13
13
  import { View } from "../../components/ui";
14
+ import { AudioOnlyOverlay } from "./ui/audio-only-overlay";
14
15
  import Video from "./video";
15
16
  import VideoRetry from "./video-retry";
16
17
 
@@ -105,6 +106,7 @@ export function Fullscreen(props: {
105
106
  pictureInPictureEnabled={props.pictureInPictureEnabled}
106
107
  />
107
108
  </VideoRetry>
109
+ <AudioOnlyOverlay />
108
110
  <DanmuOverlay
109
111
  enabled={danmuEnabled}
110
112
  opacity={danmuOpacity}
@@ -0,0 +1,48 @@
1
+ import { Volume2 } from "lucide-react-native";
2
+ import { zero } from "../../..";
3
+ import { usePlayerStore } from "../../../player-store";
4
+ import { Text, View } from "../../ui";
5
+
6
+ export function AudioOnlyOverlay() {
7
+ const selectedRendition = usePlayerStore((x) => x.selectedRendition);
8
+ const setSelectedRendition = usePlayerStore((x) => x.setSelectedRendition);
9
+
10
+ if (selectedRendition !== "audio") {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <View
16
+ style={[
17
+ zero.layout.position.absolute,
18
+ zero.position.top[0],
19
+ zero.position.left[0],
20
+ zero.position.right[0],
21
+ zero.position.bottom[0],
22
+ zero.layout.flex.center,
23
+ ]}
24
+ >
25
+ <View
26
+ style={[
27
+ zero.layout.flex.column,
28
+ zero.layout.flex.alignCenter,
29
+ zero.gap.all[3],
30
+ zero.px[6],
31
+ ]}
32
+ >
33
+ <Volume2 color="#fff" size={48} />
34
+ <Text size="lg" weight="semibold" center>
35
+ Audio Only mode
36
+ </Text>
37
+ <Text
38
+ size="sm"
39
+ color="muted"
40
+ center
41
+ onPress={() => setSelectedRendition("source")}
42
+ >
43
+ Go to Settings &gt; Quality to switch back to video.
44
+ </Text>
45
+ </View>
46
+ </View>
47
+ );
48
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./audio-only-overlay";
1
2
  export * from "./autoplay-button";
2
3
  export * from "./countdown";
3
4
  export * from "./input";
@@ -7,7 +7,6 @@ const { gap, h, layout, mt, p, position, px, py, sizes, w } = atoms;
7
7
  type InputPanelProps = {
8
8
  title: string | undefined;
9
9
  setTitle: (title: string) => void;
10
- ingestStarting: boolean;
11
10
  toggleGoLive: () => void;
12
11
  isLive: boolean;
13
12
  toggleStopStream?: () => void;
@@ -16,7 +15,6 @@ type InputPanelProps = {
16
15
  export function InputPanel({
17
16
  title,
18
17
  setTitle,
19
- ingestStarting,
20
18
  toggleGoLive,
21
19
  isLive,
22
20
  toggleStopStream,
@@ -51,9 +49,7 @@ export function InputPanel({
51
49
  />
52
50
  </View>
53
51
  )}
54
- {ingestStarting ? (
55
- <Text>Starting your stream...</Text>
56
- ) : isLive ? (
52
+ {isLive ? (
57
53
  <View style={[layout.flex.center]}>
58
54
  <Pressable
59
55
  onPress={toggleStopStream}
@@ -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 && (
@@ -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