# tauri-plugin-native-audio
Native mobile audio playback for Tauri applications.
`tauri-plugin-native-audio` gives you one JavaScript API over native playback engines:
- Android: Media3 ExoPlayer + MediaSessionService + notification controls
- iOS: AVPlayer + MPNowPlayingInfoCenter + MPRemoteCommandCenter
It is designed for real mobile playback flows: background-friendly behavior, lock screen / notification controls, and continuous state synchronization in the frontend.
| Linux | - |
| Windows | - |
| macOS | - |
| Android | ✓ |
| iOS | ✓ |
## Install
_This plugin requires a Rust version of at least **1.77.2**._
There are three general methods of installation:
1. crates.io + npm (simplest)
2. Git dependency for Rust + npm package for JS
3. Git/path dependency for both Rust and JS in monorepo/submodule setup
Install the core plugin in `src-tauri/Cargo.toml`:
```toml
[dependencies]
tauri-plugin-native-audio = "1"
# or from Git:
tauri-plugin-native-audio = { git = "https://github.com/uvarov-frontend/tauri-plugin-native-audio", tag = "v1.0.5" }
```
Install the JavaScript guest bindings:
```sh
pnpm add tauri-plugin-native-audio-api
# or
npm add tauri-plugin-native-audio-api
# or
yarn add tauri-plugin-native-audio-api
```
## Setting up
### 1. Register the plugin in Rust
`src-tauri/src/lib.rs`
```rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_native_audio::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### 2. Enable permissions in capability
Add plugin permissions to your capability file (example: `src-tauri/capabilities/default.json`):
```json
{
"identifier": "default",
"description": "Default capability",
"windows": ["main"],
"permissions": [
"core:default",
"native-audio:default"
]
}
```
`native-audio:default` includes:
- `allow-initialize`
- `allow-register-listener`
- `allow-remove-listener`
- `allow-set-source`
- `allow-play`
- `allow-pause`
- `allow-seek-to`
- `allow-set-rate`
- `allow-get-state`
- `allow-get-progress-checkpoint`
- `allow-dispose`
If you need stricter ACL, use explicit allow/deny permissions from `permissions/autogenerated/commands/*.toml`.
### 3. Platform notes
Android:
- Requires Android 8.0+ (API 26).
- `play()` starts a foreground service (`NativeAudioService`) for media playback notification.
- On Android 13+ notification permission is requested during `initialize()`.
- Plugin manifest declares:
- `android.permission.INTERNET`
- `android.permission.FOREGROUND_SERVICE`
- `android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK`
- `android.permission.POST_NOTIFICATIONS`
- `android.permission.WAKE_LOCK`
iOS:
- Requires iOS 14.0+.
- Uses `AVAudioSession` category `.playback`.
- For true background audio, enable Background Modes -> Audio in your iOS target.
- Lock screen controls and now playing metadata are integrated through `MediaPlayer` framework APIs.
- If your app target minimum is iOS 14.x, ensure `LD_RUNPATH_SEARCH_PATHS` includes `/usr/lib/swift` (along with `@executable_path/Frameworks`) to avoid launch-time Swift runtime loading issues.
- For iOS 16+ targets, this extra runpath setting is typically not required.
## Configuration
This plugin does not require plugin-specific `tauri.conf.json > plugins.native-audio` configuration.
Runtime behavior is configured from JS commands:
- `setSource({ src, id?, title, artist, artworkUrl })`
- `setRate(rate)`
- `seekTo(positionSeconds)`
## Usage
After plugin registration, APIs are available in JavaScript:
```ts
import {
initialize,
setSource,
play,
pause,
seekTo,
setRate,
getState,
getProgressCheckpoint,
dispose,
addStateListener
} from "tauri-plugin-native-audio-api";
const unlisten = await addStateListener((state) => {
console.log("native-audio state", state);
});
await initialize();
await setSource({
src: "https://example.com/audio.mp3",
id: 123,
title: "Episode 1",
artist: "My Podcast",
artworkUrl: "https://example.com/cover.jpg"
});
await setRate(1.25);
await play();
// later
await seekTo(42);
await pause();
console.log(await getState());
console.log(await getProgressCheckpoint());
unlisten();
await dispose();
```
## JavaScript API
Types:
```ts
export type NativeAudioState = {
status: NativeAudioStatus;
currentTime: number;
duration: number;
isPlaying: boolean;
buffering: boolean;
rate: number;
error?: string;
};
export type NativeAudioSetSourcePayload = {
src: string;
id?: number;
title?: string;
artist?: string;
artworkUrl?: string;
};
export type NativeAudioProgressCheckpoint = {
id: number;
currentTime: number;
updatedAtMs: number;
status?: NativeAudioStatus;
};
```
Commands:
- `initialize(): Promise<NativeAudioState>`
- `setSource(payload: NativeAudioSetSourcePayload): Promise<NativeAudioState>`
- `play(): Promise<NativeAudioState>`
- `pause(): Promise<NativeAudioState>`
- `seekTo(position: number): Promise<NativeAudioState>`
- `setRate(rate: number): Promise<NativeAudioState>`
- `getState(): Promise<NativeAudioState>`
- `getProgressCheckpoint(): Promise<NativeAudioProgressCheckpoint | null>`
- `dispose(): Promise<void>`
- `addStateListener(handler): Promise<() => void>`
Validation and errors:
- `setSource` rejects if `src` is empty.
- `seekTo` rejects if `position` is missing/non-finite.
- `setRate` rejects if `rate <= 0` or non-finite.
- Native failures can also be reflected in `NativeAudioState.error` with status `error`.
State semantics:
- `idle`: player idle or paused
- `loading`: buffering
- `playing`: active playback
- `ended`: reached media end
- `error`: runtime/player error
## Native Progress Checkpoint (mobile)
The plugin can persist a lightweight playback checkpoint natively (Android/iOS) for background playback scenarios where the WebView/JS runtime may be suspended.
Checkpoint shape:
- `id`
- `currentTime`
- `updatedAtMs`
- `status` (optional)
How it works:
- Pass `id` in `setSource(...)` to associate playback with an app-level item.
- During playback, the plugin stores a single checkpoint (v1) in native storage:
- Android: `SharedPreferences`
- iOS: `UserDefaults`
- Checkpoint updates are deterministic: on pause, successful seek commit, end, dispose, and throttled play ticks.
- Near-start micro progress (<= 0.25s) is ignored to avoid noisy checkpoints.
Read the last checkpoint:
```ts
const checkpoint = await getProgressCheckpoint();
if (checkpoint) {
console.log(checkpoint.id, checkpoint.currentTime, checkpoint.updatedAtMs);
}
```
Notes:
- The plugin stores only audio progress checkpoint data, not your app's story cache.
- v1 stores only the latest active story checkpoint (single record).
## Source handling
Supported source forms:
- remote URL (`https://...`, and platform-dependent `http://...`)
- local file URL (`file://...`)
- local file path
iOS extras:
- `asset://localhost/...` and `http://asset.localhost/...` resolve to local file paths.
- Local files with missing/`.bin` extension are inspected by header and remapped via temporary alias (`mp3`, `wav`, `m4a`, `ogg`) for better AVPlayer compatibility.
## Troubleshooting
`unknown command plugin:native-audio|...`
- Ensure Rust registration exists: `.plugin(tauri_plugin_native_audio::init())`
- Ensure capability includes `native-audio:default` (or explicit allow permissions)
Android notification controls not shown:
- Call `initialize()` before playback
- Start playback using `play()`
- Grant notifications permission on Android 13+
iOS background playback not working:
- Enable Background Modes -> Audio for the iOS app target
- Verify stream URL is reachable and valid
iOS HTTP URL fails:
- ATS can block plain HTTP
- Prefer HTTPS or configure ATS exceptions in app settings
iOS app closes right after launch on iOS 14:
- Ensure app target `LD_RUNPATH_SEARCH_PATHS` includes both `@executable_path/Frameworks` and `/usr/lib/swift`
- Clean/reinstall the app after changing build settings
## iOS Runtime
iOS playback runs through a single actor-owned runtime.
- Playback commands (`setSource`, `play`, `pause`, `seekTo`, `setRate`) are serialized in one state owner.
- `setSource` and `seekTo` use revision fencing (`sourceRevision`, `seekRevision`) to ignore stale async callbacks.
- Seek is processed as a deterministic two-phase flow (`pending -> committed`).
- Native state is emitted through `native_audio_state` with the `NativeAudioState` payload.
## AI Notice
This plugin was fully developed with the help of AI.
No junior developers were harmed, only a few CPU cores worked overtime 😂
Yes, this duck was also invented by AI 🤣
## License
MIT or Apache-2.0, where applicable.