Skip to content

Migrate from Livepeer Studio

Livepeer Studio and FrameWorks overlap on live streaming basics, but the API shape and a few access-control features differ enough that a straight endpoint swap is not enough.

Livepeer Studio is a hosted Livepeer gateway; FrameWorks is another gateway on the same network, with a different product shape around it. The rest of this page maps Studio’s surface to FrameWorks: what’s a clean swap, what changes shape, and where they diverge.

  • What stays: RTMP and WHIP ingest, HLS and WebRTC playback, multistream targets, clips, recordings.
  • What changes: REST → GraphQL · API key → JWT or developer token · Studio webhooks → FrameWorks webhooks with Standard Webhooks signatures and mapped event names (or the tenantEvents subscription) · playback access policies are recreated with FrameWorks signing keys or webhooks · @livepeer/react → @livepeer-frameworks/player-*.
  • Capabilities to evaluate: E-RTMP ingest (HEVC/AV1 contribution over RTMP) · SRT ingest · LL-HLS/DASH delivery · sprite-sheet seek previews · AV1 in the codec list · self-hosting / hybrid options · MCP, wallet auth, and x402 for agents · Skipper · experimental device-discovery productization.
  • What’s not yet at parity: AI Pipelines and C2PA signing. TypeScript, Go, and Python SDKs ship. See the gap list before planning a production cutover.

The shapes you know in Studio, mapped to FrameWorks:

StudioFrameWorks
POST /streamcreateStream(input) mutation
GET /stream/{id}stream(id) query
PATCH /stream/{id}updateStream(id, input) mutation
DELETE /stream/{id}deleteStream(id) mutation
DELETE /stream/{id}/terminateNo direct equivalent today; stop the encoder, or delete the stream if you are retiring it
record: true flag at createrecord: true at create, or startDVR(streamId) post-create
POST /stream with pull: { source } configcreateStream(input: { ingestMode: PULL, pullSource: { sourceUri }, sourceLocation }) — see Pull-input streams
StudioFrameWorks
POST /clipcreateClip(input) mutation
GET /session/{id}/clipsstorageArtifactsConnection(input: { kinds: [CLIP] }) or clip(id)
POST /asset/request-upload (direct/TUS upload)createVodUpload(input) → S3 multipart upload
POST /asset/upload/urlimportVodAsset(input: { url, filename, title }) — see Import a video from a URL
Tasks API for upload/transcode progressvodUploadStatus, tenantEvents (upload.*), or liveProcessingEvents depending on the workflow
GET /asset/{id}vodAsset(id) query
DELETE /asset/{id}deleteVodAsset(id) mutation
StudioFrameWorks
POST /multistream/targetNo global target library; use createPushTarget(streamId, input)
PATCH /multistream/target/{id}updatePushTarget(id, input) mutation
DELETE /multistream/target/{id}deletePushTarget(id) mutation
Inline or attach target to a streamcreatePushTarget(streamId, input) creates a stream-scoped target
Per-target rendition profile / videoOnlyNot exposed today

Studio uses the term “access control” for two separate concerns: authenticating your application to the API, and deciding whether a viewer can play a stream or asset. FrameWorks covers both: developer tokens handle API access, and playback policies handle viewer-side authorization.

API and dashboard access

StudioFrameWorks
API key (Bearer)Developer token (Bearer) — see Agent Access for wallet/x402 alternatives
Team loginEmail/password login
—Wallet sign-in (EIP-191)

Viewer-side playback access

StudioFrameWorks
Default public playback IDDefault public playback ID
POST /access-control/signing-keycreateSigningKey(input: { name }) — see Playback Access Control
playbackPolicy: { type: "jwt" }setPlaybackPolicy({ streamId, policy: { type: JWT, jwt: { allowedKids } } })
playbackPolicy: { type: "webhook" }setPlaybackPolicy({ ..., policy: { type: WEBHOOK, webhook: { url, secret, timeoutMs } } })
Livepeer-Jwt header?jwt=<token> universally; Authorization: Bearer where playback requests can carry headers
?jwt= query (MP4 only on Studio)?jwt=<token> works for HLS, LL-HLS, DASH, WebRTC/WHEP, and MP4 — single transport
accessKey for a webhook policyPass the key as ?jwt=<key>; your webhook receives it as viewerToken
playback.accessControl webhookWebhook policy: HMAC-signed POST to your URL; any 2xx allows, anything else denies
playbackPolicy.webhookContextwebhook.context: a JSON object sent as context in every access request
playbackPolicy.allowedOriginspolicy.allowedOrigins: same scheme://host[:port] / * entries
playbackPolicy.refreshIntervalNot configurable; the decision holds for the viewer session (about 10 minutes)

FrameWorks events in the right column are delivered as webhooks and over the tenantEvents subscription, with the same payloads.

Studio webhookFrameWorks event
stream.startedstream.live (and stream.connected when the encoder connects)
stream.idlestream.idle
recording.startedrecording.started
recording.waitingrecording.stopped (capture ended, recording being finalized)
recording.readyrecording.ready (or recording.failed)
asset.createdupload.created
asset.updatedupload.completed when processing starts; no per-step progress event
asset.readyupload.ready
asset.failedupload.failed (with a reason)
asset.deletedNo event; the deletion is in your audit log
multistream.connected / .error / .disconnectedmultistream.status_changed (status, previousStatus)
task.*No Task API; clips have clip.requested/ready/failed, imports use upload.*
playback.accessControlWebhook playback policy (setPlaybackPolicy with type: WEBHOOK), a blocking call
playback.user.newliveConnectionEvents(streamId) subscription (Studio never sends this webhook)
stream.detectionNot available (Studio has it switched off)

Payload fields:

StudioFrameWorks
eventtype
id (event ID, stable across retries)id and the webhook-id header (stable across retries and replays)
stream.iddata.streamId
stream.playbackIddata.playbackId on stream.connected, stream.live, stream.idle
stream.name, userTags, other stream fieldsNot in the event; query stream(id)
payload.asset.iddata.artifact.artifactId
payload.asset.snapshot.playbackIdNot in the event; look it up by data.artifact.artifactId (playback IDs can be rotated)
payload.recordingUrl / payload.mp4UrlNot in the event; pass the looked-up playback ID to the player or resolveViewerEndpoint
payload.sessiondata.artifact (a recording is its own artifact)

Delivery differences:

BehaviourStudioFrameWorks
SignatureLivepeer-Signature: t=<ts>,v1=<hex HMAC-SHA256 of body>Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature over id.timestamp.body
Timeout30 s10 s
RetriesUp to 33, doubling from 5 s to a 1 h cap (about a day); 4xx never retried12 attempts over about 3 days; any non-2xx retried
Failing endpointEmail, never disabledEmail, and disabled after 20 failures in a row spanning 5 days; re-enable to resume
HistoryPer-webhook request log, resend one request30-day delivery log with attempt details, replay one delivery or a time range, test send
Per-stream webhookstreamId field (not applied when delivering)Filter in your receiver on data.streamId

1. Create a FrameWorks account and a stream

Section titled “1. Create a FrameWorks account and a stream”

Sign up at https://chartroom.frameworks.network — email + password, or sign in with an EVM wallet if you’d rather skip email entirely. Then:

mutation CreateStream {
createStream(input: { name: "my-first-stream" }) {
... on Stream {
id
streamKey
playbackId
}
}
}

You get back a streamKey (secret — for your encoder) and a playbackId (public — for viewers).

Studio uses GeoDNS-routed rtmp://rtmp.livepeer.com/live and a WHIP endpoint at https://playback.livepeer.studio/webrtc/{streamKey}. FrameWorks publishes its equivalents via the dashboard or resolveIngestEndpoint(streamKey). RTMP and WHIP remain the low-effort path; FrameWorks additionally accepts E-RTMP (HEVC/AV1 contribution over RTMP) and SRT when your encoder supports them.

OBS / FFmpeg / hardware encoder → swap the URL, keep the rest.

If your existing integration uses the older @livepeer/react package:

import { Player } from "@livepeer/react";
<Player playbackId={playbackId} />
import { Player } from "@livepeer-frameworks/player-react";
<Player contentType="live" contentId={playbackId} options={{ gatewayUrl: "https://bridge.frameworks.network/graphql" }} />

The player also ships as:

  • @livepeer-frameworks/player-svelte
  • @livepeer-frameworks/player-wc (<fw-player>)
  • @livepeer-frameworks/player-core (headless / vanilla)

The component prop names and theming model differ — see Player (Playback) for the full surface.

Pick your client:

  • Houdini (SvelteKit-native GraphQL codegen)
  • urql or Apollo for React
  • graphql-request for plain Node/Bun scripts

Codegen against the schema gives you typed queries:

Terminal window
# example with graphql-codegen
pnpm dlx graphql-codegen \
--schema https://bridge.frameworks.network/graphql \
--documents 'src/**/*.graphql' \
--generates ./src/gql/generated.ts:typescript,typescript-operations

If you want to read the schema directly, it lives at pkg/graphql/schema.graphql on GitHub, and the playground is at https://bridge.frameworks.network/graphql/playground.

5. Point your webhook receiver at FrameWorks

Section titled “5. Point your webhook receiver at FrameWorks”

Create an endpoint under Developer → Webhooks (or with createWebhookEndpoint) for the event types you handle, and store the whsec_... secret it returns. Then swap the signature check and the event names in your receiver.

Before (Studio):

// Express endpoint receiving webhooks
app.post("/livepeer-webhook", verifyStudioSignature, (req, res) => {
if (req.body.event === "stream.started") {
notifyDashboard(req.body.stream.id, req.body.stream.playbackId);
}
if (req.body.event === "asset.ready") {
publishVideo(req.body.payload.asset.snapshot.playbackId);
}
res.sendStatus(200);
});

After (FrameWorks):

import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.FRAMEWORKS_WEBHOOK_SECRET);
app.post("/frameworks-webhook", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = wh.verify(req.body.toString("utf8"), {
"webhook-id": req.header("webhook-id"),
"webhook-timestamp": req.header("webhook-timestamp"),
"webhook-signature": req.header("webhook-signature"),
});
} catch {
return res.sendStatus(401);
}
if (event.type === "stream.live") {
notifyDashboard(event.data.streamId, event.data.playbackId);
}
if (event.type === "upload.ready") {
// Look up the current playback ID: storageArtifactsConnection with artifactHash = artifactId.
publishVideo(await playbackIdFor(event.data.artifact.artifactId));
}
res.sendStatus(204);
});

The SDKs verify and parse in one call: createWebhookReceiver (TypeScript), NewWebhookReceiver (Go), and WebhookReceiver (Python). See Webhooks.

The body is { id, type, api_version, created_at, data }; deduplicate on id. The webhooks guide covers retries, auto-disable, and replay. If a server-side process holds a connection anyway, the tenantEvents subscription delivers the same events without an endpoint.

If your Studio streams or assets are public, there is no extra work here: move to the new playbackId and update embeds.

If you use Studio signing keys, playbackPolicy: { type: "jwt" }, Livepeer-Jwt, or playback.accessControl webhooks, FrameWorks now ships an equivalent surface — see Playback Access Control. The translation:

  • POST /access-control/signing-key → createSigningKey mutation (ES256, asymmetric, private PEM returned once).
  • playbackPolicy: { type: "jwt" } → setPlaybackPolicy with type: JWT.
  • Livepeer-Jwt header → ?jwt=<token> query string (universal across HLS, LL-HLS, DASH, WebRTC/WHEP, MP4). Authorization: Bearer works on playback request paths where headers are available.
  • playback.accessControl webhook → setPlaybackPolicy with type: WEBHOOK. FrameWorks POSTs an HMAC-signed body; any 2xx allows, anything else denies, as on Studio.
  • Your gate endpoint needs three changes:
    • Signature: verify X-Frameworks-Signature: sha256=<hex HMAC-SHA256 of the body> instead of Livepeer-Signature.
    • Viewer key: read viewerToken instead of accessKey. Viewers send the key as ?jwt=<key>.
    • Context: webhookContext becomes the policy’s webhook.context and arrives as context, as on Studio.
  • A {"ret": 403} body does not override the status code: return the status you mean.
  • allowedOrigins carries over as policy.allowedOrigins with the same entries. It is checked on every browser request. Players without a browser (RTMP, SRT) send no origin and are denied unless the list includes *.

Note that signing keys do not transfer between platforms — reissue on FrameWorks. Migration concierge available via in-app ticket if you have a large library of pre-signed JWTs to roll over.

Targets are stream-scoped on both platforms — they don’t carry over when a stream is recreated. After your stream is up:

mutation Restream {
createPushTarget(
streamId: "stream_xxx"
input: {
platform: "twitch"
name: "Twitch — main channel"
targetUri: "rtmp://live.twitch.tv/app/live_xxxxxxxxxx_xxx"
}
) {
id
status
}
}

