521 lines
17 KiB
Rust
521 lines
17 KiB
Rust
mod screencast_gui;
|
|
|
|
use {
|
|
crate::{
|
|
dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply},
|
|
pipewire::{
|
|
pw_ifs::pw_client_node::{
|
|
PwClientNode, PwClientNodeBufferConfig, PwClientNodeOwner, PwClientNodePort,
|
|
PwClientNodePortSupportedFormats, SUPPORTED_META_VIDEO_CROP,
|
|
},
|
|
pw_pod::{
|
|
spa_point, spa_rectangle, spa_region, PwPodRectangle, SPA_DATA_DmaBuf,
|
|
SPA_MEDIA_SUBTYPE_raw, SPA_MEDIA_TYPE_video, SpaChunkFlags, SPA_STATUS_HAVE_DATA,
|
|
},
|
|
},
|
|
portal::{
|
|
ptl_display::{PortalDisplay, PortalDisplayId, PortalOutput},
|
|
ptl_screencast::screencast_gui::SelectionGui,
|
|
PortalState, PORTAL_SUCCESS,
|
|
},
|
|
utils::{
|
|
clonecell::{CloneCell, UnsafeCellCloneSafe},
|
|
copyhashmap::CopyHashMap,
|
|
},
|
|
video::dmabuf::{DmaBuf, PlaneVec},
|
|
wire::jay_screencast::Ready,
|
|
wire_dbus::{
|
|
org,
|
|
org::freedesktop::impl_::portal::{
|
|
screen_cast::{
|
|
CreateSession, CreateSessionReply, SelectSources, SelectSourcesReply, Start,
|
|
StartReply,
|
|
},
|
|
session::{CloseReply as SessionCloseReply, Closed},
|
|
},
|
|
},
|
|
wl_usr::usr_ifs::{
|
|
usr_jay_screencast::{UsrJayScreencast, UsrJayScreencastOwner},
|
|
usr_jay_select_toplevel::UsrJaySelectToplevel,
|
|
usr_jay_toplevel::UsrJayToplevel,
|
|
},
|
|
},
|
|
std::{
|
|
borrow::Cow,
|
|
cell::{Cell, RefCell},
|
|
ops::Deref,
|
|
rc::Rc,
|
|
sync::atomic::Ordering::{Acquire, Relaxed, Release},
|
|
},
|
|
};
|
|
|
|
shared_ids!(ScreencastSessionId);
|
|
pub struct ScreencastSession {
|
|
_id: ScreencastSessionId,
|
|
state: Rc<PortalState>,
|
|
pub app: String,
|
|
session_obj: DbusObject,
|
|
pub phase: CloneCell<ScreencastPhase>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum ScreencastPhase {
|
|
Init,
|
|
SourcesSelected,
|
|
Selecting(Rc<SelectingScreencast>),
|
|
SelectingWindow(Rc<SelectingWindowScreencast>),
|
|
Starting(Rc<StartingScreencast>),
|
|
Started(Rc<StartedScreencast>),
|
|
Terminated,
|
|
}
|
|
|
|
unsafe impl UnsafeCellCloneSafe for ScreencastPhase {}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SelectingScreencastCore {
|
|
pub session: Rc<ScreencastSession>,
|
|
pub request_obj: Rc<DbusObject>,
|
|
pub reply: Rc<PendingReply<StartReply<'static>>>,
|
|
}
|
|
|
|
pub struct SelectingScreencast {
|
|
pub core: SelectingScreencastCore,
|
|
pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
|
|
}
|
|
|
|
pub struct SelectingWindowScreencast {
|
|
pub core: SelectingScreencastCore,
|
|
pub dpy: Rc<PortalDisplay>,
|
|
pub selector: Rc<UsrJaySelectToplevel>,
|
|
}
|
|
|
|
pub struct StartingScreencast {
|
|
pub session: Rc<ScreencastSession>,
|
|
pub request_obj: Rc<DbusObject>,
|
|
pub reply: Rc<PendingReply<StartReply<'static>>>,
|
|
pub node: Rc<PwClientNode>,
|
|
pub dpy: Rc<PortalDisplay>,
|
|
pub target: ScreencastTarget,
|
|
}
|
|
|
|
pub enum ScreencastTarget {
|
|
Output(Rc<PortalOutput>),
|
|
Toplevel(Rc<UsrJayToplevel>),
|
|
}
|
|
|
|
pub struct StartedScreencast {
|
|
session: Rc<ScreencastSession>,
|
|
node: Rc<PwClientNode>,
|
|
port: Rc<PwClientNodePort>,
|
|
buffers: RefCell<PlaneVec<DmaBuf>>,
|
|
buffers_valid: Cell<bool>,
|
|
dpy: Rc<PortalDisplay>,
|
|
jay_screencast: Rc<UsrJayScreencast>,
|
|
}
|
|
|
|
bitflags! {
|
|
CursorModes: u32;
|
|
|
|
HIDDEN = 1,
|
|
EMBEDDED = 2,
|
|
METADATA = 4,
|
|
}
|
|
|
|
bitflags! {
|
|
SourceTypes: u32;
|
|
|
|
MONITOR = 1,
|
|
WINDOW = 2,
|
|
}
|
|
|
|
impl PwClientNodeOwner for StartingScreencast {
|
|
fn bound_id(&self, node_id: u32) {
|
|
{
|
|
let inner_type = DynamicType::DictEntry(
|
|
Box::new(DynamicType::String),
|
|
Box::new(DynamicType::Variant),
|
|
);
|
|
let kt = DynamicType::Struct(vec![
|
|
DynamicType::U32,
|
|
DynamicType::Array(Box::new(inner_type.clone())),
|
|
]);
|
|
let variants = &[DictEntry {
|
|
key: "streams".into(),
|
|
value: Variant::Array(
|
|
kt,
|
|
vec![Variant::U32(node_id), Variant::Array(inner_type, vec![])],
|
|
),
|
|
}];
|
|
self.reply.ok(&StartReply {
|
|
response: PORTAL_SUCCESS,
|
|
results: Cow::Borrowed(variants),
|
|
});
|
|
}
|
|
let port = self.node.create_port(true);
|
|
port.can_alloc_buffers.set(true);
|
|
port.supported_metas.set(SUPPORTED_META_VIDEO_CROP);
|
|
let jsc = self.dpy.jc.create_screencast();
|
|
match &self.target {
|
|
ScreencastTarget::Output(o) => jsc.set_output(&o.jay),
|
|
ScreencastTarget::Toplevel(t) => jsc.set_toplevel(t),
|
|
}
|
|
jsc.set_use_linear_buffers(true);
|
|
jsc.set_allow_all_workspaces(true);
|
|
jsc.configure();
|
|
if let ScreencastTarget::Toplevel(t) = &self.target {
|
|
self.dpy.con.remove_obj(&**t);
|
|
}
|
|
let started = Rc::new(StartedScreencast {
|
|
session: self.session.clone(),
|
|
node: self.node.clone(),
|
|
port,
|
|
buffers: Default::default(),
|
|
buffers_valid: Cell::new(false),
|
|
dpy: self.dpy.clone(),
|
|
jay_screencast: jsc,
|
|
});
|
|
self.session
|
|
.phase
|
|
.set(ScreencastPhase::Started(started.clone()));
|
|
started.jay_screencast.owner.set(Some(started.clone()));
|
|
self.node.owner.set(Some(started.clone()));
|
|
}
|
|
}
|
|
|
|
impl PwClientNodeOwner for StartedScreencast {
|
|
fn port_format_changed(&self, port: &Rc<PwClientNodePort>) {
|
|
self.node.send_port_update(port, false);
|
|
}
|
|
|
|
fn use_buffers(&self, port: &Rc<PwClientNodePort>) {
|
|
self.node
|
|
.send_port_output_buffers(port, &self.buffers.borrow_mut());
|
|
self.buffers_valid.set(true);
|
|
}
|
|
|
|
fn start(self: Rc<Self>) {
|
|
self.jay_screencast.set_running(true);
|
|
self.jay_screencast.configure();
|
|
}
|
|
|
|
fn pause(self: Rc<Self>) {
|
|
self.jay_screencast.set_running(false);
|
|
self.jay_screencast.configure();
|
|
}
|
|
|
|
fn suspend(self: Rc<Self>) {
|
|
self.jay_screencast.set_running(false);
|
|
self.jay_screencast.configure();
|
|
}
|
|
}
|
|
|
|
impl SelectingScreencastCore {
|
|
pub fn starting(&self, dpy: &Rc<PortalDisplay>, target: ScreencastTarget) {
|
|
let node = dpy.state.pw_con.create_client_node(&[
|
|
("media.class".to_string(), "Video/Source".to_string()),
|
|
("node.name".to_string(), "jay-desktop-portal".to_string()),
|
|
("node.driver".to_string(), "true".to_string()),
|
|
]);
|
|
let starting = Rc::new(StartingScreencast {
|
|
session: self.session.clone(),
|
|
request_obj: self.request_obj.clone(),
|
|
reply: self.reply.clone(),
|
|
node,
|
|
dpy: dpy.clone(),
|
|
target,
|
|
});
|
|
self.session
|
|
.phase
|
|
.set(ScreencastPhase::Starting(starting.clone()));
|
|
starting.node.owner.set(Some(starting.clone()));
|
|
dpy.screencasts.set(
|
|
self.session.session_obj.path().to_owned(),
|
|
self.session.clone(),
|
|
);
|
|
}
|
|
}
|
|
|
|
impl ScreencastSession {
|
|
pub(super) fn kill(&self) {
|
|
self.session_obj.emit_signal(&Closed);
|
|
self.state.screencasts.remove(self.session_obj.path());
|
|
match self.phase.set(ScreencastPhase::Terminated) {
|
|
ScreencastPhase::Init => {}
|
|
ScreencastPhase::SourcesSelected => {}
|
|
ScreencastPhase::Terminated => {}
|
|
ScreencastPhase::Selecting(s) => {
|
|
s.core.reply.err("Session has been terminated");
|
|
for (_, gui) in s.guis.lock().drain() {
|
|
gui.kill(false);
|
|
}
|
|
}
|
|
ScreencastPhase::SelectingWindow(s) => {
|
|
s.dpy.con.remove_obj(&*s.selector);
|
|
s.core.reply.err("Session has been terminated");
|
|
}
|
|
ScreencastPhase::Starting(s) => {
|
|
s.reply.err("Session has been terminated");
|
|
s.node.con.destroy_obj(s.node.deref());
|
|
s.dpy.screencasts.remove(self.session_obj.path());
|
|
if let ScreencastTarget::Toplevel(t) = &s.target {
|
|
s.dpy.con.remove_obj(&**t);
|
|
}
|
|
}
|
|
ScreencastPhase::Started(s) => {
|
|
s.jay_screencast.con.remove_obj(s.jay_screencast.deref());
|
|
s.node.con.destroy_obj(s.node.deref());
|
|
s.dpy.screencasts.remove(self.session_obj.path());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn dbus_select_sources(
|
|
self: &Rc<Self>,
|
|
_req: SelectSources,
|
|
reply: PendingReply<SelectSourcesReply<'static>>,
|
|
) {
|
|
match self.phase.get() {
|
|
ScreencastPhase::Init => {}
|
|
_ => {
|
|
self.kill();
|
|
reply.err("Sources have already been selected");
|
|
return;
|
|
}
|
|
}
|
|
self.phase.set(ScreencastPhase::SourcesSelected);
|
|
reply.ok(&SelectSourcesReply {
|
|
response: PORTAL_SUCCESS,
|
|
results: Default::default(),
|
|
});
|
|
}
|
|
|
|
fn dbus_start(self: &Rc<Self>, req: Start<'_>, reply: PendingReply<StartReply<'static>>) {
|
|
match self.phase.get() {
|
|
ScreencastPhase::SourcesSelected => {}
|
|
_ => {
|
|
self.kill();
|
|
reply.err("Session is not in the correct phase for starting");
|
|
return;
|
|
}
|
|
}
|
|
let request_obj = match self.state.dbus.add_object(req.handle.to_string()) {
|
|
Ok(r) => r,
|
|
Err(_) => {
|
|
self.kill();
|
|
reply.err("Request handle is not unique");
|
|
return;
|
|
}
|
|
};
|
|
{
|
|
use org::freedesktop::impl_::portal::request::*;
|
|
request_obj.add_method::<Close, _>({
|
|
let slf = self.clone();
|
|
move |_, pr| {
|
|
slf.kill();
|
|
pr.ok(&CloseReply);
|
|
}
|
|
});
|
|
}
|
|
let guis = CopyHashMap::new();
|
|
for dpy in self.state.displays.lock().values() {
|
|
if dpy.outputs.len() > 0 {
|
|
guis.set(dpy.id, SelectionGui::new(self, dpy));
|
|
}
|
|
}
|
|
if guis.is_empty() {
|
|
self.kill();
|
|
reply.err("There are no running displays");
|
|
return;
|
|
}
|
|
self.phase
|
|
.set(ScreencastPhase::Selecting(Rc::new(SelectingScreencast {
|
|
core: SelectingScreencastCore {
|
|
session: self.clone(),
|
|
request_obj: Rc::new(request_obj),
|
|
reply: Rc::new(reply),
|
|
},
|
|
guis,
|
|
})));
|
|
}
|
|
}
|
|
|
|
impl UsrJayScreencastOwner for StartedScreencast {
|
|
fn buffers(&self, buffers: PlaneVec<DmaBuf>) {
|
|
if buffers.len() == 0 {
|
|
return;
|
|
}
|
|
let buffer = &buffers[0];
|
|
*self.port.supported_formats.borrow_mut() = Some(PwClientNodePortSupportedFormats {
|
|
media_type: Some(SPA_MEDIA_TYPE_video),
|
|
media_sub_type: Some(SPA_MEDIA_SUBTYPE_raw),
|
|
video_size: Some(PwPodRectangle {
|
|
width: buffer.width as _,
|
|
height: buffer.height as _,
|
|
}),
|
|
formats: vec![buffer.format],
|
|
modifiers: vec![buffer.modifier],
|
|
});
|
|
let bc = PwClientNodeBufferConfig {
|
|
num_buffers: buffers.len(),
|
|
planes: buffer.planes.len(),
|
|
stride: Some(buffer.planes[0].stride),
|
|
size: Some(buffer.planes[0].stride * buffer.height as u32),
|
|
align: 16,
|
|
data_type: SPA_DATA_DmaBuf,
|
|
};
|
|
self.port.buffer_config.set(Some(bc));
|
|
self.node.send_port_update(&self.port, true);
|
|
self.node.send_active(true);
|
|
*self.buffers.borrow_mut() = buffers;
|
|
self.buffers_valid.set(false);
|
|
}
|
|
|
|
fn ready(&self, ev: &Ready) {
|
|
let idx = ev.idx as usize;
|
|
if !self.buffers_valid.get() {
|
|
self.jay_screencast.release_buffer(idx);
|
|
return;
|
|
}
|
|
unsafe {
|
|
let mut used = false;
|
|
if let Some(io) = self.port.io_buffers.lock().values().next() {
|
|
let io = io.write();
|
|
let status = io.status.load(Acquire);
|
|
if status != SPA_STATUS_HAVE_DATA.0 {
|
|
used = true;
|
|
let buffer_id = io.buffer_id.load(Relaxed);
|
|
if buffer_id != ev.idx {
|
|
if (buffer_id as usize) < self.buffers.borrow_mut().len() {
|
|
self.jay_screencast.release_buffer(buffer_id as usize);
|
|
}
|
|
}
|
|
io.buffer_id.store(ev.idx, Relaxed);
|
|
io.status.store(SPA_STATUS_HAVE_DATA.0, Release);
|
|
}
|
|
}
|
|
if !used {
|
|
self.jay_screencast.release_buffer(idx);
|
|
}
|
|
{
|
|
let pbuffers = self.port.buffers.borrow_mut();
|
|
let buffers = self.buffers.borrow_mut();
|
|
if let Some(pbuffer) = pbuffers.get(idx) {
|
|
let buffer = &buffers[idx];
|
|
for (chunk, plane) in pbuffer.chunks.iter().zip(buffer.planes.iter()) {
|
|
let chunk = chunk.write();
|
|
chunk.flags = SpaChunkFlags::none();
|
|
chunk.offset = plane.offset;
|
|
chunk.stride = plane.stride;
|
|
chunk.size = plane.stride * buffer.height as u32;
|
|
}
|
|
if let Some(crop) = &pbuffer.meta_video_crop {
|
|
crop.write().region = spa_region {
|
|
position: spa_point { x: 0, y: 0 },
|
|
size: spa_rectangle {
|
|
width: buffer.width as _,
|
|
height: buffer.height as _,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(wfd) = self.port.node.transport_out.get() {
|
|
let _ = uapi::eventfd_write(wfd.raw(), 1);
|
|
}
|
|
}
|
|
|
|
fn destroyed(&self) {
|
|
self.session.kill();
|
|
}
|
|
}
|
|
|
|
pub(super) fn add_screencast_dbus_members(state_: &Rc<PortalState>, object: &DbusObject) {
|
|
use org::freedesktop::impl_::portal::screen_cast::*;
|
|
let state = state_.clone();
|
|
object.add_method::<CreateSession, _>(move |req, pr| {
|
|
dbus_create_session(&state, req, pr);
|
|
});
|
|
let state = state_.clone();
|
|
object.add_method::<SelectSources, _>(move |req, pr| {
|
|
dbus_select_sources(&state, req, pr);
|
|
});
|
|
let state = state_.clone();
|
|
object.add_method::<Start, _>(move |req, pr| {
|
|
dbus_start(&state, req, pr);
|
|
});
|
|
object.set_property::<AvailableSourceTypes>(Variant::U32(MONITOR.0));
|
|
object.set_property::<AvailableCursorModes>(Variant::U32(EMBEDDED.0));
|
|
object.set_property::<version>(Variant::U32(4));
|
|
}
|
|
|
|
fn dbus_create_session(
|
|
state: &Rc<PortalState>,
|
|
req: CreateSession,
|
|
reply: PendingReply<CreateSessionReply<'static>>,
|
|
) {
|
|
log::info!("Create Session {:#?}", req);
|
|
if state.screencasts.contains(req.session_handle.0.deref()) {
|
|
reply.err("Session already exists");
|
|
return;
|
|
}
|
|
let obj = match state.dbus.add_object(req.session_handle.0.to_string()) {
|
|
Ok(obj) => obj,
|
|
Err(_) => {
|
|
reply.err("Session path is not unique");
|
|
return;
|
|
}
|
|
};
|
|
let session = Rc::new(ScreencastSession {
|
|
_id: state.id(),
|
|
state: state.clone(),
|
|
app: req.app_id.to_string(),
|
|
session_obj: obj,
|
|
phase: CloneCell::new(ScreencastPhase::Init),
|
|
});
|
|
{
|
|
use org::freedesktop::impl_::portal::session::*;
|
|
let ses = session.clone();
|
|
session.session_obj.add_method::<Close, _>(move |_, pr| {
|
|
ses.kill();
|
|
pr.ok(&SessionCloseReply);
|
|
});
|
|
session.session_obj.set_property::<version>(Variant::U32(4));
|
|
}
|
|
state
|
|
.screencasts
|
|
.set(req.session_handle.0.to_string(), session);
|
|
reply.ok(&CreateSessionReply {
|
|
response: PORTAL_SUCCESS,
|
|
results: Default::default(),
|
|
});
|
|
}
|
|
|
|
fn dbus_select_sources(
|
|
state: &Rc<PortalState>,
|
|
req: SelectSources,
|
|
reply: PendingReply<SelectSourcesReply<'static>>,
|
|
) {
|
|
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
|
|
s.dbus_select_sources(req, reply);
|
|
}
|
|
}
|
|
|
|
fn dbus_start(state: &Rc<PortalState>, req: Start, reply: PendingReply<StartReply<'static>>) {
|
|
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
|
|
s.dbus_start(req, reply);
|
|
}
|
|
}
|
|
|
|
fn get_session<T>(
|
|
state: &Rc<PortalState>,
|
|
reply: &PendingReply<T>,
|
|
handle: &str,
|
|
) -> Option<Rc<ScreencastSession>> {
|
|
let res = state.screencasts.get(handle);
|
|
if res.is_none() {
|
|
let msg = format!("Screencast session `{}` does not exist", handle);
|
|
reply.err(&msg);
|
|
}
|
|
res
|
|
}
|