64569f4736
Four unresolved-import/type errors from the release build (first real compile): - toast.rs: generic IMap moved to the windows-collections crate; read the reply from the ValueSet returned by UserInput() directly (HasKey/Lookup are exposed on the class). - jumplist.rs: PROPVARIANT lives in Win32::System::Com::StructuredStorage (not windows::core); IObjectArray/IObjectCollection in Win32::System::Com (not UI::Shell); PKEY_Title in Win32::Storage::EnhancedStorage (feature added); build the title PROPVARIANT via From<&str> (VT_LPWSTR). - smtc.rs: event registrations return a plain i64 token in windows 0.61 (the EventRegistrationToken newtype is gone). - thumbbar.rs: HICON was imported inside the fn body but used in its signature — fully qualify the return type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191 lines
7.1 KiB
Rust
191 lines
7.1 KiB
Rust
//! 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. (windows 0.61 event registrations return a
|
|
// plain i64 token — the EventRegistrationToken newtype is gone.)
|
|
_token: i64,
|
|
}
|
|
|
|
/// 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(())
|
|
}
|