Remedia

Discord bot harness

Run the manual discord.js app that exercises Remedia voice playback.

Run apps/discord to exercise voice joining, playback, filters, recovery, and crossfading against a test Discord guild. The app uses @remedia/client with embedded or remote nodes and discord.js for the bot Gateway and commands. It keeps a small in-memory queue for manual testing.

Setup and run

Create apps/discord/.env with the bot token and application ID:

DISCORD_TOKEN=your-bot-token
APPLICATION_ID=your-application-id

Install the workspace and native dependencies described in the Node.js binding setup, then build the Node addon, register commands, and start the source through tsx:

pnpm install --frozen-lockfile
pnpm --filter @remedia/node build
pnpm --filter discord-app deploy
pnpm --filter discord-app start

The Discord application requires the Guilds and GuildVoiceStates Gateway intents. It forwards raw Gateway dispatches to Remedia and sends OP 4 through the discord.js shard that owns the target guild. It does not collect or expose voice session IDs, tokens, or endpoints.

Commands

  • /play url:<http-or-https-url> joins the command user's current voice channel and starts the URL. If another track is playing, the URL is appended to that server's in-memory queue. Replies use the title from Remedia media metadata when available. Otherwise they show a sanitized URL.
  • /pause and /resume control the current track and include its title when Remedia provided one.
  • /seek seconds:<number> seeks to a position in seconds and identifies the current track when available.
  • /queue list shows the current item and up to ten upcoming URLs; /queue clear removes upcoming items but keeps the current track.
  • /loop mode:<off|track|queue> controls this guild's playback loop. track repeats the current track after natural completion; /skip still advances to the next item. queue rotates naturally completed and manually skipped tracks to the end of the queue. Failed tracks are not repeated. off restores normal one-pass playback.
  • /skip stops the current item and starts the next queued URL, showing titles when Remedia metadata is available.
  • /nowplaying shows the Remedia title when available, playback state, and an elapsed-time / progress-indicator / duration line against the original media timeline. It uses mm:ss and switches to hh:mm:ss for long tracks. Estimated source positions are prefixed with ~. Unknown or live durations show no fabricated progress bar or percentage. Its fields show requester, queue count, filters, volume, and loop mode.
  • /stop stops playback and clears the queue while keeping the voice connection open.
  • /disconnect stops playback, clears the queue, and leaves the voice channel.
  • /filter set <preset> toggles one of 8d, bassboost, nightcore, vaporwave, tremolo, or vibrato. An inactive preset is appended to the active filters. Selecting an active preset removes only it. The remaining graphs keep their order, and removing the last preset clears the graph. /filter clear disables all active graphs, and /filter list displays the choices and current selection.
  • /volume reads the current setting; /volume percent:<0..200> sets linear gain. Zero mutes, 100 is unity, and values above 100 may clip.
  • /nowplaying also shows the selected loop mode.

Queue lifetime and limits

The queue belongs to this sample application, not remedia-core or @remedia/node. It is held in memory per guild and is lost when the bot exits. It holds up to 100 upcoming tracks in addition to the current track. The loop mode is stored per guild, remains selected across tracks and /stop, and resets when the voice session is disconnected. In queue mode, the current track counts as part of the loop. If it is the only item, it repeats by itself. Upcoming queue entries have not been opened by Remedia yet, so they display their sanitized URLs until playback begins and metadata becomes available.

When a track reaches natural end-of-stream, the app advances to the next queued URL. A track that fails to open or decode is skipped. The failure is retained in the playback snapshot and shown by /nowplaying. The process log reports the guild without printing the media URL. Commands are restricted to users currently in the same voice channel as the bot. The bot does not move an existing session to another channel implicitly. Disconnect first, then issue /play from the new channel.

Filter presets

The six names above are convenience presets defined only in apps/discord. Each maps to a raw FFmpeg graph passed to the generic Node API. /filter set toggles the selected preset: adding one appends its graph, while toggling it off removes only that graph and retains the order of other selections. Removing the last selection follows the same graph-clear path as /filter clear.

Changes apply immediately to the active track and carry over to queued tracks and skips. Filter changes do not change volume, and volume changes do not rebuild the graph. A disconnect or bot shutdown disposes the guild player and drops both settings. Engine callers can use arbitrary trusted filtergraphs instead of these names.

Command replies and notifications

Slash command replies use consistent Remedia embeds. Successful command responses are public and include the invoking user. Validation and operation errors remain ephemeral. The helper defers privately because Discord does not allow a deferred reply to change visibility, then deletes that private defer and posts a public follow-up on success. Error replies edit the ephemeral defer. /nowplaying formats source time and duration around a compact progress bar. Estimated source positions are prefixed with ~.

The app subscribes to typed @remedia/client session events and posts public embeds for playback start, natural completion, stop, failure, pause, resume, seek, filter changes, volume changes, and terminal voice failure. These notifications go to the text channel retained by the guild queue and use the customMetadata attached to that playback generation to find the originating text channel. The first /play request that creates a guild queue stores its sendable channel object in that queue's in-memory metadata, and each playback passes that reference through Remedia's JavaScript-only customMetadata. The reference is not serialized to Rust or a remote Remedia server.

