mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-05-14 02:50:47 +00:00
minor refactor + cargo fmt --all
This commit is contained in:
parent
81e088800b
commit
f10380f60a
6 changed files with 852 additions and 842 deletions
8
build.rs
8
build.rs
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue