@livo-tv/sdk 1.3.2 → 1.4.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/README.md CHANGED
@@ -19,9 +19,12 @@ Server (Node / Workers):
19
19
  import { createLivoServerClient } from "@livo-tv/sdk/server";
20
20
 
21
21
  const livo = createLivoServerClient({ apiKey: process.env.LIVO_API_KEY! });
22
- const { hostUrl, guestUrl } = await livo.mintHostSession(streamId, {
23
- displayName: "Host",
24
- });
22
+ const { hostUrl, guestUrl, hostToken, guestToken } = await livo.mintHostSession(
23
+ streamId,
24
+ {
25
+ displayName: "Host",
26
+ },
27
+ );
25
28
  ```
26
29
 
27
30
  GitHub `livo-tv/sdk` + npm OIDC publish are a human handoff (same as `@livo-tv/blocks`).
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme.ts","../src/urls.ts","../src/server.ts"],"names":[],"mappings":"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","file":"server.cjs","sourcesContent":["export type EmbedTheme = {\n\tprimary?: string;\n\tbackground?: string;\n\tforeground?: string;\n\tradius?: string;\n\tfontFamily?: string;\n\tmode?: \"light\" | \"dark\";\n};\n\nconst HEX =\n\t/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\nconst COLOR_FN = /^(?:oklch|rgb|rgba|hsl|hsla)\\([0-9a-zA-Z.\\s%,./-]+\\)$/;\nconst RADIUS = /^\\d+(?:\\.\\d+)?(?:px|rem|em)$/;\nconst FONT = /^[a-zA-Z0-9\\s,\"'-]{1,120}$/;\n\nfunction sanitizeColor(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\tif (HEX.test(trimmed) || COLOR_FN.test(trimmed)) return trimmed;\n\treturn undefined;\n}\n\nexport function sanitizeEmbedTheme(value: unknown): EmbedTheme | null {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn null;\n\t}\n\tconst row = value as Record<string, unknown>;\n\tconst theme: EmbedTheme = {};\n\tconst primary = sanitizeColor(row.primary);\n\tconst background = sanitizeColor(row.background);\n\tconst foreground = sanitizeColor(row.foreground);\n\tif (typeof row.radius === \"string\" && RADIUS.test(row.radius.trim())) {\n\t\ttheme.radius = row.radius.trim();\n\t}\n\tif (typeof row.fontFamily === \"string\" && FONT.test(row.fontFamily.trim())) {\n\t\ttheme.fontFamily = row.fontFamily.trim();\n\t}\n\tif (primary) theme.primary = primary;\n\tif (background) theme.background = background;\n\tif (foreground) theme.foreground = foreground;\n\tif (row.mode === \"light\" || row.mode === \"dark\") theme.mode = row.mode;\n\treturn Object.keys(theme).length > 0 ? theme : null;\n}\n\nexport function encodeEmbedTheme(theme: EmbedTheme): string {\n\tconst json = JSON.stringify(theme);\n\tconst bytes = new TextEncoder().encode(json);\n\tlet binary = \"\";\n\tfor (const byte of bytes) binary += String.fromCharCode(byte);\n\treturn btoa(binary)\n\t\t.replace(/\\+/g, \"-\")\n\t\t.replace(/\\//g, \"_\")\n\t\t.replace(/=+$/, \"\");\n}\n\nexport function embedThemeCssVars(theme: EmbedTheme): Record<string, string> {\n\tconst vars: Record<string, string> = {};\n\tif (theme.primary) {\n\t\tvars[\"--livo-primary\"] = theme.primary;\n\t\tvars[\"--primary\"] = theme.primary;\n\t}\n\tif (theme.background) {\n\t\tvars[\"--livo-background\"] = theme.background;\n\t\tvars[\"--background\"] = theme.background;\n\t}\n\tif (theme.foreground) {\n\t\tvars[\"--livo-foreground\"] = theme.foreground;\n\t\tvars[\"--foreground\"] = theme.foreground;\n\t}\n\tif (theme.radius) {\n\t\tvars[\"--livo-radius\"] = theme.radius;\n\t\tvars[\"--radius\"] = theme.radius;\n\t}\n\tif (theme.fontFamily) vars[\"--font-sans\"] = theme.fontFamily;\n\treturn vars;\n}\n\nexport function decodeEmbedTheme(\n\traw: string | null | undefined,\n): EmbedTheme | null {\n\tif (!raw) return null;\n\ttry {\n\t\tconst padded = raw.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\t\tconst pad =\n\t\t\tpadded.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (padded.length % 4));\n\t\tconst binary = atob(padded + pad);\n\t\tconst bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n\t\treturn sanitizeEmbedTheme(JSON.parse(new TextDecoder().decode(bytes)));\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function applyEmbedTheme(\n\troot: HTMLElement,\n\ttheme: EmbedTheme | null,\n): void {\n\tif (!theme) return;\n\tif (theme.mode === \"dark\") root.classList.add(\"dark\");\n\tif (theme.mode === \"light\") root.classList.remove(\"dark\");\n\tfor (const [key, value] of Object.entries(embedThemeCssVars(theme))) {\n\t\troot.style.setProperty(key, value);\n\t}\n\tif (theme.fontFamily) root.style.fontFamily = theme.fontFamily;\n}\n","import { encodeEmbedTheme, type EmbedTheme } from \"./theme\";\n\nexport type UrlOrigins = {\n\tplayerOrigin?: string;\n\tappOrigin?: string;\n};\n\nfunction trimSlash(value: string): string {\n\treturn value.replace(/\\/$/, \"\");\n}\n\nexport function playerUrl(\n\tstreamId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/${streamId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function vodPlayerUrl(\n\tvodId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/vod/${vodId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioGuestUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/join/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioHostUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/host/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n","export type LivoServerClientOptions = {\n\tapiUrl?: string;\n\tapiKey: string;\n\tfetchFn?: typeof fetch;\n};\n\nexport class LivoServerClient {\n\tprivate readonly apiUrl: string;\n\tprivate readonly apiKey: string;\n\tprivate readonly fetchFn: typeof fetch;\n\n\tconstructor(options: LivoServerClientOptions) {\n\t\tthis.apiUrl = (options.apiUrl ?? \"https://media-svc.livo.tv\").replace(\n\t\t\t/\\/$/,\n\t\t\t\"\",\n\t\t);\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t}\n\n\tprivate async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n\t\tconst headers = new Headers(init.headers);\n\t\theaders.set(\"X-Api-Key\", this.apiKey);\n\t\theaders.set(\"accept\", \"application/json\");\n\t\tif (init.body && !headers.has(\"content-type\")) {\n\t\t\theaders.set(\"content-type\", \"application/json\");\n\t\t}\n\t\tconst res = await this.fetchFn(`${this.apiUrl}${path}`, {\n\t\t\t...init,\n\t\t\theaders,\n\t\t});\n\t\tconst body: unknown = await res.json().catch(() => ({}));\n\t\tif (!res.ok) {\n\t\t\tconst error = new Error(`Livo API ${res.status}`);\n\t\t\t(error as Error & { status: number; body: unknown }).status = res.status;\n\t\t\t(error as Error & { status: number; body: unknown }).body = body;\n\t\t\tthrow error;\n\t\t}\n\t\treturn body as T;\n\t}\n\n\tcreateStream(input: {\n\t\ttitle: string;\n\t\tmode?: \"instant\" | \"scheduled\";\n\t\tsource?: \"rtmp\" | \"studio\";\n\t\tstartsAt?: number;\n\t\tdurationSeconds?: number;\n\t\tcurtainId?: string;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{ stream: { id: string } }>(\"/streams\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetStream(id: string) {\n\t\treturn this.request<{ stream: { id: string; status: string } }>(\n\t\t\t`/streams/${encodeURIComponent(id)}`,\n\t\t);\n\t}\n\n\tstartStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/start`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpublishStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/publish`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpatchStream(\n\t\tid: string,\n\t\tinput: {\n\t\t\ttitle?: string;\n\t\t\tstartsAt?: number | null;\n\t\t\tdurationSeconds?: number | null;\n\t\t\tcurtainId?: string | null;\n\t\t\tcaptionsEnabled?: boolean;\n\t\t},\n\t) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}`, {\n\t\t\tmethod: \"PATCH\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tstopStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/stop`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistVods() {\n\t\treturn this.request<{ vods: Array<{ id: string; title: string }> }>(\n\t\t\t\"/vods\",\n\t\t);\n\t}\n\n\tcreateVod(input: {\n\t\tfilename: string;\n\t\tcontentType: string;\n\t\tbyteSize: number;\n\t\tsha256: string;\n\t\ttitle?: string;\n\t\treleaseAt?: number | null;\n\t\tcurtainId?: string | null;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{\n\t\t\tvodId: string;\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/vods\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\trequestVodPartUrl(vodId: string, partNumber: number) {\n\t\treturn this.request<{ uploadUrl: string }>(\n\t\t\t`/vods/${encodeURIComponent(vodId)}/upload-part`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: JSON.stringify({ partNumber }),\n\t\t\t},\n\t\t);\n\t}\n\n\tcompleteVodUpload(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/complete-upload`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\ttranscodeVod(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/transcode`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tenableVodCaptions(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/captions`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistCurtains() {\n\t\treturn this.request<{ curtains: Array<{ id: string; title: string }> }>(\n\t\t\t\"/curtains\",\n\t\t);\n\t}\n\n\tcreateCurtain(input: Record<string, unknown>) {\n\t\treturn this.request<{\n\t\t\tcurtain: { id: string };\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/curtains\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetOrganizationSettings() {\n\t\treturn this.request<{\n\t\t\tdefaultCurtainId: string | null;\n\t\t\twebhookUrl: string | null;\n\t\t}>(\"/organization/settings\");\n\t}\n\n\tputOrganizationSettings(input: {\n\t\tdefaultCurtainId?: string | null;\n\t\twebhookUrl?: string | null;\n\t\twebhookSecret?: string | null;\n\t}) {\n\t\treturn this.request(\"/organization/settings\", {\n\t\t\tmethod: \"PUT\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tmintHostSession(\n\t\tstreamId: string,\n\t\tinput: { displayName: string; picture?: string },\n\t) {\n\t\treturn this.request<{\n\t\t\thostUrl: string;\n\t\t\tguestUrl: string;\n\t\t\texpiresAt: number;\n\t\t}>(`/streams/${encodeURIComponent(streamId)}/studio/host-session`, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n}\n\nexport function createLivoServerClient(options: LivoServerClientOptions) {\n\treturn new LivoServerClient(options);\n}\n\nexport { studioGuestUrl as getGuestLink } from \"./urls\";\n"]}
1
+ {"version":3,"sources":["../src/theme.ts","../src/urls.ts","../src/server.ts"],"names":[],"mappings":"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","file":"server.cjs","sourcesContent":["export type EmbedTheme = {\n\tprimary?: string;\n\tbackground?: string;\n\tforeground?: string;\n\tradius?: string;\n\tfontFamily?: string;\n\tmode?: \"light\" | \"dark\";\n};\n\nconst HEX =\n\t/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\nconst COLOR_FN = /^(?:oklch|rgb|rgba|hsl|hsla)\\([0-9a-zA-Z.\\s%,./-]+\\)$/;\nconst RADIUS = /^\\d+(?:\\.\\d+)?(?:px|rem|em)$/;\nconst FONT = /^[a-zA-Z0-9\\s,\"'-]{1,120}$/;\n\nfunction sanitizeColor(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\tif (HEX.test(trimmed) || COLOR_FN.test(trimmed)) return trimmed;\n\treturn undefined;\n}\n\nexport function sanitizeEmbedTheme(value: unknown): EmbedTheme | null {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn null;\n\t}\n\tconst row = value as Record<string, unknown>;\n\tconst theme: EmbedTheme = {};\n\tconst primary = sanitizeColor(row.primary);\n\tconst background = sanitizeColor(row.background);\n\tconst foreground = sanitizeColor(row.foreground);\n\tif (typeof row.radius === \"string\" && RADIUS.test(row.radius.trim())) {\n\t\ttheme.radius = row.radius.trim();\n\t}\n\tif (typeof row.fontFamily === \"string\" && FONT.test(row.fontFamily.trim())) {\n\t\ttheme.fontFamily = row.fontFamily.trim();\n\t}\n\tif (primary) theme.primary = primary;\n\tif (background) theme.background = background;\n\tif (foreground) theme.foreground = foreground;\n\tif (row.mode === \"light\" || row.mode === \"dark\") theme.mode = row.mode;\n\treturn Object.keys(theme).length > 0 ? theme : null;\n}\n\nexport function encodeEmbedTheme(theme: EmbedTheme): string {\n\tconst json = JSON.stringify(theme);\n\tconst bytes = new TextEncoder().encode(json);\n\tlet binary = \"\";\n\tfor (const byte of bytes) binary += String.fromCharCode(byte);\n\treturn btoa(binary)\n\t\t.replace(/\\+/g, \"-\")\n\t\t.replace(/\\//g, \"_\")\n\t\t.replace(/=+$/, \"\");\n}\n\nexport function embedThemeCssVars(theme: EmbedTheme): Record<string, string> {\n\tconst vars: Record<string, string> = {};\n\tif (theme.primary) {\n\t\tvars[\"--livo-primary\"] = theme.primary;\n\t\tvars[\"--primary\"] = theme.primary;\n\t}\n\tif (theme.background) {\n\t\tvars[\"--livo-background\"] = theme.background;\n\t\tvars[\"--background\"] = theme.background;\n\t}\n\tif (theme.foreground) {\n\t\tvars[\"--livo-foreground\"] = theme.foreground;\n\t\tvars[\"--foreground\"] = theme.foreground;\n\t}\n\tif (theme.radius) {\n\t\tvars[\"--livo-radius\"] = theme.radius;\n\t\tvars[\"--radius\"] = theme.radius;\n\t}\n\tif (theme.fontFamily) vars[\"--font-sans\"] = theme.fontFamily;\n\treturn vars;\n}\n\nexport function decodeEmbedTheme(\n\traw: string | null | undefined,\n): EmbedTheme | null {\n\tif (!raw) return null;\n\ttry {\n\t\tconst padded = raw.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\t\tconst pad =\n\t\t\tpadded.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (padded.length % 4));\n\t\tconst binary = atob(padded + pad);\n\t\tconst bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n\t\treturn sanitizeEmbedTheme(JSON.parse(new TextDecoder().decode(bytes)));\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function applyEmbedTheme(\n\troot: HTMLElement,\n\ttheme: EmbedTheme | null,\n): void {\n\tif (!theme) return;\n\tif (theme.mode === \"dark\") root.classList.add(\"dark\");\n\tif (theme.mode === \"light\") root.classList.remove(\"dark\");\n\tfor (const [key, value] of Object.entries(embedThemeCssVars(theme))) {\n\t\troot.style.setProperty(key, value);\n\t}\n\tif (theme.fontFamily) root.style.fontFamily = theme.fontFamily;\n}\n","import { encodeEmbedTheme, type EmbedTheme } from \"./theme\";\n\nexport type UrlOrigins = {\n\tplayerOrigin?: string;\n\tappOrigin?: string;\n};\n\nfunction trimSlash(value: string): string {\n\treturn value.replace(/\\/$/, \"\");\n}\n\nexport function playerUrl(\n\tstreamId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/${streamId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function vodPlayerUrl(\n\tvodId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/vod/${vodId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioGuestUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/join/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioHostUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/host/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n","export type LivoServerClientOptions = {\n\tapiUrl?: string;\n\tapiKey: string;\n\tfetchFn?: typeof fetch;\n};\n\nexport class LivoServerClient {\n\tprivate readonly apiUrl: string;\n\tprivate readonly apiKey: string;\n\tprivate readonly fetchFn: typeof fetch;\n\n\tconstructor(options: LivoServerClientOptions) {\n\t\tthis.apiUrl = (options.apiUrl ?? \"https://media-svc.livo.tv\").replace(\n\t\t\t/\\/$/,\n\t\t\t\"\",\n\t\t);\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t}\n\n\tprivate async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n\t\tconst headers = new Headers(init.headers);\n\t\theaders.set(\"X-Api-Key\", this.apiKey);\n\t\theaders.set(\"accept\", \"application/json\");\n\t\tif (init.body && !headers.has(\"content-type\")) {\n\t\t\theaders.set(\"content-type\", \"application/json\");\n\t\t}\n\t\tconst res = await this.fetchFn(`${this.apiUrl}${path}`, {\n\t\t\t...init,\n\t\t\theaders,\n\t\t});\n\t\tconst body: unknown = await res.json().catch(() => ({}));\n\t\tif (!res.ok) {\n\t\t\tconst error = new Error(`Livo API ${res.status}`);\n\t\t\t(error as Error & { status: number; body: unknown }).status = res.status;\n\t\t\t(error as Error & { status: number; body: unknown }).body = body;\n\t\t\tthrow error;\n\t\t}\n\t\treturn body as T;\n\t}\n\n\tcreateStream(input: {\n\t\ttitle: string;\n\t\tmode?: \"instant\" | \"scheduled\";\n\t\tsource?: \"rtmp\" | \"studio\";\n\t\tstartsAt?: number;\n\t\tdurationSeconds?: number;\n\t\tcurtainId?: string;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{ stream: { id: string } }>(\"/streams\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetStream(id: string) {\n\t\treturn this.request<{ stream: { id: string; status: string } }>(\n\t\t\t`/streams/${encodeURIComponent(id)}`,\n\t\t);\n\t}\n\n\tstartStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/start`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpublishStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/publish`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpatchStream(\n\t\tid: string,\n\t\tinput: {\n\t\t\ttitle?: string;\n\t\t\tstartsAt?: number | null;\n\t\t\tdurationSeconds?: number | null;\n\t\t\tcurtainId?: string | null;\n\t\t\tcaptionsEnabled?: boolean;\n\t\t},\n\t) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}`, {\n\t\t\tmethod: \"PATCH\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tstopStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/stop`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistVods() {\n\t\treturn this.request<{ vods: Array<{ id: string; title: string }> }>(\n\t\t\t\"/vods\",\n\t\t);\n\t}\n\n\tcreateVod(input: {\n\t\tfilename: string;\n\t\tcontentType: string;\n\t\tbyteSize: number;\n\t\tsha256: string;\n\t\ttitle?: string;\n\t\treleaseAt?: number | null;\n\t\tcurtainId?: string | null;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{\n\t\t\tvodId: string;\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/vods\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\trequestVodPartUrl(vodId: string, partNumber: number) {\n\t\treturn this.request<{ uploadUrl: string }>(\n\t\t\t`/vods/${encodeURIComponent(vodId)}/upload-part`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: JSON.stringify({ partNumber }),\n\t\t\t},\n\t\t);\n\t}\n\n\tcompleteVodUpload(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/complete-upload`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\ttranscodeVod(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/transcode`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tenableVodCaptions(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/captions`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistCurtains() {\n\t\treturn this.request<{ curtains: Array<{ id: string; title: string }> }>(\n\t\t\t\"/curtains\",\n\t\t);\n\t}\n\n\tcreateCurtain(input: Record<string, unknown>) {\n\t\treturn this.request<{\n\t\t\tcurtain: { id: string };\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/curtains\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetOrganizationSettings() {\n\t\treturn this.request<{\n\t\t\tdefaultCurtainId: string | null;\n\t\t\twebhookUrl: string | null;\n\t\t}>(\"/organization/settings\");\n\t}\n\n\tputOrganizationSettings(input: {\n\t\tdefaultCurtainId?: string | null;\n\t\twebhookUrl?: string | null;\n\t\twebhookSecret?: string | null;\n\t}) {\n\t\treturn this.request(\"/organization/settings\", {\n\t\t\tmethod: \"PUT\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tmintHostSession(\n\t\tstreamId: string,\n\t\tinput: { displayName: string; picture?: string },\n\t) {\n\t\treturn this.request<{\n\t\t\thostUrl: string;\n\t\t\tguestUrl: string;\n\t\t\thostToken: string;\n\t\t\tguestToken: string;\n\t\t\texpiresAt: number;\n\t\t}>(`/streams/${encodeURIComponent(streamId)}/studio/host-session`, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n}\n\nexport function createLivoServerClient(options: LivoServerClientOptions) {\n\treturn new LivoServerClient(options);\n}\n\nexport { studioGuestUrl as getGuestLink } from \"./urls\";\n"]}
package/dist/server.d.cts CHANGED
@@ -95,6 +95,8 @@ declare class LivoServerClient {
95
95
  }): Promise<{
96
96
  hostUrl: string;
97
97
  guestUrl: string;
98
+ hostToken: string;
99
+ guestToken: string;
98
100
  expiresAt: number;
99
101
  }>;
100
102
  }
package/dist/server.d.ts CHANGED
@@ -95,6 +95,8 @@ declare class LivoServerClient {
95
95
  }): Promise<{
96
96
  hostUrl: string;
97
97
  guestUrl: string;
98
+ hostToken: string;
99
+ guestToken: string;
98
100
  expiresAt: number;
99
101
  }>;
100
102
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme.ts","../src/urls.ts","../src/server.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["export type EmbedTheme = {\n\tprimary?: string;\n\tbackground?: string;\n\tforeground?: string;\n\tradius?: string;\n\tfontFamily?: string;\n\tmode?: \"light\" | \"dark\";\n};\n\nconst HEX =\n\t/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\nconst COLOR_FN = /^(?:oklch|rgb|rgba|hsl|hsla)\\([0-9a-zA-Z.\\s%,./-]+\\)$/;\nconst RADIUS = /^\\d+(?:\\.\\d+)?(?:px|rem|em)$/;\nconst FONT = /^[a-zA-Z0-9\\s,\"'-]{1,120}$/;\n\nfunction sanitizeColor(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\tif (HEX.test(trimmed) || COLOR_FN.test(trimmed)) return trimmed;\n\treturn undefined;\n}\n\nexport function sanitizeEmbedTheme(value: unknown): EmbedTheme | null {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn null;\n\t}\n\tconst row = value as Record<string, unknown>;\n\tconst theme: EmbedTheme = {};\n\tconst primary = sanitizeColor(row.primary);\n\tconst background = sanitizeColor(row.background);\n\tconst foreground = sanitizeColor(row.foreground);\n\tif (typeof row.radius === \"string\" && RADIUS.test(row.radius.trim())) {\n\t\ttheme.radius = row.radius.trim();\n\t}\n\tif (typeof row.fontFamily === \"string\" && FONT.test(row.fontFamily.trim())) {\n\t\ttheme.fontFamily = row.fontFamily.trim();\n\t}\n\tif (primary) theme.primary = primary;\n\tif (background) theme.background = background;\n\tif (foreground) theme.foreground = foreground;\n\tif (row.mode === \"light\" || row.mode === \"dark\") theme.mode = row.mode;\n\treturn Object.keys(theme).length > 0 ? theme : null;\n}\n\nexport function encodeEmbedTheme(theme: EmbedTheme): string {\n\tconst json = JSON.stringify(theme);\n\tconst bytes = new TextEncoder().encode(json);\n\tlet binary = \"\";\n\tfor (const byte of bytes) binary += String.fromCharCode(byte);\n\treturn btoa(binary)\n\t\t.replace(/\\+/g, \"-\")\n\t\t.replace(/\\//g, \"_\")\n\t\t.replace(/=+$/, \"\");\n}\n\nexport function embedThemeCssVars(theme: EmbedTheme): Record<string, string> {\n\tconst vars: Record<string, string> = {};\n\tif (theme.primary) {\n\t\tvars[\"--livo-primary\"] = theme.primary;\n\t\tvars[\"--primary\"] = theme.primary;\n\t}\n\tif (theme.background) {\n\t\tvars[\"--livo-background\"] = theme.background;\n\t\tvars[\"--background\"] = theme.background;\n\t}\n\tif (theme.foreground) {\n\t\tvars[\"--livo-foreground\"] = theme.foreground;\n\t\tvars[\"--foreground\"] = theme.foreground;\n\t}\n\tif (theme.radius) {\n\t\tvars[\"--livo-radius\"] = theme.radius;\n\t\tvars[\"--radius\"] = theme.radius;\n\t}\n\tif (theme.fontFamily) vars[\"--font-sans\"] = theme.fontFamily;\n\treturn vars;\n}\n\nexport function decodeEmbedTheme(\n\traw: string | null | undefined,\n): EmbedTheme | null {\n\tif (!raw) return null;\n\ttry {\n\t\tconst padded = raw.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\t\tconst pad =\n\t\t\tpadded.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (padded.length % 4));\n\t\tconst binary = atob(padded + pad);\n\t\tconst bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n\t\treturn sanitizeEmbedTheme(JSON.parse(new TextDecoder().decode(bytes)));\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function applyEmbedTheme(\n\troot: HTMLElement,\n\ttheme: EmbedTheme | null,\n): void {\n\tif (!theme) return;\n\tif (theme.mode === \"dark\") root.classList.add(\"dark\");\n\tif (theme.mode === \"light\") root.classList.remove(\"dark\");\n\tfor (const [key, value] of Object.entries(embedThemeCssVars(theme))) {\n\t\troot.style.setProperty(key, value);\n\t}\n\tif (theme.fontFamily) root.style.fontFamily = theme.fontFamily;\n}\n","import { encodeEmbedTheme, type EmbedTheme } from \"./theme\";\n\nexport type UrlOrigins = {\n\tplayerOrigin?: string;\n\tappOrigin?: string;\n};\n\nfunction trimSlash(value: string): string {\n\treturn value.replace(/\\/$/, \"\");\n}\n\nexport function playerUrl(\n\tstreamId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/${streamId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function vodPlayerUrl(\n\tvodId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/vod/${vodId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioGuestUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/join/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioHostUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/host/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n","export type LivoServerClientOptions = {\n\tapiUrl?: string;\n\tapiKey: string;\n\tfetchFn?: typeof fetch;\n};\n\nexport class LivoServerClient {\n\tprivate readonly apiUrl: string;\n\tprivate readonly apiKey: string;\n\tprivate readonly fetchFn: typeof fetch;\n\n\tconstructor(options: LivoServerClientOptions) {\n\t\tthis.apiUrl = (options.apiUrl ?? \"https://media-svc.livo.tv\").replace(\n\t\t\t/\\/$/,\n\t\t\t\"\",\n\t\t);\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t}\n\n\tprivate async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n\t\tconst headers = new Headers(init.headers);\n\t\theaders.set(\"X-Api-Key\", this.apiKey);\n\t\theaders.set(\"accept\", \"application/json\");\n\t\tif (init.body && !headers.has(\"content-type\")) {\n\t\t\theaders.set(\"content-type\", \"application/json\");\n\t\t}\n\t\tconst res = await this.fetchFn(`${this.apiUrl}${path}`, {\n\t\t\t...init,\n\t\t\theaders,\n\t\t});\n\t\tconst body: unknown = await res.json().catch(() => ({}));\n\t\tif (!res.ok) {\n\t\t\tconst error = new Error(`Livo API ${res.status}`);\n\t\t\t(error as Error & { status: number; body: unknown }).status = res.status;\n\t\t\t(error as Error & { status: number; body: unknown }).body = body;\n\t\t\tthrow error;\n\t\t}\n\t\treturn body as T;\n\t}\n\n\tcreateStream(input: {\n\t\ttitle: string;\n\t\tmode?: \"instant\" | \"scheduled\";\n\t\tsource?: \"rtmp\" | \"studio\";\n\t\tstartsAt?: number;\n\t\tdurationSeconds?: number;\n\t\tcurtainId?: string;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{ stream: { id: string } }>(\"/streams\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetStream(id: string) {\n\t\treturn this.request<{ stream: { id: string; status: string } }>(\n\t\t\t`/streams/${encodeURIComponent(id)}`,\n\t\t);\n\t}\n\n\tstartStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/start`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpublishStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/publish`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpatchStream(\n\t\tid: string,\n\t\tinput: {\n\t\t\ttitle?: string;\n\t\t\tstartsAt?: number | null;\n\t\t\tdurationSeconds?: number | null;\n\t\t\tcurtainId?: string | null;\n\t\t\tcaptionsEnabled?: boolean;\n\t\t},\n\t) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}`, {\n\t\t\tmethod: \"PATCH\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tstopStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/stop`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistVods() {\n\t\treturn this.request<{ vods: Array<{ id: string; title: string }> }>(\n\t\t\t\"/vods\",\n\t\t);\n\t}\n\n\tcreateVod(input: {\n\t\tfilename: string;\n\t\tcontentType: string;\n\t\tbyteSize: number;\n\t\tsha256: string;\n\t\ttitle?: string;\n\t\treleaseAt?: number | null;\n\t\tcurtainId?: string | null;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{\n\t\t\tvodId: string;\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/vods\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\trequestVodPartUrl(vodId: string, partNumber: number) {\n\t\treturn this.request<{ uploadUrl: string }>(\n\t\t\t`/vods/${encodeURIComponent(vodId)}/upload-part`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: JSON.stringify({ partNumber }),\n\t\t\t},\n\t\t);\n\t}\n\n\tcompleteVodUpload(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/complete-upload`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\ttranscodeVod(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/transcode`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tenableVodCaptions(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/captions`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistCurtains() {\n\t\treturn this.request<{ curtains: Array<{ id: string; title: string }> }>(\n\t\t\t\"/curtains\",\n\t\t);\n\t}\n\n\tcreateCurtain(input: Record<string, unknown>) {\n\t\treturn this.request<{\n\t\t\tcurtain: { id: string };\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/curtains\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetOrganizationSettings() {\n\t\treturn this.request<{\n\t\t\tdefaultCurtainId: string | null;\n\t\t\twebhookUrl: string | null;\n\t\t}>(\"/organization/settings\");\n\t}\n\n\tputOrganizationSettings(input: {\n\t\tdefaultCurtainId?: string | null;\n\t\twebhookUrl?: string | null;\n\t\twebhookSecret?: string | null;\n\t}) {\n\t\treturn this.request(\"/organization/settings\", {\n\t\t\tmethod: \"PUT\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tmintHostSession(\n\t\tstreamId: string,\n\t\tinput: { displayName: string; picture?: string },\n\t) {\n\t\treturn this.request<{\n\t\t\thostUrl: string;\n\t\t\tguestUrl: string;\n\t\t\texpiresAt: number;\n\t\t}>(`/streams/${encodeURIComponent(streamId)}/studio/host-session`, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n}\n\nexport function createLivoServerClient(options: LivoServerClientOptions) {\n\treturn new LivoServerClient(options);\n}\n\nexport { studioGuestUrl as getGuestLink } from \"./urls\";\n"]}
1
+ {"version":3,"sources":["../src/theme.ts","../src/urls.ts","../src/server.ts"],"names":[],"mappings":";AA4CO,SAAS,iBAAiB,KAAA,EAA2B;AAC3D,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AACjC,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,OAAO,IAAI,CAAA;AAC3C,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAC5D,EAAA,OAAO,IAAA,CAAK,MAAM,CAAA,CAChB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,OAAO,EAAE,CAAA;AACpB;;;AC9CA,SAAS,UAAU,KAAA,EAAuB;AACzC,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAC/B;AAoCO,SAAS,cAAA,CACf,KAAA,EACA,OAAA,GAAgE,EAAC,EACxD;AACT,EAAA,MAAM,MAAA,GAAS,SAAA,CAAU,OAAA,CAAQ,SAAA,IAAa,qBAAqB,CAAA;AACnE,EAAA,MAAM,MAAM,IAAI,GAAA,CAAI,GAAG,MAAM,CAAA,aAAA,EAAgB,KAAK,CAAA,CAAE,CAAA;AACpD,EAAA,IAAI,QAAQ,KAAA,KAAU,KAAA,MAAW,YAAA,CAAa,GAAA,CAAI,SAAS,GAAG,CAAA;AAC9D,EAAA,IAAI,OAAA,CAAQ,KAAA;AACX,IAAA,GAAA,CAAI,aAAa,GAAA,CAAI,OAAA,EAAS,gBAAA,CAAiB,OAAA,CAAQ,KAAK,CAAC,CAAA;AAC9D,EAAA,OAAO,IAAI,QAAA,EAAS;AACrB;;;ACjDO,IAAM,mBAAN,MAAuB;AAAA,EACZ,MAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EAEjB,YAAY,OAAA,EAAkC;AAC7C,IAAA,IAAA,CAAK,MAAA,GAAA,CAAU,OAAA,CAAQ,MAAA,IAAU,2BAAA,EAA6B,OAAA;AAAA,MAC7D,KAAA;AAAA,MACA;AAAA,KACD;AACA,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,KAAA;AAAA,EACnC;AAAA,EAEA,MAAc,OAAA,CAAW,IAAA,EAAc,IAAA,GAAoB,EAAC,EAAe;AAC1E,IAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACxC,IAAA,OAAA,CAAQ,GAAA,CAAI,WAAA,EAAa,IAAA,CAAK,MAAM,CAAA;AACpC,IAAA,OAAA,CAAQ,GAAA,CAAI,UAAU,kBAAkB,CAAA;AACxC,IAAA,IAAI,KAAK,IAAA,IAAQ,CAAC,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA,EAAG;AAC9C,MAAA,OAAA,CAAQ,GAAA,CAAI,gBAAgB,kBAAkB,CAAA;AAAA,IAC/C;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAQ,GAAG,IAAA,CAAK,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI;AAAA,MACvD,GAAG,IAAA;AAAA,MACH;AAAA,KACA,CAAA;AACD,IAAA,MAAM,IAAA,GAAgB,MAAM,GAAA,CAAI,IAAA,GAAO,KAAA,CAAM,OAAO,EAAC,CAAE,CAAA;AACvD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACZ,MAAA,MAAM,QAAQ,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAChD,MAAC,KAAA,CAAoD,SAAS,GAAA,CAAI,MAAA;AAClE,MAAC,MAAoD,IAAA,GAAO,IAAA;AAC5D,MAAA,MAAM,KAAA;AAAA,IACP;AACA,IAAA,OAAO,IAAA;AAAA,EACR;AAAA,EAEA,aAAa,KAAA,EAQV;AACF,IAAA,OAAO,IAAA,CAAK,QAAoC,UAAA,EAAY;AAAA,MAC3D,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,KAC1B,CAAA;AAAA,EACF;AAAA,EAEA,UAAU,EAAA,EAAY;AACrB,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,MACX,CAAA,SAAA,EAAY,kBAAA,CAAmB,EAAE,CAAC,CAAA;AAAA,KACnC;AAAA,EACD;AAAA,EAEA,YAAY,EAAA,EAAY;AACvB,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,SAAA,EAAY,kBAAA,CAAmB,EAAE,CAAC,CAAA,MAAA,CAAA,EAAU;AAAA,MAC/D,MAAA,EAAQ;AAAA,KACR,CAAA;AAAA,EACF;AAAA,EAEA,cAAc,EAAA,EAAY;AACzB,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,SAAA,EAAY,kBAAA,CAAmB,EAAE,CAAC,CAAA,QAAA,CAAA,EAAY;AAAA,MACjE,MAAA,EAAQ;AAAA,KACR,CAAA;AAAA,EACF;AAAA,EAEA,WAAA,CACC,IACA,KAAA,EAOC;AACD,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,SAAA,EAAY,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA,EAAI;AAAA,MACzD,MAAA,EAAQ,OAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,KAC1B,CAAA;AAAA,EACF;AAAA,EAEA,WAAW,EAAA,EAAY;AACtB,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,SAAA,EAAY,kBAAA,CAAmB,EAAE,CAAC,CAAA,KAAA,CAAA,EAAS;AAAA,MAC9D,MAAA,EAAQ;AAAA,KACR,CAAA;AAAA,EACF;AAAA,EAEA,QAAA,GAAW;AACV,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,MACX;AAAA,KACD;AAAA,EACD;AAAA,EAEA,UAAU,KAAA,EASP;AACF,IAAA,OAAO,IAAA,CAAK,QAIT,OAAA,EAAS;AAAA,MACX,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,KAC1B,CAAA;AAAA,EACF;AAAA,EAEA,iBAAA,CAAkB,OAAe,UAAA,EAAoB;AACpD,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,MACX,CAAA,MAAA,EAAS,kBAAA,CAAmB,KAAK,CAAC,CAAA,YAAA,CAAA;AAAA,MAClC;AAAA,QACC,MAAA,EAAQ,MAAA;AAAA,QACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,YAAY;AAAA;AACpC,KACD;AAAA,EACD;AAAA,EAEA,kBAAkB,KAAA,EAAe;AAChC,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,MAAA,EAAS,kBAAA,CAAmB,KAAK,CAAC,CAAA,gBAAA,CAAA,EAAoB;AAAA,MACzE,MAAA,EAAQ;AAAA,KACR,CAAA;AAAA,EACF;AAAA,EAEA,aAAa,KAAA,EAAe;AAC3B,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,MAAA,EAAS,kBAAA,CAAmB,KAAK,CAAC,CAAA,UAAA,CAAA,EAAc;AAAA,MACnE,MAAA,EAAQ;AAAA,KACR,CAAA;AAAA,EACF;AAAA,EAEA,kBAAkB,KAAA,EAAe;AAChC,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAA,MAAA,EAAS,kBAAA,CAAmB,KAAK,CAAC,CAAA,SAAA,CAAA,EAAa;AAAA,MAClE,MAAA,EAAQ;AAAA,KACR,CAAA;AAAA,EACF;AAAA,EAEA,YAAA,GAAe;AACd,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,MACX;AAAA,KACD;AAAA,EACD;AAAA,EAEA,cAAc,KAAA,EAAgC;AAC7C,IAAA,OAAO,IAAA,CAAK,QAIT,WAAA,EAAa;AAAA,MACf,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,KAC1B,CAAA;AAAA,EACF;AAAA,EAEA,uBAAA,GAA0B;AACzB,IAAA,OAAO,IAAA,CAAK,QAGT,wBAAwB,CAAA;AAAA,EAC5B;AAAA,EAEA,wBAAwB,KAAA,EAIrB;AACF,IAAA,OAAO,IAAA,CAAK,QAAQ,wBAAA,EAA0B;AAAA,MAC7C,MAAA,EAAQ,KAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,KAC1B,CAAA;AAAA,EACF;AAAA,EAEA,eAAA,CACC,UACA,KAAA,EACC;AACD,IAAA,OAAO,KAAK,OAAA,CAMT,CAAA,SAAA,EAAY,kBAAA,CAAmB,QAAQ,CAAC,CAAA,oBAAA,CAAA,EAAwB;AAAA,MAClE,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,KAC1B,CAAA;AAAA,EACF;AACD;AAEO,SAAS,uBAAuB,OAAA,EAAkC;AACxE,EAAA,OAAO,IAAI,iBAAiB,OAAO,CAAA;AACpC","file":"server.js","sourcesContent":["export type EmbedTheme = {\n\tprimary?: string;\n\tbackground?: string;\n\tforeground?: string;\n\tradius?: string;\n\tfontFamily?: string;\n\tmode?: \"light\" | \"dark\";\n};\n\nconst HEX =\n\t/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\nconst COLOR_FN = /^(?:oklch|rgb|rgba|hsl|hsla)\\([0-9a-zA-Z.\\s%,./-]+\\)$/;\nconst RADIUS = /^\\d+(?:\\.\\d+)?(?:px|rem|em)$/;\nconst FONT = /^[a-zA-Z0-9\\s,\"'-]{1,120}$/;\n\nfunction sanitizeColor(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\tif (HEX.test(trimmed) || COLOR_FN.test(trimmed)) return trimmed;\n\treturn undefined;\n}\n\nexport function sanitizeEmbedTheme(value: unknown): EmbedTheme | null {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn null;\n\t}\n\tconst row = value as Record<string, unknown>;\n\tconst theme: EmbedTheme = {};\n\tconst primary = sanitizeColor(row.primary);\n\tconst background = sanitizeColor(row.background);\n\tconst foreground = sanitizeColor(row.foreground);\n\tif (typeof row.radius === \"string\" && RADIUS.test(row.radius.trim())) {\n\t\ttheme.radius = row.radius.trim();\n\t}\n\tif (typeof row.fontFamily === \"string\" && FONT.test(row.fontFamily.trim())) {\n\t\ttheme.fontFamily = row.fontFamily.trim();\n\t}\n\tif (primary) theme.primary = primary;\n\tif (background) theme.background = background;\n\tif (foreground) theme.foreground = foreground;\n\tif (row.mode === \"light\" || row.mode === \"dark\") theme.mode = row.mode;\n\treturn Object.keys(theme).length > 0 ? theme : null;\n}\n\nexport function encodeEmbedTheme(theme: EmbedTheme): string {\n\tconst json = JSON.stringify(theme);\n\tconst bytes = new TextEncoder().encode(json);\n\tlet binary = \"\";\n\tfor (const byte of bytes) binary += String.fromCharCode(byte);\n\treturn btoa(binary)\n\t\t.replace(/\\+/g, \"-\")\n\t\t.replace(/\\//g, \"_\")\n\t\t.replace(/=+$/, \"\");\n}\n\nexport function embedThemeCssVars(theme: EmbedTheme): Record<string, string> {\n\tconst vars: Record<string, string> = {};\n\tif (theme.primary) {\n\t\tvars[\"--livo-primary\"] = theme.primary;\n\t\tvars[\"--primary\"] = theme.primary;\n\t}\n\tif (theme.background) {\n\t\tvars[\"--livo-background\"] = theme.background;\n\t\tvars[\"--background\"] = theme.background;\n\t}\n\tif (theme.foreground) {\n\t\tvars[\"--livo-foreground\"] = theme.foreground;\n\t\tvars[\"--foreground\"] = theme.foreground;\n\t}\n\tif (theme.radius) {\n\t\tvars[\"--livo-radius\"] = theme.radius;\n\t\tvars[\"--radius\"] = theme.radius;\n\t}\n\tif (theme.fontFamily) vars[\"--font-sans\"] = theme.fontFamily;\n\treturn vars;\n}\n\nexport function decodeEmbedTheme(\n\traw: string | null | undefined,\n): EmbedTheme | null {\n\tif (!raw) return null;\n\ttry {\n\t\tconst padded = raw.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\t\tconst pad =\n\t\t\tpadded.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (padded.length % 4));\n\t\tconst binary = atob(padded + pad);\n\t\tconst bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n\t\treturn sanitizeEmbedTheme(JSON.parse(new TextDecoder().decode(bytes)));\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function applyEmbedTheme(\n\troot: HTMLElement,\n\ttheme: EmbedTheme | null,\n): void {\n\tif (!theme) return;\n\tif (theme.mode === \"dark\") root.classList.add(\"dark\");\n\tif (theme.mode === \"light\") root.classList.remove(\"dark\");\n\tfor (const [key, value] of Object.entries(embedThemeCssVars(theme))) {\n\t\troot.style.setProperty(key, value);\n\t}\n\tif (theme.fontFamily) root.style.fontFamily = theme.fontFamily;\n}\n","import { encodeEmbedTheme, type EmbedTheme } from \"./theme\";\n\nexport type UrlOrigins = {\n\tplayerOrigin?: string;\n\tappOrigin?: string;\n};\n\nfunction trimSlash(value: string): string {\n\treturn value.replace(/\\/$/, \"\");\n}\n\nexport function playerUrl(\n\tstreamId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/${streamId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function vodPlayerUrl(\n\tvodId: string,\n\toptions: UrlOrigins & {\n\t\ttheme?: EmbedTheme;\n\t\tlocale?: string;\n\t\tautoplay?: boolean;\n\t} = {},\n): string {\n\tconst origin = trimSlash(options.playerOrigin ?? \"https://player.livo.tv\");\n\tconst url = new URL(`${origin}/vod/${vodId}`);\n\tif (options.locale) url.searchParams.set(\"lang\", options.locale);\n\tif (options.autoplay) url.searchParams.set(\"autoplay\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioGuestUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/join/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n\nexport function studioHostUrl(\n\ttoken: string,\n\toptions: UrlOrigins & { theme?: EmbedTheme; embed?: boolean } = {},\n): string {\n\tconst origin = trimSlash(options.appOrigin ?? \"https://app.livo.tv\");\n\tconst url = new URL(`${origin}/studio/host/${token}`);\n\tif (options.embed !== false) url.searchParams.set(\"embed\", \"1\");\n\tif (options.theme)\n\t\turl.searchParams.set(\"theme\", encodeEmbedTheme(options.theme));\n\treturn url.toString();\n}\n","export type LivoServerClientOptions = {\n\tapiUrl?: string;\n\tapiKey: string;\n\tfetchFn?: typeof fetch;\n};\n\nexport class LivoServerClient {\n\tprivate readonly apiUrl: string;\n\tprivate readonly apiKey: string;\n\tprivate readonly fetchFn: typeof fetch;\n\n\tconstructor(options: LivoServerClientOptions) {\n\t\tthis.apiUrl = (options.apiUrl ?? \"https://media-svc.livo.tv\").replace(\n\t\t\t/\\/$/,\n\t\t\t\"\",\n\t\t);\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t}\n\n\tprivate async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n\t\tconst headers = new Headers(init.headers);\n\t\theaders.set(\"X-Api-Key\", this.apiKey);\n\t\theaders.set(\"accept\", \"application/json\");\n\t\tif (init.body && !headers.has(\"content-type\")) {\n\t\t\theaders.set(\"content-type\", \"application/json\");\n\t\t}\n\t\tconst res = await this.fetchFn(`${this.apiUrl}${path}`, {\n\t\t\t...init,\n\t\t\theaders,\n\t\t});\n\t\tconst body: unknown = await res.json().catch(() => ({}));\n\t\tif (!res.ok) {\n\t\t\tconst error = new Error(`Livo API ${res.status}`);\n\t\t\t(error as Error & { status: number; body: unknown }).status = res.status;\n\t\t\t(error as Error & { status: number; body: unknown }).body = body;\n\t\t\tthrow error;\n\t\t}\n\t\treturn body as T;\n\t}\n\n\tcreateStream(input: {\n\t\ttitle: string;\n\t\tmode?: \"instant\" | \"scheduled\";\n\t\tsource?: \"rtmp\" | \"studio\";\n\t\tstartsAt?: number;\n\t\tdurationSeconds?: number;\n\t\tcurtainId?: string;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{ stream: { id: string } }>(\"/streams\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetStream(id: string) {\n\t\treturn this.request<{ stream: { id: string; status: string } }>(\n\t\t\t`/streams/${encodeURIComponent(id)}`,\n\t\t);\n\t}\n\n\tstartStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/start`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpublishStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/publish`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tpatchStream(\n\t\tid: string,\n\t\tinput: {\n\t\t\ttitle?: string;\n\t\t\tstartsAt?: number | null;\n\t\t\tdurationSeconds?: number | null;\n\t\t\tcurtainId?: string | null;\n\t\t\tcaptionsEnabled?: boolean;\n\t\t},\n\t) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}`, {\n\t\t\tmethod: \"PATCH\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tstopStream(id: string) {\n\t\treturn this.request(`/streams/${encodeURIComponent(id)}/stop`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistVods() {\n\t\treturn this.request<{ vods: Array<{ id: string; title: string }> }>(\n\t\t\t\"/vods\",\n\t\t);\n\t}\n\n\tcreateVod(input: {\n\t\tfilename: string;\n\t\tcontentType: string;\n\t\tbyteSize: number;\n\t\tsha256: string;\n\t\ttitle?: string;\n\t\treleaseAt?: number | null;\n\t\tcurtainId?: string | null;\n\t\tcaptionsEnabled?: boolean;\n\t}) {\n\t\treturn this.request<{\n\t\t\tvodId: string;\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/vods\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\trequestVodPartUrl(vodId: string, partNumber: number) {\n\t\treturn this.request<{ uploadUrl: string }>(\n\t\t\t`/vods/${encodeURIComponent(vodId)}/upload-part`,\n\t\t\t{\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tbody: JSON.stringify({ partNumber }),\n\t\t\t},\n\t\t);\n\t}\n\n\tcompleteVodUpload(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/complete-upload`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\ttranscodeVod(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/transcode`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tenableVodCaptions(vodId: string) {\n\t\treturn this.request(`/vods/${encodeURIComponent(vodId)}/captions`, {\n\t\t\tmethod: \"POST\",\n\t\t});\n\t}\n\n\tlistCurtains() {\n\t\treturn this.request<{ curtains: Array<{ id: string; title: string }> }>(\n\t\t\t\"/curtains\",\n\t\t);\n\t}\n\n\tcreateCurtain(input: Record<string, unknown>) {\n\t\treturn this.request<{\n\t\t\tcurtain: { id: string };\n\t\t\tuploadId: string;\n\t\t\tpartSize: number;\n\t\t}>(\"/curtains\", {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tgetOrganizationSettings() {\n\t\treturn this.request<{\n\t\t\tdefaultCurtainId: string | null;\n\t\t\twebhookUrl: string | null;\n\t\t}>(\"/organization/settings\");\n\t}\n\n\tputOrganizationSettings(input: {\n\t\tdefaultCurtainId?: string | null;\n\t\twebhookUrl?: string | null;\n\t\twebhookSecret?: string | null;\n\t}) {\n\t\treturn this.request(\"/organization/settings\", {\n\t\t\tmethod: \"PUT\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n\n\tmintHostSession(\n\t\tstreamId: string,\n\t\tinput: { displayName: string; picture?: string },\n\t) {\n\t\treturn this.request<{\n\t\t\thostUrl: string;\n\t\t\tguestUrl: string;\n\t\t\thostToken: string;\n\t\t\tguestToken: string;\n\t\t\texpiresAt: number;\n\t\t}>(`/streams/${encodeURIComponent(streamId)}/studio/host-session`, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(input),\n\t\t});\n\t}\n}\n\nexport function createLivoServerClient(options: LivoServerClientOptions) {\n\treturn new LivoServerClient(options);\n}\n\nexport { studioGuestUrl as getGuestLink } from \"./urls\";\n"]}
@@ -1274,15 +1274,15 @@ function AudioWaveform({
1274
1274
  if (!canvas || !track || reducedMotion) return;
1275
1275
  const AudioCtx = window.AudioContext || window.webkitAudioContext;
1276
1276
  if (!AudioCtx) return;
1277
- const ctx2 = new AudioCtx();
1278
- const source = ctx2.createMediaStreamSource(new MediaStream([track]));
1279
- const analyser = ctx2.createAnalyser();
1277
+ const ctx3 = new AudioCtx();
1278
+ const source = ctx3.createMediaStreamSource(new MediaStream([track]));
1279
+ const analyser = ctx3.createAnalyser();
1280
1280
  analyser.fftSize = 64;
1281
1281
  source.connect(analyser);
1282
1282
  const data = new Uint8Array(analyser.frequencyBinCount);
1283
1283
  const graphic = canvas.getContext("2d");
1284
1284
  const surface = canvas;
1285
- let frame = 0;
1285
+ let frame2 = 0;
1286
1286
  function draw() {
1287
1287
  if (!graphic) return;
1288
1288
  analyser.getByteFrequencyData(data);
@@ -1300,12 +1300,12 @@ function AudioWaveform({
1300
1300
  barHeight
1301
1301
  );
1302
1302
  }
1303
- frame = requestAnimationFrame(draw);
1303
+ frame2 = requestAnimationFrame(draw);
1304
1304
  }
1305
- frame = requestAnimationFrame(draw);
1305
+ frame2 = requestAnimationFrame(draw);
1306
1306
  return () => {
1307
- cancelAnimationFrame(frame);
1308
- void ctx2.close();
1307
+ cancelAnimationFrame(frame2);
1308
+ void ctx3.close();
1309
1309
  };
1310
1310
  }, [track, reducedMotion]);
