@livo-tv/sdk 1.11.0-rc.4 → 1.11.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
@@ -1,75 +1,17 @@
1
1
  # @livo-tv/sdk
2
2
 
3
- Embed Livo **in-DOM** on your origin. Native React is the partner path: `LivoPlayer`, `LivoHostStudio`, and `LivoGuestStudio`. Do not iframe `player.livo.tv`.
3
+ Embed the Livo **player** (native React) and **studio** (native room or themed iframe) in a third-party app.
4
4
 
5
5
  ```tsx
6
- import { LivoPlayer, applyEmbedTheme, type EmbedTheme } from "@livo-tv/sdk";
7
- import {
8
- LivoHostStudio,
9
- LivoGuestStudio,
10
- StudioWindowGate,
11
- studioRoomLabels,
12
- } from "@livo-tv/sdk/studio";
6
+ import { LivoPlayer, LivoStudio, playerUrl } from "@livo-tv/sdk";
7
+ import { LivoHostStudio, LivoGuestStudio } from "@livo-tv/sdk/studio";
13
8
  import "@livo-tv/sdk/styles.css";
14
9
 
15
- const theme: EmbedTheme = {
16
- accent: "#0F6FF5",
17
- background: "#0b1220",
18
- foreground: "#f8fafc",
19
- radius: "8px",
20
- fontFamily: "Inter, sans-serif",
21
- mode: "dark",
22
- };
23
-
24
- <LivoPlayer
25
- streamId="s1"
26
- theme={theme}
27
- onPlaybackState={(state) => {
28
- /* loading | waiting | playing | paused | ended | error */
29
- }}
30
- onError={(error) => {
31
- /* not_found | playback */
32
- }}
33
- />;
10
+ <LivoPlayer streamId="s1" theme={{ primary: "#2563eb", mode: "dark" }} />
11
+ <LivoStudio guestToken="..." theme={{ primary: "#2563eb" }} />
34
12
  ```
35
13
 
36
- `@livo-tv/sdk`, `@livo-tv/sdk/studio`, and `@livo-tv/sdk/condo` (`LivoCommunity`) are client entries (the published bundles start with `"use client"`). Import them from `"use client"` files in Next.js App Router — do not import those entries from a Server Component page. Tailwind hosts must `@source` `node_modules/@livo-tv/sdk/dist/*.{js,cjs,mjs}` like `@livo-tv/blocks`.
37
-
38
- Root `<LivoStudio>` still iframes `app.livo.tv` and is **not** the partner contract. Native studio has no runtime dependency on `player.livo.tv` (no `postMessage` bridge, no hard-coded player origin). The hosted-player URL helpers (`playerUrl`) stay available for Livo's own player app.
39
-
40
- ## Native studio (RealtimeKit)
41
-
42
- Peers (install in the host app):
43
-
44
- - `@cloudflare/realtimekit` (optional peer, lazy-imported)
45
- - `react` / `react-dom`
46
- - `hls.js` for `<LivoPlayer>`
47
-
48
- Send this header on the route that mounts the studio (camera, mic, and screen share):
49
-
50
- ```
51
- Permissions-Policy: camera=(self), microphone=(self), display-capture=(self)
52
- ```
53
-
54
- Wrap the studio in `StudioWindowGate` with a **stable** `lockKey` (stream id or host token), not the partner pathname. The lock uses `BroadcastChannel` and is origin-scoped, so `/dashboard/studio` and `/events/123/studio` on the same origin still see each other.
55
-
56
- ```tsx
57
- <StudioWindowGate lockKey={`host:${streamId}`} labels={gateLabels}>
58
- <LivoHostStudio
59
- token={hostToken}
60
- apiUrl="https://api.livo.tv"
61
- labels={studioRoomLabels((key) => key)}
62
- embed
63
- theme={theme}
64
- />
65
- </StudioWindowGate>
66
- ```
67
-
68
- `theme` is an `EmbedTheme` object (or a hosted encoded string). `applyEmbedTheme` writes CSS variables onto **the studio container**, not `document.documentElement`, so a partner shell keeps its own chrome.
69
-
70
- Omit `playerOrigin` in native embeds. The watch-share control only appears when you pass a player origin; native partners should share their own watch URL.
71
-
72
- Regression page: `pnpm example:embed` serves `examples/partner-embed` on `http://localhost:3003`.
14
+ `@livo-tv/sdk`, `@livo-tv/sdk/studio`, and `@livo-tv/sdk/condo` (`LivoCommunity`) are client entries (the published bundles start with `"use client"`). Import them from `"use client"` files in Next.js App Router — do not import those entries from a Server Component page. Tailwind hosts must `@source` `node_modules/@livo-tv/sdk/dist/*.{js,cjs,mjs}` like `@livo-tv/blocks`. RealtimeKit is an optional peer (`@cloudflare/realtimekit`). Root `<LivoStudio>` remains the iframe fallback.
73
15
 
74
16
  Community (condo) — comments on every video, live Q&A only while a stream or webinar is `preview` / `public`. After the event, Q&A stays closed. Comments stay closed unless the organizer sets `commentsAfterEnd`.
75
17
 
@@ -78,10 +20,12 @@ Community (condo) — comments on every video, live Q&A only while a stream or w
78
20
  **Custom layout:** mount `<LivoCommunity>` next to a theater-only player, or call `createCommunityClient` / the public HTTP API for a fully custom UI. Pass a public `apiUrl` for viewers (`mode="viewer"`, polls). Pass JWT / `X-Api-Key` headers and `mode="control"` for settings, pin/hide/delete, and Q&A highlight/dismiss/answer. Unregistered posters send `displayName`, optional `picture` URL, and `guestId`. Supply `displayName` (and optional `picture`) to skip the name field; otherwise the composer asks once and stores `livo-community-identity`. Hosted iframes use `?name=` / `?avatar=` when the built-in panel is on.
79
21
 
80
22
  ```tsx
81
- import { LivoPlayer } from "@livo-tv/sdk";
23
+ import { LivoPlayer, playerUrl } from "@livo-tv/sdk";
82
24
  import { LivoCommunity } from "@livo-tv/sdk/condo";
83
25
  import "@livo-tv/sdk/styles.css";
84
26
 
27
+ <iframe src={playerUrl(stream.id, { community: false })} />
28
+
85
29
  <LivoPlayer streamId={stream.id} />
86
30
 
87
31
  <LivoCommunity
@@ -116,20 +60,12 @@ Server (Node / Workers):
116
60
  import { createLivoServerClient } from "@livo-tv/sdk/server";
117
61
 
118
62
  const livo = createLivoServerClient({ apiKey: process.env.LIVO_API_KEY! });
119
- const { vodId, uploadId, partSize } = await livo.createVod({
120
- filename: "talk.mp4",
121
- contentType: "video/mp4",
122
- byteSize: 12_000_000,
123
- sha256: "64-lowercase-hex",
124
- });
125
- await livo.completeVodUpload(vodId, {
126
- parts: [{ partNumber: 1, etag: '"etag-from-put"' }],
127
- });
128
- const { hostToken, guestToken } = await livo.mintHostSession(streamId, {
129
- displayName: "Host",
130
- });
63
+ const { hostUrl, guestUrl, hostToken, guestToken } = await livo.mintHostSession(
64
+ streamId,
65
+ {
66
+ displayName: "Host",
67
+ },
68
+ );
131
69
  ```
132
70
 
133
- Browser part `PUT`s go to the presigned R2 URL. Bucket CORS is `PUT *` with `ETag` exposed; mint the URL with `lk_` on your server.
134
-
135
71
  GitHub `livo-tv/sdk` + npm OIDC publish are a human handoff (same as `@livo-tv/blocks`).
