diff --git a/packages/app/src/Components/LiveStream/LiveEvent.tsx b/packages/app/src/Components/LiveStream/LiveEvent.tsx index 35e6534d..aba21347 100644 --- a/packages/app/src/Components/LiveStream/LiveEvent.tsx +++ b/packages/app/src/Components/LiveStream/LiveEvent.tsx @@ -11,18 +11,19 @@ import NoteAppHandler from "../Event/Note/NoteAppHandler"; import ProfileImage from "../User/ProfileImage"; const LiveKitRoom = lazy(() => import("./livekit")); - export function LiveEvent({ ev }: { ev: TaggedNostrEvent }) { const service = ev.tags.find(a => a[0] === "streaming")?.at(1); function inner() { if (service?.endsWith(".m3u8")) { - return + return ; } else if (service?.startsWith("wss+livekit://")) { - return - - + return ( + + + + ); } - return + return ; } return inner(); diff --git a/packages/app/src/Components/LiveStream/livekit.tsx b/packages/app/src/Components/LiveStream/livekit.tsx index 4de72e31..3051c01a 100644 --- a/packages/app/src/Components/LiveStream/livekit.tsx +++ b/packages/app/src/Components/LiveStream/livekit.tsx @@ -15,103 +15,117 @@ import Avatar from "../User/Avatar"; import { AvatarGroup } from "../User/AvatarGroup"; import DisplayName from "../User/DisplayName"; -export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent, canJoin?: boolean }) { - const { stream, service, id } = extractStreamInfo(ev); - const { publisher } = useEventPublisher(); - const [join, setJoin] = useState(false); - const [token, setToken] = useState(); +export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; canJoin?: boolean }) { + const { stream, service, id } = extractStreamInfo(ev); + const { publisher } = useEventPublisher(); + const [join, setJoin] = useState(false); + const [token, setToken] = useState(); - async function getToken() { - if (!service || !publisher) - return; - const url = `${service}/api/v1/nests/${id}`; - const auth = await publisher.generic(eb => { - eb.kind(EventKind.HttpAuthentication); - eb.tag(["url", url]); - eb.tag(["u", url]) - eb.tag(["method", "GET"]); - return eb; - }); - const rsp = await fetch(url, { - headers: { - authorization: `Nostr ${window.btoa(JSON.stringify(auth))}`, - } - }); + async function getToken() { + if (!service || !publisher) return; + const url = `${service}/api/v1/nests/${id}`; + const auth = await publisher.generic(eb => { + eb.kind(EventKind.HttpAuthentication); + eb.tag(["url", url]); + eb.tag(["u", url]); + eb.tag(["method", "GET"]); + return eb; + }); + const rsp = await fetch(url, { + headers: { + authorization: `Nostr ${window.btoa(JSON.stringify(auth))}`, + }, + }); - const text = await rsp.text(); - if (rsp.ok) { - return JSON.parse(text) as { token: string }; - } + const text = await rsp.text(); + if (rsp.ok) { + return JSON.parse(text) as { token: string }; } + } - useEffect(() => { - if (join && !token) { - getToken().then(t => setToken(t?.token)).catch(console.error); - } - }, [join]); - - if (!join) { - return
- - {(canJoin ?? false) && setJoin(true)}> - - } -
+ useEffect(() => { + if (join && !token) { + getToken() + .then(t => setToken(t?.token)) + .catch(console.error); } - return - - + }, [join]); + + if (!join) { + return ( +
+ + {(canJoin ?? false) && ( + setJoin(true)}> + + + )} +
+ ); + } + return ( + + + + ); } function RoomHeader({ ev }: { ev: TaggedNostrEvent }) { - const { image, title } = extractStreamInfo(ev); - return
- {image ? : -
} -
-
- {title} -
-
- -
+ const { image, title } = extractStreamInfo(ev); + return ( +
+ {image ? :
} +
+
{title}
+
+
- +
+ ); } function ParticipantList({ ev }: { ev: TaggedNostrEvent }) { - const participants = useParticipants() - return
- -

- -

-
- {participants.map(a => )} -
- + const participants = useParticipants(); + return ( +
+ +

+ +

+
+ {participants.map(a => ( + + ))} +
+ ); } function NostrParticipants({ ev }: { ev: TaggedNostrEvent }) { - const link = NostrLink.fromEvent(ev); - const sub = useMemo(() => { - const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`); - sub.withFilter().replyToLink([link]).kinds([10_312 as EventKind]).since(unixNow() - 600); - return sub; - }, [link.tagKey]); + const link = NostrLink.fromEvent(ev); + const sub = useMemo(() => { + const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`); + sub + .withFilter() + .replyToLink([link]) + .kinds([10_312 as EventKind]) + .since(unixNow() - 600); + return sub; + }, [link.tagKey]); - const presense = useRequestBuilder(sub); - return a.pubkey))} size={32} /> + const presense = useRequestBuilder(sub); + return a.pubkey))} size={32} />; } function LiveKitUser({ p }: { p: RemoteParticipant | LocalParticipant }) { - const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity - const profile = useUserProfile(pubkey); - return
- - + const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity; + const profile = useUserProfile(pubkey); + return ( +
+ +
-} \ No newline at end of file + ); +} diff --git a/packages/app/src/Components/User/AvatarGroup.tsx b/packages/app/src/Components/User/AvatarGroup.tsx index 9d6193b9..913e1752 100644 --- a/packages/app/src/Components/User/AvatarGroup.tsx +++ b/packages/app/src/Components/User/AvatarGroup.tsx @@ -3,10 +3,17 @@ import React from "react"; import ProfileImage from "@/Components/User/ProfileImage"; -export function AvatarGroup({ ids, onClick, size }: { ids: HexKey[]; onClick?: () => void, size?: number }) { +export function AvatarGroup({ ids, onClick, size }: { ids: HexKey[]; onClick?: () => void; size?: number }) { return ids.map((a, index) => (
0 ? "-ml-4" : ""}`} key={a} style={{ zIndex: ids.length - index }}> - +
)); } diff --git a/packages/app/src/Utils/stream.ts b/packages/app/src/Utils/stream.ts index 6932746b..0d384a9b 100644 --- a/packages/app/src/Utils/stream.ts +++ b/packages/app/src/Utils/stream.ts @@ -1,72 +1,71 @@ import { TaggedNostrEvent } from "@snort/system"; export function getHost(ev?: TaggedNostrEvent) { - return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? ""; + return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? ""; } export type StreamState = "live" | "ended" | "planned"; export interface StreamInfo { - id?: string; - title?: string; - summary?: string; - image?: string; - thumbnail?: string; - status?: StreamState; - stream?: string; - recording?: string; - contentWarning?: string; - tags: Array; - goal?: string; - participants?: string; - starts?: string; - ends?: string; - service?: string; - host?: string; - gameId?: string; + id?: string; + title?: string; + summary?: string; + image?: string; + thumbnail?: string; + status?: StreamState; + stream?: string; + recording?: string; + contentWarning?: string; + tags: Array; + goal?: string; + participants?: string; + starts?: string; + ends?: string; + service?: string; + host?: string; + gameId?: string; } const gameTagFormat = /^[a-z-]+:[a-z0-9-]+$/i; export function extractStreamInfo(ev?: TaggedNostrEvent) { - const ret = { - host: getHost(ev), - } as StreamInfo; - const matchTag = (tag: Array, k: string, into: (v: string) => void) => { - if (tag[0] === k) { - into(tag[1]); - } - }; - - for (const t of ev?.tags ?? []) { - matchTag(t, "d", v => (ret.id = v)); - matchTag(t, "title", v => (ret.title = v)); - matchTag(t, "summary", v => (ret.summary = v)); - matchTag(t, "image", v => (ret.image = v)); - matchTag(t, "thumbnail", v => (ret.thumbnail = v)); - matchTag(t, "status", v => (ret.status = v as StreamState)); - if (t[0] === "streaming") { - matchTag(t, "streaming", v => (ret.stream = v)); - } - matchTag(t, "recording", v => (ret.recording = v)); - matchTag(t, "url", v => (ret.recording = v)); - matchTag(t, "content-warning", v => (ret.contentWarning = v)); - matchTag(t, "current_participants", v => (ret.participants = v)); - matchTag(t, "goal", v => (ret.goal = v)); - matchTag(t, "starts", v => (ret.starts = v)); - matchTag(t, "ends", v => (ret.ends = v)); - matchTag(t, "service", v => (ret.service = v)); + const ret = { + host: getHost(ev), + } as StreamInfo; + const matchTag = (tag: Array, k: string, into: (v: string) => void) => { + if (tag[0] === k) { + into(tag[1]); } - const { regularTags } = sortStreamTags(ev?.tags ?? []); - ret.tags = regularTags; + }; - return ret; + for (const t of ev?.tags ?? []) { + matchTag(t, "d", v => (ret.id = v)); + matchTag(t, "title", v => (ret.title = v)); + matchTag(t, "summary", v => (ret.summary = v)); + matchTag(t, "image", v => (ret.image = v)); + matchTag(t, "thumbnail", v => (ret.thumbnail = v)); + matchTag(t, "status", v => (ret.status = v as StreamState)); + if (t[0] === "streaming") { + matchTag(t, "streaming", v => (ret.stream = v)); + } + matchTag(t, "recording", v => (ret.recording = v)); + matchTag(t, "url", v => (ret.recording = v)); + matchTag(t, "content-warning", v => (ret.contentWarning = v)); + matchTag(t, "current_participants", v => (ret.participants = v)); + matchTag(t, "goal", v => (ret.goal = v)); + matchTag(t, "starts", v => (ret.starts = v)); + matchTag(t, "ends", v => (ret.ends = v)); + matchTag(t, "service", v => (ret.service = v)); + } + const { regularTags } = sortStreamTags(ev?.tags ?? []); + ret.tags = regularTags; + + return ret; } - export function sortStreamTags(tags: Array>) { - const plainTags = tags.filter(a => (Array.isArray(a) ? a[0] === "t" : true)).map(a => (Array.isArray(a) ? a[1] : a)); + const plainTags = tags.filter(a => (Array.isArray(a) ? a[0] === "t" : true)).map(a => (Array.isArray(a) ? a[1] : a)); - const regularTags = plainTags.filter(a => !a.match(gameTagFormat)) ?? []; - const prefixedTags = plainTags.filter(a => !regularTags.includes(a)); - return { regularTags, prefixedTags }; -} \ No newline at end of file + const regularTags = plainTags.filter(a => !a.match(gameTagFormat)) ?? []; + const prefixedTags = plainTags.filter(a => !regularTags.includes(a)); + return { regularTags, prefixedTags }; +}