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
tenantEventssubscription) · 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.
Translation table
Section titled “Translation table”The shapes you know in Studio, mapped to FrameWorks:
Streams
Section titled “Streams”| Studio | FrameWorks |
|---|---|
POST /stream | createStream(input) mutation |
GET /stream/{id} | stream(id) query |
PATCH /stream/{id} | updateStream(id, input) mutation |
DELETE /stream/{id} | deleteStream(id) mutation |
DELETE /stream/{id}/terminate | No direct equivalent today; stop the encoder, or delete the stream if you are retiring it |
record: true flag at create | record: true at create, or startDVR(streamId) post-create |
POST /stream with pull: { source } config | createStream(input: { ingestMode: PULL, pullSource: { sourceUri }, sourceLocation }) — see Pull-input streams |
Recordings, clips, and VOD
Section titled “Recordings, clips, and VOD”| Studio | FrameWorks |
|---|---|
POST /clip | createClip(input) mutation |
GET /session/{id}/clips | storageArtifactsConnection(input: { kinds: [CLIP] }) or clip(id) |
POST /asset/request-upload (direct/TUS upload) | createVodUpload(input) → S3 multipart upload |
POST /asset/upload/url | importVodAsset(input: { url, filename, title }) — see Import a video from a URL |
| Tasks API for upload/transcode progress | vodUploadStatus, tenantEvents (upload.*), or liveProcessingEvents depending on the workflow |
GET /asset/{id} | vodAsset(id) query |
DELETE /asset/{id} | deleteVodAsset(id) mutation |
Multistream
Section titled “Multistream”| Studio | FrameWorks |
|---|---|
POST /multistream/target | No 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 stream | createPushTarget(streamId, input) creates a stream-scoped target |
Per-target rendition profile / videoOnly | Not exposed today |
Access control
Section titled “Access control”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
| Studio | FrameWorks |
|---|---|
| API key (Bearer) | Developer token (Bearer) — see Agent Access for wallet/x402 alternatives |
| Team login | Email/password login |
| — | Wallet sign-in (EIP-191) |
Viewer-side playback access
| Studio | FrameWorks |
|---|---|
| Default public playback ID | Default public playback ID |
POST /access-control/signing-key | createSigningKey(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 policy | Pass the key as ?jwt=<key>; your webhook receives it as viewerToken |
playback.accessControl webhook | Webhook policy: HMAC-signed POST to your URL; any 2xx allows, anything else denies |
playbackPolicy.webhookContext | webhook.context: a JSON object sent as context in every access request |
playbackPolicy.allowedOrigins | policy.allowedOrigins: same scheme://host[:port] / * entries |
playbackPolicy.refreshInterval | Not configurable; the decision holds for the viewer session (about 10 minutes) |
Real-time events
Section titled “Real-time events”FrameWorks events in the right column are delivered as webhooks and over
the tenantEvents subscription, with the same payloads.
| Studio webhook | FrameWorks event |
|---|---|
stream.started | stream.live (and stream.connected when the encoder connects) |
stream.idle | stream.idle |
recording.started | recording.started |
recording.waiting | recording.stopped (capture ended, recording being finalized) |
recording.ready | recording.ready (or recording.failed) |
asset.created | upload.created |
asset.updated | upload.completed when processing starts; no per-step progress event |
asset.ready | upload.ready |
asset.failed | upload.failed (with a reason) |
asset.deleted | No event; the deletion is in your audit log |
multistream.connected / .error / .disconnected | multistream.status_changed (status, previousStatus) |
task.* | No Task API; clips have clip.requested/ready/failed, imports use upload.* |
playback.accessControl | Webhook playback policy (setPlaybackPolicy with type: WEBHOOK), a blocking call |
playback.user.new | liveConnectionEvents(streamId) subscription (Studio never sends this webhook) |
stream.detection | Not available (Studio has it switched off) |
Payload fields:
| Studio | FrameWorks |
|---|---|
event | type |
id (event ID, stable across retries) | id and the webhook-id header (stable across retries and replays) |
stream.id | data.streamId |
stream.playbackId | data.playbackId on stream.connected, stream.live, stream.idle |
stream.name, userTags, other stream fields | Not in the event; query stream(id) |
payload.asset.id | data.artifact.artifactId |
payload.asset.snapshot.playbackId | Not in the event; look it up by data.artifact.artifactId (playback IDs can be rotated) |
payload.recordingUrl / payload.mp4Url | Not in the event; pass the looked-up playback ID to the player or resolveViewerEndpoint |
payload.session | data.artifact (a recording is its own artifact) |
Delivery differences:
| Behaviour | Studio | FrameWorks |
|---|---|---|
| Signature | Livepeer-Signature: t=<ts>,v1=<hex HMAC-SHA256 of body> | Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature over id.timestamp.body |
| Timeout | 30 s | 10 s |
| Retries | Up to 33, doubling from 5 s to a 1 h cap (about a day); 4xx never retried | 12 attempts over about 3 days; any non-2xx retried |
| Failing endpoint | Email, never disabled | Email, and disabled after 20 failures in a row spanning 5 days; re-enable to resume |
| History | Per-webhook request log, resend one request | 30-day delivery log with attempt details, replay one delivery or a time range, test send |
| Per-stream webhook | streamId field (not applied when delivering) | Filter in your receiver on data.streamId |
Step-by-step
Section titled “Step-by-step”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).
2. Repoint your encoder
Section titled “2. Repoint your encoder”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.
3. Swap the player
Section titled “3. Swap the player”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.
4. Rewire REST calls as GraphQL
Section titled “4. Rewire REST calls as GraphQL”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:
# example with graphql-codegenpnpm dlx graphql-codegen \ --schema https://bridge.frameworks.network/graphql \ --documents 'src/**/*.graphql' \ --generates ./src/gql/generated.ts:typescript,typescript-operationsIf 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 webhooksapp.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.
6. Plan playback access control
Section titled “6. Plan playback access control”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→createSigningKeymutation (ES256, asymmetric, private PEM returned once).playbackPolicy: { type: "jwt" }→setPlaybackPolicywithtype: JWT.Livepeer-Jwtheader →?jwt=<token>query string (universal across HLS, LL-HLS, DASH, WebRTC/WHEP, MP4).Authorization: Bearerworks on playback request paths where headers are available.playback.accessControlwebhook →setPlaybackPolicywithtype: 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 ofLivepeer-Signature. - Viewer key: read
viewerTokeninstead ofaccessKey. Viewers send the key as?jwt=<key>. - Context:
webhookContextbecomes the policy’swebhook.contextand arrives ascontext, as on Studio.
- Signature: verify
- A
{"ret": 403}body does not override the status code: return the status you mean. allowedOriginscarries over aspolicy.allowedOriginswith 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.
7. Re-attach multistream targets
Section titled “7. Re-attach multistream targets”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 }}8. Validate side by side
Section titled “8. Validate side by side”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.
What you gain on FrameWorks
Section titled “What you gain on FrameWorks”- 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 —
importVodAssetimports 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://andar://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
creatorIdanduserTagshave 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-stylehttps://.../*.flvupstream 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.
Beta and tricky bits
Section titled “Beta and tricky bits”- 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", not7340032; 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-Jwtto?jwt=orAuthorization: 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.
Get help
Section titled “Get help”| Channel | Best for | Where |
|---|---|---|
| Skipper (AI consultant) | Quick troubleshooting, API discovery, diagnostics, after-hours, non-human callers | Dashboard /skipper · ⌘J in docs · MCP at /mcp |
| In-app ticketing | Account-specific issues, billing, anything tied to your tenant data, library-migration help | Dashboard /support |
| Forum | Long-form Q&A, recipes, searchable community knowledge | forum.frameworks.network |
| Discord | Real-time chat, casual questions | Join |
| GitHub | Bugs, feature requests, deeper technical threads | Livepeer-FrameWorks/monorepo |