PrismCore / Getting started
Quick start
Play your first MKV through AVPlayer, then drive a PrismCoreSession directly for languages, subtitles and the tvOS display handshake.
One call: probe, decide, start
PrismCoreEngine.open(url:) probes the source, picks the path and returns something already started:
import AVFoundation
import PrismCore
func play(_ mkvURL: URL, in player: AVPlayer) async throws {
switch try await PrismCoreEngine.open(url: mkvURL) {
case .remux(let session, let playlist):
// AVPlayer plays it. Keep `session` alive for as long as the item plays.
player.replaceCurrentItem(with: AVPlayerItem(url: playlist))
player.play()
// … when playback ends:
await session.stop()
case .software(let pipeline):
// PrismCore plays it: the pipeline owns decode, renderers and clock.
// `displayLayer` is set once the pipeline is loaded, which `open` guarantees.
print("software path, layer:", pipeline.displayLayer as Any)
pipeline.play()
// … when done:
pipeline.stop()
}
}The remux path is the one that matters: it is what keeps hardware decode, Atmos passthrough and Dolby Vision. open accepts httpHeaders: (a Plex token, a WebDAV Authorization), display:, segmentCacheBytes:, audioDelaySeconds: and coordinatedHTTP:; everything else is configured on a session you build yourself.
If PrismCore declines a source, open throws PrismCoreEngine.RoutingFailure — probeFailed, noVideoStream, noDecoderForVideo(codecName:) or startupFailed. Every case means "give this to your other engine", not "this file is broken".
Driving a session directly
PrismCoreSession is an actor. Build one when you want the detail — preferred languages, sidecar subtitles, a keyframe cache, startup progress:
let session = try PrismCoreSession(
url: mkvURL,
display: await DisplayCapabilities.current(),
keyframeIndexCacheDirectory: cachesDirectory,
preferredAudioLanguage: "cs", // "ces", "cze" and "cs-CZ" match too
preferredSubtitleLanguage: "cs"
)
// Before start(): a sidecar joins the WebVTT renditions.
try await session.addExternalSubtitle(url: srtURL, language: "cs", name: "Čeština")
let playlistURL = try await session.start() // default startup budget: 20 s
player.replaceCurrentItem(with: AVPlayerItem(url: playlistURL))
player.play()
// …
await session.stop()display:decides which HDR and Dolby Vision claims the master playlist may make.DisplayCapabilities.current()(main actor) reads them from the screen the host is playing on;.conservativeclaims nothing.- A session is single use: one
start(), onestop(). To play the same title differently — a dialogue-boost level, a smaller cache — mint a successor withmakeSession(changing:). - Registration calls such as
addExternalSubtitleonly make sense beforestart(); after it they throwSessionError.alreadyStarted.
Watching startup
start() can take seconds on a slow origin. Subscribe before calling it to get each stage as it lands, timed from the call:
let checkpoints = try await session.startupCheckpoints() // before start()
Task {
for await mark in checkpoints {
switch mark.phase {
case .sourceOpened: print("Opening…")
case .streamInfoResolved(let info): print(info.video?.codecName ?? "no video")
case .segmentPlanReady(let origin, _): print(origin == .sequential ? "Indexing on first play…" : "Preparing…")
case .firstVideoSegmentWritten: print("Starting playback…")
case .playlistServable: break
}
print(mark.elapsed, mark.phase)
}
// The stream finishes on success, on failure and on stop().
}
let playlist = try await session.start()There is deliberately no percentage: nothing can know in advance how long a probe over a slow origin takes, and the engine does not report numbers it cannot measure.
tvOS: program the panel first
Over HDMI the panel's mode must be set before AVPlayer sees the playlist. tvOS validates an HDR variant's VIDEO-RANGE against the panel's current mode, so a PQ master handed to an SDR-parked panel fails outright (-11848 / -11868) instead of switching. Set appliesPreferredDisplayCriteriaAutomatically = false on your AVPlayerViewController and let the engine drive the order:
let controller = DisplayCriteriaController(window: window) // tvOS only
let session = try PrismCoreSession(url: source, display: await DisplayCapabilities.current())
let playlist = try await session.start()
if let choice = await session.displayCriteria {
controller.apply(choice) // 1. program the panel
await controller.waitForSwitch() // 2. let the handshake settle
}
player.replaceCurrentItem(with: AVPlayerItem(url: playlist)) // 3. only then load
player.play()
// On teardown: controller.reset()session.displayCriteria is clamped to the display the session was built for: a non-Dolby-Vision panel is asked for the base layer's range, a non-HDR panel for a frame-rate-only switch — which is also what makes Match Frame Rate engage on SDR content. Built-in panels (iPhone, iPad, Mac) engage HDR on demand and need none of this.
PrismCoreSession.isMasterRejection(_:) together with makeMasterRejectionFallbackSession() remains the backstop for the one state no API can detect beforehand: Match Content switched off on an HDR-capable panel.
Handling failures
PrismCoreError.classify(_:) turns anything the engine — or your AVPlayer — threw into one machine-readable case:
do {
let playlist = try await session.start()
player.replaceCurrentItem(with: AVPlayerItem(url: playlist))
} catch {
switch PrismCoreError.classify(error) {
case .originRefused: print("refresh the token, then retry")
case .originRateLimited(_, let retryAfter, _): print("back off for", retryAfter ?? 5, "s")
case .videoCodecNotRemuxable: print("route to the software path")
case .masterRejectedByPlayer: print("make the rejection fallback session")
default: print(error)
}
}More in Diagnostics.
Next steps
- Seeking and segment plans — what makes a seek instant, and when it is not.
- Closed captions and subtitles.
- The PrismCoreSession reference — every initializer parameter, documented.