Save and Restore a Media Blocks Pipeline as JSON in C¶
Table of Contents¶
- Overview
- The document model
- Building a pipeline from JSON
- Validating before you build
- Capturing a live pipeline
- Relative asset paths
- Diagnostics
- Discovering block types
- Demos
Overview¶
A Media Blocks pipeline is normally built in code: you construct blocks, connect their pads, and start the pipeline. The persistence layer lets you do the same thing with a document — an ordinary JSON file that names the blocks, their settings and their connections. That document can be written by your application, edited by hand, shipped as a preset, or produced by a visual editor.
Three types do the work, and each has a single responsibility:
| Type | What it does |
|---|---|
MediaBlocksPipelineJsonSerializer | JSON text ⇄ MediaBlocksPipelineDocument |
MediaBlocksPipelineValidator | checks a document before anything is built |
MediaBlocksPipelineMaterializer | document → live MediaBlocksPipeline |
The reverse direction — live pipeline → document — is MediaBlocksPipelineSnapshot, reached through MediaBlocksPipeline.ToDocument().
flowchart LR
JSON[JSON file] -->|Load| Document[MediaBlocksPipelineDocument]
Document -->|Validate| Diagnostics[Diagnostics]
Document -->|Materialize| Pipeline[MediaBlocksPipeline]
Pipeline -->|ToDocument| Document
Document -->|Serialize| JSON The document model¶
MediaBlocksPipelineDocument is a plain DTO tree:
SchemaVersion— the document format version. Older versions are migrated on load.Pipeline—Id,Nameand optional pipeline-level settings.Blocks— aMediaBlockDocumenteach:Id(aGuid),Type(theMediaBlockTypevalue),TypeName,Name, and aSettingspayload.Connections— aMediaBlockConnectionDocumenteach, with aFromand aToMediaBlockPadReference.
A pad reference is a block id plus a pad id, and the pad id follows one convention:
| Pad id | Meaning |
|---|---|
input / output | the block's main pad |
input:N / output:N | element N of the block's Inputs / Outputs list |
The indexed form is the one to prefer when you generate documents: it addresses any block, while the unindexed form needs a main pad that not every block has. For a muxing sink such as MP4SinkBlock, input:0 and input:1 are the video and audio streams.
A minimal document:
{
"schemaVersion": 1,
"pipeline": { "id": "6f0d1b6e-1f7a-4f1e-9a1a-0a0f3d5f7a11", "name": "Player" },
"blocks": [
{
"id": "10000000-0000-4000-8000-000000000001",
"type": 3,
"typeName": "UniversalSource",
"name": "Media File",
"settings": { "uri": "clips/sample.mp4", "renderVideo": true, "renderAudio": true }
},
{
"id": "10000000-0000-4000-8000-000000000002",
"type": 21,
"typeName": "VideoRenderer",
"name": "Preview"
}
],
"connections": [
{
"from": { "blockId": "10000000-0000-4000-8000-000000000001", "padId": "output:0" },
"to": { "blockId": "10000000-0000-4000-8000-000000000002", "padId": "input" }
}
]
}
Building a pipeline from JSON¶
The shortest path replaces an existing pipeline's contents:
var pipeline = new MediaBlocksPipeline();
var result = await pipeline.LoadJsonAsync(
File.ReadAllText("player.json"),
resolver: null,
baseDirectory: Path.GetDirectoryName(Path.GetFullPath("player.json")));
if (!result.Success)
{
foreach (var diagnostic in result.Diagnostics)
{
Console.WriteLine(diagnostic);
}
return;
}
await pipeline.StartAsync();
LoadJsonAsync requires the pipeline to be stopped, and it never leaves a half-built graph behind: if materialization fails partway, the pipeline is cleared.
To build a pipeline without owning one first, use the materializer directly. It returns the pipeline and the mapping from document ids to live blocks, which is what you need to reach a block afterwards:
var loaded = MediaBlocksPipelineJsonSerializer.Load(json);
var result = MediaBlocksPipelineMaterializer.Materialize(loaded.Document, resolver: null, baseDirectory: folder);
if (result.Success)
{
var overlay = (TextOverlayBlock)result.Blocks[captionId];
overlay.Settings.Text = "Live";
}
Network sources are probed when the pipeline starts¶
A document carries a source's endpoint, never its media information: restoring one contacts nothing. Two sources need that information before they can be built — RTSPRAWSourceBlock picks its depayloader and parser from the stream layout, and the desktop NDISourceBlock creates its converters from the stream counts — so StartAsync probes them for you, once, before the graph is built. The camera or sender has to be reachable at that moment; if it is not, StartAsync returns false and logs which source could not be read.
The synchronous Start has nowhere to await a probe and does not do this, and neither do the VideoCaptureCoreX and MediaPlayerCoreX engines, which build their graph through it. Restore a document whose pipeline you intend to start that way by replacing those settings with ones built through RTSPRAWSourceSettings.CreateAsync / NDISourceSettings.CreateAsync after materialization.
Audio devices are matched back to this machine¶
A document stores what identifies an audio device — its name, the API it belongs to, the endpoint path where the platform publishes one, and, on macOS, the stable CoreAudio unique-id — and never the enumerator's live handle, which means nothing outside the process that made it. Materialization matches that identity against the devices this machine has now and hands the block a live device again. That is also why a restored pipeline opens the right endpoint: the numeric CoreAudio id and the WASAPI device path are reassigned between runs, and the match re-reads the current ones.
If the device is gone, the pipeline still builds — on another device of the same API, reported as an MBS063 warning naming both. If the machine has no device of that API at all, the block keeps the document's identity and materialization reports MBS064; the block will not open anything.
Matching enumerates devices, which on a cold process starts a GStreamer device monitor and can take seconds, so call Materialize off the UI thread — or enumerate once beforehand. Inputs and outputs are cached separately: AudioSourcesAsync warms capture devices, AudioOutputsAsync warms renderers, and on Apple platforms the asynchronous capture call is also the one that asks for microphone permission. The synchronous match inside Materialize does neither.
Validating before you build¶
Validate answers the same questions materialization would, without constructing anything — so a UI can show a document's problems while the user edits it:
var diagnostics = MediaBlocksPipelineValidator.Validate(
document,
resolver: null,
baseDirectory: folder);
var errors = diagnostics.Where(d => d.Severity == MediaBlocksDiagnosticSeverity.Error).ToList();
It checks the document's structure, that every block type is known to this build, that each block has a construction path, that pad references parse and point at pads the block actually has, that no feedback cycle exists, and — when you pass a baseDirectory — that the files a source reads are present.
Pass baseDirectory: null to skip the file-system checks entirely; that is the default, and it keeps the behaviour of a document you validate before its assets are in place.
Capturing a live pipeline¶
ToDocument() walks the live graph and produces a document. It is safe while the pipeline is running — the block list is taken under the pipeline's own lock and nothing is changed — which makes it the save-during-Run path:
var snapshot = pipeline.ToDocument();
if (!snapshot.Complete)
{
foreach (var diagnostic in snapshot.Diagnostics)
{
Console.WriteLine(diagnostic);
}
}
await MediaBlocksPipelineJsonSerializer.SaveFileAsync(snapshot.Document, "captured.json");
ToJson() and SaveJsonAsync(path) are the one-line forms; they send the diagnostics to the SDK log instead of returning them.
The graph is captured exactly; the settings are not always
A block's identity, type and connections can be read from any block, so the shape of a pipeline always round-trips. Its configuration can only be read from blocks that expose their settings — 153 of this SDK's 404 block types do. For the rest there is nothing to read, and rather than write a document that looks complete and rebuilds the block on its defaults, the snapshot reports an MBS060 diagnostic and leaves that block's settings empty.
Check MediaBlocksPipelineSnapshotResult.Complete before treating a snapshot as a faithful copy. A pipeline that was itself built from a document round-trips fully, because materialization stamps the document's block ids onto the blocks it creates.
Relative asset paths¶
Settings that name a file may be relative to the document. Pass the document's folder as baseDirectory, and both validation and materialization resolve them against it:
var folder = Path.GetDirectoryName(Path.GetFullPath(documentPath));
var result = MediaBlocksPipelineMaterializer.Materialize(document, resolver: null, baseDirectory: folder);
The rewriting covers Uri-typed settings and string settings named *Path, *File, Filename or *Location. Values that name an endpoint rather than a file are left alone — anything with a scheme (rtsp://, srt://:8888/), an IPv4 literal, or a first segment that reads as a host name (cam.local/stream.m3u8). A folder whose name reads like a host is ambiguous, and the validator says so with an MBS057 warning; prefix such a value with ./ to force the folder reading.
Diagnostics¶
Every entry point returns MediaBlocksDiagnostic values rather than throwing, so a host can show the whole list at once. Each carries a Severity, a Code, a Message and the BlockId it belongs to.
| Code | Meaning |
|---|---|
MBS031 | unknown block type — this build's catalog has no such block |
MBS032 | the block has no construction path |
MBS040 | the block could not be constructed |
MBS048 / MBS054 | a pad reference could not be resolved, or a pad is used twice |
MBS049 | the settings have no default constructor and the document carries no usable payload |
MBS050 | the settings payload could not be deserialized |
MBS055 | a file a source reads is not there |
MBS056 | a resources entry could not be applied |
MBS057 | a value that reads as a network endpoint was left as written |
MBS060 | a block did not expose its settings to a snapshot |
MBS061 | a block could not be named in a snapshot |
MBS062 | a connection could not be expressed in a snapshot |
MBS063 | the audio device a block names is gone, and another device of the same API is used instead |
MBS064 | the audio device a block names could not be attached at all — nothing of that API is present, or the lookup failed |
Discovering block types¶
MediaBlockCatalog is the index the persistence layer builds on, and it is useful on its own: it lists every block this build carries, with its settings type, its editable properties and an availability probe.
foreach (var descriptor in MediaBlockCatalog.All)
{
Console.WriteLine($"{descriptor.TypeName} - {descriptor.DisplayName} ({descriptor.Category})");
}
var mp4 = MediaBlockCatalog.Get(MediaBlockType.MP4Sink);
Console.WriteLine(mp4.IsSink); // true - it ends a branch
Console.WriteLine(mp4.AcceptsDynamicInputs); // true - one input pad per stream
Demos¶
A runnable console sample - build a pipeline in code, save it as JSON, rebuild it from that JSON and compare the two - ships with the SDK samples under Media Blocks SDK / Console.