mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-08-09 00:27:24 +00:00
Merge branch 'main' of https://github.com/Stremio/stremio-shell-ng into feat/streamurl-cache
This commit is contained in:
commit
bc22074aee
12 changed files with 623 additions and 28 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1624,7 +1624,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "stremio-shell-ng"
|
||||
version = "5.0.22"
|
||||
version = "5.0.23"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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::<String>();
|
||||
let web_endpoint = WEB_ENDPOINT.trim_end_matches('/');
|
||||
|
||||
format!("{web_endpoint}/#/?streamingServerUrl={streaming_server_url}")
|
||||
}
|
||||
|
|
|
|||
193
src/stremio_app/gpu_video_processing.rs
Normal file
193
src/stremio_app/gpu_video_processing.rs
Normal file
|
|
@ -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<bool> =
|
||||
Lazy::new(detect_gpu_video_processing_supported);
|
||||
|
||||
#[repr(C)]
|
||||
struct NvidiaRtxSuperResolutionExtension {
|
||||
version: u32,
|
||||
method: u32,
|
||||
enable: u32,
|
||||
}
|
||||
|
||||
struct ComPtr<T> {
|
||||
ptr: NonNull<T>,
|
||||
}
|
||||
|
||||
impl<T> ComPtr<T> {
|
||||
unsafe fn from_raw(ptr: *mut T) -> Option<Self> {
|
||||
NonNull::new(ptr).map(|ptr| Self { ptr })
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *mut T {
|
||||
self.ptr.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for ComPtr<T> {
|
||||
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::<ID3D11Device>();
|
||||
let mut device_context = ptr::null_mut::<ID3D11DeviceContext>();
|
||||
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::<ID3D11VideoDevice, _>(device.as_ptr()) else {
|
||||
return false;
|
||||
};
|
||||
let Some(video_context) = query_interface::<ID3D11VideoContext, _>(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::<ID3D11VideoProcessorEnumerator>();
|
||||
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::<ID3D11VideoProcessor>();
|
||||
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<T: Interface, U>(source: *mut U) -> Option<ComPtr<T>> {
|
||||
let mut out = ptr::null_mut::<c_void>();
|
||||
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<ID3D11VideoContext>,
|
||||
video_processor: &ComPtr<ID3D11VideoProcessor>,
|
||||
) -> 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::<NvidiaRtxSuperResolutionExtension>() as UINT,
|
||||
&mut extension as *mut _ as *mut c_void,
|
||||
))
|
||||
}
|
||||
|
||||
unsafe fn nvidia_true_hdr_supported(
|
||||
video_context: &ComPtr<ID3D11VideoContext>,
|
||||
video_processor: &ComPtr<ID3D11VideoProcessor>,
|
||||
) -> 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
|
||||
}
|
||||
|
|
@ -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<Option<(flume::Sender<String>, flume::Receiver<String>)>>;
|
||||
|
|
@ -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()]],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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::<Vec<String>>();
|
||||
|
||||
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<Mpv>,
|
||||
window_handle: isize,
|
||||
gpu_video_processing: Arc<AtomicBool>,
|
||||
) -> 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<f64> {
|
||||
let video_params = mpv.get_property::<String>("video-params").ok()?;
|
||||
let video_params = serde_json::from_str::<serde_json::Value>(&video_params).ok()?;
|
||||
video_params.get("h").and_then(serde_json::Value::as_f64)
|
||||
}
|
||||
|
||||
fn current_monitor_height(window_handle: HWND) -> Option<f64> {
|
||||
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::<MONITORINFO>() 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<bool> {
|
||||
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::<MONITORINFOEXW>() 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<Vec<DISPLAYCONFIG_PATH_INFO>> {
|
||||
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::<DISPLAYCONFIG_PATH_INFO>() }; path_count as usize];
|
||||
let mut modes =
|
||||
vec![unsafe { mem::zeroed::<DISPLAYCONFIG_MODE_INFO>() }; 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<DISPLAYCONFIG_SOURCE_DEVICE_NAME> {
|
||||
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::<DISPLAYCONFIG_SOURCE_DEVICE_NAME>() 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<bool> {
|
||||
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::<DisplayconfigGetAdvancedColorInfo2>() 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<ObserveProperty>,
|
||||
|
|
@ -163,6 +488,8 @@ fn create_event_thread(
|
|||
|
||||
fn create_message_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
window_handle: isize,
|
||||
gpu_video_processing: Arc<AtomicBool>,
|
||||
observe_property_sender: Sender<ObserveProperty>,
|
||||
in_msg_receiver: Receiver<String>,
|
||||
) -> 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);
|
||||
|
|
|
|||
|
|
@ -27,12 +27,17 @@ pub struct StremioServer {
|
|||
parent: nwg::ControlHandle,
|
||||
crash_notice: nwg::Notice,
|
||||
logs: Arc<Mutex<String>>,
|
||||
server_url: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl StremioServer {
|
||||
pub fn start(&self) {
|
||||
pub fn server_url(&self) -> Option<String> {
|
||||
self.server_url.lock().ok().and_then(|url| url.clone())
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Option<String> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())")) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue