PrismCore / Guides

Diagnostics

Classified errors, startup checkpoints, playback events, the redacted diagnostic report, the FFmpeg build report and the engine's log — everything a host needs to act on a failure or file a useful bug.

Classify every failure

PrismCoreError.classify(_:) turns anything PrismCore — or your AVPlayer — throws into one case of a taxonomy, so a failure can be acted on rather than logged:

CaseMeaningRetryability
originRefused(status:url:)401, 403 or 407.permanent
originRateLimited(status:retryAfter:url:)429, 503 or 509, with the origin's own Retry-After when it sent one.retryable
originUnreachable(status:url:underlying:)No answer, a dropped connection, a 404 or another error status.retryable with no status or a 5xx; .permanent for other statuses
noVideoStreamThe source has no video.permanent
videoCodecNotRemuxable(codecName:streamIndex:)The video cannot ride HLS-fMP4.permanent
videoCodecUnplayable(codecName:)No path can decode it.permanent
startupBudgetExpiredStartup ran out of its budget.unknown
masterRejectedByPlayer(underlying:)AVPlayer refused the master (-11868, -11848, -1002).permanent
workDirectoryOutOfSpace(underlying:)The volume is full.unknown
ffmpeg(code:operation:message:)A libav* failure with no honest mapping.unknown
unknown(underlying:)Anything else.unknown

retryability is three-valued on purpose: for a startup budget or a full volume the engine genuinely cannot say, and a Bool would have to invent an answer. Where the engine cannot tell two situations apart they share a case — an origin that never answered and one that vanished mid-session are both originUnreachable; you know which it was from whether start() had returned. After startup, session.remuxFailure is the same classification of session.remuxError.

On a refused master, make the fallback session — it first drops the Dolby Vision claim, then falls back to the muxed shape:

Swift
func recover(from error: any Error, playing session: PrismCoreSession) async throws -> URL? {
    guard PrismCoreSession.isMasterRejection(error) else { return nil }
    let fallback = try await session.makeMasterRejectionFallbackSession()
    let playlist = try await fallback.start()
    await session.stop()
    return playlist
}

Startup checkpoints

startupCheckpoints() — called before start() — yields each startup stage with the time since start() was called: sourceOpened, streamInfoResolved(SourceInfo), segmentPlanReady(origin:segments:), firstVideoSegmentWritten(index:), playlistServable(URL). The stream finishes on success, failure and stop(); calling it after start() throws SessionError.alreadyStarted. See the quick start for a full loop.

Playback events

playbackEvents() streams what happens after startup:

Swift
for await event in await session.playbackEvents() {
    switch event {
    case .slowServe(let path, let waited): print("slow serve", path, waited)
    case .serveTimedOut(let path): print("timed out", path)
    case .producerStalled(let since, _): print("producer stalled for", since)
    case .originThrottled(let retryAfter): print("origin throttling, retry after", retryAfter as Any)
    case .originRecovered: print("origin recovered")
    case .segmentPlanAvailable(let segments): print("a plan of", segments, "segments is stored")
    case .producerRecovered(let attempt, let cause): print("reconnected (\(attempt)/3) after", cause)
    case .producerFailed(let failure): print("playback failed:", failure)   // == session.remuxFailure
    }
}
  • slowServe — a serve took more than 2 s; serveTimedOut — the 15 s production window passed.
  • producerStalled — 5 s since the last completed segment while a request waits (never while the producer is parked on purpose).
  • originThrottled / originRecovered — only with coordinatedHTTP, and they reach every session reading that origin.
  • producerRecovered / producerFailed — the self-healing outcome. producerFailed is sent exactly once per session that fails, mid-film or at startup, and never for a stop().

The stream keeps the newest 64 events for a slow consumer, does not replay earlier ones, and finishes on stop().

When the source fails mid-film

In a planned session a read that fails after start() — a Wi-Fi roam, a proxy restart, a 5xx — no longer ends the session. The producer closes the source, waits, re-opens it exactly the way startup does, checks that the streams are the same ones, and re-anchors at the segment it was producing. Fetches that arrive meanwhile are held, so AVPlayer sees a slow segment, not an error.

  • Retried: .retryable and .unknown failures. .permanent ones (404, 403 and other 4xx) are not. An .unknown failure that recurs right after a successful re-open, before a segment has landed, is not retried either.
  • How often: after 1, 2 and 4 s, and at most three re-opens in any 60 s. stop() ends a backoff at once.
  • A Matroska demuxer that resyncs past a failed read (reporting success with a hole behind it) is caught and repaired, not served.
  • Sequential sessions behave as before: the failure ends the remux, and the playlist stays valid up to the last segment written.
  • A host PrismCoreInputFactory is called again for each re-open and must hand out a fresh instance over the same bytes, from any thread.

The diagnostic report

One Codable value with everything the engine knows about a playback — built to be attached to a bug report:

Swift
let report = await session.diagnosticReport()
let json = try report.jsonData()                  // pretty-printed, sorted keys: two reports diff line by line
print(report.schemaVersion, json.count)

It contains the PrismCore and FFmpeg versions (with any ABI mismatch) and capabilities, the platform, the source's description and container layout, the routing verdict and its reason, the session's options, every startup checkpoint with its time, the plan (basis, segment count, longest entry, tail, whether the index covered the end), the probe's phase timings, and the run so far: the newest 64 playback events, timestamp repairs, audio rendition production, object-audio and Dolby Vision findings, retention evictions and the classified failure. The session records checkpoints and events itself, so the report has them even if you never subscribed.

A source that never got a session — declined, or sent to the software path — gets the same report from its probe:

Swift
let probed = try SourceProbe.open(url: mkvURL)
let report = PrismCoreEngine.diagnosticReport(
    for: probed,
    decision: Result { try PrismCoreEngine.decide(for: probed.info) }
)

What a report leaves out. It is built to be pasted into a public issue, so it never carries HTTP header values (only how many there were), the URL's query string, fragment or user:password@, the LAN access token, or any path on the device's disk — the work directory, keyframeIndexCacheDirectory (reported as on or off) and sidecar subtitle files are all withheld, and a local source is named by its file name alone. Error descriptions are scrubbed of the same values. Two things stay in: a remote URL's host and path, and the track titles, languages and file name the media itself carries. If your URLs keep a token in the path, cut it before sharing.

schemaVersion (currently SessionDiagnosticReport.currentSchemaVersion, 1) names the shape; fields are only ever added, never renamed, and init(jsonData:) reads a report back.

The FFmpeg build

Swift
print(FFmpegBuild.summary)

prints FFmpeg's version string, a line per linked library (with the header version beside it whenever they disagree), and the capability answers that change what the engine does: the eac3 encoder, the audio bridge target, the AV1 decoder and whether this device has hardware for it, dialogue boost, and the GPU deinterlacer. "Audio track is missing" is often unreproducible until you know which FFmpeg was asked — log it once at launch. FFmpegBuild.configuration carries libavcodec's full configure line.

The engine's log

PrismCore writes its notices to the unified log under the subsystem cz.zmrhal.prismcore (category engine). Nothing it logs carries user data. Read it on a Mac with:

Shell
log show --predicate 'subsystem == "cz.zmrhal.prismcore"' --last 10m

There is no public API to change the engine's log level; prismcore-cli -v turns on verbose libav logging for terminal runs.

From a terminal

prismcore-cli runs the engine on one source on a Mac, so a field report can be reproduced without a device build — probe --json prints the diagnostic report, and serve / segverify take --report FILE. See Testing.