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:
| Source | Decision |
|---|---|
| No video stream | Throws 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 encoder | Remux, audio bridged |
| Video copies, every audio track needs the bridge, no bridge encoder | Software, which decodes the audio itself |
| Video cannot be copied (VP9, MPEG-2, VC-1, …) or is verified interlaced | Software, if the build has a decoder for it |
| No native path and no software decoder | Throws 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
| Codec | Path |
|---|---|
| H.264, HEVC (incl. Main 10) | Stream-copied into fMP4 (remux) |
| AV1 | Stream-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, Theora | Software path |
| Interlaced H.264 | Software 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 profile | Handling |
|---|---|
| 5 | dvh1 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.4 | hvc1 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.2 | Plays 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
eac3encoder; otherwise AAC, which every FFmpeg build has. Everything downstream — the playlistCODECS, the init segment's sample entry,FFmpegBuild— follows the target.FFmpegBuild.capabilities.audioBridgeEncodernames 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 channels | E-AC-3 target | AAC target |
|---|---|---|
| 1 | mono | mono |
| 2 | stereo | stereo |
| 3 | closest layout the encoder lists | 3.0 |
| 4 | closest layout the encoder lists | 4.0 |
| 5 | closest layout the encoder lists | 5.0 |
| 6 (5.1, incl. 5.1(side)) | 5.1 | 5.1 |
| 7.1 and wider | downmixed to 5.1 | downmixed 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
| Kind | Handling |
|---|---|
| Text: SubRip, ASS/SSA, WebVTT, mov_text, plain text | Converted 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 / .vtt | addExternalSubtitle(url:language:name:isForced:) before start() — first-class renditions |
| Bitmap: PGS, DVB, DVD | On-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 SEI | Decoded into WebVTT renditions CC1–CC4 — see Closed captions |
| CEA-708 | Recognised 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,nilkeeps 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.
residentRangeslists the video already on disk (source timestamps).cachedThumbnail(at:maxDimension:)andcachedAudio(from:duration:renditionName:)read only what is resident — they never fetch, seek or disturb playback. - Scrub previews.
SeekPreviewServicedecodes the keyframe covering any position into aCGImageon 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
Rangerequests; every reply must be a206with a validContent-Range. A server that answers200to 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 asPlaybackEvent.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-valuedretryability.startupCheckpoints(),playbackEvents()anddiagnosticReport()cover startup timing, runtime events and a redactedCodablereport 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.