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:
@@ -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