From 1f63cefd0371ea7623dbc4124377fe60cac3ebcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Fri, 1 Apr 2022 13:25:27 +0200 Subject: [PATCH 1/7] graceful shutdown on quit --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- src/stremio_app/app.rs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52efa76..ced3ed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,9 +212,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" diff --git a/Cargo.toml b/Cargo.toml index a978baa..14dd1a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,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/src/stremio_app/app.rs b/src/stremio_app/app.rs index c704730..05f1a21 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -210,5 +210,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(); } } From ce55fa1fe1a8c5434164a4ac89ca0d2a38bafba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Fri, 1 Apr 2022 15:19:07 +0200 Subject: [PATCH 2/7] libmpv integration --- Cargo.lock | 177 +-------- Cargo.toml | 3 +- .../stremio_player/communication.rs | 24 +- src/stremio_app/stremio_player/player.rs | 347 +++++++++++------- 4 files changed, 244 insertions(+), 307 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ced3ed9..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" @@ -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 14dd1a6..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" diff --git a/src/stremio_app/stremio_player/communication.rs b/src/stremio_app/stremio_player/communication.rs index 106fb1d..4f06bdb 100644 --- a/src/stremio_app/stremio_player/communication.rs +++ b/src/stremio_app/stremio_player/communication.rs @@ -2,6 +2,7 @@ use core::convert::TryFrom; use parse_display::{Display, FromStr}; use serde::{Deserialize, Serialize}; use std::fmt; +use libmpv::{events::PropertyData, EndFileReason, mpv_end_file_reason}; // Responses const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; @@ -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/player.rs b/src/stremio_app/stremio_player/player.rs index b477add..7e548c5 100644 --- a/src/stremio_app/stremio_player/player.rs +++ b/src/stremio_app/stremio_player/player.rs @@ -1,14 +1,21 @@ use crate::stremio_app::ipc; use crate::stremio_app::RPCResponse; +use flume::{Receiver, Sender}; +use libmpv::{Mpv, events::Event, Format, SetData}; use native_windows_gui::{self as nwg, PartialUi}; -use std::cell::RefCell; -use std::thread; +use winapi::shared::windef::HWND; +use std::{thread::{self, JoinHandle}, sync::Arc}; use crate::stremio_app::stremio_player::{ InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, - PropKey, PropVal, + PropKey, PropVal, CmdVal, }; +struct ObserveProperty { + name: String, + format: Format, +} + #[derive(Default)] pub struct Player { pub channel: ipc::Channel, @@ -16,147 +23,213 @@ 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") + let (in_msg_sender, in_msg_receiver) = flume::unbounded(); + let (rpc_response_sender, rpc_response_receiver) = flume::unbounded(); + + data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver))); + + let window_handle = parent + .expect("no parent window") .into() .hwnd() - .expect("Cannot obtain window handle") as i64; + .expect("cannot obtain window handle"); + // @TODO replace all `expect`s with proper error handling? - 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 mpv = create_shareable_mpv(window_handle); + let (observe_property_sender, observe_property_receiver) = flume::unbounded(); - // FIXME: very often the audio track isn't selected when using "aid" = "auto" - mpv_built.set_property("aid", 1).ok(); - - 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; - } - - // 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 + 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 + Ok(()) } } + +fn create_shareable_mpv(window_handle: HWND) -> Arc { + let mpv = Mpv::with_initializer(|initializer| { + initializer.set_property("wid", window_handle as i64).expect("failed setting wid"); + // 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) + initializer.set_property("gpu-context", "angle").expect("failed setting gpu-contex"); + initializer.set_property("gpu-api", "auto").expect("failed setting gpu-api"); + initializer.set_property("title", "Stremio").expect("failed setting title"); + initializer.set_property("terminal", "yes").expect("failed setting terminal"); + initializer.set_property("msg-level", "all=no,cplayer=debug").expect("failed setting msg-level"); + initializer.set_property("quiet", "yes").expect("failed setting quiet"); + initializer.set_property("hwdec", "auto").expect("failed setting hwdec"); + // FIXME: very often the audio track isn't selected when using "aid" = "auto" + initializer.set_property("aid", 1).expect("failed setting aid"); + Ok(()) + }).expect("cannot build MPV"); + + Arc::new(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"); + + 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 resp_event = match event { + Event::PropertyChange { + name, + change, + .. + } => PlayerResponse( + "mpv-prop-change", + PlayerEvent::PropChange(PlayerProprChange::from_name_value( + name.to_string(), + change, + )), + ) + .to_value(), + Event::EndFile(reason) => PlayerResponse( + "mpv-event-ended", + PlayerEvent::End(PlayerEnded::from_end_reason(reason)), + ) + .to_value(), + Event::Shutdown => { + break; + } + _ => None, + }; + if resp_event.is_some() { + rpc_response_sender.send(RPCResponse::response_message(resp_event)).ok(); + } + } + }) +} + +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()) + }; + mpv.command(&name.to_string(), &[&arg]).expect("failed to execute MPV command"); + }; + + 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(prop, PropVal::Bool(value)), + ) => { + set_property(prop, value, &mpv); + } + InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(prop, PropVal::Num(value)), + ) => { + set_property(prop, value, &mpv); + } + InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(prop, PropVal::Str(value)), + ) => { + set_property(prop, 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()) } + } +} From ccbe52525992759c9424de7903229df8dc113f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Fri, 1 Apr 2022 22:39:20 +0200 Subject: [PATCH 3/7] minor refactor + cargo fmt --all --- build.rs | 8 +- src/stremio_app/app.rs | 433 +++++++-------- src/stremio_app/ipc.rs | 204 ++++--- .../stremio_player/communication.rs | 504 +++++++++--------- src/stremio_app/stremio_player/player.rs | 481 +++++++++-------- src/stremio_app/stremio_server/server.rs | 64 +-- 6 files changed, 852 insertions(+), 842 deletions(-) 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 05f1a21..26db97f 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -1,215 +1,218 @@ -use native_windows_derive::NwgUi; -use native_windows_gui as nwg; -use serde_json; -use std::cell::RefCell; -use std::thread; -use winapi::um::winuser::WS_EX_TOPMOST; - -use crate::stremio_app::ipc::{RPCRequest, RPCResponse}; -use crate::stremio_app::splash::SplashImage; -use crate::stremio_app::stremio_player::Player; -use crate::stremio_app::stremio_wevbiew::WebView; -use crate::stremio_app::systray::SystemTray; -use crate::stremio_app::window_helper::WindowStyle; - -#[derive(Default, NwgUi)] -pub struct MainWindow { - pub webui_url: String, - pub dev_tools: bool, - pub saved_window_style: RefCell, - #[nwg_resource] - pub embed: nwg::EmbedResource, - #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] - pub window_icon: nwg::Icon, - #[nwg_control(icon: Some(&data.window_icon), title: "Stremio", flags: "MAIN_WINDOW|VISIBLE")] - #[nwg_events( OnWindowClose: [Self::on_quit(SELF, EVT_DATA)], OnInit: [Self::on_init], OnPaint: [Self::on_paint], OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)], OnWindowMaximize: [Self::transmit_window_state_change], OnWindowMinimize: [Self::transmit_window_state_change] )] - pub window: nwg::Window, - #[nwg_partial(parent: window)] - #[nwg_events((tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost]) ] - pub tray: SystemTray, - #[nwg_partial(parent: window)] - pub webview: WebView, - #[nwg_partial(parent: window)] - pub player: Player, - #[nwg_partial(parent: window)] - pub splash_screen: SplashImage, - #[nwg_control] - #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] - pub toggle_fullscreen_notice: nwg::Notice, - #[nwg_control] - #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] - pub quit_notice: nwg::Notice, - #[nwg_control] - #[nwg_events(OnNotice: [Self::on_hide_splash_notice] )] - pub hide_splash_notice: nwg::Notice, -} - -impl MainWindow { - const MIN_WIDTH: i32 = 1000; - const MIN_HEIGHT: i32 = 600; - fn transmit_window_full_screen_change(&self, prevent_close: bool) { - let web_channel = self.webview.channel.borrow(); - let (web_tx, _) = web_channel - .as_ref() - .expect("Cannont obtain communication channel for the Web UI"); - let web_tx_app = web_tx.clone(); - let saved_style = self.saved_window_style.borrow(); - web_tx_app - .send(RPCResponse::visibility_change( - self.window.visible(), - prevent_close as u32, - saved_style.full_screen, - )) - .ok(); - } - fn transmit_window_state_change(&self) { - if let Some(hwnd) = self.window.handle.hwnd() { - let web_channel = self.webview.channel.borrow(); - let (web_tx, _) = web_channel - .as_ref() - .expect("Cannont obtain communication channel for the Web UI"); - let web_tx_app = web_tx.clone(); - let style = self.saved_window_style.borrow(); - let state = style.clone().get_window_state(hwnd); - web_tx_app.send(RPCResponse::state_change(state)).ok(); - } - } - fn on_init(&self) { - self.webview.endpoint.set(self.webui_url.clone()).ok(); - self.webview.dev_tools.set(self.dev_tools).ok(); - if let Some(hwnd) = self.window.handle.hwnd() { - let mut saved_style = self.saved_window_style.borrow_mut(); - saved_style.center_window(hwnd, Self::MIN_WIDTH, Self::MIN_HEIGHT); - } - - self.tray.tray_show_hide.set_checked(true); - - let player_channel = self.player.channel.borrow(); - let (player_tx, player_rx) = player_channel - .as_ref() - .expect("Cannont obtain communication channel for the Player"); - let player_tx = player_tx.clone(); - let player_rx = player_rx.clone(); - - let web_channel = self.webview.channel.borrow(); - let (web_tx, web_rx) = web_channel - .as_ref() - .expect("Cannont obtain communication channel for the Web UI"); - let web_tx_player = web_tx.clone(); - let web_tx_web = web_tx.clone(); - 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); - }); // thread - - let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); - let quit_sender = self.quit_notice.sender(); - let hide_splash_sender = self.hide_splash_notice.sender(); - thread::spawn(move || loop { - if let Some(msg) = web_rx - .recv() - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - { - match msg.get_method() { - // The handshake. Here we send some useful data to the WEB UI - None if msg.is_handshake() => { - web_tx_web.send(RPCResponse::get_handshake()).ok(); - } - Some("win-set-visibility") => toggle_fullscreen_sender.notice(), - Some("quit") => quit_sender.notice(), - Some("app-ready") => { - hide_splash_sender.notice(); - web_tx_web - .send(RPCResponse::visibility_change(true, 1, false)) - .ok(); - } - Some("app-error") => { - hide_splash_sender.notice(); - if let Some(arg) = msg.get_params() { - // TODO: Make this modal dialog - eprintln!("Web App Error: {}", arg.to_string()); - } - } - Some("open-external") => { - if let Some(arg) = msg.get_params() { - // FIXME: THIS IS NOT SAFE BY ANY MEANS - // open::that("calc").ok(); does exactly that - let arg = arg.as_str().unwrap_or(""); - let arg_lc = arg.to_lowercase(); - if arg_lc.starts_with("http://") - || arg_lc.starts_with("https://") - || arg_lc.starts_with("rtp://") - || arg_lc.starts_with("rtps://") - || arg_lc.starts_with("ftp://") - || arg_lc.starts_with("ipfs://") - { - open::that(arg).ok(); - } - } - } - Some(player_command) if player_command.starts_with("mpv-") => { - let resp_json = serde_json::to_string( - &msg.args.expect("Cannot have method without args"), - ) - .expect("Cannot build response"); - player_tx.send(resp_json).ok(); - } - Some(unknown) => { - eprintln!("Unsupported command {}({:?})", unknown, msg.get_params()) - } - None => {} - } - } // recv - }); // thread - } - fn on_min_max(&self, data: &nwg::EventData) { - let data = data.on_min_max(); - data.set_min_size(Self::MIN_WIDTH, Self::MIN_HEIGHT); - } - fn on_paint(&self) { - if self.splash_screen.visible() { - self.splash_screen.resize(self.window.size()); - } else { - self.transmit_window_state_change(); - } - } - fn on_toggle_fullscreen_notice(&self) { - if let Some(hwnd) = self.window.handle.hwnd() { - let mut saved_style = self.saved_window_style.borrow_mut(); - saved_style.toggle_full_screen(hwnd); - self.tray.tray_topmost.set_enabled(!saved_style.full_screen); - self.tray - .tray_topmost - .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); - } - self.transmit_window_full_screen_change(true); - } - fn on_hide_splash_notice(&self) { - self.splash_screen.hide(); - } - fn on_toggle_topmost(&self) { - if let Some(hwnd) = self.window.handle.hwnd() { - let mut saved_style = self.saved_window_style.borrow_mut(); - saved_style.toggle_topmost(hwnd); - self.tray - .tray_topmost - .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); - } - } - fn on_show_hide(&self) { - self.window.set_visible(!self.window.visible()); - self.tray.tray_show_hide.set_checked(self.window.visible()); - self.transmit_window_state_change(); - } - fn on_quit(&self, data: &nwg::EventData) { - if let nwg::EventData::OnWindowClose(data) = data { - data.close(false); - } - 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(); - } -} +use native_windows_derive::NwgUi; +use native_windows_gui as nwg; +use serde_json; +use std::cell::RefCell; +use std::thread; +use winapi::um::winuser::WS_EX_TOPMOST; + +use crate::stremio_app::ipc::{RPCRequest, RPCResponse}; +use crate::stremio_app::splash::SplashImage; +use crate::stremio_app::stremio_player::Player; +use crate::stremio_app::stremio_wevbiew::WebView; +use crate::stremio_app::systray::SystemTray; +use crate::stremio_app::window_helper::WindowStyle; + +#[derive(Default, NwgUi)] +pub struct MainWindow { + pub webui_url: String, + pub dev_tools: bool, + pub saved_window_style: RefCell, + #[nwg_resource] + pub embed: nwg::EmbedResource, + #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] + pub window_icon: nwg::Icon, + #[nwg_control(icon: Some(&data.window_icon), title: "Stremio", flags: "MAIN_WINDOW|VISIBLE")] + #[nwg_events( OnWindowClose: [Self::on_quit(SELF, EVT_DATA)], OnInit: [Self::on_init], OnPaint: [Self::on_paint], OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)], OnWindowMaximize: [Self::transmit_window_state_change], OnWindowMinimize: [Self::transmit_window_state_change] )] + pub window: nwg::Window, + #[nwg_partial(parent: window)] + #[nwg_events((tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost]) ] + pub tray: SystemTray, + #[nwg_partial(parent: window)] + pub webview: WebView, + #[nwg_partial(parent: window)] + pub player: Player, + #[nwg_partial(parent: window)] + pub splash_screen: SplashImage, + #[nwg_control] + #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] + pub toggle_fullscreen_notice: nwg::Notice, + #[nwg_control] + #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] + pub quit_notice: nwg::Notice, + #[nwg_control] + #[nwg_events(OnNotice: [Self::on_hide_splash_notice] )] + pub hide_splash_notice: nwg::Notice, +} + +impl MainWindow { + const MIN_WIDTH: i32 = 1000; + const MIN_HEIGHT: i32 = 600; + fn transmit_window_full_screen_change(&self, prevent_close: bool) { + let web_channel = self.webview.channel.borrow(); + let (web_tx, _) = web_channel + .as_ref() + .expect("Cannont obtain communication channel for the Web UI"); + let web_tx_app = web_tx.clone(); + let saved_style = self.saved_window_style.borrow(); + web_tx_app + .send(RPCResponse::visibility_change( + self.window.visible(), + prevent_close as u32, + saved_style.full_screen, + )) + .ok(); + } + fn transmit_window_state_change(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + let web_channel = self.webview.channel.borrow(); + let (web_tx, _) = web_channel + .as_ref() + .expect("Cannont obtain communication channel for the Web UI"); + let web_tx_app = web_tx.clone(); + let style = self.saved_window_style.borrow(); + let state = style.clone().get_window_state(hwnd); + web_tx_app.send(RPCResponse::state_change(state)).ok(); + } + } + fn on_init(&self) { + self.webview.endpoint.set(self.webui_url.clone()).ok(); + self.webview.dev_tools.set(self.dev_tools).ok(); + if let Some(hwnd) = self.window.handle.hwnd() { + let mut saved_style = self.saved_window_style.borrow_mut(); + saved_style.center_window(hwnd, Self::MIN_WIDTH, Self::MIN_HEIGHT); + } + + self.tray.tray_show_hide.set_checked(true); + + let player_channel = self.player.channel.borrow(); + let (player_tx, player_rx) = player_channel + .as_ref() + .expect("Cannont obtain communication channel for the Player"); + let player_tx = player_tx.clone(); + let player_rx = player_rx.clone(); + + let web_channel = self.webview.channel.borrow(); + let (web_tx, web_rx) = web_channel + .as_ref() + .expect("Cannont obtain communication channel for the Web UI"); + let web_tx_player = web_tx.clone(); + let web_tx_web = web_tx.clone(); + 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); + }); // thread + + let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); + let quit_sender = self.quit_notice.sender(); + let hide_splash_sender = self.hide_splash_notice.sender(); + thread::spawn(move || loop { + if let Some(msg) = web_rx + .recv() + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + { + match msg.get_method() { + // The handshake. Here we send some useful data to the WEB UI + None if msg.is_handshake() => { + web_tx_web.send(RPCResponse::get_handshake()).ok(); + } + Some("win-set-visibility") => toggle_fullscreen_sender.notice(), + Some("quit") => quit_sender.notice(), + Some("app-ready") => { + hide_splash_sender.notice(); + web_tx_web + .send(RPCResponse::visibility_change(true, 1, false)) + .ok(); + } + Some("app-error") => { + hide_splash_sender.notice(); + if let Some(arg) = msg.get_params() { + // TODO: Make this modal dialog + eprintln!("Web App Error: {}", arg.to_string()); + } + } + Some("open-external") => { + if let Some(arg) = msg.get_params() { + // FIXME: THIS IS NOT SAFE BY ANY MEANS + // open::that("calc").ok(); does exactly that + let arg = arg.as_str().unwrap_or(""); + let arg_lc = arg.to_lowercase(); + if arg_lc.starts_with("http://") + || arg_lc.starts_with("https://") + || arg_lc.starts_with("rtp://") + || arg_lc.starts_with("rtps://") + || arg_lc.starts_with("ftp://") + || arg_lc.starts_with("ipfs://") + { + open::that(arg).ok(); + } + } + } + Some(player_command) if player_command.starts_with("mpv-") => { + let resp_json = serde_json::to_string( + &msg.args.expect("Cannot have method without args"), + ) + .expect("Cannot build response"); + player_tx.send(resp_json).ok(); + } + Some(unknown) => { + eprintln!("Unsupported command {}({:?})", unknown, msg.get_params()) + } + None => {} + } + } // recv + }); // thread + } + fn on_min_max(&self, data: &nwg::EventData) { + let data = data.on_min_max(); + data.set_min_size(Self::MIN_WIDTH, Self::MIN_HEIGHT); + } + fn on_paint(&self) { + if self.splash_screen.visible() { + self.splash_screen.resize(self.window.size()); + } else { + self.transmit_window_state_change(); + } + } + fn on_toggle_fullscreen_notice(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + let mut saved_style = self.saved_window_style.borrow_mut(); + saved_style.toggle_full_screen(hwnd); + self.tray.tray_topmost.set_enabled(!saved_style.full_screen); + self.tray + .tray_topmost + .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); + } + self.transmit_window_full_screen_change(true); + } + fn on_hide_splash_notice(&self) { + self.splash_screen.hide(); + } + fn on_toggle_topmost(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + let mut saved_style = self.saved_window_style.borrow_mut(); + saved_style.toggle_topmost(hwnd); + self.tray + .tray_topmost + .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); + } + } + fn on_show_hide(&self) { + self.window.set_visible(!self.window.visible()); + self.tray.tray_show_hide.set_checked(self.window.visible()); + self.transmit_window_state_change(); + } + fn on_quit(&self, data: &nwg::EventData) { + if let nwg::EventData::OnWindowClose(data) = data { + data.close(false); + } + 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..af7c833 100644 --- a/src/stremio_app/ipc.rs +++ b/src/stremio_app/ipc.rs @@ -1,104 +1,100 @@ -use serde::{Deserialize, Serialize}; -use serde_json::{self, json}; -use std::cell::RefCell; - -pub type Channel = RefCell, flume::Receiver)>>; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RPCRequest { - pub id: u64, - pub args: Option>, -} - -impl RPCRequest { - pub fn is_handshake(&self) -> bool { - self.id == 0 - } - pub fn get_method(&self) -> Option<&str> { - self.args - .as_ref() - .and_then(|args| args.first()) - .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 - } - }) - } -} -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RPCResponseDataTransport { - pub properties: Vec>, - pub signals: Vec, - pub methods: Vec>, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RPCResponseData { - pub transport: RPCResponseDataTransport, -} - -#[derive(Default, Serialize, Deserialize, Debug, Clone)] -pub struct RPCResponse { - pub id: u64, - pub object: String, - #[serde(rename = "type")] - pub response_type: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub args: Option, -} - -impl RPCResponse { - pub fn get_handshake() -> String { - let resp = RPCResponse { - id: 0, - object: "transport".to_string(), - response_type: 3, - data: Some(RPCResponseData { - transport: RPCResponseDataTransport { - properties: vec![ - vec![], - vec![ - "".to_string(), - "shellVersion".to_string(), - "".to_string(), - "5.0.0".to_string(), - ], - ], - signals: vec![], - methods: vec![vec!["onEvent".to_string(), "".to_string()]], - }, - }), - ..Default::default() - }; - serde_json::to_string(&resp).expect("Cannot build response") - } - pub fn response_message(msg: Option) -> String { - let resp = RPCResponse { - id: 1, - object: "transport".to_string(), - response_type: 1, - args: msg, - ..Default::default() - }; - serde_json::to_string(&resp).expect("Cannot build response") - } - pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String { - Self::response_message(Some(json!(["win-visibility-changed" ,{ - "visible": visible, - "visibility": visibility, - "isFullscreen": is_full_screen - }]))) - } - pub fn state_change(state: u32) -> String { - Self::response_message(Some(json!(["win-state-changed" ,{ - "state": state, - }]))) - } -} +use serde::{Deserialize, Serialize}; +use serde_json::{self, json}; +use std::cell::RefCell; + +pub type Channel = RefCell, flume::Receiver)>>; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RPCRequest { + pub id: u64, + pub args: Option>, +} + +impl RPCRequest { + pub fn is_handshake(&self) -> bool { + self.id == 0 + } + pub fn get_method(&self) -> Option<&str> { + self.args + .as_ref() + .and_then(|args| args.first()) + .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 }) + } +} +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RPCResponseDataTransport { + pub properties: Vec>, + pub signals: Vec, + pub methods: Vec>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RPCResponseData { + pub transport: RPCResponseDataTransport, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +pub struct RPCResponse { + pub id: u64, + pub object: String, + #[serde(rename = "type")] + pub response_type: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub args: Option, +} + +impl RPCResponse { + pub fn get_handshake() -> String { + let resp = RPCResponse { + id: 0, + object: "transport".to_string(), + response_type: 3, + data: Some(RPCResponseData { + transport: RPCResponseDataTransport { + properties: vec![ + vec![], + vec![ + "".to_string(), + "shellVersion".to_string(), + "".to_string(), + "5.0.0".to_string(), + ], + ], + signals: vec![], + methods: vec![vec!["onEvent".to_string(), "".to_string()]], + }, + }), + ..Default::default() + }; + serde_json::to_string(&resp).expect("Cannot build response") + } + pub fn response_message(msg: Option) -> String { + let resp = RPCResponse { + id: 1, + object: "transport".to_string(), + response_type: 1, + args: msg, + ..Default::default() + }; + serde_json::to_string(&resp).expect("Cannot build response") + } + pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String { + Self::response_message(Some(json!(["win-visibility-changed" ,{ + "visible": visible, + "visibility": visibility, + "isFullscreen": is_full_screen + }]))) + } + pub fn state_change(state: u32) -> String { + Self::response_message(Some(json!(["win-state-changed" ,{ + "state": state, + }]))) + } +} diff --git a/src/stremio_app/stremio_player/communication.rs b/src/stremio_app/stremio_player/communication.rs index 4f06bdb..72815db 100644 --- a/src/stremio_app/stremio_player/communication.rs +++ b/src/stremio_app/stremio_player/communication.rs @@ -1,252 +1,252 @@ -use core::convert::TryFrom; -use parse_display::{Display, FromStr}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use libmpv::{events::PropertyData, EndFileReason, mpv_end_file_reason}; - -// Responses -const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct PlayerProprChange { - name: String, - data: serde_json::Value, -} -impl PlayerProprChange { - fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { - match data { - 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"), - ), - PropertyData::Double(d) => serde_json::Value::Number( - serde_json::Number::from_f64(d).expect("MPV returned invalid number"), - ), - 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: PropertyData) -> Self { - let is_json = JSON_RESPONSES.contains(&name.as_str()); - Self { - name, - data: Self::value_from_format(value, is_json), - } - } -} -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct PlayerEnded { - reason: String, -} -impl PlayerEnded { - fn string_from_end_reason(data: EndFileReason) -> String { - match data { - 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: EndFileReason) -> Self { - Self { - reason: Self::string_from_end_reason(data), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlayerError { - pub error: String, -} -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(untagged)] -pub enum PlayerEvent { - PropChange(PlayerProprChange), - End(PlayerEnded), - Error(PlayerError), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); -impl PlayerResponse<'_> { - pub fn to_value(&self) -> Option { - serde_json::to_value(self).ok() - } -} - -// Player incoming messages from the web UI -/* -Message general case - ["function-name", ["arguments", ...]] -The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. - -["mpv-observe-prop", "prop-name"] -["mpv-set-prop", ["prop-name", prop-val]] -["mpv-command", ["command-name"<, "arguments">]] - -All the function and property names are in kebab-case. - -MPV requires type for any prop-name when observing or setting it's value. -The type for setting is not always the same as the type for observing the prop. - -"mpv-observe-prop" function is the only one that accepts single string -instead of array of arguments - -"mpv-command" function always takes an array even if the command doesn't -have any arguments. For example this are the commands we support: - -["mpv-command", ["loadfile", "file name"]] -["mpv-command", ["stop"]] -*/ -macro_rules! stringable { - ($t:ident) => { - impl From<$t> for String { - fn from(s: $t) -> Self { - s.to_string() - } - } - impl TryFrom for $t { - type Error = parse_display::ParseError; - fn try_from(s: String) -> Result { - s.parse() - } - } - }; -} - -#[allow(clippy::enum_variant_names)] -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum InMsgFn { - MpvSetProp, - MpvCommand, - MpvObserveProp, -} -stringable!(InMsgFn); -// Bool -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum BoolProp { - Pause, - PausedForCache, - Seeking, - EofReached, -} -stringable!(BoolProp); -// Int -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum IntProp { - Aid, - Vid, - Sid, -} -stringable!(IntProp); -// Fp -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum FpProp { - TimePos, - Volume, - Duration, - SubScale, - CacheBufferingState, - SubPos, - Speed, -} -stringable!(FpProp); -// Str -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum StrProp { - FfmpegVersion, - Hwdec, - InputDefaltBindings, - InputVoKeyboard, - Metadata, - MpvVersion, - Osc, - Path, - SubAssOverride, - SubBackColor, - SubBorderColor, - SubColor, - TrackList, - VideoParams, - // Vo, -} -stringable!(StrProp); - -// Any -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum PropKey { - Bool(BoolProp), - Int(IntProp), - Fp(FpProp), - Str(StrProp), -} -impl fmt::Display for PropKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Bool(v) => write!(f, "{}", v), - Self::Int(v) => write!(f, "{}", v), - Self::Fp(v) => write!(f, "{}", v), - Self::Str(v) => write!(f, "{}", v), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum PropVal { - Bool(bool), - Str(String), - Num(f64), -} - -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -#[serde(untagged)] -pub enum MpvCmd { - Loadfile, - Stop, -} -stringable!(MpvCmd); - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum CmdVal { - Single((MpvCmd,)), - Double(MpvCmd, String), -} -impl From for Vec { - fn from(cmd: CmdVal) -> Vec { - match cmd { - CmdVal::Single(cmd) => vec![cmd.0.to_string()], - CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum InMsgArgs { - StProp(PropKey, PropVal), - Cmd(CmdVal), - ObProp(PropKey), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct InMsg(pub InMsgFn, pub InMsgArgs); +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; + +// Responses +const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct PlayerProprChange { + name: String, + data: serde_json::Value, +} +impl PlayerProprChange { + fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { + match data { + 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"), + ), + PropertyData::Double(d) => serde_json::Value::Number( + serde_json::Number::from_f64(d).expect("MPV returned invalid number"), + ), + 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: PropertyData) -> Self { + let is_json = JSON_RESPONSES.contains(&name.as_str()); + Self { + name, + data: Self::value_from_format(value, is_json), + } + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct PlayerEnded { + reason: String, +} +impl PlayerEnded { + fn string_from_end_reason(data: EndFileReason) -> String { + match data { + 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: EndFileReason) -> Self { + Self { + reason: Self::string_from_end_reason(data), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlayerError { + pub error: String, +} +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum PlayerEvent { + PropChange(PlayerProprChange), + End(PlayerEnded), + Error(PlayerError), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); +impl PlayerResponse<'_> { + pub fn to_value(&self) -> Option { + serde_json::to_value(self).ok() + } +} + +// Player incoming messages from the web UI +/* +Message general case - ["function-name", ["arguments", ...]] +The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. + +["mpv-observe-prop", "prop-name"] +["mpv-set-prop", ["prop-name", prop-val]] +["mpv-command", ["command-name"<, "arguments">]] + +All the function and property names are in kebab-case. + +MPV requires type for any prop-name when observing or setting it's value. +The type for setting is not always the same as the type for observing the prop. + +"mpv-observe-prop" function is the only one that accepts single string +instead of array of arguments + +"mpv-command" function always takes an array even if the command doesn't +have any arguments. For example this are the commands we support: + +["mpv-command", ["loadfile", "file name"]] +["mpv-command", ["stop"]] +*/ +macro_rules! stringable { + ($t:ident) => { + impl From<$t> for String { + fn from(s: $t) -> Self { + s.to_string() + } + } + impl TryFrom for $t { + type Error = parse_display::ParseError; + fn try_from(s: String) -> Result { + s.parse() + } + } + }; +} + +#[allow(clippy::enum_variant_names)] +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum InMsgFn { + MpvSetProp, + MpvCommand, + MpvObserveProp, +} +stringable!(InMsgFn); +// Bool +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum BoolProp { + Pause, + PausedForCache, + Seeking, + EofReached, +} +stringable!(BoolProp); +// Int +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum IntProp { + Aid, + Vid, + Sid, +} +stringable!(IntProp); +// Fp +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum FpProp { + TimePos, + Volume, + Duration, + SubScale, + CacheBufferingState, + SubPos, + Speed, +} +stringable!(FpProp); +// Str +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum StrProp { + FfmpegVersion, + Hwdec, + InputDefaltBindings, + InputVoKeyboard, + Metadata, + MpvVersion, + Osc, + Path, + SubAssOverride, + SubBackColor, + SubBorderColor, + SubColor, + TrackList, + VideoParams, + // Vo, +} +stringable!(StrProp); + +// Any +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum PropKey { + Bool(BoolProp), + Int(IntProp), + Fp(FpProp), + Str(StrProp), +} +impl fmt::Display for PropKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Bool(v) => write!(f, "{}", v), + Self::Int(v) => write!(f, "{}", v), + Self::Fp(v) => write!(f, "{}", v), + Self::Str(v) => write!(f, "{}", v), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum PropVal { + Bool(bool), + Str(String), + Num(f64), +} + +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +#[serde(untagged)] +pub enum MpvCmd { + Loadfile, + Stop, +} +stringable!(MpvCmd); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum CmdVal { + Single((MpvCmd,)), + Double(MpvCmd, String), +} +impl From for Vec { + fn from(cmd: CmdVal) -> Vec { + match cmd { + CmdVal::Single(cmd) => vec![cmd.0.to_string()], + CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum InMsgArgs { + StProp(PropKey, PropVal), + Cmd(CmdVal), + ObProp(PropKey), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InMsg(pub InMsgFn, pub InMsgArgs); diff --git a/src/stremio_app/stremio_player/player.rs b/src/stremio_app/stremio_player/player.rs index 7e548c5..54cfefa 100644 --- a/src/stremio_app/stremio_player/player.rs +++ b/src/stremio_app/stremio_player/player.rs @@ -1,235 +1,246 @@ -use crate::stremio_app::ipc; -use crate::stremio_app::RPCResponse; -use flume::{Receiver, Sender}; -use libmpv::{Mpv, events::Event, Format, SetData}; -use native_windows_gui::{self as nwg, PartialUi}; -use winapi::shared::windef::HWND; -use std::{thread::{self, JoinHandle}, sync::Arc}; - -use crate::stremio_app::stremio_player::{ - InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, - PropKey, PropVal, CmdVal, -}; - -struct ObserveProperty { - name: String, - format: Format, -} - -#[derive(Default)] -pub struct Player { - pub channel: ipc::Channel, -} - -impl PartialUi for Player { - fn build_partial>( - // @TODO replace with `&mut self`? - data: &mut Self, - parent: Option, - ) -> Result<(), nwg::NwgError> { - let (in_msg_sender, in_msg_receiver) = flume::unbounded(); - let (rpc_response_sender, rpc_response_receiver) = flume::unbounded(); - - data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver))); - - let window_handle = parent - .expect("no parent window") - .into() - .hwnd() - .expect("cannot obtain window handle"); - // @TODO replace all `expect`s with proper error handling? - - let mpv = create_shareable_mpv(window_handle); - let (observe_property_sender, observe_property_receiver) = flume::unbounded(); - - 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 - - Ok(()) - } -} - -fn create_shareable_mpv(window_handle: HWND) -> Arc { - let mpv = Mpv::with_initializer(|initializer| { - initializer.set_property("wid", window_handle as i64).expect("failed setting wid"); - // 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) - initializer.set_property("gpu-context", "angle").expect("failed setting gpu-contex"); - initializer.set_property("gpu-api", "auto").expect("failed setting gpu-api"); - initializer.set_property("title", "Stremio").expect("failed setting title"); - initializer.set_property("terminal", "yes").expect("failed setting terminal"); - initializer.set_property("msg-level", "all=no,cplayer=debug").expect("failed setting msg-level"); - initializer.set_property("quiet", "yes").expect("failed setting quiet"); - initializer.set_property("hwdec", "auto").expect("failed setting hwdec"); - // FIXME: very often the audio track isn't selected when using "aid" = "auto" - initializer.set_property("aid", 1).expect("failed setting aid"); - Ok(()) - }).expect("cannot build MPV"); - - Arc::new(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"); - - 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 resp_event = match event { - Event::PropertyChange { - name, - change, - .. - } => PlayerResponse( - "mpv-prop-change", - PlayerEvent::PropChange(PlayerProprChange::from_name_value( - name.to_string(), - change, - )), - ) - .to_value(), - Event::EndFile(reason) => PlayerResponse( - "mpv-event-ended", - PlayerEvent::End(PlayerEnded::from_end_reason(reason)), - ) - .to_value(), - Event::Shutdown => { - break; - } - _ => None, - }; - if resp_event.is_some() { - rpc_response_sender.send(RPCResponse::response_message(resp_event)).ok(); - } - } - }) -} - -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()) - }; - mpv.command(&name.to_string(), &[&arg]).expect("failed to execute MPV command"); - }; - - 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(prop, PropVal::Bool(value)), - ) => { - set_property(prop, value, &mpv); - } - InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(prop, PropVal::Num(value)), - ) => { - set_property(prop, value, &mpv); - } - InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(prop, PropVal::Str(value)), - ) => { - set_property(prop, 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()) } - } -} +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::{ + sync::Arc, + thread::{self, JoinHandle}, +}; +use winapi::shared::windef::HWND; + +use crate::stremio_app::stremio_player::{ + 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, +} + +impl PartialUi for Player { + fn build_partial>( + // @TODO replace with `&mut self`? + data: &mut Self, + parent: Option, + ) -> Result<(), nwg::NwgError> { + // @TODO replace all `expect`s with proper error handling? + + let window_handle = parent + .expect("no parent window") + .into() + .hwnd() + .expect("cannot obtain window handle"); + + 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))); + + let mpv = create_shareable_mpv(window_handle); + + 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 + + Ok(()) + } +} + +fn create_shareable_mpv(window_handle: HWND) -> Arc { + let mpv = Mpv::with_initializer(|initializer| { + initializer + .set_property("wid", window_handle as i64) + .expect("failed setting wid"); + // 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) + initializer + .set_property("gpu-context", "angle") + .expect("failed setting gpu-contex"); + initializer + .set_property("gpu-api", "auto") + .expect("failed setting gpu-api"); + initializer + .set_property("title", "Stremio") + .expect("failed setting title"); + initializer + .set_property("terminal", "yes") + .expect("failed setting terminal"); + initializer + .set_property("msg-level", "all=no,cplayer=debug") + .expect("failed setting msg-level"); + initializer + .set_property("quiet", "yes") + .expect("failed setting quiet"); + initializer + .set_property("hwdec", "auto") + .expect("failed setting hwdec"); + // FIXME: very often the audio track isn't selected when using "aid" = "auto" + initializer + .set_property("aid", 1) + .expect("failed setting aid"); + Ok(()) + }) + .expect("cannot build MPV"); + + Arc::new(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"); + + 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..54a8f31 100644 --- a/src/stremio_app/stremio_server/server.rs +++ b/src/stremio_app/stremio_server/server.rs @@ -1,32 +1,32 @@ -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; - -pub struct StremioServer {} - -impl StremioServer { - pub fn new() -> StremioServer { - thread::spawn(move || { - let job = Job::create().expect("Cannont create job"); - let mut info = job.query_extended_limit_info().expect("Cannont get info"); - info.limit_kill_on_job_close(); - job.set_extended_limit_info(&mut info).ok(); - job.assign_current_process().ok(); - loop { - let mut child = Command::new("node") - .arg("server.js") - .creation_flags(CREATE_NO_WINDOW) - .spawn() - .expect("Cannot run the server"); - child.wait().expect("Cannot wait for the server"); - thread::sleep(Duration::from_millis(500)); - dbg!("Trying to restart the server..."); - } - }); - StremioServer {} - } -} +use std::os::windows::process::CommandExt; +use std::process::Command; +use std::thread; +use std::time::Duration; +use win32job::Job; + +const CREATE_NO_WINDOW: u32 = 0x08000000; + +pub struct StremioServer {} + +impl StremioServer { + pub fn new() -> StremioServer { + thread::spawn(move || { + let job = Job::create().expect("Cannont create job"); + let mut info = job.query_extended_limit_info().expect("Cannont get info"); + info.limit_kill_on_job_close(); + job.set_extended_limit_info(&mut info).ok(); + job.assign_current_process().ok(); + loop { + let mut child = Command::new("node") + .arg("server.js") + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .expect("Cannot run the server"); + child.wait().expect("Cannot wait for the server"); + thread::sleep(Duration::from_millis(500)); + dbg!("Trying to restart the server..."); + } + }); + StremioServer {} + } +} From dc7e3cd29420690b80e4c086f1057dfece8427d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Fri, 1 Apr 2022 23:10:07 +0200 Subject: [PATCH 4/7] set_property! macro --- src/stremio_app/stremio_player/player.rs | 77 ++++++++++-------------- 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/src/stremio_app/stremio_player/player.rs b/src/stremio_app/stremio_player/player.rs index 54cfefa..e83695b 100644 --- a/src/stremio_app/stremio_player/player.rs +++ b/src/stremio_app/stremio_player/player.rs @@ -59,45 +59,32 @@ impl PartialUi for Player { fn create_shareable_mpv(window_handle: HWND) -> Arc { let mpv = Mpv::with_initializer(|initializer| { - initializer - .set_property("wid", window_handle as i64) - .expect("failed setting wid"); + 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) - initializer - .set_property("gpu-context", "angle") - .expect("failed setting gpu-contex"); - initializer - .set_property("gpu-api", "auto") - .expect("failed setting gpu-api"); - initializer - .set_property("title", "Stremio") - .expect("failed setting title"); - initializer - .set_property("terminal", "yes") - .expect("failed setting terminal"); - initializer - .set_property("msg-level", "all=no,cplayer=debug") - .expect("failed setting msg-level"); - initializer - .set_property("quiet", "yes") - .expect("failed setting quiet"); - initializer - .set_property("hwdec", "auto") - .expect("failed setting hwdec"); + 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" - initializer - .set_property("aid", 1) - .expect("failed setting aid"); + set_property!("aid", "1"); Ok(()) - }) - .expect("cannot build MPV"); - - Arc::new(mpv) + }); + Arc::new(mpv.expect("cannot build MPV")) } fn create_event_thread( @@ -111,6 +98,8 @@ fn create_event_thread( .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 @@ -131,21 +120,17 @@ fn create_event_thread( // 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::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; } From e15ba04454098599fab56ffe403512f9bce2e97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Fri, 1 Apr 2022 23:25:25 +0200 Subject: [PATCH 5/7] fix clippy warnings and tests --- src/stremio_app/app.rs | 2 +- .../stremio_player/communication_tests.rs | 333 +++++++++--------- 2 files changed, 168 insertions(+), 167 deletions(-) diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index 26db97f..b3dc944 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -132,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") => { diff --git a/src/stremio_app/stremio_player/communication_tests.rs b/src/stremio_app/stremio_player/communication_tests.rs index 855e939..bd893a7 100644 --- a/src/stremio_app/stremio_player/communication_tests.rs +++ b/src/stremio_app/stremio_player/communication_tests.rs @@ -1,166 +1,167 @@ -use crate::stremio_app::stremio_player::communication::{ - BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey, - PropVal, -}; - -use serde_test::{assert_tokens, Token}; - -#[test] -fn propr_change_tokens() { - let prop = "test-prop"; - let tokens: [Token; 6] = [ - Token::Struct { - name: "PlayerProprChange", - len: 2, - }, - Token::Str("name"), - Token::None, - Token::Str("data"), - Token::None, - Token::StructEnd, - ]; - - fn tokens_by_type(tokens: &[Token; 6], name: &'static str, val: mpv::Format, token: Token) { - let mut typed_tokens = tokens.clone(); - typed_tokens[2] = Token::Str(name); - typed_tokens[4] = token; - assert_tokens( - &PlayerProprChange::from_name_value(name.to_string(), val), - &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")); - - // JSON response - tokens_by_type( - &tokens, - "track-list", - mpv::Format::Str(r#""ok""#), - Token::Str("ok"), - ); - tokens_by_type( - &tokens, - "video-params", - mpv::Format::Str(r#""ok""#), - Token::Str("ok"), - ); - tokens_by_type( - &tokens, - "metadata", - mpv::Format::Str(r#""ok""#), - Token::Str("ok"), - ); -} - -#[test] -fn ended_tokens() { - let tokens: [Token; 4] = [ - Token::Struct { - name: "PlayerEnded", - len: 1, - }, - Token::Str("reason"), - Token::None, - Token::StructEnd, - ]; - 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), - &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), - &typed_tokens, - ); -} - -#[test] -fn ob_propr_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvObserveProp, - InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-observe-prop"), - Token::Str("pause"), - Token::TupleStructEnd, - ], - ); -} - -#[test] -fn set_propr_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-set-prop"), - Token::Tuple { len: 2 }, - Token::Str("pause"), - Token::Bool(true), - Token::TupleEnd, - Token::TupleStructEnd, - ], - ); -} - -#[test] -fn command_stop_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvCommand, - InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-command"), - Token::Tuple { len: 1 }, - Token::Str("stop"), - Token::TupleEnd, - Token::TupleStructEnd, - ], - ); -} - -#[test] -fn command_loadfile_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvCommand, - InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-command"), - Token::Tuple { len: 2 }, - Token::Str("loadfile"), - Token::Str("some_file"), - Token::TupleEnd, - Token::TupleStructEnd, - ], - ); -} +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}; + +#[test] +fn propr_change_tokens() { + let prop = "test-prop"; + let tokens: [Token; 6] = [ + Token::Struct { + name: "PlayerProprChange", + len: 2, + }, + Token::Str("name"), + Token::None, + Token::Str("data"), + Token::None, + Token::StructEnd, + ]; + + 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; + assert_tokens( + &PlayerProprChange::from_name_value(name.to_string(), val), + &typed_tokens, + ); + } + 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", + PropertyData::Str(r#""ok""#), + Token::Str("ok"), + ); + tokens_by_type( + &tokens, + "video-params", + PropertyData::Str(r#""ok""#), + Token::Str("ok"), + ); + tokens_by_type( + &tokens, + "metadata", + PropertyData::Str(r#""ok""#), + Token::Str("ok"), + ); +} + +#[test] +fn ended_tokens() { + let tokens: [Token; 4] = [ + Token::Struct { + name: "PlayerEnded", + len: 1, + }, + Token::Str("reason"), + Token::None, + Token::StructEnd, + ]; + let mut typed_tokens = tokens.clone(); + typed_tokens[2] = Token::Str("error"); + assert_tokens( + &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_end_file_reason::Quit), + &typed_tokens, + ); +} + +#[test] +fn ob_propr_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvObserveProp, + InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-observe-prop"), + Token::Str("pause"), + Token::TupleStructEnd, + ], + ); +} + +#[test] +fn set_propr_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-set-prop"), + Token::Tuple { len: 2 }, + Token::Str("pause"), + Token::Bool(true), + Token::TupleEnd, + Token::TupleStructEnd, + ], + ); +} + +#[test] +fn command_stop_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvCommand, + InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-command"), + Token::Tuple { len: 1 }, + Token::Str("stop"), + Token::TupleEnd, + Token::TupleStructEnd, + ], + ); +} + +#[test] +fn command_loadfile_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvCommand, + InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-command"), + Token::Tuple { len: 2 }, + Token::Str("loadfile"), + Token::Str("some_file"), + Token::TupleEnd, + Token::TupleStructEnd, + ], + ); +} From 90ad77017121e160b58967b2b95a6070c00ee232 Mon Sep 17 00:00:00 2001 From: Vladimir Borisov Date: Fri, 8 Apr 2022 09:16:46 +0300 Subject: [PATCH 6/7] Use Windows line endings for easier diff --- src/stremio_app/ipc.rs | 200 +++---- .../stremio_player/communication.rs | 504 +++++++++--------- .../stremio_player/communication_tests.rs | 334 ++++++------ src/stremio_app/stremio_player/player.rs | 462 ++++++++-------- src/stremio_app/stremio_server/server.rs | 64 +-- 5 files changed, 782 insertions(+), 782 deletions(-) diff --git a/src/stremio_app/ipc.rs b/src/stremio_app/ipc.rs index af7c833..ea197d7 100644 --- a/src/stremio_app/ipc.rs +++ b/src/stremio_app/ipc.rs @@ -1,100 +1,100 @@ -use serde::{Deserialize, Serialize}; -use serde_json::{self, json}; -use std::cell::RefCell; - -pub type Channel = RefCell, flume::Receiver)>>; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RPCRequest { - pub id: u64, - pub args: Option>, -} - -impl RPCRequest { - pub fn is_handshake(&self) -> bool { - self.id == 0 - } - pub fn get_method(&self) -> Option<&str> { - self.args - .as_ref() - .and_then(|args| args.first()) - .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 }) - } -} -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RPCResponseDataTransport { - pub properties: Vec>, - pub signals: Vec, - pub methods: Vec>, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RPCResponseData { - pub transport: RPCResponseDataTransport, -} - -#[derive(Default, Serialize, Deserialize, Debug, Clone)] -pub struct RPCResponse { - pub id: u64, - pub object: String, - #[serde(rename = "type")] - pub response_type: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub args: Option, -} - -impl RPCResponse { - pub fn get_handshake() -> String { - let resp = RPCResponse { - id: 0, - object: "transport".to_string(), - response_type: 3, - data: Some(RPCResponseData { - transport: RPCResponseDataTransport { - properties: vec![ - vec![], - vec![ - "".to_string(), - "shellVersion".to_string(), - "".to_string(), - "5.0.0".to_string(), - ], - ], - signals: vec![], - methods: vec![vec!["onEvent".to_string(), "".to_string()]], - }, - }), - ..Default::default() - }; - serde_json::to_string(&resp).expect("Cannot build response") - } - pub fn response_message(msg: Option) -> String { - let resp = RPCResponse { - id: 1, - object: "transport".to_string(), - response_type: 1, - args: msg, - ..Default::default() - }; - serde_json::to_string(&resp).expect("Cannot build response") - } - pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String { - Self::response_message(Some(json!(["win-visibility-changed" ,{ - "visible": visible, - "visibility": visibility, - "isFullscreen": is_full_screen - }]))) - } - pub fn state_change(state: u32) -> String { - Self::response_message(Some(json!(["win-state-changed" ,{ - "state": state, - }]))) - } -} +use serde::{Deserialize, Serialize}; +use serde_json::{self, json}; +use std::cell::RefCell; + +pub type Channel = RefCell, flume::Receiver)>>; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RPCRequest { + pub id: u64, + pub args: Option>, +} + +impl RPCRequest { + pub fn is_handshake(&self) -> bool { + self.id == 0 + } + pub fn get_method(&self) -> Option<&str> { + self.args + .as_ref() + .and_then(|args| args.first()) + .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 }) + } +} +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RPCResponseDataTransport { + pub properties: Vec>, + pub signals: Vec, + pub methods: Vec>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RPCResponseData { + pub transport: RPCResponseDataTransport, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +pub struct RPCResponse { + pub id: u64, + pub object: String, + #[serde(rename = "type")] + pub response_type: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub args: Option, +} + +impl RPCResponse { + pub fn get_handshake() -> String { + let resp = RPCResponse { + id: 0, + object: "transport".to_string(), + response_type: 3, + data: Some(RPCResponseData { + transport: RPCResponseDataTransport { + properties: vec![ + vec![], + vec![ + "".to_string(), + "shellVersion".to_string(), + "".to_string(), + "5.0.0".to_string(), + ], + ], + signals: vec![], + methods: vec![vec!["onEvent".to_string(), "".to_string()]], + }, + }), + ..Default::default() + }; + serde_json::to_string(&resp).expect("Cannot build response") + } + pub fn response_message(msg: Option) -> String { + let resp = RPCResponse { + id: 1, + object: "transport".to_string(), + response_type: 1, + args: msg, + ..Default::default() + }; + serde_json::to_string(&resp).expect("Cannot build response") + } + pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String { + Self::response_message(Some(json!(["win-visibility-changed" ,{ + "visible": visible, + "visibility": visibility, + "isFullscreen": is_full_screen + }]))) + } + pub fn state_change(state: u32) -> String { + Self::response_message(Some(json!(["win-state-changed" ,{ + "state": state, + }]))) + } +} diff --git a/src/stremio_app/stremio_player/communication.rs b/src/stremio_app/stremio_player/communication.rs index 72815db..ed08da8 100644 --- a/src/stremio_app/stremio_player/communication.rs +++ b/src/stremio_app/stremio_player/communication.rs @@ -1,252 +1,252 @@ -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; - -// Responses -const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct PlayerProprChange { - name: String, - data: serde_json::Value, -} -impl PlayerProprChange { - fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { - match data { - 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"), - ), - PropertyData::Double(d) => serde_json::Value::Number( - serde_json::Number::from_f64(d).expect("MPV returned invalid number"), - ), - 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: PropertyData) -> Self { - let is_json = JSON_RESPONSES.contains(&name.as_str()); - Self { - name, - data: Self::value_from_format(value, is_json), - } - } -} -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct PlayerEnded { - reason: String, -} -impl PlayerEnded { - fn string_from_end_reason(data: EndFileReason) -> String { - match data { - 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: EndFileReason) -> Self { - Self { - reason: Self::string_from_end_reason(data), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlayerError { - pub error: String, -} -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(untagged)] -pub enum PlayerEvent { - PropChange(PlayerProprChange), - End(PlayerEnded), - Error(PlayerError), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); -impl PlayerResponse<'_> { - pub fn to_value(&self) -> Option { - serde_json::to_value(self).ok() - } -} - -// Player incoming messages from the web UI -/* -Message general case - ["function-name", ["arguments", ...]] -The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. - -["mpv-observe-prop", "prop-name"] -["mpv-set-prop", ["prop-name", prop-val]] -["mpv-command", ["command-name"<, "arguments">]] - -All the function and property names are in kebab-case. - -MPV requires type for any prop-name when observing or setting it's value. -The type for setting is not always the same as the type for observing the prop. - -"mpv-observe-prop" function is the only one that accepts single string -instead of array of arguments - -"mpv-command" function always takes an array even if the command doesn't -have any arguments. For example this are the commands we support: - -["mpv-command", ["loadfile", "file name"]] -["mpv-command", ["stop"]] -*/ -macro_rules! stringable { - ($t:ident) => { - impl From<$t> for String { - fn from(s: $t) -> Self { - s.to_string() - } - } - impl TryFrom for $t { - type Error = parse_display::ParseError; - fn try_from(s: String) -> Result { - s.parse() - } - } - }; -} - -#[allow(clippy::enum_variant_names)] -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum InMsgFn { - MpvSetProp, - MpvCommand, - MpvObserveProp, -} -stringable!(InMsgFn); -// Bool -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum BoolProp { - Pause, - PausedForCache, - Seeking, - EofReached, -} -stringable!(BoolProp); -// Int -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum IntProp { - Aid, - Vid, - Sid, -} -stringable!(IntProp); -// Fp -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum FpProp { - TimePos, - Volume, - Duration, - SubScale, - CacheBufferingState, - SubPos, - Speed, -} -stringable!(FpProp); -// Str -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum StrProp { - FfmpegVersion, - Hwdec, - InputDefaltBindings, - InputVoKeyboard, - Metadata, - MpvVersion, - Osc, - Path, - SubAssOverride, - SubBackColor, - SubBorderColor, - SubColor, - TrackList, - VideoParams, - // Vo, -} -stringable!(StrProp); - -// Any -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum PropKey { - Bool(BoolProp), - Int(IntProp), - Fp(FpProp), - Str(StrProp), -} -impl fmt::Display for PropKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Bool(v) => write!(f, "{}", v), - Self::Int(v) => write!(f, "{}", v), - Self::Fp(v) => write!(f, "{}", v), - Self::Str(v) => write!(f, "{}", v), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum PropVal { - Bool(bool), - Str(String), - Num(f64), -} - -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -#[serde(untagged)] -pub enum MpvCmd { - Loadfile, - Stop, -} -stringable!(MpvCmd); - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum CmdVal { - Single((MpvCmd,)), - Double(MpvCmd, String), -} -impl From for Vec { - fn from(cmd: CmdVal) -> Vec { - match cmd { - CmdVal::Single(cmd) => vec![cmd.0.to_string()], - CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum InMsgArgs { - StProp(PropKey, PropVal), - Cmd(CmdVal), - ObProp(PropKey), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct InMsg(pub InMsgFn, pub InMsgArgs); +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; + +// Responses +const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct PlayerProprChange { + name: String, + data: serde_json::Value, +} +impl PlayerProprChange { + fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { + match data { + 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"), + ), + PropertyData::Double(d) => serde_json::Value::Number( + serde_json::Number::from_f64(d).expect("MPV returned invalid number"), + ), + 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: PropertyData) -> Self { + let is_json = JSON_RESPONSES.contains(&name.as_str()); + Self { + name, + data: Self::value_from_format(value, is_json), + } + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct PlayerEnded { + reason: String, +} +impl PlayerEnded { + fn string_from_end_reason(data: EndFileReason) -> String { + match data { + 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: EndFileReason) -> Self { + Self { + reason: Self::string_from_end_reason(data), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlayerError { + pub error: String, +} +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum PlayerEvent { + PropChange(PlayerProprChange), + End(PlayerEnded), + Error(PlayerError), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); +impl PlayerResponse<'_> { + pub fn to_value(&self) -> Option { + serde_json::to_value(self).ok() + } +} + +// Player incoming messages from the web UI +/* +Message general case - ["function-name", ["arguments", ...]] +The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. + +["mpv-observe-prop", "prop-name"] +["mpv-set-prop", ["prop-name", prop-val]] +["mpv-command", ["command-name"<, "arguments">]] + +All the function and property names are in kebab-case. + +MPV requires type for any prop-name when observing or setting it's value. +The type for setting is not always the same as the type for observing the prop. + +"mpv-observe-prop" function is the only one that accepts single string +instead of array of arguments + +"mpv-command" function always takes an array even if the command doesn't +have any arguments. For example this are the commands we support: + +["mpv-command", ["loadfile", "file name"]] +["mpv-command", ["stop"]] +*/ +macro_rules! stringable { + ($t:ident) => { + impl From<$t> for String { + fn from(s: $t) -> Self { + s.to_string() + } + } + impl TryFrom for $t { + type Error = parse_display::ParseError; + fn try_from(s: String) -> Result { + s.parse() + } + } + }; +} + +#[allow(clippy::enum_variant_names)] +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum InMsgFn { + MpvSetProp, + MpvCommand, + MpvObserveProp, +} +stringable!(InMsgFn); +// Bool +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum BoolProp { + Pause, + PausedForCache, + Seeking, + EofReached, +} +stringable!(BoolProp); +// Int +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum IntProp { + Aid, + Vid, + Sid, +} +stringable!(IntProp); +// Fp +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum FpProp { + TimePos, + Volume, + Duration, + SubScale, + CacheBufferingState, + SubPos, + Speed, +} +stringable!(FpProp); +// Str +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum StrProp { + FfmpegVersion, + Hwdec, + InputDefaltBindings, + InputVoKeyboard, + Metadata, + MpvVersion, + Osc, + Path, + SubAssOverride, + SubBackColor, + SubBorderColor, + SubColor, + TrackList, + VideoParams, + // Vo, +} +stringable!(StrProp); + +// Any +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum PropKey { + Bool(BoolProp), + Int(IntProp), + Fp(FpProp), + Str(StrProp), +} +impl fmt::Display for PropKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Bool(v) => write!(f, "{}", v), + Self::Int(v) => write!(f, "{}", v), + Self::Fp(v) => write!(f, "{}", v), + Self::Str(v) => write!(f, "{}", v), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum PropVal { + Bool(bool), + Str(String), + Num(f64), +} + +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +#[serde(untagged)] +pub enum MpvCmd { + Loadfile, + Stop, +} +stringable!(MpvCmd); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum CmdVal { + Single((MpvCmd,)), + Double(MpvCmd, String), +} +impl From for Vec { + fn from(cmd: CmdVal) -> Vec { + match cmd { + CmdVal::Single(cmd) => vec![cmd.0.to_string()], + CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum InMsgArgs { + StProp(PropKey, PropVal), + Cmd(CmdVal), + ObProp(PropKey), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InMsg(pub InMsgFn, pub InMsgArgs); diff --git a/src/stremio_app/stremio_player/communication_tests.rs b/src/stremio_app/stremio_player/communication_tests.rs index bd893a7..afb7726 100644 --- a/src/stremio_app/stremio_player/communication_tests.rs +++ b/src/stremio_app/stremio_player/communication_tests.rs @@ -1,167 +1,167 @@ -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}; - -#[test] -fn propr_change_tokens() { - let prop = "test-prop"; - let tokens: [Token; 6] = [ - Token::Struct { - name: "PlayerProprChange", - len: 2, - }, - Token::Str("name"), - Token::None, - Token::Str("data"), - Token::None, - Token::StructEnd, - ]; - - 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; - assert_tokens( - &PlayerProprChange::from_name_value(name.to_string(), val), - &typed_tokens, - ); - } - 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", - PropertyData::Str(r#""ok""#), - Token::Str("ok"), - ); - tokens_by_type( - &tokens, - "video-params", - PropertyData::Str(r#""ok""#), - Token::Str("ok"), - ); - tokens_by_type( - &tokens, - "metadata", - PropertyData::Str(r#""ok""#), - Token::Str("ok"), - ); -} - -#[test] -fn ended_tokens() { - let tokens: [Token; 4] = [ - Token::Struct { - name: "PlayerEnded", - len: 1, - }, - Token::Str("reason"), - Token::None, - Token::StructEnd, - ]; - let mut typed_tokens = tokens.clone(); - typed_tokens[2] = Token::Str("error"); - assert_tokens( - &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_end_file_reason::Quit), - &typed_tokens, - ); -} - -#[test] -fn ob_propr_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvObserveProp, - InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-observe-prop"), - Token::Str("pause"), - Token::TupleStructEnd, - ], - ); -} - -#[test] -fn set_propr_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvSetProp, - InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-set-prop"), - Token::Tuple { len: 2 }, - Token::Str("pause"), - Token::Bool(true), - Token::TupleEnd, - Token::TupleStructEnd, - ], - ); -} - -#[test] -fn command_stop_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvCommand, - InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-command"), - Token::Tuple { len: 1 }, - Token::Str("stop"), - Token::TupleEnd, - Token::TupleStructEnd, - ], - ); -} - -#[test] -fn command_loadfile_tokens() { - assert_tokens( - &InMsg( - InMsgFn::MpvCommand, - InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())), - ), - &[ - Token::TupleStruct { - name: "InMsg", - len: 2, - }, - Token::Str("mpv-command"), - Token::Tuple { len: 2 }, - Token::Str("loadfile"), - Token::Str("some_file"), - Token::TupleEnd, - Token::TupleStructEnd, - ], - ); -} +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}; + +#[test] +fn propr_change_tokens() { + let prop = "test-prop"; + let tokens: [Token; 6] = [ + Token::Struct { + name: "PlayerProprChange", + len: 2, + }, + Token::Str("name"), + Token::None, + Token::Str("data"), + Token::None, + Token::StructEnd, + ]; + + 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; + assert_tokens( + &PlayerProprChange::from_name_value(name.to_string(), val), + &typed_tokens, + ); + } + 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", + PropertyData::Str(r#""ok""#), + Token::Str("ok"), + ); + tokens_by_type( + &tokens, + "video-params", + PropertyData::Str(r#""ok""#), + Token::Str("ok"), + ); + tokens_by_type( + &tokens, + "metadata", + PropertyData::Str(r#""ok""#), + Token::Str("ok"), + ); +} + +#[test] +fn ended_tokens() { + let tokens: [Token; 4] = [ + Token::Struct { + name: "PlayerEnded", + len: 1, + }, + Token::Str("reason"), + Token::None, + Token::StructEnd, + ]; + let mut typed_tokens = tokens.clone(); + typed_tokens[2] = Token::Str("error"); + assert_tokens( + &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_end_file_reason::Quit), + &typed_tokens, + ); +} + +#[test] +fn ob_propr_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvObserveProp, + InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-observe-prop"), + Token::Str("pause"), + Token::TupleStructEnd, + ], + ); +} + +#[test] +fn set_propr_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-set-prop"), + Token::Tuple { len: 2 }, + Token::Str("pause"), + Token::Bool(true), + Token::TupleEnd, + Token::TupleStructEnd, + ], + ); +} + +#[test] +fn command_stop_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvCommand, + InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-command"), + Token::Tuple { len: 1 }, + Token::Str("stop"), + Token::TupleEnd, + Token::TupleStructEnd, + ], + ); +} + +#[test] +fn command_loadfile_tokens() { + assert_tokens( + &InMsg( + InMsgFn::MpvCommand, + InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())), + ), + &[ + Token::TupleStruct { + name: "InMsg", + len: 2, + }, + Token::Str("mpv-command"), + Token::Tuple { len: 2 }, + Token::Str("loadfile"), + Token::Str("some_file"), + Token::TupleEnd, + Token::TupleStructEnd, + ], + ); +} diff --git a/src/stremio_app/stremio_player/player.rs b/src/stremio_app/stremio_player/player.rs index e83695b..7d3dd45 100644 --- a/src/stremio_app/stremio_player/player.rs +++ b/src/stremio_app/stremio_player/player.rs @@ -1,231 +1,231 @@ -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::{ - sync::Arc, - thread::{self, JoinHandle}, -}; -use winapi::shared::windef::HWND; - -use crate::stremio_app::stremio_player::{ - 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, -} - -impl PartialUi for Player { - fn build_partial>( - // @TODO replace with `&mut self`? - data: &mut Self, - parent: Option, - ) -> Result<(), nwg::NwgError> { - // @TODO replace all `expect`s with proper error handling? - - let window_handle = parent - .expect("no parent window") - .into() - .hwnd() - .expect("cannot obtain window handle"); - - 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))); - - let mpv = create_shareable_mpv(window_handle); - - 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 - - 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()) } - } -} +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::{ + sync::Arc, + thread::{self, JoinHandle}, +}; +use winapi::shared::windef::HWND; + +use crate::stremio_app::stremio_player::{ + 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, +} + +impl PartialUi for Player { + fn build_partial>( + // @TODO replace with `&mut self`? + data: &mut Self, + parent: Option, + ) -> Result<(), nwg::NwgError> { + // @TODO replace all `expect`s with proper error handling? + + let window_handle = parent + .expect("no parent window") + .into() + .hwnd() + .expect("cannot obtain window handle"); + + 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))); + + let mpv = create_shareable_mpv(window_handle); + + 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 + + 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 54a8f31..32eb871 100644 --- a/src/stremio_app/stremio_server/server.rs +++ b/src/stremio_app/stremio_server/server.rs @@ -1,32 +1,32 @@ -use std::os::windows::process::CommandExt; -use std::process::Command; -use std::thread; -use std::time::Duration; -use win32job::Job; - -const CREATE_NO_WINDOW: u32 = 0x08000000; - -pub struct StremioServer {} - -impl StremioServer { - pub fn new() -> StremioServer { - thread::spawn(move || { - let job = Job::create().expect("Cannont create job"); - let mut info = job.query_extended_limit_info().expect("Cannont get info"); - info.limit_kill_on_job_close(); - job.set_extended_limit_info(&mut info).ok(); - job.assign_current_process().ok(); - loop { - let mut child = Command::new("node") - .arg("server.js") - .creation_flags(CREATE_NO_WINDOW) - .spawn() - .expect("Cannot run the server"); - child.wait().expect("Cannot wait for the server"); - thread::sleep(Duration::from_millis(500)); - dbg!("Trying to restart the server..."); - } - }); - StremioServer {} - } -} +use std::os::windows::process::CommandExt; +use std::process::Command; +use std::thread; +use std::time::Duration; +use win32job::Job; + +const CREATE_NO_WINDOW: u32 = 0x08000000; + +pub struct StremioServer {} + +impl StremioServer { + pub fn new() -> StremioServer { + thread::spawn(move || { + let job = Job::create().expect("Cannont create job"); + let mut info = job.query_extended_limit_info().expect("Cannont get info"); + info.limit_kill_on_job_close(); + job.set_extended_limit_info(&mut info).ok(); + job.assign_current_process().ok(); + loop { + let mut child = Command::new("node") + .arg("server.js") + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .expect("Cannot run the server"); + child.wait().expect("Cannot wait for the server"); + thread::sleep(Duration::from_millis(500)); + dbg!("Trying to restart the server..."); + } + }); + StremioServer {} + } +} From 2dcae521ac38c7035878320703c6bc44e411aed5 Mon Sep 17 00:00:00 2001 From: Vladimir Borisov Date: Fri, 8 Apr 2022 09:17:51 +0300 Subject: [PATCH 7/7] Windows line endings --- src/stremio_app/app.rs | 436 ++++++++++++++++++++--------------------- 1 file changed, 218 insertions(+), 218 deletions(-) diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index b3dc944..b447b63 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -1,218 +1,218 @@ -use native_windows_derive::NwgUi; -use native_windows_gui as nwg; -use serde_json; -use std::cell::RefCell; -use std::thread; -use winapi::um::winuser::WS_EX_TOPMOST; - -use crate::stremio_app::ipc::{RPCRequest, RPCResponse}; -use crate::stremio_app::splash::SplashImage; -use crate::stremio_app::stremio_player::Player; -use crate::stremio_app::stremio_wevbiew::WebView; -use crate::stremio_app::systray::SystemTray; -use crate::stremio_app::window_helper::WindowStyle; - -#[derive(Default, NwgUi)] -pub struct MainWindow { - pub webui_url: String, - pub dev_tools: bool, - pub saved_window_style: RefCell, - #[nwg_resource] - pub embed: nwg::EmbedResource, - #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] - pub window_icon: nwg::Icon, - #[nwg_control(icon: Some(&data.window_icon), title: "Stremio", flags: "MAIN_WINDOW|VISIBLE")] - #[nwg_events( OnWindowClose: [Self::on_quit(SELF, EVT_DATA)], OnInit: [Self::on_init], OnPaint: [Self::on_paint], OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)], OnWindowMaximize: [Self::transmit_window_state_change], OnWindowMinimize: [Self::transmit_window_state_change] )] - pub window: nwg::Window, - #[nwg_partial(parent: window)] - #[nwg_events((tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost]) ] - pub tray: SystemTray, - #[nwg_partial(parent: window)] - pub webview: WebView, - #[nwg_partial(parent: window)] - pub player: Player, - #[nwg_partial(parent: window)] - pub splash_screen: SplashImage, - #[nwg_control] - #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] - pub toggle_fullscreen_notice: nwg::Notice, - #[nwg_control] - #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] - pub quit_notice: nwg::Notice, - #[nwg_control] - #[nwg_events(OnNotice: [Self::on_hide_splash_notice] )] - pub hide_splash_notice: nwg::Notice, -} - -impl MainWindow { - const MIN_WIDTH: i32 = 1000; - const MIN_HEIGHT: i32 = 600; - fn transmit_window_full_screen_change(&self, prevent_close: bool) { - let web_channel = self.webview.channel.borrow(); - let (web_tx, _) = web_channel - .as_ref() - .expect("Cannont obtain communication channel for the Web UI"); - let web_tx_app = web_tx.clone(); - let saved_style = self.saved_window_style.borrow(); - web_tx_app - .send(RPCResponse::visibility_change( - self.window.visible(), - prevent_close as u32, - saved_style.full_screen, - )) - .ok(); - } - fn transmit_window_state_change(&self) { - if let Some(hwnd) = self.window.handle.hwnd() { - let web_channel = self.webview.channel.borrow(); - let (web_tx, _) = web_channel - .as_ref() - .expect("Cannont obtain communication channel for the Web UI"); - let web_tx_app = web_tx.clone(); - let style = self.saved_window_style.borrow(); - let state = style.clone().get_window_state(hwnd); - web_tx_app.send(RPCResponse::state_change(state)).ok(); - } - } - fn on_init(&self) { - self.webview.endpoint.set(self.webui_url.clone()).ok(); - self.webview.dev_tools.set(self.dev_tools).ok(); - if let Some(hwnd) = self.window.handle.hwnd() { - let mut saved_style = self.saved_window_style.borrow_mut(); - saved_style.center_window(hwnd, Self::MIN_WIDTH, Self::MIN_HEIGHT); - } - - self.tray.tray_show_hide.set_checked(true); - - let player_channel = self.player.channel.borrow(); - let (player_tx, player_rx) = player_channel - .as_ref() - .expect("Cannont obtain communication channel for the Player"); - let player_tx = player_tx.clone(); - let player_rx = player_rx.clone(); - - let web_channel = self.webview.channel.borrow(); - let (web_tx, web_rx) = web_channel - .as_ref() - .expect("Cannont obtain communication channel for the Web UI"); - let web_tx_player = web_tx.clone(); - let web_tx_web = web_tx.clone(); - 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); - }); // thread - - let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); - let quit_sender = self.quit_notice.sender(); - let hide_splash_sender = self.hide_splash_notice.sender(); - thread::spawn(move || loop { - if let Some(msg) = web_rx - .recv() - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - { - match msg.get_method() { - // The handshake. Here we send some useful data to the WEB UI - None if msg.is_handshake() => { - web_tx_web.send(RPCResponse::get_handshake()).ok(); - } - Some("win-set-visibility") => toggle_fullscreen_sender.notice(), - Some("quit") => quit_sender.notice(), - Some("app-ready") => { - hide_splash_sender.notice(); - web_tx_web - .send(RPCResponse::visibility_change(true, 1, false)) - .ok(); - } - Some("app-error") => { - hide_splash_sender.notice(); - if let Some(arg) = msg.get_params() { - // TODO: Make this modal dialog - eprintln!("Web App Error: {}", arg); - } - } - Some("open-external") => { - if let Some(arg) = msg.get_params() { - // FIXME: THIS IS NOT SAFE BY ANY MEANS - // open::that("calc").ok(); does exactly that - let arg = arg.as_str().unwrap_or(""); - let arg_lc = arg.to_lowercase(); - if arg_lc.starts_with("http://") - || arg_lc.starts_with("https://") - || arg_lc.starts_with("rtp://") - || arg_lc.starts_with("rtps://") - || arg_lc.starts_with("ftp://") - || arg_lc.starts_with("ipfs://") - { - open::that(arg).ok(); - } - } - } - Some(player_command) if player_command.starts_with("mpv-") => { - let resp_json = serde_json::to_string( - &msg.args.expect("Cannot have method without args"), - ) - .expect("Cannot build response"); - player_tx.send(resp_json).ok(); - } - Some(unknown) => { - eprintln!("Unsupported command {}({:?})", unknown, msg.get_params()) - } - None => {} - } - } // recv - }); // thread - } - fn on_min_max(&self, data: &nwg::EventData) { - let data = data.on_min_max(); - data.set_min_size(Self::MIN_WIDTH, Self::MIN_HEIGHT); - } - fn on_paint(&self) { - if self.splash_screen.visible() { - self.splash_screen.resize(self.window.size()); - } else { - self.transmit_window_state_change(); - } - } - fn on_toggle_fullscreen_notice(&self) { - if let Some(hwnd) = self.window.handle.hwnd() { - let mut saved_style = self.saved_window_style.borrow_mut(); - saved_style.toggle_full_screen(hwnd); - self.tray.tray_topmost.set_enabled(!saved_style.full_screen); - self.tray - .tray_topmost - .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); - } - self.transmit_window_full_screen_change(true); - } - fn on_hide_splash_notice(&self) { - self.splash_screen.hide(); - } - fn on_toggle_topmost(&self) { - if let Some(hwnd) = self.window.handle.hwnd() { - let mut saved_style = self.saved_window_style.borrow_mut(); - saved_style.toggle_topmost(hwnd); - self.tray - .tray_topmost - .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); - } - } - fn on_show_hide(&self) { - self.window.set_visible(!self.window.visible()); - self.tray.tray_show_hide.set_checked(self.window.visible()); - self.transmit_window_state_change(); - } - fn on_quit(&self, data: &nwg::EventData) { - if let nwg::EventData::OnWindowClose(data) = data { - data.close(false); - } - 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(); - } -} +use native_windows_derive::NwgUi; +use native_windows_gui as nwg; +use serde_json; +use std::cell::RefCell; +use std::thread; +use winapi::um::winuser::WS_EX_TOPMOST; + +use crate::stremio_app::ipc::{RPCRequest, RPCResponse}; +use crate::stremio_app::splash::SplashImage; +use crate::stremio_app::stremio_player::Player; +use crate::stremio_app::stremio_wevbiew::WebView; +use crate::stremio_app::systray::SystemTray; +use crate::stremio_app::window_helper::WindowStyle; + +#[derive(Default, NwgUi)] +pub struct MainWindow { + pub webui_url: String, + pub dev_tools: bool, + pub saved_window_style: RefCell, + #[nwg_resource] + pub embed: nwg::EmbedResource, + #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] + pub window_icon: nwg::Icon, + #[nwg_control(icon: Some(&data.window_icon), title: "Stremio", flags: "MAIN_WINDOW|VISIBLE")] + #[nwg_events( OnWindowClose: [Self::on_quit(SELF, EVT_DATA)], OnInit: [Self::on_init], OnPaint: [Self::on_paint], OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)], OnWindowMaximize: [Self::transmit_window_state_change], OnWindowMinimize: [Self::transmit_window_state_change] )] + pub window: nwg::Window, + #[nwg_partial(parent: window)] + #[nwg_events((tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost]) ] + pub tray: SystemTray, + #[nwg_partial(parent: window)] + pub webview: WebView, + #[nwg_partial(parent: window)] + pub player: Player, + #[nwg_partial(parent: window)] + pub splash_screen: SplashImage, + #[nwg_control] + #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] + pub toggle_fullscreen_notice: nwg::Notice, + #[nwg_control] + #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] + pub quit_notice: nwg::Notice, + #[nwg_control] + #[nwg_events(OnNotice: [Self::on_hide_splash_notice] )] + pub hide_splash_notice: nwg::Notice, +} + +impl MainWindow { + const MIN_WIDTH: i32 = 1000; + const MIN_HEIGHT: i32 = 600; + fn transmit_window_full_screen_change(&self, prevent_close: bool) { + let web_channel = self.webview.channel.borrow(); + let (web_tx, _) = web_channel + .as_ref() + .expect("Cannont obtain communication channel for the Web UI"); + let web_tx_app = web_tx.clone(); + let saved_style = self.saved_window_style.borrow(); + web_tx_app + .send(RPCResponse::visibility_change( + self.window.visible(), + prevent_close as u32, + saved_style.full_screen, + )) + .ok(); + } + fn transmit_window_state_change(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + let web_channel = self.webview.channel.borrow(); + let (web_tx, _) = web_channel + .as_ref() + .expect("Cannont obtain communication channel for the Web UI"); + let web_tx_app = web_tx.clone(); + let style = self.saved_window_style.borrow(); + let state = style.clone().get_window_state(hwnd); + web_tx_app.send(RPCResponse::state_change(state)).ok(); + } + } + fn on_init(&self) { + self.webview.endpoint.set(self.webui_url.clone()).ok(); + self.webview.dev_tools.set(self.dev_tools).ok(); + if let Some(hwnd) = self.window.handle.hwnd() { + let mut saved_style = self.saved_window_style.borrow_mut(); + saved_style.center_window(hwnd, Self::MIN_WIDTH, Self::MIN_HEIGHT); + } + + self.tray.tray_show_hide.set_checked(true); + + let player_channel = self.player.channel.borrow(); + let (player_tx, player_rx) = player_channel + .as_ref() + .expect("Cannont obtain communication channel for the Player"); + let player_tx = player_tx.clone(); + let player_rx = player_rx.clone(); + + let web_channel = self.webview.channel.borrow(); + let (web_tx, web_rx) = web_channel + .as_ref() + .expect("Cannont obtain communication channel for the Web UI"); + let web_tx_player = web_tx.clone(); + let web_tx_web = web_tx.clone(); + 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); + }); // thread + + let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); + let quit_sender = self.quit_notice.sender(); + let hide_splash_sender = self.hide_splash_notice.sender(); + thread::spawn(move || loop { + if let Some(msg) = web_rx + .recv() + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + { + match msg.get_method() { + // The handshake. Here we send some useful data to the WEB UI + None if msg.is_handshake() => { + web_tx_web.send(RPCResponse::get_handshake()).ok(); + } + Some("win-set-visibility") => toggle_fullscreen_sender.notice(), + Some("quit") => quit_sender.notice(), + Some("app-ready") => { + hide_splash_sender.notice(); + web_tx_web + .send(RPCResponse::visibility_change(true, 1, false)) + .ok(); + } + Some("app-error") => { + hide_splash_sender.notice(); + if let Some(arg) = msg.get_params() { + // TODO: Make this modal dialog + eprintln!("Web App Error: {}", arg); + } + } + Some("open-external") => { + if let Some(arg) = msg.get_params() { + // FIXME: THIS IS NOT SAFE BY ANY MEANS + // open::that("calc").ok(); does exactly that + let arg = arg.as_str().unwrap_or(""); + let arg_lc = arg.to_lowercase(); + if arg_lc.starts_with("http://") + || arg_lc.starts_with("https://") + || arg_lc.starts_with("rtp://") + || arg_lc.starts_with("rtps://") + || arg_lc.starts_with("ftp://") + || arg_lc.starts_with("ipfs://") + { + open::that(arg).ok(); + } + } + } + Some(player_command) if player_command.starts_with("mpv-") => { + let resp_json = serde_json::to_string( + &msg.args.expect("Cannot have method without args"), + ) + .expect("Cannot build response"); + player_tx.send(resp_json).ok(); + } + Some(unknown) => { + eprintln!("Unsupported command {}({:?})", unknown, msg.get_params()) + } + None => {} + } + } // recv + }); // thread + } + fn on_min_max(&self, data: &nwg::EventData) { + let data = data.on_min_max(); + data.set_min_size(Self::MIN_WIDTH, Self::MIN_HEIGHT); + } + fn on_paint(&self) { + if self.splash_screen.visible() { + self.splash_screen.resize(self.window.size()); + } else { + self.transmit_window_state_change(); + } + } + fn on_toggle_fullscreen_notice(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + let mut saved_style = self.saved_window_style.borrow_mut(); + saved_style.toggle_full_screen(hwnd); + self.tray.tray_topmost.set_enabled(!saved_style.full_screen); + self.tray + .tray_topmost + .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); + } + self.transmit_window_full_screen_change(true); + } + fn on_hide_splash_notice(&self) { + self.splash_screen.hide(); + } + fn on_toggle_topmost(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + let mut saved_style = self.saved_window_style.borrow_mut(); + saved_style.toggle_topmost(hwnd); + self.tray + .tray_topmost + .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); + } + } + fn on_show_hide(&self) { + self.window.set_visible(!self.window.visible()); + self.tray.tray_show_hide.set_checked(self.window.visible()); + self.transmit_window_state_change(); + } + fn on_quit(&self, data: &nwg::EventData) { + if let nwg::EventData::OnWindowClose(data) = data { + data.close(false); + } + 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(); + } +}