discord-php-helpers/voice
Discord voice support for DiscordPHP: join voice channels, send and receive audio, with DAVE end-to-end encryption, gateway opcode handling, and FFI-based libdave integration. Requires PHP 8.3+, ext-json, and ext-ffi with libdave installed.
Install Dependencies
composer require discord-php-helpers/voice
Ensure ext-ffi, ext-json, and ext-gmp (for 32-bit PHP) are enabled.
Setup libdave
Run the provided script to fetch the native library (as of v8.3.1 it defaults to libdave 1.2.1):
./vendor/discord-php-helpers/voice/scripts/setup-libdave.sh
Set the environment variable (Linux example):
export DISCORDPHP_DAVE_LIBRARY="$PWD/.cache/libdave/lib/libdave.so"
Add this to your .env for Laravel:
DISCORDPHP_DAVE_LIBRARY=/path/to/libdave.so
Initialize DiscordPHP with Voice In a Laravel service provider or boot method:
use Discord\Discord;
use Discord\Voice\VoiceManager;
$discord = new Discord([
'token' => env('DISCORD_TOKEN'),
'intents' => Discord::INTENTS_ALL,
]);
$voiceManager = new VoiceManager($discord);
$discord->setVoiceManager($voiceManager);
$discord->run();
First Use Case: Play Audio in a Voice Channel
$discord->on('ready', function (Discord $discord) {
$channel = $discord->getChannel('CHANNEL_ID');
// One-argument call: the manager defaults to the client it was constructed with (v8.3.1+)
$discord->voice->joinChannel($channel)
->then(function ($voiceClient) {
$voiceClient->on('ready', function () use ($voiceClient) {
$voiceClient->playFile('/path/to/audio.mp3');
});
$voiceClient->on('end', function () use ($voiceClient) {
$voiceClient->disconnect();
});
}, function (\Throwable $e) {
// Always attach a rejection handler — join failures are silent otherwise
report($e);
});
});
Where to look first
Manager::joinChannel() / VoiceClient if you need to understand the connect → ready → play lifecycle.stop()" behaviour.Voice Client Lifecycle
ready, end, and error events to manage the voice client lifecycle.ready as the only safe point to start playback — joining resolves before the voice socket is ready.$voiceClient->on('ready', fn() => $voiceClient->playFile('audio.mp3'));
$voiceClient->on('end', fn() => $voiceClient->disconnect());
Handling Inbound Audio
channel-pcm or channel-opus events for raw audio data.$voiceClient->on('channel-pcm', function (string $pcm) {
// Process raw PCM data (e.g., save to file, analyze, etc.)
});
Recording Users
RecordingFormat for automated recording:
$voiceClient->record(
RecordingFormat::WAV,
fn(string $userId) => "/tmp/recording_{$userId}.wav"
);
Service Provider Setup
Bind the VoiceManager and VoiceClient to Laravel's container:
public function register()
{
$this->app->singleton(VoiceManager::class, function ($app) {
$discord = $app->make(Discord::class);
return new VoiceManager($discord);
});
}
Because the manager remembers the client it was constructed with, you can now call
$app->make(VoiceManager::class)->joinChannel($channel) without threading $discord through every call site.
Queue-Based Audio Playback Use Laravel queues to defer audio playback (e.g., for long-running tasks):
Queue::push(function () use ($voiceClient, $audioPath) {
$voiceClient->playFile($audioPath);
});
Real-Time Audio Processing Combine with Laravel Echo/Pusher for real-time updates:
$voiceClient->on('channel-pcm', function (string $pcm) {
broadcast(new AudioProcessed($pcm))->toOthers();
});
Error Handling
Catch VoiceException and always handle promise rejection from joinChannel():
try {
$voiceClient = $discord->voice->joinChannel($channel)->wait();
} catch (VoiceException $e) {
Log::error("Voice error: " . $e->getMessage());
// Retry logic or fallback
}
Passing an explicit client still works if you need to join with a different Discord instance than the one the manager was built with.
Dynamic Audio Switching
Use playRawStream or playOggStream for dynamic audio sources (e.g., live streams):
$stream = fopen('php://memory', 'r+');
$voiceClient->playOggStream($stream);
User-Specific Audio Route audio per user using SSRC mapping:
$voiceClient->on('speaking', function ($userId, $isSpeaking) {
if ($isSpeaking) {
$voiceClient->playFile("user_{$userId}_audio.mp3");
}
});
Missing libdave
LibDaveNotFoundException if ext-ffi or libdave is missing.setup-libdave.sh (defaults to libdave 1.2.1 in v8.3.1) and verify DISCORDPHP_DAVE_LIBRARY is set correctly.php -m for ffi and ensure the library path is accessible.libdave Version Skew
libdave left over in .cache may not be compatible..so/.dll/.dylib you are actually loading, not just the one on the download path.Unhandled Join Rejections
joinChannel() returns a promise; without a rejection handler, failed joins surface only as silent hangs or unlogged exceptions.->then() or use ->otherwise(), and log inside it.report() the throwable to confirm the failure stage (auth vs. voice endpoint vs. DAVE negotiation).Playing Before ready
ready event listener.ready to see the gap.Pending Playback Promise After stop()
stop() does not reject/resolve the outstanding playback promise; awaiting it will hang.stop() as terminal; do not chain further work onto the stale promise.->otherwise() temporarily to see whether the promise ever settles.FFmpeg Dependencies
FFmpegNotFoundException if FFmpeg is not installed for audio playback.PATH:
sudo apt-get install ffmpeg # Linux
brew install ffmpeg # macOS
ffmpeg -version
Resource Limits
MAX_DECODERS (25) may drop new speakers if too many users are active.Timing Sensitivity
addTimer calls).ReactPHP timers:
$loop->addTimer(0.02, fn() => $voiceClient->sendAudioFrame());
DAVE Protocol Negotiation
libsodium is missing.libsodium:
pecl install libsodium
LibSodiumNotFoundException.Cross-Platform Paths
libdave.dll not found).DISCORDPHP_DAVE_LIBRARY=C:\path\to\libdave.dll
Enable Verbose Logging
Configure the VoiceManager for debug logs:
$voiceManager = new VoiceManager($discord, [
'debug' => true,
'logLevel' => \Monolog\Logger::DEBUG,
]);
Check UDP Socket
tcpdump or Wireshark to inspect UDP traffic:
sudo tcpdump -i any -w discord_voice.pcap port 443
30000-32767).Validate Audio Formats
ffmpeg -i audio.mp3
Test with Minimal Example Start with the basic example in the README (now includes readiness + rejection handling) to isolate issues:
$voiceClient->playFile(__DIR__ . '/test.mp3');
Remember CI Does Not Place a Call
Custom Audio Processing
Override VoiceClient methods to inject custom logic:
class CustomVoiceClient extends VoiceClient {
protected function handleAudioData(Packet $packet) {
// Custom processing before routing to users
parent::handleAudioData($packet);
}
}
Plugin System Use Laravel’s service container to swap implementations:
$this->app->bind(VoiceClient::class, function ($app) {
return new CustomVoiceClient($app->make(VoiceManager::class));
});
Manager Subclassing
Since joinChannel() now derives the client from the manager, subclassing Manager is a clean seam for injecting per-guild defaults (e.g., auto-record on join) without changing call sites.
Event Filtering Filter events globally using Laravel’s event system:
Event::listen(VoiceClient::class . '.channel-pcm', function ($pcm) {
if (strpos($pcm, 'noise') !== false) {
// Ignore noise
}
});
Mocking for Tests Use Laravel’s mocking to test voice interactions:
$mockVoiceClient = Mockery::mock(VoiceClient::class);
How can I help you explore Laravel packages today?