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_recordings;
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 MeetingState {
50 in_meeting: bool,
51 app: String,
52 recording: Option<Recording>,
53 }
54
55 impl MeetingListener {
58 pub fn new() -> Self {
60 Self {
61 inner: Arc::new(Inner {
62 config: Mutex::new(Config::default()),
63 handlers: RwLock::new(Vec::new()),
64 auto_record: AtomicBool::new(false),
65 meeting: Mutex::new(MeetingState {
66 in_meeting: false,
67 app: String::new(),
68 recording: None,
69 }),
70 monitor: Mutex::new(None),
71 }),
72 }
73 }
74
75 pub fn sample_rate(&self, hz: u32) -> &Self {
77 self.inner.config.lock().unwrap().sample_rate = hz;
78 self
79 }
80
81 pub fn output_dir<P: Into<PathBuf>>(&self, dir: P) -> &Self {
83 self.inner.config.lock().unwrap().output_dir = dir.into();
84 self
85 }
86
87 pub fn on<F: Fn(&Event) + Send + Sync + 'static>(&self, f: F) -> &Self {
100 self.inner.handlers.write().unwrap().push(Box::new(f));
101 self
102 }
103
104 pub fn auto_record(&self) -> &Self {
107 self.inner.auto_record.store(true, Ordering::Relaxed);
108 self
109 }
110
111 pub fn record(&self) {
117 let (sample_rate, chunk_ms, output_dir) = {
118 let cfg = self.inner.config.lock().unwrap();
119 (cfg.sample_rate, cfg.chunk_ms, cfg.output_dir.clone())
120 };
121
122 let mut state = self.inner.meeting.lock().unwrap();
123 if !state.in_meeting || state.recording.is_some() { return; }
124
125 let app = state.app.clone();
126
127 let tap = match platform::start_tap(sample_rate, chunk_ms) {
128 Ok(r) => r,
129 Err(e) => {
130 drop(state);
131 emit(&self.inner, &Event::Error { message: e.to_string() });
132 return;
133 }
134 };
135 let mic = match platform::start_mic(sample_rate, chunk_ms) {
136 Ok(r) => r,
137 Err(e) => {
138 drop(state);
139 emit(&self.inner, &Event::Error { message: e.to_string() });
140 return;
141 }
142 };
143
144 let mixed = mix_recordings(tap, mic, sample_rate);
145 let rx = mixed.rx.clone();
146 let path = output_dir.join(format!("{}-meeting.wav", unix_secs()));
147 state.recording = Some(mixed);
148 drop(state);
149
150 emit(&self.inner, &Event::RecordingStarted { app: app.clone() });
151
152 let inner = Arc::clone(&self.inner);
153 thread::spawn(move || {
154 let mut pcm: Vec<i16> = Vec::new();
155 for chunk in rx.iter() { pcm.extend_from_slice(&chunk.pcm); }
156 if pcm.is_empty() { return; }
157
158 emit(&inner, &Event::RecordingEnded { app: app.clone() });
159
160 if write_wav(&path, &pcm, sample_rate).is_ok() {
161 emit(&inner, &Event::RecordingReady { path, app });
162 } else {
163 emit(&inner, &Event::Error {
164 message: format!("failed to write WAV"),
165 });
166 }
167 });
168 }
169
170 pub fn start(&self) -> Result<()> {
173 check_and_emit_permissions(&self.inner);
175
176 let mut mon = Monitor::new();
177 let inner_ref = Arc::clone(&self.inner);
178
179 mon.on_detection(move |det: Detection| {
180 on_detection(&inner_ref, det);
181 });
182
183 mon.start()?;
184 *self.inner.monitor.lock().unwrap() = Some(mon);
185 Ok(())
186 }
187
188 pub fn stop(&self) {
190 if let Some(mon) = self.inner.monitor.lock().unwrap().take() {
191 mon.stop();
192 }
193 self.inner.meeting.lock().unwrap().recording = None;
194 }
195 }
196
197 impl Default for MeetingListener {
198 fn default() -> Self { Self::new() }
199 }
200
201 fn check_and_emit_permissions(inner: &Arc<Inner>) {
204 #[cfg(target_os = "macos")]
205 {
206 let sc = check_screen_capture();
207 emit(inner, &Event::PermissionStatus {
208 permission: Permission::ScreenCapture,
209 status: sc,
210 });
211 emit(inner, &Event::PermissionStatus {
213 permission: Permission::Microphone,
214 status: PermissionGranted::NotRequested,
215 });
216 if sc == PermissionGranted::Granted {
217 emit(inner, &Event::PermissionsGranted);
218 }
219 }
220 #[cfg(not(target_os = "macos"))]
221 {
222 emit(inner, &Event::PermissionsGranted);
224 }
225 }
226
227 #[cfg(target_os = "macos")]
228 fn check_screen_capture() -> PermissionGranted {
229 extern "C" { fn CGPreflightScreenCaptureAccess() -> bool; }
232 if unsafe { CGPreflightScreenCaptureAccess() } {
233 PermissionGranted::Granted
234 } else {
235 PermissionGranted::NotRequested
236 }
237 }
238
239 fn on_detection(inner: &Arc<Inner>, det: Detection) {
242 match det.kind {
243 DetectionKind::Started => {
244 {
245 let mut m = inner.meeting.lock().unwrap();
246 m.in_meeting = true;
247 m.app = det.app.clone();
248 }
249 emit(inner, &Event::MeetingDetected { app: det.app.clone() });
250
251 if inner.auto_record.load(Ordering::Relaxed) {
252 MeetingListener { inner: Arc::clone(inner) }.record();
253 }
254 }
255
256 DetectionKind::Updated => {
257 if let Some(title) = det.title {
259 emit(inner, &Event::MeetingUpdated { app: det.app, title });
260 }
261 }
262
263 DetectionKind::Ended => {
264 inner.meeting.lock().unwrap().recording = None;
267 emit(inner, &Event::MeetingEnded { app: det.app });
268 inner.meeting.lock().unwrap().in_meeting = false;
269 }
270 }
271 }
272
273 fn emit(inner: &Arc<Inner>, event: &Event) {
276 let handlers = inner.handlers.read().unwrap();
277 for h in handlers.iter() { h(event); }
278 }
279
280 fn unix_secs() -> u64 {
281 std::time::SystemTime::now()
282 .duration_since(std::time::UNIX_EPOCH)
283 .map(|d| d.as_secs())
284 .unwrap_or(0)
285 }
286
287 fn write_wav(path: &Path, pcm: &[i16], sample_rate: u32) -> std::io::Result<()> {
288 let mut f = std::fs::File::create(path)?;
289 let data_len = (pcm.len() * 2) as u32;
290 let byte_rate = sample_rate * 2;
291
292 f.write_all(b"RIFF")?;
293 f.write_all(&(36 + data_len).to_le_bytes())?;
294 f.write_all(b"WAVE")?;
295 f.write_all(b"fmt ")?;
296 f.write_all(&16u32.to_le_bytes())?;
297 f.write_all(&1u16.to_le_bytes())?;
298 f.write_all(&1u16.to_le_bytes())?;
299 f.write_all(&sample_rate.to_le_bytes())?;
300 f.write_all(&byte_rate.to_le_bytes())?;
301 f.write_all(&2u16.to_le_bytes())?;
302 f.write_all(&16u16.to_le_bytes())?;
303 f.write_all(b"data")?;
304 f.write_all(&data_len.to_le_bytes())?;
305 for &s in pcm { f.write_all(&s.to_le_bytes())?; }
306 Ok(())
307 }