include blue-recorder-core in same repo

This commit is contained in:
Salem Yaslem 2024-12-20 22:08:00 +03:00
parent 6faba6e6ee
commit b090f02beb
24 changed files with 5980 additions and 41 deletions

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
/target
*.mo
**/target
*.mo
cargo.lock
**/cargo.lock

30
Cargo.lock generated
View File

@ -486,12 +486,10 @@ dependencies = [
[[package]]
name = "blue-recorder-core"
version = "0.1.0"
source = "git+https://github.com/ochibani/blue-recorder-core.git?rev=ae9fa71#ae9fa71d47c846221239c405fce051fa843b6527"
dependencies = [
"anyhow",
"ffmpeg-sidecar",
"open",
"rdev",
"tempfile",
]
@ -557,9 +555,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.4"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"jobserver",
"libc",
@ -1938,9 +1936,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "hyper"
version = "1.5.1"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
dependencies = [
"bytes",
"futures-channel",
@ -1958,9 +1956,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.3"
version = "0.27.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http",
@ -2406,9 +2404,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.168"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libfuzzer-sys"
@ -2551,9 +2549,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
dependencies = [
"adler2",
"simd-adler32",
@ -2957,9 +2955,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "png"
version = "0.17.15"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
@ -3518,9 +3516,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.1"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
dependencies = [
"core-foundation-sys 0.8.7",
"libc",

View File

@ -1,25 +1,7 @@
[package]
name = "blue-recorder"
version = "0.1.0"
edition = "2021"
[workspace]
resolver = "2"
[dependencies]
anyhow = "1.0.86"
async-std = {version = "1.12.0", features = ["attributes"]}
blue-recorder-core = { git = "https://github.com/ochibani/blue-recorder-core.git", rev = "ae9fa71" }
cpal = "0.15.3"
dark-light = "1.0.0"
dirs = "4.0.0"
fluent-bundle = "0.15.3"
glib = "0.10.3"
libadwaita = { version = "0.1.1" }
rdev = "0.5"
regex = "1.4.3"
rust-ini = "0.16"
secfmt = "0.1.1"
# Windows-only dependency
[target.'cfg(windows)'.dependencies]
display-info = "0.5.1"
x-win = "2.0.2"
winapi = { version = "0.3", features = ["winuser"] }
members = [
"blue-recorder-core",
"blue-recorder",
]

208
blue-recorder-core/Cargo.lock generated Normal file
View File

@ -0,0 +1,208 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "blue-recorder-core"
version = "0.1.0"
dependencies = [
"anyhow",
"ffmpeg-sidecar",
"open",
"tempfile",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "ffmpeg-sidecar"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec830aba6cd3da7621c58e8f06727e3b750e8383bcd934d789f2b9c3c4ea595"
dependencies = [
"anyhow",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "open"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"rustix",
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@ -0,0 +1,10 @@
[package]
name = "blue-recorder-core"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
ffmpeg-sidecar = "1.1.0"
open = "5.1.4"
tempfile = "3.10.1"

View File

@ -0,0 +1,354 @@
use anyhow::{anyhow, Error, Result};
use ffmpeg_sidecar::child::FfmpegChild;
use ffmpeg_sidecar::command::FfmpegCommand;
use tempfile;
use std::{cell::RefCell, time::Instant};
use std::path::Path;
use std::rc::Rc;
use std::thread::sleep;
use std::time::Duration;
use crate::utils::{is_input_audio_record, is_output_audio_record, is_valide, is_video_record, RecordMode};
#[derive(Clone)]
pub struct Ffmpeg {
pub audio_input_id: String,
pub audio_output_id: String,
pub filename: String,
pub output: String,
pub temp_input_audio_filename: String,
pub temp_output_audio_filename: String,
pub temp_video_filename: String,
pub height: Option<u16>,
pub input_audio_process: Option<Rc<RefCell<FfmpegChild>>>,
pub output_audio_process: Option<Rc<RefCell<FfmpegChild>>>,
pub video_process: Option<Rc<RefCell<FfmpegChild>>>,
pub audio_record_bitrate: u16,
pub record_delay: u16,
pub record_frames: u16,
pub video_record_bitrate: u16,
pub follow_mouse: bool,
pub record_mouse: bool,
}
impl Ffmpeg {
// Start video recording
pub fn start_video(&mut self, width: u16, height: u16, x: u16, y: u16, mode: RecordMode) -> Result<()> {
let display = format!("{}+{},{}",
std::env::var("DISPLAY").unwrap_or_else(|_| ":0".to_string())
.as_str(),
x,
y
);
let mut ffmpeg_command = FfmpegCommand::new();
let format = "x11grab";
self.height = Some(height);
// Record video to tmp if audio record enabled
if !self.audio_input_id.is_empty()
|| !self.audio_output_id.is_empty()
|| self.output == "gif"
{
let suffix = if self.output == "gif" {
".mp4"
} else {
&format!(".{}", &self.output)
};
let video_tempfile = tempfile::Builder::new().prefix("ffmpeg-video-")
.suffix(suffix)
.tempfile()?
.keep()?;
self.temp_video_filename = Path::new(&video_tempfile.1).to_string_lossy()
.to_string();
}
// Record video with specified width and hight
if self.follow_mouse {
match mode {
RecordMode::Screen => {
let width = width as f32 * 0.95;
let height = height as f32 * 0.95;
ffmpeg_command.size(width as u32, height as u32);
},
_=> {
ffmpeg_command.size(width.into(), height.into());
}
}
} else {
ffmpeg_command.size(width.into(), height.into());
}
// If show mouse switch is enabled, draw the mouse to video
if self.record_mouse {
ffmpeg_command.args(["-draw_mouse", "1"]);
} else {
ffmpeg_command.args(["-draw_mouse", "0"]);
};
// Follow the mouse
if self.follow_mouse {
ffmpeg_command.args(["-follow_mouse", "centered"]);
}
// Disable frame rate if value is zero
if self.record_frames > 0 {
ffmpeg_command.args(["-framerate", &self.record_frames.to_string()]);
}
// Video format && input
ffmpeg_command.format(format)
.input(display);
// Disable bitrate if value is zero
if self.video_record_bitrate > 0 {
ffmpeg_command.args([
"-b:v",
&format!("{}K", self.video_record_bitrate),
]);
}
// tmp file
if self.audio_input_id.is_empty() &&
self.audio_output_id.is_empty() &&
self.output != "gif"
{
ffmpeg_command.args(["-hls_flags", "temp_file"]);
}
// Remove metadate
ffmpeg_command.args(["-map_metadata", "-1"]);
// Output
ffmpeg_command.args([
{
if !self.audio_input_id.is_empty()
|| !self.audio_output_id.is_empty()
|| self.output == "gif"
{
&self.temp_video_filename
} else {
&self.filename
}
},
]);
ffmpeg_command.overwrite();
// Sleep for delay
sleep(Duration::from_secs(self.record_delay as u64));
// Start recording and return the process id
self.video_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn()?)));
Ok(())
}
// Stop video recording
pub fn stop_video(&mut self) -> Result<()> {
// Quit the process to stop recording
if self.video_process.is_some() {
self.video_process
.clone()
.ok_or_else(|| anyhow!("not exiting the video recording process successfully"))?
.borrow_mut()
.quit()?;
}
Ok(())
}
// Start audio input recording
pub fn start_input_audio(&mut self) -> Result<()> {
let input_audio_tempfile = tempfile::Builder::new().prefix("ffmpeg-audio-")
.suffix(".ogg")
.tempfile()?
.keep()?;
self.temp_input_audio_filename = Path::new(&input_audio_tempfile.1).to_string_lossy()
.to_string();
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.format("pulse")
.input(&self.audio_input_id)
.format("ogg");
// Disable bitrate if value is zero
if self.audio_record_bitrate > 0 {
ffmpeg_command.args([
"-b:a",
&format!("{}K", self.audio_record_bitrate),
]);
}
// Remove metadate
ffmpeg_command.args(["-map_metadata", "-1"]);
ffmpeg_command.arg(&self.temp_input_audio_filename);
ffmpeg_command.overwrite();
// Sleep for delay
if !is_video_record(&self.temp_video_filename) {
sleep(Duration::from_secs(self.record_delay as u64));
}
// Start recording and return the process id
self.input_audio_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn()?)));
Ok(())
}
// Stop audio input recording
pub fn stop_input_audio(&mut self) -> Result<()> {
// Quit the process to stop recording
if self.input_audio_process.is_some() {
self.input_audio_process
.clone()
.ok_or_else(|| anyhow!("not exiting the input audio recording process successfully"))?
.borrow_mut()
.quit()?;
}
Ok(())
}
// Start audio output recording
pub fn start_output_audio(&mut self) -> Result<()> {
let output_audio_tempfile = tempfile::Builder::new().prefix("ffmpeg-audio-")
.suffix(".ogg")
.tempfile()?
.keep()?;
self.temp_output_audio_filename = Path::new(&output_audio_tempfile.1).to_string_lossy()
.to_string();
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.format("pulse")
.input(&self.audio_output_id)
.format("ogg");
// Remove metadate
ffmpeg_command.args(["-map_metadata", "-1"]);
ffmpeg_command.arg(&self.temp_output_audio_filename);
ffmpeg_command.overwrite();
// Sleep for delay
if !is_video_record(&self.temp_video_filename) && !is_input_audio_record(&self.temp_input_audio_filename) {
sleep(Duration::from_secs(self.record_delay as u64));
}
// Start recording and return the process id
self.output_audio_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn()?)));
Ok(())
}
// Stop audio output recording
pub fn stop_output_audio(&mut self) -> Result<()> {
// Quit the process to stop recording
if self.output_audio_process.is_some() {
self.output_audio_process
.clone()
.ok_or_else(|| anyhow!("not exiting the output audio recording process successfully"))?
.borrow_mut()
.quit()?;
}
Ok(())
}
// Merge tmp to target format
pub fn merge(&mut self) -> Result<()> {
if is_video_record(&self.temp_video_filename) {
if self.output != "gif" {
// Validate video file integrity
let start_time = Instant::now();
let duration = Duration::from_secs(60);
loop {
if is_valide(&self.temp_video_filename)? {
break;
} else if Instant::now().duration_since(start_time) >= duration {
return Err(Error::msg("Unable to validate tmp video file"));
}
}
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.input(&self.temp_video_filename);
ffmpeg_command.format("ogg");
if is_input_audio_record(&self.temp_input_audio_filename) {
ffmpeg_command.input(&self.temp_input_audio_filename);
}
if is_output_audio_record(&self.temp_output_audio_filename) {
ffmpeg_command.input(&self.temp_output_audio_filename);
}
ffmpeg_command.args([
"-c:a",
"aac",
&self.filename,
]);
ffmpeg_command.overwrite()
.spawn()?
.wait()?;
} else {
// Validate video file integrity
let start_time = Instant::now();
let duration = Duration::from_secs(60);
loop {
if is_valide(&self.temp_video_filename)? {
break;
} else if Instant::now().duration_since(start_time) >= duration {
return Err(Error::msg("Unable to validate tmp video file"));
}
}
// Convert MP4 to GIF
let filter = format!("fps={},scale={}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
self.record_frames,self.height.ok_or_else
(|| anyhow!("unable to get height value"))?);
let ffmpeg_convert = format!("ffmpeg -i file:{} -filter_complex '{}' \
-loop 0 {} -y", &self.temp_video_filename,filter,&self.filename);
std::process::Command::new("sh").arg("-c").arg(&ffmpeg_convert).output()?;
}
} else if is_input_audio_record(&self.temp_input_audio_filename) {
// Validate audio file integrity
let start_time = Instant::now();
let duration = Duration::from_secs(60);
loop {
if is_valide(&self.temp_input_audio_filename)? {
break;
} else if Instant::now().duration_since(start_time) >= duration {
return Err(Error::msg("Unable to validate tmp video file"));
}
}
// If only audio is recording then convert it to chosen format
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.format("ogg");
ffmpeg_command.input(&self.temp_input_audio_filename);
if is_output_audio_record(&self.temp_output_audio_filename) {
ffmpeg_command.input(&self.temp_output_audio_filename);
}
ffmpeg_command.args([
"-c:a",
"aac",
&self.filename,
]).overwrite()
.spawn()?
.wait()?;
} else {
// Validate audio file integrity
let start_time = Instant::now();
let duration = Duration::from_secs(60);
loop {
if is_valide(&self.temp_output_audio_filename)? {
break;
} else if Instant::now().duration_since(start_time) >= duration {
return Err(Error::msg("Unable to validate tmp video file"));
}
}
// If only output audio is recording then convert it to chosen format
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.format("ogg");
ffmpeg_command.input(&self.temp_output_audio_filename);
ffmpeg_command.arg(&self.filename)
.overwrite()
.spawn()?
.wait()?;
}
Ok(())
}
// Clean tmp
pub fn clean(&mut self) -> Result<()> {
let tmp_files = vec![ &self.temp_input_audio_filename, &self.temp_output_audio_filename, &self.temp_video_filename ];
for file in tmp_files {
if Path::new(file).try_exists()? {
std::fs::remove_file(file)?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,122 @@
use anyhow::{anyhow, Result};
use ffmpeg_sidecar::child::FfmpegChild;
use ffmpeg_sidecar::command::FfmpegCommand;
use tempfile;
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use std::thread::sleep;
use std::time::Duration;
#[derive(Clone)]
pub struct Ffmpeg {
pub filename: String,
pub output: String,
pub temp_video_filename: String,
pub window_title: String,
pub command: Option<String>,
pub audio_process: Option<Rc<RefCell<FfmpegChild>>>,
pub video_process: Option<Rc<RefCell<FfmpegChild>>>,
pub audio_record_bitrate: u16,
pub record_delay: u16,
pub record_frames: u16,
pub video_record_bitrate: u16,
pub record_audio: bool,
pub record_mouse: bool,
pub record_video: bool,
}
impl Ffmpeg {
// Start video recording
pub fn start_video(&mut self, width: u16, height: u16, x: u16, y: u16, mode: RecordMode) -> Result<()> {
let display = match mode {
RecordMode::Area => "desktop",
RecordMode::Screen => "desktop",
RecordMode::Window => &format!("title={}", &self.window_title),
};
let mut ffmpeg_command = FfmpegCommand::new();
let format = "gdigrab";
if self.output == "gif" {
self.output = String::from("mp4");
}
// Record video to tmp if audio record enabled
if self.record_audio {
let video_tempfile = tempfile::Builder::new().suffix(&format!(".{}", &self.output))
.tempfile()?
.keep()?;
self.temp_video_filename = Path::new(&video_tempfile.1).file_name()
.ok_or_else(|| anyhow!("cannot get video temporary file name"))?
.to_string_lossy()
.to_string();
}
// Video format
ffmpeg_command.format(format);
// if show mouse switch is enabled, draw the mouse to video
if self.record_mouse {
ffmpeg_command.args(["-draw_mouse", "1"]);
} else {
ffmpeg_command.args(["-draw_mouse", "0"]);
};
// Disable frame rate if value is zero
if self.record_frames > 0 {
ffmpeg_command.args(["-framerate", &self.record_frames.to_string()]);
}
// Record video with specified width and hight
ffmpeg_command.size(width.into(), height.into()).args([
"-offset_x", &x.to_string(),
"-offset_y", &y.to_string()
]);
// input
ffmpeg_command.input(display);
// Disable bitrate if value is zero
if self.video_record_bitrate > 0 {
ffmpeg_command.args([
"-b:v",
&format!("{}K", self.video_record_bitrate),
]);
}
// tmp file
if !self.record_audio {
ffmpeg_command.args(["-hls_flags", "temp_file"]);
}
// Output
ffmpeg_command.args([
{
if self.record_audio {
&self.temp_video_filename
} else {
&self.filename
}
},
]);
ffmpeg_command.overwrite();
// Sleep for delay
sleep(Duration::from_secs(self.record_delay as u64));
// Start recording and return the process id
self.video_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn()?)));
Ok(())
}
pub fn stop_video(&mut self) -> Result<()> {
// kill the process to stop recording
if self.video_process.is_some() {
self.video_process
.clone()
.ok_or_else(|| anyhow!("not exiting the video recording process successfully"))?
.borrow_mut()
.quit()?;
}
Ok(())
}
}

