@streamplace/components 0.6.37 → 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 (138) hide show
  1. package/dist/components/chat/chat-box.js +109 -0
  2. package/dist/components/chat/chat-message.js +76 -0
  3. package/dist/components/chat/chat.js +56 -0
  4. package/dist/components/chat/mention-suggestions.js +39 -0
  5. package/dist/components/chat/mod-view.js +33 -0
  6. package/dist/components/mobile-player/fullscreen.js +69 -0
  7. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  8. package/dist/components/mobile-player/player.js +103 -0
  9. package/dist/components/mobile-player/props.js +1 -0
  10. package/dist/components/mobile-player/shared.js +51 -0
  11. package/dist/components/mobile-player/ui/countdown.js +79 -0
  12. package/dist/components/mobile-player/ui/index.js +5 -0
  13. package/dist/components/mobile-player/ui/input.js +38 -0
  14. package/dist/components/mobile-player/ui/metrics.js +40 -0
  15. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  17. package/dist/components/mobile-player/use-webrtc.js +232 -0
  18. package/dist/components/mobile-player/video.js +375 -0
  19. package/dist/components/mobile-player/video.native.js +238 -0
  20. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  21. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  22. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  23. package/dist/components/ui/button.js +220 -0
  24. package/dist/components/ui/dialog.js +203 -0
  25. package/dist/components/ui/dropdown.js +148 -0
  26. package/dist/components/ui/icons.js +22 -0
  27. package/dist/components/ui/index.js +22 -0
  28. package/dist/components/ui/input.js +202 -0
  29. package/dist/components/ui/loader.js +7 -0
  30. package/dist/components/ui/primitives/button.js +121 -0
  31. package/dist/components/ui/primitives/input.js +202 -0
  32. package/dist/components/ui/primitives/modal.js +203 -0
  33. package/dist/components/ui/primitives/text.js +286 -0
  34. package/dist/components/ui/resizeable.js +101 -0
  35. package/dist/components/ui/text.js +175 -0
  36. package/dist/components/ui/textarea.js +17 -0
  37. package/dist/components/ui/toast.js +129 -0
  38. package/dist/components/ui/view.js +250 -0
  39. package/dist/hooks/index.js +9 -0
  40. package/dist/hooks/useAvatars.js +32 -0
  41. package/dist/hooks/useCameraToggle.js +9 -0
  42. package/dist/hooks/useKeyboard.js +33 -0
  43. package/dist/hooks/useKeyboardSlide.js +11 -0
  44. package/dist/hooks/useLivestreamInfo.js +62 -0
  45. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  46. package/dist/hooks/usePlayerDimensions.js +19 -0
  47. package/dist/hooks/useSegmentTiming.js +62 -0
  48. package/dist/index.js +10 -0
  49. package/dist/lib/facet.js +88 -0
  50. package/dist/lib/theme/atoms.js +620 -0
  51. package/dist/lib/theme/atoms.types.js +5 -0
  52. package/dist/lib/theme/index.js +9 -0
  53. package/dist/lib/theme/theme.js +248 -0
  54. package/dist/lib/theme/tokens.js +383 -0
  55. package/dist/lib/utils.js +94 -0
  56. package/dist/livestream-provider/index.js +8 -3
  57. package/dist/livestream-store/chat.js +89 -65
  58. package/dist/livestream-store/index.js +1 -0
  59. package/dist/livestream-store/livestream-store.js +3 -0
  60. package/dist/livestream-store/stream-key.js +115 -0
  61. package/dist/player-store/player-provider.js +0 -1
  62. package/dist/player-store/player-store.js +13 -0
  63. package/dist/streamplace-store/block.js +23 -0
  64. package/dist/streamplace-store/index.js +1 -0
  65. package/dist/streamplace-store/stream.js +193 -0
  66. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  67. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  68. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  69. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  70. package/package.json +20 -4
  71. package/src/components/chat/chat-box.tsx +195 -0
  72. package/src/components/chat/chat-message.tsx +192 -0
  73. package/src/components/chat/chat.tsx +128 -0
  74. package/src/components/chat/mention-suggestions.tsx +71 -0
  75. package/src/components/chat/mod-view.tsx +118 -0
  76. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  77. package/src/components/mobile-player/fullscreen.tsx +79 -0
  78. package/src/components/mobile-player/player.tsx +134 -0
  79. package/src/components/mobile-player/props.tsx +11 -0
  80. package/src/components/mobile-player/shared.tsx +56 -0
  81. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  82. package/src/components/mobile-player/ui/index.ts +5 -0
  83. package/src/components/mobile-player/ui/input.tsx +85 -0
  84. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  85. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  86. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  87. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  88. package/src/components/mobile-player/video.native.tsx +360 -0
  89. package/src/components/mobile-player/video.tsx +557 -0
  90. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  91. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  92. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  93. package/src/components/ui/button.tsx +309 -0
  94. package/src/components/ui/dialog.tsx +376 -0
  95. package/src/components/ui/dropdown.tsx +399 -0
  96. package/src/components/ui/icons.tsx +50 -0
  97. package/src/components/ui/index.ts +33 -0
  98. package/src/components/ui/input.tsx +350 -0
  99. package/src/components/ui/loader.tsx +9 -0
  100. package/src/components/ui/primitives/button.tsx +292 -0
  101. package/src/components/ui/primitives/input.tsx +422 -0
  102. package/src/components/ui/primitives/modal.tsx +421 -0
  103. package/src/components/ui/primitives/text.tsx +499 -0
  104. package/src/components/ui/resizeable.tsx +169 -0
  105. package/src/components/ui/text.tsx +330 -0
  106. package/src/components/ui/textarea.tsx +34 -0
  107. package/src/components/ui/toast.tsx +203 -0
  108. package/src/components/ui/view.tsx +344 -0
  109. package/src/hooks/index.ts +9 -0
  110. package/src/hooks/useAvatars.tsx +44 -0
  111. package/src/hooks/useCameraToggle.ts +12 -0
  112. package/src/hooks/useKeyboard.tsx +41 -0
  113. package/src/hooks/useKeyboardSlide.ts +12 -0
  114. package/src/hooks/useLivestreamInfo.ts +67 -0
  115. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  116. package/src/hooks/usePlayerDimensions.ts +23 -0
  117. package/src/hooks/useSegmentTiming.tsx +88 -0
  118. package/src/index.tsx +21 -0
  119. package/src/lib/facet.ts +131 -0
  120. package/src/lib/theme/atoms.ts +760 -0
  121. package/src/lib/theme/atoms.types.ts +258 -0
  122. package/src/lib/theme/index.ts +48 -0
  123. package/src/lib/theme/theme.tsx +436 -0
  124. package/src/lib/theme/tokens.ts +409 -0
  125. package/src/lib/utils.ts +132 -0
  126. package/src/livestream-provider/index.tsx +13 -2
  127. package/src/livestream-store/chat.tsx +115 -78
  128. package/src/livestream-store/index.tsx +1 -0
  129. package/src/livestream-store/livestream-state.tsx +3 -0
  130. package/src/livestream-store/livestream-store.tsx +3 -0
  131. package/src/livestream-store/stream-key.tsx +124 -0
  132. package/src/player-store/player-provider.tsx +0 -1
  133. package/src/player-store/player-state.tsx +28 -0
  134. package/src/player-store/player-store.tsx +22 -0
  135. package/src/streamplace-store/block.tsx +29 -0
  136. package/src/streamplace-store/index.tsx +1 -0
  137. package/src/streamplace-store/stream.tsx +262 -0
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useContext, useRef } from "react";
3
3
  import { LivestreamContext, makeLivestreamStore } from "../livestream-store";