1311
1311
  if (reducedMotion) {
@@ -1591,10 +1591,10 @@ async function playSpeakerTest(deviceId) {
1591
1591
  if (!AudioCtx) {
1592
1592
  throw new Error("AudioContext unavailable");
1593
1593
  }
1594
- const ctx2 = new AudioCtx();
1595
- void ctx2.resume();
1596
- const oscillator = ctx2.createOscillator();
1597
- const gain = ctx2.createGain();
1594
+ const ctx3 = new AudioCtx();
1595
+ void ctx3.resume();
1596
+ const oscillator = ctx3.createOscillator();
1597
+ const gain = ctx3.createGain();
1598
1598
  oscillator.type = "sine";
1599
1599
  oscillator.frequency.value = TEST_HZ;
1600
1600
  gain.gain.value = 0.18;
@@ -1604,7 +1604,7 @@ async function playSpeakerTest(deviceId) {
1604
1604
  const probe = document.createElement("audio");
1605
1605
  const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
1606
1606
  if (canRoute && deviceId) {
1607
- const destination = ctx2.createMediaStreamDestination();
1607
+ const destination = ctx3.createMediaStreamDestination();
1608
1608
  gain.connect(destination);
1609
1609
  audio = probe;
1610
1610
  audio.autoplay = true;
@@ -1615,13 +1615,13 @@ async function playSpeakerTest(deviceId) {
1615
1615
  await audio.play();
1616
1616
  } catch {
1617
1617
  gain.disconnect();
1618
- gain.connect(ctx2.destination);
1618
+ gain.connect(ctx3.destination);
1619
1619
  audio.srcObject = null;
1620
1620
  audio.remove();
1621
1621
  audio = null;
1622
1622
  }
1623
1623
  } else {
1624
- gain.connect(ctx2.destination);
1624
+ gain.connect(ctx3.destination);
1625
1625
  }
1626
1626
  oscillator.start();
1627
1627
  await new Promise((resolve) => {
@@ -1633,7 +1633,7 @@ async function playSpeakerTest(deviceId) {
1633
1633
  audio.srcObject = null;
1634
1634
  audio.remove();
1635
1635
  }
1636
- await ctx2.close();
1636
+ await ctx3.close();
1637
1637
  }
1638
1638
  }
1639
1639
  function StudioDeviceSelectGroup({
@@ -2416,7 +2416,7 @@ function StudioChatPanel({
2416
2416
  isModerator: isModerator2,
2417
2417
  labels
2418
2418
  }) {
2419
- const [tick, setTick] = react.useState(0);
2419
+ const [tick2, setTick] = react.useState(0);
2420
2420
  const [draft, setDraft] = react.useState("");
2421
2421
  const [editingId, setEditingId] = react.useState(null);
2422
2422
  const listRef = react.useRef(null);
@@ -2426,9 +2426,9 @@ function StudioChatPanel({
2426
2426
  );
2427
2427
  const messages = react.useMemo(
2428
2428
  () => meeting.chatMessages().filter((row) => !row.targetUserIds?.length),
2429
- [meeting, tick]
2429
+ [meeting, tick2]
2430
2430
  );
2431
- const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting, tick]);
2431
+ const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting, tick2]);
2432
2432
  const limit = meeting.chatMaxTextLimit();
2433
2433
  const pictures = react.useMemo(() => {
2434
2434
  const map = /* @__PURE__ */ new Map();
@@ -2928,6 +2928,171 @@ function StudioSidePanel({
2928
2928
  ] })
2929
2929
  ] });
2930
2930
  }
2931
+
2932
+ // src/studio/lib/audio-levels.ts
2933
+ var LEVEL_SENSITIVITY = 4;
2934
+ var LEVEL_ATTACK = 0.55;
2935
+ var LEVEL_DECAY = 0.12;
2936
+ var SPEAKING_ON = 0.12;
2937
+ var SPEAKING_OFF = 0.06;
2938
+ var SPEAKING_HOLD_MS = 180;
2939
+ var tracks = /* @__PURE__ */ new Map();
2940
+ var ctx2 = null;
2941
+ var frame = 0;
2942
+ var tickRunning = false;
2943
+ function audioContextCtor2() {
2944
+ if (typeof window === "undefined") return null;
2945
+ return window.AudioContext || window.webkitAudioContext || null;
2946
+ }
2947
+ function ensureContext2() {
2948
+ const AudioCtx = audioContextCtor2();
2949
+ if (!AudioCtx) return null;
2950
+ if (!ctx2 || ctx2.state === "closed") {
2951
+ ctx2 = new AudioCtx();
2952
+ }
2953
+ return ctx2;
2954
+ }
2955
+ function rmsFromTimeDomain(data) {
2956
+ if (data.length === 0) return 0;
2957
+ let sum = 0;
2958
+ for (let i = 0; i < data.length; i += 1) {
2959
+ const centered = ((data[i] ?? 128) - 128) / 128;
2960
+ sum += centered * centered;
2961
+ }
2962
+ return Math.sqrt(sum / data.length);
2963
+ }
2964
+ function normalizeAudioLevel(rms) {
2965
+ return Math.min(1, Math.max(0, rms * LEVEL_SENSITIVITY));
2966
+ }
2967
+ function smoothAudioLevel(previous, raw) {
2968
+ const factor = raw > previous ? LEVEL_ATTACK : LEVEL_DECAY;
2969
+ return previous + (raw - previous) * factor;
2970
+ }
2971
+ function nextSpeakingGate(gate, level, now) {
2972
+ if (!gate.speaking) {
2973
+ if (level >= SPEAKING_ON) {
2974
+ return { speaking: true, quietSince: null };
2975
+ }
2976
+ return { speaking: false, quietSince: null };
2977
+ }
2978
+ if (level >= SPEAKING_OFF) {
2979
+ return { speaking: true, quietSince: null };
2980
+ }
2981
+ const quietSince = gate.quietSince ?? now;
2982
+ if (now - quietSince >= SPEAKING_HOLD_MS) {
2983
+ return { speaking: false, quietSince: null };
2984
+ }
2985
+ return { speaking: true, quietSince };
2986
+ }
2987
+ function tick() {
2988
+ for (const entry of tracks.values()) {
2989
+ entry.analyser.getByteTimeDomainData(entry.data);
2990
+ const raw = normalizeAudioLevel(rmsFromTimeDomain(entry.data));
2991
+ entry.smoothed = smoothAudioLevel(entry.smoothed, raw);
2992
+ for (const cb of entry.listeners) {
2993
+ cb(entry.smoothed);
2994
+ }
2995
+ }
2996
+ if (tracks.size > 0) {
2997
+ frame = requestAnimationFrame(tick);
2998
+ return;
2999
+ }
3000
+ tickRunning = false;
3001
+ frame = 0;
3002
+ }
3003
+ function startTick() {
3004
+ if (tickRunning) return;
3005
+ tickRunning = true;
3006
+ frame = requestAnimationFrame(tick);
3007
+ }
3008
+ function stopTickIfIdle() {
3009
+ if (tracks.size > 0 || !frame) return;
3010
+ cancelAnimationFrame(frame);
3011
+ tickRunning = false;
3012
+ frame = 0;
3013
+ }
3014
+ function subscribeStudioAudioLevel(track, cb) {
3015
+ const context = ensureContext2();
3016
+ if (!context) return () => void 0;
3017
+ void context.resume();
3018
+ const id = track.id;
3019
+ let entry = tracks.get(id);
3020
+ if (!entry) {
3021
+ const source = context.createMediaStreamSource(new MediaStream([track]));
3022
+ const analyser = context.createAnalyser();
3023
+ analyser.fftSize = 512;
3024
+ analyser.smoothingTimeConstant = 0;
3025
+ source.connect(analyser);
3026
+ entry = {
3027
+ source,
3028
+ analyser,
3029
+ data: new Uint8Array(analyser.fftSize),
3030
+ refs: 0,
3031
+ smoothed: 0,
3032
+ listeners: /* @__PURE__ */ new Set()
3033
+ };
3034
+ tracks.set(id, entry);
3035
+ }
3036
+ entry.refs += 1;
3037
+ entry.listeners.add(cb);
3038
+ startTick();
3039
+ return () => {
3040
+ const current = tracks.get(id);
3041
+ if (!current) return;
3042
+ current.listeners.delete(cb);
3043
+ current.refs -= 1;
3044
+ if (current.refs <= 0) {
3045
+ current.source.disconnect();
3046
+ current.analyser.disconnect();
3047
+ tracks.delete(id);
3048
+ }
3049
+ stopTickIfIdle();
3050
+ };
3051
+ }
3052
+
3053
+ // src/studio/hooks/use-speaking-glow.ts
3054
+ function clearSpeakingGlow(el) {
3055
+ el.style.removeProperty("--livo-speaking-level");
3056
+ el.removeAttribute("data-speaking");
3057
+ }
3058
+ function useSpeakingGlow({
3059
+ track,
3060
+ enabled,
3061
+ reducedMotion
3062
+ }) {
3063
+ const [el, setEl] = react.useState(null);
3064
+ const gateRef = react.useRef({ speaking: false, quietSince: null });
3065
+ const setRef = react.useCallback((node) => {
3066
+ setEl(node);
3067
+ }, []);
3068
+ react.useEffect(() => {
3069
+ if (!el || !enabled || !track) {
3070
+ if (el) clearSpeakingGlow(el);
3071
+ gateRef.current = { speaking: false, quietSince: null };
3072
+ return;
3073
+ }
3074
+ const unsubscribe = subscribeStudioAudioLevel(track, (level) => {
3075
+ const node = el;
3076
+ gateRef.current = nextSpeakingGate(gateRef.current, level, Date.now());
3077
+ if (gateRef.current.speaking) {
3078
+ node.dataset.speaking = "true";
3079
+ if (!reducedMotion) {
3080
+ node.style.setProperty("--livo-speaking-level", String(level));
3081
+ } else {
3082
+ node.style.removeProperty("--livo-speaking-level");
3083
+ }
3084
+ return;
3085
+ }
3086
+ clearSpeakingGlow(node);
3087
+ });
3088
+ return () => {
3089
+ unsubscribe();
3090
+ clearSpeakingGlow(el);
3091
+ gateRef.current = { speaking: false, quietSince: null };
3092
+ };
3093
+ }, [el, enabled, reducedMotion, track]);
3094
+ return setRef;
3095
+ }
2931
3096
  function DropdownMenu({ ...props }) {
2932
3097
  return /* @__PURE__ */ jsxRuntime.jsx(menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
2933
3098
  }
@@ -3076,12 +3241,18 @@ function StudioTile({
3076
3241
  const onStage = isOnStage(tile.stageStatus);
3077
3242
  const [confirm, setConfirm] = react.useState(null);
3078
3243
  const showMenu = (canModerate || canManageStage) && !tile.isSelf;
3244
+ const glowRef = useSpeakingGlow({
3245
+ track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
3246
+ enabled: onStage,
3247
+ reducedMotion
3248
+ });
3079
3249
  return /* @__PURE__ */ jsxRuntime.jsxs(
3080
3250
  "article",
3081
3251
  {
3252
+ ref: glowRef,
3082
3253
  "aria-label": tile.name,
3083
3254
  className: cn(
3084
- "bg-muted relative overflow-hidden rounded-xl",
3255
+ "livo-studio-tile bg-muted relative overflow-hidden rounded-xl",
3085
3256
  compact ? "aspect-video w-40 shrink-0 md:w-full" : tile.kind === "screen" ? "min-h-0 flex-1 md:min-h-48" : fill ? "size-full min-h-0 md:aspect-video md:h-auto" : "aspect-video"
3086
3257
  ),
3087
3258
  children: [
@@ -3111,6 +3282,13 @@ function StudioTile({
3111
3282
  contain: tile.kind === "screen"
3112
3283
  }
3113
3284
  ),
3285
+ /* @__PURE__ */ jsxRuntime.jsx(
3286
+ "div",
3287
+ {
3288
+ "aria-hidden": true,
3289
+ className: "livo-studio-tile-glow pointer-events-none absolute inset-0 z-20 rounded-xl"
3290
+ }
3291
+ ),
3114
3292
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 bottom-0 z-10 flex items-end justify-between gap-2 bg-linear-to-t from-black/70 to-transparent p-2", children: [
3115
3293
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "min-w-0 truncate text-xs font-medium text-white", children: [
3116
3294
  tile.name,
@@ -3253,9 +3431,7 @@ function StudioStage({
3253
3431
  onOpenPeople
3254
3432
  }) {
3255
3433
  const displayTiles = expandStudioTiles(participants);
3256
- const stage = studioStageLayout(displayTiles, {
3257
- activeSpeakerId: meeting.activeSpeakerId()
3258
- });
3434
+ const stage = studioStageLayout(displayTiles);
3259
3435
  const sharing = stage.spotlight.length > 0;
3260
3436
  const overflowLabel = labels.stageOverflow.replace(
3261
3437
  "{count}",
@@ -3588,7 +3764,7 @@ function StudioRoom({
3588
3764
  const [screenRequestPending, setScreenRequestPending] = react.useState(false);
3589
3765
  const [pending, setPending] = react.useState(false);
3590
3766
  const [publishing, setPublishing] = react.useState(false);
3591
- const [tick, setTick] = react.useState(0);
3767
+ const [tick2, setTick] = react.useState(0);
3592
3768
  const [leaveState, setLeaveState] = react.useState(null);
3593
3769
  const [sideTab, setSideTab] = react.useState(null);
3594
3770
  const [settingsOpen, setSettingsOpen] = react.useState(false);
@@ -3832,7 +4008,7 @@ function StudioRoom({
3832
4008
  });
3833
4009
  }, [meeting]);
3834
4010
  const sawParticipants = react.useRef(false);
3835
- const participants = react.useMemo(() => meeting.participants(), [meeting, tick]);
4011
+ const participants = react.useMemo(() => meeting.participants(), [meeting, tick2]);
3836
4012
  react.useEffect(() => {
3837
4013
  const other = duplicateSelfParticipant(participants);
3838
4014
  if (!other) return;
@@ -3847,22 +4023,22 @@ function StudioRoom({
3847
4023
  () => participants.filter((row) => isOnStage(row.stageStatus)),
3848
4024
  [participants]
3849
4025
  );
3850
- const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick]);
4026
+ const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick2]);
3851
4027
  const connectionState = react.useMemo(
3852
4028
  () => meeting.connectionState(),
3853
- [meeting, tick]
4029
+ [meeting, tick2]
3854
4030
  );
3855
4031
  const lastMeetingError = react.useMemo(
3856
4032
  () => meeting.lastMeetingError(),
3857
- [meeting, tick]
4033
+ [meeting, tick2]
3858
4034
  );
3859
4035
  const canManageStage = meeting.canManageStage();
3860
4036
  const stageEnabled = meeting.stageEnabled();
3861
4037
  const stageRequests = react.useMemo(
3862
4038
  () => canManageStage ? meeting.stageRequests() : [],
3863
- [canManageStage, meeting, tick]
4039
+ [canManageStage, meeting, tick2]
3864
4040
  );
3865
- const chatMessages = react.useMemo(() => meeting.chatMessages(), [meeting, tick]);
4041
+ const chatMessages = react.useMemo(() => meeting.chatMessages(), [meeting, tick2]);
3866
4042
  const inWaitingRoom = meeting.inWaitingRoom();
3867
4043
  const screenAvailable = canStartScreenShare(participants);
3868
4044
  const durableScreenBlock = screenShareBlock(
@@ -5781,6 +5957,9 @@ function studioMeetingFromRtk(raw) {
5781
5957
  localVideoTrack() {
5782
5958
  return meeting.self?.videoTrack ?? null;
5783
5959
  },
5960
+ participantAudioTrack(participantId) {
5961
+ return participantFor(participantId)?.audioTrack ?? null;
5962
+ },
5784
5963
  participantVideoTrack(participantId) {
5785
5964
  return participantFor(participantId)?.videoTrack ?? null;
5786
5965
  },