@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 +15 -79
- package/dist/index.cjs +2 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -7
- package/dist/index.d.ts +5 -7
- package/dist/index.js +3 -41
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +2 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -8
- package/dist/server.d.ts +3 -8
- package/dist/server.js +2 -3
- package/dist/server.js.map +1 -1
- package/dist/studio/index.cjs +21 -51
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.cts +3 -4
- package/dist/studio/index.d.ts +3 -4
- package/dist/studio/index.js +22 -51
- package/dist/studio/index.js.map +1 -1
- package/dist/{theme-DBX7D2y9.d.cts → theme-CjyIlZUs.d.cts} +2 -4
- package/dist/{theme-DBX7D2y9.d.ts → theme-CjyIlZUs.d.ts} +2 -4
- package/dist/{urls-XJsScURl.d.cts → urls-BEamzWIG.d.cts} +1 -1
- package/dist/{urls-lKMjRmmv.d.ts → urls-CLHoM-31.d.ts} +1 -1
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -1,75 +1,17 @@
|
|
|
1
1
|
# @livo-tv/sdk
|
|
2
2
|
|
|
3
|
-
Embed Livo **
|
|
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,
|
|
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
|
-
|
|
16
|
-
|
|
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 {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 (!
|
|
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;
|