@livo-tv/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,343 @@
1
+ import { VideoPlayer } from '@livo-tv/blocks';
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // src/player.tsx
6
+
7
+ // src/public-api.ts
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function defaultApiUrl() {
12
+ return "https://media-svc.livo.tv";
13
+ }
14
+ function parsePublicStream(value) {
15
+ if (!isRecord(value) || typeof value.id !== "string") return null;
16
+ if (typeof value.title !== "string" || typeof value.status !== "string") {
17
+ return null;
18
+ }
19
+ return {
20
+ id: value.id,
21
+ title: value.title,
22
+ status: value.status,
23
+ playbackUrl: typeof value.playbackUrl === "string" ? value.playbackUrl : null,
24
+ captionsUrl: typeof value.captionsUrl === "string" ? value.captionsUrl : null,
25
+ viewers: typeof value.viewers === "number" ? value.viewers : 0,
26
+ curtain: isRecord(value.curtain) && typeof value.curtain.url === "string" ? {
27
+ kind: value.curtain.kind === "video" ? "video" : "image",
28
+ url: value.curtain.url
29
+ } : null
30
+ };
31
+ }
32
+ function parsePublicVod(value) {
33
+ if (!isRecord(value) || typeof value.id !== "string") return null;
34
+ if (typeof value.title !== "string" || typeof value.status !== "string") {
35
+ return null;
36
+ }
37
+ return {
38
+ id: value.id,
39
+ title: value.title,
40
+ status: value.status,
41
+ playbackUrl: typeof value.playbackUrl === "string" ? value.playbackUrl : null,
42
+ posterUrl: typeof value.posterUrl === "string" ? value.posterUrl : null,
43
+ curtain: isRecord(value.curtain) && typeof value.curtain.url === "string" ? {
44
+ kind: value.curtain.kind === "video" ? "video" : "image",
45
+ url: value.curtain.url
46
+ } : null
47
+ };
48
+ }
49
+ async function fetchPublicStream(apiUrl, streamId, fetchFn = fetch) {
50
+ const res = await fetchFn(
51
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
52
+ { cache: "no-store" }
53
+ );
54
+ if (!res.ok) return null;
55
+ return parsePublicStream(await res.json());
56
+ }
57
+ async function fetchPublicVod(apiUrl, vodId, fetchFn = fetch) {
58
+ const res = await fetchFn(
59
+ `${apiUrl.replace(/\/$/, "")}/public/vods/${encodeURIComponent(vodId)}`,
60
+ { cache: "no-store" }
61
+ );
62
+ if (!res.ok) return null;
63
+ return parsePublicVod(await res.json());
64
+ }
65
+ async function heartbeatPresence(apiUrl, streamId, sessionId, fetchFn = fetch) {
66
+ await fetchFn(
67
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}/presence`,
68
+ {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({ sessionId })
72
+ }
73
+ ).catch(() => void 0);
74
+ }
75
+
76
+ // src/theme.ts
77
+ var HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
78
+ var COLOR_FN = /^(?:oklch|rgb|rgba|hsl|hsla)\([0-9a-zA-Z.\s%,./-]+\)$/;
79
+ var RADIUS = /^\d+(?:\.\d+)?(?:px|rem|em)$/;
80
+ var FONT = /^[a-zA-Z0-9\s,"'-]{1,120}$/;
81
+ function sanitizeColor(value) {
82
+ if (typeof value !== "string") return void 0;
83
+ const trimmed = value.trim();
84
+ if (HEX.test(trimmed) || COLOR_FN.test(trimmed)) return trimmed;
85
+ return void 0;
86
+ }
87
+ function sanitizeEmbedTheme(value) {
88
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
89
+ return null;
90
+ }
91
+ const row = value;
92
+ const theme = {};
93
+ const primary = sanitizeColor(row.primary);
94
+ const background = sanitizeColor(row.background);
95
+ const foreground = sanitizeColor(row.foreground);
96
+ if (typeof row.radius === "string" && RADIUS.test(row.radius.trim())) {
97
+ theme.radius = row.radius.trim();
98
+ }
99
+ if (typeof row.fontFamily === "string" && FONT.test(row.fontFamily.trim())) {
100
+ theme.fontFamily = row.fontFamily.trim();
101
+ }
102
+ if (primary) theme.primary = primary;
103
+ if (background) theme.background = background;
104
+ if (foreground) theme.foreground = foreground;
105
+ if (row.mode === "light" || row.mode === "dark") theme.mode = row.mode;
106
+ return Object.keys(theme).length > 0 ? theme : null;
107
+ }
108
+ function encodeEmbedTheme(theme) {
109
+ const json = JSON.stringify(theme);
110
+ const bytes = new TextEncoder().encode(json);
111
+ let binary = "";
112
+ for (const byte of bytes) binary += String.fromCharCode(byte);
113
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
114
+ }
115
+ function embedThemeCssVars(theme) {
116
+ const vars = {};
117
+ if (theme.primary) {
118
+ vars["--livo-primary"] = theme.primary;
119
+ vars["--primary"] = theme.primary;
120
+ }
121
+ if (theme.background) {
122
+ vars["--livo-background"] = theme.background;
123
+ vars["--background"] = theme.background;
124
+ }
125
+ if (theme.foreground) {
126
+ vars["--livo-foreground"] = theme.foreground;
127
+ vars["--foreground"] = theme.foreground;
128
+ }
129
+ if (theme.radius) {
130
+ vars["--livo-radius"] = theme.radius;
131
+ vars["--radius"] = theme.radius;
132
+ }
133
+ if (theme.fontFamily) vars["--font-sans"] = theme.fontFamily;
134
+ return vars;
135
+ }
136
+ function LivoPlayer({
137
+ streamId,
138
+ vodId,
139
+ apiUrl = defaultApiUrl(),
140
+ theme,
141
+ locale,
142
+ autoplay = false,
143
+ muted,
144
+ labels,
145
+ className,
146
+ onStatusChange,
147
+ onProgress,
148
+ onEnded,
149
+ onError
150
+ }) {
151
+ const [stream, setStream] = useState(null);
152
+ const [vod, setVod] = useState(null);
153
+ const safeTheme = sanitizeEmbedTheme(theme ?? {});
154
+ useEffect(() => {
155
+ let cancelled = false;
156
+ async function poll() {
157
+ if (streamId) {
158
+ const next = await fetchPublicStream(apiUrl, streamId);
159
+ if (cancelled) return;
160
+ if (!next) {
161
+ onError?.({ code: "not_found" });
162
+ return;
163
+ }
164
+ setStream(next);
165
+ onStatusChange?.(next.status);
166
+ if (next.status === "ended") onEnded?.();
167
+ return;
168
+ }
169
+ if (vodId) {
170
+ const next = await fetchPublicVod(apiUrl, vodId);
171
+ if (cancelled) return;
172
+ if (!next) {
173
+ onError?.({ code: "not_found" });
174
+ return;
175
+ }
176
+ setVod(next);
177
+ onStatusChange?.(next.status);
178
+ }
179
+ }
180
+ void poll();
181
+ const timer = window.setInterval(() => void poll(), 5e3);
182
+ return () => {
183
+ cancelled = true;
184
+ window.clearInterval(timer);
185
+ };
186
+ }, [apiUrl, onEnded, onError, onStatusChange, streamId, vodId]);
187
+ useEffect(() => {
188
+ if (!streamId) return;
189
+ const sessionId = crypto.randomUUID();
190
+ const beat = () => {
191
+ void heartbeatPresence(apiUrl, streamId, sessionId);
192
+ };
193
+ beat();
194
+ const timer = window.setInterval(beat, 15e3);
195
+ return () => window.clearInterval(timer);
196
+ }, [apiUrl, streamId]);
197
+ useEffect(() => {
198
+ const timer = window.setInterval(() => {
199
+ const media = document.querySelector("video");
200
+ if (!media) return;
201
+ onProgress?.({
202
+ currentTime: media.currentTime,
203
+ duration: Number.isFinite(media.duration) ? media.duration : null
204
+ });
205
+ }, 5e3);
206
+ return () => window.clearInterval(timer);
207
+ }, [onProgress]);
208
+ const playbackUrl = stream?.playbackUrl ?? vod?.playbackUrl;
209
+ const style = useMemo(
210
+ () => safeTheme ? embedThemeCssVars(safeTheme) : void 0,
211
+ [safeTheme]
212
+ );
213
+ const rootClass = ["livo-sdk", className].filter(Boolean).join(" ");
214
+ if (!playbackUrl) {
215
+ return /* @__PURE__ */ jsx(
216
+ "div",
217
+ {
218
+ className: rootClass,
219
+ style,
220
+ "data-livo-player": "waiting",
221
+ "data-locale": locale,
222
+ children: stream?.title ?? vod?.title ?? "Livo"
223
+ }
224
+ );
225
+ }
226
+ return /* @__PURE__ */ jsx(
227
+ "div",
228
+ {
229
+ className: rootClass,
230
+ style,
231
+ "data-livo-player": "playing",
232
+ "data-locale": locale,
233
+ children: /* @__PURE__ */ jsx(
234
+ VideoPlayer,
235
+ {
236
+ src: playbackUrl,
237
+ mode: vod || stream?.status === "ended" ? "vod" : "live",
238
+ autoPlay: autoplay,
239
+ muted: muted ?? autoplay,
240
+ fit: "contain",
241
+ labels,
242
+ liveCaptionsBaseUrl: stream?.status === "public" ? stream.captionsUrl ?? void 0 : void 0,
243
+ onError: () => onError?.({ code: "playback" })
244
+ }
245
+ )
246
+ }
247
+ );
248
+ }
249
+
250
+ // src/urls.ts
251
+ function trimSlash(value) {
252
+ return value.replace(/\/$/, "");
253
+ }
254
+ function playerUrl(streamId, options = {}) {
255
+ const origin = trimSlash(options.playerOrigin ?? "https://player.livo.tv");
256
+ const url = new URL(`${origin}/${streamId}`);
257
+ if (options.locale) url.searchParams.set("lang", options.locale);
258
+ if (options.theme)
259
+ url.searchParams.set("theme", encodeEmbedTheme(options.theme));
260
+ return url.toString();
261
+ }
262
+ function vodPlayerUrl(vodId, options = {}) {
263
+ const origin = trimSlash(options.playerOrigin ?? "https://player.livo.tv");
264
+ const url = new URL(`${origin}/vod/${vodId}`);
265
+ if (options.locale) url.searchParams.set("lang", options.locale);
266
+ if (options.theme)
267
+ url.searchParams.set("theme", encodeEmbedTheme(options.theme));
268
+ return url.toString();
269
+ }
270
+ function studioGuestUrl(token, options = {}) {
271
+ const origin = trimSlash(options.appOrigin ?? "https://app.livo.tv");
272
+ const url = new URL(`${origin}/studio/join/${token}`);
273
+ if (options.embed !== false) url.searchParams.set("embed", "1");
274
+ if (options.theme)
275
+ url.searchParams.set("theme", encodeEmbedTheme(options.theme));
276
+ return url.toString();
277
+ }
278
+ function studioHostUrl(token, options = {}) {
279
+ const origin = trimSlash(options.appOrigin ?? "https://app.livo.tv");
280
+ const url = new URL(`${origin}/studio/host/${token}`);
281
+ if (options.embed !== false) url.searchParams.set("embed", "1");
282
+ if (options.theme)
283
+ url.searchParams.set("theme", encodeEmbedTheme(options.theme));
284
+ return url.toString();
285
+ }
286
+ function LivoStudio({
287
+ guestToken,
288
+ hostToken,
289
+ appOrigin,
290
+ theme,
291
+ className,
292
+ title = "Livo studio",
293
+ onEvent
294
+ }) {
295
+ const safeTheme = sanitizeEmbedTheme(theme ?? {});
296
+ const src = useMemo(() => {
297
+ if (hostToken) {
298
+ return studioHostUrl(hostToken, {
299
+ appOrigin,
300
+ theme: safeTheme ?? void 0
301
+ });
302
+ }
303
+ if (guestToken) {
304
+ return studioGuestUrl(guestToken, {
305
+ appOrigin,
306
+ theme: safeTheme ?? void 0
307
+ });
308
+ }
309
+ return null;
310
+ }, [appOrigin, guestToken, hostToken, safeTheme]);
311
+ useEffect(() => {
312
+ if (!onEvent) return;
313
+ const emit = onEvent;
314
+ function onMessage(event) {
315
+ const data = event.data;
316
+ if (typeof data !== "object" || data === null || data.source !== "livo") {
317
+ return;
318
+ }
319
+ emit({
320
+ type: String(data.type),
321
+ payload: data.payload
322
+ });
323
+ }
324
+ window.addEventListener("message", onMessage);
325
+ return () => window.removeEventListener("message", onMessage);
326
+ }, [onEvent]);
327
+ if (!src) return null;
328
+ return /* @__PURE__ */ jsx(
329
+ "iframe",
330
+ {
331
+ src,
332
+ title,
333
+ className,
334
+ style: { border: 0, width: "100%", minHeight: 480 },
335
+ allow: "camera; microphone; display-capture; autoplay; fullscreen",
336
+ allowFullScreen: true
337
+ }
338
+ );
339
+ }
340
+
341
+ export { LivoPlayer, LivoStudio, encodeEmbedTheme, playerUrl, sanitizeEmbedTheme, studioGuestUrl, studioHostUrl, vodPlayerUrl };
342
+ //# sourceMappingURL=index.js.map
343
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/public-api.ts","../src/theme.ts","../src/player.tsx","../src/urls.ts","../src/studio.tsx"],"names":["useMemo","useEffect","jsx"],"mappings":"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","file":"index.js","sourcesContent":["export type PublicStream = {\n\tid: string;\n\ttitle: string;\n\tstatus: string;\n\tplaybackUrl: string | null;\n\tposterUrl?: string | null;\n\tcaptionsUrl: string | null;\n\tviewers: number;\n\tcurtain: { kind: \"image\" | \"video\"; url: string } | null;\n};\n\nexport type PublicVod = {\n\tid: string;\n\ttitle: string;\n\tstatus: string;\n\tplaybackUrl: string | null;\n\tposterUrl: string | null;\n\tcurtain: { kind: \"image\" | \"video\"; url: string } | null;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function defaultApiUrl(): string {\n\treturn \"https://media-svc.livo.tv\";\n}\n\nexport function parsePublicStream(value: unknown): PublicStream | null {\n\tif (!isRecord(value) || typeof value.id !== \"string\") return null;\n\tif (typeof value.title !== \"string\" || typeof value.status !== \"string\") {\n\t\treturn null;\n\t}\n\treturn {\n\t\tid: value.id,\n\t\ttitle: value.title,\n\t\tstatus: value.status,\n\t\tplaybackUrl:\n\t\t\ttypeof value.playbackUrl === \"string\" ? value.playbackUrl : null,\n\t\tcaptionsUrl:\n\t\t\ttypeof value.captionsUrl === \"string\" ? value.captionsUrl : null,\n\t\tviewers: typeof value.viewers === \"number\" ? value.viewers : 0,\n\t\tcurtain:\n\t\t\tisRecord(value.curtain) && typeof value.curtain.url === \"string\"\n\t\t\t\t? {\n\t\t\t\t\t\tkind: value.curtain.kind === \"video\" ? \"video\" : \"image\",\n\t\t\t\t\t\turl: value.curtain.url,\n\t\t\t\t\t}\n\t\t\t\t: null,\n\t};\n}\n\nexport function parsePublicVod(value: unknown): PublicVod | null {\n\tif (!isRecord(value) || typeof value.id !== \"string\") return null;\n\tif (typeof value.title !== \"string\" || typeof value.status !== \"string\") {\n\t\treturn null;\n\t}\n\treturn {\n\t\tid: value.id,\n\t\ttitle: value.title,\n\t\tstatus: value.status,\n\t\tplaybackUrl:\n\t\t\ttypeof value.playbackUrl === \"string\" ? value.playbackUrl : null,\n\t\tposterUrl: typeof value.posterUrl === \"string\" ? value.posterUrl : null,\n\t\tcurtain:\n\t\t\tisRecord(value.curtain) && typeof value.curtain.url === \"string\"\n\t\t\t\t? {\n\t\t\t\t\t\tkind: value.curtain.kind === \"video\" ? \"video\" : \"image\",\n\t\t\t\t\t\turl: value.curtain.url,\n\t\t\t\t\t}\n\t\t\t\t: null,\n\t};\n}\n\nexport async function fetchPublicStream(\n\tapiUrl: string,\n\tstreamId: string,\n\tfetchFn: typeof fetch = fetch,\n): Promise<PublicStream | null> {\n\tconst res = await fetchFn(\n\t\t`${apiUrl.replace(/\\/$/, \"\")}/public/streams/${encodeURIComponent(streamId)}`,\n\t\t{ cache: \"no-store\" },\n\t);\n\tif (!res.ok) return null;\n\treturn parsePublicStream(await res.json());\n}\n\nexport async function fetchPublicVod(\n\tapiUrl: string,\n\tvodId: string,\n\tfetchFn: typeof fetch = fetch,\n): Promise<PublicVod | null> {\n\tconst res = await fetchFn(\n\t\t`${apiUrl.replace(/\\/$/, \"\")}/public/vods/${encodeURIComponent(vodId)}`,\n\t\t{ cache: \"no-store\" },\n\t);\n\tif (!res.ok) return null;\n\treturn parsePublicVod(await res.json());\n}\n\nexport async function heartbeatPresence(\n\tapiUrl: string,\n\tstreamId: string,\n\tsessionId: string,\n\tfetchFn: typeof fetch = fetch,\n): Promise<void> {\n\tawait fetchFn(\n\t\t`${apiUrl.replace(/\\/$/, \"\")}/public/streams/${encodeURIComponent(streamId)}/presence`,\n\t\t{\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({ sessionId }),\n\t\t},\n\t).catch(() => undefined);\n}\n","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","import { VideoPlayer, type VideoPlayerLabels } from \"@livo-tv/blocks\";\nimport { useEffect, useMemo, useState, type CSSProperties } from \"react\";\nimport {\n\tdefaultApiUrl,\n\tfetchPublicStream,\n\tfetchPublicVod,\n\theartbeatPresence,\n\ttype PublicStream,\n\ttype PublicVod,\n} from \"./public-api\";\nimport {\n\tembedThemeCssVars,\n\tsanitizeEmbedTheme,\n\ttype EmbedTheme,\n} from \"./theme\";\n\nexport type LivoPlayerProps = {\n\tstreamId?: string;\n\tvodId?: string;\n\tapiUrl?: string;\n\ttheme?: EmbedTheme;\n\tlocale?: string;\n\tautoplay?: boolean;\n\tmuted?: boolean;\n\tlabels?: VideoPlayerLabels;\n\tclassName?: string;\n\tonStatusChange?: (status: string) => void;\n\tonProgress?: (payload: {\n\t\tcurrentTime: number;\n\t\tduration: number | null;\n\t}) => void;\n\tonEnded?: () => void;\n\tonError?: (error: { code: string }) => void;\n};\n\nexport function LivoPlayer({\n\tstreamId,\n\tvodId,\n\tapiUrl = defaultApiUrl(),\n\ttheme,\n\tlocale,\n\tautoplay = false,\n\tmuted,\n\tlabels,\n\tclassName,\n\tonStatusChange,\n\tonProgress,\n\tonEnded,\n\tonError,\n}: LivoPlayerProps) {\n\tconst [stream, setStream] = useState<PublicStream | null>(null);\n\tconst [vod, setVod] = useState<PublicVod | null>(null);\n\tconst safeTheme = sanitizeEmbedTheme(theme ?? {});\n\n\tuseEffect(() => {\n\t\tlet cancelled = false;\n\t\tasync function poll() {\n\t\t\tif (streamId) {\n\t\t\t\tconst next = await fetchPublicStream(apiUrl, streamId);\n\t\t\t\tif (cancelled) return;\n\t\t\t\tif (!next) {\n\t\t\t\t\tonError?.({ code: \"not_found\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetStream(next);\n\t\t\t\tonStatusChange?.(next.status);\n\t\t\t\tif (next.status === \"ended\") onEnded?.();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (vodId) {\n\t\t\t\tconst next = await fetchPublicVod(apiUrl, vodId);\n\t\t\t\tif (cancelled) return;\n\t\t\t\tif (!next) {\n\t\t\t\t\tonError?.({ code: \"not_found\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetVod(next);\n\t\t\t\tonStatusChange?.(next.status);\n\t\t\t}\n\t\t}\n\t\tvoid poll();\n\t\tconst timer = window.setInterval(() => void poll(), 5000);\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\twindow.clearInterval(timer);\n\t\t};\n\t}, [apiUrl, onEnded, onError, onStatusChange, streamId, vodId]);\n\n\tuseEffect(() => {\n\t\tif (!streamId) return;\n\t\tconst sessionId = crypto.randomUUID();\n\t\tconst beat = () => {\n\t\t\tvoid heartbeatPresence(apiUrl, streamId, sessionId);\n\t\t};\n\t\tbeat();\n\t\tconst timer = window.setInterval(beat, 15_000);\n\t\treturn () => window.clearInterval(timer);\n\t}, [apiUrl, streamId]);\n\n\tuseEffect(() => {\n\t\tconst timer = window.setInterval(() => {\n\t\t\tconst media = document.querySelector(\"video\");\n\t\t\tif (!media) return;\n\t\t\tonProgress?.({\n\t\t\t\tcurrentTime: media.currentTime,\n\t\t\t\tduration: Number.isFinite(media.duration) ? media.duration : null,\n\t\t\t});\n\t\t}, 5000);\n\t\treturn () => window.clearInterval(timer);\n\t}, [onProgress]);\n\n\tconst playbackUrl = stream?.playbackUrl ?? vod?.playbackUrl;\n\tconst style = useMemo(\n\t\t() =>\n\t\t\tsafeTheme ? (embedThemeCssVars(safeTheme) as CSSProperties) : undefined,\n\t\t[safeTheme],\n\t);\n\n\tconst rootClass = [\"livo-sdk\", className].filter(Boolean).join(\" \");\n\n\tif (!playbackUrl) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName={rootClass}\n\t\t\t\tstyle={style}\n\t\t\t\tdata-livo-player=\"waiting\"\n\t\t\t\tdata-locale={locale}\n\t\t\t>\n\t\t\t\t{stream?.title ?? vod?.title ?? \"Livo\"}\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName={rootClass}\n\t\t\tstyle={style}\n\t\t\tdata-livo-player=\"playing\"\n\t\t\tdata-locale={locale}\n\t\t>\n\t\t\t<VideoPlayer\n\t\t\t\tsrc={playbackUrl}\n\t\t\t\tmode={vod || stream?.status === \"ended\" ? \"vod\" : \"live\"}\n\t\t\t\tautoPlay={autoplay}\n\t\t\t\tmuted={muted ?? autoplay}\n\t\t\t\tfit=\"contain\"\n\t\t\t\tlabels={labels}\n\t\t\t\tliveCaptionsBaseUrl={\n\t\t\t\t\tstream?.status === \"public\"\n\t\t\t\t\t\t? (stream.captionsUrl ?? undefined)\n\t\t\t\t\t\t: undefined\n\t\t\t\t}\n\t\t\t\tonError={() => onError?.({ code: \"playback\" })}\n\t\t\t/>\n\t\t</div>\n\t);\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 & { theme?: EmbedTheme; locale?: string } = {},\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.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 & { theme?: EmbedTheme; locale?: string } = {},\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.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","import { useEffect, useMemo, type CSSProperties } from \"react\";\nimport { studioGuestUrl, studioHostUrl } from \"./urls\";\nimport { sanitizeEmbedTheme, type EmbedTheme } from \"./theme\";\n\nexport type LivoStudioEvent = {\n\ttype: string;\n\tpayload?: unknown;\n};\n\nexport type LivoStudioProps = {\n\tguestToken?: string;\n\thostToken?: string;\n\tappOrigin?: string;\n\ttheme?: EmbedTheme;\n\tclassName?: string;\n\ttitle?: string;\n\tonEvent?: (event: LivoStudioEvent) => void;\n};\n\nexport function LivoStudio({\n\tguestToken,\n\thostToken,\n\tappOrigin,\n\ttheme,\n\tclassName,\n\ttitle = \"Livo studio\",\n\tonEvent,\n}: LivoStudioProps) {\n\tconst safeTheme = sanitizeEmbedTheme(theme ?? {});\n\tconst src = useMemo(() => {\n\t\tif (hostToken) {\n\t\t\treturn studioHostUrl(hostToken, {\n\t\t\t\tappOrigin,\n\t\t\t\ttheme: safeTheme ?? undefined,\n\t\t\t});\n\t\t}\n\t\tif (guestToken) {\n\t\t\treturn studioGuestUrl(guestToken, {\n\t\t\t\tappOrigin,\n\t\t\t\ttheme: safeTheme ?? undefined,\n\t\t\t});\n\t\t}\n\t\treturn null;\n\t}, [appOrigin, guestToken, hostToken, safeTheme]);\n\n\tuseEffect(() => {\n\t\tif (!onEvent) return;\n\t\tconst emit = onEvent;\n\t\tfunction onMessage(event: MessageEvent) {\n\t\t\tconst data = event.data;\n\t\t\tif (\n\t\t\t\ttypeof data !== \"object\" ||\n\t\t\t\tdata === null ||\n\t\t\t\t(data as { source?: string }).source !== \"livo\"\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\temit({\n\t\t\t\ttype: String((data as { type: string }).type),\n\t\t\t\tpayload: (data as { payload?: unknown }).payload,\n\t\t\t});\n\t\t}\n\t\twindow.addEventListener(\"message\", onMessage);\n\t\treturn () => window.removeEventListener(\"message\", onMessage);\n\t}, [onEvent]);\n\n\tif (!src) return null;\n\treturn (\n\t\t<iframe\n\t\t\tsrc={src}\n\t\t\ttitle={title}\n\t\t\tclassName={className}\n\t\t\tstyle={\n\t\t\t\t{ border: 0, width: \"100%\", minHeight: 480 } satisfies CSSProperties\n\t\t\t}\n\t\t\tallow=\"camera; microphone; display-capture; autoplay; fullscreen\"\n\t\t\tallowFullScreen\n\t\t/>\n\t);\n}\n"]}
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ // src/theme.ts
4
+ function encodeEmbedTheme(theme) {
5
+ const json = JSON.stringify(theme);
6
+ const bytes = new TextEncoder().encode(json);
7
+ let binary = "";
8
+ for (const byte of bytes) binary += String.fromCharCode(byte);
9
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
+ }
11
+
12
+ // src/urls.ts
13
+ function trimSlash(value) {
14
+ return value.replace(/\/$/, "");
15
+ }
16
+ function studioGuestUrl(token, options = {}) {
17
+ const origin = trimSlash(options.appOrigin ?? "https://app.livo.tv");
18
+ const url = new URL(`${origin}/studio/join/${token}`);
19
+ if (options.embed !== false) url.searchParams.set("embed", "1");
20
+ if (options.theme)
21
+ url.searchParams.set("theme", encodeEmbedTheme(options.theme));
22
+ return url.toString();
23
+ }
24
+
25
+ // src/server.ts
26
+ var LivoServerClient = class {
27
+ apiUrl;
28
+ apiKey;
29
+ fetchFn;
30
+ constructor(options) {
31
+ this.apiUrl = (options.apiUrl ?? "https://media-svc.livo.tv").replace(
32
+ /\/$/,
33
+ ""
34
+ );
35
+ this.apiKey = options.apiKey;
36
+ this.fetchFn = options.fetchFn ?? fetch;
37
+ }
38
+ async request(path, init = {}) {
39
+ const headers = new Headers(init.headers);
40
+ headers.set("X-Api-Key", this.apiKey);
41
+ headers.set("accept", "application/json");
42
+ if (init.body && !headers.has("content-type")) {
43
+ headers.set("content-type", "application/json");
44
+ }
45
+ const res = await this.fetchFn(`${this.apiUrl}${path}`, {
46
+ ...init,
47
+ headers
48
+ });
49
+ const body = await res.json().catch(() => ({}));
50
+ if (!res.ok) {
51
+ const error = new Error(`Livo API ${res.status}`);
52
+ error.status = res.status;
53
+ error.body = body;
54
+ throw error;
55
+ }
56
+ return body;
57
+ }
58
+ createStream(input) {
59
+ return this.request("/streams", {
60
+ method: "POST",
61
+ body: JSON.stringify(input)
62
+ });
63
+ }
64
+ getStream(id) {
65
+ return this.request(
66
+ `/streams/${encodeURIComponent(id)}`
67
+ );
68
+ }
69
+ stopStream(id) {
70
+ return this.request(`/streams/${encodeURIComponent(id)}/stop`, {
71
+ method: "POST"
72
+ });
73
+ }
74
+ listVods() {
75
+ return this.request(
76
+ "/vods"
77
+ );
78
+ }
79
+ mintHostSession(streamId, input) {
80
+ return this.request(`/streams/${encodeURIComponent(streamId)}/studio/host-session`, {
81
+ method: "POST",
82
+ body: JSON.stringify(input)
83
+ });
84
+ }
85
+ };
86
+ function createLivoServerClient(options) {
87
+ return new LivoServerClient(options);
88
+ }
89
+
90
+ exports.LivoServerClient = LivoServerClient;
91
+ exports.createLivoServerClient = createLivoServerClient;
92
+ exports.getGuestLink = studioGuestUrl;
93
+ //# sourceMappingURL=server.cjs.map
94
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +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","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 & { theme?: EmbedTheme; locale?: string } = {},\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.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 & { theme?: EmbedTheme; locale?: string } = {},\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.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: { title: string; mode?: \"instant\" | \"scheduled\" }) {\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\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\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"]}
@@ -0,0 +1,46 @@
1
+ export { a as getGuestLink } from './urls-BXZnKexK.cjs';
2
+
3
+ type LivoServerClientOptions = {
4
+ apiUrl?: string;
5
+ apiKey: string;
6
+ fetchFn?: typeof fetch;
7
+ };
8
+ declare class LivoServerClient {
9
+ private readonly apiUrl;
10
+ private readonly apiKey;
11
+ private readonly fetchFn;
12
+ constructor(options: LivoServerClientOptions);
13
+ private request;
14
+ createStream(input: {
15
+ title: string;
16
+ mode?: "instant" | "scheduled";
17
+ }): Promise<{
18
+ stream: {
19
+ id: string;
20
+ };
21
+ }>;
22
+ getStream(id: string): Promise<{
23
+ stream: {
24
+ id: string;
25
+ status: string;
26
+ };
27
+ }>;
28
+ stopStream(id: string): Promise<unknown>;
29
+ listVods(): Promise<{
30
+ vods: Array<{
31
+ id: string;
32
+ title: string;
33
+ }>;
34
+ }>;
35
+ mintHostSession(streamId: string, input: {
36
+ displayName: string;
37
+ picture?: string;
38
+ }): Promise<{
39
+ hostUrl: string;
40
+ guestUrl: string;
41
+ expiresAt: number;
42
+ }>;
43
+ }
44
+ declare function createLivoServerClient(options: LivoServerClientOptions): LivoServerClient;
45
+
46
+ export { LivoServerClient, type LivoServerClientOptions, createLivoServerClient };
@@ -0,0 +1,46 @@
1
+ export { a as getGuestLink } from './urls-BXZnKexK.js';
2
+
3
+ type LivoServerClientOptions = {
4
+ apiUrl?: string;
5
+ apiKey: string;
6
+ fetchFn?: typeof fetch;
7
+ };
8
+ declare class LivoServerClient {
9
+ private readonly apiUrl;
10
+ private readonly apiKey;
11
+ private readonly fetchFn;
12
+ constructor(options: LivoServerClientOptions);
13
+ private request;
14
+ createStream(input: {
15
+ title: string;
16
+ mode?: "instant" | "scheduled";
17
+ }): Promise<{
18
+ stream: {
19
+ id: string;
20
+ };
21
+ }>;
22
+ getStream(id: string): Promise<{
23
+ stream: {
24
+ id: string;
25
+ status: string;
26
+ };
27
+ }>;
28
+ stopStream(id: string): Promise<unknown>;
29
+ listVods(): Promise<{
30
+ vods: Array<{
31
+ id: string;
32
+ title: string;
33
+ }>;
34
+ }>;
35
+ mintHostSession(streamId: string, input: {
36
+ displayName: string;
37
+ picture?: string;
38
+ }): Promise<{
39
+ hostUrl: string;
40
+ guestUrl: string;
41
+ expiresAt: number;
42
+ }>;
43
+ }
44
+ declare function createLivoServerClient(options: LivoServerClientOptions): LivoServerClient;
45
+
46
+ export { LivoServerClient, type LivoServerClientOptions, createLivoServerClient };