Skip to content

refactor(voice): Strict type checking in voice internals & DAVE Support (rec)#3159

Open
Paillat-dev wants to merge 36 commits into
masterfrom
fix/voice-rec-2
Open

refactor(voice): Strict type checking in voice internals & DAVE Support (rec)#3159
Paillat-dev wants to merge 36 commits into
masterfrom
fix/voice-rec-2

Conversation

@Paillat-dev

Copy link
Copy Markdown
Member

Summary

Information

  • This PR fixes an issue.
  • This PR adds something new (e.g. new method or parameters).
  • This PR is a breaking change (e.g. methods or parameters removed/renamed).
  • This PR is not a code change (e.g. documentation, README, typehinting,
    examples, ...).

Checklist

  • I have searched the open pull requests for duplicates.
  • If code changes were made then they have been tested.
    • I have updated the documentation to reflect the changes.
  • If type: ignore comments were used, a comment is also left explaining why.
  • I have updated the changelog to include these changes.
  • AI Usage has been disclosed.
    • If AI has been used, I understand fully what the code does

Discord enforces DAVE (E2E encryption) on all voice channels. py-cord
currently decodes Opus packets before stripping the DAVE application-layer
encryption, causing OpusError: corrupted stream and making voice reception
completely non-functional.

Changes:
- reader.py: Rewrite decrypt_rtp() to call dave.decrypt() BEFORE Opus
  decoding. Handles SSRC-to-user_id race condition by trying all known
  DAVE user IDs when the mapping is not yet populated, then caching the
  result. Falls back to OPUS_SILENCE on decrypt failure.
- opus.py: Remove erroneous dave.decrypt() call in _decode_packet() that
  was applied to already-decoded PCM data, corrupting the audio stream.
- router.py: Catch OpusError and AssertionError in _do_run() gracefully,
  emitting a single warning instead of crashing the router thread.

Fixes: #3139
@pycord-app

pycord-app Bot commented Mar 19, 2026

Copy link
Copy Markdown

Thanks for opening this pull request!
Please make sure you have read the Contributing Guidelines and Code of Conduct.

This pull request can be checked-out with:

git fetch origin pull/3159/head:pr-3159
git checkout pr-3159

This pull request can be installed with:

pip install git+https://github.com/Pycord-Development/pycord@refs/pull/3159/head

@Paillat-dev Paillat-dev added this to the Next minor rc1 milestone Mar 19, 2026
Comment thread discord/voice/client.py
Comment thread discord/voice/client.py Outdated
Comment thread discord/sinks/wave.py Outdated
Comment thread discord/voice/receive/reader.py Outdated
Comment thread discord/voice/receive/router.py

@Paillat-dev Paillat-dev left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changeling

Signed-off-by: Paillat <paillat@pycord.dev>
@cinderblock

This comment has been minimized.

Deniwn22

This comment was marked as off-topic.

@digitaldevotion

Copy link
Copy Markdown

FWIW, I've been running this PR for a few weeks now on both Discord iOS (cellular and WiFi) and macOS clients, and it's been rock solid. No issues.

Setup:

  • Python 3.14.4, macOS 26 (Tahoe) and 27.0 (Golden Gate), both arm64
  • Custom Sink subclass (discord.sinks.Sink subclass overriding write()),
    is_opus()=False, consuming VoiceData.pcm (48kHz stereo s16le) with
    per-user buffering + webrtcvad end-of-utterance detection feeding WAV to STT

General testing has been multi-user voice channels for up to 30 minutes with bot joining/leaving/reattaching mid-session.

Great job!

Comment on lines +182 to +188
if self.after and self.args:
try:
self.after(self.error)
r = self.after(self.sink, *self.args)
if isinstance(r, Coroutine):
self.client.loop.create_task(
r
) # pyright: ignore[reportUnknownArgumentType]

@tylanphear tylanphear Jul 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an issue I observed using this API with a callback with no args is that the callback was silently skipped on stop_recording(). For example:

async def callback(sink):
    pass

vc = await ctx.author.voice.channel.connect()
vc.start_recording(discord.sinks.WaveSink(), callback)
vc.stop_recording()

callback will not fire when vc.stop_recording() is called, because self.args is empty. Pretty sure it's this line of code causing the issue (should be a check for self.args is not None maybe?)

Could be holding this wrong; I've never used Pycord before, and the original code which triggered this issue was authored by AI. Although I did trace the issue and write this comment/repro with my human brain.

self.keep_alive.stop()

if self.after:
if self.after and self.args:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if self.after and self.args:
if self.after is not None and self.args is not None:

When passing a callback with no extra args, self.args is an empty tuple (), which is falsy in python, causing the issue described by @tylanphear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file feature Implements a feature help wanted Extra attention is needed hold: testing This pull request requires further testing PA: All Contributors pending priority: medium Medium Priority status: in progress Work in Progess voice Related to the voice chat feature

Projects

Status: Todo