Sign a live stream
StreamSdk signs live HLS and DASH at the segment level. Each fMP4 segment produced by the encoder is individually signed. For finished video files, use AssetSdk instead.
The SDK is a small set of primitives. It does not generate playlists, decide CDN topology, or know about your delivery layer. It hands back signed bytes; you wire them into whatever pipeline you already run.
The idea
A C2PA live stream is one initialization segment with one session-key roster, followed by media segments each individually signed under a key in that roster. StreamSdk keeps that model simple: one stream owns one session key for its entire lifetime. When the key nears expiry (sign returns rotationDue: true), you end the stream and start a new one. There is no in-place key rotation, no overlap window, no roster mutation. See Key rotation for the rotation model and discontinuity handling.
The session key signs each media segment locally so the org key never sits on the per-segment path. Keys live in a customer-operated SigningVault and per-stream state in a customer-operated SessionLedger. See Stream storage for that model.
Primitives
import { SignerClient, StreamSdk } from "@limboai/verifox-sdk-issuer";
// One client per process. Share it across SDKs.
const client = await SignerClient.connect("https://api.trylimbo.com", {
headers: { authorization: `Bearer ${process.env.LIMBO_API_KEY}` },
});
const sdk = StreamSdk.create(client, ledger, vault);
// Begin a stream. Mints a session key and returns the signed init segment.
const init = await sdk.init(streamId, initSegmentBytes, title, storage, rotationPolicy, manifest);
// Sign one media segment. Local, never touches the Signer.
const out = await sdk.sign(streamId, segmentBytes);
// Tear down. Forgets the session key and clears the ledger entry.
await sdk.stop(streamId);
That's the entire surface.
init(id, initBody, title, storage, rotationPolicy, manifest) → InitOutcome
Mints a session key, signs the init segment under the org key, and records fresh session state in the ledger.
| Argument | Notes |
|---|---|
id | The StreamId. Re-initing under the same id rotates the key; see Key rotation. |
initBody | The fMP4 initialization segment bytes. |
title | Required. Manifest title. |
storage | Required. A ManifestStorage: { mode: "embedded" }, { mode: "remote", bindings }, or { mode: "both", bindings } (see Storage modes). A live stream cannot carry a soft binding — there is nothing to attach (the iscc sign option does not apply to streams) — so remote/both should pass bindings: []. |
rotationPolicy | Required. { validityPeriodSecs, refreshPercent /*0-100*/ }. |
manifest | The ManifestSpec — provenance only, the same type the asset sign takes. Required: origin (CreatedOrigin | OpenedOrigin); chain rotations via origin.parent, see Key rotation. Optional: identity (see Identity), components (see Ingredients), actions, assertions (see Assertions), and thumbnail. |
InitOutcome: { signedInitBytes, validUntilEpochSecs }. Publish signedInitBytes as the initialization segment.
rotationPolicy is passed per init call, so different streams can run different policies in the same process. See Key rotation.
sign(id, body) → SignedSegmentOutcome
Signs one media segment locally with the session key. Never touches the Signer.
SignedSegmentOutcome: { bytes, rotationDue, validUntilEpochSecs }.
bytes: the signed media segment, ready to publish.rotationDue:trueoncerotationPolicy.refreshPercent% of the session key's validity has elapsed. A signal to start a fresh stream viainit. See Key rotation.validUntilEpochSecs: when the session key expires.
stop(id)
Releases the session key and clears the ledger entry. Call it when the broadcast ends. You do not need to call it before re-initing; init cleans up the previous key on its own.
Classifying segments
If your ingest yields a mixed stream of bytes, classifySegment tells you whether each chunk is an init or media segment:
import { classifySegment, SegmentKind } from "@limboai/verifox-sdk-issuer";
switch (classifySegment(chunk)) {
case SegmentKind.Init:
// re-init the stream with this initialization segment
break;
case SegmentKind.Media:
// sign and publish
break;
case SegmentKind.Unknown:
// pass through untouched
break;
}
SegmentKind is { Init = 0, Media = 1, Unknown = 2 }.
Example: init and sign loop
let stream = await sdk.init(streamId, initBytes, title, storage, rotationPolicy, manifest);
publishInit(stream.signedInitBytes);
for await (const segment of encoderSegments) {
const out = await sdk.sign(streamId, segment.bytes);
publishSegment(segment.name, out.bytes);
if (out.rotationDue) {
const fresh = await encoder.initSegment();
stream = await sdk.init(streamId, fresh, title, storage, rotationPolicy, manifest);
publishInit(stream.signedInitBytes);
// Whatever your delivery layer does to signal a discontinuity goes here.
}
}
await sdk.stop(streamId);
See Key rotation for what the rotationDue branch and the discontinuity signal are doing.
See also
- Key rotation:
rotationDue, chaining manifests, discontinuities. - Stream storage:
SigningVaultandSessionLedger. - Live TV broadcast: end-to-end scenario.