Skip to content

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.
SchemeMist inputNotes
rtsp://RTSPBasic-auth in URI accepted; rtsps:// is not supported yet.
srt://SRTStream ID and passphrase use standard SRT URL params.
rist://RISTMPEG-TS over RIST.
dtsc://DTSCPull from another Mist node. Useful for bridging self-hosted media planes.
https://.../*.m3u8HLSMatched by suffix.
https://.../*.tsTSSingle-file TS pulls; for live TS-over-HTTP, prefer HLS or tsudp://.
https://.../*.mkvEBMLIncludes WebM (.webm). Useful for VOD-as-live and some WHIP recordings.
tsudp://host:portTS over UDPUnicast 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.

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.

A stream’s sourceLocation says where FrameWorks may connect to the upstream:

ModeMeaning
ANYNo stream-level restriction. FrameWorks chooses among your entitled media clusters.
RESTRICTEDOnly the listed clusters. On clusters you own, each entry can be narrowed to specific nodes, and avoidNodeIds excludes nodes entirely.
CUSTOMRead-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.

  • clusters must be non-empty for RESTRICTED and empty for ANY. Each cluster must be one your account can use (clustersAccessConnection).
  • nodeIds and avoidNodeIds accept 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.
  • updateStream with sourceLocation replaces the location; omitting it keeps the current one. A CUSTOM location cannot be replaced from updateStream.
  • Rejections come back as ValidationError with the reason, such as a cluster you cannot use, a node you do not own, or a private source without a consenting cluster.

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 as ClusterAccess.allowPrivatePullSources.
  • The stream’s sourceLocation must be RESTRICTED to 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: true

Find 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 shapeResult
Platform cluster, public source URIAllowed.
Platform cluster, private literal source URIRejected.
Self-hosted edge with opt-in + restricted locationAllowed; dialed only from the listed clusters and nodes.
Hostname resolving to private IP from an edgeTreated 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).

Pull streams are on-demand:

  1. A viewer requests /play/{playback_id}.
  2. Foghorn routes the viewer to an eligible media edge, even if the stream is currently inactive.
  3. MistServer starts the pull+ stream and asks Foghorn for the source.
  4. 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.
  5. The first edge pulls upstream media; later viewers prefer in-cluster DTSC fanout once available.
  6. 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.

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.

  • 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.

Once media is active, pull streams behave like push streams:

For a lower-level service map, see the architecture note in docs/architecture/pull-streams.md.