Improved player communication. Test for the messages

This commit is contained in:
Vladimir Borisov 2021-08-02 15:24:42 +03:00
parent 1705b7155c
commit 0105cf1898
No known key found for this signature in database
GPG key ID: F9A584BE4FCB6603
6 changed files with 440 additions and 99 deletions

31
Cargo.lock generated
View file

@ -496,12 +496,22 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_test"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de9e52f2f83e2608a121618b6d3885b514613aac702306232c4f035ff60fdb56"
dependencies = [
"serde",
]
[[package]]
name = "stremio-shell-ng"
version = "0.1.0"
dependencies = [
"bitflags",
"embed-resource",
"heck",
"mpv",
"native-windows-derive",
"native-windows-gui",
@ -509,7 +519,10 @@ dependencies = [
"open",
"serde",
"serde_json",
"serde_test",
"structopt",
"strum",
"strum_macros",
"urlencoding",
"webview2",
"webview2-sys",
@ -556,6 +569,24 @@ dependencies = [
"syn",
]
[[package]]
name = "strum"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
[[package]]
name = "strum_macros"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.73"

View file

@ -19,5 +19,10 @@ structopt = "0.3"
open = "1"
urlencoding = "2.1.0"
bitflags = "1.2.1"
strum = "0.21"
strum_macros = "0.21"
heck = "0.3"
[build-dependencies]
embed-resource = "1.3"
[dev-dependencies]
serde_test = "1.0.*"

View file

@ -1,8 +1,14 @@
use core::convert::TryFrom;
use heck::KebabCase;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use strum_macros::EnumString;
// Responses
const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"];
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PlayerProprChange {
name: String,
data: serde_json::Value,
@ -35,7 +41,7 @@ impl PlayerProprChange {
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PlayerEnded {
reason: String,
}
@ -73,3 +79,180 @@ impl PlayerResponse<'_> {
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 fmt::Display for $t {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", format!("{:?}", self).to_kebab_case())
}
}
impl From<$t> for String {
fn from(s: $t) -> Self {
s.to_string()
}
}
impl TryFrom<String> for $t {
type Error = strum::ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::from_str(s.as_str())
}
}
};
}
#[allow(clippy::enum_variant_names)]
#[derive(Serialize, Deserialize, Debug, Clone, EnumString, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[strum(serialize_all = "kebab-case")]
pub enum InMsgFn {
MpvSetProp,
MpvCommand,
MpvObserveProp,
}
stringable!(InMsgFn);
// Bool
#[derive(Serialize, Deserialize, Debug, Clone, EnumString, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[strum(serialize_all = "kebab-case")]
pub enum BoolProp {
Pause,
PausedForCache,
Seeking,
EofReached,
}
stringable!(BoolProp);
// Int
#[derive(Serialize, Deserialize, Debug, Clone, EnumString, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[strum(serialize_all = "kebab-case")]
pub enum IntProp {
Aid,
Vid,
Sid,
}
stringable!(IntProp);
// Fp
#[derive(Serialize, Deserialize, Debug, Clone, EnumString, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[strum(serialize_all = "kebab-case")]
pub enum FpProp {
TimePos,
Volume,
Duration,
SubScale,
CacheBufferingState,
SubPos,
Speed,
}
stringable!(FpProp);
// Str
#[derive(Serialize, Deserialize, Debug, Clone, EnumString, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[strum(serialize_all = "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(Serialize, Deserialize, Debug, Clone, EnumString, PartialEq)]
#[serde(try_from = "String", into = "String")]
#[strum(serialize_all = "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);

View file

