From b959262fbad0530962749ab753e12b38c6d16b68 Mon Sep 17 00:00:00 2001 From: Salem Yaslem Date: Thu, 19 Oct 2023 06:58:46 +0300 Subject: [PATCH] add wayland record and mouse toggle --- Cargo.toml | 2 +- src/ffmpeg_interface.rs | 183 ++++++++++++++++++++++++++++------------ src/main.rs | 5 +- src/utils.rs | 4 + src/wayland_record.rs | 57 ++++++++----- 5 files changed, 175 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d6d5fe..b38b720 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,4 @@ subprocess = "0.2.6" dark-light = "1.0.0" async-std = {version = "1.12.0", features = ["attributes"]} gstreamer = "0.20.5" -zbus = "3.12.0" \ No newline at end of file +zbus = "3.12.0" diff --git a/src/ffmpeg_interface.rs b/src/ffmpeg_interface.rs index 0a3b68b..3cc669b 100644 --- a/src/ffmpeg_interface.rs +++ b/src/ffmpeg_interface.rs @@ -1,4 +1,6 @@ extern crate subprocess; +use crate::utils::{is_snap, is_wayland}; +use crate::wayland_record::{CursorModeTypes, RecordTypes, WaylandRecorder}; use chrono::prelude::*; use gettextrs::gettext; use gtk::{prelude::*, ResponseType}; @@ -12,7 +14,6 @@ use std::sync::mpsc::Sender; use std::thread::sleep; use std::time::Duration; use subprocess::Exec; -use crate::wayland_record::WaylandRecorder; #[derive(Clone)] pub struct ProgressWidget { @@ -61,7 +62,8 @@ pub struct Ffmpeg { pub unbound: Option>, pub progress_widget: ProgressWidget, pub window: Window, - pub record_wayland: WaylandRecorder + pub record_wayland: WaylandRecorder, + pub main_context: gtk::glib::MainContext, } impl Ffmpeg { @@ -99,7 +101,7 @@ impl Ffmpeg { &gettext("File already exist. Do you want to overwrite it?"), ); - let answer = glib::MainContext::default().block_on(message_dialog.run_future()); + let answer = self.main_context.block_on(message_dialog.run_future()); message_dialog.close(); if answer != ResponseType::Yes { @@ -107,23 +109,7 @@ impl Ffmpeg { } } - if self.record_audio.is_active() { - let mut ffmpeg_command = Command::new("ffmpeg"); - ffmpeg_command.arg("-f"); - ffmpeg_command.arg("pulse"); - ffmpeg_command.arg("-i"); - ffmpeg_command.arg(&self.audio_id.active_id().unwrap()); - ffmpeg_command.arg("-f"); - ffmpeg_command.arg("ogg"); - ffmpeg_command.arg(format!( - "{}.temp.audio", - self.saved_filename.as_ref().unwrap() - )); - ffmpeg_command.arg("-y"); - self.audio_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn().unwrap()))); - } - - if self.record_video.is_active() { + if self.record_video.is_active() && !is_wayland() { let mut ffmpeg_command: Command = Command::new("ffmpeg"); // record video with specified width and hight @@ -183,19 +169,57 @@ impl Ffmpeg { // 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() { + sleep(Duration::from_secs(self.record_delay.value() as u64)); + if !self + .main_context + .block_on(self.record_wayland.start( + format!( + "{}.temp.without.audio.webm", + self.saved_filename.as_ref().unwrap() + ), + 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 = Command::new("ffmpeg"); + ffmpeg_command.arg("-f"); + ffmpeg_command.arg("pulse"); + ffmpeg_command.arg("-i"); + ffmpeg_command.arg(&self.audio_id.active_id().unwrap()); + ffmpeg_command.arg("-f"); + ffmpeg_command.arg("ogg"); + ffmpeg_command.arg(format!( + "{}.temp.audio", + self.saved_filename.as_ref().unwrap() + )); + ffmpeg_command.arg("-y"); + self.audio_process = Some(Rc::new(RefCell::new(ffmpeg_command.spawn().unwrap()))); } Some(()) } - pub fn stop_record(&self) { + pub fn stop_record(&mut self) { self.progress_widget.show(); - // kill the process to stop recording - self.progress_widget.set_progress("".to_string(), 1, 6); + self.progress_widget.set_progress("".to_string(), 1, 7); + // kill the process to stop recording if self.video_process.is_some() { self.progress_widget - .set_progress("Stop Recording Video".to_string(), 1, 6); + .set_progress("Stop Recording Video".to_string(), 1, 7); Command::new("kill") .arg(format!( @@ -213,13 +237,16 @@ impl Ffmpeg { .unwrap(); println!("video killed"); + } else if is_wayland() { + self.main_context + .block_on(self.record_wayland.stop()); } - self.progress_widget.set_progress("".to_string(), 2, 6); + self.progress_widget.set_progress("".to_string(), 2, 7); if self.audio_process.is_some() { self.progress_widget - .set_progress("Stop Recording Audio".to_string(), 2, 6); + .set_progress("Stop Recording Audio".to_string(), 2, 7); Command::new("kill") .arg(format!( @@ -238,35 +265,83 @@ impl Ffmpeg { println!("audio killed"); } - let video_filename = format!( - "{}.temp.without.audio.{}", - self.saved_filename.as_ref().unwrap(), - self.filename.2.active_id().unwrap() - ); + let video_filename = { + if is_wayland() { + format!( + "{}.temp.without.audio.webm", + self.saved_filename.as_ref().unwrap() + ) + } 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_video_record = { + if is_wayland() { + std::path::Path::new(&format!( + "{}.temp.without.audio.webm", + self.saved_filename.as_ref().unwrap() + )) + .exists() + } else { + 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 { - 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 !is_wayland() { + 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(); + } else { + println!("convert webm to specified format"); - self.progress_widget.set_progress("".to_string(), 4, 6); + // convert webm to specified format + self.progress_widget.set_progress( + "Convert screen-cast to specified format".to_string(), + 4, + 7, + ); + + Command::new("ffmpeg") + .args([ + "-i", + format!( + "{}.temp.without.audio.webm", + self.saved_filename.as_ref().unwrap() + ) + .as_str(), + "-crf", + "23", // default quality + "-c:a", + self.filename.2.active_id().unwrap().as_str(), + self.saved_filename.as_ref().unwrap(), + "-y", + ]) + .output() + .unwrap(); + } + + self.progress_widget.set_progress("".to_string(), 5, 7); // if audio record, then merge video with audio if is_audio_record { self.progress_widget - .set_progress("Save Audio Recording".to_string(), 4, 6); + .set_progress("Save Audio Recording".to_string(), 5, 7); Command::new("ffmpeg") .args([ @@ -276,15 +351,17 @@ impl Ffmpeg { "ogg", "-i", audio_filename.as_str(), - "-c:v", - "copy", + "-crf", + "23", // default quality "-c:a", "aac", self.saved_filename.as_ref().unwrap(), "-y", ]) - .output() - .expect("failed to merge video with audio"); + .spawn() + .expect("failed to merge video with audio") + .wait() + .unwrap(); std::fs::remove_file(video_filename).unwrap(); std::fs::remove_file(audio_filename).unwrap(); @@ -293,7 +370,7 @@ impl Ffmpeg { // if only audio is recording then convert it to chosen format else if is_audio_record { self.progress_widget - .set_progress("Convert Audio to choosen format".to_string(), 4, 6); + .set_progress("Convert Audio to choosen format".to_string(), 5, 7); Command::new("ffmpeg") .args([ @@ -309,7 +386,7 @@ impl Ffmpeg { std::fs::remove_file(audio_filename).unwrap(); } - self.progress_widget.set_progress("".to_string(), 5, 6); + self.progress_widget.set_progress("".to_string(), 6, 7); // execute command after finish recording if self.command.text().trim() != "" { @@ -322,7 +399,7 @@ impl Ffmpeg { } self.progress_widget - .set_progress("Finish".to_string(), 6, 6); + .set_progress("Finish".to_string(), 7, 7); self.progress_widget.hide(); } @@ -344,7 +421,3 @@ impl Ffmpeg { } } } - -fn is_snap() -> bool { - !std::env::var("SNAP").unwrap_or_default().is_empty() -} diff --git a/src/main.rs b/src/main.rs index 097844c..4994122 100644 --- a/src/main.rs +++ b/src/main.rs @@ -448,6 +448,8 @@ pub fn build_ui(application: &Application) { let _delay_spin = delay_spin.clone(); + let main_context = glib::MainContext::default(); + let wayland_record = main_context.block_on(WaylandRecorder::new()); // Init record struct let ffmpeg_record_interface: Rc> = Rc::new(RefCell::new(Ffmpeg { filename: ( @@ -469,7 +471,8 @@ pub fn build_ui(application: &Application) { progress_widget: ProgressWidget::new(progress_dialog, progress_bar), window: main_window.clone(), record_delay: delay_spin, - record_wayland: glib::MainContext::default().block_on(WaylandRecorder::new()) + record_wayland: wayland_record, + main_context: main_context, })); // Record Button diff --git a/src/utils.rs b/src/utils.rs index 0a2b68c..ca07b52 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,4 +2,8 @@ pub fn is_wayland() -> bool { std::env::var("XDG_SESSION_TYPE") .unwrap_or_default() .eq_ignore_ascii_case("wayland") +} + +pub fn is_snap() -> bool { + !std::env::var("SNAP").unwrap_or_default().is_empty() } \ No newline at end of file diff --git a/src/wayland_record.rs b/src/wayland_record.rs index 7bd35b3..a829d64 100644 --- a/src/wayland_record.rs +++ b/src/wayland_record.rs @@ -5,7 +5,7 @@ use zbus::{ dbus_proxy, export::futures_util::TryStreamExt, zvariant::{ObjectPath, OwnedObjectPath, Structure, Value}, - Connection, MessageStream, MessageType, Result, + Connection, MessageStream, MessageType, Result }; #[derive(Clone, Copy)] @@ -13,6 +13,7 @@ pub enum RecordTypes { Default, Monitor, Window, + MonitorOrWindow, } #[derive(Clone, Copy)] @@ -46,6 +47,7 @@ trait ScreenCast { pub struct WaylandRecorder { connection: Connection, screen_cast_proxy: ScreenCastProxy<'static>, + session_path: String, pipeline: Option, filename: String, } @@ -53,32 +55,43 @@ pub struct WaylandRecorder { impl WaylandRecorder { pub async fn new() -> Self { let connection = Connection::session().await.expect("failed to connect to session bus"); - let screen_cast_proxy = ScreenCastProxy::new(&connection).await.expect("failed to create dbus proxy"); + let screen_cast_proxy = ScreenCastProxy::new(&connection).await.expect("failed to create dbus proxy for screen-cast"); gst::init().expect("failed to initialize gstreamer"); WaylandRecorder { connection, screen_cast_proxy, + session_path: String::new(), filename: String::from("blue_recorder.webm"), pipeline: None, } } - pub async fn start(&mut self, record_type: RecordTypes, cursor_mode_type: CursorModeTypes) -> Result<()> { + pub async fn start(&mut self, filename: String, record_type: RecordTypes, cursor_mode_type: CursorModeTypes) -> bool { self.screen_cast_proxy.create_session(HashMap::from([ - ("handle_token", Value::from("blue_recorder_1")), - ("session_handle_token", Value::from("blue_recorder_1")), - ])) - .await?; + ("handle_token", Value::from("blue_recorder_1")), + ("session_handle_token", Value::from("blue_recorder_1")), + ])) + .await.expect("failed to create session"); let mut message_stream = MessageStream::from(self.connection.clone()); + + self.filename = filename.clone(); - while let Some(msg) = message_stream.try_next().await? { + let mut first_empty_signal_called = false; + + while let Some(msg) = message_stream.try_next().await.expect("failed to get message") { match msg.message_type() { MessageType::Signal => { - let (_, response) = msg.body::<(u32, HashMap<&str, Value>)>()?; + let (_, response) = msg.body::<(u32, HashMap<&str, Value>)>().expect("failed to get body"); + if response.len() == 0 { - continue; + if first_empty_signal_called { + return false; + } else { + first_empty_signal_called = true; + continue; + } } if response.contains_key("session_handle") { @@ -88,35 +101,38 @@ impl WaylandRecorder { record_type, cursor_mode_type ) - .await?; + .await.expect("failed to handle session"); continue; } if response.contains_key("streams") { - // TODO: start recording on separate thread - self.record_screen_cast(response.clone()).await?; + self.record_screen_cast(response.clone()).await.expect("failed to record screen cast"); break; } } - MessageType::MethodReturn => { - println!("\n\nMethodReturn message: {:?}", msg); - } _ => { println!("\n\nUnkown message: {:?}", msg); } } } - Ok(()) + true } - pub fn stop(self) { - if let Some(pipeline) = self.pipeline { + pub async fn stop(&mut self) { + if let Some(pipeline) = self.pipeline.clone() { pipeline .set_state(gst::State::Null) .expect("failed to stop pipeline"); } + + if self.session_path.len() > 0 { + println!("Closing session...: {:?}", self.session_path.replace("request", "session")); + self.connection.clone().call_method(Some("org.freedesktop.portal.Desktop"), self.session_path.clone().replace("request", "session"), Some("org.freedesktop.portal.Session"), "Close", &()).await.expect("failed to close session"); + self.session_path = String::new(); + } } + async fn handle_session( &mut self, screen_cast_proxy: ScreenCastProxy<'_>, @@ -131,6 +147,8 @@ impl WaylandRecorder { .downcast::() .expect("cannot down cast session_handle"); + self.session_path = response_session_handle.clone(); + screen_cast_proxy .select_sources( ObjectPath::try_from(response_session_handle.clone())?, @@ -141,6 +159,7 @@ impl WaylandRecorder { Value::from(match record_type { RecordTypes::Monitor => 1u32, RecordTypes::Window => 2u32, + RecordTypes::MonitorOrWindow => 3u32, _ => 0u32, }), ),