View File

@ -0,0 +1,7 @@
#[cfg(target_os = "linux")]
pub mod ffmpeg_linux;
#[cfg(target_os = "windows")]
pub mod ffmpeg_windows;
pub mod utils;

View File

@ -0,0 +1,64 @@
use anyhow::Result;
use std::process::Command;
// Select recording mode
#[derive(Clone, Copy)]
pub enum RecordMode {
Area,
Screen,
Window,
}
// Check if tmp input video file exist
pub fn is_input_audio_record(audio_filename: &str) -> bool {
std::path::Path::new(audio_filename).exists()
}
// Check if tmp output video file exist
pub fn is_output_audio_record(audio_filename: &str) -> bool {
std::path::Path::new(audio_filename).exists()
}
// Detect if snap package is used
pub fn is_snap() -> bool {
!std::env::var("SNAP").unwrap_or_default().is_empty()
}
// Validate audio/video file integrity
pub fn is_valide(filename: &str) -> Result<bool> {
let validate = Command::new("ffmpeg")
.args(["-v", "error",
"-i", filename,
"-f", "null", "-"
]).output()?;
if validate.status.success() {
Ok(true)
} else {
Ok(false)
}
}
// Check if tmp video file exist
pub fn is_video_record(video_filename: &str) -> bool {
std::path::Path::new(video_filename).exists()
}
// Detect wayland session
pub fn is_wayland() -> bool {
std::env::var("XDG_SESSION_TYPE")
.unwrap_or_default()
.eq_ignore_ascii_case("wayland")
}
// Play recorded file
pub fn play_record(file_name: &str) -> Result<()> {
if is_snap() {
// open the video using snapctrl for snap package
Command::new("snapctl").arg("user-open")
.arg(file_name)
.spawn()?;
} else {
open::that(file_name)?;
}
Ok(())
}

5166
blue-recorder/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
blue-recorder/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "blue-recorder"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
async-std = {version = "1.12.0", features = ["attributes"]}
blue-recorder-core = { path = "../blue-recorder-core" }
cpal = "0.15.3"
dark-light = "1.0.0"
dirs = "4.0.0"
fluent-bundle = "0.15.3"
glib = "0.10.3"
libadwaita = { version = "0.1.1" }
rdev = "0.5"
regex = "1.4.3"
rust-ini = "0.16"
secfmt = "0.1.1"
# Windows-only dependency
[target.'cfg(windows)'.dependencies]
display-info = "0.5.1"
x-win = "2.0.2"
winapi = { version = "0.3", features = ["winuser"] }