mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-23 15:52:49 +00:00
Sender SDK
This commit is contained in:
parent
fdbefc63e0
commit
afc46f3022
147 changed files with 17638 additions and 114 deletions
11
sdk/common/fcast-protocol/Cargo.toml
Normal file
11
sdk/common/fcast-protocol/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "fcast-protocol"
|
||||
version = "0.1.0"
|
||||
license.workspace = true
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_repr = "0.1.20"
|
||||
thiserror.workspace = true
|
125
sdk/common/fcast-protocol/src/lib.rs
Normal file
125
sdk/common/fcast-protocol/src/lib.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
//! # FCast Protocol
|
||||
//!
|
||||
//! Implementation of the data models documented [here](https://gitlab.futo.org/videostreaming/fcast/-/wikis/Protocol-version-3).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod v2;
|
||||
pub mod v3;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TryFromByteError {
|
||||
#[error("Unknown opcode: {0}")]
|
||||
UnknownOpcode(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
pub enum Opcode {
|
||||
/// Not used
|
||||
None = 0,
|
||||
/// Sender message to play media content, body is [`v3::PlayMessage`]
|
||||
Play = 1,
|
||||
/// Sender message to pause media content, no body
|
||||
Pause = 2,
|
||||
/// Sender message to resume media content, no body
|
||||
Resume = 3,
|
||||
/// Sender message to stop media content, no body
|
||||
Stop = 4,
|
||||
/// Sender message to seek, body is [`SeekMessage`]
|
||||
Seek = 5,
|
||||
/// Receiver message to notify an updated playback state, body is [`v3::PlaybackUpdateMessage`]
|
||||
PlaybackUpdate = 6,
|
||||
/// Receiver message to notify when the volume has changed, body is [`VolumeUpdateMessage`]
|
||||
VolumeUpdate = 7,
|
||||
/// Sender message to change volume, body is [`SetVolumeMessage`]
|
||||
SetVolume = 8,
|
||||
/// Server message to notify the sender a playback error happened, body is [`PlaybackErrorMessage`]
|
||||
PlaybackError = 9,
|
||||
/// Sender message to change playback speed, body is [`SetSpeedMessage`]
|
||||
SetSpeed = 10,
|
||||
/// Message to notify the other of the current version, body is [`VersionMessage`]
|
||||
Version = 11,
|
||||
/// Message to get the other party to pong, no body
|
||||
Ping = 12,
|
||||
/// Message to respond to a ping from the other party, no body
|
||||
Pong = 13,
|
||||
/// Message to notify the other party of device information and state, body is InitialSenderMessage
|
||||
/// if receiver or [`v3::InitialReceiverMessage`] if sender
|
||||
Initial = 14,
|
||||
/// Receiver message to notify all senders when any device has sent a [`v3::PlayMessage`], body is
|
||||
/// [`v3::PlayUpdateMessage`]
|
||||
PlayUpdate = 15,
|
||||
/// Sender message to set the item index in a playlist to play content from, body is
|
||||
/// [`v3::SetPlaylistItemMessage`]
|
||||
SetPlaylistItem = 16,
|
||||
/// Sender message to subscribe to a receiver event, body is [`v3::SubscribeEventMessage`]
|
||||
SubscribeEvent = 17,
|
||||
/// Sender message to unsubscribe to a receiver event, body is [`v3::UnsubscribeEventMessage`]
|
||||
UnsubscribeEvent = 18,
|
||||
/// Receiver message to notify when a sender subscribed event has occurred, body is [`v3::EventMessage`]
|
||||
Event = 19,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Opcode {
|
||||
type Error = TryFromByteError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
0 => Opcode::None,
|
||||
1 => Opcode::Play,
|
||||
2 => Opcode::Pause,
|
||||
3 => Opcode::Resume,
|
||||
4 => Opcode::Stop,
|
||||
5 => Opcode::Seek,
|
||||
6 => Opcode::PlaybackUpdate,
|
||||
7 => Opcode::VolumeUpdate,
|
||||
8 => Opcode::SetVolume,
|
||||
9 => Opcode::PlaybackError,
|
||||
10 => Opcode::SetSpeed,
|
||||
11 => Opcode::Version,
|
||||
12 => Opcode::Ping,
|
||||
13 => Opcode::Pong,
|
||||
14 => Opcode::Initial,
|
||||
15 => Opcode::PlayUpdate,
|
||||
16 => Opcode::SetPlaylistItem,
|
||||
17 => Opcode::SubscribeEvent,
|
||||
18 => Opcode::UnsubscribeEvent,
|
||||
19 => Opcode::Event,
|
||||
_ => return Err(TryFromByteError::UnknownOpcode(value)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PlaybackErrorMessage {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct VersionMessage {
|
||||
pub version: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct SetSpeedMessage {
|
||||
pub speed: f64,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct VolumeUpdateMessage {
|
||||
#[serde(rename = "generationTime")]
|
||||
pub generation_time: u64,
|
||||
pub volume: f64, //(0-1)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct SetVolumeMessage {
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct SeekMessage {
|
||||
pub time: f64,
|
||||
}
|
24
sdk/common/fcast-protocol/src/v2.rs
Normal file
24
sdk/common/fcast-protocol/src/v2.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct PlayMessage {
|
||||
pub container: String,
|
||||
pub url: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub time: Option<f64>,
|
||||
pub speed: Option<f64>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PlaybackUpdateMessage {
|
||||
#[serde(rename = "generationTime")]
|
||||
pub generation_time: u64,
|
||||
pub time: f64,
|
||||
pub duration: f64,
|
||||
pub speed: f64,
|
||||
pub state: u8, //0 = None, 1 = Playing, 2 = Paused
|
||||
}
|
786
sdk/common/fcast-protocol/src/v3.rs
Normal file
786
sdk/common/fcast-protocol/src/v3.rs
Normal file
|
@ -0,0 +1,786 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{de, ser, Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
macro_rules! get_from_map {
|
||||
($map:expr, $key:expr) => {
|
||||
$map.get($key).ok_or(de::Error::missing_field($key))
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum MetadataObject {
|
||||
Generic {
|
||||
title: Option<String>,
|
||||
thumbnail_url: Option<String>,
|
||||
custom: Option<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Serialize for MetadataObject {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
MetadataObject::Generic {
|
||||
title,
|
||||
thumbnail_url,
|
||||
custom,
|
||||
} => {
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert("type".to_owned(), json!(0u64));
|
||||
map.insert(
|
||||
"title".to_owned(),
|
||||
match title {
|
||||
Some(t) => Value::String(t.to_owned()),
|
||||
None => Value::Null,
|
||||
},
|
||||
);
|
||||
map.insert(
|
||||
"thumbnailUrl".to_owned(),
|
||||
match thumbnail_url {
|
||||
Some(t) => Value::String(t.to_owned()),
|
||||
None => Value::Null,
|
||||
},
|
||||
);
|
||||
if let Some(custom) = custom {
|
||||
map.insert("custom".to_owned(), custom.clone());
|
||||
}
|
||||
map.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MetadataObject {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut map = serde_json::Map::deserialize(deserializer)?;
|
||||
|
||||
let type_ = map
|
||||
.remove("type")
|
||||
.ok_or(de::Error::missing_field("type"))?
|
||||
.as_u64()
|
||||
.ok_or(de::Error::custom("`type` is not an integer"))?;
|
||||
let rest = Value::Object(map);
|
||||
|
||||
match type_ {
|
||||
0 => {
|
||||
let title = match rest.get("title") {
|
||||
Some(t) => Some(
|
||||
t.as_str()
|
||||
.ok_or(de::Error::custom("`title` is not a string"))?
|
||||
.to_owned(),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let thumbnail_url = match rest.get("thumbnailUrl") {
|
||||
Some(t) => Some(
|
||||
t.as_str()
|
||||
.ok_or(de::Error::custom("`thumbnailUrl` is not a string"))?
|
||||
.to_owned(),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
Ok(Self::Generic {
|
||||
title,
|
||||
thumbnail_url,
|
||||
custom: rest.get("custom").cloned(),
|
||||
})
|
||||
}
|
||||
_ => Err(de::Error::custom(format!("Unknown metadata type {type_}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PlayMessage {
|
||||
/// The MIME type (video/mp4)
|
||||
pub container: String,
|
||||
// The URL to load (optional)
|
||||
pub url: Option<String>,
|
||||
// The content to load (i.e. a DASH manifest, json content, optional)
|
||||
pub content: Option<String>,
|
||||
// The time to start playing in seconds
|
||||
pub time: Option<f64>,
|
||||
// The desired volume (0-1)
|
||||
pub volume: Option<f64>,
|
||||
// The factor to multiply playback speed by (defaults to 1.0)
|
||||
pub speed: Option<f64>,
|
||||
// HTTP request headers to add to the play request Map<string, string>
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub metadata: Option<MetadataObject>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize_repr, Serialize_repr, Debug, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum ContentType {
|
||||
#[default]
|
||||
Playlist = 0,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)]
|
||||
pub struct MediaItem {
|
||||
/// The MIME type (video/mp4)
|
||||
pub container: String,
|
||||
/// The URL to load (optional)
|
||||
pub url: Option<String>,
|
||||
/// The content to load (i.e. a DASH manifest, json content, optional)
|
||||
pub content: Option<String>,
|
||||
/// The time to start playing in seconds
|
||||
pub time: Option<f64>,
|
||||
/// The desired volume (0-1)
|
||||
pub volume: Option<f64>,
|
||||
/// The factor to multiply playback speed by (defaults to 1.0)
|
||||
pub speed: Option<f64>,
|
||||
/// Indicates if the receiver should preload the media item
|
||||
pub cache: Option<bool>,
|
||||
/// Indicates how long the item content is presented on screen in seconds
|
||||
#[serde(rename = "showDuration")]
|
||||
pub show_duration: Option<f64>,
|
||||
/// HTTP request headers to add to the play request Map<string, string>
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub metadata: Option<MetadataObject>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
pub struct PlaylistContent {
|
||||
#[serde(rename = "contentType")]
|
||||
pub variant: ContentType,
|
||||
pub items: Vec<MediaItem>,
|
||||
/// Start position of the first item to play from the playlist
|
||||
pub offset: Option<u64>, // int or float?
|
||||
/// The desired volume (0-1)
|
||||
pub volume: Option<f64>,
|
||||
/// The factor to multiply playback speed by (defaults to 1.0)
|
||||
pub speed: Option<f64>,
|
||||
/// Count of media items should be pre-loaded forward from the current view index
|
||||
#[serde(rename = "forwardCache")]
|
||||
pub forward_cache: Option<u64>,
|
||||
/// Count of media items should be pre-loaded backward from the current view index
|
||||
#[serde(rename = "backwardCache")]
|
||||
pub backward_cache: Option<u64>,
|
||||
pub metadata: Option<MetadataObject>,
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum PlaybackState {
|
||||
Idle = 0,
|
||||
Playing = 1,
|
||||
Paused = 2,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PlaybackUpdateMessage {
|
||||
// The time the packet was generated (unix time milliseconds)
|
||||
#[serde(rename = "generationTime")]
|
||||
pub generation_time: u64,
|
||||
// The playback state
|
||||
pub state: PlaybackState,
|
||||
// The current time playing in seconds
|
||||
pub time: Option<f64>,
|
||||
// The duration in seconds
|
||||
pub duration: Option<f64>,
|
||||
// The playback speed factor
|
||||
pub speed: Option<f64>,
|
||||
// The playlist item index currently being played on receiver
|
||||
#[serde(rename = "itemIndex")]
|
||||
pub item_index: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct InitialSenderMessage {
|
||||
#[serde(rename = "displayName")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(rename = "appName")]
|
||||
pub app_name: Option<String>,
|
||||
#[serde(rename = "appVersion")]
|
||||
pub app_version: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct InitialReceiverMessage {
|
||||
#[serde(rename = "displayName")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(rename = "appName")]
|
||||
pub app_name: Option<String>,
|
||||
#[serde(rename = "appVersion")]
|
||||
pub app_version: Option<String>,
|
||||
#[serde(rename = "playData")]
|
||||
pub play_data: Option<PlayMessage>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PlayUpdateMessage {
|
||||
#[serde(rename = "generationTime")]
|
||||
pub generation_time: Option<u64>,
|
||||
#[serde(rename = "playData")]
|
||||
pub play_data: Option<PlayMessage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct SetPlaylistItemMessage {
|
||||
#[serde(rename = "itemIndex")]
|
||||
pub item_index: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub enum KeyNames {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Enter,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
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)]
|
||||
pub enum EventSubscribeObject {
|
||||
MediaItemStart,
|
||||
MediaItemEnd,
|
||||
MediaItemChanged,
|
||||
KeyDown { keys: Vec<String> },
|
||||
KeyUp { keys: Vec<String> },
|
||||
}
|
||||
|
||||
impl Serialize for EventSubscribeObject {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serde_json::Map::new();
|
||||
let type_val: u64 = match self {
|
||||
EventSubscribeObject::MediaItemStart => 0,
|
||||
EventSubscribeObject::MediaItemEnd => 1,
|
||||
EventSubscribeObject::MediaItemChanged => 2,
|
||||
EventSubscribeObject::KeyDown { .. } => 3,
|
||||
EventSubscribeObject::KeyUp { .. } => 4,
|
||||
};
|
||||
|
||||
map.insert("type".to_owned(), json!(type_val));
|
||||
|
||||
let keys = match self {
|
||||
EventSubscribeObject::KeyDown { keys } => Some(keys),
|
||||
EventSubscribeObject::KeyUp { keys } => Some(keys),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(keys) = keys {
|
||||
map.insert("keys".to_owned(), json!(keys));
|
||||
}
|
||||
|
||||
map.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EventSubscribeObject {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut map = serde_json::Map::deserialize(deserializer)?;
|
||||
let type_ = map
|
||||
.remove("type")
|
||||
.ok_or(de::Error::missing_field("type"))?
|
||||
.as_u64()
|
||||
.ok_or(de::Error::custom("`type` is not an integer"))?;
|
||||
let rest = Value::Object(map);
|
||||
|
||||
match type_ {
|
||||
0 => Ok(Self::MediaItemStart),
|
||||
1 => Ok(Self::MediaItemEnd),
|
||||
2 => Ok(Self::MediaItemChanged),
|
||||
3 | 4 => {
|
||||
let keys = get_from_map!(rest, "keys")?
|
||||
.as_array()
|
||||
.ok_or(de::Error::custom("`type` is not an array"))?
|
||||
.iter()
|
||||
.map(|v| v.as_str().map(|s| s.to_owned()))
|
||||
.collect::<Option<Vec<String>>>()
|
||||
.ok_or(de::Error::custom("`type` is not an array of strings"))?;
|
||||
if type_ == 3 {
|
||||
Ok(Self::KeyDown { keys })
|
||||
} else {
|
||||
Ok(Self::KeyUp { keys })
|
||||
}
|
||||
}
|
||||
_ => Err(de::Error::custom(format!("Unknown event type {type_}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SubscribeEventMessage {
|
||||
pub event: EventSubscribeObject,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct UnsubscribeEventMessage {
|
||||
pub event: EventSubscribeObject,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum EventType {
|
||||
MediaItemStart = 0,
|
||||
MediaItemEnd = 1,
|
||||
MediaItemChange = 2,
|
||||
KeyDown = 3,
|
||||
KeyUp = 4,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum EventObject {
|
||||
MediaItem {
|
||||
variant: EventType,
|
||||
item: MediaItem,
|
||||
},
|
||||
Key {
|
||||
variant: EventType,
|
||||
key: String,
|
||||
repeat: bool,
|
||||
handled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Serialize for EventObject {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serde_json::Map::new();
|
||||
|
||||
match self {
|
||||
EventObject::MediaItem { variant, item } => {
|
||||
map.insert("type".to_owned(), json!(*variant as u8));
|
||||
map.insert(
|
||||
"item".to_owned(),
|
||||
serde_json::to_value(item).map_err(ser::Error::custom)?,
|
||||
);
|
||||
}
|
||||
EventObject::Key {
|
||||
variant,
|
||||
key,
|
||||
repeat,
|
||||
handled,
|
||||
} => {
|
||||
map.insert("type".to_owned(), json!(*variant as u8));
|
||||
map.insert(
|
||||
"key".to_owned(),
|
||||
serde_json::to_value(key).map_err(ser::Error::custom)?,
|
||||
);
|
||||
map.insert(
|
||||
"repeat".to_owned(),
|
||||
serde_json::to_value(repeat).map_err(ser::Error::custom)?,
|
||||
);
|
||||
map.insert(
|
||||
"handled".to_owned(),
|
||||
serde_json::to_value(handled).map_err(ser::Error::custom)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
map.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EventObject {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let mut map = serde_json::Map::deserialize(deserializer)?;
|
||||
let type_ = map
|
||||
.remove("type")
|
||||
.ok_or(de::Error::missing_field("type"))?
|
||||
.as_u64()
|
||||
.ok_or(de::Error::custom("`type` is not an integer"))?;
|
||||
let rest = Value::Object(map);
|
||||
|
||||
match type_ {
|
||||
#[allow(clippy::manual_range_patterns)]
|
||||
0 | 1 | 2 => {
|
||||
let variant = match type_ {
|
||||
0 => EventType::MediaItemStart,
|
||||
1 => EventType::MediaItemEnd,
|
||||
_ => EventType::MediaItemChange,
|
||||
};
|
||||
let item = get_from_map!(rest, "item")?;
|
||||
Ok(Self::MediaItem {
|
||||
variant,
|
||||
item: MediaItem::deserialize(item).map_err(de::Error::custom)?,
|
||||
})
|
||||
}
|
||||
3 | 4 => {
|
||||
let variant = if type_ == 3 {
|
||||
EventType::KeyDown
|
||||
} else {
|
||||
EventType::KeyUp
|
||||
};
|
||||
Ok(Self::Key {
|
||||
variant,
|
||||
key: get_from_map!(rest, "key")?
|
||||
.as_str()
|
||||
.ok_or(de::Error::custom("`key` is not a string"))?
|
||||
.to_owned(),
|
||||
repeat: get_from_map!(rest, "repeat")?
|
||||
.as_bool()
|
||||
.ok_or(de::Error::custom("`repeat` is not a bool"))?,
|
||||
handled: get_from_map!(rest, "handled")?
|
||||
.as_bool()
|
||||
.ok_or(de::Error::custom("`handled` is not a bool"))?,
|
||||
})
|
||||
}
|
||||
_ => Err(de::Error::custom(format!("Unknown event type {type_}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EventMessage {
|
||||
#[serde(rename = "generationTime")]
|
||||
pub generation_time: u64,
|
||||
pub event: EventObject,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! s {
|
||||
($s:expr) => {
|
||||
($s).to_string()
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_metadata_object() {
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&MetadataObject::Generic {
|
||||
title: Some(s!("abc")),
|
||||
thumbnail_url: Some(s!("def")),
|
||||
custom: Some(serde_json::Value::Null),
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"custom":null,"thumbnailUrl":"def","title":"abc","type":0}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&MetadataObject::Generic {
|
||||
title: None,
|
||||
thumbnail_url: None,
|
||||
custom: Some(serde_json::Value::Null),
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"custom":null,"thumbnailUrl":null,"title":null,"type":0}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&MetadataObject::Generic {
|
||||
title: Some(s!("abc")),
|
||||
thumbnail_url: Some(s!("def")),
|
||||
custom: None,
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"thumbnailUrl":"def","title":"abc","type":0}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_metadata_object() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<MetadataObject>(
|
||||
r#"{"type":0,"title":"abc","thumbnailUrl":"def","custom":null}"#
|
||||
)
|
||||
.unwrap(),
|
||||
MetadataObject::Generic {
|
||||
title: Some(s!("abc")),
|
||||
thumbnail_url: Some(s!("def")),
|
||||
custom: Some(serde_json::Value::Null),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<MetadataObject>(r#"{"type":0,"custom":null}"#).unwrap(),
|
||||
MetadataObject::Generic {
|
||||
title: None,
|
||||
thumbnail_url: None,
|
||||
custom: Some(serde_json::Value::Null),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<MetadataObject>(r#"{"type":0}"#).unwrap(),
|
||||
MetadataObject::Generic {
|
||||
title: None,
|
||||
thumbnail_url: None,
|
||||
custom: None,
|
||||
}
|
||||
);
|
||||
assert!(serde_json::from_str::<MetadataObject>(r#"{"type":1"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_event_sub_obj() {
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::MediaItemStart).unwrap(),
|
||||
r#"{"type":0}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::MediaItemEnd).unwrap(),
|
||||
r#"{"type":1}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::MediaItemChanged).unwrap(),
|
||||
r#"{"type":2}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::KeyDown { keys: vec![] }).unwrap(),
|
||||
r#"{"keys":[],"type":3}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::KeyDown { keys: vec![] }).unwrap(),
|
||||
r#"{"keys":[],"type":3}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::KeyUp {
|
||||
keys: vec![s!("abc"), s!("def")]
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"keys":["abc","def"],"type":4}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::KeyDown {
|
||||
keys: vec![s!("abc"), s!("def")]
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"keys":["abc","def"],"type":3}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventSubscribeObject::KeyDown {
|
||||
keys: vec![s!("\"\"")]
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"keys":["\"\""],"type":3}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_event_sub_obj() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"type":0}"#).unwrap(),
|
||||
EventSubscribeObject::MediaItemStart
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"type":1}"#).unwrap(),
|
||||
EventSubscribeObject::MediaItemEnd
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"type":2}"#).unwrap(),
|
||||
EventSubscribeObject::MediaItemChanged
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"keys":[],"type":3}"#).unwrap(),
|
||||
EventSubscribeObject::KeyDown { keys: vec![] }
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"keys":[],"type":4}"#).unwrap(),
|
||||
EventSubscribeObject::KeyUp { keys: vec![] }
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"keys":["abc","def"],"type":3}"#)
|
||||
.unwrap(),
|
||||
EventSubscribeObject::KeyDown {
|
||||
keys: vec![s!("abc"), s!("def")]
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventSubscribeObject>(r#"{"keys":["abc","def"],"type":4}"#)
|
||||
.unwrap(),
|
||||
EventSubscribeObject::KeyUp {
|
||||
keys: vec![s!("abc"), s!("def")]
|
||||
}
|
||||
);
|
||||
assert!(serde_json::from_str::<EventSubscribeObject>(r#""type":5}"#).is_err());
|
||||
}
|
||||
|
||||
const EMPTY_TEST_MEDIA_ITEM: MediaItem = MediaItem {
|
||||
container: String::new(),
|
||||
url: None,
|
||||
content: None,
|
||||
time: None,
|
||||
volume: None,
|
||||
speed: None,
|
||||
cache: None,
|
||||
show_duration: None,
|
||||
headers: None,
|
||||
metadata: None,
|
||||
};
|
||||
const TEST_MEDIA_ITEM_JSON: &str = r#"{"cache":null,"container":"","content":null,"headers":null,"metadata":null,"showDuration":null,"speed":null,"time":null,"url":null,"volume":null}"#;
|
||||
|
||||
#[test]
|
||||
fn serialize_event_obj() {
|
||||
assert_eq!(
|
||||
serde_json::to_string(&EventObject::MediaItem {
|
||||
variant: EventType::MediaItemStart,
|
||||
item: EMPTY_TEST_MEDIA_ITEM.clone(),
|
||||
})
|
||||
.unwrap(),
|
||||
format!(r#"{{"item":{TEST_MEDIA_ITEM_JSON},"type":0}}"#),
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&EventObject::MediaItem {
|
||||
variant: EventType::MediaItemEnd,
|
||||
item: EMPTY_TEST_MEDIA_ITEM.clone(),
|
||||
})
|
||||
.unwrap(),
|
||||
format!(r#"{{"item":{TEST_MEDIA_ITEM_JSON},"type":1}}"#),
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&EventObject::MediaItem {
|
||||
variant: EventType::MediaItemChange,
|
||||
item: EMPTY_TEST_MEDIA_ITEM.clone(),
|
||||
})
|
||||
.unwrap(),
|
||||
format!(r#"{{"item":{TEST_MEDIA_ITEM_JSON},"type":2}}"#),
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventObject::Key {
|
||||
variant: EventType::KeyDown,
|
||||
key: s!(""),
|
||||
repeat: false,
|
||||
handled: false,
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"handled":false,"key":"","repeat":false,"type":3}"#
|
||||
);
|
||||
assert_eq!(
|
||||
&serde_json::to_string(&EventObject::Key {
|
||||
variant: EventType::KeyUp,
|
||||
key: s!(""),
|
||||
repeat: false,
|
||||
handled: false,
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"handled":false,"key":"","repeat":false,"type":4}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_event_obj() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventObject>(&format!(
|
||||
r#"{{"item":{TEST_MEDIA_ITEM_JSON},"type":0}}"#
|
||||
))
|
||||
.unwrap(),
|
||||
EventObject::MediaItem {
|
||||
variant: EventType::MediaItemStart,
|
||||
item: EMPTY_TEST_MEDIA_ITEM.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventObject>(&format!(
|
||||
r#"{{"item":{TEST_MEDIA_ITEM_JSON},"type":1}}"#
|
||||
))
|
||||
.unwrap(),
|
||||
EventObject::MediaItem {
|
||||
variant: EventType::MediaItemEnd,
|
||||
item: EMPTY_TEST_MEDIA_ITEM.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventObject>(&format!(
|
||||
r#"{{"item":{TEST_MEDIA_ITEM_JSON},"type":2}}"#
|
||||
))
|
||||
.unwrap(),
|
||||
EventObject::MediaItem {
|
||||
variant: EventType::MediaItemChange,
|
||||
item: EMPTY_TEST_MEDIA_ITEM.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventObject>(
|
||||
r#"{"handled":false,"key":"","repeat":false,"type":3}"#
|
||||
)
|
||||
.unwrap(),
|
||||
EventObject::Key {
|
||||
variant: EventType::KeyDown,
|
||||
key: s!(""),
|
||||
repeat: false,
|
||||
handled: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<EventObject>(
|
||||
r#"{"handled":false,"key":"","repeat":false,"type":4}"#
|
||||
)
|
||||
.unwrap(),
|
||||
EventObject::Key {
|
||||
variant: EventType::KeyUp,
|
||||
key: s!(""),
|
||||
repeat: false,
|
||||
handled: false,
|
||||
}
|
||||
);
|
||||
assert!(serde_json::from_str::<EventObject>(r#"{"type":5}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_playlist_content() {
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PlaylistContent {
|
||||
variant: ContentType::Playlist,
|
||||
items: Vec::new(),
|
||||
offset: None,
|
||||
volume: None,
|
||||
speed: None,
|
||||
forward_cache: None,
|
||||
backward_cache: None,
|
||||
metadata: None
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"contentType":0,"items":[],"offset":null,"volume":null,"speed":null,"forwardCache":null,"backwardCache":null,"metadata":null}"#,
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PlaylistContent {
|
||||
variant: ContentType::Playlist,
|
||||
items: vec![MediaItem {
|
||||
container: "video/mp4".to_string(),
|
||||
url: Some("abc".to_string()),
|
||||
content: None,
|
||||
time: None,
|
||||
volume: None,
|
||||
speed: None,
|
||||
cache: None,
|
||||
show_duration: None,
|
||||
headers: None,
|
||||
metadata: None
|
||||
}],
|
||||
offset: None,
|
||||
volume: None,
|
||||
speed: None,
|
||||
forward_cache: None,
|
||||
backward_cache: None,
|
||||
metadata: None
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"contentType":0,"items":[{"container":"video/mp4","url":"abc","content":null,"time":null,"volume":null,"speed":null,"cache":null,"showDuration":null,"headers":null,"metadata":null}],"offset":null,"volume":null,"speed":null,"forwardCache":null,"backwardCache":null,"metadata":null}"#,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue