Skip to main content

tauri_plugin_media_session/
lib.rs

1use tauri::{
2    plugin::{Builder, TauriPlugin},
3    Manager, Runtime,
4};
5
6#[cfg(mobile)]
7use serde::Serialize;
8
9#[cfg(target_os = "android")]
10const PLUGIN_IDENTIFIER: &str = "app.tauri.mediasession";
11
12#[cfg(target_os = "ios")]
13tauri::ios_plugin_binding!(init_plugin_media_session);
14
15/// Media playback state.
16///
17/// All fields are optional — omitted fields preserve their previous values
18/// on the native side (merge semantics).
19#[cfg(mobile)]
20#[derive(Debug, Default, Clone, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct MediaState {
23    /// Track title.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub title: Option<String>,
26    /// Artist name.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub artist: Option<String>,
29    /// Album name.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub album: Option<String>,
32    /// Base64-encoded image (JPEG/PNG, no `data:` prefix).
33    /// Pass an empty string to clear artwork.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub artwork: Option<String>,
36    /// URL to an image (JPEG/PNG). Downloaded natively (no CORS).
37    /// Use this when the image is on a CDN or external server.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub artwork_url: Option<String>,
40    /// Track duration in seconds.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub duration: Option<f64>,
43    /// Current playback position in seconds.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub position: Option<f64>,
46    /// Playback speed multiplier (default: 1.0).
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub playback_speed: Option<f64>,
49    /// Whether media is currently playing.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub is_playing: Option<bool>,
52    /// Whether the "previous track" action is available.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub can_prev: Option<bool>,
55    /// Whether the "next track" action is available.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub can_next: Option<bool>,
58    /// Whether seeking is available (default: true).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub can_seek: Option<bool>,
61}
62
63/// Lightweight timeline update (position, duration, speed only).
64///
65/// Skips notification rebuild — ideal for frequent position syncs.
66#[cfg(mobile)]
67#[derive(Debug, Default, Clone, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct TimelineUpdate {
70    /// Current playback position in seconds.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub position: Option<f64>,
73    /// Track duration in seconds.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub duration: Option<f64>,
76    /// Playback speed multiplier.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub playback_speed: Option<f64>,
79}
80
81/// Handle to the native media session.
82#[cfg(mobile)]
83pub struct MediaSession<R: Runtime>(tauri::plugin::PluginHandle<R>);
84
85#[cfg(mobile)]
86impl<R: Runtime> MediaSession<R> {
87    /// Update the media session state and notification.
88    ///
89    /// Auto-initializes the session on first call.
90    /// Only include the fields that changed — previous values are preserved.
91    pub fn update_state(&self, state: MediaState) -> Result<(), String> {
92        self.0
93            .run_mobile_plugin("updateState", state)
94            .map_err(|e| format!("{e}"))
95    }
96
97    /// Lightweight timeline update — only touches `PlaybackState`, skips notification rebuild.
98    ///
99    /// Use this for frequent position syncs during playback.
100    /// The session must already be initialized via [`update_state`](Self::update_state).
101    pub fn update_timeline(&self, timeline: TimelineUpdate) -> Result<(), String> {
102        self.0
103            .run_mobile_plugin("updateTimeline", timeline)
104            .map_err(|e| format!("{e}"))
105    }
106
107    /// Clear the media session, dismiss the notification, and release resources.
108    pub fn clear(&self) -> Result<(), String> {
109        self.0
110            .run_mobile_plugin::<()>("clear", ())
111            .map_err(|e| format!("{e}"))
112    }
113
114    /// Pre-initialize the session and request notification permissions.
115    ///
116    /// Optional — [`update_state`](Self::update_state) auto-initializes when needed.
117    pub fn initialize(&self) -> Result<(), String> {
118        self.0
119            .run_mobile_plugin::<()>("initialize", ())
120            .map_err(|e| format!("{e}"))
121    }
122}
123
124/// Extension trait for accessing the media session from any Tauri manager.
125#[cfg(mobile)]
126pub trait MediaSessionExt<R: Runtime> {
127    fn media_session(&self) -> &MediaSession<R>;
128}
129
130#[cfg(mobile)]
131impl<R: Runtime, T: Manager<R>> MediaSessionExt<R> for T {
132    fn media_session(&self) -> &MediaSession<R> {
133        self.state::<MediaSession<R>>().inner()
134    }
135}
136
137/// Initialize the media-session plugin.
138///
139/// On non-mobile platforms this registers a no-op plugin so that
140/// cross-platform apps compile without `cfg` gates around the plugin call.
141pub fn init<R: Runtime>() -> TauriPlugin<R> {
142    Builder::new("media-session")
143        .setup(|_app, _api| {
144            #[cfg(target_os = "android")]
145            let handle =
146                _api.register_android_plugin(PLUGIN_IDENTIFIER, "MediaSessionPlugin")?;
147
148            #[cfg(target_os = "ios")]
149            let handle = _api.register_ios_plugin(init_plugin_media_session)?;
150
151            #[cfg(mobile)]
152            _app.manage(MediaSession(handle));
153
154            Ok(())
155        })
156        .build()
157}