Skip to content

5.29.0 - Container chapters, opt-in video Now-Playing, second-open HLS reroute

Choose a tag to compare

@superuser404notfound superuser404notfound released this 29 Jul 09:40

Two external contributions from natedogg058, both merged and then hardened here, plus a routing fix reported by qoli. No breaking changes.

Added

  • Container chapters as mediaChapters. Matroska and MP4 chapters are read off the probe demuxer at load and published as @Published mediaChapters: [ChapterInfo], so a host can bind a chapter picker for ordinary files the way it already can for discs. Empty for disc sources, which keep publishing discChapters, so exactly one of the two is ever populated. The two differ in time axis: a disc chapter is title-relative and is seeked through selectChapter(id:), which adds the backend's content-start base, whereas a container chapter's startSeconds is already a timestamp on the seek(to:) axis and a host passes it straight through. selectChapter(id:) stays disc-only and no-ops for a container chapter id. Ids are sequential in start order, so they stay usable as list indices, and untitled entries are numbered "Chapter N". Contributed by natedogg058 (#179).

    A chapter's duration runs to the next chapter's start rather than to its declared end, because muxers routinely write end == start; the last entry falls back to its declared end and then to the container duration, since the next-start rule cannot cover it.

  • System Now-Playing on the native video path, opt-in. A host with its own transport can let the engine own an MPNowPlayingSession bound to the native player: set ownsVideoNowPlayingSession before load(), register transport commands on the published videoNowPlayingSession, and stage identity metadata with setVideoNowPlayingInfo(_:). The staged dictionary is replayed onto every fresh AVPlayerItem, so the readiness-gate master reload, the media fallback and an in-place PiP swap keep the system card instead of dropping it mid-session. The session lives on the native host and therefore survives native->native reloads with it, which is what keeps Now-Playing ownership across a background pause. The session auto-publishes elapsed time, rate and duration from the player, so nothing writes MPNowPlayingInfoCenter.default().nowPlayingInfo. Contributed by natedogg058 (#180).

    It is off by default and must stay off under AVPlayerViewController. AVKit owns Now-Playing itself there, through its own registration, and WWDC22's guidance is not to bring a session when using AVKit. Two owners produce the half-working state: an empty identity card displaces AVKit's, and remote commands route into a session with no handlers on it. docs/formats.md previously told custom-transport hosts to hold their own session against currentAVPlayer; it now points at this API.

Fixed

  • A remote HLS VOD source died when the load-time probe failed for an unrelated reason. The AE#154 reroute onto the native remote-HLS bypass keys on the reader's typed hlsPlaylistOnVODPath classification, and that classification only reached it when the load-time probe was the open that read the playlist body. If the probe failed transiently first, probeFailure was inconclusive, the load fell through to the loopback path with no preopened demuxer, and HLSVideoEngine.start() reopened the URL. That second open is then the first one to see the body, so it produced the classification, which was interpolated into openFailed(reason: "\(error)") and lost its domain one frame before anything could act on it. The result was a terminal HLS playlist supplied to the VOD loopback path for a source AVPlayer plays fine, while a manual retry whose first probe happened to see the body routed natively and reached readyToPlay. Reported by qoli (#246).

    The fallback open now rethrows both HLS classifications verbatim (hlsPlaylistOnRawLivePath rides along, so the AE#140 fail-closed contract stays typed at every open too), and load() answers one from either origin with the same reroute. It is taken as a full load() restart, the shape the #168 reroute already uses, because the half-built loopback session has to be cleared; the bypass runs no probe, so it cannot bounce back a second time. No isLive guard is needed: the reader only classifies as hlsPlaylistOnVODPath on the non-live path.

  • The remote-HLS bypass ignored the resume position. LoadOptions.nativeRemoteHLS routes before the probe, and that branch never forwarded startPosition, so a VOD playlist restarted at zero whether a host requested the bypass directly or arrived on it through a reroute. Without this the reroute above would have preserved URL and headers and then quietly dropped the anchor. VOD honors it now; live keeps its no-initial-seek contract even when a host passes one.

Hardening applied to the merged contributions

  • The container-chapter wipe sat in stopInternal with a snapshot/restore pair around the reload helper, but teardownVideoForBackground and the #127 expiry call stopInternal outside that helper, so an iOS background roundtrip emptied the list for the rest of the session. Chapters belong to the source URL rather than to the pipeline, which is the rule fontAttachments already follows, so the wipe moved to stop() and both reload hunks came out. tvOS was never affected.
  • A zero time-base numerator is rejected rather than accepted; it scales every chapter timestamp to zero.
  • The Now-Playing identity write and its per-item replay are gated on ownership, so an AVKit host cannot have a second, host-authored identity fed into AVKit's own publication.
  • becomeActiveNowPlaying() re-asserts on the load path, matching the audio path. A host preserved across a reload never re-runs init, so a session another app took over was never claimed back.
  • pendingVideoNowPlayingInfo clears in stop() next to pendingExternalMetadata, while still surviving stopInternal so a reload keeps the card through the seam.

Tests

Issue246SecondOpenRerouteTests covers the open-failure mapping and the reroute decision on the error it now preserves, including the regression direction: a wrapped classification must not reroute, which is why it must not be wrapped. ContainerChapterMappingTests covers ordering, ids, naming and all three duration rules over the pure mapping function. VideoNowPlayingOptInTests covers the opt-in gate and the staging semantics. Suite is 1272 tests, green, with -strict-concurrency=complete clean and the tvOS Simulator build succeeding.