Repository navigation
7.48.0 - VP9 hardware decode, AV1 Dolby Vision, MPEG-DASH
Added
-
VP9 can use VideoToolbox hardware decoding on the software playback host (AE#878, thanks @jonluca). WebM streams
synthesize the requiredvpcCrecord from probed parameters. The hardware candidate accepts
8-bit profile 0 and 10-bit profile 2 with 4:2:0 chroma, requires a hardware session, and falls
back to libavcodec if registration, configuration or session creation fails. Decoder diagnostics
report the implementation that actually opened. A key frame that changes the coded size rebuilds the
session at the new size; a session built for the old one rejects every frame after the switch. -
Hosts can own renderer audio-session activation (AE#872, thanks @jonluca).
rendererAudioSessionActivationEnabled = falsebeforeload()stops software and audio-only loads, track
reloads and interruption recovery from activating the session, and route changes from writing the channel
preference, for hosts that coordinate audible focus across several players. The category is now declared
off-main by the first load instead of at init, shared by concurrent engines and never repeated after it
succeeded, so a host that switches the mode afterwards (music on.default) keeps it. -
AV1 Dolby Vision Profile 10.1 plays as Dolby Vision on the software host.
AVFoundation and VideoToolbox ignore the RPU an AV1 stream carries in its ITU-T T.35 metadata OBU, so every
Apple route showed a 10.1 source's HDR10 base layer, and an Apple TV, where AV1 always plays through dav1d,
had no Dolby Vision for AV1 at all. Each temporal unit's RPU is now rewrapped by libdovi into theunspec62
form VideoToolbox attaches to an HEVC 8.1 frame and attached to the decoded frame underDoviDisplayMetadata's
keys, which now compile without theDualLayerDolbyVisiontrait. Measured on an Apple TV 4K (3rd generation,
tvOS 27) into an LG CX and on macOS 27 (M5 Pro). Seedocs/formats.md. Every RPU passesDoviRpuGuard(CRC and the
known panic paths) before libdovi, built panic=abort, parses it, as every HEVC RPU already does. -
Frame-accurate downscaled stills through
FrameExtractor.preview(at:maxWidth:)(AE#871, thanks @jonluca).
Previews decode forward to the requested frame while retaining full reconstruction and high-quality
scaling. They yield to starved playback and keep a small, separate cache. The CLI exposes the same
behavior withaetherctl extract --preview --width <px>. -
Hosts can retain a paused video pipeline through background suspension (AE#870, thanks @jonluca).
retainsVideoPipelineWhenBackgrounded = trueskips the background teardown of a PAUSED session, so a return
that finds the decoder intact pays no rebuild; off by default, because the system may still invalidate it
and the host then reloads. A retained session that resumes while still backgrounded (lock screen, Control
Center) gets the playing-session policy at that moment. The software host flushes a failed video queue on
play before the feeder wakes, and keeps a healthy one's frames. -
Remote MPEG-DASH on-demand manifests play (AE#859, thanks @qoli).
A static, single-Period MPD whose representations are single files addressed bySegmentBase
failed at open withInvalid data found when processing input: the FFmpeg build has no DASH
demuxer and no network protocols. The engine now recognizes the manifest (by.mpd, or by its
body on an extensionless URL), fetches each selected representation's initialization segment and
segment index, and lays the video plus one representation per audio adaptation set out as one
fragmented MP4 that the normalmovpath demuxes, so both playback routes, seeks, audio track
switching andprobework unchanged. Headers ride every request. Live manifests,SegmentTemplate
/SegmentListand encrypted content are refused with the reason. Seedocs/formats.md. -
isClockHeldandclockHoldEventspublish the software path's clock hold (AE#800, thanks @cmcpherson274).
The clock that waits for the first frames after a start, a seek, an in-place switch or a resume
stood at rate 0 with nothing but an[AudioOutput]log line to say so, so a host judging stalls
from the clock counted the hold as one, and with several engines at once could not tell whose line
it was. The level and a.began/.released(.ready | .timedOut | .cancelled)stream now come from
each engine instance. Seedocs/api.md. -
The software host (the Dolby Vision FEL route) can now show a Now-Playing card and answer remote
commands. It has noAVPlayer, so it had neither. With the newownsSoftwareNowPlayingset (off by
default, independent ofownsVideoNowPlayingSession), the engine publishes title, artwork, duration,
elapsed and rate from the stagedexternalMetadata/setVideoNowPlayingInfo, and serves play, pause,
toggle, skip (remoteSkipInterval, default 10 s) and change-position from the Siri Remote, headsets,
Control Center and media keys. It republishes on seek, pause, speed change, stall and end.
softwareNowPlayingActivetells a host when to add its own next/previous. The AVPlayer paths are
unchanged. -
Scrub thumbnails work on a software session that plays a local file.
supportsCacheBackedStillswas false there andscrubThumbnailandscrubPreviewFrameanswered
nil, so a Dolby Vision FEL title (which plays through the software host) had no scrub stills
unless the host built its ownFrameExtractor. The engine now decodes them from the file, the
base layer for a dual-layer title, with no extra connection and through the same calls the
AVPlayer path answers:scrubPreviewFramehonoursisCancelledand the 750 ms / 900 packet bound
and reports the decoded frame's own time. A file on a network mount opens a second file handle.
A Dolby Vision Profile 5 / 10.0 file without theDolbyVisionStillReshapingtrait keeps
supportsCacheBackedStillsfalse, since none of its frames can become a still. Remote software
sessions already served stills from the packet cache. -
The software host follows the system caption style. Subtitles composited into software-path
PiP frames now use the user's Settings > Accessibility > Subtitles & Captioning choices (font,
size, text colour and opacity, background and window colour and opacity, edge style) the way
AVPlayer's legible output does, and re-render when the setting changes. A field counts as chosen
only where it differs from the system default, and carries the system's precedence against the
cue's own styling.SubtitleCaptionStyle.current()andobserveChanges(_:)expose the same style
to a host that draws its own overlay. Bitmap subtitles are untouched. -
The software playback host keeps Atmos. On the software host (the Dolby Vision FEL route and
video the hardware decoder cannot play), E-AC-3 JOC is handed to the audio renderer compressed,
with the decoder primed once per process, and TrueHD Atmos and DTS:X are rendered to APAC under the
existingobjectAudioRenderingoption (DTS:X into 7.1.4 or 9.1.6 only, and a TrueHD track whose
profile the probe missed read from its own major sync, as on the loopback route). Other audio,
live streams and.offstill decode to PCM, and a TrueHD or DTS track kept on PCM says why in the
log. The automatic audio pick now prefers a TrueHD Atmos or DTS:X track on the software host too. A
renderer error on these routes is logged in release builds. On tvOS the Atmos output activator
runs on HDMI beside the routes that carry Dolby Atmos (TrueHD Atmos as APAC and compressed E-AC-3
JOC, not DTS:X), as it does beside the loopback route's TrueHD APAC, so a sink that needs it shows
Atmos for the software host too.audioDeliveryreports them as.bridged(APAC) and
.streamCopy(compressed E-AC-3 JOC), andactiveAudioDecodernames them (for example
"TRUEHD Atmos → APAC 7.1.4"); the PCM decode stays.decoded. The clock's "audio is not
recovering" fallback waits for the APAC route's first buffer (about 100 TrueHD access units) instead
of anchoring every TrueHD load on video after 50 packets. -
The renderer paths pause when a removable audio output goes away. On iOS and tvOS the software
host and the audio host now pause, as AVPlayer does, when headphones, a Bluetooth device (A2DP, LE,
HFP), an AirPlay output or a USB output is removed while playing or seeking. HDMI is not one of
them: an ARC / eARC soundbar or receiver switching on or off on an Apple TV keeps playing. The
native path is unchanged.
Changed
-
FFmpegBuild 3.10.1, for a libzvbi security update. libzvbi 0.2.46 fixes an out-of-bounds
write in the teletext DRCS conversion (CVE-2026-110654, GHSA-p2vx-rpw8-hp43, medium). The
libzvbi_teletextdecoder is built and the teletext path is wired end to end, so a DVB teletext
stream reaches that code. FFmpeg (n8.1.3), dav1d and zimg are unchanged in content. -
A Profile 7 FEL source keeps the native route while the host reports an AirPlay video receiver on the audio route (iOS).
The dual-layer route draws into a local layer that no receiver picks up, so a TV the viewer sent the
title to played sound only. When the host says the receiver shows video
(setAirPlayReceiverShowsVideo(_:), new, default off), the load takes the native route instead (8.1
where the display takes Dolby Vision, the enhancement layer is lost), logs
an AirPlay video receiver ...as the reason and skips theel_typescan. An audio-only output such
as a HomePod does not set it, so the title keeps the dual-layer route; the engine cannot tell the two
apart itself. The route is decided at each load: on a URL sourcereloadAtCurrentPosition()decides
again, a custom source keeps its route until the nextload(). -
The software host waits for a running display-mode switch before it starts playing (tvOS). The
native path already did; the software path gave up on an HDR switch at the pre-flight cap and never
waited for a rate-only one. -
airPlayPictureStaysLocalreads false while the screen is mirrored. The host reports it through
setScreenMirrored(_:)(new). -
TrueHD Atmos is decoded exactly and its objects are rendered again (#731).
Since 7.40.0 theTrueHDObjectAudiopath built every output out of the stream's 7.1 channel
presentation, split by where the objects' energy lay, with the objects decoded without their matrix
interpolation and taken as plain points. Against the exact render that left the heights 0.4 to
1.1 dB low and at a correlation of 0.91 to 0.96, and could not tell front heights from rear ones
once the energy sat between them. The path now decodes the object presentation exactly (LibTrueHD
3.1.0, which ships its interpolating decode again) and mixes the objects into the layout, with
size, snap, zone masks, screen anchoring and the elevation switch, as before 7.40.0.BedSplitter
and theTrueHDReferenceRendermeasurement trait are gone. The trait stays opt-in, and the
integrator remains responsible for the licensing of what it enables. -
PGS subtitle tracks no longer hold the playback probe to its byte ceiling (#858, thanks @qoli).
find_stream_infowill not let a PGS track go until a decoded composition segment has given it
a width. PGS is sparse, so on a source whose first cue lies past the probe, one such track kept
the main open reading to the 50 MiB ceiling (about 14 s on the reported remote Matroska source
with four of them). Known PGS tracks now sit out the probe the way undecodable audio does
(#466). Every track keeps its index, language and codec. The canvas comes from the decoded
composition segment, with the video size as fallback, just as on the subtitle side reader
since #87.
Fixed
-
A native VOD loaded with a start position no longer reports an earlier time once it plays (AE#881, thanks
@martijnklomp). AVPlayer finishes the mount seek the moment it is issued, before the item is ready, and that
completion released the clock hold. The periodic observer then published the keyframe or segment start the
position falls behind (0.000 for a start at 6 s) for up to 300 ms, so a progress bar flashed to 0:00 and a host
saving the resume point at that moment stored 0. A completion before readiness is no longer a landing; the
item's first roll releases the hold. -
The Atmos output activator rides through a short pause on HDMI (AE#848, thanks @nulltuple).
A host pause stopped the silent Atmos stream at once, so a receiver that needs it dropped its
Dolby Atmos badge on every pause and scrub and re-locked on resume. It now keeps playing for 60 s
and pauses only if the host is still paused then. Leaving APAC, leaving an all-HDMI route (AE#834)
or tearing the session down still releases it immediately. -
A refused HDR master no longer latches when the panel is shown to present HDR (AE#887).
A live PQ master refused with -11868 on an Apple TV whose output was fixed to 4K HDR latched the
process-wide panel refusal, so every later HDR title went media-direct without its subtitle
renditions until the app was backgrounded. The refusal no longer latches while the host asserts
panelIsInHDRMode(the assertion already decides the route), and an HDR headroom reading during
playback withdraws a latch taken earlier. -
The audio tap no longer re-decodes long segments at a parked playhead (AE#864, thanks @qoli).
A reader decodes whole segments, so a 15 s segment legitimately runs the decode lead past 10 s,
and the pacing read that overshoot as a backward seek: it jumped back to the playhead's segment and
decoded the same two segments over and over (120 s of PCM from 30 s of content, with a
discontinuity every cycle). A backward seek is now read off the playhead's own movement. The same
whole-segment decode also yielded a segment in one burst, well past the stream's 64-buffer bound,
so a consumer lost audio however fast it drained; decoded buffers are now released along the
playhead (at most tolerance + lead, 12 s, in flight) and the stream keeps 256. Loopback and
remote-HLS VOD alike. -
The HLS fixture reads its redirect port under the lifecycle lock. A request worker could read
the bound port whilestop()reset it under the lock. Host-only redirects now take the same lock,
and the port stays private to the server. Explicit--redirect-porttargets are unchanged. -
Source failures keep their cause and request ownership. Initial live probes preserve HTTP,
URL and TLS failures without changing the live-startliveSourceUnavailablekind (HTTP and
certificate classifications stay specific). A transport failure at open (a reset or a timeout from
an origin still spinning up) is still ridden out by the reconnect ladder; its URL code is recorded
and attached toliveSourceUnavailableasunderlyingDomain/underlyingCodeonly when a live
start finally gives up. HLS and demux errors retain their existing NSError domains/case codes while
exposing bounded diagnostic reasons and the original FFmpeg code separately. Optional
SourceNetworkDiagnosticsevents carry a captured playback/attempt context across redirects and
internal reloads and AVIO thread crossings, including streamed held HTTP/1 bodies. Immutable
events use extensible string-backed roles and phases. Refused AES key requests use the historical
playlistUnreachable(status:)case to preserve their HTTP status. A held HTTP/1 open that fails
after its response head reports the hop that answered and its status. The observer does not alter
request policy and requires hosts to redact source URLs. -
Still extraction respects requested size and precise snapshot position on cache hits.
Reusing one extractor for different thumbnail widths or snapshot bounds no longer returns the
first size rendered. Snapshot requests no longer share a coarse time bucket across frame boundaries. -
Precise pixel aspect ratios no longer fall back to square pixels. The sanity gate bounds the
ratio's magnitude instead of each rational component, preserving values such as 759:712 while
still rejecting extreme pixel shapes and implausible resulting display aspects. -
VideoToolbox output metadata respects immutable reference buffers. Colour and pixel-aspect
attachments are updated in place only when the decode callback marks the buffer modifiable.
Otherwise a pooled owned copy receives the attachments; matching output is passed through. -
Software playback teardown no longer blocks the main actor on renderer removal (AE#873, thanks @jonluca).
The video renderer leaves the synchronizer immediately, also when its clock is stopped, and the
retiring output and layer stay alive until AVFoundation reports the removal complete. -
A remote disc image on an extensionless URL is recognized by its
Content-Type(AE#860, thanks @qoli).
The HTTP disc adapter was gated on the.iso/.img/.udfextension alone, so an ISO behind an
opaque proxy path reached libavformat as raw filesystem bytes and failed withInvalid data found when processing input, even when the origin declaredapplication/x-iso9660-image. The type of the
response the open already makes (or of a warm) now sends the source to the disc adapter, with no
extra request;application/x-cd-image,application/vnd.efi.iso,application/x-udf-imageand
application/x-raw-disk-imagecount too, parameters and case ignored. The type is a hint: the
ISO 9660 / UDF readers still decide, and a mislabelled body opens as ordinary media. A URL found to
be a disc is remembered for the process, so the next open, a controlledprobeand
updateRequestHeaderstreat it like an.iso. -
A forward seek in a fragmented MP4 no longer lands a fragment or more past its target (AE#859).
libavformat indexes a fragmented file one fragment at a time, and a stream seek whose next sync
sample started an unread fragment resolved to whatever later fragment an earlier read had indexed:
a session started at 150 s and seeked to 60 s rendered 122 s. The seek now lands on the sync
sample before the target when the index has a hole there. -
A pump that cannot keep up on the efficiency class stops dropping to it (#850, thanks @imbenjamin).
The AE#286 relaxation moved the segment pump to.utilityonce it was 16 s ahead, on the
assumption that it still produces faster than real time there. A 4K Dolby Vision remux with
TrueHD Atmos rendered to APAC on an A15 Apple TV made 0.10 to 0.51x on.utility, so every
relaxed stretch drained the lead, the forward window never filled and VOD stalled every few
minutes. The pump now measures what it produces against wall time while relaxed and stays on
.userInitiatedfor the rest of the session once that falls below real time, and it rechecks
once a second instead of only at segment boundaries, where one slow segment had outlasted the
whole lead. A session that keeps up relaxes exactly as before. -
HDR10+ reaches the display on the software path (#814, thanks @williamliu0516). The VideoToolbox decoder there never read the
ST 2094-40 SEI, so a PQ HEVC title played as plain HDR10 andvideoFormatstayed.hdr10; it now
reports.hdr10Pluson the first message and hands each frame its payload as
kCMSampleAttachmentKey_HDR10PlusPerFrameData. The libavcodec decoder attached one per frame already,
but fromav_dynamic_hdr_plus_to_t35, which leaves out the six T.35 header bytes the attachment must
start with (0xB5 0x00 0x3C 0x00 0x01 0x04); they are put back. The loopback path needed nothing:
the SEI is in every served fragment from MKV, MP4 and MPEG-TS sources, and AVPlayer's decoded buffers
carry the per-frame metadata (measured on macOS 27 with a two-scene fixture). -
A plain-URL live open the origin answers with a status fails with that answer (#796, thanks @cmcpherson274).
7.38.0 typed the refusal in the reader, but the live fail-fast after the probe dropped it, so a 503
reached the host asliveSourceUnavailableandloadthrewDemuxerError.openFailed(-5). It now
publishessourceRateLimited(429/503/509) orsourceRefusedwith the status inunderlyingCode,
andloadthrowsAVIOReaderError.httpStatus, as the release notes of 7.38.0 said it did. A live
open that reaches nothing keepsliveSourceUnavailable. -
4K AV1 in software no longer starts its clock ahead of the picture after a seek (AE#800, thanks @cmcpherson274).
The preroll gave up after a fixed 2 s, and libdav1d on an Apple TV 4K (A15) took 2.9 to 3.9 s to
decode from the keyframe up to a seek target in a 4K AV1 title, so the clock started 0.6 to 1.6 s
before the first frame and the frames due in that time dropped. The hold now runs on for as long as
the decoder keeps producing frames (the ones it skips on the way to the target included), up to 6 s;
a decoder that produces nothing still times out after 2 s. -
A title whose audio ends before its video plays the rest of the picture in silence (AE#847, thanks @narenh).
AVPlayer ends a muxed HLS item the moment its audio runs out, so a hybrid remux (a web video track
over a disc's audio) stopped at the end of the audio and its credits never played, and a resume
inside that tail ended the item at load. A re-seek in place (the AE#287 cure for the opposite case)
does not get past it, and neither does disabling the audio track. The final segment now carries
silence from where its audio stops to where its video does, encoded in the codec the track is
delivered in; AVPlayer bridges any gap in front of it on its own. Covered: AC-3, E-AC-3 and FLAC
stream copy, every FLAC / E-AC-3 bridge (TrueHD, DTS, PCM and the rest), and AAC (LC) and ALAC through
the encoders FFmpegBuild 3.10.0 adds (now pinned). A codec this build cannot encode (MP3, Opus, an APAC
bed) logsAE#847 audio tail not paddedand ends as before. Tracks that end within half a second
of each other are muxed exactly as before. When the source arrives barely faster than its bitrate
and the silent tail is longer than the producer's lead, playback waits at the end of the audio until
the producer has reached the end of the file, then plays the tail. -
A seek that misses the software path's packet cache no longer ends playback at its landing (AE#811, thanks @williamliu0516).
The miss empties the read-ahead store while holding the store's operation lock, and a demux-loop
read that began inside the seek's window could leave its wait at once (its reader was not current
yet), get the lock only after the seek had landed, find the store empty and return nil, which the
loop takes for end of stream: "end of media" at the landing. An empty store is now the end only
when the producer has seen the source's own end; before that the read throws.interruptedand the
loop reads again. Seen on DVD-Video, whose time-map seek settles at once: a seek past the first
cell ended playback in 7 of 8 runs, in none of 8 with the fix (48 seeks that missed the cache). -
A session-preserving reload recovers from its own master rejection again (#849, thanks @adityabakare).
The reactive master-to-media fallback is single-shot per session, and its latch was reset only in
load()'s prologue, whichreloadWithAudioOverrideandreloadAtCurrentPositionnever pass
through. A fallback spent on the outgoing session's master (a Dolby Vision P8.1 source's one
-11868at mount) carried into the reload, so the rebuilt master's own rejection (an audio switch
to AC-3 on macOS) was signalled but never mounted the media playlist and ended the session at
.masterPlaylistRejected("Cannot open"). The latch now resets instopInternal, the teardown
every rebuild passes through. -
An audio switch keeps the media playlist its session fell back to (#849). When the display
refused the session's HDR master (-11868/-11848), the switch rebuilt a master that differs
only in its audio CODECS and paid the same rejection and in-place fallback again. It now serves
the media playlist straight away, and the rebuilt session carries the refusal so the switch after
it does too. A parse failure, an SDR refusal,selectTitleandreloadAtCurrentPositionstill
route afresh: those either change the picture or follow a return from the background, where the
display may have changed. -
A refused HDR master no longer latches the panel refusal on iOS and macOS (#849). The AE#588
latch only moves the panel answer, and the routing on a panel that engages HDR on demand reads the
display's eligibility instead, so on those platforms it changed nothing except a log line claiming
every later HDR title went media-direct. Such a refusal is not a fact about the panel either: on a
MacBook Air M1 SDR panel a Dolby Vision P8.1 master with and withoutSUPPLEMENTAL-CODECS, a
High-tier 3832x1600 PQ master and an audio switch to AC-3 on each were all accepted. The item still
falls back, later titles are still offered the master, and the log now says so. tvOS is unchanged. -
A seek no longer waits on a segment pump running at the efficiency class. A VOD pump far
enough ahead of playback relaxes to.utility(AE#286), and a seek's restart stops it and waits
for it to finish from the loopback server's.userInitiatedqueue. A condition donates no
priority to the thread it waits for, so the pump tore down and signalled its finish at.utility,
which Xcode's Thread Performance Checker reported as a hang risk in
HLSSegmentProducer.waitForFinish. The pump now returns to.userInitiatedas it leaves its read
loop and stays there until it has finished.
Full changelog: CHANGELOG.md · 7.47.0...7.48.0