Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s

Builds out the full v1 scope: a vendored+patched openscreen subset for
low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the
existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's
Idle/Casting state machine with a private IPC socket, the breadcast GTK4
popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd
integration (device discovery, start/stop, mirroring lifecycle events).
Also adds bakery/systemd/Forgejo CI packaging.

Validated end-to-end against a real Chromecast/Google TV: negotiated
Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/
stop through the actual popup.
This commit is contained in:
Breadway 2026-08-03 09:07:21 +08:00
parent 887c29002f
commit 8c745d18e0
283 changed files with 36788 additions and 0 deletions

8
vendor/rust_cast-0.21.0/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.vscode
.idea
**/*.rs.bk
target
out
*.iml
node_modules
Cargo.lock

1
vendor/rust_cast-0.21.0/.husky/commit-msg vendored Executable file
View file

@ -0,0 +1 @@
npx --no -- commitlint --edit ${1}

30
vendor/rust_cast-0.21.0/.husky/pre-push vendored Executable file
View file

@ -0,0 +1,30 @@
#!/bin/sh
set -eu
if ! cargo +nightly fmt --all -- --check
then
echo "There are some code style issues."
echo "Run `cargo fmt` first."
exit 1
fi
if ! cargo clippy --all-targets -- -D warnings
then
echo "There are some clippy issues."
exit 1
fi
if ! cargo test
then
echo "There are some test issues."
exit 1
fi
if ! cargo test --features thread_safe
then
echo "There are some test issues (with `thread_safe` feature)."
exit 1
fi
exit 0

41
vendor/rust_cast-0.21.0/Cargo.toml vendored Normal file
View file

@ -0,0 +1,41 @@
[package]
name = "rust_cast"
description = "Library that allows you to communicate with Google Cast enabled devices (e.g. Chromecast)."
documentation = "https://docs.rs/crate/rust_cast/"
homepage = "https://github.com/azasypkin/rust-cast"
repository = "https://github.com/azasypkin/rust-cast"
readme = "README.md"
license = "MIT"
keywords = ["cast", "chromecast", "google"]
version = "0.21.0"
authors = ["Aleh Zasypkin <aleh.zasypkin@gmail.com>"]
categories = ["api-bindings", "hardware-support", "multimedia"]
edition = "2024"
exclude = [
".github/*",
"examples/*",
"protobuf/*",
]
[dependencies]
byteorder = "1.5"
log = "0.4"
protobuf = "=3.7.2"
rustls = "0.23"
rustls-native-certs = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
[dev-dependencies]
ansi_term = "0.12"
docopt = "1"
env_logger = "0.11"
mdns-sd = "0.17"
[build-dependencies]
protobuf-codegen = "=3.7.2"
[features]
thread_safe = []
cast = []

21
vendor/rust_cast-0.21.0/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Aleh Zasypkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

37
vendor/rust_cast-0.21.0/PATCHES.md vendored Normal file
View file

@ -0,0 +1,37 @@
# Vendoring notes
This is [rust_cast 0.21.0](https://github.com/azasypkin/rust-cast) (MIT
licensed), vendored via `[patch.crates-io]` in the workspace `Cargo.toml`
purely to add one method upstream doesn't have: a generic point-to-point
send on an arbitrary CASTV2 namespace.
## Why
breadcast's Cast Streaming integration (`breadcast-caststream-sys`, wrapping
a vendored `chromium/openscreen`) needs to exchange OFFER/ANSWER JSON with a
launched receiver app on the `urn:x-cast:com.google.cast.webrtc` namespace --
a point-to-point conversation targeting that app's `transport_id`, the same
target `ConnectionChannel`/`MediaChannel` already use. None of rust_cast's
built-in channels expose that: `ReceiverChannel::broadcast_message()` is the
closest, but it hardcodes destination `"*"`, which is a different
conversation than a namespace-specific exchange with one particular app.
Receiving such messages needs no patch -- `CastDevice::receive()` already
returns them as `ChannelMessage::Raw(CastMessage)` whenever no built-in
channel claims the namespace.
## The patch
`src/lib.rs`: added `CastDevice::send_message<M: Serialize>(&self, namespace,
destination, message)`, built the same way `ReceiverChannel::broadcast_message()`
is internally, but with a caller-supplied `destination` instead of a
hardcoded `"*"`. See the doc comment on that method for the exact rationale
(marked "LOCAL PATCH (breadcast, not upstream)").
## Rolling the pin
To move to a newer rust_cast release: copy the new version from
`~/.cargo/registry/src/*/rust_cast-<version>/`, re-apply the same method
addition (small enough to redo by hand), swap the version in this
directory's own `Cargo.toml`, and update the `path` if the directory name
changes.

102
vendor/rust_cast-0.21.0/README.md vendored Normal file
View file

@ -0,0 +1,102 @@
[![Docs](https://docs.rs/rust_cast/badge.svg)](https://docs.rs/crate/rust_cast/)
![Build Status](https://github.com/azasypkin/rust-cast/actions/workflows/ci.yml/badge.svg)
# Usage
* [Documentation](https://docs.rs/crate/rust_cast/)
* Try out [Rust Caster](./examples/rust_caster.rs) example to see this crate in action!
# Build
Proto files are taken from [Chromium Open Screen GitHub mirror](https://chromium.googlesource.com/openscreen/+/37a17677e5ded963fc41a3d8dee7a59484e5ec13/cast/common/channel/proto).
By default `cargo build` won't try to generate Rust code from the files located at `protobuf/*`, if you want to do that
use `GENERATE_PROTO` environment variable during build and make sure you have `protoc` binary in `$PATH`:
```bash
$ GENERATE_PROTO=true cargo build
```
# Run example
## Generic features
First, you need to figure out the address of the device to connect to. For example, you can use `avahi` with the following command:
```bash
$ avahi-browse -a --resolve
```
```bash
// Get some info about the Google Cast enabled device (e.g. Chromecast).
$ cargo run --example rust_caster -- -a 192.168.0.100 -i
Number of apps run: 1
App#0: Default Media Receiver (CC1AD845)
Volume level: 1
Muted: false
// Run specific app on the Chromecast.
$ cargo run --example rust_caster -- -a 192.168.0.100 -r youtube
// Stop specific active app.
$ cargo run --example rust_caster -- -a 192.168.0.100 -s youtube
// Stop currently active app.
$ cargo run --example rust_caster -- -a 192.168.0.100 --stop-current
The following app has been stopped: Default Media Receiver (CC1AD845)
```
## Media features
```bash
// Stream a video.
$ cargo run --example rust_caster -- -a 192.168.0.100 -m http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
// Stream a video of specific format with buffering.
$ cargo run --example rust_caster -- -a 192.168.0.100 -m http://xxx.webm --media-type video/webm --media-stream-type buffered
// Stream video from YouTube (doesn't work with the latest YouTube app, fix is welcome).
$ cargo run --example rust_caster -- -a 192.168.0.100 -m 7LcUOEP7Brc --media-app youtube
// Display an image.
$ cargo run --example rust_caster -- -a 192.168.0.100 -m https://azasypkin.github.io/style-my-image/images/mozilla.jpg
// Change volume level.
$ cargo run --example rust_caster -- -a 192.168.0.100 --media-volume 0.5
// Mute/unmute media.
$ cargo run --example rust_caster -- -a 192.168.0.100 --media-mute [--media-unmute]
// Pause media.
$ cargo run --example rust_caster -- -a 192.168.0.100 --media-app youtube --media-pause
// Resume/play media.
$ cargo run --example rust_caster -- -a 192.168.0.100 --media-app youtube --media-play
// Seek media.
$ cargo run --example rust_caster -- -a 192.168.0.100 --media-app youtube --media-seek 100
```
For all possible values of `--media-type` see [Supported Media for Google Cast](https://developers.google.com/cast/docs/media).
# DNS TXT Record description
* `md` - Model Name (e.g. "Chromecast");
* `id` - UUID without hyphens of the particular device (e.g. xx12x3x456xx789xx01xx234x56789x0);
* `fn` - Friendly Name of the device (e.g. "Living Room");
* `rs` - Unknown (recent share???) (e.g. "Youtube TV");
* `bs` - Uknonwn (e.g. "XX1XXX2X3456");
* `st` - Unknown (e.g. "1");
* `ca` - Unknown (e.g. "1234");
* `ic` - Icon path (e.g. "/setup/icon.png");
* `ve` - Version (e.g. "04").
# Model names
* `Chromecast` - Regular chromecast, supports video/audio;
* `Chromecast Audio` - Chromecast Audio device, supports only audio.
# Useful links and sources of inspiration
* [DIAL Protocol](http://www.dial-multiscreen.org/);
* [An implementation of the Chromecast CASTV2 protocol in JS](https://github.com/thibauts/node-castv2);
* [Chromecast - steps closer to a python native api](http://www.clift.org/fred/chromecast-steps-closer-to-a-python-native-api.html);

22
vendor/rust_cast-0.21.0/build.rs vendored Normal file
View file

@ -0,0 +1,22 @@
use protobuf_codegen::{Codegen, Customize};
use std::env;
fn main() {
let generate_proto = env::var("GENERATE_PROTO").unwrap_or_else(|_| "false".to_string());
if generate_proto == "true" {
Codegen::new()
.out_dir("src/cast")
.inputs([
"protobuf/authority_keys.proto",
"protobuf/cast_channel.proto",
])
.includes(["protobuf"])
.customize(Customize::default().gen_mod_rs(false))
.run()
.expect("protoc");
}
println!("rerun-if-env-changed=GENERATE_PROTO");
println!("rerun-if-changed=protobuf/authority_keys.proto");
println!("rerun-if-changed=protobuf/cast_channel.proto");
}

2
vendor/rust_cast-0.21.0/rustfmt.toml vendored Normal file
View file

@ -0,0 +1,2 @@
unstable_features = true
imports_granularity = "Crate"

View file

@ -0,0 +1,306 @@
// This file is generated by rust-protobuf 3.7.2. Do not edit
// .proto file is parsed by protoc 33.2
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]
#![allow(unused_attributes)]
#![cfg_attr(rustfmt, rustfmt::skip)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unused_results)]
#![allow(unused_mut)]
//! Generated file from `authority_keys.proto`
// Generated for lite runtime
/// Generated files are compatible only with the same version
/// of protobuf runtime.
const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_3_7_2;
// @@protoc_insertion_point(message:openscreen.cast.proto.AuthorityKeys)
#[derive(PartialEq,Clone,Default,Debug)]
pub struct AuthorityKeys {
// message fields
// @@protoc_insertion_point(field:openscreen.cast.proto.AuthorityKeys.keys)
pub keys: ::std::vec::Vec<authority_keys::Key>,
// special fields
// @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthorityKeys.special_fields)
pub special_fields: ::protobuf::SpecialFields,
}
impl<'a> ::std::default::Default for &'a AuthorityKeys {
fn default() -> &'a AuthorityKeys {
<AuthorityKeys as ::protobuf::Message>::default_instance()
}
}
impl AuthorityKeys {
pub fn new() -> AuthorityKeys {
::std::default::Default::default()
}
}
impl ::protobuf::Message for AuthorityKeys {
const NAME: &'static str = "AuthorityKeys";
fn is_initialized(&self) -> bool {
for v in &self.keys {
if !v.is_initialized() {
return false;
}
};
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> {
while let Some(tag) = is.read_raw_tag_or_eof()? {
match tag {
10 => {
self.keys.push(is.read_message()?);
},
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u64 {
let mut my_size = 0;
for value in &self.keys {
let len = value.compute_size();
my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len;
};
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> {
for v in &self.keys {
::protobuf::rt::write_message_field_with_cached_size(1, v, os)?;
};
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
fn special_fields(&self) -> &::protobuf::SpecialFields {
&self.special_fields
}
fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields {
&mut self.special_fields
}
fn new() -> AuthorityKeys {
AuthorityKeys::new()
}
fn clear(&mut self) {
self.keys.clear();
self.special_fields.clear();
}
fn default_instance() -> &'static AuthorityKeys {
static instance: AuthorityKeys = AuthorityKeys {
keys: ::std::vec::Vec::new(),
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
}
}
/// Nested message and enums of message `AuthorityKeys`
pub mod authority_keys {
// @@protoc_insertion_point(message:openscreen.cast.proto.AuthorityKeys.Key)
#[derive(PartialEq,Clone,Default,Debug)]
pub struct Key {
// message fields
// @@protoc_insertion_point(field:openscreen.cast.proto.AuthorityKeys.Key.fingerprint)
pub fingerprint: ::std::option::Option<::std::vec::Vec<u8>>,
// @@protoc_insertion_point(field:openscreen.cast.proto.AuthorityKeys.Key.public_key)
pub public_key: ::std::option::Option<::std::vec::Vec<u8>>,
// special fields
// @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthorityKeys.Key.special_fields)
pub special_fields: ::protobuf::SpecialFields,
}
impl<'a> ::std::default::Default for &'a Key {
fn default() -> &'a Key {
<Key as ::protobuf::Message>::default_instance()
}
}
impl Key {
pub fn new() -> Key {
::std::default::Default::default()
}
// required bytes fingerprint = 1;
pub fn fingerprint(&self) -> &[u8] {
match self.fingerprint.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_fingerprint(&mut self) {
self.fingerprint = ::std::option::Option::None;
}
pub fn has_fingerprint(&self) -> bool {
self.fingerprint.is_some()
}
// Param is passed by value, moved
pub fn set_fingerprint(&mut self, v: ::std::vec::Vec<u8>) {
self.fingerprint = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_fingerprint(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.fingerprint.is_none() {
self.fingerprint = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.fingerprint.as_mut().unwrap()
}
// Take field
pub fn take_fingerprint(&mut self) -> ::std::vec::Vec<u8> {
self.fingerprint.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
// required bytes public_key = 2;
pub fn public_key(&self) -> &[u8] {
match self.public_key.as_ref() {
Some(v) => v,
None => &[],
}
}
pub fn clear_public_key(&mut self) {
self.public_key = ::std::option::Option::None;
}
pub fn has_public_key(&self) -> bool {
self.public_key.is_some()
}
// Param is passed by value, moved
pub fn set_public_key(&mut self, v: ::std::vec::Vec<u8>) {
self.public_key = ::std::option::Option::Some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_public_key(&mut self) -> &mut ::std::vec::Vec<u8> {
if self.public_key.is_none() {
self.public_key = ::std::option::Option::Some(::std::vec::Vec::new());
}
self.public_key.as_mut().unwrap()
}
// Take field
pub fn take_public_key(&mut self) -> ::std::vec::Vec<u8> {
self.public_key.take().unwrap_or_else(|| ::std::vec::Vec::new())
}
}
impl ::protobuf::Message for Key {
const NAME: &'static str = "Key";
fn is_initialized(&self) -> bool {
if self.fingerprint.is_none() {
return false;
}
if self.public_key.is_none() {
return false;
}
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> {
while let Some(tag) = is.read_raw_tag_or_eof()? {
match tag {
10 => {
self.fingerprint = ::std::option::Option::Some(is.read_bytes()?);
},
18 => {
self.public_key = ::std::option::Option::Some(is.read_bytes()?);
},
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u64 {
let mut my_size = 0;
if let Some(v) = self.fingerprint.as_ref() {
my_size += ::protobuf::rt::bytes_size(1, &v);
}
if let Some(v) = self.public_key.as_ref() {
my_size += ::protobuf::rt::bytes_size(2, &v);
}
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> {
if let Some(v) = self.fingerprint.as_ref() {
os.write_bytes(1, v)?;
}
if let Some(v) = self.public_key.as_ref() {
os.write_bytes(2, v)?;
}
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
fn special_fields(&self) -> &::protobuf::SpecialFields {
&self.special_fields
}
fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields {
&mut self.special_fields
}
fn new() -> Key {
Key::new()
}
fn clear(&mut self) {
self.fingerprint = ::std::option::Option::None;
self.public_key = ::std::option::Option::None;
self.special_fields.clear();
}
fn default_instance() -> &'static Key {
static instance: Key = Key {
fingerprint: ::std::option::Option::None,
public_key: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
pub mod authority_keys;
pub mod cast_channel;
pub mod proxies;

View file

@ -0,0 +1,529 @@
/// Proxy classes for the `connection` channel.
pub mod connection {
use serde::Serialize;
#[derive(Serialize, Debug)]
pub struct ConnectionRequest {
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "userAgent")]
pub user_agent: String,
}
}
/// Proxy classes for the `heartbeat` channel.
pub mod heartbeat {
use serde::Serialize;
#[derive(Serialize, Debug)]
pub struct HeartBeatRequest {
#[serde(rename = "type")]
pub typ: String,
}
}
/// Proxy classes for the `media` channel.
pub mod media {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub struct GetStatusRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "mediaSessionId", skip_serializing_if = "Option::is_none")]
pub media_session_id: Option<i32>,
}
// Really LoadRequest
/// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.LoadRequest
#[derive(Serialize, Debug)]
pub struct MediaRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "sessionId")]
pub session_id: String,
#[serde(rename = "type")]
pub typ: String,
pub media: Media,
#[serde(rename = "currentTime")]
pub current_time: f64,
#[serde(rename = "customData")]
pub custom_data: CustomData,
pub autoplay: bool,
#[serde(rename = "queueData", skip_serializing_if = "Option::is_none")]
pub queue_data: Option<QueueData>,
}
/// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueItem
#[derive(Serialize, Debug)]
pub struct QueueItem {
#[serde(rename = "activeTrackIds")]
#[serde(skip_serializing_if = "Option::is_none")]
pub active_track_ids: Option<Vec<u16>>,
pub autoplay: bool,
#[serde(rename = "customData")]
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_data: Option<CustomData>,
#[serde(rename = "itemId")]
#[serde(skip_serializing_if = "Option::is_none")]
pub item_id: Option<u16>,
pub media: Media,
#[serde(rename = "playbackDuration")]
pub playback_duration: Option<f64>,
#[serde(rename = "preloadTime")]
pub preload_time: f64,
#[serde(rename = "startTime")]
pub start_time: f64,
}
/// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueLoadRequest
#[derive(Serialize, Debug)]
pub struct QueueLoadRequest {
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "customData")]
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_data: Option<CustomData>,
pub items: Vec<QueueItem>,
// This is from https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData
#[serde(rename = "queueType")]
#[serde(skip_serializing_if = "Option::is_none")]
pub queue_type: Option<String>,
#[serde(rename = "repeatMode")]
pub repeat_mode: String,
#[serde(rename = "startIndex")]
pub start_index: u16,
}
/// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData
#[derive(Serialize, Debug)]
pub struct QueueData {
pub items: Vec<QueueItem>,
#[serde(rename = "queueType")]
#[serde(skip_serializing_if = "Option::is_none")]
pub queue_type: Option<String>,
#[serde(rename = "repeatMode")]
pub repeat_mode: String,
#[serde(rename = "startIndex")]
pub start_index: u16,
}
#[derive(Serialize, Debug)]
pub struct PlaybackGenericRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "mediaSessionId")]
pub media_session_id: i32,
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "customData")]
pub custom_data: CustomData,
}
#[derive(Serialize, Debug)]
pub struct PlaybackSeekRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "mediaSessionId")]
pub media_session_id: i32,
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "resumeState")]
pub resume_state: Option<String>,
#[serde(rename = "currentTime")]
pub current_time: Option<f32>,
#[serde(rename = "customData")]
pub custom_data: CustomData,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Media {
#[serde(rename = "contentId")]
pub content_id: String,
#[serde(rename = "streamType", default)]
pub stream_type: String,
#[serde(rename = "contentType")]
pub content_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<f32>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Metadata {
#[serde(rename = "metadataType")]
pub metadata_type: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "seriesTitle")]
pub series_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "albumName")]
pub album_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtitle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "albumArtist")]
pub album_artist: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artist: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub composer: Option<String>,
pub images: Vec<Image>,
#[serde(skip_serializing_if = "Option::is_none", rename = "releaseDate")]
pub release_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "originalAirDate")]
pub original_air_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "creationDateTime")]
pub creation_date_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub studio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latitude: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub longitude: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub season: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub episode: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "trackNumber")]
pub track_number: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "discNumber")]
pub disc_number: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
}
impl Metadata {
pub fn new(metadata_type: u32) -> Metadata {
Metadata {
metadata_type,
title: None,
series_title: None,
album_name: None,
subtitle: None,
album_artist: None,
artist: None,
composer: None,
images: Vec::new(),
release_date: None,
original_air_date: None,
creation_date_time: None,
studio: None,
location: None,
latitude: None,
longitude: None,
season: None,
episode: None,
track_number: None,
disc_number: None,
width: None,
height: None,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Image {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
}
#[derive(Serialize, Debug)]
pub struct CustomData {}
impl CustomData {
pub fn new() -> CustomData {
CustomData {}
}
}
#[derive(Deserialize, Debug)]
pub struct ExtendedStatus {
#[serde(rename = "playerState")]
pub player_state: String,
#[serde(rename = "mediaSessionId")]
pub media_session_id: Option<i32>,
pub media: Option<Media>,
}
#[derive(Deserialize, Debug)]
pub struct Status {
#[serde(rename = "mediaSessionId")]
pub media_session_id: i32,
#[serde(default)]
pub media: Option<Media>,
#[serde(rename = "playbackRate")]
pub playback_rate: f32,
#[serde(rename = "playerState")]
pub player_state: String,
#[serde(rename = "currentItemId")]
pub current_item_id: Option<u16>,
#[serde(rename = "loadingItemId")]
pub loading_item_id: Option<u16>,
#[serde(rename = "preloadedItemId")]
pub preloaded_item_id: Option<u16>,
#[serde(rename = "idleReason")]
pub idle_reason: Option<String>,
#[serde(rename = "extendedStatus")]
pub extended_status: Option<ExtendedStatus>,
#[serde(rename = "currentTime")]
pub current_time: Option<f32>,
#[serde(rename = "supportedMediaCommands")]
pub supported_media_commands: u32,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct StatusReply {
#[serde(rename = "requestId", default)]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
pub status: Vec<Status>,
}
#[derive(Deserialize, Debug)]
pub struct LoadCancelledReply {
#[serde(rename = "requestId")]
pub request_id: u32,
}
#[derive(Deserialize, Debug)]
pub struct LoadFailedReply {
#[serde(rename = "requestId")]
pub request_id: u32,
}
#[derive(Deserialize, Debug)]
pub struct InvalidPlayerStateReply {
#[serde(rename = "requestId")]
pub request_id: u32,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct InvalidRequestReply {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
pub reason: Option<String>,
}
/// The media error encountered during media operations.
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MediaErrorReply {
/// The detailed error code associated with the media error.
pub detailed_error_code: i32,
/// The type of the error message.
#[serde(rename = "type")]
pub message_type: String,
}
}
/// Proxy classes for the `receiver` channel.
pub mod receiver {
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub struct AppLaunchRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "appId")]
pub app_id: String,
}
#[derive(Serialize, Debug)]
pub struct AppStopRequest<'a> {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
#[serde(rename = "sessionId")]
pub session_id: Cow<'a, str>,
}
#[derive(Serialize, Debug)]
pub struct GetStatusRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
}
#[derive(Serialize, Debug)]
pub struct SetVolumeRequest {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
pub volume: Volume,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct StatusReply {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
pub status: Status,
}
#[derive(Deserialize, Debug)]
pub struct Status {
#[serde(default)]
pub applications: Vec<Application>,
#[serde(rename = "isActiveInput", default)]
pub is_active_input: bool,
#[serde(rename = "isStandBy", default)]
pub is_stand_by: bool,
/// Volume parameters of the currently active cast device.
pub volume: Volume,
}
#[derive(Deserialize, Debug)]
pub struct Application {
#[serde(rename = "appId")]
pub app_id: String,
#[serde(rename = "sessionId")]
pub session_id: String,
#[serde(rename = "transportId", default)]
pub transport_id: String,
#[serde(default)]
pub namespaces: Vec<AppNamespace>,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "statusText")]
pub status_text: String,
}
#[derive(Deserialize, Debug)]
pub struct AppNamespace {
pub name: String,
}
/// Structure that describes possible cast device volume options.
#[derive(Deserialize, Serialize, Debug)]
pub struct Volume {
/// Volume level.
pub level: Option<f32>,
/// Mute/unmute state.
pub muted: Option<bool>,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct LaunchErrorReply {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
pub reason: Option<String>,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct InvalidRequestReply {
#[serde(rename = "requestId")]
pub request_id: u32,
#[serde(rename = "type")]
pub typ: String,
pub reason: Option<String>,
}
}

View file

@ -0,0 +1,113 @@
use std::{
borrow::Cow,
io::{Read, Write},
};
use crate::{
Lrc,
cast::proxies,
errors::Error,
message_manager::{CastMessage, CastMessagePayload, MessageManager},
};
pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.tp.connection";
const CHANNEL_USER_AGENT: &str = "RustCast";
const MESSAGE_TYPE_CONNECT: &str = "CONNECT";
const MESSAGE_TYPE_CLOSE: &str = "CLOSE";
#[derive(Clone, Debug)]
pub enum ConnectionResponse {
Connect,
Close,
NotImplemented(String, serde_json::Value),
}
pub struct ConnectionChannel<'a, W>
where
W: Read + Write,
{
sender: Cow<'a, str>,
message_manager: Lrc<MessageManager<W>>,
}
impl<'a, W> ConnectionChannel<'a, W>
where
W: Read + Write,
{
pub fn new<S>(sender: S, message_manager: Lrc<MessageManager<W>>) -> ConnectionChannel<'a, W>
where
S: Into<Cow<'a, str>>,
{
ConnectionChannel {
sender: sender.into(),
message_manager,
}
}
pub fn connect<S>(&self, destination: S) -> Result<(), Error>
where
S: Into<Cow<'a, str>>,
{
let payload = serde_json::to_string(&proxies::connection::ConnectionRequest {
typ: MESSAGE_TYPE_CONNECT.to_string(),
user_agent: CHANNEL_USER_AGENT.to_string(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})
}
pub fn disconnect<S>(&self, destination: S) -> Result<(), Error>
where
S: Into<Cow<'a, str>>,
{
let payload = serde_json::to_string(&proxies::connection::ConnectionRequest {
typ: MESSAGE_TYPE_CLOSE.to_string(),
user_agent: CHANNEL_USER_AGENT.to_string(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: destination.into().to_string(),
payload: CastMessagePayload::String(payload),
})
}
pub fn can_handle(&self, message: &CastMessage) -> bool {
message.namespace == CHANNEL_NAMESPACE
}
pub fn parse(&self, message: &CastMessage) -> Result<ConnectionResponse, 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_CONNECT => ConnectionResponse::Connect,
MESSAGE_TYPE_CLOSE => ConnectionResponse::Close,
_ => ConnectionResponse::NotImplemented(message_type.to_string(), reply),
};
Ok(response)
}
}

View file

@ -0,0 +1,110 @@
use std::{
borrow::Cow,
io::{Read, Write},
};
use crate::{
Lrc,
cast::proxies,
errors::Error,
message_manager::{CastMessage, CastMessagePayload, MessageManager},
};
pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.tp.heartbeat";
const MESSAGE_TYPE_PING: &str = "PING";
const MESSAGE_TYPE_PONG: &str = "PONG";
#[derive(Clone, Debug)]
pub enum HeartbeatResponse {
Ping,
Pong,
NotImplemented(String, serde_json::Value),
}
pub struct HeartbeatChannel<'a, W>
where
W: Read + Write,
{
sender: Cow<'a, str>,
receiver: Cow<'a, str>,
message_manager: Lrc<MessageManager<W>>,
}
impl<'a, W> HeartbeatChannel<'a, W>
where
W: Read + Write,
{
pub fn new<S>(
sender: S,
receiver: S,
message_manager: Lrc<MessageManager<W>>,
) -> HeartbeatChannel<'a, W>
where
S: Into<Cow<'a, str>>,
{
HeartbeatChannel {
sender: sender.into(),
receiver: receiver.into(),
message_manager,
}
}
pub fn ping(&self) -> Result<(), Error> {
let payload = serde_json::to_string(&proxies::heartbeat::HeartBeatRequest {
typ: MESSAGE_TYPE_PING.to_string(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: self.receiver.to_string(),
payload: CastMessagePayload::String(payload),
})
}
pub fn pong(&self) -> Result<(), Error> {
let payload = serde_json::to_string(&proxies::heartbeat::HeartBeatRequest {
typ: MESSAGE_TYPE_PONG.to_string(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: self.receiver.to_string(),
payload: CastMessagePayload::String(payload),
})
}
pub fn can_handle(&self, message: &CastMessage) -> bool {
message.namespace == CHANNEL_NAMESPACE
}
pub fn parse(&self, message: &CastMessage) -> Result<HeartbeatResponse, 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_PING => HeartbeatResponse::Ping,
MESSAGE_TYPE_PONG => HeartbeatResponse::Pong,
_ => HeartbeatResponse::NotImplemented(message_type.to_string(), reply),
};
Ok(response)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
pub mod connection;
pub mod heartbeat;
pub mod media;
pub mod receiver;

View file

@ -0,0 +1,518 @@
use std::{
borrow::Cow,
convert::Into,
fmt,
io::{Read, Write},
str::FromStr,
string::ToString,
};
use serde::Serialize;
use crate::{
Lrc,
cast::proxies,
errors::Error,
message_manager::{CastMessage, CastMessagePayload, MessageManager},
};
pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.receiver";
const MESSAGE_TYPE_LAUNCH: &str = "LAUNCH";
const MESSAGE_TYPE_STOP: &str = "STOP";
const MESSAGE_TYPE_GET_STATUS: &str = "GET_STATUS";
const MESSAGE_TYPE_SET_VOLUME: &str = "SET_VOLUME";
const MESSAGE_TYPE_RECEIVER_STATUS: &str = "RECEIVER_STATUS";
const MESSAGE_TYPE_LAUNCH_ERROR: &str = "LAUNCH_ERROR";
const MESSAGE_TYPE_INVALID_REQUEST: &str = "INVALID_REQUEST";
const APP_DEFAULT_MEDIA_RECEIVER_ID: &str = "CC1AD845";
const APP_BACKDROP_ID: &str = "E8C28D3C";
const APP_YOUTUBE_ID: &str = "233637DE";
/// Structure that describes possible cast device volume options.
#[derive(Copy, Clone, Debug)]
pub struct Volume {
/// Volume level.
pub level: Option<f32>,
/// Mute/unmute state.
pub muted: Option<bool>,
}
/// This `From<f32>` implementation is useful when only volume level is needed.
impl From<f32> for Volume {
fn from(level: f32) -> Self {
Self {
level: Some(level),
muted: None,
}
}
}
/// This `From<bool>` implementation is useful when only mute/unmute state is needed.
impl From<bool> for Volume {
fn from(muted: bool) -> Self {
Self {
level: None,
muted: Some(muted),
}
}
}
/// This `From<(f32, bool)>` implementation is useful when both volume level and mute/unmute state are
/// needed.
impl From<(f32, bool)> for Volume {
fn from((level, muted): (f32, bool)) -> Self {
Self {
level: Some(level),
muted: Some(muted),
}
}
}
/// Structure that describes currently run Cast Device application.
#[derive(Clone, Debug)]
pub struct Application {
/// The identifier of the Cast application. Not for display.
pub app_id: String,
/// Session id of the currently active application.
pub session_id: String,
/// Name of the `pipe` to talk to the application.
pub transport_id: String,
/// A list of the namespaces supported by the receiver application.
pub namespaces: Vec<String>,
/// The human-readable name of the Cast application, for example, "YouTube".
pub display_name: String,
/// Descriptive text for the current application content, for example “My vacations”.
pub status_text: String,
}
/// Describes the current status of the receiver cast device.
#[derive(Clone, Debug)]
pub struct Status {
/// Unique id of the request that requested the status.
pub request_id: u32,
/// Contains the list of applications that are currently run.
pub applications: Vec<Application>,
/// Determines whether the Cast device is the active input or not.
pub is_active_input: bool,
/// Determines whether the Cast device is in stand by mode.
pub is_stand_by: bool,
/// Volume parameters of the currently active cast device.
pub volume: Volume,
}
/// Describes the application launch error.
#[derive(Clone, Debug)]
pub struct LaunchError {
/// Unique id of the request that tried to launch application.
pub request_id: u32,
/// Description of the launch error reason if available.
pub reason: Option<String>,
}
/// Describes the invalid request error.
#[derive(Clone, Debug)]
pub struct InvalidRequest {
/// Unique id of the invalid request.
pub request_id: u32,
/// Description of the invalid request reason if available.
pub reason: Option<String>,
}
/// Represents all currently supported incoming messages that receiver channel can handle.
#[derive(Clone, Debug)]
pub enum ReceiverResponse {
/// Status of the currently active receiver.
Status(Status),
/// Error indicating that receiver failed to launch application.
LaunchError(LaunchError),
/// Error indicating that request is not valid.
InvalidRequest(InvalidRequest),
/// Used every time when channel can't parse the message. Associated data contains `type` string
/// field and raw JSON data returned from cast device.
NotImplemented(String, serde_json::Value),
}
#[derive(Clone, Debug, PartialEq)]
pub enum CastDeviceApp {
DefaultMediaReceiver,
Backdrop,
YouTube,
Custom(String),
}
impl FromStr for CastDeviceApp {
type Err = ();
fn from_str(s: &str) -> Result<CastDeviceApp, ()> {
let app = match s {
APP_DEFAULT_MEDIA_RECEIVER_ID | "default" => CastDeviceApp::DefaultMediaReceiver,
APP_BACKDROP_ID | "backdrop" => CastDeviceApp::Backdrop,
APP_YOUTUBE_ID | "youtube" => CastDeviceApp::YouTube,
custom => CastDeviceApp::Custom(custom.to_string()),
};
Ok(app)
}
}
impl fmt::Display for CastDeviceApp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = match *self {
CastDeviceApp::DefaultMediaReceiver => APP_DEFAULT_MEDIA_RECEIVER_ID.to_string(),
CastDeviceApp::Backdrop => APP_BACKDROP_ID.to_string(),
CastDeviceApp::YouTube => APP_YOUTUBE_ID.to_string(),
CastDeviceApp::Custom(ref app_id) => app_id.to_string(),
};
write!(f, "{}", str)
}
}
pub struct ReceiverChannel<'a, W>
where
W: Write + Read,
{
sender: Cow<'a, str>,
receiver: Cow<'a, str>,
message_manager: Lrc<MessageManager<W>>,
}
impl<'a, W> ReceiverChannel<'a, W>
where
W: Write + Read,
{
pub fn new<S>(
sender: S,
receiver: S,
message_manager: Lrc<MessageManager<W>>,
) -> ReceiverChannel<'a, W>
where
S: Into<Cow<'a, str>>,
{
ReceiverChannel {
sender: sender.into(),
receiver: receiver.into(),
message_manager,
}
}
/// Launches the specified receiver's application.
///
/// # Examples
///
/// ```no_run
/// use std::str::FromStr;
/// use rust_cast::{CastDevice, channels::receiver::CastDeviceApp};
///
/// # let cast_device = CastDevice::connect_without_host_verification("host", 1234).unwrap();
/// cast_device.receiver.launch_app(&CastDeviceApp::from_str("youtube").unwrap());
/// ```
///
/// # Arguments
///
/// * `app` - `CastDeviceApp` instance reference to run.
pub fn launch_app(&self, app: &CastDeviceApp) -> Result<Application, Error> {
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::receiver::AppLaunchRequest {
typ: MESSAGE_TYPE_LAUNCH.to_string(),
request_id,
app_id: app.to_string(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: self.receiver.to_string(),
payload: CastMessagePayload::String(payload),
})?;
// Once application is run cast receiver device should emit status update event, or launch
// error event if something went wrong.
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
match self.parse(message)? {
ReceiverResponse::Status(mut status) => {
if status.request_id == request_id {
return Ok(Some(status.applications.remove(0)));
}
}
ReceiverResponse::LaunchError(error) => {
if error.request_id == request_id {
return Err(Error::Internal(format!(
"Could not run application ({}).",
error.reason.unwrap_or_else(|| "Unknown".to_string())
)));
}
}
_ => {}
}
Ok(None)
})
}
/// Broadcasts a message over a cast device's message bus.
///
/// Receiver can observe messages using `context.addCustomMessageListener` with custom namespace.
///
/// ```javascript, no_run
/// context.addCustomMessageListener('urn:x-cast:com.example.castdata', function(customEvent) {
/// // do something with message
/// });
/// ```
///
/// Namespace should start with `urn:x-cast:`
///
/// # Arguments
///
/// * `namespace` - Message namespace that should start with `urn:x-cast:`.
/// * `message` - Message instance to send.
pub fn broadcast_message<M: Serialize>(
&self,
namespace: &str,
message: &M,
) -> Result<(), Error> {
if !namespace.starts_with("urn:x-cast:") {
return Err(Error::Namespace(format!(
"'{}' should start with 'urn:x-cast:' prefix",
namespace
)));
}
let payload = serde_json::to_string(message)?;
self.message_manager.send(CastMessage {
namespace: namespace.to_string(),
source: self.sender.to_string(),
destination: "*".into(),
payload: CastMessagePayload::String(payload),
})?;
Ok(())
}
/// Stops currently active app using corresponding `session_id`.
///
/// # Arguments
/// * `session_id` - identifier of the active application session from `Application` instance.
pub fn stop_app<S>(&self, session_id: S) -> Result<(), Error>
where
S: Into<Cow<'a, str>>,
{
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::receiver::AppStopRequest {
typ: MESSAGE_TYPE_STOP.to_string(),
request_id,
session_id: session_id.into(),
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: self.receiver.to_string(),
payload: CastMessagePayload::String(payload),
})?;
// Once application is stopped cast receiver device should emit status update event, or
// invalid request event if provided session id is not valid.
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
match self.parse(message)? {
ReceiverResponse::Status(status) => {
if status.request_id == request_id {
return Ok(Some(()));
}
}
ReceiverResponse::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)
})
}
/// Retrieves status of the cast device receiver.
///
/// # Return value
///
/// Returned `Result` should consist of either `Status` instance or an `Error`.
pub fn get_status(&self) -> Result<Status, Error> {
let request_id = self.message_manager.generate_request_id().get();
let payload = serde_json::to_string(&proxies::receiver::GetStatusRequest {
typ: MESSAGE_TYPE_GET_STATUS.to_string(),
request_id,
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: self.receiver.to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
let message = self.parse(message)?;
if let ReceiverResponse::Status(status) = message
&& status.request_id == request_id
{
return Ok(Some(status));
}
Ok(None)
})
}
/// Sets volume for the active cast device.
///
/// # Arguments
///
/// * `volume` - anything that can be converted to a valid `Volume` structure. It's possible to
/// set volume level, mute/unmute state or both altogether.
///
/// # Return value
///
/// Actual `Volume` instance returned by receiver.
///
/// # Errors
///
/// Usually method can fail only if network connection with cast device is lost for some reason.
pub fn set_volume<T>(&self, volume: T) -> Result<Volume, Error>
where
T: Into<Volume>,
{
let request_id = self.message_manager.generate_request_id().get();
let volume = volume.into();
let payload = serde_json::to_string(&proxies::receiver::SetVolumeRequest {
typ: MESSAGE_TYPE_SET_VOLUME.to_string(),
request_id,
volume: proxies::receiver::Volume {
level: volume.level,
muted: volume.muted,
},
})?;
self.message_manager.send(CastMessage {
namespace: CHANNEL_NAMESPACE.to_string(),
source: self.sender.to_string(),
destination: self.receiver.to_string(),
payload: CastMessagePayload::String(payload),
})?;
self.message_manager.receive_find_map(|message| {
if !self.can_handle(message) {
return Ok(None);
}
let message = self.parse(message)?;
if let ReceiverResponse::Status(status) = message
&& status.request_id == request_id
{
return Ok(Some(status.volume));
}
Ok(None)
})
}
pub fn can_handle(&self, message: &CastMessage) -> bool {
message.namespace == CHANNEL_NAMESPACE
}
pub fn parse(&self, message: &CastMessage) -> Result<ReceiverResponse, 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_RECEIVER_STATUS => {
let status_reply: proxies::receiver::StatusReply =
serde_json::value::from_value(reply)?;
let status = Status {
request_id: status_reply.request_id,
applications: status_reply
.status
.applications
.iter()
.map(|app| Application {
app_id: app.app_id.clone(),
session_id: app.session_id.clone(),
transport_id: app.transport_id.clone(),
namespaces: app
.namespaces
.iter()
.map(|ns| ns.name.clone())
.collect::<Vec<String>>(),
display_name: app.display_name.clone(),
status_text: app.status_text.clone(),
})
.collect::<Vec<Application>>(),
is_active_input: status_reply.status.is_active_input,
is_stand_by: status_reply.status.is_stand_by,
volume: Volume {
level: status_reply.status.volume.level,
muted: status_reply.status.volume.muted,
},
};
ReceiverResponse::Status(status)
}
MESSAGE_TYPE_LAUNCH_ERROR => {
let reply: proxies::receiver::LaunchErrorReply =
serde_json::value::from_value(reply)?;
ReceiverResponse::LaunchError(LaunchError {
request_id: reply.request_id,
reason: reply.reason,
})
}
MESSAGE_TYPE_INVALID_REQUEST => {
let reply: proxies::receiver::InvalidRequestReply =
serde_json::value::from_value(reply)?;
ReceiverResponse::InvalidRequest(InvalidRequest {
request_id: reply.request_id,
reason: reply.reason,
})
}
_ => ReceiverResponse::NotImplemented(message_type.to_string(), reply),
};
Ok(response)
}
}

69
vendor/rust_cast-0.21.0/src/errors.rs vendored Normal file
View file

@ -0,0 +1,69 @@
use std::io::Error as IoError;
use protobuf::Error as ProtobufError;
use rustls::pki_types::InvalidDnsNameError;
use serde_json::error::Error as SerializationError;
use thiserror::Error;
/// Consolidates possible error types that can occur in the lib.
#[derive(Debug, Error)]
pub enum Error {
/// This variant is used when error occurs in the lib logic.
#[error("an internal error occurred, {0}")]
Internal(String),
/// This variant includes everything related to the network connection.
#[error("{0}")]
Io(IoError),
/// This variant includes all possible errors that come from Protobuf layer.
#[error("{0}")]
Protobuf(ProtobufError),
/// Errors with JSON (de)serialization of incoming and outgoing
/// messages.
#[error("{0}")]
Serialization(SerializationError),
/// Errors parsing messages (valid JSON but bad semantics)
#[error("{0}")]
Parsing(String),
/// This variant is used to indicate invalid DNS name used to connect to Cast device.
#[error("{0}")]
Dns(InvalidDnsNameError),
/// This variant includes any error that comes from rustls.
#[error("{0}")]
Tls(rustls::Error),
/// Problems with given namespace
#[error("{0}")]
Namespace(String),
/// This variant is used when message retrieval takes too long.
#[error("{0}")]
Timeout(String),
}
impl From<IoError> for Error {
fn from(err: IoError) -> Error {
Error::Io(err)
}
}
impl From<ProtobufError> for Error {
fn from(err: ProtobufError) -> Error {
Error::Protobuf(err)
}
}
impl From<SerializationError> for Error {
fn from(err: SerializationError) -> Error {
Error::Serialization(err)
}
}
impl From<rustls::Error> for Error {
fn from(err: rustls::Error) -> Error {
Error::Tls(err)
}
}
impl From<InvalidDnsNameError> for Error {
fn from(err: InvalidDnsNameError) -> Error {
Error::Dns(err)
}
}

571
vendor/rust_cast-0.21.0/src/lib.rs vendored Normal file
View file

@ -0,0 +1,571 @@
#![deny(warnings)]
use std::{borrow::Cow, net::TcpStream, sync::Arc};
use rustls::{
ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, StreamOwned,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{aws_lc_rs::default_provider, verify_tls12_signature, verify_tls13_signature},
pki_types::{CertificateDer, ServerName, UnixTime},
};
use channels::{
connection::{ConnectionChannel, ConnectionResponse},
heartbeat::{HeartbeatChannel, HeartbeatResponse},
media::{MediaChannel, MediaResponse},
receiver::{ReceiverChannel, ReceiverResponse},
};
use errors::Error;
use message_manager::{CastMessage, CastMessagePayload, MessageManager};
#[cfg(not(feature = "cast"))]
mod cast;
#[cfg(feature = "cast")]
pub mod cast;
pub mod channels;
pub mod errors;
pub mod message_manager;
mod utils;
const DEFAULT_SENDER_ID: &str = "sender-0";
const DEFAULT_RECEIVER_ID: &str = "receiver-0";
#[cfg(feature = "thread_safe")]
type Lrc<T> = std::sync::Arc<T>;
#[cfg(not(feature = "thread_safe"))]
type Lrc<T> = std::rc::Rc<T>;
/// Supported channel message types.
#[derive(Clone, Debug)]
pub enum ChannelMessage {
/// Message to be processed by `ConnectionChannel`.
Connection(ConnectionResponse),
/// Message to be processed by `HeartbeatChannel`.
Heartbeat(HeartbeatResponse),
/// Message to be processed by `MediaChannel`.
Media(MediaResponse),
/// Message to be processed by `ReceiverChannel`.
Receiver(ReceiverResponse),
/// Raw message is returned when built-in channels can't process it (e.g. because of unknown
/// `namespace`).
Raw(CastMessage),
}
/// Structure that manages connection to a cast device.
pub struct CastDevice<'a> {
message_manager: Lrc<MessageManager<StreamOwned<ClientConnection, TcpStream>>>,
/// Channel that manages connection responses/requests.
pub connection: ConnectionChannel<'a, StreamOwned<ClientConnection, TcpStream>>,
/// Channel that allows connection to stay alive (via ping-pong requests/responses).
pub heartbeat: HeartbeatChannel<'a, StreamOwned<ClientConnection, TcpStream>>,
/// Channel that manages various media stuff.
pub media: MediaChannel<'a, StreamOwned<ClientConnection, TcpStream>>,
/// Channel that manages receiving platform (e.g. Chromecast).
pub receiver: ReceiverChannel<'a, StreamOwned<ClientConnection, TcpStream>>,
}
impl<'a> CastDevice<'a> {
/// Connects to the cast device using host name and port.
///
/// # Examples
///
/// ```no_run
/// use rust_cast::CastDevice;
///
/// let device = CastDevice::connect("192.168.1.2", 8009)?;
/// # Ok::<(), rust_cast::errors::Error>(())
/// ```
///
/// # Arguments
///
/// * `host` - Cast device host name.
/// * `port` - Cast device port number.
///
/// # Errors
///
/// This method may fail if connection to Cast device can't be established for some reason
/// (e.g. wrong host name or port).
///
/// # Return value
///
/// Instance of `CastDevice` that allows you to manage connection.
pub fn connect<S>(host: S, port: u16) -> Result<CastDevice<'a>, Error>
where
S: Into<Cow<'a, str>>,
{
let host = host.into();
log::debug!("Establishing connection with cast device at {host}:{port}…");
let mut root_store = RootCertStore::empty();
let (valid, invalid) = root_store.add_parsable_certificates(
rustls_native_certs::load_native_certs().expect("Could not load platform certs."),
);
if invalid > 0 {
log::warn!(
"Failed to parse {invalid} out of {} root certificates.",
valid + invalid
);
} else {
log::debug!("Successfully parsed {valid} root certificates.");
}
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
config.key_log = Arc::new(rustls::KeyLogFile::new());
let conn = ClientConnection::new(
config.into(),
ServerName::try_from(host.as_ref())?.to_owned(),
)?;
let stream = StreamOwned::new(conn, TcpStream::connect((host.as_ref(), port))?);
log::debug!("Connection with {host}:{port} successfully established.");
CastDevice::connect_to_device(stream)
}
/// Connects to the cast device using host name and port _without_ host verification. Use on
/// your own risk!
///
/// # Examples
///
/// ```no_run
/// use rust_cast::CastDevice;
///
/// let device = CastDevice::connect_without_host_verification("192.168.1.2", 8009)?;
/// # Ok::<(), rust_cast::errors::Error>(())
/// ```
///
/// # Arguments
///
/// * `host` - Cast device host name.
/// * `port` - Cast device port number.
///
/// # Errors
///
/// This method may fail if connection to Cast device can't be established for some reason
/// (e.g. wrong host name or port).
///
/// # Return value
///
/// Instance of `CastDevice` that allows you to manage connection.
pub fn connect_without_host_verification<S>(host: S, port: u16) -> Result<CastDevice<'a>, Error>
where
S: Into<Cow<'a, str>>,
{
let host = host.into();
log::debug!("Establishing non-verified connection with cast device at {host}:{port}…");
let mut config = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification {}))
.with_no_client_auth();
config.key_log = Arc::new(rustls::KeyLogFile::new());
let stream = StreamOwned::new(
ClientConnection::new(
Arc::new(config),
ServerName::try_from(host.as_ref())?.to_owned(),
)?,
TcpStream::connect((host.as_ref(), port))?,
);
log::debug!("Connection with {host}:{port} successfully established.");
CastDevice::connect_to_device(stream)
}
/// Waits for any message returned by cast device (e.g. Chromecast) and returns its parsed
/// version.
///
/// # Examples
///
/// ```no_run
/// use rust_cast::ChannelMessage;
///
/// # use rust_cast::CastDevice;
/// # let cast_device = CastDevice::connect_without_host_verification("192.168.1.2", 8009)?;
///
/// match cast_device.receive() {
/// Ok(ChannelMessage::Connection(res)) => log::debug!("Connection message: {:?}", res),
/// Ok(ChannelMessage::Heartbeat(_)) => cast_device.heartbeat.pong()?,
/// Ok(_) => {},
/// Err(err) => log::error!("Error occurred while receiving message {}", err)
/// }
/// # Ok::<(), rust_cast::errors::Error>(())
/// ```
///
/// # Errors
///
/// Usually fails if message returned by device can't be parsed.
///
/// # Returned values
///
/// Parsed channel message.
pub fn receive(&self) -> Result<ChannelMessage, Error> {
let cast_message = self.message_manager.receive()?;
if self.connection.can_handle(&cast_message) {
return Ok(ChannelMessage::Connection(
self.connection.parse(&cast_message)?,
));
}
if self.heartbeat.can_handle(&cast_message) {
return Ok(ChannelMessage::Heartbeat(
self.heartbeat.parse(&cast_message)?,
));
}
if self.media.can_handle(&cast_message) {
return Ok(ChannelMessage::Media(self.media.parse(&cast_message)?));
}
if self.receiver.can_handle(&cast_message) {
return Ok(ChannelMessage::Receiver(
self.receiver.parse(&cast_message)?,
));
}
Ok(ChannelMessage::Raw(cast_message))
}
/// Connects to the cast device using provided ssl stream.
///
/// # Arguments
///
/// * `ssl_stream` - SSL Stream for the TCP connection established with the device.
///
/// # Return value
///
/// Instance of `CastDevice` that allows you to manage connection.
fn connect_to_device(
ssl_stream: StreamOwned<ClientConnection, TcpStream>,
) -> Result<CastDevice<'a>, Error> {
let message_manager_rc = Lrc::new(MessageManager::new(ssl_stream));
let heartbeat = HeartbeatChannel::new(
DEFAULT_SENDER_ID,
DEFAULT_RECEIVER_ID,
Lrc::clone(&message_manager_rc),
);
let connection = ConnectionChannel::new(DEFAULT_SENDER_ID, Lrc::clone(&message_manager_rc));
let receiver = ReceiverChannel::new(
DEFAULT_SENDER_ID,
DEFAULT_RECEIVER_ID,
Lrc::clone(&message_manager_rc),
);
let media = MediaChannel::new(DEFAULT_SENDER_ID, Lrc::clone(&message_manager_rc));
Ok(CastDevice {
message_manager: message_manager_rc,
heartbeat,
connection,
receiver,
media,
})
}
/// Sends `message` (a raw, already-serialized string payload -- e.g.
/// JSON text) on an arbitrary `namespace` to a specific `destination`
/// (typically a launched app's `transport_id`, the same id `connection`/`media`
/// target).
///
/// LOCAL PATCH (breadcast, not upstream): none of the built-in channels expose a
/// generic point-to-point send -- `ReceiverChannel::broadcast_message()` is the
/// closest, but it hardcodes destination `"*"`, which isn't the same conversation
/// as a namespace-specific exchange with one particular launched app (e.g. Cast
/// Streaming's OFFER/ANSWER negotiation on `urn:x-cast:com.google.cast.webrtc`,
/// which breadcast's `breadcast-caststream-sys` crate needs -- there, the payload
/// is already-serialized JSON produced by the vendored openscreen C++, so this
/// takes a raw string rather than a `Serialize` value like
/// `broadcast_message()` does, to avoid double-encoding it). Receiving such
/// messages needs no patch: `CastDevice::receive()` already returns them as
/// `ChannelMessage::Raw(CastMessage)` whenever no built-in channel claims the
/// namespace.
pub fn send_message(&self, namespace: &str, destination: &str, message: &str) -> Result<(), Error> {
self.message_manager.send(CastMessage {
namespace: namespace.to_string(),
source: DEFAULT_SENDER_ID.to_string(),
destination: destination.to_string(),
payload: CastMessagePayload::String(message.to_string()),
})
}
}
#[cfg(test)]
pub(crate) mod tests {
use byteorder::{BigEndian, WriteBytesExt};
use log::warn;
use protobuf::Message;
use std::{
fmt::Display,
io::{Read, Write},
sync::{Arc, RwLock},
};
use crate::{cast::cast_channel, utils::read_u32_from_buffer};
#[test]
#[cfg(feature = "thread_safe")]
fn test_thread_safe() {
use crate::CastDevice;
fn is_sync<T: Sync>() {}
fn is_send<T: Send>() {}
is_sync::<CastDevice>();
is_send::<CastDevice>();
}
/// A mock implementation of a TCP stream for testing purposes.
///
/// # Example
///
/// ```rust
/// use rust_cast::channels::media::MediaChannel;
/// use rust_cast::message_manager::MessageManager;
/// use rust_cast::Lrc;
///
/// let stream = MockTcpStream::new();
/// let message_manager = Lrc::new(MessageManager::new(stream));
/// let channel = MediaChannel::new(
/// "sender-0",
/// message_manager
/// );
/// ```
#[derive(Debug, Default, Clone)]
pub struct MockTcpStream {
/// Inner stream of the TCP stream which allows cloning and referencing the same stream source.
inner: Arc<RwLock<InnerStream>>,
}
impl MockTcpStream {
/// Creates a new empty `MockTcpStream` instance.
pub fn new() -> Self {
MockTcpStream {
inner: Arc::new(RwLock::new(InnerStream::default())),
}
}
/// Add a response message to be returned by read operations on the stream.
pub fn add_message<M: protobuf::Message>(&mut self, message: M) {
let message = message.write_to_bytes().unwrap();
let mut mutex = self.inner.write().unwrap();
mutex.response_messages.push(message);
}
/// Returns the received message at the given index if present, else [None].
pub fn received_message(&self, index: usize) -> Option<TcpMessage> {
self.inner
.read()
.expect("expected to acquire read lock")
.received_messages
.get(index)
.cloned()
}
fn inner_read(&self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.write().unwrap().read(buf)
}
fn inner_write(&self, buf: &[u8]) -> std::io::Result<usize> {
self.inner.write().unwrap().write(buf)
}
fn inner_flush(&self) -> std::io::Result<()> {
self.inner.write().unwrap().flush()
}
}
impl Read for MockTcpStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner_read(buf)
}
}
impl Write for MockTcpStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.inner_write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner_flush()
}
}
/// Represents a TCP message containing a received payload from the sender.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct TcpMessage {
/// The known length of the message.
pub message_length: u32,
/// The payload of the message.
pub payload: Vec<u8>,
}
impl TcpMessage {
/// Parses and returns the CastMessage contained in the payload.
pub fn message(&self) -> cast_channel::CastMessage {
<cast_channel::CastMessage as Message>::parse_from_bytes(self.payload.as_slice())
.unwrap()
}
}
impl Display for TcpMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", String::from_utf8_lossy(self.payload.as_slice()))
}
}
#[derive(Debug, Clone, PartialEq)]
enum CursorLocation {
Length,
Payload,
}
#[derive(Debug, Clone)]
struct ReadCursor {
pub location: CursorLocation,
pub index: usize,
}
impl ReadCursor {
pub fn next(&self) -> Self {
match self.location {
CursorLocation::Length => Self {
location: CursorLocation::Payload,
index: self.index,
},
CursorLocation::Payload => Self {
location: CursorLocation::Length,
index: self.index + 1,
},
}
}
}
impl Default for ReadCursor {
fn default() -> Self {
Self {
location: CursorLocation::Length,
index: 0,
}
}
}
/// Inner representation of a stream used by `MockTcpStream` for testing purposes.
#[derive(Debug, Default)]
struct InnerStream {
/// The current position of the read cursor.
cursor: ReadCursor,
/// Buffer containing the messages which should be returned by the read operation.
response_messages: Vec<Vec<u8>>,
/// Buffer for storing the payload of the current message being written.
payload_buffer: Option<TcpMessage>,
/// Vector containing the received messages from the sender.
received_messages: Vec<TcpMessage>,
}
impl Read for InnerStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if let Some(message) = self.response_messages.get(self.cursor.index) {
let result: std::io::Result<usize> = match &self.cursor.location {
CursorLocation::Length => {
let mut len = Vec::<u8>::new();
len.write_u32::<BigEndian>(message.len() as u32).unwrap();
buf[..4].copy_from_slice(len.as_slice());
Ok(4)
}
CursorLocation::Payload => {
let len = message.len();
buf[..len].copy_from_slice(message.as_slice());
Ok(len)
}
};
self.cursor = self.cursor.next();
result
} else {
warn!("No more messages to read");
Ok(0)
}
}
}
impl Write for InnerStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Some(mut payload_buffer) = self.payload_buffer.take() {
payload_buffer.payload = buf.to_vec();
self.received_messages.push(payload_buffer);
} else {
let length = read_u32_from_buffer(buf).unwrap();
self.payload_buffer = Some(TcpMessage {
message_length: length,
payload: vec![],
});
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
// flush is never called when sending messages
// so we don't execute any logic here
Ok(())
}
}
}
#[derive(Debug)]
pub struct NoCertificateVerification;
impl ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls12_signature(
message,
cert,
dss,
&default_provider().signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls13_signature(
message,
cert,
dss,
&default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}

View file

@ -0,0 +1,347 @@
use std::{
io::{Read, Write},
num::NonZeroU32,
ops::{Deref, DerefMut},
};
use crate::{
cast::{
cast_channel,
cast_channel::cast_message::{PayloadType, ProtocolVersion},
},
errors::Error,
utils,
};
struct Lock<T>(
#[cfg(feature = "thread_safe")] std::sync::Mutex<T>,
#[cfg(not(feature = "thread_safe"))] std::cell::RefCell<T>,
);
struct LockGuardMut<'a, T>(
#[cfg(feature = "thread_safe")] std::sync::MutexGuard<'a, T>,
#[cfg(not(feature = "thread_safe"))] std::cell::RefMut<'a, T>,
);
impl<'a, T> Deref for LockGuardMut<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
impl<'a, T> DerefMut for LockGuardMut<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.deref_mut()
}
}
impl<T> Lock<T> {
fn new(data: T) -> Self {
Lock({
#[cfg(feature = "thread_safe")]
let lock = std::sync::Mutex::new(data);
#[cfg(not(feature = "thread_safe"))]
let lock = std::cell::RefCell::new(data);
lock
})
}
fn borrow_mut(&self) -> LockGuardMut<'_, T> {
LockGuardMut({
#[cfg(feature = "thread_safe")]
let guard = self.0.lock().unwrap();
#[cfg(not(feature = "thread_safe"))]
let guard = self.0.borrow_mut();
guard
})
}
}
/// Type of the payload that `CastMessage` can have.
#[derive(Debug, Clone, PartialEq)]
pub enum CastMessagePayload {
/// Payload represented by UTF-8 string (usually it's just a JSON string).
String(String),
/// Payload represented by binary data.
Binary(Vec<u8>),
}
/// Base structure that represents messages that are exchanged between Receiver and Sender.
#[derive(Debug, Clone, PartialEq)]
pub struct CastMessage {
/// A namespace is a labeled protocol. That is, messages that are exchanged throughout the
/// Cast ecosystem utilize namespaces to identify the protocol of the message being sent.
pub namespace: String,
/// Unique identifier of the `sender` application.
pub source: String,
/// Unique identifier of the `receiver` application.
pub destination: String,
/// Payload data attached to the message (either string or binary).
pub payload: CastMessagePayload,
}
/// Static structure that is responsible for (de)serializing and sending/receiving Cast protocol
/// messages.
pub struct MessageManager<S>
where
S: Write + Read,
{
message_buffer: Lock<Vec<CastMessage>>,
stream: Lock<S>,
request_counter: Lock<NonZeroU32>,
}
impl<S> MessageManager<S>
where
S: Write + Read,
{
pub fn new(stream: S) -> Self {
MessageManager {
stream: Lock::new(stream),
message_buffer: Lock::new(vec![]),
request_counter: Lock::new(NonZeroU32::MIN),
}
}
/// Sends `message` to the Cast Device.
///
/// # Arguments
///
/// * `message` - `CastMessage` instance to be sent to the Cast Device.
pub fn send(&self, message: CastMessage) -> Result<(), Error> {
let mut raw_message = cast_channel::CastMessage::new();
raw_message.set_protocol_version(ProtocolVersion::CASTV2_1_0);
raw_message.set_namespace(message.namespace);
raw_message.set_source_id(message.source);
raw_message.set_destination_id(message.destination);
match message.payload {
CastMessagePayload::String(payload) => {
raw_message.set_payload_type(PayloadType::STRING);
raw_message.set_payload_utf8(payload);
}
CastMessagePayload::Binary(payload) => {
raw_message.set_payload_type(PayloadType::BINARY);
raw_message.set_payload_binary(payload);
}
};
let message_content_buffer = utils::to_vec(&raw_message)?;
let message_length_buffer =
utils::write_u32_to_buffer(message_content_buffer.len() as u32)?;
let writer = &mut *self.stream.borrow_mut();
writer.write_all(&message_length_buffer)?;
writer.write_all(&message_content_buffer)?;
log::debug!("Message sent: {:?}", raw_message);
Ok(())
}
/// Waits for the next `CastMessage` available. Can also return existing message from the
/// internal message buffer containing messages that have been received previously, but haven't
/// been consumed for some reason (e.g. during `receive_find_map` call).
///
/// # Return value
///
/// `Result` containing parsed `CastMessage` or `Error`.
pub fn receive(&self) -> Result<CastMessage, Error> {
let mut message_buffer = self.message_buffer.borrow_mut();
// If we have messages in the buffer, let's return them from it.
if message_buffer.is_empty() {
self.read()
} else {
Ok(message_buffer.remove(0))
}
}
/// Waits for the next `CastMessage` for which `f` returns valid mapped value. Messages in which
/// `f` is not interested are placed into internal message buffer and can be later retrieved
/// with `receive`. This method always reads from the stream.
///
/// # Example
///
/// ```no_run
/// # use std::net::TcpStream;
/// # use rust_cast::message_manager::{CastMessage, MessageManager};
/// # use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
/// # use rustls::pki_types::ServerName;
/// # let config = ClientConfig::builder()
/// # .with_root_certificates(RootCertStore::empty())
/// # .with_no_client_auth();
/// # let server_name = ServerName::try_from("0")?.to_owned();
/// # let conn = ClientConnection::new(config.into(), server_name)?;
/// # let tcp_stream = TcpStream::connect(("0", 8009)).unwrap();
/// # let ssl_stream = StreamOwned::new(conn, tcp_stream);
/// # let message_manager = MessageManager::new(ssl_stream);
/// # fn can_handle(message: &CastMessage) -> bool { unimplemented!() }
/// # fn parse(message: &CastMessage) { unimplemented!() }
/// message_manager.receive_find_map(|message| {
/// if !can_handle(message) {
/// return Ok(None);
/// }
///
/// parse(message);
///
/// Ok(Some(()))
/// })?;
/// # Ok::<(), rust_cast::errors::Error>(())
/// ```
///
/// # Arguments
///
/// * `f` - Function that analyzes and maps `CastMessage` to any other type. If message doesn't
/// look like something `f` is looking for, then `Ok(None)` should be returned so that message
/// is not lost and placed into internal message buffer for later retrieval.
///
/// # Return value
///
/// `Result` containing parsed `CastMessage` or `Error`.
pub fn receive_find_map<F, B>(&self, f: F) -> Result<B, Error>
where
F: Fn(&CastMessage) -> Result<Option<B>, Error>,
{
loop {
let message = self.read()?;
// If message is found, just return mapped result, otherwise keep unprocessed message
// in the buffer, it can be later retrieved with `receive`.
match f(&message)? {
Some(r) => return Ok(r),
None => self.message_buffer.borrow_mut().push(message),
}
}
}
/// Generates unique integer number that is used in some requests to map them with the response.
///
/// # Return value
///
/// Unique (in the scope of this particular `MessageManager` instance) integer number.
pub fn generate_request_id(&self) -> NonZeroU32 {
let mut counter = self.request_counter.borrow_mut();
let request_id = *counter;
*counter = counter.checked_add(1).unwrap();
request_id
}
/// Reads next `CastMessage` from the stream.
///
/// # Return value
///
/// `Result` containing parsed `CastMessage` or `Error`.
fn read(&self) -> Result<CastMessage, Error> {
let mut buffer: [u8; 4] = [0; 4];
let reader = &mut *self.stream.borrow_mut();
reader.read_exact(&mut buffer)?;
let length = utils::read_u32_from_buffer(&buffer)?;
let mut buffer: Vec<u8> = Vec::with_capacity(length as usize);
let mut limited_reader = reader.take(u64::from(length));
limited_reader.read_to_end(&mut buffer)?;
let raw_message = utils::from_vec::<cast_channel::CastMessage>(buffer.to_vec())?;
log::debug!("Message received: {:?}", raw_message);
Ok(CastMessage {
namespace: raw_message.namespace().to_string(),
source: raw_message.source_id().to_string(),
destination: raw_message.destination_id().to_string(),
payload: match raw_message.payload_type() {
PayloadType::STRING => {
CastMessagePayload::String(raw_message.payload_utf8().to_string())
}
PayloadType::BINARY => {
CastMessagePayload::Binary(raw_message.payload_binary().to_owned())
}
},
})
}
}
#[cfg(test)]
mod tests {
use protobuf::EnumOrUnknown;
use crate::{DEFAULT_RECEIVER_ID, DEFAULT_SENDER_ID, tests::MockTcpStream};
use super::*;
#[test]
fn test_receive() {
let mut stream = MockTcpStream::new();
let payload = r#"{"type":"PING"}"#;
stream.add_message(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(crate::channels::heartbeat::CHANNEL_NAMESPACE.to_string()),
payload_type: Some(EnumOrUnknown::new(PayloadType::STRING)),
payload_utf8: Some(payload.to_string()),
payload_binary: None,
continued: None,
remaining_length: None,
special_fields: Default::default(),
});
let message_manager = MessageManager::new(stream);
let expected_result = CastMessage {
namespace: crate::channels::heartbeat::CHANNEL_NAMESPACE.to_string(),
source: DEFAULT_RECEIVER_ID.to_string(),
destination: DEFAULT_SENDER_ID.to_string(),
payload: CastMessagePayload::String(payload.to_string()),
};
let result = message_manager
.receive()
.expect("expected to receive a message");
assert_eq!(expected_result, result);
}
#[test]
fn test_send() {
let payload = r#"{"type":"PONG"}"#;
let namespace = crate::channels::heartbeat::CHANNEL_NAMESPACE;
let stream = MockTcpStream::new();
let message_manager = MessageManager::new(stream.clone());
let expected_message = cast_channel::CastMessage {
protocol_version: Some(EnumOrUnknown::new(ProtocolVersion::CASTV2_1_0)),
source_id: Some(DEFAULT_SENDER_ID.to_string()),
destination_id: Some(DEFAULT_RECEIVER_ID.to_string()),
namespace: Some(namespace.to_string()),
payload_type: Some(EnumOrUnknown::new(PayloadType::STRING)),
payload_utf8: Some(payload.to_string()),
payload_binary: None,
continued: None,
remaining_length: None,
special_fields: Default::default(),
};
message_manager
.send(CastMessage {
namespace: namespace.to_string(),
source: DEFAULT_SENDER_ID.to_string(),
destination: DEFAULT_RECEIVER_ID.to_string(),
payload: CastMessagePayload::String(payload.to_string()),
})
.unwrap();
let tcp_message = stream
.received_message(0)
.expect("expected a message to have been received");
assert_eq!(expected_message, tcp_message.message());
}
}

29
vendor/rust_cast-0.21.0/src/utils.rs vendored Normal file
View file

@ -0,0 +1,29 @@
use crate::errors::Error;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::Cursor;
pub fn read_u32_from_buffer(buffer: &[u8]) -> Result<u32, Error> {
Ok(Cursor::new(buffer).read_u32::<BigEndian>()?)
}
pub fn write_u32_to_buffer(number: u32) -> Result<Vec<u8>, Error> {
let mut buffer = vec![];
buffer.write_u32::<BigEndian>(number)?;
Ok(buffer)
}
pub fn to_vec<M: protobuf::Message>(message: &M) -> Result<Vec<u8>, Error> {
let mut buffer = vec![];
message.write_to_writer(&mut buffer)?;
Ok(buffer)
}
pub fn from_vec<M: protobuf::Message>(buffer: Vec<u8>) -> Result<M, Error> {
let mut read_buffer = Cursor::new(buffer);
Ok(M::parse_from_reader(&mut read_buffer)?)
}