Skip to main content

tauri_plugin_media_session/
lib.rs

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