mirror of
https://github.com/xlmnxp/blue-recorder.git
synced 2025-03-31 23:04:55 +03:00
include blue-recorder-core in same repo
This commit is contained in:
parent
a035f60052
commit
ae9ed6cdd5
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
/target
|
||||
*.mo
|
||||
**/target
|
||||
*.mo
|
||||
cargo.lock
|
||||
**/cargo.lock
|
30
Cargo.lock
generated
30
Cargo.lock
generated
@ -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",
|
||||
|
30
Cargo.toml
30
Cargo.toml
@ -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
208
blue-recorder-core/Cargo.lock
generated
Normal 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"
|
10
blue-recorder-core/Cargo.toml
Normal file
10
blue-recorder-core/Cargo.toml
Normal 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"
|
354
blue-recorder-core/src/ffmpeg_linux.rs
Normal file
354
blue-recorder-core/src/ffmpeg_linux.rs
Normal 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(())
|
||||
}
|
||||
|
||||
}
|
122
blue-recorder-core/src/ffmpeg_windows.rs
Normal file
122
blue-recorder-core/src/ffmpeg_windows.rs
Normal 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(())
|
||||
}
|
||||
}
|
7
blue-recorder-core/src/lib.rs
Normal file
7
blue-recorder-core/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ffmpeg_linux;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod ffmpeg_windows;
|
||||
|
||||
pub mod utils;
|
64
blue-recorder-core/src/utils.rs
Normal file
64
blue-recorder-core/src/utils.rs
Normal 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
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
25
blue-recorder/Cargo.toml
Normal 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"] }
|
Loading…
Reference in New Issue
Block a user