572 lines
19 KiB
Markdown
572 lines
19 KiB
Markdown
# Rust Backend Architecture
|
|
|
|
**Location**: `src-tauri/src/`
|
|
|
|
## Media Session State Machine
|
|
|
|
**Location**: `src-tauri/src/player/session.rs`
|
|
|
|
The media session tracks the high-level playback context (what kind of media is being consumed) and persists beyond individual playback states. This enables persistent UI (miniplayer for audio) and proper transitions between content types.
|
|
|
|
**Architecture Note:** The session manager is a separate app-level state manager (not inside PlayerController), coordinated by the commands layer. This maintains clean separation of concerns.
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Idle
|
|
|
|
Idle --> AudioActive : play_queue(audio)
|
|
Idle --> MovieActive : play_item(movie)
|
|
Idle --> TvShowActive : play_item(episode)
|
|
|
|
state "Audio Session" as AudioSession {
|
|
[*] --> AudioActive
|
|
AudioActive --> AudioInactive : playback_ended
|
|
AudioInactive --> AudioActive : resume/play
|
|
AudioActive --> AudioActive : next/previous
|
|
}
|
|
|
|
state "Movie Session" as MovieSession {
|
|
[*] --> MovieActive
|
|
MovieActive --> MovieInactive : playback_ended
|
|
MovieInactive --> MovieActive : resume
|
|
}
|
|
|
|
state "TV Show Session" as TvShowSession {
|
|
[*] --> TvShowActive
|
|
TvShowActive --> TvShowInactive : playback_ended
|
|
TvShowInactive --> TvShowActive : next_episode/resume
|
|
}
|
|
|
|
AudioSession --> Idle : dismiss/clear_queue
|
|
AudioSession --> MovieSession : play_item(movie)
|
|
AudioSession --> TvShowSession : play_item(episode)
|
|
|
|
MovieSession --> Idle : dismiss/playback_complete
|
|
MovieSession --> AudioSession : play_queue(audio)
|
|
|
|
TvShowSession --> Idle : dismiss/series_complete
|
|
TvShowSession --> AudioSession : play_queue(audio)
|
|
|
|
note right of Idle
|
|
No active media session
|
|
Queue may exist but not playing
|
|
No miniplayer/video player shown
|
|
end note
|
|
|
|
note right of AudioSession
|
|
SHOW: Miniplayer (always visible)
|
|
- Active: Play/pause/skip controls enabled
|
|
- Inactive: Play button to resume queue
|
|
Persists until explicit dismiss
|
|
end note
|
|
|
|
note right of MovieSession
|
|
SHOW: Full video player
|
|
- Active: Video playing/paused
|
|
- Inactive: Resume dialog
|
|
Auto-dismiss when playback ends
|
|
end note
|
|
|
|
note right of TvShowSession
|
|
SHOW: Full video player + Next Episode UI
|
|
- Active: Video playing/paused
|
|
- Inactive: Next episode prompt
|
|
Auto-dismiss when series ends
|
|
end note
|
|
```
|
|
|
|
**Session State Enum:**
|
|
```rust
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum MediaSessionType {
|
|
/// No active session - browsing library
|
|
Idle,
|
|
|
|
/// Audio playback session (music, audiobooks, podcasts)
|
|
/// Persists until explicitly dismissed
|
|
Audio {
|
|
/// Last/current track being played
|
|
last_item: Option<MediaItem>,
|
|
/// True = playing/paused, False = stopped/ended
|
|
is_active: bool,
|
|
},
|
|
|
|
/// Movie playback (single video, auto-dismiss on end)
|
|
Movie {
|
|
item: MediaItem,
|
|
is_active: bool, // true = playing/paused, false = ended
|
|
},
|
|
|
|
/// TV show playback (supports next episode auto-advance)
|
|
TvShow {
|
|
item: MediaItem,
|
|
series_id: String,
|
|
is_active: bool, // true = playing/paused, false = ended
|
|
},
|
|
}
|
|
```
|
|
|
|
**State Transitions & Rules:**
|
|
|
|
| From State | Event | To State | UI Behavior | Notes |
|
|
|------------|-------|----------|-------------|-------|
|
|
| Idle | `play_queue(audio)` | Audio (active) | Show miniplayer | Creates audio session |
|
|
| Idle | `play_item(movie)` | Movie (active) | Show video player | Creates movie session |
|
|
| Idle | `play_item(episode)` | TvShow (active) | Show video player | Creates TV session |
|
|
| Audio (active) | `playback_ended` | Audio (inactive) | Miniplayer stays visible | Queue preserved |
|
|
| Audio (inactive) | `play/resume` | Audio (active) | Miniplayer enabled | Resume from queue |
|
|
| Audio (active/inactive) | `dismiss` | Idle | Hide miniplayer | Clear session |
|
|
| Audio (active/inactive) | `play_item(movie)` | Movie (active) | Switch to video player | Replace session |
|
|
| Movie (active) | `playback_ended` | Idle | Hide video player | Auto-dismiss |
|
|
| Movie (active) | `dismiss` | Idle | Hide video player | User dismiss |
|
|
| TvShow (active) | `playback_ended` | TvShow (inactive) | Show next episode UI | Wait for user choice |
|
|
| TvShow (inactive) | `next_episode` | TvShow (active) | Play next episode | Stay in session |
|
|
| TvShow (inactive) | `series_complete` | Idle | Hide video player | No more episodes |
|
|
|
|
**Key Design Decisions:**
|
|
|
|
1. **Audio Sessions Persist**: Miniplayer stays visible even when queue ends, allows easy resume
|
|
2. **Video Sessions Auto-Dismiss**: Movies auto-close when finished (unless paused)
|
|
3. **Single Active Session**: Playing new content type replaces current session
|
|
4. **Explicit Dismiss for Audio**: User must click close button to clear audio session
|
|
5. **Session != PlayerState**: Session is higher-level, PlayerState tracks playing/paused/seeking
|
|
|
|
**Edge Cases Handled:**
|
|
|
|
- Album finishes: Session goes inactive, miniplayer shows last track with play disabled
|
|
- User wants to dismiss: Close button clears session -> Idle
|
|
- Switch content types: New session replaces old (audio -> movie)
|
|
- Paused for extended time: Session persists indefinitely
|
|
- Playback errors: Session stays inactive, allows retry
|
|
- Queue operations while idle: Queue exists but no session created until play
|
|
|
|
## Player State Machine (Low-Level Playback)
|
|
|
|
**Location**: `src-tauri/src/player/state.rs`
|
|
|
|
The player uses a deterministic state machine with 6 states (operates within a media session):
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Idle
|
|
Idle --> Loading : Load
|
|
Loading --> Playing : MediaLoaded
|
|
Playing --> Paused : Pause
|
|
Paused --> Playing : Play
|
|
Paused --> Seeking : Seek
|
|
Seeking --> Playing : PositionUpdate
|
|
Playing --> Idle : Stop
|
|
Paused --> Idle : Stop
|
|
Idle --> Error : Error
|
|
Loading --> Error : Error
|
|
Playing --> Error : Error
|
|
Paused --> Error : Error
|
|
Seeking --> Error : Error
|
|
|
|
state Playing {
|
|
[*] : position, duration
|
|
}
|
|
state Paused {
|
|
[*] : position, duration
|
|
}
|
|
state Seeking {
|
|
[*] : target
|
|
}
|
|
state Error {
|
|
[*] : error message
|
|
}
|
|
```
|
|
|
|
**State Enum:**
|
|
```rust
|
|
pub enum PlayerState {
|
|
Idle,
|
|
Loading { media: MediaItem },
|
|
Playing { media: MediaItem, position: f64, duration: f64 },
|
|
Paused { media: MediaItem, position: f64, duration: f64 },
|
|
Seeking { media: MediaItem, target: f64 },
|
|
Error { media: Option<MediaItem>, error: String },
|
|
}
|
|
```
|
|
|
|
**Event Enum:**
|
|
```rust
|
|
pub enum PlayerEvent {
|
|
Load(MediaItem),
|
|
Play,
|
|
Pause,
|
|
Stop,
|
|
Seek(f64),
|
|
Next,
|
|
Previous,
|
|
MediaLoaded(f64), // duration
|
|
PositionUpdate(f64), // position
|
|
PlaybackEnded,
|
|
Error(String),
|
|
}
|
|
```
|
|
|
|
## Playback Mode State Machine
|
|
|
|
**Location**: `src-tauri/src/playback_mode/mod.rs`
|
|
|
|
The playback mode manages whether media is playing locally on the device or remotely on another Jellyfin session (TV, browser, etc.):
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Idle
|
|
|
|
Idle --> Local : play_queue()
|
|
Idle --> Remote : transfer_to_remote(session_id)
|
|
|
|
Local --> Remote : transfer_to_remote(session_id)
|
|
Local --> Idle : stop()
|
|
|
|
Remote --> Local : transfer_to_local()
|
|
Remote --> Idle : session_disconnected()
|
|
Remote --> Idle : stop()
|
|
|
|
state Local {
|
|
[*] : Playing on device
|
|
[*] : ExoPlayer active
|
|
[*] : Volume buttons -> device
|
|
}
|
|
|
|
state Remote {
|
|
[*] : Controlling session
|
|
[*] : session_id
|
|
[*] : Volume buttons -> remote
|
|
[*] : Android: VolumeProvider active
|
|
}
|
|
|
|
state Idle {
|
|
[*] : No active playback
|
|
}
|
|
```
|
|
|
|
**State Enum:**
|
|
```rust
|
|
pub enum PlaybackMode {
|
|
Local, // Playing on local device
|
|
Remote { session_id: String }, // Controlling remote Jellyfin session
|
|
Idle, // No active playback
|
|
}
|
|
```
|
|
|
|
**State Transitions:**
|
|
|
|
| From | Event | To | Side Effects |
|
|
|------|-------|-----|----|
|
|
| Idle | `play_queue()` | Local | Start local playback |
|
|
| Idle | `transfer_to_remote(session_id)` | Remote | Send queue to remote session |
|
|
| Local | `transfer_to_remote(session_id)` | Remote | Stop local, send queue to remote, enable remote volume (Android) |
|
|
| Local | `stop()` | Idle | Stop local playback |
|
|
| Remote | `transfer_to_local()` | Local | Get remote state, stop remote, start local at same position, disable remote volume |
|
|
| Remote | `stop()` | Idle | Stop remote playback, disable remote volume |
|
|
| Remote | `session_disconnected()` | Idle | Session lost, disable remote volume |
|
|
|
|
**Integration with Player State Machine:**
|
|
|
|
- When `PlaybackMode = Local`: Player state machine is active (Idle/Loading/Playing/Paused/etc.)
|
|
- When `PlaybackMode = Remote`: Player state is typically Idle (remote session controls playback)
|
|
- When `PlaybackMode = Idle`: Player state is Idle
|
|
|
|
**Android Volume Control Integration:**
|
|
|
|
When transitioning to `Remote` mode on Android:
|
|
1. Call `enable_remote_volume(initial_volume)`
|
|
2. VolumeProviderCompat intercepts hardware volume buttons
|
|
3. PlaybackStateCompat is set to STATE_PLAYING (shows volume UI)
|
|
4. Volume commands routed to remote session via Jellyfin API
|
|
|
|
When transitioning away from `Remote` mode:
|
|
1. Call `disable_remote_volume()`
|
|
2. Volume buttons return to controlling device volume
|
|
3. PlaybackStateCompat set to STATE_NONE
|
|
4. VolumeProviderCompat is cleared
|
|
|
|
## Media Item & Source
|
|
|
|
**Location**: `src-tauri/src/player/media.rs`
|
|
|
|
```rust
|
|
pub struct MediaItem {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub artist: Option<String>,
|
|
pub album: Option<String>,
|
|
pub duration: Option<f64>,
|
|
pub artwork_url: Option<String>,
|
|
pub media_type: MediaType,
|
|
pub source: MediaSource,
|
|
}
|
|
|
|
pub enum MediaType {
|
|
Audio,
|
|
Video,
|
|
}
|
|
|
|
pub enum MediaSource {
|
|
Remote {
|
|
stream_url: String,
|
|
jellyfin_item_id: String,
|
|
},
|
|
Local {
|
|
file_path: PathBuf,
|
|
jellyfin_item_id: Option<String>,
|
|
},
|
|
DirectUrl {
|
|
url: String,
|
|
},
|
|
}
|
|
```
|
|
|
|
The `MediaSource` enum enables:
|
|
- **Remote**: Streaming from Jellyfin server
|
|
- **Local**: Downloaded/cached files (future offline support)
|
|
- **DirectUrl**: Direct URLs (channel plugins, external sources)
|
|
|
|
## Queue Manager
|
|
|
|
**Location**: `src-tauri/src/player/queue.rs`
|
|
|
|
```rust
|
|
pub struct QueueManager {
|
|
items: Vec<MediaItem>,
|
|
current_index: Option<usize>,
|
|
shuffle: bool,
|
|
repeat: RepeatMode,
|
|
shuffle_order: Vec<usize>, // Fisher-Yates permutation
|
|
history: Vec<usize>, // For back navigation in shuffle
|
|
}
|
|
|
|
pub enum RepeatMode {
|
|
Off,
|
|
All,
|
|
One,
|
|
}
|
|
```
|
|
|
|
**Queue Navigation Logic:**
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
QM[QueueManager]
|
|
QM --> Shuffle
|
|
QM --> Repeat
|
|
QM --> History
|
|
|
|
subgraph Shuffle["Shuffle Mode"]
|
|
ShuffleOff["OFF<br/>next() returns index + 1"]
|
|
ShuffleOn["ON<br/>next() follows shuffle_order[]"]
|
|
end
|
|
|
|
subgraph Repeat["Repeat Mode"]
|
|
RepeatOff["OFF<br/>next() at end: -> None"]
|
|
RepeatAll["ALL<br/>next() at end: -> wrap to index 0"]
|
|
RepeatOne["ONE<br/>next() returns same item"]
|
|
end
|
|
|
|
subgraph History["History"]
|
|
HistoryDesc["Used for previous()<br/>in shuffle mode"]
|
|
end
|
|
```
|
|
|
|
## Favorites System
|
|
|
|
**Location**:
|
|
- Service: `src/lib/services/favorites.ts`
|
|
- Component: `src/lib/components/FavoriteButton.svelte`
|
|
- Backend: `src-tauri/src/commands/storage.rs`
|
|
|
|
The favorites system implements optimistic updates with server synchronization:
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
UI[FavoriteButton] -->|Click| Service[toggleFavorite]
|
|
Service -->|1. Optimistic| LocalDB[(SQLite user_data)]
|
|
Service -->|2. Sync| JellyfinAPI[Jellyfin API]
|
|
Service -->|3. Mark Synced| LocalDB
|
|
|
|
JellyfinAPI -->|POST| MarkFav["/Users/{id}/FavoriteItems/{itemId}"]
|
|
JellyfinAPI -->|DELETE| UnmarkFav["/Users/{id}/FavoriteItems/{itemId}"]
|
|
|
|
LocalDB -->|is_favorite<br/>pending_sync| UserData[user_data table]
|
|
```
|
|
|
|
**Flow**:
|
|
1. User clicks heart button in UI (MiniPlayer, AudioPlayer, or detail pages)
|
|
2. `toggleFavorite()` service function handles the logic:
|
|
- Updates local SQLite database immediately (optimistic update)
|
|
- Attempts to sync with Jellyfin server
|
|
- Marks as synced if successful, otherwise leaves `pending_sync = 1`
|
|
3. UI reflects the change immediately without waiting for server response
|
|
|
|
**Components**:
|
|
|
|
- **FavoriteButton.svelte**: Reusable heart button component
|
|
- Configurable size (sm/md/lg)
|
|
- Red when favorited, gray when not
|
|
- Loading state during toggle
|
|
- Bindable `isFavorite` prop for two-way binding
|
|
|
|
- **Integration Points**:
|
|
- MiniPlayer: Shows favorite button for audio tracks (hidden on small screens)
|
|
- Full AudioPlayer: Shows favorite button (planned)
|
|
- Album/Artist detail pages: Shows favorite button (planned)
|
|
|
|
**Database Schema**:
|
|
- `user_data.is_favorite`: Boolean flag (stored as INTEGER 0/1)
|
|
- `user_data.pending_sync`: Indicates if local changes need syncing
|
|
|
|
**Tauri Commands**:
|
|
- `storage_toggle_favorite`: Updates favorite status in local database
|
|
- `storage_mark_synced`: Clears pending_sync flag after successful sync
|
|
|
|
**API Methods**:
|
|
- `LibraryApi.markFavorite(itemId)`: POST to Jellyfin
|
|
- `LibraryApi.unmarkFavorite(itemId)`: DELETE from Jellyfin
|
|
|
|
## Player Backend Trait
|
|
|
|
**Location**: `src-tauri/src/player/backend.rs`
|
|
|
|
```rust
|
|
pub trait PlayerBackend: Send + Sync {
|
|
fn load(&mut self, media: &MediaItem) -> Result<(), PlayerError>;
|
|
fn play(&mut self) -> Result<(), PlayerError>;
|
|
fn pause(&mut self) -> Result<(), PlayerError>;
|
|
fn stop(&mut self) -> Result<(), PlayerError>;
|
|
fn seek(&mut self, position: f64) -> Result<(), PlayerError>;
|
|
fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError>;
|
|
fn position(&self) -> f64;
|
|
fn duration(&self) -> Option<f64>;
|
|
fn state(&self) -> PlayerState;
|
|
fn is_loaded(&self) -> bool;
|
|
fn volume(&self) -> f32;
|
|
}
|
|
```
|
|
|
|
**Implementations:**
|
|
- `NullBackend` - Mock backend for testing
|
|
- `MpvBackend` - Linux playback via libmpv (see [05-platform-backends.md](05-platform-backends.md))
|
|
- `ExoPlayerBackend` - Android playback via ExoPlayer/Media3 (see [05-platform-backends.md](05-platform-backends.md))
|
|
|
|
## Player Controller
|
|
|
|
**Location**: `src-tauri/src/player/mod.rs`
|
|
|
|
The `PlayerController` orchestrates playback:
|
|
|
|
```rust
|
|
pub struct PlayerController {
|
|
backend: Arc<Mutex<Box<dyn PlayerBackend>>>,
|
|
queue: Arc<Mutex<QueueManager>>,
|
|
muted: bool,
|
|
sleep_timer: Arc<Mutex<SleepTimerState>>,
|
|
autoplay_settings: Arc<Mutex<AutoplaySettings>>,
|
|
autoplay_episode_count: Arc<Mutex<u32>>, // Session-based counter
|
|
repository: Arc<Mutex<Option<Arc<dyn MediaRepository>>>>,
|
|
event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
|
|
// ... other fields
|
|
}
|
|
```
|
|
|
|
**Key Methods:**
|
|
- `play_item(item)`: Load and play single item (resets autoplay counter)
|
|
- `play_queue(items, start_index)`: Load queue and start playback (resets autoplay counter)
|
|
- `next()` / `previous()`: Queue navigation (resets autoplay counter)
|
|
- `toggle_shuffle()` / `cycle_repeat()`: Mode changes
|
|
- `set_sleep_timer(mode)` / `cancel_sleep_timer()`: Sleep timer control
|
|
- `on_playback_ended()`: Autoplay decision making (checks sleep timer, episode limit, queue)
|
|
|
|
## Playlist System
|
|
|
|
**Location**: `src-tauri/src/commands/playlist.rs`, `src-tauri/src/repository/`
|
|
|
|
**TRACES**: UR-014 | JA-019 | JA-020
|
|
|
|
The playlist system provides full CRUD operations for Jellyfin playlists with offline support through the cache-first repository pattern.
|
|
|
|
**Types:**
|
|
|
|
```rust
|
|
/// A media item within a playlist, with its distinct playlist entry ID
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PlaylistEntry {
|
|
/// Jellyfin's PlaylistItemId (distinct from the media item ID)
|
|
pub playlist_item_id: String,
|
|
#[serde(flatten)]
|
|
pub item: MediaItem,
|
|
}
|
|
|
|
/// Result of creating a new playlist
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PlaylistCreatedResult {
|
|
pub id: String,
|
|
}
|
|
```
|
|
|
|
**Key Design Decision**: `PlaylistEntry` wraps a `MediaItem` with a distinct `playlist_item_id`. This is critical because removing items from a playlist requires the playlist entry ID (not the media item ID), since the same track can appear multiple times.
|
|
|
|
**MediaRepository Trait Methods:**
|
|
```rust
|
|
async fn create_playlist(&self, name: &str, item_ids: Option<Vec<String>>) -> Result<PlaylistCreatedResult, RepoError>;
|
|
async fn delete_playlist(&self, playlist_id: &str) -> Result<(), RepoError>;
|
|
async fn rename_playlist(&self, playlist_id: &str, name: &str) -> Result<(), RepoError>;
|
|
async fn get_playlist_items(&self, playlist_id: &str) -> Result<Vec<PlaylistEntry>, RepoError>;
|
|
async fn add_to_playlist(&self, playlist_id: &str, item_ids: Vec<String>) -> Result<(), RepoError>;
|
|
async fn remove_from_playlist(&self, playlist_id: &str, entry_ids: Vec<String>) -> Result<(), RepoError>;
|
|
async fn move_playlist_item(&self, playlist_id: &str, item_id: &str, new_index: u32) -> Result<(), RepoError>;
|
|
```
|
|
|
|
**Cache Strategy:**
|
|
- **Write operations** (create, delete, rename, add, remove, move): Delegate directly to online repository
|
|
- **Read operation** (`get_playlist_items`): Uses cache-first parallel racing (100ms cache timeout, server fallback)
|
|
- Background cache update after server fetch via `save_playlist_items_to_cache()`
|
|
|
|
**Playlist Tauri Commands:**
|
|
|
|
| Command | Parameters | Returns |
|
|
|---------|------------|---------|
|
|
| `playlist_create` | `handle, name, item_ids?` | `PlaylistCreatedResult` |
|
|
| `playlist_delete` | `handle, playlist_id` | `()` |
|
|
| `playlist_rename` | `handle, playlist_id, name` | `()` |
|
|
| `playlist_get_items` | `handle, playlist_id` | `Vec<PlaylistEntry>` |
|
|
| `playlist_add_items` | `handle, playlist_id, item_ids` | `()` |
|
|
| `playlist_remove_items` | `handle, playlist_id, entry_ids` | `()` |
|
|
| `playlist_move_item` | `handle, playlist_id, item_id, new_index` | `()` |
|
|
|
|
## Tauri Commands (Player)
|
|
|
|
**Location**: `src-tauri/src/commands/player.rs`
|
|
|
|
| Command | Parameters | Returns |
|
|
|---------|------------|---------|
|
|
| `player_play_item` | `PlayItemRequest` | `PlayerStatus` |
|
|
| `player_play_queue` | `items, start_index, shuffle` | `PlayerStatus` |
|
|
| `player_play` | - | `PlayerStatus` |
|
|
| `player_pause` | - | `PlayerStatus` |
|
|
| `player_toggle` | - | `PlayerStatus` |
|
|
| `player_stop` | - | `PlayerStatus` |
|
|
| `player_next` | - | `PlayerStatus` |
|
|
| `player_previous` | - | `PlayerStatus` |
|
|
| `player_seek` | `position: f64` | `PlayerStatus` |
|
|
| `player_set_volume` | `volume: f32` | `PlayerStatus` |
|
|
| `player_toggle_shuffle` | - | `QueueStatus` |
|
|
| `player_cycle_repeat` | - | `QueueStatus` |
|
|
| `player_get_status` | - | `PlayerStatus` |
|
|
| `player_get_queue` | - | `QueueStatus` |
|
|
| `player_get_session` | - | `MediaSessionType` |
|
|
| `player_dismiss_session` | - | `()` |
|
|
| `player_set_sleep_timer` | `mode: SleepTimerMode` | `()` |
|
|
| `player_cancel_sleep_timer` | - | `()` |
|
|
| `player_set_video_settings` | `settings: VideoSettings` | `VideoSettings` |
|
|
| `player_get_video_settings` | - | `VideoSettings` |
|
|
| `player_set_autoplay_settings` | `settings: AutoplaySettings` | `AutoplaySettings` |
|
|
| `player_get_autoplay_settings` | - | `AutoplaySettings` |
|
|
| `player_on_playback_ended` | - | `()` |
|