diff --git a/src/stremio_app/stremio_player/player.rs b/src/stremio_app/stremio_player/player.rs index 31ee374..95d0d8b 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 libmpv2::{events::EventContext, 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 = EventContext::new(mpv.ctx); - 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:#}", &msg); - 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 { libmpv2_sys::mpv_wakeup(self.ctx.as_ptr()) } - } -} +use crate::stremio_app::ipc; +use crate::stremio_app::RPCResponse; +use flume::{Receiver, Sender}; +use libmpv2::{events::Event, events::EventContext, 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 = EventContext::new(mpv.ctx); + 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:#}", &msg); + 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 { libmpv2_sys::mpv_wakeup(self.ctx.as_ptr()) } + } +}