Remedia

Events and hooks

Handle playback generations, voice state, application metadata, and pre-play middleware.

Session events describe changes in the engine and voice connection. Subscribe on a DiscordSession for one guild or on RemediaClient to receive forwarded session events and node notifications.

Subscribe to playback events

import { PlaybackSessionEvents } from '@remedia/client';

interface TrackContext {
  requestedBy: string;
  trackId: string;
}

const voice = client.createDiscordSession<TrackContext>({ guildId, channelId, userId });

voice.on(PlaybackSessionEvents.playbackCompleted, (event) => {
  if (event.continuation?.type === 'promoted') {
    // The staged entry is already playing. Reconcile the queue without calling play again.
    return;
  }
  console.log('Finished', event.generation, event.customMetadata?.trackId);
});

await voice.connect();
await voice.play(
  { url: 'https://media.example.com/track.mp3' },
  { customMetadata: { requestedBy: userId, trackId: 'track-123' } },
);

The exported PlaybackSessionEvents, VoiceSessionEvents, and RemediaClientEventNames constants work with on, once, and off. Matching string-union types are exported too. Direct @remedia/node sessions use the same playback and voice event names.

Playback event payloads

Every playback event carries sessionId, generation, nodeId, and optional local customMetadata. Client Discord sessions also carry guildId. Direct Node sessions have nodeId: null, and standalone playback has no guild ID.

EventAdditional data and meaning
playbackStartedstate: 'playing', info, optional crossfade transition
playbackCompletedstate: 'completed', position, optional promoted continuation
playbackStoppedstate: 'stopped', optional reason
playbackFailedstate: 'failed', error
playbackPaused, playbackResumedSuccessful pause or resume state change
playbackSeekedpositionMs, previousPositionMs
filterChanged, volumeChangedgraph / previousGraph or volume / previousVolume
sessionDisposedFinal disposal notification, emitted once

A generation identifies the active source timeline. A seek in a crossfade session can advance it without emitting a new start event. Bind application queue decisions and waiters to it so a delayed event from an old source cannot finish its replacement. Idempotent controls emit no duplicate change events, and rejected controls emit no success event.

playbackStarted confirms that processing began. playbackCompleted reports the engine's natural completion path, including output completion where supported. Neither measures when a Discord listener heard a sample. See playback positions.

Crossfade events

EventAdditional data
prepareRequestedremainingMs, transitionInMs
stageReady, stageInvalidatedentryId, queueRevision, sourceRevision
stageFailedThe same stage identity and reason
transitionCancelledfailedGeneration, entryId

On promotion, incoming playbackStarted.transition identifies the outgoing generation and entry. Outgoing playbackCompleted.continuation identifies the promoted generation. Use these fields to update your queue once. See Crossfading.

Voice and node events

Sessions emit voiceStateChanged and voiceFailed. The client also emits nodeConnected, nodeDisconnected, sessionResumed, sessionLost, nodeEvent, eventsLost, guildAssigned, guildReleased, and guildMigrated. See session recovery and node migration.

Playback and voice failures use named events. An unhandled Node.js error event is not part of this contract. Synchronous listener exceptions produce a REMEDIA_LISTENER_ERROR process warning and do not fail playback. Catch rejected promises inside asynchronous listeners yourself.

Attach application metadata

play(source, { customMetadata }) retains the original object reference for that source generation and includes it in lifecycle events, including delayed terminal events. Functions and class instances are allowed because this metadata stays in JavaScript. Rust, RPC snapshots, and server logs never receive it.

The reference is released after the source's terminal event or session disposal. It is not persisted across process restarts. stageNext() accepts metadata for the incoming source in the same way.

Run middleware before playback

const removeHook = voice.hooks.beforePlay(async ({ source, customMetadata, signal }) => {
  signal.throwIfAborted();
  console.log('Requested track', customMetadata?.trackId);
  // Return { cancel: true, reason: '...' } to prevent this source from starting.
});

// Remove the hook when the owning feature shuts down.
removeHook();

Hooks run in registration order and are awaited before native playback or RPC starts. Returning a cancellation result or throwing prevents startup and its start event. The hook context includes the source, metadata, and an AbortSignal. Cancellation while a hook is pending cancels startup. Cancellation after startup requests a stop for that generation.

Disposal clears hooks. Hooks are local application policy. The server still applies its own authentication and media authorization.

Cached state and waiting

wait() resolves with completed, failed, or stopped for the source generation it observes. Old completion events cannot finish a new source's waiter. A superseded client generation is reported as stopped. On a remote event gap or reconnect, the adapter reconciles player.state before resolving waiters.

A single wait() is not a crossfade queue-completion API. Promotion changes the active source generation, so a client waiter captured before promotion may resolve as stopped. Direct Node waits follow the native playback worker, which can span promoted sources. Drive queue advancement from lifecycle events and their continuation fields.

Remote getters reflect cached snapshots and lifecycle observations. Use await voice.refresh() to request fresh state. If a remote terminal notification was missed, the client can emit one reconstructed event with reconstructed: true. It does not invent a historical start event or replay a detached event history.

RPC rejections use RemediaRpcError with a stable server error code. Connection, session-loss, and protocol errors use the client error classes. A rejected command alone does not mark a healthy node disconnected.

On this page