Deserialize<'a> + Debug>(mut reader: R) -> Result {
let mut length_buffer = [0u8; 4];
reader
.read_exact(&mut length_buffer)
.map_err(ReadError::ReadingLengthHeader)?;
let length = u32::from_le_bytes(length_buffer);
tracing::trace!("Length: {}", length);
let mut payload_buffer = vec![0u8; length as usize];
reader
.read_exact(&mut payload_buffer)
.map_err(ReadError::ReadingPayload)?;
let payload_string = str::from_utf8(&payload_buffer).map_err(ReadError::PayloadUTFError)?;
let payload = serde_json::from_str(payload_string).map_err(ReadError::ParsingPayload)?;
tracing::debug!("read payload: {:?}", &payload);
Ok(payload)
}
pub fn write(payload: P, mut writer: W) -> Result<(), WriteError> {
let payload = serde_json::to_string(&payload).map_err(WriteError::SerializingPayload)?;
tracing::debug!("writing payload: {}", payload);
let payload = payload.as_bytes();
let length = payload.len();
let length_buffer = u32::try_from(length).unwrap().to_le_bytes();
writer
.write_all(&length_buffer)
.map_err(WriteError::WritingLengthHeader)?;
writer
.write_all(payload)
.map_err(WriteError::WritingPayload)?;
writer.flush().map_err(WriteError::Flushing)?;
Ok(())
}
================================================
FILE: native/connector-rs/src/helpers/pipewire.rs
================================================
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct NodeProperties {
#[serde(rename = "application.name")]
application_name: Option,
#[serde(rename = "media.name")]
media_name: String,
#[serde(rename = "object.serial")]
object_serial: i64,
#[serde(skip_serializing)]
#[allow(unused)]
media_class: String,
}
#[derive(Debug, Serialize)]
pub struct OutputNode {
id: u32,
properties: NodeProperties,
}
impl From for NodeProperties {
fn from(
pipewire_utils::NodeProperties {
application_name,
media_name,
object_serial,
media_class,
}: pipewire_utils::NodeProperties,
) -> Self {
Self {
application_name,
media_name,
object_serial,
media_class,
}
}
}
impl From for OutputNode {
fn from(pipewire_utils::OutputNode { id, properties }: pipewire_utils::OutputNode) -> Self {
Self {
id,
properties: properties.into(),
}
}
}
================================================
FILE: native/connector-rs/src/helpers.rs
================================================
use serde_json::Value;
pub mod io;
pub mod pipewire;
pub fn parse_numeric_argument(value: Value) -> i64 {
if value.is_i64() {
value.as_number().unwrap().as_i64().unwrap()
} else {
value.as_str().unwrap().parse::().unwrap()
}
}
================================================
FILE: native/connector-rs/src/ipc.rs
================================================
use std::{
error::Error,
fs, io,
os::unix::net::{UnixListener, UnixStream},
path::{Path, PathBuf},
thread,
time::Duration,
};
use crate::io as ipc_io;
use crate::{daemon, dirs::get_runtime_path};
fn get_ipc_socket_path() -> PathBuf {
let mut path = get_runtime_path();
path.push("ipc.sock");
path
}
fn ensure_stopped(path: &Path) -> Result<(), Box> {
if path.exists() {
let stream = connect_inner(1)?;
ipc_io::write(daemon::Command::Stop, &stream)?;
}
Ok(())
}
pub fn listen() -> io::Result {
let path = get_ipc_socket_path();
let _ = ensure_stopped(&path);
let _ = fs::remove_file(&path);
UnixListener::bind(path)
}
pub fn fake_connect() {
let _ = UnixStream::connect(get_ipc_socket_path());
}
pub fn connect_inner(tries: usize) -> io::Result {
let mut retries = tries;
let path = get_ipc_socket_path();
let mut last_error = None;
while retries > 0 {
tracing::debug!("connecting");
match UnixStream::connect(path.clone()) {
Ok(socket) => return Ok(socket),
Err(err) => last_error = Some(err),
}
retries -= 1;
if retries > 0 {
thread::sleep(Duration::from_millis(100));
}
}
Err(last_error.unwrap())
}
pub fn connect() -> io::Result {
connect_inner(1)
}
================================================
FILE: native/connector-rs/src/ipc_request.rs
================================================
use std::process::ChildStdout;
use crate::{daemon, helpers::io, ipc};
pub fn is_daemon_running() -> Result {
let pipe = ipc::connect().map_err(|err| err.to_string())?;
io::write(daemon::Command::Ping, &pipe).map_err(|err| err.to_string())?;
let res: daemon::Response = io::read(&pipe).map_err(|err| err.to_string())?;
let daemon::Response::PingResult = res else {
tracing::error!("invalid response for Ping, {res:?}");
return Err(format!("invalid response for Ping, {res:?}"));
};
Ok(true)
}
pub fn stop_daemon() -> Result<(), String> {
let pipe = ipc::connect().map_err(|err| err.to_string())?;
io::write(daemon::Command::Stop, &pipe).map_err(|err| err.to_string())?;
let res: daemon::Response = io::read(&pipe).map_err(|err| err.to_string())?;
let daemon::Response::StopResult = res else {
tracing::error!("invalid response for Stop, {res:?}");
return Err(format!("invalid response for Stop, {res:?}"));
};
Ok(())
}
pub fn set_instance_identifier(instance_identifier: &str) -> Result<(), String> {
let pipe = ipc::connect().map_err(|err| err.to_string())?;
io::write(
daemon::Command::SetInstanceIdentifier {
instance_identifier: instance_identifier.to_owned(),
},
&pipe,
)
.map_err(|err| err.to_string())?;
let res: daemon::Response = io::read(&pipe).map_err(|err| err.to_string())?;
let daemon::Response::SetInstanceIdentifierResult = res else {
tracing::error!("invalid response for SetExcludedTitle, {res:?}");
return Err(format!("invalid response for SetExcludedTitle, {res:?}"));
};
Ok(())
}
pub fn read_start_result(daemon_stdout: ChildStdout) -> Result {
let status: daemon::Response = io::read(daemon_stdout)
.map_err(|err| format!("error obtaining first response from daemon: {err}"))?;
let daemon::Response::StartResult { mic_id } = status else {
return Err(format!(
"first response from daemon has unexpected format: {status:?}"
));
};
Ok(mic_id)
}
================================================
FILE: native/connector-rs/src/main.rs
================================================
use std::{
env,
error::Error,
io::{stdin, stdout},
panic,
path::PathBuf,
process,
};
mod command;
mod daemon;
mod dirs;
mod helpers;
mod ipc;
mod ipc_request;
mod monitor;
use helpers::io;
use serde_json::json;
use tracing::{level_filters::LevelFilter, Level};
use tracing_appender::rolling::RollingFileAppender;
use tracing_panic::panic_hook;
use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter, Layer, Registry};
use crate::{daemon::monitor_and_connect_nodes, dirs::get_runtime_path};
fn get_logs_path() -> PathBuf {
let mut path = get_runtime_path();
path.push("logs");
path
}
fn main() -> Result<(), Box> {
let mut args = env::args();
let _ = args.next().expect("binary path should be the first arg");
let subcommand = args.next();
let file_appender = RollingFileAppender::builder()
.filename_prefix(
match subcommand.as_deref() {
Some("daemon") => "daemon",
_ => "connector",
}
.to_owned(),
)
.build(get_logs_path())
.unwrap();
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let subscriber = Registry::default()
.with(
fmt::Layer::default()
.with_writer(non_blocking)
.with_ansi(false)
.with_filter(LevelFilter::from_level(Level::DEBUG)),
)
.with(
fmt::Layer::default()
.with_writer(std::io::stderr)
.with_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::ERROR.into())
.from_env_lossy(),
),
);
tracing::subscriber::set_global_default(subscriber).expect("unable to set global subscriber");
let span = tracing::info_span!("main", pid = process::id());
let _span_handle = span.enter();
panic::set_hook(Box::new(panic_hook));
match subcommand.as_deref() {
Some("daemon") => {
if let Err(err) = monitor_and_connect_nodes() {
tracing::error!("error: {}", err);
Err(Box::new(err))?;
}
}
Some(_) | None => {
let payload = io::read(stdin()).unwrap();
tracing::info!(payload = format!("{payload:?}"), "running connector");
match command::run(payload) {
Ok(result) => {
let _ = io::write(
json!({
"success": true,
"response": result,
}),
stdout(),
);
}
Err(err) => {
tracing::error!("command error: {}", err);
let _ = io::write(
json!({
"success": false,
"errorMessage": err,
}),
stdout(),
);
}
}
}
}
Ok(())
}
================================================
FILE: native/connector-rs/src/monitor.rs
================================================
use std::{
io,
rc::Rc,
thread::{self, JoinHandle},
};
use pipewire_utils::{
cancellation_signal::{CancellationController, CancellationSignal},
PipewireClient, PipewireError, Ports,
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("pipewire error")]
PipewireError(#[from] PipewireError),
#[error("thread spawning error")]
ThreadSpawnError(io::Error),
}
pub type Result = std::result::Result;
pub struct MonitorThreadHandle {
join_handle: Option>>,
cancellation_controller: CancellationController,
}
impl MonitorThreadHandle {
pub fn launch_monitor_thread(
mic_ports: Ports,
instance_identifier: Option,
) -> Result {
tracing::debug!("starting monitor thread");
let (controller, signal) = CancellationSignal::pair();
let join_handle = thread::Builder::new()
.name("monitor and connector thread".to_owned())
.spawn(move || {
let client = PipewireClient::new()?;
let instance_identifier = instance_identifier.map(Rc::new);
client.monitor_and_connect_nodes(mic_ports, signal, move |node_app_name| {
tracing::trace!(
node_app_name,
instance_identifier = instance_identifier.as_ref().map(|ii| ii.as_str()),
"filtering"
);
instance_identifier
.as_ref()
.is_none_or(move |excluded_node_name| {
node_app_name
.is_none_or(|node_app_name| !node_app_name.ends_with(excluded_node_name.as_ref()))
})
})
})
.map_err(Error::ThreadSpawnError)?;
Ok(MonitorThreadHandle {
join_handle: Some(join_handle),
cancellation_controller: controller,
})
}
pub fn stop(&mut self) {
tracing::debug!("stopping monitor thread");
self.cancellation_controller.cancel();
if let Some(handle) = self.join_handle.take() {
if let Err(err) = handle.join().unwrap() {
tracing::error!("monitor thread returned error: {err}");
}
}
}
}
impl Drop for MonitorThreadHandle {
fn drop(&mut self) {
self.stop();
}
}
================================================
FILE: native/native-messaging-hosts/com.icedborn.pipewirescreenaudioconnector.json
================================================
{
"name": "com.icedborn.pipewirescreenaudioconnector",
"description": "Connector to communicate with the browser",
"path": "CONNECTOR_BINARY_PATH",
"type": "stdio",
"ALLOWED_FIELD": ["ALLOWED_VALUE"]
}
================================================
FILE: package.json
================================================
{
"name": "pipewire-screenaudio",
"version": "0.4.2",
"scripts": {
"build": "vite build extension/react"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.8",
"@mui/material": "^5.14.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.17.0",
"react-router-dom": "^6.17.0",
"use-debounce": "^9.0.4"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.4",
"eslint": "^8.47.0",
"eslint-plugin-react": "^7.33.2",
"nodemon": "^3.0.1",
"prettier": "^3.0.1",
"vite": "^4.4.9"
}
}