@livo-tv/sdk
v1.13.0
Published
Embed the Livo player and studio in a third-party app
Maintainers
Readme
@livo-tv/sdk
Embed Livo in-DOM on your origin. Native React is the partner path: LivoPlayer, LivoHostStudio, and LivoGuestStudio. Do not iframe player.livo.tv.
import { LivoPlayer, applyEmbedTheme, type EmbedTheme } from "@livo-tv/sdk";
import {
LivoHostStudio,
LivoGuestStudio,
StudioWindowGate,
studioRoomLabels,
} from "@livo-tv/sdk/studio";
import "@livo-tv/sdk/styles.css";
const theme: EmbedTheme = {
accent: "#0F6FF5",
background: "#0b1220",
foreground: "#f8fafc",
radius: "8px",
fontFamily: "Inter, sans-serif",
mode: "dark",
};
<LivoPlayer
streamId="s1"
theme={theme}
onPlaybackState={(state) => {
/* loading | waiting | playing | paused | ended | error */
}}
onError={(error) => {
/* not_found | playback */
}}
/>;@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. Do not install @livo-tv/blocks or add transpilePackages — the SDK bundles its UI internals. Tailwind hosts that restyle the embed can @source node_modules/@livo-tv/sdk/dist/*.{js,cjs,mjs}.
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.
Native studio (RealtimeKit)
Install:
pnpm add @livo-tv/sdk
# studio only:
pnpm add @cloudflare/realtimekitPeers:
react/react-dom(required)@cloudflare/realtimekit(optional, lazy-imported; studio only)hls.js(optional, for<LivoPlayer>on browsers without native HLS)
Do not add @livo-tv/blocks as a host dependency. The published player and studio bundles inline it (and the UI kit). Next.js does not need transpilePackages.
Send this header on the route that mounts the studio (camera, mic, and screen share):
Permissions-Policy: camera=(self), microphone=(self), display-capture=(self)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.
<StudioWindowGate lockKey={`host:${streamId}`} labels={gateLabels}>
<LivoHostStudio
token={hostToken}
apiUrl="https://api.livo.tv"
labels={studioRoomLabels((key) => key)}
embed
theme={theme}
/>
</StudioWindowGate>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.
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.
Regression page: pnpm example:embed serves examples/partner-embed on http://localhost:3003.
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.
Theater-only: native <LivoPlayer> does not show comments/Q&A unless you pass showCommunity. Hosted iframes show the panel by default; hide it with ?community=0 (or playerUrl(id, { community: false })).
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.
import { LivoPlayer } from "@livo-tv/sdk";
import { LivoCommunity } from "@livo-tv/sdk/condo";
import "@livo-tv/sdk/styles.css";
<LivoPlayer streamId={stream.id} />
<LivoCommunity
apiUrl="https://api.livo.tv"
targetKind="stream"
targetId={stream.id}
flags={stream.community}
mode="viewer"
displayName="Ada"
picture="https://cdn.example/ada.jpg"
guestId="guest-ada"
/>;Headless client (same public routes, your own UI):
import { createCommunityClient } from "@livo-tv/sdk/condo";
const client = createCommunityClient({
apiUrl: "https://api.livo.tv",
targetKind: "stream",
targetId: stream.id,
mode: "viewer",
});
await client.list({ type: "comment" });Server (Node / Workers):
import { createLivoServerClient } from "@livo-tv/sdk/server";
const livo = createLivoServerClient({ apiKey: process.env.LIVO_API_KEY! });
const { vodId, uploadId, partSize } = await livo.createVod({
filename: "talk.mp4",
contentType: "video/mp4",
byteSize: 12_000_000,
sha256: "64-lowercase-hex",
});
await livo.completeVodUpload(vodId, {
parts: [{ partNumber: 1, etag: '"etag-from-put"' }],
});
const { hostToken, guestToken } = await livo.mintHostSession(streamId, {
displayName: "Host",
});Browser part PUTs go to the presigned R2 URL. Bucket CORS is PUT * with ETag exposed; mint the URL with lk_ on your server.
GitHub livo-tv/sdk + npm OIDC publish are a human handoff (same as @livo-tv/blocks).
