@streamplace/components 0.0.1 → 0.7.0

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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,159 @@
1
+ import { useContext } from "react";
2
+ import { createStore, useStore } from "zustand";
3
+ import { PlayerContext } from "./context";
4
+ import { PlayerProtocol, PlayerStatus, } from "./player-state";
5
+ export const makePlayerStore = (id) => {
6
+ return createStore()((set) => ({
7
+ id: id || Math.random().toString(36).slice(8),
8
+ selectedRendition: "source",
9
+ setSelectedRendition: (rendition) => set((state) => ({ ...state, selectedRendition: rendition })),
10
+ protocol: PlayerProtocol.WEBRTC,
11
+ setProtocol: (protocol) => set((state) => ({ ...state, protocol: protocol })),
12
+ src: "",
13
+ setSrc: (src) => set(() => ({ src })),
14
+ ingestStarting: false,
15
+ setIngestStarting: (ingestStarting) => set(() => ({ ingestStarting })),
16
+ ingestMediaSource: undefined,
17
+ setIngestMediaSource: (ingestMediaSource) => set(() => ({ ingestMediaSource })),
18
+ ingestCamera: "user",
19
+ setIngestCamera: (ingestCamera) => set(() => ({ ingestCamera })),
20
+ ingestConnectionState: null,
21
+ setIngestConnectionState: (ingestConnectionState) => set(() => ({ ingestConnectionState })),
22
+ ingestAutoStart: false,
23
+ setIngestAutoStart: (ingestAutoStart) => set(() => ({ ingestAutoStart })),
24
+ ingestStarted: null,
25
+ setIngestStarted: (timestamp) => set(() => ({ ingestStarted: timestamp })),
26
+ muted: false,
27
+ setMuted: (isMuted) => set(() => ({ muted: isMuted, muteWasForced: false })),
28
+ volume: 1.0,
29
+ setVolume: (volume) => set(() => ({ volume, muteWasForced: false })),
30
+ fullscreen: false,
31
+ setFullscreen: (isFullscreen) => set(() => ({ fullscreen: isFullscreen })),
32
+ status: PlayerStatus.START,
33
+ setStatus: (status) => set(() => ({ status })),
34
+ playTime: 0,
35
+ setPlayTime: (playTime) => set(() => ({ playTime })),
36
+ offline: false,
37
+ setOffline: (offline) => set(() => ({ offline })),
38
+ videoRef: undefined,
39
+ setVideoRef: (videoRef) => set(() => ({ videoRef })),
40
+ pipMode: false,
41
+ setPipMode: (pipMode) => set(() => ({ pipMode })),
42
+ // Player element width/height setters for global sync
43
+ playerWidth: undefined,
44
+ setPlayerWidth: (playerWidth) => set(() => ({ playerWidth })),
45
+ playerHeight: undefined,
46
+ setPlayerHeight: (playerHeight) => set(() => ({ playerHeight })),
47
+ // * Whether mute was forced by the browser or not for autoplay
48
+ // * Will get set to 'false' if the user has interacted with the volume
49
+ muteWasForced: false,
50
+ setMuteWasForced: (muteWasForced) => set(() => ({ muteWasForced })),
51
+ embedded: false,
52
+ setEmbedded: (embedded) => set(() => ({ embedded })),
53
+ showControls: true,
54
+ controlsTimeout: undefined,
55
+ setShowControls: (showControls) => set({ showControls, controlsTimeout: undefined }),
56
+ telemetry: true,
57
+ setTelemetry: (telemetry) => set(() => ({ telemetry })),
58
+ ingestLive: false,
59
+ setIngestLive: (ingestLive) => set(() => ({ ingestLive })),
60
+ playerEvent: async (url, time, eventType, meta) => set((x) => {
61
+ const data = {
62
+ time: time,
63
+ playerId: x.id,
64
+ eventType: eventType,
65
+ meta: {
66
+ ...meta,
67
+ },
68
+ };
69
+ try {
70
+ // fetch url from sp provider
71
+ fetch(`${url}/api/player-event`, {
72
+ method: "POST",
73
+ body: JSON.stringify(data),
74
+ });
75
+ }
76
+ catch (e) {
77
+ console.error("error sending player telemetry", e);
78
+ }
79
+ return {};
80
+ }),
81
+ // Clear the controls timeout, if it exists.
82
+ // Should be called on player unmount.
83
+ clearControlsTimeout: () => set((state) => {
84
+ if (state.controlsTimeout) {
85
+ clearTimeout(state.controlsTimeout);
86
+ }
87
+ return { controlsTimeout: undefined };
88
+ }),
89
+ setUserInteraction: () => set((p) => {
90
+ // controls timeout
91
+ if (p.controlsTimeout) {
92
+ clearTimeout(p.controlsTimeout);
93
+ }
94
+ let controlsTimeout = setTimeout(() => p.setShowControls(false), 1000);
95
+ return { showControls: true, controlsTimeout };
96
+ }),
97
+ showDebugInfo: false,
98
+ setShowDebugInfo: (showDebugInfo) => set(() => ({ showDebugInfo })),
99
+ modMessage: null,
100
+ setModMessage: (modMessage) => set(() => ({ modMessage })),
101
+ }));
102
+ };
103
+ export function usePlayerContext() {
104
+ const context = useContext(PlayerContext);
105
+ if (!context) {
106
+ throw new Error("usePlayerContext must be used within a PlayerProvider");
107
+ }
108
+ return context;
109
+ }
110
+ // Get a specific player store by ID
111
+ export function getPlayerStoreById(id) {
112
+ const { players } = usePlayerContext();
113
+ const playerStore = players[id];
114
+ if (!playerStore) {
115
+ throw new Error(`No player found with ID: ${id}`);
116
+ }
117
+ return playerStore;
118
+ }
119
+ // Will get the first player ID in the context
120
+ export function getFirstPlayerID() {
121
+ const { players } = usePlayerContext();
122
+ const playerIds = Object.keys(players);
123
+ if (playerIds.length === 0) {
124
+ throw new Error("No players found in context");
125
+ }
126
+ return playerIds[0];
127
+ }
128
+ export function getPlayerStoreFromContext() {
129
+ console.warn("getPlayerStoreFromContext is deprecated. Use getPlayerStoreById instead.");
130
+ const { players } = usePlayerContext();
131
+ const playerIds = Object.keys(players);
132
+ if (playerIds.length === 0) {
133
+ throw new Error("No players found in context");
134
+ }
135
+ return players[playerIds[0]];
136
+ }
137
+ // Use a specific player store by ID
138
+ // If no ID is provided, it will use the first player in the context
139
+ export function usePlayerStore(selector, playerId) {
140
+ if (!playerId) {
141
+ playerId = Object.keys(usePlayerContext().players)[0];
142
+ }
143
+ const store = getPlayerStoreById(playerId);
144
+ return useStore(store, selector);
145
+ }
146
+ /* Convenience selectors/hooks */
147
+ export const usePlayerProtocol = (playerId) => usePlayerStore((x) => [x.protocol, x.setProtocol], playerId);
148
+ export const intoPlayerProtocol = (protocol) => {
149
+ switch (protocol) {
150
+ case "hls":
151
+ return PlayerProtocol.HLS;
152
+ case "progressive-mp4":
153
+ return PlayerProtocol.PROGRESSIVE_MP4;
154
+ case "progressive-webm":
155
+ return PlayerProtocol.PROGRESSIVE_WEBM;
156
+ default:
157
+ return PlayerProtocol.WEBRTC;
158
+ }
159
+ };
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { createContext, useContext, useMemo } from "react";
3
+ import { useStore } from "zustand";
4
+ import { usePlayerContext } from "../player-store";
5
+ import { PlayerProtocol } from "./player-state";
6
+ const SinglePlayerContext = createContext(null);
7
+ /**
8
+ * Provider component for a single player that creates a scoped context
9
+ * This allows components to access a specific player's state without passing IDs around
10
+ */
11
+ export const SinglePlayerProvider = ({ children, playerId: providedPlayerId, protocol = PlayerProtocol.WEBRTC, rendition = "auto", }) => {
12
+ const { players, createPlayer } = usePlayerContext();
13
+ // Create or get a player ID
14
+ const playerId = useMemo(() => {
15
+ // If a player ID is provided and exists, use it
16
+ if (providedPlayerId && players[providedPlayerId]) {
17
+ return providedPlayerId;
18
+ }
19
+ // If a player ID is provided but doesn't exist, create it
20
+ if (providedPlayerId) {
21
+ return createPlayer(providedPlayerId);
22
+ }
23
+ // Otherwise create a new player
24
+ return createPlayer();
25
+ }, [providedPlayerId, players, createPlayer]);
26
+ // Get the player store
27
+ const playerStore = useMemo(() => {
28
+ return players[playerId];
29
+ }, [players, playerId]);
30
+ // Set initial protocol and rendition if provided
31
+ React.useEffect(() => {
32
+ if (protocol) {
33
+ playerStore.setState((state) => ({
34
+ ...state,
35
+ protocol,
36
+ }));
37
+ }
38
+ if (rendition) {
39
+ playerStore.setState((state) => ({
40
+ ...state,
41
+ selectedRendition: rendition,
42
+ }));
43
+ }
44
+ }, [playerStore, protocol, rendition]);
45
+ // Create context value
46
+ const contextValue = useMemo(() => ({
47
+ playerId,
48
+ playerStore,
49
+ }), [playerId, playerStore]);
50
+ return (_jsx(SinglePlayerContext.Provider, { value: contextValue, children: children }));
51
+ };
52
+ /**
53
+ * Hook to access the current single player context
54
+ */
55
+ export function useSinglePlayerContext() {
56
+ const context = useContext(SinglePlayerContext);
57
+ if (!context) {
58
+ throw new Error("useSinglePlayerContext must be used within a SinglePlayerProvider");
59
+ }
60
+ return context;
61
+ }
62
+ /**
63
+ * Hook to access the current player ID from the single player context
64
+ */
65
+ export function useCurrentPlayerId() {
66
+ const { playerId } = useSinglePlayerContext();
67
+ return playerId;
68
+ }
69
+ /**
70
+ * Hook to access state from the current player without needing to specify the ID
71
+ */
72
+ export function useCurrentPlayerStore(selector) {
73
+ const { playerStore } = useSinglePlayerContext();
74
+ return useStore(playerStore, selector);
75
+ }
76
+ /**
77
+ * Hook to get the protocol of the current player
78
+ */
79
+ export function useCurrentPlayerProtocol() {
80
+ return useCurrentPlayerStore((state) => [state.protocol, state.setProtocol]);
81
+ }
82
+ /**
83
+ * Hook to get the selected rendition of the current player
84
+ */
85
+ export function useCurrentPlayerRendition() {
86
+ return useCurrentPlayerStore((state) => [state.selectedRendition, state.setSelectedRendition]);
87
+ }
88
+ /**
89
+ * Hook to get the ingest state of the current player
90
+ */
91
+ export function useCurrentPlayerIngest() {
92
+ return useCurrentPlayerStore((state) => ({
93
+ starting: state.ingestStarting,
94
+ setStarting: state.setIngestStarting,
95
+ connectionState: state.ingestConnectionState,
96
+ setConnectionState: state.setIngestConnectionState,
97
+ startedTimestamp: state.ingestStarted,
98
+ setStartedTimestamp: state.setIngestStarted,
99
+ }));
100
+ }
101
+ /**
102
+ * HOC to wrap components with a SinglePlayerProvider
103
+ */
104
+ export function withSinglePlayer(Component) {
105
+ return function WithSinglePlayer(props) {
106
+ const { playerId, protocol, rendition, ...componentProps } = props;
107
+ return (_jsx(SinglePlayerProvider, { playerId: playerId, protocol: protocol, rendition: rendition, children: _jsx(Component, { ...componentProps }) }));
108
+ };
109
+ }
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const StreamplaceContext = createContext(null);
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { makeStreamplaceStore } from "../streamplace-store/streamplace-store";
4
+ import { StreamplaceContext } from "./context";
5
+ import Poller from "./poller";
6
+ export function StreamplaceProvider({ children, url, oauthSession, }) {
7
+ // todo: handle url changes?
8
+ const store = useRef(makeStreamplaceStore({ url })).current;
9
+ useEffect(() => {
10
+ store.setState({ url });
11
+ }, [url]);
12
+ useEffect(() => {
13
+ store.setState({ oauthSession });
14
+ }, [oauthSession]);
15
+ return (_jsx(StreamplaceContext.Provider, { value: { store: store }, children: _jsx(Poller, { children: children }) }));
16
+ }
@@ -0,0 +1,46 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect } from "react";
3
+ import { StreamplaceAgent } from "streamplace";
4
+ import { useDID, useGetBskyProfile, useGetChatProfile, useStreamplaceStore, } from "../streamplace-store";
5
+ import { usePDSAgent } from "../streamplace-store/xrpc";
6
+ export default function Poller({ children }) {
7
+ const url = useStreamplaceStore((state) => state.url);
8
+ const setLiveUsers = useStreamplaceStore((state) => state.setLiveUsers);
9
+ const did = useDID();
10
+ const pdsAgent = usePDSAgent();
11
+ const getChatProfile = useGetChatProfile();
12
+ const getBskyProfile = useGetBskyProfile();
13
+ const liveUserRefresh = useStreamplaceStore((state) => state.liveUsersRefresh);
14
+ useEffect(() => {
15
+ if (pdsAgent && did) {
16
+ getChatProfile();
17
+ getBskyProfile();
18
+ }
19
+ }, [pdsAgent, did]);
20
+ useEffect(() => {
21
+ const agent = new StreamplaceAgent(url);
22
+ const go = async () => {
23
+ setLiveUsers({
24
+ liveUsersLoading: true,
25
+ });
26
+ try {
27
+ const res = await agent.place.stream.live.getLiveUsers();
28
+ setLiveUsers({
29
+ liveUsers: res.data.streams || [],
30
+ liveUsersLoading: false,
31
+ liveUsersError: null,
32
+ });
33
+ }
34
+ catch (e) {
35
+ setLiveUsers({
36
+ liveUsersLoading: false,
37
+ liveUsersError: e.message,
38
+ });
39
+ }
40
+ };
41
+ go();
42
+ const handle = setInterval(go, 3000);
43
+ return () => clearInterval(handle);
44
+ }, [url, liveUserRefresh]);
45
+ return _jsx(_Fragment, { children: children });
46
+ }
File without changes
@@ -0,0 +1,23 @@
1
+ import { usePDSAgent } from "./xrpc";
2
+ export function useCreateBlockRecord() {
3
+ let agent = usePDSAgent();
4
+ return async (subjectDID) => {
5
+ if (!agent) {
6
+ throw new Error("No PDS agent found");
7
+ }
8
+ if (!agent.did) {
9
+ throw new Error("No user DID found, assuming not logged in");
10
+ }
11
+ const record = {
12
+ $type: "app.bsky.graph.block",
13
+ subject: subjectDID,
14
+ createdAt: new Date().toISOString(),
15
+ };
16
+ return await agent.com.atproto.repo.createRecord({
17
+ repo: agent.did,
18
+ collection: "app.bsky.graph.block",
19
+ record,
20
+ });
21
+ return record;
22
+ };
23
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./stream";
2
+ export * from "./streamplace-store";
3
+ export * from "./user";
@@ -0,0 +1,193 @@
1
+ import { RichText } from "@atproto/api";
2
+ import { useUrl } from "./streamplace-store";
3
+ import { usePDSAgent } from "./xrpc";
4
+ const uploadThumbnail = async (pdsAgent, customThumbnail) => {
5
+ if (customThumbnail) {
6
+ let tries = 0;
7
+ try {
8
+ let thumbnail = await pdsAgent.uploadBlob(customThumbnail);
9
+ while (thumbnail.data.blob.size === 0 &&
10
+ customThumbnail.size !== 0 &&
11
+ tries < 3) {
12
+ console.warn("Reuploading blob as blob sizes don't match! Blob size recieved is", thumbnail.data.blob.size, "and sent blob size is", customThumbnail.size);
13
+ thumbnail = await pdsAgent.uploadBlob(customThumbnail);
14
+ }
15
+ if (tries === 3) {
16
+ throw new Error("Could not successfully upload blob (tried thrice)");
17
+ }
18
+ if (thumbnail.success) {
19
+ console.log("Successfully uploaded thumbnail");
20
+ return thumbnail.data.blob;
21
+ }
22
+ }
23
+ catch (e) {
24
+ throw new Error("Error uploading thumbnail: " + e);
25
+ }
26
+ }
27
+ };
28
+ async function createNewPost(agent, record) {
29
+ try {
30
+ const post = await agent.post(record);
31
+ return { uri: post.uri, cid: post.cid };
32
+ }
33
+ catch (error) {
34
+ console.error("Error creating new post:", error);
35
+ throw error;
36
+ }
37
+ }
38
+ function buildGoLivePost(text, url, profile, params, thumbnail) {
39
+ const now = new Date();
40
+ const linkUrl = `${url.protocol}//${url.host}/${profile.handle}?${params.toString()}`;
41
+ const prefix = `🔴 LIVE `;
42
+ const textUrl = `${url.protocol}//${url.host}/${profile.handle}`;
43
+ const suffix = ` ${text}`;
44
+ const content = prefix + textUrl + suffix;
45
+ const rt = new RichText({ text: content });
46
+ rt.detectFacetsWithoutResolution();
47
+ const record = {
48
+ $type: "app.bsky.feed.post",
49
+ text: content,
50
+ "place.stream.livestream": {
51
+ url: linkUrl,
52
+ title: text,
53
+ },
54
+ facets: rt.facets,
55
+ createdAt: now.toISOString(),
56
+ };
57
+ record.embed = {
58
+ $type: "app.bsky.embed.external",
59
+ external: {
60
+ description: text,
61
+ thumb: thumbnail,
62
+ title: `@${profile.handle} is 🔴LIVE on ${url.host}!`,
63
+ uri: linkUrl,
64
+ },
65
+ };
66
+ return record;
67
+ }
68
+ export function useCreateStreamRecord() {
69
+ let agent = usePDSAgent();
70
+ let url = useUrl();
71
+ return async (title, customThumbnail, submitPost = true) => {
72
+ if (!agent) {
73
+ throw new Error("No PDS agent found");
74
+ }
75
+ if (!agent.did) {
76
+ throw new Error("No user DID found, assuming not logged in");
77
+ }
78
+ let thumbnail = undefined;
79
+ const u = new URL(url);
80
+ if (customThumbnail) {
81
+ try {
82
+ thumbnail = await uploadThumbnail(agent, customThumbnail);
83
+ }
84
+ catch (e) {
85
+ throw new Error(`Custom thumbnail upload failed ${e}`);
86
+ }
87
+ }
88
+ else {
89
+ // No custom thumbnail: fetch the server-side image and upload it
90
+ // try thrice lel
91
+ let tries = 0;
92
+ try {
93
+ for (; tries < 3; tries++) {
94
+ try {
95
+ console.log(`Fetching thumbnail from ${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`);
96
+ const thumbnailRes = await fetch(`${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`);
97
+ if (!thumbnailRes.ok) {
98
+ throw new Error(`Failed to fetch thumbnail: ${thumbnailRes.status})`);
99
+ }
100
+ const thumbnailBlob = await thumbnailRes.blob();
101
+ console.log(thumbnailBlob);
102
+ thumbnail = await uploadThumbnail(agent, thumbnailBlob);
103
+ }
104
+ catch (e) {
105
+ console.warn(`Failed to fetch thumbnail, retrying (${tries + 1}/3): ${e}`);
106
+ // Wait 1 second before retrying
107
+ await new Promise((resolve) => setTimeout(resolve, 2000));
108
+ if (tries === 2) {
109
+ throw new Error(`Failed to fetch thumbnail after 3 tries: ${e}`);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ catch (e) {
115
+ throw new Error(`Thumbnail upload failed ${e}`);
116
+ }
117
+ }
118
+ let newPost = undefined;
119
+ if (submitPost) {
120
+ const did = agent.did;
121
+ const profile = await agent.getProfile({ actor: did });
122
+ if (!profile) {
123
+ throw new Error("No profile found for the user DID");
124
+ }
125
+ const params = new URLSearchParams({
126
+ did: did,
127
+ time: new Date().toISOString(),
128
+ });
129
+ let post = buildGoLivePost(title, u, profile.data, params, thumbnail);
130
+ newPost = await createNewPost(agent, post);
131
+ if (!newPost.uri || !newPost.cid) {
132
+ throw new Error("Cannot read properties of undefined (reading 'uri' or 'cid')");
133
+ }
134
+ }
135
+ const record = {
136
+ title: title,
137
+ url: url,
138
+ createdAt: new Date().toISOString(),
139
+ post: newPost,
140
+ thumb: thumbnail,
141
+ };
142
+ await agent.com.atproto.repo.createRecord({
143
+ repo: agent.did,
144
+ collection: "place.stream.livestream",
145
+ record,
146
+ });
147
+ return record;
148
+ };
149
+ }
150
+ export function useUpdateStreamRecord() {
151
+ let agent = usePDSAgent();
152
+ let url = useUrl();
153
+ return async (title, livestream, customThumbnail) => {
154
+ if (!agent) {
155
+ throw new Error("No PDS agent found");
156
+ }
157
+ if (!agent.did) {
158
+ throw new Error("No user DID found, assuming not logged in");
159
+ }
160
+ if (!livestream) {
161
+ throw new Error("No latest record");
162
+ }
163
+ let rkey = livestream.uri.split("/").pop();
164
+ let oldRecordValue = livestream.record;
165
+ if (!rkey) {
166
+ throw new Error("No rkey?");
167
+ }
168
+ let thumbnail = oldRecordValue.thumb;
169
+ // update thumbnail if a new one is provided
170
+ if (customThumbnail) {
171
+ try {
172
+ thumbnail = await uploadThumbnail(agent, customThumbnail);
173
+ }
174
+ catch (e) {
175
+ throw new Error(`Custom thumbnail upload failed ${e}`);
176
+ }
177
+ }
178
+ const record = {
179
+ title: title,
180
+ url: url,
181
+ createdAt: new Date().toISOString(),
182
+ post: oldRecordValue.post,
183
+ thumb: thumbnail,
184
+ };
185
+ await agent.com.atproto.repo.putRecord({
186
+ repo: agent.did,
187
+ collection: "place.stream.livestream",
188
+ rkey,
189
+ record,
190
+ });
191
+ return record;
192
+ };
193
+ }
@@ -0,0 +1,37 @@
1
+ import { useContext } from "react";
2
+ import { createStore, useStore } from "zustand";
3
+ import { StreamplaceContext } from "../streamplace-provider/context";
4
+ export const makeStreamplaceStore = ({ url, }) => {
5
+ return createStore()((set) => ({
6
+ url,
7
+ liveUsers: null,
8
+ setLiveUsers: (opts) => {
9
+ set({
10
+ ...opts,
11
+ });
12
+ },
13
+ liveUsersRefresh: 0,
14
+ liveUsersLoading: true,
15
+ liveUsersError: null,
16
+ oauthSession: null,
17
+ handle: null,
18
+ chatProfile: null,
19
+ }));
20
+ };
21
+ export function getStreamplaceStoreFromContext() {
22
+ const context = useContext(StreamplaceContext);
23
+ if (!context) {
24
+ throw new Error("useStreamplaceStore must be used within a StreamplaceProvider");
25
+ }
26
+ return context.store;
27
+ }
28
+ export function useStreamplaceStore(selector) {
29
+ return useStore(getStreamplaceStoreFromContext(), selector);
30
+ }
31
+ export const useUrl = () => useStreamplaceStore((x) => x.url);
32
+ export const useDID = () => useStreamplaceStore((x) => x.oauthSession?.did);
33
+ export const useHandle = () => useStreamplaceStore((x) => x.handle);
34
+ export const useSetHandle = () => {
35
+ const store = getStreamplaceStoreFromContext();
36
+ return (handle) => store.setState({ handle });
37
+ };
@@ -0,0 +1,47 @@
1
+ import { PlaceStreamChatProfile } from "streamplace";
2
+ import { getStreamplaceStoreFromContext, useDID, useStreamplaceStore, } from "./streamplace-store";
3
+ import { usePDSAgent } from "./xrpc";
4
+ export function useGetChatProfile() {
5
+ const did = useDID();
6
+ const pdsAgent = usePDSAgent();
7
+ const store = getStreamplaceStoreFromContext();
8
+ return async () => {
9
+ if (!did || !pdsAgent) {
10
+ throw new Error("No DID or PDS agent");
11
+ }
12
+ const res = await pdsAgent.com.atproto.repo.getRecord({
13
+ repo: did,
14
+ collection: "place.stream.chat.profile",
15
+ rkey: "self",
16
+ });
17
+ if (!res.success) {
18
+ throw new Error("Failed to get chat profile record");
19
+ }
20
+ if (PlaceStreamChatProfile.isRecord(res.data.value)) {
21
+ store.setState({ chatProfile: res.data.value });
22
+ }
23
+ else {
24
+ console.log("not a record", res.data.value);
25
+ }
26
+ };
27
+ }
28
+ export function useGetBskyProfile() {
29
+ const did = useDID();
30
+ const pdsAgent = usePDSAgent();
31
+ const store = getStreamplaceStoreFromContext();
32
+ return async () => {
33
+ if (!did || !pdsAgent) {
34
+ throw new Error("No DID or PDS agent");
35
+ }
36
+ const res = await pdsAgent.app.bsky.actor.getProfile({
37
+ actor: did,
38
+ });
39
+ if (!res.success) {
40
+ throw new Error("Failed to get chat profile record");
41
+ }
42
+ store.setState({ handle: res.data.handle });
43
+ };
44
+ }
45
+ export function useChatProfile() {
46
+ return useStreamplaceStore((x) => x.chatProfile);
47
+ }
@@ -0,0 +1,12 @@
1
+ import { useMemo } from "react";
2
+ import { StreamplaceAgent } from "streamplace";
3
+ import { useStreamplaceStore } from ".";
4
+ export function usePDSAgent() {
5
+ const oauthSession = useStreamplaceStore((state) => state.oauthSession);
6
+ return useMemo(() => {
7
+ if (!oauthSession) {
8
+ return null;
9
+ }
10
+ return new StreamplaceAgent(oauthSession);
11
+ }, [oauthSession]);
12
+ }