Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ changing your configuration, and the firewall and portal settings to check.
Audio-only devices (speakers, cast groups) show a single **Cast audio** action instead.
4. To end, click the icon and choose **Stop casting**.

Preferences (resolution cap, framerate, bitrate) are under the ⚙ menu entry or `gnome-extensions prefs gnome-shell-cast@oxygenws.com`.
Preferences (resolution cap, framerate, bitrate, codec and encoder) are under
the ⚙ menu entry or
`gnome-extensions prefs gnome-shell-cast@oxygenws.com`. Leave the codec on
*Automatic* normally; force VP8 if a receiver freezes or shows a broken picture.
Comment thread
tomasmark79 marked this conversation as resolved.
Outdated

## Troubleshooting

Expand Down
70 changes: 63 additions & 7 deletions daemon/src/cast.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,58 @@
use std::net::IpAddr;
use std::net::{IpAddr, SocketAddr, TcpStream};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;

use anyhow::{Result, anyhow};
use anyhow::{Context, Result, anyhow};
use log::{debug, info, warn};
use rust_cast::channels::heartbeat::HeartbeatResponse;
use rust_cast::ChannelMessage;
use rust_cast::channels::connection::ConnectionChannel;
use rust_cast::channels::heartbeat::{HeartbeatChannel, HeartbeatResponse};
use rust_cast::channels::media::MediaChannel;
use rust_cast::channels::media::{Media, Metadata, MusicTrackMediaMetadata, StreamType};
use rust_cast::channels::receiver::CastDeviceApp;
use rust_cast::{CastDevice, ChannelMessage};
use rust_cast::channels::receiver::{CastDeviceApp, ReceiverChannel};
use rust_cast::errors::Error as CastError;
use rust_cast::message_manager::MessageManager;
use rustls::{ClientConnection, StreamOwned};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::oneshot;

use crate::tls;

const DESTINATION_ID: &str = "receiver-0";

type CastStream = StreamOwned<ClientConnection, TcpStream>;
type CastManager = MessageManager<CastStream>;

struct CastDeviceCompat {
manager: Rc<CastManager>,
connection: ConnectionChannel<'static, CastStream>,
heartbeat: HeartbeatChannel<'static, CastStream>,
media: MediaChannel<'static, CastStream>,
receiver: ReceiverChannel<'static, CastStream>,
}

impl CastDeviceCompat {
fn receive(&self) -> Result<ChannelMessage, CastError> {
let message = self.manager.receive()?;
if self.connection.can_handle(&message) {
return Ok(ChannelMessage::Connection(self.connection.parse(&message)?));
}
if self.heartbeat.can_handle(&message) {
return Ok(ChannelMessage::Heartbeat(self.heartbeat.parse(&message)?));
}
if self.media.can_handle(&message) {
return Ok(ChannelMessage::Media(self.media.parse(&message)?));
}
if self.receiver.can_handle(&message) {
return Ok(ChannelMessage::Receiver(self.receiver.parse(&message)?));
}
Ok(ChannelMessage::Raw(message))
}
}

/// The media to load once the stream is ready: URL and HTTP content type (HLS
/// playlist for screen casts, a progressive audio type for audio-only casts).
#[derive(Debug)]
Expand Down Expand Up @@ -83,8 +121,7 @@ fn run(
events: &UnboundedSender<CastEvent>,
) -> Result<()> {
info!("connecting to chromecast at {addr}:{port}");
let device = CastDevice::connect_without_host_verification(addr.to_string(), port)
.map_err(|e| anyhow!("connecting: {e}"))?;
let device = connect(addr, port)?;

device
.connection
Expand Down Expand Up @@ -172,6 +209,25 @@ fn run(
}
}

fn connect(addr: IpAddr, port: u16) -> Result<CastDeviceCompat> {
let socket_addr = SocketAddr::new(addr, port);
let tcp = TcpStream::connect_timeout(&socket_addr, Duration::from_secs(4))
.with_context(|| format!("connecting to {socket_addr}"))?;
let config = tls::client_config();
let server_name = rustls::pki_types::ServerName::try_from(addr.to_string())
.context("building TLS server name")?;
let connection =
ClientConnection::new(Arc::new(config), server_name).context("creating TLS connection")?;
let manager = Rc::new(MessageManager::new(StreamOwned::new(connection, tcp)));
Ok(CastDeviceCompat {
connection: ConnectionChannel::new("sender-0", Rc::clone(&manager)),
heartbeat: HeartbeatChannel::new("sender-0", DESTINATION_ID, Rc::clone(&manager)),
media: MediaChannel::new("sender-0", Rc::clone(&manager)),
receiver: ReceiverChannel::new("sender-0", DESTINATION_ID, Rc::clone(&manager)),
manager,
})
}

impl Drop for CastControl {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
Expand Down
1 change: 1 addition & 0 deletions daemon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod net;
mod pipeline;
mod session;
mod streaming;
mod tls;
mod volume;

use std::collections::HashMap;
Expand Down
27 changes: 26 additions & 1 deletion daemon/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use gstreamer::prelude::*;
use log::{info, warn};
use zbus::zvariant::OwnedValue;

use crate::streaming::encoder::{self, EncoderPolicy, EncodingPolicy, FormatPolicy};
use crate::streaming::encoder::{self, EncoderPolicy, EncodingPolicy, FormatPolicy, VideoCodec};

pub const PLAYLIST_NAME: &str = "stream.m3u8";

Expand All @@ -20,6 +20,8 @@ pub struct StreamSettings {
pub fps: Option<i32>,
pub bitrate_kbps: Option<i32>,
pub audio_bitrate_kbps: Option<i32>,
/// A forced Cast Streaming codec; `None` offers every usable codec.
pub video_codec: Option<VideoCodec>,
/// Which encoder and raw format the user will accept; `Auto` by default.
pub encoding: EncodingPolicy,
}
Expand Down Expand Up @@ -76,6 +78,9 @@ impl StreamSettings {
if let Some(format) = get_string("video-format") {
settings.encoding.format = FormatPolicy::parse(&format);
}
if let Some(codec) = get_string("video-codec") {
settings.video_codec = VideoCodec::parse(&codec);
}
settings
}
}
Expand Down Expand Up @@ -439,6 +444,26 @@ mod tests {
assert_eq!(settings.bitrate_kbps, Some(100));
}

#[test]
fn video_codec_option_is_parsed_and_unknown_values_are_automatic() {
let mut options = HashMap::new();
options.insert(
"video-codec".to_owned(),
OwnedValue::from(zbus::zvariant::Str::from("vp8")),
);
assert_eq!(
StreamSettings::from_options(options).video_codec,
Some(VideoCodec::Vp8)
);

let mut options = HashMap::new();
options.insert(
"video-codec".to_owned(),
OwnedValue::from(zbus::zvariant::Str::from("future-codec")),
);
assert_eq!(StreamSettings::from_options(options).video_codec, None);
}

