Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s
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:
parent
887c29002f
commit
8c745d18e0
283 changed files with 36788 additions and 0 deletions
8
vendor/rust_cast-0.21.0/.gitignore
vendored
Normal file
8
vendor/rust_cast-0.21.0/.gitignore
vendored
Normal 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
1
vendor/rust_cast-0.21.0/.husky/commit-msg
vendored
Executable file
|
|
@ -0,0 +1 @@
|
|||
npx --no -- commitlint --edit ${1}
|
||||
30
vendor/rust_cast-0.21.0/.husky/pre-push
vendored
Executable file
30
vendor/rust_cast-0.21.0/.husky/pre-push
vendored
Executable 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
41
vendor/rust_cast-0.21.0/Cargo.toml
vendored
Normal 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
21
vendor/rust_cast-0.21.0/LICENSE
vendored
Normal 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
37
vendor/rust_cast-0.21.0/PATCHES.md
vendored
Normal 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
102
vendor/rust_cast-0.21.0/README.md
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
[](https://docs.rs/crate/rust_cast/)
|
||||

|
||||
|
||||
# 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
22
vendor/rust_cast-0.21.0/build.rs
vendored
Normal 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
2
vendor/rust_cast-0.21.0/rustfmt.toml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
unstable_features = true
|
||||
imports_granularity = "Crate"
|
||||
306
vendor/rust_cast-0.21.0/src/cast/authority_keys.rs
vendored
Normal file
306
vendor/rust_cast-0.21.0/src/cast/authority_keys.rs
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1555
vendor/rust_cast-0.21.0/src/cast/cast_channel.rs
vendored
Normal file
1555
vendor/rust_cast-0.21.0/src/cast/cast_channel.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
3
vendor/rust_cast-0.21.0/src/cast/mod.rs
vendored
Normal file
3
vendor/rust_cast-0.21.0/src/cast/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod authority_keys;
|
||||
pub mod cast_channel;
|
||||
pub mod proxies;
|
||||
529
vendor/rust_cast-0.21.0/src/cast/proxies.rs
vendored
Normal file
529
vendor/rust_cast-0.21.0/src/cast/proxies.rs
vendored
Normal 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>,
|
||||
}
|
||||
}
|
||||
113
vendor/rust_cast-0.21.0/src/channels/connection.rs
vendored
Normal file
113
vendor/rust_cast-0.21.0/src/channels/connection.rs
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
110
vendor/rust_cast-0.21.0/src/channels/heartbeat.rs
vendored
Normal file
110
vendor/rust_cast-0.21.0/src/channels/heartbeat.rs
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
1643
vendor/rust_cast-0.21.0/src/channels/media.rs
vendored
Normal file
1643
vendor/rust_cast-0.21.0/src/channels/media.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
4
vendor/rust_cast-0.21.0/src/channels/mod.rs
vendored
Normal file
4
vendor/rust_cast-0.21.0/src/channels/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod connection;
|
||||
pub mod heartbeat;
|
||||
pub mod media;
|
||||
pub mod receiver;
|
||||
518
vendor/rust_cast-0.21.0/src/channels/receiver.rs
vendored
Normal file
518
vendor/rust_cast-0.21.0/src/channels/receiver.rs
vendored
Normal 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
69
vendor/rust_cast-0.21.0/src/errors.rs
vendored
Normal 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
571
vendor/rust_cast-0.21.0/src/lib.rs
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
347
vendor/rust_cast-0.21.0/src/message_manager.rs
vendored
Normal file
347
vendor/rust_cast-0.21.0/src/message_manager.rs
vendored
Normal 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
29
vendor/rust_cast-0.21.0/src/utils.rs
vendored
Normal 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)?)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue