Implement DAVE protocol integration paths, add unit/feature tests, and address reviewer feedback#4
Merged
Merged
Conversation
Agent-Logs-Url: https://github.com/discord-php/DiscordPHP-Voice/sessions/2863a724-b762-4b24-8065-6426b8d698d5 Co-authored-by: alexandre433 <10622522+alexandre433@users.noreply.github.com>
Agent-Logs-Url: https://github.com/discord-php/DiscordPHP-Voice/sessions/2863a724-b762-4b24-8065-6426b8d698d5 Co-authored-by: alexandre433 <10622522+alexandre433@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
alexandre433
April 7, 2026 12:49
View session
alexandre433
requested changes
Apr 7, 2026
alexandre433
left a comment
Member
There was a problem hiding this comment.
@copilot Also unit & feature tests are missing.
We haven't implemented any so far. but you can implement them.
Agent-Logs-Url: https://github.com/discord-php/DiscordPHP-Voice/sessions/3d14b61f-9945-4853-9a5d-8a78bf0d7b97 Co-authored-by: alexandre433 <10622522+alexandre433@users.noreply.github.com>
…ixes Agent-Logs-Url: https://github.com/discord-php/DiscordPHP-Voice/sessions/3d14b61f-9945-4853-9a5d-8a78bf0d7b97 Co-authored-by: alexandre433 <10622522+alexandre433@users.noreply.github.com>
Contributor
Author
Added in commit c4fb678. I implemented unit and feature tests, including runtime callback behavior, VoiceClient DAVE frame encryption/decryption behavior, and WS MLS proposals handling behavior. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Copilot
AI
changed the title
Add foundational DAVE protocol gateway integration and frame hooks
Implement DAVE protocol integration paths and add unit/feature tests
Apr 7, 2026
- Introduced SessionHandle class for managing DAVE sessions. - Expanded Runtime class with new methods for session lifecycle management, including creation, initialization, and resetting. - Added support for encryptor and decryptor management in the State class, allowing for dynamic handling of encryption states. - Updated VoiceClient to utilize the new DAVE state management for encrypting and decrypting frames. - Implemented tests for session lifecycle, key package generation, and encryptor/decryptor functionality. - Refactored existing tests to reset the DAVE runtime state appropriately. - Added WSSequenceAckTest to ensure correct sequence acknowledgment handling in heartbeat and resume scenarios.
- Register Op 23 (VOICE_DAVE_TRANSITION_READY), 26 (VOICE_DAVE_MLS_KEY_PACKAGE), 28 (VOICE_DAVE_MLS_COMMIT_WELCOME), and 31 (VOICE_DAVE_MLS_INVALID_COMMIT_WELCOME) in VOICE_OP_HANDLERS so Discord packets are no longer silently dropped. - handleDaveTransitionReady (Op 23): applies the self encryptor for the pending protocol version then calls daveState->executeTransition() to finalize state. - handleDaveMlsKeyPackage (Op 26): validates the frame is a BinaryFrame and returns passively; the server owns proposal aggregation so no outbound action is required. - handleDaveMlsCommitWelcome (Op 28): first tries processCommit; on success calls prepareDaveMediaTransition + sendDaveTransitionReady. Falls back to processWelcome on commit failure; on success takes the same path. If both fail, delegates to handleInvalidDaveTransition for recovery. - handleDaveMlsInvalidCommitWelcome (Op 31): logs a warning and calls handleInvalidDaveTransition(0, true) to send INVALID_COMMIT_WELCOME and re-enter the MLS handshake via initializeDaveRuntimeState + sendDaveKeyPackage. Adds WSDaveStubHandlersTest covering all four handlers (5 test cases). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add WSDaveMlsAnnounceCommitTest: 7 tests for announce-commit-transition handler - Add WSDaveMlsWelcomeTest: 8 tests for MLS welcome handler - Add WSDaveStubHandlersTest: 5 tests for newly-implemented stub handlers - Add BinaryFrameTest, StateTest, RuntimeTest, PacketTest, BufferTest - Add OggPageTest, OggStreamTest, OpusHeadTest, OpusTagsTest - Add ReceiveStreamTest, RecieveStreamTest, ProcessWrapperTest - Update test infrastructure: Pest.php, phpunit.xml, bootstrap, fixtures - Update CI workflow and composer scripts for parallel/coverage runs - Fix ByteBuffer, Helpers/Buffer, ProcessAbstract, RecieveStream Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…verage - fix: RTP extension stripping only on libsodium path ($daveApplied flag) - fix: $regenerateKeyPackage param now gates key-package re-send - fix: O(1) SSRC→userId map in VoiceClient (ssrcToUserId array) - feat: max_dave_protocol_version in RESUME payload; handleResumed re-parses version - feat: error logging on all configure*() bool returns in WS.php - feat: BinaryFrame fromServerPayload/toServerPayload canonical aliases (deprecate fromPayload/toPayload) - test: WSDaveErrorRecoveryTest (8 tests) - test: WSDaveReconnectTest (10 tests) - test: WSDaveRemoteUsersTest (10 tests) - test: WSDaveSessionInitTest (15 tests) - test: WSDaveTransitionsTest (11 tests) - test: SSRCResolutionTest (4 tests) - test: VoiceClientDaveFrameTest extended (2 new scenarios) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SCORDPHP_DAVE_LIBRARY is set Runtime::reset() only clears the singleton; the next isAvailable() call reloads from the env var, so the 'unavailable' branch was unreachable when the env var was present. Fix: temporarily unset the env var and reset the singleton inside the test, restoring both in a finally block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elcome; fix FFI-safe test mocking - Runtime::configureCallbacks() now accepts createSessionCallback and availabilityOverride parameters, giving tests a safe injection point without needing real libdave - State::recognizedUsers() now always returns list<string> (fixes PHP numeric-string array key coercion that caused strlen() TypeError in makeStringPointerArray) - WSDaveSessionInitTest: use createSessionCallback to simulate session creation failure; use availabilityOverride to test the no-libdave resolveDaveProtocolVersion path; update 'resets existing session' assertion to work with or without libdave - WSDaveTransitionsTest: use availabilityOverride instead of putenv/reset for 'unavailable' tests (dlopen SONAME caching prevents putenv from unloading libdave) - StateTest: update recognizedUsers() expectations from int to string (correct per docblock) - All 156 tests pass with DISCORDPHP_DAVE_LIBRARY set (0 skipped) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ndException if missing Discord mandated the DAVE E2EE protocol for all voice/video connections since March 1st, 2026. Previously, the client silently fell back to protocol version 0 (passthrough) when libdave was not available, which Discord now rejects. Changes: - Add LibDaveNotFoundException with a static fromRuntimeError() factory that builds a descriptive message including the Discord announcement URL, installation instructions, and the FFI load error (if any). - Throw LibDaveNotFoundException::fromRuntimeError() as the very first statement in Manager::__construct() and WS::__construct() so no connection attempt is ever made without a working libdave. - Remove the now-dead isAvailable() branch from WS::resolveDaveProtocolVersion(). - Add feature tests for both enforcement points (WSDaveRequirementTest, ManagerDaveRequirementTest) and update existing DAVE tests that were using availabilityOverride: false on paths that are now unreachable. Ref: https://discord.com/developers/docs/change-log#future-deprecation-and-discontinuation-of-non-e2ee-voice Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ons, and test patterns - Fix critical DAVE fallback mismatch: instructions previously said the client falls back to protocol v0 when libdave is absent; in reality Manager and WS both throw LibDaveNotFoundException at construction. - Add architecture entries for: Dave handle types (SessionHandle, EncryptorHandle, DecryptorHandle, KeyRatchetHandle), BinaryFrame, Dave\State fields, OggPage/OggStream/OpusHead/OpusTags, ByteBuffer (active vs legacy Helpers duplicate), Processes subsystem (Ffmpeg, DCA, OpusFfi, OpusDecoderInterface), Client\User value object, the full Exceptions hierarchy (Channels/ and Libraries/), VoicePayload, OpEnum, and UserConnected. - Add convention: Exceptions\Libraries\ use bare \Exception subclasses; runtime-context messages go in a fromRuntimeError() static factory. - Add '## Test patterns' section documenting: Feature vs Unit directory split, inline helpers convention, reflection-based construction, PHPUnit getMockBuilder mocking, payload-capture pattern, the three DAVE stubbing strategies (availabilityOverride false/true/real), mandatory afterEach(Runtime::reset()), markTestSkipped convention, and BinaryFrame construction for gateway message tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap all caller-supplied strings with escapeshellarg() in Ffmpeg::encode(), Ffmpeg::decode(), and ProcessAbstract::checkForExecutable() to prevent shell metacharacter injection via filenames and preArgs. - Fix VoiceClient::setData() ignoring its $data parameter. The method checked $this->data (never updated) for the boot condition, so WS::make() never fired through Manager::joinChannel(). Added $this->data = array_merge($this->data, $data) as the first statement. - Fix Packet::decrypt() finally block overriding the catch return. A return inside finally unconditionally overrides return false in catch, causing authentication failures to surface as null instead of false. Restructured so catch sets $resultMessage = false, finally only assigns $this->decryptedAudio, and a single return follows the block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…crypt error handling - ProcessWrapperTest: assert shell metacharacters in filenames passed to Ffmpeg::encode() are escaped by escapeshellarg() and never appear raw in the constructed command string. - VoiceClientSetDataTest (new): three tests covering setData() — data is merged into $this->data; boot() is not called when required keys are missing; boot() is called exactly once and endpoint/dnsConfig are set correctly when all four keys are present. - PacketTest: assert decrypt() returns false (not null) when libsodium throws a SodiumException, verified by injecting a wrong-length key via reflection to force the catch branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ty error VoiceClient::$readOpusTimer was declared as a typed nullable property without a default value. reset() accesses it unconditionally, so calling stopRecording() before any audio playback threw: Typed property Discord\Voice\VoiceClient::$readOpusTimer must not be accessed before initialization Add = null to the declaration to match the existing $heartbeat pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com>
- UDP::handleMessages now drops packets with RTP version != 2 or with payload type in 72-95 (RTCP PT 200-223 per RFC 5761) before constructing a Packet — prevents RTCP Sender/Receiver Reports from being mis-decrypted and logged as 'No audio data' - Packet::unpack format string keys corrected from 'first'/'second' to 'version_and_flags'/'payload_type' so $versionPlusFlags and $payloadType are actually populated - Both properties typed as ?int (unpack 'C' returns int, not string) and default-initialised to null - Tests: 5 new UDP RTCP-filter tests, 2 new Packet unpack-key tests (552 total, up from 545) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ord() - VoiceClient::handleAudioData(): add writePCM() call in the FFI decoder path so channel-pcm events fire with decoded audio (was the root cause of all recordings being 0 bytes) - VoiceClient::handleAudioData(): add else branch for the no-FFI-decoder case that writes raw Opus frames via writeOpus() so channel-opus still fires even without libopus installed - VoiceClient::record(): auto-initialize OpusFfi when opusdecoder is null and libopus is available, removing the requirement for callers to call setDecoder() before recording works out of the box - Add four new tests covering the FFI PCM path, the non-FFI Opus path, record() not overriding a pre-set decoder, and record() auto-init Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When audio frames arrive before the speaking event (op 5), ssrcToUserId has no entry for the SSRC yet, so resolveDaveRemoteUserId returns null. MediaCryptoService::decrypt previously skipped the per-user decryptor path entirely and fell back to decryptMediaFrame which always fails. The same race affects the userId-known case: user in channel but their decryptor is not yet set up (MLS group not ready). Fix: replace the strict guard with a fan-out - resolve the target decryptor when possible, otherwise iterate all known decryptors. AEAD verification failures are non-mutating so trying the wrong handle is safe. Stop on first string result or on explicit false (non-DAVE frame). The existing decryptMediaFrame fallback still runs if every handle returns null. Runtime::configureCallbacks gains decryptWithDecryptorCallback parameter so unit tests can stub the per-handle decrypt path without real libdave. State::getAllDecryptors() exposes the decryptors map for iteration. 4 new tests cover the new paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uzzing OpusFfi::decode() was creating and destroying an Opus decoder on every single frame. Opus is a stateful codec — destroying the decoder between frames discards pitch predictors, SILK/CELT context, and PLC state, causing audible buzzing and static artifacts at each 20ms boundary. Fix: lazily create one decoder handle per (channels, rate) pair inside OpusFfi and reuse it across calls. Add __destruct to clean up handles. Also fix the over-allocated PCM buffer (frameSize*channels*2 → frameSize*channels). VoiceClient now stores per-SSRC OpusFfi instances in $ffiDecoders[] so different speakers never share codec state. The single $opusdecoder property is preserved as the feature-flag / non-FFI fallback path. stopRecording() clears ffiDecoders alongside voiceDecoders. Tests: +2 per-SSRC isolation tests (324 total, 0 failures) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CHANGELOG: add unreleased entries for RecordingFormat enum/WavWriter,
record() format param, OpusFfi persistent decoder fix, per-SSRC
ffiDecoders isolation, readOpusTimer null init; update test count to 581
- README: fix channel-pcm callback signature (string $pcm, VoiceClient $vc),
add WAV auto-recording example, add RecordingFormat format note
- docs/AUDIO_PIPELINE: update inbound diagram with per-SSRC OpusFfi node
fix(examples): replace deprecated discord on('ready') with on('init')
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Buffer, RecieveStream, and VoiceClient classes
CLAUDE.md gives future Claude Code sessions the project entry-point and architecture guidance distilled from .github/copilot-instructions.md and docs/. .agents/, skills-lock.json, and todos.db are local Claude/skill state and should not be tracked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, surface Ogg load errors - MediaCryptoService::decrypt: revert e06cfe3's "pass through ciphertext as Opus on auth failure" — DAVE is E2EE; on auth fail the spec is to drop the frame, not emit decryptable garbage to the upstream codec. Restores string|false return for the auth-fail path. - Helpers/Buffer::read: reject immediately with "Buffer closed" when the buffer is already closed and not enough bytes are buffered, instead of parking a Deferred that will never resolve. d30113e moved EOF detection from immediate-timeout to close()-driven, but read() never honoured the close flag, so post-close reads hung indefinitely. Three Ogg tests fixed by closing the bounded fixture buffer to signal EOF. - VoiceClient::playOggStream: attach a rejection handler to OggStream::fromBuffer so a buffer-close mid-handshake (e.g. ffmpeg exits before producing a valid Ogg header) reaches the playback Deferred instead of leaking as an unhandled-promise-rejection notice. - Ffmpeg::encode: keep "file" in -protocol_whitelist. SSRF defence is enforced one layer up in playFile() (URL scheme allowlist + private/ reserved/loopback IP block); without "file" in the whitelist, every local path passed to playFile fails with "Protocol 'file' not on whitelist", which makes local audio playback impossible. Rewrote the PlayFileSecurityTest aspirational-policy assertion to document the intentional trade-off rather than failing on it. - Tests for BufferTest/Encoder/ProcessWrapper updated to expect the new reject-on-close behaviour and the corrected ffmpeg flag set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion suites Unit coverage additions: - ByteBufferRoundtripTest: writeIntN/readIntN round-trips, getLastEmptyPosition, array-access trait helpers (writeUInt32BE/writeUInt64LE/writeShort/writeRaw, offsetGet/Set/Exists/Unset). Bumps ByteBuffer/Buffer 39%→88%, trait 30%→~85%. - SocketFactoryTest: covers the constructor's resolver-or-WS guard. - VoiceClientChannelOpsTest: switchChannel happy/error paths, disconnect, mainSend payload forwarding via a Discord double. - VoiceClientStateTest: setBitrate/setVolume/setAudioApplication validation and "while playing" guards, close/pause/unpause/stop guard rails, isReady, getReceiveStream / getRecieveStream alias, handleVoiceStateUpdate ignore branches, playPcmFile rejects on missing path. - VoiceClientPlayPathsTest: playOggStream / playDCAStream / playRawStream / playFile / playPcmFile early-reject paths including the SSRF guards (scheme allowlist, private/reserved IPs, localhost). - VoiceClientPcmRecordingTest: handleAudioData unknown-SSRC warn branch and RecordingFormat::PCM per-user file-handle wiring (including dirname mkdir). Integration coverage (gated on DISCORD_BOT_TOKEN + CHANNEL_ID): - New "full session: join → play → record → disconnect" test runs the entire outbound RTP+DAVE pipeline plus the inbound UDP listener in a single bot connection, avoiding Discord's rapid-rejoin rate limits. Drives ~10s of real Opus through readOggOpus → UDP::sendBuffer → DAVE encrypt and ~3s of recording before stopRecording. - Other four integration tests gated behind INTEGRATION_FULL=1 because they re-connect the bot and conflict with the full-session test in batch. - beforeEach adds a one-shot 8s cooldown between integration tests to give Discord's voice gateway time to clear the previous session. Together: VoiceClient 51%→67%, WS 71%→86%, UDP 78%→84%, suite 559→644 tests passing, total statement coverage 69.9%→76.8%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for the thin WS surface the existing feature suite did not already exercise: getVoiceClient, getDaveProtocolVersion, getRawKey, handleCloseVoiceDisconnected, handleFlags, handlePlatform, and handleUndocumented. All five are reflection-driven and stay in pure-unit territory (no live socket). Bumps total statement coverage to 76.99% with the live integration suite and 74.28% from unit-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ining Runtime callbacks - Drop final on Discord\\Voice\\Client\\UDP so PHPUnit can subclass-mock it. readOggOpus + sendBuffer are otherwise untestable from unit-level because every path touches UDP, and PHPUnit cannot generate a mock subclass for a final class. Mockery is not used in this project, so this is the minimum-impact option; the class remains effectively final at the documentation level (single concrete implementation, no other subclass). - VoiceClientReadOggOpusTest covers the three live readOggOpus branches: paused-frame insertSilence + reschedule, Ogg EOF deferred-resolve, and the non-paused getPacket → sendBuffer dispatch loop. Drives a real React loop briefly so timer scheduling + packet ordering execute. - CoordinatorHandlersTest exercises GatewayCoordinator without going through WS at all: a small TestCoordHost implements GatewayCoordinatorHost and captures sendDaveBinary / send / close calls. Runtime::configureCallbacks() injects fake processCommit / processWelcome / keyPackage results so the success branches in handleDaveMlsCommitWelcome (clean commit + commit-ignored fall-through to welcome), handleDaveMlsAnnounceCommitTransition (success + ignored), handleDaveMlsWelcome, and handleDaveMlsExternalSender execute end-to-end. - RuntimeExtraCallbacksTest fills the Runtime callback hooks the existing RuntimeCallbacksTest had not exercised: keyPackageCallback, keyRatchetCallback, createDecryptorCallback, decryptorPassthroughCallback, decryptorKeyRatchetCallback, decryptWithDecryptorCallback (string + auth-fail false), buildMlsCommitWelcome short-circuit at v0, and the isAvailable=false fall-throughs for encryptMediaFrame / decryptMediaFrame / buildMlsCommitWelcome. Bumps total statement coverage to 78% (GatewayCoordinator 82.5%→88.7%, WS 86.4%→88.3%, suite 644→667 passing tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/Discord/Voice/Helpers/ (duplicate ByteBuffer kept only for backwards compatibility) and src/Discord/WebSockets/OpEnum.php are already excluded from PHPStan analysis per phpstan.neon. Mirror that in phpunit.xml so the coverage report reflects what we actually maintain — total goes from ~78% to 83.26% with the same test set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…DE.md updates - CHANGELOG.md "Unreleased" now documents the security/correctness fixes (DAVE auth-failure drops the frame, Buffer rejects on close, playOggStream surfaces fromBuffer errors, Ffmpeg keeps the file protocol with the higher-layer SSRF guard) and the test/coverage work (83.26% statements with legacy BC excluded; 78% raw; new unit + live integration files; PHPUnit source exclusions). UDP `final` removal is flagged under Changed. - TODO.md tracks the remaining real coverage gaps that need real subprocesses or real libdave handles, and records the ssrcToUserId / speakingStatus / UDP visibility BC items plus the security-policy rationale for keeping the `file` protocol whitelisted. - CLAUDE.md: integration-test section now documents the INTEGRATION_FULL gating, and the "Conventions That Bite" entry for ssrcToUserId is rewritten to match the actual current visibility (protected, not public; tests use ReflectionProperty). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d test coverage expansion - Full DAVE/MLS E2EE protocol via libdave FFI (mandatory from 2026-03-01) - RecordingFormat enum + WavWriter for per-user WAV/OGG recording - VoiceException marker interface for unified catch blocks - PHPStan level 5 clean (0 errors), CS-Fixer, phplint via composer - 689 unit tests at 83.26% statement coverage; guarded native FFI test suite - Dead code removed (Packet init methods, WS delegate stubs) - Security: DAVE auth failure drops frame instead of leaking ciphertext Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests in VoiceClientReceiveExtTest that instantiate OpusFfi directly were crashing because libopus.so.0 was absent from the runner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FFI::addr($buffer[0]) passes a PHP string to FFI::addr — invalid.
$ffi->cast('char*', $buffer) correctly yields a char* to the buffer start.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old Fixed entry incorrectly stated ssrcToUserId was widened to public; it was later narrowed back to protected via updateSpeakingStatus(). Correct the entry and add [BC break] notices for speakingStatus, ssrcToUserId, and voiceDecoders visibility/type changes under the Changed section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… deprecation Static FFI::cast() is deprecated in PHP 8.4; use $ffi->cast() instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
alexandre433
approved these changes
May 13, 2026
…erId Allows external callers to read the two protected properties without widening visibility back to public. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
valzargaming
approved these changes
Jun 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Delivers foundational DAVE support in the voice stack and follows through on review feedback to make the new paths functional, test-covered, and consistent across docs and helpers.
Foundational DAVE work:
Discord\Voice\Dave\Runtime,Discord\Voice\Dave\State,Discord\Voice\Dave\BinaryFrame.VoiceClient::encryptDaveFrame()andVoiceClient::decryptDaveFrame()behavior with protocol-aware runtime integration and failure handling.Test coverage added:
tests/Unit/Dave/RuntimeTest.phptests/Unit/Voice/VoiceClientDaveFrameTest.phptests/Feature/Voice/WSDaveMlsProposalsTest.phptests/bootstrap.phpReviewer feedback addressed (round 1):
LibDaveNotFoundException::fromRuntimeError()only appends the "Load error:" suffix when the load error is a non-empty string (skips bothnulland'').composer.jsonunitscript no longer hard-codes a Linux-onlyDISCORDPHP_DAVE_LIBRARY=.cache/libdave/lib/libdave.so; per-platform export is documented in CONTRIBUTING/README instead.CONTRIBUTING.mdPHP prerequisite aligned withcomposer.json's^8.3requirement.README.mdno longer claims an automatic fallback to DAVE protocol version0; clarifies that libdave is required andLibDaveNotFoundExceptionis thrown when unavailable.BufferArrayAccessTrait::writeUInt()switched to big-endian (N) so it round-trips withreadUInt()and matches the RTP byte order used byPacket.Ffmpeg::encode()now includespipein-protocol_whitelistso the defaultpipe:0stdin input is not rejected by ffmpeg; affected tests updated to expect the new whitelist value.Reviewer feedback addressed (round 2):
.github/dependabot.yml: reverted both thecomposerandgithub-actionsschedules fromweeklytodaily.voice->join(...)/manager->join(...)snippets with the actual Manager API namejoinChannel(...)(and normalized to a single-arg form for consistency) acrossdocs/TROUBLESHOOTING.md,examples/play-file.php,examples/queue.php,examples/record.php,README.md, and theDiscord\Voice\Exceptions\VoiceExceptiondocblock.VoiceClient::ALLOWED_URL_SCHEMEStightened to HTTPS-only (droppedhttp) soplayFile()rejects plain-HTTP URLs;tests/Unit/Voice/VoiceClientSecurityTest.phpupdated to assert the new scheme set.BufferArrayAccessTrait: theDiscord\Voice\Helpers\ByteBuffer\BufferArrayAccessTraitcopy is the documented legacy back-compat shim and was left in place; removal is deferred to a separate, focused PR.Type of change
Checklist
composer check(Pint + CS-Fixer dry run + unit tests pass)composer phpstan(PHPStan level 5 passes)Related issues