diff --git a/Cargo.lock b/Cargo.lock index 9ad6f88..538d7c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1624,7 +1624,7 @@ dependencies = [ [[package]] name = "stremio-shell-ng" -version = "5.0.22" +version = "5.0.23" dependencies = [ "anyhow", "bitflags 2.4.2", diff --git a/Cargo.toml b/Cargo.toml index 70f6105..4fac471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stremio-shell-ng" -version = "5.0.22" +version = "5.0.23" edition = "2018" [dependencies] @@ -14,12 +14,18 @@ native-windows-gui = { git = "https://github.com/Stremio/native-windows-gui", fe ] } native-windows-derive = "1" winapi = { version = "0.3.9", features = [ + "d3d11", + "d3dcommon", "dwmapi", + "dxgi", + "dxgiformat", + "dxgitype", "libloaderapi", "handleapi", "jobapi2", "wincon", "winuser", + "wingdi", "namedpipeapi", ] } webview2 = "0.1.4" diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index c9b9e64..c7e0118 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -16,7 +16,10 @@ use url::Url; use winapi::um::{winbase::CREATE_BREAKAWAY_FROM_JOB, winuser::WS_EX_TOPMOST}; use crate::stremio_app::{ - constants::{APP_NAME, UPDATE_ENDPOINT, UPDATE_INTERVAL, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH}, + constants::{ + web_endpoint_with_streaming_server, APP_NAME, UPDATE_ENDPOINT, UPDATE_INTERVAL, + WEB_ENDPOINT, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH, + }, ipc::{RPCRequest, RPCResponse}, splash::SplashImage, stremio_player::Player, @@ -130,7 +133,16 @@ impl MainWindow { } } fn on_init(&self) { - self.webview.endpoint.set(self.webui_url.clone()).ok(); + let webui_url = + if self.webui_url.trim_end_matches('/') == WEB_ENDPOINT.trim_end_matches('/') { + self.server + .server_url() + .map(|server_url| web_endpoint_with_streaming_server(&server_url)) + .unwrap_or_else(|| self.webui_url.clone()) + } else { + self.webui_url.clone() + }; + self.webview.endpoint.set(webui_url).ok(); self.webview.dev_tools.set(self.dev_tools).ok(); if let Some(hwnd) = self.window.handle.hwnd() { if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() { diff --git a/src/stremio_app/constants.rs b/src/stremio_app/constants.rs index 27f8317..4cd6e9d 100644 --- a/src/stremio_app/constants.rs +++ b/src/stremio_app/constants.rs @@ -15,3 +15,12 @@ pub const STREMIO_SERVER_DEV_MODE: &str = "STREMIO_SERVER_DEV_MODE"; pub const SRV_BUFFER_SIZE: usize = 1024; pub const SERVER_IPC_KEY: &str = "SERVER_IPC_KEY"; pub const SRV_LOG_SIZE: usize = 20; + +pub fn web_endpoint_with_streaming_server(server_url: &str) -> String { + let server_url = server_url.trim_end_matches('/'); + let streaming_server_url = + url::form_urlencoded::byte_serialize(server_url.as_bytes()).collect::(); + let web_endpoint = WEB_ENDPOINT.trim_end_matches('/'); + + format!("{web_endpoint}/#/?streamingServerUrl={streaming_server_url}") +} diff --git a/src/stremio_app/gpu_video_processing.rs b/src/stremio_app/gpu_video_processing.rs new file mode 100644 index 0000000..057468b --- /dev/null +++ b/src/stremio_app/gpu_video_processing.rs @@ -0,0 +1,193 @@ +use once_cell::sync::Lazy; +use std::{ + mem, + ptr::{self, NonNull}, +}; +use winapi::{ + ctypes::c_void, + shared::{dxgitype::DXGI_RATIONAL, guiddef::GUID, minwindef::UINT, winerror::SUCCEEDED}, + um::{ + d3d11::{ + D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11VideoContext, + ID3D11VideoDevice, ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_SDK_VERSION, + D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, D3D11_VIDEO_PROCESSOR_CONTENT_DESC, + D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, + }, + d3dcommon::D3D_DRIVER_TYPE_HARDWARE, + unknwnbase::IUnknown, + }, + Interface, +}; + +const NVIDIA_PPE_INTERFACE_GUID: GUID = GUID { + Data1: 0xd43ce1b3, + Data2: 0x1f4b, + Data3: 0x48ac, + Data4: [0xba, 0xee, 0xc3, 0xc2, 0x53, 0x75, 0xe6, 0xf7], +}; + +const NVIDIA_TRUE_HDR_INTERFACE_GUID: GUID = GUID { + Data1: 0xfdd62bb4, + Data2: 0x620b, + Data3: 0x4fd7, + Data4: [0x9a, 0xb3, 0x1e, 0x59, 0xd0, 0xd5, 0x44, 0xb3], +}; + +static GPU_VIDEO_PROCESSING_SUPPORTED: Lazy = + Lazy::new(detect_gpu_video_processing_supported); + +#[repr(C)] +struct NvidiaRtxSuperResolutionExtension { + version: u32, + method: u32, + enable: u32, +} + +struct ComPtr { + ptr: NonNull, +} + +impl ComPtr { + unsafe fn from_raw(ptr: *mut T) -> Option { + NonNull::new(ptr).map(|ptr| Self { ptr }) + } + + fn as_ptr(&self) -> *mut T { + self.ptr.as_ptr() + } +} + +impl Drop for ComPtr { + fn drop(&mut self) { + unsafe { + (*(self.ptr.as_ptr() as *mut IUnknown)).Release(); + } + } +} + +pub fn gpu_video_processing_supported() -> bool { + *GPU_VIDEO_PROCESSING_SUPPORTED +} + +fn detect_gpu_video_processing_supported() -> bool { + unsafe { detect_gpu_video_processing_supported_inner() } +} + +unsafe fn detect_gpu_video_processing_supported_inner() -> bool { + let mut device = ptr::null_mut::(); + let mut device_context = ptr::null_mut::(); + if !SUCCEEDED(D3D11CreateDevice( + ptr::null_mut(), + D3D_DRIVER_TYPE_HARDWARE, + ptr::null_mut(), + D3D11_CREATE_DEVICE_VIDEO_SUPPORT as UINT, + ptr::null(), + 0, + D3D11_SDK_VERSION, + &mut device, + ptr::null_mut(), + &mut device_context, + )) { + return false; + } + + let Some(device) = ComPtr::from_raw(device) else { + return false; + }; + let Some(device_context) = ComPtr::from_raw(device_context) else { + return false; + }; + let Some(video_device) = query_interface::(device.as_ptr()) else { + return false; + }; + let Some(video_context) = query_interface::(device_context.as_ptr()) + else { + return false; + }; + + let frame_rate = DXGI_RATIONAL { + Numerator: 60, + Denominator: 1, + }; + let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC { + InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + InputFrameRate: frame_rate, + InputWidth: 1280, + InputHeight: 720, + OutputFrameRate: frame_rate, + OutputWidth: 1920, + OutputHeight: 1080, + Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, + }; + let mut enumerator = ptr::null_mut::(); + if !SUCCEEDED( + (*video_device.as_ptr()).CreateVideoProcessorEnumerator(&content_desc, &mut enumerator), + ) { + return false; + } + let Some(enumerator) = ComPtr::from_raw(enumerator) else { + return false; + }; + + let mut video_processor = ptr::null_mut::(); + if !SUCCEEDED((*video_device.as_ptr()).CreateVideoProcessor( + enumerator.as_ptr(), + 0, + &mut video_processor, + )) { + return false; + } + let Some(video_processor) = ComPtr::from_raw(video_processor) else { + return false; + }; + + // Keep this aligned with mpv/video/filter/vf_d3d11vpp.c; it also supports + // Intel VSR, so this probe can extend when shell enables that scaling mode. + nvidia_rtx_super_resolution_supported(&video_context, &video_processor) + || nvidia_true_hdr_supported(&video_context, &video_processor) +} + +unsafe fn query_interface(source: *mut U) -> Option> { + let mut out = ptr::null_mut::(); + let hr = (*(source as *mut IUnknown)).QueryInterface(&T::uuidof(), &mut out); + if SUCCEEDED(hr) { + ComPtr::from_raw(out as *mut T) + } else { + None + } +} + +unsafe fn nvidia_rtx_super_resolution_supported( + video_context: &ComPtr, + video_processor: &ComPtr, +) -> bool { + let mut extension = NvidiaRtxSuperResolutionExtension { + version: 1, + method: 2, + enable: 1, + }; + + SUCCEEDED((*video_context.as_ptr()).VideoProcessorSetStreamExtension( + video_processor.as_ptr(), + 0, + &NVIDIA_PPE_INTERFACE_GUID, + mem::size_of::() as UINT, + &mut extension as *mut _ as *mut c_void, + )) +} + +unsafe fn nvidia_true_hdr_supported( + video_context: &ComPtr, + video_processor: &ComPtr, +) -> bool { + let mut supported = 0u32; + + SUCCEEDED((*video_context.as_ptr()).VideoProcessorGetStreamExtension( + video_processor.as_ptr(), + 0, + &NVIDIA_TRUE_HDR_INTERFACE_GUID, + mem::size_of_val(&supported) as UINT, + &mut supported as *mut _ as *mut c_void, + )) && supported != 0 +} diff --git a/src/stremio_app/ipc.rs b/src/stremio_app/ipc.rs index 9de343b..125a9ab 100644 --- a/src/stremio_app/ipc.rs +++ b/src/stremio_app/ipc.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{self, json}; use std::cell::RefCell; +use crate::stremio_app::gpu_video_processing; + const VERSION: &str = env!("CARGO_PKG_VERSION"); pub type Channel = RefCell, flume::Receiver)>>; @@ -68,6 +70,12 @@ impl RPCResponse { "".to_string(), VERSION.to_string(), ], + vec![ + "".to_string(), + "gpuVideoProcessing".to_string(), + "".to_string(), + gpu_video_processing::gpu_video_processing_supported().to_string(), + ], ], signals: vec![], methods: vec![vec!["onEvent".to_string(), "".to_string()]], diff --git a/src/stremio_app/mod.rs b/src/stremio_app/mod.rs index d3cb534..edd960e 100644 --- a/src/stremio_app/mod.rs +++ b/src/stremio_app/mod.rs @@ -1,6 +1,7 @@ pub mod app; pub use app::MainWindow; pub mod discord; +pub mod gpu_video_processing; pub mod ipc; pub mod stremio_player; pub mod stremio_server; diff --git a/src/stremio_app/stremio_player/communication.rs b/src/stremio_app/stremio_player/communication.rs index c53ee8a..a023633 100644 --- a/src/stremio_app/stremio_player/communication.rs +++ b/src/stremio_app/stremio_player/communication.rs @@ -141,11 +141,12 @@ macro_rules! stringable { #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[serde(try_from = "String", into = "String")] #[display(style = "kebab-case")] -pub enum InMsgFn { - MpvSetProp, - MpvCommand, - MpvObserveProp, -} +pub enum InMsgFn { + MpvSetProp, + MpvCommand, + MpvObserveProp, + MpvSetGpuVideoProcessing, +} stringable!(InMsgFn); // Bool #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] @@ -194,7 +195,7 @@ stringable!(FpProp); pub enum StrProp { FfmpegVersion, Hwdec, - InputDefaultBindings, + InputDefaultBindings, InputVoKeyboard, Metadata, MpvVersion, @@ -278,6 +279,7 @@ pub enum InMsgArgs { StProp(PropKey, PropVal), Cmd(CmdVal), ObProp(PropKey), + Flag(bool), } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] diff --git a/src/stremio_app/stremio_player/communication_tests.rs b/src/stremio_app/stremio_player/communication_tests.rs index d033751..81d3fbf 100644 --- a/src/stremio_app/stremio_player/communication_tests.rs +++ b/src/stremio_app/stremio_player/communication_tests.rs @@ -165,6 +165,22 @@ fn set_propr_tokens() { ); } +#[test] +fn set_gpu_video_processing_tokens() { + assert_tokens( + &InMsg(InMsgFn::MpvSetGpuVideoProcessing, InMsgArgs::Flag(true)), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-set-gpu-video-processing"), + Token::Bool(true), + Token::TupleStructEnd, + ], + ); +} + #[test] fn command_stop_tokens() { assert_tokens( diff --git a/src/stremio_app/stremio_player/player.rs b/src/stremio_app/stremio_player/player.rs index 47e4da1..0bf8e7d 100644 --- a/src/stremio_app/stremio_player/player.rs +++ b/src/stremio_app/stremio_player/player.rs @@ -4,10 +4,30 @@ use flume::{Receiver, Sender}; use libmpv2::{events::Event, Format, Mpv, SetData}; use native_windows_gui::{self as nwg, PartialUi}; use std::{ - sync::Arc, + mem, ptr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, thread::{self, JoinHandle}, + time::Duration, +}; +use winapi::shared::{ + minwindef::{DWORD, UINT}, + windef::{HMONITOR, HWND}, + winerror::{ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS}, +}; +use winapi::um::{ + wingdi::{ + DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_HEADER, + DISPLAYCONFIG_MODE_INFO, DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_SOURCE_DEVICE_NAME, + QDC_ONLY_ACTIVE_PATHS, + }, + winnt::LONG, + winuser::{ + GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, + }, }; -use winapi::shared::windef::HWND; use crate::stremio_app::stremio_player::{ CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, @@ -19,6 +39,71 @@ struct ObserveProperty { format: Format, } +#[link(name = "user32")] +extern "system" { + fn GetDisplayConfigBufferSizes( + flags: UINT, + num_path_array_elements: *mut UINT, + num_mode_info_array_elements: *mut UINT, + ) -> LONG; + fn QueryDisplayConfig( + flags: UINT, + num_path_array_elements: *mut UINT, + path_array: *mut DISPLAYCONFIG_PATH_INFO, + num_mode_info_array_elements: *mut UINT, + mode_info_array: *mut DISPLAYCONFIG_MODE_INFO, + current_topology_id: *mut u32, + ) -> LONG; + fn DisplayConfigGetDeviceInfo(request_packet: *mut DISPLAYCONFIG_DEVICE_INFO_HEADER) -> LONG; +} + +const DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2: u32 = 15; +const DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR: u32 = 2; + +#[repr(C)] +struct DisplayconfigGetAdvancedColorInfo2 { + header: DISPLAYCONFIG_DEVICE_INFO_HEADER, + value: u32, + color_encoding: u32, + bits_per_color_channel: u32, + active_color_mode: u32, +} + +fn with_gpu_next_fallback(vo: String) -> String { + let mut outputs = vo + .split(',') + .filter(|output| !output.is_empty()) + .map(String::from) + .collect::>(); + + let has_gpu_next = outputs.iter().any(|output| output == "gpu-next"); + let has_gpu = outputs.iter().any(|output| output == "gpu"); + + if outputs.is_empty() { + outputs.push("gpu-next".to_string()); + outputs.push("gpu".to_string()); + } else if has_gpu_next && !has_gpu { + outputs.push("gpu".to_string()); + } else if has_gpu && !has_gpu_next { + outputs.push("gpu-next".to_string()); + } + + format!("{},", outputs.join(",")) +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum DisplayOutputMode { + Hdr, + Sdr, + Auto, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +struct DisplayOutputState { + mode: DisplayOutputMode, + scale_percent: u32, +} + #[derive(Default)] pub struct Player { pub channel: ipc::Channel, @@ -53,7 +138,19 @@ impl PartialUi for Player { observe_property_receiver, rpc_response_sender, ); - let _message_thread = create_message_thread(mpv, observe_property_sender, in_msg_receiver); + let gpu_video_processing = Arc::new(AtomicBool::new(false)); + let _display_thread = create_display_output_thread( + Arc::clone(&mpv), + window_handle as isize, + Arc::clone(&gpu_video_processing), + ); + let _message_thread = create_message_thread( + mpv, + window_handle as isize, + gpu_video_processing, + observe_property_sender, + in_msg_receiver, + ); // @TODO implement a mechanism to stop threads on `Player` drop if needed Ok(()) @@ -97,12 +194,240 @@ fn create_mpv(window_handle: HWND) -> Mpv { set_property!("msg-level", "all=no"); set_property!("quiet", "yes"); set_property!("hwdec", "auto"); - // set_property!("vo", "gpu-next,"); + // gpu-next: libplacebo VO with modern HDR tone-mapping; gpu, is the fallback. + set_property!("vo", "gpu-next,gpu,"); + for (name, value) in [ + ("gpu-context", "d3d11"), + ("d3d11-output-format", "auto"), + ("d3d11-output-csp", "auto"), + ("target-colorspace-hint", "auto"), + ("target-colorspace-hint-mode", "target"), + ("tone-mapping", "bt.2390"), + ("dither-depth", "auto"), + ("deband", "yes"), + ("scale", "spline36"), + ("cscale", "spline36"), + ] { + if let Err(error) = initializer.set_property(name, value) { + eprintln!("mpv: cannot set {name}={value}: {error:?}"); + } + } Ok(()) }); mpv.expect("cannot build MPV") } +fn create_display_output_thread( + mpv: Arc, + window_handle: isize, + gpu_video_processing: Arc, +) -> JoinHandle<()> { + thread::spawn(move || { + let mut last_state = None; + + loop { + let state = current_display_output_state(&mpv, window_handle as HWND); + let gpu = gpu_video_processing.load(Ordering::Relaxed); + if last_state != Some((state, gpu)) { + apply_display_output_mode(&mpv, state, gpu); + last_state = Some((state, gpu)); + } + thread::sleep(Duration::from_millis(500)); + } + }) +} + +fn current_display_output_state(mpv: &Mpv, window_handle: HWND) -> DisplayOutputState { + DisplayOutputState { + mode: current_display_output_mode(window_handle), + scale_percent: current_video_filter_scale(mpv, window_handle), + } +} + +fn current_display_output_mode(window_handle: HWND) -> DisplayOutputMode { + let monitor = unsafe { MonitorFromWindow(window_handle, MONITOR_DEFAULTTONEAREST) }; + match monitor_hdr_active(monitor) { + Some(true) => DisplayOutputMode::Hdr, + Some(false) => DisplayOutputMode::Sdr, + None => DisplayOutputMode::Auto, + } +} + +fn current_video_filter_scale(mpv: &Mpv, window_handle: HWND) -> u32 { + let Some(video_height) = current_video_height(mpv) else { + return 100; + }; + let Some(display_height) = current_monitor_height(window_handle) else { + return 100; + }; + if video_height <= 0.0 || display_height <= video_height { + return 100; + } + + ((display_height / video_height).min(4.0) * 100.0).round() as u32 +} + +fn current_video_height(mpv: &Mpv) -> Option { + let video_params = mpv.get_property::("video-params").ok()?; + let video_params = serde_json::from_str::(&video_params).ok()?; + video_params.get("h").and_then(serde_json::Value::as_f64) +} + +fn current_monitor_height(window_handle: HWND) -> Option { + let monitor = unsafe { MonitorFromWindow(window_handle, MONITOR_DEFAULTTONEAREST) }; + if monitor.is_null() { + return None; + } + + let mut monitor_info: MONITORINFO = unsafe { mem::zeroed() }; + monitor_info.cbSize = mem::size_of::() as DWORD; + if unsafe { GetMonitorInfoW(monitor, &mut monitor_info) } == 0 { + return None; + } + + Some((monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top) as f64) +} + +fn apply_display_output_mode(mpv: &Mpv, state: DisplayOutputState, gpu_video_processing: bool) { + // Target colorspace follows the display so native HDR still passes through; the RTX + // HDR filter is the opt-in part and only makes sense on an HDR display. + let vf = if gpu_video_processing { + let scale = state.scale_percent as f64 / 100.0; + let mut vf = format!("d3d11vpp=scaling-mode=nvidia:scale={scale:.2}"); + if state.mode == DisplayOutputMode::Hdr { + vf.push_str(":format=x2bgr10:nvidia-true-hdr"); + } + vf + } else { + String::new() + }; + let color = match state.mode { + DisplayOutputMode::Hdr | DisplayOutputMode::Auto => [ + ("d3d11-output-csp", "auto"), + ("target-colorspace-hint", "auto"), + ("target-trc", "auto"), + ("target-prim", "auto"), + ], + DisplayOutputMode::Sdr => [ + ("d3d11-output-csp", "srgb"), + ("target-colorspace-hint", "yes"), + ("target-trc", "srgb"), + ("target-prim", "bt.709"), + ], + }; + + for (name, value) in std::iter::once(("vf", vf.as_str())).chain(color) { + if let Err(error) = mpv.set_property(name, value) { + eprintln!("mpv: cannot set {name}={value}: {error:?}"); + } + } +} + +fn monitor_hdr_active(monitor: HMONITOR) -> Option { + if monitor.is_null() { + return None; + } + + let device_name = monitor_device_name(monitor)?; + for path in active_display_paths()? { + let Some(source_name) = display_source_name(&path) else { + continue; + }; + if source_name.viewGdiDeviceName != device_name { + continue; + } + + return display_hdr_active(&path); + } + + None +} + +fn monitor_device_name(monitor: HMONITOR) -> Option<[u16; 32]> { + let mut monitor_info: MONITORINFOEXW = unsafe { mem::zeroed() }; + monitor_info.cbSize = mem::size_of::() as DWORD; + + let result = + unsafe { GetMonitorInfoW(monitor, &mut monitor_info as *mut _ as *mut MONITORINFO) }; + if result == 0 { + None + } else { + Some(monitor_info.szDevice) + } +} + +fn active_display_paths() -> Option> { + for _ in 0..3 { + let mut path_count = 0; + let mut mode_count = 0; + let buffer_status = unsafe { + GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut path_count, &mut mode_count) + }; + if buffer_status != ERROR_SUCCESS as LONG { + return None; + } + + let mut paths = + vec![unsafe { mem::zeroed::() }; path_count as usize]; + let mut modes = + vec![unsafe { mem::zeroed::() }; mode_count as usize]; + let query_status = unsafe { + QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + &mut path_count, + paths.as_mut_ptr(), + &mut mode_count, + modes.as_mut_ptr(), + ptr::null_mut(), + ) + }; + + if query_status == ERROR_SUCCESS as LONG { + paths.truncate(path_count as usize); + return Some(paths); + } + if query_status != ERROR_INSUFFICIENT_BUFFER as LONG { + return None; + } + } + + None +} + +fn display_source_name(path: &DISPLAYCONFIG_PATH_INFO) -> Option { + let mut source_name: DISPLAYCONFIG_SOURCE_DEVICE_NAME = unsafe { mem::zeroed() }; + source_name.header = DISPLAYCONFIG_DEVICE_INFO_HEADER { + _type: DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, + size: mem::size_of::() as u32, + adapterId: path.sourceInfo.adapterId, + id: path.sourceInfo.id, + }; + + let status = unsafe { DisplayConfigGetDeviceInfo(&mut source_name.header) }; + if status == ERROR_SUCCESS as LONG { + Some(source_name) + } else { + None + } +} + +fn display_hdr_active(path: &DISPLAYCONFIG_PATH_INFO) -> Option { + let mut color_info: DisplayconfigGetAdvancedColorInfo2 = unsafe { mem::zeroed() }; + color_info.header = DISPLAYCONFIG_DEVICE_INFO_HEADER { + _type: DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2, + size: mem::size_of::() as u32, + adapterId: path.targetInfo.adapterId, + id: path.targetInfo.id, + }; + + let status = unsafe { DisplayConfigGetDeviceInfo(&mut color_info.header) }; + if status == ERROR_SUCCESS as LONG { + Some(color_info.active_color_mode == DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR) + } else { + None + } +} + fn create_event_thread( mut mpv_event_client: Mpv, observe_property_receiver: Receiver, @@ -163,6 +488,8 @@ fn create_event_thread( fn create_message_thread( mpv: Arc, + window_handle: isize, + gpu_video_processing: Arc, observe_property_sender: Sender, in_msg_receiver: Receiver, ) -> JoinHandle<()> { @@ -228,17 +555,29 @@ fn create_message_thread( set_property(name, value, &mpv); } InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Str(value))) => { - let value = if name.to_string() == "vo" { - let mut value = value; - if !value.is_empty() && !value.ends_with(',') { - value.push(','); - } - value.push_str("gpu-next,"); - value + let is_vo = name.to_string() == "vo"; + let value = if is_vo { + with_gpu_next_fallback(value) } else { value }; set_property(name, value, &mpv); + // vo reinit reverts color props to defaults; re-assert for the current display. + if is_vo { + apply_display_output_mode( + &mpv, + current_display_output_state(&mpv, window_handle as HWND), + gpu_video_processing.load(Ordering::Relaxed), + ); + } + } + InMsg(InMsgFn::MpvSetGpuVideoProcessing, InMsgArgs::Flag(enabled)) => { + gpu_video_processing.store(enabled, Ordering::Relaxed); + apply_display_output_mode( + &mpv, + current_display_output_state(&mpv, window_handle as HWND), + enabled, + ); } InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => { send_command(cmd); diff --git a/src/stremio_app/stremio_server/server.rs b/src/stremio_app/stremio_server/server.rs index 15ae361..ef781af 100644 --- a/src/stremio_app/stremio_server/server.rs +++ b/src/stremio_app/stremio_server/server.rs @@ -27,12 +27,17 @@ pub struct StremioServer { parent: nwg::ControlHandle, crash_notice: nwg::Notice, logs: Arc>, + server_url: Arc>>, } impl StremioServer { - pub fn start(&self) { + pub fn server_url(&self) -> Option { + self.server_url.lock().ok().and_then(|url| url.clone()) + } + + pub fn start(&self) -> Option { if self.development { - return; + return None; } let (tx, rx) = flume::unbounded(); let logs = self.logs.clone(); @@ -173,8 +178,12 @@ impl StremioServer { sender.notice(); }); - // Wait for the server to start - rx.recv().unwrap(); + // Wait for the server to start and keep the exact endpoint printed by server.js. + let server_url = rx.recv().unwrap(); + if let Ok(mut stored_url) = self.server_url.lock() { + *stored_url = Some(server_url.clone()); + } + Some(server_url) } } @@ -193,7 +202,7 @@ impl PartialUi for StremioServer { .parent(data.parent) .build(&mut data.crash_notice) .ok(); - data.start(); + let _ = data.start(); println!("Stremio server started"); Ok(()) } @@ -210,7 +219,7 @@ impl PartialUi for StremioServer { "Stremio server crash log", self.logs.lock().unwrap().deref(), ); - self.start(); + let _ = self.start(); } } } diff --git a/upload.ps1 b/upload.ps1 index 70d870a..8483192 100644 --- a/upload.ps1 +++ b/upload.ps1 @@ -1,4 +1,4 @@ -$tag = $(git describe --abbrev=0) +$tag = $(git describe --tags --abbrev=0) foreach ($installer in (get-item .\StremioSetup*.exe)) { if ($tag.StartsWith("v$($installer.VersionInfo.ProductVersion.Trim())")) {