1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-08-23 07:42:49 +00:00

Sender SDK

This commit is contained in:
Marcus Hanestad 2025-08-21 14:49:52 +00:00
parent fdbefc63e0
commit afc46f3022
147 changed files with 17638 additions and 114 deletions

View 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

View 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,
}

View 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
}

View 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}"#,
);
}
}