minor refactor + cargo fmt --all

This commit is contained in:
Martin Kavík 2022-04-01 22:39:20 +02:00
parent 81e088800b
commit f10380f60a
6 changed files with 852 additions and 842 deletions

View file

@ -1,4 +1,4 @@
extern crate embed_resource; extern crate embed_resource;
fn main() { fn main() {
embed_resource::compile("resources.rc"); embed_resource::compile("resources.rc");
} }

View file

@ -1,215 +1,218 @@
use native_windows_derive::NwgUi; use native_windows_derive::NwgUi;
use native_windows_gui as nwg; use native_windows_gui as nwg;
use serde_json; use serde_json;
use std::cell::RefCell; use std::cell::RefCell;
use std::thread; use std::thread;
use winapi::um::winuser::WS_EX_TOPMOST; use winapi::um::winuser::WS_EX_TOPMOST;
use crate::stremio_app::ipc::{RPCRequest, RPCResponse}; use crate::stremio_app::ipc::{RPCRequest, RPCResponse};
use crate::stremio_app::splash::SplashImage; use crate::stremio_app::splash::SplashImage;
use crate::stremio_app::stremio_player::Player; use crate::stremio_app::stremio_player::Player;
use crate::stremio_app::stremio_wevbiew::WebView; use crate::stremio_app::stremio_wevbiew::WebView;
use crate::stremio_app::systray::SystemTray; use crate::stremio_app::systray::SystemTray;
use crate::stremio_app::window_helper::WindowStyle; use crate::stremio_app::window_helper::WindowStyle;
#[derive(Default, NwgUi)] #[derive(Default, NwgUi)]
pub struct MainWindow { pub struct MainWindow {
pub webui_url: String, pub webui_url: String,
pub dev_tools: bool, pub dev_tools: bool,
pub saved_window_style: RefCell<WindowStyle>, pub saved_window_style: RefCell<WindowStyle>,
#[nwg_resource] #[nwg_resource]
pub embed: nwg::EmbedResource, pub embed: nwg::EmbedResource,
#[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))]
pub window_icon: nwg::Icon, pub window_icon: nwg::Icon,
#[nwg_control(icon: Some(&data.window_icon), title: "Stremio", flags: "MAIN_WINDOW|VISIBLE")] #[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] )] #[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, pub window: nwg::Window,
#[nwg_partial(parent: 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]) ] #[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, pub tray: SystemTray,
#[nwg_partial(parent: window)] #[nwg_partial(parent: window)]
pub webview: WebView, pub webview: WebView,
#[nwg_partial(parent: window)] #[nwg_partial(parent: window)]
pub player: Player, pub player: Player,
#[nwg_partial(parent: window)] #[nwg_partial(parent: window)]
pub splash_screen: SplashImage, pub splash_screen: SplashImage,
#[nwg_control] #[nwg_control]
#[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )]
pub toggle_fullscreen_notice: nwg::Notice, pub toggle_fullscreen_notice: nwg::Notice,
#[nwg_control] #[nwg_control]
#[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )]
pub quit_notice: nwg::Notice, pub quit_notice: nwg::Notice,
#[nwg_control] #[nwg_control]
#[nwg_events(OnNotice: [Self::on_hide_splash_notice] )] #[nwg_events(OnNotice: [Self::on_hide_splash_notice] )]
pub hide_splash_notice: nwg::Notice, pub hide_splash_notice: nwg::Notice,
} }
impl MainWindow { impl MainWindow {
const MIN_WIDTH: i32 = 1000; const MIN_WIDTH: i32 = 1000;
const MIN_HEIGHT: i32 = 600; const MIN_HEIGHT: i32 = 600;
fn transmit_window_full_screen_change(&self, prevent_close: bool) { fn transmit_window_full_screen_change(&self, prevent_close: bool) {
let web_channel = self.webview.channel.borrow(); let web_channel = self.webview.channel.borrow();
let (web_tx, _) = web_channel let (web_tx, _) = web_channel
.as_ref() .as_ref()
.expect("Cannont obtain communication channel for the Web UI"); .expect("Cannont obtain communication channel for the Web UI");
let web_tx_app = web_tx.clone(); let web_tx_app = web_tx.clone();
let saved_style = self.saved_window_style.borrow(); let saved_style = self.saved_window_style.borrow();
web_tx_app web_tx_app
.send(RPCResponse::visibility_change( .send(RPCResponse::visibility_change(
self.window.visible(), self.window.visible(),
prevent_close as u32, prevent_close as u32,
saved_style.full_screen, saved_style.full_screen,
)) ))
.ok(); .ok();
} }
fn transmit_window_state_change(&self) { fn transmit_window_state_change(&self) {
if let Some(hwnd) = self.window.handle.hwnd() { if let Some(hwnd) = self.window.handle.hwnd() {
let web_channel = self.webview.channel.borrow(); let web_channel = self.webview.channel.borrow();
let (web_tx, _) = web_channel let (web_tx, _) = web_channel
.as_ref() .as_ref()
.expect("Cannont obtain communication channel for the Web UI"); .expect("Cannont obtain communication channel for the Web UI");
let web_tx_app = web_tx.clone(); let web_tx_app = web_tx.clone();
let style = self.saved_window_style.borrow(); let style = self.saved_window_style.borrow();
let state = style.clone().get_window_state(hwnd); let state = style.clone().get_window_state(hwnd);
web_tx_app.send(RPCResponse::state_change(state)).ok(); web_tx_app.send(RPCResponse::state_change(state)).ok();
} }
} }
fn on_init(&self) { fn on_init(&self) {
self.webview.endpoint.set(self.webui_url.clone()).ok(); self.webview.endpoint.set(self.webui_url.clone()).ok();
self.webview.dev_tools.set(self.dev_tools).ok(); self.webview.dev_tools.set(self.dev_tools).ok();
if let Some(hwnd) = self.window.handle.hwnd() { if let Some(hwnd) = self.window.handle.hwnd() {
let mut saved_style = self.saved_window_style.borrow_mut(); let mut saved_style = self.saved_window_style.borrow_mut();
saved_style.center_window(hwnd, Self::MIN_WIDTH, Self::MIN_HEIGHT); saved_style.center_window(hwnd, Self::MIN_WIDTH, Self::MIN_HEIGHT);
} }
self.tray.tray_show_hide.set_checked(true); self.tray.tray_show_hide.set_checked(true);
let player_channel = self.player.channel.borrow(); let player_channel = self.player.channel.borrow();
let (player_tx, player_rx) = player_channel let (player_tx, player_rx) = player_channel
.as_ref() .as_ref()
.expect("Cannont obtain communication channel for the Player"); .expect("Cannont obtain communication channel for the Player");
let player_tx = player_tx.clone(); let player_tx = player_tx.clone();
let player_rx = player_rx.clone(); let player_rx = player_rx.clone();
let web_channel = self.webview.channel.borrow(); let web_channel = self.webview.channel.borrow();
let (web_tx, web_rx) = web_channel let (web_tx, web_rx) = web_channel
.as_ref() .as_ref()
.expect("Cannont obtain communication channel for the Web UI"); .expect("Cannont obtain communication channel for the Web UI");
let web_tx_player = web_tx.clone(); let web_tx_player = web_tx.clone();
let web_tx_web = web_tx.clone(); let web_tx_web = web_tx.clone();
let web_rx = web_rx.clone(); let web_rx = web_rx.clone();
// Read message from player // Read message from player
thread::spawn(move || loop { thread::spawn(move || loop {
player_rx.iter().map(|msg| web_tx_player.send(msg)).for_each(drop); player_rx
}); // thread .iter()
.map(|msg| web_tx_player.send(msg))
let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); .for_each(drop);
let quit_sender = self.quit_notice.sender(); }); // thread
let hide_splash_sender = self.hide_splash_notice.sender();
thread::spawn(move || loop { let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender();
if let Some(msg) = web_rx let quit_sender = self.quit_notice.sender();
.recv() let hide_splash_sender = self.hide_splash_notice.sender();
.ok() thread::spawn(move || loop {
.and_then(|s| serde_json::from_str::<RPCRequest>(&s).ok()) if let Some(msg) = web_rx
{ .recv()
match msg.get_method() { .ok()
// The handshake. Here we send some useful data to the WEB UI .and_then(|s| serde_json::from_str::<RPCRequest>(&s).ok())
None if msg.is_handshake() => { {
web_tx_web.send(RPCResponse::get_handshake()).ok(); match msg.get_method() {
} // The handshake. Here we send some useful data to the WEB UI
Some("win-set-visibility") => toggle_fullscreen_sender.notice(), None if msg.is_handshake() => {
Some("quit") => quit_sender.notice(), web_tx_web.send(RPCResponse::get_handshake()).ok();
Some("app-ready") => { }
hide_splash_sender.notice(); Some("win-set-visibility") => toggle_fullscreen_sender.notice(),
web_tx_web Some("quit") => quit_sender.notice(),
.send(RPCResponse::visibility_change(true, 1, false)) Some("app-ready") => {
.ok(); hide_splash_sender.notice();
} web_tx_web
Some("app-error") => { .send(RPCResponse::visibility_change(true, 1, false))
hide_splash_sender.notice(); .ok();
if let Some(arg) = msg.get_params() { }
// TODO: Make this modal dialog Some("app-error") => {
eprintln!("Web App Error: {}", arg.to_string()); hide_splash_sender.notice();
} if let Some(arg) = msg.get_params() {
} // TODO: Make this modal dialog
Some("open-external") => { eprintln!("Web App Error: {}", arg.to_string());
if let Some(arg) = msg.get_params() { }
// FIXME: THIS IS NOT SAFE BY ANY MEANS }
// open::that("calc").ok(); does exactly that Some("open-external") => {
let arg = arg.as_str().unwrap_or(""); if let Some(arg) = msg.get_params() {
let arg_lc = arg.to_lowercase(); // FIXME: THIS IS NOT SAFE BY ANY MEANS
if arg_lc.starts_with("http://") // open::that("calc").ok(); does exactly that
|| arg_lc.starts_with("https://") let arg = arg.as_str().unwrap_or("");
|| arg_lc.starts_with("rtp://") let arg_lc = arg.to_lowercase();
|| arg_lc.starts_with("rtps://") if arg_lc.starts_with("http://")
|| arg_lc.starts_with("ftp://") || arg_lc.starts_with("https://")
|| arg_lc.starts_with("ipfs://") || arg_lc.starts_with("rtp://")
{ || arg_lc.starts_with("rtps://")
open::that(arg).ok(); || 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"), }
) Some(player_command) if player_command.starts_with("mpv-") => {
.expect("Cannot build response"); let resp_json = serde_json::to_string(
player_tx.send(resp_json).ok(); &msg.args.expect("Cannot have method without args"),
} )
Some(unknown) => { .expect("Cannot build response");
eprintln!("Unsupported command {}({:?})", unknown, msg.get_params()) player_tx.send(resp_json).ok();
} }
None => {} Some(unknown) => {
} eprintln!("Unsupported command {}({:?})", unknown, msg.get_params())
} // recv }
}); // thread None => {}
} }
fn on_min_max(&self, data: &nwg::EventData) { } // recv
let data = data.on_min_max(); }); // thread
data.set_min_size(Self::MIN_WIDTH, Self::MIN_HEIGHT); }
} fn on_min_max(&self, data: &nwg::EventData) {
fn on_paint(&self) { let data = data.on_min_max();
if self.splash_screen.visible() { data.set_min_size(Self::MIN_WIDTH, Self::MIN_HEIGHT);
self.splash_screen.resize(self.window.size()); }
} else { fn on_paint(&self) {
self.transmit_window_state_change(); if self.splash_screen.visible() {
} self.splash_screen.resize(self.window.size());
} } else {
fn on_toggle_fullscreen_notice(&self) { self.transmit_window_state_change();
if let Some(hwnd) = self.window.handle.hwnd() { }
let mut saved_style = self.saved_window_style.borrow_mut(); }
saved_style.toggle_full_screen(hwnd); fn on_toggle_fullscreen_notice(&self) {
self.tray.tray_topmost.set_enabled(!saved_style.full_screen); if let Some(hwnd) = self.window.handle.hwnd() {
self.tray let mut saved_style = self.saved_window_style.borrow_mut();
.tray_topmost saved_style.toggle_full_screen(hwnd);
.set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); self.tray.tray_topmost.set_enabled(!saved_style.full_screen);
} self.tray
self.transmit_window_full_screen_change(true); .tray_topmost
} .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST);
fn on_hide_splash_notice(&self) { }
self.splash_screen.hide(); self.transmit_window_full_screen_change(true);
} }
fn on_toggle_topmost(&self) { fn on_hide_splash_notice(&self) {
if let Some(hwnd) = self.window.handle.hwnd() { self.splash_screen.hide();
let mut saved_style = self.saved_window_style.borrow_mut(); }
saved_style.toggle_topmost(hwnd); fn on_toggle_topmost(&self) {
self.tray if let Some(hwnd) = self.window.handle.hwnd() {
.tray_topmost let mut saved_style = self.saved_window_style.borrow_mut();
.set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); saved_style.toggle_topmost(hwnd);
} self.tray
} .tray_topmost
fn on_show_hide(&self) { .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST);
self.window.set_visible(!self.window.visible()); }
self.tray.tray_show_hide.set_checked(self.window.visible()); }
self.transmit_window_state_change(); fn on_show_hide(&self) {
} self.window.set_visible(!self.window.visible());
fn on_quit(&self, data: &nwg::EventData) { self.tray.tray_show_hide.set_checked(self.window.visible());
if let nwg::EventData::OnWindowClose(data) = data { self.transmit_window_state_change();
data.close(false); }
} fn on_quit(&self, data: &nwg::EventData) {
self.window.set_visible(false); if let nwg::EventData::OnWindowClose(data) = data {
self.tray.tray_show_hide.set_checked(self.window.visible()); data.close(false);
self.transmit_window_full_screen_change(false); }
nwg::stop_thread_dispatch(); 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();
}
}

