feat(native): sync yocto codes (#14243)

#### PR Dependency Tree


* **PR #14243** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Batch management API for coordinated document mutations and change
tracking.
* New document accessors (IDs, state snapshots, change/delete set
queries) and subscriber count.

* **Chores**
  * Upgraded Rust edition across packages to 2024.
  * Repository-wide formatting, stylistic cleanups and test adjustments.

* **Breaking Changes**
* Removed the Node native bindings package and its JS/TS declarations
and tests (no longer published/available).

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-01-11 06:08:33 +08:00
committed by GitHub
parent d515d295ce
commit ca2462f987
143 changed files with 1396 additions and 4841 deletions

View File

@@ -1,5 +1,5 @@
[package]
edition = "2021"
edition = "2024"
name = "affine_media_capture"
version = "0.0.0"

View File

@@ -4,7 +4,7 @@ use std::hint::black_box;
#[cfg(target_os = "macos")]
use affine_media_capture::macos::audio_buffer::{mix_audio_samples, mix_audio_samples_scalar};
#[cfg(target_os = "macos")]
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
#[cfg(target_os = "macos")]
fn generate_test_samples() -> [f32; 1024] {

View File

@@ -1,8 +1,8 @@
use std::{io::Cursor, path::Path};
use napi::{
bindgen_prelude::{AbortSignal, AsyncTask, Float32Array, Result, Status, Uint8Array},
Task,
bindgen_prelude::{AbortSignal, AsyncTask, Float32Array, Result, Status, Uint8Array},
};
use napi_derive::napi;
use rubato::{Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType};
@@ -26,9 +26,7 @@ fn decode<B: AsRef<[u8]> + Send + Sync + 'static>(
// Create a probe hint using the file extension
let mut hint = Hint::new();
if let Some(ext) =
filename.and_then(|filename| Path::new(filename).extension().and_then(|ext| ext.to_str()))
{
if let Some(ext) = filename.and_then(|filename| Path::new(filename).extension().and_then(|ext| ext.to_str())) {
hint.with_extension(ext);
}
@@ -106,9 +104,7 @@ fn decode<B: AsRef<[u8]> + Send + Sync + 'static>(
let mut waves_out = resampler
.process(&waves_in, None)
.map_err(|_| Error::Unsupported("Failed to run resampler"))?;
output = waves_out
.pop()
.ok_or(Error::Unsupported("No resampled output found"))?;
output = waves_out.pop().ok_or(Error::Unsupported("No resampled output found"))?;
}
Ok(output)

View File

@@ -170,10 +170,7 @@ pub struct AudioBufferList {
}
unsafe impl Encode for AudioBufferList {
const ENCODING: Encoding = Encoding::Struct(
"AudioBufferList",
&[<u32>::ENCODING, <[AudioBuffer; 1]>::ENCODING],
);
const ENCODING: Encoding = Encoding::Struct("AudioBufferList", &[<u32>::ENCODING, <[AudioBuffer; 1]>::ENCODING]);
}
unsafe impl RefEncode for AudioBufferList {

View File

@@ -1,6 +1,6 @@
use std::fmt::Display;
use coreaudio::sys::{kAudioTapPropertyFormat, AudioObjectID};
use coreaudio::sys::{AudioObjectID, kAudioTapPropertyFormat};
use objc2::{Encode, Encoding, RefEncode};
use crate::{error::CoreAudioError, utils::get_global_main_property};
@@ -226,9 +226,8 @@ impl Display for AudioStreamDescription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"AudioStreamBasicDescription {{ mSampleRate: {}, mFormatID: {:?}, mFormatFlags: {}, \
mBytesPerPacket: {}, mFramesPerPacket: {}, mBytesPerFrame: {}, mChannelsPerFrame: {}, \
mBitsPerChannel: {}, mReserved: {} }}",
"AudioStreamBasicDescription {{ mSampleRate: {}, mFormatID: {:?}, mFormatFlags: {}, mBytesPerPacket: {}, \
mFramesPerPacket: {}, mBytesPerFrame: {}, mChannelsPerFrame: {}, mBitsPerChannel: {}, mReserved: {} }}",
self.0.mSampleRate,
AudioFormatID::from(self.0.mFormatID),
AudioFormatFlags(self.0.mFormatFlags),

View File

@@ -1,15 +1,12 @@
use std::ptr;
use objc2::{
msg_send,
AllocAnyThread, msg_send,
runtime::{AnyClass, AnyObject},
AllocAnyThread,
};
use objc2_foundation::{NSDictionary, NSError, NSNumber, NSString, NSUInteger, NSURL};
use crate::{
av_audio_format::AVAudioFormat, av_audio_pcm_buffer::AVAudioPCMBuffer, error::CoreAudioError,
};
use crate::{av_audio_format::AVAudioFormat, av_audio_pcm_buffer::AVAudioPCMBuffer, error::CoreAudioError};
#[allow(unused)]
pub(crate) struct AVAudioFile {
@@ -32,10 +29,7 @@ impl AVAudioFile {
&*NSString::from_str("AVNumberOfChannelsKey"),
],
&[
NSNumber::initWithUnsignedInt(
NSNumber::alloc(),
format.audio_stream_basic_description.0.mFormatID,
),
NSNumber::initWithUnsignedInt(NSNumber::alloc(), format.audio_stream_basic_description.0.mFormatID),
NSNumber::initWithDouble(NSNumber::alloc(), format.get_sample_rate()),
NSNumber::initWithUnsignedInt(NSNumber::alloc(), format.get_channel_count()),
],
@@ -61,8 +55,7 @@ impl AVAudioFile {
pub(crate) fn write(&self, buffer: AVAudioPCMBuffer) -> Result<(), CoreAudioError> {
let mut error: *mut NSError = ptr::null_mut();
let success: bool =
unsafe { msg_send![self.inner, writeFromBuffer: buffer.inner, error: &mut error] };
let success: bool = unsafe { msg_send![self.inner, writeFromBuffer: buffer.inner, error: &mut error] };
if !success {
return Err(CoreAudioError::WriteAVAudioFileFailed);
}

View File

@@ -1,7 +1,6 @@
use objc2::{
msg_send,
Encode, Encoding, RefEncode, msg_send,
runtime::{AnyClass, AnyObject},
Encode, Encoding, RefEncode,
};
use crate::{audio_stream_basic_desc::AudioStreamDescription, error::CoreAudioError};
@@ -37,11 +36,7 @@ unsafe impl Encode for AVAudioFormatRef {
1,
&Encoding::Struct(
"AudioChannelDescription",
&[
Encoding::UInt,
Encoding::UInt,
Encoding::Array(3, &Encoding::Float),
],
&[Encoding::UInt, Encoding::UInt, Encoding::Array(3, &Encoding::Float)],
),
),
Encoding::UInt,
@@ -62,16 +57,13 @@ unsafe impl RefEncode for AVAudioFormatRef {
#[allow(unused)]
impl AVAudioFormat {
pub fn new(
audio_stream_basic_description: AudioStreamDescription,
) -> Result<Self, CoreAudioError> {
pub fn new(audio_stream_basic_description: AudioStreamDescription) -> Result<Self, CoreAudioError> {
let cls = AnyClass::get(c"AVAudioFormat").ok_or(CoreAudioError::AVAudioFormatClassNotFound)?;
let obj: *mut AnyObject = unsafe { msg_send![cls, alloc] };
if obj.is_null() {
return Err(CoreAudioError::AllocAVAudioFormatFailed);
}
let obj: *mut AnyObject =
unsafe { msg_send![obj, initWithStreamDescription: &audio_stream_basic_description.0] };
let obj: *mut AnyObject = unsafe { msg_send![obj, initWithStreamDescription: &audio_stream_basic_description.0] };
if obj.is_null() {
return Err(CoreAudioError::InitAVAudioFormatFailed);
}

View File

@@ -13,12 +13,8 @@ pub(crate) struct AVAudioPCMBuffer {
#[allow(unused)]
impl AVAudioPCMBuffer {
pub(crate) fn new(
audio_format: &AVAudioFormat,
buffer_list: *const AudioBufferList,
) -> Result<Self, CoreAudioError> {
let cls =
AnyClass::get(c"AVAudioPCMBuffer").ok_or(CoreAudioError::AVAudioPCMBufferClassNotFound)?;
pub(crate) fn new(audio_format: &AVAudioFormat, buffer_list: *const AudioBufferList) -> Result<Self, CoreAudioError> {
let cls = AnyClass::get(c"AVAudioPCMBuffer").ok_or(CoreAudioError::AVAudioPCMBufferClassNotFound)?;
let obj: *mut AnyObject = unsafe { msg_send![cls, alloc] };
if obj.is_null() {
return Err(CoreAudioError::AllocAVAudioPCMBufferFailed);

View File

@@ -4,9 +4,8 @@ use core_foundation::{
};
use coreaudio::sys::AudioObjectID;
use objc2::{
msg_send,
AllocAnyThread, msg_send,
runtime::{AnyClass, AnyObject},
AllocAnyThread,
};
use objc2_foundation::{NSArray, NSNumber, NSString, NSUUID};
@@ -17,19 +16,14 @@ pub(crate) struct CATapDescription {
}
impl CATapDescription {
pub fn init_stereo_mixdown_of_processes(
process: AudioObjectID,
) -> std::result::Result<Self, CoreAudioError> {
let cls =
AnyClass::get(c"CATapDescription").ok_or(CoreAudioError::CATapDescriptionClassNotFound)?;
pub fn init_stereo_mixdown_of_processes(process: AudioObjectID) -> std::result::Result<Self, CoreAudioError> {
let cls = AnyClass::get(c"CATapDescription").ok_or(CoreAudioError::CATapDescriptionClassNotFound)?;
let obj: *mut AnyObject = unsafe { msg_send![cls, alloc] };
if obj.is_null() {
return Err(CoreAudioError::AllocCATapDescriptionFailed);
}
let processes_array =
NSArray::from_retained_slice(&[NSNumber::initWithUnsignedInt(NSNumber::alloc(), process)]);
let obj: *mut AnyObject =
unsafe { msg_send![obj, initStereoMixdownOfProcesses: &*processes_array] };
let processes_array = NSArray::from_retained_slice(&[NSNumber::initWithUnsignedInt(NSNumber::alloc(), process)]);
let obj: *mut AnyObject = unsafe { msg_send![obj, initStereoMixdownOfProcesses: &*processes_array] };
if obj.is_null() {
return Err(CoreAudioError::InitStereoMixdownOfProcessesFailed);
}
@@ -44,8 +38,7 @@ impl CATapDescription {
pub fn init_stereo_global_tap_but_exclude_processes(
processes: &[AudioObjectID],
) -> std::result::Result<Self, CoreAudioError> {
let cls =
AnyClass::get(c"CATapDescription").ok_or(CoreAudioError::CATapDescriptionClassNotFound)?;
let cls = AnyClass::get(c"CATapDescription").ok_or(CoreAudioError::CATapDescriptionClassNotFound)?;
let obj: *mut AnyObject = unsafe { msg_send![cls, alloc] };
if obj.is_null() {
return Err(CoreAudioError::AllocCATapDescriptionFailed);
@@ -57,8 +50,7 @@ impl CATapDescription {
.collect::<Vec<_>>()
.as_slice(),
);
let obj: *mut AnyObject =
unsafe { msg_send![obj, initStereoGlobalTapButExcludeProcesses: &*processes_array] };
let obj: *mut AnyObject = unsafe { msg_send![obj, initStereoGlobalTapButExcludeProcesses: &*processes_array] };
if obj.is_null() {
return Err(CoreAudioError::InitStereoGlobalTapButExcludeProcessesFailed);
}

View File

@@ -60,14 +60,7 @@ impl ToCoreFoundation for CFDictionary<CFType, CFType> {
impl<T: ToCoreFoundation> ToCoreFoundation for Vec<T> {
fn to_cf(&self) -> CFType {
CFArray::from_CFTypes(
self
.iter()
.map(|t| t.to_cf())
.collect::<Vec<_>>()
.as_slice(),
)
.as_CFType()
CFArray::from_CFTypes(self.iter().map(|t| t.to_cf()).collect::<Vec<_>>().as_slice()).as_CFType()
}
}

View File

@@ -2,8 +2,7 @@ use std::ptr;
use core_foundation::{base::TCFType, string::CFString};
use coreaudio::sys::{
kAudioDevicePropertyDeviceUID, kAudioObjectSystemObject, AudioDeviceID, AudioObjectID,
CFStringRef,
AudioDeviceID, AudioObjectID, CFStringRef, kAudioDevicePropertyDeviceUID, kAudioObjectSystemObject,
};
use crate::{error::CoreAudioError, utils::get_global_main_property};
@@ -13,20 +12,14 @@ pub(crate) fn get_device_uid(
) -> std::result::Result<(AudioObjectID, CFString), CoreAudioError> {
let system_audio_id = get_device_audio_id(device_id)?;
let mut output_uid: CFStringRef = ptr::null_mut();
get_global_main_property(
system_audio_id,
kAudioDevicePropertyDeviceUID,
&mut output_uid,
)?;
get_global_main_property(system_audio_id, kAudioDevicePropertyDeviceUID, &mut output_uid)?;
Ok((system_audio_id, unsafe {
CFString::wrap_under_create_rule(output_uid.cast())
}))
}
pub(crate) fn get_device_audio_id(
device_id: AudioDeviceID,
) -> std::result::Result<AudioObjectID, CoreAudioError> {
pub(crate) fn get_device_audio_id(device_id: AudioDeviceID) -> std::result::Result<AudioObjectID, CoreAudioError> {
let mut system_output_id: AudioObjectID = 0;
get_global_main_property(kAudioObjectSystemObject, device_id, &mut system_output_id)?;

View File

@@ -1,10 +1,9 @@
use std::{mem::MaybeUninit, ptr};
use coreaudio::sys::{
kAudioHardwareNoError, kAudioHardwarePropertyProcessObjectList, kAudioObjectPropertyElementMain,
kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, AudioObjectGetPropertyData,
AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectPropertySelector,
AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectPropertySelector, kAudioHardwareNoError, kAudioHardwarePropertyProcessObjectList,
kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject,
};
use crate::error::CoreAudioError;
@@ -17,15 +16,8 @@ pub fn audio_process_list() -> Result<Vec<AudioObjectID>, CoreAudioError> {
};
let mut data_size = 0u32;
let status = unsafe {
AudioObjectGetPropertyDataSize(
kAudioObjectSystemObject,
&address,
0,
ptr::null_mut(),
&mut data_size,
)
};
let status =
unsafe { AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address, 0, ptr::null_mut(), &mut data_size) };
if status != kAudioHardwareNoError as i32 {
return Err(CoreAudioError::GetProcessObjectListSizeFailed(status));
@@ -63,9 +55,7 @@ pub fn get_process_property<T: Sized>(
};
let mut data_size = 0u32;
let status = unsafe {
AudioObjectGetPropertyDataSize(object_id, &address, 0, ptr::null_mut(), &mut data_size)
};
let status = unsafe { AudioObjectGetPropertyDataSize(object_id, &address, 0, ptr::null_mut(), &mut data_size) };
if status != kAudioHardwareNoError as i32 {
return Err(CoreAudioError::AudioObjectGetPropertyDataSizeFailed(status));

View File

@@ -3,8 +3,8 @@ use std::{
ffi::c_void,
ptr,
sync::{
atomic::{AtomicPtr, Ordering},
Arc, LazyLock, RwLock,
atomic::{AtomicPtr, Ordering},
},
};
@@ -14,11 +14,10 @@ use core_foundation::{
string::{CFString, CFStringRef},
};
use coreaudio::sys::{
kAudioHardwarePropertyProcessObjectList, kAudioObjectPropertyElementMain,
AudioObjectAddPropertyListenerBlock, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectRemovePropertyListenerBlock, kAudioHardwarePropertyProcessObjectList, kAudioObjectPropertyElementMain,
kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, kAudioProcessPropertyBundleID,
kAudioProcessPropertyIsRunning, kAudioProcessPropertyIsRunningInput, kAudioProcessPropertyPID,
AudioObjectAddPropertyListenerBlock, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectRemovePropertyListenerBlock,
};
use libc;
use napi::{
@@ -27,9 +26,8 @@ use napi::{
};
use napi_derive::napi;
use objc2::{
msg_send,
Encode, Encoding, msg_send,
runtime::{AnyClass, AnyObject},
Encode, Encoding,
};
use objc2_foundation::NSString;
use screencapturekit::shareable_content::SCShareableContent;
@@ -71,20 +69,16 @@ unsafe impl Encode for CGRect {
const ENCODING: Encoding = Encoding::Struct("CGRect", &[<CGPoint>::ENCODING, <CGSize>::ENCODING]);
}
static RUNNING_APPLICATIONS: LazyLock<
RwLock<std::result::Result<Vec<AudioObjectID>, CoreAudioError>>,
> = LazyLock::new(|| RwLock::new(audio_process_list()));
static RUNNING_APPLICATIONS: LazyLock<RwLock<std::result::Result<Vec<AudioObjectID>, CoreAudioError>>> =
LazyLock::new(|| RwLock::new(audio_process_list()));
type ApplicationStateChangedSubscriberMap =
HashMap<AudioObjectID, HashMap<Uuid, Arc<ThreadsafeFunction<(), ()>>>>;
type ApplicationStateChangedSubscriberMap = HashMap<AudioObjectID, HashMap<Uuid, Arc<ThreadsafeFunction<(), ()>>>>;
static APPLICATION_STATE_CHANGED_SUBSCRIBERS: LazyLock<
RwLock<ApplicationStateChangedSubscriberMap>,
> = LazyLock::new(|| RwLock::new(HashMap::new()));
static APPLICATION_STATE_CHANGED_SUBSCRIBERS: LazyLock<RwLock<ApplicationStateChangedSubscriberMap>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
static APPLICATION_STATE_CHANGED_LISTENER_BLOCKS: LazyLock<
RwLock<HashMap<AudioObjectID, AtomicPtr<c_void>>>,
> = LazyLock::new(|| RwLock::new(HashMap::new()));
static APPLICATION_STATE_CHANGED_LISTENER_BLOCKS: LazyLock<RwLock<HashMap<AudioObjectID, AtomicPtr<c_void>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
static NSRUNNING_APPLICATION_CLASS: LazyLock<Option<&'static AnyClass>> =
LazyLock::new(|| AnyClass::get(c"NSRunningApplication"));
@@ -155,14 +149,12 @@ impl ApplicationInfo {
}
// If not available, try to get from the audio process property
if self.object_id > 0 {
if let Ok(bundle_id) =
get_process_property::<CFStringRef>(&self.object_id, kAudioProcessPropertyBundleID)
{
// Safely convert CFStringRef to Rust String
let cf_string = unsafe { CFString::wrap_under_get_rule(bundle_id) };
return cf_string.to_string();
}
if self.object_id > 0
&& let Ok(bundle_id) = get_process_property::<CFStringRef>(&self.object_id, kAudioProcessPropertyBundleID)
{
// Safely convert CFStringRef to Rust String
let cf_string = unsafe { CFString::wrap_under_get_rule(bundle_id) };
return cf_string.to_string();
}
String::new()
@@ -281,8 +273,7 @@ impl ApplicationInfo {
let _: () = msg_send![properties, setObject: compression_value, forKey: &*compression_key];
// Get PNG data with properties
let png_data: *mut AnyObject =
msg_send![bitmap, representationUsingType: 4u64, properties: properties]; // 4 = PNG
let png_data: *mut AnyObject = msg_send![bitmap, representationUsingType: 4u64, properties: properties]; // 4 = PNG
if png_data.is_null() {
return Ok(Buffer::from(Vec::<u8>::new()));
@@ -338,10 +329,7 @@ impl ApplicationListChangedSubscriber {
match result {
Ok(status) => {
if status != 0 {
return Err(Error::new(
Status::GenericFailure,
"Failed to remove property listener",
));
return Err(Error::new(Status::GenericFailure, "Failed to remove property listener"));
}
Ok(())
}
@@ -363,31 +351,31 @@ pub struct ApplicationStateChangedSubscriber {
impl ApplicationStateChangedSubscriber {
#[napi]
pub fn unsubscribe(&self) {
if let Ok(mut lock) = APPLICATION_STATE_CHANGED_SUBSCRIBERS.write() {
if let Some(subscribers) = lock.get_mut(&self.object_id) {
subscribers.remove(&self.id);
if subscribers.is_empty() {
lock.remove(&self.object_id);
if let Some(listener_block) = APPLICATION_STATE_CHANGED_LISTENER_BLOCKS
.write()
.ok()
.as_mut()
.and_then(|map| map.remove(&self.object_id))
{
// Wrap in catch_unwind to prevent crashes during shutdown
let _ = std::panic::catch_unwind(|| unsafe {
AudioObjectRemovePropertyListenerBlock(
self.object_id,
&AudioObjectPropertyAddress {
mSelector: kAudioProcessPropertyIsRunning,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
},
ptr::null_mut(),
listener_block.load(Ordering::Relaxed),
);
});
}
if let Ok(mut lock) = APPLICATION_STATE_CHANGED_SUBSCRIBERS.write()
&& let Some(subscribers) = lock.get_mut(&self.object_id)
{
subscribers.remove(&self.id);
if subscribers.is_empty() {
lock.remove(&self.object_id);
if let Some(listener_block) = APPLICATION_STATE_CHANGED_LISTENER_BLOCKS
.write()
.ok()
.as_mut()
.and_then(|map| map.remove(&self.object_id))
{
// Wrap in catch_unwind to prevent crashes during shutdown
let _ = std::panic::catch_unwind(|| unsafe {
AudioObjectRemovePropertyListenerBlock(
self.object_id,
&AudioObjectPropertyAddress {
mSelector: kAudioProcessPropertyIsRunning,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
},
ptr::null_mut(),
listener_block.load(Ordering::Relaxed),
);
});
}
}
}
@@ -402,9 +390,7 @@ pub struct ShareableContent {
#[napi]
impl ShareableContent {
#[napi]
pub fn on_application_list_changed(
callback: ThreadsafeFunction<(), ()>,
) -> Result<ApplicationListChangedSubscriber> {
pub fn on_application_list_changed(callback: ThreadsafeFunction<(), ()>) -> Result<ApplicationListChangedSubscriber> {
let callback_arc = Arc::new(callback);
let callback_clone = callback_arc.clone();
let callback_block: RcBlock<dyn Fn(u32, *mut c_void)> =
@@ -442,10 +428,7 @@ impl ShareableContent {
)
};
if status != 0 {
return Err(Error::new(
Status::GenericFailure,
"Failed to add property listener",
));
return Err(Error::new(Status::GenericFailure, "Failed to add property listener"));
}
Ok(ApplicationListChangedSubscriber {
listener_block: callback_block,
@@ -480,16 +463,15 @@ impl ShareableContent {
)
};
for address in addresses {
if address.mSelector == kAudioProcessPropertyIsRunning {
if let Some(subscribers) = APPLICATION_STATE_CHANGED_SUBSCRIBERS
if address.mSelector == kAudioProcessPropertyIsRunning
&& let Some(subscribers) = APPLICATION_STATE_CHANGED_SUBSCRIBERS
.read()
.ok()
.as_ref()
.and_then(|map| map.get(&object_id))
{
for callback in subscribers.values() {
callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking);
}
{
for callback in subscribers.values() {
callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking);
}
}
}
@@ -501,18 +483,10 @@ impl ShareableContent {
};
let listener_block = &*list_change as *const Block<dyn Fn(u32, *mut c_void)>;
let status = unsafe {
AudioObjectAddPropertyListenerBlock(
object_id,
&address,
ptr::null_mut(),
listener_block.cast_mut().cast(),
)
AudioObjectAddPropertyListenerBlock(object_id, &address, ptr::null_mut(), listener_block.cast_mut().cast())
};
if status != 0 {
return Err(Error::new(
Status::GenericFailure,
"Failed to add property listener",
));
return Err(Error::new(Status::GenericFailure, "Failed to add property listener"));
}
let subscribers = {
let mut map = HashMap::new();
@@ -659,16 +633,16 @@ impl ShareableContent {
}
// Find the audio object ID for this process
if let Ok(app_list) = RUNNING_APPLICATIONS.read() {
if let Ok(app_list) = app_list.as_ref() {
for object_id in app_list {
let pid = get_process_property(object_id, kAudioProcessPropertyPID).unwrap_or(-1);
if pid == process_id as i32 {
// Check if the process is actively using input (microphone)
match get_process_property(object_id, kAudioProcessPropertyIsRunningInput) {
Ok(is_running) => return Ok(is_running),
Err(_) => continue,
}
if let Ok(app_list) = RUNNING_APPLICATIONS.read()
&& let Ok(app_list) = app_list.as_ref()
{
for object_id in app_list {
let pid = get_process_property(object_id, kAudioProcessPropertyPID).unwrap_or(-1);
if pid == process_id as i32 {
// Check if the process is actively using input (microphone)
match get_process_property(object_id, kAudioProcessPropertyIsRunningInput) {
Ok(is_running) => return Ok(is_running),
Err(_) => continue,
}
}
}

View File

@@ -8,20 +8,17 @@ use core_foundation::{
uuid::CFUUID,
};
use coreaudio::sys::{
kAudioAggregateDeviceClockDeviceKey, kAudioAggregateDeviceIsPrivateKey,
kAudioAggregateDeviceIsStackedKey, kAudioAggregateDeviceMainSubDeviceKey,
kAudioAggregateDeviceNameKey, kAudioAggregateDeviceSubDeviceListKey,
kAudioAggregateDeviceTapAutoStartKey, kAudioAggregateDeviceTapListKey,
kAudioAggregateDeviceUIDKey, kAudioDevicePropertyDeviceIsAlive,
kAudioDevicePropertyNominalSampleRate, kAudioHardwareBadDeviceError,
kAudioHardwareBadStreamError, kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice,
kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyElementMain,
kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, kAudioSubDeviceUIDKey,
kAudioSubTapUIDKey, AudioDeviceCreateIOProcIDWithBlock, AudioDeviceDestroyIOProcID,
AudioDeviceIOProcID, AudioDeviceStart, AudioDeviceStop, AudioHardwareCreateAggregateDevice,
AudioHardwareDestroyAggregateDevice, AudioObjectAddPropertyListenerBlock,
AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectRemovePropertyListenerBlock, AudioTimeStamp, OSStatus,
AudioDeviceCreateIOProcIDWithBlock, AudioDeviceDestroyIOProcID, AudioDeviceIOProcID, AudioDeviceStart,
AudioDeviceStop, AudioHardwareCreateAggregateDevice, AudioHardwareDestroyAggregateDevice,
AudioObjectAddPropertyListenerBlock, AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
AudioObjectRemovePropertyListenerBlock, AudioTimeStamp, OSStatus, kAudioAggregateDeviceClockDeviceKey,
kAudioAggregateDeviceIsPrivateKey, kAudioAggregateDeviceIsStackedKey, kAudioAggregateDeviceMainSubDeviceKey,
kAudioAggregateDeviceNameKey, kAudioAggregateDeviceSubDeviceListKey, kAudioAggregateDeviceTapAutoStartKey,
kAudioAggregateDeviceTapListKey, kAudioAggregateDeviceUIDKey, kAudioDevicePropertyDeviceIsAlive,
kAudioDevicePropertyNominalSampleRate, kAudioHardwareBadDeviceError, kAudioHardwareBadStreamError,
kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice, kAudioHardwarePropertyDefaultOutputDevice,
kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, kAudioSubDeviceUIDKey,
kAudioSubTapUIDKey,
};
use napi::{
bindgen_prelude::{Float32Array, Result, Status},
@@ -41,11 +38,8 @@ use crate::{
utils::{cfstring_from_bytes_with_nul, get_global_main_property},
};
extern "C" {
fn AudioHardwareCreateProcessTap(
inDescription: *mut AnyObject,
outTapID: *mut AudioObjectID,
) -> OSStatus;
unsafe extern "C" {
fn AudioHardwareCreateProcessTap(inDescription: *mut AnyObject, outTapID: *mut AudioObjectID) -> OSStatus;
fn AudioHardwareDestroyProcessTap(tapID: AudioObjectID) -> OSStatus;
}
@@ -80,12 +74,10 @@ impl AggregateDevice {
return Err(CoreAudioError::CreateProcessTapFailed(status).into());
}
let (input_device_id, default_input_uid) =
get_device_uid(kAudioHardwarePropertyDefaultInputDevice)?;
let (input_device_id, default_input_uid) = get_device_uid(kAudioHardwarePropertyDefaultInputDevice)?;
// Get the default output device ID
let (output_device_id, output_device_uid) =
get_device_uid(kAudioHardwarePropertyDefaultOutputDevice)?;
let (output_device_id, output_device_uid) = get_device_uid(kAudioHardwarePropertyDefaultOutputDevice)?;
let description_dict = Self::create_aggregate_description(
tap_id,
tap_description.get_uuid()?,
@@ -96,10 +88,7 @@ impl AggregateDevice {
let mut aggregate_device_id: AudioObjectID = 0;
let status = unsafe {
AudioHardwareCreateAggregateDevice(
description_dict.as_concrete_TypeRef().cast(),
&mut aggregate_device_id,
)
AudioHardwareCreateAggregateDevice(description_dict.as_concrete_TypeRef().cast(), &mut aggregate_device_id)
};
if status != 0 {
@@ -119,8 +108,7 @@ impl AggregateDevice {
pub fn create_global_tap_but_exclude_processes(processes: &[AudioObjectID]) -> Result<Self> {
let mut tap_id: AudioObjectID = 0;
let tap_description =
CATapDescription::init_stereo_global_tap_but_exclude_processes(processes)?;
let tap_description = CATapDescription::init_stereo_global_tap_but_exclude_processes(processes)?;
let status = unsafe { AudioHardwareCreateProcessTap(tap_description.inner, &mut tap_id) };
if status != 0 {
@@ -128,12 +116,10 @@ impl AggregateDevice {
}
// Get the default input device (microphone) UID and ID
let (input_device_id, default_input_uid) =
get_device_uid(kAudioHardwarePropertyDefaultInputDevice)?;
let (input_device_id, default_input_uid) = get_device_uid(kAudioHardwarePropertyDefaultInputDevice)?;
// Get the default output device ID
let (output_device_id, output_device_uid) =
get_device_uid(kAudioHardwarePropertyDefaultOutputDevice)?;
let (output_device_id, output_device_uid) = get_device_uid(kAudioHardwarePropertyDefaultOutputDevice)?;
let description_dict = Self::create_aggregate_description(
tap_id,
@@ -145,10 +131,7 @@ impl AggregateDevice {
let mut aggregate_device_id: AudioObjectID = 0;
let status = unsafe {
AudioHardwareCreateAggregateDevice(
description_dict.as_concrete_TypeRef().cast(),
&mut aggregate_device_id,
)
AudioHardwareCreateAggregateDevice(description_dict.as_concrete_TypeRef().cast(), &mut aggregate_device_id)
};
// Check the status and return the appropriate result
@@ -184,11 +167,7 @@ impl AggregateDevice {
fn get_aggregate_device_stats(&self) -> Result<AudioStats> {
let mut sample_rate: f64 = 0.0;
get_global_main_property(
self.id,
kAudioDevicePropertyNominalSampleRate,
&mut sample_rate,
)?;
get_global_main_property(self.id, kAudioDevicePropertyNominalSampleRate, &mut sample_rate)?;
let audio_stats = AudioStats {
sample_rate,
@@ -270,9 +249,7 @@ impl AggregateDevice {
// Use the consistent stats for the stream object returned
let audio_stats_for_stream = current_audio_stats;
let in_io_block: RcBlock<
dyn Fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void) -> i32,
>;
let in_io_block: RcBlock<dyn Fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void) -> i32>;
{
in_io_block = RcBlock::new(
move |_in_now: *mut c_void,
@@ -286,9 +263,7 @@ impl AggregateDevice {
if *mSampleTime < 0.0 {
return kAudioHardwareNoError as i32;
}
let Ok(dua_audio_buffer_list) =
(unsafe { InputAndOutputAudioBufferList::from_raw(in_input_data) })
else {
let Ok(dua_audio_buffer_list) = (unsafe { InputAndOutputAudioBufferList::from_raw(in_input_data) }) else {
return kAudioHardwareBadDeviceError as i32;
};
@@ -301,10 +276,7 @@ impl AggregateDevice {
};
// Send the processed audio data to JavaScript
audio_stream_callback.call(
Ok(mixed_samples.into()),
ThreadsafeFunctionCallMode::NonBlocking,
);
audio_stream_callback.call(Ok(mixed_samples.into()), ThreadsafeFunctionCallMode::NonBlocking);
kAudioHardwareNoError as i32
},
@@ -316,10 +288,7 @@ impl AggregateDevice {
&mut in_proc_id,
self.id,
dispatch2::DispatchRetained::as_ptr(&queue).as_ptr().cast(),
(&*in_io_block
as *const Block<
dyn Fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void) -> i32,
>)
(&*in_io_block as *const Block<dyn Fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void, *mut c_void) -> i32>)
.cast_mut()
.cast(),
)
@@ -374,34 +343,16 @@ impl AggregateDevice {
let mut cf_dict_builder = CFDictionaryBuilder::new();
cf_dict_builder
.add(
kAudioAggregateDeviceNameKey.as_slice(),
aggregate_device_name,
)
.add(
kAudioAggregateDeviceUIDKey.as_slice(),
aggregate_device_uid_string,
)
.add(
kAudioAggregateDeviceMainSubDeviceKey.as_slice(),
&output_device_id,
)
.add(kAudioAggregateDeviceNameKey.as_slice(), aggregate_device_name)
.add(kAudioAggregateDeviceUIDKey.as_slice(), aggregate_device_uid_string)
.add(kAudioAggregateDeviceMainSubDeviceKey.as_slice(), &output_device_id)
.add(kAudioAggregateDeviceIsPrivateKey.as_slice(), true)
// can't be stacked because we're using a tap
.add(kAudioAggregateDeviceIsStackedKey.as_slice(), false)
.add(kAudioAggregateDeviceTapAutoStartKey.as_slice(), true)
.add(
kAudioAggregateDeviceSubDeviceListKey.as_slice(),
capture_device_list,
)
.add(
kAudioAggregateDeviceClockDeviceKey.as_slice(),
input_device_id,
)
.add(
kAudioAggregateDeviceTapListKey.as_slice(),
vec![tap_device_dict],
);
.add(kAudioAggregateDeviceSubDeviceListKey.as_slice(), capture_device_list)
.add(kAudioAggregateDeviceClockDeviceKey.as_slice(), input_device_id)
.add(kAudioAggregateDeviceTapListKey.as_slice(), vec![tap_device_dict]);
Ok(cf_dict_builder.build())
}
@@ -627,18 +578,13 @@ impl AggregateDeviceManager {
// Start the initial stream
// Pass the initially determined consistent audio stats
let original_audio_stats = self
.device
.get_aggregate_device_stats()
.unwrap_or(AudioStats {
sample_rate: 48000.0, // Match fallback in setup_device_change_listeners
channels: 2,
});
let original_audio_stats = self.device.get_aggregate_device_stats().unwrap_or(AudioStats {
sample_rate: 48000.0, // Match fallback in setup_device_change_listeners
channels: 2,
});
self.original_audio_stats = Some(original_audio_stats); // Store for listener use
let initial_audio_tap_stream = self
.device
.start(audio_stream_callback.clone(), original_audio_stats)?; // Pass clone of callback
let initial_audio_tap_stream = self.device.start(audio_stream_callback.clone(), original_audio_stats)?; // Pass clone of callback
// Setup device change listeners AFTER getting initial stats and stream
self.setup_device_change_listeners()?;
@@ -679,70 +625,68 @@ impl AggregateDeviceManager {
};
// Create a block that will handle device changes
let device_changed_block = RcBlock::new(
move |_in_number_addresses: u32, _in_addresses: *mut c_void| {
// Skip if we don't have all required information
let Some(stream_mutex) = stream_arc.as_ref() else {
return;
};
let Some(callback) = callback_arc.as_ref() else {
return;
};
let device_changed_block = RcBlock::new(move |_in_number_addresses: u32, _in_addresses: *mut c_void| {
// Skip if we don't have all required information
let Some(stream_mutex) = stream_arc.as_ref() else {
return;
};
let Some(callback) = callback_arc.as_ref() else {
return;
};
// Try to lock the stream mutex
let Ok(mut stream_guard) = stream_mutex.lock() else {
return;
};
// Try to lock the stream mutex
let Ok(mut stream_guard) = stream_mutex.lock() else {
return;
};
// Create a new device with updated default devices
let result: Result<AggregateDevice> = {
if is_app_specific {
if let Some(id) = app_id {
// For device change listener, we need to create a minimal ApplicationInfo
// We don't have the name here, so we'll use an empty string
let app = ApplicationInfo::new(id as i32, String::new(), id);
AggregateDevice::new(&app)
} else {
Err(CoreAudioError::CreateProcessTapFailed(0).into())
}
// Create a new device with updated default devices
let result: Result<AggregateDevice> = {
if is_app_specific {
if let Some(id) = app_id {
// For device change listener, we need to create a minimal ApplicationInfo
// We don't have the name here, so we'll use an empty string
let app = ApplicationInfo::new(id as i32, String::new(), id);
AggregateDevice::new(&app)
} else {
AggregateDevice::create_global_tap_but_exclude_processes(&excluded_processes)
Err(CoreAudioError::CreateProcessTapFailed(0).into())
}
};
} else {
AggregateDevice::create_global_tap_but_exclude_processes(&excluded_processes)
}
};
// If we successfully created a new device, stop the old stream and start a new
// one
match result {
Ok(mut new_device) => {
// Stop and drop the old stream if it exists
if let Some(mut old_stream) = stream_guard.take() {
// Explicitly drop the old stream's Box before creating the new device.
// The drop implementation handles cleanup.
// We call stop() directly.
let stop_result = old_stream.stop();
match stop_result {
Ok(_) => {}
Err(e) => println!("DEBUG: Error stopping old stream (proceeding anyway): {e}"),
};
drop(old_stream); // Ensure it's dropped now
}
match new_device.start(callback.clone(), original_audio_stats) {
Ok(new_stream) => {
// Use the existing stream_guard which already holds the lock
*stream_guard = Some(new_stream);
}
Err(e) => {
println!("DEBUG: Failed to start new stream: {e}");
}
}
// If we successfully created a new device, stop the old stream and start a new
// one
match result {
Ok(mut new_device) => {
// Stop and drop the old stream if it exists
if let Some(mut old_stream) = stream_guard.take() {
// Explicitly drop the old stream's Box before creating the new device.
// The drop implementation handles cleanup.
// We call stop() directly.
let stop_result = old_stream.stop();
match stop_result {
Ok(_) => {}
Err(e) => println!("DEBUG: Error stopping old stream (proceeding anyway): {e}"),
};
drop(old_stream); // Ensure it's dropped now
}
Err(e) => {
println!("DEBUG: Failed to create new device: {e}");
match new_device.start(callback.clone(), original_audio_stats) {
Ok(new_stream) => {
// Use the existing stream_guard which already holds the lock
*stream_guard = Some(new_stream);
}
Err(e) => {
println!("DEBUG: Failed to start new stream: {e}");
}
}
}
},
);
Err(e) => {
println!("DEBUG: Failed to create new device: {e}");
}
}
});
// Create pointers to the device_changed_block that can be used in C functions
let block_ptr = &*device_changed_block as *const Block<dyn Fn(u32, *mut c_void)>;
@@ -1026,9 +970,7 @@ impl AudioCaptureSession {
if let Some(manager) = &self.manager {
manager
.get_current_actual_sample_rate()? // Propagate CoreAudioError
.ok_or_else(|| {
napi::Error::from_reason("No active audio stream to get actual sample rate from")
})
.ok_or_else(|| napi::Error::from_reason("No active audio stream to get actual sample rate from"))
} else if let Some(cached_rate) = self.sample_rate {
// Return cached sample rate as the best approximation when session is stopped
Ok(cached_rate)

View File

@@ -2,8 +2,8 @@ use std::{cell::RefCell, collections::HashMap, ffi::c_void, mem::size_of};
use core_foundation::string::CFString;
use coreaudio::sys::{
kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, AudioObjectGetPropertyData,
AudioObjectID, AudioObjectPropertyAddress,
AudioObjectGetPropertyData, AudioObjectID, AudioObjectPropertyAddress, kAudioObjectPropertyElementMain,
kAudioObjectPropertyScopeGlobal,
};
use rubato::{FastFixedIn, PolynomialDegree, Resampler};
@@ -29,7 +29,7 @@ impl BufferedResampler {
let ratio = to_sr / from_sr;
let resampler = FastFixedIn::<f32>::new(
ratio,
1.0, // max_resample_ratio_relative (must be >= 1.0, use 1.0 for fixed ratio)
1.0, // max_resample_ratio_relative (must be >= 1.0, use 1.0 for fixed ratio)
PolynomialDegree::Linear, // Use Linear interpolation quality
RESAMPLER_INPUT_CHUNK,
channels,
@@ -58,9 +58,7 @@ impl BufferedResampler {
// Drain exactly one chunk per channel
let mut chunk: Vec<Vec<f32>> = Vec::with_capacity(self.channels);
for ch in 0..self.channels {
let tail = self.fifo[ch]
.drain(..RESAMPLER_INPUT_CHUNK)
.collect::<Vec<_>>();
let tail = self.fifo[ch].drain(..RESAMPLER_INPUT_CHUNK).collect::<Vec<_>>();
chunk.push(tail);
}
@@ -171,20 +169,14 @@ pub fn process_audio_frame(
if current_sample_rate != target_sample_rate {
// Use (or create) a persistent BufferedResampler
let out_vec = RESAMPLER_CACHE.with(|cache| {
RESAMPLER_CACHE.with(|cache| {
let mut map = cache.borrow_mut();
let key = (
current_sample_rate as u32,
target_sample_rate as u32,
2usize,
);
let key = (current_sample_rate as u32, target_sample_rate as u32, 2usize);
let resampler = map
.entry(key)
.or_insert_with(|| BufferedResampler::new(current_sample_rate, target_sample_rate, 2));
resampler.feed(&[left, right])
});
out_vec
})
} else {
// No resampling needed, just interleave existing left/right data
let mut interleaved: Vec<f32> = Vec::with_capacity(left.len() * 2);
@@ -202,11 +194,7 @@ pub fn process_audio_frame(
if current_sample_rate != target_sample_rate {
let out_vec = RESAMPLER_CACHE.with(|cache| {
let mut map = cache.borrow_mut();
let key = (
current_sample_rate as u32,
target_sample_rate as u32,
1usize,
);
let key = (current_sample_rate as u32, target_sample_rate as u32, 1usize);
let resampler = map
.entry(key)
.or_insert_with(|| BufferedResampler::new(current_sample_rate, target_sample_rate, 1));

View File

@@ -2,21 +2,21 @@ use std::{
cell::RefCell,
collections::HashMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
thread::JoinHandle,
};
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
SampleRate,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use crossbeam_channel::unbounded;
use napi::{
Error, Status,
bindgen_prelude::{Float32Array, Result},
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
Error, Status,
};
use napi_derive::napi;
use rubato::{FastFixedIn, PolynomialDegree, Resampler};
@@ -95,11 +95,7 @@ thread_local! {
static RESAMPLER_CACHE: RefCell<HashMap<(u32, u32, usize), BufferedResampler>> = RefCell::new(HashMap::new());
}
fn process_audio_with_resampler(
samples: Vec<f32>,
from_sample_rate: u32,
to_sample_rate: u32,
) -> Vec<f32> {
fn process_audio_with_resampler(samples: Vec<f32>, from_sample_rate: u32, to_sample_rate: u32) -> Vec<f32> {
if from_sample_rate == to_sample_rate {
return samples;
}
@@ -220,16 +216,13 @@ impl Drop for AudioCaptureSession {
}
}
pub fn start_recording(
audio_buffer_callback: ThreadsafeFunction<Float32Array, ()>,
) -> Result<AudioCaptureSession> {
pub fn start_recording(audio_buffer_callback: ThreadsafeFunction<Float32Array, ()>) -> Result<AudioCaptureSession> {
let available_hosts = cpal::available_hosts();
let host_id = available_hosts
.first()
.ok_or_else(|| Error::new(Status::GenericFailure, "No CPAL hosts available"))?;
let host =
cpal::host_from_id(*host_id).map_err(|e| Error::new(Status::GenericFailure, format!("{e}")))?;
let host = cpal::host_from_id(*host_id).map_err(|e| Error::new(Status::GenericFailure, format!("{e}")))?;
let mic = host
.default_input_device()
@@ -267,9 +260,7 @@ pub fn start_recording(
.build_input_stream(
&mic_stream_config,
move |data: &[f32], _| {
let _ = tx_mic.send(AudioBuffer {
data: data.to_vec(),
});
let _ = tx_mic.send(AudioBuffer { data: data.to_vec() });
},
|err| eprintln!("CPAL mic stream error: {err}"),
None,
@@ -282,9 +273,7 @@ pub fn start_recording(
.build_input_stream(
&lb_stream_config,
move |data: &[f32], _| {
let _ = tx_lb.send(AudioBuffer {
data: data.to_vec(),
});
let _ = tx_lb.send(AudioBuffer { data: data.to_vec() });
},
|err| eprintln!("CPAL loopback stream error: {err}"),
None,
@@ -306,11 +295,7 @@ pub fn start_recording(
let mono_samples: Vec<f32> = if mic_channels == 1 {
buf.data
} else {
buf
.data
.chunks(mic_channels as usize)
.map(to_mono)
.collect()
buf.data.chunks(mic_channels as usize).map(to_mono).collect()
};
pre_mic.extend_from_slice(&mono_samples);
}
@@ -347,10 +332,7 @@ pub fn start_recording(
let lb_chunk: Vec<f32> = post_lb.drain(..TARGET_FRAME_SIZE).collect();
let mixed = mix(&mic_chunk, &lb_chunk);
if !mixed.is_empty() {
let _ = audio_buffer_callback.call(
Ok(mixed.clone().into()),
ThreadsafeFunctionCallMode::NonBlocking,
);
let _ = audio_buffer_callback.call(Ok(mixed.clone().into()), ThreadsafeFunctionCallMode::NonBlocking);
}
}

View File

@@ -3,32 +3,31 @@ use std::{
os::windows::ffi::OsStringExt,
process,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
};
use napi::{
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
Result,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use windows::{
core::Interface,
Win32::{
Foundation::CloseHandle,
Media::Audio::{
eCapture, eCommunications, eConsole, AudioSessionState, AudioSessionStateActive,
IAudioSessionControl, IAudioSessionControl2, IAudioSessionEnumerator, IAudioSessionEvents,
IAudioSessionEvents_Impl, IAudioSessionManager2, IAudioSessionNotification,
IAudioSessionNotification_Impl, IMMDevice, IMMDeviceCollection, IMMDeviceEnumerator,
MMDeviceEnumerator, DEVICE_STATE_ACTIVE,
AudioSessionState, AudioSessionStateActive, DEVICE_STATE_ACTIVE, IAudioSessionControl, IAudioSessionControl2,
IAudioSessionEnumerator, IAudioSessionEvents, IAudioSessionEvents_Impl, IAudioSessionManager2,
IAudioSessionNotification, IAudioSessionNotification_Impl, IMMDevice, IMMDeviceCollection, IMMDeviceEnumerator,
MMDeviceEnumerator, eCapture, eCommunications, eConsole,
},
System::{
Com::{CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED},
Com::{CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx},
ProcessStatus::{GetModuleFileNameExW, GetProcessImageFileNameW},
Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ},
},
},
core::Interface,
};
use windows_core::implement;
@@ -148,9 +147,7 @@ impl IAudioSessionEvents_Impl for SessionEvents_Impl {
let currently_recording = newstate == AudioSessionStateActive;
// Atomically swap the flag tracking this particular session
let previously_recording = self
.session_is_active
.swap(currently_recording, Ordering::SeqCst);
let previously_recording = self.session_is_active.swap(currently_recording, Ordering::SeqCst);
// Update the global counter accordingly
if !previously_recording && currently_recording {
@@ -272,12 +269,7 @@ impl SessionNotifier {
if should_notify {
self.callback.call(
Ok((
true,
process_name,
self.device_id.clone(),
self.device_name.clone(),
)),
Ok((true, process_name, self.device_id.clone(), self.device_name.clone())),
ThreadsafeFunctionCallMode::NonBlocking,
);
}
@@ -309,8 +301,7 @@ pub fn register_audio_device_status_callback(
let enumerator: IMMDeviceEnumerator = CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?;
// Get all active capture devices
let device_collection: IMMDeviceCollection =
enumerator.EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)?;
let device_collection: IMMDeviceCollection = enumerator.EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)?;
let device_count = device_collection.GetCount()?;
let mut session_notifiers = Vec::new();
@@ -374,17 +365,14 @@ impl MicrophoneListener {
let is_running = Arc::new(AtomicBool::new(false));
let active_sessions = Arc::new(AtomicUsize::new(0));
let session_notifiers = match register_audio_device_status_callback(
is_running.clone(),
active_sessions.clone(),
Arc::new(callback),
) {
Ok(notifiers) => notifiers,
Err(_) => {
// If registration fails, create a listener with empty notifiers
Vec::new()
}
};
let session_notifiers =
match register_audio_device_status_callback(is_running.clone(), active_sessions.clone(), Arc::new(callback)) {
Ok(notifiers) => notifiers,
Err(_) => {
// If registration fails, create a listener with empty notifiers
Vec::new()
}
};
Self {
is_running,
@@ -401,17 +389,13 @@ impl MicrophoneListener {
pub fn is_process_using_microphone(process_id: u32) -> bool {
// Use the proven get_all_audio_processes logic
match get_all_audio_processes() {
Ok(processes) => processes
.iter()
.any(|p| p.process_id == process_id && p.is_running),
Ok(processes) => processes.iter().any(|p| p.process_id == process_id && p.is_running),
Err(_) => false,
}
}
}
fn get_mgr_audio_session_running_status(
mgr: &IAudioSessionManager2,
) -> windows_core::Result<(bool, String)> {
fn get_mgr_audio_session_running_status(mgr: &IAudioSessionManager2) -> windows_core::Result<(bool, String)> {
let list: IAudioSessionEnumerator = unsafe { mgr.GetSessionEnumerator()? };
let sessions = unsafe { list.GetCount()? };
for idx in 0..sessions {
@@ -445,8 +429,7 @@ fn get_mgr_audio_session_running_status(
fn get_process_name(pid: u32) -> Option<String> {
unsafe {
// Open process with required access rights
let process_handle =
OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid).ok()?;
let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid).ok()?;
// Allocate a buffer large enough to hold extended-length paths (up to ~32K
// characters) instead of the legacy MAX_PATH (260) limit.
@@ -489,8 +472,8 @@ pub fn list_audio_processes() -> Result<Vec<AudioProcess>> {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
};
let result = get_all_audio_processes()
.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
let result =
get_all_audio_processes().map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
Ok(result)
}
@@ -501,8 +484,7 @@ pub fn list_audio_devices() -> Result<Vec<AudioDevice>> {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
};
let result = get_all_audio_devices()
.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
let result = get_all_audio_devices().map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
Ok(result)
}
@@ -511,8 +493,7 @@ fn get_all_audio_processes() -> windows_core::Result<Vec<AudioProcess>> {
unsafe {
let enumerator: IMMDeviceEnumerator = CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?;
let device_collection: IMMDeviceCollection =
enumerator.EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)?;
let device_collection: IMMDeviceCollection = enumerator.EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)?;
let device_count = device_collection.GetCount()?;
let mut all_processes = Vec::new();
@@ -569,8 +550,7 @@ fn get_all_audio_devices() -> windows_core::Result<Vec<AudioDevice>> {
unsafe {
let enumerator: IMMDeviceEnumerator = CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?;
let device_collection: IMMDeviceCollection =
enumerator.EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)?;
let device_collection: IMMDeviceCollection = enumerator.EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)?;
let device_count = device_collection.GetCount()?;
let mut devices = Vec::new();
@@ -619,8 +599,8 @@ pub fn get_active_audio_processes() -> Result<Vec<AudioProcess>> {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
};
let result = get_all_audio_processes()
.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
let result =
get_all_audio_processes().map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
// Filter to only return active/running processes
let active_processes = result.into_iter().filter(|p| p.is_running).collect();
@@ -633,8 +613,8 @@ pub fn is_process_actively_using_microphone(pid: u32) -> Result<bool> {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
};
let result = get_all_audio_processes()
.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
let result =
get_all_audio_processes().map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.message()))?;
// Check if the PID exists in the list of active processes
let is_active = result

View File

@@ -3,8 +3,8 @@ use std::{
ffi::OsString,
os::windows::ffi::OsStringExt,
sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
Arc, LazyLock, RwLock,
atomic::{AtomicBool, AtomicU32, Ordering},
},
thread,
time::Duration,
@@ -18,9 +18,9 @@ use napi_derive::napi;
// Windows API imports
use windows::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE}; // HWND removed
use windows::Win32::System::{
Com::{CoInitializeEx, COINIT_MULTITHREADED},
Com::{COINIT_MULTITHREADED, CoInitializeEx},
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS,
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS,
},
ProcessStatus::{GetModuleFileNameExW, GetProcessImageFileNameW},
Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ},
@@ -34,20 +34,17 @@ pub type AudioObjectID = u32;
// Global storage for running applications (Windows equivalent of macOS audio
// process list)
static RUNNING_APPLICATIONS: LazyLock<RwLock<Vec<u32>>> =
LazyLock::new(|| RwLock::new(get_running_processes()));
static RUNNING_APPLICATIONS: LazyLock<RwLock<Vec<u32>>> = LazyLock::new(|| RwLock::new(get_running_processes()));
// Simple counter for generating unique handles
static NEXT_HANDLE: AtomicU32 = AtomicU32::new(1);
// Global storage for active watchers
static ACTIVE_APP_WATCHERS: LazyLock<
RwLock<Vec<(u32, u32, Arc<ThreadsafeFunction<(), ()>>, Arc<AtomicBool>)>>,
> = LazyLock::new(|| RwLock::new(Vec::new()));
static ACTIVE_APP_WATCHERS: LazyLock<RwLock<Vec<(u32, u32, Arc<ThreadsafeFunction<(), ()>>, Arc<AtomicBool>)>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
static ACTIVE_LIST_WATCHERS: LazyLock<
RwLock<Vec<(u32, Arc<ThreadsafeFunction<(), ()>>, Arc<AtomicBool>)>>,
> = LazyLock::new(|| RwLock::new(Vec::new()));
static ACTIVE_LIST_WATCHERS: LazyLock<RwLock<Vec<(u32, Arc<ThreadsafeFunction<(), ()>>, Arc<AtomicBool>)>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
// Plain struct for efficient transmission via napi-rs
#[napi]
@@ -103,10 +100,7 @@ impl ApplicationListChangedSubscriber {
#[napi]
pub fn unsubscribe(&self) -> Result<()> {
if let Ok(mut watchers) = ACTIVE_LIST_WATCHERS.write() {
if let Some(pos) = watchers
.iter()
.position(|(handle, _, _)| *handle == self.handle)
{
if let Some(pos) = watchers.iter().position(|(handle, _, _)| *handle == self.handle) {
let (_, _, should_stop) = &watchers[pos];
should_stop.store(true, Ordering::Relaxed);
watchers.remove(pos);
@@ -132,10 +126,7 @@ impl ApplicationStateChangedSubscriber {
#[napi]
pub fn unsubscribe(&self) {
if let Ok(mut watchers) = ACTIVE_APP_WATCHERS.write() {
if let Some(pos) = watchers
.iter()
.position(|(handle, _, _, _)| *handle == self.handle)
{
if let Some(pos) = watchers.iter().position(|(handle, _, _, _)| *handle == self.handle) {
let (_, _, _, should_stop) = &watchers[pos];
should_stop.store(true, Ordering::Relaxed);
watchers.remove(pos);
@@ -152,9 +143,7 @@ pub struct ShareableContent {
#[napi]
impl ShareableContent {
#[napi]
pub fn on_application_list_changed(
callback: ThreadsafeFunction<(), ()>,
) -> Result<ApplicationListChangedSubscriber> {
pub fn on_application_list_changed(callback: ThreadsafeFunction<(), ()>) -> Result<ApplicationListChangedSubscriber> {
let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
let callback_arc = Arc::new(callback);
@@ -189,25 +178,20 @@ impl ShareableContent {
#[napi(constructor)]
pub fn new() -> Self {
unsafe {
CoInitializeEx(None, COINIT_MULTITHREADED)
.ok()
.unwrap_or_else(|_| {
// COM initialization failed, but we can't return an error from
// constructor This is typically not fatal as COM might
// already be initialized
});
CoInitializeEx(None, COINIT_MULTITHREADED).ok().unwrap_or_else(|_| {
// COM initialization failed, but we can't return an error from
// constructor This is typically not fatal as COM might
// already be initialized
});
}
Self {}
}
#[napi]
pub fn applications() -> Result<Vec<ApplicationInfo>> {
let processes = RUNNING_APPLICATIONS.read().map_err(|_| {
Error::new(
Status::GenericFailure,
"Failed to read running applications",
)
})?;
let processes = RUNNING_APPLICATIONS
.read()
.map_err(|_| Error::new(Status::GenericFailure, "Failed to read running applications"))?;
let mut apps = Vec::new();
for &process_id in processes.iter() {
@@ -326,8 +310,7 @@ fn is_process_running(process_id: u32) -> bool {
fn get_process_name(pid: u32) -> Option<String> {
unsafe {
let process_handle =
OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid).ok()?;
let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid).ok()?;
// Allocate a buffer large enough to hold extended-length paths (up to ~32K
// characters) instead of the legacy MAX_PATH (260) limit. 32 768 is the
// maximum length supported by the Win32 APIs when the path is prefixed
@@ -352,8 +335,7 @@ fn get_process_name(pid: u32) -> Option<String> {
fn get_process_executable_path(pid: u32) -> Option<String> {
unsafe {
let process_handle =
OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid).ok()?;
let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid).ok()?;
// Use a buffer that can hold extended-length paths. See rationale above.
let mut buffer: Vec<u16> = std::iter::repeat(0).take(32_768).collect();
@@ -372,11 +354,7 @@ fn get_process_executable_path(pid: u32) -> Option<String> {
}
// Helper function to start monitoring a specific process
fn start_process_monitoring(
handle: u32,
process_id: u32,
callback: Arc<ThreadsafeFunction<(), ()>>,
) {
fn start_process_monitoring(handle: u32, process_id: u32, callback: Arc<ThreadsafeFunction<(), ()>>) {
let should_stop = Arc::new(AtomicBool::new(false));
let should_stop_clone = should_stop.clone();