Run a low-traffic test stream through both platforms in parallel for a day. Watch the FrameWorks side via liveStreamEvents and liveViewerMetrics, compare against Studio’s dashboard, and only point production traffic over once you trust the numbers.

  • SRT ingest — available for encoders and networks where SRT is the better fit.
  • Pull-input streams — point FrameWorks at an RTSP camera, an SRT/RIST contribution feed, an HLS re-stream, or MPEG-TS over UDP and we open the upstream connection for you. Self-hosted edges can pull from RFC1918 / multicast sources when the source is pinned to clusters with allow_private_pull_sources=true — Studio doesn’t have an equivalent. See Pull-input streams. Experimental media-engine discovery exists locally today; FrameWorks packaging through Edge, Tray, CLI, API, and dashboard workflows is tracked on the roadmap.
  • LL-HLS and DASH — served from MistServer’s CMAF output on every cluster, alongside HLS and WebRTC.
  • AV1 in the codec list where the selected cluster and process configuration support it.
  • Sprite-sheet seek previews — a 10×10 grid plus a WebVTT cue file, generated for every recording, ready for hover-scrub UIs.
  • Managed hybrid edges. Run MistServer and Helmsman edge capacity on your hardware while FrameWorks operates Foghorn, federation, Livepeer, Chandler, storage, DNS/TLS, and the control plane.
  • Cross-cluster federation — one viewer pool routed across clusters; the closer edge wins automatically.
  • Agent-first surface. MCP server at /.well-known/mcp.json, wallet auth (EIP-191), and gasless USDC payments via x402 (EIP-3009). See Agent Access.
  • Skipper — an in-product AI consultant for API discovery and diagnostics. Reach it from the dashboard, the docs (⌘J), or MCP.
  • Player packages for React, Svelte, Web Components, and vanilla JS.

What’s not yet at parity, and what’s coming

Section titled “What’s not yet at parity, and what’s coming”

Current gaps to account for:

  • AI Pipelines (text-to-image, image-to-video, ASR, live-video-to-video, segmentation) — not exposed as a FrameWorks API today. The Livepeer team’s productized AI offering is Daydream (real-time AI video, runs on the same Livepeer network); keep AI workloads there or on existing managed Livepeer surfaces until FrameWorks exposes an equivalent.
  • C2PA signing and creator-ID provenance on transcode — not exposed as a public FrameWorks API today.
  • Derived thumbnail asset enforcement — playback access control protects endpoint resolution and media sessions. Chandler thumbnail URLs are preview assets and are not a separate auth boundary yet.
  • Studio library importer — importVodAsset imports one video per call from a public http(s) URL, such as a Studio asset’s download URL, but there is no one-shot import of a whole Studio library. ipfs:// and ar:// URLs are not accepted; pass a public gateway’s https link. If you have a sizeable library, open an in-app ticket or message us in Discord so we can scope the migration.
  • Creator metadata — Studio’s creatorId and userTags have no FrameWorks field yet; keep that mapping in your own records, keyed by stream ID or artifact ID.
  • Per-stream transcode profiles — FrameWorks uses a fixed rendition ladder per billing tier; Studio’s profiles[] on a stream or asset has no equivalent yet.
  • Per-target rendition selection on multistream — Studio lets you pick which rendition goes to each push target. We push the default output today.
  • RTMP and FLV-over-HTTP pull sources — our MistServer fork has no RTMP/FLV pull module, so a pull: { source: "rtmp://…" } or a Studio-style https://.../*.flv upstream won’t work. Everything else Mist documents (RTSP, SRT, RIST, HLS, DTSC, MPEG-TS over UDP including multicast) does. If RTMP pull is a critical requirement, contact support before planning your migration — there is no committed timeline yet.
  • Billing rates are still maturing. Rates may adjust as we scale, but you keep the allowances you signed up for.
  • Self-hosted and hybrid production cutovers need operator validation. Test DNS, TLS, firewall policy, capacity, node enrollment, and upgrade runbooks before moving production traffic.
  • Marketplace primitives ship; the vetted public marketplace program is on the roadmap. Validate operator vetting, pricing, approval flow, and support boundaries before relying on third-party clusters for production traffic.
  • GraphQL learning curve. If you’ve never written a query, the docs playground is the quickest way to inspect available fields. Skipper can also draft queries.
  • Webhook bodies render 64-bit integers as strings. sizeBytes, durationMs, and money amounts arrive as "7340032", not 7340032; parse them explicitly.
  • The player swap is not API-compatible. Props and theming differ — budget a few hours to walk through Player (Playback) examples.
  • Stream keys, playback IDs, and asset IDs are not transferable. Reissue stream keys and update embeds.
  • Playback signing is not key-portable. Re-create signing keys in FrameWorks and update your token transport from Livepeer-Jwt to ?jwt= or Authorization: Bearer.
  • No webhook history backfill. Past Studio events stay on Studio. A FrameWorks webhook endpoint receives events from the moment it is created, so create it before you move traffic.
ChannelBest forWhere
Skipper (AI consultant)Quick troubleshooting, API discovery, diagnostics, after-hours, non-human callersDashboard /skipper · ⌘J in docs · MCP at /mcp
In-app ticketingAccount-specific issues, billing, anything tied to your tenant data, library-migration helpDashboard /support
ForumLong-form Q&A, recipes, searchable community knowledgeforum.frameworks.network
DiscordReal-time chat, casual questionsJoin
GitHubBugs, feature requests, deeper technical threadsLivepeer-FrameWorks/monorepo