Files
cinny-desktop/src-tauri/src/native/smtc.rs
T

190 lines
7.0 KiB
Rust
Raw Normal View History

//! P5-43 — System Media Transport Controls (SMTC) call surface (Windows).
//!
//! Surfaces the active voice/video call to the Windows media overlay (the
//! volume flyout / SMTC card) so the user can mute or hang up from the OS
//! media controls. We create the SMTC via the WinRT interop factory
//! (`ISystemMediaTransportControlsInterop::GetForWindow`), keep the object in
//! managed state, and let the web client drive its state through
//! `set_smtc_call_state` as the call-embed atom / mic state changes.
//!
//! Button mapping: **Play/Pause → mute toggle**, **Stop → end call**. Presses
//! are forwarded to the web client as a `smtc-action` DOM CustomEvent (see
//! `super::emit_to_web`) with `action` in `"mute" | "end"`; the web hook
//! (`useTauriSmtc`) translates them into `CallControl.toggleMicrophone()` /
//! `CallEmbed.hangup()`.
//!
//! RUNTIME NOTE: SMTC is designed for real media apps. For a non-media app the
//! card may not actually appear unless the process owns an active audio session
//! recognised by the system. This module prioritises a clean compile and
//! correct WinRT API usage; visibility of the overlay at runtime is uncertain
//! and may depend on the embedded Element Call iframe holding an audio session.
//!
//! Other platforms are a no-op (SMTC is Windows-only); the command keeps an
//! identical cross-platform signature so the web side stays unconditional.
use tauri::AppHandle;
/// Payload for the `smtc-action` DOM event forwarded to the web client.
#[cfg(target_os = "windows")]
#[derive(serde::Serialize)]
struct Ev {
action: String,
}
/// Holds the SMTC object (and its `ButtonPressed` registration token) in Tauri
/// managed state so `set_smtc_call_state` can update it at runtime. Mirrors the
/// `TrayUnreadState` managed-state pattern in lib.rs.
#[cfg(target_os = "windows")]
struct SmtcState {
controls: std::sync::Mutex<Option<windows::Media::SystemMediaTransportControls>>,
// Kept alive so the ButtonPressed handler stays registered for the app's
// lifetime; never unregistered.
_token: windows::Foundation::EventRegistrationToken,
}
/// Called once from `native::setup()`. Creates and configures the SMTC on
/// Windows; no-op elsewhere. SMTC init failures are logged and swallowed so a
/// missing/unsupported overlay never blocks app startup.
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
#[cfg(target_os = "windows")]
{
if let Err(err) = init_smtc(app) {
eprintln!("smtc: failed to initialize System Media Transport Controls: {err:?}");
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = app;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn init_smtc(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
use tauri::Manager;
use windows::core::{factory, HSTRING};
use windows::Foundation::TypedEventHandler;
use windows::Media::{
MediaPlaybackStatus, MediaPlaybackType, SystemMediaTransportControls,
SystemMediaTransportControlsButton, SystemMediaTransportControlsButtonPressedEventArgs,
};
use windows::Win32::Foundation::HWND;
use windows::Win32::System::WinRT::ISystemMediaTransportControlsInterop;
let window = app
.get_webview_window("main")
.ok_or("smtc: main window not found")?;
// Match the HWND conversion used by set_badge_count in lib.rs.
let hwnd = HWND(window.hwnd()?.0 as _);
// SMTC has no WinRT constructor; it's obtained per-window via the interop
// factory. `factory::<C, I>()` fetches the activation factory for the
// runtime class `C` cast to the classic COM interop interface `I`.
let interop =
factory::<SystemMediaTransportControls, ISystemMediaTransportControlsInterop>()?;
let controls: SystemMediaTransportControls = unsafe { interop.GetForWindow(hwnd)? };
controls.SetIsEnabled(true)?;
controls.SetIsPlayEnabled(true)?;
controls.SetIsPauseEnabled(true)?;
controls.SetIsStopEnabled(true)?;
// Configure the card metadata once ("In call"); the web side only toggles
// playback status afterwards.
let updater = controls.DisplayUpdater()?;
updater.SetType(MediaPlaybackType::Music)?;
let music = updater.MusicProperties()?;
music.SetTitle(&HSTRING::from("In call"))?;
updater.Update()?;
// Idle until a call becomes active (set_smtc_call_state flips this).
controls.SetPlaybackStatus(MediaPlaybackStatus::Closed)?;
// ButtonPressed → forward a normalized action to the web client.
let app_for_handler = app.clone();
let handler = TypedEventHandler::<
SystemMediaTransportControls,
SystemMediaTransportControlsButtonPressedEventArgs,
>::new(move |_sender, args| {
if let Some(args) = args.as_ref() {
let button = args.Button()?;
let action = if button == SystemMediaTransportControlsButton::Play
|| button == SystemMediaTransportControlsButton::Pause
{
Some("mute")
} else if button == SystemMediaTransportControlsButton::Stop {
Some("end")
} else {
None
};
if let Some(action) = action {
let payload = serde_json::to_string(&Ev {
action: action.to_string(),
})
.unwrap_or_default();
super::emit_to_web(&app_for_handler, "smtc-action", &payload);
}
}
Ok(())
});
let token = controls.ButtonPressed(&handler)?;
app.manage(SmtcState {
controls: std::sync::Mutex::new(Some(controls)),
_token: token,
});
Ok(())
}
/// Reflect the call state onto the SMTC. When `active`, enable the controls and
/// set playback status to Playing (unmuted) / Paused (muted); when inactive,
/// mark the card Closed and disable it. Windows-only; no-op elsewhere.
#[tauri::command]
pub fn set_smtc_call_state(app: AppHandle, active: bool, muted: bool) {
#[cfg(target_os = "windows")]
{
use tauri::Manager;
if let Some(state) = app.try_state::<SmtcState>() {
if let Ok(guard) = state.controls.lock() {
if let Some(controls) = guard.as_ref() {
let _ = apply_call_state(controls, active, muted);
}
}
}
}
#[cfg(not(target_os = "windows"))]
{
// No-op off Windows; bind args so the signature is identical everywhere
// and no unused warnings fire.
let _ = (&app, active, muted);
}
}
#[cfg(target_os = "windows")]
fn apply_call_state(
controls: &windows::Media::SystemMediaTransportControls,
active: bool,
muted: bool,
) -> windows::core::Result<()> {
use windows::Media::MediaPlaybackStatus;
if active {
controls.SetIsEnabled(true)?;
let status = if muted {
MediaPlaybackStatus::Paused
} else {
MediaPlaybackStatus::Playing
};
controls.SetPlaybackStatus(status)?;
} else {
controls.SetPlaybackStatus(MediaPlaybackStatus::Closed)?;
controls.SetIsEnabled(false)?;
}
Ok(())
}