Skip to main content

Verify a live stream

StreamSdk validates each segment of an HLS or DASH stream as it lands, so the verified state stays current throughout playback.

StreamSdk.create(ledger: VerifierLedger, options: { trustAnchors: string[]; trustC2paAnchors?: boolean }): StreamSdk

options.trustAnchors are PEM trust anchors (see Trust anchors). ledger is a caller-provided cache of per-stream state (see Stream ledger).

The lifecycle

A stream lives between two events: the first call to validate(streamId, ...) for that id, and a final call to stop(streamId) when playback ends. In between, call validate once per segment, in arrival order, with the same streamId.

verify-stream.ts
import { StreamSdk, type VerifierLedger } from '@limboai/verifox-sdk-verifier';

const sdk = StreamSdk.create(ledger, { trustAnchors: [orgCaPem] });

const streamId = 'home-page-live';

async function onSegmentArrived(bytes: Uint8Array) {
const result = await sdk.validate(streamId, bytes);

switch (result.kind) {
case 'init':
// First segment of a session: the signed init manifest.
updateBadge(result.state);
break;
case 'media':
// Each subsequent media segment.
if (!result.valid) markTampered(result.codes);
break;
case 'unknown':
// Not a recognizable C2PA stream segment; ignore.
break;
}
}

Use any stable string for streamId (a URL or a player instance id both work). Different ids are validated independently. Calls for the same id must not overlap: the SDK reads and writes per-stream state through your ledger, which must provide atomic per-streamId read-modify-write (see the atomicity rule).

The result union

validate resolves to a SegmentValidation, a discriminated union on kind. Switch on kind and never read fields off the wrong arm.

type SegmentValidation = InitValidation | MediaValidation | UnknownSegment;

interface InitValidation {
kind: 'init';
reader: ManifestStore; // parsed manifest store for the init manifest, from `@contentauth/c2pa-types`
state: ValidationState; // "Invalid" | "Valid" | "Trusted"
codes: ValidationCode[];
sessionKeys: SessionKeySummary[]; // keys this session's media segments may use
}

interface MediaValidation {
kind: 'media';
valid: boolean;
keyId: Uint8Array; // session key that signed this segment
timing: SegmentTiming;
manifestId: string; // init manifest this segment belongs to
codes: ValidationCode[];
}

interface UnknownSegment {
kind: 'unknown';
}
  • init: the signed init manifest opening a session. state drives the badge (see Validation states); sessionKeys are cached for the media segments that follow.
  • media: a media segment. valid is the headline; codes carry the detail when it is false (see Failure codes).
  • unknown: bytes that are not a recognizable stream segment. No-op.

SegmentTiming

interface SegmentTiming {
sequenceNumber: number;
timescale?: number;
eventDurationSecs?: number;
}

sequenceNumber orders segments within a session. timescale and eventDurationSecs describe the segment's media duration when the container provides them.

SessionKeySummary

interface SessionKeySummary {
keyId: Uint8Array;
publicKey: Uint8Array;
}

Returned on InitValidation. Each entry is a session key the issuer minted for this session; media segments reference one by keyId. The SDK caches these in the ledger so later media validations resolve their signing key without re-reading the init manifest.

Cleanup

await sdk.stop(streamId);

stop invokes forget(streamId) on the ledger. Call it from the player's unmount or destroy hook.

See also