mirror of
https://github.com/FluxaMedia/fluxa-desktop.git
synced 2026-08-09 16:37:30 +00:00
feat(desktop): embed libVLC playback instead of a separate window
- libvlc_render.rs: locate/load libvlc more robustly (Windows DLL search path, plugin path discovery), route libvlc log messages into the app logger, lazily create the media player, and add audio mute/track and subtitle-track control bindings. - macos_player_surface.rs: attach the libVLC output to the native NSView so it renders inside the app window (macOS), switching between the Vulkan and libVLC render paths based on the active engine. - Update the player-engine description strings to reflect that libVLC now renders embedded rather than in its own window.
This commit is contained in:
parent
6c1bc94a07
commit
6087dcdf97
4 changed files with 397 additions and 46 deletions
|
|
@ -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<PathBuf>,
|
||||
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::<Vec<_>>();
|
||||
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<Self, String> {
|
||||
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<String> {
|
||||
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<PathBuf> {
|
||||
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<PlayerEvent>,
|
||||
|
|
@ -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::<f64>().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::<f64>().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::<c_int>().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::<c_int>().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::<f64>().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::<Vec<_>>();
|
||||
let amount = parts
|
||||
.get(1)
|
||||
.and_then(|value| value.parse::<f64>().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,
|
||||
|
|
|
|||
|
|
@ -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<NativePlayerSurface, String> {
|
|||
);
|
||||
|
||||
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<NativePlayerSurface, String> {
|
|||
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<NativePlayerSurface, String> {
|
|||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let _ = app.emit("native-player-show", ());
|
||||
let state = app.state::<DesktopState>();
|
||||
|
||||
*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<NativePlayerSurface, String> {
|
|||
});
|
||||
let _ = app.emit("native-player-hide", ());
|
||||
let state = app.state::<DesktopState>();
|
||||
|
||||
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<NativePlayerSurface, String> {
|
|||
}
|
||||
}
|
||||
|
||||
if *app
|
||||
.state::<DesktopState>()
|
||||
.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::<DesktopState>();
|
||||
|
||||
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<NativePlayerSurface, String> {
|
|||
}
|
||||
{
|
||||
let state = app.state::<DesktopState>();
|
||||
|
||||
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<NativePlayerSurface, String> {
|
|||
}
|
||||
}
|
||||
let state = app.state::<DesktopState>();
|
||||
|
||||
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<NativePlayerSurface, String> {
|
|||
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));
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue