@streamplace/components 0.7.25 → 0.7.27
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 +0 -6
- package/dist/components/chat/chat-message.js +4 -0
- package/dist/components/mobile-player/ui/autoplay-button.js +1 -1
- package/dist/components/mobile-player/use-webrtc.js +7 -1
- package/dist/components/mobile-player/video-async.native.js +4 -4
- package/dist/components/mobile-player/video.js +3 -3
- package/dist/components/mobile-player/video.native.js +10 -1
- package/dist/crypto-polyfill.js +0 -0
- package/dist/crypto-polyfill.native.js +24 -0
- package/dist/index.js +5 -1
- package/dist/livestream-store/chat.js +9 -2
- package/dist/livestream-store/stream-key.js +1 -1
- package/dist/player-store/player-provider.js +10 -2
- package/dist/player-store/player-store.js +0 -4
- package/dist/player-store/single-player-provider.js +1 -1
- package/dist/storage/index.js +5 -0
- package/dist/storage/lock.js +40 -0
- package/dist/storage/storage.js +14 -0
- package/dist/storage/storage.native.js +44 -0
- package/dist/storage/storage.shared.js +2 -0
- package/dist/streamplace-provider/index.js +1 -0
- package/dist/streamplace-store/stream.js +1 -1
- package/dist/streamplace-store/streamplace-store.js +75 -2
- package/dist/streamplace-store/xrpc.js +10 -1
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +3 -2
- package/src/components/chat/chat-box.tsx +0 -11
- package/src/components/chat/chat-message.tsx +3 -0
- package/src/components/mobile-player/ui/autoplay-button.tsx +2 -2
- package/src/components/mobile-player/use-webrtc.tsx +10 -1
- package/src/components/mobile-player/video-async.native.tsx +6 -4
- package/src/components/mobile-player/video.native.tsx +19 -4
- package/src/components/mobile-player/video.tsx +6 -3
- package/src/crypto-polyfill.native.tsx +24 -0
- package/src/crypto-polyfill.tsx +0 -0
- package/src/index.tsx +6 -0
- package/src/livestream-store/chat.tsx +13 -3
- package/src/livestream-store/stream-key.tsx +1 -1
- package/src/player-store/player-provider.tsx +13 -1
- package/src/player-store/player-state.tsx +0 -12
- package/src/player-store/player-store.tsx +0 -8
- package/src/player-store/single-player-provider.tsx +1 -1
- package/src/storage/index.tsx +3 -0
- package/src/storage/lock.tsx +38 -0
- package/src/storage/storage.native.tsx +42 -0
- package/src/storage/storage.shared.tsx +5 -0
- package/src/storage/storage.tsx +15 -0
- package/src/streamplace-provider/index.tsx +2 -1
- package/src/streamplace-store/stream.tsx +1 -1
- package/src/streamplace-store/streamplace-store.tsx +92 -2
- package/src/streamplace-store/xrpc.tsx +9 -2
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { View } from "react-native";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
3
|
import { VideoNativeProps } from "./props";
|
|
4
4
|
|
|
5
5
|
let importPromise: Promise<typeof import("./video-async.native")> | null = null;
|
|
@@ -13,12 +13,27 @@ export default function VideoNative(props: VideoNativeProps) {
|
|
|
13
13
|
typeof import("./video-async.native") | null
|
|
14
14
|
>(null);
|
|
15
15
|
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
16
18
|
useEffect(() => {
|
|
17
|
-
importPromise
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
importPromise
|
|
20
|
+
?.then((module) => {
|
|
21
|
+
setVideoNativeModule(module);
|
|
22
|
+
})
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
setError(err.message);
|
|
25
|
+
});
|
|
20
26
|
}, []);
|
|
21
27
|
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error(error);
|
|
30
|
+
return (
|
|
31
|
+
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
32
|
+
<Text>{error}</Text>
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
if (!videoNativeModule) {
|
|
23
38
|
return <View></View>;
|
|
24
39
|
}
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
IngestMediaSource,
|
|
5
5
|
PlayerProtocol,
|
|
6
6
|
PlayerStatus,
|
|
7
|
+
useEffectiveVolume,
|
|
8
|
+
useMuted,
|
|
7
9
|
usePlayerStore,
|
|
10
|
+
useSetMuted,
|
|
8
11
|
useStreamplaceStore,
|
|
9
12
|
} from "../..";
|
|
10
13
|
import { borderRadius, colors, mt } from "../../lib/theme/atoms";
|
|
@@ -135,11 +138,11 @@ const VideoElement = forwardRef<
|
|
|
135
138
|
const x = usePlayerStore((x) => x);
|
|
136
139
|
const url = useStreamplaceStore((x) => x.url);
|
|
137
140
|
const playerEvent = usePlayerStore((x) => x.playerEvent);
|
|
138
|
-
const setMuted = usePlayerStore((x) => x.setMuted);
|
|
139
141
|
const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
|
|
140
|
-
const muted = usePlayerStore((x) => x.muted);
|
|
141
142
|
const ingest = usePlayerStore((x) => x.ingestConnectionState !== null);
|
|
142
|
-
const volume =
|
|
143
|
+
const volume = useEffectiveVolume();
|
|
144
|
+
const muted = useMuted();
|
|
145
|
+
const setMuted = useSetMuted();
|
|
143
146
|
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
144
147
|
const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
|
|
145
148
|
const setVideoRef = usePlayerStore((x) => x.setVideoRef);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// awkward use of require()? you betcha. but import() with Metro causes it to try and
|
|
2
|
+
// resolve the package at compile-time even if not installed, so here we are.
|
|
3
|
+
let rnqc: any | null = null;
|
|
4
|
+
let expoCrypto: any | null = null;
|
|
5
|
+
try {
|
|
6
|
+
rnqc = require("react-native-quick-crypto");
|
|
7
|
+
} catch (err) {}
|
|
8
|
+
try {
|
|
9
|
+
expoCrypto = require("expo-crypto");
|
|
10
|
+
} catch (err) {}
|
|
11
|
+
|
|
12
|
+
if (!rnqc && !expoCrypto) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Livestreaming requires one of react-native-quick-crypto or expo-crypto",
|
|
15
|
+
);
|
|
16
|
+
} else if (!rnqc && expoCrypto) {
|
|
17
|
+
// @atproto/crypto dependencies expect crypto.getRandomValues to be a function
|
|
18
|
+
if (typeof globalThis.crypto === "undefined") {
|
|
19
|
+
globalThis.crypto = {} as any;
|
|
20
|
+
}
|
|
21
|
+
if (typeof globalThis.crypto.getRandomValues === "undefined") {
|
|
22
|
+
globalThis.crypto.getRandomValues = expoCrypto.getRandomValues;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
File without changes
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// barrel file :)
|
|
2
|
+
import "./crypto-polyfill";
|
|
3
|
+
|
|
2
4
|
export * from "./livestream-provider";
|
|
3
5
|
export * from "./livestream-store";
|
|
4
6
|
export * from "./player-store";
|
|
@@ -37,3 +39,7 @@ export * from "./components/keep-awake";
|
|
|
37
39
|
|
|
38
40
|
// Dashboard components
|
|
39
41
|
export * as Dashboard from "./components/dashboard";
|
|
42
|
+
|
|
43
|
+
// Storage exports
|
|
44
|
+
export { default as storage } from "./storage";
|
|
45
|
+
export type { AQStorage } from "./storage/storage.shared";
|
|
@@ -75,6 +75,19 @@ export const useCreateChatMessage = () => {
|
|
|
75
75
|
const rt = new RichText({ text: msg.text });
|
|
76
76
|
await rt.detectFacets(pdsAgent);
|
|
77
77
|
|
|
78
|
+
// filter out any facets that aren't in the allowed list
|
|
79
|
+
rt.facets = rt.facets?.filter((facet) => {
|
|
80
|
+
return (
|
|
81
|
+
// if all features are in the allowed list
|
|
82
|
+
facet.features.every((feature) =>
|
|
83
|
+
[
|
|
84
|
+
"app.bsky.richtext.facet#link",
|
|
85
|
+
"app.bsky.richtext.facet#mention",
|
|
86
|
+
].includes(feature.$type),
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
78
91
|
const record: PlaceStreamChatMessage.Record = {
|
|
79
92
|
$type: "place.stream.chat.message",
|
|
80
93
|
text: msg.text,
|
|
@@ -199,12 +212,9 @@ export const reduceChatIncremental = (
|
|
|
199
212
|
let hasChanges = false;
|
|
200
213
|
const removedKeys = new Set<string>();
|
|
201
214
|
|
|
202
|
-
console.log("newMessages", newMessages);
|
|
203
|
-
|
|
204
215
|
for (const msg of newMessages) {
|
|
205
216
|
if (msg.deleted) {
|
|
206
217
|
hasChanges = true;
|
|
207
|
-
console.log("deleted", msg.uri);
|
|
208
218
|
removedKeys.add(msg.uri);
|
|
209
219
|
}
|
|
210
220
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "crypto";
|
|
2
1
|
import React, { useCallback, useMemo, useState } from "react";
|
|
3
2
|
import { StoreApi } from "zustand";
|
|
4
3
|
import { PlayerContext } from "./context";
|
|
@@ -11,6 +10,19 @@ interface PlayerProviderProps {
|
|
|
11
10
|
defaultId?: string;
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
function randomUUID(): string {
|
|
14
|
+
let dt = new Date().getTime();
|
|
15
|
+
var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
|
16
|
+
/[xy]/g,
|
|
17
|
+
function (c) {
|
|
18
|
+
var r = (dt + Math.random() * 16) % 16 | 0;
|
|
19
|
+
dt = Math.floor(dt / 16);
|
|
20
|
+
return (c == "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
return uuid;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export const PlayerProvider: React.FC<PlayerProviderProps> = ({
|
|
15
27
|
children,
|
|
16
28
|
initialPlayers = [],
|
|
@@ -69,18 +69,6 @@ export interface PlayerState {
|
|
|
69
69
|
/** Function to set the ingestStarted timestamp */
|
|
70
70
|
setIngestStarted: (timestamp: number | null) => void;
|
|
71
71
|
|
|
72
|
-
/** Player muted state */
|
|
73
|
-
muted: boolean;
|
|
74
|
-
|
|
75
|
-
/** Function to set the muted state */
|
|
76
|
-
setMuted: (isMuted: boolean) => void;
|
|
77
|
-
|
|
78
|
-
/** Player volume level (0.0 to 1.0) */
|
|
79
|
-
volume: number;
|
|
80
|
-
|
|
81
|
-
/** Function to set the volume level */
|
|
82
|
-
setVolume: (volume: number) => void;
|
|
83
|
-
|
|
84
72
|
/** Player fullscreen state */
|
|
85
73
|
fullscreen: boolean;
|
|
86
74
|
|
|
@@ -52,14 +52,6 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
|
|
|
52
52
|
setIngestStarted: (timestamp: number | null) =>
|
|
53
53
|
set(() => ({ ingestStarted: timestamp })),
|
|
54
54
|
|
|
55
|
-
muted: false,
|
|
56
|
-
setMuted: (isMuted: boolean) =>
|
|
57
|
-
set(() => ({ muted: isMuted, muteWasForced: false })),
|
|
58
|
-
|
|
59
|
-
volume: 1.0,
|
|
60
|
-
setVolume: (volume: number) =>
|
|
61
|
-
set(() => ({ volume, muteWasForced: false })),
|
|
62
|
-
|
|
63
55
|
fullscreen: false,
|
|
64
56
|
setFullscreen: (isFullscreen: boolean) =>
|
|
65
57
|
set(() => ({ fullscreen: isFullscreen })),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useContext, useMemo } from "react";
|
|
2
2
|
import { StoreApi, useStore } from "zustand";
|
|
3
|
-
import { usePlayerContext } from "../player-store";
|
|
4
3
|
import { PlayerProtocol, PlayerState } from "./player-state";
|
|
4
|
+
import { usePlayerContext } from "./player-store";
|
|
5
5
|
|
|
6
6
|
// Context for a single player
|
|
7
7
|
interface SinglePlayerContextType {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type Cont = () => void;
|
|
2
|
+
|
|
3
|
+
export class Lock {
|
|
4
|
+
private readonly queue: Cont[] = [];
|
|
5
|
+
private acquired = false;
|
|
6
|
+
|
|
7
|
+
public async acquire(): Promise<void> {
|
|
8
|
+
if (!this.acquired) {
|
|
9
|
+
this.acquired = true;
|
|
10
|
+
} else {
|
|
11
|
+
return new Promise<void>((resolve, _) => {
|
|
12
|
+
this.queue.push(resolve);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async release(): Promise<void> {
|
|
18
|
+
if (this.queue.length === 0 && this.acquired) {
|
|
19
|
+
this.acquired = false;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const continuation = this.queue.shift();
|
|
24
|
+
return new Promise((res: Cont) => {
|
|
25
|
+
continuation!();
|
|
26
|
+
res();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async critical<T>(task: () => Promise<T>) {
|
|
31
|
+
await this.acquire();
|
|
32
|
+
try {
|
|
33
|
+
return await task();
|
|
34
|
+
} finally {
|
|
35
|
+
await this.release();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Storage from "expo-sqlite/kv-store";
|
|
2
|
+
import { Lock } from "./lock";
|
|
3
|
+
import { AQStorage } from "./storage.shared";
|
|
4
|
+
|
|
5
|
+
// Needed because concurrent calls seem to return with a locked database
|
|
6
|
+
const lock = new Lock();
|
|
7
|
+
|
|
8
|
+
export default class NativeStorage implements AQStorage {
|
|
9
|
+
async getItem(key: string): Promise<string | null> {
|
|
10
|
+
return lock.critical(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const value = await Storage.getItem(key);
|
|
13
|
+
return value ?? null;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error(`error in NativeStorage.getItem: ${e}`);
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
22
|
+
return lock.critical(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await Storage.setItem(key, value);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(`error in NativeStorage.setItem: ${e}`);
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async removeItem(key: string): Promise<void> {
|
|
33
|
+
return lock.critical(async () => {
|
|
34
|
+
try {
|
|
35
|
+
await Storage.removeItem(key);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(`error in NativeStorage.removeItem: ${e}`);
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AQStorage } from "./storage.shared";
|
|
2
|
+
|
|
3
|
+
export default class WebStorage implements AQStorage {
|
|
4
|
+
async getItem(key: string): Promise<string | null> {
|
|
5
|
+
return localStorage.getItem(key);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
9
|
+
localStorage.setItem(key, value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async removeItem(key: string): Promise<void> {
|
|
13
|
+
localStorage.removeItem(key);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -11,8 +11,9 @@ export function StreamplaceProvider({
|
|
|
11
11
|
}: {
|
|
12
12
|
children: React.ReactNode;
|
|
13
13
|
url: string;
|
|
14
|
-
oauthSession?: SessionManager;
|
|
14
|
+
oauthSession?: SessionManager | null;
|
|
15
15
|
}) {
|
|
16
|
+
console.log("session in provider is", oauthSession);
|
|
16
17
|
// todo: handle url changes?
|
|
17
18
|
const store = useRef(makeStreamplaceStore({ url })).current;
|
|
18
19
|
|
|
@@ -154,7 +154,7 @@ export function useCreateStreamRecord() {
|
|
|
154
154
|
|
|
155
155
|
// Use customUrl if provided, otherwise fall back to the store URL
|
|
156
156
|
const finalUrl = customUrl || url;
|
|
157
|
-
const u = new URL(
|
|
157
|
+
const u = new URL(url);
|
|
158
158
|
|
|
159
159
|
let thumbnail: BlobRef | undefined = undefined;
|
|
160
160
|
|
|
@@ -2,6 +2,7 @@ import { SessionManager } from "@atproto/api/dist/session-manager";
|
|
|
2
2
|
import { useContext } from "react";
|
|
3
3
|
import { PlaceStreamChatProfile, PlaceStreamLivestream } from "streamplace";
|
|
4
4
|
import { createStore, StoreApi, useStore } from "zustand";
|
|
5
|
+
import storage from "../storage";
|
|
5
6
|
import { StreamplaceContext } from "../streamplace-provider/context";
|
|
6
7
|
|
|
7
8
|
// there are three categories of XRPC that we need to handle:
|
|
@@ -28,9 +29,15 @@ export interface StreamplaceState {
|
|
|
28
29
|
liveUsersRefresh: number;
|
|
29
30
|
liveUsersLoading: boolean;
|
|
30
31
|
liveUsersError: string | null;
|
|
31
|
-
oauthSession: SessionManager | null;
|
|
32
|
+
oauthSession: SessionManager | null | undefined;
|
|
32
33
|
handle: string | null;
|
|
33
34
|
chatProfile: PlaceStreamChatProfile.Record | null;
|
|
35
|
+
|
|
36
|
+
// Volume state
|
|
37
|
+
volume: number;
|
|
38
|
+
muted: boolean;
|
|
39
|
+
setVolume: (volume: number) => void;
|
|
40
|
+
setMuted: (muted: boolean) => void;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export type StreamplaceStore = StoreApi<StreamplaceState>;
|
|
@@ -40,7 +47,10 @@ export const makeStreamplaceStore = ({
|
|
|
40
47
|
}: {
|
|
41
48
|
url: string;
|
|
42
49
|
}): StoreApi<StreamplaceState> => {
|
|
43
|
-
|
|
50
|
+
const VOLUME_STORAGE_KEY = "globalVolume";
|
|
51
|
+
const MUTED_STORAGE_KEY = "globalMuted";
|
|
52
|
+
|
|
53
|
+
const store = createStore<StreamplaceState>()((set) => ({
|
|
44
54
|
url,
|
|
45
55
|
liveUsers: null,
|
|
46
56
|
setLiveUsers: (opts: {
|
|
@@ -59,7 +69,73 @@ export const makeStreamplaceStore = ({
|
|
|
59
69
|
oauthSession: null,
|
|
60
70
|
handle: null,
|
|
61
71
|
chatProfile: null,
|
|
72
|
+
|
|
73
|
+
// Volume state - start with defaults
|
|
74
|
+
volume: 1.0,
|
|
75
|
+
muted: false,
|
|
76
|
+
|
|
77
|
+
setVolume: (volume: number) => {
|
|
78
|
+
// Ensure the value is finite and within bounds
|
|
79
|
+
if (!Number.isFinite(volume)) {
|
|
80
|
+
console.warn("Invalid volume value:", volume, "- using 1.0");
|
|
81
|
+
volume = 1.0;
|
|
82
|
+
}
|
|
83
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
84
|
+
|
|
85
|
+
set({ volume: clampedVolume });
|
|
86
|
+
|
|
87
|
+
// Auto-unmute if volume > 0
|
|
88
|
+
if (clampedVolume > 0) {
|
|
89
|
+
set({ muted: false });
|
|
90
|
+
storage.setItem(MUTED_STORAGE_KEY, "false").catch(console.error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
storage
|
|
94
|
+
.setItem(VOLUME_STORAGE_KEY, clampedVolume.toString())
|
|
95
|
+
.catch(console.error);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
setMuted: (muted: boolean) => {
|
|
99
|
+
set({ muted });
|
|
100
|
+
storage.setItem(MUTED_STORAGE_KEY, muted.toString()).catch(console.error);
|
|
101
|
+
},
|
|
62
102
|
}));
|
|
103
|
+
|
|
104
|
+
// Load initial volume state from storage asynchronously
|
|
105
|
+
(async () => {
|
|
106
|
+
try {
|
|
107
|
+
const storedVolume = await storage.getItem(VOLUME_STORAGE_KEY);
|
|
108
|
+
const storedMuted = await storage.getItem(MUTED_STORAGE_KEY);
|
|
109
|
+
|
|
110
|
+
let initialVolume = 1.0;
|
|
111
|
+
let initialMuted = false;
|
|
112
|
+
|
|
113
|
+
if (storedVolume) {
|
|
114
|
+
const parsedVolume = parseFloat(storedVolume);
|
|
115
|
+
if (
|
|
116
|
+
Number.isFinite(parsedVolume) &&
|
|
117
|
+
parsedVolume >= 0 &&
|
|
118
|
+
parsedVolume <= 1
|
|
119
|
+
) {
|
|
120
|
+
initialVolume = parsedVolume;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (storedMuted) {
|
|
125
|
+
initialMuted = storedMuted === "true";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update the store with loaded values
|
|
129
|
+
store.setState({
|
|
130
|
+
volume: initialVolume,
|
|
131
|
+
muted: initialMuted,
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.warn("Failed to load volume settings from storage:", e);
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
return store;
|
|
63
139
|
};
|
|
64
140
|
|
|
65
141
|
export function getStreamplaceStoreFromContext(): StreamplaceStore {
|
|
@@ -87,3 +163,17 @@ export const useSetHandle = (): ((handle: string) => void) => {
|
|
|
87
163
|
const store = getStreamplaceStoreFromContext();
|
|
88
164
|
return (handle: string) => store.setState({ handle });
|
|
89
165
|
};
|
|
166
|
+
|
|
167
|
+
// Volume convenience hooks
|
|
168
|
+
export const useVolume = () => useStreamplaceStore((x) => x.volume);
|
|
169
|
+
export const useMuted = () => useStreamplaceStore((x) => x.muted);
|
|
170
|
+
export const useSetVolume = () => useStreamplaceStore((x) => x.setVolume);
|
|
171
|
+
export const useSetMuted = () => useStreamplaceStore((x) => x.setMuted);
|
|
172
|
+
|
|
173
|
+
// Composite hook for effective volume (0 if muted) - used by video components
|
|
174
|
+
export const useEffectiveVolume = () =>
|
|
175
|
+
useStreamplaceStore((state) => {
|
|
176
|
+
const effectiveVolume = state.muted ? 0 : state.volume;
|
|
177
|
+
// Ensure we always return a finite number for HTMLMediaElement.volume
|
|
178
|
+
return Number.isFinite(effectiveVolume) ? effectiveVolume : 1.0;
|
|
179
|
+
});
|
|
@@ -4,10 +4,17 @@ import { useStreamplaceStore } from ".";
|
|
|
4
4
|
|
|
5
5
|
export function usePDSAgent(): StreamplaceAgent | null {
|
|
6
6
|
const oauthSession = useStreamplaceStore((state) => state.oauthSession);
|
|
7
|
-
|
|
7
|
+
// oauthsession is
|
|
8
|
+
// - undefined when loading
|
|
9
|
+
// - null when logged out, and
|
|
10
|
+
// - SessionManager when logged in
|
|
8
11
|
return useMemo(() => {
|
|
9
12
|
if (!oauthSession) {
|
|
10
|
-
return null;
|
|
13
|
+
if (oauthSession === undefined) return null;
|
|
14
|
+
// TODO: change once we allow unauthed requests + profile indexing
|
|
15
|
+
// it's bluesky's AppView b/c otherwise we'd have goosewithpipe.jpg
|
|
16
|
+
// showing up everywhere
|
|
17
|
+
return new StreamplaceAgent("https://public.api.bsky.app"); // nodeUrl);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
return new StreamplaceAgent(oauthSession);
|