EDA.Voice.Dave.Manager (EDA v0.5.0-beta.2)

Copy Markdown View Source

Coordinates the DAVE (Discord Audio Video E2EE) protocol.

This is a pure struct (not a GenServer) to avoid bottlenecks on the audio hot path (50 frames/sec). The NIF resource is thread-safe (Mutex on the Rust side).

When protocol_version is 0, all operations are passthrough (zero-cost).

Summary

Types

t()

The DAVE state of one voice connection.

Functions

Returns true if DAVE E2EE is active (version > 0 and NIF session available).

The protocol version currently in effect, including transitions executed after this copy of the manager was taken.

Records a frame that failed to decrypt, and recovers once too many fail in a row.

Decrypts a DAVE-encrypted frame.

Encrypts an Opus frame through DAVE E2EE.

Handles an MLS-related voice gateway event.

Builds an OP26 key package payload from the current MLS session.

Creates a new DAVE manager. Version 0 means passthrough (no E2EE).

Returns true when the MLS session is ready to encrypt media.

Returns true when after executed a transition that before had not, leaving DAVE in effect.

Types

t()

@type t() :: %EDA.Voice.Dave.Manager{
  channel_id: non_neg_integer(),
  decrypt_failures: non_neg_integer(),
  downgraded: boolean(),
  last_transition_id: non_neg_integer() | nil,
  mls_session: reference() | nil,
  pending_transitions: %{required(non_neg_integer()) => non_neg_integer()},
  protocol_version: non_neg_integer(),
  reinitializing: boolean(),
  transitions: non_neg_integer(),
  user_id: non_neg_integer(),
  version_ref: :atomics.atomics_ref() | nil
}

The DAVE state of one voice connection.

protocol_version is the version in effect. The playback process works on a copy of this struct taken when the connection became ready, so the version is also held in version_ref, an :atomics shared by every copy: a transition executed by the session reaches playback already under way. transitions counts executed transitions, decrypt_failures the frames that failed to decrypt in a row.

Functions

active?(manager)

@spec active?(t()) :: boolean()

Returns true if DAVE E2EE is active (version > 0 and NIF session available).

current_version(manager)

@spec current_version(t()) :: non_neg_integer()

The protocol version currently in effect, including transitions executed after this copy of the manager was taken.

decrypt_failed(manager)

@spec decrypt_failed(t()) :: {t(), [term()]}

Records a frame that failed to decrypt, and recovers once too many fail in a row.

A session that stops decrypting — the group state out of step with the other members — would otherwise stay broken until the bot leaves the channel. After 36 failures in a row, the last transition is reported as invalid (OP 31) and the session is re-initialised with a fresh key package, so Discord removes the bot from the group and adds it again. Failures while that is under way, or while a transition is pending, are expected and not counted.

Returns the updated manager and the payloads to send to the voice gateway.

decrypt_frame(manager, frame, sender_user_id)

@spec decrypt_frame(t(), binary(), non_neg_integer()) ::
  {:ok, binary(), t()} | {:error, atom()}

Decrypts a DAVE-encrypted frame.

In passthrough mode, returns the frame unchanged. Returns {:ok, decrypted_frame, updated_manager} or {:error, reason}. On an error, pass the manager to decrypt_failed/1.

encrypt_frame(manager, opus_frame)

@spec encrypt_frame(t(), binary()) :: {:ok, binary(), t()} | {:error, term(), t()}

Encrypts an Opus frame through DAVE E2EE.

In passthrough mode, returns the frame unchanged. Returns {:ok, encrypted_frame, updated_manager} on success. Returns {:error, reason, updated_manager} when an active DAVE session cannot encrypt media, so callers can stop playback instead of sending undecryptable raw Opus.

handle_mls_event(manager, opcode, data)

@spec handle_mls_event(t(), non_neg_integer(), map()) :: {t(), list()}

Handles an MLS-related voice gateway event.

Returns {updated_manager, reply_payloads} where reply_payloads is a list of payloads to send back to the voice gateway.

key_package_payload(manager)

@spec key_package_payload(t()) :: {:ok, term()} | :error

Builds an OP26 key package payload from the current MLS session.

Returns {:ok, payload} when available, otherwise :error.

new(protocol_version, user_id, channel_id)

Creates a new DAVE manager. Version 0 means passthrough (no E2EE).

ready?(manager)

@spec ready?(t()) :: boolean()

Returns true when the MLS session is ready to encrypt media.

transitioned_to_dave?(manager, after_)

@spec transitioned_to_dave?(t(), t()) :: boolean()

Returns true when after executed a transition that before had not, leaving DAVE in effect.

This is when VOICE_DAVE_READY is dispatched: a transition to protocol 0 is not one.