Initial implementation of Wayland AirPlay desktop sharing
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
use airplay_discovery::{Discovery, ServiceBrowser};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let browser = ServiceBrowser::new()?;
|
||||
let devices = browser.scan(Duration::from_secs(6)).await?;
|
||||
println!("Found {} AirPlay receivers", devices.len());
|
||||
for device in devices {
|
||||
println!(
|
||||
"{} | {} | {:?} | {}",
|
||||
device.name,
|
||||
device.model,
|
||||
device.socket_addr(),
|
||||
device.id.to_mac_string()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Run the real mirroring path. --test uses a moving test card in place of a desktop.
|
||||
use airplay_discovery::{Discovery, ServiceBrowser};
|
||||
use anyhow::Context;
|
||||
use spielab::{
|
||||
capture::ScreenCapture,
|
||||
media::{AudioCapture, VideoCapture, VideoEncoder},
|
||||
mirror::MirrorSession,
|
||||
pairing::{ControlSession, CredentialStore},
|
||||
settings::{CaptureMode, LatencyMode, VideoQuality},
|
||||
};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let name = args.get(1).context(
|
||||
"Usage: mirror 'Receiver name' [--test | --extend] [--quality=720p30|1080p30|1080p60] [--latency=50|100|500]",
|
||||
)?;
|
||||
let state = std::env::var_os("SPIELAB_STATE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.context("Set SPIELAB_STATE_DIR")?;
|
||||
let store = CredentialStore::new(state)?;
|
||||
let browser = ServiceBrowser::new()?;
|
||||
let device = browser
|
||||
.scan(Duration::from_secs(5))
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|d| &d.name == name)
|
||||
.context("Receiver not found")?;
|
||||
let credentials = store.load(&device)?.context("Pair this receiver first")?;
|
||||
anyhow::ensure!(
|
||||
!(args.iter().any(|a| a == "--test") && args.iter().any(|a| a == "--extend")),
|
||||
"Use --test or --extend, not both"
|
||||
);
|
||||
let quality = match args.iter().find_map(|arg| arg.strip_prefix("--quality=")) {
|
||||
None | Some("1080p30") => VideoQuality::High,
|
||||
Some("720p30") => VideoQuality::Efficient,
|
||||
Some("1080p60") => VideoQuality::Smooth,
|
||||
Some(value) => anyhow::bail!("Unknown quality: {value}"),
|
||||
};
|
||||
let config = quality.config();
|
||||
let encoder = VideoEncoder::detect(config).await;
|
||||
println!("Encoder: {}", encoder.label());
|
||||
let latency = match args.iter().find_map(|arg| arg.strip_prefix("--latency=")) {
|
||||
None | Some("50") => LatencyMode::Interactive,
|
||||
Some("100") => LatencyMode::Responsive,
|
||||
Some("500") => LatencyMode::Buffered,
|
||||
Some(value) => anyhow::bail!("Unknown latency: {value}"),
|
||||
};
|
||||
let mode = if args.iter().any(|a| a == "--extend") {
|
||||
CaptureMode::Extend
|
||||
} else {
|
||||
CaptureMode::Mirror
|
||||
};
|
||||
let screen = if args.iter().any(|a| a == "--test") {
|
||||
None
|
||||
} else {
|
||||
Some(ScreenCapture::select_mode(mode).await?)
|
||||
};
|
||||
println!("Capture: {} · {}", mode.label(), quality.label());
|
||||
let mut control = ControlSession::open(device).await?;
|
||||
control.verify(&credentials).await?;
|
||||
println!("Pair verification succeeded; setting up mirroring");
|
||||
let result = async {
|
||||
let mut mirror =
|
||||
MirrorSession::setup_with_latency(&mut control, config.width, config.height, latency)
|
||||
.await?;
|
||||
let video = VideoCapture::start_with_encoder(screen.as_ref(), config, &encoder)?;
|
||||
let audio = AudioCapture::start()?;
|
||||
println!("Video and screen-audio SETUP accepted");
|
||||
let duration = std::env::var("SPIELAB_TEST_SECONDS")
|
||||
.unwrap_or("30".into())
|
||||
.parse::<u64>()?;
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(duration),
|
||||
mirror.run(&mut control, video, audio, |frames| {
|
||||
println!("Sent {frames} video frames")
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
.await;
|
||||
let _ = control.stop_mirroring().await;
|
||||
let _ = control.transport.close().await;
|
||||
if let Some(screen) = screen {
|
||||
let _ = screen.close().await;
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Interactive pairing smoke test, sharing the GUI's exact protocol implementation.
|
||||
use airplay_discovery::{Discovery, ServiceBrowser};
|
||||
use anyhow::Context;
|
||||
use spielab::pairing::{ControlSession, CredentialStore};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let name = std::env::args()
|
||||
.nth(1)
|
||||
.context("Usage: cargo run --example pair -- 'Receiver name'")?;
|
||||
let state = std::env::var_os("SPIELAB_STATE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.context("Set SPIELAB_STATE_DIR to the same credential directory as the GUI")?;
|
||||
let store = CredentialStore::new(state)?;
|
||||
let browser = ServiceBrowser::new()?;
|
||||
let device = browser
|
||||
.scan(Duration::from_secs(5))
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|device| device.name == name)
|
||||
.context("Receiver not found")?;
|
||||
let mut session = ControlSession::open(device).await?;
|
||||
if let Some(credentials) = store.load(&session.device)? {
|
||||
session.verify(&credentials).await?;
|
||||
} else {
|
||||
session.request_pin().await?;
|
||||
print!("Enter the code shown on Apple TV: ");
|
||||
io::stdout().flush()?;
|
||||
let mut pin = String::new();
|
||||
io::stdin().read_line(&mut pin)?;
|
||||
let credentials = session.pair(pin.trim()).await?;
|
||||
store.save(&session.device, &credentials)?;
|
||||
}
|
||||
println!("Authenticated: encrypted OPTIONS round trip succeeded.");
|
||||
session.transport.close().await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user