mirror of
https://github.com/FluxaMedia/fluxa-desktop.git
synced 2026-07-27 04:12:09 +00:00
fix(windows): synchronize native player controls and status
This commit is contained in:
parent
64025232f9
commit
52284c53f8
8 changed files with 127 additions and 10 deletions
|
|
@ -546,6 +546,7 @@ impl PlaybackEngine for LibvlcPlayer {
|
|||
cache_buffering_state: None,
|
||||
file_format: None,
|
||||
frames_rendered: 0,
|
||||
first_frame_presented: has_video && state == LIBVLC_PLAYING,
|
||||
has_video_track: has_video,
|
||||
track_list_ready: state == LIBVLC_PLAYING || state == LIBVLC_PAUSED || length_ms > 0,
|
||||
resuming: false,
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ pub struct MpvRenderer {
|
|||
loaded: bool,
|
||||
log_ring: std::collections::VecDeque<(c_int, String)>,
|
||||
frames_rendered: u64,
|
||||
first_frame_presented: bool,
|
||||
pending_unpause: bool,
|
||||
pending_seek_seconds: Option<f64>,
|
||||
waiting_for_seek_restart: bool,
|
||||
|
|
@ -264,6 +265,7 @@ pub struct PlayerStatus {
|
|||
pub cache_buffering_state: Option<String>,
|
||||
pub file_format: Option<String>,
|
||||
pub frames_rendered: u64,
|
||||
pub first_frame_presented: bool,
|
||||
pub has_video_track: bool,
|
||||
pub track_list_ready: bool,
|
||||
pub resuming: bool,
|
||||
|
|
@ -358,6 +360,7 @@ impl MpvRenderer {
|
|||
loaded: false,
|
||||
log_ring: std::collections::VecDeque::new(),
|
||||
frames_rendered: 0,
|
||||
first_frame_presented: false,
|
||||
pending_unpause: false,
|
||||
pending_seek_seconds: None,
|
||||
waiting_for_seek_restart: false,
|
||||
|
|
@ -437,6 +440,7 @@ impl MpvRenderer {
|
|||
loaded: false,
|
||||
log_ring: std::collections::VecDeque::new(),
|
||||
frames_rendered: 0,
|
||||
first_frame_presented: false,
|
||||
pending_unpause: false,
|
||||
pending_seek_seconds: None,
|
||||
waiting_for_seek_restart: false,
|
||||
|
|
@ -482,6 +486,7 @@ impl MpvRenderer {
|
|||
self.current_url = Some(url.to_string());
|
||||
self.log_ring.clear();
|
||||
self.frames_rendered = 0;
|
||||
self.first_frame_presented = false;
|
||||
self.pending_unpause = true;
|
||||
self.pending_seek_seconds = start_at.filter(|&s| s > 0).map(|s| s as f64);
|
||||
self.waiting_for_seek_restart = false;
|
||||
|
|
@ -510,10 +515,28 @@ impl MpvRenderer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn first_frame_presented(&self) -> bool {
|
||||
self.first_frame_presented
|
||||
}
|
||||
|
||||
pub fn seek_to(&self, time_pos: f64) -> Result<(), String> {
|
||||
self.command_string(&format!("seek {time_pos:.3} absolute+exact"))
|
||||
}
|
||||
|
||||
/// Dispatch a command originating from the UI. Startup resume/unpause work
|
||||
/// must never run later and overwrite an explicit user action.
|
||||
pub fn user_command(&mut self, command: &str) -> Result<(), String> {
|
||||
let command = command.trim();
|
||||
if command == "cycle pause" || command.starts_with("set pause ") {
|
||||
self.pending_unpause = false;
|
||||
}
|
||||
if command.starts_with("seek ") || command.starts_with("set time-pos ") {
|
||||
self.pending_seek_seconds = None;
|
||||
self.waiting_for_seek_restart = false;
|
||||
}
|
||||
self.command_string(command)
|
||||
}
|
||||
|
||||
pub fn render_thumbnail(&mut self, width: i32, height: i32) -> Result<Vec<u8>, String> {
|
||||
if !self.loaded {
|
||||
return Err("not loaded".to_string());
|
||||
|
|
@ -801,6 +824,9 @@ impl MpvRenderer {
|
|||
unsafe {
|
||||
(self.api.mpv_render_context_report_swap)(self.render_context);
|
||||
}
|
||||
if self.frame_ready_to_restore_audio {
|
||||
self.first_frame_presented = true;
|
||||
}
|
||||
self.restore_audio_after_first_presented_frame();
|
||||
}
|
||||
|
||||
|
|
@ -1075,6 +1101,7 @@ impl MpvRenderer {
|
|||
cache_buffering_state: self.get_string_property("cache-buffering-state"),
|
||||
file_format: self.get_string_property("file-format"),
|
||||
frames_rendered: self.frames_rendered,
|
||||
first_frame_presented: self.first_frame_presented,
|
||||
has_video_track,
|
||||
track_list_ready,
|
||||
resuming: self.pending_seek_seconds.is_some() || self.waiting_for_seek_restart,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ use crate::mpv_render::{MpvRenderer, PlayerEvent, PlayerStatus, PlayerTrackOptio
|
|||
|
||||
pub trait PlaybackEngine: Send {
|
||||
fn load(&mut self, url: &str, start_at: Option<u64>) -> Result<(), String>;
|
||||
fn user_command(&mut self, command: &str) -> Result<(), String> {
|
||||
self.command_string(command)
|
||||
}
|
||||
fn command_string(&self, command: &str) -> Result<(), String>;
|
||||
fn command_args(&self, args: &[&str]) -> Result<(), String>;
|
||||
fn apply_options(&self, options: &[(String, String)]) -> Result<(), String>;
|
||||
|
|
@ -21,6 +24,9 @@ impl PlaybackEngine for MpvRenderer {
|
|||
fn command_string(&self, command: &str) -> Result<(), String> {
|
||||
MpvRenderer::command_string(self, command)
|
||||
}
|
||||
fn user_command(&mut self, command: &str) -> Result<(), String> {
|
||||
MpvRenderer::user_command(self, command)
|
||||
}
|
||||
fn command_args(&self, args: &[&str]) -> Result<(), String> {
|
||||
MpvRenderer::command_args(self, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,39 @@ where
|
|||
Err("player renderer busy".to_string())
|
||||
}
|
||||
|
||||
fn with_renderer_retry_mut<T, F>(
|
||||
state: &DesktopState,
|
||||
attempts: usize,
|
||||
mut f: F,
|
||||
) -> Result<Option<T>, String>
|
||||
where
|
||||
F: FnMut(&mut dyn PlaybackEngine) -> Result<T, String>,
|
||||
{
|
||||
let engine = *state.active_player_engine.lock().unwrap();
|
||||
for _ in 0..attempts {
|
||||
match engine {
|
||||
PlayerEngine::Mpv => {
|
||||
if let Ok(mut guard) = state.player_renderer.try_lock() {
|
||||
if let Some(renderer) = guard.as_mut() {
|
||||
return f(renderer).map(Some);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
PlayerEngine::Vlc => {
|
||||
if let Ok(mut guard) = state.player_renderer_vlc.try_lock() {
|
||||
if let Some(renderer) = guard.as_mut() {
|
||||
return f(renderer).map(Some);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err("player renderer busy".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn player_init(app: AppHandle, state: State<'_, DesktopState>) -> Result<(), String> {
|
||||
log::info!("player_init: start");
|
||||
|
|
@ -546,11 +579,17 @@ pub fn player_get_anime4k_enabled(state: State<DesktopState>) -> bool {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn player_command(state: State<DesktopState>, command: String) -> Result<(), String> {
|
||||
pub fn player_command(_app: AppHandle, state: State<DesktopState>, command: String) -> Result<(), String> {
|
||||
if command == "stop" {
|
||||
*state.eof_next_fired.lock().unwrap() = true;
|
||||
}
|
||||
match with_renderer_retry(&state, 60, |renderer| renderer.command_string(&command)) {
|
||||
#[cfg(target_os = "windows")]
|
||||
if *state.active_player_engine.lock().unwrap() == PlayerEngine::Mpv {
|
||||
if let Some(surface) = state.native_player_surface.lock().unwrap().as_ref() {
|
||||
return surface.command(command);
|
||||
}
|
||||
}
|
||||
match with_renderer_retry_mut(&state, 60, |renderer| renderer.user_command(&command)) {
|
||||
Ok(Some(())) => Ok(()),
|
||||
Ok(None) => Err("player renderer is not initialized".to_string()),
|
||||
Err(e) => Err(e),
|
||||
|
|
@ -674,6 +713,12 @@ pub fn player_status(
|
|||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if *state.active_player_engine.lock().unwrap() == PlayerEngine::Mpv {
|
||||
if let Some(surface) = state.native_player_surface.lock().unwrap().as_ref() {
|
||||
return surface.status();
|
||||
}
|
||||
}
|
||||
match with_renderer_retry(&state, 80, |renderer| Ok(renderer.status())) {
|
||||
Ok(Some(status)) => Ok(status),
|
||||
Ok(None) => Err("player renderer is not initialized".to_string()),
|
||||
|
|
|
|||
|
|
@ -276,6 +276,8 @@ enum SurfaceCommand {
|
|||
total_duration: Option<u64>,
|
||||
},
|
||||
Hide,
|
||||
PlayerCommand(String),
|
||||
Status(mpsc::Sender<crate::mpv_render::PlayerStatus>),
|
||||
SetCursorVisible(bool),
|
||||
ShowLoading {
|
||||
title: String,
|
||||
|
|
@ -316,6 +318,20 @@ impl NativePlayerSurface {
|
|||
pub fn hide(&self) {
|
||||
let _ = self.sender.send(SurfaceCommand::Hide);
|
||||
}
|
||||
pub fn command(&self, command: String) -> Result<(), String> {
|
||||
self.sender
|
||||
.send(SurfaceCommand::PlayerCommand(command))
|
||||
.map_err(|e| format!("surface unavailable: {e}"))
|
||||
}
|
||||
pub fn status(&self) -> Result<crate::mpv_render::PlayerStatus, String> {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
self.sender
|
||||
.send(SurfaceCommand::Status(sender))
|
||||
.map_err(|e| format!("surface unavailable: {e}"))?;
|
||||
receiver
|
||||
.recv_timeout(Duration::from_millis(250))
|
||||
.map_err(|e| format!("player status unavailable: {e}"))
|
||||
}
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
let _ = self.sender.send(SurfaceCommand::SetCursorVisible(visible));
|
||||
}
|
||||
|
|
@ -620,6 +636,22 @@ fn spawn_install_thread(
|
|||
unsafe { ShowWindow(child_hwnd, SW_HIDE) };
|
||||
}
|
||||
}
|
||||
SurfaceCommand::PlayerCommand(command) => {
|
||||
let state = app.state::<DesktopState>();
|
||||
let mut renderer = state.player_renderer.lock().unwrap();
|
||||
if let Some(r) = renderer.as_mut() {
|
||||
if let Err(error) = r.user_command(&command) {
|
||||
log::warn!("player surface: command failed ({command}): {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
SurfaceCommand::Status(sender) => {
|
||||
let state = app.state::<DesktopState>();
|
||||
let renderer = state.player_renderer.lock().unwrap();
|
||||
if let Some(r) = renderer.as_ref() {
|
||||
let _ = sender.send(r.status());
|
||||
}
|
||||
}
|
||||
SurfaceCommand::Hide => {
|
||||
visible = false;
|
||||
unsafe {
|
||||
|
|
|
|||
|
|
@ -310,7 +310,6 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in
|
|||
position > 0.15;
|
||||
if (!firstFrameFiredRef.current && onFirstFrame && hasRenderedVideo) {
|
||||
firstFrameFiredRef.current = true;
|
||||
sendCmd('set pause no');
|
||||
onFirstFrame();
|
||||
}
|
||||
}
|
||||
|
|
@ -513,16 +512,22 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in
|
|||
const hasVideoDimensions =
|
||||
(parseFloat(status.width ?? '0') || 0) > 0 &&
|
||||
(parseFloat(status.height ?? '0') || 0) > 0;
|
||||
const playbackAdvancing =
|
||||
const renderedVideo =
|
||||
status.loaded &&
|
||||
status.pause !== 'yes' &&
|
||||
status.hasVideoTrack &&
|
||||
hasVideoDimensions &&
|
||||
status.voConfigured === 'yes' &&
|
||||
status.framesRendered >= 2 &&
|
||||
status.pausedForCache !== 'yes' &&
|
||||
pos > 0.15;
|
||||
const renderedVideo = status.hasVideoTrack && hasVideoDimensions && status.voConfigured === 'yes' && status.framesRendered >= 2 && playbackAdvancing;
|
||||
const activeAudioOnlyPlayback = status.trackListReady && !status.hasVideoTrack && playbackAdvancing;
|
||||
if (playbackUrl && status.path === playbackUrl && !status.resuming && (renderedVideo || activeAudioOnlyPlayback)) {
|
||||
const activeAudioOnlyPlayback =
|
||||
status.loaded &&
|
||||
status.trackListReady &&
|
||||
!status.hasVideoTrack &&
|
||||
status.pausedForCache !== 'yes' &&
|
||||
pos > 0.05;
|
||||
if (renderedVideo || activeAudioOnlyPlayback) {
|
||||
firstFrameFiredRef.current = true;
|
||||
sendCmd('set pause no');
|
||||
onFirstFrame();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export type EmbeddedMpvStatus = {
|
|||
cacheBufferingState?: string | null;
|
||||
fileFormat?: string | null;
|
||||
framesRendered: number;
|
||||
firstFramePresented: boolean;
|
||||
hasVideoTrack: boolean;
|
||||
trackListReady: boolean;
|
||||
resuming: boolean;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function subscribePlayerStatus(listener: Listener): () => void {
|
|||
listeners.add(listener);
|
||||
if (timer === null) {
|
||||
void poll();
|
||||
timer = window.setInterval(() => { void poll(); }, 500);
|
||||
timer = window.setInterval(() => { void poll(); }, 100);
|
||||
}
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
|
|
|
|||
Loading…
Reference in a new issue