Patched libmpv and FFmpeg binaries for the apps Plezy and Tortuga ship, built
from one set of pins and patch series:
| Platform group | Artifact |
|---|---|
| apple | XCFrameworks for iOS, tvOS, macOS, Mac Catalyst and the iOS/tvOS simulators, as the SwiftPM product MPVKit |
| android | libmpv-android-<key>-<abi>.tar.gz per ABI, and the mpv-android-<key>.aar library |
| linux | libmpv-linux-<key>-<arch>.tar.zst (x86_64, aarch64) |
| windows | libmpv-windows-<key>-<arch>.zip (x86_64, aarch64), the mpv-dev package shape |
Every push to main publishes the binaries that commit needs, so any commit on
main can be pinned. Each publish run uploads what it built to a prerelease of
its own, binaries-<first 12 hex of the commit built>. Assets published before
that stay where they are, in
binaries-apple,
binaries-android,
binaries-linux and
binaries-windows,
which take nothing new.
| Path | Purpose |
|---|---|
versions.json |
Every component pin (version, URL, hash or commit), with per-platform overrides; a prebuilt's upstream names the source commit it was built from. |
patches/<component>/ |
Patch pool and series files; see Patches. |
toolchain/<group>.txt |
Toolchain pins and the generation that forces a group's full rebuild. |
artifacts.json |
The published binaries, one section per platform group. Written by CI. |
Package.swift |
SwiftPM manifest, rendered from the apple section of artifacts.json. Written by CI. |
Sources/BuildScripts/ |
The Apple build driver (make build). |
Sources/_MPVKit/licenses/ |
The licence texts of everything the Apple package links, generated by scripts/licenses.py apple. |
platforms/<group>/ |
The android, linux and windows drivers, each defined by its group.json. platforms/android/lib/ is the mpv-android library. |
scripts/ |
patches.py (series), keys.py (content keys and the publish gate), licenses.py (licence texts), symbols.py (debug symbols), extract.py (code slicing for tests), and the regression tests. |
docs/ |
Apple and Android details. |
SwiftPM. Add https://github.com/edde746/mpv-build.git and pin a commit (in
Xcode, Branch/Commit → the SHA). The product is MPVKit. Semver tags
exist but carry no assets of their own. Consumer notes (pkg-config, AVPlayer
passthrough, spatial audio): docs/apple.md.
Android AAR. Resolve edde746:mpv-android:<key>@aar through an Ivy
repository on the AAR's release URL; the key, sha256 and release come from
artifacts.json. Setup, API and helpers: docs/android.md.
Linux and Windows. Download the asset named in artifacts.json
(platforms.<group>.libraries.libmpv-<group>.prebuilt.<arch>) from its
location (below) and check its sha256. The Windows zip has libmpv-2.dll,
libmpv.dll.a, include/mpv/*.h and licenses/ at its root
(platforms/windows/README.md). The Linux tarball
holds lib/, include/ and licenses/, built in the container
toolchain/linux.txt pins (glibc 2.39).
Pinning a commit. A consumer pins one commit of this repository and reads
that commit's artifacts.json for every non-SwiftPM asset. Assets are
content-addressed: the 12-character key in each name is derived from the
component pins, the resolved patch series, the build flags, the platform
tuple, the build driver and the toolchain file. A published asset never
changes, and is never deleted while a commit's manifest points at it.
An unchanged library keeps the assets an older run built, so one commit's
assets can sit in several releases, and the manifest records each asset's
location: its URL is <assetBase>/<asset>, with the assetBase beside the
asset's name (prebuilt.<variant>.assetBase, symbols.<variant>.assetBase, or
a package entry's own assetBase) or, when it has none, the group section's
platforms.<group>.assetBase (the old binaries-<group> release).
Package.swift already spells out each framework's full URL.
Known issue for pins. On Amlogic and MediaTek Android TV boxes (such as
the Google TV Streamer) the video plane reaches the screen a refresh after
a subtitle plane given the same timestamp, so subtitles lead their picture
on HDMI (plezy#2894); from the commit that adds mpv patch 0142 (Android:
queue the subtitle plane as early as the video) onwards that includes
MediaTek. Nothing on the device reports the lag, so the embedder sets
--vo-mediacodec-osd-vsync-delay=1 on those chips; from patch 0145 on, the
delay no longer makes the video judder on Amlogic.
- A change lands on
main(from a pull request;ci.ymlruns the patch checks, the host regression tests, the android build and the Demo apps). publish.ymlcomputes each group's keys, builds only what is not published (keys.py stale), uploads what it built to the run's own releasebinaries-<sha>, and records each asset with that release as its location. A run that never commits its manifest leaves a release nothing points at; the next run deletes it before opening its own.- It then commits
artifacts.jsonandPackage.swifttomainaschore: binaries for <sha> [skip binaries]. That binaries commit is the one to pin: the commit before it has no manifest entries for its new assets. - A consumer moves its pin to the binaries commit. Tortuga does it with
tools/set_native_revision.sh <sha|latest>, which rewrites itsmpv-build.lock.json, Gradle verification metadata and Apple project.
python3 scripts/keys.py keys --platform-group apple # what this tree needs
python3 scripts/keys.py stale --platform-group apple # what is not published yet
python3 scripts/keys.py verify --platform-group apple # manifest matches this treeA change to patches/mpv/* moves libmpv's key alone; editing a group's build
driver moves every key in that group. To force a rebuild without a source
change (a new Xcode, a miscompile), bump generation in that group's
toolchain/<group>.txt. check_version.yml opens weekly pull requests that
move the mpv and FFmpeg pins (and the badges above) and the prebuilt
dependencies to new upstream tags. release.yml tags a commit whose
Package.swift passes verify; it builds nothing. Each pin's provenance in
versions.json says why it is pinned; Windows' fontconfig pin is temporary
and goes once winbuild rebases its fontconfig patches.
Apple (macOS, Xcode selected with xcode-select, rustup):
rust_version="$(sed -n 's/^rust=//p' toolchain/apple.txt)"
rustup toolchain install "$rust_version" --profile minimal --no-self-update \
--component rustc,cargo,rust-std \
--target aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios,aarch64-apple-tvos,aarch64-apple-tvos-sim
make build # every platform
make build platform=ios,macos # ios,macos,tvos,tvsimulator,isimulator,maccatalyst,xros,xrsimulator
make build libs=libmpv use-prebuilt # compile libmpv, restore the rest from published binaries
make help
make clean
./mpv.sh [url] # the mpv player from a macOS buildOutput lands in dist/. Repeat the rustup step when toolchain/apple.txt's
rust pin changes.
Android:
bash platforms/android/include/download-sdk.sh
bash platforms/android/download.sh
bash platforms/android/build.sh --arch arm64
bash platforms/android/package.sh --arch arm64
bash platforms/android/lib/build-aar.sh --tarballs dist/releaseSee docs/android.md for the prerequisites and rebuild rules.
Linux (docker; the published path, in the container toolchain/linux.txt pins):
KEY="$(python3 scripts/keys.py keys --platform-group linux \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["libmpv-linux"])')"
KEY="$KEY" ARCH=x86_64 bash platforms/linux/container-build.shWithout KEY it builds but does not package; the script's header lists its
other variables.
Windows (Linux host): platforms/windows/build.sh <x86_64|aarch64> then
platforms/windows/package.sh <x86_64|aarch64>; host requirements are in
platforms/windows/README.md.
Each patched component (mpv, ffmpeg, libass, libplacebo, libdovi) keeps one platform-neutral pool and per-platform series files. Line order is the application order, and each patch's header explains it.
patches/<component>/pool/<name>.patch the patch bytes
patches/<component>/series.common applied on every platform, first
patches/<component>/series.<platform> applied after series.common
python3 scripts/patches.py check # pools, series and versions.json agree
python3 scripts/patches.py resolve mpv apple # one platform's ordered series
python3 scripts/patches.py apply mpv apple <srcdir> --check # the series still applies
python3 scripts/patches.py fetch-pinned mpv apple <srcdir> # fetch the pinned source and applyfetch-pinned checks a git pin's commit or an archive pin's sha256 before
applying. It tries each source three times, then the pin's mirror if it has
one, which has to match the same commit or sha256. The android driver's
download.sh fetches every source through it. ci.yml applies every nonempty
series to its pinned source, so a version bump that breaks a patch fails before
anything builds.
Run any of them with bash scripts/<script>. A harness that tests patched code
fetches the pinned source and applies the series with patches.py fetch-pinned; pass an already-patched source tree as the first argument to run
offline (two trees, mpv then FFmpeg, where both are compiled). Each script's
header says what it covers. "PR" is ci.yml, "main" is publish.yml.
| Script | Guards | CI |
|---|---|---|
test_patches.sh |
patches.py: series rules, check, apply, archive fetching |
PR, main |
test_extract.sh |
extract.py: slicing and hunk parsing over the whole pool |
PR, main |
test_keys.sh |
keys.py: content keys, stale, verify, recording and publishing |
PR, main |
test_symbols.sh |
symbols.py: ELF, PE and PDB identities, matching, reproducible zips |
PR, main |
test_apple_merge.sh |
merge_xcframeworks.py writes byte-identical zips twice (macOS) |
main |
test_build_date.sh <asset>... |
no file in a release asset carries its build's clock; every build leg runs it before upload | main, android.yml |
test_build_process.sh |
the Swift driver's process launch: output capture and exit codes | main |
platforms/linux/test.sh |
the linux driver's build plan and configure flags, all tools stubbed | PR |
platforms/windows/test_pin_packages.py (python3) |
pin_packages.py rewrites winbuild's packages onto the pins |
PR |
test_ao_avfoundation_fallback.sh |
AVPlayer sink: PCM fallback, no-resource-request log, clock anchor; the test drives the clock and feed ticks (macOS) | PR |
test_ao_compressed_audio_option.sh |
the compressed-audio option contract (patch 0019) | main |
test_ao_pcm_lookahead.sh |
bounded PCM lookahead (patch 0015) | main |
test_ao_probe_refusal.sh |
the compressed-audio loader refuses reads past the write head | main |
test_ao_spatial.sh, test_spatial_*.sh |
spatial audio; see docs/apple.md | PR, main |
test_avf_dovi_profile5_rpu.sh |
which frames vo_avfoundation uploads with their Dolby Vision RPU | main |
test_avf_host_present.sh |
host-mode frame stamps after a seek, when a reset re-seeds the timebase, where in a refresh interval a tvOS stamp sits so the video and OSD layers stay on the same refresh after a seek (patch 0055), and how far a frame after a seek or a resume is moved to reach the layers 3.5 refreshes ahead (patch 0056) | main |
test_avf_media_stamp.sh |
media-time frame stamps after a reset: no frame of the file before shows after the next file's (patch 0050) | main |
test_avf_osd_ahead.sh |
how long flip_page may hold video for its subtitles, including the frames right after a seek or a resume (patch 0056) | main |
test_avf_osd_blend.sh |
the OSD's NEON blends, libass masks and premultiplied BGRA, match their scalar references bit for bit (patch 0043; aarch64 host) | main |
test_avf_osd_color.sh |
an overlay buffer carries the colour of the SDR video under it, and none over HDR or untagged video, whatever a pool buffer held (patch 0052; macOS) | main |
test_avf_osd_damage.sh |
an overlay buffer drawn by tile, whatever it held, ends up exactly as a full clear and blend of the frame (patch 0048) | main |
test_avf_osd_layer_geometry.sh |
the inline OSD layer's placement (macOS) | main |
test_avf_pip_frame_space_osd.sh |
frame-space OSD geometry for PiP | main |
test_avf_queue_lead.sh |
how far ahead vo_avfoundation enqueues frames: on tvOS past where the video and OSD layers disagree on a frame's refresh, unchanged at 50 Hz and up (patch 0054) | main |
test_avf_shader_target.sh |
the size vo_avfoundation renders --glsl-shaders at, and the part of the frame that fills it, per display-layer gravity |
main |
test_avf_timebase_servo.sh |
the timebase servo and rate decisions | main |
test_audio_passthrough_fallback.sh |
the player's AO failure handling and PCM fallback (patch 0040) | PR |
test_audio_reload_held_frame.sh |
an AO reload with a held frame still opens the next AO (patch 0034) | PR, main |
test_audiotrack_timing.sh |
AudioTrack clock, writes, passthrough, recreate retry, stream silence, pause, the passthrough resume and the core's resume clock (patch 0144), a track whose core has no audio waiting instead of writing nothing (0150), and a passthrough head read once per clock sample (0154) | PR, main |
test_audiotrack_truehd.sh [media] |
raw TrueHD reassembly from IEC 61937 MAT bursts (needs host ffmpeg) | PR |
test_ass_pixel_ids.sh |
the ids mp_ass_packer gives libass bitmaps: one id means one unchanged bitmap of a list that stayed held (patch 0234) | PR, main |
test_sd_ass_seen_packets.sh |
a subtitle packet sd_ass decoded before is seen again with or without a file position (HLS), and a different one is new (patch 0242) | PR, main |
test_av_log_routing.sh |
FFmpeg log lines reach the mpv instance that logged them (patch 0209), and one that no instance takes is turned away before the lock (0233) | PR, main |
test_demux_codec_profile.sh |
demuxer-published codec profile matches the decoder's (patch 0204) | PR, main |
test_demux_mkv_defer_tags.sh |
Matroska Tags behind the clusters wait for the Cues on network streams and nowhere else; tags read after opening reach only the file's metadata (patch 0240) | PR, main |
test_demux_refresh.sh |
a refresh seek queues nothing twice and keeps the next subtitle (patch 0208) | PR, main |
test_http_persistent.sh |
HTTP keeps its connection across seeks, byte for byte, against servers that keep alive, close, ignore Range, send chunked bodies, redirect, cut a reply short, stall or go silent, and changes nothing on the wire without the new options (FFmpeg patches 0080, 0081; builds a minimal host FFmpeg) | PR, main |
test_lavc_busy_policy.sh |
decoders that wake the filter themselves are not spun | PR, main |
test_libass_series.sh |
the libass series: change detection, renderer handoff, shape cache, a frame's events however the track changed (event blocks), cache keys whatever their padding holds, layers a clip hides | main |
test_libass_rounding.c |
ARMv7 VFP rounding; cross-compile and run on an ARMv7 device (header) | manual |
test_libass_key_padding.c |
the cache key harness of test_libass_series.sh on the one ABI that pads those keys; cross-compile and run on an ARMv7 device (header) |
manual |
libass checkasm |
the 32-bit ARM NEON kernels against the C ones, byte for byte; libass' own tool, built with the series for armeabi-v7a, run on an ARMv7 device |
manual |
test_mediacodec_crop.sh |
MediaCodec output crop parsing | PR, main |
test_mediacodec_dv_filter.sh |
the Dolby Vision access-unit filter (patch 0101) | main |
test_mediacodec_eos.sh |
end of stream through the async decoder after a flush | PR, main |
test_mediacodec_error.sh |
codec errors stay the decoder's answer across seeks | PR, main |
test_mediacodec_flush.sh |
when a flush reaches the codec | PR, main |
test_mediacodec_geometry.sh |
vo_mediacodec geometry (patch 0100) | main |
test_mediacodec_input.sh |
an access unit too large for its input buffer is dropped whole | PR, main |
test_mediacodec_letterbox.sh |
the OSD plane's letterbox fill | PR, main |
test_mediacodec_osd.sh |
the shared subtitle render-ahead pipeline (patch 0031), a delayed plane repeating an unchanged frame (0214), and when a static OSD is settled (0153) | main |
test_mediacodec_paused.sh |
a paused decoder is not polled and never holds the decode thread | PR, main |
test_mediacodec_rate.sh |
the media3_rate operating-rate option |
PR, main |
test_mediacodec_rebind.sh |
OSD surface retirement without rebuilding the decoder; a letterbox-fill change repaints (patch 0143) | PR, main |
test_mediacodec_rendered.sh |
the rendered-frame feedback ring | PR, main |
test_mediacodec_reorder.sh |
presentation-order recovery (patch 0026) | PR, main |
test_mediacodec_shed.sh |
pre-decode frame shedding for H.264, HEVC, VP9 and AV1 | PR, main |
test_mediacodec_timing.sh |
vo_mediacodec cadence, vsync snapping, release timing, present stats, and a settled OSD left without requests (0153) | PR, main |
test_mediacodec_vsync.sh |
the Choreographer vsync sampler against Android 11's behaviour | PR, main |
test_pgs_palette.sh |
PGS palettes start transparent after an epoch boundary | PR, main |
test_player_queue_frames.sh |
frame queueing and durations under vo_mediacodec | PR, main |
test_player_release_held_frame.sh |
ending a file frees the decoder before the next opens (patch 0139) | PR, main |
test_player_video_restart.sh |
restored video syncs to already-running audio (patch 0210) | PR |
test_spdif_truehd_branch.sh |
TrueHD units keep their IEC 61937 carrier offsets across seamless-branch timing jumps (patch 0039) | PR, main |
test_tcp_family_lookup.sh |
per-family hostname lookup and connection race (patch 0029) | PR, main |
test_android_glue.sh |
the mpv-android JNI glue; see docs/android.md | PR, main |
The android, linux and windows groups publish one debug-symbols zip per variant beside each binary, so a crash tracker can symbolize libmpv and FFmpeg frames. Each zip holds, at its root, the debug files of what that variant ships, matched by identity (ELF GNU build-id, or a DLL's CodeView GUID and age), never by name.
| Group | Asset, per variant | Contents |
|---|---|---|
| android | mpv-android-<key>-<abi>-symbols.zip |
a debug ELF for every .so in the AAR's jni/<abi>/ (libc++_shared.so only when the NDK has its unstripped copy) |
| linux | libmpv-linux-<key>-<arch>-symbols.zip |
a debug ELF for every shared library in the tarball |
| windows | libmpv-windows-<key>-<arch>-symbols.zip |
libmpv-2.pdb |
They are recorded under the artifact's symbols entry in artifacts.json, and
keys.py treats an entry without every variant's symbols as unpublished.
scripts/symbols.py is the only packer:
python3 scripts/symbols.py id libmpv-2.dll libmpv-2.pdb # identities, as the tracker spells them
python3 scripts/symbols.py match <shipped> <candidate>... # the matching candidate with the most debug info
python3 scripts/symbols.py pack out.zip --pair <shipped> <debug> # checked, reproducible zipFFmpeg is configured with --enable-gpl on every platform, so the binaries are
distributed under the GPL; see
FFmpeg's and
mpv's terms. This
repository is under the GPL-3.0 (LICENSE).
The drivers derive from edde746/MPVKit
(from mpvkit/MPVKit and
kingslay/FFmpegKit),
edde746/libmpv-android (from
jarnedemeulemeester/libmpv-android),
Plezy's linux/packaging, and a pinned
shinchiro/mpv-winbuild-cmake.
Their history lives in those repositories.
Every artifact carries the licence texts of each component linked into it, as
licenses/<component>/<path in that component's source>, copied unchanged from
the source the build compiled. scripts/licenses.py holds the per-component
rules (vendored subtrees, licence comments cut out as <file>.txt, the licence
files of every Rust crate linked).
| Artifact | Where | Components |
|---|---|---|
libmpv-linux-<key>-<arch>.tar.zst |
licenses/ |
the components of platforms/linux/group.json |
libmpv-android-<key>-<abi>.tar.gz |
licenses/ |
the components of platforms/android/group.json, libc++ (the NDK's) and rust-std (in libdovi) |
mpv-android-<key>.aar |
assets/licenses/, so they reach the app's APK |
the tarballs' texts (identical for every ABI), and mpv-android/: the glue's NOTICE and this repository's LICENSE |
libmpv-windows-<key>-<arch>.zip |
licenses/ |
every package linked into libmpv-2.dll, named as in versions.json where it pins one, else by its mpv-winbuild-cmake package name |
| MPVKit (SwiftPM) | MPVKit__MPVKit.bundle/licenses/ in every app (the _MPVKit target's resources) |
every versions.json component whose platforms include apple, and rust-std |
- Linux, Android and Windows collect the texts while building, and packaging
runs
licenses.py check: the artifact must hold exactly one non-empty folder per linked component. A text that moved upstream fails the build.licenses.pyis a driver of those groups, so changing it moves their keys. - Apple: most XCFrameworks are prebuilts that carry no text, so
licenses.py applefetches every Apple component at the commit it was built from (a prebuilt'supstreaminversions.json), addsrust-stdfrom the toolchaintoolchain/apple.txtpins, and writesSources/_MPVKit/licenses/, which is committed. CI'sApple licence textsjob fails if it differs. After changing an Apple pin, anupstreamor a rule, regenerate it (needs git, network and that toolchain through rustup):
python3 scripts/licenses.py apple # rewrite Sources/_MPVKit/licenses/
python3 scripts/licenses.py apple --check # what CI runs