Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

FFmpegBuild

Slim FFmpeg xcframeworks for Apple platforms.
Demux, decode, and a thin HLS-fMP4 mux path for AVPlayer bridging. No network stack, no CLI binaries.


Why

Full FFmpeg builds for iOS land at 40-70 MB because they bundle a TLS stack, encoders, filters, and a dozen protocols your app will never use. For a player, most of that is dead weight. Apple already ships HTTP/3, URLSession, Network.framework, VideoToolbox and AVFoundation. So this build strips out everything you don't need and keeps what you do.

~10 MB per architecture, zero network dependencies, one build script.

In

LibraryWhat it does
libavformatDemux MKV, MP4, WebM, MPEG-TS, MPEG-PS (VOB / DVD), HLS, AVI, OGG, FLV, plus raw elementary streams
libavcodecDecode video + audio (with VideoToolbox bridge)
libavutilShared primitives
libswresampleAudio resampling / channel remap / format convert
libswscalePixel-format convert (YUV → NV12 / P010) for the SW-decode path
libavfilterTrimmed filter set: zscale + tonemap + colorspace for HDR → SDR still extraction, bwdif + yadif for CPU deinterlacing on the SW-decode path, yadif_videotoolbox + hwupload for GPU (Metal) deinterlacing of VideoToolbox frames
dav1dFast AV1 software decoder (separate xcframework)
zimgzscale's resampling / colorspace backend (separate xcframework, link-only)
libzvbiDVB teletext decoder backend for libzvbi_teletext (separate xcframework, link-only)

Out

Anything the app layer should already handle or doesn't need:

  • Network / TLS: FFmpeg reads from an avio_alloc_context callback, you wire URLSession to it
  • Encoders, except FLAC and EAC3 (kept for the audio bridge that re-encodes non-streamable sources like TrueHD / DTS / DTS-HD MA. FLAC for the lossless 7.1 path, EAC3 5.1 for the default soundbar-compat path that surfaces surround via HDMI bitstream tunnel)
  • Muxers, except MP4 / MOV / HLS (kept for the HLS-fMP4 producer that wraps streams for AVPlayer)
  • libavdevice (libavfilter is included but trimmed to a handful of filters, see In)
  • Most filters: libavfilter ships only buffer / buffersink / format / scale / zscale / tonemap / colorspace / bwdif / yadif / yadif_videotoolbox / hwupload
  • Programs (ffmpeg, ffplay, ffprobe)
  • Hardware accel layers other than VideoToolbox
  • Text subtitle rendering (do that in SwiftUI)

Build

./build.sh # all platforms, dynamic frameworks (the shipped shape)
./build.sh static # static variant, for apps that can meet LGPL 6(a) themselves
./build.sh package # repackage frameworks without recompiling
./build.sh clean # wipe everything

Needs Xcode 16+ and roughly 10-30 minutes depending on your machine. All sources (FFmpeg, dav1d, zimg, libzvbi) clone on first run.

Output lands in Sources/ as xcframeworks, ready to consume via Swift Package Manager. The shipped xcframeworks contain dynamic frameworks (dylib-in-framework, @rpath install names); Xcode embeds and signs them in the app bundle automatically when you link the package. That is what keeps the LGPL relink requirement satisfiable for closed-source apps, see License below.

Use

// Package.swift
dependencies:[.package(url:"https://github.com/superuser404notfound/FFmpegBuild", from:"3.0.0")]
// Target:
.product(name:"AetherFFmpegBuild",package:"FFmpegBuild")

Pin branch: "main" instead of a version if you want to track the latest rebuilds (that is how AetherEngine consumes it).