@ -0,0 +1,166 @@
use crate::stremio_app::stremio_player::communication::{
BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey,
PropVal,
};
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: mpv::Format, 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, mpv::Format::Flag(true), Token::Bool(true));
tokens_by_type(&tokens, prop, mpv::Format::Int(1), Token::F64(1.0));
tokens_by_type(&tokens, prop, mpv::Format::Double(1.0), Token::F64(1.0));
tokens_by_type(&tokens, prop, mpv::Format::OsdStr("ok"), Token::Str("ok"));
tokens_by_type(&tokens, prop, mpv::Format::Str("ok"), Token::Str("ok"));
// JSON response
tokens_by_type(
&tokens,
"track-list",
mpv::Format::Str(r#""ok""#),
Token::Str("ok"),
);
tokens_by_type(
&tokens,
"video-params",
mpv::Format::Str(r#""ok""#),
Token::Str("ok"),
);
tokens_by_type(
&tokens,
"metadata",
mpv::Format::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::EndFileReason::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::EndFileReason::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,
],
);
}

View file

@ -1,4 +1,9 @@
pub mod player;
pub use player::Player;
pub mod communication;
pub use communication::{PlayerEnded, PlayerError, PlayerEvent, PlayerProprChange, PlayerResponse};
pub use communication::{
BoolProp, CmdVal, FpProp, InMsg, InMsgArgs, InMsgFn, IntProp, MpvCmd, PlayerEnded, PlayerError,
PlayerEvent, PlayerProprChange, PlayerResponse, PropKey, PropVal, StrProp,
};
#[cfg(test)]
mod communication_tests;

View file

