Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3c0308c5 | ||
|
|
abc6d790bd | ||
|
|
d026e50a4b | ||
|
|
d6b9462092 |
@@ -15,8 +15,10 @@
|
||||
# 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
|
||||
# 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 7.x /
|
||||
# avcodec-61); the matrix points FFMPEG_DIR at the right one. aarch64 can't *run* on the x64 host,
|
||||
# C:\Users\Public\ffmpeg and an ARM64 tree at C:\Users\Public\ffmpeg-arm64 (both FFmpeg 8.1 /
|
||||
# avcodec-62 — version pinned in scripts/ci/provision-windows-punktfunk-extras.ps1, which
|
||||
# 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.
|
||||
#
|
||||
# The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, the x64 FFmpeg,
|
||||
|
||||
@@ -840,7 +840,7 @@ struct PadOut {
|
||||
#[cfg(windows)]
|
||||
impl PadOut {
|
||||
/// Correlate (HID container → endpoint id) and open a shared event-driven render stream ON
|
||||
/// that endpoint (`audio_wasapi::render_thread`'s shape — autoconvert, default period).
|
||||
/// that endpoint (`audio::render_thread`'s shape — autoconvert, default period).
|
||||
fn open() -> anyhow::Result<PadOut> {
|
||||
use anyhow::{anyhow, Context};
|
||||
let hid_path =
|
||||
@@ -921,8 +921,8 @@ fn pad_render_thread(
|
||||
const BLOCK_ALIGN: usize = PAD_CHANNELS * 4; // f32 interleaved
|
||||
let enumerator = wasapi::DeviceEnumerator::new().context("DeviceEnumerator")?;
|
||||
// Not `get_device`: that helper resolves through a freed string — see
|
||||
// [`crate::audio::device_by_id`] (audio_wasapi.rs, mounted as `crate::audio` on
|
||||
// Windows by lib.rs's `#[path]` swap — there is no `audio_wasapi` module name).
|
||||
// [`crate::audio::device_by_id`]. (`audio_wasapi.rs` is mounted as `crate::audio`
|
||||
// on Windows via `#[path]`, so it has no `crate::audio_wasapi` name to reach it by.)
|
||||
let device = crate::audio::device_by_id(&enumerator, &Direction::Render, endpoint_id)
|
||||
.map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?;
|
||||
let mut audio_client = device.get_iaudioclient().context("IAudioClient")?;
|
||||
|
||||
@@ -286,6 +286,9 @@ pub struct Decoder {
|
||||
/// 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.
|
||||
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 screen — kept for the mid-session Vulkan→D3D11VA demotion rung (the Windows
|
||||
/// analog of Linux's Vulkan→VAAPI rung).
|
||||
@@ -435,12 +438,78 @@ pub fn decodable_codecs_for(vk: Option<&VulkanDecodeDevice>) -> u8 {
|
||||
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
|
||||
/// (`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
|
||||
/// 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
|
||||
/// for decode debugging. Process-global; set once per decoder build (idempotent).
|
||||
/// after loss, but a raw flood in the user's terminal (it bypasses our tracing).
|
||||
///
|
||||
/// Two jobs. It sets the level (default fatal-only;
|
||||
/// `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() {
|
||||
use ffmpeg::util::log::Level;
|
||||
let level = match std::env::var("PUNKTFUNK_FFMPEG_LOG").ok().as_deref() {
|
||||
@@ -452,6 +521,33 @@ fn quiet_ffmpeg_log() {
|
||||
_ => Level::Fatal,
|
||||
};
|
||||
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 {
|
||||
@@ -492,6 +588,7 @@ impl Decoder {
|
||||
vaapi_fails: 0,
|
||||
first_fail: None,
|
||||
want_keyframe: false,
|
||||
concealed_run: 0,
|
||||
#[cfg(windows)]
|
||||
d3d11_import,
|
||||
#[cfg(windows)]
|
||||
@@ -711,6 +808,7 @@ impl Decoder {
|
||||
vaapi_fails: 0,
|
||||
first_fail: None,
|
||||
want_keyframe: false,
|
||||
concealed_run: 0,
|
||||
// A PyroWave session never demotes (nothing else decodes it — a failure
|
||||
// renegotiates the codec instead), so the D3D11VA rebuild facts are unused
|
||||
// here; keep them well-formed rather than plumbing them in for nothing.
|
||||
@@ -743,6 +841,47 @@ impl Decoder {
|
||||
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 100–300 ms round trip is 12–36 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
|
||||
/// 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
|
||||
@@ -769,6 +908,10 @@ impl Decoder {
|
||||
user_flags: u32,
|
||||
complete: bool,
|
||||
) -> 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 {
|
||||
Backend::Vulkan(v) => {
|
||||
debug_assert!(complete, "partial AUs are pyrowave-only");
|
||||
@@ -792,8 +935,19 @@ impl Decoder {
|
||||
};
|
||||
match result {
|
||||
Ok(f) => {
|
||||
self.vaapi_fails = 0;
|
||||
self.first_fail = None;
|
||||
if avcodec_error_count() > errors_before {
|
||||
self.note_concealed();
|
||||
} 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)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1132,6 +1286,82 @@ mod tests {
|
||||
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
|
||||
/// `<drm_fourcc.h>` defines, and a wrong one is what painted the Steam Deck green.
|
||||
#[test]
|
||||
|
||||
@@ -159,17 +159,6 @@ const CAP_REPROBE_WINDOWS_MAX: u32 = 128;
|
||||
/// choke again at the same place, and only backoffs at a climbed-to rate can agree within the
|
||||
/// band (a cascade's second backoff sits at ×0.7 of the first: outside it by construction).
|
||||
const DECODE_CAP_SIMILAR_DIV: u32 = 8;
|
||||
/// A deciding window that DELIVERED under `current / STARVED_DELIVERY_DIV` is STARVED: the
|
||||
/// stream barely flowed (a host-side capture stall, an outage, a mid-window pause), so whatever
|
||||
/// distress the window carries — a flush, a keyframe-ask burst — is starvation-shaped, not
|
||||
/// rate-shaped, and the decoder decoded almost nothing at the nominal rate. Such a window may
|
||||
/// still back off (real damage deserves the safe response) but must never be a decode-knee
|
||||
/// sample: latching `current_kbps` off a starved window teaches a phantom decoder cap at
|
||||
/// whatever rate the stall interrupted (the periodic-capture-stall field case: every 5 s cycle
|
||||
/// offers another pair of "backoffs" at the same rate — a bogus latch that then fights the
|
||||
/// re-probe ladder for minutes). Deliberately far below the ×¾ utilization bar climbs require:
|
||||
/// the band between them is ambiguous and keeps today's behavior.
|
||||
const STARVED_DELIVERY_DIV: u32 = 4;
|
||||
/// Rolling window (in 750 ms report windows, ~30 s) whose minimum mean is the OWD baseline.
|
||||
/// Long enough to remember the uncongested floor, short enough to follow genuine path changes.
|
||||
const BASELINE_WINDOWS: usize = 40;
|
||||
@@ -708,10 +697,6 @@ impl BitrateController {
|
||||
|| self.streak_decode_windows >= BAD_WINDOWS_TO_DECREASE
|
||||
|| (recovery_kf >= RECOVERY_KF_BAD && loss_ppm < HEAVY_LOSS_PPM)
|
||||
|| (flushed && (decode_bad || decode_mean_us.is_none()));
|
||||
// Starved deciding window (see [`STARVED_DELIVERY_DIV`]): the stream barely flowed,
|
||||
// so the window says nothing about what the decoder can hold at this rate.
|
||||
let starved =
|
||||
(actual_kbps as u64) * (STARVED_DELIVERY_DIV as u64) < self.current_kbps as u64;
|
||||
if !self.climb_since_backoff {
|
||||
// Still draining the previous backoff: the host acks a ×0.7 request in ~100 ms,
|
||||
// so this window's rate is one the decoder never choked at while keeping up —
|
||||
@@ -723,17 +708,6 @@ impl BitrateController {
|
||||
"adaptive bitrate: backoff without an intervening climb — draining the \
|
||||
previous choke, not a knee sample"
|
||||
);
|
||||
} else if starved {
|
||||
// Same "not a knee sample either way" treatment as the draining arm: neither
|
||||
// latch against a starved window nor let it erase the reference a real knee
|
||||
// set — the next genuine choke at that rate must still find its pair.
|
||||
tracing::debug!(
|
||||
at_kbps = self.current_kbps,
|
||||
actual_kbps,
|
||||
reference_kbps = self.decode_backoff_kbps,
|
||||
"adaptive bitrate: backoff in a starved window (delivery a fraction of \
|
||||
the target) — starvation-shaped distress, not a knee sample"
|
||||
);
|
||||
} else if decode_evidence {
|
||||
let rate = self.current_kbps;
|
||||
let similar = self.decode_backoff_kbps > 0
|
||||
@@ -2110,100 +2084,6 @@ mod tests {
|
||||
rate - rate / 16
|
||||
}
|
||||
|
||||
/// One capture-stall-shaped window at the current rate: almost nothing delivered
|
||||
/// (current/10), nothing decoded, no loss — but a jump-to-live flush and a keyframe-ask
|
||||
/// storm (the stall edge's damage signature). SEVERE, so it backs off; STARVED, so it must
|
||||
/// never be a knee sample.
|
||||
fn stall_choke(c: &mut BitrateController, start: Instant, tick: &mut u32) -> Option<u32> {
|
||||
*tick += 2;
|
||||
let r = c.on_window(
|
||||
ticks(start, *tick),
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
c.current_kbps / 10,
|
||||
true,
|
||||
RECOVERY_KF_SEVERE,
|
||||
);
|
||||
*tick += 1;
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_stall_windows_never_latch_a_decode_cap() {
|
||||
// The periodic-capture-stall field case (RDNA4 standby-sink, 5 s cycle): every stall
|
||||
// edge offers another flush + kf-storm "backoff" at the SAME rate — without the starved
|
||||
// guard that pair latches a phantom decoder knee at whatever rate the display driver
|
||||
// happened to interrupt, and the session then fights the re-probe ladder for minutes.
|
||||
let mut c = BitrateController::new(240_000);
|
||||
c.set_ceiling(900_000);
|
||||
let start = Instant::now();
|
||||
let mut t = 0;
|
||||
for _ in 0..4 {
|
||||
calm_window(&mut c, ticks(start, t));
|
||||
t += 1;
|
||||
}
|
||||
climb_to(&mut c, start, &mut t, 400_000);
|
||||
let at = c.current_kbps;
|
||||
let r1 = stall_choke(&mut c, start, &mut t).expect("stall damage still backs off");
|
||||
assert!(
|
||||
c.decode_cap_kbps.is_none(),
|
||||
"one starved window must not latch"
|
||||
);
|
||||
assert_eq!(
|
||||
c.decode_backoff_kbps, 0,
|
||||
"a starved window is not a knee sample — no reference recorded"
|
||||
);
|
||||
c.on_ack(r1);
|
||||
climb_to(&mut c, start, &mut t, at - at / DECODE_CAP_SIMILAR_DIV);
|
||||
let r2 = stall_choke(&mut c, start, &mut t).expect("second stall edge backs off too");
|
||||
c.on_ack(r2);
|
||||
assert!(
|
||||
c.decode_cap_kbps.is_none(),
|
||||
"a starved pair at the same rate must not latch a phantom knee"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starved_window_preserves_the_knee_reference() {
|
||||
// A REAL knee sample, then a stall edge, then the genuine re-climb choke: the starved
|
||||
// window in the middle must neither latch nor ERASE the reference the real choke set —
|
||||
// the genuine pair must still find each other around it.
|
||||
let mut c = BitrateController::new(500_000);
|
||||
c.set_ceiling(900_000);
|
||||
let start = Instant::now();
|
||||
let mut t = 0;
|
||||
for _ in 0..4 {
|
||||
calm_window(&mut c, ticks(start, t));
|
||||
t += 1;
|
||||
}
|
||||
let knee = c.current_kbps;
|
||||
let r1 = choke(&mut c, start, &mut t).expect("real choke backs off");
|
||||
assert_eq!(
|
||||
c.decode_backoff_kbps, knee,
|
||||
"real choke records the reference"
|
||||
);
|
||||
c.on_ack(r1);
|
||||
climb_to(&mut c, start, &mut t, knee - knee / DECODE_CAP_SIMILAR_DIV);
|
||||
let r2 = stall_choke(&mut c, start, &mut t).expect("stall edge backs off");
|
||||
assert_eq!(
|
||||
c.decode_backoff_kbps, knee,
|
||||
"the starved window must not erase the real reference"
|
||||
);
|
||||
assert!(c.decode_cap_kbps.is_none(), "and must not latch against it");
|
||||
c.on_ack(r2);
|
||||
climb_to(&mut c, start, &mut t, knee - knee / DECODE_CAP_SIMILAR_DIV);
|
||||
let rate = c.current_kbps;
|
||||
choke(&mut c, start, &mut t).expect("genuine re-climb choke backs off");
|
||||
assert_eq!(
|
||||
c.decode_cap_kbps,
|
||||
Some(rate - rate / 16),
|
||||
"the genuine pair still latches around the starved interruption"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_cap_latches_when_the_reclimb_chokes_at_the_same_knee() {
|
||||
// The 1440p120 field sawtooth: a decoder knee (~500 Mbps) well under the (inflated)
|
||||
|
||||
@@ -441,27 +441,26 @@ fn idd_adaptive_enabled() -> bool {
|
||||
/// Seal one access unit and send it with MICROBURST pacing (the shared
|
||||
/// [`send_pacing`](crate::send_pacing) policy, native parameterization): the first `burst_cap`
|
||||
/// bytes go out immediately (one absorbed burst the NIC / socket tx-buffer can swallow), and
|
||||
/// only the OVERFLOW beyond that is spread across the time it needs at `pace_rate_bps` in
|
||||
/// ADAPTIVE chunks — 16 packets at today's rates, coarsening to at most 64 (the GSO-segment
|
||||
/// cap) once the rate would otherwise skip every sub-floor sleep, so ≥1 Gbps frames still pace
|
||||
/// instead of collapsing into an unpaced blast (plan Phase 1.2). `burst_cap` `None` = auto:
|
||||
/// `max(128 KB, this AU's wire bytes / 4)`, so the burst stays a bounded fraction of a
|
||||
/// high-rate frame instead of swallowing it whole (plan Phase 1.3); `Some` =
|
||||
/// PUNKTFUNK_PACE_BURST_KB pinned an absolute cap. So a normal-bitrate frame (≤ cap) leaves in
|
||||
/// one immediate burst at ~0 added latency, while a genuine IDR / sustained-high-bitrate frame
|
||||
/// (≫ cap) still spreads — keeping the freeze fix exactly where it's needed (an unpaced
|
||||
/// line-rate burst overruns the kernel tx buffer → EAGAIN drop → under infinite GOP, a freeze
|
||||
/// until the next keyframe).
|
||||
/// only the OVERFLOW beyond that is spread across `min(~90% of the time to deadline, the time
|
||||
/// the overflow needs at pace_rate_bps)` in ADAPTIVE chunks — 16 packets at today's rates,
|
||||
/// coarsening to at most 64 (the GSO-segment cap) once the rate would otherwise skip every
|
||||
/// sub-floor sleep, so ≥1 Gbps frames still pace instead of collapsing into an unpaced blast
|
||||
/// (plan Phase 1.2). `burst_cap` `None` = auto: `max(128 KB, this AU's wire bytes / 4)`, so
|
||||
/// the burst stays a bounded fraction of a high-rate frame instead of swallowing it whole
|
||||
/// (plan Phase 1.3); `Some` = PUNKTFUNK_PACE_BURST_KB pinned an absolute cap. So a
|
||||
/// normal-bitrate frame (≤ cap) leaves in one immediate burst at ~0 added latency, while a
|
||||
/// genuine IDR / sustained-high-bitrate frame (≫ cap) still spreads — keeping the freeze fix
|
||||
/// exactly where it's needed (an unpaced line-rate burst overruns the kernel tx buffer →
|
||||
/// EAGAIN drop → under infinite GOP, a freeze until the next keyframe). With no slack
|
||||
/// (encode ≈ interval) the budget collapses to 0 and even the overflow goes out immediately,
|
||||
/// so this is never slower than unpaced.
|
||||
///
|
||||
/// `pace_rate_bps` (latency plan T1.2; resume-safe form, stall program T2): the caller passes
|
||||
/// ~3× the live encoder bitrate — a rate the link is proven to carry sustained — and the
|
||||
/// overflow's wire time at that rate IS the pace budget ([`crate::send_pacing::native_budget`],
|
||||
/// [`crate::send_pacing::MAX_PACE_SPREAD`]-bounded). The frame deadline no longer under-cuts
|
||||
/// the spread: for a steady-state frame the rate term was the smaller one anyway (tail gone in
|
||||
/// a fraction of the interval), and for an oversized frame (stall-resume scene delta, cold
|
||||
/// IDR) the old deadline clamp was exactly the line-rate blast → tx-overrun → freeze path this
|
||||
/// module exists to prevent. `0` = uncapped legacy deadline-only spread
|
||||
/// (PUNKTFUNK_PACE_FACTOR=0, and the fallback when the bitrate isn't known yet).
|
||||
/// `pace_rate_bps` (latency plan T1.2) bounds the spread from above: the deadline term alone
|
||||
/// smears a big frame's tail across the whole remaining interval (~15 ms at 60 fps) even when
|
||||
/// the link could drain it in 2–3 ms. The caller passes ~3× the live encoder bitrate — a rate
|
||||
/// the link is proven to carry sustained, so the bounded excursion keeps the anti-freeze
|
||||
/// property while the tail leaves as soon as the link plausibly allows. `0` = uncapped
|
||||
/// (legacy smoothness-only spread, and the fallback when the bitrate isn't known yet).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paced_submit(
|
||||
session: &mut Session,
|
||||
@@ -499,22 +498,34 @@ fn pace_sealed(
|
||||
chunk: crate::send_pacing::ChunkPolicy::Adaptive { base: 16, max: 64 },
|
||||
sleep_floor: std::time::Duration::from_micros(500),
|
||||
};
|
||||
// T1.2 rate cap, resume-safe form (stall program T2): the overflow's wire time at
|
||||
// `pace_rate_bps` IS the budget — the deadline no longer under-cuts it, so an oversized
|
||||
// frame (a stall-resume scene delta, a cold IDR) paces at the proven 3× rate instead of
|
||||
// collapsing into a line-rate blast that overruns the socket buffer and loses the very
|
||||
// frame that ends a freeze. See `send_pacing::native_budget` for the full argument.
|
||||
// T1.2 rate cap: the overflow's wire time at `pace_rate_bps`. Only the bytes past the
|
||||
// burst pace at all, so only they bound the budget.
|
||||
let overflow_bytes = wire_bytes.saturating_sub(burst_bytes) as u64;
|
||||
let budget = crate::send_pacing::native_budget(deadline, pace_rate_bps, overflow_bytes);
|
||||
let cap = if pace_rate_bps > 0 && overflow_bytes > 0 {
|
||||
std::time::Duration::from_nanos(
|
||||
(overflow_bytes * 8).saturating_mul(1_000_000_000) / pace_rate_bps,
|
||||
)
|
||||
} else {
|
||||
std::time::Duration::MAX
|
||||
};
|
||||
// Time the socket handoff per chunk and fold it into the session's SealPerf split — the
|
||||
// sleeps between chunks stay excluded, so sock_ns is pure send_gso/sendmmsg time.
|
||||
let mut sock_ns = 0u64;
|
||||
let result = crate::send_pacing::pace_frame(&refs, budget, &cfg, |chunk| {
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = session.send_sealed(chunk).map(|_| ());
|
||||
sock_ns += t0.elapsed().as_nanos() as u64;
|
||||
r
|
||||
});
|
||||
let result = crate::send_pacing::pace_frame(
|
||||
&refs,
|
||||
crate::send_pacing::PaceBudget::UntilDeadline {
|
||||
deadline,
|
||||
fraction: 0.9,
|
||||
cap,
|
||||
},
|
||||
&cfg,
|
||||
|chunk| {
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = session.send_sealed(chunk).map(|_| ());
|
||||
sock_ns += t0.elapsed().as_nanos() as u64;
|
||||
r
|
||||
},
|
||||
);
|
||||
drop(refs); // release the borrow of `wires` so it can return to the seal pool
|
||||
session.reclaim_wires(wires);
|
||||
session.note_sock_ns(sock_ns);
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) enum ChunkPolicy {
|
||||
}
|
||||
|
||||
/// The time the paced (post-burst) packets spread across.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum PaceBudget {
|
||||
/// `min((deadline − now-after-burst) × fraction, cap)`, collapsing to 0 with no slack
|
||||
/// (native: fraction 0.9). `cap` bounds the spread to the time the overflow actually needs
|
||||
@@ -68,53 +68,10 @@ pub(crate) enum PaceBudget {
|
||||
fraction: f32,
|
||||
cap: Duration,
|
||||
},
|
||||
/// A precomputed fixed budget (GameStream: ¾ of the frame interval; native: the rate-cap
|
||||
/// spread from [`native_budget`]).
|
||||
/// A precomputed fixed budget (GameStream: ¾ of the frame interval).
|
||||
Fixed(Duration),
|
||||
}
|
||||
|
||||
/// Absolute ceiling on one frame's paced spread (native plane): a pathological frame must not
|
||||
/// park the send thread for longer than this, whatever the rate math says. At the ceiling the
|
||||
/// tail is late but delivered whole — still strictly better than the blast-loss → freeze →
|
||||
/// recovery-IDR round trip it replaces.
|
||||
pub(crate) const MAX_PACE_SPREAD: Duration = Duration::from_millis(100);
|
||||
|
||||
/// The native plane's pace budget for one frame (pure — unit-tested): with the T1.2 rate cap
|
||||
/// active, the paced overflow spreads across exactly the time it needs at the pace rate
|
||||
/// (`cap`, bounded by [`MAX_PACE_SPREAD`]) and is NEVER under-cut by the frame deadline.
|
||||
///
|
||||
/// The old schedule took `min(0.9 × time-to-deadline, cap)`. For a steady-state frame the cap
|
||||
/// is the smaller term and nothing changes. But for an OVERSIZED frame — a stall-resume scene
|
||||
/// delta after seconds of frozen composition, a cold IDR — the overflow needs SEVERAL frame
|
||||
/// intervals at the pace rate, and the deadline term clamped that into the remainder of ONE:
|
||||
/// an instantaneous many-×-stream-rate blast that overruns the socket tx-buffer and loses the
|
||||
/// very frame that would have ended the freeze (field fingerprint: WSAENOBUFS 10055 +
|
||||
/// `loss_ppm` spikes at capture-stall edges, then a recovery-IDR round trip per retry). The
|
||||
/// pace rate is ~3× a rate the link demonstrably carries, so holding it past the deadline is
|
||||
/// safe by the same argument that introduced the cap — the deadline stays a *target*, not a
|
||||
/// license to blast.
|
||||
///
|
||||
/// `pace_rate_bps == 0` (PUNKTFUNK_PACE_FACTOR=0) or an overflow-free frame keeps the legacy
|
||||
/// deadline-only spread.
|
||||
pub(crate) fn native_budget(
|
||||
deadline: Instant,
|
||||
pace_rate_bps: u64,
|
||||
overflow_bytes: u64,
|
||||
) -> PaceBudget {
|
||||
if pace_rate_bps > 0 && overflow_bytes > 0 {
|
||||
let cap = Duration::from_nanos(
|
||||
(overflow_bytes * 8).saturating_mul(1_000_000_000) / pace_rate_bps,
|
||||
);
|
||||
PaceBudget::Fixed(cap.min(MAX_PACE_SPREAD))
|
||||
} else {
|
||||
PaceBudget::UntilDeadline {
|
||||
deadline,
|
||||
fraction: 0.9,
|
||||
cap: Duration::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-plane pacing parameters. See the module doc for the two canonical values.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct PaceCfg {
|
||||
@@ -641,43 +598,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// [`native_budget`]: with the rate cap active the budget is the overflow's wire time at
|
||||
/// the pace rate — a FIXED spread the deadline can no longer under-cut — bounded by
|
||||
/// [`MAX_PACE_SPREAD`]; rate 0 / no overflow keep the legacy deadline-only schedule.
|
||||
#[test]
|
||||
fn native_budget_is_rate_bound_never_deadline_cut() {
|
||||
// The stall-resume case the fix exists for: a 3 MB overflow at 3×240 Mbps needs
|
||||
// ~33 ms — an IMMINENT deadline (the old min() made this a blast) must not shrink it.
|
||||
let deadline = Instant::now() + Duration::from_millis(4); // 240 fps interval
|
||||
let b = native_budget(deadline, 720_000_000, 3_000_000);
|
||||
assert_eq!(b, PaceBudget::Fixed(Duration::from_nanos(33_333_333)));
|
||||
|
||||
// A steady-state frame: overflow 90 KB at 3×240 Mbps = 1 ms — identical to what the
|
||||
// old min(slack, cap) chose (cap was the smaller term), so nothing regresses.
|
||||
let b = native_budget(deadline, 720_000_000, 90_000);
|
||||
assert_eq!(b, PaceBudget::Fixed(Duration::from_micros(1_000)));
|
||||
|
||||
// A crater-rate resume (ABR backed off to 20 Mbps, pace 60 Mbps): the raw rate math
|
||||
// says 400 ms for 3 MB — the absolute ceiling bounds the send thread's stall.
|
||||
let b = native_budget(deadline, 60_000_000, 3_000_000);
|
||||
assert_eq!(b, PaceBudget::Fixed(MAX_PACE_SPREAD));
|
||||
|
||||
// Rate cap off (PUNKTFUNK_PACE_FACTOR=0): the legacy deadline-only spread, uncapped.
|
||||
let b = native_budget(deadline, 0, 3_000_000);
|
||||
assert!(matches!(
|
||||
b,
|
||||
PaceBudget::UntilDeadline {
|
||||
fraction,
|
||||
cap: Duration::MAX,
|
||||
..
|
||||
} if fraction == 0.9
|
||||
));
|
||||
|
||||
// No overflow (the whole frame bursts): budget is never consulted — legacy shape.
|
||||
let b = native_budget(deadline, 720_000_000, 0);
|
||||
assert!(matches!(b, PaceBudget::UntilDeadline { .. }));
|
||||
}
|
||||
|
||||
/// `inject_video_drop` is a no-op when the knob is off (the default test env).
|
||||
#[test]
|
||||
fn drop_injection_off_by_default() {
|
||||
|
||||
@@ -29,7 +29,7 @@ Source code
|
||||
The bundled binaries are unmodified builds produced by the BtbN/FFmpeg-Builds
|
||||
project. The exact source for the FFmpeg release used is available from:
|
||||
|
||||
* FFmpeg project source: https://ffmpeg.org/download.html (release n7.1)
|
||||
* FFmpeg project source: https://ffmpeg.org/download.html (release n8.1)
|
||||
* Exact build recipe: https://github.com/BtbN/FFmpeg-Builds
|
||||
|
||||
A copy of the corresponding FFmpeg source for the version shipped here is
|
||||
|
||||
+9
-16
@@ -21,15 +21,12 @@
|
||||
],
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"undici": "^8.9.0",
|
||||
},
|
||||
"packages": {
|
||||
"@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.98", "", { "dependencies": { "swagger2openapi": "^7.0.8" }, "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.98", "effect": "^4.0.0-beta.98" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-7bqawr/HqJWqQ8H/bHyzBlLPA3LIIm3Y+cGYlIxnC/QVK795QpiEXb7uxTnP7V7w49V0sBtTerv4/9ZjsMffLQ=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.103", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.103" } }, "sha512-0aCZMBid5ifqmY55TkfCDLaGTIM8qu3bNFUW7qL9vh/7jFOkaIAMX2MA8muG4deqW17XWxawddWu4v0fK+UW3g=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.99", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.99" } }, "sha512-POBAowafsAAb3bH1x1rJlWnv32yMAazFgEuRW5LhkW/JJA5VGoEk9OnuoUkIH1OW6K/X6IrdNpqcO+5e9lPQJA=="],
|
||||
|
||||
"@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="],
|
||||
|
||||
@@ -47,15 +44,15 @@
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
||||
|
||||
"@punktfunk/host": ["@punktfunk/host@file:../sdk", { "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", "bun2nix": "2.1.2", "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" }, "bin": { "punktfunk-scripting": "./dist/runner-cli.js" } }],
|
||||
"@punktfunk/host": ["@punktfunk/host@file:../sdk", { "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" }, "bin": { "punktfunk-scripting": "./dist/runner-cli.js" } }],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="],
|
||||
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
@@ -65,8 +62,6 @@
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"bun2nix": ["bun2nix@2.1.2", "", { "dependencies": { "sade": "^1.8.1" }, "bin": { "bun2nix": "index.ts" } }, "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="],
|
||||
|
||||
"call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
@@ -113,11 +108,9 @@
|
||||
|
||||
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="],
|
||||
"msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
|
||||
|
||||
@@ -151,8 +144,6 @@
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"should": ["should@13.2.3", "", { "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", "should-type": "^1.4.0", "should-type-adaptors": "^1.0.1", "should-util": "^1.0.0" } }, "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ=="],
|
||||
|
||||
"should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="],
|
||||
@@ -179,7 +170,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
@@ -191,7 +182,7 @@
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
|
||||
"ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
@@ -201,6 +192,8 @@
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@effect/platform-node/undici": ["undici@8.8.0", "", {}, "sha512-ubshXMXwF3MQIMF1y/WxZdNBnjEKeSg2wF5mcGUtU55YTw34tnVVpKRlLf7ruDXZ5344KokPVX4RBx1wJm64Bw=="],
|
||||
|
||||
"oas-linter/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
||||
|
||||
"oas-resolver/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
||||
|
||||
@@ -57,8 +57,5 @@
|
||||
"@types/react": "^19.2.16",
|
||||
"effect": "4.0.0-beta.99",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"undici": "^8.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,39 @@ if (Test-Path $rustup) {
|
||||
# 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
|
||||
# shipped installer/MSIX stay consistent with punktfunk's MIT OR Apache-2.0 posture.
|
||||
# MIGRATION: a runner previously provisioned with the old *gpl-shared* trees must be
|
||||
# re-provisioned - delete C:\Users\Public\ffmpeg and C:\Users\Public\ffmpeg-arm64, then re-run.
|
||||
# VERSION: n8.1 (libavcodec 62). Bumped from n7.1 on 2026-08-05 — FFmpeg's **Vulkan Video
|
||||
# hwaccel** is the youngest code in our decode chain (merged ~6.1/7.0), 7.1 is a stabilisation
|
||||
# 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
|
||||
# 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
|
||||
# signed DLLs in users' installs. The pins below were captured 2026-07-10 from the then-current
|
||||
# n7.1 lgpl-shared build. When BtbN re-rolls `latest`, this fetch FAILS CLOSED (hash mismatch) —
|
||||
# signed DLLs in users' installs. The pins below were captured 2026-08-05 from the then-current
|
||||
# n8.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.
|
||||
# Refresh a pin: (Get-FileHash .\ffmpeg-<tag>.zip -Algorithm SHA256).Hash
|
||||
$ffmpegVersion = 'n8.1'
|
||||
function Get-BtbnFfmpeg {
|
||||
param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64) or 'winarm64' (ARM64 cross tree)
|
||||
if (Test-Path (Join-Path $Dir 'lib\avcodec.lib')) { info "FFmpeg ($ZipTag) already present at $Dir"; return }
|
||||
info "fetching FFmpeg ($ZipTag, BtbN lgpl-shared, SHA-256 pinned)"
|
||||
$url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-$ZipTag-lgpl-shared-7.1.zip"
|
||||
# Version-stamped marker, NOT a bare file-existence test — see the MIGRATION note above. Written
|
||||
# only after a successful extract, so a half-finished provision re-runs rather than being
|
||||
# mistaken for a good tree.
|
||||
$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"
|
||||
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||
$got = (Get-FileHash $zip -Algorithm SHA256).Hash
|
||||
@@ -58,10 +77,11 @@ function Get-BtbnFfmpeg {
|
||||
$inner = Get-ChildItem $tmp -Directory | Select-Object -First 1
|
||||
if (Test-Path $Dir) { Remove-Item -Recurse -Force $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
|
||||
}
|
||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64' -Sha '89F3469706E5D53AEA5CF34AEE63E62CE746E6159D7AEE473D330B02A47558E6'
|
||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64' -Sha 'D96B4CE08CEBDCC6AD0E3934A3F962915E440EEFB9D73831AFEA4D80E35129A5'
|
||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64' -Sha '0D0F7449A5600AB5DF9AF19DA861B24CA1534279EDE099D6541F1FEFB17BFBA9'
|
||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64' -Sha 'CDC81352B7781DBAD87D8069AF7835FEC86C039F1ADC2B41BB27B3A295695A70'
|
||||
|
||||
# --- 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
|
||||
|
||||
+3
-4
@@ -20,9 +20,6 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"undici": "^8.9.0",
|
||||
},
|
||||
"packages": {
|
||||
"@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.98", "", { "dependencies": { "swagger2openapi": "^7.0.8" }, "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.98", "effect": "^4.0.0-beta.98" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-7bqawr/HqJWqQ8H/bHyzBlLPA3LIIm3Y+cGYlIxnC/QVK795QpiEXb7uxTnP7V7w49V0sBtTerv4/9ZjsMffLQ=="],
|
||||
|
||||
@@ -172,7 +169,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
@@ -194,6 +191,8 @@
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@effect/platform-node/undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="],
|
||||
|
||||
"oas-linter/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
||||
|
||||
"oas-resolver/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
||||
|
||||
+7
-3
@@ -313,9 +313,13 @@
|
||||
url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz";
|
||||
hash = "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==";
|
||||
};
|
||||
"undici@8.10.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz";
|
||||
hash = "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==";
|
||||
"undici@7.28.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz";
|
||||
hash = "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==";
|
||||
};
|
||||
"undici@8.7.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz";
|
||||
hash = "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==";
|
||||
};
|
||||
"uuid@14.0.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz";
|
||||
|
||||
@@ -55,8 +55,5 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"undici": "^7.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"undici": "^8.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -46,16 +46,16 @@
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": "^5.0.9",
|
||||
"brace-expansion": "^5.0.8",
|
||||
"dompurify": "^3.4.12",
|
||||
"fast-uri": "^3.1.5",
|
||||
"fast-uri": "^3.1.4",
|
||||
"immutable": "^4.3.9",
|
||||
"js-yaml": "^4.3.0",
|
||||
"linkify-it": "^5.0.2",
|
||||
"postcss": "^8.5.25",
|
||||
"postcss": "^8.5.10",
|
||||
"sharp": "^0.35.3",
|
||||
"tar": "^7.5.21",
|
||||
"undici": "^7.29.0",
|
||||
"undici": "^7.28.0",
|
||||
},
|
||||
"packages": {
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
|
||||
@@ -1118,7 +1118,7 @@
|
||||
|
||||
"body-scroll-lock": ["body-scroll-lock@4.0.0-beta.0", "", {}, "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||
"brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
@@ -1388,7 +1388,7 @@
|
||||
|
||||
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="],
|
||||
"fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="],
|
||||
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||
|
||||
@@ -1876,7 +1876,7 @@
|
||||
|
||||
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="],
|
||||
"postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
||||
|
||||
@@ -2196,7 +2196,7 @@
|
||||
|
||||
"unctx": ["unctx@2.5.0", "", { "dependencies": { "acorn": "^8.15.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", "unplugin": "^2.3.11" } }, "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg=="],
|
||||
|
||||
"undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
|
||||
+20
-12
@@ -2151,6 +2151,10 @@
|
||||
url = "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz";
|
||||
hash = "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==";
|
||||
};
|
||||
"balanced-match@1.0.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz";
|
||||
hash = "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==";
|
||||
};
|
||||
"balanced-match@4.0.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz";
|
||||
hash = "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==";
|
||||
@@ -2195,9 +2199,13 @@
|
||||
url = "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz";
|
||||
hash = "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==";
|
||||
};
|
||||
"brace-expansion@5.0.9" = fetchurl {
|
||||
url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz";
|
||||
hash = "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==";
|
||||
"brace-expansion@2.1.2" = fetchurl {
|
||||
url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz";
|
||||
hash = "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==";
|
||||
};
|
||||
"brace-expansion@5.0.7" = fetchurl {
|
||||
url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz";
|
||||
hash = "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==";
|
||||
};
|
||||
"braces@3.0.3" = fetchurl {
|
||||
url = "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz";
|
||||
@@ -2807,9 +2815,9 @@
|
||||
url = "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz";
|
||||
hash = "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==";
|
||||
};
|
||||
"fast-uri@3.1.5" = fetchurl {
|
||||
url = "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz";
|
||||
hash = "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==";
|
||||
"fast-uri@3.1.4" = fetchurl {
|
||||
url = "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz";
|
||||
hash = "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==";
|
||||
};
|
||||
"fastq@1.20.1" = fetchurl {
|
||||
url = "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz";
|
||||
@@ -3903,9 +3911,9 @@
|
||||
url = "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz";
|
||||
hash = "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==";
|
||||
};
|
||||
"postcss@8.5.25" = fetchurl {
|
||||
url = "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz";
|
||||
hash = "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==";
|
||||
"postcss@8.5.22" = fetchurl {
|
||||
url = "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz";
|
||||
hash = "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==";
|
||||
};
|
||||
"powershell-utils@0.1.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz";
|
||||
@@ -4611,9 +4619,9 @@
|
||||
url = "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz";
|
||||
hash = "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==";
|
||||
};
|
||||
"undici@7.29.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz";
|
||||
hash = "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==";
|
||||
"undici@7.28.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz";
|
||||
hash = "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==";
|
||||
};
|
||||
"unenv@2.0.0-rc.24" = fetchurl {
|
||||
url = "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz";
|
||||
|
||||
+4
-4
@@ -64,11 +64,11 @@
|
||||
"dompurify": "^3.4.12",
|
||||
"linkify-it": "^5.0.2",
|
||||
"sharp": "^0.35.3",
|
||||
"fast-uri": "^3.1.5",
|
||||
"fast-uri": "^3.1.4",
|
||||
"immutable": "^4.3.9",
|
||||
"undici": "^7.29.0",
|
||||
"postcss": "^8.5.25",
|
||||
"undici": "^7.28.0",
|
||||
"postcss": "^8.5.10",
|
||||
"js-yaml": "^4.3.0",
|
||||
"brace-expansion": "^5.0.9"
|
||||
"brace-expansion": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user