Adding black borders

This commit is contained in:
Benjamin Bädorf 2020-11-19 18:58:56 +01:00
parent 0827583370
commit da4dcf9968
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
3 changed files with 310 additions and 76 deletions

16
Cargo.lock generated
View file

@ -1,5 +1,18 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.6" version = "0.4.6"
@ -73,11 +86,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
name = "wlstreamer" name = "wlstreamer"
version = "0.3.0" version = "0.3.0"
dependencies = [ dependencies = [
"itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[metadata] [metadata]
"checksum either 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" "checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
"checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" "checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" "checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"

View file

@ -7,3 +7,4 @@ edition = "2018"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"]} serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0" serde_json = "1.0"
itertools = "0.9"

View file

@ -1,66 +1,218 @@
use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashMap;
use std::env; use std::env;
use std::io::{BufRead, BufReader, Error}; use std::io::{BufRead, BufReader, Error};
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
struct SwayScreenRect {
x: usize,
y: usize,
width: usize,
height: usize,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct SwayWorkspace { struct SwayWorkspace {
id: u32, id: usize,
name: String, name: String,
focus: Vec<u32>, focus: Vec<usize>,
output: String, output: String,
focused: bool, focused: bool,
rect: SwayScreenRect,
visible: bool, visible: bool,
num: u32, num: usize,
#[serde(rename = "type")] #[serde(rename = "type")]
type_name: String, type_name: String,
representation: String, representation: String,
} }
#[derive(Serialize, Deserialize, Clone, Debug)]
struct SwayOutputMode {
width: usize,
height: usize,
refresh: usize,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct SwayOutput {
id: usize,
name: String,
rect: SwayScreenRect,
layout: String,
orientation: String,
#[serde(rename = "type")]
type_name: String,
make: String,
model: String,
serial: String,
current_mode: SwayOutputMode,
focused: bool,
}
#[derive(Copy, Clone, Hash, Eq, Debug)]
struct Resolution {
height: usize,
width: usize,
}
#[derive(Debug)]
struct Config { struct Config {
current_screen: String, current_output: String,
output: String, devices_from: usize,
current_device_index: usize,
screen_blacklist: Vec<String>, screen_blacklist: Vec<String>,
workspace_blacklist: Vec<u32>, workspace_blacklist: Vec<usize>,
verbose: bool, verbose: bool,
resolutions: Vec<Resolution>,
outputs: HashMap<Resolution, usize>,
}
impl PartialEq<Resolution> for Resolution {
fn eq(&self, other: &Resolution) -> bool {
self.width == other.width && self.height == other.height
}
} }
const VERSION: &'static str = env!("CARGO_PKG_VERSION"); const VERSION: &'static str = env!("CARGO_PKG_VERSION");
fn help() { fn help() {
println!("Wrapper around wf-recorder and ffmpeg that automatically switches the screen being recorded based on current window focus");
println!("");
println!("Usage: wlstreamer [options]"); println!("Usage: wlstreamer [options]");
println!("Wrapper around wf-recorder and ffmpeg that automatically switches the screen being recorded based on current window focus");
println!(""); println!("");
println!("Options:"); println!("Options:");
println!(" --not-ws <ws-num> Do not show this workspace. Can be used multiple times. Example: 3"); println!(" --not-ws <ws-num> Do not show this workspace. Can be used multiple times. Example: 3");
println!(" --not-screen <screen> Do not show this screen. Can be used multiple times. Example: HDMI-A-1"); println!(" --not-screen <screen> Do not show this screen. Can be used multiple times. Example: HDMI-A-1");
println!(" -o|--output <output> Output to this device. Defaults to /dev/video0"); println!(" -d|--devices-from <id> Use video devices starting at $id. Defaults to 0. /dev/video$id will be used as output. See DIFFERENT RESOLUTIONS below.");
println!(" -v|--version Display version and exit"); println!(" -v|--version Display version and exit");
println!(" --verbose Verbose logging"); println!(" --verbose Verbose logging");
println!(""); println!("");
println!( println!(
"If there are no screens available for streaming, a black screen will be shown instead." "If there are no screens available for streaming, a black screen will be shown instead."
); );
println!("");
println!("DIFFERENT RESOLUTIONS");
println!("");
println!("When running outputs with different resolutions, the resulting stream will be the smallest possible resolution that can fit all output resolutions.");
println!("For example, two outputs, one 1600x1200, another 1920x1080, will result in an output stream of 1920x1200. Any remaining space will be padded black.");
println!("Another example, two outputs, one 640x480, another 1920x1080, will result in an output stream of 1920x1080. Space will only be padded black on the smaller screen.");
println!("");
println!("To support this behaviour, wlstreamer needs access to a v4l2loopback device for each resolution, included the combined upscaled one if applicable.");
println!("For the first example above, this would mean you would need 3 devices. For the second, you'd need two. If all your outputs have the same resolution, you only need an output device.");
println!("");
println!("DYNAMICALLY CHANGING RESOLUTIONS");
println!("");
println!("As long as you have enough v4l2loopback devices available for new resolutions, it should be fine to change resolutions on an output.");
println!("However, if your resolution is either wider or taller than the output resolution, this will result in failures, since dynamically changing the v4l2loopback device resolution is not possible.");
std::process::exit(0); std::process::exit(0);
} }
fn record_screen(config: &mut Config, valid_screens: &Vec<String>) -> Result<Child, Error> { fn stream_black(config: &mut Config) -> Result<Vec<Child>, Error> {
if valid_screens.len() == 0 { let mut cmd = Command::new("ffmpeg")
let cmd = Command::new("ffmpeg") .args(&[
"-i",
format!(
"color=c=black:s={}x{}:r=25/1",
config.resolutions[0].width, config.resolutions[0].height
)
.as_str(),
"-vcodec",
"rawvideo",
"-pix_fmt",
"yuyv422",
"-f",
"v4l2",
format!("/dev/video{}", config.devices_from).as_str(),
])
.stdin(Stdio::piped())
.stdout(if config.verbose {
Stdio::piped()
} else {
Stdio::inherit()
})
.stderr(if config.verbose {
Stdio::piped()
} else {
Stdio::inherit()
})
.spawn()?;
config.current_output = "".to_string();
return Ok(vec![cmd]);
}
fn record_screen(config: &mut Config, output: SwayOutput) -> Result<Vec<Child>, Error> {
let resolution = Resolution {
height: output.current_mode.height,
width: output.current_mode.width,
};
let device_number = match config.outputs.get(&resolution) {
Some(device_number) => *device_number,
None => {
config
.outputs
.insert(resolution, config.current_device_index);
config.current_device_index += 1;
config.current_device_index
}
};
if config.verbose {
println!("Using device number {}", device_number);
}
let output_str = format!("--file=/dev/video{}", device_number);
let screen_str = format!("-o{}", output.name.as_str());
let recorder = Command::new("wf-recorder")
.args(&[
"--muxer=v4l2",
"--codec=rawvideo",
"--pixel-format=yuyv422",
screen_str.as_str(),
output_str.as_str(),
])
.stdin(Stdio::piped())
.stdout(if config.verbose {
Stdio::inherit()
} else {
Stdio::piped()
})
.stderr(if config.verbose {
Stdio::inherit()
} else {
Stdio::piped()
})
.spawn()?;
config.current_output = output.name.as_str().to_string();
let mut processes = vec![recorder];
if device_number != config.devices_from {
if config.verbose {
println!("Does not have the maximum combined resolution, filtering through ffmpeg");
}
let upscaler = Command::new("ffmpeg")
.args(&[ .args(&[
"-f",
"lavfi",
"-i", "-i",
"color=c=black:s=1920x1080:r=25/1", format!("/dev/video{}", device_number).as_str(),
"-vcodec", "-vcodec",
"rawvideo", "rawvideo",
"-pix_fmt", "-pix_fmt",
"yuyv422", "yuyv422",
"-f", "-f",
"v4l2", "v4l2",
config.output.as_str(), "-vf",
format!("scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2,setsar=1",
config.resolutions[0].width, config.resolutions[0].height,
config.resolutions[0].width, config.resolutions[0].height).as_str(),
format!("/dev/video{}", config.devices_from).as_str(),
]) ])
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(if config.verbose { .stdout(if config.verbose {
@ -75,46 +227,94 @@ fn record_screen(config: &mut Config, valid_screens: &Vec<String>) -> Result<Chi
}) })
.spawn()?; .spawn()?;
config.current_screen = "".to_string(); processes.push(upscaler);
}
format!("/dev/video{}", device_number).as_str();
return Ok(cmd); return Ok(processes);
} else {
let output_str = format!("--file={}", config.output.as_str());
let screen_str = format!("-o{}", valid_screens[0]);
println!("Outputting to {}", config.output.as_str());
let cmd = Command::new("wf-recorder")
.args(&[
"--muxer=v4l2",
"--codec=rawvideo",
"--pixel-format=yuyv422",
screen_str.as_str(),
output_str.as_str(),
])
.stdin(Stdio::piped())
.stdout(if config.verbose {
Stdio::inherit()
} else {
Stdio::piped()
})
.stderr(if config.verbose {
Stdio::inherit()
} else {
Stdio::piped()
})
.spawn()?;
config.current_screen = valid_screens[0].as_str().to_string();
return Ok(cmd);
};
} }
fn get_valid_screens_for_recording(config: &Config) -> Vec<String> { fn get_outputs(config: &mut Config) -> Vec<SwayOutput> {
let mut command = "swaymsg -t get_workspaces"; let command = "swaymsg -t get_outputs";
let output = Command::new("sh") let output = Command::new("sh")
.args(&["-c", command]) .args(&["-c", command])
.output() .output()
.expect("Couldn't get current focus"); .expect("Error running swaymsg");
let stdout_string = String::from_utf8(output.stdout).expect("Invalid UTF-8 from get_outputs");
let outputs: Vec<SwayOutput> =
serde_json::from_str(stdout_string.as_str()).expect("Invalid json from get_outputs");
if config.verbose {
println!("Found outputs");
for elem in outputs.iter() {
println!("{:?}", elem);
}
}
return outputs;
}
fn get_output(config: &mut Config, screen: &str) -> SwayOutput {
let outputs = get_outputs(config);
let output = match outputs.iter().find(|o| o.name == screen) {
Some(o) => o.to_owned(),
None => panic!("Could not find output"),
};
return output.clone();
}
fn get_resolutions(config: &mut Config) -> Vec<Resolution> {
let outputs = get_outputs(config);
let mut resolutions: Vec<Resolution> = outputs
.iter()
.map(|o| Resolution {
height: o.current_mode.height,
width: o.current_mode.width,
})
.unique()
.collect_vec();
if config.verbose {
println!("{:?}", resolutions);
}
let combined_resolution: Resolution = resolutions.iter().fold(
Resolution {
width: 0,
height: 0,
},
|acc, r| Resolution {
height: if acc.height > r.height {
acc.height
} else {
r.height
},
width: if acc.width > r.width {
acc.width
} else {
r.width
},
},
);
if config.verbose {
println!("Combined maximum resolution {:?}", combined_resolution);
}
resolutions.insert(0, combined_resolution);
resolutions = resolutions.into_iter().unique().collect_vec();
return resolutions;
}
fn get_valid_screens_for_recording(config: &Config) -> Vec<SwayWorkspace> {
let command = "swaymsg -t get_workspaces";
let output = Command::new("sh")
.args(&["-c", command])
.output()
.expect("Error running swaymsg");
let stdout_string = let stdout_string =
String::from_utf8(output.stdout).expect("Invalid UTF-8 from get_workspaces"); String::from_utf8(output.stdout).expect("Invalid UTF-8 from get_workspaces");
@ -156,16 +356,20 @@ fn get_valid_screens_for_recording(config: &Config) -> Vec<String> {
Ordering::Greater Ordering::Greater
} }
}); });
return workspaces.into_iter().map(|w| w.output).collect();
return workspaces;
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = Config { let mut config = Config {
current_screen: "".to_string(), current_output: "".to_string(),
output: "/dev/video0".to_string(), devices_from: 0,
current_device_index: 0,
screen_blacklist: Vec::new(), screen_blacklist: Vec::new(),
workspace_blacklist: Vec::new(), workspace_blacklist: Vec::new(),
verbose: false, verbose: false,
resolutions: Vec::new(),
outputs: HashMap::new(),
}; };
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
@ -180,13 +384,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
i += 1; i += 1;
config config
.workspace_blacklist .workspace_blacklist
.push(args[i].clone().parse::<u32>().unwrap()); .push(args[i].clone().parse::<usize>().unwrap());
} else if arg == "--not-screen" { } else if arg == "--not-screen" {
i += 1; i += 1;
config.screen_blacklist.push(args[i].clone()); config.screen_blacklist.push(args[i].clone());
} else if arg == "-o" || arg == "--output" { } else if arg == "-d" || arg == "--devices-from" {
i += 1; i += 1;
config.output = args[i].clone(); config.devices_from = args[i].clone().parse::<usize>().unwrap();
config.current_device_index = config.devices_from;
} else if arg == "--verbose" { } else if arg == "--verbose" {
config.verbose = true; config.verbose = true;
} else if arg == "-v" || arg == "--version" { } else if arg == "-v" || arg == "--version" {
@ -201,8 +406,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
i += 1; i += 1;
} }
let valid_screens = get_valid_screens_for_recording(&config); config.resolutions = get_resolutions(&mut config);
let mut recorder = record_screen(&mut config, &valid_screens)?; config
.outputs
.insert(config.resolutions[0], config.devices_from);
config.current_device_index += 1;
let stdout = match Command::new("sh") let stdout = match Command::new("sh")
.args(&["-c", "swaymsg -t subscribe -m \"['window']\""]) .args(&["-c", "swaymsg -t subscribe -m \"['window']\""])
@ -213,28 +421,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.stdout .stdout
{ {
Some(stdout) => stdout, Some(stdout) => stdout,
None => panic!("Could not open stdout"), None => panic!("Could not open swaymsg stdout"),
}; };
let reader = BufReader::new(stdout); let reader = BufReader::new(stdout);
reader.lines().filter_map(|line| line.ok()).for_each(|_| { reader
println!("Switched focus"); .lines()
let valid_screens = get_valid_screens_for_recording(&config); .filter_map(|line| line.ok())
if valid_screens.len() > 0 && valid_screens[0] == config.current_screen { .for_each(|_| -> Result<(), Error> {
return; println!("Switched focus");
} let valid_screens = get_valid_screens_for_recording(&config);
if valid_screens.len() > 0 && valid_screens[0].output == config.current_output {
return Ok(());
}
let output = get_output(&mut config, valid_screens[0].output.as_str());
for recorder in recorders.iter() {
match recorder.kill() {
Ok(_) => {}
Err(err) => panic!("{:?}", err),
};
}
match recorder.kill() { let mut recorders: Vec<Child> = if valid_screens.len() != 0 {
Ok(_) => {} stream_black(&mut config)?
Err(err) => panic!("{:?}", err), } else {
}; let output = get_output(&mut config, valid_screens[0].output.as_str());
record_screen(&mut config, output)?
};
println!("Recording {}", config.current_output);
recorder = match record_screen(&mut config, &valid_screens) { Ok(())
Ok(recorder) => recorder, });
Err(err) => panic!("{:?}", err),
};
println!("Recording {}", config.current_screen);
});
Ok(()) Ok(())
} }