mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-03-11 17:15:49 +00:00
Use Windows line endings for easier diff
This commit is contained in:
parent
55ea78721f
commit
7a7494f726
5 changed files with 782 additions and 782 deletions
|
|
@ -1,100 +1,100 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{self, json};
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub type Channel = RefCell<Option<(flume::Sender<String>, flume::Receiver<String>)>>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCRequest {
|
||||
pub id: u64,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl RPCRequest {
|
||||
pub fn is_handshake(&self) -> bool {
|
||||
self.id == 0
|
||||
}
|
||||
pub fn get_method(&self) -> Option<&str> {
|
||||
self.args
|
||||
.as_ref()
|
||||
.and_then(|args| args.first())
|
||||
.and_then(|arg| arg.as_str())
|
||||
}
|
||||
pub fn get_params(&self) -> Option<&serde_json::Value> {
|
||||
self.args
|
||||
.as_ref()
|
||||
.and_then(|args| if args.len() > 1 { Some(&args[1]) } else { None })
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponseDataTransport {
|
||||
pub properties: Vec<Vec<String>>,
|
||||
pub signals: Vec<String>,
|
||||
pub methods: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponseData {
|
||||
pub transport: RPCResponseDataTransport,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponse {
|
||||
pub id: u64,
|
||||
pub object: String,
|
||||
#[serde(rename = "type")]
|
||||
pub response_type: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<RPCResponseData>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl RPCResponse {
|
||||
pub fn get_handshake() -> String {
|
||||
let resp = RPCResponse {
|
||||
id: 0,
|
||||
object: "transport".to_string(),
|
||||
response_type: 3,
|
||||
data: Some(RPCResponseData {
|
||||
transport: RPCResponseDataTransport {
|
||||
properties: vec![
|
||||
vec![],
|
||||
vec![
|
||||
"".to_string(),
|
||||
"shellVersion".to_string(),
|
||||
"".to_string(),
|
||||
"5.0.0".to_string(),
|
||||
],
|
||||
],
|
||||
signals: vec![],
|
||||
methods: vec![vec!["onEvent".to_string(), "".to_string()]],
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
serde_json::to_string(&resp).expect("Cannot build response")
|
||||
}
|
||||
pub fn response_message(msg: Option<serde_json::Value>) -> String {
|
||||
let resp = RPCResponse {
|
||||
id: 1,
|
||||
object: "transport".to_string(),
|
||||
response_type: 1,
|
||||
args: msg,
|
||||
..Default::default()
|
||||
};
|
||||
serde_json::to_string(&resp).expect("Cannot build response")
|
||||
}
|
||||
pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String {
|
||||
Self::response_message(Some(json!(["win-visibility-changed" ,{
|
||||
"visible": visible,
|
||||
"visibility": visibility,
|
||||
"isFullscreen": is_full_screen
|
||||
}])))
|
||||
}
|
||||
pub fn state_change(state: u32) -> String {
|
||||
Self::response_message(Some(json!(["win-state-changed" ,{
|
||||
"state": state,
|
||||
}])))
|
||||
}
|
||||
}
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{self, json};
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub type Channel = RefCell<Option<(flume::Sender<String>, flume::Receiver<String>)>>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCRequest {
|
||||
pub id: u64,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl RPCRequest {
|
||||
pub fn is_handshake(&self) -> bool {
|
||||
self.id == 0
|
||||
}
|
||||
pub fn get_method(&self) -> Option<&str> {
|
||||
self.args
|
||||
.as_ref()
|
||||
.and_then(|args| args.first())
|
||||
.and_then(|arg| arg.as_str())
|
||||
}
|
||||
pub fn get_params(&self) -> Option<&serde_json::Value> {
|
||||
self.args
|
||||
.as_ref()
|
||||
.and_then(|args| if args.len() > 1 { Some(&args[1]) } else { None })
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponseDataTransport {
|
||||
pub properties: Vec<Vec<String>>,
|
||||
pub signals: Vec<String>,
|
||||
pub methods: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponseData {
|
||||
pub transport: RPCResponseDataTransport,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RPCResponse {
|
||||
pub id: u64,
|
||||
pub object: String,
|
||||
#[serde(rename = "type")]
|
||||
pub response_type: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<RPCResponseData>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl RPCResponse {
|
||||
pub fn get_handshake() -> String {
|
||||
let resp = RPCResponse {
|
||||
id: 0,
|
||||
object: "transport".to_string(),
|
||||
response_type: 3,
|
||||
data: Some(RPCResponseData {
|
||||
transport: RPCResponseDataTransport {
|
||||
properties: vec![
|
||||
vec![],
|
||||
vec![
|
||||
"".to_string(),
|
||||
"shellVersion".to_string(),
|
||||
"".to_string(),
|
||||
"5.0.0".to_string(),
|
||||
],
|
||||
],
|
||||
signals: vec![],
|
||||
methods: vec![vec!["onEvent".to_string(), "".to_string()]],
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
serde_json::to_string(&resp).expect("Cannot build response")
|
||||
}
|
||||
pub fn response_message(msg: Option<serde_json::Value>) -> String {
|
||||
let resp = RPCResponse {
|
||||
id: 1,
|
||||
object: "transport".to_string(),
|
||||
response_type: 1,
|
||||
args: msg,
|
||||
..Default::default()
|
||||
};
|
||||
serde_json::to_string(&resp).expect("Cannot build response")
|
||||
}
|
||||
pub fn visibility_change(visible: bool, visibility: u32, is_full_screen: bool) -> String {
|
||||
Self::response_message(Some(json!(["win-visibility-changed" ,{
|
||||
"visible": visible,
|
||||
"visibility": visibility,
|
||||
"isFullscreen": is_full_screen
|
||||
}])))
|
||||
}
|
||||
pub fn state_change(state: u32) -> String {
|
||||
Self::response_message(Some(json!(["win-state-changed" ,{
|
||||
"state": state,
|
||||
}])))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,252 +1,252 @@
|
|||
use core::convert::TryFrom;
|
||||
use libmpv::{events::PropertyData, mpv_end_file_reason, EndFileReason};
|
||||
use parse_display::{Display, FromStr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
// Responses
|
||||
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PlayerProprChange {
|
||||
name: String,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
impl PlayerProprChange {
|
||||
fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value {
|
||||
match data {
|
||||
PropertyData::Flag(d) => serde_json::Value::Bool(d),
|
||||
PropertyData::Int64(d) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"),
|
||||
),
|
||||
PropertyData::Double(d) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(d).expect("MPV returned invalid number"),
|
||||
),
|
||||
PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()),
|
||||
PropertyData::Str(s) => {
|
||||
if as_json {
|
||||
serde_json::from_str(s).expect("MPV returned invalid JSON data")
|
||||
} else {
|
||||
serde_json::Value::String(s.to_string())
|
||||
}
|
||||
}
|
||||
PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"),
|
||||
}
|
||||
}
|
||||
pub fn from_name_value(name: String, value: PropertyData) -> Self {
|
||||
let is_json = JSON_RESPONSES.contains(&name.as_str());
|
||||
Self {
|
||||
name,
|
||||
data: Self::value_from_format(value, is_json),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PlayerEnded {
|
||||
reason: String,
|
||||
}
|
||||
impl PlayerEnded {
|
||||
fn string_from_end_reason(data: EndFileReason) -> String {
|
||||
match data {
|
||||
mpv_end_file_reason::Error => "error".to_string(),
|
||||
mpv_end_file_reason::Quit => "quit".to_string(),
|
||||
_ => "other".to_string(),
|
||||
}
|
||||
}
|
||||
pub fn from_end_reason(data: EndFileReason) -> Self {
|
||||
Self {
|
||||
reason: Self::string_from_end_reason(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlayerError {
|
||||
pub error: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum PlayerEvent {
|
||||
PropChange(PlayerProprChange),
|
||||
End(PlayerEnded),
|
||||
Error(PlayerError),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
|
||||
impl PlayerResponse<'_> {
|
||||
pub fn to_value(&self) -> Option<serde_json::Value> {
|
||||
serde_json::to_value(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
// Player incoming messages from the web UI
|
||||
/*
|
||||
Message general case - ["function-name", ["arguments", ...]]
|
||||
The function could be either mpv-observe-prop, mpv-set-prop or mpv-command.
|
||||
|
||||
["mpv-observe-prop", "prop-name"]
|
||||
["mpv-set-prop", ["prop-name", prop-val]]
|
||||
["mpv-command", ["command-name"<, "arguments">]]
|
||||
|
||||
All the function and property names are in kebab-case.
|
||||
|
||||
MPV requires type for any prop-name when observing or setting it's value.
|
||||
The type for setting is not always the same as the type for observing the prop.
|
||||
|
||||
"mpv-observe-prop" function is the only one that accepts single string
|
||||
instead of array of arguments
|
||||
|
||||
"mpv-command" function always takes an array even if the command doesn't
|
||||
have any arguments. For example this are the commands we support:
|
||||
|
||||
["mpv-command", ["loadfile", "file name"]]
|
||||
["mpv-command", ["stop"]]
|
||||
*/
|
||||
macro_rules! stringable {
|
||||
($t:ident) => {
|
||||
impl From<$t> for String {
|
||||
fn from(s: $t) -> Self {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
impl TryFrom<String> for $t {
|
||||
type Error = parse_display::ParseError;
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
s.parse()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum InMsgFn {
|
||||
MpvSetProp,
|
||||
MpvCommand,
|
||||
MpvObserveProp,
|
||||
}
|
||||
stringable!(InMsgFn);
|
||||
// Bool
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum BoolProp {
|
||||
Pause,
|
||||
PausedForCache,
|
||||
Seeking,
|
||||
EofReached,
|
||||
}
|
||||
stringable!(BoolProp);
|
||||
// Int
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum IntProp {
|
||||
Aid,
|
||||
Vid,
|
||||
Sid,
|
||||
}
|
||||
stringable!(IntProp);
|
||||
// Fp
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum FpProp {
|
||||
TimePos,
|
||||
Volume,
|
||||
Duration,
|
||||
SubScale,
|
||||
CacheBufferingState,
|
||||
SubPos,
|
||||
Speed,
|
||||
}
|
||||
stringable!(FpProp);
|
||||
// Str
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum StrProp {
|
||||
FfmpegVersion,
|
||||
Hwdec,
|
||||
InputDefaltBindings,
|
||||
InputVoKeyboard,
|
||||
Metadata,
|
||||
MpvVersion,
|
||||
Osc,
|
||||
Path,
|
||||
SubAssOverride,
|
||||
SubBackColor,
|
||||
SubBorderColor,
|
||||
SubColor,
|
||||
TrackList,
|
||||
VideoParams,
|
||||
// Vo,
|
||||
}
|
||||
stringable!(StrProp);
|
||||
|
||||
// Any
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PropKey {
|
||||
Bool(BoolProp),
|
||||
Int(IntProp),
|
||||
Fp(FpProp),
|
||||
Str(StrProp),
|
||||
}
|
||||
impl fmt::Display for PropKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Bool(v) => write!(f, "{}", v),
|
||||
Self::Int(v) => write!(f, "{}", v),
|
||||
Self::Fp(v) => write!(f, "{}", v),
|
||||
Self::Str(v) => write!(f, "{}", v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PropVal {
|
||||
Bool(bool),
|
||||
Str(String),
|
||||
Num(f64),
|
||||
}
|
||||
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
#[serde(untagged)]
|
||||
pub enum MpvCmd {
|
||||
Loadfile,
|
||||
Stop,
|
||||
}
|
||||
stringable!(MpvCmd);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum CmdVal {
|
||||
Single((MpvCmd,)),
|
||||
Double(MpvCmd, String),
|
||||
}
|
||||
impl From<CmdVal> for Vec<String> {
|
||||
fn from(cmd: CmdVal) -> Vec<String> {
|
||||
match cmd {
|
||||
CmdVal::Single(cmd) => vec![cmd.0.to_string()],
|
||||
CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum InMsgArgs {
|
||||
StProp(PropKey, PropVal),
|
||||
Cmd(CmdVal),
|
||||
ObProp(PropKey),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct InMsg(pub InMsgFn, pub InMsgArgs);
|
||||
use core::convert::TryFrom;
|
||||
use libmpv::{events::PropertyData, mpv_end_file_reason, EndFileReason};
|
||||
use parse_display::{Display, FromStr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
// Responses
|
||||
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PlayerProprChange {
|
||||
name: String,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
impl PlayerProprChange {
|
||||
fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value {
|
||||
match data {
|
||||
PropertyData::Flag(d) => serde_json::Value::Bool(d),
|
||||
PropertyData::Int64(d) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"),
|
||||
),
|
||||
PropertyData::Double(d) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(d).expect("MPV returned invalid number"),
|
||||
),
|
||||
PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()),
|
||||
PropertyData::Str(s) => {
|
||||
if as_json {
|
||||
serde_json::from_str(s).expect("MPV returned invalid JSON data")
|
||||
} else {
|
||||
serde_json::Value::String(s.to_string())
|
||||
}
|
||||
}
|
||||
PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"),
|
||||
}
|
||||
}
|
||||
pub fn from_name_value(name: String, value: PropertyData) -> Self {
|
||||
let is_json = JSON_RESPONSES.contains(&name.as_str());
|
||||
Self {
|
||||
name,
|
||||
data: Self::value_from_format(value, is_json),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PlayerEnded {
|
||||
reason: String,
|
||||
}
|
||||
impl PlayerEnded {
|
||||
fn string_from_end_reason(data: EndFileReason) -> String {
|
||||
match data {
|
||||
mpv_end_file_reason::Error => "error".to_string(),
|
||||
mpv_end_file_reason::Quit => "quit".to_string(),
|
||||
_ => "other".to_string(),
|
||||
}
|
||||
}
|
||||
pub fn from_end_reason(data: EndFileReason) -> Self {
|
||||
Self {
|
||||
reason: Self::string_from_end_reason(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlayerError {
|
||||
pub error: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum PlayerEvent {
|
||||
PropChange(PlayerProprChange),
|
||||
End(PlayerEnded),
|
||||
Error(PlayerError),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
|
||||
impl PlayerResponse<'_> {
|
||||
pub fn to_value(&self) -> Option<serde_json::Value> {
|
||||
serde_json::to_value(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
// Player incoming messages from the web UI
|
||||
/*
|
||||
Message general case - ["function-name", ["arguments", ...]]
|
||||
The function could be either mpv-observe-prop, mpv-set-prop or mpv-command.
|
||||
|
||||
["mpv-observe-prop", "prop-name"]
|
||||
["mpv-set-prop", ["prop-name", prop-val]]
|
||||
["mpv-command", ["command-name"<, "arguments">]]
|
||||
|
||||
All the function and property names are in kebab-case.
|
||||
|
||||
MPV requires type for any prop-name when observing or setting it's value.
|
||||
The type for setting is not always the same as the type for observing the prop.
|
||||
|
||||
"mpv-observe-prop" function is the only one that accepts single string
|
||||
instead of array of arguments
|
||||
|
||||
"mpv-command" function always takes an array even if the command doesn't
|
||||
have any arguments. For example this are the commands we support:
|
||||
|
||||
["mpv-command", ["loadfile", "file name"]]
|
||||
["mpv-command", ["stop"]]
|
||||
*/
|
||||
macro_rules! stringable {
|
||||
($t:ident) => {
|
||||
impl From<$t> for String {
|
||||
fn from(s: $t) -> Self {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
impl TryFrom<String> for $t {
|
||||
type Error = parse_display::ParseError;
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
s.parse()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum InMsgFn {
|
||||
MpvSetProp,
|
||||
MpvCommand,
|
||||
MpvObserveProp,
|
||||
}
|
||||
stringable!(InMsgFn);
|
||||
// Bool
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum BoolProp {
|
||||
Pause,
|
||||
PausedForCache,
|
||||
Seeking,
|
||||
EofReached,
|
||||
}
|
||||
stringable!(BoolProp);
|
||||
// Int
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum IntProp {
|
||||
Aid,
|
||||
Vid,
|
||||
Sid,
|
||||
}
|
||||
stringable!(IntProp);
|
||||
// Fp
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum FpProp {
|
||||
TimePos,
|
||||
Volume,
|
||||
Duration,
|
||||
SubScale,
|
||||
CacheBufferingState,
|
||||
SubPos,
|
||||
Speed,
|
||||
}
|
||||
stringable!(FpProp);
|
||||
// Str
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum StrProp {
|
||||
FfmpegVersion,
|
||||
Hwdec,
|
||||
InputDefaltBindings,
|
||||
InputVoKeyboard,
|
||||
Metadata,
|
||||
MpvVersion,
|
||||
Osc,
|
||||
Path,
|
||||
SubAssOverride,
|
||||
SubBackColor,
|
||||
SubBorderColor,
|
||||
SubColor,
|
||||
TrackList,
|
||||
VideoParams,
|
||||
// Vo,
|
||||
}
|
||||
stringable!(StrProp);
|
||||
|
||||
// Any
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PropKey {
|
||||
Bool(BoolProp),
|
||||
Int(IntProp),
|
||||
Fp(FpProp),
|
||||
Str(StrProp),
|
||||
}
|
||||
impl fmt::Display for PropKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Bool(v) => write!(f, "{}", v),
|
||||
Self::Int(v) => write!(f, "{}", v),
|
||||
Self::Fp(v) => write!(f, "{}", v),
|
||||
Self::Str(v) => write!(f, "{}", v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PropVal {
|
||||
Bool(bool),
|
||||
Str(String),
|
||||
Num(f64),
|
||||
}
|
||||
|
||||
#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[display(style = "kebab-case")]
|
||||
#[serde(untagged)]
|
||||
pub enum MpvCmd {
|
||||
Loadfile,
|
||||
Stop,
|
||||
}
|
||||
stringable!(MpvCmd);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum CmdVal {
|
||||
Single((MpvCmd,)),
|
||||
Double(MpvCmd, String),
|
||||
}
|
||||
impl From<CmdVal> for Vec<String> {
|
||||
fn from(cmd: CmdVal) -> Vec<String> {
|
||||
match cmd {
|
||||
CmdVal::Single(cmd) => vec![cmd.0.to_string()],
|
||||
CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum InMsgArgs {
|
||||
StProp(PropKey, PropVal),
|
||||
Cmd(CmdVal),
|
||||
ObProp(PropKey),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct InMsg(pub InMsgFn, pub InMsgArgs);
|
||||
|
|
|
|||
|
|
@ -1,167 +1,167 @@
|
|||
use crate::stremio_app::stremio_player::communication::{
|
||||
BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey,
|
||||
PropVal,
|
||||
};
|
||||
use libmpv::{events::PropertyData, mpv_end_file_reason};
|
||||
|
||||
use serde_test::{assert_tokens, Token};
|
||||
|
||||
#[test]
|
||||
fn propr_change_tokens() {
|
||||
let prop = "test-prop";
|
||||
let tokens: [Token; 6] = [
|
||||
Token::Struct {
|
||||
name: "PlayerProprChange",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("name"),
|
||||
Token::None,
|
||||
Token::Str("data"),
|
||||
Token::None,
|
||||
Token::StructEnd,
|
||||
];
|
||||
|
||||
fn tokens_by_type(tokens: &[Token; 6], name: &'static str, val: PropertyData, token: Token) {
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str(name);
|
||||
typed_tokens[4] = token;
|
||||
assert_tokens(
|
||||
&PlayerProprChange::from_name_value(name.to_string(), val),
|
||||
&typed_tokens,
|
||||
);
|
||||
}
|
||||
tokens_by_type(&tokens, prop, PropertyData::Flag(true), Token::Bool(true));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Int64(1), Token::F64(1.0));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Double(1.0), Token::F64(1.0));
|
||||
tokens_by_type(&tokens, prop, PropertyData::OsdStr("ok"), Token::Str("ok"));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Str("ok"), Token::Str("ok"));
|
||||
|
||||
// JSON response
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"track-list",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"video-params",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"metadata",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ended_tokens() {
|
||||
let tokens: [Token; 4] = [
|
||||
Token::Struct {
|
||||
name: "PlayerEnded",
|
||||
len: 1,
|
||||
},
|
||||
Token::Str("reason"),
|
||||
Token::None,
|
||||
Token::StructEnd,
|
||||
];
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str("error");
|
||||
assert_tokens(
|
||||
&PlayerEnded::from_end_reason(mpv_end_file_reason::Error),
|
||||
&typed_tokens,
|
||||
);
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str("quit");
|
||||
assert_tokens(
|
||||
&PlayerEnded::from_end_reason(mpv_end_file_reason::Quit),
|
||||
&typed_tokens,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ob_propr_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvObserveProp,
|
||||
InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-observe-prop"),
|
||||
Token::Str("pause"),
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_propr_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvSetProp,
|
||||
InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-set-prop"),
|
||||
Token::Tuple { len: 2 },
|
||||
Token::Str("pause"),
|
||||
Token::Bool(true),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_stop_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvCommand,
|
||||
InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-command"),
|
||||
Token::Tuple { len: 1 },
|
||||
Token::Str("stop"),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_loadfile_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvCommand,
|
||||
InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-command"),
|
||||
Token::Tuple { len: 2 },
|
||||
Token::Str("loadfile"),
|
||||
Token::Str("some_file"),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
use crate::stremio_app::stremio_player::communication::{
|
||||
BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey,
|
||||
PropVal,
|
||||
};
|
||||
use libmpv::{events::PropertyData, mpv_end_file_reason};
|
||||
|
||||
use serde_test::{assert_tokens, Token};
|
||||
|
||||
#[test]
|
||||
fn propr_change_tokens() {
|
||||
let prop = "test-prop";
|
||||
let tokens: [Token; 6] = [
|
||||
Token::Struct {
|
||||
name: "PlayerProprChange",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("name"),
|
||||
Token::None,
|
||||
Token::Str("data"),
|
||||
Token::None,
|
||||
Token::StructEnd,
|
||||
];
|
||||
|
||||
fn tokens_by_type(tokens: &[Token; 6], name: &'static str, val: PropertyData, token: Token) {
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str(name);
|
||||
typed_tokens[4] = token;
|
||||
assert_tokens(
|
||||
&PlayerProprChange::from_name_value(name.to_string(), val),
|
||||
&typed_tokens,
|
||||
);
|
||||
}
|
||||
tokens_by_type(&tokens, prop, PropertyData::Flag(true), Token::Bool(true));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Int64(1), Token::F64(1.0));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Double(1.0), Token::F64(1.0));
|
||||
tokens_by_type(&tokens, prop, PropertyData::OsdStr("ok"), Token::Str("ok"));
|
||||
tokens_by_type(&tokens, prop, PropertyData::Str("ok"), Token::Str("ok"));
|
||||
|
||||
// JSON response
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"track-list",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"video-params",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
tokens_by_type(
|
||||
&tokens,
|
||||
"metadata",
|
||||
PropertyData::Str(r#""ok""#),
|
||||
Token::Str("ok"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ended_tokens() {
|
||||
let tokens: [Token; 4] = [
|
||||
Token::Struct {
|
||||
name: "PlayerEnded",
|
||||
len: 1,
|
||||
},
|
||||
Token::Str("reason"),
|
||||
Token::None,
|
||||
Token::StructEnd,
|
||||
];
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str("error");
|
||||
assert_tokens(
|
||||
&PlayerEnded::from_end_reason(mpv_end_file_reason::Error),
|
||||
&typed_tokens,
|
||||
);
|
||||
let mut typed_tokens = tokens.clone();
|
||||
typed_tokens[2] = Token::Str("quit");
|
||||
assert_tokens(
|
||||
&PlayerEnded::from_end_reason(mpv_end_file_reason::Quit),
|
||||
&typed_tokens,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ob_propr_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvObserveProp,
|
||||
InMsgArgs::ObProp(PropKey::Bool(BoolProp::Pause)),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-observe-prop"),
|
||||
Token::Str("pause"),
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_propr_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvSetProp,
|
||||
InMsgArgs::StProp(PropKey::Bool(BoolProp::Pause), PropVal::Bool(true)),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-set-prop"),
|
||||
Token::Tuple { len: 2 },
|
||||
Token::Str("pause"),
|
||||
Token::Bool(true),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_stop_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvCommand,
|
||||
InMsgArgs::Cmd(CmdVal::Single((MpvCmd::Stop,))),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-command"),
|
||||
Token::Tuple { len: 1 },
|
||||
Token::Str("stop"),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_loadfile_tokens() {
|
||||
assert_tokens(
|
||||
&InMsg(
|
||||
InMsgFn::MpvCommand,
|
||||
InMsgArgs::Cmd(CmdVal::Double(MpvCmd::Loadfile, "some_file".to_string())),
|
||||
),
|
||||
&[
|
||||
Token::TupleStruct {
|
||||
name: "InMsg",
|
||||
len: 2,
|
||||
},
|
||||
Token::Str("mpv-command"),
|
||||
Token::Tuple { len: 2 },
|
||||
Token::Str("loadfile"),
|
||||
Token::Str("some_file"),
|
||||
Token::TupleEnd,
|
||||
Token::TupleStructEnd,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,231 +1,231 @@
|
|||
use crate::stremio_app::ipc;
|
||||
use crate::stremio_app::RPCResponse;
|
||||
use flume::{Receiver, Sender};
|
||||
use libmpv::{events::Event, Format, Mpv, SetData};
|
||||
use native_windows_gui::{self as nwg, PartialUi};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use winapi::shared::windef::HWND;
|
||||
|
||||
use crate::stremio_app::stremio_player::{
|
||||
CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
|
||||
PropKey, PropVal,
|
||||
};
|
||||
|
||||
struct ObserveProperty {
|
||||
name: String,
|
||||
format: Format,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Player {
|
||||
pub channel: ipc::Channel,
|
||||
}
|
||||
|
||||
impl PartialUi for Player {
|
||||
fn build_partial<W: Into<nwg::ControlHandle>>(
|
||||
// @TODO replace with `&mut self`?
|
||||
data: &mut Self,
|
||||
parent: Option<W>,
|
||||
) -> Result<(), nwg::NwgError> {
|
||||
// @TODO replace all `expect`s with proper error handling?
|
||||
|
||||
let window_handle = parent
|
||||
.expect("no parent window")
|
||||
.into()
|
||||
.hwnd()
|
||||
.expect("cannot obtain window handle");
|
||||
|
||||
let (in_msg_sender, in_msg_receiver) = flume::unbounded();
|
||||
let (rpc_response_sender, rpc_response_receiver) = flume::unbounded();
|
||||
let (observe_property_sender, observe_property_receiver) = flume::unbounded();
|
||||
data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver)));
|
||||
|
||||
let mpv = create_shareable_mpv(window_handle);
|
||||
|
||||
let _event_thread = create_event_thread(
|
||||
Arc::clone(&mpv),
|
||||
observe_property_receiver,
|
||||
rpc_response_sender,
|
||||
);
|
||||
let _message_thread = create_message_thread(mpv, observe_property_sender, in_msg_receiver);
|
||||
// @TODO implement a mechanism to stop threads on `Player` drop if needed
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_shareable_mpv(window_handle: HWND) -> Arc<Mpv> {
|
||||
let mpv = Mpv::with_initializer(|initializer| {
|
||||
macro_rules! set_property {
|
||||
($name:literal, $value:expr) => {
|
||||
initializer
|
||||
.set_property($name, $value)
|
||||
.expect(concat!("failed to set ", $name));
|
||||
};
|
||||
}
|
||||
set_property!("wid", window_handle as i64);
|
||||
// initializer.set_property("vo", "gpu").expect("unable to set vo");
|
||||
// win, opengl: works but least performancy, 10-15% CPU
|
||||
// winvk, vulkan: works as good as d3d11
|
||||
// d3d11, d1d11: works great
|
||||
// dxinterop, auto: works, slightly more cpu use than d3d11
|
||||
// default (auto) seems to be d3d11 (vo/gpu/d3d11)
|
||||
set_property!("gpu-context", "angle");
|
||||
set_property!("gpu-api", "auto");
|
||||
set_property!("title", "Stremio");
|
||||
set_property!("terminal", "yes");
|
||||
set_property!("msg-level", "all=no,cplayer=debug");
|
||||
set_property!("quiet", "yes");
|
||||
set_property!("hwdec", "auto");
|
||||
// FIXME: very often the audio track isn't selected when using "aid" = "auto"
|
||||
set_property!("aid", "1");
|
||||
Ok(())
|
||||
});
|
||||
Arc::new(mpv.expect("cannot build MPV"))
|
||||
}
|
||||
|
||||
fn create_event_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
observe_property_receiver: Receiver<ObserveProperty>,
|
||||
rpc_response_sender: Sender<String>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut event_context = mpv.create_event_context();
|
||||
event_context
|
||||
.disable_deprecated_events()
|
||||
.expect("failed to disable deprecated MPV events");
|
||||
|
||||
// -- Event handler loop --
|
||||
|
||||
loop {
|
||||
for ObserveProperty { name, format } in observe_property_receiver.drain() {
|
||||
event_context
|
||||
.observe_property(&name, format, 0)
|
||||
.expect("failed to observer MPV property");
|
||||
}
|
||||
|
||||
// -1.0 means to block and wait for an event.
|
||||
let event = match event_context.wait_event(-1.) {
|
||||
Some(Ok(event)) => event,
|
||||
Some(Err(error)) => {
|
||||
eprintln!("Event errored: {error:?}");
|
||||
continue;
|
||||
}
|
||||
// dummy event received (may be created on a wake up call or on timeout)
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// even if you don't do anything with the events, it is still necessary to empty the event loop
|
||||
let player_response = match event {
|
||||
Event::PropertyChange { name, change, .. } => PlayerResponse(
|
||||
"mpv-prop-change",
|
||||
PlayerEvent::PropChange(PlayerProprChange::from_name_value(
|
||||
name.to_string(),
|
||||
change,
|
||||
)),
|
||||
),
|
||||
Event::EndFile(reason) => PlayerResponse(
|
||||
"mpv-event-ended",
|
||||
PlayerEvent::End(PlayerEnded::from_end_reason(reason)),
|
||||
),
|
||||
Event::Shutdown => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
rpc_response_sender
|
||||
.send(RPCResponse::response_message(player_response.to_value()))
|
||||
.expect("failed to send RPCResponse");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
observe_property_sender: Sender<ObserveProperty>,
|
||||
in_msg_receiver: Receiver<String>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
// -- Helpers --
|
||||
|
||||
let observe_property = |name: String, format: Format| {
|
||||
observe_property_sender
|
||||
.send(ObserveProperty { name, format })
|
||||
.expect("cannot send ObserveProperty");
|
||||
mpv.wake_up();
|
||||
};
|
||||
|
||||
let send_command = |cmd: CmdVal| {
|
||||
let (name, arg) = match cmd {
|
||||
CmdVal::Double(name, arg) => (name, format!(r#""{arg}""#)),
|
||||
CmdVal::Single((name,)) => (name, String::new()),
|
||||
};
|
||||
if let Err(error) = mpv.command(&name.to_string(), &[&arg]) {
|
||||
eprintln!("failed to execute MPV command: '{error:#}'")
|
||||
}
|
||||
};
|
||||
|
||||
fn set_property(name: impl ToString, value: impl SetData, mpv: &Mpv) {
|
||||
if let Err(error) = mpv.set_property(&name.to_string(), value) {
|
||||
eprintln!("cannot set MPV property: '{error:#}'")
|
||||
}
|
||||
}
|
||||
|
||||
// -- InMsg handler loop --
|
||||
|
||||
for msg in in_msg_receiver.iter() {
|
||||
let in_msg: InMsg = match serde_json::from_str(&msg) {
|
||||
Ok(in_msg) => in_msg,
|
||||
Err(error) => {
|
||||
eprintln!("cannot parse InMsg: {error:#}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match in_msg {
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Bool(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Flag);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Int(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Int64);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Fp(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Double);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Str(prop))) => {
|
||||
observe_property(prop.to_string(), Format::String);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Bool(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Num(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Str(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => {
|
||||
send_command(cmd);
|
||||
}
|
||||
msg => {
|
||||
eprintln!("MPV unsupported message: '{msg:?}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
trait MpvExt {
|
||||
fn wake_up(&self);
|
||||
}
|
||||
|
||||
impl MpvExt for Mpv {
|
||||
// @TODO create a PR to the `libmpv` crate and then remove `libmpv-sys` from Cargo.toml?
|
||||
fn wake_up(&self) {
|
||||
unsafe { libmpv_sys::mpv_wakeup(self.ctx.as_ptr()) }
|
||||
}
|
||||
}
|
||||
use crate::stremio_app::ipc;
|
||||
use crate::stremio_app::RPCResponse;
|
||||
use flume::{Receiver, Sender};
|
||||
use libmpv::{events::Event, Format, Mpv, SetData};
|
||||
use native_windows_gui::{self as nwg, PartialUi};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use winapi::shared::windef::HWND;
|
||||
|
||||
use crate::stremio_app::stremio_player::{
|
||||
CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
|
||||
PropKey, PropVal,
|
||||
};
|
||||
|
||||
struct ObserveProperty {
|
||||
name: String,
|
||||
format: Format,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Player {
|
||||
pub channel: ipc::Channel,
|
||||
}
|
||||
|
||||
impl PartialUi for Player {
|
||||
fn build_partial<W: Into<nwg::ControlHandle>>(
|
||||
// @TODO replace with `&mut self`?
|
||||
data: &mut Self,
|
||||
parent: Option<W>,
|
||||
) -> Result<(), nwg::NwgError> {
|
||||
// @TODO replace all `expect`s with proper error handling?
|
||||
|
||||
let window_handle = parent
|
||||
.expect("no parent window")
|
||||
.into()
|
||||
.hwnd()
|
||||
.expect("cannot obtain window handle");
|
||||
|
||||
let (in_msg_sender, in_msg_receiver) = flume::unbounded();
|
||||
let (rpc_response_sender, rpc_response_receiver) = flume::unbounded();
|
||||
let (observe_property_sender, observe_property_receiver) = flume::unbounded();
|
||||
data.channel = ipc::Channel::new(Some((in_msg_sender, rpc_response_receiver)));
|
||||
|
||||
let mpv = create_shareable_mpv(window_handle);
|
||||
|
||||
let _event_thread = create_event_thread(
|
||||
Arc::clone(&mpv),
|
||||
observe_property_receiver,
|
||||
rpc_response_sender,
|
||||
);
|
||||
let _message_thread = create_message_thread(mpv, observe_property_sender, in_msg_receiver);
|
||||
// @TODO implement a mechanism to stop threads on `Player` drop if needed
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_shareable_mpv(window_handle: HWND) -> Arc<Mpv> {
|
||||
let mpv = Mpv::with_initializer(|initializer| {
|
||||
macro_rules! set_property {
|
||||
($name:literal, $value:expr) => {
|
||||
initializer
|
||||
.set_property($name, $value)
|
||||
.expect(concat!("failed to set ", $name));
|
||||
};
|
||||
}
|
||||
set_property!("wid", window_handle as i64);
|
||||
// initializer.set_property("vo", "gpu").expect("unable to set vo");
|
||||
// win, opengl: works but least performancy, 10-15% CPU
|
||||
// winvk, vulkan: works as good as d3d11
|
||||
// d3d11, d1d11: works great
|
||||
// dxinterop, auto: works, slightly more cpu use than d3d11
|
||||
// default (auto) seems to be d3d11 (vo/gpu/d3d11)
|
||||
set_property!("gpu-context", "angle");
|
||||
set_property!("gpu-api", "auto");
|
||||
set_property!("title", "Stremio");
|
||||
set_property!("terminal", "yes");
|
||||
set_property!("msg-level", "all=no,cplayer=debug");
|
||||
set_property!("quiet", "yes");
|
||||
set_property!("hwdec", "auto");
|
||||
// FIXME: very often the audio track isn't selected when using "aid" = "auto"
|
||||
set_property!("aid", "1");
|
||||
Ok(())
|
||||
});
|
||||
Arc::new(mpv.expect("cannot build MPV"))
|
||||
}
|
||||
|
||||
fn create_event_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
observe_property_receiver: Receiver<ObserveProperty>,
|
||||
rpc_response_sender: Sender<String>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut event_context = mpv.create_event_context();
|
||||
event_context
|
||||
.disable_deprecated_events()
|
||||
.expect("failed to disable deprecated MPV events");
|
||||
|
||||
// -- Event handler loop --
|
||||
|
||||
loop {
|
||||
for ObserveProperty { name, format } in observe_property_receiver.drain() {
|
||||
event_context
|
||||
.observe_property(&name, format, 0)
|
||||
.expect("failed to observer MPV property");
|
||||
}
|
||||
|
||||
// -1.0 means to block and wait for an event.
|
||||
let event = match event_context.wait_event(-1.) {
|
||||
Some(Ok(event)) => event,
|
||||
Some(Err(error)) => {
|
||||
eprintln!("Event errored: {error:?}");
|
||||
continue;
|
||||
}
|
||||
// dummy event received (may be created on a wake up call or on timeout)
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// even if you don't do anything with the events, it is still necessary to empty the event loop
|
||||
let player_response = match event {
|
||||
Event::PropertyChange { name, change, .. } => PlayerResponse(
|
||||
"mpv-prop-change",
|
||||
PlayerEvent::PropChange(PlayerProprChange::from_name_value(
|
||||
name.to_string(),
|
||||
change,
|
||||
)),
|
||||
),
|
||||
Event::EndFile(reason) => PlayerResponse(
|
||||
"mpv-event-ended",
|
||||
PlayerEvent::End(PlayerEnded::from_end_reason(reason)),
|
||||
),
|
||||
Event::Shutdown => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
rpc_response_sender
|
||||
.send(RPCResponse::response_message(player_response.to_value()))
|
||||
.expect("failed to send RPCResponse");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_message_thread(
|
||||
mpv: Arc<Mpv>,
|
||||
observe_property_sender: Sender<ObserveProperty>,
|
||||
in_msg_receiver: Receiver<String>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
// -- Helpers --
|
||||
|
||||
let observe_property = |name: String, format: Format| {
|
||||
observe_property_sender
|
||||
.send(ObserveProperty { name, format })
|
||||
.expect("cannot send ObserveProperty");
|
||||
mpv.wake_up();
|
||||
};
|
||||
|
||||
let send_command = |cmd: CmdVal| {
|
||||
let (name, arg) = match cmd {
|
||||
CmdVal::Double(name, arg) => (name, format!(r#""{arg}""#)),
|
||||
CmdVal::Single((name,)) => (name, String::new()),
|
||||
};
|
||||
if let Err(error) = mpv.command(&name.to_string(), &[&arg]) {
|
||||
eprintln!("failed to execute MPV command: '{error:#}'")
|
||||
}
|
||||
};
|
||||
|
||||
fn set_property(name: impl ToString, value: impl SetData, mpv: &Mpv) {
|
||||
if let Err(error) = mpv.set_property(&name.to_string(), value) {
|
||||
eprintln!("cannot set MPV property: '{error:#}'")
|
||||
}
|
||||
}
|
||||
|
||||
// -- InMsg handler loop --
|
||||
|
||||
for msg in in_msg_receiver.iter() {
|
||||
let in_msg: InMsg = match serde_json::from_str(&msg) {
|
||||
Ok(in_msg) => in_msg,
|
||||
Err(error) => {
|
||||
eprintln!("cannot parse InMsg: {error:#}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match in_msg {
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Bool(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Flag);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Int(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Int64);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Fp(prop))) => {
|
||||
observe_property(prop.to_string(), Format::Double);
|
||||
}
|
||||
InMsg(InMsgFn::MpvObserveProp, InMsgArgs::ObProp(PropKey::Str(prop))) => {
|
||||
observe_property(prop.to_string(), Format::String);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Bool(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Num(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvSetProp, InMsgArgs::StProp(name, PropVal::Str(value))) => {
|
||||
set_property(name, value, &mpv);
|
||||
}
|
||||
InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd)) => {
|
||||
send_command(cmd);
|
||||
}
|
||||
msg => {
|
||||
eprintln!("MPV unsupported message: '{msg:?}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
trait MpvExt {
|
||||
fn wake_up(&self);
|
||||
}
|
||||
|
||||
impl MpvExt for Mpv {
|
||||
// @TODO create a PR to the `libmpv` crate and then remove `libmpv-sys` from Cargo.toml?
|
||||
fn wake_up(&self) {
|
||||
unsafe { libmpv_sys::mpv_wakeup(self.ctx.as_ptr()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use win32job::Job;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
pub struct StremioServer {}
|
||||
|
||||
impl StremioServer {
|
||||
pub fn new() -> StremioServer {
|
||||
thread::spawn(move || {
|
||||
let job = Job::create().expect("Cannont create job");
|
||||
let mut info = job.query_extended_limit_info().expect("Cannont get info");
|
||||
info.limit_kill_on_job_close();
|
||||
job.set_extended_limit_info(&mut info).ok();
|
||||
job.assign_current_process().ok();
|
||||
loop {
|
||||
let mut child = Command::new("node")
|
||||
.arg("server.js")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn()
|
||||
.expect("Cannot run the server");
|
||||
child.wait().expect("Cannot wait for the server");
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
dbg!("Trying to restart the server...");
|
||||
}
|
||||
});
|
||||
StremioServer {}
|
||||
}
|
||||
}
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use win32job::Job;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
pub struct StremioServer {}
|
||||
|
||||
impl StremioServer {
|
||||
pub fn new() -> StremioServer {
|
||||
thread::spawn(move || {
|
||||
let job = Job::create().expect("Cannont create job");
|
||||
let mut info = job.query_extended_limit_info().expect("Cannont get info");
|
||||
info.limit_kill_on_job_close();
|
||||
job.set_extended_limit_info(&mut info).ok();
|
||||
job.assign_current_process().ok();
|
||||
loop {
|
||||
let mut child = Command::new("node")
|
||||
.arg("server.js")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn()
|
||||
.expect("Cannot run the server");
|
||||
child.wait().expect("Cannot wait for the server");
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
dbg!("Trying to restart the server...");
|
||||
}
|
||||
});
|
||||
StremioServer {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue