fix(sync): harden probe analysis and uac auto selection
This commit is contained in:
parent
0650965e52
commit
5d6d07d375
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1676,7 +1676,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -289,9 +289,9 @@ fn server_chip_state_tracks_connection_not_just_reachability() {
|
|||||||
assert_eq!(server_version_label(&state), "-");
|
assert_eq!(server_version_label(&state), "-");
|
||||||
|
|
||||||
state.set_server_available(true);
|
state.set_server_available(true);
|
||||||
state.set_server_version(Some("0.13.0".to_string()));
|
state.set_server_version(Some("0.13.1".to_string()));
|
||||||
assert_eq!(server_light_state(&state, false), StatusLightState::Live);
|
assert_eq!(server_light_state(&state, false), StatusLightState::Live);
|
||||||
assert_eq!(server_version_label(&state), "v0.13.0");
|
assert_eq!(server_version_label(&state), "v0.13.1");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server_light_state(&state, true),
|
server_light_state(&state, true),
|
||||||
|
|||||||
@ -24,7 +24,7 @@ pub fn analyze_capture(
|
|||||||
options: &SyncAnalysisOptions,
|
options: &SyncAnalysisOptions,
|
||||||
) -> Result<SyncAnalysisReport> {
|
) -> Result<SyncAnalysisReport> {
|
||||||
let timestamps = extract_video_timestamps(capture_path)?;
|
let timestamps = extract_video_timestamps(capture_path)?;
|
||||||
let brightness = extract_video_brightness(capture_path)?;
|
let brightness = extract_video_brightness(capture_path, timestamps.len())?;
|
||||||
let video_segments = detect_video_segments(×tamps, &brightness)?;
|
let video_segments = detect_video_segments(×tamps, &brightness)?;
|
||||||
|
|
||||||
let audio_samples = extract_audio_samples(capture_path)?;
|
let audio_samples = extract_audio_samples(capture_path)?;
|
||||||
@ -47,7 +47,8 @@ pub fn analyze_capture(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::test_support::{
|
use super::test_support::{
|
||||||
audio_samples_to_bytes, click_track_samples, frame_json, with_fake_media_tools,
|
audio_samples_to_bytes, click_track_samples, frame_json, thumbnail_video_bytes,
|
||||||
|
with_fake_media_tools,
|
||||||
};
|
};
|
||||||
use super::{SyncAnalysisOptions, analyze_capture};
|
use super::{SyncAnalysisOptions, analyze_capture};
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ mod tests {
|
|||||||
|
|
||||||
with_fake_media_tools(
|
with_fake_media_tools(
|
||||||
&frame_json(×tamps),
|
&frame_json(×tamps),
|
||||||
&brightness,
|
&thumbnail_video_bytes(&brightness),
|
||||||
&audio_samples_to_bytes(&audio),
|
&audio_samples_to_bytes(&audio),
|
||||||
|capture_path| {
|
|capture_path| {
|
||||||
let report = analyze_capture(
|
let report = analyze_capture(
|
||||||
|
|||||||
@ -3,6 +3,8 @@ use serde::Deserialize;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
const VIDEO_ANALYSIS_SIDE_PX: usize = 32;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ProbeFrameResponse {
|
struct ProbeFrameResponse {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -44,7 +46,13 @@ pub(super) fn extract_video_timestamps(capture_path: &Path) -> Result<Vec<f64>>
|
|||||||
Ok(timestamps)
|
Ok(timestamps)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn extract_video_brightness(capture_path: &Path) -> Result<Vec<u8>> {
|
pub(super) fn extract_video_brightness(
|
||||||
|
capture_path: &Path,
|
||||||
|
expected_frames: usize,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
if expected_frames == 0 {
|
||||||
|
bail!("expected at least one video frame when extracting brightness");
|
||||||
|
}
|
||||||
let output = run_command(
|
let output = run_command(
|
||||||
Command::new("ffmpeg")
|
Command::new("ffmpeg")
|
||||||
.arg("-hide_banner")
|
.arg("-hide_banner")
|
||||||
@ -55,7 +63,10 @@ pub(super) fn extract_video_brightness(capture_path: &Path) -> Result<Vec<u8>> {
|
|||||||
.arg("-map")
|
.arg("-map")
|
||||||
.arg("0:v:0")
|
.arg("0:v:0")
|
||||||
.arg("-vf")
|
.arg("-vf")
|
||||||
.arg("scale=1:1,format=gray")
|
.arg(format!(
|
||||||
|
"scale={side}:{side}:flags=area,format=gray",
|
||||||
|
side = VIDEO_ANALYSIS_SIDE_PX
|
||||||
|
))
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
.arg("rawvideo")
|
.arg("rawvideo")
|
||||||
.arg("-pix_fmt")
|
.arg("-pix_fmt")
|
||||||
@ -66,7 +77,27 @@ pub(super) fn extract_video_brightness(capture_path: &Path) -> Result<Vec<u8>> {
|
|||||||
if output.is_empty() {
|
if output.is_empty() {
|
||||||
bail!("ffmpeg did not emit any video brightness data");
|
bail!("ffmpeg did not emit any video brightness data");
|
||||||
}
|
}
|
||||||
Ok(output)
|
|
||||||
|
let frame_pixels = VIDEO_ANALYSIS_SIDE_PX * VIDEO_ANALYSIS_SIDE_PX;
|
||||||
|
if output.len() % frame_pixels != 0 {
|
||||||
|
bail!(
|
||||||
|
"ffmpeg emitted {} bytes of video brightness data, which is not divisible by the {}-pixel analysis frame size",
|
||||||
|
output.len(),
|
||||||
|
frame_pixels
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let extracted_frames = output.len() / frame_pixels;
|
||||||
|
if extracted_frames < expected_frames {
|
||||||
|
bail!(
|
||||||
|
"ffmpeg emitted only {extracted_frames} brightness frames for {expected_frames} expected timestamps"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output
|
||||||
|
.chunks_exact(frame_pixels)
|
||||||
|
.take(expected_frames)
|
||||||
|
.map(summarize_frame_brightness)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn extract_audio_samples(capture_path: &Path) -> Result<Vec<i16>> {
|
pub(super) fn extract_audio_samples(capture_path: &Path) -> Result<Vec<i16>> {
|
||||||
@ -110,13 +141,27 @@ pub(super) fn run_command(command: &mut Command, description: &str) -> Result<Ve
|
|||||||
Ok(output.stdout)
|
Ok(output.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn summarize_frame_brightness(frame: &[u8]) -> u8 {
|
||||||
|
let crop_start = VIDEO_ANALYSIS_SIDE_PX / 4;
|
||||||
|
let crop_end = VIDEO_ANALYSIS_SIDE_PX - crop_start;
|
||||||
|
let mut total = 0u64;
|
||||||
|
let mut pixels = 0u64;
|
||||||
|
for y in crop_start..crop_end {
|
||||||
|
for x in crop_start..crop_end {
|
||||||
|
total += u64::from(frame[y * VIDEO_ANALYSIS_SIDE_PX + x]);
|
||||||
|
pixels += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
((total / pixels.max(1)).min(u64::from(u8::MAX))) as u8
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
extract_audio_samples, extract_video_brightness, extract_video_timestamps, run_command,
|
extract_audio_samples, extract_video_brightness, extract_video_timestamps, run_command,
|
||||||
};
|
};
|
||||||
use crate::sync_probe::analyze::test_support::{
|
use crate::sync_probe::analyze::test_support::{
|
||||||
audio_samples_to_bytes, frame_json, with_fake_media_tools,
|
audio_samples_to_bytes, frame_json, thumbnail_video_bytes, with_fake_media_tools,
|
||||||
};
|
};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@ -162,11 +207,11 @@ mod tests {
|
|||||||
let brightness = vec![5u8, 100, 250];
|
let brightness = vec![5u8, 100, 250];
|
||||||
with_fake_media_tools(
|
with_fake_media_tools(
|
||||||
br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
|
br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
|
||||||
&brightness,
|
&thumbnail_video_bytes(&brightness),
|
||||||
&[1, 0],
|
&[1, 0],
|
||||||
|capture_path| {
|
|capture_path| {
|
||||||
let parsed = extract_video_brightness(capture_path).expect("video brightness");
|
let parsed = extract_video_brightness(capture_path, 1).expect("video brightness");
|
||||||
assert_eq!(parsed, brightness);
|
assert_eq!(parsed, vec![brightness[0]]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -178,7 +223,8 @@ mod tests {
|
|||||||
&[],
|
&[],
|
||||||
&[1, 0],
|
&[1, 0],
|
||||||
|capture_path| {
|
|capture_path| {
|
||||||
let error = extract_video_brightness(capture_path).expect_err("empty brightness");
|
let error =
|
||||||
|
extract_video_brightness(capture_path, 1).expect_err("empty brightness");
|
||||||
assert!(
|
assert!(
|
||||||
error
|
error
|
||||||
.to_string()
|
.to_string()
|
||||||
@ -188,6 +234,29 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_video_brightness_uses_center_weighted_thumbnail_average() {
|
||||||
|
let brightness = vec![20u8, 45, 20];
|
||||||
|
with_fake_media_tools(
|
||||||
|
&frame_json(&[0.0, 0.1, 0.2]),
|
||||||
|
&thumbnail_video_bytes(&brightness),
|
||||||
|
&[1, 0],
|
||||||
|
|capture_path| {
|
||||||
|
let parsed = extract_video_brightness(capture_path, 3).expect("video brightness");
|
||||||
|
assert_eq!(parsed, brightness);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_video_brightness_rejects_truncated_frame_data() {
|
||||||
|
with_fake_media_tools(&frame_json(&[0.0]), &[1, 2, 3], &[1, 0], |capture_path| {
|
||||||
|
let error =
|
||||||
|
extract_video_brightness(capture_path, 1).expect_err("truncated frame bytes");
|
||||||
|
assert!(error.to_string().contains("not divisible"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extract_audio_samples_reads_fake_ffmpeg_output() {
|
fn extract_audio_samples_reads_fake_ffmpeg_output() {
|
||||||
let samples = vec![1i16, -2, 32_000];
|
let samples = vec![1i16, -2, 32_000];
|
||||||
|
|||||||
@ -7,7 +7,10 @@ mod tests;
|
|||||||
pub(crate) use correlation::correlate_segments;
|
pub(crate) use correlation::correlate_segments;
|
||||||
|
|
||||||
pub(super) const DEFAULT_AUDIO_SAMPLE_RATE_HZ: u32 = 48_000;
|
pub(super) const DEFAULT_AUDIO_SAMPLE_RATE_HZ: u32 = 48_000;
|
||||||
const MIN_VIDEO_CONTRAST: u8 = 16;
|
// Real HDMI/capture paths can preserve pulse shape while compressing its absolute
|
||||||
|
// luma swing into a narrower band, so keep this guard modest and let the
|
||||||
|
// segment logic reject genuinely flat/noisy traces.
|
||||||
|
const MIN_VIDEO_CONTRAST: u8 = 8;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub(crate) struct PulseSegment {
|
pub(crate) struct PulseSegment {
|
||||||
|
|||||||
@ -69,6 +69,21 @@ pub(super) fn click_track_samples(click_times_s: &[f64], total_samples: usize) -
|
|||||||
samples
|
samples
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn thumbnail_video_bytes(brightness_values: &[u8]) -> Vec<u8> {
|
||||||
|
const SIDE: usize = 32;
|
||||||
|
let mut bytes = Vec::with_capacity(brightness_values.len() * SIDE * SIDE);
|
||||||
|
for brightness in brightness_values {
|
||||||
|
let mut frame = vec![20u8; SIDE * SIDE];
|
||||||
|
for y in SIDE / 4..SIDE - SIDE / 4 {
|
||||||
|
for x in SIDE / 4..SIDE - SIDE / 4 {
|
||||||
|
frame[y * SIDE + x] = *brightness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes.extend_from_slice(&frame);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn audio_samples_to_bytes(samples: &[i16]) -> Vec<u8> {
|
pub(super) fn audio_samples_to_bytes(samples: &[i16]) -> Vec<u8> {
|
||||||
samples
|
samples
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -182,11 +182,11 @@
|
|||||||
},
|
},
|
||||||
"client/src/sync_probe/analyze.rs": {
|
"client/src/sync_probe/analyze.rs": {
|
||||||
"line_percent": 97.92,
|
"line_percent": 97.92,
|
||||||
"loc": 86
|
"loc": 87
|
||||||
},
|
},
|
||||||
"client/src/sync_probe/analyze/media_extract.rs": {
|
"client/src/sync_probe/analyze/media_extract.rs": {
|
||||||
"line_percent": 98.36,
|
"line_percent": 97.88,
|
||||||
"loc": 240
|
"loc": 309
|
||||||
},
|
},
|
||||||
"client/src/sync_probe/analyze/onset_detection.rs": {
|
"client/src/sync_probe/analyze/onset_detection.rs": {
|
||||||
"line_percent": 96.77,
|
"line_percent": 96.77,
|
||||||
@ -201,8 +201,8 @@
|
|||||||
"loc": 59
|
"loc": 59
|
||||||
},
|
},
|
||||||
"client/src/sync_probe/analyze/test_support.rs": {
|
"client/src/sync_probe/analyze/test_support.rs": {
|
||||||
"line_percent": 98.44,
|
"line_percent": 98.67,
|
||||||
"loc": 85
|
"loc": 100
|
||||||
},
|
},
|
||||||
"client/src/sync_probe/capture.rs": {
|
"client/src/sync_probe/capture.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
@ -377,8 +377,8 @@
|
|||||||
"loc": 260
|
"loc": 260
|
||||||
},
|
},
|
||||||
"server/src/runtime_support/audio_discovery.rs": {
|
"server/src/runtime_support/audio_discovery.rs": {
|
||||||
"line_percent": 98.54,
|
"line_percent": 98.55,
|
||||||
"loc": 279
|
"loc": 281
|
||||||
},
|
},
|
||||||
"server/src/runtime_support/hid_recovery.rs": {
|
"server/src/runtime_support/hid_recovery.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -129,15 +129,17 @@ fn preferred_uac_device_candidates(preferred: &str) -> Vec<String> {
|
|||||||
"hw:Lesavka,0",
|
"hw:Lesavka,0",
|
||||||
];
|
];
|
||||||
let allow_aliases = auto_family.contains(&preferred);
|
let allow_aliases = auto_family.contains(&preferred);
|
||||||
push_audio_candidate_family(&mut out, &mut seen, preferred);
|
|
||||||
if allow_aliases {
|
if allow_aliases {
|
||||||
for detected in detect_uac_card_candidates() {
|
for detected in detect_uac_card_candidates() {
|
||||||
push_audio_candidate_family(&mut out, &mut seen, &detected);
|
push_audio_candidate_family(&mut out, &mut seen, &detected);
|
||||||
}
|
}
|
||||||
for alias in auto_family {
|
for alias in auto_family {
|
||||||
push_audio_candidate_family(&mut out, &mut seen, alias);
|
if alias != preferred {
|
||||||
|
push_audio_candidate_family(&mut out, &mut seen, alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
push_audio_candidate_family(&mut out, &mut seen, preferred);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,13 +179,13 @@ fn detect_uac_card_candidates() -> Vec<String> {
|
|||||||
.map(parse_uac_numeric_card_ids)
|
.map(parse_uac_numeric_card_ids)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if let Some(cards) = card_data.as_deref() {
|
if let Some(pcm) = asound_pcm_snapshot() {
|
||||||
for candidate in parse_uac_named_card_candidates(cards) {
|
for candidate in parse_uac_pcm_candidates(&pcm, &numeric_card_ids) {
|
||||||
push_audio_candidate(&mut out, &mut seen, &candidate);
|
push_audio_candidate(&mut out, &mut seen, &candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(pcm) = asound_pcm_snapshot() {
|
if let Some(cards) = card_data.as_deref() {
|
||||||
for candidate in parse_uac_pcm_candidates(&pcm, &numeric_card_ids) {
|
for candidate in parse_uac_named_card_candidates(cards) {
|
||||||
push_audio_candidate(&mut out, &mut seen, &candidate);
|
push_audio_candidate(&mut out, &mut seen, &candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,23 +85,33 @@ fn audio_candidate_helpers_dedupe_and_pair_hw_plughw_forms() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn preferred_uac_candidates_include_detected_cards_before_static_aliases() {
|
fn preferred_uac_candidates_include_detected_cards_before_alias_fallbacks() {
|
||||||
temp_env::with_vars(
|
temp_env::with_vars(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"LESAVKA_TEST_ASOUND_CARDS",
|
"LESAVKA_TEST_ASOUND_CARDS",
|
||||||
Some(
|
Some(
|
||||||
" 7 [DetectedGadget ]: USB-Audio - UAC2 Gadget\n\
|
" 7 [DetectedGadget ]: USB-Audio - UAC2 Gadget
|
||||||
8 [LesavkaAudio ]: USB-Audio - Lesavka\n",
|
\
|
||||||
|
8 [LesavkaAudio ]: USB-Audio - Lesavka
|
||||||
|
",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"LESAVKA_TEST_ASOUND_PCM",
|
"LESAVKA_TEST_ASOUND_PCM",
|
||||||
Some("07-03: USB Audio : USB Audio : playback 1 : capture 1\n"),
|
Some(
|
||||||
|
"07-03: USB Audio : USB Audio : playback 1 : capture 1
|
||||||
|
",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|| {
|
|| {
|
||||||
let candidates = preferred_uac_device_candidates("hw:UAC2Gadget,0");
|
let candidates = preferred_uac_device_candidates("hw:UAC2Gadget,0");
|
||||||
|
assert_eq!(
|
||||||
|
candidates.first().map(String::as_str),
|
||||||
|
Some("hw:7,3")
|
||||||
|
);
|
||||||
|
assert!(candidates.iter().any(|value| value == "plughw:7,3"));
|
||||||
assert!(
|
assert!(
|
||||||
candidates
|
candidates
|
||||||
.iter()
|
.iter()
|
||||||
@ -109,6 +119,20 @@ fn preferred_uac_candidates_include_detected_cards_before_static_aliases() {
|
|||||||
);
|
);
|
||||||
assert!(candidates.iter().any(|value| value == "hw:LesavkaAudio,0"));
|
assert!(candidates.iter().any(|value| value == "hw:LesavkaAudio,0"));
|
||||||
assert!(candidates.iter().any(|value| value == "hw:7,3"));
|
assert!(candidates.iter().any(|value| value == "hw:7,3"));
|
||||||
|
let preferred_index = candidates
|
||||||
|
.iter()
|
||||||
|
.position(|value| value == "hw:UAC2Gadget,0")
|
||||||
|
.expect("preferred alias present");
|
||||||
|
let numeric_index = candidates
|
||||||
|
.iter()
|
||||||
|
.position(|value| value == "hw:7,3")
|
||||||
|
.expect("numeric candidate present");
|
||||||
|
let detected_index = candidates
|
||||||
|
.iter()
|
||||||
|
.position(|value| value == "hw:DetectedGadget,0")
|
||||||
|
.expect("detected candidate present");
|
||||||
|
assert!(numeric_index < detected_index);
|
||||||
|
assert!(detected_index < preferred_index);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user