Later requests and queued tracks continue to notify the original queue channel until the guild session is disconnected and disposed. Each playback notification identifies the track requester when available. Successful command replies remain separate from lifecycle notifications. Queue snapshots remain public. If the bot cannot send in the retained channel, playback continues and a warning is written only when REMEDIA_DEBUG=1.

Queue operations and source generations

Queue operations are serialized per guild. Skip stops and joins the current playback before opening the next URL while retaining the connected voice transport. Completion waits are bound to the source generation active when wait() was called, so a delayed completion from an old item cannot clear or advance a replacement item.

Only direct HTTP(S) media URLs are accepted. The installed FFmpeg libraries must support the container and codec. There is no search, URL resolution, playlist import, client stream upload, or file-path command. Displayed URLs omit query strings and credentials, while the original URL is passed to the decoder.

stop leaves Discord connected so another /play can reuse the session. disconnect or process shutdown releases the playback session and native voice transport. Playback is unpaced by the decoder. The Discord transport schedules encoded packets in real time. See the voice output guide for Opus, DAVE, transport encryption, and protocol limitations.

This harness is intended for manual integration with a configured bot. Live Discord bot playback has been exercised. Automated tests use deterministic mock sessions and do not require Discord credentials. CI does not establish a live connection.

Manual voice region recovery

Run this procedure in a test guild where you can change the voice channel's Region Override. It checks that a server credential update replaces transport delivery while the existing playback continues.

  1. Start the bot in embedded mode, or start remedia-server and configure the bot with REMEDIA_MODE=remote and REMEDIA_NODES as described in the Discord app README.
  2. Join the bot to a voice channel with /play and use a track long enough to remain active through the change. Confirm that the public Now playing event appears in the queue's text channel.
  3. While audio is playing, change the channel's Region Override to another available region. Wait for audio to resume and check /nowplaying. The same track should continue near its previous position and must not produce another Now playing event.
  4. Repeat while playback is paused. Confirm it remains paused after transport recovery, then /resume and verify that audio continues.
  5. Let a track finish, then start another track in the same voice session. Confirm that the second track plays without the bot leaving and rejoining the channel. Restore the channel's Region Override to Automatic when finished.

The replacement may cause a short audible gap while Discord renegotiates the voice transport and DAVE state. The source decoder and playback generation should continue. Logs include guild, playback, sender, and transport generations plus recovery outcomes. They must not include voice tokens or session IDs.

Manual crossfade smoke test

Register the updated commands with pnpm --filter discord-app deploy, start the bot, join a voice channel, and run:

/crossfade first:https://example.com/song-a.mp3 second:https://example.com/song-b.mp3

Supply two real, authorized direct HTTP(S) media URLs. Use a finite first track longer than 35 seconds and a second track longer than 5 seconds. The session uses crossfade: { enabled: true, durationMs: 5000, prepareBeforeMs: 30000, curve: 'equalPower' }. Preparation begins about 35 seconds before the first source ends. URL staging is triggered by Remedia's clock, with no JavaScript transition timer.

The test requires a new guild session. Run /disconnect before starting it if the bot already owns any session, including an idle session after /stop. The command refuses to interrupt or replace unrelated playback. Its session remains reserved for the test afterward: use /disconnect before another /crossfade or ordinary /play queue. Looping is disabled during the two-track operation.

Both modes use the same @remedia/client API. In remote mode, explicitly enable audio.crossfade.enabled in the server's remedia.config.json, restart the server, and reconnect the bot so it discovers the current capability. The manager selects a healthy node advertising available crossfade capacity and reports its ID. A server that disables or does not advertise crossfade produces an actionable error before a voice join. Embedded nodes receive the options directly at session creation and need no server configuration.

  1. Confirm Crossfade session enabled identifies the intended node and 5000 ms overlap. Now playing identifies the first source generation.
  2. Confirm Preparation requested includes remaining time (about 35000 ms) and transition lead (about 30000 ms), followed by Second source staging started and Stage ready. These show the unique queue-entry ID, expected generation, queue revision, and source revision. Duplicate preparation events are ignored.
  3. Near the end of the first source, listen for five seconds of overlapping audio. Transition started identifies the outgoing and incoming generations and entry ID. /nowplaying must now show the second track, and /queue list must no longer list it as upcoming.
  4. Confirm Second source promoted reports the first source's completion with continuation: type=promoted, the incoming generation, and entry ID. The bot must not issue another play() for that source. Let the second source finish. Expect one final Track ended, with no replay or extra queue advancement.
  5. Repeat with /seek during preparation: expect Stage invalidated, a new source generation, and a fresh engine preparation request if the new position reaches its threshold. Try /pause and /resume. The stage should be retained while transition timing is frozen. Try /stop, /skip, and /disconnect in separate runs during preparation and overlap to verify cancellation.

Missing, ignored, failed or late preparation does not stall the first source. Without promotion, the existing normal completion path starts the queued second URL and reports that fallback explicitly. This is not a successful crossfade. A transition cancelled by Remedia restores the outgoing track before normal completion fallback. /skip cancels preparation/overlap, stops the current source, and starts any remaining queued track normally; /stop clears both items. Lifecycle errors show their reason with media URLs and credentials redacted.

Automated command and manager tests use mocks in both modes, including duplicate and stale events, continuation reconciliation, cancellation, and ordinary queues. This procedure must still be performed manually against real Discord voice. Passing automated tests does not establish audible overlap or live transport behavior.

On this page