Initial implementation of Wayland AirPlay desktop sharing

This commit is contained in:
Niklas Haiden
2026-09-09 22:04:30 +02:00
commit 7cda80d840
18 changed files with 11044 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
use spielab::{
media::{VideoCapture, VideoEncoder},
settings::{VideoConfig, VideoQuality},
};
use std::{process::Stdio, time::Duration};
use tokio::{io::AsyncWriteExt, process::Command};
/// Exercises the actual encoder, Annex-B reader and an independent H.264 decoder.
#[tokio::test]
#[ignore = "requires FFmpeg with libx264 installed"]
async fn encoded_access_units_decode_to_three_video_frames() {
decode_frames(VideoConfig {
width: 320,
height: 240,
..VideoQuality::Efficient.config()
})
.await;
}
#[tokio::test]
#[ignore = "requires FFmpeg with libx264 installed"]
async fn every_quality_preset_encodes_the_requested_resolution_and_frame_rate() {
for quality in VideoQuality::ALL {
decode_frames(quality.config()).await;
}
}
async fn decode_frames(config: VideoConfig) {
decode_with_encoder(config, &VideoEncoder::Software, 3).await;
}
#[tokio::test]
#[ignore = "requires a working VA-API H.264 encoder on /dev/dri/renderD128"]
async fn hardware_frames_decode_at_every_quality_and_auto_detection_selects_gpu() {
let encoder = VideoEncoder::Vaapi("/dev/dri/renderD128".into());
for quality in VideoQuality::ALL {
encoder.probe(quality.config()).await.unwrap();
decode_with_encoder(
quality.config(),
&encoder,
quality.config().fps as usize + 2,
)
.await;
}
assert!(matches!(
VideoEncoder::detect(VideoQuality::High.config()).await,
VideoEncoder::Vaapi(_)
));
}
async fn decode_with_encoder(config: VideoConfig, encoder: &VideoEncoder, count: usize) {
let mut capture = VideoCapture::start_with_encoder(None, config, encoder).unwrap();
let mut stream = vec![];
for index in 0..count {
let nals = tokio::time::timeout(Duration::from_secs(5), capture.frame())
.await
.unwrap()
.unwrap();
if index == 0 {
assert!(nals.iter().any(|n| n[0] & 31 == 7));
assert!(nals.iter().any(|n| n[0] & 31 == 8));
}
for nal in nals {
stream.extend_from_slice(&[0, 0, 0, 1]);
stream.extend_from_slice(&nal);
}
}
drop(capture);
let mut decoder = Command::new("ffmpeg")
.args([
"-hide_banner",
"-loglevel",
"error",
"-f",
"h264",
"-i",
"pipe:0",
"-f",
"framehash",
"pipe:1",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
decoder
.stdin
.take()
.unwrap()
.write_all(&stream)
.await
.unwrap();
let output = tokio::time::timeout(Duration::from_secs(5), decoder.wait_with_output())
.await
.unwrap()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let decoded = String::from_utf8(output.stdout).unwrap();
assert!(
decoded.contains(&format!(
"#dimensions 0: {}x{}",
config.width, config.height
)),
"{decoded}"
);
assert!(
decoded.contains(&format!("#tb 0: 1/{}", config.fps)),
"{decoded}"
);
assert_eq!(
decoded
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.count(),
count
);
}