Playback fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m28s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m37s
Build & Release / Run Tests (push) Successful in 4m12s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 18m57s
Build & Release / Create Release (push) Successful in 13s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m28s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m37s
Build & Release / Run Tests (push) Successful in 4m12s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 18m57s
Build & Release / Create Release (push) Successful in 13s
This commit is contained in:
@@ -247,3 +247,51 @@ pub async fn player_on_playback_ended(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ===== HTML5 video state-report commands =====
|
||||
//
|
||||
// On platforms where video renders in the webview (Linux WebKitGTK HTML5
|
||||
// <video>), the real player lives outside the native backend, so the frontend
|
||||
// HTML5 adapter reports DOM events back through these commands. The controller
|
||||
// re-emits them through the same PlayerStatusEvent pipeline the native backends
|
||||
// use, keeping the Rust controller the single source of truth and the frontend
|
||||
// player store fed from one place (playerEvents.ts) in both modes.
|
||||
|
||||
/// Report an HTML5 <video> state change (playing/paused/loading/stopped/idle).
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn player_report_state(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
state: String,
|
||||
media_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let controller = player.0.lock().await;
|
||||
controller.report_html5_state(state, media_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Report an HTML5 <video> position tick (seconds). The adapter should throttle
|
||||
/// these to roughly match the native backends' ~250ms cadence.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn player_report_position(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
position: f64,
|
||||
duration: f64,
|
||||
) -> Result<(), String> {
|
||||
let controller = player.0.lock().await;
|
||||
controller.report_html5_position(position, duration);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Report that the HTML5 <video> finished loading and knows its duration.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn player_report_media_loaded(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
duration: f64,
|
||||
) -> Result<(), String> {
|
||||
let controller = player.0.lock().await;
|
||||
controller.report_html5_media_loaded(duration);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ use commands::{
|
||||
player_set_sleep_timer, player_cancel_sleep_timer, player_get_sleep_timer,
|
||||
player_get_autoplay_settings, player_set_autoplay_settings,
|
||||
player_cancel_autoplay_countdown, player_play_next_episode, player_on_playback_ended,
|
||||
// HTML5 video state-report commands
|
||||
player_report_state, player_report_position, player_report_media_loaded,
|
||||
// Queue manipulation commands
|
||||
player_add_to_queue, player_add_track_by_id, player_add_tracks_by_ids,
|
||||
player_remove_from_queue, player_move_in_queue, player_skip_to,
|
||||
@@ -476,6 +478,9 @@ fn specta_builder() -> Builder<tauri::Wry> {
|
||||
player_cancel_autoplay_countdown,
|
||||
player_play_next_episode,
|
||||
player_on_playback_ended,
|
||||
player_report_state,
|
||||
player_report_position,
|
||||
player_report_media_loaded,
|
||||
// Preload commands
|
||||
player_preload_upcoming,
|
||||
player_set_cache_config,
|
||||
|
||||
@@ -777,6 +777,44 @@ impl PlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HTML5 video report methods =====
|
||||
//
|
||||
// On platforms where video is rendered in the webview (Linux WebKitGTK
|
||||
// HTML5 <video>), the real player lives outside the native backend, so it
|
||||
// cannot emit PlayerStatusEvents itself. The frontend HTML5 adapter reports
|
||||
// DOM events here, and these methods re-emit them through the SAME event
|
||||
// pipeline the native backends use. This keeps the frontend's player store
|
||||
// fed from one place (playerEvents.ts) in both native and HTML5 modes, so
|
||||
// the Rust controller stays the single source of truth for player state.
|
||||
|
||||
/// Report an HTML5 <video> state change (playing/paused/loading/stopped).
|
||||
///
|
||||
/// Re-emits a `StateChanged` event identical to what MpvBackend/ExoPlayer
|
||||
/// would emit, so `playerEvents.ts` needs no HTML5-specific branch.
|
||||
pub fn report_html5_state(&self, state: String, media_id: Option<String>) {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::StateChanged { state, media_id });
|
||||
}
|
||||
}
|
||||
|
||||
/// Report an HTML5 <video> position tick.
|
||||
///
|
||||
/// Re-emits a `PositionUpdate` event mirroring the native backends' periodic
|
||||
/// position updates (the adapter is expected to throttle to ~250ms like MPV).
|
||||
pub fn report_html5_position(&self, position: f64, duration: f64) {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::PositionUpdate { position, duration });
|
||||
}
|
||||
}
|
||||
|
||||
/// Report that the HTML5 <video> element finished loading and knows its
|
||||
/// duration. Mirrors the native `MediaLoaded` event.
|
||||
pub fn report_html5_media_loaded(&self, duration: f64) {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::MediaLoaded { duration });
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Autoplay Methods =====
|
||||
|
||||
/// Get autoplay settings
|
||||
@@ -1139,6 +1177,83 @@ impl Default for PlayerController {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Test emitter that captures events for asserting the HTML5 report methods
|
||||
/// re-emit through the normal PlayerStatusEvent pipeline.
|
||||
struct CapturingEmitter {
|
||||
events: std::sync::Mutex<Vec<PlayerStatusEvent>>,
|
||||
}
|
||||
|
||||
impl CapturingEmitter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
events: std::sync::Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
fn events(&self) -> Vec<PlayerStatusEvent> {
|
||||
self.events.lock_safe().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerEventEmitter for CapturingEmitter {
|
||||
fn emit(&self, event: PlayerStatusEvent) {
|
||||
self.events.lock_safe().push(event);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_html5_state_emits_state_changed() {
|
||||
let controller = PlayerController::default();
|
||||
let emitter = Arc::new(CapturingEmitter::new());
|
||||
controller.set_event_emitter(emitter.clone());
|
||||
|
||||
controller.report_html5_state("playing".to_string(), Some("item-1".to_string()));
|
||||
|
||||
let events = emitter.events();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
PlayerStatusEvent::StateChanged { state, media_id } => {
|
||||
assert_eq!(state, "playing");
|
||||
assert_eq!(media_id.as_deref(), Some("item-1"));
|
||||
}
|
||||
other => panic!("expected StateChanged, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_html5_position_emits_position_update() {
|
||||
let controller = PlayerController::default();
|
||||
let emitter = Arc::new(CapturingEmitter::new());
|
||||
controller.set_event_emitter(emitter.clone());
|
||||
|
||||
controller.report_html5_position(12.5, 300.0);
|
||||
|
||||
let events = emitter.events();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
PlayerStatusEvent::PositionUpdate { position, duration } => {
|
||||
assert_eq!(*position, 12.5);
|
||||
assert_eq!(*duration, 300.0);
|
||||
}
|
||||
other => panic!("expected PositionUpdate, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_html5_media_loaded_emits_media_loaded() {
|
||||
let controller = PlayerController::default();
|
||||
let emitter = Arc::new(CapturingEmitter::new());
|
||||
controller.set_event_emitter(emitter.clone());
|
||||
|
||||
controller.report_html5_media_loaded(420.0);
|
||||
|
||||
let events = emitter.events();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
PlayerStatusEvent::MediaLoaded { duration } => assert_eq!(*duration, 420.0),
|
||||
other => panic!("expected MediaLoaded, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_controller_volume_default() {
|
||||
let controller = PlayerController::default();
|
||||
|
||||
Reference in New Issue
Block a user