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:
| Case | Meaning | Retryability |
|---|---|---|
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 |
noVideoStream | The source has no video | .permanent |
videoCodecNotRemuxable(codecName:streamIndex:) | The video cannot ride HLS-fMP4 | .permanent |
videoCodecUnplayable(codecName:) | No path can decode it | .permanent |
startupBudgetExpired | Startup 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:
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:
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 withcoordinatedHTTP, and they reach every session reading that origin.producerRecovered/producerFailed— the self-healing outcome.producerFailedis sent exactly once per session that fails, mid-film or at startup, and never for astop().
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:
.retryableand.unknownfailures..permanentones (404, 403 and other 4xx) are not. An.unknownfailure 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
PrismCoreInputFactoryis 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:
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:
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
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:
log show --predicate 'subsystem == "cz.zmrhal.prismcore"' --last 10mThere 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.