Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions crates/teamtalk/src/client/recording/options.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::types::{AudioCodec, ChannelId};
use crate::types::{AudioCodec, ChannelId, StreamTypes};
use std::time::Duration;
use teamtalk_sys as ffi;

Expand All @@ -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.
Expand Down
16 changes: 12 additions & 4 deletions crates/teamtalk/src/client/recording/raw.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<StreamTypes>` bound.
#[must_use]
pub fn start_recording_streams(
&self,
stream_types: u32,
stream_types: impl Into<StreamTypes>,
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.
Expand Down
9 changes: 6 additions & 3 deletions crates/teamtalk/src/client/recording/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<StreamTypes>` bound.
pub fn start_streams(
client: &'a Client,
stream_types: u32,
stream_types: impl Into<StreamTypes>,
codec: AudioCodec,
options: RecordingOptions,
) -> Result<Self> {
let mut session = Self {
client,
target: RecordingTarget::Streams {
stream_types,
stream_types: stream_types.into(),
codec,
},
options,
Expand Down
28 changes: 19 additions & 9 deletions crates/teamtalk/src/client/recording/synced/session.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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<i32>,
Expand All @@ -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,
Expand All @@ -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<StreamTypes>` 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<StreamTypes>) -> Self {
self.stream_types = types.into();
self
}

Expand Down Expand Up @@ -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);
Expand All @@ -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());
}
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
133 changes: 133 additions & 0 deletions crates/teamtalk/src/types/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,136 @@ 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 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.
#[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<u32> for StreamTypes {
fn from(raw: u32) -> Self {
Self(raw)
}
}

impl From<StreamTypes> for u32 {
fn from(value: StreamTypes) -> Self {
value.0
}
}
6 changes: 4 additions & 2 deletions crates/teamtalk/tests/recording_synced_tests.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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)
.with_silence_policy(SilencePolicy::OnlyWhileConnected);

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));
Expand Down
Loading
Loading