1330 lines
42 KiB
Rust
1330 lines
42 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
use super::devices::DeviceCatalog;
|
|
use lesavka_common::eye_source::{
|
|
EyeSourceMode, default_eye_source_mode, display_size_for_source_mode, native_eye_source_modes,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum InputRouting {
|
|
Local,
|
|
Remote,
|
|
}
|
|
|
|
impl InputRouting {
|
|
pub fn as_env(self) -> &'static str {
|
|
match self {
|
|
Self::Local => "0",
|
|
Self::Remote => "1",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ViewMode {
|
|
Unified,
|
|
Breakout,
|
|
}
|
|
|
|
impl ViewMode {
|
|
pub fn as_env(self) -> &'static str {
|
|
match self {
|
|
Self::Unified => "unified",
|
|
Self::Breakout => "breakout",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum DisplaySurface {
|
|
Preview,
|
|
Window,
|
|
}
|
|
|
|
impl DisplaySurface {
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Preview => "preview",
|
|
Self::Window => "window",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum FeedSourcePreset {
|
|
ThisEye,
|
|
OtherEye,
|
|
Off,
|
|
}
|
|
|
|
impl FeedSourcePreset {
|
|
pub fn as_id(self) -> &'static str {
|
|
match self {
|
|
Self::ThisEye => "self",
|
|
Self::OtherEye => "other",
|
|
Self::Off => "off",
|
|
}
|
|
}
|
|
|
|
pub fn from_id(raw: &str) -> Option<Self> {
|
|
match raw {
|
|
"self" => Some(Self::ThisEye),
|
|
"other" => Some(Self::OtherEye),
|
|
"off" => Some(Self::Off),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn label(self, monitor_id: usize) -> &'static str {
|
|
match (monitor_id, self) {
|
|
(_, Self::Off) => "Off",
|
|
(0, Self::ThisEye) => "Left Eye",
|
|
(0, Self::OtherEye) => "Right Eye",
|
|
(1, Self::ThisEye) => "Right Eye",
|
|
(1, Self::OtherEye) => "Left Eye",
|
|
_ => "This Eye",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum BreakoutSizePreset {
|
|
P360,
|
|
P540,
|
|
P720,
|
|
P900,
|
|
P1080,
|
|
P1440,
|
|
Source,
|
|
FillDisplay,
|
|
}
|
|
|
|
impl BreakoutSizePreset {
|
|
pub fn as_id(self) -> &'static str {
|
|
match self {
|
|
Self::P360 => "360p",
|
|
Self::P540 => "540p",
|
|
Self::P720 => "720p",
|
|
Self::P900 => "900p",
|
|
Self::P1080 => "1080p",
|
|
Self::P1440 => "1440p",
|
|
Self::Source => "source",
|
|
Self::FillDisplay => "fill",
|
|
}
|
|
}
|
|
|
|
pub fn from_id(raw: &str) -> Option<Self> {
|
|
match raw {
|
|
"360p" => Some(Self::P360),
|
|
"540p" => Some(Self::P540),
|
|
"720p" => Some(Self::P720),
|
|
"900p" => Some(Self::P900),
|
|
"1080p" => Some(Self::P1080),
|
|
"1440p" => Some(Self::P1440),
|
|
"source" => Some(Self::Source),
|
|
"fill" => Some(Self::FillDisplay),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::P360 => "360p",
|
|
Self::P540 => "540p",
|
|
Self::P720 => "720p",
|
|
Self::P900 => "900p",
|
|
Self::P1080 => "1080p",
|
|
Self::P1440 => "1440p",
|
|
Self::Source => "Source",
|
|
Self::FillDisplay => "Display",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum CaptureSizePreset {
|
|
#[serde(alias = "P360")]
|
|
Vga,
|
|
#[serde(alias = "P540")]
|
|
P480,
|
|
P576,
|
|
P720,
|
|
#[serde(alias = "P900", alias = "P1440", alias = "Source")]
|
|
P1080,
|
|
}
|
|
|
|
impl CaptureSizePreset {
|
|
pub fn as_id(self) -> &'static str {
|
|
match self {
|
|
Self::Vga => "vga",
|
|
Self::P480 => "480p",
|
|
Self::P576 => "576p",
|
|
Self::P720 => "720p",
|
|
Self::P1080 => "1080p",
|
|
}
|
|
}
|
|
|
|
pub fn from_id(raw: &str) -> Option<Self> {
|
|
match raw {
|
|
"vga" | "360p" => Some(Self::Vga),
|
|
"480p" | "540p" => Some(Self::P480),
|
|
"576p" => Some(Self::P576),
|
|
"720p" => Some(Self::P720),
|
|
"900p" | "1080p" | "1440p" | "source" => Some(Self::P1080),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Vga => "VGA",
|
|
Self::P480 => "480p",
|
|
Self::P576 => "576p",
|
|
Self::P720 => "720p",
|
|
Self::P1080 => "1080p",
|
|
}
|
|
}
|
|
|
|
pub fn transport_label(self) -> &'static str {
|
|
"device H.264 pass-through"
|
|
}
|
|
|
|
pub fn source_mode(self) -> EyeSourceMode {
|
|
match normalize_capture_size_preset(self) {
|
|
Self::P720 => native_eye_source_modes()[1],
|
|
Self::P1080 => native_eye_source_modes()[0],
|
|
Self::Vga | Self::P480 | Self::P576 => native_eye_source_modes()[1],
|
|
}
|
|
}
|
|
|
|
pub fn from_source_mode(mode: EyeSourceMode) -> Self {
|
|
match (mode.width, mode.height, mode.fps) {
|
|
(1280, 720, 60) => Self::P720,
|
|
_ => Self::P1080,
|
|
}
|
|
}
|
|
|
|
pub fn display_size(self) -> (u32, u32) {
|
|
display_size_for_source_mode(self.source_mode())
|
|
}
|
|
|
|
pub fn display_aspect_ratio(self) -> f32 {
|
|
let (width, height) = self.display_size();
|
|
width.max(1) as f32 / height.max(1) as f32
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct PreviewSourceSize {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub fps: u32,
|
|
}
|
|
|
|
impl Default for PreviewSourceSize {
|
|
fn default() -> Self {
|
|
let mode = default_eye_source_mode();
|
|
Self {
|
|
width: mode.width,
|
|
height: mode.height,
|
|
fps: mode.fps,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct BreakoutSizeChoice {
|
|
pub preset: BreakoutSizePreset,
|
|
pub width: i32,
|
|
pub height: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct CaptureSizeChoice {
|
|
pub preset: CaptureSizePreset,
|
|
pub width: i32,
|
|
pub height: i32,
|
|
pub fps: u32,
|
|
pub max_bitrate_kbit: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct FeedSourceChoice {
|
|
pub preset: FeedSourcePreset,
|
|
pub label: &'static str,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct CaptureFpsChoice {
|
|
pub fps: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct CaptureBitrateChoice {
|
|
pub max_bitrate_kbit: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct CapturePowerStatus {
|
|
pub available: bool,
|
|
pub enabled: bool,
|
|
pub unit: String,
|
|
pub detail: String,
|
|
pub active_leases: u32,
|
|
pub mode: String,
|
|
pub detected_devices: u32,
|
|
}
|
|
|
|
impl Default for CapturePowerStatus {
|
|
fn default() -> Self {
|
|
Self {
|
|
available: false,
|
|
enabled: false,
|
|
unit: "relay.service".to_string(),
|
|
detail: "unknown".to_string(),
|
|
active_leases: 0,
|
|
mode: "auto".to_string(),
|
|
detected_devices: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
pub struct DeviceSelection {
|
|
pub camera: Option<String>,
|
|
pub microphone: Option<String>,
|
|
pub speaker: Option<String>,
|
|
pub keyboard: Option<String>,
|
|
pub mouse: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LauncherState {
|
|
pub server_available: bool,
|
|
pub server_version: Option<String>,
|
|
pub routing: InputRouting,
|
|
pub view_mode: ViewMode,
|
|
pub displays: [DisplaySurface; 2],
|
|
pub feed_sources: [FeedSourcePreset; 2],
|
|
pub preview_source: PreviewSourceSize,
|
|
pub breakout_limit: PreviewSourceSize,
|
|
pub breakout_display: PreviewSourceSize,
|
|
pub capture_sizes: [CaptureSizePreset; 2],
|
|
pub capture_fps: [u32; 2],
|
|
pub capture_bitrates_kbit: [u32; 2],
|
|
pub breakout_sizes: [BreakoutSizePreset; 2],
|
|
pub devices: DeviceSelection,
|
|
pub swap_key: String,
|
|
pub swap_key_binding: bool,
|
|
pub swap_key_binding_token: u64,
|
|
pub capture_power: CapturePowerStatus,
|
|
pub remote_active: bool,
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
impl Default for LauncherState {
|
|
fn default() -> Self {
|
|
Self {
|
|
server_available: false,
|
|
server_version: None,
|
|
routing: InputRouting::Remote,
|
|
view_mode: ViewMode::Unified,
|
|
displays: [DisplaySurface::Preview, DisplaySurface::Preview],
|
|
feed_sources: [FeedSourcePreset::ThisEye, FeedSourcePreset::ThisEye],
|
|
preview_source: PreviewSourceSize::default(),
|
|
breakout_limit: PreviewSourceSize::default(),
|
|
breakout_display: PreviewSourceSize::default(),
|
|
capture_sizes: [CaptureSizePreset::P1080, CaptureSizePreset::P1080],
|
|
capture_fps: [60, 60],
|
|
capture_bitrates_kbit: [18_000, 18_000],
|
|
breakout_sizes: [BreakoutSizePreset::Source, BreakoutSizePreset::Source],
|
|
devices: DeviceSelection::default(),
|
|
swap_key: "pause".to_string(),
|
|
swap_key_binding: false,
|
|
swap_key_binding_token: 0,
|
|
capture_power: CapturePowerStatus::default(),
|
|
remote_active: false,
|
|
notes: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LauncherState {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn set_routing(&mut self, routing: InputRouting) {
|
|
self.routing = routing;
|
|
}
|
|
|
|
pub fn set_server_available(&mut self, available: bool) {
|
|
self.server_available = available;
|
|
}
|
|
|
|
pub fn set_server_version(&mut self, version: Option<String>) {
|
|
self.server_version = version.and_then(|value| {
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn set_view_mode(&mut self, view_mode: ViewMode) {
|
|
self.view_mode = view_mode;
|
|
self.displays = match view_mode {
|
|
ViewMode::Unified => [DisplaySurface::Preview, DisplaySurface::Preview],
|
|
ViewMode::Breakout => [DisplaySurface::Window, DisplaySurface::Window],
|
|
};
|
|
}
|
|
|
|
pub fn display_surface(&self, monitor_id: usize) -> DisplaySurface {
|
|
self.displays
|
|
.get(monitor_id)
|
|
.copied()
|
|
.unwrap_or(DisplaySurface::Preview)
|
|
}
|
|
|
|
pub fn feed_source_preset(&self, monitor_id: usize) -> FeedSourcePreset {
|
|
self.feed_sources
|
|
.get(monitor_id)
|
|
.copied()
|
|
.unwrap_or(FeedSourcePreset::ThisEye)
|
|
}
|
|
|
|
pub fn set_feed_source_preset(&mut self, monitor_id: usize, preset: FeedSourcePreset) {
|
|
if let Some(slot) = self.feed_sources.get_mut(monitor_id) {
|
|
*slot = preset;
|
|
}
|
|
}
|
|
|
|
pub fn feed_source_options(&self, monitor_id: usize) -> Vec<FeedSourceChoice> {
|
|
vec![
|
|
FeedSourceChoice {
|
|
preset: FeedSourcePreset::ThisEye,
|
|
label: FeedSourcePreset::ThisEye.label(monitor_id),
|
|
},
|
|
FeedSourceChoice {
|
|
preset: FeedSourcePreset::OtherEye,
|
|
label: FeedSourcePreset::OtherEye.label(monitor_id),
|
|
},
|
|
FeedSourceChoice {
|
|
preset: FeedSourcePreset::Off,
|
|
label: FeedSourcePreset::Off.label(monitor_id),
|
|
},
|
|
]
|
|
}
|
|
|
|
pub fn resolved_feed_monitor_id(&self, monitor_id: usize) -> Option<usize> {
|
|
match self.feed_source_preset(monitor_id) {
|
|
FeedSourcePreset::ThisEye => Some(monitor_id.min(1)),
|
|
FeedSourcePreset::OtherEye => Some(1_usize.saturating_sub(monitor_id.min(1))),
|
|
FeedSourcePreset::Off => None,
|
|
}
|
|
}
|
|
|
|
pub fn set_display_surface(&mut self, monitor_id: usize, surface: DisplaySurface) {
|
|
if let Some(slot) = self.displays.get_mut(monitor_id) {
|
|
*slot = surface;
|
|
self.view_mode = if self
|
|
.displays
|
|
.iter()
|
|
.any(|display| matches!(display, DisplaySurface::Window))
|
|
{
|
|
ViewMode::Breakout
|
|
} else {
|
|
ViewMode::Unified
|
|
};
|
|
}
|
|
}
|
|
|
|
pub fn breakout_count(&self) -> usize {
|
|
self.displays
|
|
.iter()
|
|
.filter(|surface| matches!(surface, DisplaySurface::Window))
|
|
.count()
|
|
}
|
|
|
|
pub fn preview_source_size(&self) -> PreviewSourceSize {
|
|
self.preview_source
|
|
}
|
|
|
|
pub fn set_preview_source_profile(&mut self, width: u32, height: u32, fps: u32) {
|
|
if width == 0 || height == 0 {
|
|
return;
|
|
}
|
|
self.preview_source = PreviewSourceSize {
|
|
width,
|
|
height,
|
|
fps: fps.max(1),
|
|
};
|
|
}
|
|
|
|
pub fn breakout_limit_size(&self) -> PreviewSourceSize {
|
|
self.breakout_limit
|
|
}
|
|
|
|
pub fn set_breakout_limit_size(&mut self, width: u32, height: u32) {
|
|
if width == 0 || height == 0 {
|
|
return;
|
|
}
|
|
self.breakout_limit = PreviewSourceSize {
|
|
width,
|
|
height,
|
|
fps: self.breakout_limit.fps.max(1),
|
|
};
|
|
}
|
|
|
|
pub fn breakout_display_size(&self) -> PreviewSourceSize {
|
|
self.breakout_display
|
|
}
|
|
|
|
pub fn set_breakout_display_size(&mut self, width: u32, height: u32) {
|
|
if width == 0 || height == 0 {
|
|
return;
|
|
}
|
|
self.breakout_display = PreviewSourceSize {
|
|
width,
|
|
height,
|
|
fps: self.breakout_display.fps.max(1),
|
|
};
|
|
}
|
|
|
|
pub fn capture_size_preset(&self, monitor_id: usize) -> CaptureSizePreset {
|
|
normalize_capture_size_preset(
|
|
self.capture_sizes
|
|
.get(monitor_id)
|
|
.copied()
|
|
.unwrap_or(CaptureSizePreset::P1080),
|
|
)
|
|
}
|
|
|
|
pub fn display_capture_size_preset(&self, monitor_id: usize) -> Option<CaptureSizePreset> {
|
|
self.resolved_feed_monitor_id(monitor_id)
|
|
.map(|source_id| self.capture_size_preset(source_id))
|
|
}
|
|
|
|
pub fn set_capture_size_preset(&mut self, monitor_id: usize, preset: CaptureSizePreset) {
|
|
let preset = normalize_capture_size_preset(preset);
|
|
if let Some(slot) = self.capture_sizes.get_mut(monitor_id) {
|
|
*slot = preset;
|
|
}
|
|
let defaults = default_profile_for_preset(self.preview_source, preset);
|
|
self.set_capture_fps(monitor_id, defaults.fps);
|
|
self.set_capture_bitrate_kbit(monitor_id, defaults.max_bitrate_kbit);
|
|
}
|
|
|
|
pub fn capture_fps(&self, monitor_id: usize) -> u32 {
|
|
self.capture_fps
|
|
.get(monitor_id)
|
|
.copied()
|
|
.unwrap_or(default_eye_source_mode().fps)
|
|
.max(1)
|
|
}
|
|
|
|
pub fn display_capture_fps(&self, monitor_id: usize) -> Option<u32> {
|
|
self.resolved_feed_monitor_id(monitor_id)
|
|
.map(|source_id| self.capture_fps(source_id))
|
|
}
|
|
|
|
pub fn set_capture_fps(&mut self, monitor_id: usize, fps: u32) {
|
|
if let Some(slot) = self.capture_fps.get_mut(monitor_id) {
|
|
*slot = fps.max(1);
|
|
}
|
|
}
|
|
|
|
pub fn capture_bitrate_kbit(&self, monitor_id: usize) -> u32 {
|
|
self.capture_bitrates_kbit
|
|
.get(monitor_id)
|
|
.copied()
|
|
.unwrap_or(estimate_source_bitrate_kbit(
|
|
default_eye_source_mode().width as i32,
|
|
default_eye_source_mode().height as i32,
|
|
default_eye_source_mode().fps,
|
|
))
|
|
.max(800)
|
|
}
|
|
|
|
pub fn display_capture_bitrate_kbit(&self, monitor_id: usize) -> Option<u32> {
|
|
self.resolved_feed_monitor_id(monitor_id)
|
|
.map(|source_id| self.capture_bitrate_kbit(source_id))
|
|
}
|
|
|
|
pub fn set_capture_bitrate_kbit(&mut self, monitor_id: usize, max_bitrate_kbit: u32) {
|
|
if let Some(slot) = self.capture_bitrates_kbit.get_mut(monitor_id) {
|
|
*slot = max_bitrate_kbit.max(800);
|
|
}
|
|
}
|
|
|
|
pub fn capture_size_choice(&self, monitor_id: usize) -> CaptureSizeChoice {
|
|
capture_size_choice(
|
|
self.preview_source,
|
|
self.capture_size_preset(monitor_id),
|
|
self.capture_fps(monitor_id),
|
|
self.capture_bitrate_kbit(monitor_id),
|
|
)
|
|
}
|
|
|
|
pub fn display_capture_size_choice(&self, monitor_id: usize) -> Option<CaptureSizeChoice> {
|
|
self.resolved_feed_monitor_id(monitor_id)
|
|
.map(|source_id| self.capture_size_choice(source_id))
|
|
}
|
|
|
|
pub fn effective_preview_source_size(&self, monitor_id: usize) -> PreviewSourceSize {
|
|
let capture = self
|
|
.display_capture_size_choice(monitor_id)
|
|
.unwrap_or_else(|| self.capture_size_choice(monitor_id));
|
|
PreviewSourceSize {
|
|
width: capture.width.max(1) as u32,
|
|
height: capture.height.max(1) as u32,
|
|
fps: capture.fps.max(1),
|
|
}
|
|
}
|
|
|
|
pub fn capture_size_options(&self) -> Vec<CaptureSizeChoice> {
|
|
capture_size_options(self.preview_source)
|
|
}
|
|
|
|
pub fn capture_fps_options(&self) -> Vec<CaptureFpsChoice> {
|
|
capture_fps_options(self.preview_source)
|
|
}
|
|
|
|
pub fn capture_bitrate_options(&self) -> Vec<CaptureBitrateChoice> {
|
|
capture_bitrate_options(self.preview_source)
|
|
}
|
|
|
|
pub fn breakout_size_preset(&self, monitor_id: usize) -> BreakoutSizePreset {
|
|
self.breakout_sizes
|
|
.get(monitor_id)
|
|
.copied()
|
|
.unwrap_or(BreakoutSizePreset::Source)
|
|
}
|
|
|
|
pub fn set_breakout_size_preset(&mut self, monitor_id: usize, preset: BreakoutSizePreset) {
|
|
if let Some(slot) = self.breakout_sizes.get_mut(monitor_id) {
|
|
*slot = preset;
|
|
}
|
|
}
|
|
|
|
pub fn breakout_size_choice(&self, monitor_id: usize) -> BreakoutSizeChoice {
|
|
breakout_size_choice(
|
|
self.breakout_limit,
|
|
self.breakout_display,
|
|
self.effective_preview_source_size(monitor_id),
|
|
self.breakout_size_preset(monitor_id),
|
|
)
|
|
}
|
|
|
|
pub fn breakout_size_options(&self, monitor_id: usize) -> Vec<BreakoutSizeChoice> {
|
|
breakout_size_options(
|
|
self.breakout_limit,
|
|
self.breakout_display,
|
|
self.effective_preview_source_size(monitor_id),
|
|
)
|
|
}
|
|
|
|
pub fn select_camera(&mut self, camera: Option<String>) {
|
|
self.devices.camera = normalize_selection(camera);
|
|
}
|
|
|
|
pub fn select_microphone(&mut self, microphone: Option<String>) {
|
|
self.devices.microphone = normalize_selection(microphone);
|
|
}
|
|
|
|
pub fn select_speaker(&mut self, speaker: Option<String>) {
|
|
self.devices.speaker = normalize_selection(speaker);
|
|
}
|
|
|
|
pub fn select_keyboard(&mut self, keyboard: Option<String>) {
|
|
self.devices.keyboard = normalize_selection(keyboard);
|
|
}
|
|
|
|
pub fn select_mouse(&mut self, mouse: Option<String>) {
|
|
self.devices.mouse = normalize_selection(mouse);
|
|
}
|
|
|
|
pub fn apply_catalog_defaults(&mut self, catalog: &DeviceCatalog) {
|
|
let _ = catalog;
|
|
}
|
|
|
|
pub fn set_swap_key(&mut self, swap_key: impl Into<String>) {
|
|
self.swap_key = normalize_swap_key(swap_key.into());
|
|
}
|
|
|
|
pub fn begin_swap_key_binding(&mut self) -> u64 {
|
|
self.swap_key_binding = true;
|
|
self.swap_key_binding_token = self.swap_key_binding_token.wrapping_add(1);
|
|
self.swap_key_binding_token
|
|
}
|
|
|
|
pub fn finish_swap_key_binding(&mut self) {
|
|
self.swap_key_binding = false;
|
|
}
|
|
|
|
pub fn cancel_swap_key_binding(&mut self, token: u64) -> bool {
|
|
if self.swap_key_binding && self.swap_key_binding_token == token {
|
|
self.swap_key_binding = false;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn complete_swap_key_binding(&mut self, swap_key: impl Into<String>) {
|
|
self.set_swap_key(swap_key);
|
|
self.finish_swap_key_binding();
|
|
}
|
|
|
|
pub fn start_remote(&mut self) -> bool {
|
|
if self.remote_active {
|
|
return false;
|
|
}
|
|
self.remote_active = true;
|
|
true
|
|
}
|
|
|
|
pub fn stop_remote(&mut self) -> bool {
|
|
if !self.remote_active {
|
|
return false;
|
|
}
|
|
self.remote_active = false;
|
|
true
|
|
}
|
|
|
|
pub fn push_note(&mut self, note: impl Into<String>) {
|
|
self.notes.push(note.into());
|
|
}
|
|
|
|
pub fn set_capture_power(&mut self, power: CapturePowerStatus) {
|
|
self.capture_power = power;
|
|
}
|
|
|
|
pub fn status_line(&self) -> String {
|
|
format!(
|
|
"server={} mode={} view={} active={} power={} source={}x{} d1={} d2={} s1={} s2={} camera={} mic={} speaker={} kbd={} mouse={} swap={}",
|
|
self.server_available,
|
|
match self.routing {
|
|
InputRouting::Local => "local",
|
|
InputRouting::Remote => "remote",
|
|
},
|
|
match self.view_mode {
|
|
ViewMode::Unified => "unified",
|
|
ViewMode::Breakout => "breakout",
|
|
},
|
|
self.remote_active,
|
|
if self.capture_power.enabled {
|
|
"on"
|
|
} else {
|
|
"off"
|
|
},
|
|
self.preview_source.width,
|
|
self.preview_source.height,
|
|
self.displays[0].label(),
|
|
self.displays[1].label(),
|
|
self.feed_source_preset(0).as_id(),
|
|
self.feed_source_preset(1).as_id(),
|
|
self.devices.camera.as_deref().unwrap_or("auto"),
|
|
self.devices.microphone.as_deref().unwrap_or("auto"),
|
|
self.devices.speaker.as_deref().unwrap_or("auto"),
|
|
self.devices.keyboard.as_deref().unwrap_or("all"),
|
|
self.devices.mouse.as_deref().unwrap_or("all"),
|
|
self.swap_key,
|
|
)
|
|
}
|
|
}
|
|
|
|
fn breakout_size_choice(
|
|
physical_limit: PreviewSourceSize,
|
|
display_fill: PreviewSourceSize,
|
|
source: PreviewSourceSize,
|
|
preset: BreakoutSizePreset,
|
|
) -> BreakoutSizeChoice {
|
|
let physical_width = physical_limit.width.max(1) as i32;
|
|
let physical_height = physical_limit.height.max(1) as i32;
|
|
let display_width = display_fill.width.max(1) as i32;
|
|
let display_height = display_fill.height.max(1) as i32;
|
|
let (width, height) = match preset {
|
|
BreakoutSizePreset::P360 => {
|
|
fit_standard_dimensions(physical_width, physical_height, 640, 360)
|
|
}
|
|
BreakoutSizePreset::P540 => {
|
|
fit_standard_dimensions(physical_width, physical_height, 960, 540)
|
|
}
|
|
BreakoutSizePreset::P720 => {
|
|
fit_standard_dimensions(physical_width, physical_height, 1280, 720)
|
|
}
|
|
BreakoutSizePreset::P900 => {
|
|
fit_standard_dimensions(physical_width, physical_height, 1600, 900)
|
|
}
|
|
BreakoutSizePreset::P1080 => {
|
|
fit_standard_dimensions(physical_width, physical_height, 1920, 1080)
|
|
}
|
|
BreakoutSizePreset::P1440 => {
|
|
fit_standard_dimensions(physical_width, physical_height, 2560, 1440)
|
|
}
|
|
BreakoutSizePreset::Source => fit_standard_dimensions(
|
|
physical_width,
|
|
physical_height,
|
|
source.width.max(1) as i32,
|
|
source.height.max(1) as i32,
|
|
),
|
|
BreakoutSizePreset::FillDisplay => (display_width, display_height),
|
|
};
|
|
BreakoutSizeChoice {
|
|
preset,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
fn breakout_size_options(
|
|
physical_limit: PreviewSourceSize,
|
|
display_fill: PreviewSourceSize,
|
|
source: PreviewSourceSize,
|
|
) -> Vec<BreakoutSizeChoice> {
|
|
let mut options = Vec::new();
|
|
for preset in [
|
|
BreakoutSizePreset::Source,
|
|
BreakoutSizePreset::P360,
|
|
BreakoutSizePreset::P540,
|
|
BreakoutSizePreset::P720,
|
|
BreakoutSizePreset::P900,
|
|
BreakoutSizePreset::P1080,
|
|
BreakoutSizePreset::P1440,
|
|
BreakoutSizePreset::FillDisplay,
|
|
] {
|
|
let choice = breakout_size_choice(physical_limit, display_fill, source, preset);
|
|
let allow_duplicate_label = matches!(
|
|
preset,
|
|
BreakoutSizePreset::Source | BreakoutSizePreset::FillDisplay
|
|
);
|
|
if !allow_duplicate_label
|
|
&& options.iter().any(|existing: &BreakoutSizeChoice| {
|
|
existing.width == choice.width && existing.height == choice.height
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
options.push(choice);
|
|
}
|
|
options
|
|
}
|
|
|
|
fn capture_size_choice(
|
|
_source: PreviewSourceSize,
|
|
preset: CaptureSizePreset,
|
|
selected_fps: u32,
|
|
selected_bitrate_kbit: u32,
|
|
) -> CaptureSizeChoice {
|
|
let preset = normalize_capture_size_preset(preset);
|
|
let mode = preset.source_mode();
|
|
let _ = (selected_fps, selected_bitrate_kbit);
|
|
let (width, height, fps, max_bitrate_kbit) = (
|
|
mode.width as i32,
|
|
mode.height as i32,
|
|
mode.fps,
|
|
estimate_source_bitrate_kbit(mode.width as i32, mode.height as i32, mode.fps),
|
|
);
|
|
CaptureSizeChoice {
|
|
preset,
|
|
width,
|
|
height,
|
|
fps,
|
|
max_bitrate_kbit,
|
|
}
|
|
}
|
|
|
|
fn estimate_source_bitrate_kbit(width: i32, height: i32, fps: u32) -> u32 {
|
|
let pixels_per_second = width.max(1) as u64 * height.max(1) as u64 * fps.max(1) as u64;
|
|
match pixels_per_second {
|
|
p if p >= 1920_u64 * 1080 * 50 => 18_000,
|
|
p if p >= 1920_u64 * 1080 * 24 => 12_000,
|
|
p if p >= 1280_u64 * 720 * 24 => 6_000,
|
|
_ => 2_500,
|
|
}
|
|
}
|
|
|
|
fn capture_size_options(source: PreviewSourceSize) -> Vec<CaptureSizeChoice> {
|
|
native_eye_source_modes()
|
|
.iter()
|
|
.copied()
|
|
.filter(|mode| mode.width <= source.width && mode.height <= source.height)
|
|
.map(CaptureSizePreset::from_source_mode)
|
|
.map(|preset| {
|
|
let defaults = default_profile_for_preset(source, preset);
|
|
capture_size_choice(source, preset, defaults.fps, defaults.max_bitrate_kbit)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn capture_fps_options(source: PreviewSourceSize) -> Vec<CaptureFpsChoice> {
|
|
vec![CaptureFpsChoice {
|
|
fps: source.fps.max(1),
|
|
}]
|
|
}
|
|
|
|
fn capture_bitrate_options(source: PreviewSourceSize) -> Vec<CaptureBitrateChoice> {
|
|
vec![CaptureBitrateChoice {
|
|
max_bitrate_kbit: estimate_source_bitrate_kbit(
|
|
source.width as i32,
|
|
source.height as i32,
|
|
source.fps,
|
|
),
|
|
}]
|
|
}
|
|
|
|
fn default_profile_for_preset(
|
|
_source: PreviewSourceSize,
|
|
preset: CaptureSizePreset,
|
|
) -> CaptureSizeChoice {
|
|
let preset = normalize_capture_size_preset(preset);
|
|
let mode = preset.source_mode();
|
|
let (width, height, fps, max_bitrate_kbit) = (
|
|
mode.width as i32,
|
|
mode.height as i32,
|
|
mode.fps,
|
|
estimate_source_bitrate_kbit(mode.width as i32, mode.height as i32, mode.fps),
|
|
);
|
|
CaptureSizeChoice {
|
|
preset,
|
|
width,
|
|
height,
|
|
fps,
|
|
max_bitrate_kbit,
|
|
}
|
|
}
|
|
|
|
fn normalize_capture_size_preset(preset: CaptureSizePreset) -> CaptureSizePreset {
|
|
match preset {
|
|
CaptureSizePreset::Vga | CaptureSizePreset::P480 | CaptureSizePreset::P576 => {
|
|
CaptureSizePreset::P720
|
|
}
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
fn fit_standard_dimensions(
|
|
limit_width: i32,
|
|
limit_height: i32,
|
|
wanted_width: i32,
|
|
wanted_height: i32,
|
|
) -> (i32, i32) {
|
|
let width = wanted_width.min(limit_width).max(2);
|
|
let height = wanted_height.min(limit_height).max(2);
|
|
if width == limit_width && height == limit_height {
|
|
return (width, height);
|
|
}
|
|
let width_from_height = round_down_even((height * 16) / 9);
|
|
if width_from_height <= limit_width {
|
|
(round_down_even(width_from_height), round_down_even(height))
|
|
} else {
|
|
let height_from_width = round_down_even((width * 9) / 16);
|
|
(round_down_even(width), round_down_even(height_from_width))
|
|
}
|
|
}
|
|
|
|
fn round_down_even(value: i32) -> i32 {
|
|
let rounded = value.max(2);
|
|
rounded - (rounded % 2)
|
|
}
|
|
|
|
fn normalize_selection(value: Option<String>) -> Option<String> {
|
|
value.and_then(|v| {
|
|
let trimmed = v.trim();
|
|
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("auto") {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
})
|
|
}
|
|
|
|
fn normalize_swap_key(value: String) -> String {
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
"off".to_string()
|
|
} else {
|
|
trimmed.to_ascii_lowercase()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn routing_and_view_env_values_are_stable() {
|
|
assert_eq!(InputRouting::Local.as_env(), "0");
|
|
assert_eq!(InputRouting::Remote.as_env(), "1");
|
|
assert_eq!(ViewMode::Unified.as_env(), "unified");
|
|
assert_eq!(ViewMode::Breakout.as_env(), "breakout");
|
|
}
|
|
|
|
#[test]
|
|
fn defaults_pick_remote_unified_and_inactive_session() {
|
|
let state = LauncherState::new();
|
|
assert_eq!(state.routing, InputRouting::Remote);
|
|
assert_eq!(state.view_mode, ViewMode::Unified);
|
|
assert_eq!(state.display_surface(0), DisplaySurface::Preview);
|
|
assert_eq!(state.display_surface(1), DisplaySurface::Preview);
|
|
assert_eq!(state.preview_source_size(), PreviewSourceSize::default());
|
|
assert_eq!(state.breakout_limit_size(), PreviewSourceSize::default());
|
|
assert_eq!(state.capture_size_preset(0), CaptureSizePreset::P1080);
|
|
assert_eq!(state.breakout_size_preset(0), BreakoutSizePreset::Source);
|
|
assert!(!state.server_available);
|
|
assert!(!state.remote_active);
|
|
assert!(state.devices.camera.is_none());
|
|
assert!(state.devices.microphone.is_none());
|
|
assert!(state.devices.speaker.is_none());
|
|
assert!(state.devices.keyboard.is_none());
|
|
assert!(state.devices.mouse.is_none());
|
|
assert_eq!(state.capture_power.unit, "relay.service");
|
|
assert_eq!(state.capture_power.mode, "auto");
|
|
}
|
|
|
|
#[test]
|
|
fn display_surface_updates_global_view_summary() {
|
|
let mut state = LauncherState::new();
|
|
state.set_display_surface(1, DisplaySurface::Window);
|
|
assert_eq!(state.view_mode, ViewMode::Breakout);
|
|
assert_eq!(state.breakout_count(), 1);
|
|
|
|
state.set_display_surface(1, DisplaySurface::Preview);
|
|
assert_eq!(state.view_mode, ViewMode::Unified);
|
|
assert_eq!(state.breakout_count(), 0);
|
|
|
|
state.set_view_mode(ViewMode::Breakout);
|
|
assert_eq!(state.display_surface(0), DisplaySurface::Window);
|
|
assert_eq!(state.display_surface(1), DisplaySurface::Window);
|
|
}
|
|
|
|
#[test]
|
|
fn feed_sources_can_mirror_or_disable_a_pane() {
|
|
let mut state = LauncherState::new();
|
|
state.set_capture_size_preset(1, CaptureSizePreset::P1080);
|
|
|
|
assert_eq!(state.resolved_feed_monitor_id(0), Some(0));
|
|
assert_eq!(state.resolved_feed_monitor_id(1), Some(1));
|
|
|
|
state.set_feed_source_preset(0, FeedSourcePreset::OtherEye);
|
|
assert_eq!(state.resolved_feed_monitor_id(0), Some(1));
|
|
assert_eq!(
|
|
state.display_capture_size_choice(0),
|
|
Some(state.capture_size_choice(1))
|
|
);
|
|
|
|
state.set_feed_source_preset(1, FeedSourcePreset::OtherEye);
|
|
assert_eq!(state.resolved_feed_monitor_id(1), Some(0));
|
|
|
|
state.set_feed_source_preset(0, FeedSourcePreset::Off);
|
|
assert_eq!(state.resolved_feed_monitor_id(0), None);
|
|
assert!(state.display_capture_size_choice(0).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn mirrored_panes_use_their_effective_source_size_for_breakout_source_labels() {
|
|
let mut state = LauncherState::new();
|
|
state.set_capture_size_preset(1, CaptureSizePreset::P720);
|
|
state.set_feed_source_preset(0, FeedSourcePreset::OtherEye);
|
|
|
|
let mirrored_source = state.effective_preview_source_size(0);
|
|
assert_eq!(mirrored_source.width, 1280);
|
|
assert_eq!(mirrored_source.height, 720);
|
|
assert_eq!(mirrored_source.fps, 60);
|
|
|
|
let mirrored_breakout = state.breakout_size_choice(0);
|
|
assert_eq!(mirrored_breakout.preset, BreakoutSizePreset::Source);
|
|
assert_eq!(mirrored_breakout.width, 1280);
|
|
assert_eq!(mirrored_breakout.height, 720);
|
|
}
|
|
|
|
#[test]
|
|
fn selecting_auto_or_blank_clears_explicit_device() {
|
|
let mut state = LauncherState::new();
|
|
state.select_camera(Some("/dev/video0".to_string()));
|
|
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video0"));
|
|
|
|
state.select_camera(Some("auto".to_string()));
|
|
assert!(state.devices.camera.is_none());
|
|
|
|
state.select_microphone(Some(" ".to_string()));
|
|
assert!(state.devices.microphone.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn catalog_defaults_do_not_auto_stage_media_devices() {
|
|
let mut state = LauncherState::new();
|
|
state.select_camera(Some("/dev/video-special".to_string()));
|
|
|
|
let catalog = DeviceCatalog {
|
|
cameras: vec!["/dev/video0".to_string()],
|
|
microphones: vec!["alsa_input.usb".to_string()],
|
|
speakers: vec!["alsa_output.usb".to_string()],
|
|
keyboards: vec!["/dev/input/event10".to_string()],
|
|
mice: vec!["/dev/input/event11".to_string()],
|
|
};
|
|
|
|
state.apply_catalog_defaults(&catalog);
|
|
|
|
assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special"));
|
|
assert!(state.devices.microphone.is_none());
|
|
assert!(state.devices.speaker.is_none());
|
|
|
|
let mut fresh = LauncherState::new();
|
|
fresh.apply_catalog_defaults(&catalog);
|
|
assert!(fresh.devices.camera.is_none());
|
|
assert!(fresh.devices.microphone.is_none());
|
|
assert!(fresh.devices.speaker.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn start_and_stop_remote_only_report_changes_once() {
|
|
let mut state = LauncherState::new();
|
|
assert!(state.start_remote());
|
|
assert!(!state.start_remote());
|
|
assert!(state.remote_active);
|
|
|
|
assert!(state.stop_remote());
|
|
assert!(!state.stop_remote());
|
|
assert!(!state.remote_active);
|
|
}
|
|
|
|
#[test]
|
|
fn status_line_mentions_all_user_visible_controls() {
|
|
let mut state = LauncherState::new();
|
|
state.set_server_available(true);
|
|
state.set_routing(InputRouting::Local);
|
|
state.set_view_mode(ViewMode::Unified);
|
|
state.select_camera(Some("/dev/video0".to_string()));
|
|
state.select_microphone(Some("alsa_input.usb".to_string()));
|
|
state.select_speaker(Some("alsa_output.usb".to_string()));
|
|
state.select_keyboard(Some("/dev/input/event-kbd".to_string()));
|
|
state.select_mouse(Some("/dev/input/event-mouse".to_string()));
|
|
state.set_preview_source_profile(1920, 1080, 30);
|
|
state.start_remote();
|
|
|
|
let status = state.status_line();
|
|
assert!(status.contains("mode=local"));
|
|
assert!(status.contains("server=true"));
|
|
assert!(status.contains("view=unified"));
|
|
assert!(status.contains("active=true"));
|
|
assert!(status.contains("source=1920x1080"));
|
|
assert!(status.contains("d1=preview"));
|
|
assert!(status.contains("d2=preview"));
|
|
assert!(status.contains("camera=/dev/video0"));
|
|
assert!(status.contains("mic=alsa_input.usb"));
|
|
assert!(status.contains("speaker=alsa_output.usb"));
|
|
assert!(status.contains("kbd=/dev/input/event-kbd"));
|
|
assert!(status.contains("mouse=/dev/input/event-mouse"));
|
|
}
|
|
|
|
#[test]
|
|
fn capture_power_status_updates_snapshot_state() {
|
|
let mut state = LauncherState::new();
|
|
state.set_capture_power(CapturePowerStatus {
|
|
available: true,
|
|
enabled: true,
|
|
unit: "relay.service".to_string(),
|
|
detail: "active/running".to_string(),
|
|
active_leases: 2,
|
|
mode: "forced-on".to_string(),
|
|
detected_devices: 2,
|
|
});
|
|
|
|
assert!(state.capture_power.available);
|
|
assert!(state.capture_power.enabled);
|
|
assert_eq!(state.capture_power.active_leases, 2);
|
|
assert!(state.status_line().contains("power=on"));
|
|
}
|
|
|
|
#[test]
|
|
fn server_availability_tracks_reachability() {
|
|
let mut state = LauncherState::new();
|
|
assert!(!state.server_available);
|
|
state.set_server_available(true);
|
|
assert!(state.server_available);
|
|
}
|
|
|
|
#[test]
|
|
fn breakout_size_choices_track_the_negotiated_source_size() {
|
|
let mut state = LauncherState::new();
|
|
state.set_preview_source_profile(1920, 1080, 60);
|
|
state.set_breakout_limit_size(2560, 1440);
|
|
|
|
let source = state.capture_size_choice(0);
|
|
assert_eq!(source.width, 1920);
|
|
assert_eq!(source.height, 1080);
|
|
assert_eq!(source.fps, 60);
|
|
assert_eq!(source.max_bitrate_kbit, 18_000);
|
|
|
|
state.set_capture_size_preset(0, CaptureSizePreset::P480);
|
|
let compact_capture = state.capture_size_choice(0);
|
|
assert_eq!(compact_capture.preset, CaptureSizePreset::P720);
|
|
assert_eq!(compact_capture.width, 1280);
|
|
assert_eq!(compact_capture.height, 720);
|
|
assert_eq!(compact_capture.fps, 60);
|
|
assert_eq!(compact_capture.max_bitrate_kbit, 12_000);
|
|
|
|
let effective_source = state.effective_preview_source_size(0);
|
|
assert_eq!(effective_source.width, 1280);
|
|
assert_eq!(effective_source.height, 720);
|
|
assert_eq!(effective_source.fps, 60);
|
|
|
|
let display = state.breakout_size_choice(0);
|
|
assert_eq!(display.width, 1280);
|
|
assert_eq!(display.height, 720);
|
|
|
|
state.set_breakout_size_preset(0, BreakoutSizePreset::P360);
|
|
let smaller = state.breakout_size_choice(0);
|
|
assert_eq!(smaller.width, 640);
|
|
assert_eq!(smaller.height, 360);
|
|
|
|
state.set_breakout_size_preset(0, BreakoutSizePreset::P540);
|
|
let compact = state.breakout_size_choice(0);
|
|
assert_eq!(compact.width, 960);
|
|
assert_eq!(compact.height, 540);
|
|
|
|
let capture_options = state.capture_size_options();
|
|
assert_eq!(capture_options.len(), 2);
|
|
assert_eq!(capture_options[0].preset, CaptureSizePreset::P1080);
|
|
assert_eq!(capture_options[0].width, 1920);
|
|
assert_eq!(capture_options[0].height, 1080);
|
|
assert_eq!(capture_options[0].fps, 60);
|
|
assert_eq!(capture_options[0].max_bitrate_kbit, 18_000);
|
|
|
|
let breakout_options = state.breakout_size_options(0);
|
|
assert!(breakout_options.len() >= 5);
|
|
assert!(breakout_options.iter().any(|choice| {
|
|
choice.preset == BreakoutSizePreset::Source
|
|
&& choice.width == 1280
|
|
&& choice.height == 720
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn swap_key_binding_tracks_selected_key_and_binding_mode() {
|
|
let mut state = LauncherState::new();
|
|
assert_eq!(state.swap_key, "pause");
|
|
assert!(!state.swap_key_binding);
|
|
|
|
let token = state.begin_swap_key_binding();
|
|
assert!(state.swap_key_binding);
|
|
assert_eq!(token, state.swap_key_binding_token);
|
|
|
|
state.set_swap_key("F8");
|
|
assert_eq!(state.swap_key, "f8");
|
|
|
|
state.set_swap_key(" ");
|
|
assert_eq!(state.swap_key, "off");
|
|
|
|
state.finish_swap_key_binding();
|
|
assert!(!state.swap_key_binding);
|
|
}
|
|
|
|
#[test]
|
|
fn swap_key_binding_timeout_only_cancels_matching_attempt() {
|
|
let mut state = LauncherState::new();
|
|
let first = state.begin_swap_key_binding();
|
|
let second = state.begin_swap_key_binding();
|
|
|
|
assert!(!state.cancel_swap_key_binding(first));
|
|
assert!(state.swap_key_binding);
|
|
assert!(state.cancel_swap_key_binding(second));
|
|
assert!(!state.swap_key_binding);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_swap_key_binding_updates_value_and_ends_binding() {
|
|
let mut state = LauncherState::new();
|
|
state.begin_swap_key_binding();
|
|
|
|
state.complete_swap_key_binding("F12");
|
|
|
|
assert_eq!(state.swap_key, "f12");
|
|
assert!(!state.swap_key_binding);
|
|
}
|
|
|
|
#[test]
|
|
fn push_note_accumulates_operator_context() {
|
|
let mut state = LauncherState::new();
|
|
state.push_note("preview warm");
|
|
state.push_note("relay linked");
|
|
|
|
assert_eq!(state.notes, vec!["preview warm", "relay linked"]);
|
|
}
|
|
|
|
#[test]
|
|
fn capture_size_presets_map_to_real_device_modes() {
|
|
let mut state = LauncherState::new();
|
|
state.set_preview_source_profile(1920, 1080, 60);
|
|
state.set_capture_size_preset(0, CaptureSizePreset::P1080);
|
|
let source = state.capture_size_choice(0);
|
|
assert_eq!(source.width, 1920);
|
|
assert_eq!(source.height, 1080);
|
|
assert_eq!(source.fps, 60);
|
|
assert!(source.max_bitrate_kbit >= 18_000);
|
|
|
|
state.set_capture_size_preset(0, CaptureSizePreset::P720);
|
|
let hd = state.capture_size_choice(0);
|
|
assert_eq!(hd.preset, CaptureSizePreset::P720);
|
|
assert_eq!(hd.width, 1280);
|
|
assert_eq!(hd.height, 720);
|
|
assert_eq!(hd.fps, 60);
|
|
|
|
state.set_capture_size_preset(0, CaptureSizePreset::P576);
|
|
let compact = state.capture_size_choice(0);
|
|
assert_eq!(compact.preset, CaptureSizePreset::P720);
|
|
assert_eq!(compact.width, 1280);
|
|
assert_eq!(compact.height, 720);
|
|
assert_eq!(compact.fps, 60);
|
|
|
|
state.set_capture_size_preset(0, CaptureSizePreset::Vga);
|
|
let small = state.capture_size_choice(0);
|
|
assert_eq!(small.preset, CaptureSizePreset::P720);
|
|
assert_eq!(small.width, 1280);
|
|
assert_eq!(small.height, 720);
|
|
assert_eq!(small.fps, 60);
|
|
}
|
|
|
|
#[test]
|
|
fn source_capture_knobs_follow_the_selected_native_mode() {
|
|
let mut state = LauncherState::new();
|
|
state.set_preview_source_profile(1920, 1080, 60);
|
|
|
|
state.set_capture_size_preset(1, CaptureSizePreset::P1080);
|
|
let defaults = state.capture_size_choice(1);
|
|
assert_eq!(defaults.width, 1920);
|
|
assert_eq!(defaults.height, 1080);
|
|
assert_eq!(defaults.fps, 60);
|
|
assert_eq!(defaults.max_bitrate_kbit, 18_000);
|
|
|
|
state.set_capture_fps(1, 24);
|
|
state.set_capture_bitrate_kbit(1, 8_500);
|
|
let tuned = state.capture_size_choice(1);
|
|
assert_eq!(tuned.preset, CaptureSizePreset::P1080);
|
|
assert_eq!(tuned.width, 1920);
|
|
assert_eq!(tuned.height, 1080);
|
|
assert_eq!(tuned.fps, 60);
|
|
assert_eq!(tuned.max_bitrate_kbit, 18_000);
|
|
}
|
|
|
|
#[test]
|
|
fn source_capture_ignores_manual_fps_and_bitrate_knobs() {
|
|
let mut state = LauncherState::new();
|
|
state.set_preview_source_profile(1920, 1080, 60);
|
|
state.set_capture_size_preset(0, CaptureSizePreset::P720);
|
|
state.set_capture_fps(0, 60);
|
|
state.set_capture_bitrate_kbit(0, 24_000);
|
|
|
|
let source = state.capture_size_choice(0);
|
|
assert_eq!(source.preset, CaptureSizePreset::P720);
|
|
assert_eq!(source.width, 1280);
|
|
assert_eq!(source.height, 720);
|
|
assert_eq!(source.fps, 60);
|
|
assert_eq!(source.max_bitrate_kbit, 12_000);
|
|
}
|
|
}
|