@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
@@ -1,15 +1,15 @@
1
1
  import { useState } from "react";
2
2
  import { useLivestreamStore } from "../livestream-store";
3
3
  import { usePlayerStore } from "../player-store";
4
- import { useCreateStreamRecord } from "../streamplace-store";
4
+ import { useCreateStreamRecord, useEndLivestream } from "../streamplace-store";
5
5
 
6
6
  export function useLivestreamInfo(url?: string) {
7
7
  const ingest = usePlayerStore((x) => x.ingestConnectionState);
8
8
  const profile = useLivestreamStore((x) => x.profile);
9
- const ingestStarting = usePlayerStore((x) => x.ingestStarting);
10
- const setIngestStarting = usePlayerStore((x) => x.setIngestStarting);
11
- const setIngestLive = usePlayerStore((x) => x.setIngestLive);
12
-
9
+ const endLivestream = useEndLivestream();
10
+ const setLocalLivestreamURI = useLivestreamStore(
11
+ (x) => x.setLocalLivestreamURI,
12
+ );
13
13
  const createStreamRecord = useCreateStreamRecord();
14
14
 
15
15
  const [title, setTitle] = useState<string>("");
@@ -21,10 +21,11 @@ export function useLivestreamInfo(url?: string) {
21
21
  if (title !== "") {
22
22
  setRecordSubmitted(true);
23
23
  // Create the livestream record with title and custom url if available
24
- await createStreamRecord({
24
+ const { uri } = await createStreamRecord({
25
25
  title,
26
26
  canonicalUrl: url || undefined,
27
27
  });
28
+ setLocalLivestreamURI(uri);
28
29
  }
29
30
  } catch (error) {
30
31
  console.error("Error creating livestream:", error);
@@ -38,20 +39,19 @@ export function useLivestreamInfo(url?: string) {
38
39
  keyboardHeight?: number,
39
40
  closeKeyboard?: () => void,
40
41
  ) => {
41
- if (!ingestStarting) {
42
- // Optionally close keyboard if provided
43
- if (closeKeyboard) closeKeyboard();
44
- setShowCountdown(true);
45
- setIngestStarting(true);
46
- setIngestLive(true);
47
- // wait ~3 seconds before announcing
48
- setTimeout(() => {
49
- handleSubmit();
50
- }, 3000);
51
- } else {
52
- setIngestStarting(false);
53
- setIngestLive(false);
54
- }
42
+ // Optionally close keyboard if provided
43
+ if (closeKeyboard) closeKeyboard();
44
+ setShowCountdown(true);
45
+ // wait ~3 seconds before announcing
46
+ setTimeout(() => {
47
+ handleSubmit();
48
+ }, 3000);
49
+ };
50
+
51
+ // Stop the current broadcast
52
+ const toggleStopStream = () => {
53
+ console.log("Stopping stream...");
54
+ endLivestream();
55
55
  };
56
56
 
57
57
  return {
@@ -63,9 +63,8 @@ export function useLivestreamInfo(url?: string) {
63
63
  setShowCountdown,
64
64
  recordSubmitted,
65
65
  setRecordSubmitted,
66
- ingestStarting,
67
- setIngestStarting,
68
66
  handleSubmit,
69
67
  toggleGoLive,
68
+ toggleStopStream,
70
69
  };
71
70
  }
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { useLivestreamStore } from "../livestream-store";
2
+ import { useLivestream, useLivestreamStore } from "../livestream-store";
3
3
 
4
- export type ConnectionQuality = "good" | "degraded" | "poor";
4
+ export type ConnectionQuality = "good" | "degraded" | "poor" | "pre-live";
5
5
 
6
6
  function getLiveConnectionQuality(
7
7
  timeBetweenSegments: number | null,
@@ -24,6 +24,7 @@ export function useSegmentTiming() {
24
24
  const [segmentDeltas, setSegmentDeltas] = useState<number[]>([]);
25
25
  const prevSegmentRef = useRef<any>();
26
26
  const prevTimestampRef = useRef<number | null>(null);
27
+ const ls = useLivestream();
27
28
 
28
29
  // Dummy state to force update every second
29
30
  const [, setNow] = useState(Date.now());
@@ -84,5 +85,9 @@ export function useSegmentTiming() {
84
85
  segmentDeltas.length,
85
86
  );
86
87
 
88
+ if (!ls) {
89
+ to_ret.connectionQuality = "pre-live";
90
+ }
91
+
87
92
  return to_ret;
88
93
  }
@@ -10,6 +10,10 @@ import frFRCommon from "../../public/locales/fr-FR/common.json";
10
10
  import frFRSettings from "../../public/locales/fr-FR/settings.json";
11
11
  import ptBRCommon from "../../public/locales/pt-BR/common.json";
12
12
  import ptBRSettings from "../../public/locales/pt-BR/settings.json";
13
+ import roROCommon from "../../public/locales/ro-RO/common.json";
14
+ import roROSettings from "../../public/locales/ro-RO/settings.json";
15
+ import zhHansCommon from "../../public/locales/zh-Hans/common.json";
16
+ import zhHansSettings from "../../public/locales/zh-Hans/settings.json";
13
17
  import zhHantCommon from "../../public/locales/zh-Hant/common.json";
14
18
  import zhHantSettings from "../../public/locales/zh-Hant/settings.json";
15
19
 
@@ -20,10 +24,14 @@ const translationMap: Record<string, any> = {
20
24
  "pt-BR/settings": ptBRSettings,
21
25
  "es-ES/common": esESCommon,
22
26
  "es-ES/settings": esESSettings,
27
+ "zh-Hans/common": zhHansCommon,
28
+ "zh-Hans/settings": zhHansSettings,
23
29
  "zh-Hant/common": zhHantCommon,
24
30
  "zh-Hant/settings": zhHantSettings,
25
31
  "fr-FR/common": frFRCommon,
26
32
  "fr-FR/settings": frFRSettings,
33
+ "ro-RO/common": roROCommon,
34
+ "ro-RO/settings": roROSettings,
27
35
  };
28
36
 
29
37
  export async function loadTranslationData(
@@ -39,6 +47,7 @@ export async function loadTranslationData(
39
47
  es: "es-ES",
40
48
  zh: "zh-Hant",
41
49
  fr: "fr-FR",
50
+ ro: "ro-RO",
42
51
  }[locale] || locale;
43
52
 
44
53
  const localeNamespaceKey = `${fullLocale}/${namespace}`;
@@ -21,16 +21,84 @@ export async function deleteTeleport(
21
21
  });
22
22
  }
23
23
 
24
+ export async function createTeleport(
25
+ pdsAgent: StreamplaceAgent,
26
+ userDID: string,
27
+ targetHandle: string,
28
+ countdownSeconds: number,
29
+ setActiveTeleportUri?: (uri: string | null) => void,
30
+ ): Promise<{ success: boolean; error?: string }> {
31
+ if (countdownSeconds < 5 || countdownSeconds > 300) {
32
+ return {
33
+ success: false,
34
+ error: "Countdown must be between 5 seconds and 5 minutes",
35
+ };
36
+ }
37
+
38
+ let targetDID: string;
39
+ try {
40
+ const resolution = await pdsAgent.resolveHandle({
41
+ handle: targetHandle,
42
+ });
43
+ targetDID = resolution.data.did;
44
+ } catch (err) {
45
+ return {
46
+ success: false,
47
+ error: `Could not resolve handle: ${targetHandle}`,
48
+ };
49
+ }
50
+
51
+ if (targetDID === userDID) {
52
+ return {
53
+ success: false,
54
+ error: "You cannot teleport to yourself",
55
+ };
56
+ }
57
+
58
+ const startsAt = new Date(Date.now() + countdownSeconds * 1000).toISOString();
59
+
60
+ const record: PlaceStreamLiveTeleport.Record = {
61
+ $type: "place.stream.live.teleport",
62
+ streamer: targetDID,
63
+ startsAt,
64
+ countdownSeconds,
65
+ };
66
+
67
+ try {
68
+ const result = await pdsAgent.com.atproto.repo.createRecord({
69
+ repo: userDID,
70
+ collection: "place.stream.live.teleport",
71
+ record,
72
+ });
73
+
74
+ if (setActiveTeleportUri) {
75
+ setActiveTeleportUri(result.data.uri);
76
+ }
77
+
78
+ return { success: true };
79
+ } catch (err) {
80
+ return {
81
+ success: false,
82
+ error: err instanceof Error ? err.message : "Failed to create teleport",
83
+ };
84
+ }
85
+ }
86
+
24
87
  export function registerTeleportCommand(
25
88
  pdsAgent: StreamplaceAgent,
26
89
  userDID: string,
27
90
  setActiveTeleportUri?: (uri: string | null) => void,
91
+ onOpenModal?: () => void,
28
92
  ) {
29
93
  const teleportHandler: SlashCommandHandler = async (
30
94
  args,
31
95
  rawInput,
32
96
  ): Promise<SlashCommandResult> => {
33
97
  if (args.length === 0) {
98
+ if (onOpenModal) {
99
+ onOpenModal();
100
+ return { handled: true };
101
+ }
34
102
  return {
35
103
  handled: true,
36
104
  error: "Usage: /teleport @handle.bsky.social [duration_seconds]",
@@ -155,6 +155,18 @@ export const useDeleteChatMessage = () => {
155
155
  };
156
156
  };
157
157
 
158
+ export const useAddSystemMessage = () => {
159
+ const store = getStoreFromContext();
160
+ return useCallback(
161
+ (message: ChatMessageViewHydrated) => {
162
+ const state = store.getState();
163
+ const newState = reduceChat(state, [message], []);
164
+ store.setState(newState);
165
+ },
166
+ [store],
167
+ );
168
+ };
169
+
158
170
  const buildSortedChatList = (
159
171
  chatIndex: { [key: string]: ChatMessageViewHydrated },
160
172
  existingChatList: ChatMessageViewHydrated[],
@@ -32,6 +32,8 @@ export interface LivestreamState {
32
32
  setModerationPermissions: (
33
33
  permissions: PlaceStreamModerationPermission.Record[],
34
34
  ) => void;
35
+ localLivestreamURI: string | null;
36
+ setLocalLivestreamURI: (uri: string | null) => void;
35
37
  }
36
38
 
37
39
  export interface LivestreamProblem {
@@ -29,6 +29,8 @@ export const makeLivestreamStore = (): StoreApi<LivestreamState> => {
29
29
  hasReceivedSegment: false,
30
30
  moderationPermissions: [],
31
31
  setModerationPermissions: (perms) => set({ moderationPermissions: perms }),
32
+ localLivestreamURI: null,
33
+ setLocalLivestreamURI: (uri) => set({ localLivestreamURI: uri }),
32
34
  }));
33
35
  };
34
36
 
@@ -62,7 +64,13 @@ export const useProfile = () => useLivestreamStore((x) => x.profile);
62
64
 
63
65
  export const useViewers = () => useLivestreamStore((x) => x.viewers);
64
66
 
65
- export const useLivestream = () => useLivestreamStore((x) => x.livestream);
67
+ export const useLivestream = (includeEnded: boolean = false) =>
68
+ useLivestreamStore((x) => {
69
+ const ls = x.livestream;
70
+ if (!ls) return null;
71
+ if (!includeEnded && ls.record.endedAt !== undefined) return null;
72
+ return ls;
73
+ });
66
74
 
67
75
  export const useSegment = () => useLivestreamStore((x) => x.segment);
68
76
 
@@ -80,6 +80,7 @@ export const handleWebSocketMessages = (
80
80
  chatProfile: (message as any).chatProfile,
81
81
  replyTo: (message as any).replyTo,
82
82
  deleted: message.deleted,
83
+ badges: message.badges,
83
84
  };
84
85
  state = reduceChat(state, [hydrated], [], []);
85
86
  } else if (PlaceStreamSegment.isRecord(message)) {
@@ -32,18 +32,12 @@ export interface PlayerState {
32
32
  protocol: PlayerProtocol;
33
33
  setProtocol: (protocol: PlayerProtocol) => void;
34
34
 
35
- /** Source */
35
+ /** Source (streamer did) */
36
36
  src: string;
37
37
 
38
38
  /** Function to set the source URL */
39
39
  setSrc: (src: string) => void;
40
40
 
41
- /** Flag indicating if ingest (stream input) is currently starting */
42
- ingestStarting: boolean;
43
-
44
- /** Function to set the ingestStarting flag */
45
- setIngestStarting: (ingestStarting: boolean) => void;
46
-
47
41
  /** Flag indicating if ingest is live */
48
42
  ingestLive: boolean;
49
43
  setIngestLive: (ingestLive: boolean) => void;
@@ -63,6 +57,9 @@ export interface PlayerState {
63
57
  ingestAutoStart?: boolean;
64
58
  setIngestAutoStart?: (autoStart: boolean) => void;
65
59
 
60
+ /** stop ingest process, again with a slight delay to allow UI to update */
61
+ stopIngest: () => void;
62
+
66
63
  /** Timestamp (number) when ingest started, or null if not started */
67
64
  ingestStarted: number | null;
68
65
 
@@ -20,7 +20,18 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
20
20
  id: id || Math.random().toString(36).slice(8),
21
21
  selectedRendition: "source",
22
22
  setSelectedRendition: (rendition: string) =>
23
- set((state) => ({ ...state, selectedRendition: rendition })),
23
+ set((state) => {
24
+ if (rendition === "audio" && state.controlsTimeout) {
25
+ clearTimeout(state.controlsTimeout);
26
+ return {
27
+ ...state,
28
+ selectedRendition: rendition,
29
+ showControls: true,
30
+ controlsTimeout: undefined,
31
+ };
32
+ }
33
+ return { ...state, selectedRendition: rendition };
34
+ }),
24
35
  protocol: PlayerProtocol.WEBRTC,
25
36
  setProtocol: (protocol: PlayerProtocol) =>
26
37
  set((state) => ({ ...state, protocol: protocol })),
@@ -28,10 +39,6 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
28
39
  src: "",
29
40
  setSrc: (src: string) => set(() => ({ src })),
30
41
 
31
- ingestStarting: false,
32
- setIngestStarting: (ingestStarting: boolean) =>
33
- set(() => ({ ingestStarting })),
34
-
35
42
  ingestMediaSource: undefined,
36
43
  setIngestMediaSource: (ingestMediaSource: IngestMediaSource | undefined) =>
37
44
  set(() => ({ ingestMediaSource })),
@@ -45,7 +52,7 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
45
52
  ingestConnectionState: RTCPeerConnectionState | null,
46
53
  ) => set(() => ({ ingestConnectionState })),
47
54
 
48
- ingestAutoStart: false,
55
+ ingestAutoStart: true,
49
56
  setIngestAutoStart: (ingestAutoStart: boolean) =>
50
57
  set(() => ({ ingestAutoStart })),
51
58
 
@@ -53,6 +60,23 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
53
60
  setIngestStarted: (timestamp: number | null) =>
54
61
  set(() => ({ ingestStarted: timestamp })),
55
62
 
63
+ stopIngest: () => {
64
+ set(() => ({
65
+ ingestLive: false,
66
+ ingestConnectionState: "new",
67
+ ingestStarted: null,
68
+ })),
69
+ setTimeout(
70
+ () =>
71
+ set(() => ({
72
+ ingestLive: false,
73
+ ingestConnectionState: "new",
74
+ ingestStarted: null,
75
+ })),
76
+ 200,
77
+ );
78
+ },
79
+
56
80
  fullscreen: false,
57
81
  setFullscreen: (isFullscreen: boolean) =>
58
82
  set(() => ({ fullscreen: isFullscreen })),
@@ -154,10 +178,12 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
154
178
 
155
179
  setUserInteraction: () =>
156
180
  set((p) => {
157
- // controls timeout
158
181
  if (p.controlsTimeout) {
159
182
  clearTimeout(p.controlsTimeout);
160
183
  }
184
+ if (p.selectedRendition === "audio") {
185
+ return { showControls: true, controlsTimeout: undefined };
186
+ }
161
187
  let controlsTimeout = setTimeout(() => p.setShowControls(false), 1000);
162
188
  return { showControls: true, controlsTimeout };
163
189
  }),
@@ -143,16 +143,12 @@ export function useCurrentPlayerRendition(): [
143
143
  * Hook to get the ingest state of the current player
144
144
  */
145
145
  export function useCurrentPlayerIngest(): {
146
- starting: boolean;
147
- setStarting: (starting: boolean) => void;
148
146
  connectionState: RTCPeerConnectionState | null;
149
147
  setConnectionState: (state: RTCPeerConnectionState | null) => void;
150
148
  startedTimestamp: number | null;
151
149
  setStartedTimestamp: (timestamp: number | null) => void;
152
150
  } {
153
151
  return useCurrentPlayerStore((state) => ({
154
- starting: state.ingestStarting,
155
- setStarting: state.setIngestStarting,
156
152
  connectionState: state.ingestConnectionState,
157
153
  setConnectionState: state.setIngestConnectionState,
158
154
  startedTimestamp: state.ingestStarted,
@@ -25,11 +25,66 @@ const blobToBase64 = (blob: Blob): Promise<string> => {
25
25
  });
26
26
  };
27
27
 
28
+ const PropsInHeader = [
29
+ "siteTitle",
30
+ "siteDescription",
31
+ "primaryColor",
32
+ "accentColor",
33
+ "defaultStreamer",
34
+ "mainLogo",
35
+ "favicon",
36
+ "sidebarBg",
37
+ "legalLinks",
38
+ ];
39
+
40
+ function getMetaContent(key: string): BrandingAsset | null {
41
+ if (typeof window === "undefined" || !window.document) return null;
42
+ const meta = document.querySelector(`meta[name="internal-brand:${key}`);
43
+ if (meta && meta.getAttribute("content")) {
44
+ let content = meta.getAttribute("content");
45
+ if (content) return JSON.parse(content) as BrandingAsset;
46
+ }
47
+
48
+ return null;
49
+ }
50
+
28
51
  // hook to fetch broadcaster DID (unauthenticated)
29
52
  export function useFetchBroadcasterDID() {
30
53
  const streamplaceAgent = usePossiblyUnauthedPDSAgent();
31
54
  const store = getStreamplaceStoreFromContext();
32
55
 
56
+ // prefetch from meta records, if on web
57
+ useEffect(() => {
58
+ if (typeof window !== "undefined" && window.document) {
59
+ try {
60
+ const metaRecords = PropsInHeader.reduce(
61
+ (acc, key) => {
62
+ const meta = document.querySelector(
63
+ `meta[name="internal-brand:${key}`,
64
+ );
65
+ // hrmmmmmmmmmmmm
66
+ if (meta && meta.getAttribute("content")) {
67
+ let content = meta.getAttribute("content");
68
+ if (content) acc[key] = JSON.parse(content) as BrandingAsset;
69
+ }
70
+ return acc;
71
+ },
72
+ {} as Record<string, BrandingAsset>,
73
+ );
74
+
75
+ console.log("Found meta records for broadcaster DID:", metaRecords);
76
+ // filter out all non-text values, can get on second fetch?
77
+ for (const key of Object.keys(metaRecords)) {
78
+ if (metaRecords[key].mimeType != "text/plain") {
79
+ delete metaRecords[key];
80
+ }
81
+ }
82
+ } catch (e) {
83
+ console.warn("Failed to parse broadcaster DID from meta tags", e);
84
+ }
85
+ }
86
+ }, []);
87
+
33
88
  return useCallback(async () => {
34
89
  try {
35
90
  if (!streamplaceAgent) {
@@ -140,7 +195,11 @@ export function useFetchBranding() {
140
195
 
141
196
  // hook to get a specific branding asset by key
142
197
  export function useBrandingAsset(key: string): BrandingAsset | undefined {
143
- return useStreamplaceStore((state) => state.branding?.[key]);
198
+ return (
199
+ useStreamplaceStore((state) => state.branding?.[key]) ||
200
+ getMetaContent(key) ||
201
+ undefined
202
+ );
144
203
  }
145
204
 
146
205
  // convenience hook for main logo
@@ -127,111 +127,25 @@ export function useCreateStreamRecord() {
127
127
  let agent = usePDSAgent();
128
128
  let url = useUrl();
129
129
  const uploadThumbnail = useUploadThumbnail();
130
-
131
130
  return async ({
132
131
  title,
133
132
  customThumbnail,
134
133
  submitPost,
135
134
  canonicalUrl,
136
135
  notificationSettings,
136
+ idleTimeoutSeconds,
137
137
  }: {
138
138
  title: string;
139
139
  customThumbnail?: Blob;
140
140
  submitPost?: boolean;
141
141
  canonicalUrl?: string;
142
142
  notificationSettings?: PlaceStreamLivestream.NotificationSettings;
143
+ idleTimeoutSeconds?: number;
143
144
  }) => {
144
- if (typeof submitPost !== "boolean") {
145
- submitPost = true;
146
- }
147
145
  if (!agent) {
148
146
  throw new Error("No PDS agent found");
149
147
  }
150
148
 
151
- if (!agent.did) {
152
- throw new Error("No user DID found, assuming not logged in");
153
- }
154
-
155
- const u = new URL(url);
156
-
157
- let thumbnail: BlobRef | undefined = undefined;
158
-
159
- if (customThumbnail) {
160
- try {
161
- thumbnail = await uploadThumbnail(agent, customThumbnail);
162
- } catch (e) {
163
- throw new Error(`Custom thumbnail upload failed ${e}`);
164
- }
165
- } else {
166
- // No custom thumbnail: fetch the server-side image and upload it
167
- // try thrice lel
168
- let tries = 0;
169
- try {
170
- for (; tries < 3; tries++) {
171
- try {
172
- console.log(
173
- `Fetching thumbnail from ${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`,
174
- );
175
- const thumbnailRes = await fetch(
176
- `${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`,
177
- );
178
- if (!thumbnailRes.ok) {
179
- throw new Error(
180
- `Failed to fetch thumbnail: ${thumbnailRes.status})`,
181
- );
182
- }
183
- const thumbnailBlob = await thumbnailRes.blob();
184
- console.log(thumbnailBlob);
185
- thumbnail = await uploadThumbnail(agent, thumbnailBlob);
186
- } catch (e) {
187
- console.warn(
188
- `Failed to fetch thumbnail, retrying (${tries + 1}/3): ${e}`,
189
- );
190
- // Wait 1 second before retrying
191
- await new Promise((resolve) => setTimeout(resolve, 2000));
192
- if (tries === 2) {
193
- throw new Error(`Failed to fetch thumbnail after 3 tries: ${e}`);
194
- }
195
- }
196
- }
197
- } catch (e) {
198
- throw new Error(`Thumbnail upload failed ${e}`);
199
- }
200
- }
201
-
202
- let newPost: undefined | { uri: string; cid: string } = undefined;
203
-
204
- const did = agent.did;
205
- const profile = await agent.getProfile({ actor: did });
206
-
207
- if (submitPost) {
208
- if (!profile) {
209
- throw new Error("No profile found for the user DID");
210
- }
211
-
212
- const params = new URLSearchParams({
213
- did: did,
214
- time: new Date().toISOString(),
215
- });
216
-
217
- let post = await buildGoLivePost(
218
- title,
219
- u,
220
- profile.data,
221
- params,
222
- thumbnail,
223
- agent,
224
- );
225
-
226
- newPost = await createNewPost(agent, post);
227
-
228
- if (!newPost.uri || !newPost.cid) {
229
- throw new Error(
230
- "Cannot read properties of undefined (reading 'uri' or 'cid')",
231
- );
232
- }
233
- }
234
-
235
149
  let platform: string = Platform.OS;
236
150
  let platVersion: string = Platform.Version
237
151
  ? Platform.Version.toString()
@@ -244,36 +158,50 @@ export function useCreateStreamRecord() {
244
158
  ) {
245
159
  platVersion = getBrowserName(window.navigator.userAgent);
246
160
  }
247
-
248
- const thisUrl = `${url}/${profile.data.handle}`;
249
- if (!canonicalUrl) {
250
- canonicalUrl = thisUrl;
161
+ if (!agent.did) {
162
+ throw new Error("No user DID found, assuming not logged in");
251
163
  }
252
164
 
165
+ const thisUrl = `${url}/${agent.did}`;
166
+
253
167
  const record: PlaceStreamLivestream.Record = {
254
168
  $type: "place.stream.livestream",
255
169
  title: title,
256
170
  url: thisUrl,
257
171
  createdAt: new Date().toISOString(),
172
+ lastSeenAt: new Date().toISOString(),
258
173
  // would match up with e.g. https://stream.place/iame.li
259
174
  canonicalUrl: canonicalUrl,
260
175
  // user agent style string
261
176
  // e.g. `@streamplace/components/0.1.0 (ios, 32.0)`
262
177
  agent: `@streamplace/components/${PackageJson.version} (${platform}, ${platVersion})`,
263
- post: newPost,
264
- thumb: thumbnail,
178
+ idleTimeoutSeconds: idleTimeoutSeconds,
265
179
  };
266
180
 
267
181
  if (notificationSettings) {
268
182
  record.notificationSettings = notificationSettings;
269
183
  }
270
184
 
271
- await agent.com.atproto.repo.createRecord({
272
- repo: agent.did,
273
- collection: "place.stream.livestream",
274
- record,
185
+ if (customThumbnail) {
186
+ try {
187
+ const thumbnail = await uploadThumbnail(agent, customThumbnail);
188
+ record.thumb = thumbnail;
189
+ } catch (e) {
190
+ throw new Error(`Custom thumbnail upload failed ${e}`);
191
+ }
192
+ }
193
+
194
+ const output = await agent.place.stream.live.startLivestream({
195
+ livestream: record,
196
+ streamer: agent.did,
197
+ createBlueskyPost: submitPost,
275
198
  });
276
- return record;
199
+
200
+ if (!output.success) {
201
+ throw new Error("Failed to start livestream");
202
+ }
203
+
204
+ return output.data;
277
205
  };
278
206
  }
279
207
 
@@ -339,3 +267,18 @@ export function useUpdateStreamRecord(customUrl: string | null = null) {
339
267
  return record;
340
268
  };
341
269
  }
270
+
271
+ export function useEndLivestream() {
272
+ let agent = usePDSAgent();
273
+ return async () => {
274
+ if (!agent) {
275
+ throw new Error("No PDS agent found");
276
+ }
277
+
278
+ if (!agent.did) {
279
+ throw new Error("No user DID found, assuming not logged in");
280
+ }
281
+
282
+ return await agent.place.stream.live.stopLivestream({});
283
+ };
284
+ }