1 use std::io::Write as _;
4 use std::path::{Path, PathBuf};
5 use std::sync::{Arc, Mutex, RwLock};
6 use std::sync::atomic::{AtomicBool, Ordering};
7 use std::thread;
8
9 use crate::mix::mix_pcm;
10 use crate::monitor::Monitor;
11 use crate::platform;
12 use crate::{Detection, DetectionKind, Event, Permission, PermissionGranted, Recording, Result};
13
14 #[derive(Clone)]
21 pub struct MeetingListener {
22 inner: Arc<Inner>,
23 }
24
25 struct Inner {
26 config: Mutex<Config>,
27 handlers: RwLock<Vec<Box<dyn Fn(&Event) + Send + Sync + 'static>>>,
28 auto_record: AtomicBool,
29 meeting: Mutex<MeetingState>,
30 monitor: Mutex<Option<Monitor>>,
31 }
32
33 struct Config {
34 sample_rate: u32,
35 chunk_ms: u32,
36 output_dir: PathBuf,
37 }
38
39 impl Default for Config {
40 fn default() -> Self {
41 Self {
42 sample_rate: 16_000,
43 chunk_ms: 200,
44 output_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
45 }
46 }
47 }
48
49 struct RecordingHandle {
52 _tap_stop: Option<Box<dyn FnOnce() + Send>>,
53 _mic_stop: Option<Box<dyn FnOnce() + Send>>,
54 }
55
56 impl Drop for RecordingHandle {
57 fn drop(&mut self) {
58 if let Some(f) = self._tap_stop.take() { f(); }
59 if let Some(f) = self._mic_stop.take() { f(); }
60 }
61 }
62
63 struct MeetingState {
64 in_meeting: bool,
65 app: String,
66 recording: Option<RecordingHandle>,
67 }
68
69 impl MeetingListener {
72 pub fn new() -> Self {
74 Self {
75 inner: Arc::new(Inner {
76 config: Mutex::new(Config::default()),
77 handlers: RwLock::new(Vec::new()),
78 auto_record: AtomicBool::new(false),
79 meeting: Mutex::new(MeetingState {
80 in_meeting: false,
81 app: String::new(),
82 recording: None,
83 }),
84 monitor: Mutex::new(None),
85 }),
86 }
87 }
88
89 pub fn sample_rate(&self, hz: u32) -> &Self {
91 self.inner.config.lock().unwrap().sample_rate = hz;
92 self
93 }
94
95 pub fn output_dir<P: Into<PathBuf>>(&self, dir: P) -> &Self {
97 self.inner.config.lock().unwrap().output_dir = dir.into();
98 self
99 }
100
101 pub fn on<F: Fn(&Event) + Send + Sync + 'static>(&self, f: F) -> &Self {
114 self.inner.handlers.write().unwrap().push(Box::new(f));
115 self
116 }
117
118 pub fn auto_record(&self) -> &Self {
121 self.inner.auto_record.store(true, Ordering::Relaxed);
122 self
123 }
124
125 pub fn request_permissions(&self) {
139 #[cfg(target_os = "macos")]
140 {
141 extern "C" { fn CGRequestScreenCaptureAccess() -> bool; }
146 unsafe { CGRequestScreenCaptureAccess(); }
147 }
148 check_and_emit_permissions(&self.inner);
151 }
152
153 pub fn record(&self) {
159 #[cfg(target_os = "macos")]
163 {
164 if check_screen_capture() == PermissionGranted::Denied {
165 emit(&self.inner, &Event::PermissionStatus {
166 permission: Permission::ScreenCapture,
167 status: PermissionGranted::Denied,
168 });
169 emit(&self.inner, &Event::Error {
170 message: "Screen Recording access required — call request_permissions() \
171 to open System Settings, grant access, then restart the listener"
172 .into(),
173 });
174 return;
175 }
176 }
177
178 #[cfg(target_os = "macos")]
181 {
182 match check_microphone() {
183 PermissionGranted::Granted => {}
184 PermissionGranted::NotRequested => {
185 let status = request_microphone_access();
187 emit(&self.inner, &Event::PermissionStatus {
188 permission: Permission::Microphone,
189 status,
190 });
191 if status != PermissionGranted::Granted {
192 emit(&self.inner, &Event::Error {
193 message: "Microphone access denied — grant permission in System Settings > Privacy > Microphone".into(),
194 });
195 return;
196 }
197 }
198 PermissionGranted::Denied => {
199 emit(&self.inner, &Event::PermissionStatus {
200 permission: Permission::Microphone,
201 status: PermissionGranted::Denied,
202 });
203 emit(&self.inner, &Event::Error {
204 message: "Microphone access denied — grant permission in System Settings > Privacy > Microphone".into(),
205 });
206 return;
207 }
208 }
209 }
210
211 let (sample_rate, chunk_ms, output_dir) = {
212 let cfg = self.inner.config.lock().unwrap();
213 (cfg.sample_rate, cfg.chunk_ms, cfg.output_dir.clone())
214 };
215
216 let mut state = self.inner.meeting.lock().unwrap();
217 if !state.in_meeting || state.recording.is_some() { return; }
218
219 let app = state.app.clone();
220
221 let tap = match platform::start_tap(sample_rate, chunk_ms) {
222 Ok(r) => r,
223 Err(e) => { drop(state); emit(&self.inner, &Event::Error { message: e.to_string() }); return; }
224 };
225 let mic = match platform::start_mic(sample_rate, chunk_ms) {
226 Ok(r) => r,
227 Err(e) => { drop(state); emit(&self.inner, &Event::Error { message: e.to_string() }); return; }
228 };
229
230 let mut tap = tap; let mut mic = mic;
232 let tap_stop = tap.stop_fn.take();
233 let mic_stop = mic.stop_fn.take();
234 let tap_rx = tap.rx.clone();
235 let mic_rx = mic.rx.clone();
236 drop(tap); drop(mic);
237
238 let stem = output_dir.join(format!("{}-meeting", unix_secs()));
239 let mixed_path = stem.with_extension("wav");
240 let others_path = PathBuf::from(format!("{}-others.wav", stem.display()));
241 let self_path = PathBuf::from(format!("{}-self.wav", stem.display()));
242
243 state.recording = Some(RecordingHandle {
245 _tap_stop: tap_stop,
246 _mic_stop: mic_stop,
247 });
248 drop(state);
249
250 emit(&self.inner, &Event::RecordingStarted { app: app.clone() });
251
252 let inner = Arc::clone(&self.inner);
253 thread::spawn(move || {
254 use std::sync::mpsc::sync_channel;
256 let (tap_tx, tap_done) = sync_channel::<Vec<i16>>(0);
257 let (mic_tx, mic_done) = sync_channel::<Vec<i16>>(0);
258
259 thread::spawn(move || {
260 let mut pcm = Vec::new();
261 for chunk in tap_rx { pcm.extend_from_slice(&chunk.pcm); }
262 let _ = tap_tx.send(pcm);
263 });
264 thread::spawn(move || {
265 let mut pcm = Vec::new();
266 for chunk in mic_rx { pcm.extend_from_slice(&chunk.pcm); }
267 let _ = mic_tx.send(pcm);
268 });
269
270 let others_pcm = tap_done.recv().unwrap_or_default();
271 let self_pcm = mic_done.recv().unwrap_or_default();
272
273 if others_pcm.is_empty() && self_pcm.is_empty() { return; }
274
275 let mixed_pcm = mix_pcm(&others_pcm, &self_pcm);
276
277 emit(&inner, &Event::RecordingEnded { app: app.clone() });
278
279 let ok = write_wav(&others_path, &others_pcm, sample_rate).is_ok()
280 & write_wav(&self_path, &self_pcm, sample_rate).is_ok()
281 & write_wav(&mixed_path, &mixed_pcm, sample_rate).is_ok();
282
283 if ok {
284 emit(&inner, &Event::RecordingReady {
285 mixed_path,
286 others_path,
287 self_path,
288 app,
289 });
290 } else {
291 emit(&inner, &Event::Error { message: "failed to write WAV files".into() });
292 }
293 });
294 }
295
296 pub fn start(&self) -> Result<()> {
299 check_and_emit_permissions(&self.inner);
301
302 let mut mon = Monitor::new();
303 let inner_ref = Arc::clone(&self.inner);
304
305 mon.on_detection(move |det: Detection| {
306 on_detection(&inner_ref, det);
307 });
308
309 mon.start()?;
310 *self.inner.monitor.lock().unwrap() = Some(mon);
311 Ok(())
312 }
313
314 pub fn stop(&self) {
316 if let Some(mon) = self.inner.monitor.lock().unwrap().take() {
317 mon.stop();
318 }
319 self.inner.meeting.lock().unwrap().recording = None;
320 }
321 }
322
323 impl Default for MeetingListener {
324 fn default() -> Self { Self::new() }
325 }
326
327 fn check_and_emit_permissions(inner: &Arc<Inner>) {
330 #[cfg(target_os = "macos")]
331 {
332 let sc = check_screen_capture();
333 let mic = check_microphone();
334 emit(inner, &Event::PermissionStatus {
335 permission: Permission::ScreenCapture,
336 status: sc,
337 });
338 emit(inner, &Event::PermissionStatus {
339 permission: Permission::Microphone,
340 status: mic,
341 });
342 if sc == PermissionGranted::Granted {
343 emit(inner, &Event::PermissionsGranted);
344 }
345 }
346 #[cfg(not(target_os = "macos"))]
347 {
348 emit(inner, &Event::PermissionsGranted);
350 }
351 }
352
353 #[cfg(target_os = "macos")]
354 fn check_screen_capture() -> PermissionGranted {
355 extern "C" { fn CGPreflightScreenCaptureAccess() -> bool; }
356 if unsafe { CGPreflightScreenCaptureAccess() } {
357 PermissionGranted::Granted
358 } else {
359 PermissionGranted::Denied
366 }
367 }
368
369 #[cfg(target_os = "macos")]
374 fn check_microphone() -> PermissionGranted {
375 use std::ffi::c_void;
376 type ID = *mut c_void;
377 type SEL = *const c_void;
378
379 extern "C" {
380 fn objc_getClass(name: *const u8) -> *const c_void;
381 fn sel_registerName(name: *const u8) -> SEL;
382 }
383
384 let msg_send_ptr = unsafe {
385 libc::dlsym(libc::RTLD_DEFAULT, b"objc_msgSend\0".as_ptr() as _)
386 };
387 if msg_send_ptr.is_null() { return PermissionGranted::NotRequested; }
388
389 unsafe {
392 libc::dlopen(
393 b"/System/Library/Frameworks/AVFoundation.framework/AVFoundation\0".as_ptr() as *const libc::c_char,
394 libc::RTLD_LAZY | libc::RTLD_GLOBAL,
395 );
396 }
397
398 unsafe {
399 let ns_string_cls = objc_getClass(b"NSString\0".as_ptr());
400 let av_device_cls = objc_getClass(b"AVCaptureDevice\0".as_ptr());
401 if ns_string_cls.is_null() || av_device_cls.is_null() {
402 return PermissionGranted::NotRequested;
403 }
404
405 let sel_utf8 = sel_registerName(b"stringWithUTF8String:\0".as_ptr());
407 type FnStr = unsafe extern "C" fn(*const c_void, SEL, *const u8) -> ID;
408 let fn_str: FnStr = std::mem::transmute(msg_send_ptr);
409 let media_type = fn_str(ns_string_cls, sel_utf8, b"soun\0".as_ptr());
410 if media_type.is_null() { return PermissionGranted::NotRequested; }
411
412 let sel_auth = sel_registerName(b"authorizationStatusForMediaType:\0".as_ptr());
415 type FnAuth = unsafe extern "C" fn(*const c_void, SEL, ID) -> isize;
416 let fn_auth: FnAuth = std::mem::transmute(msg_send_ptr);
417 match fn_auth(av_device_cls, sel_auth, media_type) {
418 3 => PermissionGranted::Granted,
419 1 | 2 => PermissionGranted::Denied,
420 _ => PermissionGranted::NotRequested, }
422 }
423 }
424
425 #[cfg(target_os = "macos")]
430 fn request_microphone_access() -> PermissionGranted {
431 use std::ffi::c_void;
432 use std::sync::atomic::{AtomicBool, Ordering};
433
434 type ID = *mut c_void;
435 type SEL = *const c_void;
436
437 extern "C" {
438 fn objc_getClass(name: *const u8) -> *const c_void;
439 fn sel_registerName(name: *const u8) -> SEL;
440 fn dispatch_semaphore_create(value: isize) -> *mut c_void;
441 fn dispatch_semaphore_signal(sema: *mut c_void) -> isize;
442 fn dispatch_semaphore_wait(sema: *mut c_void, timeout: u64) -> isize;
443 fn dispatch_release(obj: *mut c_void);
444 }
445
446 #[repr(C)]
450 struct BoolBlock {
451 isa: *const c_void,
452 flags: i32,
453 reserved: i32,
454 invoke: unsafe extern "C" fn(*const BoolBlock, bool),
455 desc: *const BlockDesc,
456 granted: *const AtomicBool, sema: *mut c_void, }
459
460 #[repr(C)]
461 struct BlockDesc { reserved: usize, size: usize }
462 static BLOCK_DESC: BlockDesc = BlockDesc {
463 reserved: 0,
464 size: core::mem::size_of::<BoolBlock>(),
465 };
466
467 unsafe extern "C" fn block_invoke(block: *const BoolBlock, granted: bool) {
468 (*(*block).granted).store(granted, Ordering::SeqCst);
469 dispatch_semaphore_signal((*block).sema);
470 }
471
472 let msg_send_ptr = unsafe {
473 libc::dlsym(libc::RTLD_DEFAULT, b"objc_msgSend\0".as_ptr() as _)
474 };
475 let stack_block_isa = unsafe {
476 libc::dlsym(libc::RTLD_DEFAULT, b"_NSConcreteStackBlock\0".as_ptr() as _)
477 };
478 if msg_send_ptr.is_null() || stack_block_isa.is_null() {
479 return PermissionGranted::NotRequested;
480 }
481
482 unsafe {
484 libc::dlopen(
485 b"/System/Library/Frameworks/AVFoundation.framework/AVFoundation\0".as_ptr() as *const libc::c_char,
486 libc::RTLD_LAZY | libc::RTLD_GLOBAL,
487 );
488 }
489
490 let granted = AtomicBool::new(false);
491
492 unsafe {
493 let sema = dispatch_semaphore_create(0);
494 if sema.is_null() { return PermissionGranted::NotRequested; }
495
496 let mut block = BoolBlock {
497 isa: stack_block_isa,
498 flags: 0,
499 reserved: 0,
500 invoke: block_invoke,
501 desc: &BLOCK_DESC,
502 granted: &granted,
503 sema,
504 };
505
506 let ns_string_cls = objc_getClass(b"NSString\0".as_ptr());
507 let av_device_cls = objc_getClass(b"AVCaptureDevice\0".as_ptr());
508 if ns_string_cls.is_null() || av_device_cls.is_null() {
509 dispatch_release(sema);
510 return PermissionGranted::NotRequested;
511 }
512
513 let sel_utf8 = sel_registerName(b"stringWithUTF8String:\0".as_ptr());
514 type FnStr = unsafe extern "C" fn(*const c_void, SEL, *const u8) -> ID;
515 let fn_str: FnStr = core::mem::transmute(msg_send_ptr);
516 let media_type = fn_str(ns_string_cls, sel_utf8, b"soun\0".as_ptr());
517
518 let sel_req = sel_registerName(b"requestAccessForMediaType:completionHandler:\0".as_ptr());
519 type FnReq = unsafe extern "C" fn(*const c_void, SEL, ID, *mut BoolBlock);
520 let fn_req: FnReq = core::mem::transmute(msg_send_ptr);
521 fn_req(av_device_cls, sel_req, media_type, &mut block);
522
523 dispatch_semaphore_wait(sema, u64::MAX);
525 dispatch_release(sema);
526 }
527
528 if granted.load(Ordering::SeqCst) {
529 PermissionGranted::Granted
530 } else {
531 PermissionGranted::Denied
532 }
533 }
534
535
536 fn on_detection(inner: &Arc<Inner>, det: Detection) {
539 match det.kind {
540 DetectionKind::Started => {
541 {
542 let mut m = inner.meeting.lock().unwrap();
543 m.in_meeting = true;
544 m.app = det.app.clone();
545 }
546 emit(inner, &Event::MeetingDetected { app: det.app.clone(), pid: det.pid });
547
548 if inner.auto_record.load(Ordering::Relaxed) {
549 MeetingListener { inner: Arc::clone(inner) }.record();
550 }
551 }
552
553 DetectionKind::Updated => {
554 if let Some(title) = det.title {
556 emit(inner, &Event::MeetingUpdated { app: det.app, title });
557 }
558 }
559
560 DetectionKind::Ended => {
561 inner.meeting.lock().unwrap().recording = None;
564 emit(inner, &Event::MeetingEnded { app: det.app });
565 inner.meeting.lock().unwrap().in_meeting = false;
566 }
567
568 DetectionKind::SpeakerChanged => {
569 emit(inner, &Event::SpeakerChanged { speakers: det.speakers, app: det.app });
570 }
571 }
572 }
573
574 fn emit(inner: &Arc<Inner>, event: &Event) {
577 let handlers = inner.handlers.read().unwrap();
578 for h in handlers.iter() { h(event); }
579 }
580
581 fn unix_secs() -> u64 {
582 std::time::SystemTime::now()
583 .duration_since(std::time::UNIX_EPOCH)
584 .map(|d| d.as_secs())
585 .unwrap_or(0)
586 }
587
588 fn write_wav(path: &Path, pcm: &[i16], sample_rate: u32) -> std::io::Result<()> {
589 let mut f = std::fs::File::create(path)?;
590 let data_len = (pcm.len() * 2) as u32;
591 let byte_rate = sample_rate * 2;
592
593 f.write_all(b"RIFF")?;
594 f.write_all(&(36 + data_len).to_le_bytes())?;
595 f.write_all(b"WAVE")?;
596 f.write_all(b"fmt ")?;
597 f.write_all(&16u32.to_le_bytes())?;
598 f.write_all(&1u16.to_le_bytes())?;
599 f.write_all(&1u16.to_le_bytes())?;
600 f.write_all(&sample_rate.to_le_bytes())?;
601 f.write_all(&byte_rate.to_le_bytes())?;
602 f.write_all(&2u16.to_le_bytes())?;
603 f.write_all(&16u16.to_le_bytes())?;
604 f.write_all(b"data")?;
605 f.write_all(&data_len.to_le_bytes())?;
606 for &s in pcm { f.write_all(&s.to_le_bytes())?; }
607 Ok(())
608 }