Skip to content

Media placement preview

Placement ships in 0.3.0, and live routing and final admission resolve through it. Read-only capacity preview and source-aware preview for owned push streams are implemented. Pull and managed streams can configure viewer policy, but their stream-specific preview is deliberately unavailable: a truthful result must observe whether their configured source can originate or relay, and a capacity-only answer would not prove a playable route. Live routing still places those streams. Saving placement intent must not be treated as proof that every media path enforces it: rules take effect once every cell holding the content acknowledges them.

For configured pull and managed Mist-native streams, a serving edge that may originate the input uses its local configured source. If policy selects an edge in another cell, FrameWorks prepares a single receipt-bound DTSC relay from the live origin. The remote edge does not receive the upstream URI or managed source specification. Direct RTMP, SRT and WHIP publishers use their own final ingest-admission path and are not converted into configured relays.

Placement separates ingest from serving viewers. A viewer’s distance is measured from the viewer location, not from the coordinator receiving the request. An account rule can prefer self-hosted capacity, prohibit a cluster class, or compare explicitly priced capacity. Stream-level hard constraints remain in effect when previewing an account draft against that stream.

The viewer resolver accepts protocol: MediaViewerProtocol. For example:

query ResolveViewerDestination($contentId: String!) {
resolveViewerEndpoint(contentId: $contentId, protocol: HLS) {
primary {
nodeId
protocol
url
outputs
}
metadata {
contentId
contentType
status
}
}
}

This is a requirement before selection, not a preference to apply after choosing a node. WHEP and WEBRTC are distinct; HLS_CMAF requests an HLS manifest, while DASH requests MPD. Omit the argument to place against any browser-playable Mist output and receive the selected node’s full output catalog. The low-level player GatewayClient also accepts protocol: "HLS" (typed as ViewerProtocol). It rejects a mismatched result and does not retry without the requirement when an older GraphQL endpoint rejects the argument.

Standalone integrations use the same resolver contract:

import { useViewerEndpoints } from "@livepeer-frameworks/player-react";
// React: rerendering with new request fields cancels the old resolution.
const { endpoints, status, error } = useViewerEndpoints({
contentId: playbackId,
protocol: "HLS",
playbackAuth: { token: viewerJwt },
});
import { onDestroy } from "svelte";
import { createEndpointResolver } from "@livepeer-frameworks/player-svelte";
// Inside a Svelte component's script; subscribe with $resolver.
const resolver = createEndpointResolver({ contentId: playbackId, protocol: "HLS" });
onDestroy(() => resolver.destroy());

The Svelte store supports resolver.update({ protocol: "DASH" }) to replace the request and resolver.refetch() to request a fresh destination. Both wrappers expose only the current result and clear the previous destination while loading a replacement. authToken authenticates the Gateway request; playbackAuth forwards the viewer JWT separately. Cancellation includes pending retries.

For direct live HTTP resolution, an explicit ?jwt=... viewer credential is preserved on returned playback URLs. Treat those URLs as credentials: do not publish or log them. Header/cookie credentials are not converted into query parameters; your client must attach them to requests to the selected edge. Do not assume a browser forwards authorization across an edge redirect. This live-URL behavior does not apply to object-storage signed URLs.

Gateway/Foghorn chooses the serving MistServer node; it does not choose the player’s protocol. An unqualified resolution returns that node’s complete advertised output catalog rather than a single format selected by the control plane. The player fetches stream info from that selected MistServer and uses Mist’s source list as the authoritative catalog of protocols, URLs, tokens and capabilities. It can therefore fall back among HLS, DASH, WebRTC and other formats without re-running placement for each format. The selected edge’s normal playback trigger still admits every actual media request. Direct-Mist mode uses the same Mist source discovery without the Gateway placement step.

viewerProtocol: "HLS" intentionally filters Mist’s catalog when an application requires one format; React/Svelte expose it through options, and the web component uses viewer-protocol="HLS".

Changing those player request fields replaces the active controller, including when its previous resolution is still pending. Update supplied destinations by replacing the endpoints object, not by mutating it in place. Equivalent request values do not trigger reconnection.

React, Svelte and web-component players apply debug, autoplay and muted updates without reconnecting, even while resolution is pending. Removing these options restores their defaults: debug/muted false and autoplay true. An unrelated option change does not reset the viewer’s mute control. Changing autoplay affects subsequent autoplay attempts; it does not immediately play or pause the current session.

