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:

Swift
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:

Swift
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; .conservative claims nothing.
  • A session is single use: one start(), one stop(). To play the same title differently — a dialogue-boost level, a smaller cache — mint a successor with makeSession(changing:).
  • Registration calls such as addExternalSubtitle only make sense before start(); after it they throw SessionError.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:

Swift
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:

Swift
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:

Swift
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