diff --git a/Cargo.lock b/Cargo.lock index 52efa76..84e6c1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,15 +118,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "enum_primitive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" -dependencies = [ - "num-traits 0.1.43", -] - [[package]] name = "flume" version = "0.10.9" @@ -140,12 +131,6 @@ dependencies = [ "spin", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures-core" version = "0.3.17" @@ -212,9 +197,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] name = "libm" @@ -222,6 +207,21 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" +[[package]] +name = "libmpv" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a58e2d19b34775e81e0fdca194b3b8ee8de973b092e7582b416343979e22e7" +dependencies = [ + "libmpv-sys", +] + +[[package]] +name = "libmpv-sys" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df938d3145cd8f134572721a27afa3a51f9bc1c26ae30a1d5077162f96d074b" + [[package]] name = "lock_api" version = "0.4.5" @@ -231,15 +231,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -dependencies = [ - "log 0.4.14", -] - [[package]] name = "log" version = "0.4.14" @@ -255,16 +246,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" -[[package]] -name = "mpv" -version = "0.2.3" -source = "git+https://github.com/Stremio/mpv-rs.git#3ef09f637803711f474edfe2c73a9f7dd35067e2" -dependencies = [ - "enum_primitive", - "log 0.3.9", - "num", -] - [[package]] name = "muldiv" version = "0.2.1" @@ -308,84 +289,6 @@ dependencies = [ "winapi-build", ] -[[package]] -name = "num" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits 0.2.14", -] - -[[package]] -name = "num-bigint" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" -dependencies = [ - "num-integer", - "num-traits 0.2.14", - "rand", - "rustc-serialize", -] - -[[package]] -name = "num-complex" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" -dependencies = [ - "num-traits 0.2.14", - "rustc-serialize", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits 0.2.14", -] - -[[package]] -name = "num-iter" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" -dependencies = [ - "autocfg", - "num-integer", - "num-traits 0.2.14", -] - -[[package]] -name = "num-rational" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits 0.2.14", - "rustc-serialize", -] - -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -dependencies = [ - "num-traits 0.2.14", -] - [[package]] name = "num-traits" version = "0.2.14" @@ -469,7 +372,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" dependencies = [ - "num-traits 0.2.14", + "num-traits", "plotters-backend", "wasm-bindgen", "web-sys", @@ -532,43 +435,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "regex" version = "1.5.4" @@ -586,12 +452,6 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "ryu" version = "1.0.5" @@ -660,7 +520,8 @@ dependencies = [ "bitflags", "embed-resource", "flume", - "mpv", + "libmpv", + "libmpv-sys", "native-windows-derive", "native-windows-gui", "once_cell", @@ -869,7 +730,7 @@ checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" dependencies = [ "bumpalo", "lazy_static", - "log 0.4.14", + "log", "proc-macro2", "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index a978baa..3be9d65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,8 @@ winapi = { version = "0.3.9", features = [ ] } webview2 = "0.1.0" webview2-sys = "0.1.0-beta.1" -mpv = { git = "https://github.com/Stremio/mpv-rs.git" } +libmpv = "2.0.1" +libmpv-sys = "3.1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" structopt = "0.3" @@ -26,4 +27,4 @@ flume = "0.10.9" [build-dependencies] embed-resource = "1.3" [dev-dependencies] -serde_test = "1.0.*" \ No newline at end of file +serde_test = "1.0.*" diff --git a/build.rs b/build.rs index 344b707..588147f 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -extern crate embed_resource; -fn main() { - embed_resource::compile("resources.rc"); -} \ No newline at end of file +extern crate embed_resource; +fn main() { + embed_resource::compile("resources.rc"); +} diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index c704730..b447b63 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -100,7 +100,10 @@ impl MainWindow { let web_rx = web_rx.clone(); // Read message from player thread::spawn(move || loop { - player_rx.iter().map(|msg| web_tx_player.send(msg)).for_each(drop); + player_rx + .iter() + .map(|msg| web_tx_player.send(msg)) + .for_each(drop); }); // thread let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); @@ -129,7 +132,7 @@ impl MainWindow { hide_splash_sender.notice(); if let Some(arg) = msg.get_params() { // TODO: Make this modal dialog - eprintln!("Web App Error: {}", arg.to_string()); + eprintln!("Web App Error: {}", arg); } } Some("open-external") => { @@ -210,5 +213,6 @@ impl MainWindow { self.window.set_visible(false); self.tray.tray_show_hide.set_checked(self.window.visible()); self.transmit_window_full_screen_change(false); + nwg::stop_thread_dispatch(); } } diff --git a/src/stremio_app/ipc.rs b/src/stremio_app/ipc.rs index 91cb511..ea197d7 100644 --- a/src/stremio_app/ipc.rs +++ b/src/stremio_app/ipc.rs @@ -21,13 +21,9 @@ impl RPCRequest { .and_then(|arg| arg.as_str()) } pub fn get_params(&self) -> Option<&serde_json::Value> { - self.args.as_ref().and_then(|args| { - if args.len() > 1 { - Some(&args[1]) - } else { - None - } - }) + self.args + .as_ref() + .and_then(|args| if args.len() > 1 { Some(&args[1]) } else { None }) } } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/src/stremio_app/stremio_player/communication.rs b/src/stremio_app/stremio_player/communication.rs index 106fb1d..ed08da8 100644 --- a/src/stremio_app/stremio_player/communication.rs +++ b/src/stremio_app/stremio_player/communication.rs @@ -1,4 +1,5 @@ use core::convert::TryFrom; +use libmpv::{events::PropertyData, mpv_end_file_reason, EndFileReason}; use parse_display::{Display, FromStr}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -12,26 +13,27 @@ pub struct PlayerProprChange { data: serde_json::Value, } impl PlayerProprChange { - fn value_from_format(data: mpv::Format, as_json: bool) -> serde_json::Value { + fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { match data { - mpv::Format::Flag(d) => serde_json::Value::Bool(d), - mpv::Format::Int(d) => serde_json::Value::Number( + PropertyData::Flag(d) => serde_json::Value::Bool(d), + PropertyData::Int64(d) => serde_json::Value::Number( serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"), ), - mpv::Format::Double(d) => serde_json::Value::Number( + PropertyData::Double(d) => serde_json::Value::Number( serde_json::Number::from_f64(d).expect("MPV returned invalid number"), ), - mpv::Format::OsdStr(s) => serde_json::Value::String(s.to_string()), - mpv::Format::Str(s) => { + PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()), + PropertyData::Str(s) => { if as_json { serde_json::from_str(s).expect("MPV returned invalid JSON data") } else { serde_json::Value::String(s.to_string()) } } + PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"), } } - pub fn from_name_value(name: String, value: mpv::Format) -> Self { + pub fn from_name_value(name: String, value: PropertyData) -> Self { let is_json = JSON_RESPONSES.contains(&name.as_str()); Self { name, @@ -44,14 +46,14 @@ pub struct PlayerEnded { reason: String, } impl PlayerEnded { - fn string_from_end_reason(data: mpv::EndFileReason) -> String { + fn string_from_end_reason(data: EndFileReason) -> String { match data { - mpv::EndFileReason::MPV_END_FILE_REASON_ERROR => "error".to_string(), - mpv::EndFileReason::MPV_END_FILE_REASON_QUIT => "quit".to_string(), + mpv_end_file_reason::Error => "error".to_string(), + mpv_end_file_reason::Quit => "quit".to_string(), _ => "other".to_string(), } } - pub fn from_end_reason(data: mpv::EndFileReason) -> Self { + pub fn from_end_reason(data: EndFileReason) -> Self { Self { reason: Self::string_from_end_reason(data), } diff --git a/src/stremio_app/stremio_player/communication_tests.rs b/src/stremio_app/stremio_player/communication_tests.rs index 855e939..afb7726 100644 --- a/src/stremio_app/stremio_player/communication_tests.rs +++ b/src/stremio_app/stremio_player/communication_tests.rs @@ -2,6 +2,7 @@ use crate::stremio_app::stremio_player::communication::{ BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey, PropVal, }; +use libmpv::{events::PropertyData, mpv_end_file_reason}; use serde_test::{assert_tokens, Token}; @@ -20,7 +21,7 @@ fn propr_change_tokens() { Token::StructEnd, ]; - fn tokens_by_type(tokens: &[Token; 6], name: &'static str, val: mpv::Format, token: Token) { + fn tokens_by_type(tokens: &[Token; 6], name: &'static str, val: PropertyData, token: Token) { let mut typed_tokens = tokens.clone(); typed_tokens[2] = Token::Str(name); typed_tokens[4] = token; @@ -29,29 +30,29 @@ fn propr_change_tokens() { &typed_tokens, ); } - tokens_by_type(&tokens, prop, mpv::Format::Flag(true), Token::Bool(true)); - tokens_by_type(&tokens, prop, mpv::Format::Int(1), Token::F64(1.0)); - tokens_by_type(&tokens, prop, mpv::Format::Double(1.0), Token::F64(1.0)); - tokens_by_type(&tokens, prop, mpv::Format::OsdStr("ok"), Token::Str("ok")); - tokens_by_type(&tokens, prop, mpv::Format::Str("ok"), Token::Str("ok")); + tokens_by_type(&tokens, prop, PropertyData::Flag(true), Token::Bool(true)); + tokens_by_type(&tokens, prop, PropertyData::Int64(1), Token::F64(1.0)); + tokens_by_type(&tokens, prop, PropertyData::Double(1.0), Token::F64(1.0)); + tokens_by_type(&tokens, prop, PropertyData::OsdStr("ok"), Token::Str("ok")); + tokens_by_type(&tokens, prop, PropertyData::Str("ok"), Token::Str("ok")); // JSON response tokens_by_type( &tokens, "track-list", - mpv::Format::Str(r#""ok""#), + PropertyData::Str(r#""ok""#), Token::Str("ok"), ); tokens_by_type( &tokens, "video-params", - mpv::Format::Str(r#""ok""#), + PropertyData::Str(r#""ok""#), Token::Str("ok"), ); tokens_by_type( &tokens, "metadata", - mpv::Format::Str(r#""ok""#), + PropertyData::Str(r#""ok""#), Token::Str("ok"), ); } @@ -70,13 +71,13 @@ fn ended_tokens() { let mut typed_tokens = tokens.clone(); typed_tokens[2] = Token::Str("error"); assert_tokens( - &PlayerEnded::from_end_reason(mpv::EndFileReason::MPV_END_FILE_REASON_ERROR), + &PlayerEnded::from_end_reason(mpv_end_file_reason::Error), &typed_tokens, ); let mut typed_tokens = tokens.clone(); typed_tokens[2] = Token::Str("quit"); assert_tokens( - &PlayerEnded::from_end_reason(mpv::EndFileReason::MPV_END_FILE_REASON_QUIT), + &PlayerEnded::from_end_reason(mpv_end_file_reason::Quit), &typed_tokens, ); } diff --git a/src/stremio_app/stremio_player/player.rs b/src/stremio_app/stremio_player/player.rs index b477add..7d3dd45 100644 --- a/src/stremio_app/stremio_player/player.rs +++ b/src/stremio_app/stremio_player/player.rs @@ -1,14 +1,24 @@ use crate::stremio_app::ipc; use crate::stremio_app::RPCResponse; +use flume::{Receiver, Sender}; +use libmpv::{events::Event, Format, Mpv, SetData}; use native_windows_gui::{self as nwg, PartialUi}; -use std::cell::RefCell; -use std::thread; +use std::{ + sync::Arc, + thread::{self, JoinHandle}, +}; +use winapi::shared::windef::HWND; use crate::stremio_app::stremio_player::{ - InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, + CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, PropKey, PropVal, }; +struct ObserveProperty { + name: String, + format: Format, +} + #[derive(Default)] pub struct Player { pub channel: ipc::Channel, @@ -16,147 +26,206 @@ pub struct Player { impl PartialUi for Player { fn build_partial>( + // @TODO replace with `&mut self`? data: &mut Self, parent: Option, ) -> Result<(), nwg::NwgError> { - let (tx, rx) = flume::unbounded(); - let (tx1, rx1) = flume::unbounded(); - data.channel = RefCell::new(Some((tx, rx1))); - let hwnd = parent - .expect("No parent window") + // @TODO replace all `expect`s with proper error handling? + + let window_handle = parent + .expect("no parent window") .into() .hwnd() - .expect("Cannot obtain window handle") as i64; + .expect("cannot obtain window handle"); - thread::spawn(move || { - let mut mpv_builder = - mpv::MpvHandlerBuilder::new().expect("Error while creating MPV builder"); - mpv_builder - .set_option("wid", hwnd) - .expect("failed setting wid"); - // mpv_builder.set_option("vo", "gpu").expect("unable to set vo"); - // win, opengl: works but least performancy, 10-15% CPU - // winvk, vulkan: works as good as d3d11 - // d3d11, d1d11: works great - // dxinterop, auto: works, slightly more cpu use than d3d11 - // default (auto) seems to be d3d11 (vo/gpu/d3d11) - mpv_builder - .set_option("gpu-context", "angle") - .and_then(|_| mpv_builder.set_option("gpu-api", "auto")) - .expect("setting gpu options failed"); - mpv_builder - .try_hardware_decoding() - .expect("failed setting hwdec"); - mpv_builder - .set_option("title", "Stremio") - .expect("failed setting title"); - mpv_builder - .set_option("terminal", "yes") - .expect("failed setting terminal"); - mpv_builder - .set_option("msg-level", "all=no,cplayer=debug") - .expect("failed setting msg-level"); - mpv_builder - .set_option("quiet", "yes") - .expect("failed setting msg-level"); - let mut mpv_built = mpv_builder.build().expect("Cannot build MPV"); + let (in_msg_sender, in_msg_receiver) = flume::unbounded(); + let (rpc_response_sender, rpc_response_receiver) = flume::unbounded(); + let (observe_property_sender, observe_property_receiver) = flume::unbounded(); + data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver))); - // FIXME: very often the audio track isn't selected when using "aid" = "auto" - mpv_built.set_property("aid", 1).ok(); + let mpv = create_shareable_mpv(window_handle); - let mut mpv = mpv_built.clone(); - let event_thread = thread::spawn(move || { - // -1.0 means to block and wait for an event. - while let Some(event) = mpv.wait_event(-1.0) { - if mpv.raw().is_null() { - return; - } + let _event_thread = create_event_thread( + Arc::clone(&mpv), + observe_property_receiver, + rpc_response_sender, + ); + let _message_thread = create_message_thread(mpv, observe_property_sender, in_msg_receiver); + // @TODO implement a mechanism to stop threads on `Player` drop if needed - // even if you don't do anything with the events, it is still necessary to empty - // the event loop - let resp_event = match event { - mpv::Event::PropertyChange { - name, - change, - reply_userdata: _, - } => PlayerResponse( - "mpv-prop-change", - PlayerEvent::PropChange(PlayerProprChange::from_name_value( - name.to_string(), - change, - )), - ) - .to_value(), - mpv::Event::EndFile(Ok(reason)) => PlayerResponse( - "mpv-event-ended", - PlayerEvent::End(PlayerEnded::from_end_reason(reason)), - ) - .to_value(), - mpv::Event::Shutdown => { - break; - } - _ => None, - }; - if resp_event.is_some() { - tx1.send(RPCResponse::response_message(resp_event)).ok(); - } - } // event drain loop - }); // event thread - - let mut mpv = mpv_built.clone(); - let message_thread = thread::spawn(move || { - for msg in rx.iter() { - if mpv.raw().is_null() { - return; - } - match serde_json::from_str::(msg.as_str()) { - Ok(InMsg( - InMsgFn::MpvObserveProp, - InMsgArgs::ObProp(PropKey::Bool(prop)), - )) => mpv.observe_property::(prop.to_string().as_str(), 0), - Ok(InMsg( - InMsgFn::MpvObserveProp, - InMsgArgs::ObProp(PropKey::Int(prop)), - )) => mpv.observe_property::(prop.to_string().as_str(), 0), - Ok(InMsg( - InMsgFn::MpvObserveProp, - InMsgArgs::ObProp(PropKey::Fp(prop)), - )) => mpv.observe_property::(prop.to_string().as_str(), 0), - Ok(InMsg( - InMsgFn::MpvObserveProp, - InMsgArgs::ObProp(PropKey::Str(prop)), - )) => mpv.observe_property::<&str>(prop.to_string().as_str(), 0), - Ok(InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(prop, PropVal::Bool(val)), - )) => mpv.set_property(prop.to_string().as_str(), val), - Ok(InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(prop, PropVal::Num(val)), - )) => mpv.set_property(prop.to_string().as_str(), val), - Ok(InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(prop, PropVal::Str(val)), - )) => mpv.set_property(prop.to_string().as_str(), val.as_str()), - Ok(InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd))) => { - let cmd: Vec = cmd.into(); - mpv.command(&cmd.iter().map(|s| s.as_str()).collect::>()) - } - _ => { - eprintln!("MPV unsupported message {}", msg); - Ok(()) - } - } - .ok(); - } // incoming message drain loop - }); // message thread - - // If we don't join our communication threads - // the `mpv_built` gets dropped and we have - // "use after free" errors which is very bad - event_thread.join().ok(); - message_thread.join().ok(); - }); // builder thread Ok(()) } } + +fn create_shareable_mpv(window_handle: HWND) -> Arc { + let mpv = Mpv::with_initializer(|initializer| { + macro_rules! set_property { + ($name:literal, $value:expr) => { + initializer + .set_property($name, $value) + .expect(concat!("failed to set ", $name)); + }; + } + set_property!("wid", window_handle as i64); + // initializer.set_property("vo", "gpu").expect("unable to set vo"); + // win, opengl: works but least performancy, 10-15% CPU + // winvk, vulkan: works as good as d3d11 + // d3d11, d1d11: works great + // dxinterop, auto: works, slightly more cpu use than d3d11 + // default (auto) seems to be d3d11 (vo/gpu/d3d11) + set_property!("gpu-context", "angle"); + set_property!("gpu-api", "auto"); + set_property!("title", "Stremio"); + set_property!("terminal", "yes"); + set_property!("msg-level", "all=no,cplayer=debug"); + set_property!("quiet", "yes"); + set_property!("hwdec", "auto"); + // FIXME: very often the audio track isn't selected when using "aid" = "auto" + set_property!("aid", "1"); + Ok(()) + }); + Arc::new(mpv.expect("cannot build MPV")) +} + +fn create_event_thread( + mpv: Arc, + observe_property_receiver: Receiver, + rpc_response_sender: Sender, +) -> JoinHandle<()> { + thread::spawn(move || { + let mut event_context = mpv.create_event_context(); + event_context + .disable_deprecated_events() + .expect("failed to disable deprecated MPV events"); + + // -- Event handler loop -- + + loop { + for ObserveProperty { name, format } in observe_property_receiver.drain() { + event_context + .observe_property(&name, format, 0) + .expect("failed to observer MPV property"); + } + + // -1.0 means to block and wait for an event. + let event = match event_context.wait_event(-1.) { + Some(Ok(event)) => event, + Some(Err(error)) => { + eprintln!("Event errored: {error:?}"); + continue; + } + // dummy event received (may be created on a wake up call or on timeout) + None => continue, + }; + + // even if you don't do anything with the events, it is still necessary to empty the event loop + let player_response = match event { + Event::PropertyChange { name, change, .. } => PlayerResponse( + "mpv-prop-change", + PlayerEvent::PropChange(PlayerProprChange::from_name_value( + name.to_string(), + change, + )), + ), + Event::EndFile(reason) => PlayerResponse( + "mpv-event-ended", + PlayerEvent::End(PlayerEnded::from_end_reason(reason)), + ), + Event::Shutdown => { + break; + } + _ => continue, + }; + + rpc_response_sender + .send(RPCResponse::response_message(player_response.to_value())) + .expect("failed to send RPCResponse"); + } + }) +} + +fn create_message_thread( + mpv: Arc, + observe_property_sender: Sender, + in_msg_receiver: Receiver, +) -> JoinHandle<()> { + thread::spawn(move || { + // -- Helpers -- + + let observe_property = |name: String, format: Format| { + observe_property_sender + .send(ObserveProperty { name, format }) + .expect("cannot send ObserveProperty"); + mpv.wake_up(); + }; + + let send_command = |cmd: CmdVal| { + let (name, arg) = match cmd { + CmdVal::Double(name, arg) => (name, format!(r#""{arg}""#)), + CmdVal::Single((name,)) => (name, String::new()), + }; + if let Err(error) = mpv.command(&name.to_string(), &[&arg]) { + eprintln!("failed to execute MPV command: '{error:#}'") + } + }; + + fn set_property(name: impl ToString, value: impl SetData, mpv: &Mpv) { + if let Err(error) = mpv.set_property(&name.to_string(), value) { + eprintln!("cannot set MPV property: '{error:#}'") + } + } + + // -- InMsg handler loop -- + + for msg in in_msg_receiver.iter() { + let in_msg: InMsg = match serde_json::from_str(&msg) { + Ok(in_msg) => in_msg, + Err(error) => { + eprintln!("cannot parse InMsg: {error:#}"); + continue; + } + }; + + match in_msg { + InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Bool(prop))) => { + observe_property(prop.to_string(), Format::Flag); + } + InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Int(prop))) => { + observe_property(prop.to_string(), Format::Int64); + } + InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Fp(prop))) => { + observe_property(prop.to_string(), Format::Double); + } + InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Str(prop))) => { + observe_property(prop.to_string(), Format::String); + } + InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Bool(value))) => { + set_property(name, value, &mpv); + } + InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Num(value))) => { + set_property(name, value, &mpv); + } + InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Str(value))) => { + set_property(name, value, &mpv); + } + InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => { + send_command(cmd); + } + msg => { + eprintln!("MPV unsupported message: '{msg:?}'"); + } + } + } + }) +} + +trait MpvExt { + fn wake_up(&self); +} + +impl MpvExt for Mpv { + // @TODO create a PR to the `libmpv` crate and then remove `libmpv-sys` from Cargo.toml? + fn wake_up(&self) { + unsafe { libmpv_sys::mpv_wakeup(self.ctx.as_ptr()) } + } +} diff --git a/src/stremio_app/stremio_server/server.rs b/src/stremio_app/stremio_server/server.rs index bb84d66..32eb871 100644 --- a/src/stremio_app/stremio_server/server.rs +++ b/src/stremio_app/stremio_server/server.rs @@ -1,8 +1,8 @@ +use std::os::windows::process::CommandExt; use std::process::Command; use std::thread; use std::time::Duration; use win32job::Job; -use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000;