Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Voice Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup for Laravel

  1. Install Dependencies

    composer require discord-php-helpers/voice
    

    Ensure ext-ffi, ext-json, and ext-gmp (for 32-bit PHP) are enabled.

  2. 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
    
  3. 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();
    
  4. 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);
            });
    });
    
  5. Where to look first

    • The package README examples (now updated for readiness and promise handling).
    • Manager::joinChannel() / VoiceClient if you need to understand the connect → ready → play lifecycle.
    • Troubleshooting notes for the "pending playback promise after stop()" behaviour.

Implementation Patterns

Event-Driven Workflows

  1. Voice Client Lifecycle

    • Use ready, end, and error events to manage the voice client lifecycle.
    • Treat ready as the only safe point to start playback — joining resolves before the voice socket is ready.
    • Example:
      $voiceClient->on('ready', fn() => $voiceClient->playFile('audio.mp3'));
      $voiceClient->on('end', fn() => $voiceClient->disconnect());
      
  2. Handling Inbound Audio

    • Listen for channel-pcm or channel-opus events for raw audio data.
    • Example (raw PCM):
      $voiceClient->on('channel-pcm', function (string $pcm) {
          // Process raw PCM data (e.g., save to file, analyze, etc.)
      });
      
  3. Recording Users

    • Use RecordingFormat for automated recording:
      $voiceClient->record(
          RecordingFormat::WAV,
          fn(string $userId) => "/tmp/recording_{$userId}.wav"
      );
      

Integration with Laravel

  1. 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.

  2. 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);
    });
    
  3. 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();
    });
    

Common Patterns

  1. 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.

  2. Dynamic Audio Switching Use playRawStream or playOggStream for dynamic audio sources (e.g., live streams):

    $stream = fopen('php://memory', 'r+');
    $voiceClient->playOggStream($stream);
    
  3. User-Specific Audio Route audio per user using SSRC mapping:

    $voiceClient->on('speaking', function ($userId, $isSpeaking) {
        if ($isSpeaking) {
            $voiceClient->playFile("user_{$userId}_audio.mp3");
        }
    });
    

Gotchas and Tips

Pitfalls

  1. Missing libdave

    • Issue: LibDaveNotFoundException if ext-ffi or libdave is missing.
    • Fix: Run setup-libdave.sh (defaults to libdave 1.2.1 in v8.3.1) and verify DISCORDPHP_DAVE_LIBRARY is set correctly.
    • Debug: Check php -m for ffi and ensure the library path is accessible.
  2. libdave Version Skew

    • Issue: v8.3.1 accepts libdave 1.2.1's namespaced opaque-handle macros during header preprocessing; an older prebuilt libdave left over in .cache may not be compatible.
    • Fix: Re-run the setup script after upgrading, and delete any stale cached library/bindings.
    • Debug: Confirm the version of the .so/.dll/.dylib you are actually loading, not just the one on the download path.
  3. Unhandled Join Rejections

    • Issue: joinChannel() returns a promise; without a rejection handler, failed joins surface only as silent hangs or unlogged exceptions.
    • Fix: Pass the second callback to ->then() or use ->otherwise(), and log inside it.
    • Debug: Temporarily report() the throwable to confirm the failure stage (auth vs. voice endpoint vs. DAVE negotiation).
  4. Playing Before ready

    • Issue: Starting playback immediately after the join promise resolves can drop audio, since the voice socket may not have completed handshake.
    • Fix: Gate all playback inside the ready event listener.
    • Debug: Log timestamps on join resolution and ready to see the gap.
  5. Pending Playback Promise After stop()

    • Issue: stop() does not reject/resolve the outstanding playback promise; awaiting it will hang.
    • Fix: Track the returned promise yourself and treat stop() as terminal; do not chain further work onto the stale promise.
    • Debug: Attach ->otherwise() temporarily to see whether the promise ever settles.
  6. FFmpeg Dependencies

    • Issue: FFmpegNotFoundException if FFmpeg is not installed for audio playback.
    • Fix: Install FFmpeg and ensure it’s in PATH:
      sudo apt-get install ffmpeg  # Linux
      brew install ffmpeg           # macOS
      
    • Debug: Test FFmpeg manually:
      ffmpeg -version
      
  7. Resource Limits

    • Issue: MAX_DECODERS (25) may drop new speakers if too many users are active.
    • Fix: Monitor active decoders and implement user prioritization or throttling.
  8. Timing Sensitivity

    • Issue: Audio glitches or delays due to improper timing (e.g., missed addTimer calls).
    • Fix: Ensure audio frames are scheduled every 20ms (50fps) using ReactPHP timers:
      $loop->addTimer(0.02, fn() => $voiceClient->sendAudioFrame());
      
  9. DAVE Protocol Negotiation

    • Issue: DAVE (E2EE) may fail if libsodium is missing.
    • Fix: Install libsodium:
      pecl install libsodium
      
    • Debug: Check for LibSodiumNotFoundException.
  10. Cross-Platform Paths

    • Issue: Path issues on Windows (e.g., libdave.dll not found).
    • Fix: Use absolute paths and verify the library is in the correct location:
      DISCORDPHP_DAVE_LIBRARY=C:\path\to\libdave.dll
      

Debugging Tips

  1. Enable Verbose Logging Configure the VoiceManager for debug logs:

    $voiceManager = new VoiceManager($discord, [
        'debug' => true,
        'logLevel' => \Monolog\Logger::DEBUG,
    ]);
    
  2. Check UDP Socket

    • Use tcpdump or Wireshark to inspect UDP traffic:
      sudo tcpdump -i any -w discord_voice.pcap port 443
      
    • Filter for Discord’s voice UDP ports (typically 30000-32767).
  3. Validate Audio Formats

    • Ensure audio files are compatible (e.g., MP3, OGG, WAV).
    • Use FFmpeg to validate:
      ffmpeg -i audio.mp3
      
  4. 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');
    
  5. Remember CI Does Not Place a Call

    • v8.3.1 was verified by Pest/PHPStan/lint/coverage against libdave 1.2.1, but no live Discord voice call was made. Treat real-call behaviour as your own smoke test.

Extension Points

  1. 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);
        }
    }
    
  2. Plugin System Use Laravel’s service container to swap implementations:

    $this->app->bind(VoiceClient::class, function ($app) {
        return new CustomVoiceClient($app->make(VoiceManager::class));
    });
    
  3. 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.

  4. 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
        }
    });
    
  5. Mocking for Tests Use Laravel’s mocking to test voice interactions:

    $mockVoiceClient = Mockery::mock(VoiceClient::class);
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator