diff --git a/necromancer/examples/watch.rs b/necromancer/examples/watch.rs index f2d1a06..578415c 100644 --- a/necromancer/examples/watch.rs +++ b/necromancer/examples/watch.rs @@ -45,6 +45,12 @@ async fn main() -> Result<()> { info!("ME {me}: Program {pgm:?}, Preview {pre:?}"); } + if state.topology.auxs > 0 { + for aux in 0..state.topology.auxs { + let src = state.get_aux_source(aux).unwrap_or_default(); + info!("AUX {aux}: {src:?}"); + } + } info!( "Supported DVE transition styles (scale={:?}, rotate={:?}): {:?}", state.dve_can_scale_up, state.dve_can_rotate, state.dve_supported_transition_styles, @@ -67,6 +73,10 @@ async fn main() -> Result<()> { info!("Preview sources: {:?}", state.get_preview_sources()); } + if update.contains(StateUpdate::AUX_SOURCE) { + info!("AUX sources: {:?}", state.get_aux_sources()); + } + if update.contains(StateUpdate::FADE_TO_BLACK_RATE) { info!("Fade to black rates: {:?}", state.get_fade_to_black_rates()); } diff --git a/necromancer/src/controller.rs b/necromancer/src/controller.rs index b494659..9bd9f1c 100644 --- a/necromancer/src/controller.rs +++ b/necromancer/src/controller.rs @@ -3,7 +3,7 @@ use crate::{ protocol::{ atom::{ Atom, Auto, Cut, CutToBlack, FadeToBlackAuto, FileTransferChunkParams, FileType, - FinishFileDownload, MediaPlayerSourceID, MediaPoolLock, Payload, + FinishFileDownload, MediaPlayerSourceID, MediaPoolLock, Payload, ChangeAuxSource, SetColourGeneratorParams, SetMediaPlayerSource, SetPreviewInput, SetProgramInput, SetupFileDownload, SetupFileUpload, TimecodeRequest, TransferChunk, CAPTURE_STILL, CLEAR_MEDIA_POOL, CLEAR_STARTUP_SETTINGS, RESTORE_STARTUP_SETTINGS, @@ -354,6 +354,22 @@ impl AtemController { self.send(vec![cmd]).await } + /// Sets the current source for a given AUX bus. + pub async fn set_aux_source(&self, aux_bus: u8, video_source: VideoSource) -> Result<(), Error> { + let state = self.get_state().await; + if aux_bus >= state.topology.auxs { + error!( + "aux bus #{aux_bus} does not exist, switcher has {} aux bus(es)", + state.topology.auxs + ); + return Err(Error::ParameterOutOfRange); + } + drop(state); + + let cmd = Atom::new(ChangeAuxSource { aux_bus, video_source }); + self.send(vec![cmd]).await + } + pub async fn cut_black(&self, me: u8, black: bool) -> Result<(), Error> { let cmd = Atom::new(CutToBlack { me, black }); self.send(vec![cmd]).await diff --git a/necromancer/src/state.rs b/necromancer/src/state.rs index b00bc76..fc09461 100644 --- a/necromancer/src/state.rs +++ b/necromancer/src/state.rs @@ -42,6 +42,7 @@ bitflags! { const FAIRLIGHT_INPUT_SOURCE_PROPS = 1 << 19; const FAIRLIGHT_FREQUENCY_RANGES = 1 << 20; const DVE_CAPABILITIES = 1 << 21; + const AUX_SOURCE = 1 << 22; const PREVIEW_OR_PROGRAM_SOURCE = Self::PREVIEW_SOURCE.bits() | Self::PROGRAM_SOURCE.bits(); const UNSUPPORTED_COMMAND = 1 << 31; @@ -75,6 +76,8 @@ pub struct AtemState { me_capabilities: [MixEffectBlockCapabilities; MAX_MES], program_source: [VideoSource; MAX_MES], preview_source: [VideoSource; MAX_MES], + /// Current source for each AUX bus. + pub aux_sources: Vec, /// Transition position for each ME. pub transition_position: HashMap, /// Current tally state for each source. @@ -197,6 +200,10 @@ impl AtemState { self.topology.media_players, ); } + if self.aux_sources.len() != usize::from(self.topology.auxs) { + self.aux_sources + .resize(usize::from(self.topology.auxs), VideoSource::default()); + } updated_fields |= StateUpdate::TOPOLOGY; } @@ -314,6 +321,20 @@ impl AtemState { updated_fields |= StateUpdate::MEDIA_PLAYER_SOURCE; } + Payload::AuxSource(aux) => { + debug!(?aux, "updated aux source"); + let idx = usize::from(aux.aux_bus); + if idx >= usize::from(self.topology.auxs) { + continue; + } + if self.aux_sources.len() <= idx { + self.aux_sources + .resize(usize::from(self.topology.auxs), VideoSource::default()); + } + self.aux_sources[idx] = aux.video_source; + updated_fields |= StateUpdate::AUX_SOURCE; + } + Payload::ColourGeneratorParams(colv) => { debug!(?colv, "updated colour generator params"); if colv.id >= MAX_COLOUR_GENERATORS { @@ -423,6 +444,22 @@ impl AtemState { &self.preview_source[0..self.topology.mes as usize] } + /// Get the current source for a given AUX bus. + /// + /// Returns `None` if the bus index is invalid for this switcher's topology. + pub fn get_aux_source(&self, bus: u8) -> Option { + if bus >= self.topology.auxs { + return None; + } + let idx = usize::from(bus); + self.aux_sources.get(idx).copied() + } + + /// Get the current sources for all AUX buses. + pub fn get_aux_sources(&self) -> &[VideoSource] { + &self.aux_sources + } + pub const fn get_fade_to_black_status(&self, me: u8) -> Option { if me >= self.topology.mes { return None; @@ -500,6 +537,7 @@ impl std::fmt::Debug for AtemState { "preview_source", &&self.preview_source[..MAX_MES.min(self.topology.mes as usize)], ) + .field("aux_sources", &self.aux_sources) .field("transition_position", &self.transition_position) .field("tally_by_source", &self.tally_by_source) .field("supported_video_modes", &self.supported_video_modes) diff --git a/necromancer_protocol/src/atom/aux_.rs b/necromancer_protocol/src/atom/aux_.rs index 83e3813..32fe484 100644 --- a/necromancer_protocol/src/atom/aux_.rs +++ b/necromancer_protocol/src/atom/aux_.rs @@ -1,8 +1,42 @@ -//! # Aux; 0/2 atoms -//! -//! ## Unimplemented atoms (2) -//! -//! FourCC | Atom name | Length -//! ------ | --------- | ------ -//! `AuxS` | `AuxSource` | 0xc -//! `CAuS` | `ChangeAuxSource` | 0xc +//! Aux (auxiliary) video outputs. + +use crate::structs::VideoSource; +use binrw::binrw; + +/// `AuxS`: current aux source (`AuxSource`) +/// +/// Sent by the switcher to report the current source on an AUX bus. +/// +/// ## Packet format +/// +/// * `u8`: aux bus ID +/// * 1 byte padding +/// * `u16`: video source +#[binrw] +#[brw(big)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AuxSource { + #[brw(pad_after = 1)] + pub aux_bus: u8, + pub video_source: VideoSource, +} + +/// `CAuS`: change aux source (`ChangeAuxSource`) +/// +/// Sent by a client to change the source on an AUX bus. +/// +/// sofie-atem-connection serialises this as: +/// +/// * `u8`: setting mask (always `0x01`) +/// * `u8`: aux bus ID +/// * `u16`: video source +#[binrw] +#[brw(big)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ChangeAuxSource { + #[br(temp)] + #[bw(calc = 0x01)] + _setting_mask: u8, + pub aux_bus: u8, + pub video_source: VideoSource, +} diff --git a/necromancer_protocol/src/atom/mod.rs b/necromancer_protocol/src/atom/mod.rs index 5fe339d..d79b787 100644 --- a/necromancer_protocol/src/atom/mod.rs +++ b/necromancer_protocol/src/atom/mod.rs @@ -82,6 +82,7 @@ use binrw::{binrw, helpers::until_eof, io::TakeSeekExt}; use std::{fmt::Debug, io::SeekFrom}; pub use self::{ + aux::{AuxSource, ChangeAuxSource}, camera::{CameraCommand, CameraControl}, colour::{ColourGeneratorParams, SetColourGeneratorParams}, fairlight::{ @@ -246,6 +247,7 @@ atom_payloads!( b"_ver" => Version, b"_VMC" => SupportedVideoModes, b"AMBP" => FairlightAudioMixerMasterOutEqualiserBandProperties, + b"AuxS" => AuxSource, b"Capt" => CaptureStill, b"CCdP" => CameraControl, b"CClV" => SetColourGeneratorParams, @@ -254,6 +256,7 @@ atom_payloads!( b"ColV" => ColourGeneratorParams, b"CPgI" => SetProgramInput, b"CPvI" => SetPreviewInput, + b"CAuS" => ChangeAuxSource, b"CTCC" => SetTimecodeConfig, b"CVdM" => SetVideoMode, b"DAut" => Auto,