2023-09-08 17:54:02 +04:00
|
|
|
use std::cell::{Cell, RefCell};
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
use std::mem;
|
2023-09-24 11:04:30 +04:00
|
|
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
2023-09-08 17:54:02 +04:00
|
|
|
use std::rc::Rc;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use anyhow::Context as _;
|
|
|
|
|
use pipewire::spa::data::DataType;
|
|
|
|
|
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
|
|
|
|
|
use pipewire::spa::param::format_utils::parse_format;
|
|
|
|
|
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
|
|
|
|
|
use pipewire::spa::param::ParamType;
|
|
|
|
|
use pipewire::spa::pod::serialize::PodSerializer;
|
|
|
|
|
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
|
|
|
|
|
use pipewire::spa::sys::*;
|
|
|
|
|
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
|
|
|
|
|
use pipewire::spa::Direction;
|
|
|
|
|
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
|
|
|
|
|
use pipewire::{Context, Core, MainLoop, Properties};
|
|
|
|
|
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
|
|
|
|
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
|
|
|
|
|
use smithay::backend::allocator::Fourcc;
|
|
|
|
|
use smithay::backend::drm::DrmDeviceFd;
|
|
|
|
|
use smithay::output::Output;
|
|
|
|
|
use smithay::reexports::calloop::generic::Generic;
|
|
|
|
|
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
|
|
|
|
|
use smithay::reexports::gbm::Modifier;
|
|
|
|
|
use zbus::SignalContext;
|
|
|
|
|
|
2023-10-10 08:55:54 +04:00
|
|
|
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
|
2023-09-24 17:30:06 +04:00
|
|
|
use crate::niri::State;
|
2023-09-08 17:54:02 +04:00
|
|
|
|
|
|
|
|
pub struct PipeWire {
|
|
|
|
|
_context: Context<MainLoop>,
|
|
|
|
|
pub core: Core,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Cast {
|
|
|
|
|
pub session_id: usize,
|
|
|
|
|
pub stream: Rc<Stream>,
|
|
|
|
|
_listener: StreamListener<()>,
|
|
|
|
|
pub is_active: Rc<Cell<bool>>,
|
|
|
|
|
pub output: Output,
|
|
|
|
|
pub cursor_mode: CursorMode,
|
|
|
|
|
pub last_frame_time: Duration,
|
|
|
|
|
pub min_time_between_frames: Rc<Cell<Duration>>,
|
|
|
|
|
pub dmabufs: Rc<RefCell<HashMap<i32, Dmabuf>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PipeWire {
|
2023-09-24 17:30:06 +04:00
|
|
|
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
2023-09-08 17:54:02 +04:00
|
|
|
let main_loop = MainLoop::new().context("error creating MainLoop")?;
|
|
|
|
|
let context = Context::new(&main_loop).context("error creating Context")?;
|
|
|
|
|
let core = context.connect(None).context("error creating Core")?;
|
|
|
|
|
|
|
|
|
|
let listener = core
|
|
|
|
|
.add_listener_local()
|
|
|
|
|
.error(|id, seq, res, message| {
|
|
|
|
|
warn!(id, seq, res, message, "pw error");
|
|
|
|
|
})
|
|
|
|
|
.register();
|
|
|
|
|
mem::forget(listener);
|
|
|
|
|
|
2023-09-24 11:04:30 +04:00
|
|
|
struct AsFdWrapper(MainLoop);
|
|
|
|
|
impl AsFd for AsFdWrapper {
|
|
|
|
|
fn as_fd(&self) -> BorrowedFd<'_> {
|
|
|
|
|
self.0.fd()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
2023-09-08 17:54:02 +04:00
|
|
|
event_loop
|
2023-09-24 11:04:30 +04:00
|
|
|
.insert_source(generic, move |_, wrapper, _| {
|
2023-09-08 17:54:02 +04:00
|
|
|
let _span = tracy_client::span!("pipewire iteration");
|
2023-09-24 11:04:30 +04:00
|
|
|
wrapper.0.iterate(Duration::ZERO);
|
2023-09-08 17:54:02 +04:00
|
|
|
Ok(PostAction::Continue)
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
_context: context,
|
|
|
|
|
core,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn start_cast(
|
|
|
|
|
&self,
|
2023-10-10 08:55:54 +04:00
|
|
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
2023-09-08 17:54:02 +04:00
|
|
|
gbm: GbmDevice<DrmDeviceFd>,
|
|
|
|
|
session_id: usize,
|
|
|
|
|
output: Output,
|
|
|
|
|
cursor_mode: CursorMode,
|
|
|
|
|
signal_ctx: SignalContext<'static>,
|
|
|
|
|
) -> anyhow::Result<Cast> {
|
|
|
|
|
let _span = tracy_client::span!("PipeWire::start_cast");
|
|
|
|
|
|
2024-01-18 21:16:36 +04:00
|
|
|
let to_niri_ = to_niri.clone();
|
2023-09-08 17:54:02 +04:00
|
|
|
let stop_cast = move || {
|
2024-01-18 21:16:36 +04:00
|
|
|
if let Err(err) = to_niri_.send(ScreenCastToNiri::StopCast { session_id }) {
|
2023-09-08 17:54:02 +04:00
|
|
|
warn!("error sending StopCast to niri: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-18 21:16:36 +04:00
|
|
|
let weak = output.downgrade();
|
|
|
|
|
let redraw = move || {
|
|
|
|
|
if let Some(output) = weak.upgrade() {
|
|
|
|
|
if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) {
|
|
|
|
|
warn!("error sending Redraw to niri: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-09-08 17:54:02 +04:00
|
|
|
|
|
|
|
|
let mode = output.current_mode().unwrap();
|
|
|
|
|
let size = mode.size;
|
|
|
|
|
let refresh = mode.refresh;
|
|
|
|
|
|
|
|
|
|
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
|
|
|
|
|
.context("error creating Stream")?;
|
|
|
|
|
|
|
|
|
|
// Like in good old wayland-rs times...
|
|
|
|
|
let stream = Rc::new(stream);
|
|
|
|
|
let node_id = Rc::new(Cell::new(None));
|
|
|
|
|
let is_active = Rc::new(Cell::new(false));
|
|
|
|
|
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
|
|
|
|
|
let dmabufs = Rc::new(RefCell::new(HashMap::new()));
|
|
|
|
|
|
|
|
|
|
let listener = stream
|
|
|
|
|
.add_local_listener_with_user_data(())
|
|
|
|
|
.state_changed({
|
|
|
|
|
let stream = stream.clone();
|
|
|
|
|
let is_active = is_active.clone();
|
|
|
|
|
let stop_cast = stop_cast.clone();
|
|
|
|
|
move |old, new| {
|
|
|
|
|
debug!("pw stream: state changed: {old:?} -> {new:?}");
|
|
|
|
|
|
|
|
|
|
match new {
|
|
|
|
|
StreamState::Paused => {
|
|
|
|
|
if node_id.get().is_none() {
|
|
|
|
|
let id = stream.node_id();
|
|
|
|
|
node_id.set(Some(id));
|
|
|
|
|
debug!("pw stream: sending signal with {id}");
|
|
|
|
|
|
|
|
|
|
let _span = tracy_client::span!("sending PipeWireStreamAdded");
|
|
|
|
|
async_io::block_on(async {
|
|
|
|
|
let res = mutter_screen_cast::Stream::pipe_wire_stream_added(
|
|
|
|
|
&signal_ctx,
|
|
|
|
|
id,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if let Err(err) = res {
|
|
|
|
|
warn!("error sending PipeWireStreamAdded: {err:?}");
|
|
|
|
|
stop_cast();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is_active.set(false);
|
|
|
|
|
}
|
|
|
|
|
StreamState::Error(_) => {
|
|
|
|
|
if is_active.get() {
|
|
|
|
|
is_active.set(false);
|
|
|
|
|
stop_cast();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
StreamState::Unconnected => (),
|
|
|
|
|
StreamState::Connecting => (),
|
|
|
|
|
StreamState::Streaming => {
|
|
|
|
|
is_active.set(true);
|
2024-01-18 21:16:36 +04:00
|
|
|
redraw();
|
2023-09-08 17:54:02 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.param_changed({
|
|
|
|
|
let min_time_between_frames = min_time_between_frames.clone();
|
|
|
|
|
move |stream, id, _data, pod| {
|
|
|
|
|
let id = ParamType::from_raw(id);
|
|
|
|
|
trace!(?id, "pw stream: param_changed");
|
|
|
|
|
|
|
|
|
|
if id != ParamType::Format {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some(pod) = pod else { return };
|
|
|
|
|
|
|
|
|
|
let (m_type, m_subtype) = match parse_format(pod) {
|
|
|
|
|
Ok(x) => x,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("pw stream: error parsing format: {err:?}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if m_type != MediaType::Video || m_subtype != MediaSubtype::Raw {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut format = VideoInfoRaw::new();
|
|
|
|
|
format.parse(pod).unwrap();
|
|
|
|
|
trace!("pw stream: got format = {format:?}");
|
|
|
|
|
|
|
|
|
|
let max_frame_rate = format.max_framerate();
|
|
|
|
|
// Subtract 0.5 ms to improve edge cases when equal to refresh rate.
|
|
|
|
|
let min_frame_time = Duration::from_secs_f64(
|
|
|
|
|
max_frame_rate.denom as f64 / max_frame_rate.num as f64,
|
|
|
|
|
) - Duration::from_micros(500);
|
|
|
|
|
min_time_between_frames.set(min_frame_time);
|
|
|
|
|
|
|
|
|
|
const BPP: u32 = 4;
|
|
|
|
|
let stride = format.size().width * BPP;
|
|
|
|
|
let size = stride * format.size().height;
|
|
|
|
|
|
|
|
|
|
let o1 = pod::object!(
|
|
|
|
|
SpaTypes::ObjectParamBuffers,
|
|
|
|
|
ParamType::Buffers,
|
|
|
|
|
Property::new(
|
|
|
|
|
SPA_PARAM_BUFFERS_buffers,
|
|
|
|
|
pod::Value::Choice(ChoiceValue::Int(Choice(
|
|
|
|
|
ChoiceFlags::empty(),
|
|
|
|
|
ChoiceEnum::Range {
|
|
|
|
|
default: 16,
|
|
|
|
|
min: 2,
|
|
|
|
|
max: 16
|
|
|
|
|
}
|
|
|
|
|
))),
|
|
|
|
|
),
|
|
|
|
|
Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(1)),
|
|
|
|
|
Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)),
|
|
|
|
|
Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)),
|
|
|
|
|
Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)),
|
|
|
|
|
Property::new(
|
|
|
|
|
SPA_PARAM_BUFFERS_dataType,
|
|
|
|
|
pod::Value::Choice(ChoiceValue::Int(Choice(
|
|
|
|
|
ChoiceFlags::empty(),
|
|
|
|
|
ChoiceEnum::Flags {
|
|
|
|
|
default: 1 << DataType::DmaBuf.as_raw(),
|
|
|
|
|
flags: vec![1 << DataType::DmaBuf.as_raw()],
|
|
|
|
|
},
|
|
|
|
|
))),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// FIXME: Hidden / embedded / metadata cursor
|
|
|
|
|
|
|
|
|
|
// let o2 = pod::object!(
|
|
|
|
|
// SpaTypes::ObjectParamMeta,
|
|
|
|
|
// ParamType::Meta,
|
|
|
|
|
// Property::new(SPA_PARAM_META_type,
|
|
|
|
|
// pod::Value::Id(Id(SPA_META_Header))),
|
|
|
|
|
// Property::new(
|
|
|
|
|
// SPA_PARAM_META_size,
|
|
|
|
|
// pod::Value::Int(size_of::<spa_meta_header>() as i32)
|
|
|
|
|
// ),
|
|
|
|
|
// );
|
|
|
|
|
let mut b1 = vec![];
|
|
|
|
|
// let mut b2 = vec![];
|
|
|
|
|
let mut params = [
|
|
|
|
|
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
|
|
|
|
|
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
|
|
|
|
|
];
|
|
|
|
|
stream.update_params(&mut params).unwrap();
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.add_buffer({
|
|
|
|
|
let dmabufs = dmabufs.clone();
|
|
|
|
|
let stop_cast = stop_cast.clone();
|
|
|
|
|
move |buffer| {
|
|
|
|
|
trace!("pw stream: add_buffer");
|
|
|
|
|
|
|
|
|
|
unsafe {
|
|
|
|
|
let spa_buffer = (*buffer).buffer;
|
|
|
|
|
let spa_data = (*spa_buffer).datas;
|
|
|
|
|
assert!((*spa_buffer).n_datas > 0);
|
|
|
|
|
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
|
|
|
|
|
|
|
|
|
|
let bo = match gbm.create_buffer_object::<()>(
|
|
|
|
|
size.w as u32,
|
|
|
|
|
size.h as u32,
|
|
|
|
|
Fourcc::Xrgb8888,
|
|
|
|
|
GbmBufferFlags::RENDERING | GbmBufferFlags::LINEAR,
|
|
|
|
|
) {
|
|
|
|
|
Ok(bo) => bo,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error creating GBM buffer object: {err:?}");
|
|
|
|
|
stop_cast();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let dmabuf = match bo.export() {
|
|
|
|
|
Ok(dmabuf) => dmabuf,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error exporting GBM buffer object as dmabuf: {err:?}");
|
|
|
|
|
stop_cast();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let fd = dmabuf.handles().next().unwrap().as_raw_fd();
|
|
|
|
|
|
|
|
|
|
(*spa_data).type_ = DataType::DmaBuf.as_raw();
|
|
|
|
|
(*spa_data).maxsize = dmabuf.strides().next().unwrap() * size.h as u32;
|
|
|
|
|
(*spa_data).fd = fd as i64;
|
|
|
|
|
(*spa_data).flags = SPA_DATA_FLAG_READWRITE;
|
|
|
|
|
|
|
|
|
|
assert!(dmabufs.borrow_mut().insert(fd, dmabuf).is_none());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.remove_buffer({
|
|
|
|
|
let dmabufs = dmabufs.clone();
|
|
|
|
|
move |buffer| {
|
|
|
|
|
trace!("pw stream: remove_buffer");
|
|
|
|
|
|
|
|
|
|
unsafe {
|
|
|
|
|
let spa_buffer = (*buffer).buffer;
|
|
|
|
|
let spa_data = (*spa_buffer).datas;
|
|
|
|
|
assert!((*spa_buffer).n_datas > 0);
|
|
|
|
|
|
|
|
|
|
let fd = (*spa_data).fd as i32;
|
|
|
|
|
dmabufs.borrow_mut().remove(&fd);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.register()
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let object = pod::object!(
|
|
|
|
|
SpaTypes::ObjectParamFormat,
|
|
|
|
|
ParamType::EnumFormat,
|
|
|
|
|
pod::property!(FormatProperties::MediaType, Id, MediaType::Video),
|
|
|
|
|
pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw),
|
|
|
|
|
pod::property!(FormatProperties::VideoFormat, Id, VideoFormat::BGRx),
|
|
|
|
|
Property {
|
|
|
|
|
key: FormatProperties::VideoModifier.as_raw(),
|
|
|
|
|
value: pod::Value::Long(u64::from(Modifier::Invalid) as i64),
|
|
|
|
|
flags: PropertyFlags::MANDATORY,
|
|
|
|
|
},
|
|
|
|
|
pod::property!(
|
|
|
|
|
FormatProperties::VideoSize,
|
|
|
|
|
Rectangle,
|
|
|
|
|
Rectangle {
|
|
|
|
|
width: size.w as u32,
|
|
|
|
|
height: size.h as u32,
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
pod::property!(
|
|
|
|
|
FormatProperties::VideoFramerate,
|
|
|
|
|
Fraction,
|
|
|
|
|
Fraction { num: 0, denom: 1 }
|
|
|
|
|
),
|
|
|
|
|
pod::property!(
|
|
|
|
|
FormatProperties::VideoMaxFramerate,
|
|
|
|
|
Choice,
|
|
|
|
|
Range,
|
|
|
|
|
Fraction,
|
|
|
|
|
Fraction {
|
|
|
|
|
num: refresh as u32,
|
|
|
|
|
denom: 1000
|
|
|
|
|
},
|
|
|
|
|
Fraction { num: 1, denom: 1 },
|
|
|
|
|
Fraction {
|
|
|
|
|
num: refresh as u32,
|
|
|
|
|
denom: 1000
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut buffer = vec![];
|
|
|
|
|
let mut params = [make_pod(&mut buffer, object)];
|
|
|
|
|
stream
|
|
|
|
|
.connect(
|
|
|
|
|
Direction::Output,
|
|
|
|
|
None,
|
|
|
|
|
StreamFlags::DRIVER | StreamFlags::ALLOC_BUFFERS,
|
|
|
|
|
&mut params,
|
|
|
|
|
)
|
|
|
|
|
.context("error connecting stream")?;
|
|
|
|
|
|
|
|
|
|
let cast = Cast {
|
|
|
|
|
session_id,
|
|
|
|
|
stream,
|
|
|
|
|
_listener: listener,
|
|
|
|
|
is_active,
|
|
|
|
|
output,
|
|
|
|
|
cursor_mode,
|
|
|
|
|
last_frame_time: Duration::ZERO,
|
|
|
|
|
min_time_between_frames,
|
|
|
|
|
dmabufs,
|
|
|
|
|
};
|
|
|
|
|
Ok(cast)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn make_pod(buffer: &mut Vec<u8>, object: pod::Object) -> &Pod {
|
|
|
|
|
PodSerializer::serialize(Cursor::new(&mut *buffer), &pod::Value::Object(object)).unwrap();
|
|
|
|
|
Pod::from_bytes(buffer).unwrap()
|
|
|
|
|
}
|