tauri-plugin-audio 0.1.0

Desktop audio capture plugin for Tauri
use std::path::Path;

use tauri::{
    plugin::{Builder as PluginBuilder, TauriPlugin},
    Manager, Runtime,
};
use tauri_specta::{collect_commands, Builder as SpectaBuilder, ErrorHandlingMode};

mod devices;
#[cfg(target_os = "linux")]
mod pipewire;
mod stream;

pub fn specta_builder<R: Runtime>() -> SpectaBuilder<R> {
    SpectaBuilder::<R>::new()
        .plugin_name("audio")
        .commands(collect_commands![
            devices::get_devices,
            stream::create_stream,
            stream::stop_stream
        ])
}

pub fn public_api_builder<R: Runtime>() -> SpectaBuilder<R> {
    specta_builder().error_handling(ErrorHandlingMode::Throw)
}

pub fn export_public_api_bindings<R: Runtime>(
    path: impl AsRef<Path>,
) -> Result<(), specta_typescript::Error> {
    public_api_builder::<R>().export(specta_typescript::Typescript::default(), path.as_ref())
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    PluginBuilder::new("audio")
        .setup(|app, _api| {
            app.manage(stream::ActiveInputStream::default());
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            devices::get_devices,
            stream::create_stream,
            stream::stop_stream
        ])
        .build()
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::{export_public_api_bindings, specta_builder};

    #[test]
    fn exports_plugin_prefixed_typescript_bindings() {
        let output_path = std::env::temp_dir().join("tauri-plugin-audio-bindings-test.ts");
        specta_builder::<tauri::Wry>()
            .export(specta_typescript::Typescript::default(), &output_path)
            .expect("typescript binding export should succeed");

        let output = std::fs::read_to_string(&output_path).expect("bindings should be readable");
        assert!(output.contains(r#"__TAURI_INVOKE("plugin:audio|get_devices")"#));
        assert!(output.contains(r#"__TAURI_INVOKE("plugin:audio|create_stream""#));
        assert!(output.contains(r#"__TAURI_INVOKE("plugin:audio|stop_stream")"#));

        let _ = std::fs::remove_file(output_path);
    }

    #[test]
    fn public_guest_api_matches_generated_bindings() {
        let output = generated_public_api("public-guest-api");
        let guest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("guest-js/index.ts");
        let checked_in =
            std::fs::read_to_string(guest_path).expect("guest API bindings should be readable");

        assert_eq!(
            checked_in, output,
            "run `pnpm --dir tauri-plugin-audio generate` to refresh guest-js/index.ts"
        );
    }

    fn generated_public_api(test_name: &str) -> String {
        let output_path = std::env::temp_dir().join(format!(
            "tauri-plugin-audio-{test_name}-{}.ts",
            std::process::id()
        ));
        export_public_api_bindings::<tauri::Wry>(&output_path)
            .expect("typescript public API export should succeed");

        let output =
            std::fs::read_to_string(&output_path).expect("public API bindings should be readable");
        let _ = std::fs::remove_file(output_path);
        output
    }
}