#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::io::Write;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
const LATENCY: &str = "125";
const PIPE: &str = "/tmp/scrcpy_pipe";
struct PhoneMicState {
module_id: Option<u32>,
parec: Option<Child>,
scrcpy: Option<Child>,
}
impl PhoneMicState {
fn new() -> Self {
Self { module_id: None, parec: None, scrcpy: None }
}
fn is_active(&self) -> bool {
self.scrcpy.is_some()
}
fn start(&mut self) -> Result<(), String> {
let output = Command::new("pactl")
.args([
"load-module",
"module-pipe-source",
"source_name=Scrcpy",
"channels=2",
"format=16",
"rate=48000",
&format!("file={}", PIPE),
])
.output()
.map_err(|e| format!("pactl failed: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("pactl load-module failed: {}", stderr));
}
let module_id: u32 = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.map_err(|e| format!("invalid module id: {}", e))?;
self.module_id = Some(module_id);
let mut parec = Command::new("parec")
.args(["--fix-rate", "-d", "Scrcpy", "--raw"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("parec failed: {}", e))?;
let mut scrcpy = Command::new("scrcpy")
.args([
"--no-video",
"--no-window",
"--no-playback",
"--audio-source=mic",
"--audio-codec=raw",
"--record-format=wav",
&format!("--record={}", PIPE),
&format!("--audio-buffer={}", LATENCY),
"--audio-output-buffer=10",
])
.spawn()
.map_err(|e| format!("scrcpy failed: {}", e))?;
thread::sleep(Duration::from_millis(500));
if let Some(status) = scrcpy.try_wait().map_err(|e| format!("scrcpy wait failed: {}", e))? {
let _ = parec.kill();
let _ = parec.wait();
let _ = Command::new("pactl")
.args(["unload-module", &module_id.to_string()])
.output();
return Err(format!("scrcpy exited early (status: {}). Check your phone is connected.", status));
}
self.parec = Some(parec);
self.scrcpy = Some(scrcpy);
Ok(())
}
fn stop(&mut self) {
if let Some(mut child) = self.scrcpy.take() {
let _ = child.kill();
let _ = child.wait();
}
if let Some(mut child) = self.parec.take() {
let _ = child.kill();
let _ = child.wait();
}
if let Some(id) = self.module_id.take() {
let _ = Command::new("pactl")
.args(["unload-module", &id.to_string()])
.output();
}
}
}
enum TrayMessage {
Toggle,
Quit,
}
fn prime_adb() {
let _ = Command::new("adb")
.args(["devices"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(not(target_os = "windows"))]
mod tray {
use super::*;
use ksni;
pub struct AppData {
pub active: bool,
pub systemd: bool,
pub tx: mpsc::Sender<TrayMessage>,
}
struct PhoneMicTray {
data: Arc<Mutex<AppData>>,
}
impl ksni::Tray for PhoneMicTray {
fn id(&self) -> String {
"phone-mic".to_string()
}
fn icon_name(&self) -> String {
let active = self.data.lock().unwrap().active;
if active { "media-record" } else { "phone-apple-iphone-symbolic" }.to_string()
}
fn title(&self) -> String {
"Phone Mic".to_string()
}
fn tool_tip(&self) -> ksni::ToolTip {
let data = self.data.lock().unwrap();
let status = if data.active { "Active" } else { "Inactive" };
let suffix = if data.systemd { " (systemd)" } else { "" };
ksni::ToolTip {
title: "Phone Mic".to_string(),
description: format!("Use your phone as a microphone — {}{}", status, suffix),
..Default::default()
}
}
fn activate(&mut self, _x: i32, _y: i32) {
let data = self.data.lock().unwrap();
let _ = data.tx.send(TrayMessage::Toggle);
}
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
use ksni::menu::*;
vec![
StandardItem {
label: "Quit".to_string(),
activate: Box::new(|this: &mut PhoneMicTray| {
let data = this.data.lock().unwrap();
let _ = data.tx.send(TrayMessage::Quit);
}),
..Default::default()
}
.into(),
]
}
}
pub struct Handle {
inner: ksni::Handle<PhoneMicTray>,
}
pub fn spawn(data: Arc<Mutex<AppData>>) -> Handle {
let tray = PhoneMicTray { data };
let service = ksni::TrayService::new(tray);
let handle = service.handle();
service.spawn();
Handle { inner: handle }
}
impl Handle {
pub fn update(&self) {
self.inner.update(|_| {});
}
}
}
#[cfg(target_os = "windows")]
mod tray {
use super::*;
use tray_item::{IconSource, TrayItem};
pub struct AppData {
pub active: bool,
pub systemd: bool,
pub tx: mpsc::Sender<TrayMessage>,
}
fn build_tray_item(tx: mpsc::Sender<TrayMessage>, active: bool) -> TrayItem {
let icon = if active { IconSource::Resource("media-record") } else { IconSource::Resource("phone-mic") };
let mut tray = TrayItem::new("Phone Mic", icon).unwrap();
tray.add_label("Phone Mic").unwrap();
let quit_tx = tx;
tray.add_menu_item("Quit", move || {
let _ = quit_tx.send(TrayMessage::Quit);
})
.unwrap();
tray
}
pub struct Handle {
tray: Mutex<Option<TrayItem>>,
tx: mpsc::Sender<TrayMessage>,
}
pub fn spawn(data: Arc<Mutex<AppData>>) -> Handle {
let tx = data.lock().unwrap().tx.clone();
let tray_item = build_tray_item(tx.clone(), false);
Handle { tray: Mutex::new(Some(tray_item)), tx }
}
impl Handle {
pub fn update(&self) {
let has_tray = self.tray.lock().unwrap().is_some();
let new = build_tray_item(self.tx.clone(), has_tray);
*self.tray.lock().unwrap() = Some(new);
}
}
}
fn main() {
#[cfg(not(target_os = "windows"))]
if let Some(arg) = std::env::args().nth(1) {
if arg.eq_ignore_ascii_case("systemd") {
let service = include_str!("systemd/phone-mic.service");
print!("{}", service);
std::io::stdout().flush().ok();
std::process::exit(0);
}
}
let instance = single_instance::SingleInstance::new("phone-mic").unwrap();
if !instance.is_single() {
eprintln!("Another instance of phone-mic is already running");
std::process::exit(1);
}
prime_adb();
let (tx, rx) = mpsc::channel();
let systemd = std::env::var("SYSTEMD").is_ok();
let data = Arc::new(Mutex::new(tray::AppData {
active: false,
systemd,
tx,
}));
let handle = tray::spawn(data.clone());
let mut state = PhoneMicState::new();
loop {
match rx.recv() {
Ok(TrayMessage::Toggle) => {
if state.is_active() {
state.stop();
data.lock().unwrap().active = false;
handle.update();
} else if let Err(e) = state.start() {
eprintln!("phone-mic error: {}", e);
} else {
data.lock().unwrap().active = true;
handle.update();
}
}
Ok(TrayMessage::Quit) => {
state.stop();
std::process::exit(0);
}
Err(_) => break,
}
}
}