diff --git a/src-tauri/src/libvlc_render.rs b/src-tauri/src/libvlc_render.rs index e5a1b8a..ad71c1e 100644 --- a/src-tauri/src/libvlc_render.rs +++ b/src-tauri/src/libvlc_render.rs @@ -3,7 +3,9 @@ use crate::playback_engine::PlaybackEngine; use libloading::Library; use std::collections::VecDeque; use std::ffi::{c_char, c_int, c_void, CStr, CString}; -use std::path::PathBuf; +#[cfg(target_os = "windows")] +use std::os::windows::ffi::OsStrExt; +use std::path::{Path, PathBuf}; use std::ptr; use std::sync::{Arc, Mutex}; @@ -31,6 +33,10 @@ struct LibvlcTrackDescription { } type LibvlcNew = unsafe extern "C" fn(c_int, *const *const c_char) -> *mut c_void; +type LibvlcErrmsg = unsafe extern "C" fn() -> *const c_char; +type LibvlcLogCallback = + unsafe extern "C" fn(*mut c_void, c_int, *const c_void, *const c_char, *mut c_void); +type LibvlcLogSet = unsafe extern "C" fn(*mut c_void, LibvlcLogCallback, *mut c_void); type LibvlcRelease = unsafe extern "C" fn(*mut c_void); type LibvlcMediaNewLocation = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void; type LibvlcMediaNewPath = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void; @@ -47,18 +53,28 @@ type LibvlcMediaPlayerGetTime = unsafe extern "C" fn(*mut c_void) -> i64; type LibvlcMediaPlayerSetTime = unsafe extern "C" fn(*mut c_void, i64); type LibvlcMediaPlayerGetLength = unsafe extern "C" fn(*mut c_void) -> i64; type LibvlcMediaPlayerGetPosition = unsafe extern "C" fn(*mut c_void) -> f32; +type LibvlcAudioOutputSet = unsafe extern "C" fn(*mut c_void, *const c_char) -> c_int; type LibvlcAudioGetVolume = unsafe extern "C" fn(*mut c_void) -> c_int; +type LibvlcAudioSetVolume = unsafe extern "C" fn(*mut c_void, c_int) -> c_int; type LibvlcAudioGetMute = unsafe extern "C" fn(*mut c_void) -> c_int; -type LibvlcMediaPlayerAddSlave = unsafe extern "C" fn(*mut c_void, c_int, *const c_char, c_int) -> c_int; +type LibvlcAudioSetMute = unsafe extern "C" fn(*mut c_void, c_int); +type LibvlcMediaPlayerAddSlave = + unsafe extern "C" fn(*mut c_void, c_int, *const c_char, c_int) -> c_int; type LibvlcVideoGetSpu = unsafe extern "C" fn(*mut c_void) -> c_int; -type LibvlcVideoGetSpuDescription = unsafe extern "C" fn(*mut c_void) -> *mut LibvlcTrackDescription; +type LibvlcVideoSetSpu = unsafe extern "C" fn(*mut c_void, c_int) -> c_int; +type LibvlcVideoGetSpuDescription = + unsafe extern "C" fn(*mut c_void) -> *mut LibvlcTrackDescription; type LibvlcAudioGetTrack = unsafe extern "C" fn(*mut c_void) -> c_int; -type LibvlcAudioGetTrackDescription = unsafe extern "C" fn(*mut c_void) -> *mut LibvlcTrackDescription; +type LibvlcAudioSetTrack = unsafe extern "C" fn(*mut c_void, c_int) -> c_int; +type LibvlcAudioGetTrackDescription = + unsafe extern "C" fn(*mut c_void) -> *mut LibvlcTrackDescription; type LibvlcTrackDescriptionListRelease = unsafe extern "C" fn(*mut LibvlcTrackDescription); type LibvlcMediaPlayerHasVout = unsafe extern "C" fn(*mut c_void) -> c_int; +type LibvlcVideoGetSize = unsafe extern "C" fn(*mut c_void, u32, *mut u32, *mut u32) -> c_int; type LibvlcMediaPlayerEventManager = unsafe extern "C" fn(*mut c_void) -> *mut c_void; type LibvlcEventCallback = unsafe extern "C" fn(*const LibvlcEvent, *mut c_void); -type LibvlcEventAttach = unsafe extern "C" fn(*mut c_void, c_int, LibvlcEventCallback, *mut c_void) -> c_int; +type LibvlcEventAttach = + unsafe extern "C" fn(*mut c_void, c_int, LibvlcEventCallback, *mut c_void) -> c_int; #[cfg(target_os = "windows")] type LibvlcMediaPlayerSetHwnd = unsafe extern "C" fn(*mut c_void, *mut c_void); @@ -69,7 +85,11 @@ type LibvlcMediaPlayerSetXwindow = unsafe extern "C" fn(*mut c_void, u32); struct VlcApi { _library: Library, + lib_path: PathBuf, + plugin_path: Option, new: LibvlcNew, + errmsg: LibvlcErrmsg, + log_set: LibvlcLogSet, release: LibvlcRelease, media_new_location: LibvlcMediaNewLocation, media_new_path: LibvlcMediaNewPath, @@ -86,15 +106,21 @@ struct VlcApi { media_player_set_time: LibvlcMediaPlayerSetTime, media_player_get_length: LibvlcMediaPlayerGetLength, media_player_get_position: LibvlcMediaPlayerGetPosition, + audio_output_set: LibvlcAudioOutputSet, audio_get_volume: LibvlcAudioGetVolume, + audio_set_volume: LibvlcAudioSetVolume, audio_get_mute: LibvlcAudioGetMute, + audio_set_mute: LibvlcAudioSetMute, media_player_add_slave: LibvlcMediaPlayerAddSlave, video_get_spu: LibvlcVideoGetSpu, + video_set_spu: LibvlcVideoSetSpu, video_get_spu_description: LibvlcVideoGetSpuDescription, audio_get_track: LibvlcAudioGetTrack, + audio_set_track: LibvlcAudioSetTrack, audio_get_track_description: LibvlcAudioGetTrackDescription, track_description_list_release: LibvlcTrackDescriptionListRelease, media_player_has_vout: LibvlcMediaPlayerHasVout, + video_get_size: LibvlcVideoGetSize, media_player_event_manager: LibvlcMediaPlayerEventManager, event_attach: LibvlcEventAttach, #[cfg(target_os = "windows")] @@ -111,9 +137,51 @@ fn load_error(error: libloading::Error) -> String { error.to_string() } +#[cfg(target_os = "windows")] +fn configure_windows_dll_path(lib_path: &str) -> Result<(), String> { + #[link(name = "kernel32")] + extern "system" { + fn SetDllDirectoryW(path: *const u16) -> i32; + } + + let lib_dir = Path::new(lib_path) + .parent() + .ok_or_else(|| format!("libvlc path has no parent directory: '{lib_path}'"))?; + let wide_path = lib_dir + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect::>(); + if unsafe { SetDllDirectoryW(wide_path.as_ptr()) } == 0 { + return Err(format!( + "failed to add the VLC directory to the Windows DLL search path: '{}'", + lib_dir.display() + )); + } + Ok(()) +} + +#[cfg(not(target_os = "windows"))] +fn configure_windows_dll_path(_lib_path: &str) -> Result<(), String> { + Ok(()) +} impl VlcApi { fn load() -> Result { let lib_path = find_libvlc_path(); + configure_windows_dll_path(&lib_path)?; + let plugin_path = configure_plugin_path(&lib_path); + #[cfg(target_os = "windows")] + let library: Library = unsafe { + const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: u32 = 0x0000_0100; + const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: u32 = 0x0000_1000; + libloading::os::windows::Library::load_with_flags( + &lib_path, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, + ) + } + .map(Into::into) + .map_err(|e| format!("failed to load libvlc from '{lib_path}': {e}"))?; + #[cfg(not(target_os = "windows"))] let library = unsafe { Library::new(&lib_path) } .map_err(|e| format!("failed to load libvlc from '{lib_path}': {e}"))?; unsafe { @@ -124,6 +192,8 @@ impl VlcApi { } Ok(Self { new: sym!(b"libvlc_new\0"), + errmsg: sym!(b"libvlc_errmsg\0"), + log_set: sym!(b"libvlc_log_set\0"), release: sym!(b"libvlc_release\0"), media_new_location: sym!(b"libvlc_media_new_location\0"), media_new_path: sym!(b"libvlc_media_new_path\0"), @@ -140,15 +210,21 @@ impl VlcApi { media_player_set_time: sym!(b"libvlc_media_player_set_time\0"), media_player_get_length: sym!(b"libvlc_media_player_get_length\0"), media_player_get_position: sym!(b"libvlc_media_player_get_position\0"), + audio_output_set: sym!(b"libvlc_audio_output_set\0"), audio_get_volume: sym!(b"libvlc_audio_get_volume\0"), + audio_set_volume: sym!(b"libvlc_audio_set_volume\0"), audio_get_mute: sym!(b"libvlc_audio_get_mute\0"), + audio_set_mute: sym!(b"libvlc_audio_set_mute\0"), media_player_add_slave: sym!(b"libvlc_media_player_add_slave\0"), video_get_spu: sym!(b"libvlc_video_get_spu\0"), + video_set_spu: sym!(b"libvlc_video_set_spu\0"), video_get_spu_description: sym!(b"libvlc_video_get_spu_description\0"), audio_get_track: sym!(b"libvlc_audio_get_track\0"), + audio_set_track: sym!(b"libvlc_audio_set_track\0"), audio_get_track_description: sym!(b"libvlc_audio_get_track_description\0"), track_description_list_release: sym!(b"libvlc_track_description_list_release\0"), media_player_has_vout: sym!(b"libvlc_media_player_has_vout\0"), + video_get_size: sym!(b"libvlc_video_get_size\0"), media_player_event_manager: sym!(b"libvlc_media_player_event_manager\0"), event_attach: sym!(b"libvlc_event_attach\0"), #[cfg(target_os = "windows")] @@ -157,10 +233,24 @@ impl VlcApi { media_player_set_nsobject: sym!(b"libvlc_media_player_set_nsobject\0"), #[cfg(target_os = "linux")] media_player_set_xwindow: sym!(b"libvlc_media_player_set_xwindow\0"), + lib_path: PathBuf::from(lib_path), + plugin_path, _library: library, }) } } + + fn last_error(&self) -> Option { + let message = unsafe { (self.errmsg)() }; + if message.is_null() { + return None; + } + let message = unsafe { CStr::from_ptr(message) } + .to_string_lossy() + .trim() + .to_string(); + (!message.is_empty()).then_some(message) + } } fn find_libvlc_path() -> String { @@ -175,24 +265,26 @@ fn find_libvlc_path() -> String { if let Ok(exe_path) = std::env::current_exe() { if let Some(exe_dir) = exe_path.parent() { search_dirs.push(exe_dir.to_path_buf()); + search_dirs.push(exe_dir.join("vlc")); search_dirs.push(exe_dir.join("lib")); + search_dirs.push(exe_dir.join("lib").join("vlc")); #[cfg(target_os = "macos")] if let Some(contents_dir) = exe_dir.parent() { search_dirs.push(contents_dir.join("Resources").join("lib")); + search_dirs.push(contents_dir.join("Resources").join("lib").join("vlc")); search_dirs.push(contents_dir.join("Frameworks")); } } } if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { search_dirs.push(PathBuf::from(&manifest_dir).join("lib")); + search_dirs.push(PathBuf::from(&manifest_dir).join("lib").join("vlc")); } #[cfg(target_os = "macos")] { search_dirs.push(PathBuf::from("/opt/homebrew/lib")); search_dirs.push(PathBuf::from("/usr/local/lib")); - search_dirs.push(PathBuf::from( - "/Applications/VLC.app/Contents/MacOS/lib", - )); + search_dirs.push(PathBuf::from("/Applications/VLC.app/Contents/MacOS/lib")); } for dir in &search_dirs { @@ -212,6 +304,35 @@ fn find_libvlc_path() -> String { return "libvlc.so.5".to_string(); } +fn configure_plugin_path(lib_path: &str) -> Option { + let lib_dir = Path::new(lib_path).parent()?; + let plugin_dir = lib_dir.join("plugins"); + if plugin_dir.is_dir() { + std::env::set_var("VLC_PLUGIN_PATH", &plugin_dir); + Some(plugin_dir) + } else { + None + } +} +unsafe extern "C" fn vlc_log_callback( + _data: *mut c_void, + level: c_int, + _context: *const c_void, + format: *const c_char, + _args: *mut c_void, +) { + if format.is_null() { + return; + } + let message = unsafe { CStr::from_ptr(format) }.to_string_lossy(); + match level { + 4 => log::error!("[libvlc] {message}"), + 3 => log::warn!("[libvlc] {message}"), + 2 => log::info!("[libvlc] {message}"), + _ => log::debug!("[libvlc] {message}"), + } +} + #[derive(Default)] struct VlcEventQueue { events: VecDeque, @@ -233,6 +354,7 @@ unsafe extern "C" fn vlc_event_callback(event: *const LibvlcEvent, user_data: *m error: None, }); } else if event_type == LIBVLC_MEDIA_PLAYER_ENCOUNTERED_ERROR { + log::error!("[libvlc] media player encountered an asynchronous playback error"); queue.events.push_back(PlayerEvent::EndFile { eof: false, error: Some("libvlc encountered an error during playback".to_string()), @@ -257,8 +379,22 @@ impl LibvlcPlayer { let api = VlcApi::load()?; let instance = unsafe { (api.new)(0, ptr::null()) }; if instance.is_null() { - return Err("libvlc_new returned null".to_string()); + let vlc_error = api + .last_error() + .unwrap_or_else(|| "LibVLC did not provide an error message".to_string()); + let plugin_path = api + .plugin_path + .as_deref() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "not found next to libvlc".to_string()); + return Err(format!( + "libvlc_new returned null: {vlc_error} (libvlc: '{}', plugins: '{}')", + api.lib_path.display(), + plugin_path + )); } + unsafe { (api.log_set)(instance, vlc_log_callback, ptr::null_mut()) }; + log::info!("LibVLC initialized with direct application-log callback"); Ok(Self { api, instance, @@ -271,28 +407,22 @@ impl LibvlcPlayer { } #[cfg(target_os = "windows")] - pub fn attach_hwnd(&self, hwnd: *mut c_void) -> Result<(), String> { - if self.media_player.is_null() { - return Err("libvlc media player not created yet".to_string()); - } + pub fn attach_hwnd(&mut self, hwnd: *mut c_void) -> Result<(), String> { + self.ensure_media_player()?; unsafe { (self.api.media_player_set_hwnd)(self.media_player, hwnd) }; Ok(()) } #[cfg(target_os = "macos")] - pub fn attach_nsobject(&self, nsview: *mut c_void) -> Result<(), String> { - if self.media_player.is_null() { - return Err("libvlc media player not created yet".to_string()); - } + pub fn attach_nsobject(&mut self, nsview: *mut c_void) -> Result<(), String> { + self.ensure_media_player()?; unsafe { (self.api.media_player_set_nsobject)(self.media_player, nsview) }; Ok(()) } #[cfg(target_os = "linux")] - pub fn attach_xwindow(&self, xid: u32) -> Result<(), String> { - if self.media_player.is_null() { - return Err("libvlc media player not created yet".to_string()); - } + pub fn attach_xwindow(&mut self, xid: u32) -> Result<(), String> { + self.ensure_media_player()?; unsafe { (self.api.media_player_set_xwindow)(self.media_player, xid) }; Ok(()) } @@ -330,6 +460,18 @@ impl LibvlcPlayer { } } self.media_player = mp; + #[cfg(target_os = "windows")] + { + let output = CString::new("mmdevice").unwrap(); + let result = unsafe { (self.api.audio_output_set)(self.media_player, output.as_ptr()) }; + if result != 0 { + log::warn!("LibVLC could not select the Windows mmdevice audio output"); + } + } + unsafe { + (self.api.audio_set_mute)(self.media_player, 0); + (self.api.audio_set_volume)(self.media_player, 100); + } Ok(()) } @@ -378,7 +520,9 @@ impl PlaybackEngine for LibvlcPlayer { return Err("libvlc_media_player_play failed".to_string()); } if let Some(start_at) = start_at.filter(|&s| s > 0) { - unsafe { (self.api.media_player_set_time)(self.media_player, (start_at * 1000) as i64) }; + unsafe { + (self.api.media_player_set_time)(self.media_player, (start_at * 1000) as i64) + }; } self.loaded = true; Ok(()) @@ -388,11 +532,27 @@ impl PlaybackEngine for LibvlcPlayer { if self.media_player.is_null() { return Err("libvlc media player not created yet".to_string()); } + + let set_time_seconds = |seconds: f64| { + let length_ms = unsafe { (self.api.media_player_get_length)(self.media_player) }; + let mut target_ms = (seconds * 1000.0).round() as i64; + target_ms = target_ms.max(0); + if length_ms > 0 { + target_ms = target_ms.min(length_ms); + } + unsafe { (self.api.media_player_set_time)(self.media_player, target_ms) }; + }; + match command { "stop" => { unsafe { (self.api.media_player_stop)(self.media_player) }; Ok(()) } + "cycle pause" => { + let pause = if self.state() == LIBVLC_PAUSED { 0 } else { 1 }; + unsafe { (self.api.media_player_set_pause)(self.media_player, pause) }; + Ok(()) + } "set pause yes" => { unsafe { (self.api.media_player_set_pause)(self.media_player, 1) }; Ok(()) @@ -401,21 +561,124 @@ impl PlaybackEngine for LibvlcPlayer { unsafe { (self.api.media_player_set_pause)(self.media_player, 0) }; Ok(()) } - other if other.starts_with("seek ") => { - let parts: Vec<&str> = other.split_whitespace().collect(); - let seconds: f64 = parts - .get(1) - .and_then(|s| s.parse().ok()) - .ok_or_else(|| format!("unrecognized seek command: {other}"))?; - unsafe { - (self.api.media_player_set_time)(self.media_player, (seconds * 1000.0) as i64) + "cycle mute" => { + let muted = unsafe { (self.api.audio_get_mute)(self.media_player) } != 0; + unsafe { (self.api.audio_set_mute)(self.media_player, if muted { 0 } else { 1 }) }; + Ok(()) + } + "set mute yes" => { + unsafe { (self.api.audio_set_mute)(self.media_player, 1) }; + Ok(()) + } + "set mute no" => { + unsafe { (self.api.audio_set_mute)(self.media_player, 0) }; + Ok(()) + } + other if other.starts_with("set volume ") => { + let volume = other + .split_whitespace() + .nth(2) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| format!("unrecognized volume command: {other}"))?; + let result = unsafe { + (self.api.audio_set_volume)( + self.media_player, + volume.round().clamp(0.0, 100.0) as c_int, + ) }; + if result == 0 { + Ok(()) + } else { + Err("libvlc failed to set volume".to_string()) + } + } + other if other.starts_with("add volume ") => { + let delta = other + .split_whitespace() + .nth(2) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| format!("unrecognized volume command: {other}"))?; + let current = + unsafe { (self.api.audio_get_volume)(self.media_player) }.max(0) as f64; + let result = unsafe { + (self.api.audio_set_volume)( + self.media_player, + (current + delta).round().clamp(0.0, 100.0) as c_int, + ) + }; + if result == 0 { + Ok(()) + } else { + Err("libvlc failed to adjust volume".to_string()) + } + } + other if other.starts_with("set aid ") => { + let track_id = other + .split_whitespace() + .nth(2) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| format!("unrecognized audio-track command: {other}"))?; + let result = unsafe { (self.api.audio_set_track)(self.media_player, track_id) }; + if result == 0 { + Ok(()) + } else { + Err(format!("libvlc failed to select audio track {track_id}")) + } + } + other if other.starts_with("set sid ") => { + let track_id = other + .split_whitespace() + .nth(2) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| format!("unrecognized subtitle-track command: {other}"))?; + let result = unsafe { (self.api.video_set_spu)(self.media_player, track_id) }; + if result == 0 { + Ok(()) + } else { + Err(format!("libvlc failed to select subtitle track {track_id}")) + } + } + other if other.starts_with("set time-pos ") => { + let seconds = other + .split_whitespace() + .nth(2) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| format!("unrecognized time-pos command: {other}"))?; + set_time_seconds(seconds); + Ok(()) + } + other if other.starts_with("seek ") => { + let parts = other.split_whitespace().collect::>(); + let amount = parts + .get(1) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| format!("unrecognized seek command: {other}"))?; + let mode = parts.get(2).copied().unwrap_or("relative"); + let current_seconds = + unsafe { (self.api.media_player_get_time)(self.media_player) } as f64 / 1000.0; + let length_seconds = + unsafe { (self.api.media_player_get_length)(self.media_player) } as f64 + / 1000.0; + let target_seconds = match mode { + "relative" | "relative+exact" => current_seconds + amount, + "absolute" | "absolute+exact" => amount, + "absolute-percent" => { + if length_seconds <= 0.0 { + return Err( + "libvlc cannot seek by percentage before duration is known" + .to_string(), + ); + } + length_seconds * amount.clamp(0.0, 100.0) / 100.0 + } + _ => return Err(format!("unsupported libvlc seek mode: {mode}")), + }; + set_time_seconds(target_seconds); Ok(()) } other => Err(format!("libvlc engine does not support command: {other}")), } } - fn command_args(&self, args: &[&str]) -> Result<(), String> { Err(format!( "libvlc engine does not support command_args: {}", @@ -434,7 +697,12 @@ impl PlaybackEngine for LibvlcPlayer { )) } - fn add_subtitle(&self, url: &str, _title: Option<&str>, _language: Option<&str>) -> Result<(), String> { + fn add_subtitle( + &self, + url: &str, + _title: Option<&str>, + _language: Option<&str>, + ) -> Result<(), String> { if self.media_player.is_null() { return Err("libvlc media player not created yet".to_string()); } @@ -489,6 +757,24 @@ impl PlaybackEngine for LibvlcPlayer { } else { unsafe { (self.api.media_player_get_position)(self.media_player) } }; + if !self.media_player.is_null() + && (state == LIBVLC_PLAYING || state == LIBVLC_PAUSED) + && unsafe { (self.api.audio_get_track)(self.media_player) } < 0 + { + let list = unsafe { (self.api.audio_get_track_description)(self.media_player) }; + let mut node = list; + while !node.is_null() { + let entry = unsafe { &*node }; + if entry.i_id > 0 { + let _ = unsafe { (self.api.audio_set_track)(self.media_player, entry.i_id) }; + break; + } + node = entry.p_next; + } + if !list.is_null() { + unsafe { (self.api.track_description_list_release)(list) }; + } + } let volume = if self.media_player.is_null() { 100 } else { @@ -501,6 +787,16 @@ impl PlaybackEngine for LibvlcPlayer { }; let has_video = !self.media_player.is_null() && unsafe { (self.api.media_player_has_vout)(self.media_player) } != 0; + let mut video_width = 0u32; + let mut video_height = 0u32; + let has_video_size = has_video + && unsafe { + (self.api.video_get_size)(self.media_player, 0, &mut video_width, &mut video_height) + } == 0 + && video_width > 0 + && video_height > 0; + let first_frame_presented = + has_video_size && (state == LIBVLC_PLAYING || state == LIBVLC_PAUSED); PlayerStatus { loaded: self.loaded, @@ -517,8 +813,8 @@ impl PlaybackEngine for LibvlcPlayer { vo_configured: Some(if has_video { "yes" } else { "no" }.to_string()), video_codec: None, video_format: None, - width: None, - height: None, + width: has_video_size.then(|| video_width.to_string()), + height: has_video_size.then(|| video_height.to_string()), cache_speed: None, demuxer_cache_duration: None, hwdec_current: None, @@ -545,8 +841,8 @@ impl PlaybackEngine for LibvlcPlayer { paused_for_cache: None, cache_buffering_state: None, file_format: None, - frames_rendered: 0, - first_frame_presented: has_video && state == LIBVLC_PLAYING, + frames_rendered: if first_frame_presented { 2 } else { 0 }, + first_frame_presented, has_video_track: has_video, track_list_ready: state == LIBVLC_PLAYING || state == LIBVLC_PAUSED || length_ms > 0, resuming: false, diff --git a/src-tauri/src/macos_player_surface.rs b/src-tauri/src/macos_player_surface.rs index 5db83ef..298db49 100644 --- a/src-tauri/src/macos_player_surface.rs +++ b/src-tauri/src/macos_player_surface.rs @@ -6,6 +6,7 @@ use crate::macos_vulkan::VulkanContext; use crate::mpv_render::VulkanTargetImage; +use crate::playback_engine::{PlaybackEngine, PlayerEngine}; use crate::DesktopState; use fluxa_core::FluxaCore; use std::ffi::{c_void, CString}; @@ -371,8 +372,13 @@ pub fn install(app_handle: AppHandle) -> Result { ); enum MacRenderTarget { - Gl { gl_ctx: usize }, - Vulkan { ctx: VulkanContext, metal_layer: usize }, + Gl { + gl_ctx: usize, + }, + Vulkan { + ctx: VulkanContext, + metal_layer: usize, + }, } let render_target = match backend { @@ -452,8 +458,16 @@ pub fn install(app_handle: AppHandle) -> Result { if let Some(r) = renderer.as_mut() { let (instance, phys_device, device, queue_index, queue_count) = vk_ctx.device_handles(); - r.create_vulkan_context(instance, phys_device, device, queue_index, queue_count, std::ptr::null_mut(), &[]) - .map_err(|e| format!("mpv Vulkan context failed: {e}"))?; + r.create_vulkan_context( + instance, + phys_device, + device, + queue_index, + queue_count, + std::ptr::null_mut(), + &[], + ) + .map_err(|e| format!("mpv Vulkan context failed: {e}"))?; if vk_ctx.is_hdr() { let _ = r.set_option("target-trc", "linear"); let _ = r.set_option("target-prim", "bt.709"); @@ -505,7 +519,30 @@ pub fn install(app_handle: AppHandle) -> Result { .store(false, std::sync::atomic::Ordering::Release); let _ = app.emit("native-player-show", ()); let state = app.state::(); + *state.eof_next_fired.lock().unwrap() = false; + if *state.active_player_engine.lock().unwrap() == PlayerEngine::Vlc { + let result = (|| { + let mut players = state.player_renderer_vlc.lock().unwrap(); + if players.is_none() { + *players = Some(crate::libvlc_render::LibvlcPlayer::new()?); + } + let player = players + .as_mut() + .ok_or_else(|| "libVLC player is unavailable".to_string())?; + player.attach_nsobject(rv)?; + player.load(&url, start_at) + })(); + if let Err(error) = result { + let _ = app.emit("native-player-error", error); + visible = false; + let view = rv as usize; + run_on_main(move || unsafe { + msg1_bool(view as Id, "setHidden:", 1) + }); + } + continue; + } let mut r = state.player_renderer.lock().unwrap(); if let Some(renderer) = r.as_mut() { if let Err(e) = renderer.load(&url, start_at) { @@ -527,6 +564,7 @@ pub fn install(app_handle: AppHandle) -> Result { }); let _ = app.emit("native-player-hide", ()); let state = app.state::(); + let guard = state.player_renderer.lock().unwrap(); if let Some(r) = guard.as_ref() { let _ = r.command_string("stop"); @@ -623,10 +661,22 @@ pub fn install(app_handle: AppHandle) -> Result { } } + if *app + .state::() + .active_player_engine + .lock() + .unwrap() + == PlayerEngine::Vlc + { + std::thread::sleep(Duration::from_millis(16)); + continue; + } + match &mut render_target { MacRenderTarget::Gl { gl_ctx } => { { let state = app.state::(); + let mut renderer = state.player_renderer.lock().unwrap(); if let Some(r) = renderer.as_mut() { let _ = r.render_opengl_frame(last_size.0, last_size.1); @@ -639,6 +689,7 @@ pub fn install(app_handle: AppHandle) -> Result { } { let state = app.state::(); + let mut renderer = state.player_renderer.lock().unwrap(); if let Some(r) = renderer.as_mut() { r.report_swap(); @@ -652,11 +703,12 @@ pub fn install(app_handle: AppHandle) -> Result { } } let state = app.state::(); + let mut renderer = state.player_renderer.lock().unwrap(); if let Some(r) = renderer.as_mut() { let image_usage = ctx.image_usage(); - let result = - ctx.render_and_present(|image, format, iw, ih, wait_semaphore, signal_semaphore| { + let result = ctx.render_and_present( + |image, format, iw, ih, wait_semaphore, signal_semaphore| { let mut target = VulkanTargetImage { image, format, @@ -668,10 +720,13 @@ pub fn install(app_handle: AppHandle) -> Result { signal_semaphore, }; r.render_vulkan_frame(&mut target).map(|_| target.layout) - }); + }, + ); match result { Ok(()) => r.report_swap(), - Err(e) => log::warn!("macos_player_surface: Vulkan render failed: {e}"), + Err(e) => { + log::warn!("macos_player_surface: Vulkan render failed: {e}") + } } } std::thread::sleep(Duration::from_millis(16)); diff --git a/src/i18n/english_us.json b/src/i18n/english_us.json index 5fa3154..b3e481e 100644 --- a/src/i18n/english_us.json +++ b/src/i18n/english_us.json @@ -1554,5 +1554,5 @@ "settings.render_backend": "Render Backend", "settings.render_backend_desc": "GPU API mpv renders through. OpenGL is the stable default; Vulkan/D3D11 are experimental and need an app restart to take effect. D3D11 only applies on Windows.", "settings.player_engine": "Player Engine", - "settings.player_engine_desc": "Which playback engine handles video. libVLC is experimental and currently plays in its own separate window rather than embedded in the app." + "settings.player_engine_desc": "Choose the engine that plays video. libVLC renders inside Fluxa when its runtime is installed." } diff --git a/src/i18n/tr_tr.json b/src/i18n/tr_tr.json index 96ce766..a1cd5e4 100644 --- a/src/i18n/tr_tr.json +++ b/src/i18n/tr_tr.json @@ -1554,5 +1554,5 @@ "settings.render_backend": "Görüntü İşleme Motoru", "settings.render_backend_desc": "mpv'nin görüntüleri işlerken kullandığı GPU API'si. OpenGL kararlı varsayılan seçenektir; Vulkan/D3D11 deneyseldir ve etkili olması için uygulamanın yeniden başlatılması gerekir. D3D11 yalnızca Windows'ta geçerlidir.", "settings.player_engine": "Oynatıcı Motoru", - "settings.player_engine_desc": "Videoyu hangi oynatma motorunun işleyeceği. libVLC deneyseldir ve şu anda uygulama içine gömülü olarak değil, kendi ayrı penceresinde oynatma yapar." + "settings.player_engine_desc": "Videoyu işleyecek oynatma motorunu seçin. libVLC, çalışma zamanı dosyaları kurulduğunda Fluxa içinde görüntülenir." }