View file

@ -1,104 +1,100 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{self, json}; use serde_json::{self, json};
use std::cell::RefCell; use std::cell::RefCell;
pub type Channel = RefCell<Option<(flume::Sender<String>, flume::Receiver<String>)>>; pub type Channel = RefCell<Option<(flume::Sender<String>, flume::Receiver<String>)>>;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RPCRequest { pub struct RPCRequest {
pub id: u64, pub id: u64,
pub args: Option<Vec<serde_json::Value>>, pub args: Option<Vec<serde_json::Value>>,
} }
impl RPCRequest { impl RPCRequest {
pub fn is_handshake(&self) -> bool { pub fn is_handshake(&self) -> bool {
self.id == 0 self.id == 0
} }
pub fn get_method(&self) -> Option<&str> { pub fn get_method(&self) -> Option<&str> {
self.args self.args
.as_ref() .as_ref()
.and_then(|args| args.first()) .and_then(|args| args.first())
.and_then(|arg| arg.as_str()) .and_then(|arg| arg.as_str())
} }
pub fn get_params(&self) -> Option<&serde_json::Value> { pub fn get_params(&self) -> Option<&serde_json::Value> {
self.args.as_ref().and_then(|args| { self.args
if args.len() > 1 { .as_ref()
Some(&args[1]) .and_then(|args| if args.len() > 1 { Some(&args[1]) } else { None })
} else { }
None }
} #[derive(Serialize, Deserialize, Debug, Clone)]
}) pub struct RPCResponseDataTransport {
} pub properties: Vec<Vec<String>>,
} pub signals: Vec<String>,
#[derive(Serialize, Deserialize, Debug, Clone)] pub methods: Vec<Vec<String>>,
pub struct RPCResponseDataTransport { }
pub properties: Vec<Vec<String>>,
pub signals: Vec<String>, #[derive(Serialize, Deserialize, Debug, Clone)]
pub methods: Vec<Vec<String>>, pub struct RPCResponseData {
} pub transport: RPCResponseDataTransport,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RPCResponseData { #[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub transport: RPCResponseDataTransport, pub struct RPCResponse {
} pub id: u64,
pub object: String,
#[derive(Default, Serialize, Deserialize, Debug, Clone)] #[serde(rename = "type")]
pub struct RPCResponse { pub response_type: u32,
pub id: u64, #[serde(skip_serializing_if = "Option::is_none")]
pub object: String, pub data: Option<RPCResponseData>,
#[serde(rename = "type")] #[serde(skip_serializing_if = "Option::is_none")]
pub response_type: u32, pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")] }
pub data: Option<RPCResponseData>,
#[serde(skip_serializing_if = "Option::is_none")] impl RPCResponse {
pub args: Option<serde_json::Value>, pub fn get_handshake() -> String {
} let resp = RPCResponse {
id: 0,
impl RPCResponse { object: "transport".to_string(),
pub fn get_handshake() -> String { response_type: 3,
let resp = RPCResponse { data: Some(RPCResponseData {
id: 0, transport: RPCResponseDataTransport {
object: "transport".to_string(), properties: vec![
response_type: 3, vec![],
data: Some(RPCResponseData { vec![
transport: RPCResponseDataTransport { "".to_string(),
properties: vec![ "shellVersion".to_string(),
vec![], "".to_string(),
vec![ "5.0.0".to_string(),
"".to_string(), ],
"shellVersion".to_string(), ],
"".to_string(), signals: vec![],
"5.0.0".to_string(), methods: vec![vec!["onEvent".to_string(), "".to_string()]],
], },
], }),
signals: vec![], ..Default::default()
methods: vec![vec!["onEvent".to_string(), "".to_string()]], };
}, serde_json::to_string(&resp).expect("Cannot build response")
}), }
..Default::default() pub fn response_message(msg: Option<serde_json::Value>) -> String {
}; let resp = RPCResponse {
serde_json::to_string(&resp).expect("Cannot build response") id: 1,
} object: "transport".to_string(),
pub fn response_message(msg: Option<serde_json::Value>) -> String { response_type: 1,
let resp = RPCResponse { args: msg,
id: 1, ..Default::default()
object: "transport".to_string(), };
response_type: 1, serde_json::to_string(&resp).expect("Cannot build response")
args: msg, }
..Default::default() pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String {
}; Self::response_message(Some(json!(["win-visibility-changed" ,{
serde_json::to_string(&resp).expect("Cannot build response") "visible": visible,
} "visibility": visibility,
pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String { "isFullscreen": is_full_screen
Self::response_message(Some(json!(["win-visibility-changed" ,{ }])))
"visible": visible, }
"visibility": visibility, pub fn state_change(state: u32) -> String {
"isFullscreen": is_full_screen Self::response_message(Some(json!(["win-state-changed" ,{
}]))) "state": state,
} }])))
pub fn state_change(state: u32) -> String { }
Self::response_message(Some(json!(["win-state-changed" ,{ }
"state": state,
}])))
}
}