@ -2,19 +2,18 @@ use crate::stremio_app::ipc;
use crate::stremio_app::RPCResponse;
use native_windows_gui::{self as nwg, PartialUi};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::stremio_app::stremio_player::communication::{
PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
use crate::stremio_app::stremio_player::{
InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse,
PropKey, PropVal,
};
#[derive(Default)]
pub struct Player {
pub channel: ipc::Channel,
message_queue: Arc<Mutex<VecDeque<String>>>,
}
impl PartialUi for Player {
@ -30,7 +29,6 @@ impl PartialUi for Player {
.into()
.hwnd()
.expect("Cannot obtain window handle") as i64;
let message = data.message_queue.clone();
thread::spawn(move || {
let mut mpv_builder =
mpv::MpvHandlerBuilder::new().expect("Error while creating MPV builder");
@ -50,29 +48,23 @@ impl PartialUi for Player {
mpv_builder
.try_hardware_decoding()
.expect("failed setting hwdec");
mpv_builder
.set_option("title", "Stremio")
.expect("failed setting title");
mpv_builder
.set_option("terminal", "yes")
.expect("failed setting terminal");
mpv_builder
.set_option("msg-level", "all=v")
.set_option("msg-level", "all=no,cplayer=debug")
.expect("failed setting msg-level");
mpv_builder
.set_option("quiet", "yes")
.expect("failed setting msg-level");
let mut mpv = mpv_builder.build().expect("Cannot build MPV");
let thread_messages = Arc::clone(&message);
thread::spawn(move || loop {
if let Ok(msg) = rx.recv() {
let mut messages = thread_messages.lock().unwrap();
messages.push_back(msg);
}
});
'main: loop {
// wait up to X seconds for an event.
while let Some(event) = mpv.wait_event(0.03) {
while let Some(event) = mpv.wait_event(0.0) {
// even if you don't do anything with the events, it is still necessary to empty
// the event loop
@ -105,90 +97,49 @@ impl PartialUi for Player {
} // event processing
thread::sleep(std::time::Duration::from_millis(30));
let mut in_message = message.lock().unwrap();
for msg in in_message.drain(..) {
let (command, data): (String, serde_json::Value) =
serde_json::from_str(msg.as_str()).unwrap();
match command.as_str() {
"mpv-observe-prop" => {
let property = data.as_str().unwrap_or_default();
match property {
"pause" | "paused-for-cache" | "seeking" | "eof-reached" => {
mpv.observe_property::<bool>(property, 0).ok();
}
"aid" | "vid" | "sid" => {
mpv.observe_property::<i64>(property, 0).ok();
}
"time-pos"
| "volume"
| "duration"
| "sub-scale"
| "cache-buffering-state"
| "sub-pos" => {
mpv.observe_property::<f64>(property, 0).ok();
}
"path" | "mpv-version" | "ffmpeg-version" | "track-list"
| "video-params" | "metadata" => {
mpv.observe_property::<&str>(property, 0).ok();
}
other => {
eprintln!("mpv-observe-prop: not implemented for `{}`", other);
}
};
for msg in rx.try_iter() {
match serde_json::from_str::<InMsg>(msg.as_str()) {
Ok(InMsg(
InMsgFn::MpvObserveProp,
InMsgArgs::ObProp(PropKey::Bool(prop)),
)) => mpv.observe_property::<bool>(prop.to_string().as_str(), 0),
Ok(InMsg(
InMsgFn::MpvObserveProp,
InMsgArgs::ObProp(PropKey::Int(prop)),
)) => mpv.observe_property::<i64>(prop.to_string().as_str(), 0),
Ok(InMsg(
InMsgFn::MpvObserveProp,
InMsgArgs::ObProp(PropKey::Fp(prop)),
)) => mpv.observe_property::<f64>(prop.to_string().as_str(), 0),
Ok(InMsg(
InMsgFn::MpvObserveProp,
InMsgArgs::ObProp(PropKey::Str(prop)),
)) => mpv.observe_property::<&str>(prop.to_string().as_str(), 0),
Ok(InMsg(
InMsgFn::MpvSetProp,
InMsgArgs::StProp(prop, PropVal::Bool(val)),
)) => mpv.set_property(prop.to_string().as_str(), val),
Ok(InMsg(
InMsgFn::MpvSetProp,
InMsgArgs::StProp(prop, PropVal::Num(val)),
)) => mpv.set_property(prop.to_string().as_str(), val),
Ok(InMsg(
InMsgFn::MpvSetProp,
InMsgArgs::StProp(prop, PropVal::Str(val)),
)) => mpv.set_property(prop.to_string().as_str(), val.as_str()),
Ok(InMsg(InMsgFn::MpvCommand, InMsgArgs::Cmd(cmd))) => {
let cmd: Vec<String> = cmd.into();
mpv.command(&cmd.iter().map(|s| s.as_str()).collect::<Vec<_>>())
}
"mpv-set-prop" => {
match serde_json::from_value::<Vec<serde_json::Value>>(data.clone()) {
Ok(prop_vector) if prop_vector.len() == 2 => {
let prop =
prop_vector[0].as_str().expect("Property is not a string");
let val = prop_vector[1].clone();
// If we change vo MPV panics
if prop != "vo" {
match val {
serde_json::Value::Bool(v) => {
mpv.set_property(prop, v).ok();
}
serde_json::Value::Number(v) => {
mpv.set_property(prop, v.as_f64().unwrap()).ok();
}
serde_json::Value::String(v) => {
mpv.set_property(prop, v.as_str()).ok();
}
val => eprintln!(
"mpv-set-prop unsupported value {:?} for: {}",
val, prop
),
};
};
}
Ok(prop_vector) => {
eprintln!("mpv-set-prop not implemented for: {:?}", prop_vector)
}
Err(e) => {
eprintln!("mpv-set-prop Error: {:?} for data {}", e, data)
}
};
_ => {
eprintln!("MPV unsupported message {}", msg);
Ok(())
}
"mpv-command" => {
match serde_json::from_value::<Vec<String>>(data.clone()) {
Ok(data) if data.len() > 0 => {
let data: Vec<_> = data.iter().map(|s| s.as_str()).collect();
if data[0] != "run" {
mpv.command(&data).ok();
}
}
Ok(_) => {}
Err(e) => {
eprintln!("mpv-command Error: {:?} for data {}", e, data)
}
}
}
_ => {}
};
}
.ok();
} // incoming message drain loop
} // main loop
});
}); // builder thread
Ok(())
}
}