From 05b5bf5f85d5d1fed8f37725a2754afb407bf099 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 25 Jun 2026 20:55:43 -0400 Subject: [PATCH 1/3] editor: zero-copy H.264 for video-only export too Video-only (no-audio) H.264 export now uses the same background-thread zero-copy VAAPI path as video+audio, instead of the software encoder. It writes the output .mp4 directly (no mux step) and reports through the single-export progress channel; completion clears that channel so the dialog closes cleanly. Factored the encoder/renderer/resources setup shared by both entry points into ExportOrchestrator::try_build_zero_copy. start_video_export gains the document/ video_manager/raster_store/container_path inputs to seed the off-thread render from a Document snapshot. Non-H264 / non-VAAPI still falls back to software. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-editor/src/export/mod.rs | 229 +++++++++++------- .../lightningbeam-editor/src/main.rs | 9 +- 2 files changed, 150 insertions(+), 88 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index 4657465..d3cca41 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -833,21 +833,89 @@ impl ExportOrchestrator { } } - /// Start a video export in the background (encoder thread) + /// Build a zero-copy VAAPI H.264 encoder targeting `output_path` (with its own vello + /// renderer + GPU resources + RGBA texture on the encoder's device), or `None` when the + /// codec isn't H.264 or VAAPI / the GPU device is unavailable — in which case the caller + /// falls back to the software encoder path. Used by both the video-only and video+audio + /// export entry points. + fn try_build_zero_copy( + settings: &VideoExportSettings, + width: u32, + height: u32, + framerate: f64, + output_path: &std::path::Path, + ) -> Option { + if !matches!(settings.codec, lightningbeam_core::export::VideoCodec::H264) { + return None; + } + let encoder = match gpu_video_encoder::encoder::ZeroCopyEncoder::new( + width, + height, + framerate.round() as i32, + settings.quality.bitrate_kbps(), + output_path, + ) { + Ok(e) => e, + Err(e) => { + println!("🎬 [EXPORT] zero-copy unavailable ({e}); software path"); + return None; + } + }; + let renderer = match vello::Renderer::new( + encoder.device(), + vello::RendererOptions { + use_cpu: false, + antialiasing_support: vello::AaSupport::all(), + num_init_threads: None, + pipeline_cache: None, + }, + ) { + Ok(r) => r, + Err(e) => { + println!("🎬 [EXPORT] zero-copy renderer init failed ({e}); software path"); + return None; + } + }; + let gpu_resources = video_exporter::ExportGpuResources::new(encoder.device(), width, height); + let rgba = encoder.device().create_texture(&wgpu::TextureDescriptor { + label: Some("zerocopy_export_rgba"), + size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + println!("🎬 [EXPORT] zero-copy VAAPI H.264 enabled"); + Some(ZeroCopyVideo { encoder, renderer, gpu_resources, rgba }) + } + + /// Start a video export in the background. /// - /// Returns immediately after spawning encoder thread. Caller must call - /// `render_next_video_frame()` repeatedly from the main thread to feed frames. + /// For H.264 with VAAPI available this renders + hardware-encodes on a background thread + /// (writing `output_path` directly); otherwise it spawns the software encoder thread and the + /// caller drives frames via `render_next_video_frame()` from the main thread. /// /// # Arguments /// * `settings` - Video export settings /// * `output_path` - Output file path + /// * `document`/`video_manager`/`raster_store`/`container_path` - scene data; the zero-copy + /// path snapshots the document and renders off-thread (the UI keeps the live one). /// /// # Returns /// Ok(()) on success, Err on failure + #[allow(clippy::too_many_arguments)] pub fn start_video_export( &mut self, settings: VideoExportSettings, output_path: PathBuf, + document: &Document, + video_manager: Arc>, + raster_store: lightningbeam_core::raster_store::RasterStore, + container_path: Option, ) -> Result<(), String> { println!("🎬 [VIDEO EXPORT] Starting video export"); @@ -870,38 +938,69 @@ impl ExportOrchestrator { self.cancel_flag.store(false, Ordering::Relaxed); let cancel_flag = Arc::clone(&self.cancel_flag); - // Spawn encoder thread - let handle = std::thread::spawn(move || { - Self::run_video_encoder( - settings, - output_path, - frame_rx, - progress_tx, - cancel_flag, - total_frames, - ); - }); + // Zero-copy VAAPI H.264 writes the final output directly (no audio → no mux), reporting + // through the single-export progress channel. Otherwise use the software encoder thread. + let zero_copy = Self::try_build_zero_copy(&settings, width, height, framerate, &output_path); + + let (handle, video_state) = match zero_copy { + Some(zc) => { + drop(frame_rx); + let document_snapshot = document.clone(); + let mut image_cache = ImageCache::new(); + image_cache.set_container_path(container_path); + let handle = std::thread::spawn(move || { + Self::run_zerocopy_video_export( + zc, + document_snapshot, + image_cache, + video_manager, + raster_store, + total_frames, + start_time, + framerate, + width, + height, + output_path, + progress_tx, + cancel_flag, + ); + }); + (handle, None) + } + None => { + let handle = std::thread::spawn(move || { + Self::run_video_encoder( + settings, + output_path, + frame_rx, + progress_tx, + cancel_flag, + total_frames, + ); + }); + // GPU resources + readback pipeline init lazily on the first frame (needs device). + let state = VideoExportState { + current_frame: 0, + total_frames, + start_time, + end_time, + framerate, + width, + height, + frame_tx: Some(frame_tx), + gpu_resources: None, + readback_pipeline: None, + cpu_yuv_converter: None, + frames_in_flight: 0, + next_frame_to_encode: 0, + perf_metrics: Some(perf_metrics::ExportMetrics::new()), + }; + (handle, Some(state)) + } + }; self.thread_handle = Some(handle); - - // Initialize video export state - // GPU resources and readback pipeline will be initialized lazily on first frame (needs device) - self.video_state = Some(VideoExportState { - current_frame: 0, - total_frames, - start_time, - end_time, - framerate, - width, - height, - frame_tx: Some(frame_tx), - gpu_resources: None, - readback_pipeline: None, - cpu_yuv_converter: None, - frames_in_flight: 0, - next_frame_to_encode: 0, - perf_metrics: Some(perf_metrics::ExportMetrics::new()), - }); + self.video_state = video_state; println!("🎬 [VIDEO EXPORT] Encoder thread spawned, ready for frames"); Ok(()) @@ -994,60 +1093,16 @@ impl ExportOrchestrator { let video_cancel_flag = Arc::clone(&self.cancel_flag); let audio_cancel_flag = Arc::clone(&self.cancel_flag); - // Try the zero-copy VAAPI path for H.264: render + hardware-encode each frame inline - // on its own device, writing the temp .mp4 directly (no readback / swscale / encoder - // thread). Falls back to the software encoder thread when unavailable. - let zero_copy = if matches!(video_settings.codec, lightningbeam_core::export::VideoCodec::H264) { - match gpu_video_encoder::encoder::ZeroCopyEncoder::new( - video_width, - video_height, - video_framerate.round() as i32, - video_settings.quality.bitrate_kbps(), - &temp_video_path, - ) { - Ok(encoder) => match vello::Renderer::new( - encoder.device(), - vello::RendererOptions { - use_cpu: false, - antialiasing_support: vello::AaSupport::all(), - num_init_threads: None, - pipeline_cache: None, - }, - ) { - Ok(renderer) => { - let gpu_resources = video_exporter::ExportGpuResources::new( - encoder.device(), - video_width, - video_height, - ); - let rgba = encoder.device().create_texture(&wgpu::TextureDescriptor { - label: Some("zerocopy_export_rgba"), - size: wgpu::Extent3d { width: video_width, height: video_height, depth_or_array_layers: 1 }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - println!("🎬 [PARALLEL EXPORT] zero-copy VAAPI H.264 enabled"); - Some(ZeroCopyVideo { encoder, renderer, gpu_resources, rgba }) - } - Err(e) => { - println!("🎬 [PARALLEL EXPORT] zero-copy renderer init failed ({e}); software path"); - None - } - }, - Err(e) => { - println!("🎬 [PARALLEL EXPORT] zero-copy unavailable ({e}); software path"); - None - } - } - } else { - None - }; + // Try the zero-copy VAAPI path for H.264 (renders + hardware-encodes on its own device, + // writing the temp .mp4 directly); falls back to the software encoder thread when the + // codec isn't H.264 or VAAPI is unavailable. + let zero_copy = Self::try_build_zero_copy( + &video_settings, + video_width, + video_height, + video_framerate, + &temp_video_path, + ); // Spawn the video thread: either the background zero-copy renderer/encoder (which owns a // document snapshot + its own device, decoupled from the UI's vsync loop) or the software diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index aa3ed74..70e7789 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -6107,7 +6107,14 @@ impl eframe::App for EditorApp { ExportResult::VideoOnly(settings, output_path) => { println!("🎬 [MAIN] Starting video-only export: {}", output_path.display()); - match orchestrator.start_video_export(settings, output_path) { + match orchestrator.start_video_export( + settings, + output_path, + self.action_executor.document(), + Arc::clone(&self.video_manager), + self.raster_store.clone(), + self.current_file_path.clone(), + ) { Ok(()) => true, Err(err) => { eprintln!("❌ Failed to start video export: {}", err); From 7ebd64391ce7716390d73abbe75393f5d5e15342 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 25 Jun 2026 21:27:06 -0400 Subject: [PATCH 2/3] editor: gate zero-copy VAAPI export to Linux (fix macOS/Windows build) gpu_video_encoder's encoder/vaapi/vk_device/dmabuf modules are #[cfg(linux)] (VAAPI is Linux-only), but the editor referenced them unconditionally, so macOS/Windows failed to compile (cannot find `encoder` in `gpu_video_encoder`). Gate the zero-copy path (ZeroCopyVideo, try_build_zero_copy, run_zerocopy_video_export, and the branches in start_video_export / start_video_with_audio_export) behind cfg(target_os = "linux"). Factor the software encoder-thread spawn into spawn_software_video so both the None arm and the non-Linux path share it without duplication. Non-Linux always uses the software path. Co-Authored-By: Claude Opus 4.8 --- .../lightningbeam-editor/src/export/mod.rs | 209 +++++++++--------- 1 file changed, 100 insertions(+), 109 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index d3cca41..62381bc 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -70,6 +70,9 @@ pub struct VideoExportState { /// Zero-copy VAAPI video production: renders each frame to RGBA and hardware-encodes it /// into a VAAPI surface, all on the encoder's own wgpu device (no readback / swscale). +/// VAAPI is Linux-only, so this and its machinery are `cfg`-gated; other platforms always +/// use the software encoder path. +#[cfg(target_os = "linux")] struct ZeroCopyVideo { encoder: gpu_video_encoder::encoder::ZeroCopyEncoder, renderer: vello::Renderer, @@ -833,11 +836,53 @@ impl ExportOrchestrator { } } + /// Spawn the software video-encoder thread and build its UI-driven export state. Used by every + /// non-zero-copy path (non-H.264, VAAPI unavailable, or non-Linux platforms). The caller drives + /// frames into it via `render_next_video_frame()`. + #[allow(clippy::too_many_arguments)] + fn spawn_software_video( + settings: VideoExportSettings, + output_path: PathBuf, + frame_rx: Receiver, + frame_tx: Sender, + progress_tx: Sender, + cancel_flag: Arc, + total_frames: usize, + start_time: f64, + end_time: f64, + framerate: f64, + width: u32, + height: u32, + ) -> (std::thread::JoinHandle<()>, VideoExportState) { + let handle = std::thread::spawn(move || { + Self::run_video_encoder(settings, output_path, frame_rx, progress_tx, cancel_flag, total_frames); + }); + // GPU resources + readback pipeline init lazily on the first frame (needs device). + let state = VideoExportState { + current_frame: 0, + total_frames, + start_time, + end_time, + framerate, + width, + height, + frame_tx: Some(frame_tx), + gpu_resources: None, + readback_pipeline: None, + cpu_yuv_converter: None, + frames_in_flight: 0, + next_frame_to_encode: 0, + perf_metrics: Some(perf_metrics::ExportMetrics::new()), + }; + (handle, state) + } + /// Build a zero-copy VAAPI H.264 encoder targeting `output_path` (with its own vello /// renderer + GPU resources + RGBA texture on the encoder's device), or `None` when the /// codec isn't H.264 or VAAPI / the GPU device is unavailable — in which case the caller /// falls back to the software encoder path. Used by both the video-only and video+audio - /// export entry points. + /// export entry points. VAAPI is Linux-only, so this whole path is `cfg`-gated. + #[cfg(target_os = "linux")] fn try_build_zero_copy( settings: &VideoExportSettings, width: u32, @@ -938,65 +983,39 @@ impl ExportOrchestrator { self.cancel_flag.store(false, Ordering::Relaxed); let cancel_flag = Arc::clone(&self.cancel_flag); - // Zero-copy VAAPI H.264 writes the final output directly (no audio → no mux), reporting - // through the single-export progress channel. Otherwise use the software encoder thread. - let zero_copy = Self::try_build_zero_copy(&settings, width, height, framerate, &output_path); - - let (handle, video_state) = match zero_copy { - Some(zc) => { + // Zero-copy VAAPI H.264 (Linux only) writes the final output directly on a background + // thread (no audio → no mux), reporting through the single-export progress channel. On + // success it returns here; otherwise we fall through to the software encoder thread. + #[cfg(target_os = "linux")] + { + if let Some(zc) = Self::try_build_zero_copy(&settings, width, height, framerate, &output_path) { drop(frame_rx); let document_snapshot = document.clone(); let mut image_cache = ImageCache::new(); image_cache.set_container_path(container_path); let handle = std::thread::spawn(move || { Self::run_zerocopy_video_export( - zc, - document_snapshot, - image_cache, - video_manager, - raster_store, - total_frames, - start_time, - framerate, - width, - height, - output_path, - progress_tx, - cancel_flag, + zc, document_snapshot, image_cache, video_manager, raster_store, + total_frames, start_time, framerate, width, height, + output_path, progress_tx, cancel_flag, ); }); - (handle, None) - } - None => { - let handle = std::thread::spawn(move || { - Self::run_video_encoder( - settings, - output_path, - frame_rx, - progress_tx, - cancel_flag, - total_frames, - ); - }); - // GPU resources + readback pipeline init lazily on the first frame (needs device). - let state = VideoExportState { - current_frame: 0, - total_frames, - start_time, - end_time, - framerate, - width, - height, - frame_tx: Some(frame_tx), - gpu_resources: None, - readback_pipeline: None, - cpu_yuv_converter: None, - frames_in_flight: 0, - next_frame_to_encode: 0, - perf_metrics: Some(perf_metrics::ExportMetrics::new()), - }; - (handle, Some(state)) + self.thread_handle = Some(handle); + self.video_state = None; + println!("🎬 [VIDEO EXPORT] zero-copy thread spawned"); + return Ok(()); } + } + // Zero-copy isn't used here; the scene-snapshot inputs are Linux-zero-copy-only. + #[cfg(not(target_os = "linux"))] + let _ = (document, &video_manager, &raster_store, &container_path); + + let (handle, video_state) = { + let (h, s) = Self::spawn_software_video( + settings, output_path, frame_rx, frame_tx, progress_tx, cancel_flag, + total_frames, start_time, end_time, framerate, width, height, + ); + (h, Some(s)) }; self.thread_handle = Some(handle); @@ -1093,21 +1112,14 @@ impl ExportOrchestrator { let video_cancel_flag = Arc::clone(&self.cancel_flag); let audio_cancel_flag = Arc::clone(&self.cancel_flag); - // Try the zero-copy VAAPI path for H.264 (renders + hardware-encodes on its own device, - // writing the temp .mp4 directly); falls back to the software encoder thread when the - // codec isn't H.264 or VAAPI is unavailable. - let zero_copy = Self::try_build_zero_copy( - &video_settings, - video_width, - video_height, - video_framerate, - &temp_video_path, - ); - - // Spawn the video thread: either the background zero-copy renderer/encoder (which owns a - // document snapshot + its own device, decoupled from the UI's vsync loop) or the software - // encoder thread fed by `render_next_video_frame` on the UI thread. - let (video_thread, video_state) = match zero_copy { + // Spawn the video thread: on Linux, try the background zero-copy renderer/encoder (its own + // device + a document snapshot, decoupled from the UI vsync loop, writing the temp .mp4 + // directly); otherwise (non-H.264, no VAAPI, or non-Linux) the software encoder thread fed + // by `render_next_video_frame` on the UI thread. + #[cfg(target_os = "linux")] + let (video_thread, video_state) = match Self::try_build_zero_copy( + &video_settings, video_width, video_height, video_framerate, &temp_video_path, + ) { Some(zc) => { drop(frame_rx); // the zero-copy path renders internally, no frame channel let document_snapshot = document.clone(); @@ -1118,56 +1130,34 @@ impl ExportOrchestrator { let temp_video_path = temp_video_path.clone(); let handle = std::thread::spawn(move || { Self::run_zerocopy_video_export( - zc, - document_snapshot, - image_cache, - video_manager, - raster_store, - total_frames, - video_start_time, - video_framerate, - video_width, - video_height, - temp_video_path, - video_progress_tx, - video_cancel_flag, + zc, document_snapshot, image_cache, video_manager, raster_store, + total_frames, video_start_time, video_framerate, video_width, video_height, + temp_video_path, video_progress_tx, video_cancel_flag, ); }); // No UI-thread video state: rendering happens entirely on the background thread. (Some(handle), None) } None => { - let video_settings_clone = video_settings.clone(); - let temp_video_path_clone = temp_video_path.clone(); - let handle = std::thread::spawn(move || { - Self::run_video_encoder( - video_settings_clone, - temp_video_path_clone, - frame_rx, - video_progress_tx, - video_cancel_flag, - total_frames, - ); - }); - let state = VideoExportState { - current_frame: 0, - total_frames, - start_time: video_start_time, - end_time: video_end_time, - framerate: video_framerate, - width: video_width, - height: video_height, - frame_tx: Some(frame_tx), - gpu_resources: None, - readback_pipeline: None, - cpu_yuv_converter: None, - frames_in_flight: 0, - next_frame_to_encode: 0, - perf_metrics: Some(perf_metrics::ExportMetrics::new()), - }; - (Some(handle), Some(state)) + let (h, s) = Self::spawn_software_video( + video_settings.clone(), temp_video_path.clone(), frame_rx, frame_tx, + video_progress_tx, video_cancel_flag, total_frames, + video_start_time, video_end_time, video_framerate, video_width, video_height, + ); + (Some(h), Some(s)) } }; + #[cfg(not(target_os = "linux"))] + let (video_thread, video_state) = { + // VAAPI/zero-copy is Linux-only; these scene-snapshot inputs are unused elsewhere. + let _ = (document, &video_manager, &raster_store, &container_path); + let (h, s) = Self::spawn_software_video( + video_settings.clone(), temp_video_path.clone(), frame_rx, frame_tx, + video_progress_tx, video_cancel_flag, total_frames, + video_start_time, video_end_time, video_framerate, video_width, video_height, + ); + (Some(h), Some(s)) + }; // Spawn audio export thread let temp_audio_path_clone = temp_audio_path.clone(); @@ -1410,7 +1400,8 @@ impl ExportOrchestrator { /// `.mp4`. Runs entirely off the UI thread (its own device + a `Document` snapshot), so it's /// not throttled by egui's vsync'd repaint loop. Reports progress through `progress_tx` /// (the same channel the software encoder thread uses); `poll_parallel_progress` muxes with - /// the audio track once both stream's `Complete` arrive. + /// the audio track once both stream's `Complete` arrive. VAAPI is Linux-only. + #[cfg(target_os = "linux")] #[allow(clippy::too_many_arguments)] fn run_zerocopy_video_export( mut zc: ZeroCopyVideo, From e7c239f2035ebaf03420be3805053b7608aa7728 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 25 Jun 2026 21:27:06 -0400 Subject: [PATCH 3/3] ci: enable VAAPI in the from-source ffmpeg build The libva guard fired: the release ffmpeg had no h264_vaapi. ffmpeg-sys-next configures with --disable-autodetect and only passes --enable-vaapi when its build-vaapi feature is set; ffmpeg-next 8.0 doesn't forward it, so installing libva-dev alone did nothing. Inject a direct ffmpeg-sys-next dependency with the build-vaapi feature (Linux only) in both the GitHub Actions and container build paths. With it, FFmpeg gets --enable-vaapi (and fails loudly if libva-dev is missing rather than silently shipping software-only). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build.yml | 13 +++++++++++++ packaging/container-build.sh | 3 +++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 644bf44..379155f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,6 +118,19 @@ jobs: run: | sed -i.bak 's/ffmpeg-next = { version = "8.0", features = \["static"\] }/ffmpeg-next = { version = "8.0", features = ["build", "static"] }/' lightningbeam-ui/lightningbeam-editor/Cargo.toml + # ffmpeg-sys-next configures FFmpeg with --disable-autodetect and only passes --enable-vaapi + # when its build-vaapi feature is set; ffmpeg-next 8.0 doesn't forward it, so depend on the + # sys crate directly. Linux only (the feature is a no-op elsewhere and we only ship VAAPI on + # Linux). With it set, a missing libva-dev makes FFmpeg configure fail loudly instead of + # silently shipping a software-only build. + - name: Enable VAAPI in FFmpeg build (Linux) + if: matrix.platform == 'ubuntu-24.04' + shell: bash + run: | + sed -i '/^ffmpeg-next = /a ffmpeg-sys-next = { version = "8.0", features = ["build-vaapi"] }' \ + lightningbeam-ui/lightningbeam-editor/Cargo.toml + grep -n 'ffmpeg-sys-next' lightningbeam-ui/lightningbeam-editor/Cargo.toml + - name: Setup FFmpeg (Windows) if: matrix.platform == 'windows-latest' shell: pwsh diff --git a/packaging/container-build.sh b/packaging/container-build.sh index e586056..5fae87a 100755 --- a/packaging/container-build.sh +++ b/packaging/container-build.sh @@ -67,6 +67,9 @@ cd /build/src/lightningbeam-ui # Build FFmpeg from source and link statically (no .so bundling needed) sed -i 's/ffmpeg-next = { version = \"8.0\", features = \[\"static\"\] }/ffmpeg-next = { version = \"8.0\", features = [\"build\", \"static\"] }/' lightningbeam-editor/Cargo.toml +# Enable VAAPI: ffmpeg-sys-next configures with --disable-autodetect and only passes +# --enable-vaapi when its build-vaapi feature is set (ffmpeg-next 8.0 doesn't forward it). +sed -i '/^ffmpeg-next = /a ffmpeg-sys-next = { version = "8.0", features = ["build-vaapi"] }' lightningbeam-editor/Cargo.toml export FFMPEG_STATIC=1 # Stage factory presets for packaging