From f10380f60a38758eae9d069b98af92ebfea178fb 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] 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 {} + } +}