View file

@ -1,252 +1,252 @@
use core::convert::TryFrom; use core::convert::TryFrom;
use parse_display::{Display, FromStr}; use libmpv::{events::PropertyData, mpv_end_file_reason, EndFileReason};
use serde::{Deserialize, Serialize}; use parse_display::{Display, FromStr};
use std::fmt; use serde::{Deserialize, Serialize};
use libmpv::{events::PropertyData, EndFileReason, mpv_end_file_reason}; use std::fmt;
// Responses // Responses
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PlayerProprChange { pub struct PlayerProprChange {
name: String, name: String,
data: serde_json::Value, data: serde_json::Value,
} }
impl PlayerProprChange { impl PlayerProprChange {
fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value {
match data { match data {
PropertyData::Flag(d) => serde_json::Value::Bool(d), PropertyData::Flag(d) => serde_json::Value::Bool(d),
PropertyData::Int64(d) => serde_json::Value::Number( PropertyData::Int64(d) => serde_json::Value::Number(
serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"), serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"),
), ),
PropertyData::Double(d) => serde_json::Value::Number( PropertyData::Double(d) => serde_json::Value::Number(
serde_json::Number::from_f64(d).expect("MPV returned invalid number"), serde_json::Number::from_f64(d).expect("MPV returned invalid number"),
), ),
PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()), PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()),
PropertyData::Str(s) => { PropertyData::Str(s) => {
if as_json { if as_json {
serde_json::from_str(s).expect("MPV returned invalid JSON data") serde_json::from_str(s).expect("MPV returned invalid JSON data")
} else { } else {
serde_json::Value::String(s.to_string()) serde_json::Value::String(s.to_string())
} }
} }
PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"), PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"),
} }
} }
pub fn from_name_value(name: String, value: PropertyData) -> Self { pub fn from_name_value(name: String, value: PropertyData) -> Self {
let is_json = JSON_RESPONSES.contains(&name.as_str()); let is_json = JSON_RESPONSES.contains(&name.as_str());
Self { Self {
name, name,
data: Self::value_from_format(value, is_json), data: Self::value_from_format(value, is_json),
} }
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PlayerEnded { pub struct PlayerEnded {
reason: String, reason: String,
} }
impl PlayerEnded { impl PlayerEnded {
fn string_from_end_reason(data: EndFileReason) -> String { fn string_from_end_reason(data: EndFileReason) -> String {
match data { match data {
mpv_end_file_reason::Error => "error".to_string(), mpv_end_file_reason::Error => "error".to_string(),
mpv_end_file_reason::Quit => "quit".to_string(), mpv_end_file_reason::Quit => "quit".to_string(),
_ => "other".to_string(), _ => "other".to_string(),
} }
} }
pub fn from_end_reason(data: EndFileReason) -> Self { pub fn from_end_reason(data: EndFileReason) -> Self {
Self { Self {
reason: Self::string_from_end_reason(data), reason: Self::string_from_end_reason(data),
} }
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerError { pub struct PlayerError {
pub error: String, pub error: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)] #[serde(untagged)]
pub enum PlayerEvent { pub enum PlayerEvent {
PropChange(PlayerProprChange), PropChange(PlayerProprChange),
End(PlayerEnded), End(PlayerEnded),
Error(PlayerError), Error(PlayerError),
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
impl PlayerResponse<'_> { impl PlayerResponse<'_> {
pub fn to_value(&self) -> Option<serde_json::Value> { pub fn to_value(&self) -> Option<serde_json::Value> {
serde_json::to_value(self).ok() serde_json::to_value(self).ok()
} }
} }
// Player incoming messages from the web UI // Player incoming messages from the web UI
/* /*
Message general case - ["function-name", ["arguments", ...]] Message general case - ["function-name", ["arguments", ...]]
The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. The function could be either mpv-observe-prop, mpv-set-prop or mpv-command.
["mpv-observe-prop", "prop-name"] ["mpv-observe-prop", "prop-name"]
["mpv-set-prop", ["prop-name", prop-val]] ["mpv-set-prop", ["prop-name", prop-val]]
["mpv-command", ["command-name"<, "arguments">]] ["mpv-command", ["command-name"<, "arguments">]]
All the function and property names are in kebab-case. All the function and property names are in kebab-case.
MPV requires type for any prop-name when observing or setting it's value. 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. 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 "mpv-observe-prop" function is the only one that accepts single string
instead of array of arguments instead of array of arguments
"mpv-command" function always takes an array even if the command doesn't "mpv-command" function always takes an array even if the command doesn't
have any arguments. For example this are the commands we support: have any arguments. For example this are the commands we support:
["mpv-command", ["loadfile", "file name"]] ["mpv-command", ["loadfile", "file name"]]
["mpv-command", ["stop"]] ["mpv-command", ["stop"]]
*/ */
macro_rules! stringable { macro_rules! stringable {
($t:ident) => { ($t:ident) => {
impl From<$t> for String { impl From<$t> for String {
fn from(s: $t) -> Self { fn from(s: $t) -> Self {
s.to_string() s.to_string()
} }
} }
impl TryFrom<String> for $t { impl TryFrom<String> for $t {
type Error = parse_display::ParseError; type Error = parse_display::ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> { fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse() s.parse()
} }
} }
}; };
} }
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")] #[display(style = "kebab-case")]
pub enum InMsgFn { pub enum InMsgFn {
MpvSetProp, MpvSetProp,
MpvCommand, MpvCommand,
MpvObserveProp, MpvObserveProp,
} }
stringable!(InMsgFn); stringable!(InMsgFn);
// Bool // Bool
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")] #[display(style = "kebab-case")]
pub enum BoolProp { pub enum BoolProp {
Pause, Pause,
PausedForCache, PausedForCache,
Seeking, Seeking,
EofReached, EofReached,
} }
stringable!(BoolProp); stringable!(BoolProp);
// Int // Int
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")] #[display(style = "kebab-case")]
pub enum IntProp { pub enum IntProp {
Aid, Aid,
Vid, Vid,
Sid, Sid,
} }
stringable!(IntProp); stringable!(IntProp);
// Fp // Fp
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")] #[display(style = "kebab-case")]
pub enum FpProp { pub enum FpProp {
TimePos, TimePos,
Volume, Volume,
Duration, Duration,
SubScale, SubScale,
CacheBufferingState, CacheBufferingState,
SubPos, SubPos,
Speed, Speed,
} }
stringable!(FpProp); stringable!(FpProp);
// Str // Str
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")] #[display(style = "kebab-case")]
pub enum StrProp { pub enum StrProp {
FfmpegVersion, FfmpegVersion,
Hwdec, Hwdec,
InputDefaltBindings, InputDefaltBindings,
InputVoKeyboard, InputVoKeyboard,
Metadata, Metadata,
MpvVersion, MpvVersion,
Osc, Osc,
Path, Path,
SubAssOverride, SubAssOverride,
SubBackColor, SubBackColor,
SubBorderColor, SubBorderColor,
SubColor, SubColor,
TrackList, TrackList,
VideoParams, VideoParams,
// Vo, // Vo,
} }
stringable!(StrProp); stringable!(StrProp);
// Any // Any
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum PropKey { pub enum PropKey {
Bool(BoolProp), Bool(BoolProp),
Int(IntProp), Int(IntProp),
Fp(FpProp), Fp(FpProp),
Str(StrProp), Str(StrProp),
} }
impl fmt::Display for PropKey { impl fmt::Display for PropKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Self::Bool(v) => write!(f, "{}", v), Self::Bool(v) => write!(f, "{}", v),
Self::Int(v) => write!(f, "{}", v), Self::Int(v) => write!(f, "{}", v),
Self::Fp(v) => write!(f, "{}", v), Self::Fp(v) => write!(f, "{}", v),
Self::Str(v) => write!(f, "{}", v), Self::Str(v) => write!(f, "{}", v),
} }
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum PropVal { pub enum PropVal {
Bool(bool), Bool(bool),
Str(String), Str(String),
Num(f64), Num(f64),
} }
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
#[display(style = "kebab-case")] #[display(style = "kebab-case")]
#[serde(untagged)] #[serde(untagged)]
pub enum MpvCmd { pub enum MpvCmd {
Loadfile, Loadfile,
Stop, Stop,
} }
stringable!(MpvCmd); stringable!(MpvCmd);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum CmdVal { pub enum CmdVal {
Single((MpvCmd,)), Single((MpvCmd,)),
Double(MpvCmd, String), Double(MpvCmd, String),
} }
impl From<CmdVal> for Vec<String> { impl From<CmdVal> for Vec<String> {
fn from(cmd: CmdVal) -> Vec<String> { fn from(cmd: CmdVal) -> Vec<String> {
match cmd { match cmd {
CmdVal::Single(cmd) => vec![cmd.0.to_string()], CmdVal::Single(cmd) => vec![cmd.0.to_string()],
CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg],
} }
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum InMsgArgs { pub enum InMsgArgs {
StProp(PropKey, PropVal), StProp(PropKey, PropVal),
Cmd(CmdVal), Cmd(CmdVal),
ObProp(PropKey), ObProp(PropKey),
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InMsg(pub InMsgFn, pub InMsgArgs); pub struct InMsg(pub InMsgFn, pub InMsgArgs);

View file

@ -1,235 +1,246 @@
use crate::stremio_app::ipc; use crate::stremio_app::ipc;
use crate::stremio_app::RPCResponse; use crate::stremio_app::RPCResponse;
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use libmpv::{Mpv, events::Event, Format, SetData}; use libmpv::{events::Event, Format, Mpv, SetData};
use native_windows_gui::{self as nwg, PartialUi}; use native_windows_gui::{self as nwg, PartialUi};
use winapi::shared::windef::HWND; use std::{
use std::{thread::{self, JoinHandle}, sync::Arc}; sync::Arc,
thread::{self, JoinHandle},
use crate::stremio_app::stremio_player::{ };
InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, use winapi::shared::windef::HWND;
PropKey, PropVal, CmdVal,
}; use crate::stremio_app::stremio_player::{
CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
struct ObserveProperty { PropKey, PropVal,
name: String, };
format: Format,
} struct ObserveProperty {
name: String,
#[derive(Default)] format: Format,
pub struct Player { }
pub channel: ipc::Channel,
} #[derive(Default)]
pub struct Player {
impl PartialUi for Player { pub channel: ipc::Channel,
fn build_partial<W: Into<nwg::ControlHandle>>( }
// @TODO replace with `&mut self`?
data: &mut Self, impl PartialUi for Player {
parent: Option<W>, fn build_partial<W: Into<nwg::ControlHandle>>(
) -> Result<(), nwg::NwgError> { // @TODO replace with `&mut self`?
let (in_msg_sender, in_msg_receiver) = flume::unbounded(); data: &mut Self,
let (rpc_response_sender, rpc_response_receiver) = flume::unbounded(); parent: Option<W>,
) -> Result<(), nwg::NwgError> {
data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver))); // @TODO replace all `expect`s with proper error handling?
let window_handle = parent let window_handle = parent
.expect("no parent window") .expect("no parent window")
.into() .into()
.hwnd() .hwnd()
.expect("cannot obtain window handle"); .expect("cannot obtain window handle");
// @TODO replace all `expect`s with proper error handling?
let (in_msg_sender, in_msg_receiver) = flume::unbounded();
let mpv = create_shareable_mpv(window_handle); let (rpc_response_sender, rpc_response_receiver) = flume::unbounded();
let (observe_property_sender, observe_property_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 _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); let mpv = create_shareable_mpv(window_handle);
// @TODO implement a mechanism to stop threads on `Player` drop if needed
let _event_thread = create_event_thread(
Ok(()) Arc::clone(&mpv),
} observe_property_receiver,
} rpc_response_sender,
);
fn create_shareable_mpv(window_handle: HWND) -> Arc<Mpv> { let _message_thread = create_message_thread(mpv, observe_property_sender, in_msg_receiver);
let mpv = Mpv::with_initializer(|initializer| { // @TODO implement a mechanism to stop threads on `Player` drop if needed
initializer.set_property("wid", window_handle as i64).expect("failed setting wid");
// initializer.set_property("vo", "gpu").expect("unable to set vo"); Ok(())
// 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 fn create_shareable_mpv(window_handle: HWND) -> Arc<Mpv> {
// default (auto) seems to be d3d11 (vo/gpu/d3d11) let mpv = Mpv::with_initializer(|initializer| {
initializer.set_property("gpu-context", "angle").expect("failed setting gpu-contex"); initializer
initializer.set_property("gpu-api", "auto").expect("failed setting gpu-api"); .set_property("wid", window_handle as i64)
initializer.set_property("title", "Stremio").expect("failed setting title"); .expect("failed setting wid");
initializer.set_property("terminal", "yes").expect("failed setting terminal"); // initializer.set_property("vo", "gpu").expect("unable to set vo");
initializer.set_property("msg-level", "all=no,cplayer=debug").expect("failed setting msg-level"); // win, opengl: works but least performancy, 10-15% CPU
initializer.set_property("quiet", "yes").expect("failed setting quiet"); // winvk, vulkan: works as good as d3d11
initializer.set_property("hwdec", "auto").expect("failed setting hwdec"); // d3d11, d1d11: works great
// FIXME: very often the audio track isn't selected when using "aid" = "auto" // dxinterop, auto: works, slightly more cpu use than d3d11
initializer.set_property("aid", 1).expect("failed setting aid"); // default (auto) seems to be d3d11 (vo/gpu/d3d11)
Ok(()) initializer
}).expect("cannot build MPV"); .set_property("gpu-context", "angle")
.expect("failed setting gpu-contex");
Arc::new(mpv) initializer
} .set_property("gpu-api", "auto")
.expect("failed setting gpu-api");
fn create_event_thread( initializer
mpv: Arc<Mpv>, .set_property("title", "Stremio")
observe_property_receiver: Receiver<ObserveProperty>, .expect("failed setting title");
rpc_response_sender: Sender<String> initializer
) -> JoinHandle<()> { .set_property("terminal", "yes")
thread::spawn(move || { .expect("failed setting terminal");
let mut event_context = mpv.create_event_context(); initializer
event_context.disable_deprecated_events().expect("failed to disable deprecated MPV events"); .set_property("msg-level", "all=no,cplayer=debug")
.expect("failed setting msg-level");
loop { initializer
for ObserveProperty { name, format } in observe_property_receiver.drain() { .set_property("quiet", "yes")
event_context.observe_property(&name, format, 0).expect("failed to observer MPV property"); .expect("failed setting quiet");
} initializer
.set_property("hwdec", "auto")
// -1.0 means to block and wait for an event. .expect("failed setting hwdec");
let event = match event_context.wait_event(-1.) { // FIXME: very often the audio track isn't selected when using "aid" = "auto"
Some(Ok(event)) => event, initializer
Some(Err(error)) => { .set_property("aid", 1)
eprintln!("Event errored: {error:?}"); .expect("failed setting aid");
continue; Ok(())
} })
// dummy event received (may be created on a wake up call or on timeout) .expect("cannot build MPV");
None => continue,
}; Arc::new(mpv)
}
// even if you don't do anything with the events, it is still necessary to empty the event loop
let resp_event = match event { fn create_event_thread(
Event::PropertyChange { mpv: Arc<Mpv>,
name, observe_property_receiver: Receiver<ObserveProperty>,
change, rpc_response_sender: Sender<String>,
.. ) -> JoinHandle<()> {
} => PlayerResponse( thread::spawn(move || {
"mpv-prop-change", let mut event_context = mpv.create_event_context();
PlayerEvent::PropChange(PlayerProprChange::from_name_value( event_context
name.to_string(), .disable_deprecated_events()
change, .expect("failed to disable deprecated MPV events");
)),
) loop {
.to_value(), for ObserveProperty { name, format } in observe_property_receiver.drain() {
Event::EndFile(reason) => PlayerResponse( event_context
"mpv-event-ended", .observe_property(&name, format, 0)
PlayerEvent::End(PlayerEnded::from_end_reason(reason)), .expect("failed to observer MPV property");
) }
.to_value(),
Event::Shutdown => { // -1.0 means to block and wait for an event.
break; let event = match event_context.wait_event(-1.) {
} Some(Ok(event)) => event,
_ => None, Some(Err(error)) => {
}; eprintln!("Event errored: {error:?}");
if resp_event.is_some() { continue;
rpc_response_sender.send(RPCResponse::response_message(resp_event)).ok(); }
} // 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
fn create_message_thread( let player_response = match event {
mpv: Arc<Mpv>, Event::PropertyChange { name, change, .. } => {
observe_property_sender: Sender<ObserveProperty>, PlayerResponse(
in_msg_receiver: Receiver<String> "mpv-prop-change",
) -> JoinHandle<()> { PlayerEvent::PropChange(PlayerProprChange::from_name_value(
thread::spawn(move || { name.to_string(),
// -- Helpers -- change,
)),
let observe_property = |name: String, format: Format| { )
observe_property_sender.send(ObserveProperty { name, format }).expect("cannot send ObserveProperty"); }
mpv.wake_up(); Event::EndFile(reason) => {
}; PlayerResponse(
"mpv-event-ended",
let send_command = |cmd: CmdVal| { PlayerEvent::End(PlayerEnded::from_end_reason(reason)),
let (name, arg) = match cmd { )
CmdVal::Double(name, arg) => (name, format!(r#""{arg}""#)), }
CmdVal::Single((name,)) => (name, String::new()) Event::Shutdown => {
}; break;
mpv.command(&name.to_string(), &[&arg]).expect("failed to execute MPV command"); }
}; _ => continue,
};
fn set_property(name: impl ToString, value: impl SetData, mpv: &Mpv) {
if let Err(error) = mpv.set_property(&name.to_string(), value) { rpc_response_sender
eprintln!("cannot set MPV property: '{error:#}'") .send(RPCResponse::response_message(player_response.to_value()))
}; .expect("failed to send RPCResponse");
} }
})
// -- InMsg handler loop -- }
for msg in in_msg_receiver.iter() { fn create_message_thread(
let in_msg: InMsg = match serde_json::from_str(&msg) { mpv: Arc<Mpv>,
Ok(in_msg) => in_msg, observe_property_sender: Sender<ObserveProperty>,
Err(error) => { in_msg_receiver: Receiver<String>,
eprintln!("cannot parse InMsg: {error:#}"); ) -> JoinHandle<()> {
continue; thread::spawn(move || {
} // -- Helpers --
};
let observe_property = |name: String, format: Format| {
match in_msg { observe_property_sender
InMsg( .send(ObserveProperty { name, format })
InMsgFn::MpvObserveProp, .expect("cannot send ObserveProperty");
InMsgArgs::ObProp(PropKey::Bool(prop)), mpv.wake_up();
) => { };
observe_property(prop.to_string(), Format::Flag);
}, let send_command = |cmd: CmdVal| {
InMsg( let (name, arg) = match cmd {
InMsgFn::MpvObserveProp, CmdVal::Double(name, arg) => (name, format!(r#""{arg}""#)),
InMsgArgs::ObProp(PropKey::Int(prop)), CmdVal::Single((name,)) => (name, String::new()),
) => { };
observe_property(prop.to_string(), Format::Int64); if let Err(error) = mpv.command(&name.to_string(), &[&arg]) {
}, eprintln!("failed to execute MPV command: '{error:#}'")
InMsg( }
InMsgFn::MpvObserveProp, };
InMsgArgs::ObProp(PropKey::Fp(prop)),
) => { fn set_property(name: impl ToString, value: impl SetData, mpv: &Mpv) {
observe_property(prop.to_string(), Format::Double); if let Err(error) = mpv.set_property(&name.to_string(), value) {
}, eprintln!("cannot set MPV property: '{error:#}'")
InMsg( }
InMsgFn::MpvObserveProp, }
InMsgArgs::ObProp(PropKey::Str(prop)),
) => { // -- InMsg handler loop --
observe_property(prop.to_string(), Format::String);
}, for msg in in_msg_receiver.iter() {
InMsg( let in_msg: InMsg = match serde_json::from_str(&msg) {
InMsgFn::MpvSetProp, Ok(in_msg) => in_msg,
InMsgArgs::StProp(prop, PropVal::Bool(value)), Err(error) => {
) => { eprintln!("cannot parse InMsg: {error:#}");
set_property(prop, value, &mpv); continue;
} }
InMsg( };
InMsgFn::MpvSetProp,
InMsgArgs::StProp(prop, PropVal::Num(value)), match in_msg {
) => { InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Bool(prop))) => {
set_property(prop, value, &mpv); observe_property(prop.to_string(), Format::Flag);
} }
InMsg( InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Int(prop))) => {
InMsgFn::MpvSetProp, observe_property(prop.to_string(), Format::Int64);
InMsgArgs::StProp(prop, PropVal::Str(value)), }
) => { InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Fp(prop))) => {
set_property(prop, value, &mpv); observe_property(prop.to_string(), Format::Double);
} }
InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => { InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Str(prop))) => {
send_command(cmd); observe_property(prop.to_string(), Format::String);
} }
msg => { InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Bool(value))) => {
eprintln!("MPV unsupported message: '{msg:?}'"); 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);
}
trait MpvExt { InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => {
fn wake_up(&self); send_command(cmd);
} }
msg => {
impl MpvExt for Mpv { eprintln!("MPV unsupported message: '{msg:?}'");
// @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()) } }
} })
} }
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()) }
}
}

View file

@ -1,32 +1,32 @@
use std::process::Command; use std::os::windows::process::CommandExt;
use std::thread; use std::process::Command;
use std::time::Duration; use std::thread;
use win32job::Job; use std::time::Duration;
use std::os::windows::process::CommandExt; use win32job::Job;
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
pub struct StremioServer {} pub struct StremioServer {}
impl StremioServer { impl StremioServer {
pub fn new() -> StremioServer { pub fn new() -> StremioServer {
thread::spawn(move || { thread::spawn(move || {
let job = Job::create().expect("Cannont create job"); let job = Job::create().expect("Cannont create job");
let mut info = job.query_extended_limit_info().expect("Cannont get info"); let mut info = job.query_extended_limit_info().expect("Cannont get info");
info.limit_kill_on_job_close(); info.limit_kill_on_job_close();
job.set_extended_limit_info(&mut info).ok(); job.set_extended_limit_info(&mut info).ok();
job.assign_current_process().ok(); job.assign_current_process().ok();
loop { loop {
let mut child = Command::new("node") let mut child = Command::new("node")
.arg("server.js") .arg("server.js")
.creation_flags(CREATE_NO_WINDOW) .creation_flags(CREATE_NO_WINDOW)
.spawn() .spawn()
.expect("Cannot run the server"); .expect("Cannot run the server");
child.wait().expect("Cannot wait for the server"); child.wait().expect("Cannot wait for the server");
thread::sleep(Duration::from_millis(500)); thread::sleep(Duration::from_millis(500));
dbg!("Trying to restart the server..."); dbg!("Trying to restart the server...");
} }
}); });
StremioServer {} StremioServer {}
} }
} }