use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::time::Duration;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use screencapturekit::cm::CMTime;
use screencapturekit::prelude::*;
use crate::pcm::{self, Resampler, SAMPLE_RATE};
use crate::source::{Capture, CaptureError};
const ZOOM_BUNDLE: &str = "us.zoom.xos";
fn drain(rx: &Receiver<Vec<i16>>) -> Result<Vec<i16>, ()> {
let mut out = Vec::new();
match rx.recv_timeout(Duration::from_millis(50)) {
Ok(block) => out.extend(block),
Err(RecvTimeoutError::Timeout) => return Ok(out),
Err(RecvTimeoutError::Disconnected) => return Err(()),
}
while let Ok(block) = rx.try_recv() {
out.extend(block);
}
Ok(out)
}
pub fn zoom_process() -> Option<u32> {
let content = SCShareableContent::get().ok()?;
content
.applications()
.iter()
.find(|a| a.bundle_identifier() == ZOOM_BUNDLE)
.map(|a| a.process_id() as u32)
}
struct AudioOut {
tx: Sender<Vec<i16>>,
}
impl SCStreamOutputTrait for AudioOut {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, of_type: SCStreamOutputType) {
if !matches!(of_type, SCStreamOutputType::Audio) {
return;
}
let Ok(list) = sample.audio_buffer_list() else {
return;
};
let Some(buffer) = list.get(0) else { return };
let floats: Vec<f32> = buffer
.data()
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_le_bytes(*c))
.collect();
let _ = self.tx.send(pcm::f32_to_i16(&floats));
}
}
pub struct ZoomAudio {
stream: SCStream,
rx: Receiver<Vec<i16>>,
}
impl ZoomAudio {
pub fn open(pid: u32) -> Result<Self, CaptureError> {
let err = |e: &dyn std::fmt::Display| {
CaptureError::Loopback(format!(
"{e}. Разрешите запись звука: Системные настройки → Конфиденциальность и безопасность → \
Запись экрана и системного звука"
))
};
let content = SCShareableContent::get().map_err(|e| err(&e))?;
let apps = content.applications();
let zoom = apps
.iter()
.find(|a| a.process_id() as u32 == pid)
.ok_or_else(|| err(&"Zoom закрыли"))?;
let displays = content.displays();
let display = displays.first().ok_or_else(|| err(&"нет экрана"))?;
let filter = SCContentFilter::create()
.with_display(display)
.with_including_applications(&[zoom], &[])
.build()
.map_err(|e| err(&e))?;
let config = SCStreamConfiguration::new()
.with_width(2)
.with_height(2)
.with_minimum_frame_interval(&CMTime::new(1, 1))
.with_captures_audio(true)
.with_excludes_current_process_audio(true)
.with_sample_rate(SAMPLE_RATE as i32)
.with_channel_count(1);
let (tx, rx) = mpsc::channel();
let mut stream = SCStream::new(&filter, &config).map_err(|e| err(&e))?;
stream
.add_output_handler(AudioOut { tx }, SCStreamOutputType::Audio)
.map_err(|e| err(&e))?;
stream.start_capture().map_err(|e| err(&e))?;
Ok(Self { stream, rx })
}
}
impl Capture for ZoomAudio {
fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
drain(&self.rx).map_err(|_| CaptureError::Loopback("поток звука Zoom остановился".into()))
}
}
impl Drop for ZoomAudio {
fn drop(&mut self) {
let _ = self.stream.stop_capture();
}
}
pub struct Microphone {
rx: Receiver<Vec<i16>>,
stop: Option<Sender<()>>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl Microphone {
pub fn open() -> Result<Self, CaptureError> {
let (tx, rx) = mpsc::channel::<Vec<i16>>();
let (stop_tx, stop_rx) = mpsc::channel::<()>();
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
let thread = std::thread::Builder::new()
.name("meet-mic".into())
.spawn(move || {
let started = (|| -> Result<cpal::Stream, String> {
let device = cpal::default_host()
.default_input_device()
.ok_or("микрофон не найден")?;
let config = device.default_input_config().map_err(|e| e.to_string())?;
let channels = config.channels() as usize;
let rate = config.sample_rate();
let mut resampler = Resampler::new(channels, rate);
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => device.build_input_stream(
config.into(),
move |data: &[f32], _: &_| {
let _ = tx.send(resampler.push(data));
},
|e| eprintln!("микрофон: {e}"),
None,
),
cpal::SampleFormat::I16 => device.build_input_stream(
config.into(),
move |data: &[i16], _: &_| {
let floats: Vec<f32> =
data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
let _ = tx.send(resampler.push(&floats));
},
|e| eprintln!("микрофон: {e}"),
None,
),
other => return Err(format!("формат {other:?} не поддерживается")),
}
.map_err(|e| e.to_string())?;
stream.play().map_err(|e| e.to_string())?;
Ok(stream)
})();
match started {
Ok(stream) => {
let _ = ready_tx.send(Ok(()));
let _ = stop_rx.recv();
drop(stream);
}
Err(e) => {
let _ = ready_tx.send(Err(e));
}
}
})
.map_err(|e| CaptureError::Microphone(e.to_string()))?;
ready_rx
.recv()
.map_err(|_| CaptureError::Microphone("поток микрофона не запустился".into()))?
.map_err(|e| {
CaptureError::Microphone(format!(
"{e}. Проверьте доступ: Системные настройки → Конфиденциальность и безопасность → Микрофон"
))
})?;
Ok(Self {
rx,
stop: Some(stop_tx),
thread: Some(thread),
})
}
}
impl Capture for Microphone {
fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
drain(&self.rx).map_err(|_| CaptureError::Microphone("поток микрофона остановился".into()))
}
}
impl Drop for Microphone {
fn drop(&mut self) {
drop(self.stop.take());
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}