mirror of
https://github.com/xlmnxp/blue-recorder.git
synced 2024-11-23 17:13:11 +03:00
add wayland recorder
This commit is contained in:
parent
f857ba542c
commit
329c4a7d09
1479
Cargo.lock
generated
1479
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ dirs = "4.0.0"
|
||||
gdk = { git = "https://github.com/gtk-rs/gtk4-rs.git", package = "gdk4"}
|
||||
gdk-pixbuf = "0.9.0"
|
||||
gettext-rs = "0.7.0"
|
||||
gtk = { version = "0.4.8", package = "gtk4" }
|
||||
gtk = { version = "0.4.8", package = "gtk4", features = ["v4_8"] }
|
||||
gtk-sys = "0.15"
|
||||
glib = "0.10.3"
|
||||
rust-ini = "0.16"
|
||||
@ -19,6 +19,9 @@ regex = "1.4.3"
|
||||
secfmt = "0.1.1"
|
||||
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"
|
||||
|
||||
[dependencies.gio]
|
||||
version = "0.15.0"
|
||||
|
@ -1 +0,0 @@
|
||||
// TODO: add wayland screen record support
|
223
src/wayland_record.rs
Normal file
223
src/wayland_record.rs
Normal file
@ -0,0 +1,223 @@
|
||||
use gst::prelude::*;
|
||||
use gstreamer as gst;
|
||||
use std::collections::HashMap;
|
||||
use zbus::{
|
||||
dbus_proxy,
|
||||
export::futures_util::TryStreamExt,
|
||||
zvariant::{ObjectPath, OwnedObjectPath, Structure, Value},
|
||||
Connection, MessageStream, MessageType, Result,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum RecordTypes {
|
||||
Default,
|
||||
Monitor,
|
||||
Window,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CursorModeTypes {
|
||||
Default,
|
||||
Hidden,
|
||||
Show
|
||||
}
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.freedesktop.portal.ScreenCast",
|
||||
default_service = "org.freedesktop.portal.Desktop",
|
||||
default_path = "/org/freedesktop/portal/desktop"
|
||||
)]
|
||||
trait ScreenCast {
|
||||
async fn create_session(&self, options: HashMap<&str, Value<'_>>) -> Result<OwnedObjectPath>;
|
||||
async fn select_sources(
|
||||
&self,
|
||||
session_handle: ObjectPath<'_>,
|
||||
options: HashMap<&str, Value<'_>>,
|
||||
) -> Result<OwnedObjectPath>;
|
||||
async fn start(
|
||||
&self,
|
||||
session_handle: ObjectPath<'_>,
|
||||
parent_window: &str,
|
||||
options: HashMap<&str, Value<'_>>,
|
||||
) -> Result<OwnedObjectPath>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WaylandRecorder {
|
||||
connection: Connection,
|
||||
screen_cast_proxy: ScreenCastProxy<'static>,
|
||||
pipeline: Option<gst::Pipeline>,
|
||||
filename: String,
|
||||
}
|
||||
|
||||
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");
|
||||
gst::init().expect("failed to initialize gstreamer");
|
||||
|
||||
WaylandRecorder {
|
||||
connection,
|
||||
screen_cast_proxy,
|
||||
filename: String::from("blue_recorder.webm"),
|
||||
pipeline: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&mut self, record_type: RecordTypes, cursor_mode_type: CursorModeTypes) -> Result<()> {
|
||||
self.screen_cast_proxy.create_session(HashMap::from([
|
||||
("handle_token", Value::from("blue_recorder_1")),
|
||||
("session_handle_token", Value::from("blue_recorder_1")),
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let mut message_stream = MessageStream::from(self.connection.clone());
|
||||
|
||||
while let Some(msg) = message_stream.try_next().await? {
|
||||
match msg.message_type() {
|
||||
MessageType::Signal => {
|
||||
let (_, response) = msg.body::<(u32, HashMap<&str, Value>)>()?;
|
||||
if response.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if response.contains_key("session_handle") {
|
||||
self.handle_session(
|
||||
self.screen_cast_proxy.clone(),
|
||||
response.clone(),
|
||||
record_type,
|
||||
cursor_mode_type
|
||||
)
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if response.contains_key("streams") {
|
||||
// TODO: start recording on separate thread
|
||||
self.record_screen_cast(response.clone()).await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
MessageType::MethodReturn => {
|
||||
println!("\n\nMethodReturn message: {:?}", msg);
|
||||
}
|
||||
_ => {
|
||||
println!("\n\nUnkown message: {:?}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(self) {
|
||||
if let Some(pipeline) = self.pipeline {
|
||||
pipeline
|
||||
.set_state(gst::State::Null)
|
||||
.expect("failed to stop pipeline");
|
||||
}
|
||||
}
|
||||
async fn handle_session(
|
||||
&mut self,
|
||||
screen_cast_proxy: ScreenCastProxy<'_>,
|
||||
response: HashMap<&str, Value<'_>>,
|
||||
record_type: RecordTypes,
|
||||
cursor_mode_type: CursorModeTypes
|
||||
) -> Result<()> {
|
||||
let response_session_handle = response
|
||||
.get("session_handle")
|
||||
.expect("cannot get session_handle")
|
||||
.clone()
|
||||
.downcast::<String>()
|
||||
.expect("cannot down cast session_handle");
|
||||
|
||||
screen_cast_proxy
|
||||
.select_sources(
|
||||
ObjectPath::try_from(response_session_handle.clone())?,
|
||||
HashMap::from([
|
||||
("handle_token", Value::from("blue_recorder_1")),
|
||||
(
|
||||
"types",
|
||||
Value::from(match record_type {
|
||||
RecordTypes::Monitor => 1u32,
|
||||
RecordTypes::Window => 2u32,
|
||||
_ => 0u32,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"cursor_mode",
|
||||
Value::from(match cursor_mode_type {
|
||||
CursorModeTypes::Hidden => 1u32,
|
||||
CursorModeTypes::Show => 2u32,
|
||||
_ => 0u32,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.await?;
|
||||
|
||||
screen_cast_proxy
|
||||
.start(
|
||||
ObjectPath::try_from(response_session_handle.clone())?,
|
||||
"parent_window",
|
||||
HashMap::from([("handle_token", Value::from("blue_recorder_1"))]),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn record_screen_cast(&mut self, response: HashMap<&str, Value<'_>>) -> Result<()> {
|
||||
let streams: &Value<'_> = response.get("streams").expect("cannot get streams");
|
||||
|
||||
// get fields from nested structure inside elements
|
||||
// NOTICE: this is not the best way to get node_id, but it works for now
|
||||
let stream_node_id: u32 = streams
|
||||
.clone()
|
||||
.downcast::<Vec<Value>>()
|
||||
.expect("cannot down cast streams to vec array")
|
||||
.get(0)
|
||||
.expect("cannot get first object from streams array")
|
||||
.clone()
|
||||
.downcast::<Structure>()
|
||||
.expect("cannot down cast first object to structure")
|
||||
.fields()
|
||||
.get(0)
|
||||
.expect("cannot get first field from structure")
|
||||
.clone()
|
||||
.downcast::<u32>()
|
||||
.expect("cannot down cast first field to u32");
|
||||
|
||||
// launch gstreamer pipeline
|
||||
let gst_element: gst::Element = gst::parse_launch(&format!(
|
||||
"pipewiresrc path={stream_node_id} ! videorate ! video/x-raw,framerate=30/1 ! videoconvert chroma-mode=none dither=none matrix-mode=output-only ! vp8enc max-quantizer=17 deadline=1 keyframe-mode=disabled buffer-size=20000 ! webmmux ! filesink location={filename}",
|
||||
filename = self.filename
|
||||
)).expect("failed to launch gstreamer pipeline");
|
||||
|
||||
// start pipeline
|
||||
let pipeline: gst::Pipeline = gst_element
|
||||
.dynamic_cast::<gst::Pipeline>()
|
||||
.expect("pipeline error");
|
||||
|
||||
self.pipeline = Some(pipeline.clone());
|
||||
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.expect("failed to start pipeline");
|
||||
|
||||
println!("Recording Wayland screen cast...");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// #[async_std::main]
|
||||
// async fn main() -> Result<()> {
|
||||
// let mut recorder = WaylandRecorder::new().await;
|
||||
// recorder.start(RecordTypes::Monitor, CursorModeTypes::Show).await?;
|
||||
|
||||
// // wait for user input to stop recording
|
||||
// let mut input = String::new();
|
||||
// std::io::stdin().read_line(&mut input).unwrap();
|
||||
|
||||
// recorder.stop();
|
||||
// Ok(())
|
||||
// }
|
Loading…
Reference in New Issue
Block a user