From 6e7af6132b846f79545f82baddc93d0e02fd4db3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:38:25 +0000 Subject: [PATCH 1/2] feat(types): add StreamTypes newtype for stream-kind bit masks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Public recording APIs previously accepted the TeamTalk stream-kind bitmask as a raw `u32`, which made it easy to pass an unrelated integer or to lose the intent of a combined mask (e.g. voice + desktop). The new `StreamTypes` newtype wraps the same `u32` bitmask with: * Associated constants mirroring `teamtalk_sys::StreamType::STREAMTYPE_*` (VOICE, VIDEO_CAPTURE, MEDIAFILE_AUDIO, MEDIAFILE_VIDEO, DESKTOP, DESKTOP_INPUT, MEDIAFILE, CHANNEL_MSG, LOCAL_MEDIAPLAYBACK_AUDIO, CLASSROOM_ALL). * Standard bit operators (`|`, `|=`, `&`, `&=`, `!`). * `contains_any` / `contains_all` / `is_empty` inspectors. * `raw()` / `from_raw()` round-trip with arbitrary `u32` so callers reading a mask from an event do not lose unknown bits. * `From` / `From for u32` so the new type slides into existing `impl Into` parameters with no caller changes at the `u32` call sites. Public recording surfaces are migrated to the new type: * `RecordingTarget::Streams { stream_types: StreamTypes, .. }` * `SyncedUserRecordingOptions.stream_types: StreamTypes` (default now `StreamTypes::VOICE` instead of a raw `STREAMTYPE_VOICE as u32` cast). * `Client::start_recording_streams(stream_types: impl Into, ..)` * `RecordingSession::start_streams(stream_types: impl Into, ..)` * `SyncedUserRecordingOptions::with_stream_types(types: impl Into)` Internal boundaries (the `TeamTalkBackend` trait, FFI `acquire_user_audio_block` / `enable_audio_block_event`, `AudioBlockView.stream_types`, and low-level debug helpers) stay `u32` for now; the newtype converts at the public boundary via `into().raw()` so this PR is strictly additive at those layers and can be followed up with further type-safety passes later without touching FFI. Tests: * `crates/teamtalk/tests/stream_types_tests.rs` adds 9 new integration tests: - Constants match the FFI enum values bit-for-bit. - `empty()` is zero and equals `Default::default()`. - `from_raw` round-trips arbitrary 32-bit patterns. - `|`, `|=`, `&`, `&=`, `!` behave as expected. - `contains_any` vs `contains_all` semantics. - `impl Into` accepts both raw `u32` and typed combinations. - `CLASSROOM_ALL` composition (voice + video_capture + mediafile_audio + mediafile_video + desktop + channel_msg = 95; desktop_input intentionally excluded). * `tests/recording_synced_tests.rs` is updated to compare the field as a `StreamTypes` and also asserts the raw value. Local verification: * cargo fmt --all clean. * cargo clippy --workspace --all-targets --all-features -- -D warnings clean. * cargo test --workspace --all-features — all tests pass, including the 9 new stream_types integration tests. --- .../teamtalk/src/client/recording/options.rs | 6 +- crates/teamtalk/src/client/recording/raw.rs | 16 +- .../teamtalk/src/client/recording/session.rs | 9 +- .../src/client/recording/synced/session.rs | 28 ++- crates/teamtalk/src/types/base.rs | 130 ++++++++++++++ .../teamtalk/tests/recording_synced_tests.rs | 6 +- crates/teamtalk/tests/stream_types_tests.rs | 162 ++++++++++++++++++ 7 files changed, 337 insertions(+), 20 deletions(-) create mode 100644 crates/teamtalk/tests/stream_types_tests.rs diff --git a/crates/teamtalk/src/client/recording/options.rs b/crates/teamtalk/src/client/recording/options.rs index 351c705..67a1945 100644 --- a/crates/teamtalk/src/client/recording/options.rs +++ b/crates/teamtalk/src/client/recording/options.rs @@ -1,4 +1,4 @@ -use crate::types::{AudioCodec, ChannelId}; +use crate::types::{AudioCodec, ChannelId, StreamTypes}; use std::time::Duration; use teamtalk_sys as ffi; @@ -19,7 +19,9 @@ pub enum RecordingTarget { CurrentChannel, /// Record muxed streams using a specific codec. Streams { - stream_types: u32, + /// Mask of stream kinds to include in the recording. Use + /// [`StreamTypes::VOICE`], [`StreamTypes::MEDIAFILE`], etc. + stream_types: StreamTypes, codec: AudioCodec, }, /// Record muxed audio using a specific codec. diff --git a/crates/teamtalk/src/client/recording/raw.rs b/crates/teamtalk/src/client/recording/raw.rs index f11a919..4f64484 100644 --- a/crates/teamtalk/src/client/recording/raw.rs +++ b/crates/teamtalk/src/client/recording/raw.rs @@ -1,6 +1,6 @@ use super::super::Client; use crate::events::{Error, Result}; -use crate::types::{AudioCodec, ChannelId}; +use crate::types::{AudioCodec, ChannelId, StreamTypes}; use teamtalk_sys as ffi; impl Client { @@ -29,16 +29,24 @@ impl Client { } /// Starts recording a set of stream types. + /// + /// `stream_types` accepts both a raw `u32` bitmask and any + /// [`StreamTypes`] combination via the `Into` bound. #[must_use] pub fn start_recording_streams( &self, - stream_types: u32, + stream_types: impl Into, codec: &AudioCodec, file_path: &str, format: ffi::AudioFileFormat, ) -> bool { - self.backend() - .start_recording_streams(self.ptr.0, stream_types, codec, file_path, format) + self.backend().start_recording_streams( + self.ptr.0, + stream_types.into().raw(), + codec, + file_path, + format, + ) } /// Stops recording a muxed audio file. diff --git a/crates/teamtalk/src/client/recording/session.rs b/crates/teamtalk/src/client/recording/session.rs index 98ec599..6192951 100644 --- a/crates/teamtalk/src/client/recording/session.rs +++ b/crates/teamtalk/src/client/recording/session.rs @@ -2,7 +2,7 @@ use super::super::Client; use super::options::{RecordingOptions, RecordingTarget, segment_path}; use crate::client::Message; use crate::events::{Error, Event, Result}; -use crate::types::{AudioCodec, ChannelId}; +use crate::types::{AudioCodec, ChannelId, StreamTypes}; use std::fs; use std::time::Instant; @@ -44,16 +44,19 @@ impl<'a> RecordingSession<'a> { } /// Starts a managed recording session for muxed streams. + /// + /// `stream_types` accepts both a raw `u32` bitmask and any + /// [`StreamTypes`] combination via the `Into` bound. pub fn start_streams( client: &'a Client, - stream_types: u32, + stream_types: impl Into, codec: AudioCodec, options: RecordingOptions, ) -> Result { let mut session = Self { client, target: RecordingTarget::Streams { - stream_types, + stream_types: stream_types.into(), codec, }, options, diff --git a/crates/teamtalk/src/client/recording/synced/session.rs b/crates/teamtalk/src/client/recording/synced/session.rs index 4d873c9..117ab6f 100644 --- a/crates/teamtalk/src/client/recording/synced/session.rs +++ b/crates/teamtalk/src/client/recording/synced/session.rs @@ -1,9 +1,10 @@ use super::writer::{AudioBlockGuard, UserTrack}; use super::{ Arc, AudioBlockView, Client, Duration, Error, Event, HashMap, Instant, Message, - RecordingSampleFormat, Result, UnpoisonedMutex, User, UserId, ffi, fs, is_synced_bus_event, + RecordingSampleFormat, Result, UnpoisonedMutex, User, UserId, fs, is_synced_bus_event, should_warn_missing_audio_subscriptions, synced_audio_subscription_mask, }; +use crate::types::StreamTypes; #[non_exhaustive] #[derive(Clone, Debug)] @@ -19,7 +20,9 @@ pub struct SyncedUserRecordingOptions { pub folder: String, pub file_vars: String, pub format: RecordingSampleFormat, - pub stream_types: u32, + /// Mask of stream kinds to capture. Defaults to + /// [`StreamTypes::VOICE`]. + pub stream_types: StreamTypes, pub tick_interval: Duration, pub subscribe_audio: bool, pub default_sample_rate: Option, @@ -33,7 +36,7 @@ impl SyncedUserRecordingOptions { folder: folder.into(), file_vars: "user-%user_id%-%username%".to_string(), format: RecordingSampleFormat::PcmS16Le, - stream_types: ffi::StreamType::STREAMTYPE_VOICE as u32, + stream_types: StreamTypes::VOICE, tick_interval: Duration::from_millis(250), subscribe_audio: true, default_sample_rate: None, @@ -54,9 +57,13 @@ impl SyncedUserRecordingOptions { self } + /// Sets the mask of stream kinds to capture. + /// + /// Accepts both a raw `u32` bitmask and any [`StreamTypes`] + /// combination via the `Into` bound. #[must_use] - pub fn with_stream_types(mut self, types: u32) -> Self { - self.stream_types = types; + pub fn with_stream_types(mut self, types: impl Into) -> Self { + self.stream_types = types.into(); self } @@ -205,7 +212,7 @@ impl SyncedUserRecordingSession { if self.options.subscribe_audio { let _ = client.subscribe(user_id, synced_audio_subscription_mask()); } - let _ = client.enable_audio_block_event(user_id, self.options.stream_types, true); + let _ = client.enable_audio_block_event(user_id, self.options.stream_types.raw(), true); if let Err(err) = self.drain_pending_blocks(client, user_id) { self.stop_user(client, user_id); @@ -215,7 +222,7 @@ impl SyncedUserRecordingSession { } fn stop_user(&mut self, client: &Client, user_id: UserId) { - let _ = client.enable_audio_block_event(user_id, self.options.stream_types, false); + let _ = client.enable_audio_block_event(user_id, self.options.stream_types.raw(), false); if self.options.subscribe_audio { let _ = client.unsubscribe(user_id, synced_audio_subscription_mask()); } @@ -230,7 +237,8 @@ impl SyncedUserRecordingSession { } fn on_audio_block(&mut self, client: &Client, user_id: UserId) -> Result<()> { - let Some(ptr) = client.acquire_user_audio_block(self.options.stream_types, user_id) else { + let Some(ptr) = client.acquire_user_audio_block(self.options.stream_types.raw(), user_id) + else { return Ok(()); }; let guard = AudioBlockGuard::new(client, ptr); @@ -253,7 +261,9 @@ impl SyncedUserRecordingSession { } fn drain_pending_blocks(&mut self, client: &Client, user_id: UserId) -> Result<()> { - while let Some(ptr) = client.acquire_user_audio_block(self.options.stream_types, user_id) { + while let Some(ptr) = + client.acquire_user_audio_block(self.options.stream_types.raw(), user_id) + { let guard = AudioBlockGuard::new(client, ptr); let block = unsafe { &*guard.ptr() }; let Some(view) = AudioBlockView::from_block(block) else { diff --git a/crates/teamtalk/src/types/base.rs b/crates/teamtalk/src/types/base.rs index e2b1725..2f56450 100644 --- a/crates/teamtalk/src/types/base.rs +++ b/crates/teamtalk/src/types/base.rs @@ -379,3 +379,133 @@ impl std::ops::BitOrAssign for UserRights { self.0 |= rhs.0; } } + +/// Typed bit-mask of TeamTalk media stream kinds. +/// +/// Wraps the raw `u32` bitmask accepted by the TeamTalk SDK for +/// `StreamType_*` flags (voice, video capture, media file audio/video, +/// desktop/desktop-input, channel text, and local media playback). The +/// newtype exists so that callers cannot accidentally pass an +/// unrelated `u32` to stream-typed APIs, and so that combinations can +/// be expressed with the standard bit operators. +/// +/// Constants mirror the FFI `StreamType::STREAMTYPE_*` values so a +/// direct conversion to/from the raw bitmask is lossless. See +/// [`Self::raw`] / [`Self::from_raw`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct StreamTypes(pub(crate) u32); + +impl StreamTypes { + /// Empty stream-type mask (no streams selected). + pub const NONE: Self = Self(0); + /// Voice stream. + pub const VOICE: Self = Self(1); + /// Video capture stream. + pub const VIDEO_CAPTURE: Self = Self(2); + /// Media file audio stream. + pub const MEDIAFILE_AUDIO: Self = Self(4); + /// Media file video stream. + pub const MEDIAFILE_VIDEO: Self = Self(8); + /// Desktop sharing stream. + pub const DESKTOP: Self = Self(16); + /// Desktop input events stream. + pub const DESKTOP_INPUT: Self = Self(32); + /// Combined media file (audio + video). + pub const MEDIAFILE: Self = Self(12); + /// Channel text messages stream. + pub const CHANNEL_MSG: Self = Self(64); + /// Local media file playback audio stream. + pub const LOCAL_MEDIAPLAYBACK_AUDIO: Self = Self(128); + /// Classroom default: voice + media file + desktop + desktop input + /// (value `95` from TeamTalk). + pub const CLASSROOM_ALL: Self = Self(95); + + /// Creates an empty mask. + #[must_use] + pub const fn empty() -> Self { + Self::NONE + } + + /// Creates a mask from a raw bit pattern. + /// + /// Accepts any `u32`; bits outside the defined `STREAMTYPE_*` + /// values are preserved and round-trip through [`Self::raw`] so + /// callers reading a mask from an event can inspect it without + /// truncation. + #[must_use] + pub const fn from_raw(raw: u32) -> Self { + Self(raw) + } + + /// Returns the raw bit pattern accepted by the SDK. + #[must_use] + pub const fn raw(self) -> u32 { + self.0 + } + + /// Returns `true` if any of the bits in `other` are set in `self`. + #[must_use] + pub const fn contains_any(self, other: Self) -> bool { + (self.0 & other.0) != 0 + } + + /// Returns `true` if all bits in `other` are set in `self`. + #[must_use] + pub const fn contains_all(self, other: Self) -> bool { + (self.0 & other.0) == other.0 + } + + /// Returns `true` if the mask is empty. + #[must_use] + pub const fn is_empty(self) -> bool { + self.0 == 0 + } +} + +impl std::ops::BitOr for StreamTypes { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl std::ops::BitOrAssign for StreamTypes { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +impl std::ops::BitAnd for StreamTypes { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } +} + +impl std::ops::BitAndAssign for StreamTypes { + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } +} + +impl std::ops::Not for StreamTypes { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl From for StreamTypes { + fn from(raw: u32) -> Self { + Self(raw) + } +} + +impl From for u32 { + fn from(value: StreamTypes) -> Self { + value.0 + } +} diff --git a/crates/teamtalk/tests/recording_synced_tests.rs b/crates/teamtalk/tests/recording_synced_tests.rs index 91629d2..2222d64 100644 --- a/crates/teamtalk/tests/recording_synced_tests.rs +++ b/crates/teamtalk/tests/recording_synced_tests.rs @@ -1,12 +1,13 @@ use teamtalk::RecordingSampleFormat; use teamtalk::client::recording::{SilencePolicy, SyncedUserRecordingOptions}; +use teamtalk::types::StreamTypes; #[test] fn synced_user_recording_options_builder_sets_fields() { let options = SyncedUserRecordingOptions::new("out") .with_format(RecordingSampleFormat::PcmS16Le) .with_file_vars("%username%") - .with_stream_types(123) + .with_stream_types(123u32) .with_tick_interval(std::time::Duration::from_millis(150)) .with_default_audio_format(48_000, 2) .with_subscribe_audio(false) @@ -14,7 +15,8 @@ fn synced_user_recording_options_builder_sets_fields() { assert_eq!(options.folder, "out"); assert_eq!(options.file_vars, "%username%".to_string()); - assert_eq!(options.stream_types, 123); + assert_eq!(options.stream_types, StreamTypes::from_raw(123)); + assert_eq!(options.stream_types.raw(), 123); assert_eq!(options.tick_interval, std::time::Duration::from_millis(150)); assert_eq!(options.default_sample_rate, Some(48_000)); assert_eq!(options.default_channels, Some(2)); diff --git a/crates/teamtalk/tests/stream_types_tests.rs b/crates/teamtalk/tests/stream_types_tests.rs new file mode 100644 index 0000000..03fe382 --- /dev/null +++ b/crates/teamtalk/tests/stream_types_tests.rs @@ -0,0 +1,162 @@ +//! Integration tests for the `StreamTypes` newtype. +//! +//! The newtype wraps a `u32` bitmask and must: +//! +//! * Round-trip through `from_raw` / `raw` for any input pattern. +//! * Expose the same constants as the underlying +//! `teamtalk_sys::StreamType::STREAMTYPE_*` enum so callers can +//! migrate without behaviour changes. +//! * Support the usual bitwise combinators (`|`, `|=`, `&`, `&=`, +//! `!`) and `contains_*` helpers. +//! * Convert to / from `u32` through `From` so existing callers can +//! pass either a raw bitmask or a typed value to APIs that take +//! `impl Into`. + +use teamtalk::types::StreamTypes; +use teamtalk_sys as ffi; + +#[test] +fn constants_match_ffi_stream_type_values() { + assert_eq!( + StreamTypes::NONE.raw(), + ffi::StreamType::STREAMTYPE_NONE as u32 + ); + assert_eq!( + StreamTypes::VOICE.raw(), + ffi::StreamType::STREAMTYPE_VOICE as u32 + ); + assert_eq!( + StreamTypes::VIDEO_CAPTURE.raw(), + ffi::StreamType::STREAMTYPE_VIDEOCAPTURE as u32 + ); + assert_eq!( + StreamTypes::MEDIAFILE_AUDIO.raw(), + ffi::StreamType::STREAMTYPE_MEDIAFILE_AUDIO as u32 + ); + assert_eq!( + StreamTypes::MEDIAFILE_VIDEO.raw(), + ffi::StreamType::STREAMTYPE_MEDIAFILE_VIDEO as u32 + ); + assert_eq!( + StreamTypes::DESKTOP.raw(), + ffi::StreamType::STREAMTYPE_DESKTOP as u32 + ); + assert_eq!( + StreamTypes::DESKTOP_INPUT.raw(), + ffi::StreamType::STREAMTYPE_DESKTOPINPUT as u32 + ); + assert_eq!( + StreamTypes::MEDIAFILE.raw(), + ffi::StreamType::STREAMTYPE_MEDIAFILE as u32 + ); + assert_eq!( + StreamTypes::CHANNEL_MSG.raw(), + ffi::StreamType::STREAMTYPE_CHANNELMSG as u32 + ); + assert_eq!( + StreamTypes::LOCAL_MEDIAPLAYBACK_AUDIO.raw(), + ffi::StreamType::STREAMTYPE_LOCALMEDIAPLAYBACK_AUDIO as u32 + ); + assert_eq!( + StreamTypes::CLASSROOM_ALL.raw(), + ffi::StreamType::STREAMTYPE_CLASSROOM_ALL as u32 + ); +} + +#[test] +fn empty_is_zero_and_default() { + assert_eq!(StreamTypes::empty().raw(), 0); + assert!(StreamTypes::empty().is_empty()); + assert_eq!(StreamTypes::default(), StreamTypes::empty()); + assert!(!StreamTypes::VOICE.is_empty()); +} + +#[test] +fn from_raw_round_trips_arbitrary_bits() { + // The newtype must preserve unknown bits so callers reading a + // bitmask from an event do not silently lose information. + let weird = 0xABCD_1234_u32; + let wrapped = StreamTypes::from_raw(weird); + assert_eq!(wrapped.raw(), weird); + let u: u32 = wrapped.into(); + assert_eq!(u, weird); + let back: StreamTypes = weird.into(); + assert_eq!(back, wrapped); +} + +#[test] +fn bitor_combines_and_is_commutative() { + let a = StreamTypes::VOICE | StreamTypes::MEDIAFILE_AUDIO; + let b = StreamTypes::MEDIAFILE_AUDIO | StreamTypes::VOICE; + assert_eq!(a, b); + assert_eq!(a.raw(), 1 | 4); + assert!(a.contains_all(StreamTypes::VOICE)); + assert!(a.contains_all(StreamTypes::MEDIAFILE_AUDIO)); + assert!(!a.contains_any(StreamTypes::DESKTOP)); +} + +#[test] +fn bitor_assign_updates_in_place() { + let mut mask = StreamTypes::VOICE; + mask |= StreamTypes::DESKTOP; + assert_eq!(mask, StreamTypes::VOICE | StreamTypes::DESKTOP); + assert!(mask.contains_all(StreamTypes::VOICE)); + assert!(mask.contains_all(StreamTypes::DESKTOP)); +} + +#[test] +fn bitand_and_not_mask_correctly() { + let both = StreamTypes::VOICE | StreamTypes::DESKTOP; + let only_voice = both & StreamTypes::VOICE; + assert_eq!(only_voice, StreamTypes::VOICE); + + let mut mask = both; + mask &= StreamTypes::DESKTOP; + assert_eq!(mask, StreamTypes::DESKTOP); + + // Dropping voice from CLASSROOM_ALL should leave exactly the + // other bits (media + desktop + desktop_input = 94). + let classroom_without_voice = StreamTypes::CLASSROOM_ALL & !StreamTypes::VOICE; + assert_eq!(classroom_without_voice.raw(), 95 & !1); +} + +#[test] +fn contains_any_vs_all() { + let mask = StreamTypes::VOICE | StreamTypes::DESKTOP; + let voice_or_video = StreamTypes::VOICE | StreamTypes::VIDEO_CAPTURE; + assert!(mask.contains_any(voice_or_video)); + assert!(!mask.contains_all(voice_or_video)); + assert!(mask.contains_all(StreamTypes::VOICE)); + assert!(mask.contains_all(StreamTypes::DESKTOP)); +} + +#[test] +fn u32_and_stream_types_are_interchangeable_via_into() { + // The public recording APIs accept `impl Into`; + // verify both a raw u32 and a typed StreamTypes round-trip to + // the same wrapped value. + fn accept(types: impl Into) -> StreamTypes { + types.into() + } + assert_eq!(accept(1u32), StreamTypes::VOICE); + assert_eq!(accept(StreamTypes::VOICE), StreamTypes::VOICE); + assert_eq!( + accept(StreamTypes::VOICE | StreamTypes::DESKTOP).raw(), + 1 | 16, + ); +} + +#[test] +fn classroom_all_matches_documented_value() { + // The SDK documents CLASSROOM_ALL as 95. Bit composition: + // voice(1) + video_capture(2) + mediafile_audio(4) + + // mediafile_video(8) + desktop(16) + channel_msg(64) = 95. + // Note that desktop_input(32) is NOT included; CHANNEL_MSG is. + assert_eq!(StreamTypes::CLASSROOM_ALL.raw(), 95); + assert!(StreamTypes::CLASSROOM_ALL.contains_all(StreamTypes::VOICE)); + assert!(StreamTypes::CLASSROOM_ALL.contains_all(StreamTypes::VIDEO_CAPTURE)); + assert!(StreamTypes::CLASSROOM_ALL.contains_all(StreamTypes::MEDIAFILE)); + assert!(StreamTypes::CLASSROOM_ALL.contains_all(StreamTypes::DESKTOP)); + assert!(StreamTypes::CLASSROOM_ALL.contains_all(StreamTypes::CHANNEL_MSG)); + assert!(!StreamTypes::CLASSROOM_ALL.contains_any(StreamTypes::DESKTOP_INPUT)); +} From 19b99e3230d0df471730b9f7e8568f70510f5ef7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:49:41 +0000 Subject: [PATCH 2/2] docs(types): correct StreamTypes::CLASSROOM_ALL composition comment Devin Review pointed out that the previous doc comment on StreamTypes::CLASSROOM_ALL listed "voice + media file + desktop + desktop input" but the actual value 95 decomposes to voice(1) + video_capture(2) + mediafile_audio(4) + mediafile_video(8) + desktop(16) + channel_msg(64). DESKTOP_INPUT(32) is intentionally **not** part of this mask. The test `classroom_all_matches_documented_value` already pins down this composition, so only the doc comment needed fixing. Also mention the intentional absence of DESKTOP_INPUT so future readers do not try to add it. --- crates/teamtalk/src/types/base.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/teamtalk/src/types/base.rs b/crates/teamtalk/src/types/base.rs index 2f56450..317a25b 100644 --- a/crates/teamtalk/src/types/base.rs +++ b/crates/teamtalk/src/types/base.rs @@ -416,8 +416,11 @@ impl StreamTypes { pub const CHANNEL_MSG: Self = Self(64); /// Local media file playback audio stream. pub const LOCAL_MEDIAPLAYBACK_AUDIO: Self = Self(128); - /// Classroom default: voice + media file + desktop + desktop input - /// (value `95` from TeamTalk). + /// Classroom default mask (`95`): voice + video capture + media file + /// audio + media file video + desktop + channel msg. + /// + /// Note that `DESKTOP_INPUT` (bit `32`) is intentionally not part of + /// this composition. pub const CLASSROOM_ALL: Self = Self(95); /// Creates an empty mask.