Pull-input Streams
A normal stream is push-in: your encoder opens RTMP/E-RTMP, SRT, or WHIP to FrameWorks and uploads media. A pull-input stream is the inverse: FrameWorks stores an upstream URL, then a media edge opens that source when playback needs it.
The payoff is operational: you can onboard venue cameras, contribution feeds, and existing origins without asking every source to become a FrameWorks encoder. Pull input also composes with the rest of the platform — playback access control, DVR, clips, thumbnails, analytics, and multistreaming all attach to the stream once media is active.
Use pull-input streams when:
- The source is an existing camera, encoder, or origin server that cannot push to FrameWorks.
- A venue sends you an SRT/RIST contribution feed.
- You need to restream an HLS or MPEG-TS source.
- You run self-hosted edge nodes that can reach private cameras or LAN/VPC sources.
Supported Source Schemes
Section titled “Supported Source Schemes”| Scheme | Mist input | Notes |
|---|---|---|
rtsp:// | RTSP | Basic-auth in URI accepted; rtsps:// is not supported yet. |
srt:// | SRT | Stream ID and passphrase use standard SRT URL params. |
rist:// | RIST | MPEG-TS over RIST. |
dtsc:// | DTSC | Pull from another Mist node. Useful for bridging self-hosted media planes. |
https://.../*.m3u8 | HLS | Matched by suffix. |
https://.../*.ts | TS | Single-file TS pulls; for live TS-over-HTTP, prefer HLS or tsudp://. |
https://.../*.mkv | EBML | Includes WebM (.webm). Useful for VOD-as-live and some WHIP recordings. |
tsudp://host:port | TS over UDP | Unicast or multicast. Private and multicast destinations need an opted-in edge cluster. |
RTMP pull is not supported. RTMP stays push-only. If you need to pull from an RTMP source, run a
converter such as FFmpeg, MistServer, or srt-live-transmit and pull its SRT, RIST, RTSP, or HLS
output instead.
Create a Pull Stream
Section titled “Create a Pull Stream”In the dashboard, create a stream and choose Pull as the ingest mode. Provide the upstream source URI and keep the source enabled if it should start when viewers request playback. Under Where can this source be reached?, keep Any cluster FrameWorks chooses for a public source, or choose Only these clusters to restrict it.
Through GraphQL, use createStream with ingestMode: PULL and pullSource:
mutation CreatePullStream($input: CreateStreamInput!) { createStream(input: $input) { __typename ... on Stream { id playbackId ingestMode pullSource { sourceUriRedacted enabled class } sourceLocation { mode clusters { clusterId nodeIds } avoidNodeIds } } ... on ValidationError { message field } }}{ "input": { "name": "Lobby Camera", "ingestMode": "PULL", "pullSource": { "sourceUri": "rtsp://camera.example.net/live", "enabled": true } }}The source URI is stored encrypted and returned only in redacted form. Omitting sourceLocation
leaves it ANY: FrameWorks may run a public source on any media cluster your account can use.
Source Location
Section titled “Source Location”A stream’s sourceLocation says where FrameWorks may connect to the upstream:
| Mode | Meaning |
|---|---|
ANY | No stream-level restriction. FrameWorks chooses among your entitled media clusters. |
RESTRICTED | Only the listed clusters. On clusters you own, each entry can be narrowed to specific nodes, and avoidNodeIds excludes nodes entirely. |
CUSTOM | Read-only. The stream’s ingest placement rules hold more than a location can express; edit them with media placement. |
The location is stored as the stream’s own ingest placement rules. The edge that dials the source is checked against those rules node by node, so a camera restricted to one node is never opened from another node, even in the same cluster. Other nodes and clusters serve viewers by relaying from the node that holds the source. Writing a location replaces only where the source may run; viewer placement rules and ingest preferences stay as they are.
clustersmust be non-empty forRESTRICTEDand empty forANY. Each cluster must be one your account can use (clustersAccessConnection).nodeIdsandavoidNodeIdsaccept only nodes of clusters your account owns, and are accepted once every media cell serving your account supports node placement. A node cannot be both allowed and avoided.updateStreamwithsourceLocationreplaces the location; omitting it keeps the current one. ACUSTOMlocation cannot be replaced fromupdateStream.- Rejections come back as
ValidationErrorwith the reason, such as a cluster you cannot use, a node you do not own, or a private source without a consenting cluster.
Private Upstreams on Self-hosted Edges
Section titled “Private Upstreams on Self-hosted Edges”A camera on a LAN or a multicast feed is reachable only from edges on that network. Private literals (RFC1918, ULA, or non-link-local multicast addresses) require two pieces of configuration:
- The media cluster must opt in with
allow_private_pull_sources: true. Tenants see this asClusterAccess.allowPrivatePullSources. - The stream’s
sourceLocationmust beRESTRICTEDto clusters that opted in. A restriction to those clusters in your account-level ingest rules also satisfies this.
clusters: warehouse-edge: name: Warehouse Edge type: edge owner_tenant: acme roles: [media] allow_private_pull_sources: trueFind the cluster and, if the camera is reachable from only some edges, the node IDs:
query SourceLocationChoices { clustersAccessConnection { nodes { clusterId clusterName allowPrivatePullSources } } mediaPlacementOptions( scope: { kind: TENANT } filter: { kind: NODE, clusterId: "warehouse-edge" } ) { __typename ... on MediaPlacementOptionsConnection { nodes { id name clusterId eligible reason } } ... on MediaPlacementError { code message } }}Then create the pull stream restricted to that cluster, and to the node on the camera’s network:
{ "input": { "name": "Warehouse Camera", "ingestMode": "PULL", "pullSource": { "sourceUri": "rtsp://192.168.10.50/live", "enabled": true }, "sourceLocation": { "mode": "RESTRICTED", "clusters": [{ "clusterId": "warehouse-edge", "nodeIds": ["warehouse-edge-1"] }] } }}Leave nodeIds empty to allow every node of the cluster. To keep a source off one node without
listing the others, use "avoidNodeIds": ["warehouse-edge-3"]. To change an existing stream:
mutation RestrictSource($id: ID!) { updateStream( id: $id input: { sourceLocation: { mode: RESTRICTED clusters: [{ clusterId: "warehouse-edge", nodeIds: ["warehouse-edge-1"] }] } } ) { __typename ... on Stream { sourceLocation { mode clusters { clusterId nodeIds } avoidNodeIds } } ... on ValidationError { message } ... on NotFoundError { message } }}Through MCP, create_stream and update_stream take the same location as source_location:
{ "name": "Warehouse Camera", "ingest_mode": "pull", "pull_source": { "source_uri": "rtsp://192.168.10.50/live" }, "source_location": { "mode": "restricted", "clusters": [{ "cluster_id": "warehouse-edge", "node_ids": ["warehouse-edge-1"] }] }}This keeps platform-managed clusters from trying to reach sources they cannot access and keeps private-source placement on edge nodes you operate.
| Deployment shape | Result |
|---|---|
| Platform cluster, public source URI | Allowed. |
| Platform cluster, private literal source URI | Rejected. |
| Self-hosted edge with opt-in + restricted location | Allowed; dialed only from the listed clusters and nodes. |
| Hostname resolving to private IP from an edge | Treated as public at validation; DNS reachability is operator-owned. |
Operator-declared pull streams set the same restriction in the bootstrap overlay with
source_location (see Cluster manifest).
Runtime Behavior
Section titled “Runtime Behavior”Pull streams are on-demand:
- A viewer requests
/play/{playback_id}. - Foghorn routes the viewer to an eligible media edge, even if the stream is currently inactive.
- MistServer starts the
pull+stream and asks Foghorn for the source. - Foghorn ignores untrusted fallback query params and opens the stored source URI from its verified cell-local signed authority. Connected Commodore lookup is a mixed-version fallback only before that authority is marked ready.
- The first edge pulls upstream media; later viewers prefer in-cluster DTSC fanout once available.
- When the last viewer leaves, MistServer drops the upstream after the standard grace period.
Idle pull streams do not fetch from the upstream and do not accrue live processing or viewer costs. Once active, metering is the same as push streams: viewer minutes, egress, transcoding, recording, and storage apply normally.
Resolution Events
Section titled “Resolution Events”Every STREAM_SOURCE evaluation records the resolution outcome. The dashboard shows the latest
events on the stream setup panel, and GraphQL exposes the same feed:
query PullSourceEvents($id: ID!) { stream(id: $id) { ... on Stream { ingestMode recentPullSourceEvents(limit: 10) { eventKind detail createdAt } } }}eventKind is one of resolved, not_found, disabled, blocked_uri,
private_not_allowed, commodore_error, or foghorn_base_unresolved. These are source
resolution events: they explain whether Foghorn handed MistServer a source. Upstream
connect/disconnect/retry telemetry depends on MistServer emitting additional trigger types.
Failure Modes
Section titled “Failure Modes”- Upstream unreachable - The Mist input exits. The next viewer triggers a fresh pull attempt.
- Two viewers arrive at once - More than one edge can briefly pull from the upstream before fanout takes over.
- Source URI changed - Update the pull source from the dashboard or GraphQL. Operator-managed pull streams can also be updated by re-running bootstrap with the new URI. Active sessions reconnect against the new origin after the media plane sees the updated source.
- Source sends silence or black - FrameWorks does not synthesize fallback media; viewers see what the upstream sends.
Feature Parity
Section titled “Feature Parity”Once media is active, pull streams behave like push streams:
- Playback access control can gate viewers with JWT or webhook policies.
- Recording and DVR work with the same stream-level recording flag.
- Multistreaming can push the pulled source onward to RTMP targets.
- Thumbnails and previews use the same generation pipeline.
For a lower-level service map, see the architecture note in
docs/architecture/pull-streams.md.