mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
rs-terminal: support v3 PlayMessage and event subscriptions
This commit is contained in:
parent
3a7b7675ba
commit
91a2bbee13
3 changed files with 184 additions and 35 deletions
|
@ -1,18 +1,30 @@
|
||||||
use std::sync::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::v2::PlaybackUpdateMessage,
|
models::{
|
||||||
models::{PlaybackErrorMessage, VersionMessage, VolumeUpdateMessage},
|
v2::{self, PlaybackUpdateMessage},
|
||||||
|
v3, PlaybackErrorMessage, VersionMessage, VolumeUpdateMessage,
|
||||||
|
},
|
||||||
transport::Transport,
|
transport::Transport,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum ProtoVersion {
|
||||||
|
V2,
|
||||||
|
V3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
enum SessionState {
|
enum SessionState {
|
||||||
Idle = 0,
|
Idle,
|
||||||
|
Connected(ProtoVersion),
|
||||||
Disconnected,
|
Disconnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +44,6 @@ pub enum Opcode {
|
||||||
Version = 11,
|
Version = 11,
|
||||||
Ping = 12,
|
Ping = 12,
|
||||||
Pong = 13,
|
Pong = 13,
|
||||||
|
|
||||||
Initial = 14,
|
Initial = 14,
|
||||||
PlayUpdate = 15,
|
PlayUpdate = 15,
|
||||||
SetPlaylistItem = 16,
|
SetPlaylistItem = 16,
|
||||||
|
@ -58,7 +69,6 @@ impl Opcode {
|
||||||
11 => Opcode::Version,
|
11 => Opcode::Version,
|
||||||
12 => Opcode::Ping,
|
12 => Opcode::Ping,
|
||||||
13 => Opcode::Pong,
|
13 => Opcode::Pong,
|
||||||
|
|
||||||
14 => Opcode::Initial,
|
14 => Opcode::Initial,
|
||||||
15 => Opcode::PlayUpdate,
|
15 => Opcode::PlayUpdate,
|
||||||
16 => Opcode::SetPlaylistItem,
|
16 => Opcode::SetPlaylistItem,
|
||||||
|
@ -103,10 +113,25 @@ impl<'a> FCastSession<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg: VersionMessage =
|
let msg: VersionMessage =
|
||||||
serde_json::from_str(&body.ok_or("Version messages required body".to_owned())?)?;
|
serde_json::from_str(&body.ok_or("Version requires body".to_owned())?)?;
|
||||||
|
|
||||||
if msg.version == 3 {
|
if msg.version == 3 {
|
||||||
todo!("Send/recv initial message");
|
let initial = v3::InitialSenderMessage {
|
||||||
|
display_name: None,
|
||||||
|
app_name: Some(env!("CARGO_PKG_NAME").to_owned()),
|
||||||
|
app_version: Some(env!("CARGO_PKG_VERSION").to_owned()),
|
||||||
|
};
|
||||||
|
session.send_message(Opcode::Initial, initial)?;
|
||||||
|
let (opcode, body) = session.read_packet()?;
|
||||||
|
if opcode != Opcode::Initial {
|
||||||
|
return Err(format!("Expected Opcode::Initial, got {opcode:?}").into());
|
||||||
|
}
|
||||||
|
let inital_receiver: v3::InitialReceiverMessage =
|
||||||
|
serde_json::from_str(&body.ok_or("InitialReceiverMessage requires body")?)?;
|
||||||
|
dbg!(inital_receiver);
|
||||||
|
session.state = SessionState::Connected(ProtoVersion::V3);
|
||||||
|
} else {
|
||||||
|
session.state = SessionState::Connected(ProtoVersion::V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
|
@ -175,6 +200,32 @@ impl<'a> FCastSession<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&mut self, event: v3::EventType) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if self.state != SessionState::Connected(ProtoVersion::V3) {
|
||||||
|
return Err(format!(
|
||||||
|
"Cannot subscribe to events in the current state ({:?})",
|
||||||
|
self.state
|
||||||
|
).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = match event {
|
||||||
|
v3::EventType::MediaItemStart => v3::EventSubscribeObject::MediaItemStart,
|
||||||
|
v3::EventType::MediaItemEnd => v3::EventSubscribeObject::MediaItemEnd,
|
||||||
|
v3::EventType::MediaItemChange => v3::EventSubscribeObject::MediaItemChanged,
|
||||||
|
v3::EventType::KeyDown => v3::EventSubscribeObject::KeyDown {
|
||||||
|
keys: v3::KeyNames::all(),
|
||||||
|
},
|
||||||
|
v3::EventType::KeyUp => v3::EventSubscribeObject::KeyUp {
|
||||||
|
keys: v3::KeyNames::all(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_message(
|
||||||
|
Opcode::SubscribeEvent,
|
||||||
|
v3::SubscribeEventMessage { event: obj },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_empty(&mut self, opcode: Opcode) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn send_empty(&mut self, opcode: Opcode) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let json = String::new();
|
let json = String::new();
|
||||||
let data = json.as_bytes();
|
let data = json.as_bytes();
|
||||||
|
@ -202,6 +253,46 @@ impl<'a> FCastSession<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_play_message(
|
||||||
|
&mut self,
|
||||||
|
mime_type: String,
|
||||||
|
url: Option<String>,
|
||||||
|
content: Option<String>,
|
||||||
|
time: Option<f64>,
|
||||||
|
speed: Option<f64>,
|
||||||
|
headers: Option<HashMap<String, String>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match self.state {
|
||||||
|
SessionState::Connected(ProtoVersion::V2) => {
|
||||||
|
let msg = v2::PlayMessage {
|
||||||
|
container: mime_type,
|
||||||
|
url,
|
||||||
|
content,
|
||||||
|
time,
|
||||||
|
speed,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
self.send_message(Opcode::Play, msg)?;
|
||||||
|
}
|
||||||
|
SessionState::Connected(ProtoVersion::V3) => {
|
||||||
|
let msg = v3::PlayMessage {
|
||||||
|
container: mime_type,
|
||||||
|
url,
|
||||||
|
content,
|
||||||
|
time,
|
||||||
|
volume: Some(1.0),
|
||||||
|
speed,
|
||||||
|
headers,
|
||||||
|
metadata: None,
|
||||||
|
};
|
||||||
|
self.send_message(Opcode::Play, msg)?;
|
||||||
|
}
|
||||||
|
_ => return Err("invalid state for sending play message".into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_packet(
|
fn handle_packet(
|
||||||
&mut self,
|
&mut self,
|
||||||
opcode: Opcode,
|
opcode: Opcode,
|
||||||
|
@ -267,6 +358,30 @@ impl<'a> FCastSession<'a> {
|
||||||
self.send_empty(Opcode::Pong)?;
|
self.send_empty(Opcode::Pong)?;
|
||||||
println!("Sent pong");
|
println!("Sent pong");
|
||||||
}
|
}
|
||||||
|
Opcode::PlayUpdate => {
|
||||||
|
if let Some(body_str) = body {
|
||||||
|
if let Ok(play_update_msg) =
|
||||||
|
serde_json::from_str::<v3::PlayUpdateMessage>(&body_str)
|
||||||
|
{
|
||||||
|
println!("Received play update {play_update_msg:?}");
|
||||||
|
} else {
|
||||||
|
println!("Received play update with malformed body.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Received play update with no body.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Opcode::Event => {
|
||||||
|
if let Some(body_str) = body {
|
||||||
|
if let Ok(event_msg) = serde_json::from_str::<v3::EventMessage>(&body_str) {
|
||||||
|
println!("Received event {event_msg:?}");
|
||||||
|
} else {
|
||||||
|
println!("Received event with malformed body.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Received event with no body.");
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Error handling packet");
|
println!("Error handling packet");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use clap::{App, Arg, SubCommand};
|
use clap::{App, Arg, SubCommand};
|
||||||
|
use fcast::models::v3;
|
||||||
use fcast::transport::WebSocket;
|
use fcast::transport::WebSocket;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
@ -17,7 +18,6 @@ use url::Url;
|
||||||
use fcast::fcastsession::Opcode;
|
use fcast::fcastsession::Opcode;
|
||||||
use fcast::{
|
use fcast::{
|
||||||
fcastsession::FCastSession,
|
fcastsession::FCastSession,
|
||||||
models::v2::PlayMessage,
|
|
||||||
models::{SeekMessage, SetSpeedMessage, SetVolumeMessage},
|
models::{SeekMessage, SetSpeedMessage, SetVolumeMessage},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,6 +59,16 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.required(false)
|
.required(false)
|
||||||
.takes_value(true),
|
.takes_value(true),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("subscribe")
|
||||||
|
.short('s')
|
||||||
|
.long("subscribe")
|
||||||
|
.value_name("SUBSCRIPTIONS")
|
||||||
|
.help("A comma separated list of events to subscribe to (e.g. MediaItemStart,KeyDown). \
|
||||||
|
Available events: [MediaItemStart, MediaItemEnd, MediaItemChange, KeyDown, KeyUp]")
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("play")
|
SubCommand::with_name("play")
|
||||||
.about("Play media")
|
.about("Play media")
|
||||||
|
@ -213,6 +223,25 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
println!("Connection established.");
|
println!("Connection established.");
|
||||||
|
|
||||||
|
if let Some(subscriptions) = matches.value_of("subscribe") {
|
||||||
|
let subs = subscriptions.split(',');
|
||||||
|
for sub in subs {
|
||||||
|
let event = match sub.to_lowercase().as_str() {
|
||||||
|
"mediaitemstart" => v3::EventType::MediaItemStart,
|
||||||
|
"mediaitemend" => v3::EventType::MediaItemEnd,
|
||||||
|
"mediaitemchange" => v3::EventType::MediaItemChange,
|
||||||
|
"keydown" => v3::EventType::KeyDown,
|
||||||
|
"keyup" => v3::EventType::KeyUp,
|
||||||
|
_ => {
|
||||||
|
println!("Invalid event in subscriptions list: {sub}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
session.subscribe(event)?;
|
||||||
|
println!("Subscribed to {event:?} events");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut join_handle: Option<JoinHandle<Result<(), String>>> = None;
|
let mut join_handle: Option<JoinHandle<Result<(), String>>> = None;
|
||||||
if let Some(play_matches) = matches.subcommand_matches("play") {
|
if let Some(play_matches) = matches.subcommand_matches("play") {
|
||||||
let file_path = play_matches.value_of("file");
|
let file_path = play_matches.value_of("file");
|
||||||
|
@ -257,7 +286,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.collect::<HashMap<String, String>>()
|
.collect::<HashMap<String, String>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut play_message = if let Some(file_path) = file_path {
|
#[allow(unused_assignments)]
|
||||||
|
let mut url = None;
|
||||||
|
let mut content = None;
|
||||||
|
|
||||||
|
if let Some(file_path) = file_path {
|
||||||
match local_ip {
|
match local_ip {
|
||||||
Some(lip) => {
|
Some(lip) => {
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
@ -274,36 +307,25 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Waiting for Ctrl+C...");
|
println!("Waiting for Ctrl+C...");
|
||||||
|
|
||||||
let result = host_file_and_get_url(&lip, file_path, &mime_type, &running)?;
|
let result = host_file_and_get_url(&lip, file_path, &mime_type, &running)?;
|
||||||
let url = result.0;
|
url = Some(result.0);
|
||||||
join_handle = Some(result.1);
|
join_handle = Some(result.1);
|
||||||
|
|
||||||
PlayMessage::new(mime_type, Some(url), None, time, speed, headers)
|
|
||||||
}
|
}
|
||||||
_ => return Err("Local IP was not able to be resolved.".into()),
|
_ => return Err("Local IP was not able to be resolved.".into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PlayMessage::new(
|
url = play_matches.value_of("url").map(|s| s.to_owned());
|
||||||
mime_type,
|
content = play_matches.value_of("content").map(|s| s.to_owned());
|
||||||
play_matches.value_of("url").map(|s| s.to_owned()),
|
}
|
||||||
play_matches.value_of("content").map(|s| s.to_owned()),
|
|
||||||
time,
|
|
||||||
speed,
|
|
||||||
headers,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if play_message.content.is_none() && play_message.url.is_none() {
|
if content.is_none() && url.is_none() {
|
||||||
println!("Reading content from stdin...");
|
println!("Reading content from stdin...");
|
||||||
|
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
std::io::stdin().read_to_string(&mut buffer)?;
|
std::io::stdin().read_to_string(&mut buffer)?;
|
||||||
play_message.content = Some(buffer);
|
content = Some(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = serde_json::to_string(&play_message);
|
session.send_play_message(mime_type, url, content, time, speed, headers)?;
|
||||||
println!("Sent play {:?}", json);
|
|
||||||
|
|
||||||
session.send_message(Opcode::Play, &play_message)?;
|
|
||||||
} else if let Some(seek_matches) = matches.subcommand_matches("seek") {
|
} else if let Some(seek_matches) = matches.subcommand_matches("seek") {
|
||||||
let seek_message = SeekMessage::new(match seek_matches.value_of("timestamp") {
|
let seek_message = SeekMessage::new(match seek_matches.value_of("timestamp") {
|
||||||
Some(s) => s.parse::<f64>()?,
|
Some(s) => s.parse::<f64>()?,
|
||||||
|
|
|
@ -218,6 +218,18 @@ pub enum KeyNames {
|
||||||
Enter,
|
Enter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeyNames {
|
||||||
|
pub fn all() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"ArrowLeft".to_owned(),
|
||||||
|
"ArrowRight".to_owned(),
|
||||||
|
"ArrowUp".to_owned(),
|
||||||
|
"ArrowDown".to_owned(),
|
||||||
|
"Enter".to_owned(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum EventSubscribeObject {
|
pub enum EventSubscribeObject {
|
||||||
MediaItemStart,
|
MediaItemStart,
|
||||||
|
@ -292,14 +304,14 @@ impl<'de> Deserialize<'de> for EventSubscribeObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct SubscribeEventMessage {
|
pub struct SubscribeEventMessage {
|
||||||
event: EventSubscribeObject,
|
pub event: EventSubscribeObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct UnsubscribeEventMessage {
|
pub struct UnsubscribeEventMessage {
|
||||||
event: EventSubscribeObject,
|
pub event: EventSubscribeObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue