@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.
- package/dist/components/chat/chat-box.js +109 -0
- package/dist/components/chat/chat-message.js +76 -0
- package/dist/components/chat/chat.js +56 -0
- package/dist/components/chat/mention-suggestions.js +39 -0
- package/dist/components/chat/mod-view.js +33 -0
- package/dist/components/mobile-player/fullscreen.js +69 -0
- package/dist/components/mobile-player/fullscreen.native.js +151 -0
- package/dist/components/mobile-player/player.js +103 -0
- package/dist/components/mobile-player/props.js +1 -0
- package/dist/components/mobile-player/shared.js +51 -0
- package/dist/components/mobile-player/ui/countdown.js +79 -0
- package/dist/components/mobile-player/ui/index.js +5 -0
- package/dist/components/mobile-player/ui/input.js +38 -0
- package/dist/components/mobile-player/ui/metrics.js +40 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
- package/dist/components/mobile-player/use-webrtc.js +232 -0
- package/dist/components/mobile-player/video.js +375 -0
- package/dist/components/mobile-player/video.native.js +238 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
- package/dist/components/mobile-player/webrtc-primitives.js +25 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
- package/dist/components/ui/button.js +220 -0
- package/dist/components/ui/dialog.js +203 -0
- package/dist/components/ui/dropdown.js +148 -0
- package/dist/components/ui/icons.js +22 -0
- package/dist/components/ui/index.js +22 -0
- package/dist/components/ui/input.js +202 -0
- package/dist/components/ui/loader.js +7 -0
- package/dist/components/ui/primitives/button.js +121 -0
- package/dist/components/ui/primitives/input.js +202 -0
- package/dist/components/ui/primitives/modal.js +203 -0
- package/dist/components/ui/primitives/text.js +286 -0
- package/dist/components/ui/resizeable.js +101 -0
- package/dist/components/ui/text.js +175 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/toast.js +129 -0
- package/dist/components/ui/view.js +250 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useAvatars.js +32 -0
- package/dist/hooks/useCameraToggle.js +9 -0
- package/dist/hooks/useKeyboard.js +33 -0
- package/dist/hooks/useKeyboardSlide.js +11 -0
- package/dist/hooks/useLivestreamInfo.js +62 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
- package/dist/hooks/usePlayerDimensions.js +19 -0
- package/dist/hooks/useSegmentTiming.js +62 -0
- package/dist/index.js +10 -0
- package/dist/lib/facet.js +88 -0
- package/dist/lib/theme/atoms.js +620 -0
- package/dist/lib/theme/atoms.types.js +5 -0
- package/dist/lib/theme/index.js +9 -0
- package/dist/lib/theme/theme.js +248 -0
- package/dist/lib/theme/tokens.js +383 -0
- package/dist/lib/utils.js +94 -0
- package/dist/livestream-provider/index.js +8 -3
- package/dist/livestream-store/chat.js +89 -65
- package/dist/livestream-store/index.js +1 -0
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/stream-key.js +115 -0
- package/dist/player-store/player-provider.js +0 -1
- package/dist/player-store/player-store.js +13 -0
- package/dist/streamplace-store/block.js +23 -0
- package/dist/streamplace-store/index.js +1 -0
- package/dist/streamplace-store/stream.js +193 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +20 -4
- package/src/components/chat/chat-box.tsx +195 -0
- package/src/components/chat/chat-message.tsx +192 -0
- package/src/components/chat/chat.tsx +128 -0
- package/src/components/chat/mention-suggestions.tsx +71 -0
- package/src/components/chat/mod-view.tsx +118 -0
- package/src/components/mobile-player/fullscreen.native.tsx +193 -0
- package/src/components/mobile-player/fullscreen.tsx +79 -0
- package/src/components/mobile-player/player.tsx +134 -0
- package/src/components/mobile-player/props.tsx +11 -0
- package/src/components/mobile-player/shared.tsx +56 -0
- package/src/components/mobile-player/ui/countdown.tsx +119 -0
- package/src/components/mobile-player/ui/index.ts +5 -0
- package/src/components/mobile-player/ui/input.tsx +85 -0
- package/src/components/mobile-player/ui/metrics.tsx +69 -0
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
- package/src/components/mobile-player/use-webrtc.tsx +282 -0
- package/src/components/mobile-player/video.native.tsx +360 -0
- package/src/components/mobile-player/video.tsx +557 -0
- package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
- package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
- package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
- package/src/components/ui/button.tsx +309 -0
- package/src/components/ui/dialog.tsx +376 -0
- package/src/components/ui/dropdown.tsx +399 -0
- package/src/components/ui/icons.tsx +50 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +350 -0
- package/src/components/ui/loader.tsx +9 -0
- package/src/components/ui/primitives/button.tsx +292 -0
- package/src/components/ui/primitives/input.tsx +422 -0
- package/src/components/ui/primitives/modal.tsx +421 -0
- package/src/components/ui/primitives/text.tsx +499 -0
- package/src/components/ui/resizeable.tsx +169 -0
- package/src/components/ui/text.tsx +330 -0
- package/src/components/ui/textarea.tsx +34 -0
- package/src/components/ui/toast.tsx +203 -0
- package/src/components/ui/view.tsx +344 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAvatars.tsx +44 -0
- package/src/hooks/useCameraToggle.ts +12 -0
- package/src/hooks/useKeyboard.tsx +41 -0
- package/src/hooks/useKeyboardSlide.ts +12 -0
- package/src/hooks/useLivestreamInfo.ts +67 -0
- package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
- package/src/hooks/usePlayerDimensions.ts +23 -0
- package/src/hooks/useSegmentTiming.tsx +88 -0
- package/src/index.tsx +21 -0
- package/src/lib/facet.ts +131 -0
- package/src/lib/theme/atoms.ts +760 -0
- package/src/lib/theme/atoms.types.ts +258 -0
- package/src/lib/theme/index.ts +48 -0
- package/src/lib/theme/theme.tsx +436 -0
- package/src/lib/theme/tokens.ts +409 -0
- package/src/lib/utils.ts +132 -0
- package/src/livestream-provider/index.tsx +13 -2
- package/src/livestream-store/chat.tsx +115 -78
- package/src/livestream-store/index.tsx +1 -0
- package/src/livestream-store/livestream-state.tsx +3 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/stream-key.tsx +124 -0
- package/src/player-store/player-provider.tsx +0 -1
- package/src/player-store/player-state.tsx +28 -0
- package/src/player-store/player-store.tsx +22 -0
- package/src/streamplace-store/block.tsx +29 -0
- package/src/streamplace-store/index.tsx +1 -0
- package/src/streamplace-store/stream.tsx +262 -0
- 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
|
|
17
|
+
export function WebsocketWatcher({ src }) {
|
|
18
18
|
useLivestreamWebsocket(src);
|
|
19
|
-
return _jsx(_Fragment, {
|
|
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 {
|
|
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.
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
//
|
|
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
|
|
109
|
-
const msg =
|
|
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 (
|
|
115
|
-
delete
|
|
138
|
+
if (existingLocalKey) {
|
|
139
|
+
delete newChatIndex[existingLocalKey];
|
|
140
|
+
removedKeys.add(existingLocalKey);
|
|
141
|
+
hasChanges = true;
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
167
|
+
messagesToAdd.push({ key, message: processedMessage });
|
|
168
|
+
hasChanges = true;
|
|
143
169
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
newChatList =
|
|
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:
|
|
182
|
+
chatIndex: newChatIndex,
|
|
160
183
|
chat: newChatList,
|
|
161
184
|
};
|
|
162
185
|
};
|
|
186
|
+
export const reduceChat = reduceChatIncremental;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|