client: prefer hardware H264 media paths
This commit is contained in:
parent
f9d23526c5
commit
c5a383d508
@ -195,6 +195,20 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
/// Vulkan H.264 hardware encode should stay live-call shaped when available.
|
||||||
|
fn vulkan_h264_encoder_options_keep_cbr_and_keyframes() {
|
||||||
|
temp_env::with_var("LESAVKA_CAM_H264_KBIT", Some("6000"), || {
|
||||||
|
let options = CameraCapture::encoder_options("vulkanh264enc", Some("idr-period"), 30);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
options,
|
||||||
|
"vulkanh264enc bitrate=6000 rate-control=cbr idr-period=30"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
/// HEVC should recover quickly after freshness drops without changing H.264 knobs.
|
/// HEVC should recover quickly after freshness drops without changing H.264 knobs.
|
||||||
|
|||||||
@ -75,7 +75,7 @@ impl CameraCapture {
|
|||||||
if output_hevc {
|
if output_hevc {
|
||||||
Self::choose_hevc_encoder()
|
Self::choose_hevc_encoder()
|
||||||
} else {
|
} else {
|
||||||
("x264enc", Some("key-int-max"))
|
Self::choose_encoder()
|
||||||
}
|
}
|
||||||
} else if output_hevc {
|
} else if output_hevc {
|
||||||
Self::choose_hevc_encoder()
|
Self::choose_hevc_encoder()
|
||||||
@ -84,7 +84,7 @@ impl CameraCapture {
|
|||||||
};
|
};
|
||||||
match source_profile {
|
match source_profile {
|
||||||
CameraSourceProfile::Mjpeg if !output_mjpeg => {
|
CameraSourceProfile::Mjpeg if !output_mjpeg => {
|
||||||
tracing::info!("📸 using MJPG source with software encode");
|
tracing::info!("📸 using MJPG source with transcoded output");
|
||||||
}
|
}
|
||||||
CameraSourceProfile::AutoDecode => {
|
CameraSourceProfile::AutoDecode => {
|
||||||
tracing::info!("📸 using auto-decoded webcam source (raw/MJPEG accepted)");
|
tracing::info!("📸 using auto-decoded webcam source (raw/MJPEG accepted)");
|
||||||
@ -121,6 +121,12 @@ impl CameraCapture {
|
|||||||
"videoconvert ! video/x-raw,format=I420,width={width},height={height},framerate={fps}/1 !"
|
"videoconvert ! video/x-raw,format=I420,width={width},height={height},framerate={fps}/1 !"
|
||||||
),
|
),
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
|
"vulkanh264enc" =>
|
||||||
|
format!(
|
||||||
|
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 ! \
|
||||||
|
vulkanupload ! video/x-raw(memory:VulkanImage),format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||||
|
),
|
||||||
|
#[cfg(not(coverage))]
|
||||||
"vaapih264enc" | "vah265enc" | "vaapih265enc" | "v4l2h265enc" =>
|
"vaapih264enc" | "vah265enc" | "vaapih265enc" | "v4l2h265enc" =>
|
||||||
format!(
|
format!(
|
||||||
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 !"
|
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||||
@ -140,7 +146,7 @@ impl CameraCapture {
|
|||||||
// tracing::debug!(%desc, "📸 pipeline-desc");
|
// tracing::debug!(%desc, "📸 pipeline-desc");
|
||||||
// Build a pipeline that works for any of the three encoders.
|
// Build a pipeline that works for any of the three encoders.
|
||||||
// * NVIDIA encoders prefer NV12, using NVMM when Jetson's converter is present.
|
// * NVIDIA encoders prefer NV12, using NVMM when Jetson's converter is present.
|
||||||
// * VAAPI/V4L2 hardware encoders also get explicit NV12 system-memory caps.
|
// * Vulkan/VAAPI/V4L2 hardware encoders also get explicit NV12 caps.
|
||||||
// * x264enc/x265enc keep their software-friendly raw caps.
|
// * x264enc/x265enc keep their software-friendly raw caps.
|
||||||
let preview_tap_path = camera_preview_tap_path();
|
let preview_tap_path = camera_preview_tap_path();
|
||||||
let preview_tap_branch = camera_preview_tap_branch(width, height, fps);
|
let preview_tap_branch = camera_preview_tap_branch(width, height, fps);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ impl CameraCapture {
|
|||||||
fn pick_encoder() -> (&'static str, &'static str) {
|
fn pick_encoder() -> (&'static str, &'static str) {
|
||||||
let encoders = &[
|
let encoders = &[
|
||||||
("nvh264enc", "video/x-raw(memory:NVMM),format=NV12"),
|
("nvh264enc", "video/x-raw(memory:NVMM),format=NV12"),
|
||||||
|
("vulkanh264enc", "video/x-raw(memory:VulkanImage),format=NV12"),
|
||||||
("vaapih264enc", "video/x-raw,format=NV12"),
|
("vaapih264enc", "video/x-raw,format=NV12"),
|
||||||
("v4l2h264enc", "video/x-raw"), // RPi, Jetson, etc.
|
("v4l2h264enc", "video/x-raw"), // RPi, Jetson, etc.
|
||||||
("x264enc", "video/x-raw"), // software
|
("x264enc", "video/x-raw"), // software
|
||||||
@ -33,6 +34,12 @@ impl CameraCapture {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if buildable_encoder("vulkanh264enc") {
|
||||||
|
return (
|
||||||
|
"vulkanh264enc",
|
||||||
|
supported_encoder_property("vulkanh264enc", &["idr-period"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
if buildable_encoder("vaapih264enc") {
|
if buildable_encoder("vaapih264enc") {
|
||||||
return (
|
return (
|
||||||
"vaapih264enc",
|
"vaapih264enc",
|
||||||
@ -56,6 +63,7 @@ impl CameraCapture {
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
{
|
{
|
||||||
Some("nvh264enc") => ("nvh264enc", None),
|
Some("nvh264enc") => ("nvh264enc", None),
|
||||||
|
Some("vulkanh264enc") => ("vulkanh264enc", Some("idr-period")),
|
||||||
Some("vaapih264enc") => ("vaapih264enc", Some("keyframe-period")),
|
Some("vaapih264enc") => ("vaapih264enc", Some("keyframe-period")),
|
||||||
Some("v4l2h264enc") => ("v4l2h264enc", Some("idrcount")),
|
Some("v4l2h264enc") => ("v4l2h264enc", Some("idrcount")),
|
||||||
_ => ("x264enc", Some("key-int-max")),
|
_ => ("x264enc", Some("key-int-max")),
|
||||||
@ -107,6 +115,12 @@ impl CameraCapture {
|
|||||||
format!(
|
format!(
|
||||||
"{enc} tune=zerolatency speed-preset=faster bitrate={bitrate_kbit}{keyframe_opt}"
|
"{enc} tune=zerolatency speed-preset=faster bitrate={bitrate_kbit}{keyframe_opt}"
|
||||||
)
|
)
|
||||||
|
} else if enc == "vulkanh264enc" {
|
||||||
|
let bitrate_kbit = env_u32("LESAVKA_CAM_H264_KBIT", 4500);
|
||||||
|
let keyframe_opt = kf_prop
|
||||||
|
.map(|property| format!(" {property}={keyframe_interval}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!("{enc} bitrate={bitrate_kbit} rate-control=cbr{keyframe_opt}")
|
||||||
} else if enc == "x265enc" {
|
} else if enc == "x265enc" {
|
||||||
let bitrate_kbit = env_u32("LESAVKA_CAM_HEVC_KBIT", 3000);
|
let bitrate_kbit = env_u32("LESAVKA_CAM_HEVC_KBIT", 3000);
|
||||||
let keyframe_opt = kf_prop
|
let keyframe_opt = kf_prop
|
||||||
|
|||||||
@ -269,6 +269,7 @@ fn decoder_label_is_hardware(label: &str) -> bool {
|
|||||||
let lower = label.to_ascii_lowercase();
|
let lower = label.to_ascii_lowercase();
|
||||||
lower.contains("nvh264dec")
|
lower.contains("nvh264dec")
|
||||||
|| lower.contains("nvdec")
|
|| lower.contains("nvdec")
|
||||||
|
|| lower.contains("vulkanh264dec")
|
||||||
|| lower.contains("vah264dec")
|
|| lower.contains("vah264dec")
|
||||||
|| lower.contains("vaapih264dec")
|
|| lower.contains("vaapih264dec")
|
||||||
|| lower.contains("v4l2slh264dec")
|
|| lower.contains("v4l2slh264dec")
|
||||||
|
|||||||
@ -14,7 +14,7 @@ impl PreviewFeed {
|
|||||||
let session_active_flag = Arc::clone(&session_active);
|
let session_active_flag = Arc::clone(&session_active);
|
||||||
let active_bindings_flag = Arc::clone(&active_bindings);
|
let active_bindings_flag = Arc::clone(&active_bindings);
|
||||||
let running_flag = Arc::clone(&running);
|
let running_flag = Arc::clone(&running);
|
||||||
std::thread::spawn(move || {
|
let worker = std::thread::spawn(move || {
|
||||||
if let Err(err) = run_preview_feed(
|
if let Err(err) = run_preview_feed(
|
||||||
server_addr,
|
server_addr,
|
||||||
monitor_id,
|
monitor_id,
|
||||||
@ -46,6 +46,7 @@ impl PreviewFeed {
|
|||||||
session_active,
|
session_active,
|
||||||
active_bindings,
|
active_bindings,
|
||||||
running,
|
running,
|
||||||
|
worker: Arc::new(Mutex::new(Some(worker))),
|
||||||
profile,
|
profile,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})
|
})
|
||||||
@ -61,6 +62,7 @@ impl PreviewFeed {
|
|||||||
session_active: Arc::new(AtomicBool::new(false)),
|
session_active: Arc::new(AtomicBool::new(false)),
|
||||||
active_bindings: Arc::new(AtomicUsize::new(0)),
|
active_bindings: Arc::new(AtomicUsize::new(0)),
|
||||||
running: Arc::new(AtomicBool::new(false)),
|
running: Arc::new(AtomicBool::new(false)),
|
||||||
|
worker: Arc::new(Mutex::new(None)),
|
||||||
profile,
|
profile,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
}
|
}
|
||||||
@ -95,6 +97,11 @@ impl PreviewFeed {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
if let Ok(mut worker) = self.worker.lock()
|
||||||
|
&& let Some(handle) = worker.take()
|
||||||
|
{
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_status(&self, status: impl Into<String>, clear_picture: bool) {
|
fn replace_status(&self, status: impl Into<String>, clear_picture: bool) {
|
||||||
@ -236,7 +243,7 @@ fn run_preview_feed(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
let sample_worker = {
|
||||||
let shared = Arc::clone(&shared);
|
let shared = Arc::clone(&shared);
|
||||||
let appsink = appsink.clone();
|
let appsink = appsink.clone();
|
||||||
let parser = parser.clone();
|
let parser = parser.clone();
|
||||||
@ -268,19 +275,20 @@ fn run_preview_feed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
};
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.context("building preview tokio runtime")?;
|
.context("building preview tokio runtime")?;
|
||||||
|
|
||||||
|
let running_for_loop = Arc::clone(&running);
|
||||||
let _ = rt.block_on(async move {
|
let _ = rt.block_on(async move {
|
||||||
let mut was_active = false;
|
let mut was_active = false;
|
||||||
let mut retry_delay = Duration::from_millis(750);
|
let mut retry_delay = Duration::from_millis(750);
|
||||||
loop {
|
loop {
|
||||||
if !running.load(Ordering::Relaxed) {
|
if !running_for_loop.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let active_now = session_active.load(Ordering::Relaxed)
|
let active_now = session_active.load(Ordering::Relaxed)
|
||||||
@ -391,7 +399,7 @@ fn run_preview_feed(
|
|||||||
);
|
);
|
||||||
loop {
|
loop {
|
||||||
if !session_active.load(Ordering::Relaxed)
|
if !session_active.load(Ordering::Relaxed)
|
||||||
|| !running.load(Ordering::Relaxed)
|
|| !running_for_loop.load(Ordering::Relaxed)
|
||||||
|| active_bindings.load(Ordering::Relaxed) == 0
|
|| active_bindings.load(Ordering::Relaxed) == 0
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@ -487,6 +495,8 @@ fn run_preview_feed(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let _ = pipeline.set_state(gst::State::Null);
|
let _ = pipeline.set_state(gst::State::Null);
|
||||||
|
running.store(false, Ordering::Relaxed);
|
||||||
|
let _ = sample_worker.join();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ struct PreviewFeed {
|
|||||||
session_active: Arc<AtomicBool>,
|
session_active: Arc<AtomicBool>,
|
||||||
active_bindings: Arc<AtomicUsize>,
|
active_bindings: Arc<AtomicUsize>,
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
|
worker: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
|
||||||
profile: PreviewProfile,
|
profile: PreviewProfile,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,11 +113,15 @@ fn sanitize_preview_request(
|
|||||||
requested_fps: u32,
|
requested_fps: u32,
|
||||||
max_bitrate_kbit: u32,
|
max_bitrate_kbit: u32,
|
||||||
) -> (i32, i32, u32, u32) {
|
) -> (i32, i32, u32, u32) {
|
||||||
|
let requested_width = requested_width.max(2);
|
||||||
|
let requested_height = requested_height.max(2);
|
||||||
|
let requested_fps = requested_fps.max(1);
|
||||||
|
let max_bitrate_kbit = max_bitrate_kbit.max(800);
|
||||||
(
|
(
|
||||||
requested_width.max(2),
|
requested_width.min(INLINE_PREVIEW_REQUEST_WIDTH),
|
||||||
requested_height.max(2),
|
requested_height.min(INLINE_PREVIEW_REQUEST_HEIGHT),
|
||||||
requested_fps.max(1),
|
requested_fps.min(INLINE_PREVIEW_REQUEST_FPS),
|
||||||
max_bitrate_kbit.max(800),
|
max_bitrate_kbit.min(INLINE_PREVIEW_MAX_KBIT),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use crate::video_support::pick_h264_decoder;
|
use crate::video_support::{h264_decoder_launch_fragment, pick_h264_decoder};
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
@ -34,13 +34,13 @@ const PREVIEW_WIDTH: i32 = 960;
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const PREVIEW_HEIGHT: i32 = 540;
|
const PREVIEW_HEIGHT: i32 = 540;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const INLINE_PREVIEW_REQUEST_WIDTH: i32 = DEFAULT_EYE_SOURCE_WIDTH;
|
const INLINE_PREVIEW_REQUEST_WIDTH: i32 = 1280;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const INLINE_PREVIEW_REQUEST_HEIGHT: i32 = DEFAULT_EYE_SOURCE_HEIGHT;
|
const INLINE_PREVIEW_REQUEST_HEIGHT: i32 = 720;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const INLINE_PREVIEW_REQUEST_FPS: u32 = DEFAULT_EYE_SOURCE_FPS;
|
const INLINE_PREVIEW_REQUEST_FPS: u32 = 30;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const INLINE_PREVIEW_MAX_KBIT: u32 = DEFAULT_EYE_SOURCE_MAX_KBIT;
|
const INLINE_PREVIEW_MAX_KBIT: u32 = 6_000;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
const DEFAULT_EYE_SOURCE_WIDTH: i32 = 1920;
|
const DEFAULT_EYE_SOURCE_WIDTH: i32 = 1920;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
|
|||||||
@ -184,13 +184,13 @@ fn build_preview_pipeline(
|
|||||||
profile.requested_width.max(2) as u32,
|
profile.requested_width.max(2) as u32,
|
||||||
profile.requested_height.max(2) as u32,
|
profile.requested_height.max(2) as u32,
|
||||||
);
|
);
|
||||||
|
let decoder_fragment = h264_decoder_launch_fragment(decoder_name);
|
||||||
let desc = format!(
|
let desc = format!(
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
||||||
queue max-size-buffers=6 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
queue max-size-buffers=6 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||||
h264parse name=preview_parse disable-passthrough=true ! {} name=decoder ! videoconvert ! \
|
h264parse name=preview_parse disable-passthrough=true ! {decoder_fragment} ! videoconvert ! \
|
||||||
video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
|
video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
|
||||||
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true",
|
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true",
|
||||||
decoder_name,
|
|
||||||
);
|
);
|
||||||
let pipeline = gst::parse::launch(&desc)?
|
let pipeline = gst::parse::launch(&desc)?
|
||||||
.downcast::<gst::Pipeline>()
|
.downcast::<gst::Pipeline>()
|
||||||
@ -234,6 +234,7 @@ fn preview_decoder_candidates() -> Vec<String> {
|
|||||||
for name in [
|
for name in [
|
||||||
"avdec_h264",
|
"avdec_h264",
|
||||||
"openh264dec",
|
"openh264dec",
|
||||||
|
"vulkanh264dec",
|
||||||
"vah264dec",
|
"vah264dec",
|
||||||
"vaapih264dec",
|
"vaapih264dec",
|
||||||
"v4l2h264dec",
|
"v4l2h264dec",
|
||||||
|
|||||||
@ -232,9 +232,9 @@ fn breakout_preview_profile_defaults_to_higher_quality() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preview_request_sanitizer_keeps_requested_source_geometry() {
|
fn preview_request_sanitizer_caps_docked_preview_budget() {
|
||||||
let adapted = sanitize_preview_request(1920, 1080, 60, 18_000);
|
let adapted = sanitize_preview_request(1920, 1080, 60, 18_000);
|
||||||
assert_eq!(adapted, (1920, 1080, 60, 18_000));
|
assert_eq!(adapted, (1280, 720, 30, 6_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -333,10 +333,10 @@ fn inline_preview_requests_selected_source_profile_on_wire() {
|
|||||||
if let Some(request) = requests.lock().unwrap().last().cloned() {
|
if let Some(request) = requests.lock().unwrap().last().cloned() {
|
||||||
assert_eq!(request.id, 1);
|
assert_eq!(request.id, 1);
|
||||||
assert_eq!(request.source_id, Some(1));
|
assert_eq!(request.source_id, Some(1));
|
||||||
assert_eq!(request.requested_width, 1920);
|
assert_eq!(request.requested_width, 1280);
|
||||||
assert_eq!(request.requested_height, 1080);
|
assert_eq!(request.requested_height, 720);
|
||||||
assert_eq!(request.requested_fps, 60);
|
assert_eq!(request.requested_fps, 30);
|
||||||
assert_eq!(request.max_bitrate, 18_000);
|
assert_eq!(request.max_bitrate, 6_000);
|
||||||
preview.shutdown_all();
|
preview.shutdown_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -385,10 +385,10 @@ fn inline_preview_requests_honest_source_profile_on_wire() {
|
|||||||
if let Some(request) = requests.lock().unwrap().last().cloned() {
|
if let Some(request) = requests.lock().unwrap().last().cloned() {
|
||||||
assert_eq!(request.id, 1);
|
assert_eq!(request.id, 1);
|
||||||
assert_eq!(request.source_id, Some(1));
|
assert_eq!(request.source_id, Some(1));
|
||||||
assert_eq!(request.requested_width, 1920);
|
assert_eq!(request.requested_width, 1280);
|
||||||
assert_eq!(request.requested_height, 1080);
|
assert_eq!(request.requested_height, 720);
|
||||||
assert_eq!(request.requested_fps, 60);
|
assert_eq!(request.requested_fps, 30);
|
||||||
assert_eq!(request.max_bitrate, 18_000);
|
assert_eq!(request.max_bitrate, 6_000);
|
||||||
preview.shutdown_all();
|
preview.shutdown_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -439,8 +439,8 @@ fn inline_preview_requests_native_720p_source_mode_on_wire() {
|
|||||||
assert_eq!(request.source_id, Some(1));
|
assert_eq!(request.source_id, Some(1));
|
||||||
assert_eq!(request.requested_width, 1280);
|
assert_eq!(request.requested_width, 1280);
|
||||||
assert_eq!(request.requested_height, 720);
|
assert_eq!(request.requested_height, 720);
|
||||||
assert_eq!(request.requested_fps, 60);
|
assert_eq!(request.requested_fps, 30);
|
||||||
assert_eq!(request.max_bitrate, 12_000);
|
assert_eq!(request.max_bitrate, 6_000);
|
||||||
preview.shutdown_all();
|
preview.shutdown_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -491,8 +491,8 @@ fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() {
|
|||||||
assert_eq!(request.source_id, Some(1));
|
assert_eq!(request.source_id, Some(1));
|
||||||
assert_eq!(request.requested_width, 1280);
|
assert_eq!(request.requested_width, 1280);
|
||||||
assert_eq!(request.requested_height, 720);
|
assert_eq!(request.requested_height, 720);
|
||||||
assert_eq!(request.requested_fps, 60);
|
assert_eq!(request.requested_fps, 30);
|
||||||
assert_eq!(request.max_bitrate, 12_000);
|
assert_eq!(request.max_bitrate, 6_000);
|
||||||
preview.shutdown_all();
|
preview.shutdown_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -531,8 +531,10 @@ fn preview_can_request_other_eye_as_a_distinct_stream() {
|
|||||||
if let Some(request) = requests.lock().unwrap().last().cloned() {
|
if let Some(request) = requests.lock().unwrap().last().cloned() {
|
||||||
assert_eq!(request.id, 0);
|
assert_eq!(request.id, 0);
|
||||||
assert_eq!(request.source_id, Some(1));
|
assert_eq!(request.source_id, Some(1));
|
||||||
assert_eq!(request.requested_width, 1920);
|
assert_eq!(request.requested_width, 1280);
|
||||||
assert_eq!(request.requested_height, 1080);
|
assert_eq!(request.requested_height, 720);
|
||||||
|
assert_eq!(request.requested_fps, 30);
|
||||||
|
assert_eq!(request.max_bitrate, 6_000);
|
||||||
preview.shutdown_all();
|
preview.shutdown_all();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,19 +32,19 @@ fn fresh_preview_bootstrap_is_overridden_by_launcher_state_profiles() {
|
|||||||
|
|
||||||
let bootstrap = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
|
let bootstrap = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
|
||||||
assert_eq!(bootstrap.0, 0);
|
assert_eq!(bootstrap.0, 0);
|
||||||
assert_eq!(bootstrap.3, 1920);
|
assert_eq!(bootstrap.3, 1280);
|
||||||
assert_eq!(bootstrap.4, 1080);
|
assert_eq!(bootstrap.4, 720);
|
||||||
assert_eq!(bootstrap.5, 60);
|
assert_eq!(bootstrap.5, 30);
|
||||||
assert_eq!(bootstrap.6, 18_000);
|
assert_eq!(bootstrap.6, 6_000);
|
||||||
|
|
||||||
apply_preview_profiles(&preview, &state);
|
apply_preview_profiles(&preview, &state);
|
||||||
|
|
||||||
let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
|
let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
|
||||||
assert_eq!(inline.0, 1);
|
assert_eq!(inline.0, 1);
|
||||||
assert_eq!(inline.3, 1920);
|
assert_eq!(inline.3, 1280);
|
||||||
assert_eq!(inline.4, 1080);
|
assert_eq!(inline.4, 720);
|
||||||
assert_eq!(inline.5, 60);
|
assert_eq!(inline.5, 30);
|
||||||
assert_eq!(inline.6, 18_000);
|
assert_eq!(inline.6, 6_000);
|
||||||
|
|
||||||
let window = preview.profile_for_test(1, PreviewSurface::Window).unwrap();
|
let window = preview.profile_for_test(1, PreviewSurface::Window).unwrap();
|
||||||
assert_eq!(window.0, 1);
|
assert_eq!(window.0, 1);
|
||||||
@ -57,7 +57,7 @@ fn fresh_preview_bootstrap_is_overridden_by_launcher_state_profiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn source_preview_profile_stays_honest_after_apply() {
|
fn source_preview_profile_caps_inline_after_apply() {
|
||||||
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
|
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
|
||||||
let mut state = LauncherState::default();
|
let mut state = LauncherState::default();
|
||||||
state.set_capture_size_preset(1, CaptureSizePreset::P1080);
|
state.set_capture_size_preset(1, CaptureSizePreset::P1080);
|
||||||
@ -66,10 +66,10 @@ fn source_preview_profile_stays_honest_after_apply() {
|
|||||||
|
|
||||||
let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
|
let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
|
||||||
assert_eq!(inline.0, 1);
|
assert_eq!(inline.0, 1);
|
||||||
assert_eq!(inline.3, 1920);
|
assert_eq!(inline.3, 1280);
|
||||||
assert_eq!(inline.4, 1080);
|
assert_eq!(inline.4, 720);
|
||||||
assert_eq!(inline.5, 60);
|
assert_eq!(inline.5, 30);
|
||||||
assert_eq!(inline.6, 18_000);
|
assert_eq!(inline.6, 6_000);
|
||||||
|
|
||||||
preview.shutdown_all();
|
preview.shutdown_all();
|
||||||
}
|
}
|
||||||
@ -105,8 +105,8 @@ fn mirrored_preview_profile_inherits_the_source_eye_mode() {
|
|||||||
assert_eq!(window.0, 1);
|
assert_eq!(window.0, 1);
|
||||||
assert_eq!(inline.3, 1280);
|
assert_eq!(inline.3, 1280);
|
||||||
assert_eq!(inline.4, 720);
|
assert_eq!(inline.4, 720);
|
||||||
assert_eq!(inline.5, 60);
|
assert_eq!(inline.5, 30);
|
||||||
assert_eq!(inline.6, 12_000);
|
assert_eq!(inline.6, 6_000);
|
||||||
assert_eq!(window.3, 1280);
|
assert_eq!(window.3, 1280);
|
||||||
assert_eq!(window.4, 720);
|
assert_eq!(window.4, 720);
|
||||||
assert_eq!(window.5, 60);
|
assert_eq!(window.5, 60);
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use tracing::{debug, error, info, warn};
|
|||||||
/// Inputs: optional `LESAVKA_H264_DECODER` override and
|
/// Inputs: optional `LESAVKA_H264_DECODER` override and
|
||||||
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`. Output: a decoder
|
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`. Output: a decoder
|
||||||
/// element name. Why: breakout windows should benefit from NVIDIA proprietary
|
/// element name. Why: breakout windows should benefit from NVIDIA proprietary
|
||||||
/// decode when it is present, while keeping VAAPI/V4L2 and CPU routes usable
|
/// decode when it is present, while keeping Vulkan/VAAPI/V4L2 and CPU routes usable
|
||||||
/// for open-source-driver machines and debugging.
|
/// for open-source-driver machines and debugging.
|
||||||
fn pick_h264_decoder() -> String {
|
fn pick_h264_decoder() -> String {
|
||||||
if let Ok(raw) = std::env::var("LESAVKA_H264_DECODER") {
|
if let Ok(raw) = std::env::var("LESAVKA_H264_DECODER") {
|
||||||
@ -46,6 +46,7 @@ fn h264_decoder_preference_order() -> Vec<&'static str> {
|
|||||||
const HARDWARE: &[&str] = &[
|
const HARDWARE: &[&str] = &[
|
||||||
"nvh264dec",
|
"nvh264dec",
|
||||||
"nvh264sldec",
|
"nvh264sldec",
|
||||||
|
"vulkanh264dec",
|
||||||
"vah264dec",
|
"vah264dec",
|
||||||
"vaapih264dec",
|
"vaapih264dec",
|
||||||
"v4l2h264dec",
|
"v4l2h264dec",
|
||||||
@ -89,6 +90,26 @@ fn buildable_decoder(name: &str) -> bool {
|
|||||||
gst::ElementFactory::find(name).is_some() && gst::ElementFactory::make(name).build().is_ok()
|
gst::ElementFactory::find(name).is_some() && gst::ElementFactory::make(name).build().is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the decoder stage selected by `pick_h264_decoder`.
|
||||||
|
///
|
||||||
|
/// Vulkan decode is the hardware route exposed by some proprietary NVIDIA
|
||||||
|
/// installs. It outputs Vulkan memory, so download before handing frames to the
|
||||||
|
/// existing sink path.
|
||||||
|
fn h264_decoder_launch_fragment(decoder_name: &str) -> String {
|
||||||
|
h264_decoder_launch_fragment_named(decoder_name, "decoder")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn h264_decoder_launch_fragment_named(decoder_name: &str, element_name: &str) -> String {
|
||||||
|
match decoder_name {
|
||||||
|
"vulkanh264dec" => concat!(
|
||||||
|
"vulkanh264dec name={element_name} discard-corrupted-frames=true ",
|
||||||
|
"automatic-request-sync-points=true ! vulkandownload"
|
||||||
|
)
|
||||||
|
.replace("{element_name}", element_name),
|
||||||
|
name => format!("{name} name={element_name}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MonitorWindow {
|
pub struct MonitorWindow {
|
||||||
_pipeline: gst::Pipeline,
|
_pipeline: gst::Pipeline,
|
||||||
src: gst_app::AppSrc,
|
src: gst_app::AppSrc,
|
||||||
@ -218,11 +239,12 @@ impl MonitorWindow {
|
|||||||
"glimagesink name=sink sync=false"
|
"glimagesink name=sink sync=false"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let decoder_fragment = h264_decoder_launch_fragment(&decoder_name);
|
||||||
let desc = format!(
|
let desc = format!(
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
||||||
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||||
capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! \
|
capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||||
h264parse disable-passthrough=true ! {decoder_name} name=decoder ! videoconvert ! {sink}"
|
h264parse disable-passthrough=true ! {decoder_fragment} ! videoconvert ! {sink}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
|
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
|
||||||
|
|||||||
@ -73,16 +73,18 @@ impl UnifiedMonitorWindow {
|
|||||||
"glimagesink name=sink sync=false"
|
"glimagesink name=sink sync=false"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let decoder_fragment0 = h264_decoder_launch_fragment_named(&decoder_name, "decoder0");
|
||||||
|
let decoder_fragment1 = h264_decoder_launch_fragment_named(&decoder_name, "decoder1");
|
||||||
let desc = format!(
|
let desc = format!(
|
||||||
"compositor name=mix background=black ! videoconvert ! {sink} \
|
"compositor name=mix background=black ! videoconvert ! {sink} \
|
||||||
appsrc name=src0 is-live=true format=time do-timestamp=true block=false ! \
|
appsrc name=src0 is-live=true format=time do-timestamp=true block=false ! \
|
||||||
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||||
capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! \
|
capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||||
h264parse disable-passthrough=true ! {decoder_name} name=decoder0 ! videoconvert ! videoscale ! mix. \
|
h264parse disable-passthrough=true ! {decoder_fragment0} ! videoconvert ! videoscale ! mix. \
|
||||||
appsrc name=src1 is-live=true format=time do-timestamp=true block=false ! \
|
appsrc name=src1 is-live=true format=time do-timestamp=true block=false ! \
|
||||||
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||||
capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! \
|
capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||||
h264parse disable-passthrough=true ! {decoder_name} name=decoder1 ! videoconvert ! videoscale ! mix."
|
h264parse disable-passthrough=true ! {decoder_fragment1} ! videoconvert ! videoscale ! mix."
|
||||||
);
|
);
|
||||||
|
|
||||||
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
|
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
|
||||||
|
|||||||
@ -220,18 +220,26 @@ fn build_encoded_pipeline(camera: CameraConfig) -> Result<gst::Pipeline> {
|
|||||||
/// Why: this probe should run on different developer hosts without hardcoding a
|
/// Why: this probe should run on different developer hosts without hardcoding a
|
||||||
/// single hardware encoder, while still preferring low-latency behavior.
|
/// single hardware encoder, while still preferring low-latency behavior.
|
||||||
fn pick_h264_encoder(fps: u32) -> Result<String> {
|
fn pick_h264_encoder(fps: u32) -> Result<String> {
|
||||||
|
let fps = fps.max(1);
|
||||||
|
if gst::ElementFactory::find("vulkanh264enc").is_some() {
|
||||||
|
return Ok(format!(
|
||||||
|
"video/x-raw,format=NV12 ! vulkanupload ! \
|
||||||
|
video/x-raw(memory:VulkanImage),format=NV12 ! \
|
||||||
|
vulkanh264enc bitrate=2500 rate-control=cbr idr-period={fps}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if gst::ElementFactory::find("v4l2h264enc").is_some() {
|
||||||
|
return Ok("video/x-raw,format=NV12 ! v4l2h264enc".to_string());
|
||||||
|
}
|
||||||
if gst::ElementFactory::find("x264enc").is_some() {
|
if gst::ElementFactory::find("x264enc").is_some() {
|
||||||
return Ok(format!(
|
return Ok(format!(
|
||||||
"x264enc tune=zerolatency speed-preset=ultrafast bitrate=2500 key-int-max={}",
|
"x264enc tune=zerolatency speed-preset=ultrafast bitrate=2500 key-int-max={}",
|
||||||
fps.max(1)
|
fps
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if gst::ElementFactory::find("openh264enc").is_some() {
|
if gst::ElementFactory::find("openh264enc").is_some() {
|
||||||
return Ok("openh264enc bitrate=2500000".to_string());
|
return Ok("openh264enc bitrate=2500000".to_string());
|
||||||
}
|
}
|
||||||
if gst::ElementFactory::find("v4l2h264enc").is_some() {
|
|
||||||
return Ok("v4l2h264enc".to_string());
|
|
||||||
}
|
|
||||||
bail!("no usable H.264 encoder found for sync probe")
|
bail!("no usable H.264 encoder found for sync probe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use gstreamer as gst;
|
|||||||
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`.
|
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`.
|
||||||
/// Outputs: the chosen decoder element name, or `decodebin` as a last-resort
|
/// Outputs: the chosen decoder element name, or `decodebin` as a last-resort
|
||||||
/// fallback when no explicit decoder is present.
|
/// fallback when no explicit decoder is present.
|
||||||
/// Why: Lesavka should use GPU decode on NVIDIA/VAAPI/V4L2-capable clients
|
/// Why: Lesavka should use GPU decode on NVIDIA/Vulkan/VAAPI/V4L2-capable clients
|
||||||
/// when possible, while keeping an explicit CPU route for open-source driver
|
/// when possible, while keeping an explicit CPU route for open-source driver
|
||||||
/// comparisons and driver debugging.
|
/// comparisons and driver debugging.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@ -37,12 +37,13 @@ pub fn pick_h264_decoder() -> String {
|
|||||||
///
|
///
|
||||||
/// Inputs: `LESAVKA_H264_DECODER_PREFERENCE`, if set. Output: ordered decoder
|
/// Inputs: `LESAVKA_H264_DECODER_PREFERENCE`, if set. Output: ordered decoder
|
||||||
/// element names. Why: tests and diagnostics need to prove both proprietary
|
/// element names. Why: tests and diagnostics need to prove both proprietary
|
||||||
/// NVIDIA and open-source VAAPI/V4L2 routes stay available before CPU fallback.
|
/// NVIDIA, Vulkan, and VAAPI/V4L2 routes stay available before CPU fallback.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn h264_decoder_preference_order() -> Vec<&'static str> {
|
pub fn h264_decoder_preference_order() -> Vec<&'static str> {
|
||||||
const HARDWARE: &[&str] = &[
|
const HARDWARE: &[&str] = &[
|
||||||
"nvh264dec",
|
"nvh264dec",
|
||||||
"nvh264sldec",
|
"nvh264sldec",
|
||||||
|
"vulkanh264dec",
|
||||||
"vah264dec",
|
"vah264dec",
|
||||||
"vaapih264dec",
|
"vaapih264dec",
|
||||||
"v4l2h264dec",
|
"v4l2h264dec",
|
||||||
@ -71,6 +72,36 @@ pub fn h264_decoder_preference_order() -> Vec<&'static str> {
|
|||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a parse-launch fragment for the selected H.264 decoder.
|
||||||
|
///
|
||||||
|
/// Inputs: decoder element name. Output: a pipeline fragment with a stable
|
||||||
|
/// `decoder` element name. Why: Vulkan decoders output GPU memory, so they need
|
||||||
|
/// an explicit download step before the existing CPU-side sinks can consume
|
||||||
|
/// frames; keeping that in one helper prevents hardware decode from being
|
||||||
|
/// selected and then immediately failing link negotiation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn h264_decoder_launch_fragment(decoder_name: &str) -> String {
|
||||||
|
h264_decoder_launch_fragment_named(decoder_name, "decoder")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a parse-launch fragment for the selected H.264 decoder with a caller-owned element name.
|
||||||
|
///
|
||||||
|
/// Inputs: decoder element name plus the element name to put in the pipeline.
|
||||||
|
/// Output: a pipeline fragment. Why: unified downstream rendering needs two
|
||||||
|
/// independent decoder elements, while Vulkan still needs an explicit
|
||||||
|
/// download-to-system-memory stage after each decoder.
|
||||||
|
#[must_use]
|
||||||
|
pub fn h264_decoder_launch_fragment_named(decoder_name: &str, element_name: &str) -> String {
|
||||||
|
match decoder_name {
|
||||||
|
"vulkanh264dec" => concat!(
|
||||||
|
"vulkanh264dec name={element_name} discard-corrupted-frames=true ",
|
||||||
|
"automatic-request-sync-points=true ! vulkandownload"
|
||||||
|
)
|
||||||
|
.replace("{element_name}", element_name),
|
||||||
|
name => format!("{name} name={element_name}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn buildable_decoder(name: &str) -> bool {
|
fn buildable_decoder(name: &str) -> bool {
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
if std::env::var("TEST_FAIL_GST_INIT").is_ok() {
|
if std::env::var("TEST_FAIL_GST_INIT").is_ok() {
|
||||||
|
|||||||
@ -150,6 +150,7 @@ report_client_media_acceleration() {
|
|||||||
log "1e. Inspecting client media acceleration routes"
|
log "1e. Inspecting client media acceleration routes"
|
||||||
|
|
||||||
local hevc_encoder=""
|
local hevc_encoder=""
|
||||||
|
local h264_encoder=""
|
||||||
local h264_decoder=""
|
local h264_decoder=""
|
||||||
local opus_encoder=""
|
local opus_encoder=""
|
||||||
local opus_decoder=""
|
local opus_decoder=""
|
||||||
@ -163,9 +164,16 @@ report_client_media_acceleration() {
|
|||||||
vaapih265enc \
|
vaapih265enc \
|
||||||
v4l2h265enc \
|
v4l2h265enc \
|
||||||
x265enc || true)
|
x265enc || true)
|
||||||
|
h264_encoder=$(first_available_gst_element \
|
||||||
|
nvh264enc \
|
||||||
|
vulkanh264enc \
|
||||||
|
vaapih264enc \
|
||||||
|
v4l2h264enc \
|
||||||
|
x264enc || true)
|
||||||
h264_decoder=$(first_available_gst_element \
|
h264_decoder=$(first_available_gst_element \
|
||||||
nvh264dec \
|
nvh264dec \
|
||||||
nvh264sldec \
|
nvh264sldec \
|
||||||
|
vulkanh264dec \
|
||||||
vah264dec \
|
vah264dec \
|
||||||
vaapih264dec \
|
vaapih264dec \
|
||||||
v4l2h264dec \
|
v4l2h264dec \
|
||||||
@ -181,6 +189,11 @@ report_client_media_acceleration() {
|
|||||||
proprietary_bits+=("$element")
|
proprietary_bits+=("$element")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
for element in vulkanh264enc vulkanh264dec vulkanh265dec; do
|
||||||
|
if gst_element_available "$element"; then
|
||||||
|
opensource_bits+=("$element")
|
||||||
|
fi
|
||||||
|
done
|
||||||
for element in vah265enc vaapih265enc v4l2h265enc vah264dec vaapih264dec v4l2h264dec v4l2slh264dec; do
|
for element in vah265enc vaapih265enc v4l2h265enc vah264dec vaapih264dec v4l2h264dec v4l2slh264dec; do
|
||||||
if gst_element_available "$element"; then
|
if gst_element_available "$element"; then
|
||||||
opensource_bits+=("$element")
|
opensource_bits+=("$element")
|
||||||
@ -189,6 +202,12 @@ report_client_media_acceleration() {
|
|||||||
|
|
||||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||||
echo " ↪ nvidia-smi is available; proprietary NVIDIA driver tooling is present"
|
echo " ↪ nvidia-smi is available; proprietary NVIDIA driver tooling is present"
|
||||||
|
if [[ -z $hevc_encoder ]] || [[ $hevc_encoder == x265enc ]]; then
|
||||||
|
if gst-inspect-1.0 nvcodec 2>&1 | grep -q 'Unable to initialize CUDA library'; then
|
||||||
|
echo "⚠️ NVIDIA nvcodec is installed but CUDA initialization failed; NVENC HEVC is unavailable to GStreamer."
|
||||||
|
echo " Vulkan H.264 hardware encode/decode can still be used when the relay profile is H.264."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo " ↪ nvidia-smi is not available; NVIDIA proprietary tooling was not detected"
|
echo " ↪ nvidia-smi is not available; NVIDIA proprietary tooling was not detected"
|
||||||
fi
|
fi
|
||||||
@ -199,9 +218,9 @@ report_client_media_acceleration() {
|
|||||||
echo " ↪ proprietary NVIDIA GStreamer route: not exposed"
|
echo " ↪ proprietary NVIDIA GStreamer route: not exposed"
|
||||||
fi
|
fi
|
||||||
if [[ ${#opensource_bits[@]} -gt 0 ]]; then
|
if [[ ${#opensource_bits[@]} -gt 0 ]]; then
|
||||||
echo " ↪ open-source VAAPI/V4L2 GStreamer route: ${opensource_bits[*]}"
|
echo " ↪ Vulkan/VAAPI/V4L2 GStreamer route: ${opensource_bits[*]}"
|
||||||
else
|
else
|
||||||
echo " ↪ open-source VAAPI/V4L2 GStreamer route: not exposed"
|
echo " ↪ Vulkan/VAAPI/V4L2 GStreamer route: not exposed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $hevc_encoder ]]; then
|
if [[ -n $hevc_encoder ]]; then
|
||||||
@ -209,6 +228,11 @@ report_client_media_acceleration() {
|
|||||||
else
|
else
|
||||||
echo "⚠️ no HEVC encoder was detected; upstream HEVC will need NVIDIA/VAAPI/V4L2 or x265enc"
|
echo "⚠️ no HEVC encoder was detected; upstream HEVC will need NVIDIA/VAAPI/V4L2 or x265enc"
|
||||||
fi
|
fi
|
||||||
|
if [[ -n $h264_encoder ]]; then
|
||||||
|
echo " ↪ upstream H.264 encoder candidate: $h264_encoder"
|
||||||
|
else
|
||||||
|
echo "⚠️ no H.264 encoder was detected; hardware H.264 uplink will need NVIDIA/Vulkan/VAAPI/V4L2 or x264enc"
|
||||||
|
fi
|
||||||
if [[ -n $h264_decoder ]]; then
|
if [[ -n $h264_decoder ]]; then
|
||||||
echo " ↪ downstream H.264 decoder candidate: $h264_decoder"
|
echo " ↪ downstream H.264 decoder candidate: $h264_decoder"
|
||||||
else
|
else
|
||||||
|
|||||||
@ -45,6 +45,7 @@ fn decoder_auto_order_supports_proprietary_and_open_source_routes() {
|
|||||||
let order = video_support::h264_decoder_preference_order();
|
let order = video_support::h264_decoder_preference_order();
|
||||||
assert_eq!(order.first(), Some(&"nvh264dec"));
|
assert_eq!(order.first(), Some(&"nvh264dec"));
|
||||||
assert!(order.contains(&"nvh264sldec"));
|
assert!(order.contains(&"nvh264sldec"));
|
||||||
|
assert!(order.contains(&"vulkanh264dec"));
|
||||||
assert!(order.contains(&"vah264dec"));
|
assert!(order.contains(&"vah264dec"));
|
||||||
assert!(order.contains(&"vaapih264dec"));
|
assert!(order.contains(&"vaapih264dec"));
|
||||||
assert!(order.contains(&"v4l2h264dec"));
|
assert!(order.contains(&"v4l2h264dec"));
|
||||||
@ -60,6 +61,19 @@ fn decoder_auto_order_supports_proprietary_and_open_source_routes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vulkan_decoder_fragment_downloads_gpu_memory_before_cpu_sinks() {
|
||||||
|
let fragment = video_support::h264_decoder_launch_fragment("vulkanh264dec");
|
||||||
|
assert!(fragment.contains("vulkanh264dec name=decoder"));
|
||||||
|
assert!(fragment.contains("discard-corrupted-frames=true"));
|
||||||
|
assert!(fragment.contains("automatic-request-sync-points=true"));
|
||||||
|
assert!(fragment.contains("vulkandownload"));
|
||||||
|
|
||||||
|
let named = video_support::h264_decoder_launch_fragment_named("vulkanh264dec", "decoder1");
|
||||||
|
assert!(named.contains("vulkanh264dec name=decoder1"));
|
||||||
|
assert!(named.contains("vulkandownload"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn decoder_auto_order_can_prefer_software_for_driver_comparisons() {
|
fn decoder_auto_order_can_prefer_software_for_driver_comparisons() {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ fn client_decoder_matrix_prefers_known_h264_decoders_with_decodebin_fallback() {
|
|||||||
"openh264dec",
|
"openh264dec",
|
||||||
"nvh264dec",
|
"nvh264dec",
|
||||||
"nvh264sldec",
|
"nvh264sldec",
|
||||||
|
"vulkanh264dec",
|
||||||
"vah264dec",
|
"vah264dec",
|
||||||
"vaapih264dec",
|
"vaapih264dec",
|
||||||
"v4l2h264dec",
|
"v4l2h264dec",
|
||||||
|
|||||||
@ -98,7 +98,7 @@ mod camera_include_contract {
|
|||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(
|
||||||
enc,
|
enc,
|
||||||
"nvh264enc" | "vaapih264enc" | "v4l2h264enc" | "x264enc"
|
"nvh264enc" | "vulkanh264enc" | "vaapih264enc" | "v4l2h264enc" | "x264enc"
|
||||||
),
|
),
|
||||||
"unexpected encoder: {enc}"
|
"unexpected encoder: {enc}"
|
||||||
);
|
);
|
||||||
@ -106,7 +106,7 @@ mod camera_include_contract {
|
|||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(
|
||||||
enc,
|
enc,
|
||||||
"nvh264enc" | "vaapih264enc" | "v4l2h264enc" | "x264enc"
|
"nvh264enc" | "vulkanh264enc" | "vaapih264enc" | "v4l2h264enc" | "x264enc"
|
||||||
),
|
),
|
||||||
"unexpected encoder: {enc}"
|
"unexpected encoder: {enc}"
|
||||||
);
|
);
|
||||||
@ -125,8 +125,10 @@ mod camera_include_contract {
|
|||||||
for expected in [
|
for expected in [
|
||||||
"\"nvh264enc\" | \"nvh265enc\" if have_nvvidconv",
|
"\"nvh264enc\" | \"nvh265enc\" if have_nvvidconv",
|
||||||
"\"nvh264enc\" | \"nvh265enc\" /* else */",
|
"\"nvh264enc\" | \"nvh265enc\" /* else */",
|
||||||
|
"\"vulkanh264enc\"",
|
||||||
"\"vaapih264enc\" | \"vah265enc\" | \"vaapih265enc\" | \"v4l2h265enc\"",
|
"\"vaapih264enc\" | \"vah265enc\" | \"vaapih265enc\" | \"v4l2h265enc\"",
|
||||||
"video/x-raw(memory:NVMM),format=NV12",
|
"video/x-raw(memory:NVMM),format=NV12",
|
||||||
|
"video/x-raw(memory:VulkanImage),format=NV12",
|
||||||
"video/x-raw,format=NV12",
|
"video/x-raw,format=NV12",
|
||||||
"video/x-raw,format=I420",
|
"video/x-raw,format=I420",
|
||||||
] {
|
] {
|
||||||
@ -467,6 +469,10 @@ mod camera_include_contract {
|
|||||||
CameraCapture::encoder_options("nvh264enc", None, 30),
|
CameraCapture::encoder_options("nvh264enc", None, 30),
|
||||||
"nvh264enc"
|
"nvh264enc"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
CameraCapture::encoder_options("vulkanh264enc", Some("idr-period"), 30),
|
||||||
|
"vulkanh264enc bitrate=4500 rate-control=cbr idr-period=30"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -135,6 +135,7 @@ mod video_include_contract {
|
|||||||
let order = h264_decoder_preference_order();
|
let order = h264_decoder_preference_order();
|
||||||
assert_eq!(order.first(), Some(&"nvh264dec"));
|
assert_eq!(order.first(), Some(&"nvh264dec"));
|
||||||
assert!(order.contains(&"nvh264sldec"));
|
assert!(order.contains(&"nvh264sldec"));
|
||||||
|
assert!(order.contains(&"vulkanh264dec"));
|
||||||
assert!(order.contains(&"vah264dec"));
|
assert!(order.contains(&"vah264dec"));
|
||||||
assert!(order.contains(&"vaapih264dec"));
|
assert!(order.contains(&"vaapih264dec"));
|
||||||
assert!(order.contains(&"v4l2h264dec"));
|
assert!(order.contains(&"v4l2h264dec"));
|
||||||
@ -161,6 +162,19 @@ mod video_include_contract {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vulkan_decoder_fragment_names_each_decoder_and_downloads_frames() {
|
||||||
|
let fragment = h264_decoder_launch_fragment("vulkanh264dec");
|
||||||
|
assert!(fragment.contains("vulkanh264dec name=decoder"));
|
||||||
|
assert!(fragment.contains("discard-corrupted-frames=true"));
|
||||||
|
assert!(fragment.contains("automatic-request-sync-points=true"));
|
||||||
|
assert!(fragment.contains("vulkandownload"));
|
||||||
|
|
||||||
|
let named = h264_decoder_launch_fragment_named("vulkanh264dec", "decoder1");
|
||||||
|
assert!(named.contains("vulkanh264dec name=decoder1"));
|
||||||
|
assert!(named.contains("vulkandownload"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn monitor_window_new_covers_x11_backend_path() {
|
fn monitor_window_new_covers_x11_backend_path() {
|
||||||
|
|||||||
@ -86,11 +86,17 @@ fn client_install_reports_nvidia_and_open_source_media_routes() {
|
|||||||
"gst_element_available",
|
"gst_element_available",
|
||||||
"first_available_gst_element",
|
"first_available_gst_element",
|
||||||
"nvidia-smi is available",
|
"nvidia-smi is available",
|
||||||
|
"NVIDIA nvcodec is installed but CUDA initialization failed",
|
||||||
|
"relay profile is H.264",
|
||||||
"proprietary NVIDIA GStreamer route",
|
"proprietary NVIDIA GStreamer route",
|
||||||
"open-source VAAPI/V4L2 GStreamer route",
|
"Vulkan/VAAPI/V4L2 GStreamer route",
|
||||||
"nvh265enc",
|
"nvh265enc",
|
||||||
|
"nvh264enc",
|
||||||
"nvh264dec",
|
"nvh264dec",
|
||||||
"nvh264sldec",
|
"nvh264sldec",
|
||||||
|
"vulkanh264enc",
|
||||||
|
"vulkanh264dec",
|
||||||
|
"vulkanh265dec",
|
||||||
"vah265enc",
|
"vah265enc",
|
||||||
"vaapih265enc",
|
"vaapih265enc",
|
||||||
"v4l2h265enc",
|
"v4l2h265enc",
|
||||||
@ -99,6 +105,8 @@ fn client_install_reports_nvidia_and_open_source_media_routes() {
|
|||||||
"v4l2h264dec",
|
"v4l2h264dec",
|
||||||
"v4l2slh264dec",
|
"v4l2slh264dec",
|
||||||
"x265enc",
|
"x265enc",
|
||||||
|
"x264enc",
|
||||||
|
"upstream H.264 encoder candidate",
|
||||||
"avdec_h264",
|
"avdec_h264",
|
||||||
"openh264dec",
|
"openh264dec",
|
||||||
"opusenc",
|
"opusenc",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user