Compare commits

..
Author SHA1 Message Date
enricobuehler 56adb47026 fix(client-core): pad-audio references the WASAPI module by its mounted name
ci / web (pull_request) Successful in 56s
apple / swift (pull_request) Successful in 1m25s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m40s
ci / docs-site (pull_request) Successful in 2m33s
ci / rust-arm64 (pull_request) Successful in 2m43s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m17s
android / android (pull_request) Successful in 4m12s
ci / rust (pull_request) Successful in 6m21s
The Windows build of pf-client-core has been red on main since the
pad-audio merge (#23): pad_audio.rs calls
`crate::audio_wasapi::device_by_id`, but lib.rs mounts audio_wasapi.rs AS
`crate::audio` via the #[path] per-OS swap — the `audio_wasapi` module
name never exists. Windows-gated call site, so every Linux leg stayed
green while both `windows / build` targets failed E0433.

One-line rename to the mounted path (+ the comment that pointed readers
at the phantom name). Verification is the PR's own windows leg — the
crate builds on no other platform this path compiles on.
2026-08-05 08:15:02 +02:00
5 changed files with 20 additions and 272 deletions
+2 -4
View File
@@ -15,10 +15,8 @@
# target with host tools, so no ARM64 runner is needed — the cc/cmake crates pick the ARM64 # target with host tools, so no ARM64 runner is needed — the cc/cmake crates pick the ARM64
# compiler from the target triple (SDL3 + libopus build-from-source cross-compile fine). The one # compiler from the target triple (SDL3 + libopus build-from-source cross-compile fine). The one
# arch-specific external dep is FFmpeg's import libs: the runner keeps an x64 tree at # arch-specific external dep is FFmpeg's import libs: the runner keeps an x64 tree at
# C:\Users\Public\ffmpeg and an ARM64 tree at C:\Users\Public\ffmpeg-arm64 (both FFmpeg 8.1 / # C:\Users\Public\ffmpeg and an ARM64 tree at C:\Users\Public\ffmpeg-arm64 (both FFmpeg 7.x /
# avcodec-62 — version pinned in scripts/ci/provision-windows-punktfunk-extras.ps1, which # avcodec-61); the matrix points FFMPEG_DIR at the right one. aarch64 can't *run* on the x64 host,
# re-provisions a runner automatically when that pin moves); the matrix points FFMPEG_DIR at the
# right one. aarch64 can't *run* on the x64 host,
# so fmt + test run only for x64. # so fmt + test run only for x64.
# #
# The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, the x64 FFmpeg, # The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, the x64 FFmpeg,
+3 -3
View File
@@ -840,7 +840,7 @@ struct PadOut {
#[cfg(windows)] #[cfg(windows)]
impl PadOut { impl PadOut {
/// Correlate (HID container → endpoint id) and open a shared event-driven render stream ON /// Correlate (HID container → endpoint id) and open a shared event-driven render stream ON
/// that endpoint (`audio::render_thread`'s shape — autoconvert, default period). /// that endpoint (`audio_wasapi::render_thread`'s shape — autoconvert, default period).
fn open() -> anyhow::Result<PadOut> { fn open() -> anyhow::Result<PadOut> {
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
let hid_path = let hid_path =
@@ -921,8 +921,8 @@ fn pad_render_thread(
const BLOCK_ALIGN: usize = PAD_CHANNELS * 4; // f32 interleaved const BLOCK_ALIGN: usize = PAD_CHANNELS * 4; // f32 interleaved
let enumerator = wasapi::DeviceEnumerator::new().context("DeviceEnumerator")?; let enumerator = wasapi::DeviceEnumerator::new().context("DeviceEnumerator")?;
// Not `get_device`: that helper resolves through a freed string — see // Not `get_device`: that helper resolves through a freed string — see
// [`crate::audio::device_by_id`]. (`audio_wasapi.rs` is mounted as `crate::audio` // [`crate::audio::device_by_id`] (audio_wasapi.rs, mounted as `crate::audio` on
// on Windows via `#[path]`, so it has no `crate::audio_wasapi` name to reach it by.) // Windows by lib.rs's `#[path]` swap — there is no `audio_wasapi` module name).
let device = crate::audio::device_by_id(&enumerator, &Direction::Render, endpoint_id) let device = crate::audio::device_by_id(&enumerator, &Direction::Render, endpoint_id)
.map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?; .map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?;
let mut audio_client = device.get_iaudioclient().context("IAudioClient")?; let mut audio_client = device.get_iaudioclient().context("IAudioClient")?;
+5 -235
View File
@@ -286,9 +286,6 @@ pub struct Decoder {
/// The pump drains it and asks the host — under the infinite GOP there is no periodic /// The pump drains it and asks the host — under the infinite GOP there is no periodic
/// keyframe, so a rebuilt/erroring decoder would otherwise stay gray/frozen forever. /// keyframe, so a rebuilt/erroring decoder would otherwise stay gray/frozen forever.
want_keyframe: bool, want_keyframe: bool,
/// Consecutive frames libavcodec concealed rather than decoded — see
/// [`Decoder::note_concealed`]. Separate from [`Self::vaapi_fails`] on purpose.
concealed_run: u32,
/// The presenter has the win32 external-memory import path, so D3D11VA frames can reach /// The presenter has the win32 external-memory import path, so D3D11VA frames can reach
/// the screen — kept for the mid-session Vulkan→D3D11VA demotion rung (the Windows /// the screen — kept for the mid-session Vulkan→D3D11VA demotion rung (the Windows
/// analog of Linux's Vulkan→VAAPI rung). /// analog of Linux's Vulkan→VAAPI rung).
@@ -438,78 +435,12 @@ pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 {
bits bits
} }
/// Count of libavcodec messages at `AV_LOG_ERROR` or worse since process start, written
/// by [`pf_av_log`]. [`Decoder::decode_frame`] samples it around each AU: a backend that
/// returns a frame while this moved decoded something libavcodec itself called broken.
///
/// Process-global because `av_log_set_callback` is. A second concurrent session would make
/// the attribution fuzzy (both sessions' errors land in one counter) — the consequence is a
/// spurious keyframe request on the other session, which is exactly what it would do for a
/// real error anyway, so it is not worth a per-context registry.
static AVCODEC_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Does an `av_log` level mean "this decode is wrong", as opposed to chatter?
///
/// libavcodec's ladder is PANIC 0 / FATAL 8 / ERROR 16 / WARNING 24 / INFO 32 / VERBOSE 40.
/// The cut is at ERROR deliberately: the reference-damage messages we are hunting
/// (`Error constructing the frame RPS`, `First slice in a frame missing`, `Previous slice
/// segment missing`) are all ERROR, while WARNING is full of benign noise like swscale's
/// "deprecated pixel format used" — counting that would request a keyframe on every frame
/// of a perfectly good session.
fn counts_as_decode_error(level: std::os::raw::c_int) -> bool {
const AV_LOG_ERROR: std::os::raw::c_int = 16;
level <= AV_LOG_ERROR
}
/// libavcodec's `av_log` sink.
///
/// The `va_list` argument is deliberately typed `*mut c_void` and NEVER read — formatting
/// it would need the unstable `c_variadic` feature, and we only want the level and the
/// message identity. `fmt` is the static format string (`"Error constructing the frame
/// RPS.\n"`), which is enough to say what happened; only the substituted values are lost.
///
/// # Safety
/// Called by libavcodec from decoder threads. `fmt` is a NUL-terminated static string
/// (libavcodec passes only string literals). We do not touch `avcl` or `vl`.
unsafe extern "C" fn pf_av_log(
_avcl: *mut std::os::raw::c_void,
level: std::os::raw::c_int,
fmt: *const std::os::raw::c_char,
_vl: *mut std::os::raw::c_void,
) {
if counts_as_decode_error(level) {
AVCODEC_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
if fmt.is_null() {
return;
}
// SAFETY: libavcodec only ever passes a NUL-terminated static format string here.
let msg = unsafe { std::ffi::CStr::from_ptr(fmt) }
.to_string_lossy()
.trim_end()
.to_string();
// Route into tracing rather than the raw stderr libavcodec would otherwise write to:
// these lines are decode evidence and belong in the log a field report ships us.
if counts_as_decode_error(level) {
tracing::debug!(target: "ffmpeg", level, "{msg}");
} else {
tracing::trace!(target: "ffmpeg", level, "{msg}");
}
}
/// libavcodec logs reference-frame recovery to the process stderr very verbosely /// libavcodec logs reference-frame recovery to the process stderr very verbosely
/// (`First slice in a frame missing`, `Could not find ref with POC …`, `Error /// (`First slice in a frame missing`, `Could not find ref with POC …`, `Error
/// constructing the frame RPS`) — normal chatter while the decoder waits for a keyframe /// constructing the frame RPS`) — normal chatter while the decoder waits for a keyframe
/// after loss, but a raw flood in the user's terminal (it bypasses our tracing). /// after loss, but a raw flood in the user's terminal (it bypasses our tracing). Default
/// /// it to fatal-only; `PUNKTFUNK_FFMPEG_LOG=<quiet|error|warning|info|debug>` restores it
/// Two jobs. It sets the level (default fatal-only; /// for decode debugging. Process-global; set once per decoder build (idempotent).
/// `PUNKTFUNK_FFMPEG_LOG=<quiet|error|warning|info|debug>` restores it for decode
/// debugging) AND installs [`pf_av_log`], which is what makes those messages *countable*.
/// The level only gates libavcodec's own default sink; a custom callback is handed every
/// message regardless, so quieting the terminal no longer means throwing the signal away —
/// which is what it meant before, for the whole life of this decoder.
///
/// Process-global; set once per decoder build (idempotent).
fn quiet_ffmpeg_log() { fn quiet_ffmpeg_log() {
use ffmpeg::util::log::Level; use ffmpeg::util::log::Level;
let level = match std::env::var("PUNKTFUNK_FFMPEG_LOG").ok().as_deref() { let level = match std::env::var("PUNKTFUNK_FFMPEG_LOG").ok().as_deref() {
@@ -521,33 +452,6 @@ fn quiet_ffmpeg_log() {
_ => Level::Fatal, _ => Level::Fatal,
}; };
ffmpeg::util::log::set_level(level); ffmpeg::util::log::set_level(level);
let cb: unsafe extern "C" fn(
*mut std::os::raw::c_void,
std::os::raw::c_int,
*const std::os::raw::c_char,
*mut std::os::raw::c_void,
) = pf_av_log;
// The turbofish clippy asks for cannot be written here: the target type is whatever
// bindgen generated for `va_list` on THIS target (`*mut __va_list_tag` on Linux, a
// different type on Windows), so naming it would need a cfg ladder per platform and
// per arch — the exact portability problem this signature avoids.
#[allow(clippy::missing_transmute_annotations)]
// SAFETY: `av_log_set_callback` stores a function pointer libavcodec calls for every
// message; `pf_av_log` is a `extern "C"` fn with static lifetime, so it stays valid for
// the process. The transmute only retypes the 4th parameter from our `*mut c_void` to
// whatever bindgen named `va_list` on this target — that parameter is pointer-sized on
// every target we build (x86-64/aarch64 SysV pass the va_list struct indirectly; the
// Windows x64/arm64 ABI defines `va_list` as a plain `char *`), and `pf_av_log` never
// dereferences it, so no ABI-visible difference remains.
unsafe {
ffmpeg::ffi::av_log_set_callback(Some(std::mem::transmute(cb)))
};
}
/// Snapshot of [`AVCODEC_ERRORS`], for bracketing one decode call.
fn avcodec_error_count() -> u64 {
AVCODEC_ERRORS.load(std::sync::atomic::Ordering::Relaxed)
} }
impl Decoder { impl Decoder {
@@ -588,7 +492,6 @@ impl Decoder {
vaapi_fails: 0, vaapi_fails: 0,
first_fail: None, first_fail: None,
want_keyframe: false, want_keyframe: false,
concealed_run: 0,
#[cfg(windows)] #[cfg(windows)]
d3d11_import, d3d11_import,
#[cfg(windows)] #[cfg(windows)]
@@ -808,7 +711,6 @@ impl Decoder {
vaapi_fails: 0, vaapi_fails: 0,
first_fail: None, first_fail: None,
want_keyframe: false, want_keyframe: false,
concealed_run: 0,
// A PyroWave session never demotes (nothing else decodes it — a failure // A PyroWave session never demotes (nothing else decodes it — a failure
// renegotiates the codec instead), so the D3D11VA rebuild facts are unused // renegotiates the codec instead), so the D3D11VA rebuild facts are unused
// here; keep them well-formed rather than plumbing them in for nothing. // here; keep them well-formed rather than plumbing them in for nothing.
@@ -841,47 +743,6 @@ impl Decoder {
Ok(()) Ok(())
} }
/// A decode that **succeeded loudly**: libavcodec logged an error and then concealed,
/// handing back a frame and a success code. HEVC does this for `Error constructing the
/// frame RPS` / `First slice in a frame missing` / `Previous slice segment missing`,
/// H.264 for its reference-list equivalents — every one of them means the picture was
/// built on references the decoder could not resolve, i.e. it is wrong on screen.
///
/// Before this existed the `Ok` arm reset the streak, so this class was not merely
/// undetected but actively *erased* the evidence of the errors around it: a decoder
/// concealing every second frame looked perfectly healthy, never asked for an IDR, and
/// under the infinite GOP kept the damage for the life of the session.
///
/// The response is the IDR request, which is the thing that actually repairs the
/// picture. It deliberately does NOT feed [`Self::vaapi_fails`], the hardware-demotion
/// streak: an ordinary packet loss makes the decoder conceal every AU until the
/// requested IDR lands, and at 120 fps a 100300 ms round trip is 1236 of them — far
/// past [`VAAPI_DEMOTE_AFTER`], and past [`HW_DEMOTE_MIN_STREAK`] too if that IDR is
/// itself lost. Counting concealment there would demote a perfectly good decoder for
/// the crime of surviving a lossy second. Its own counter keeps the evidence (and the
/// log line a field report needs) without arming that trigger.
fn note_concealed(&mut self) {
self.want_keyframe = true;
self.concealed_run = self.concealed_run.saturating_add(1);
// Every AU of a loss burst comes through here, so this is debug, not warn — the
// run length is the interesting number and it is on the line.
tracing::debug!(
run = self.concealed_run,
"decoder concealed a damaged frame (libavcodec logged an error but returned \
success) — requesting a keyframe"
);
}
/// Consecutive concealed frames, reset by the first clean decode. A healthy session
/// shows short runs that end when the requested IDR lands; a run that keeps climbing
/// across many IDR cycles is a decoder producing wrong pictures from good input, which
/// is the shape of the Windows FFmpeg-Vulkan field reports. Exposed so the pump can put
/// it on the stats line — nothing else can see it, because libavcodec reports this by
/// logging rather than by failing.
pub fn concealed_run(&self) -> u32 {
self.concealed_run
}
/// Feed one access unit; returns the decoded frame (the host's streams are /// Feed one access unit; returns the decoded frame (the host's streams are
/// one-in/one-out). A software decode error after packet loss is survivable — log /// one-in/one-out). A software decode error after packet loss is survivable — log
/// upstream and keep feeding. A VAAPI error re-requests an IDR and retries the hardware /// upstream and keep feeding. A VAAPI error re-requests an IDR and retries the hardware
@@ -908,10 +769,6 @@ impl Decoder {
user_flags: u32, user_flags: u32,
complete: bool, complete: bool,
) -> Result<Option<DecodedImage>> { ) -> Result<Option<DecodedImage>> {
// Bracket the decode: libavcodec reports reference damage by LOGGING and then
// concealing, returning a frame and a success code. Without this the whole class is
// invisible to us — see `pf_av_log` and `note_concealed`.
let errors_before = avcodec_error_count();
let result = match &mut self.backend { let result = match &mut self.backend {
Backend::Vulkan(v) => { Backend::Vulkan(v) => {
debug_assert!(complete, "partial AUs are pyrowave-only"); debug_assert!(complete, "partial AUs are pyrowave-only");
@@ -935,19 +792,8 @@ impl Decoder {
}; };
match result { match result {
Ok(f) => { Ok(f) => {
if avcodec_error_count() > errors_before { self.vaapi_fails = 0;
self.note_concealed(); self.first_fail = None;
} else {
if self.concealed_run > 0 {
tracing::debug!(
run = self.concealed_run,
"decoder recovered — clean frame after a concealment run"
);
self.concealed_run = 0;
}
self.vaapi_fails = 0;
self.first_fail = None;
}
Ok(f) Ok(f)
} }
Err(e) => { Err(e) => {
@@ -1286,82 +1132,6 @@ mod tests {
assert!(!decode_device(0x8086, "Intel(R) Arc(TM) Pro Graphics").prefer_vulkan_first()); assert!(!decode_device(0x8086, "Intel(R) Arc(TM) Pro Graphics").prefer_vulkan_first());
} }
/// The cut that decides whether a libavcodec message arms a keyframe request. ERROR and
/// worse mean the picture is wrong; WARNING and below are chatter. Getting this wrong is
/// not subtle in either direction — too low and every session requests keyframes forever
/// off swscale's "deprecated pixel format used", too high and the concealment class this
/// whole mechanism exists to catch goes back to being invisible.
#[test]
fn only_error_and_worse_count_as_a_bad_decode() {
// PANIC / FATAL / ERROR
assert!(counts_as_decode_error(0));
assert!(counts_as_decode_error(8));
assert!(counts_as_decode_error(16));
// WARNING / INFO / VERBOSE / DEBUG / TRACE
assert!(!counts_as_decode_error(24));
assert!(!counts_as_decode_error(32));
assert!(!counts_as_decode_error(40));
assert!(!counts_as_decode_error(48));
assert!(!counts_as_decode_error(56));
}
/// The callback itself, through the same pointer libavcodec will call it by — the FFI
/// signature and the counter increment, not just the classifier. Deltas rather than
/// absolute values because the counter is process-global and tests run in parallel.
#[test]
fn the_log_callback_counts_errors_and_ignores_chatter() {
let msg = c"pf test message\n";
let before = avcodec_error_count();
// SAFETY: exactly what libavcodec does — a NUL-terminated static format string, a
// null context, and a va_list `pf_av_log` never reads (null is therefore fine).
unsafe { pf_av_log(std::ptr::null_mut(), 16, msg.as_ptr(), std::ptr::null_mut()) };
assert!(
avcodec_error_count() > before,
"an ERROR-level message must be counted"
);
let mid = avcodec_error_count();
// SAFETY: as above.
unsafe { pf_av_log(std::ptr::null_mut(), 24, msg.as_ptr(), std::ptr::null_mut()) };
assert_eq!(
avcodec_error_count(),
mid,
"a WARNING-level message must NOT be counted"
);
// A null fmt must not be dereferenced (defensive: libavcodec always passes one).
let pre_null = avcodec_error_count();
// SAFETY: the null-fmt path returns before any dereference — that is what is under test.
unsafe {
pf_av_log(
std::ptr::null_mut(),
16,
std::ptr::null(),
std::ptr::null_mut(),
)
};
assert_eq!(avcodec_error_count(), pre_null + 1);
}
/// Installing the callback must succeed on whatever this platform's `va_list` is — the
/// transmute in `quiet_ffmpeg_log` is the one place the FFI signature could be wrong,
/// and a wrong one is a crash inside libavcodec rather than a compile error.
#[test]
fn installing_the_log_callback_is_safe_and_idempotent() {
quiet_ffmpeg_log();
quiet_ffmpeg_log();
// Drive a real message through libavcodec's own dispatcher, which now routes to
// `pf_av_log`: this is the end-to-end proof that the installed pointer is callable.
let before = avcodec_error_count();
// SAFETY: `av_log` with a literal format string and no varargs to substitute.
unsafe { ffmpeg::ffi::av_log(std::ptr::null_mut(), 16, c"pf install probe\n".as_ptr()) };
assert!(
avcodec_error_count() > before,
"libavcodec must reach our callback after quiet_ffmpeg_log()"
);
}
/// Lock the DRM FourCC magic numbers against typos — these are the exact values /// Lock the DRM FourCC magic numbers against typos — these are the exact values
/// `<drm_fourcc.h>` defines, and a wrong one is what painted the Steam Deck green. /// `<drm_fourcc.h>` defines, and a wrong one is what painted the Steam Deck green.
#[test] #[test]
@@ -29,7 +29,7 @@ Source code
The bundled binaries are unmodified builds produced by the BtbN/FFmpeg-Builds The bundled binaries are unmodified builds produced by the BtbN/FFmpeg-Builds
project. The exact source for the FFmpeg release used is available from: project. The exact source for the FFmpeg release used is available from:
* FFmpeg project source: https://ffmpeg.org/download.html (release n8.1) * FFmpeg project source: https://ffmpeg.org/download.html (release n7.1)
* Exact build recipe: https://github.com/BtbN/FFmpeg-Builds * Exact build recipe: https://github.com/BtbN/FFmpeg-Builds
A copy of the corresponding FFmpeg source for the version shipped here is A copy of the corresponding FFmpeg source for the version shipped here is
@@ -32,39 +32,20 @@ if (Test-Path $rustup) {
# the separate BSD-2 openh264 crate; NVENC is the direct NVIDIA SDK). lgpl-shared keeps the # the separate BSD-2 openh264 crate; NVENC is the direct NVIDIA SDK). lgpl-shared keeps the
# bundled DLLs LGPL-2.1+ (dynamic linking satisfies the relink duty) rather than GPL, so the # bundled DLLs LGPL-2.1+ (dynamic linking satisfies the relink duty) rather than GPL, so the
# shipped installer/MSIX stay consistent with punktfunk's MIT OR Apache-2.0 posture. # shipped installer/MSIX stay consistent with punktfunk's MIT OR Apache-2.0 posture.
# VERSION: n8.1 (libavcodec 62). Bumped from n7.1 on 2026-08-05 — FFmpeg's **Vulkan Video # MIGRATION: a runner previously provisioned with the old *gpl-shared* trees must be
# hwaccel** is the youngest code in our decode chain (merged ~6.1/7.0), 7.1 is a stabilisation # re-provisioned - delete C:\Users\Public\ffmpeg and C:\Users\Public\ffmpeg-arm64, then re-run.
# branch that does not receive its ongoing fixes, and the two field reports of silent inter-frame
# corruption on Windows (Intel B580 2026-07, AMD Xbox Ally X 2026-08) both sit on that hwaccel
# while the mature d3d11va one is clean. Linux already ships avcodec 62 (Ubuntu 26.04 = 8.0.1) and
# pf-client-core compiles clean against it, so 8.x is not new ground for our API usage.
# MIGRATION is AUTOMATIC and must stay that way: the presence check below keys off $Version, so a
# runner provisioned with an older tree re-provisions itself on the next CI job. It used to test
# only for `lib\avcodec.lib`, which meant a version bump here silently did NOTHING on every
# already-provisioned runner — CI would keep building against the old tree while this file claimed
# otherwise. If you change the layout, keep the check version-derived.
# These DLLs are bundled verbatim into the code-signed host installer/MSIX, so the download is # These DLLs are bundled verbatim into the code-signed host installer/MSIX, so the download is
# SHA-256-pinned (like VB-CABLE below): BtbN's `latest` tag is a ROLLING release whose assets are # SHA-256-pinned (like VB-CABLE below): BtbN's `latest` tag is a ROLLING release whose assets are
# re-uploaded over time, so an unverified fetch would let a hijacked/MITM'd upstream asset land # re-uploaded over time, so an unverified fetch would let a hijacked/MITM'd upstream asset land
# signed DLLs in users' installs. The pins below were captured 2026-08-05 from the then-current # signed DLLs in users' installs. The pins below were captured 2026-07-10 from the then-current
# n8.1 lgpl-shared build. When BtbN re-rolls `latest`, this fetch FAILS CLOSED (hash mismatch) — # n7.1 lgpl-shared build. When BtbN re-rolls `latest`, this fetch FAILS CLOSED (hash mismatch) —
# that is intentional: re-download, re-verify the new archive, and update the two pins here. # that is intentional: re-download, re-verify the new archive, and update the two pins here.
# Refresh a pin: (Get-FileHash .\ffmpeg-<tag>.zip -Algorithm SHA256).Hash # Refresh a pin: (Get-FileHash .\ffmpeg-<tag>.zip -Algorithm SHA256).Hash
$ffmpegVersion = 'n8.1'
function Get-BtbnFfmpeg { function Get-BtbnFfmpeg {
param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64) or 'winarm64' (ARM64 cross tree) param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64) or 'winarm64' (ARM64 cross tree)
# Version-stamped marker, NOT a bare file-existence test — see the MIGRATION note above. Written if (Test-Path (Join-Path $Dir 'lib\avcodec.lib')) { info "FFmpeg ($ZipTag) already present at $Dir"; return }
# only after a successful extract, so a half-finished provision re-runs rather than being info "fetching FFmpeg ($ZipTag, BtbN lgpl-shared, SHA-256 pinned)"
# mistaken for a good tree. $url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-$ZipTag-lgpl-shared-7.1.zip"
$stamp = Join-Path $Dir '.punktfunk-ffmpeg-version'
$short = $ffmpegVersion.TrimStart('n')
if ((Test-Path (Join-Path $Dir 'lib\avcodec.lib')) -and
(Test-Path $stamp) -and
((Get-Content $stamp -Raw).Trim() -eq $ffmpegVersion)) {
info "FFmpeg $ffmpegVersion ($ZipTag) already present at $Dir"; return
}
info "fetching FFmpeg $ffmpegVersion ($ZipTag, BtbN lgpl-shared, SHA-256 pinned)"
$url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-$ffmpegVersion-latest-$ZipTag-lgpl-shared-$short.zip"
$zip = "$Dir.zip"; $tmp = "$Dir-extract" $zip = "$Dir.zip"; $tmp = "$Dir-extract"
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
$got = (Get-FileHash $zip -Algorithm SHA256).Hash $got = (Get-FileHash $zip -Algorithm SHA256).Hash
@@ -77,11 +58,10 @@ function Get-BtbnFfmpeg {
$inner = Get-ChildItem $tmp -Directory | Select-Object -First 1 $inner = Get-ChildItem $tmp -Directory | Select-Object -First 1
if (Test-Path $Dir) { Remove-Item -Recurse -Force $Dir } if (Test-Path $Dir) { Remove-Item -Recurse -Force $Dir }
Move-Item -Path $inner.FullName -Destination $Dir Move-Item -Path $inner.FullName -Destination $Dir
Set-Content -Path $stamp -Value $ffmpegVersion -Encoding ascii
Remove-Item -Force $zip; Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue Remove-Item -Force $zip; Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
} }
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64' -Sha '0D0F7449A5600AB5DF9AF19DA861B24CA1534279EDE099D6541F1FEFB17BFBA9' Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64' -Sha '89F3469706E5D53AEA5CF34AEE63E62CE746E6159D7AEE473D330B02A47558E6'
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64' -Sha 'CDC81352B7781DBAD87D8069AF7835FEC86C039F1ADC2B41BB27B3A295695A70' Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64' -Sha 'D96B4CE08CEBDCC6AD0E3934A3F962915E440EEFB9D73831AFEA4D80E35129A5'
# --- Vulkan-Headers (pf-ffvk's bindgen: libavutil/hwcontext_vulkan.h includes <vulkan/vulkan.h>, # --- Vulkan-Headers (pf-ffvk's bindgen: libavutil/hwcontext_vulkan.h includes <vulkan/vulkan.h>,
# and Windows has no system copy). Headers only - the loader (vulkan-1.dll) is a GPU-driver # and Windows has no system copy). Headers only - the loader (vulkan-1.dll) is a GPU-driver