@livo-tv/sdk 1.11.0 → 1.12.0-rc.1
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 +79 -15
- package/dist/index.cjs +41 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +41 -3
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +3 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +8 -3
- package/dist/server.d.ts +8 -3
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/studio/index.cjs +51 -21
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.cts +4 -3
- package/dist/studio/index.d.ts +4 -3
- package/dist/studio/index.js +51 -22
- package/dist/studio/index.js.map +1 -1
- package/dist/{theme-CjyIlZUs.d.cts → theme-DBX7D2y9.d.cts} +4 -2
- package/dist/{theme-CjyIlZUs.d.ts → theme-DBX7D2y9.d.ts} +4 -2
- package/dist/{urls-BEamzWIG.d.cts → urls-XJsScURl.d.cts} +1 -1
- package/dist/{urls-CLHoM-31.d.ts → urls-lKMjRmmv.d.ts} +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,17 +1,75 @@
|
|
|
1
1
|
# @livo-tv/sdk
|
|
2
2
|
|
|
3
|
-
Embed
|
|
3
|
+
Embed Livo **in-DOM** on your origin. Native React is the partner path: `LivoPlayer`, `LivoHostStudio`, and `LivoGuestStudio`. Do not iframe `player.livo.tv`.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
import { LivoPlayer,
|
|
7
|
-
import {
|
|
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";
|
|
8
13
|
import "@livo-tv/sdk/styles.css";
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
/>;
|
|
12
34
|
```
|
|
13
35
|
|
|
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`.
|
|
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`.
|
|
15
73
|
|
|
16
74
|
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`.
|
|
17
75
|
|
|
@@ -20,12 +78,10 @@ Community (condo) — comments on every video, live Q&A only while a stream or w
|
|
|
20
78
|
**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.
|
|
21
79
|
|
|
22
80
|
```tsx
|
|
23
|
-
import { LivoPlayer
|
|
81
|
+
import { LivoPlayer } from "@livo-tv/sdk";
|
|
24
82
|
import { LivoCommunity } from "@livo-tv/sdk/condo";
|
|
25
83
|
import "@livo-tv/sdk/styles.css";
|
|
26
84
|
|
|
27
|
-
<iframe src={playerUrl(stream.id, { community: false })} />
|
|
28
|
-
|
|
29
85
|
<LivoPlayer streamId={stream.id} />
|
|
30
86
|
|
|
31
87
|
<LivoCommunity
|
|
@@ -60,12 +116,20 @@ Server (Node / Workers):
|
|
|
60
116
|
import { createLivoServerClient } from "@livo-tv/sdk/server";
|
|
61
117
|
|
|
62
118
|
const livo = createLivoServerClient({ apiKey: process.env.LIVO_API_KEY! });
|
|
63
|
-
const {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
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
|
+
});
|
|
69
131
|
```
|
|
70
132
|
|
|
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
|
+
|
|
71
135
|
GitHub `livo-tv/sdk` + npm OIDC publish are a human handoff (same as `@livo-tv/blocks`).
|
package/dist/index.cjs
CHANGED
|
@@ -1008,6 +1008,7 @@ 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);
|
|
1011
1012
|
const background = sanitizeColor(row.background);
|
|
1012
1013
|
const foreground = sanitizeColor(row.foreground);
|
|
1013
1014
|
if (typeof row.radius === "string" && RADIUS.test(row.radius.trim())) {
|
|
@@ -1017,6 +1018,7 @@ function sanitizeEmbedTheme(value) {
|
|
|
1017
1018
|
theme.fontFamily = row.fontFamily.trim();
|
|
1018
1019
|
}
|
|
1019
1020
|
if (primary) theme.primary = primary;
|
|
1021
|
+
if (accent) theme.accent = accent;
|
|
1020
1022
|
if (background) theme.background = background;
|
|
1021
1023
|
if (foreground) theme.foreground = foreground;
|
|
1022
1024
|
if (row.mode === "light" || row.mode === "dark") theme.mode = row.mode;
|
|
@@ -1031,9 +1033,17 @@ function encodeEmbedTheme(theme) {
|
|
|
1031
1033
|
}
|
|
1032
1034
|
function embedThemeCssVars(theme) {
|
|
1033
1035
|
const vars = {};
|
|
1036
|
+
const accent = theme.accent ?? theme.primary;
|
|
1034
1037
|
if (theme.primary) {
|
|
1035
1038
|
vars["--livo-primary"] = theme.primary;
|
|
1036
1039
|
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;
|
|
1037
1047
|
}
|
|
1038
1048
|
if (theme.background) {
|
|
1039
1049
|
vars["--livo-background"] = theme.background;
|
|
@@ -1062,8 +1072,12 @@ function decodeEmbedTheme(raw) {
|
|
|
1062
1072
|
return null;
|
|
1063
1073
|
}
|
|
1064
1074
|
}
|
|
1075
|
+
function resolveEmbedThemeInput(value) {
|
|
1076
|
+
if (typeof value === "string") return decodeEmbedTheme(value);
|
|
1077
|
+
return sanitizeEmbedTheme(value ?? null);
|
|
1078
|
+
}
|
|
1065
1079
|
function applyEmbedTheme(root, theme) {
|
|
1066
|
-
if (!theme) return;
|
|
1080
|
+
if (!root || !theme) return;
|
|
1067
1081
|
if (theme.mode === "dark") root.classList.add("dark");
|
|
1068
1082
|
if (theme.mode === "light") root.classList.remove("dark");
|
|
1069
1083
|
for (const [key, value] of Object.entries(embedThemeCssVars(theme))) {
|
|
@@ -1183,6 +1197,7 @@ function LivoPlayer({
|
|
|
1183
1197
|
waitingLabels,
|
|
1184
1198
|
className,
|
|
1185
1199
|
onStatusChange,
|
|
1200
|
+
onPlaybackState,
|
|
1186
1201
|
onProgress,
|
|
1187
1202
|
onEnded,
|
|
1188
1203
|
onError,
|
|
@@ -1224,6 +1239,7 @@ function LivoPlayer({
|
|
|
1224
1239
|
if (cancelled) return;
|
|
1225
1240
|
if (!next) {
|
|
1226
1241
|
onError?.({ code: "not_found" });
|
|
1242
|
+
onPlaybackState?.("error");
|
|
1227
1243
|
return;
|
|
1228
1244
|
}
|
|
1229
1245
|
setStream(next);
|
|
@@ -1237,6 +1253,7 @@ function LivoPlayer({
|
|
|
1237
1253
|
if (cancelled) return;
|
|
1238
1254
|
if (!next) {
|
|
1239
1255
|
onError?.({ code: "not_found" });
|
|
1256
|
+
onPlaybackState?.("error");
|
|
1240
1257
|
return;
|
|
1241
1258
|
}
|
|
1242
1259
|
setVod(next);
|
|
@@ -1250,7 +1267,15 @@ function LivoPlayer({
|
|
|
1250
1267
|
cancelled = true;
|
|
1251
1268
|
window.clearInterval(timer);
|
|
1252
1269
|
};
|
|
1253
|
-
}, [
|
|
1270
|
+
}, [
|
|
1271
|
+
apiUrl,
|
|
1272
|
+
onEnded,
|
|
1273
|
+
onError,
|
|
1274
|
+
onPlaybackState,
|
|
1275
|
+
onStatusChange,
|
|
1276
|
+
streamId,
|
|
1277
|
+
vodId
|
|
1278
|
+
]);
|
|
1254
1279
|
react.useEffect(() => {
|
|
1255
1280
|
if (!streamId) return;
|
|
1256
1281
|
const sessionId = crypto.randomUUID();
|
|
@@ -1277,6 +1302,15 @@ function LivoPlayer({
|
|
|
1277
1302
|
() => safeTheme ? embedThemeCssVars(safeTheme) : void 0,
|
|
1278
1303
|
[safeTheme]
|
|
1279
1304
|
);
|
|
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]);
|
|
1280
1314
|
const flags = stream?.community ?? vod?.community;
|
|
1281
1315
|
const communityTargetId = streamId ?? vodId;
|
|
1282
1316
|
const communityKind = streamId ? "stream" : "vod";
|
|
@@ -1358,6 +1392,9 @@ function LivoPlayer({
|
|
|
1358
1392
|
captionsEnabled: stream ? Boolean(stream.captionsUrl) : Boolean(vod?.captionsEnabled),
|
|
1359
1393
|
liveCaptionsBaseUrl: stream?.status === "public" ? stream.captionsUrl ?? void 0 : void 0,
|
|
1360
1394
|
onFirstPlay: () => beacon?.track({ type: "view" }),
|
|
1395
|
+
onPlayingChange: (playing) => {
|
|
1396
|
+
onPlaybackState?.(playing ? "playing" : "paused");
|
|
1397
|
+
},
|
|
1361
1398
|
onPlaybackProgress: (currentTime, duration) => {
|
|
1362
1399
|
beacon?.track({ type: "progress", currentTime, duration });
|
|
1363
1400
|
onProgress?.({
|
|
@@ -1377,6 +1414,7 @@ function LivoPlayer({
|
|
|
1377
1414
|
errorCode: error && typeof error === "object" && "kind" in error ? String(error.kind) : "playback"
|
|
1378
1415
|
});
|
|
1379
1416
|
onError?.({ code: "playback" });
|
|
1417
|
+
onPlaybackState?.("error");
|
|
1380
1418
|
}
|
|
1381
1419
|
}
|
|
1382
1420
|
)
|
|
@@ -1495,6 +1533,7 @@ exports.decodeEmbedTheme = decodeEmbedTheme;
|
|
|
1495
1533
|
exports.embedShowsCommunity = embedShowsCommunity;
|
|
1496
1534
|
exports.encodeEmbedTheme = encodeEmbedTheme;
|
|
1497
1535
|
exports.playerUrl = playerUrl;
|
|
1536
|
+
exports.resolveEmbedThemeInput = resolveEmbedThemeInput;
|
|
1498
1537
|
exports.sanitizeEmbedTheme = sanitizeEmbedTheme;
|
|
1499
1538
|
exports.studioGuestUrl = studioGuestUrl;
|
|
1500
1539
|
exports.studioHostUrl = studioHostUrl;
|