@streamplace/components 0.9.7 → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/badges/live.png +0 -0
- package/assets/badges/live_2x.png +0 -0
- package/assets/badges/mod.png +0 -0
- package/assets/badges/mod_2x.png +0 -0
- package/assets/badges/vip.png +0 -0
- package/assets/badges/vip_2x.png +0 -0
- package/dist/components/chat/badge.d.ts +10 -0
- package/dist/components/chat/badge.d.ts.map +1 -0
- package/dist/components/chat/badge.js +29 -0
- package/dist/components/chat/badge.js.map +1 -0
- package/dist/components/chat/chat-box.d.ts +5 -1
- package/dist/components/chat/chat-box.d.ts.map +1 -1
- package/dist/components/chat/chat-box.js +55 -50
- package/dist/components/chat/chat-box.js.map +1 -1
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +9 -11
- package/dist/components/chat/chat-message.js.map +1 -1
- package/dist/components/chat/chat.d.ts.map +1 -1
- package/dist/components/chat/chat.js +37 -43
- package/dist/components/chat/chat.js.map +1 -1
- package/dist/components/chat/emoji-suggestions.d.ts +7 -18
- package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
- package/dist/components/chat/emoji-suggestions.js +6 -2
- package/dist/components/chat/emoji-suggestions.js.map +1 -1
- package/dist/components/chat/system-message.d.ts.map +1 -1
- package/dist/components/chat/system-message.js +9 -1
- package/dist/components/chat/system-message.js.map +1 -1
- package/dist/components/chat/teleport-modal.d.ts +9 -0
- package/dist/components/chat/teleport-modal.d.ts.map +1 -0
- package/dist/components/chat/teleport-modal.js +148 -0
- package/dist/components/chat/teleport-modal.js.map +1 -0
- package/dist/components/chat/user-profile-card.d.ts +12 -0
- package/dist/components/chat/user-profile-card.d.ts.map +1 -0
- package/dist/components/chat/user-profile-card.js +135 -0
- package/dist/components/chat/user-profile-card.js.map +1 -0
- package/dist/components/dashboard/chat-panel.d.ts +3 -1
- package/dist/components/dashboard/chat-panel.d.ts.map +1 -1
- package/dist/components/dashboard/chat-panel.js +2 -2
- package/dist/components/dashboard/chat-panel.js.map +1 -1
- package/dist/components/dashboard/header.d.ts +2 -3
- package/dist/components/dashboard/header.d.ts.map +1 -1
- package/dist/components/dashboard/header.js +6 -2
- package/dist/components/dashboard/header.js.map +1 -1
- package/dist/components/dashboard/information-widget.d.ts.map +1 -1
- package/dist/components/dashboard/information-widget.js +15 -12
- package/dist/components/dashboard/information-widget.js.map +1 -1
- package/dist/components/mobile-player/fullscreen.d.ts.map +1 -1
- package/dist/components/mobile-player/fullscreen.js +2 -1
- package/dist/components/mobile-player/fullscreen.js.map +1 -1
- package/dist/components/mobile-player/fullscreen.native.d.ts.map +1 -1
- package/dist/components/mobile-player/fullscreen.native.js +3 -2
- package/dist/components/mobile-player/fullscreen.native.js.map +1 -1
- package/dist/components/mobile-player/player.d.ts.map +1 -1
- package/dist/components/mobile-player/player.js +15 -0
- package/dist/components/mobile-player/player.js.map +1 -1
- package/dist/components/mobile-player/ui/audio-only-overlay.d.ts +2 -0
- package/dist/components/mobile-player/ui/audio-only-overlay.d.ts.map +1 -0
- package/dist/components/mobile-player/ui/audio-only-overlay.js +29 -0
- package/dist/components/mobile-player/ui/audio-only-overlay.js.map +1 -0
- package/dist/components/mobile-player/ui/index.d.ts +1 -0
- package/dist/components/mobile-player/ui/index.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/index.js +1 -0
- package/dist/components/mobile-player/ui/index.js.map +1 -1
- package/dist/components/mobile-player/ui/input.d.ts +3 -2
- package/dist/components/mobile-player/ui/input.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/input.js +18 -2
- package/dist/components/mobile-player/ui/input.js.map +1 -1
- package/dist/components/mobile-player/ui/metrics.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/metrics.js +20 -2
- package/dist/components/mobile-player/ui/metrics.js.map +1 -1
- package/dist/components/mobile-player/ui/streamer-context-menu.d.ts +3 -1
- package/dist/components/mobile-player/ui/streamer-context-menu.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/streamer-context-menu.js +64 -2
- package/dist/components/mobile-player/ui/streamer-context-menu.js.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js +29 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
- package/dist/components/mobile-player/use-webrtc.d.ts +4 -2
- package/dist/components/mobile-player/use-webrtc.d.ts.map +1 -1
- package/dist/components/mobile-player/use-webrtc.js +89 -15
- package/dist/components/mobile-player/use-webrtc.js.map +1 -1
- package/dist/components/mobile-player/video-async.native.d.ts.map +1 -1
- package/dist/components/mobile-player/video-async.native.js +15 -5
- package/dist/components/mobile-player/video-async.native.js.map +1 -1
- package/dist/components/mobile-player/video.d.ts.map +1 -1
- package/dist/components/mobile-player/video.js +10 -7
- package/dist/components/mobile-player/video.js.map +1 -1
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +8 -0
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useAQState.d.ts +2 -0
- package/dist/hooks/useAQState.d.ts.map +1 -0
- package/dist/hooks/useAQState.js +37 -0
- package/dist/hooks/useAQState.js.map +1 -0
- package/dist/hooks/useLivestreamInfo.d.ts +1 -2
- package/dist/hooks/useLivestreamInfo.d.ts.map +1 -1
- package/dist/hooks/useLivestreamInfo.js +18 -22
- package/dist/hooks/useLivestreamInfo.js.map +1 -1
- package/dist/hooks/useSegmentTiming.d.ts +1 -1
- package/dist/hooks/useSegmentTiming.d.ts.map +1 -1
- package/dist/hooks/useSegmentTiming.js +4 -0
- package/dist/hooks/useSegmentTiming.js.map +1 -1
- package/dist/i18n/i18n-loader.native.d.ts.map +1 -1
- package/dist/i18n/i18n-loader.native.js +13 -4
- package/dist/i18n/i18n-loader.native.js.map +1 -1
- package/dist/lib/slash-commands/teleport.d.ts +5 -1
- package/dist/lib/slash-commands/teleport.d.ts.map +1 -1
- package/dist/lib/slash-commands/teleport.js +57 -1
- package/dist/lib/slash-commands/teleport.js.map +1 -1
- package/dist/lib/theme/atoms.d.ts +125 -125
- package/dist/livestream-store/chat.d.ts +1 -0
- package/dist/livestream-store/chat.d.ts.map +1 -1
- package/dist/livestream-store/chat.js +10 -1
- package/dist/livestream-store/chat.js.map +1 -1
- package/dist/livestream-store/livestream-state.d.ts +2 -0
- package/dist/livestream-store/livestream-state.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.d.ts +1 -1
- package/dist/livestream-store/livestream-store.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.js +10 -1
- package/dist/livestream-store/livestream-store.js.map +1 -1
- package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
- package/dist/livestream-store/websocket-consumer.js +1 -0
- package/dist/livestream-store/websocket-consumer.js.map +1 -1
- package/dist/player-store/player-state.d.ts +3 -5
- package/dist/player-store/player-state.d.ts.map +1 -1
- package/dist/player-store/player-store.d.ts.map +1 -1
- package/dist/player-store/player-store.js +28 -5
- package/dist/player-store/player-store.js.map +1 -1
- package/dist/player-store/single-player-provider.d.ts +0 -2
- package/dist/player-store/single-player-provider.d.ts.map +1 -1
- package/dist/player-store/single-player-provider.js +0 -2
- package/dist/player-store/single-player-provider.js.map +1 -1
- package/dist/streamplace-store/branding.d.ts.map +1 -1
- package/dist/streamplace-store/branding.js +52 -1
- package/dist/streamplace-store/branding.js.map +1 -1
- package/dist/streamplace-store/stream.d.ts +4 -2
- package/dist/streamplace-store/stream.d.ts.map +1 -1
- package/dist/streamplace-store/stream.js +36 -74
- package/dist/streamplace-store/stream.js.map +1 -1
- package/locales/en-US/common.ftl +13 -1
- package/locales/manifest.json +21 -1
- package/locales/ro-RO/common.ftl +74 -0
- package/locales/ro-RO/settings.ftl +233 -0
- package/locales/zh-Hans/common.ftl +57 -0
- package/locales/zh-Hans/settings.ftl +222 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +2 -2
- package/src/components/chat/badge.tsx +45 -0
- package/src/components/chat/chat-box.tsx +84 -54
- package/src/components/chat/chat-message.tsx +25 -21
- package/src/components/chat/chat.tsx +107 -90
- package/src/components/chat/emoji-suggestions.tsx +12 -21
- package/src/components/chat/system-message.tsx +12 -2
- package/src/components/chat/teleport-modal.tsx +310 -0
- package/src/components/chat/user-profile-card.tsx +275 -0
- package/src/components/dashboard/chat-panel.tsx +8 -0
- package/src/components/dashboard/header.tsx +8 -17
- package/src/components/dashboard/information-widget.tsx +17 -10
- package/src/components/mobile-player/fullscreen.native.tsx +3 -0
- package/src/components/mobile-player/fullscreen.tsx +2 -0
- package/src/components/mobile-player/player.tsx +22 -1
- package/src/components/mobile-player/ui/audio-only-overlay.tsx +48 -0
- package/src/components/mobile-player/ui/index.ts +1 -0
- package/src/components/mobile-player/ui/input.tsx +42 -12
- package/src/components/mobile-player/ui/metrics.tsx +17 -2
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +138 -2
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +40 -3
- package/src/components/mobile-player/use-webrtc.tsx +118 -17
- package/src/components/mobile-player/video-async.native.tsx +18 -5
- package/src/components/mobile-player/video.tsx +10 -7
- package/src/components/ui/dialog.tsx +8 -0
- package/src/components/ui/textarea.tsx +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAQState.ts +37 -0
- package/src/hooks/useLivestreamInfo.ts +21 -22
- package/src/hooks/useSegmentTiming.tsx +7 -2
- package/src/i18n/i18n-loader.native.ts +9 -0
- package/src/lib/slash-commands/teleport.ts +68 -0
- package/src/livestream-store/chat.tsx +12 -0
- package/src/livestream-store/livestream-state.tsx +2 -0
- package/src/livestream-store/livestream-store.tsx +9 -1
- package/src/livestream-store/websocket-consumer.tsx +1 -0
- package/src/player-store/player-state.tsx +4 -7
- package/src/player-store/player-store.tsx +33 -7
- package/src/player-store/single-player-provider.tsx +0 -4
- package/src/streamplace-store/branding.tsx +60 -1
- package/src/streamplace-store/stream.tsx +42 -99
- package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useLivestreamStore } from "../livestream-store";
|
|
3
3
|
import { usePlayerStore } from "../player-store";
|
|
4
|
-
import { useCreateStreamRecord } from "../streamplace-store";
|
|
4
|
+
import { useCreateStreamRecord, useEndLivestream } from "../streamplace-store";
|
|
5
5
|
|
|
6
6
|
export function useLivestreamInfo(url?: string) {
|
|
7
7
|
const ingest = usePlayerStore((x) => x.ingestConnectionState);
|
|
8
8
|
const profile = useLivestreamStore((x) => x.profile);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const endLivestream = useEndLivestream();
|
|
10
|
+
const setLocalLivestreamURI = useLivestreamStore(
|
|
11
|
+
(x) => x.setLocalLivestreamURI,
|
|
12
|
+
);
|
|
13
13
|
const createStreamRecord = useCreateStreamRecord();
|
|
14
14
|
|
|
15
15
|
const [title, setTitle] = useState<string>("");
|
|
@@ -21,10 +21,11 @@ export function useLivestreamInfo(url?: string) {
|
|
|
21
21
|
if (title !== "") {
|
|
22
22
|
setRecordSubmitted(true);
|
|
23
23
|
// Create the livestream record with title and custom url if available
|
|
24
|
-
await createStreamRecord({
|
|
24
|
+
const { uri } = await createStreamRecord({
|
|
25
25
|
title,
|
|
26
26
|
canonicalUrl: url || undefined,
|
|
27
27
|
});
|
|
28
|
+
setLocalLivestreamURI(uri);
|
|
28
29
|
}
|
|
29
30
|
} catch (error) {
|
|
30
31
|
console.error("Error creating livestream:", error);
|
|
@@ -38,20 +39,19 @@ export function useLivestreamInfo(url?: string) {
|
|
|
38
39
|
keyboardHeight?: number,
|
|
39
40
|
closeKeyboard?: () => void,
|
|
40
41
|
) => {
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
42
|
+
// Optionally close keyboard if provided
|
|
43
|
+
if (closeKeyboard) closeKeyboard();
|
|
44
|
+
setShowCountdown(true);
|
|
45
|
+
// wait ~3 seconds before announcing
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
handleSubmit();
|
|
48
|
+
}, 3000);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Stop the current broadcast
|
|
52
|
+
const toggleStopStream = () => {
|
|
53
|
+
console.log("Stopping stream...");
|
|
54
|
+
endLivestream();
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
return {
|
|
@@ -63,9 +63,8 @@ export function useLivestreamInfo(url?: string) {
|
|
|
63
63
|
setShowCountdown,
|
|
64
64
|
recordSubmitted,
|
|
65
65
|
setRecordSubmitted,
|
|
66
|
-
ingestStarting,
|
|
67
|
-
setIngestStarting,
|
|
68
66
|
handleSubmit,
|
|
69
67
|
toggleGoLive,
|
|
68
|
+
toggleStopStream,
|
|
70
69
|
};
|
|
71
70
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { useLivestreamStore } from "../livestream-store";
|
|
2
|
+
import { useLivestream, useLivestreamStore } from "../livestream-store";
|
|
3
3
|
|
|
4
|
-
export type ConnectionQuality = "good" | "degraded" | "poor";
|
|
4
|
+
export type ConnectionQuality = "good" | "degraded" | "poor" | "pre-live";
|
|
5
5
|
|
|
6
6
|
function getLiveConnectionQuality(
|
|
7
7
|
timeBetweenSegments: number | null,
|
|
@@ -24,6 +24,7 @@ export function useSegmentTiming() {
|
|
|
24
24
|
const [segmentDeltas, setSegmentDeltas] = useState<number[]>([]);
|
|
25
25
|
const prevSegmentRef = useRef<any>();
|
|
26
26
|
const prevTimestampRef = useRef<number | null>(null);
|
|
27
|
+
const ls = useLivestream();
|
|
27
28
|
|
|
28
29
|
// Dummy state to force update every second
|
|
29
30
|
const [, setNow] = useState(Date.now());
|
|
@@ -84,5 +85,9 @@ export function useSegmentTiming() {
|
|
|
84
85
|
segmentDeltas.length,
|
|
85
86
|
);
|
|
86
87
|
|
|
88
|
+
if (!ls) {
|
|
89
|
+
to_ret.connectionQuality = "pre-live";
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
return to_ret;
|
|
88
93
|
}
|
|
@@ -10,6 +10,10 @@ import frFRCommon from "../../public/locales/fr-FR/common.json";
|
|
|
10
10
|
import frFRSettings from "../../public/locales/fr-FR/settings.json";
|
|
11
11
|
import ptBRCommon from "../../public/locales/pt-BR/common.json";
|
|
12
12
|
import ptBRSettings from "../../public/locales/pt-BR/settings.json";
|
|
13
|
+
import roROCommon from "../../public/locales/ro-RO/common.json";
|
|
14
|
+
import roROSettings from "../../public/locales/ro-RO/settings.json";
|
|
15
|
+
import zhHansCommon from "../../public/locales/zh-Hans/common.json";
|
|
16
|
+
import zhHansSettings from "../../public/locales/zh-Hans/settings.json";
|
|
13
17
|
import zhHantCommon from "../../public/locales/zh-Hant/common.json";
|
|
14
18
|
import zhHantSettings from "../../public/locales/zh-Hant/settings.json";
|
|
15
19
|
|
|
@@ -20,10 +24,14 @@ const translationMap: Record<string, any> = {
|
|
|
20
24
|
"pt-BR/settings": ptBRSettings,
|
|
21
25
|
"es-ES/common": esESCommon,
|
|
22
26
|
"es-ES/settings": esESSettings,
|
|
27
|
+
"zh-Hans/common": zhHansCommon,
|
|
28
|
+
"zh-Hans/settings": zhHansSettings,
|
|
23
29
|
"zh-Hant/common": zhHantCommon,
|
|
24
30
|
"zh-Hant/settings": zhHantSettings,
|
|
25
31
|
"fr-FR/common": frFRCommon,
|
|
26
32
|
"fr-FR/settings": frFRSettings,
|
|
33
|
+
"ro-RO/common": roROCommon,
|
|
34
|
+
"ro-RO/settings": roROSettings,
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
export async function loadTranslationData(
|
|
@@ -39,6 +47,7 @@ export async function loadTranslationData(
|
|
|
39
47
|
es: "es-ES",
|
|
40
48
|
zh: "zh-Hant",
|
|
41
49
|
fr: "fr-FR",
|
|
50
|
+
ro: "ro-RO",
|
|
42
51
|
}[locale] || locale;
|
|
43
52
|
|
|
44
53
|
const localeNamespaceKey = `${fullLocale}/${namespace}`;
|
|
@@ -21,16 +21,84 @@ export async function deleteTeleport(
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export async function createTeleport(
|
|
25
|
+
pdsAgent: StreamplaceAgent,
|
|
26
|
+
userDID: string,
|
|
27
|
+
targetHandle: string,
|
|
28
|
+
countdownSeconds: number,
|
|
29
|
+
setActiveTeleportUri?: (uri: string | null) => void,
|
|
30
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
31
|
+
if (countdownSeconds < 5 || countdownSeconds > 300) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: "Countdown must be between 5 seconds and 5 minutes",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let targetDID: string;
|
|
39
|
+
try {
|
|
40
|
+
const resolution = await pdsAgent.resolveHandle({
|
|
41
|
+
handle: targetHandle,
|
|
42
|
+
});
|
|
43
|
+
targetDID = resolution.data.did;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: `Could not resolve handle: ${targetHandle}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (targetDID === userDID) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: "You cannot teleport to yourself",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const startsAt = new Date(Date.now() + countdownSeconds * 1000).toISOString();
|
|
59
|
+
|
|
60
|
+
const record: PlaceStreamLiveTeleport.Record = {
|
|
61
|
+
$type: "place.stream.live.teleport",
|
|
62
|
+
streamer: targetDID,
|
|
63
|
+
startsAt,
|
|
64
|
+
countdownSeconds,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await pdsAgent.com.atproto.repo.createRecord({
|
|
69
|
+
repo: userDID,
|
|
70
|
+
collection: "place.stream.live.teleport",
|
|
71
|
+
record,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (setActiveTeleportUri) {
|
|
75
|
+
setActiveTeleportUri(result.data.uri);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { success: true };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: err instanceof Error ? err.message : "Failed to create teleport",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
24
87
|
export function registerTeleportCommand(
|
|
25
88
|
pdsAgent: StreamplaceAgent,
|
|
26
89
|
userDID: string,
|
|
27
90
|
setActiveTeleportUri?: (uri: string | null) => void,
|
|
91
|
+
onOpenModal?: () => void,
|
|
28
92
|
) {
|
|
29
93
|
const teleportHandler: SlashCommandHandler = async (
|
|
30
94
|
args,
|
|
31
95
|
rawInput,
|
|
32
96
|
): Promise<SlashCommandResult> => {
|
|
33
97
|
if (args.length === 0) {
|
|
98
|
+
if (onOpenModal) {
|
|
99
|
+
onOpenModal();
|
|
100
|
+
return { handled: true };
|
|
101
|
+
}
|
|
34
102
|
return {
|
|
35
103
|
handled: true,
|
|
36
104
|
error: "Usage: /teleport @handle.bsky.social [duration_seconds]",
|
|
@@ -155,6 +155,18 @@ export const useDeleteChatMessage = () => {
|
|
|
155
155
|
};
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
export const useAddSystemMessage = () => {
|
|
159
|
+
const store = getStoreFromContext();
|
|
160
|
+
return useCallback(
|
|
161
|
+
(message: ChatMessageViewHydrated) => {
|
|
162
|
+
const state = store.getState();
|
|
163
|
+
const newState = reduceChat(state, [message], []);
|
|
164
|
+
store.setState(newState);
|
|
165
|
+
},
|
|
166
|
+
[store],
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
158
170
|
const buildSortedChatList = (
|
|
159
171
|
chatIndex: { [key: string]: ChatMessageViewHydrated },
|
|
160
172
|
existingChatList: ChatMessageViewHydrated[],
|
|
@@ -32,6 +32,8 @@ export interface LivestreamState {
|
|
|
32
32
|
setModerationPermissions: (
|
|
33
33
|
permissions: PlaceStreamModerationPermission.Record[],
|
|
34
34
|
) => void;
|
|
35
|
+
localLivestreamURI: string | null;
|
|
36
|
+
setLocalLivestreamURI: (uri: string | null) => void;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface LivestreamProblem {
|
|
@@ -29,6 +29,8 @@ export const makeLivestreamStore = (): StoreApi<LivestreamState> => {
|
|
|
29
29
|
hasReceivedSegment: false,
|
|
30
30
|
moderationPermissions: [],
|
|
31
31
|
setModerationPermissions: (perms) => set({ moderationPermissions: perms }),
|
|
32
|
+
localLivestreamURI: null,
|
|
33
|
+
setLocalLivestreamURI: (uri) => set({ localLivestreamURI: uri }),
|
|
32
34
|
}));
|
|
33
35
|
};
|
|
34
36
|
|
|
@@ -62,7 +64,13 @@ export const useProfile = () => useLivestreamStore((x) => x.profile);
|
|
|
62
64
|
|
|
63
65
|
export const useViewers = () => useLivestreamStore((x) => x.viewers);
|
|
64
66
|
|
|
65
|
-
export const useLivestream = (
|
|
67
|
+
export const useLivestream = (includeEnded: boolean = false) =>
|
|
68
|
+
useLivestreamStore((x) => {
|
|
69
|
+
const ls = x.livestream;
|
|
70
|
+
if (!ls) return null;
|
|
71
|
+
if (!includeEnded && ls.record.endedAt !== undefined) return null;
|
|
72
|
+
return ls;
|
|
73
|
+
});
|
|
66
74
|
|
|
67
75
|
export const useSegment = () => useLivestreamStore((x) => x.segment);
|
|
68
76
|
|
|
@@ -80,6 +80,7 @@ export const handleWebSocketMessages = (
|
|
|
80
80
|
chatProfile: (message as any).chatProfile,
|
|
81
81
|
replyTo: (message as any).replyTo,
|
|
82
82
|
deleted: message.deleted,
|
|
83
|
+
badges: message.badges,
|
|
83
84
|
};
|
|
84
85
|
state = reduceChat(state, [hydrated], [], []);
|
|
85
86
|
} else if (PlaceStreamSegment.isRecord(message)) {
|
|
@@ -32,18 +32,12 @@ export interface PlayerState {
|
|
|
32
32
|
protocol: PlayerProtocol;
|
|
33
33
|
setProtocol: (protocol: PlayerProtocol) => void;
|
|
34
34
|
|
|
35
|
-
/** Source */
|
|
35
|
+
/** Source (streamer did) */
|
|
36
36
|
src: string;
|
|
37
37
|
|
|
38
38
|
/** Function to set the source URL */
|
|
39
39
|
setSrc: (src: string) => void;
|
|
40
40
|
|
|
41
|
-
/** Flag indicating if ingest (stream input) is currently starting */
|
|
42
|
-
ingestStarting: boolean;
|
|
43
|
-
|
|
44
|
-
/** Function to set the ingestStarting flag */
|
|
45
|
-
setIngestStarting: (ingestStarting: boolean) => void;
|
|
46
|
-
|
|
47
41
|
/** Flag indicating if ingest is live */
|
|
48
42
|
ingestLive: boolean;
|
|
49
43
|
setIngestLive: (ingestLive: boolean) => void;
|
|
@@ -63,6 +57,9 @@ export interface PlayerState {
|
|
|
63
57
|
ingestAutoStart?: boolean;
|
|
64
58
|
setIngestAutoStart?: (autoStart: boolean) => void;
|
|
65
59
|
|
|
60
|
+
/** stop ingest process, again with a slight delay to allow UI to update */
|
|
61
|
+
stopIngest: () => void;
|
|
62
|
+
|
|
66
63
|
/** Timestamp (number) when ingest started, or null if not started */
|
|
67
64
|
ingestStarted: number | null;
|
|
68
65
|
|
|
@@ -20,7 +20,18 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
|
|
|
20
20
|
id: id || Math.random().toString(36).slice(8),
|
|
21
21
|
selectedRendition: "source",
|
|
22
22
|
setSelectedRendition: (rendition: string) =>
|
|
23
|
-
set((state) =>
|
|
23
|
+
set((state) => {
|
|
24
|
+
if (rendition === "audio" && state.controlsTimeout) {
|
|
25
|
+
clearTimeout(state.controlsTimeout);
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
selectedRendition: rendition,
|
|
29
|
+
showControls: true,
|
|
30
|
+
controlsTimeout: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { ...state, selectedRendition: rendition };
|
|
34
|
+
}),
|
|
24
35
|
protocol: PlayerProtocol.WEBRTC,
|
|
25
36
|
setProtocol: (protocol: PlayerProtocol) =>
|
|
26
37
|
set((state) => ({ ...state, protocol: protocol })),
|
|
@@ -28,10 +39,6 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
|
|
|
28
39
|
src: "",
|
|
29
40
|
setSrc: (src: string) => set(() => ({ src })),
|
|
30
41
|
|
|
31
|
-
ingestStarting: false,
|
|
32
|
-
setIngestStarting: (ingestStarting: boolean) =>
|
|
33
|
-
set(() => ({ ingestStarting })),
|
|
34
|
-
|
|
35
42
|
ingestMediaSource: undefined,
|
|
36
43
|
setIngestMediaSource: (ingestMediaSource: IngestMediaSource | undefined) =>
|
|
37
44
|
set(() => ({ ingestMediaSource })),
|
|
@@ -45,7 +52,7 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
|
|
|
45
52
|
ingestConnectionState: RTCPeerConnectionState | null,
|
|
46
53
|
) => set(() => ({ ingestConnectionState })),
|
|
47
54
|
|
|
48
|
-
ingestAutoStart:
|
|
55
|
+
ingestAutoStart: true,
|
|
49
56
|
setIngestAutoStart: (ingestAutoStart: boolean) =>
|
|
50
57
|
set(() => ({ ingestAutoStart })),
|
|
51
58
|
|
|
@@ -53,6 +60,23 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
|
|
|
53
60
|
setIngestStarted: (timestamp: number | null) =>
|
|
54
61
|
set(() => ({ ingestStarted: timestamp })),
|
|
55
62
|
|
|
63
|
+
stopIngest: () => {
|
|
64
|
+
set(() => ({
|
|
65
|
+
ingestLive: false,
|
|
66
|
+
ingestConnectionState: "new",
|
|
67
|
+
ingestStarted: null,
|
|
68
|
+
})),
|
|
69
|
+
setTimeout(
|
|
70
|
+
() =>
|
|
71
|
+
set(() => ({
|
|
72
|
+
ingestLive: false,
|
|
73
|
+
ingestConnectionState: "new",
|
|
74
|
+
ingestStarted: null,
|
|
75
|
+
})),
|
|
76
|
+
200,
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
|
|
56
80
|
fullscreen: false,
|
|
57
81
|
setFullscreen: (isFullscreen: boolean) =>
|
|
58
82
|
set(() => ({ fullscreen: isFullscreen })),
|
|
@@ -154,10 +178,12 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
|
|
|
154
178
|
|
|
155
179
|
setUserInteraction: () =>
|
|
156
180
|
set((p) => {
|
|
157
|
-
// controls timeout
|
|
158
181
|
if (p.controlsTimeout) {
|
|
159
182
|
clearTimeout(p.controlsTimeout);
|
|
160
183
|
}
|
|
184
|
+
if (p.selectedRendition === "audio") {
|
|
185
|
+
return { showControls: true, controlsTimeout: undefined };
|
|
186
|
+
}
|
|
161
187
|
let controlsTimeout = setTimeout(() => p.setShowControls(false), 1000);
|
|
162
188
|
return { showControls: true, controlsTimeout };
|
|
163
189
|
}),
|
|
@@ -143,16 +143,12 @@ export function useCurrentPlayerRendition(): [
|
|
|
143
143
|
* Hook to get the ingest state of the current player
|
|
144
144
|
*/
|
|
145
145
|
export function useCurrentPlayerIngest(): {
|
|
146
|
-
starting: boolean;
|
|
147
|
-
setStarting: (starting: boolean) => void;
|
|
148
146
|
connectionState: RTCPeerConnectionState | null;
|
|
149
147
|
setConnectionState: (state: RTCPeerConnectionState | null) => void;
|
|
150
148
|
startedTimestamp: number | null;
|
|
151
149
|
setStartedTimestamp: (timestamp: number | null) => void;
|
|
152
150
|
} {
|
|
153
151
|
return useCurrentPlayerStore((state) => ({
|
|
154
|
-
starting: state.ingestStarting,
|
|
155
|
-
setStarting: state.setIngestStarting,
|
|
156
152
|
connectionState: state.ingestConnectionState,
|
|
157
153
|
setConnectionState: state.setIngestConnectionState,
|
|
158
154
|
startedTimestamp: state.ingestStarted,
|
|
@@ -25,11 +25,66 @@ const blobToBase64 = (blob: Blob): Promise<string> => {
|
|
|
25
25
|
});
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const PropsInHeader = [
|
|
29
|
+
"siteTitle",
|
|
30
|
+
"siteDescription",
|
|
31
|
+
"primaryColor",
|
|
32
|
+
"accentColor",
|
|
33
|
+
"defaultStreamer",
|
|
34
|
+
"mainLogo",
|
|
35
|
+
"favicon",
|
|
36
|
+
"sidebarBg",
|
|
37
|
+
"legalLinks",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function getMetaContent(key: string): BrandingAsset | null {
|
|
41
|
+
if (typeof window === "undefined" || !window.document) return null;
|
|
42
|
+
const meta = document.querySelector(`meta[name="internal-brand:${key}`);
|
|
43
|
+
if (meta && meta.getAttribute("content")) {
|
|
44
|
+
let content = meta.getAttribute("content");
|
|
45
|
+
if (content) return JSON.parse(content) as BrandingAsset;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
// hook to fetch broadcaster DID (unauthenticated)
|
|
29
52
|
export function useFetchBroadcasterDID() {
|
|
30
53
|
const streamplaceAgent = usePossiblyUnauthedPDSAgent();
|
|
31
54
|
const store = getStreamplaceStoreFromContext();
|
|
32
55
|
|
|
56
|
+
// prefetch from meta records, if on web
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (typeof window !== "undefined" && window.document) {
|
|
59
|
+
try {
|
|
60
|
+
const metaRecords = PropsInHeader.reduce(
|
|
61
|
+
(acc, key) => {
|
|
62
|
+
const meta = document.querySelector(
|
|
63
|
+
`meta[name="internal-brand:${key}`,
|
|
64
|
+
);
|
|
65
|
+
// hrmmmmmmmmmmmm
|
|
66
|
+
if (meta && meta.getAttribute("content")) {
|
|
67
|
+
let content = meta.getAttribute("content");
|
|
68
|
+
if (content) acc[key] = JSON.parse(content) as BrandingAsset;
|
|
69
|
+
}
|
|
70
|
+
return acc;
|
|
71
|
+
},
|
|
72
|
+
{} as Record<string, BrandingAsset>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
console.log("Found meta records for broadcaster DID:", metaRecords);
|
|
76
|
+
// filter out all non-text values, can get on second fetch?
|
|
77
|
+
for (const key of Object.keys(metaRecords)) {
|
|
78
|
+
if (metaRecords[key].mimeType != "text/plain") {
|
|
79
|
+
delete metaRecords[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.warn("Failed to parse broadcaster DID from meta tags", e);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
33
88
|
return useCallback(async () => {
|
|
34
89
|
try {
|
|
35
90
|
if (!streamplaceAgent) {
|
|
@@ -140,7 +195,11 @@ export function useFetchBranding() {
|
|
|
140
195
|
|
|
141
196
|
// hook to get a specific branding asset by key
|
|
142
197
|
export function useBrandingAsset(key: string): BrandingAsset | undefined {
|
|
143
|
-
return
|
|
198
|
+
return (
|
|
199
|
+
useStreamplaceStore((state) => state.branding?.[key]) ||
|
|
200
|
+
getMetaContent(key) ||
|
|
201
|
+
undefined
|
|
202
|
+
);
|
|
144
203
|
}
|
|
145
204
|
|
|
146
205
|
// convenience hook for main logo
|
|
@@ -127,111 +127,25 @@ export function useCreateStreamRecord() {
|
|
|
127
127
|
let agent = usePDSAgent();
|
|
128
128
|
let url = useUrl();
|
|
129
129
|
const uploadThumbnail = useUploadThumbnail();
|
|
130
|
-
|
|
131
130
|
return async ({
|
|
132
131
|
title,
|
|
133
132
|
customThumbnail,
|
|
134
133
|
submitPost,
|
|
135
134
|
canonicalUrl,
|
|
136
135
|
notificationSettings,
|
|
136
|
+
idleTimeoutSeconds,
|
|
137
137
|
}: {
|
|
138
138
|
title: string;
|
|
139
139
|
customThumbnail?: Blob;
|
|
140
140
|
submitPost?: boolean;
|
|
141
141
|
canonicalUrl?: string;
|
|
142
142
|
notificationSettings?: PlaceStreamLivestream.NotificationSettings;
|
|
143
|
+
idleTimeoutSeconds?: number;
|
|
143
144
|
}) => {
|
|
144
|
-
if (typeof submitPost !== "boolean") {
|
|
145
|
-
submitPost = true;
|
|
146
|
-
}
|
|
147
145
|
if (!agent) {
|
|
148
146
|
throw new Error("No PDS agent found");
|
|
149
147
|
}
|
|
150
148
|
|
|
151
|
-
if (!agent.did) {
|
|
152
|
-
throw new Error("No user DID found, assuming not logged in");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const u = new URL(url);
|
|
156
|
-
|
|
157
|
-
let thumbnail: BlobRef | undefined = undefined;
|
|
158
|
-
|
|
159
|
-
if (customThumbnail) {
|
|
160
|
-
try {
|
|
161
|
-
thumbnail = await uploadThumbnail(agent, customThumbnail);
|
|
162
|
-
} catch (e) {
|
|
163
|
-
throw new Error(`Custom thumbnail upload failed ${e}`);
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
// No custom thumbnail: fetch the server-side image and upload it
|
|
167
|
-
// try thrice lel
|
|
168
|
-
let tries = 0;
|
|
169
|
-
try {
|
|
170
|
-
for (; tries < 3; tries++) {
|
|
171
|
-
try {
|
|
172
|
-
console.log(
|
|
173
|
-
`Fetching thumbnail from ${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`,
|
|
174
|
-
);
|
|
175
|
-
const thumbnailRes = await fetch(
|
|
176
|
-
`${u.protocol}//${u.host}/api/playback/${agent.did}/stream.png`,
|
|
177
|
-
);
|
|
178
|
-
if (!thumbnailRes.ok) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`Failed to fetch thumbnail: ${thumbnailRes.status})`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
const thumbnailBlob = await thumbnailRes.blob();
|
|
184
|
-
console.log(thumbnailBlob);
|
|
185
|
-
thumbnail = await uploadThumbnail(agent, thumbnailBlob);
|
|
186
|
-
} catch (e) {
|
|
187
|
-
console.warn(
|
|
188
|
-
`Failed to fetch thumbnail, retrying (${tries + 1}/3): ${e}`,
|
|
189
|
-
);
|
|
190
|
-
// Wait 1 second before retrying
|
|
191
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
192
|
-
if (tries === 2) {
|
|
193
|
-
throw new Error(`Failed to fetch thumbnail after 3 tries: ${e}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
} catch (e) {
|
|
198
|
-
throw new Error(`Thumbnail upload failed ${e}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
let newPost: undefined | { uri: string; cid: string } = undefined;
|
|
203
|
-
|
|
204
|
-
const did = agent.did;
|
|
205
|
-
const profile = await agent.getProfile({ actor: did });
|
|
206
|
-
|
|
207
|
-
if (submitPost) {
|
|
208
|
-
if (!profile) {
|
|
209
|
-
throw new Error("No profile found for the user DID");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const params = new URLSearchParams({
|
|
213
|
-
did: did,
|
|
214
|
-
time: new Date().toISOString(),
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
let post = await buildGoLivePost(
|
|
218
|
-
title,
|
|
219
|
-
u,
|
|
220
|
-
profile.data,
|
|
221
|
-
params,
|
|
222
|
-
thumbnail,
|
|
223
|
-
agent,
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
newPost = await createNewPost(agent, post);
|
|
227
|
-
|
|
228
|
-
if (!newPost.uri || !newPost.cid) {
|
|
229
|
-
throw new Error(
|
|
230
|
-
"Cannot read properties of undefined (reading 'uri' or 'cid')",
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
149
|
let platform: string = Platform.OS;
|
|
236
150
|
let platVersion: string = Platform.Version
|
|
237
151
|
? Platform.Version.toString()
|
|
@@ -244,36 +158,50 @@ export function useCreateStreamRecord() {
|
|
|
244
158
|
) {
|
|
245
159
|
platVersion = getBrowserName(window.navigator.userAgent);
|
|
246
160
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!canonicalUrl) {
|
|
250
|
-
canonicalUrl = thisUrl;
|
|
161
|
+
if (!agent.did) {
|
|
162
|
+
throw new Error("No user DID found, assuming not logged in");
|
|
251
163
|
}
|
|
252
164
|
|
|
165
|
+
const thisUrl = `${url}/${agent.did}`;
|
|
166
|
+
|
|
253
167
|
const record: PlaceStreamLivestream.Record = {
|
|
254
168
|
$type: "place.stream.livestream",
|
|
255
169
|
title: title,
|
|
256
170
|
url: thisUrl,
|
|
257
171
|
createdAt: new Date().toISOString(),
|
|
172
|
+
lastSeenAt: new Date().toISOString(),
|
|
258
173
|
// would match up with e.g. https://stream.place/iame.li
|
|
259
174
|
canonicalUrl: canonicalUrl,
|
|
260
175
|
// user agent style string
|
|
261
176
|
// e.g. `@streamplace/components/0.1.0 (ios, 32.0)`
|
|
262
177
|
agent: `@streamplace/components/${PackageJson.version} (${platform}, ${platVersion})`,
|
|
263
|
-
|
|
264
|
-
thumb: thumbnail,
|
|
178
|
+
idleTimeoutSeconds: idleTimeoutSeconds,
|
|
265
179
|
};
|
|
266
180
|
|
|
267
181
|
if (notificationSettings) {
|
|
268
182
|
record.notificationSettings = notificationSettings;
|
|
269
183
|
}
|
|
270
184
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
185
|
+
if (customThumbnail) {
|
|
186
|
+
try {
|
|
187
|
+
const thumbnail = await uploadThumbnail(agent, customThumbnail);
|
|
188
|
+
record.thumb = thumbnail;
|
|
189
|
+
} catch (e) {
|
|
190
|
+
throw new Error(`Custom thumbnail upload failed ${e}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const output = await agent.place.stream.live.startLivestream({
|
|
195
|
+
livestream: record,
|
|
196
|
+
streamer: agent.did,
|
|
197
|
+
createBlueskyPost: submitPost,
|
|
275
198
|
});
|
|
276
|
-
|
|
199
|
+
|
|
200
|
+
if (!output.success) {
|
|
201
|
+
throw new Error("Failed to start livestream");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return output.data;
|
|
277
205
|
};
|
|
278
206
|
}
|
|
279
207
|
|
|
@@ -339,3 +267,18 @@ export function useUpdateStreamRecord(customUrl: string | null = null) {
|
|
|
339
267
|
return record;
|
|
340
268
|
};
|
|
341
269
|
}
|
|
270
|
+
|
|
271
|
+
export function useEndLivestream() {
|
|
272
|
+
let agent = usePDSAgent();
|
|
273
|
+
return async () => {
|
|
274
|
+
if (!agent) {
|
|
275
|
+
throw new Error("No PDS agent found");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!agent.did) {
|
|
279
|
+
throw new Error("No user DID found, assuming not logged in");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return await agent.place.stream.live.stopLivestream({});
|
|
283
|
+
};
|
|
284
|
+
}
|
|
Binary file
|