4
4
  import { useLivestreamWebsocket } from "./websocket";
@@ -14,7 +14,12 @@ export function LivestreamProvider({ children, src, }) {
14
14
  window.livestreamStore = store;
15
15
  return (_jsx(LivestreamContext.Provider, { value: { store: store }, children: _jsx(LivestreamPoller, { src: src, children: children }) }));
16
16
  }
17
- export function LivestreamPoller({ children, src, }) {
17
+ export function WebsocketWatcher({ src }) {
18
18
  useLivestreamWebsocket(src);
19
- return _jsx(_Fragment, { children: children });
19
+ return _jsx(_Fragment, {});
20
+ }
21
+ export function LivestreamPoller({ children, src, }) {
22
+ // Websocket watcher is a sibling instead of a parent to avoid
23
+ // re-rendering when the websocket does stuff
24
+ return (_jsxs(_Fragment, { children: [_jsx(WebsocketWatcher, { src: src }), children] }));
20
25
  }
@@ -1,14 +1,14 @@
1
1
  import { RichText } from "@atproto/api";
2
- import { isLink, isMention, } from "@atproto/api/dist/client/types/app/bsky/richtext/facet";
2
+ import { useCallback } from "react";
3
3
  import { useChatProfile, useDID, useHandle } from "../streamplace-store";
4
4
  import { usePDSAgent } from "../streamplace-store/xrpc";
5
5
  import { getStoreFromContext, useLivestreamStore } from "./livestream-store";
6
6
  export const useReplyToMessage = () => useLivestreamStore((state) => state.replyToMessage);
7
7
  export const useSetReplyToMessage = () => {
8
8
  const store = getStoreFromContext();
9
- return (message) => {
9
+ return useCallback((message) => {
10
10
  store.setState({ replyToMessage: message });
11
- };
11
+ }, [store]);
12
12
  };
13
13
  export const useCreateChatMessage = () => {
14
14
  const pdsAgent = usePDSAgent();
@@ -26,11 +26,12 @@ export const useCreateChatMessage = () => {
26
26
  throw new Error("Profile not found");
27
27
  }
28
28
  const rt = new RichText({ text: msg.text });
29
- rt.detectFacetsWithoutResolution();
29
+ await rt.detectFacets(pdsAgent);
30
30
  const record = {
31
31
  text: msg.text,
32
32
  createdAt: new Date().toISOString(),
33
33
  streamer: streamerProfile.did,
34
+ facets: rt.facets,
34
35
  ...(msg.reply
35
36
  ? {
36
37
  reply: {
@@ -45,33 +46,6 @@ export const useCreateChatMessage = () => {
45
46
  },
46
47
  }
47
48
  : {}),
48
- ...(rt.facets && rt.facets.length > 0
49
- ? {
50
- facets: rt.facets.map((facet) => ({
51
- index: facet.index,
52
- features: facet.features
53
- .filter((feature) => feature.$type === "app.bsky.richtext.facet#link" ||
54
- feature.$type === "app.bsky.richtext.facet#mention")
55
- .map((feature) => {
56
- if (isLink(feature)) {
57
- return {
58
- $type: "app.bsky.richtext.facet#link",
59
- uri: feature.uri,
60
- };
61
- }
62
- else if (isMention(feature)) {
63
- return {
64
- $type: "app.bsky.richtext.facet#mention",
65
- did: feature.did,
66
- };
67
- }
68
- else {
69
- throw new Error("invalid code path");
70
- }
71
- }),
72
- })),
73
- }
74
- : {}),
75
49
  };
76
50
  const localChat = {
77
51
  uri: `local-${Date.now()}`,
@@ -93,39 +67,90 @@ export const useCreateChatMessage = () => {
93
67
  });
94
68
  };
95
69
  };
96
- const CHAT_LIMIT = 20;
97
- export const reduceChat = (state, messages, blocks) => {
98
- state = { ...state };
99
- let newChat = {
100
- ...state.chatIndex,
101
- };
102
- // Add new messages
103
- for (let message of messages) {
70
+ const buildSortedChatList = (chatIndex, existingChatList, newMessages, removedKeys) => {
71
+ const sortedKeys = Object.keys(chatIndex).sort((a, b) => {
72
+ const aTime = parseInt(a.split("-")[0], 10);
73
+ const bTime = parseInt(b.split("-")[0], 10);
74
+ return bTime - aTime;
75
+ });
76
+ return sortedKeys.map((key) => chatIndex[key]);
77
+ };
78
+ const profileIsDifferent = (newProfile, oldProfile) => {
79
+ if (!oldProfile) {
80
+ return true;
81
+ }
82
+ if (!newProfile) {
83
+ return false;
84
+ }
85
+ if (!oldProfile.color) {
86
+ return true;
87
+ }
88
+ if (!newProfile.color) {
89
+ // idk. shouldn't happen.
90
+ return false;
91
+ }
92
+ const { red: newRed, green: newGreen, blue: newBlue } = newProfile.color;
93
+ const { red: oldRed, green: oldGreen, blue: oldBlue } = oldProfile.color;
94
+ return newRed !== oldRed || newGreen !== oldGreen || newBlue !== oldBlue;
95
+ };
96
+ export const reduceChatIncremental = (state, newMessages, blocks) => {
97
+ if (newMessages.length === 0 && blocks.length === 0) {
98
+ return state;
99
+ }
100
+ const newChatIndex = { ...state.chatIndex };
101
+ const newAuthors = { ...state.authors };
102
+ let hasChanges = false;
103
+ const removedKeys = new Set();
104
+ // handle blocks
105
+ if (blocks.length > 0) {
106
+ const blockedDIDs = new Set(blocks.map((block) => block.record.subject));
107
+ for (const [key, message] of Object.entries(newChatIndex)) {
108
+ if (blockedDIDs.has(message.author.did)) {
109
+ delete newChatIndex[key];
110
+ removedKeys.add(key);
111
+ hasChanges = true;
112
+ }
113
+ }
114
+ }
115
+ const messagesToAdd = [];
116
+ for (const message of newMessages) {
104
117
  const date = new Date(message.record.createdAt);
105
118
  const key = `${date.getTime()}-${message.uri}`;
106
- // Remove existing local message matching the server one
119
+ // only change the ref if the profile is different to avoid re-renders elsewhere
120
+ if (profileIsDifferent(message.chatProfile, newAuthors[message.author.handle])) {
121
+ newAuthors[message.author.handle] = message.chatProfile;
122
+ }
123
+ // skip messages we already have
124
+ if (newChatIndex[key] && newChatIndex[key].uri === message.uri) {
125
+ continue;
126
+ }
127
+ // if we have a local message, replace it with the new one
107
128
  if (!message.uri.startsWith("local-")) {
108
- const existingLocalMessageKey = Object.keys(newChat).find((k) => {
109
- const msg = newChat[k];
129
+ const existingLocalKey = Object.keys(newChatIndex).find((k) => {
130
+ const msg = newChatIndex[k];
110
131
  return (msg.uri.startsWith("local-") &&
111
132
  msg.record.text === message.record.text &&
112
- msg.author.did === message.author.did);
133
+ msg.author.did === message.author.did &&
134
+ Math.abs(new Date(msg.record.createdAt).getTime() - date.getTime()) <
135
+ 10000 // Within 10 seconds
136
+ );
113
137
  });
114
- if (existingLocalMessageKey) {
115
- delete newChat[existingLocalMessageKey];
138
+ if (existingLocalKey) {
139
+ delete newChatIndex[existingLocalKey];
140
+ removedKeys.add(existingLocalKey);
141
+ hasChanges = true;
116
142
  }
117
143
  }
118
- // Handle reply information for local-first messages
144
+ // add reply info
145
+ let processedMessage = message;
119
146
  if (message.record.reply) {
120
147
  const reply = message.record.reply;
121
148
  const parentUri = reply?.parent?.uri || reply?.root?.uri;
122
149
  if (parentUri) {
123
- // First try to find the parent message in our chat
124
- const parentMsgKey = Object.keys(newChat).find((k) => newChat[k].uri === parentUri);
150
+ const parentMsgKey = Object.keys(newChatIndex).find((k) => newChatIndex[k].uri === parentUri);
125
151
  if (parentMsgKey) {
126
- // Found the parent message, add its info to our message
127
- const parentMsg = newChat[parentMsgKey];
128
- message = {
152
+ const parentMsg = newChatIndex[parentMsgKey];
153
+ processedMessage = {
129
154
  ...message,
130
155
  replyTo: {
131
156
  cid: parentMsg.cid,
@@ -139,24 +164,23 @@ export const reduceChat = (state, messages, blocks) => {
139
164
  }
140
165
  }
141
166
  }
142
- newChat[key] = message;
167
+ messagesToAdd.push({ key, message: processedMessage });
168
+ hasChanges = true;
143
169
  }
144
- for (const block of blocks) {
145
- for (const [k, v] of Object.entries(newChat)) {
146
- if (v.author.did === block.record.subject) {
147
- delete newChat[k];
148
- }
149
- }
170
+ // Add new messages to index
171
+ for (const { key, message } of messagesToAdd) {
172
+ newChatIndex[key] = message;
173
+ }
174
+ // only rebuild if we have changes
175
+ if (!hasChanges) {
176
+ return state;
150
177
  }
151
- let newChatList = Object.values(newChat).sort((a, b) => new Date(a.record.createdAt) > new Date(b.record.createdAt) ? 1 : -1);
152
- newChatList = newChatList.slice(-CHAT_LIMIT);
153
- newChat = newChatList.reduce((acc, msg) => {
154
- acc[msg.uri] = msg;
155
- return acc;
156
- }, {});
178
+ // Build the new sorted chat list efficiently
179
+ const newChatList = buildSortedChatList(newChatIndex, state.chat, messagesToAdd, removedKeys);
157
180
  return {
158
181
  ...state,
159
- chatIndex: newChat,
182
+ chatIndex: newChatIndex,
160
183
  chat: newChatList,
161
184
  };
162
185
  };
186
+ export const reduceChat = reduceChatIncremental;
@@ -1,3 +1,4 @@
1
1
  export * from "./chat";
2
2
  export * from "./context";
3
3
  export * from "./livestream-store";
4
+ export * from "./stream-key";
@@ -12,6 +12,9 @@ export const makeLivestreamStore = () => {
12
12
  segment: null,
13
13
  renditions: [],
14
14
  replyToMessage: null,
15
+ streamKey: null,
16
+ setStreamKey: (sk) => set({ streamKey: sk }),
17
+ authors: {},
15
18
  }));
16
19
  };
17
20
  export function getStoreFromContext() {
@@ -0,0 +1,115 @@
1
+ import { bytesToMultibase, Secp256k1Keypair } from "@atproto/crypto";
2
+ import { useEffect, useState } from "react";
3
+ import { Platform } from "react-native";
4
+ import { privateKeyToAccount } from "viem/accounts";
5
+ import { usePDSAgent } from "../streamplace-store/xrpc";
6
+ import { useLivestreamStore } from "./livestream-store";
7
+ function getBrowserName(userAgent) {
8
+ // The order matters here, and this may report false positives for unlisted browsers.
9
+ if (userAgent.includes("Firefox")) {
10
+ // "Mozilla/5.0 (X11; Linux i686; rv:104.0) Gecko/20100101 Firefox/104.0"
11
+ return "Mozilla Firefox";
12
+ }
13
+ else if (userAgent.includes("SamsungBrowser")) {
14
+ // "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G955F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.4 Chrome/67.0.3396.87 Mobile Safari/537.36"
15
+ return "Samsung Internet";
16
+ }
17
+ else if (userAgent.includes("Opera") || userAgent.includes("OPR")) {
18
+ // "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 OPR/90.0.4480.54"
19
+ return "Opera";
20
+ }
21
+ else if (userAgent.includes("Edge")) {
22
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
23
+ return "Microsoft Edge (Legacy)";
24
+ }
25
+ else if (userAgent.includes("Edg")) {
26
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/104.0.1293.70"
27
+ return "Microsoft Edge (Chromium)";
28
+ }
29
+ else if (userAgent.includes("Chrome")) {
30
+ // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
31
+ return "Google Chrome or Chromium";
32
+ }
33
+ else if (userAgent.includes("Safari")) {
34
+ // "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1"
35
+ return "Apple Safari";
36
+ }
37
+ return "unknown";
38
+ }
39
+ export const useStreamKey = () => {
40
+ const pdsAgent = usePDSAgent();
41
+ const streamKey = useLivestreamStore((state) => state.streamKey);
42
+ const setStreamKey = useLivestreamStore((state) => state.setStreamKey);
43
+ const [key, setKey] = useState(streamKey ? JSON.parse(streamKey) : null);
44
+ const [error, setError] = useState(null);
45
+ useEffect(() => {
46
+ if (key)
47
+ return; // already have key
48
+ const generateKey = async () => {
49
+ if (!pdsAgent) {
50
+ setError("PDS Agent is not available");
51
+ return;
52
+ }
53
+ let did = pdsAgent.did;
54
+ if (!did) {
55
+ setError("PDS Agent did is not available (not logged in?)");
56
+ return;
57
+ }
58
+ const keypair = await Secp256k1Keypair.create({ exportable: true });
59
+ const exportedKey = await keypair.export();
60
+ const didBytes = new TextEncoder().encode(did);
61
+ const combinedKey = new Uint8Array([...exportedKey, ...didBytes]);
62
+ const multibaseKey = bytesToMultibase(combinedKey, "base58btc");
63
+ const hexKey = Array.from(exportedKey)
64
+ .map((b) => b.toString(16).padStart(2, "0"))
65
+ .join("");
66
+ const account = privateKeyToAccount(`0x${hexKey}`);
67
+ const newKey = {
68
+ privateKey: multibaseKey,
69
+ did: keypair.did(),
70
+ address: account.address.toLowerCase(),
71
+ };
72
+ let platform = Platform.OS;
73
+ if (Platform.OS === "web" &&
74
+ typeof window !== "undefined" &&
75
+ window.navigator) {
76
+ if (window.navigator.userAgent.includes("streamplace-desktop")) {
77
+ platform = "Desktop";
78
+ }
79
+ else {
80
+ platform = getBrowserName(window.navigator.userAgent);
81
+ if (platform !== "unknown") {
82
+ platform = platform;
83
+ }
84
+ }
85
+ }
86
+ else if (platform === "android") {
87
+ platform = "Android";
88
+ }
89
+ else if (platform === "ios") {
90
+ platform = "iOS";
91
+ }
92
+ else if (platform === "macos") {
93
+ platform = "macOS";
94
+ }
95
+ else if (platform === "windows") {
96
+ platform = "Windows";
97
+ }
98
+ const record = {
99
+ signingKey: keypair.did(),
100
+ createdAt: new Date().toISOString(),
101
+ createdBy: "Streamplace on " + platform,
102
+ };
103
+ await pdsAgent.com.atproto.repo.createRecord({
104
+ repo: did,
105
+ collection: "place.stream.key",
106
+ record,
107
+ });
108
+ setStreamKey(JSON.stringify(newKey));
109
+ setKey(newKey);
110
+ };
111
+ generateKey();
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ }, [key, setStreamKey]);
114
+ return { streamKey: key, error };
115
+ };
@@ -16,7 +16,6 @@ export const PlayerProvider = ({ children, initialPlayers = [], defaultId = Math
16
16
  return initialPlayerStores;
17
17
  });
18
18
  const createPlayer = useCallback((id) => {
19
- console.log("Creating new player");
20
19
  const playerId = id || Math.random().toString(36).slice(8);
21
20
  const playerStore = makePlayerStore(playerId);
22
21
  setPlayers((prev) => ({
@@ -15,6 +15,8 @@ export const makePlayerStore = (id) => {
15
15
  setIngestStarting: (ingestStarting) => set(() => ({ ingestStarting })),
16
16
  ingestMediaSource: undefined,
17
17
  setIngestMediaSource: (ingestMediaSource) => set(() => ({ ingestMediaSource })),
18
+ ingestCamera: "user",
19
+ setIngestCamera: (ingestCamera) => set(() => ({ ingestCamera })),
18
20
  ingestConnectionState: null,
19
21
  setIngestConnectionState: (ingestConnectionState) => set(() => ({ ingestConnectionState })),
20
22
  ingestAutoStart: false,
@@ -37,6 +39,11 @@ export const makePlayerStore = (id) => {
37
39
  setVideoRef: (videoRef) => set(() => ({ videoRef })),
38
40
  pipMode: false,
39
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 })),
40
47
  // * Whether mute was forced by the browser or not for autoplay
41
48
  // * Will get set to 'false' if the user has interacted with the volume
42
49
  muteWasForced: false,
@@ -48,6 +55,8 @@ export const makePlayerStore = (id) => {
48
55
  setShowControls: (showControls) => set({ showControls, controlsTimeout: undefined }),
49
56
  telemetry: true,
50
57
  setTelemetry: (telemetry) => set(() => ({ telemetry })),
58
+ ingestLive: false,
59
+ setIngestLive: (ingestLive) => set(() => ({ ingestLive })),
51
60
  playerEvent: async (url, time, eventType, meta) => set((x) => {
52
61
  const data = {
53
62
  time: time,
@@ -85,6 +94,10 @@ export const makePlayerStore = (id) => {
85
94
  let controlsTimeout = setTimeout(() => p.setShowControls(false), 1000);
86
95
  return { showControls: true, controlsTimeout };
87
96
  }),
97
+ showDebugInfo: false,
98
+ setShowDebugInfo: (showDebugInfo) => set(() => ({ showDebugInfo })),
99
+ modMessage: null,
100
+ setModMessage: (modMessage) => set(() => ({ modMessage })),
88
101
  }));
89
102
  };
90
103
  export function usePlayerContext() {
@@ -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
+ }
@@ -1,2 +1,3 @@
1
+ export * from "./stream";
1
2
  export * from "./streamplace-store";
2
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
+ }