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:
KhooLy 2026-07-27 16:13:03 +03:00
parent 6c1bc94a07
commit 6087dcdf97
4 changed files with 397 additions and 46 deletions

View file

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

View file

@ -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));

View file

@ -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."
}

View file

@ -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."
}