fix(server): scale hdmi output to adapter mode
This commit is contained in:
parent
5e6935121a
commit
b322396739
@ -465,6 +465,15 @@ pub struct Voice {
|
||||
tap: ClipTap,
|
||||
}
|
||||
|
||||
fn voice_input_caps() -> gst::Caps {
|
||||
gst::Caps::builder("audio/mpeg")
|
||||
.field("mpegversion", 4i32)
|
||||
.field("stream-format", "adts")
|
||||
.field("rate", 48_000i32)
|
||||
.field("channels", 2i32)
|
||||
.build()
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
#[cfg(coverage)]
|
||||
pub async fn new(_alsa_dev: &str) -> anyhow::Result<Self> {
|
||||
@ -478,6 +487,7 @@ impl Voice {
|
||||
.expect("appsrc");
|
||||
appsrc.set_format(gst::Format::Time);
|
||||
appsrc.set_is_live(true);
|
||||
appsrc.set_caps(Some(&voice_input_caps()));
|
||||
|
||||
let sink = gst::ElementFactory::make("fakesink")
|
||||
.build()
|
||||
@ -510,14 +520,7 @@ impl Voice {
|
||||
.unwrap();
|
||||
|
||||
// dedicated AppSrc helpers exist and avoid the needless `?`
|
||||
appsrc.set_caps(Some(
|
||||
&gst::Caps::builder("audio/mpeg")
|
||||
.field("mpegversion", 4i32)
|
||||
.field("stream-format", "adts")
|
||||
.field("rate", 48_000i32)
|
||||
.field("channels", 2i32)
|
||||
.build(),
|
||||
));
|
||||
appsrc.set_caps(Some(&voice_input_caps()));
|
||||
appsrc.set_format(gst::Format::Time);
|
||||
appsrc.set_is_live(true);
|
||||
|
||||
@ -636,6 +639,22 @@ impl Voice {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod voice_caps_tests {
|
||||
use super::voice_input_caps;
|
||||
|
||||
#[test]
|
||||
fn voice_input_caps_describe_aac_adts_stereo_48k() {
|
||||
let _ = super::gst::init();
|
||||
let caps = voice_input_caps().to_string();
|
||||
assert!(caps.contains("audio/mpeg"));
|
||||
assert!(caps.contains("mpegversion=(int)4"));
|
||||
assert!(caps.contains("stream-format=(string)adts"));
|
||||
assert!(caps.contains("rate=(int)48000"));
|
||||
assert!(caps.contains("channels=(int)2"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, coverage))]
|
||||
mod tests {
|
||||
use super::Voice;
|
||||
|
||||
@ -40,10 +40,17 @@ impl CameraCodec {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct HdmiMode {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HdmiConnector {
|
||||
pub name: String,
|
||||
pub id: Option<u32>,
|
||||
pub modes: Vec<HdmiMode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -56,6 +63,33 @@ pub struct CameraConfig {
|
||||
pub hdmi: Option<HdmiConnector>,
|
||||
}
|
||||
|
||||
impl CameraConfig {
|
||||
/// Return the HDMI display mode that should be driven on the wire.
|
||||
///
|
||||
/// Inputs: the selected camera config and optional HDMI mode overrides.
|
||||
/// Outputs: a width/height pair for the physical display pipeline.
|
||||
/// Why: the client webcam uplink can stay at a known-good capture profile
|
||||
/// while HDMI output scales to a mode the capture adapter actually locks.
|
||||
pub fn hdmi_display_size(&self) -> (u32, u32) {
|
||||
if self.output != CameraOutput::Hdmi {
|
||||
return (self.width, self.height);
|
||||
}
|
||||
|
||||
if let (Some(width), Some(height)) = (
|
||||
read_u32_from_env("LESAVKA_HDMI_WIDTH"),
|
||||
read_u32_from_env("LESAVKA_HDMI_HEIGHT"),
|
||||
) {
|
||||
return (width, height);
|
||||
}
|
||||
|
||||
self.hdmi
|
||||
.as_ref()
|
||||
.and_then(|hdmi| preferred_hdmi_mode(&hdmi.modes))
|
||||
.map(|mode| (mode.width, mode.height))
|
||||
.unwrap_or((self.width, self.height))
|
||||
}
|
||||
}
|
||||
|
||||
static LAST_CONFIG: OnceLock<RwLock<CameraConfig>> = OnceLock::new();
|
||||
|
||||
/// Refresh the cached camera config from the current environment.
|
||||
@ -132,12 +166,15 @@ fn select_camera_config() -> CameraConfig {
|
||||
CameraOutput::Uvc => select_uvc_config(),
|
||||
};
|
||||
|
||||
let (display_width, display_height) = cfg.hdmi_display_size();
|
||||
info!(
|
||||
output = cfg.output.as_str(),
|
||||
codec = cfg.codec.as_str(),
|
||||
width = cfg.width,
|
||||
height = cfg.height,
|
||||
fps = cfg.fps,
|
||||
display_width,
|
||||
display_height,
|
||||
hdmi = cfg.hdmi.as_ref().map(|h| h.name.as_str()).unwrap_or("none"),
|
||||
"📷 camera output selected"
|
||||
);
|
||||
@ -160,7 +197,9 @@ fn select_hdmi_config(hdmi: Option<HdmiConnector>) -> CameraConfig {
|
||||
let fps = 30;
|
||||
#[cfg(not(coverage))]
|
||||
if !hw_decode {
|
||||
warn!("📷 HDMI output: hardware H264 decoder not detected; using 720p30");
|
||||
warn!(
|
||||
"📷 HDMI output: hardware H264 decoder not detected; requesting 720p30 camera uplink"
|
||||
);
|
||||
}
|
||||
CameraConfig {
|
||||
output: CameraOutput::Hdmi,
|
||||
@ -259,7 +298,14 @@ fn detect_hdmi_connector(require_connected: bool) -> Option<HdmiConnector> {
|
||||
let _ = require_connected;
|
||||
std::env::var("LESAVKA_HDMI_CONNECTOR")
|
||||
.ok()
|
||||
.map(|name| HdmiConnector { name, id: None })
|
||||
.map(|name| HdmiConnector {
|
||||
name,
|
||||
id: None,
|
||||
modes: std::env::var("LESAVKA_HDMI_MODES")
|
||||
.ok()
|
||||
.map(|raw| parse_hdmi_modes(&raw))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
@ -281,7 +327,11 @@ fn detect_hdmi_connector(require_connected: bool) -> Option<HdmiConnector> {
|
||||
let id = fs::read_to_string(entry.path().join("connector_id"))
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u32>().ok());
|
||||
connectors.push((name, status, id));
|
||||
let modes = fs::read_to_string(entry.path().join("modes"))
|
||||
.ok()
|
||||
.map(|raw| parse_hdmi_modes(&raw))
|
||||
.unwrap_or_default();
|
||||
connectors.push((name, status, id, modes));
|
||||
}
|
||||
connectors.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
@ -289,11 +339,12 @@ fn detect_hdmi_connector(require_connected: bool) -> Option<HdmiConnector> {
|
||||
|name: &str, preferred: &str| name == preferred || name.ends_with(preferred);
|
||||
|
||||
if let Some(pref) = preferred.as_deref() {
|
||||
for (name, status, id) in &connectors {
|
||||
for (name, status, id, modes) in &connectors {
|
||||
if matches_preferred(name, pref) && (!require_connected || status == "connected") {
|
||||
return Some(HdmiConnector {
|
||||
name: name.clone(),
|
||||
id: *id,
|
||||
modes: modes.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -307,26 +358,72 @@ fn detect_hdmi_connector(require_connected: bool) -> Option<HdmiConnector> {
|
||||
.and_then(|lock| lock.read().ok())
|
||||
.and_then(|cfg| cfg.hdmi.as_ref().map(|h| h.name.clone()));
|
||||
if let Some(prev) = previous {
|
||||
for (name, status, id) in &connectors {
|
||||
for (name, status, id, modes) in &connectors {
|
||||
if *name == prev && (!require_connected || status == "connected") {
|
||||
return Some(HdmiConnector {
|
||||
name: name.clone(),
|
||||
id: *id,
|
||||
modes: modes.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (name, status, id) in connectors {
|
||||
for (name, status, id, modes) in connectors {
|
||||
if !require_connected || status == "connected" {
|
||||
return Some(HdmiConnector { name, id });
|
||||
return Some(HdmiConnector { name, id, modes });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_hdmi_modes(raw: &str) -> Vec<HdmiMode> {
|
||||
raw.lines()
|
||||
.flat_map(|line| line.split(','))
|
||||
.filter_map(parse_hdmi_mode)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_hdmi_mode(raw: &str) -> Option<HdmiMode> {
|
||||
let raw = raw.trim();
|
||||
let (width, rest) = raw.split_once('x')?;
|
||||
let width = width.trim().parse::<u32>().ok()?;
|
||||
let height_digits: String = rest
|
||||
.trim()
|
||||
.chars()
|
||||
.take_while(|ch| ch.is_ascii_digit())
|
||||
.collect();
|
||||
let height = height_digits.parse::<u32>().ok()?;
|
||||
(width > 0 && height > 0).then_some(HdmiMode { width, height })
|
||||
}
|
||||
|
||||
fn preferred_hdmi_mode(modes: &[HdmiMode]) -> Option<HdmiMode> {
|
||||
for preferred in [
|
||||
HdmiMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
HdmiMode {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
] {
|
||||
if modes.contains(&preferred) {
|
||||
return Some(preferred);
|
||||
}
|
||||
}
|
||||
|
||||
modes
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|mode| mode.width.saturating_mul(9) == mode.height.saturating_mul(16))
|
||||
.filter(|mode| mode.width.saturating_mul(mode.height) <= 1920 * 1080)
|
||||
.max_by_key(|mode| mode.width.saturating_mul(mode.height))
|
||||
.or_else(|| modes.first().copied())
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
fn parse_env_file(text: &str) -> HashMap<String, String> {
|
||||
let mut out = HashMap::new();
|
||||
@ -360,7 +457,10 @@ fn read_u32_from_map(map: &HashMap<String, String>, key: &str) -> Option<u32> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CameraCodec, CameraOutput, current_camera_config, update_camera_config};
|
||||
use super::{
|
||||
CameraCodec, CameraConfig, CameraOutput, HdmiConnector, HdmiMode, current_camera_config,
|
||||
parse_hdmi_mode, parse_hdmi_modes, preferred_hdmi_mode, update_camera_config,
|
||||
};
|
||||
use serial_test::serial;
|
||||
use temp_env::with_var;
|
||||
|
||||
@ -389,4 +489,105 @@ mod tests {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hdmi_mode_parsing_accepts_sysfs_and_override_shapes() {
|
||||
assert_eq!(
|
||||
parse_hdmi_mode("1920x1080"),
|
||||
Some(HdmiMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hdmi_mode("1280x720p60"),
|
||||
Some(HdmiMode {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
})
|
||||
);
|
||||
assert_eq!(parse_hdmi_mode("not-a-mode"), None);
|
||||
|
||||
let modes = parse_hdmi_modes("1920x1080\n1024x768,800x600\n");
|
||||
assert_eq!(modes.len(), 3);
|
||||
assert_eq!(modes[0].width, 1920);
|
||||
assert_eq!(modes[2].height, 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferred_hdmi_mode_chooses_standard_capture_adapter_mode() {
|
||||
let modes = parse_hdmi_modes("1024x768\n1920x1080\n800x600\n");
|
||||
assert_eq!(
|
||||
preferred_hdmi_mode(&modes),
|
||||
Some(HdmiMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
})
|
||||
);
|
||||
|
||||
let modes = parse_hdmi_modes("1600x900\n1024x768\n");
|
||||
assert_eq!(
|
||||
preferred_hdmi_mode(&modes),
|
||||
Some(HdmiMode {
|
||||
width: 1600,
|
||||
height: 900,
|
||||
})
|
||||
);
|
||||
|
||||
let modes = parse_hdmi_modes("1024x768\n800x600\n");
|
||||
assert_eq!(
|
||||
preferred_hdmi_mode(&modes),
|
||||
Some(HdmiMode {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn hdmi_display_size_uses_adapter_mode_without_changing_uplink_profile() {
|
||||
let cfg = CameraConfig {
|
||||
output: CameraOutput::Hdmi,
|
||||
codec: CameraCodec::H264,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("card1-HDMI-A-2"),
|
||||
id: Some(43),
|
||||
modes: parse_hdmi_modes("1920x1080\n1024x768\n800x600\n"),
|
||||
}),
|
||||
};
|
||||
|
||||
with_var("LESAVKA_HDMI_WIDTH", None::<&str>, || {
|
||||
with_var("LESAVKA_HDMI_HEIGHT", None::<&str>, || {
|
||||
assert_eq!((cfg.width, cfg.height), (1280, 720));
|
||||
assert_eq!(cfg.hdmi_display_size(), (1920, 1080));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn hdmi_display_size_honors_explicit_local_override() {
|
||||
let cfg = CameraConfig {
|
||||
output: CameraOutput::Hdmi,
|
||||
codec: CameraCodec::H264,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("card1-HDMI-A-2"),
|
||||
id: Some(43),
|
||||
modes: parse_hdmi_modes("1920x1080\n"),
|
||||
}),
|
||||
};
|
||||
|
||||
with_var("LESAVKA_HDMI_WIDTH", Some("1024"), || {
|
||||
with_var("LESAVKA_HDMI_HEIGHT", Some("768"), || {
|
||||
assert_eq!(cfg.hdmi_display_size(), (1024, 768));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,6 +155,10 @@ pub fn camera_cfg_eq(a: &camera::CameraConfig, b: &camera::CameraConfig) -> bool
|
||||
return false;
|
||||
}
|
||||
|
||||
if a.output == camera::CameraOutput::Hdmi && a.hdmi_display_size() != b.hdmi_display_size() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (&a.hdmi, &b.hdmi) {
|
||||
(Some(left), Some(right)) => left.name == right.name && left.id == right.id,
|
||||
(None, None) => true,
|
||||
@ -178,6 +182,7 @@ mod tests {
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-1"),
|
||||
id: Some(42),
|
||||
modes: Vec::new(),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -192,6 +197,7 @@ mod tests {
|
||||
changed.hdmi = Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-2"),
|
||||
id: Some(42),
|
||||
modes: Vec::new(),
|
||||
});
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
}
|
||||
|
||||
@ -249,8 +249,11 @@ impl HdmiSink {
|
||||
gst::init()?;
|
||||
|
||||
let pipeline = gst::Pipeline::new();
|
||||
let width = cfg.width as i32;
|
||||
let height = cfg.height as i32;
|
||||
let source_width = cfg.width as i32;
|
||||
let source_height = cfg.height as i32;
|
||||
let (display_width, display_height) = cfg.hdmi_display_size();
|
||||
let width = display_width as i32;
|
||||
let height = display_height as i32;
|
||||
let fps = cfg.fps.max(1) as i32;
|
||||
|
||||
let src = gst::ElementFactory::make("appsrc")
|
||||
@ -278,6 +281,17 @@ impl HdmiSink {
|
||||
let scale = gst::ElementFactory::make("videoscale").build()?;
|
||||
let sink = build_hdmi_sink(cfg)?;
|
||||
|
||||
if (display_width, display_height) != (cfg.width, cfg.height) {
|
||||
tracing::info!(
|
||||
target: "lesavka_server::video",
|
||||
source_width = cfg.width,
|
||||
source_height = cfg.height,
|
||||
display_width,
|
||||
display_height,
|
||||
"📺 HDMI sink scaling camera uplink to adapter mode"
|
||||
);
|
||||
}
|
||||
|
||||
match cfg.codec {
|
||||
CameraCodec::H264 => {
|
||||
let caps_h264 = gst::Caps::builder("video/x-h264")
|
||||
@ -317,8 +331,8 @@ impl HdmiSink {
|
||||
CameraCodec::Mjpeg => {
|
||||
let caps_mjpeg = gst::Caps::builder("image/jpeg")
|
||||
.field("parsed", true)
|
||||
.field("width", width)
|
||||
.field("height", height)
|
||||
.field("width", source_width)
|
||||
.field("height", source_height)
|
||||
.field("framerate", gst::Fraction::new(fps, 1))
|
||||
.build();
|
||||
src.set_caps(Some(&caps_mjpeg));
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
//! Why: camera runtime generation and guardrails are core to safe stream
|
||||
//! transitions, so they need direct integration-level assertions.
|
||||
|
||||
use lesavka_server::camera::{CameraCodec, CameraConfig, CameraOutput, HdmiConnector};
|
||||
use lesavka_server::camera::{CameraCodec, CameraConfig, CameraOutput, HdmiConnector, HdmiMode};
|
||||
use lesavka_server::camera_runtime::{CameraRuntime, camera_cfg_eq};
|
||||
use serial_test::serial;
|
||||
use temp_env::with_var;
|
||||
@ -79,6 +79,7 @@ fn activate_non_uvc_returns_noop_relay_in_coverage_harness() {
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-1"),
|
||||
id: Some(1),
|
||||
modes: Vec::new(),
|
||||
}),
|
||||
};
|
||||
|
||||
@ -119,6 +120,7 @@ fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() {
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-1"),
|
||||
id: Some(7),
|
||||
modes: Vec::new(),
|
||||
}),
|
||||
};
|
||||
let hdmi_b = hdmi_a.clone();
|
||||
@ -128,6 +130,7 @@ fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() {
|
||||
different_id.hdmi = Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-1"),
|
||||
id: Some(8),
|
||||
modes: Vec::new(),
|
||||
});
|
||||
assert!(!camera_cfg_eq(&hdmi_a, &different_id));
|
||||
|
||||
@ -136,6 +139,36 @@ fn camera_cfg_eq_handles_none_and_hdmi_connector_edges() {
|
||||
assert!(!camera_cfg_eq(&missing_connector, &hdmi_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_cfg_eq_rejects_hdmi_display_mode_changes() {
|
||||
let base = CameraConfig {
|
||||
output: CameraOutput::Hdmi,
|
||||
codec: CameraCodec::H264,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-2"),
|
||||
id: Some(43),
|
||||
modes: vec![HdmiMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
}],
|
||||
}),
|
||||
};
|
||||
let mut changed = base.clone();
|
||||
changed.hdmi = Some(HdmiConnector {
|
||||
name: String::from("HDMI-A-2"),
|
||||
id: Some(43),
|
||||
modes: vec![HdmiMode {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
}],
|
||||
});
|
||||
|
||||
assert!(!camera_cfg_eq(&base, &changed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_cfg_eq_rejects_output_codec_resolution_and_fps_changes() {
|
||||
let base = CameraConfig {
|
||||
|
||||
@ -17,7 +17,7 @@ mod video_support {
|
||||
mod video_sinks_include_contract {
|
||||
include!(env!("LESAVKA_SERVER_VIDEO_SINKS_SRC"));
|
||||
|
||||
use crate::camera::CameraOutput;
|
||||
use crate::camera::{CameraOutput, HdmiConnector, HdmiMode};
|
||||
use serial_test::serial;
|
||||
use temp_env::with_var;
|
||||
|
||||
@ -32,6 +32,30 @@ mod video_sinks_include_contract {
|
||||
}
|
||||
}
|
||||
|
||||
fn hdmi_cfg_with_ugreen_like_modes(codec: CameraCodec) -> CameraConfig {
|
||||
CameraConfig {
|
||||
output: CameraOutput::Hdmi,
|
||||
codec,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
hdmi: Some(HdmiConnector {
|
||||
name: String::from("card1-HDMI-A-2"),
|
||||
id: Some(43),
|
||||
modes: vec![
|
||||
HdmiMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
HdmiMode {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_gst() {
|
||||
let _ = gst::init();
|
||||
}
|
||||
@ -70,6 +94,43 @@ mod video_sinks_include_contract {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hdmi_display_size_scales_uplink_to_capture_adapter_mode() {
|
||||
let cfg = hdmi_cfg_with_ugreen_like_modes(CameraCodec::H264);
|
||||
assert_eq!((cfg.width, cfg.height), (1280, 720));
|
||||
assert_eq!(cfg.hdmi_display_size(), (1920, 1080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(not(coverage))]
|
||||
fn build_hdmi_sink_pins_kms_connector_and_modesetting_when_available() {
|
||||
init_gst();
|
||||
if gst::ElementFactory::find("kmssink").is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
with_var("LESAVKA_HDMI_SINK", None::<&str>, || {
|
||||
with_var("LESAVKA_HDMI_DRIVER", Some("vc4"), || {
|
||||
let sink = build_hdmi_sink(&hdmi_cfg_with_ugreen_like_modes(CameraCodec::H264))
|
||||
.expect("kmssink should build");
|
||||
|
||||
if sink.has_property("force-modesetting", None) {
|
||||
assert!(
|
||||
sink.property::<bool>("force-modesetting"),
|
||||
"kmssink must drive the HDMI mode instead of relying on desktop state"
|
||||
);
|
||||
}
|
||||
if sink.has_property("connector-id", None) {
|
||||
assert_eq!(sink.property::<i32>("connector-id"), 43);
|
||||
}
|
||||
if sink.has_property("driver-name", None) {
|
||||
assert_eq!(sink.property::<String>("driver-name"), "vc4");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn camera_sink_dispatch_is_stable_for_hdmi_variant() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user