Repository navigation
build(ffmpeg): software AV1 via dav1d, with the decoder chosen per path - #40
Merged
Merged
Conversation
AV1 played as a black picture with sound on Apple TV (DVprofile10.mkv, AV1 Main 10 4K). FFmpeg's native AV1 decoder only drives hardware — without an hwaccel it returns ENOSYS for every packet (av1dec.c) — and only Apple's newest chips decode AV1 in hardware. With no other AV1 decoder in the build, the video lane failed while audio played on. build-dav1d.sh cross-compiles dav1d 1.5.4 (VideoLAN, BSD-2-Clause) with meson for each slice: static, NEON / NASM assembly on, tools and tests off. meson's host system stays 'darwin' for every slice because dav1d's own checks only know darwin/ios/tvos, and visionOS would otherwise miss the Mach-O symbol prefix its assembly needs; `subsystem` carries the real platform. build-ffmpeg.sh builds it first, links it with --enable-libdav1d through a pkg-config LIBDIR that sees only this slice's dav1d (never a host Homebrew copy), and drops libdav1d.a next to FFmpeg's libraries so make-xcframework.sh merges it into libffmpeg.a. fetch-sources.sh downloads every pinned tarball in versions.json and checks its SHA-256 (dav1d's matches VideoLAN's published sum). CI uses it instead of three copies of the FFmpeg-only download step, and installs meson/ninja where it builds slices. versions.json, the notices (new dav1d section, the BSD-2 attribution obligation for binaries, LICENSES/dav1d-BSD-2-Clause.txt verbatim) and the contributor docs move with it. The build stays LGPL: all eight local slices configure as "License: LGPL version 2.1 or later". Package.swift still points at the v0.2.0 artifact, which has no dav1d: its checksum can only exist once a release has built the new xcframework, so the bump follows the tag, as it did for v0.2.0 (#36).
With dav1d linked, avcodec_find_decoder(AV1) returns libdav1d — it is registered ahead of FFmpeg's native decoder — and libdav1d has no VideoToolbox hwaccel. Keeping that lookup would have traded the black picture for losing AV1 hardware decoding on the chips that have it. The two paths need different decoders: the native `av1` is the only one that drives VideoToolbox, but cannot decode without it; libdav1d decodes in software and only there. VideoDecoder.decoder(for:hardware:) walks the registered decoders in order (experimental ones last, like avcodec_find_decoder) and takes, for the hardware path, the first with a VideoToolbox device config and, for the software path, the first that runs without hardware — skipping AV_CODEC_CAP_HARDWARE and the native AV1 decoder, which FFmpeg flags as nothing but is hwaccel-only by implementation. The AV1 hardware path is only opened when VTIsHardwareDecodeSupported says the chip has it, since that decoder has no software fallback of its own. Everything else resolves exactly as before. A new av1.mkv fixture (AV1 Main 10-bit via SVT-AV1, which Homebrew's ffmpeg ships) checks that AV1 decodes on any host — on VideoToolbox exactly when the chip has an AV1 decoder, through dav1d otherwise — and stays 10-bit. Against the pre-dav1d xcframework the test fails: no frames, no end of stream. DVprofile10.mkv (AV1 4K 10-bit, DV profile 10.1) through the engine's software path: 176.6 fps on an M1 Pro, P010 out.
bilipp
added a commit
that referenced
this pull request
Sep 29, 2026
CI has been red since 069a818, and #39/#40 added two more failures of the same kind. The workflow asks for Xcode_26.app, which the macos-15 image no longer has; the fallback selects Xcode 26.3 — Swift 6.2 — while local development runs Xcode 27 (Swift 6.4). Code that 6.4 accepts at once, 6.2 rejects: - ChannelDrain.value: a bare `result` inside the initializer of the local `result` binds to that local on 6.2 ("initializer for conditional binding must have Optional type"). Now `self.result`. - Matrix3.multiply/inverse: nested `reduce(0)` closures over untyped literals exceed 6.2's solver ("unable to type-check this expression in reasonable time"). Plain loops with explicit types. - DolbyVisionTests.extraction: `#expect(pieces == [.polynomial(...)])` took 6.4 2.4 s to type-check; the expected values are now typed locals. HEVCBaseLayerFilterTests' hvcC array builder (0.1 s) is a loop. Found by compiling with -warn-long-expression-type-checking=25 on 6.4: nothing is above 40 ms any more, the level the existing tests already passed CI at. Behaviour is unchanged. The suite passes apart from the intermittent SRT cue test, a real duplicate-cue bug fixed in the next commit.
Merged
13 of 22 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AV1 played as a black picture with sound on Apple TV (
DVprofile10.mkv: AV1 Main 10, 4K, Dolby Vision profile 10.1).FFmpeg's native AV1 decoder only drives hardware. Without an hwaccel it returns
ENOSYSfor every packet (libavcodec/av1dec.c), and only Apple's newest chips decode AV1 in hardware. The build had no other AV1 decoder, so the video lane failed while audio played on. Lume's engine fallback never triggered, because the session stayed.playingon audio.This links dav1d for software AV1 and picks the right decoder for each path. Two commits:
build(ffmpeg): link dav1d for software AV1 decodingbuild/scripts/build-dav1d.shcross-compiles dav1d 1.5.4 (VideoLAN, BSD-2-Clause) with meson for every slice: static, NEON/NASM assembly on, tools and tests off.darwinfor every slice, because dav1d's own checks only recognise darwin/ios/tvos; visionOS would otherwise miss the Mach-O symbol prefix its assembly needs.subsystemcarries the real platform.build-ffmpeg.shbuilds dav1d first and links it with--enable-libdav1d. pkg-config is pinned (PKG_CONFIG_LIBDIR) to this slice's dav1d only, never a host Homebrew copy.libdav1d.alands next to FFmpeg's libraries, somake-xcframework.shmerges it intolibffmpeg.a.build/scripts/fetch-sources.shdownloads every pinned tarball inversions.jsonand checks its SHA-256. dav1d's matches VideoLAN's published checksum. CI uses it in place of three copies of the FFmpeg-only download step, and installs meson/ninja where it builds slices.fix(decode): choose the AV1 decoder per path instead of by registrationavcodec_find_decoder(AV1)returnslibdav1d, which is registered ahead of the native decoder and has no VideoToolbox hwaccel. Keeping that lookup would have traded the black picture for losing AV1 hardware decoding on chips that have it.VideoDecoder.decoder(for:hardware:)walks the registered decoders in order, with experimental ones last, asavcodec_find_decoderdoes:AV_CODEC_CAP_HARDWAREand the nativeav1decoder. FFmpeg sets no capability flag on it, but it is hwaccel-only by implementation.VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)says the chip has an AV1 decoder, because the native decoder has no software fallback of its own. Every other codec resolves exactly as before.Artifact URL (follow-up after the tag)
Package.swiftstill points at the v0.2.0 artifact, which has no dav1d. The new checksum can only exist once a tagged release has built the xcframework, so the bump follows the tag, as #36 did for v0.2.0. Until then, consumers resolving the package by URL keep the old binary, and AV1 fails there exactly as it does today; the decoder selection falls back correctly without dav1d. CI builds its own FFmpeg (the cache key coversbuild/scripts/**andversions.json), so this PR's tests run against dav1d.Type of change
fix— bug fixfeat— new capabilityperf— performance improvementrefactor— neither fixes a bug nor adds a featuredocs— documentation only (setup steps in AGENTS.md / README / CONTRIBUTING, the new decoder-choice invariant in AGENTS.md)test— adding or fixing testsbuild/chore— build pipeline, tooling, or maintenanceInvariants
The only data-plane change is which
AVCodecVideoDecoder.setupCodecopens; it still runs on the decode thread.Channel.TimestampUnwrapper.AVSampleBufferRenderSynchronizer(no new heuristics or clocks).as!,fatalError, orassertionFailurein engine paths; failures are typed.PlayerConfiguration.av_logstrings. The hardware-only check reads the decoder's name and capability flags, never log output.Licensing
THIRD-PARTY-NOTICES.mdhas a new dav1d section and table row, and adds the one new obligation for anyone shipping a binary: reproduce dav1d's copyright notice.LICENSES/dav1d-BSD-2-Clause.txt, a verbatim copy ofCOPYINGfrom the 1.5.4 tarball. dav1d is unmodified; no patches are applied.Verification
swift buildis warning-free. Nothing from this branch warns.Render/SystemRenderer.swift:63-64has two main-actor isolation warnings that are not touched here.swift testis green, against a locally built xcframework with dav1d:Each commit passes on its own; the build commit alone gives 89 tests.
DecoderTests"video: AV1 decodes with or without AV1 hardware" checks that decoding reaches end of stream without.failedand that frames are monotonic and stay 10-bit. It runs on VideoToolbox exactly when the host chip has an AV1 decoder, and through dav1d otherwise.generate-fixtures.sh:av1.mkv, AV1 Main 10-bit via SVT-AV1, which Homebrew's ffmpeg ships and CI already installs. No binary media committed.Platforms verified
generic/platform=iOS SimulatorandtvOS Simulatorfail to link for x86_64 locally, on core FFmpeg symbols (_av_buffer_ref…). That's because the local build has no x86_64 simulator slices; it's not caused by this change. The release job builds all 10 slices.Real-world content
x420buffers.ffmpeg -c:v libdav1don the same file decodes at 75 fps with 2 threads. The Apple TV 4K (A15) should comfortably exceed 24 fps, but hasn't been measured.