Explicit live routing requires the matching placement-enabled Foghorn path. A cell’s control replicas attest placement enforcement to the control plane before any policy-bearing authority is issued to it; until every cell serving your account has attested, routing follows the legacy path and the placement status surface reports no confirmed enforcement. Actual media proof across cells remains under development; schema/player support alone does not establish end-to-end enforcement. The read-only API demo supports only formats in its synthetic fixture.

A selector combines clusterIds, nodeIds, ownerIds, regions, classes and charging: fields combine with AND, values within a field with OR. nodeIds names individual nodes and is accepted only for nodes of media clusters your account owns. List them with mediaPlacementOptions using filter: { kind: NODE }, optionally with clusterId to limit the list to one cluster; each NODE option carries its clusterId. Nodes of platform or marketplace clusters are never offered.

Node selectors are accepted once every media cell serving your account supports node placement; until then, review and apply refuse rules that name nodes. Ingest rules are checked against the exact node that admits a publisher or dials a pull or managed source, and serve rules against the exact node chosen for a viewer.

For a pull stream, the simplest way to express “only this cluster” or “only these nodes” is the stream’s source location, which writes the same ingest rules. Use the placement rules below when you need more, such as preferences or selectors on region or owner. A stream whose own ingest rules go beyond a location reports sourceLocation.mode: CUSTOM and is edited here.

This review restricts ingest for one stream to two nodes of an owned cluster:

query ReviewNodeRestriction($streamId: ID!, $revision: String!, $parentRevision: String!) {
reviewMediaPlacementChange(
input: {
scope: { kind: STREAM, streamId: $streamId }
expectedRevision: $revision
expectedParentRevision: $parentRevision
updates: [
{
verb: INGEST
kind: SET
rules: {
constraints: {
allow: {
any: [
{
clusterIds: ["warehouse-edge"]
nodeIds: ["warehouse-edge-1", "warehouse-edge-2"]
}
]
}
}
}
}
]
}
) {
__typename
... on MediaPlacementReview {
reviewToken
expiresAt
differences {
label
before
after
}
warnings {
id
message
}
}
... on MediaPlacementError {
code
message
}
}
}

Apply it with applyMediaPlacementChange using the same updates and revisions, the returned reviewToken, the acknowledged warning IDs and a stable idempotencyKey. Through MCP, get_media_placement_options accepts filter: { "kind": "NODE", "clusterId": "..." } and the placement review/apply tools accept nodeIds in selectors.

The MCP tools expose the same typed inputs and resolver checks as GraphQL. Nested fields use the GraphQL camelCase names, and revisions remain decimal strings. For example, preview_media_placement accepts:

{
"scope": { "kind": "TENANT" },
"verb": "SERVE",
"protocol": "hls",
"coordinates": { "latitude": 39, "longitude": -77 }
}

MCP results contain { "type": "MediaPlacementPreview", "value": { ... } } in both structured content and the JSON text fallback. Typed failures set isError and retain the error code and recovery fields in value. A no-destination preview is a decision, not a transport failure.

For edits, call review_media_placement_change, inspect its warnings, then use apply_media_placement_change with the same updates and revisions, returned reviewToken, explicit acknowledgedWarningIds, and a stable idempotencyKey. If the apply response is uncertain, call get_media_placement_change with its original scope and key before retrying; do not invent a new key. Cluster consent has matching review_cluster_media_consent_change, apply_cluster_media_consent_change, and get_cluster_media_consent_change tools.

Treat null activeRevision and activeParentRevision as unconfirmed enforcement, not active revision zero. A stream without its own rules can still inherit an account policy: compare parentRevision with activeParentRevision and inspect rollout status. An explicitly activated inherited-only policy may report active stream revision "0" together with a positive active account revision. NOT_CONFIGURED describes the absence of custom rules at the requested scope; it does not prove that inherited rules are absent or enforced.

Reads and previews require placement:read. Review and apply require owner/admin authority and placement:write; API tokens also need mcp:high-risk for apply. Policy management and change recovery do not require a funded balance. They do not reserve or authorize billable media capacity.

Use a tenant-authenticated GraphQL request. API tokens require placement:read. The coordinates below are hypothetical viewer coordinates; preview does not change the account’s location or open a media connection.

query PreviewViewerPlacement {
previewMediaPlacement(
input: {
scope: { kind: TENANT }
verb: SERVE
protocol: "hls"
coordinates: { latitude: 39, longitude: -77 }
}
) {
__typename
... on MediaPlacementPreview {
revision
parentRevision
digest
reason
complete
sourceEvaluated
observedAt
expiresAt
activeIngestClusterId
selected {
clusterId
clusterName
region
distanceKm
requiresSourcePull
price {
amountMicros
currency
unit
revision
expiresAt
}
}
transitions {
fromGroup
reason
}
}
... on MediaPlacementError {
code
message
}
... on AuthError {
message
}
... on NotFoundError {
message
}
}
}

For an owned stream, use scope: { kind: STREAM, streamId: "<stream-id>" }. Alternatively, keep tenant scope and provide streamId to test account rules against that stream’s constraints. A stream from another account cannot be inspected.

To preview an unsaved change, provide draftUpdate for the same verb plus expectedRevision and expectedParentRevision from the policy read. Revisions are decimal strings, not JavaScript numbers. CLEAR previews inheritance; it does not mean deny all. A revision conflict means reload the policy and preview again. The request does not save rules or require a write-recovery key.

A selected cluster is an observed, policy-ranked destination—not a reservation or a guarantee of playback. Streamless previews have sourceEvaluated: false: source availability was not evaluated. For an owned push stream, serving preview checks its current publisher and the permissions needed to pull into the destination cluster. sourceEvaluated: true means those source observations were available; requiresSourcePull: true means the destination still needs a pull. Preview never starts that pull or proves first-frame delivery. Managed non-push source preview is currently unsupported.

If the publisher cannot be verified, an owned-stream serving preview returns an incomplete result with no selected destination. It does not substitute a capacity-only answer for a playback route.

complete: false means relevant evidence is missing. An unreachable self-hosted cluster is not proof that it is full, so it cannot unlock a capacity-only fallback. Missing cost evidence is not treated as free. Price comparisons include their currency, unit and expiry; amounts are integer micro-units represented as strings.

An active push publisher remains pinned to its existing ingest cluster. Previewing a nearer location does not migrate that publisher. Candidate node IDs are private infrastructure details and may be omitted even when a cluster is visible. Discard expired results and preview again after changing rules, stream, verb, protocol or coordinates.

In API Explorer, enable Demo and open Media Placement. The examples include a US viewer with an EU publisher, self-hosted-first preferences with geographic fallback, and a review of a “never official” restriction. Draft preview and review use the shared policy evaluator and diff logic against synthetic fixtures, without observing real capacity or preparing media.

Demo results are explicitly marked simulated. Account and stream policies remain at revision 0, there is no active-policy claim, and every Apply returns UNSUPPORTED rather than pretending to save. Change recovery returns no saved receipt. Review tokens beginning demo-preview-only: are not live apply credentials. Demo requests never use real service clients, even when sent with a signed-in account. This is API Demo Mode, not database seed data or proof of live routing.

On an owned media cluster’s detail page, Capacity permissions controls whether the cluster accepts publishers, serves viewers, and pulls media from other clusters. Same-cluster source pulls do not require the external-source permission. Consumer placement rules can narrow these permissions but cannot override them. Enabling capacity does not grant another account access, subscribe it to a marketplace offering, or change prices and credentials.

Edit the switches, select Review changes, read the impact and required warnings, then select Apply permissions. Incomplete impact counts are not a complete inventory. A saved revision is not proof of active enforcement: check its rollout status separately. Already admitted sessions retain their admission; the changes govern new decisions once enforced.

If someone else changes the revision, reload current permissions and review again. You can retain your draft when reloading. If a save times out, keep the page open and use Check save status or Retry same change. These retain the original reviewed request; do not submit a new change to resolve an uncertain save. Closing the page loses its in-memory recovery command.

For integrations, the owner API fields are clusterMediaConsent, reviewClusterMediaConsentChange, applyClusterMediaConsentChange, and clusterMediaConsentChange. Read/recovery require placement:read for API tokens; review/apply require placement:write plus the owning tenant’s authorized role. Platform-operator status does not bypass ownership. Preserve decimal revision strings, review tokens, warning IDs and the idempotency key exactly. Query recovery with the same cluster and key after an ambiguous write.

Storage and processing placement are separate planned engines. This preview does not choose durable copy locations, schedule processing jobs, or proactively replicate a stream.