#[test]
fn high_resolution_and_bitrate_pass_through() {
let mut options = HashMap::new();
Expand Down
7 changes: 2 additions & 5 deletions daemon/src/streaming/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ use std::time::{Duration, Instant};

use anyhow::{Context, Result, anyhow, bail};
use log::{debug, info, warn};
use rust_cast::NoCertificateVerification;
use rust_cast::message_manager::{CastMessage, CastMessagePayload, MessageManager};
use rustls::{ClientConnection, StreamOwned};
use serde_json::{Value, json};
use tokio::sync::mpsc::UnboundedSender;

use super::openscreen::messages::{self, Answer};
use crate::tls;

const SENDER_ID: &str = "sender-0";
const RECEIVER_ID: &str = "receiver-0";
Expand Down Expand Up @@ -300,10 +300,7 @@ fn connect_tls(addr: IpAddr, port: u16) -> Result<Manager> {
tcp.set_read_timeout(Some(READ_TIMEOUT))?;
tcp.set_nodelay(true)?;

let config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
.with_no_client_auth();
let config = tls::client_config();
let server_name = rustls::pki_types::ServerName::try_from(addr.to_string())
.context("building TLS server name")?;
let connection =
Expand Down
27 changes: 26 additions & 1 deletion daemon/src/streaming/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ pub enum VideoCodec {
}

impl VideoCodec {
/// Anything unrecognised means automatic codec negotiation.
pub fn parse(value: &str) -> Option<Self> {
match value {
"vp8" => Some(Self::Vp8),
"vp9" => Some(Self::Vp9),
"av1" => Some(Self::Av1),
"h264" => Some(Self::H264),
_ => None,
}
}

/// The `codecName` string used in the Cast OFFER.
pub fn codec_name(self) -> &'static str {
match self {
Expand Down Expand Up @@ -539,9 +550,13 @@ pub fn policy_failure_message(policy: EncodingPolicy) -> String {
/// The codecs we can encode on this host, **hardware-encodable ones first**,
/// then by efficiency. Used to build the OFFER - we advertise only codecs we
/// can produce, in the order we prefer to use them.
pub fn available_video_codecs(policy: EncodingPolicy) -> Vec<VideoCodec> {
pub fn available_video_codecs(
policy: EncodingPolicy,
preferred: Option<VideoCodec>,
) -> Vec<VideoCodec> {
let mut avail: Vec<(VideoCodec, bool)> = EFFICIENCY_ORDER
.into_iter()
.filter(|codec| preferred.is_none_or(|preferred| *codec == preferred))
.filter_map(|codec| video_encoder(codec, 4_000_000, 30, policy).map(|(_, hw)| (codec, hw)))
.collect();
avail.sort_by_key(|&(codec, hw)| (!hw, efficiency_rank(codec)));
Expand All @@ -560,6 +575,16 @@ mod tests {
assert_eq!(VideoCodec::H264.codec_name(), "h264");
}

#[test]
fn codec_preference_values_parse() {
assert_eq!(VideoCodec::parse("vp8"), Some(VideoCodec::Vp8));
assert_eq!(VideoCodec::parse("vp9"), Some(VideoCodec::Vp9));
assert_eq!(VideoCodec::parse("h264"), Some(VideoCodec::H264));
assert_eq!(VideoCodec::parse("av1"), Some(VideoCodec::Av1));
assert_eq!(VideoCodec::parse("auto"), None);
assert_eq!(VideoCodec::parse("future-codec"), None);
}

#[test]
fn vpx_bitrate_is_bits_per_second() {
let f = launch_for("vp9enc", 4_000_000, 30);
Expand Down
23 changes: 16 additions & 7 deletions daemon/src/streaming/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,20 +197,29 @@ fn offer_codecs(
if capture.is_none() {
return Ok(Vec::new());
}
let codecs = encoder::available_video_codecs(settings.encoding);
let codecs = encoder::available_video_codecs(settings.encoding, settings.video_codec);
if !codecs.is_empty() {
return Ok(codecs);
}
let error = anyhow!(encoder::policy_failure_message(settings.encoding));
let error = match settings.video_codec {
Some(codec) => anyhow!(
"no {} encoder matches the selected encoder and pixel format",
codec.codec_name()
),
None => anyhow!(encoder::policy_failure_message(settings.encoding)),
};
// A forced setting that rules everything out is reported, not worked
// around: falling back to HLS would quietly defeat the user's choice.
// With no forced setting this is just "nothing installed" - still worth
// trying HLS, which needs only H.264.
Err(if settings.encoding == encoder::EncodingPolicy::default() {
Outcome::Unavailable(error)
} else {
Outcome::Finished(Err(error))
})
Err(
if settings.encoding == encoder::EncodingPolicy::default() && settings.video_codec.is_none()
{
Outcome::Unavailable(error)
} else {
Outcome::Finished(Err(error))
},
)
}

/// Codecs the receiver accepted from our OFFER, for "show details".
Expand Down
13 changes: 6 additions & 7 deletions daemon/src/streaming/openscreen/bandwidth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ const INCREASE_DENOMINATOR: u64 = 10;
/// How often the loop may change the encoder bitrate.
pub const CONTROL_INTERVAL: Duration = Duration::from_millis(500);

/// openscreen's `SenderPacketRouter` sends at most this many packets per
/// burst, one burst per `BURST_INTERVAL`; together they bound how much of the
/// window could have been spent transmitting.
const MAX_PACKETS_PER_BURST: u64 = 64;
const BURST_INTERVAL: Duration = Duration::from_millis(10);
/// Open Screen's default 24 Mbit/s burst ceiling, divided into 10 ms bursts
/// of our 1472-byte maximum UDP payloads.
pub const MAX_PACKETS_PER_BURST: u64 = 21;
pub const BURST_INTERVAL: Duration = Duration::from_millis(10);

/// A ring buffer of byte counts over `NUM_TIMESLICES` equal slices.
struct FlowTracker {
Expand Down Expand Up @@ -271,7 +270,7 @@ mod tests {
fn confirmed_bytes_become_bits_per_second() {
let now = Instant::now();
let mut e = BandwidthEstimator::new(now);
// 12800 packets is a full window's worth of bursts.
// Far more packets than a full window can carry saturates the model.
for step in 0..20 {
e.on_burst_sent(12_500, 640, at(now, step * 100));
e.on_bytes_confirmed(12_500, at(now, step * 100));
Expand All @@ -290,7 +289,7 @@ mod tests {
let now = Instant::now();
let mut e = BandwidthEstimator::new(now);
for step in 0..20 {
e.on_burst_sent(12_500, 64, at(now, step * 100));
e.on_burst_sent(12_500, MAX_PACKETS_PER_BURST, at(now, step * 100));
e.on_bytes_confirmed(12_500, at(now, step * 100));
}
let estimate = e.estimate_bps(at(now, 2000)).unwrap();
Expand Down
Loading