blue-recorder/src/ffmpeg_interface.rs
2024-07-06 02:57:31 +02:00
Ask

471 lines
19 KiB
Rust

{8fcc96c6acf24b68482cc4ca69f2b4d995556e9c true 19504 ffmpeg_interface.rs 0xc001deabd0}

extern crate subprocess;
use crate::config_management;
use crate::utils::{get_bundle, is_snap, is_wayland};
use crate::wayland_record::{CursorModeTypes, RecordTypes, WaylandRecorder};
use chrono::prelude::*;
use ffmpeg_sidecar::child::FfmpegChild;
use ffmpeg_sidecar::command::FfmpegCommand;
use gtk::{prelude::*, ResponseType, TextBuffer, TextView};
use gtk::{ButtonsType, DialogFlags, MessageDialog, MessageType};
use gtk::{CheckButton, ComboBoxText, Entry, FileChooserNative, Label, SpinButton, Window};
use std::cell::RefCell;
use std::path::PathBuf;
use std::process::Command;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::thread::sleep;
use std::time::Duration;
use subprocess::Exec;
use filename::Filename;
#[derive(Clone)]
pub struct Ffmpeg {
pub filename: (FileChooserNative, Entry, ComboBoxText),
pub record_video: CheckButton,
pub record_audio: CheckButton,
pub audio_id: ComboBoxText,
pub record_mouse: CheckButton,
pub follow_mouse: CheckButton,
pub record_frames: SpinButton,
pub record_delay: SpinButton,
pub command: Entry,
pub video_process: Option<Rc<RefCell<FfmpegChild>>>,
pub audio_process: Option<Rc<RefCell<FfmpegChild>>>,
pub height: Option<u16>,
pub saved_filename: Option<String>,
pub unbound: Option<Sender<bool>>,
pub window: Window,
pub record_wayland: WaylandRecorder,
pub record_window: Rc<RefCell<bool>>,
pub main_context: gtk::glib::MainContext,
pub temp_video_filename: String,
pub bundle: String,
pub video_record_bitrate: SpinButton,
pub audio_record_bitrate: SpinButton,
pub error_window: MessageDialog,
pub error_window_text: Label,
pub error_details: TextView,
}
impl Ffmpeg {
pub fn start_record(&mut self, x: u16, y: u16, width: u16, height: u16) -> Option<()> {
self.saved_filename = Some(
self.filename
.0
.file()
.unwrap()
.path()
.unwrap()
.join(PathBuf::from(format!(
"{}.{}",
if self.filename.1.text().to_string().trim().eq("") {
Utc::now().to_string().replace(" UTC", "").replace(' ', "-")
} else {
self.filename.1.text().to_string().trim().to_string()
},
self.filename.2.active_id().unwrap()
)))
.as_path()
.display()
.to_string(),
);
let is_file_already_exists =
std::path::Path::new(&self.saved_filename.clone().unwrap()).exists();
if is_file_already_exists {
let message_dialog = MessageDialog::new(
Some(&self.window),
DialogFlags::all(),
MessageType::Warning,
ButtonsType::YesNo,
&self.bundle,
);
let answer = self.main_context.block_on(message_dialog.run_future());
message_dialog.close();
if answer != ResponseType::Yes {
return None;
}
}
if self.record_video.is_active() && !is_wayland() && self.filename.2.active_id().unwrap().as_str() != "gif" {
let mode = config_management::get("default", "mode");
let format = "x11grab";
let display = format!("{}+{},{}",
std::env::var("DISPLAY").unwrap_or_else(|_| ":0".to_string())
.as_str(),
x,
y
);
let mut ffmpeg_command = FfmpegCommand::new();
// record video with specified width and hight
if self.follow_mouse.is_active() && mode.as_str() == "screen" {
let width = width as f32 * 0.95;
let height = height as f32 * 0.95;
ffmpeg_command.size(width as u32, height as u32);
} else {
ffmpeg_command.size(width.into(), height.into());
}
// if show mouse switch is enabled, draw the mouse to video
if self.record_mouse.is_active() {
ffmpeg_command.args(["-draw_mouse", "1"]);
} else {
ffmpeg_command.args(["-draw_mouse", "0"]);
};
// if follow mouse switch is enabled, follow the mouse
if self.follow_mouse.is_active() {
ffmpeg_command.args(["-follow_mouse", "centered"]);
}
// Disable frame rate if value is zero
if self.record_frames.value() > 0.0 {
ffmpeg_command.args(["-framerate", &self.record_frames.value().to_string()]);
}
// Video format && input
ffmpeg_command.format(format)
.input(display);
// Disable bitrate if value is zero
if self.video_record_bitrate.value() > 0.0 {
ffmpeg_command.args([
"-b:v",
&format!("{}K", self.video_record_bitrate.value()),
]);
}
let video_filename = format!(
"{}.temp.without.audio.{}",
self.saved_filename.as_ref().unwrap(),
self.filename.2.active_id().unwrap()
);
// Output
ffmpeg_command.args([
{
if self.record_audio.is_active() {
video_filename.as_str()
} else {
self.saved_filename.as_ref().unwrap()
}
},
]);
ffmpeg_command.overwrite();
// sleep for delay
sleep(Duration::from_secs(self.record_delay.value() as u64));
// start recording and return the process id
self.video_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn().unwrap())));
} else if self.record_video.is_active() && !is_wayland() && self.filename.2.active_id().unwrap().as_str() == "gif" {
let tempfile = tempfile::Builder::new().suffix(".mp4")
.tempfile().expect("cannot create temp file")
.keep().expect("cannot keep temp file");
self.temp_video_filename = tempfile.0.file_name().expect("cannot get file name")
.to_str().unwrap().to_string();
let mode = config_management::get("default", "mode");
let format = "x11grab";
let display = format!("{}+{},{}",
std::env::var("DISPLAY").unwrap_or_else(|_| ":0".to_string())
.as_str(),
x,
y
);
let mut ffmpeg_command = FfmpegCommand::new();
// record video with specified width and hight
if self.follow_mouse.is_active() && mode.as_str() == "screen" {
let width = width as f32 * 0.95;
let height = height as f32 * 0.95;
ffmpeg_command.size(width as u32, height as u32);
} else {
ffmpeg_command.size(width.into(), height.into());
}
// if show mouse switch is enabled, draw the mouse to video
if self.record_mouse.is_active() {
ffmpeg_command.args(["-draw_mouse", "1"]);
} else {
ffmpeg_command.args(["-draw_mouse", "0"]);
};
// if follow mouse switch is enabled, follow the mouse
if self.follow_mouse.is_active() {
ffmpeg_command.args(["-follow_mouse", "centered"]);
}
// Disable frame rate if value is zero
if self.record_frames.value() > 0.0 {
ffmpeg_command.args(["-framerate", &self.record_frames.value().to_string()]);
}
// Video format && input
ffmpeg_command.format(format)
.input(display);
// Disable bitrate if value is zero
if self.video_record_bitrate.value() > 0.0 {
ffmpeg_command.args([
"-b:v",
&format!("{}K", self.video_record_bitrate.value()),
]);
}
// Output
ffmpeg_command.arg(self.temp_video_filename.clone())
.overwrite();
// sleep for delay
sleep(Duration::from_secs(self.record_delay.value() as u64));
// start recording and return the process id
self.video_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn().unwrap())));
self.height = Some(height);
} else if self.record_video.is_active() && is_wayland() {
sleep(Duration::from_secs(self.record_delay.value() as u64));
let tempfile = tempfile::NamedTempFile::new().expect("cannot create temp file").keep().expect("cannot keep temp file");
self.temp_video_filename = tempfile.0.file_name().expect("cannot get file name").to_str().unwrap().to_string();
let record_window = self.record_window.take();
self.record_window.replace(record_window);
if !self.main_context.block_on(self.record_wayland.start(
self.temp_video_filename.clone(),
if record_window {
RecordTypes::Window
} else {
RecordTypes::Monitor
},
{
if self.record_mouse.is_active() {
CursorModeTypes::Show
} else {
CursorModeTypes::Hidden
}
},
)) {
println!("failed to start recording");
return None;
}
}
if self.record_audio.is_active() {
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.format("pulse")
.input(&self.audio_id.active_id().unwrap())
.format("ogg");
ffmpeg_command.arg(format!(
"{}.temp.audio",
self.saved_filename.as_ref().unwrap()
));
ffmpeg_command.overwrite();
self.audio_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn().unwrap())));
}
Some(())
}
pub fn stop_record(&mut self) {
// kill the process to stop recording
if self.video_process.is_some() {
self.video_process
.clone()
.unwrap()
.borrow_mut()
.quit()
.unwrap();
} else if is_wayland() {
self.main_context.block_on(self.record_wayland.stop());
}
if self.audio_process.is_some() {
self.audio_process
.clone()
.unwrap()
.borrow_mut()
.quit()
.unwrap();
}
let video_filename = {
if is_wayland() {
self.temp_video_filename.clone()
} else if !is_wayland() && self.filename.2.active_id().unwrap().as_str() == "gif" {
self.temp_video_filename.clone()
} else {
format!(
"{}.temp.without.audio.{}",
self.saved_filename.as_ref().unwrap(),
self.filename.2.active_id().unwrap()
)
}
};
let audio_filename = format!("{}.temp.audio", self.saved_filename.as_ref().unwrap());
let is_video_record = {
std::path::Path::new(video_filename.as_str()).exists()
};
let is_audio_record = std::path::Path::new(audio_filename.as_str()).exists();
if is_video_record {
if is_wayland() && self.filename.2.active_id().unwrap().as_str() != "gif" {
// convert webm to specified format
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.input(self.temp_video_filename.as_str());
if self.video_record_bitrate.value() > 0.0 {
ffmpeg_command.args([
"-b:v",
&format!("{}K", self.video_record_bitrate.value()),
]);
}
ffmpeg_command.args([
"-c:a",
self.filename.2.active_id().unwrap().as_str(),
self.saved_filename.as_ref().unwrap(),
]).overwrite()
.spawn()
.unwrap().wait().unwrap();
} else if is_wayland() && self.filename.2.active_id().unwrap().as_str() == "gif" {
let fps = 100/self.record_frames.value_as_int();
let scale = self.height.unwrap();
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.input(self.temp_video_filename.as_str())
.filter_complex(
format!("fps={},scale={}:-1:flags=lanczos,[0]split[s0][s1]; [s0]palettegen[p]; [s1][p]paletteuse",
fps,scale));
if self.video_record_bitrate.value() > 0.0 {
ffmpeg_command.args([
"-b:v",
&format!("{}K", self.video_record_bitrate.value()),
]);
}
ffmpeg_command.args(["-loop", "0"])
.args([
self.filename.2.active_id().unwrap().as_str(),
self.saved_filename.as_ref().unwrap(),
])
.overwrite().spawn().unwrap().wait().expect("failed to convert video to gif");
if is_audio_record {
std::fs::remove_file(audio_filename.clone()).unwrap();
}
} else if !is_wayland() && self.filename.2.active_id().unwrap().as_str() == "gif" {
// convert mp4 to gif
let fps = 100/self.record_frames.value_as_int();
let scale = self.height.unwrap();
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.input(format!("file:{}", video_filename.as_str()))
.filter_complex(
format!("fps={},scale={}:-1:flags=lanczos,[0]split[s0][s1]; [s0]palettegen[p]; [s1][p]paletteuse",
fps,scale)
)
.args(["-loop", "0"])
.output(self.saved_filename.as_ref().unwrap())
.overwrite().spawn().unwrap().wait().expect("failed to convert video to gif");
if is_audio_record {
std::fs::remove_file(audio_filename.clone()).unwrap();
}
} else {
let mut move_command = Command::new("mv");
move_command.args([
self.saved_filename.as_ref().unwrap().as_str(),
if is_audio_record {
video_filename.as_str()
} else {
self.saved_filename.as_ref().unwrap()
},
]);
move_command.output().unwrap();
}
// if audio record, then merge video and audio
if is_audio_record && self.filename.2.active_id().unwrap().as_str() != "gif" {
FfmpegCommand::new().input(video_filename.as_str())
.format("ogg")
.input(audio_filename.as_str())
.args([
"-c:a",
"aac",
self.saved_filename.as_ref().unwrap(),
])
.overwrite()
.spawn()
.unwrap()
.wait()
.expect("failed to merge video and audio");
std::fs::remove_file(audio_filename).unwrap();
}
std::fs::remove_file(video_filename).unwrap();
}
// if only audio is recording then convert it to chosen format
else if is_audio_record {
let mut ffmpeg_command = FfmpegCommand::new();
ffmpeg_command.format("ogg").input(audio_filename.as_str()).arg(
self.saved_filename.as_ref().unwrap(),
).spawn()
.unwrap()
.wait()
.expect("failed convert audio to video");
std::fs::remove_file(audio_filename).unwrap();
}
// execute command after finish recording
if self.command.text().trim() != "" {
Exec::shell(self.command.text().trim()).popen().unwrap();
}
}
pub fn play_record(self) {
if self.saved_filename.is_some() {
if is_snap() {
// open the video using snapctrl for snap package
let snapctl = Command::new("snapctl")
.arg("user-open")
.arg(self.saved_filename.unwrap())
.spawn();
match snapctl {
Ok(_) => {
// Continue
},
Err(error) => {
let text_buffer = TextBuffer::new(None);
text_buffer.set_text(&error.to_string());
self.error_window.set_title(Some(&get_bundle("error-title", None)));
self.error_window_text.set_label(&get_bundle("play-error", None));
self.error_details.set_buffer(Some(&text_buffer));
self.error_window.set_transient_for(Some(&self.window));
self.error_window.show();
self.error_window.set_hide_on_close(true);
},
}
} else {
let open_file = open::that(self.saved_filename.unwrap());
match open_file {
Ok(_) => {
// Continue
},
Err(error) => {
let text_buffer = TextBuffer::new(None);
text_buffer.set_text(&error.to_string());
self.error_window.set_title(Some(&get_bundle("error-title", None)));
self.error_window_text.set_label(&get_bundle("play-error", None));
self.error_details.set_buffer(Some(&text_buffer));
self.error_window.set_transient_for(Some(&self.window));
self.error_window.show();
self.error_window.set_hide_on_close(true);
},
}
}
}
}
}