diff --git a/crates/oakcodec/src/ffmpeg.rs b/crates/oakcodec/src/ffmpeg.rs index d30e2c8d9..12df9dc27 100644 --- a/crates/oakcodec/src/ffmpeg.rs +++ b/crates/oakcodec/src/ffmpeg.rs @@ -219,6 +219,20 @@ impl FFmpegDecoder { state: Mutex::new(None), } } + + /// The hardware device driving this session (`None` = software + /// decoding), as a display name (`videotoolbox` / `vaapi` / + /// `cuda/nvdec` / `d3d11va`). An observability hook for the + /// hardware-decode config and the first-frame fallback — tests + /// assert on it to prove the hardware path is really taken (and + /// really abandoned on fallback). + pub fn hw_decoder_name(&self) -> Option { + let state = self.state.lock().unwrap_or_else(|e| e.into_inner()); + state + .as_ref() + .and_then(|s| s.hw_device.map(crate::hwdecode::device_type_name)) + .map(|name| name.to_string()) + } } impl Default for FFmpegDecoder { @@ -305,8 +319,17 @@ impl Decoder for FFmpegDecoder { // the requested timestamp. // Note: the Rust `RetrieveVideoParams` carries no cancellation atom // (dropped from the C++ struct), so decoding is not cancellable here. - let f = state - .retrieve_frame(&p.time, p.time == crate::decoder::k_any_timecode(), None)? + let mut decoded = state.retrieve_frame(&p.time, p.time == crate::decoder::k_any_timecode(), None); + if decoded.is_err() && state.hw_device.is_some() { + // First-frame hardware fallback: the hardware decoder opened + // but cannot actually decode this stream (unsupported profile + // / driver issue at decode time). Reopen as software and retry + // once — this is the automatic fallback path; the config + // switch is the manual one. + state.reopen_software()?; + decoded = state.retrieve_frame(&p.time, p.time == crate::decoder::k_any_timecode(), None); + } + let f = decoded? .ok_or_else(|| fail("no video frame available at the requested time"))?; let (w, h, bytes) = state.scale_video_to_f32(f, p.force_range)?; @@ -410,8 +433,14 @@ enum PacketFeed { /// One opened (filename, stream) decode session. struct DecoderState { input: ffmpeg::format::context::Input, + /// The media file (kept for the hardware-decode fallback reopen). + filename: String, stream_index: usize, inner: DecoderInner, + /// The hardware device in use (`None` = software decoding). Kept so + /// a hardware surface can be detected and the first-frame fallback + /// can reopen the session as software. + hw_device: Option, stream_time_base: FfRational, stream_start_time: i64, /// Format start time in microseconds (`AV_TIME_BASE`). @@ -475,6 +504,12 @@ struct AudioResampler { impl DecoderState { /// Open `(filename, stream_index)` for decoding. fn open(stream: &CodecStream) -> crate::error::Result { + Self::open_impl(stream, true) + } + + /// Open with the hardware-decode preference explicitly on/off (the + /// off path is the hardware fallback's software reopen). + fn open_impl(stream: &CodecStream, allow_hw: bool) -> crate::error::Result { let mut dict = Dictionary::new(); dict.set("analyzeduration", "5000000"); dict.set("probesize", "20000000"); @@ -488,8 +523,32 @@ impl DecoderState { let params = fstream.parameters(); let medium = params.medium(); let codec_id = params.id(); - let codec = ffmpeg::decoder::find(codec_id) - .ok_or_else(|| fail(format!("no decoder for codec {codec_id:?}")))?; + + // Hardware decode is the mandated default: on video streams open + // the regular decoder with the platform's hardware device + // attached (the FFmpeg 8 hwaccel model: VideoToolbox / VA-API / + // NVDEC / D3D11VA, config-gated); pure software is the fallback + // when no hardware device is available. + let mut hw_device: Option = None; + let hw_opened = if allow_hw + && matches!(medium, MediaType::Video) + && crate::hwdecode::hardware_decoding_enabled() + { + ffmpeg::decoder::find(codec_id).and_then(|codec| { + crate::hwdecode::device_type_candidates() + .iter() + .find_map(|device_type| { + crate::hwdecode::open_hw_accel(¶ms, codec, *device_type).map( + |(opened, device_type)| { + hw_device = Some(device_type); + opened + }, + ) + }) + }) + } else { + None + }; // Per-medium stream parameters (mirroring the bridge stream info); // read before `params` is moved into the codec context below. @@ -503,13 +562,20 @@ impl DecoderState { input_channel_layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits(); } - let mut open_opts = Dictionary::new(); - open_opts.set("threads", "auto"); - let opened = ffmpeg::codec::Context::from_parameters(params) - .map_err(ffmpeg_err)? - .decoder() - .open_as_with(codec, open_opts) - .map_err(ffmpeg_err)?; + let opened = match hw_opened { + Some(opened) => opened, + None => { + let codec = ffmpeg::decoder::find(codec_id) + .ok_or_else(|| fail(format!("no decoder for codec {codec_id:?}")))?; + let mut open_opts = Dictionary::new(); + open_opts.set("threads", "auto"); + ffmpeg::codec::Context::from_parameters(params) + .map_err(ffmpeg_err)? + .decoder() + .open_as_with(codec, open_opts) + .map_err(ffmpeg_err)? + } + }; let inner = match medium { MediaType::Video => DecoderInner::Video(ffmpeg::codec::decoder::Video(opened)), @@ -548,8 +614,10 @@ impl DecoderState { Ok(DecoderState { input, + filename: stream.filename().to_string(), stream_index, inner, + hw_device, stream_time_base, stream_start_time, format_start_time, @@ -562,6 +630,16 @@ impl DecoderState { }) } + /// Reopen the session as a pure software decode (the hardware decoder + /// opened but cannot decode this stream). Swaps the state in place; + /// the retry re-seeks to the requested frame on the fresh session. + fn reopen_software(&mut self) -> crate::error::Result<()> { + let stream = CodecStream::with_block(self.filename.clone(), self.stream_index as i32, None); + let mut fresh = Self::open_impl(&stream, false)?; + std::mem::swap(self, &mut fresh); + Ok(()) + } + fn send_packet(&mut self, packet: &ffmpeg::packet::Packet) -> crate::error::Result<()> { match &mut self.inner { DecoderInner::Video(d) => d.send_packet(packet).map_err(ffmpeg_err), @@ -738,7 +816,22 @@ impl DecoderState { } let frame = match self.pull()? { - Pull::Frame(DecodedFrame::Video(f)) => f, + Pull::Frame(DecodedFrame::Video(f)) => { + // A hardware decoder yields hardware surfaces + // (AV_PIX_FMT_VIDEOTOOLBOX/VAAPI/CUDA/D3D11*): + // transfer to system memory so the cache and swscale + // only ever see CPU frames. + // SAFETY: plain read of the frame's format field. + let raw_format = unsafe { (*f.as_ptr()).format }; + if self.hw_device.is_some() + && crate::hwdecode::is_hw_format(unsafe { + std::mem::transmute::(raw_format) + }) { + crate::hwdecode::transfer_to_cpu(&f)? + } else { + f + } + } Pull::Frame(_) => unreachable!("video session yields only video frames"), Pull::Eof => { // Handle an "expected" EOF by using the last cached frame diff --git a/crates/oakcodec/src/hwdecode.rs b/crates/oakcodec/src/hwdecode.rs new file mode 100644 index 000000000..c7f5ec282 --- /dev/null +++ b/crates/oakcodec/src/hwdecode.rs @@ -0,0 +1,244 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Hardware video decoding (user-mandated default): the platform's +//! hardware acceleration is preferred over pure software decoding, with +//! a config switch and an automatic software fallback. +//! +//! **FFmpeg 8 removed the standalone hardware decoders** (`h264_videotoolbox`, +//! `h264_vaapi`, `h264_nvdec`, `h264_d3d11va` are all gone from its +//! configure): hardware decode now only exists as a *hwaccel* attached +//! to the software decoder. The model here is therefore uniform across +//! platforms: create the platform's hardware device context +//! (`av_hwdevice_ctx_create`), set it as `hw_device_ctx` on the codec +//! context of the regular decoder, and FFmpeg automatically engages the +//! matching hwaccel (`h264_videotoolbox_hwaccel` & co) on open. Codecs +//! without a matching hwaccel silently stay software — the pipeline +//! below only transfers frames whose pixel format is actually a +//! hardware surface. +//! +//! - **macOS**: `AV_HWDEVICE_TYPE_VIDEOTOOLBOX` +//! - **Linux**: `VAAPI`, then `CUDA` (NVDEC) +//! - **Windows**: `D3D11VA`, then `CUDA` (NVDEC) +//! +//! Device creation can fail on machines without the device/driver (a +//! headless Linux box, no NVIDIA GPU) — the candidate is skipped and +//! the next one (or the software decoder) is used. Hardware frames +//! (`AV_PIX_FMT_VIDEOTOOLBOX` / `VAAPI` / `CUDA` / `D3D11VA_VLD` / +//! `D3D11`) are transferred to system memory with +//! `av_hwframe_transfer_data` before the swscale conversion. + +use ffmpeg::ffi as sys; +use ffmpeg::Dictionary; +use ffmpeg_next as ffmpeg; + +use crate::error::{Error, Result}; + +/// Number of hardware frames transferred to system memory so far +/// (process-wide). An observability counter: a hardware decode that +/// never produces a hardware surface stays at zero, so tests can prove +/// the hwaccel really engaged rather than silently staying software. +pub static HW_TRANSFERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +/// The config key of the hardware-decode switch (1 = prefer hardware, +/// 0 = force software). Default ON by user mandate. +pub const CONFIG_KEY_HARDWARE_DECODING: &str = "HardwareDecoding"; + +/// Whether hardware decoding is preferred (the config switch). Default +/// ON by user mandate; only an explicit `"false"` turns it off (the +/// string accessor, same convention as the app's config helpers — the +/// store's typed `get_bool` only parses pre-typed Bool entries). +pub fn hardware_decoding_enabled() -> bool { + match oakcommon::configstore::ConfigStore::instance() + .get(None, CONFIG_KEY_HARDWARE_DECODING) + { + Ok(value) => value != "false", + Err(_) => true, + } +} + +/// The hardware device types to try, most preferred first. On a machine +/// without the device/driver the candidate fails creation and the next +/// one is tried; the software decoder is the final fallback. +pub fn device_type_candidates() -> &'static [sys::AVHWDeviceType] { + #[cfg(target_os = "macos")] + { + &[sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX] + } + #[cfg(target_os = "windows")] + { + &[ + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA, + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA, + ] + } + #[cfg(all(unix, not(target_os = "macos")))] + { + &[ + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA, + ] + } +} + +/// A display name for a device type (status reporting and tests). +pub fn device_type_name(device_type: sys::AVHWDeviceType) -> &'static str { + match device_type { + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX => "videotoolbox", + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI => "vaapi", + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA => "cuda/nvdec", + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA => "d3d11va", + _ => "unknown", + } +} + +/// Try to open the software codec with a hardware device context of +/// `device_type` attached — FFmpeg then engages the matching hwaccel +/// (e.g. `h264_videotoolbox_hwaccel`) on open. Returns the opened codec +/// context plus the device type in use, or `None` when the device is +/// unavailable (the caller tries the next candidate, then pure +/// software). +pub fn open_hw_accel( + params: &ffmpeg::codec::Parameters, + codec: ffmpeg::Codec, + device_type: sys::AVHWDeviceType, +) -> Option<(ffmpeg::codec::decoder::Opened, sys::AVHWDeviceType)> { + let mut context = ffmpeg::codec::Context::from_parameters(params.clone()).ok()?; + let mut device: *mut sys::AVBufferRef = std::ptr::null_mut(); + // SAFETY: `device` is a valid out-pointer; on success it owns the + // device reference, which is handed to the codec context below. + let rc = unsafe { + sys::av_hwdevice_ctx_create( + &mut device, + device_type, + std::ptr::null(), + std::ptr::null_mut(), + 0, + ) + }; + if rc < 0 || device.is_null() { + return None; + } + // SAFETY: `hw_device_ctx` takes ownership of the reference; the codec + // context frees it with the context. + unsafe { (*context.as_mut_ptr()).hw_device_ctx = device }; + let mut opts = Dictionary::new(); + opts.set("threads", "auto"); + context + .decoder() + .open_as_with(codec, opts) + .ok() + .map(|opened| (opened, device_type)) +} + +/// Whether a decoded frame's pixel format is a hardware surface that +/// must be transferred to system memory before swscale can consume it. +pub fn is_hw_format(format: sys::AVPixelFormat) -> bool { + matches!( + format, + sys::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX + | sys::AVPixelFormat::AV_PIX_FMT_VAAPI + | sys::AVPixelFormat::AV_PIX_FMT_CUDA + | sys::AVPixelFormat::AV_PIX_FMT_D3D11VA_VLD + | sys::AVPixelFormat::AV_PIX_FMT_D3D11 + ) +} + +/// Transfer a hardware frame to system memory (`av_hwframe_transfer_data` +/// picks the software format: NV12 for 8-bit, P010LE for 10-bit sources; +/// swscale consumes both). Presentation metadata the frame cache relies +/// on is carried over. +pub fn transfer_to_cpu(frame: &ffmpeg::frame::Video) -> Result { + let mut cpu = ffmpeg::frame::Video::empty(); + // SAFETY: `cpu` and `frame` are valid AVFrames; flags 0 = default + // transfer direction (hardware -> system memory). + let rc = unsafe { sys::av_hwframe_transfer_data(cpu.as_mut_ptr(), frame.as_ptr(), 0) }; + if rc < 0 { + return Err(Error::Failed(format!( + "hwframe transfer to CPU failed (av error {rc})" + ))); + } + HW_TRANSFERS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + cpu.set_pts(frame.pts()); + // SAFETY: plain field copies between valid AVFrames. + unsafe { + (*cpu.as_mut_ptr()).pkt_dts = (*frame.as_ptr()).pkt_dts; + (*cpu.as_mut_ptr()).duration = (*frame.as_ptr()).duration; + (*cpu.as_mut_ptr()).time_base = (*frame.as_ptr()).time_base; + } + Ok(cpu) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The current platform has at least one hardware device candidate + /// (hardware decode is the mandated default on every supported + /// platform). + #[test] + fn platform_has_a_hardware_candidate() { + assert!( + !device_type_candidates().is_empty(), + "no hardware decode candidate on this platform" + ); + } + + /// Device types round-trip through their display names. + #[test] + fn device_type_names_are_stable() { + assert_eq!( + device_type_name(sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX), + "videotoolbox" + ); + assert_eq!(device_type_name(sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI), "vaapi"); + assert_eq!(device_type_name(sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA), "cuda/nvdec"); + assert_eq!( + device_type_name(sys::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA), + "d3d11va" + ); + } + + /// Software and hardware pixel formats are classified correctly. + #[test] + fn hw_format_classification() { + assert!(is_hw_format(sys::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX)); + assert!(is_hw_format(sys::AVPixelFormat::AV_PIX_FMT_VAAPI)); + assert!(is_hw_format(sys::AVPixelFormat::AV_PIX_FMT_CUDA)); + assert!(is_hw_format(sys::AVPixelFormat::AV_PIX_FMT_D3D11VA_VLD)); + assert!(!is_hw_format(sys::AVPixelFormat::AV_PIX_FMT_YUV420P)); + assert!(!is_hw_format(sys::AVPixelFormat::AV_PIX_FMT_NV12)); + } + + /// On macOS the VideoToolbox device context must be creatable (the + /// mandated default decode path) and the H.264 demo must open with + /// the hwaccel attached. + #[cfg(target_os = "macos")] + #[test] + fn videotoolbox_device_opens_for_h264() { + let mut input = ffmpeg::format::input(&"../../tests/demo.mp4".to_string()) + .expect("open demo.mp4"); + let fstream = input.stream(0).expect("stream 0"); + let params = fstream.parameters(); + let codec = ffmpeg::decoder::find(params.id()).expect("software h264 codec"); + let opened = open_hw_accel( + ¶ms, + codec, + sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX, + ); + assert!(opened.is_some(), "VideoToolbox hwaccel must open for H.264"); + } +} diff --git a/crates/oakcodec/src/lib.rs b/crates/oakcodec/src/lib.rs index ac8548942..0bb3e4615 100644 --- a/crates/oakcodec/src/lib.rs +++ b/crates/oakcodec/src/lib.rs @@ -46,6 +46,7 @@ pub mod ffmpeg; pub mod footagedescription; pub mod frame; pub mod framemanager; +pub mod hwdecode; pub mod oiio; pub mod oiioframebridge; pub mod planarfiledevice; diff --git a/crates/oakcodec/src/realmedia_tests.rs b/crates/oakcodec/src/realmedia_tests.rs index 2c4c26e41..96838a76e 100644 --- a/crates/oakcodec/src/realmedia_tests.rs +++ b/crates/oakcodec/src/realmedia_tests.rs @@ -36,6 +36,7 @@ use crate::encoder::create_from_params; use crate::ffmpeg::FFmpegDecoder; use crate::frame::Frame; use oakcore_rs::{PixelFormat, Rational, TimeRange}; +use std::sync::Arc; /// `tests/demo.mp4` at the repository root. fn demo_path() -> std::path::PathBuf { @@ -286,3 +287,78 @@ fn testmedia_audio_track_encodes() { assert!(desc.audio_stream_count() >= 1, "audio stream present"); let _ = std::fs::remove_file(&out); } + +/// Serialize the config-toggling hardware-decode test (the config store +/// is process-global; decode tests must not observe each other's flag). +static HW_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +/// Hardware decoding (the mandated default): with the switch ON the +/// session must open the platform's hardware decoder (on macOS, +/// `h264_videotoolbox` for the H.264 demo); with the switch OFF it must +/// be pure software. Both paths must decode the same frame to matching +/// pixels (small tolerance for decoder rounding). +#[test] +fn hardware_decode_matches_software_decode() { + let _guard = HW_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let config = oakcommon::configstore::ConfigStore::instance(); + let key = crate::hwdecode::CONFIG_KEY_HARDWARE_DECODING; + + let decode_at = |time: i64| -> (Option, Arc) { + let d = FFmpegDecoder::new(); + let s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None); + d.open(&s).expect("open video stream"); + let hw = d.hw_decoder_name(); + let f = d + .retrieve_video_frame(&video_params(s, Rational::new(time, 1))) + .expect("decode frame"); + (hw, f) + }; + + // Hardware path. + config.set(None, key, "true"); + let (hw_name, hw_frame) = decode_at(5); + #[cfg(target_os = "macos")] + { + assert_eq!( + hw_name.as_deref(), + Some("videotoolbox"), + "macOS must decode H.264 through VideoToolbox by default" + ); + assert!( + crate::hwdecode::HW_TRANSFERS.load(std::sync::atomic::Ordering::Relaxed) > 0, + "the VideoToolbox hwaccel must really engage (a hardware surface was transferred)" + ); + } + #[cfg(not(target_os = "macos"))] + assert!( + hw_name.is_none() + || hw_name.as_deref().unwrap().contains("vaapi") + || hw_name.as_deref().unwrap().contains("nvdec") + || hw_name.as_deref().unwrap().contains("d3d11va"), + "unexpected decoder {hw_name:?}" + ); + + // Software path (the switch off). + config.set(None, key, "false"); + let (sw_name, sw_frame) = decode_at(5); + assert!(sw_name.is_none(), "switch off must force software decoding"); + config.set(None, key, "true"); + + // Same geometry, same pixels (within decoder rounding). + assert_eq!( + (hw_frame.width(), hw_frame.height()), + (sw_frame.width(), sw_frame.height()) + ); + let (hw_data, sw_data) = (hw_frame.data().unwrap(), sw_frame.data().unwrap()); + assert_eq!(hw_data.len(), sw_data.len()); + let mut max_diff = 0.0f32; + for (a, b) in hw_data.chunks_exact(4).zip(sw_data.chunks_exact(4)) { + let fa = f32::from_le_bytes(a.try_into().unwrap()); + let fb = f32::from_le_bytes(b.try_into().unwrap()); + max_diff = max_diff.max((fa - fb).abs()); + } + assert!( + max_diff < 0.05, + "hardware and software decodes diverge (max channel diff {max_diff})" + ); +} diff --git a/src/dialogs.rs b/src/dialogs.rs index fcc40a0bf..3c8dc0523 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -95,6 +95,7 @@ pub struct PreferencesContent { theme: Entity, cache_dir: Entity, use_proxy: Entity, + hw_decode: Entity, proxy_divider: Entity, snapshot_interval: Entity, transition_length: Entity, @@ -251,6 +252,30 @@ impl PreferencesContent { combo.set_selected(Some(divider_selected), cx) }); + // --- 渲染 Rendering: hardware decoding switch ------------------- + // Default ON (user mandate); off forces software decoding. + let hw_decode = cx.new(|cx| { + CheckBox::new( + 9, + if config_get_bool("HardwareDecoding", true) { + CheckState::Checked + } else { + CheckState::Unchecked + }, + window, + cx, + ) + .with_label(i18n::tr("preferences.hwdecode.enable")) + }); + cx.subscribe(&hw_decode, |_this, check, event: &CheckBoxEvent, cx| { + if let CheckBoxEvent::Toggled { state, .. } = event { + let enabled = *state == CheckState::Checked; + config_set_bool("HardwareDecoding", enabled); + check.update(cx, |check, cx| check.set_state(*state, cx)); + } + }) + .detach(); + // --- 项目 Project: snapshot interval + default transition ---------- let snapshot_interval = cx.new(|cx| { let current = @@ -342,6 +367,7 @@ impl PreferencesContent { theme, cache_dir, use_proxy, + hw_decode, proxy_divider, snapshot_interval, transition_length, @@ -499,6 +525,7 @@ impl Render for PreferencesContent { i18n::tr("preferences.backend").into(), self.backend.clone(), )) + .child(self.hw_decode.clone()) // 缓存 Cache .child(section_header(&colors, i18n::tr("preferences.section.cache").into())) .child(form_row( diff --git a/src/i18n.rs b/src/i18n.rs index a1755eb62..42bbd3f0d 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -434,6 +434,7 @@ const EN: &[(&str, &str)] = &[ ("preferences.section.project", "Project"), ("preferences.section.audio", "Audio"), ("preferences.backend", "Renderer backend"), + ("preferences.hwdecode.enable", "Hardware-accelerated video decoding (VideoToolbox / VA-API / NVDEC / D3D11VA)"), ("preferences.backend.placeholder", "Select a backend…"), ("preferences.language", "Language"), ("preferences.language.placeholder", "Select a language…"), @@ -899,6 +900,7 @@ const ZH: &[(&str, &str)] = &[ ("preferences.section.project", "项目"), ("preferences.section.audio", "音频"), ("preferences.backend", "渲染后端"), + ("preferences.hwdecode.enable", "硬件加速视频解码(VideoToolbox / VA-API / NVDEC / D3D11VA)"), ("preferences.backend.placeholder", "选择一个后端…"), ("preferences.language", "语言"), ("preferences.language.placeholder", "选择语言…"), diff --git a/tooling/ffmpeg/build-ffmpeg.sh b/tooling/ffmpeg/build-ffmpeg.sh index 42af7a929..3befa46a4 100755 --- a/tooling/ffmpeg/build-ffmpeg.sh +++ b/tooling/ffmpeg/build-ffmpeg.sh @@ -182,10 +182,10 @@ case "$OS" in esac # NVIDIA (ffnvcodec headers are distribution-free; enable when present). if [ -d /usr/local/cuda ] || pkg-config --exists ffnvcodec 2>/dev/null; then - FLAGS+=(--enable-nvenc --enable-cuda-llvm) - echo " + nvenc/cuda" + FLAGS+=(--enable-nvdec --enable-nvenc --enable-cuda-llvm) + echo " + nvdec/nvenc/cuda" else - echo " - nvenc/cuda (no ffnvcodec headers)" + echo " - nvdec/nvenc/cuda (no ffnvcodec headers)" fi # --- Build ------------------------------------------------------------------