@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
@@ -1,16 +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
- const stopIngest = usePlayerStore((x) => x.stopIngest);
13
-
9
+ const endLivestream = useEndLivestream();
10
+ const setLocalLivestreamURI = useLivestreamStore(
11
+ (x) => x.setLocalLivestreamURI,
12
+ );
14
13
  const createStreamRecord = useCreateStreamRecord();
15
14
 
16
15
  const [title, setTitle] = useState<string>("");
@@ -22,10 +21,11 @@ export function useLivestreamInfo(url?: string) {
22
21
  if (title !== "") {
23
22
  setRecordSubmitted(true);
24
23
  // Create the livestream record with title and custom url if available
25
- await createStreamRecord({
24
+ const { uri } = await createStreamRecord({
26
25
  title,
27
26
  canonicalUrl: url || undefined,
28
27
  });
28
+ setLocalLivestreamURI(uri);
29
29
  }
30
30
  } catch (error) {
31
31
  console.error("Error creating livestream:", error);
@@ -39,26 +39,19 @@ export function useLivestreamInfo(url?: string) {
39
39
  keyboardHeight?: number,
40
40
  closeKeyboard?: () => void,
41
41
  ) => {
42
- if (!ingestStarting) {
43
- // Optionally close keyboard if provided
44
- if (closeKeyboard) closeKeyboard();
45
- setShowCountdown(true);
46
- setIngestStarting(true);
47
- setIngestLive(true);
48
- // wait ~3 seconds before announcing
49
- setTimeout(() => {
50
- handleSubmit();
51
- }, 3000);
52
- } else {
53
- setIngestStarting(false);
54
- setIngestLive(false);
55
- }
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);
56
49
  };
57
50
 
58
51
  // Stop the current broadcast
59
52
  const toggleStopStream = () => {
60
53
  console.log("Stopping stream...");
61
- stopIngest();
54
+ endLivestream();
62
55
  };
63
56
 
64
57
  return {
@@ -70,8 +63,6 @@ export function useLivestreamInfo(url?: string) {
70
63
  setShowCountdown,
71
64
  recordSubmitted,
72
65
  setRecordSubmitted,
73
- ingestStarting,
74
- setIngestStarting,
75
66
  handleSubmit,
76
67
  toggleGoLive,
77
68
  toggleStopStream,
@@ -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;
@@ -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
 
@@ -171,10 +178,12 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
171
178
 
172
179
  setUserInteraction: () =>
173
180
  set((p) => {
174
- // controls timeout
175
181
  if (p.controlsTimeout) {
176
182
  clearTimeout(p.controlsTimeout);
177
183
  }
184
+ if (p.selectedRendition === "audio") {
185
+ return { showControls: true, controlsTimeout: undefined };
186
+ }
178
187
  let controlsTimeout = setTimeout(() => p.setShowControls(false), 1000);
179
188
  return { showControls: true, controlsTimeout };
180
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,
@@ -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
+ }