package/dist/index.cjs CHANGED
@@ -1008,7 +1008,6 @@ function sanitizeEmbedTheme(value) {
1008
1008
  const row = value;
1009
1009
  const theme = {};
1010
1010
  const primary = sanitizeColor(row.primary);
1011
- const accent = sanitizeColor(row.accent);
1012
1011
  const background = sanitizeColor(row.background);
1013
1012
  const foreground = sanitizeColor(row.foreground);
1014
1013
  if (typeof row.radius === "string" && RADIUS.test(row.radius.trim())) {
@@ -1018,7 +1017,6 @@ function sanitizeEmbedTheme(value) {
1018
1017
  theme.fontFamily = row.fontFamily.trim();
1019
1018
  }
1020
1019
  if (primary) theme.primary = primary;
1021
- if (accent) theme.accent = accent;
1022
1020
  if (background) theme.background = background;
1023
1021
  if (foreground) theme.foreground = foreground;
1024
1022
  if (row.mode === "light" || row.mode === "dark") theme.mode = row.mode;
@@ -1033,17 +1031,9 @@ function encodeEmbedTheme(theme) {
1033
1031
  }
1034
1032
  function embedThemeCssVars(theme) {
1035
1033
  const vars = {};
1036
- const accent = theme.accent ?? theme.primary;
1037
1034
  if (theme.primary) {
1038
1035
  vars["--livo-primary"] = theme.primary;
1039
1036
  vars["--primary"] = theme.primary;
1040
- } else if (accent) {
1041
- vars["--livo-primary"] = accent;
1042
- vars["--primary"] = accent;
1043
- }
1044
- if (accent) {
1045
- vars["--livo-accent"] = accent;
1046
- vars["--accent"] = accent;
1047
1037
  }
1048
1038
  if (theme.background) {
1049
1039
  vars["--livo-background"] = theme.background;
@@ -1072,12 +1062,8 @@ function decodeEmbedTheme(raw) {
1072
1062
  return null;
1073
1063
  }
1074
1064
  }
1075
- function resolveEmbedThemeInput(value) {
1076
- if (typeof value === "string") return decodeEmbedTheme(value);
1077
- return sanitizeEmbedTheme(value ?? null);
1078
- }
1079
1065
  function applyEmbedTheme(root, theme) {
1080
- if (!root || !theme) return;
1066
+ if (!theme) return;
1081
1067
  if (theme.mode === "dark") root.classList.add("dark");
1082
1068
  if (theme.mode === "light") root.classList.remove("dark");
1083
1069
  for (const [key, value] of Object.entries(embedThemeCssVars(theme))) {
@@ -1197,7 +1183,6 @@ function LivoPlayer({
1197
1183
  waitingLabels,
1198
1184
  className,
1199
1185
  onStatusChange,
1200
- onPlaybackState,
1201
1186
  onProgress,
1202
1187
  onEnded,
1203
1188
  onError,
@@ -1239,7 +1224,6 @@ function LivoPlayer({
1239
1224
  if (cancelled) return;
1240
1225
  if (!next) {
1241
1226
  onError?.({ code: "not_found" });
1242
- onPlaybackState?.("error");
1243
1227
  return;
1244
1228
  }
1245
1229
  setStream(next);
@@ -1253,7 +1237,6 @@ function LivoPlayer({
1253
1237
  if (cancelled) return;
1254
1238
  if (!next) {
1255
1239
  onError?.({ code: "not_found" });
1256
- onPlaybackState?.("error");
1257
1240
  return;
1258
1241
  }
1259
1242
  setVod(next);
@@ -1267,15 +1250,7 @@ function LivoPlayer({
1267
1250
  cancelled = true;
1268
1251
  window.clearInterval(timer);
1269
1252
  };
1270
- }, [
1271
- apiUrl,
1272
- onEnded,
1273
- onError,
1274
- onPlaybackState,
1275
- onStatusChange,
1276
- streamId,
1277
- vodId
1278
- ]);
1253
+ }, [apiUrl, onEnded, onError, onStatusChange, streamId, vodId]);
1279
1254
  react.useEffect(() => {
1280
1255
  if (!streamId) return;
1281
1256
  const sessionId = crypto.randomUUID();
@@ -1302,15 +1277,6 @@ function LivoPlayer({
1302
1277
  () => safeTheme ? embedThemeCssVars(safeTheme) : void 0,
1303
1278
  [safeTheme]
1304
1279
  );
1305
- react.useEffect(() => {
1306
- if (!playbackUrl) {
1307
- onPlaybackState?.(
1308
- stream?.status === "ended" ? "ended" : resolved ? "waiting" : "loading"
1309
- );
1310
- return;
1311
- }
1312
- onPlaybackState?.("playing");
1313
- }, [onPlaybackState, playbackUrl, resolved, stream?.status]);
1314
1280
  const flags = stream?.community ?? vod?.community;
1315
1281
  const communityTargetId = streamId ?? vodId;
1316
1282
  const communityKind = streamId ? "stream" : "vod";
@@ -1392,9 +1358,6 @@ function LivoPlayer({
1392
1358
  captionsEnabled: stream ? Boolean(stream.captionsUrl) : Boolean(vod?.captionsEnabled),
1393
1359
  liveCaptionsBaseUrl: stream?.status === "public" ? stream.captionsUrl ?? void 0 : void 0,
1394
1360
  onFirstPlay: () => beacon?.track({ type: "view" }),
1395
- onPlayingChange: (playing) => {
1396
- onPlaybackState?.(playing ? "playing" : "paused");
1397
- },
1398
1361
  onPlaybackProgress: (currentTime, duration) => {
1399
1362
  beacon?.track({ type: "progress", currentTime, duration });
1400
1363
  onProgress?.({
@@ -1414,7 +1377,6 @@ function LivoPlayer({
1414
1377
  errorCode: error && typeof error === "object" && "kind" in error ? String(error.kind) : "playback"
1415
1378
  });
1416
1379
  onError?.({ code: "playback" });
1417
- onPlaybackState?.("error");
1418
1380
  }
1419
1381
  }
1420
1382
  )
@@ -1533,7 +1495,6 @@ exports.decodeEmbedTheme = decodeEmbedTheme;
1533
1495
  exports.embedShowsCommunity = embedShowsCommunity;
1534
1496
  exports.encodeEmbedTheme = encodeEmbedTheme;
1535
1497
  exports.playerUrl = playerUrl;
1536
- exports.resolveEmbedThemeInput = resolveEmbedThemeInput;
1537
1498
  exports.sanitizeEmbedTheme = sanitizeEmbedTheme;
1538
1499
  exports.studioGuestUrl = studioGuestUrl;
1539
1500
  exports.studioHostUrl = studioHostUrl;