PrismCore / Concepts

Capabilities

Exactly what PrismCore 3.5.4 handles — containers, codecs, HDR and Dolby Vision, audio bridging and layouts, subtitles and captions, seeking, caching, HTTP and diagnostics — and where each one stops.

Everything on this page is what the 3.5.4 source does. Where a behaviour depends on the FFmpeg build that is loaded, it says so; Platform requirements lists those dependencies in one place.

Containers

There is no container allow-list. Anything libavformat can open and describe is accepted — MKV, MP4, MPEG-TS, AVI, WebM, FLV and the rest. The probe is bounded: 10 s by default (SourceOpenTuning.probeBudget), with a 4 MiB probe size and 2 s analyze duration. The video track is the one libavformat calls best.

Chapters from Matroska Chapters and MP4 chapter tracks are reported as SourceInfo.chapters and PrismCoreSession.chapters (an optional title, the start, and the end in seconds where the container gives one). HLS has nowhere to carry them, so drawing chapter markers is the host's job.

Routing: remux or software

PrismCoreEngine.decide(for:) reads the probe's SourceInfo.nativeReadiness and answers:

SourceDecision
No video streamThrows RoutingFailure.noVideoStream — PrismCore is a video path
Video copies, and at least one audio track copies (or there is no audio)Remux
Video copies, every audio track needs the bridge, and the build has a bridge encoderRemux, audio bridged
Video copies, every audio track needs the bridge, no bridge encoderSoftware, which decodes the audio itself
Video cannot be copied (VP9, MPEG-2, VC-1, …) or is verified interlacedSoftware, if the build has a decoder for it
No native path and no software decoderThrows RoutingFailure.noDecoderForVideo(codecName:)

PrismCoreEngine.open(url:) runs the probe, the decision and the chosen engine in one call, handing the probe's open context to the engine so the source is not opened twice.

Video

CodecPath
H.264, HEVC (incl. Main 10)Stream-copied into fMP4 (remux)
AV1Stream-copied only where the device has a hardware AV1 decoder (VTIsHardwareDecodeSupported); otherwise the software path, via libdav1d
VP9, VP8, MPEG-1, MPEG-2, MPEG-4 Part 2, VC-1, WMV3, TheoraSoftware path
Interlaced H.264Software path, once verified (below)

HEVC sample entries. A stream-copied HEVC track gets an explicit hvc1 sample entry, because Apple's HLS rules want hvc1 and FFmpeg's muxer defaults to hev1. Because hvc1 asserts every parameter set lives in the record, the hvcC is rewritten (VPS/SPS/PPS, completeness asserted) — on the input extradata and again on the produced init segment. Sources that carry parameter sets in band get them harvested off the first keyframes.

Interlace. Only declared-interlaced H.264 is checked: the probe decodes up to 12 frames (or 120 packets) before believing the flag, because broadcast H.264 is routinely flagged interlaced around progressive frames. Only a verified verdict moves a source off the native path. On the software path interlaced video is deinterlaced at field rate — yadif_videotoolbox on VideoToolbox frames where the build has it, otherwise CPU bwdif.

Anamorphic video keeps its sample aspect ratio in the pasp box AVPlayer honours.

HDR and Dolby Vision

HDR10 (PQ) and HLG are signalled in the master playlist (VIDEO-RANGE, which also requires a FRAME-RATE) and tone-mapped by the system where the display needs it. HDR claims are only made for a display the session was told can present them — see DisplayCapabilities.

Dolby Vision profileHandling
5dvh1 sample entry, CODECS="dvh1.05.LL". On a display that is not Dolby Vision capable there is no master playlist; the source plays in the media-playlist shape.
8.1, 8.4hvc1 plus SUPPLEMENTAL-CODECS (dvh1.08.LL/db1p or /db4h) on a Dolby Vision display; otherwise the dvvC box is stripped and the HDR10/HLG base layer plays
7 (dual layer)Converted in flight to single-layer 8.1 through libdovi (enhancement layer dropped) and declared as 8.1
8.2Plays as plain SDR — its base layer is Rec. 709

The Dolby Vision boxes need -strict unofficial; a Dolby Vision source muxed without them would be HDR10 with extra bytes. PrismCoreSession.dolbyVisionConversion reports what the Profile 7 conversion did.

HDR10+ ST 2094-40 metadata rides the stream copy like any other SEI. Detection is opt-in and report-only: SourceProbe.open(url:hdr10Plus: .standard) reads a bounded run of video packets and reports seen, notSeenWithinBudget or unknown(reason) in SourceInfo.hdr10Plus — never "absent". It changes nothing about playback or display criteria.

Audio

Copied bit for bit

AAC, AC-3, E-AC-3, FLAC and ALAC are stream-copied. Every viable track becomes an HLS alternate audio rendition with its language, name and channel count, so AVPlayer gets a real AVMediaSelectionGroup.

Dolby Atmos

E-AC-3 with JOC objects is stream-copied, never decoded. AVFoundation takes the Dolby/MAT route only when the dec3 box in the init segment carries the TS 103 420 type-A extension — which FFmpeg's mp4 muxer drops on a stream copy. PrismCore reads complexity_index_type_a from the bitstream (up to 24 packets, including JOC in a dependent substream) and patches it back into the init segment. The rendition declares CHANNELS="16/JOC" when the container metadata marks the track as object audio; PrismCoreSession.objectAudio reports what the bitstream showed, including tracks the metadata missed.

Bridged: TrueHD, DTS and the rest

Codecs that cannot ride HLS-fMP4 are decoded, resampled and re-encoded by the audio bridge:

  • Bridged codecs: TrueHD, MLP, DTS (including DTS-HD MA and DTS:X), MP3, MP2, Opus, Vorbis, and PCM (S16/S24/S32 LE and BE, F32LE, Blu-ray and DVD PCM).
  • Target codec: E-AC-3 when the FFmpeg build has an eac3 encoder; otherwise AAC, which every FFmpeg build has. Everything downstream — the playlist CODECS, the init segment's sample entry, FFmpegBuild — follows the target. FFmpegBuild.capabilities.audioBridgeEncoder names it.
  • Bitrate: 128 kbps per channel (256 kbps stereo, 768 kbps 5.1), at the source's sample rate when the encoder supports it, otherwise 48 kHz.
  • Objects do not survive. FFmpeg's E-AC-3 encoder writes no JOC, so a TrueHD Atmos or DTS:X source arrives as its channel bed. A real E-AC-3+JOC source never enters the bridge — it is copied.

Channel layouts.

Source channelsE-AC-3 targetAAC target
1monomono
2stereostereo
3closest layout the encoder lists3.0
4closest layout the encoder lists4.0
5closest layout the encoder lists5.0
6 (5.1, incl. 5.1(side))5.15.1
7.1 and widerdownmixed to 5.1downmixed to 5.1

For E-AC-3 the layout is negotiated against the encoder's own list: an exact match wins, otherwise the widest layout that does not exceed the source. FFmpeg's E-AC-3 encoder tops out at 5.1.

For AAC every count maps to a layout with an implicit MPEG-4 channel configuration, so the encoder never has to write a Program Config Element — AudioToolbox does not reliably decode PCE-only configurations, and a rendition that needs one fails AVPlayer's asset check for the whole master. Side surrounds are routed into the back pair by the resampler. 3.5.4 fixed the 3-channel case: it used FFmpeg's default "2.1" layout (FL FR LFE), which has no channel configuration; it is now 3.0.

When renditions are produced

Only the DEFAULT audio rendition and stream-copied ones are produced from the first packet. A bridged track that is not the DEFAULT, and every dialogue-boost level, is declared in the master and started by AVPlayer's first fetch under its directory (a re-anchor at the demanded segment). A sequential session has no demand seam, so it leaves those renditions out of the master rather than encoding them for the whole film. audioRenditionProductions lists which was which; AudioRenditionProduction.summary(_:) is a one-line form for a startup log.

Default track and preferred language

The DEFAULT rendition is chosen in this order: a preferredAudioLanguage: match among carriable tracks; the container's original disposition; libavformat's best stream if copyable, then if bridgeable; the default disposition; any copyable track; any bridgeable track. Language tags match tolerantly — cze, ces, cs and cs-CZ are one language, an exact region beats a bare tag, and und matches nothing. No match is a no-op, never an error; nothing is ever dropped.

Dialogue boost

Opt-in with dialogueBoost: [.medium, .high]: extra renditions derived from the DEFAULT track, decoded, the centre channel favoured (the rest of the bed −6 dB or −12 dB), re-encoded with the bridge's target, and marked public.accessibility.enhances-speech-intelligibility. It is engine-side because AVFoundation ignores audioMix and audio taps on HLS items. The base track stays bit for bit. It needs a centre channel (stereo is not supported), a bridge encoder and FFmpeg's pan filter — check PrismCoreSession.isDialogueBoostAvailable; dialogueBoostRenditions reports what made it into the master.

Audio delay

audioDelaySeconds: and setAudioDelaySeconds(_:) shift audio against the picture by up to ±2 s (non-finite values become 0). On the remux path a runtime change re-anchors production at the playhead and discards every segment written with the old offset (.pendingReanchor); on a sequential session it returns .unsupported. On the software path it is applied at the renderer immediately. See the known limitation for reducing an initial positive delay.

Subtitles and captions

KindHandling
Text: SubRip, ASS/SSA, WebVTT, mov_text, plain textConverted during the remux read into segmented WebVTT renditions, cut on the video's own segment boundaries. ASS italic/bold/underline become WebVTT tags; \an / \pos placement and WebVTT cue settings ride the timing line.
Sidecar .srt / .vttaddExternalSubtitle(url:language:name:isForced:) before start() — first-class renditions
Bitmap: PGS, DVB, DVDOn-device Vision OCR into the same rendition machinery — only when the source has no text track. Otherwise a host can ask for OCR into the cue tap with requestBitmapSubtitleOCR(streamIndex:). Lossy by design: typography is lost, text survives.
CEA-608 closed captions in H.264/HEVC SEIDecoded into WebVTT renditions CC1–CC4 — see Closed captions
CEA-708Recognised and skipped, not decoded