Then import the modules you need: AetherLibavformat, AetherLibavcodec, AetherLibavutil, AetherLibswresample, AetherLibswscale, AetherLibavfilter, AetherLibdav1d. (AetherLibzimg is a link-only backend for zscale, and AetherLibzvbi a link-only backend for the teletext decoder; you don't import either directly.) The umbrella AetherFFmpegBuild product links all of them plus the system frameworks (AudioToolbox, CoreMedia, CoreVideo, VideoToolbox) in one shot.

The FFmpeg API itself is unchanged: avformat_open_input, avcodec_send_packet and the rest keep their names. Only the module and framework names carry the prefix.

Sitting next to another FFmpeg

Every FFmpeg packaged for Apple platforms declares the same target names, so before 3.0.0 this package could not resolve in an app that also had one. A fallback ladder with KSPlayer, mpv or MobileVLCKit in it is exactly such an app:

error: multiple similar targets 'Libavcodec', 'Libavfilter', 'Libavformat' and 3 others
appear in package 'ffmpegbuild' and 'ffmpegkit'

moduleAliases does not reach this: it renames Swift source targets, not binary ones. Two things had to change, and 3.0.0 changes both:

  • SwiftPM target and product names are unique across the whole dependency graph. AetherLibavcodec no longer meets Libavcodec.
  • Framework bundle and install names, because two Libavcodec.framework bundles cannot both live at App.app/Frameworks/ under one @rpath/Libavcodec.framework/Libavcodec. The shipped install name is now @rpath/AetherLibavcodec.framework/AetherLibavcodec.

What the rename does not change is the C symbols: _avcodec_open2 is still _avcodec_open2 in every FFmpeg on earth. Distinct dynamic frameworks are enough on their own, because the two-level namespace records per reference which dylib it came from. A static FFmpeg in the same executable is the case that still bites: its symbols become definitions inside the executable and win for every object linked beside them. The fix there is to link the code that calls this build into its own dynamic framework, so its _av* bind at that framework's link. AetherEngine documents the recipe in docs/api.md.

Decoder support

  • Video (hardware via VideoToolbox): H.264, HEVC up to Main10 (HDR10 / DV Profile 8)
  • Video (software): AV1 (dav1d), VP9, VP8, MPEG-2, MPEG-4, VC-1, QuickTime RLE (qtrle), and the legacy Microsoft tail: MS-MPEG4 v1 / v2 / v3 (DivX 3.x in pre-2005 AVI rips), WMV1 / WMV2, WMV3 (WMV9, inside Matroska or MPEG-TS; the asf demuxer and the WMA decoders a native .wmv also needs are not built, see build.sh)
  • Audio: AAC, AC3, EAC3 (incl. JOC detection for Atmos), FLAC, MP2, MP3, Opus, Vorbis, TrueHD, MLP, DTS, ALAC, PCM (incl. Blu-ray LPCM via pcm_bluray)
  • Subtitles: SRT, ASS, SSA, WebVTT, PGS, DVB subtitle, DVB teletext (via libzvbi), DVD

HDR metadata (BT.2020, SMPTE ST 2084 / PQ, HLG, DV RPU) is preserved end-to-end so the decode pipeline can tag frames correctly.

Size

Release configuration, dynamic framework binaries as embedded in the app (all six FFmpeg libraries plus the dav1d, zimg and zvbi backend frameworks):

TargetFFmpegdav1dzimgzvbiTotal
iOS / tvOS / visionOS arm64~8.7 MB~0.8 MB~0.3 MB~0.5 MB~10.5 MB
macOS universal (arm64 + x86_64)~18.3 MB~2.5 MB~0.9 MB~1.0 MB~22.7 MB

Assembly-optimized paths are enabled where the Apple toolchain permits.

Local FFmpeg patches

build.sh applies four small patches to the FFmpeg source after checkout (each documented in place):

  • patch_ffmpeg: balances autoreleased Metal objects in vf_yadif_videotoolbox.m (upstream over-release crashes host apps whose GCD queues pop their last-resort autorelease pool at session teardown).
  • patch_ffmpeg_pgssub: pgssubdec.c no longer flushes retained palettes/objects on an Epoch-Continue PCS (composition_state 3). An Epoch Continue set marks a seamless connection between two clips: a conformant set re-conveys everything it references (so skipping the flush is output-neutral), while a non-conformant bare PCS+WDS+END set relies on retained state and was dropped whole with "Invalid palette id 0" under the upstream flush (AetherEngine issue 142). Error resilience, not a spec requirement; Acquisition Point and Epoch Start keep flushing. Proposed upstream as FFmpeg PR 23851.
  • patch_ffmpeg_visionos: videotoolbox.c skips kCVPixelBufferOpenGLESCompatibilityKey on visionOS, where the key is unavailable because the platform has neither OpenGL ES nor OpenGL. Upstream selects it on TARGET_OS_IPHONE, which is 1 on visionOS (TARGET_OS_IOS is the one that is 0), so the hardware-decode path does not compile for xros without this. Nothing is lost: the attribute only asks CoreVideo to make the buffer bindable as a GL texture, and on visionOS every consumer is Metal, which the IOSurface properties set alongside it already cover.
  • patch_ffmpeg_matroska_tts: matroskadec.c logs a warning when a Matroska track carries a TrackTimestampScale other than 1.0. Timestamp behavior stays exactly as upstream implements it, which is what RFC 9559 specifies (block timestamps and BlockDuration are Track Ticks). The element is deprecated (maxver 3) and many readers ignore it, so a file carrying it may have been authored against such readers and mistime silently; the warning surfaces that condition (AetherEngine issue 145). Until 2.1.x this patch clamped TTS to 1.0; the clamp rested on a wrong reading of the RFC and was dropped after upstream review (FFmpeg PR 23852).

Keeping the pins current

The versions this package builds are shell variables in build.sh, not a manifest, so no dependency bot sees them. Scripts/check-upstream.py compares all five (FFmpeg, dav1d, zimg, libzvbi, and dolby_vision over in LibDovi) against what upstream has published:

python3 Scripts/check-upstream.py --libdovi ../LibDovi/build.sh

It exits 1 when something is behind and prints why, with any published security advisory for that project attached as context. FFmpeg is compared against the newest patch on the pinned minor line rather than the newest tag overall: moving off 8.1 is a deliberate decision, not a weekly reminder.

The Upstream watch workflow runs it every Monday and keeps exactly one issue: opened when a pin falls behind, rewritten while it stays behind, and closed by the run that finds everything current again. Nothing is posted when there is nothing to do.

Built with

This package is vibe-coded, assembled and maintained by Vincent Herbst in close pair-programming with Claude (Anthropic). The commit log is the receipt: nearly every commit carries a Co-Authored-By: Claude trailer.

License

LGPL-2.1-or-later (LICENSE), matching upstream FFmpeg's default license. The build enables neither --enable-gpl nor --enable-version3, so no GPL or LGPL-3.0 components are compiled in. Per component:

ComponentLicense
FFmpeg (all six libraries)LGPL-2.1-or-later
dav1dBSD-2-Clause
zimgWTFPL
libzvbi (library sources)LGPL-2.0-or-later, ure.c MIT
Build scripts / SPM stubs (this repo)LGPL-2.1-or-later

libzvbi's three GPL-2 source files (packet-830.c, pdc.c, exp-vtx.c) are excluded from the build and the two referenced entry points are replaced with LGPL stubs (build.sh, patch_zvbi), so the shipped binaries contain no GPL code. All license texts live in LICENSES/.

Shipping in an App Store app

The xcframeworks are dynamic frameworks on purpose: LGPL section 6 requires that end users can swap in a modified version of the library. With dynamic linking your app binary stays yours (closed source is fine) and the obligations reduce to:

  1. Link the package normally; Xcode embeds the frameworks in YourApp.app/Frameworks/. Do not merge them into the app binary (no mergeable-library trickery), that would recreate static linking.
  2. Reproduce the license texts from LICENSES/ somewhere reasonable (acknowledgements screen, bundled file).
  3. State that your app uses FFmpeg and friends, and link to the source of the exact build you ship (a tagged release of this repo, or your fork if you modified it).

If you build the static variant instead, those steps are not sufficient: LGPL 6(a) then requires you to provide your app's object files (or full source) so users can relink. That is realistic for open-source apps and rarely anything else, which is why static is not the shipped shape.


Used by AetherEngine.

About

Slim FFmpeg 8.1 + dav1d xcframeworks for iOS, tvOS and macOS. Demux + decode + thin HLS-fMP4 mux for AVPlayer bridging. FLAC + EAC3 encoders for the audio bridge path. No network stack, no CLI binaries.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages