use std::{
borrow::Cow,
fmt,
io::{Read, Write},
str::FromStr,
string::ToString,
};
use crate::{
Lrc,
cast::proxies,
errors::Error,
message_manager::{CastMessage, CastMessagePayload, MessageManager},
};
pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.media";
const MESSAGE_TYPE_GET_STATUS: &str = "GET_STATUS";
const MESSAGE_TYPE_LOAD: &str = "LOAD";
const MESSAGE_TYPE_QUEUE_LOAD: &str = "QUEUE_LOAD";
const MESSAGE_TYPE_PLAY: &str = "PLAY";
const MESSAGE_TYPE_PAUSE: &str = "PAUSE";
const MESSAGE_TYPE_STOP: &str = "STOP";
const MESSAGE_TYPE_SEEK: &str = "SEEK";
const MESSAGE_TYPE_MEDIA_STATUS: &str = "MEDIA_STATUS";
const MESSAGE_TYPE_LOAD_CANCELLED: &str = "LOAD_CANCELLED";
const MESSAGE_TYPE_LOAD_FAILED: &str = "LOAD_FAILED";
const MESSAGE_TYPE_INVALID_PLAYER_STATE: &str = "INVALID_PLAYER_STATE";
const MESSAGE_TYPE_INVALID_REQUEST: &str = "INVALID_REQUEST";
const MESSAGE_TYPE_ERROR: &str = "ERROR";
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum StreamType {
None,
Buffered,
Live,
}
impl FromStr for StreamType {
type Err = Error;
fn from_str(s: &str) -> Result<StreamType, Error> {
match s {
"BUFFERED" | "buffered" => Ok(StreamType::Buffered),
"LIVE" | "live" => Ok(StreamType::Live),
_ => Ok(StreamType::None),
}
}
}
impl fmt::Display for StreamType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stream_type = match *self {
StreamType::None => "NONE",
StreamType::Buffered => "BUFFERED",
StreamType::Live => "LIVE",
};
write!(f, "{}", stream_type)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Metadata {
Generic(GenericMediaMetadata),
Movie(MovieMediaMetadata),
TvShow(TvShowMediaMetadata),
MusicTrack(MusicTrackMediaMetadata),
Photo(PhotoMediaMetadata),
}
impl Metadata {
fn encode(&self) -> proxies::media::Metadata {
match self {
Metadata::Generic(x) => proxies::media::Metadata {
title: x.title.clone(),
subtitle: x.subtitle.clone(),
images: x.images.iter().map(|i| i.encode()).collect(),
release_date: x.release_date.clone(),
..proxies::media::Metadata::new(0)
},
Metadata::Movie(x) => proxies::media::Metadata {
title: x.title.clone(),
subtitle: x.subtitle.clone(),
studio: x.studio.clone(),
images: x.images.iter().map(|i| i.encode()).collect(),
release_date: x.release_date.clone(),
..proxies::media::Metadata::new(1)
},
Metadata::TvShow(x) => proxies::media::Metadata {
series_title: x.series_title.clone(),
subtitle: x.episode_title.clone(),
season: x.season,
episode: x.episode,
images: x.images.iter().map(|i| i.encode()).collect(),
original_air_date: x.original_air_date.clone(),
..proxies::media::Metadata::new(2)
},
Metadata::MusicTrack(x) => proxies::media::Metadata {
album_name: x.album_name.clone(),
title: x.title.clone(),
album_artist: x.album_artist.clone(),
artist: x.artist.clone(),
composer: x.composer.clone(),
track_number: x.track_number,
disc_number: x.disc_number,
images: x.images.iter().map(|i| i.encode()).collect(),
release_date: x.release_date.clone(),
..proxies::media::Metadata::new(3)
},
Metadata::Photo(x) => proxies::media::Metadata {
title: x.title.clone(),
artist: x.artist.clone(),
location: x.location.clone(),
latitude: x.latitude_longitude.map(|coord| coord.0),
longitude: x.latitude_longitude.map(|coord| coord.1),
width: x.dimensions.map(|dims| dims.0),
height: x.dimensions.map(|dims| dims.1),
creation_date_time: x.creation_date_time.clone(),
..proxies::media::Metadata::new(4)
},
}
}
}
impl TryFrom<&proxies::media::Metadata> for Metadata {
type Error = Error;
fn try_from(m: &proxies::media::Metadata) -> Result<Self, Error> {
Ok(match m.metadata_type {
0 => Self::Generic(GenericMediaMetadata {
title: m.title.clone(),
subtitle: m.subtitle.clone(),
images: m.images.iter().map(Image::from).collect(),
release_date: m.release_date.clone(),
}),
1 => Self::Movie(MovieMediaMetadata {
title: m.title.clone(),
subtitle: m.subtitle.clone(),
studio: m.studio.clone(),
images: m.images.iter().map(Image::from).collect(),
release_date: m.release_date.clone(),
}),
2 => Self::TvShow(TvShowMediaMetadata {
series_title: m.series_title.clone(),
episode_title: m.subtitle.clone(),
season: m.season,
episode: m.episode,
images: m.images.iter().map(Image::from).collect(),
original_air_date: m.original_air_date.clone(),
}),
3 => Self::MusicTrack(MusicTrackMediaMetadata {
album_name: m.album_name.clone(),
title: m.title.clone(),
album_artist: m.album_artist.clone(),
artist: m.artist.clone(),
composer: m.composer.clone(),
track_number: m.track_number,
disc_number: m.disc_number,
images: m.images.iter().map(Image::from).collect(),
release_date: m.release_date.clone(),
}),
4 => {
let mut dimensions = None;
let mut latitude_longitude = None;
if let Some(width) = m.width
&& let Some(height) = m.height
{
dimensions = Some((width, height))
}
if let Some(lat) = m.latitude
&& let Some(long) = m.longitude
{
latitude_longitude = Some((lat, long))
}
Self::Photo(PhotoMediaMetadata {
title: m.title.clone(),
artist: m.artist.clone(),
location: m.location.clone(),
latitude_longitude,
dimensions,
creation_date_time: m.creation_date_time.clone(),
})
}
_ => {
return Err(Error::Parsing(format!(
"Bad metadataType {}",
m.metadata_type
)));
}
})
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GenericMediaMetadata {
pub title: Option<String>,
pub subtitle: Option<String>,
pub images: Vec<Image>,
pub release_date: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MovieMediaMetadata {
pub title: Option<String>,
pub subtitle: Option<String>,
pub studio: Option<String>,
pub images: Vec<Image>,
pub release_date: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TvShowMediaMetadata {
pub series_title: Option<String>,
pub episode_title: Option<String>,
pub season: Option<u32>,
pub episode: Option<u32>,
pub images: Vec<Image>,
pub original_air_date: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MusicTrackMediaMetadata {
pub album_name: Option<String>,
pub title: Option<String>,
pub album_artist: Option<String>,
pub artist: Option<String>,
pub composer: Option<String>,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub images: Vec<Image>,
pub release_date: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PhotoMediaMetadata {
pub title: Option<String>,
pub artist: Option<String>,
pub location: Option<String>,
pub latitude_longitude: Option<(f64, f64)>,
pub dimensions: Option<(u32, u32)>,
pub creation_date_time: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Image {
pub url: String,
pub dimensions: Option<(u32, u32)>,
}
impl Image {
pub fn new(url: String) -> Image {
Image {
url,
dimensions: None,
}
}
fn encode(&self) -> proxies::media::Image {
proxies::media::Image {
url: self.url.clone(),
width: self.dimensions.map(|d| d.0),
height: self.dimensions.map(|d| d.1),
}
}
}
impl From<&proxies::media::Image> for Image {
fn from(i: &proxies::media::Image) -> Self {
let mut dimensions = None;
if let Some(width) = i.width
&& let Some(height) = i.height
{
dimensions = Some((width, height));
};
Self {
url: i.url.clone(),
dimensions,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PlayerState {
Idle,
Playing,
Buffering,
Paused,
}
impl FromStr for PlayerState {
type Err = Error;
fn from_str(s: &str) -> Result<PlayerState, Error> {
match s {
"IDLE" => Ok(PlayerState::Idle),
"PLAYING" => Ok(PlayerState::Playing),
"BUFFERING" => Ok(PlayerState::Buffering),
"PAUSED" => Ok(PlayerState::Paused),
_ => Err(Error::Internal(format!("Unknown player state {}", s))),
}
}
}
impl fmt::Display for PlayerState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let player_state = match *self {
PlayerState::Idle => "IDLE",
PlayerState::Playing => "PLAYING",
PlayerState::Buffering => "BUFFERING",
PlayerState::Paused => "PAUSED",
};
write!(f, "{}", player_state)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ExtendedPlayerState {
Loading,
}
impl FromStr for ExtendedPlayerState {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s {
"LOADING" => Ok(Self::Loading),
_ => Err(Error::Internal(format!(
"Unknown extended player state {}",
s
))),
}
}
}
impl fmt::Display for ExtendedPlayerState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let player_state = match *self {
Self::Loading => "LOADING",
};
write!(f, "{}", player_state)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum IdleReason {
Cancelled,
Interrupted,
Finished,
Error,
}
impl FromStr for IdleReason {
type Err = Error;
fn from_str(s: &str) -> Result<IdleReason, Error> {
match s {
"CANCELLED" => Ok(IdleReason::Cancelled),
"INTERRUPTED" => Ok(IdleReason::Interrupted),
"FINISHED" => Ok(IdleReason::Finished),
"ERROR" => Ok(IdleReason::Error),
_ => Err(Error::Internal(format!("Unknown idle reason {}", s))),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum QueueType {
Album,
Playlist,
Audiobook,
RadioStation,
PodcastSeries,
TvSeries,
VideoPlaylist,
LiveTv,
Movie,
}
impl FromStr for QueueType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s {
"ALBUM" => Ok(Self::Album),
"PLAYLIST" => Ok(Self::Playlist),
"AUDIOBOOK" => Ok(Self::Audiobook),
"RADIO_STATION" => Ok(Self::RadioStation),
"PODCAST_SERIES" => Ok(Self::PodcastSeries),
"TV_SERIES" => Ok(Self::TvSeries),
"VIDEO_PLAYLIST" => Ok(Self::VideoPlaylist),
"LIVE_TV" => Ok(Self::LiveTv),
"MOVIE" => Ok(Self::Movie),
_ => Err(Error::Internal(format!("Unknown queue type {}", s))),
}
}
}
impl fmt::Display for QueueType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = match self {
QueueType::Album => "ALBUM",
QueueType::Playlist => "PLAYLIST",
QueueType::Audiobook => "AUDIOBOOK",
QueueType::RadioStation => "RADIO_STATION",
QueueType::PodcastSeries => "PODCAST_SERIES",
QueueType::TvSeries => "TV_SERIES",
QueueType::VideoPlaylist => "VIDEO_PLAYLIST",
QueueType::LiveTv => "LIVE_TV",
QueueType::Movie => "MOVIE",
}
.to_string();
write!(f, "{}", str)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ResumeState {
PlaybackStart,
PlaybackPause,
}
impl FromStr for ResumeState {
type Err = Error;
fn from_str(s: &str) -> Result<ResumeState, Error> {
match s {
"PLAYBACK_START" | "start" => Ok(ResumeState::PlaybackStart),
"PLAYBACK_PAUSE" | "pause" => Ok(ResumeState::PlaybackPause),
_ => Err(Error::Internal(format!("Unknown resume state {}", s))),
}
}
}
impl fmt::Display for ResumeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let resume_state = match *self {
ResumeState::PlaybackStart => "PLAYBACK_START",
ResumeState::PlaybackPause => "PLAYBACK_PAUSE",
};
write!(f, "{}", resume_state)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Media {
pub content_id: String,
pub stream_type: StreamType,
pub content_type: String,
pub metadata: Option<Metadata>,
pub duration: Option<f32>,
}
impl Media {
fn encode(&self) -> proxies::media::Media {
let metadata = self.metadata.as_ref().map(|m| m.encode());
proxies::media::Media {
content_id: self.content_id.clone(),
stream_type: self.stream_type.to_string(),
content_type: self.content_type.clone(),
metadata,
duration: self.duration,
}
}
}
impl TryFrom<&proxies::media::Media> for Media {
type Error = Error;
fn try_from(m: &proxies::media::Media) -> Result<Self, Error> {
Ok(Self {
content_id: m.content_id.to_string(),
stream_type: StreamType::from_str(m.stream_type.as_ref())?,
content_type: m.content_type.to_string(),
metadata: m.metadata.as_ref().map(TryInto::try_into).transpose()?,
duration: m.duration,
})
}
}
#[derive(Clone, Debug)]
pub struct QueueItem {
pub media: Media,
}
impl QueueItem {
fn encode(&self) -> proxies::media::QueueItem {
proxies::media::QueueItem {
active_track_ids: None,
autoplay: true,
custom_data: None,
item_id: None,
media: self.media.encode(),
playback_duration: None,
preload_time: 20.,
start_time: 0.,
}
}
}
#[derive(Clone, Debug)]
pub struct MediaQueue {
pub items: Vec<QueueItem>,
pub start_index: u16,
pub queue_type: QueueType,
}
impl MediaQueue {
fn encode(&self) -> proxies::media::QueueData {
proxies::media::QueueData {
items: self.items.iter().map(|qi| qi.encode()).collect(),
queue_type: Some(self.queue_type.to_string()),
repeat_mode: "REPEAT_OFF".to_owned(),
start_index: self.start_index,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Status {
pub request_id: u32,
pub entries: Vec<StatusEntry>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtendedStatus {
pub player_state: ExtendedPlayerState,
pub media_session_id: Option<i32>,
pub media: Option<Media>,
}
impl TryFrom<&proxies::media::ExtendedStatus> for ExtendedStatus {
type Error = Error;
fn try_from(es: &proxies::media::ExtendedStatus) -> Result<Self, Error> {
Ok(Self {
player_state: ExtendedPlayerState::from_str(&es.player_state)?,
media_session_id: es.media_session_id,
media: es.media.as_ref().map(Media::try_from).transpose()?,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct StatusEntry {
pub media_session_id: i32,
pub media: Option<Media>,
pub playback_rate: f32,
pub player_state: PlayerState,
pub current_item_id: Option<u16>,
pub loading_item_id: Option<u16>,
pub preloaded_item_id: Option<u16>,
pub idle_reason: Option<IdleReason>,
pub extended_status: Option<ExtendedStatus>,
pub current_time: Option<f32>,
pub supported_media_commands: u32,
}
impl TryFrom<&proxies::media::Status> for StatusEntry {
type Error = Error;
fn try_from(x: &proxies::media::Status) -> Result<Self, Error> {
Ok(Self {
media_session_id: x.media_session_id,
media: x.media.as_ref().map(TryInto::try_into).transpose()?,
playback_rate: x.playback_rate,
player_state: PlayerState::from_str(x.player_state.as_ref())?,
current_item_id: x.current_item_id,
loading_item_id: x.loading_item_id,
preloaded_item_id: x.preloaded_item_id,
idle_reason: x
.idle_reason
.as_ref()
.map(|reason| IdleReason::from_str(reason))
.transpose()?,
extended_status: x
.extended_status
.as_ref()
.map(ExtendedStatus::try_from)
.transpose()?,
current_time: x.current_time,
supported_media_commands: x.supported_media_commands,
})
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct LoadCancelled {
pub request_id: u32,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct LoadFailed {
pub request_id: u32,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct LoadOptions {
pub current_time: f64,
pub autoplay: bool,
}
impl Default for LoadOptions {
fn default() -> Self {
LoadOptions {
current_time: 0f64,
autoplay: true,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct InvalidPlayerState {
pub request_id: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct InvalidRequest {
pub request_id: u32,
pub reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MediaError {
pub detailed_error_code: MediaDetailedErrorCode,
pub message_type: String,
}
#[derive(Clone, Debug, PartialEq)]
pub enum MediaDetailedErrorCode {
App = 900,
BreakClipLoadingError = 901,
BreakSeekInterceptorError = 902,
DashInvalidSegmentInfo = 423,
DashManifestNoMimeType = 422,
DashManifestNoPeriods = 421,
DashManifestUnknown = 420,
DashNetwork = 321,
DashNoInit = 322,
Generic = 999,
HlsManifestMaster = 411,
HlsManifestPlaylist = 412,
HlsNetworkInvalidSegment = 315,
HlsNetworkKeyLoad = 314,
HlsNetworkMasterPlaylist = 311,
HlsNetworkNoKeyResponse = 313,
HlsNetworkPlaylist = 312,
HlsSegmentParsing = 316,
ImageError = 903,
LoadFailed = 905,
LoadInterrupted = 904,
ManifestUnknown = 400,
MediakeysNetwork = 201,
MediakeysUnknown = 200,
MediakeysUnsupported = 202,
MediakeysWebcrypto = 203,
MediaAborted = 101,
MediaDecode = 102,
MediaErrorMessage = 906,
MediaNetwork = 103,
MediaSrcNotSupported = 104,
MediaUnknown = 100,
NetworkUnknown = 300,
SegmentNetwork = 301,
SegmentUnknown = 500,
SmoothManifest = 431,
SmoothNetwork = 331,
SmoothNoMediaData = 332,
SourceBufferFailure = 110,
TextUnknown = 600,
}
impl TryFrom<i32> for MediaDetailedErrorCode {
type Error = Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
900 => Ok(MediaDetailedErrorCode::App),
901 => Ok(MediaDetailedErrorCode::BreakClipLoadingError),
902 => Ok(MediaDetailedErrorCode::BreakSeekInterceptorError),
423 => Ok(MediaDetailedErrorCode::DashInvalidSegmentInfo),
422 => Ok(MediaDetailedErrorCode::DashManifestNoMimeType),
421 => Ok(MediaDetailedErrorCode::DashManifestNoPeriods),
420 => Ok(MediaDetailedErrorCode::DashManifestUnknown),
321 => Ok(MediaDetailedErrorCode::DashNetwork),
322 => Ok(MediaDetailedErrorCode::DashNoInit),
999 => Ok(MediaDetailedErrorCode::Generic),
411 => Ok(MediaDetailedErrorCode::HlsManifestMaster),
412 => Ok(MediaDetailedErrorCode::HlsManifestPlaylist),
315 => Ok(MediaDetailedErrorCode::HlsNetworkInvalidSegment),
314 => Ok(MediaDetailedErrorCode::HlsNetworkKeyLoad),
311 => Ok(MediaDetailedErrorCode::HlsNetworkMasterPlaylist),
313 => Ok(MediaDetailedErrorCode::HlsNetworkNoKeyResponse),
312 => Ok(MediaDetailedErrorCode::HlsNetworkPlaylist),
316 => Ok(MediaDetailedErrorCode::HlsSegmentParsing),
903 => Ok(MediaDetailedErrorCode::ImageError),
905 => Ok(MediaDetailedErrorCode::LoadFailed),
904 => Ok(MediaDetailedErrorCode::LoadInterrupted),
400 => Ok(MediaDetailedErrorCode::ManifestUnknown),
201 => Ok(MediaDetailedErrorCode::MediakeysNetwork),
200 => Ok(MediaDetailedErrorCode::MediakeysUnknown),
202 => Ok(MediaDetailedErrorCode::MediakeysUnsupported),
203 => Ok(MediaDetailedErrorCode::MediakeysWebcrypto),
101 => Ok(MediaDetailedErrorCode::MediaAborted),
102 => Ok(MediaDetailedErrorCode::MediaDecode),
906 => Ok(MediaDetailedErrorCode::MediaErrorMessage),
103 => Ok(MediaDetailedErrorCode::MediaNetwork),
104 => Ok(MediaDetailedErrorCode::MediaSrcNotSupported),
100 => Ok(MediaDetailedErrorCode::MediaUnknown),
300 => Ok(MediaDetailedErrorCode::NetworkUnknown),
301 => Ok(MediaDetailedErrorCode::SegmentNetwork),
500 => Ok(MediaDetailedErrorCode::SegmentUnknown),
431 => Ok(MediaDetailedErrorCode::SmoothManifest),
331 => Ok(MediaDetailedErrorCode::SmoothNetwork),
332 => Ok(MediaDetailedErrorCode::SmoothNoMediaData),
110 => Ok(MediaDetailedErrorCode::SourceBufferFailure),
600 => Ok(MediaDetailedErrorCode::TextUnknown),
_ => Err(Error::Parsing(format!(
"media error code {} is not supported",
value
))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum MediaResponse {
Status(Status),
LoadCancelled(LoadCancelled),
LoadFailed(LoadFailed),
InvalidPlayerState(InvalidPlayerState),
InvalidRequest(InvalidRequest),
Error(MediaError),
NotImplemented(String, serde_json::Value),
}
pub struct MediaChannel<'a, W>
where
W: Read + Write,
{
sender: Cow<'a, str>,
message_manager: Lrc<MessageManager<W>>,
}
impl<'a, W> MediaChannel<'a, W>
where
W: Read + Write,
{
pub fn new<S>(sender: S, message_manager: Lrc<MessageManager<W>>) -> MediaChannel<'a, W>
where
S: Into<Cow<'a, str>>,
{
MediaChannel {
sender: sender.into(),
message_manager,
}
}
pub fn get_status<S>(
&self,
destination: S,
media_session_id: Option<i32>,
) -> Result<Status, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::GetStatusRequest {
typ: MESSAGE_TYPE_GET_STATUS.to_string(),
request_id,
media_session_id,
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
match self.parse(message)? {
MediaResponse::Status(status) => {
if status.request_id == request_id {
return Ok(Some(status));
}
}
MediaResponse::InvalidRequest(error) => {
if error.request_id == request_id {
return Err(Error::Internal(format!(
"Invalid request ({}).",
error.reason.unwrap_or_else(|| "Unknown".to_string())
)));
}
}
_ => {}
}
Ok(None)
})
}
pub fn load<S>(&self, destination: S, session_id: S, media: &Media) -> Result<Status, Error>
where
S: Into<Cow<'a, str>>,
{
self.load_with_opts(destination, session_id, media, LoadOptions::default())
}
pub fn load_with_opts<S>(
&self,
destination: S,
session_id: S,
media: &Media,
options: LoadOptions,
) -> Result<Status, Error>
where
S: Into<Cow<'a, str>>,
{
self.load_with_queue(destination, session_id, media, None, options)
}
pub fn load_with_queue<S>(
&self,
destination: S,
session_id: S,
media: &Media,
queue: Option<&MediaQueue>,
options: LoadOptions,
) -> Result<Status, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::MediaRequest {
request_id,
session_id: session_id.into().to_string(),
typ: MESSAGE_TYPE_LOAD.to_string(),
media: media.encode(),
current_time: options.current_time,
autoplay: options.autoplay,
custom_data: proxies::media::CustomData::new(),
queue_data: queue.map(|qd| qd.encode()),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
match self.parse(message)? {
MediaResponse::Status(status) => {
if status.request_id == request_id {
return Ok(Some(status));
}
let has_media = {
status.entries.iter().any(|entry| {
if let Some(ref loaded_media) = entry.media {
return loaded_media.content_id == media.content_id;
}
false
})
};
if has_media {
return Ok(Some(status));
}
}
MediaResponse::LoadFailed(error) => {
if error.request_id == request_id {
return Err(Error::Internal("Failed to load media.".to_string()));
}
}
MediaResponse::LoadCancelled(error) => {
if error.request_id == request_id {
return Err(Error::Internal(
"Load cancelled by another request.".to_string(),
));
}
}
MediaResponse::InvalidPlayerState(error) => {
if error.request_id == request_id {
return Err(Error::Internal(
"Load failed because of invalid player state.".to_string(),
));
}
}
MediaResponse::InvalidRequest(error) => {
if error.request_id == request_id {
return Err(Error::Internal(format!(
"Load failed because of invalid media request (reason: {}).",
error.reason.unwrap_or_else(|| "UNKNOWN".to_string())
)));
}
}
_ => {}
}
Ok(None)
})
}
pub fn load_queue<S>(
&self,
destination: S,
_session_id: S,
queue: &MediaQueue,
) -> Result<Status, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::QueueLoadRequest {
typ: MESSAGE_TYPE_QUEUE_LOAD.to_string(),
request_id,
custom_data: None,
items: queue.items.iter().map(|qi| qi.encode()).collect(),
queue_type: Some(queue.queue_type.to_string()),
repeat_mode: "REPEAT_OFF".to_owned(),
start_index: queue.start_index,
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
match self.parse(message)? {
MediaResponse::Status(status) => {
if status.request_id == request_id {
return Ok(Some(status));
}
}
MediaResponse::LoadFailed(error) => {
if error.request_id == request_id {
return Err(Error::Internal("Failed to load media.".to_string()));
}
}
MediaResponse::LoadCancelled(error) => {
if error.request_id == request_id {
return Err(Error::Internal(
"Load cancelled by another request.".to_string(),
));
}
}
MediaResponse::InvalidPlayerState(error) => {
if error.request_id == request_id {
return Err(Error::Internal(
"Load failed because of invalid player state.".to_string(),
));
}
}
MediaResponse::InvalidRequest(error) => {
if error.request_id == request_id {
return Err(Error::Internal(format!(
"Load failed because of invalid media request (reason: {}).",
error.reason.unwrap_or_else(|| "UNKNOWN".to_string())
)));
}
}
_ => {}
}
Ok(None)
})
}
pub fn pause<S>(&self, destination: S, media_session_id: i32) -> Result<StatusEntry, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::PlaybackGenericRequest {
request_id,
media_session_id,
typ: MESSAGE_TYPE_PAUSE.to_string(),
custom_data: proxies::media::CustomData::new(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.receive_status_entry(request_id, media_session_id)
}
pub fn play<S>(&self, destination: S, media_session_id: i32) -> Result<StatusEntry, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::PlaybackGenericRequest {
request_id,
media_session_id,
typ: MESSAGE_TYPE_PLAY.to_string(),
custom_data: proxies::media::CustomData::new(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.receive_status_entry(request_id, media_session_id)
}
pub fn stop<S>(&self, destination: S, media_session_id: i32) -> Result<StatusEntry, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::PlaybackGenericRequest {
request_id,
media_session_id,
typ: MESSAGE_TYPE_STOP.to_string(),
custom_data: proxies::media::CustomData::new(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.receive_status_entry(request_id, media_session_id)
}
pub fn seek<S>(
&self,
destination: S,
media_session_id: i32,
current_time: Option<f32>,
resume_state: Option<ResumeState>,
) -> Result<StatusEntry, Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::media::PlaybackSeekRequest {
request_id,
media_session_id,
typ: MESSAGE_TYPE_SEEK.to_string(),
current_time,
resume_state: resume_state.map(|s| s.to_string()),
custom_data: proxies::media::CustomData::new(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.receive_status_entry(request_id, media_session_id)
}
pub fn can_handle(&self, message: &CastMessage) -> bool {
message.namespace == CHANNEL_NAMESPACE
}
pub fn parse(&self, message: &CastMessage) -> Result<MediaResponse, Error> {
let reply = match message.payload {
CastMessagePayload::String(ref payload) => {
serde_json::from_str::<serde_json::Value>(payload)?
}
_ => {
return Err(Error::Internal(
"Binary payload is not supported!".to_string(),
));
}
};
let message_type = reply
.as_object()
.and_then(|object| object.get("type"))
.and_then(|property| property.as_str())
.unwrap_or("")
.to_string();
let response = match message_type.as_ref() {
MESSAGE_TYPE_MEDIA_STATUS => {
let reply: proxies::media::StatusReply = serde_json::value::from_value(reply)?;
let entries = reply
.status
.iter()
.map(StatusEntry::try_from)
.collect::<Result<_, _>>()?;
MediaResponse::Status(Status {
request_id: reply.request_id,
entries,
})
}
MESSAGE_TYPE_LOAD_CANCELLED => {
let reply: proxies::media::LoadCancelledReply =
serde_json::value::from_value(reply)?;
MediaResponse::LoadCancelled(LoadCancelled {
request_id: reply.request_id,
})
}
MESSAGE_TYPE_LOAD_FAILED => {
let reply: proxies::media::LoadFailedReply = serde_json::value::from_value(reply)?;
MediaResponse::LoadFailed(LoadFailed {
request_id: reply.request_id,
})
}
MESSAGE_TYPE_INVALID_PLAYER_STATE => {
let reply: proxies::media::InvalidPlayerStateReply =
serde_json::value::from_value(reply)?;
MediaResponse::InvalidPlayerState(InvalidPlayerState {
request_id: reply.request_id,
})
}
MESSAGE_TYPE_INVALID_REQUEST => {
let reply: proxies::media::InvalidRequestReply =
serde_json::value::from_value(reply)?;
MediaResponse::InvalidRequest(InvalidRequest {
request_id: reply.request_id,
reason: reply.reason,
})
}
MESSAGE_TYPE_ERROR => {
let reply: proxies::media::MediaErrorReply = serde_json::value::from_value(reply)?;
let detailed_error_code =
MediaDetailedErrorCode::try_from(reply.detailed_error_code)?;
MediaResponse::Error(MediaError {
detailed_error_code,
message_type: reply.message_type,
})
}
_ => MediaResponse::NotImplemented(message_type.to_string(), reply),
};
Ok(response)
}
fn receive_status_entry(
&self,
request_id: u32,
media_session_id: i32,
) -> Result<StatusEntry, Error> {
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
match self.parse(message)? {
MediaResponse::Status(mut status) => {
if status.request_id == request_id {
let position = status
.entries
.iter()
.position(|e| e.media_session_id == media_session_id);
return Ok(position.map(|position| status.entries.remove(position)));
}
}
MediaResponse::InvalidPlayerState(error) => {
if error.request_id == request_id {
return Err(Error::Internal(
"Request failed because of invalid player state.".to_string(),
));
}
}
MediaResponse::InvalidRequest(error) => {
if error.request_id == request_id {
return Err(Error::Internal(format!(
"Invalid request ({}).",
error.reason.unwrap_or_else(|| "Unknown".to_string())
)));
}
}
_ => {}
}
Ok(None)
})
}
}
#[cfg(test)]
mod tests {
use crate::{
DEFAULT_RECEIVER_ID, DEFAULT_SENDER_ID,
cast::cast_channel::cast_message::{PayloadType, ProtocolVersion},
tests::MockTcpStream,
};
use protobuf::EnumOrUnknown;
use super::*;
#[test]
fn test_get_status() {
let mut stream = MockTcpStream::new();
let payload = format!(
r#"{{
"requestId":1,
"type":"{}",
"status":[
{{
"mediaSessionId":1,
"playerState":"PLAYING",
"playbackRate":1.0,
"supportedMediaCommands":2300
}}
]
}}"#,
MESSAGE_TYPE_MEDIA_STATUS
);
stream.add_message(crate::cast::cast_channel::CastMessage {
protocol_version: Some(EnumOrUnknown::new(ProtocolVersion::CASTV2_1_2)),
source_id: Some(DEFAULT_RECEIVER_ID.to_string()),
destination_id: Some(DEFAULT_SENDER_ID.to_string()),
namespace: Some(CHANNEL_NAMESPACE.to_string()),
payload_type: Some(EnumOrUnknown::new(PayloadType::STRING)),
payload_utf8: Some(payload),
payload_binary: None,
continued: None,
remaining_length: None,
special_fields: Default::default(),
});
let channel = MediaChannel {
sender: Cow::from(DEFAULT_SENDER_ID),
message_manager: Lrc::new(MessageManager::new(stream)),
};
let result = channel.get_status("MyAppTransportId", None).unwrap();
assert_eq!(1, result.request_id);
if let Some(entry) = result.entries.first() {
assert_eq!(1, entry.media_session_id);
assert_eq!(PlayerState::Playing, entry.player_state);
assert_eq!(1.0, entry.playback_rate);
assert_eq!(2300, entry.supported_media_commands);
}
}
#[test]
fn test_parse_media_error() {
let message = CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: DEFAULT_RECEIVER_ID.to_string(),
destination: DEFAULT_SENDER_ID.to_string(),
payload: CastMessagePayload::String(
"{\"type\":\"ERROR\",\"detailedErrorCode\":104,\"itemId\":1}".to_string(),
),
};
let channel = MediaChannel {
sender: Cow::from(DEFAULT_SENDER_ID),
message_manager: Lrc::new(MessageManager::new(MockTcpStream::new())),
};
let expected_result = MediaError {
detailed_error_code: MediaDetailedErrorCode::MediaSrcNotSupported,
message_type: MESSAGE_TYPE_ERROR.to_string(),
};
let response = channel.parse(&message).unwrap();
assert_eq!(MediaResponse::Error(expected_result), response);
}
#[test]
fn test_parse_unknown_message_type() {
let message_type = "FOO_BAR";
let payload = format!("{{\"type\":\"{}\",\"itemId\":666}}", message_type);
let expected_payload = serde_json::from_str::<serde_json::Value>(payload.as_str()).unwrap();
let message = CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: DEFAULT_RECEIVER_ID.to_string(),
destination: DEFAULT_SENDER_ID.to_string(),
payload: CastMessagePayload::String(payload),
};
let stream = MockTcpStream::new();
let channel = MediaChannel {
sender: Cow::from(DEFAULT_SENDER_ID),
message_manager: Lrc::new(MessageManager::new(stream)),
};
let expected_result =
MediaResponse::NotImplemented(message_type.to_string(), expected_payload);
let result = channel.parse(&message).unwrap();
assert_eq!(expected_result, result);
}
}