Renditions are named by their language in that language's own name ("Čeština"), with the track title added only when it says something the language cannot (SDH, Forced, Signs, Commentary). Every master says CLOSED-CAPTIONS=NONE unless the host opts into inBandClosedCaptions. setSubtitleDelaySeconds(_:) shifts subtitles by up to ±10 s for cues and segments written from then on. setTimedTextCueHandler(_:) streams every embedded cue to a host that draws its own overlay; its replay history can be bounded with subtitleCueHistory:.

Seeking and the segment plan

For seekable on-demand sources a keyframe-aligned segment plan (6 s target, 2 s first segment) is published as a complete VOD playlist before the first packet, and production is demand-driven: a fetch outside the producer's window re-anchors it to the right keyframe, with absolute tfdt so every segment lands exactly where the playlist promised. A source without a trustworthy index plays sequentially on first play while the producer harvests its keyframes; with keyframeIndexCacheDirectory set, the next play plans from that map. Details, budgets and failure modes: Seeking and segment plans and Keyframe cache identity.

Caching

  • Segment cache. segmentCacheBytes (default 1 GiB, nil keeps everything) bounds disk. In a planned session an evicted segment is reproduced on demand, so the budget bounds disk, not seekability; segments near the producer and any segment being served are never evicted.
  • Resident media. residentRanges lists the video already on disk (source timestamps). cachedThumbnail(at:maxDimension:) and cachedAudio(from:duration:renditionName:) read only what is resident — they never fetch, seek or disturb playback.
  • Scrub previews. SeekPreviewService decodes the keyframe covering any position into a CGImage on its own source context, on the CPU, with a 32-entry cache — independent of which engine is playing.

Coordinated HTTP

Remote sources are read by FFmpeg's own HTTP by default. coordinatedHTTP: true (on the session, PrismCoreEngine.open, SourceProbe, SeekPreviewService and the software pipeline's load(url:)) switches finite HTTP(S) files to PrismCore's range reader:

  • 1 MiB Range requests; every reply must be a 206 with a valid Content-Range. A server that answers 200 to a range request is not supported, nor are live streams or nested HLS playlists.
  • At most 2 requests in flight per origin, shared by every reader and session in the process, with Retry-After-aware backoff on 429/503/509 that is reported as PlaybackEvent.originThrottled / .originRecovered.
  • The validator (ETag) must stay constant across requests; it is what lets the keyframe cache prove the file has not changed.
  • Redirects are followed by hand: HTTPS→HTTP is refused and caller headers are dropped on a cross-origin hop — so an authenticated redirect that needs forwarded custom headers should use the default transport or a final URL.

Loopback server and AirPlay

Segments are served over HTTP/1.1 on 127.0.0.1 (GET and HEAD, keep-alive, pipelining, path-traversal and size limits). A serve that waits more than 2 s gets an early 200 with chunked transfer, keeping the response inside AVPlayer's media watchdog.

For AirPlay to an external receiver, opt in per session with reachability: .localNetworkUnencryptedForAirPlay: the server binds one LAN IPv4 address — never 0.0.0.0, so a VPN or an Internet Sharing bridge is never exposed. Tunnels, peer-to-peer radios (awdl, llw, nan) and self-assigned 169.254/16 addresses are excluded, and a real en interface is preferred over a bridge — and every URL carries a 192-bit token. A changed address is reported as serviceAddress == .addressLost(…) and answered with 503. This is cleartext HTTP on the local network: the token makes the server unguessable, not private. Enable it only for the duration of an AirPlay route.

Custom input

A host that holds the bytes itself — an SMB client of its own, an encrypted store, a file inside a disc image — implements PrismCoreInput (read, seek, length) and passes a PrismCoreInputFactory as input: to the session, SourceProbe or SeekPreviewService. The factory is called once per open (the probe, the producer and a preview never share a cursor) and again on every recovery re-open. An input without a length is refused with PrismCoreInputError.notSeekable; adopt CancellablePrismCoreInput so the engine can interrupt a blocking read on stop().

Recovery and diagnostics

  • A planned session whose source fails mid-film re-opens it and carries on from the segment it was producing — after 1, 2 and 4 s, at most three re-opens in any 60 s, for retryable and unknown failures only. Reported as PlaybackEvent.producerRecovered / .producerFailed.
  • PrismCoreError.classify(_:) maps any error into 11 cases with a three-valued retryability.
  • startupCheckpoints(), playbackEvents() and diagnosticReport() cover startup timing, runtime events and a redacted Codable report for bug reports.

See Diagnostics.

Not in scope

No UI, no published playback state (your AVPlayer is the source of truth), no subtitle rendering, no playlist or queue management, no analytics — nothing leaves the device — and no re-encoding of video, ever.