Adding black borders
This commit is contained in:
parent
0827583370
commit
da4dcf9968
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -1,5 +1,18 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# 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]]
|
||||
name = "itoa"
|
||||
version = "0.4.6"
|
||||
|
@ -73,11 +86,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
name = "wlstreamer"
|
||||
version = "0.3.0"
|
||||
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_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[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 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"
|
||||
|
|
|
@ -7,3 +7,4 @@ edition = "2018"
|
|||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"]}
|
||||
serde_json = "1.0"
|
||||
itertools = "0.9"
|
||||
|
|
311
src/main.rs
311
src/main.rs
|
@ -1,66 +1,131 @@
|
|||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::io::{BufRead, BufReader, Error};
|
||||
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 {
|
||||
id: u32,
|
||||
id: usize,
|
||||
name: String,
|
||||
focus: Vec<u32>,
|
||||
focus: Vec<usize>,
|
||||
output: String,
|
||||
focused: bool,
|
||||
rect: SwayScreenRect,
|
||||
visible: bool,
|
||||
num: u32,
|
||||
num: usize,
|
||||
#[serde(rename = "type")]
|
||||
type_name: 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 {
|
||||
current_screen: String,
|
||||
output: String,
|
||||
current_output: String,
|
||||
devices_from: usize,
|
||||
current_device_index: usize,
|
||||
screen_blacklist: Vec<String>,
|
||||
workspace_blacklist: Vec<u32>,
|
||||
workspace_blacklist: Vec<usize>,
|
||||
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");
|
||||
|
||||
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!("Wrapper around wf-recorder and ffmpeg that automatically switches the screen being recorded based on current window focus");
|
||||
println!("");
|
||||
println!("Options:");
|
||||
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!(" -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!(" --verbose Verbose logging");
|
||||
println!("");
|
||||
println!(
|
||||
"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);
|
||||
}
|
||||
|
||||
fn record_screen(config: &mut Config, valid_screens: &Vec<String>) -> Result<Child, Error> {
|
||||
if valid_screens.len() == 0 {
|
||||
let cmd = Command::new("ffmpeg")
|
||||
fn stream_black(config: &mut Config) -> Result<Vec<Child>, Error> {
|
||||
let mut cmd = Command::new("ffmpeg")
|
||||
.args(&[
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=black:s=1920x1080:r=25/1",
|
||||
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",
|
||||
config.output.as_str(),
|
||||
format!("/dev/video{}", config.devices_from).as_str(),
|
||||
])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(if config.verbose {
|
||||
|
@ -75,14 +140,35 @@ fn record_screen(config: &mut Config, valid_screens: &Vec<String>) -> Result<Chi
|
|||
})
|
||||
.spawn()?;
|
||||
|
||||
config.current_screen = "".to_string();
|
||||
config.current_output = "".to_string();
|
||||
|
||||
return Ok(cmd);
|
||||
} 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")
|
||||
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",
|
||||
|
@ -103,18 +189,132 @@ fn record_screen(config: &mut Config, valid_screens: &Vec<String>) -> Result<Chi
|
|||
})
|
||||
.spawn()?;
|
||||
|
||||
config.current_screen = valid_screens[0].as_str().to_string();
|
||||
config.current_output = output.name.as_str().to_string();
|
||||
|
||||
return Ok(cmd);
|
||||
};
|
||||
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");
|
||||
}
|
||||
|
||||
fn get_valid_screens_for_recording(config: &Config) -> Vec<String> {
|
||||
let mut command = "swaymsg -t get_workspaces";
|
||||
let upscaler = Command::new("ffmpeg")
|
||||
.args(&[
|
||||
"-i",
|
||||
format!("/dev/video{}", device_number).as_str(),
|
||||
"-vcodec",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"yuyv422",
|
||||
"-f",
|
||||
"v4l2",
|
||||
"-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())
|
||||
.stdout(if config.verbose {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::inherit()
|
||||
})
|
||||
.stderr(if config.verbose {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::inherit()
|
||||
})
|
||||
.spawn()?;
|
||||
|
||||
processes.push(upscaler);
|
||||
}
|
||||
format!("/dev/video{}", device_number).as_str();
|
||||
|
||||
return Ok(processes);
|
||||
}
|
||||
|
||||
fn get_outputs(config: &mut Config) -> Vec<SwayOutput> {
|
||||
let command = "swaymsg -t get_outputs";
|
||||
let output = Command::new("sh")
|
||||
.args(&["-c", command])
|
||||
.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 =
|
||||
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
|
||||
}
|
||||
});
|
||||
return workspaces.into_iter().map(|w| w.output).collect();
|
||||
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut config = Config {
|
||||
current_screen: "".to_string(),
|
||||
output: "/dev/video0".to_string(),
|
||||
current_output: "".to_string(),
|
||||
devices_from: 0,
|
||||
current_device_index: 0,
|
||||
screen_blacklist: Vec::new(),
|
||||
workspace_blacklist: Vec::new(),
|
||||
verbose: false,
|
||||
resolutions: Vec::new(),
|
||||
outputs: HashMap::new(),
|
||||
};
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
|
@ -180,13 +384,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
i += 1;
|
||||
config
|
||||
.workspace_blacklist
|
||||
.push(args[i].clone().parse::<u32>().unwrap());
|
||||
.push(args[i].clone().parse::<usize>().unwrap());
|
||||
} else if arg == "--not-screen" {
|
||||
i += 1;
|
||||
config.screen_blacklist.push(args[i].clone());
|
||||
} else if arg == "-o" || arg == "--output" {
|
||||
} else if arg == "-d" || arg == "--devices-from" {
|
||||
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" {
|
||||
config.verbose = true;
|
||||
} else if arg == "-v" || arg == "--version" {
|
||||
|
@ -201,8 +406,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
i += 1;
|
||||
}
|
||||
|
||||
let valid_screens = get_valid_screens_for_recording(&config);
|
||||
let mut recorder = record_screen(&mut config, &valid_screens)?;
|
||||
config.resolutions = get_resolutions(&mut config);
|
||||
config
|
||||
.outputs
|
||||
.insert(config.resolutions[0], config.devices_from);
|
||||
config.current_device_index += 1;
|
||||
|
||||
let stdout = match Command::new("sh")
|
||||
.args(&["-c", "swaymsg -t subscribe -m \"['window']\""])
|
||||
|
@ -213,27 +421,36 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.stdout
|
||||
{
|
||||
Some(stdout) => stdout,
|
||||
None => panic!("Could not open stdout"),
|
||||
None => panic!("Could not open swaymsg stdout"),
|
||||
};
|
||||
|
||||
let reader = BufReader::new(stdout);
|
||||
reader.lines().filter_map(|line| line.ok()).for_each(|_| {
|
||||
reader
|
||||
.lines()
|
||||
.filter_map(|line| line.ok())
|
||||
.for_each(|_| -> Result<(), Error> {
|
||||
println!("Switched focus");
|
||||
let valid_screens = get_valid_screens_for_recording(&config);
|
||||
if valid_screens.len() > 0 && valid_screens[0] == config.current_screen {
|
||||
return;
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
recorder = match record_screen(&mut config, &valid_screens) {
|
||||
Ok(recorder) => recorder,
|
||||
Err(err) => panic!("{:?}", err),
|
||||
let mut recorders: Vec<Child> = if valid_screens.len() != 0 {
|
||||
stream_black(&mut config)?
|
||||
} else {
|
||||
let output = get_output(&mut config, valid_screens[0].output.as_str());
|
||||
record_screen(&mut config, output)?
|
||||
};
|
||||
println!("Recording {}", config.current_screen);
|
||||
println!("Recording {}", config.current_output);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in a new issue