Skip to main content

Plugin Architecture

Lavalink’s plugin system is built on Spring Boot’s dependency injection framework, allowing plugins to seamlessly integrate with the core server functionality.

Architecture Overview

Plugins are loaded as JAR files and integrated into Lavalink’s Spring application context. This allows plugins to:
  • Register beans that implement plugin interfaces
  • Access Lavalink’s services and components
  • Define custom configuration properties
  • Create REST endpoints and WebSocket handlers
  • Extend audio processing capabilities

Plugin API Interfaces

The plugin API provides several interfaces for extending Lavalink. All interfaces are in the dev.arbjerg.lavalink.api package.

Core Extension Points

Define custom audio filters that can be controlled via WebSocket operations.Interface:
interface AudioFilterExtension {
    val name: String
    
    fun isEnabled(data: JsonElement): Boolean
    
    fun build(
        data: JsonElement,
        format: AudioDataFormat?,
        output: FloatPcmAudioFilter?
    ): FloatPcmAudioFilter?
}
Use Cases:
  • Custom equalizers
  • Audio effects (reverb, echo, etc.)
  • Volume normalization
  • Channel mixing
Registration: Add @Service annotation to your implementation.
Add custom fields to track and playlist JSON responses.Interface:
interface AudioPluginInfoModifier {
    fun modifyAudioTrackPluginInfo(track: AudioTrack): JsonObject?
    
    fun modifyAudioPlaylistPluginInfo(playlist: AudioPlaylist): JsonObject?
}
Use Cases:
  • Add lyrics or metadata
  • Include sponsorship information
  • Attach additional track details
  • Custom playlist metadata
Returns: A JsonObject with custom fields, or null to skip modification.
Intercept and modify HTTP requests to Lavalink’s REST API.Interface:
interface RestInterceptor : HandlerInterceptor
This extends Spring’s HandlerInterceptor, providing methods:
  • preHandle() - Before request processing
  • postHandle() - After request processing
  • afterCompletion() - After view rendering
Use Cases:
  • Custom authentication
  • Request logging
  • Rate limiting
  • Request validation
Registration: Add @Service annotation to your implementation.
Handle WebSocket lifecycle and player events.Interface:
abstract class PluginEventHandler {
    open fun onWebSocketOpen(context: ISocketContext, resumed: Boolean)
    open fun onSocketContextPaused(context: ISocketContext)
    open fun onSocketContextDestroyed(context: ISocketContext)
    open fun onWebSocketMessageOut(context: ISocketContext, message: String)
    open fun onNewPlayer(context: ISocketContext, player: IPlayer)
    open fun onDestroyPlayer(context: ISocketContext, player: IPlayer)
}
Events:
  • onWebSocketOpen - New WebSocket connection (or resumed)
  • onSocketContextPaused - WebSocket closed but resumable
  • onSocketContextDestroyed - WebSocket permanently closed
  • onWebSocketMessageOut - Outgoing WebSocket message
  • onNewPlayer - Player created for a guild
  • onDestroyPlayer - Player destroyed
Use Cases:
  • Analytics and monitoring
  • Custom logging
  • State synchronization
  • Third-party integrations
Registration: Provide as a bean with @Service or @Component.
Configure Lavalink’s AudioPlayerManager instance.Interface:
interface AudioPlayerManagerConfiguration {
    fun configure(manager: AudioPlayerManager): AudioPlayerManager
}
Use Cases:
  • Modify global player settings
  • Register custom audio source managers
  • Configure frame buffer duration
  • Set up custom audio filters
Note: For adding AudioSourceManager beans, you don’t need this interface - just provide them as Spring beans directly.

Lavaplayer Integration

Plugins can extend Lavaplayer’s audio processing capabilities by providing beans:
import org.springframework.stereotype.Service;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;

@Service
public class CustomAudioSourceManager implements AudioSourceManager {
    @Override
    public String getSourceName() {
        return "custom-source";
    }
    
    @Override
    public AudioItem loadItem(
        AudioPlayerManager manager,
        AudioReference reference
    ) {
        // Load audio from custom source
        if (!reference.identifier.startsWith("custom:")) {
            return null;
        }
        return loadCustomTrack(reference.identifier);
    }
    
    @Override
    public boolean isTrackEncodable(AudioTrack track) {
        return true;
    }
    
    @Override
    public void encodeTrack(AudioTrack track, DataOutput output) {
        // Encode track for caching
    }
    
    @Override
    public AudioTrack decodeTrack(AudioTrackInfo info, DataInput input) {
        // Decode cached track
        return new CustomAudioTrack(info, this);
    }
    
    @Override
    public void shutdown() {
        // Cleanup resources
    }
}

API Objects

The plugin API provides access to Lavalink’s core objects:

IPlayer

Represents an audio player for a specific guild.
interface IPlayer {
    val audioPlayer: AudioPlayer
    val track: AudioTrack?
    val guildId: Long
    val socketContext: ISocketContext
    val isPlaying: Boolean
    
    fun play(track: AudioTrack)
    fun stop()
    fun setPause(pause: Boolean)
    fun seekTo(position: Long)
    fun setVolume(volume: Int)
}
Key Properties:
  • audioPlayer - The underlying Lavaplayer AudioPlayer
  • track - Currently playing track (null if none)
  • guildId - Discord guild ID (immutable)
  • socketContext - Associated WebSocket connection
  • isPlaying - Whether actively producing audio
Usage Example:
fun handlePlayer(player: IPlayer) {
    // Get current track
    val currentTrack = player.track
    
    // Control playback
    player.setPause(true)
    player.setVolume(75)
    player.seekTo(Duration.seconds(30))
    
    // Access Lavaplayer features
    player.audioPlayer.addListener(myListener)
}

ISocketContext

Represents a WebSocket connection from a client.
interface ISocketContext {
    val sessionId: String
    val userId: Long
    val clientName: String?
    val players: Map<Long, IPlayer>
    val state: State
    
    fun getPlayer(guildId: Long): IPlayer
    fun destroyPlayer(guildId: Long)
    fun sendMessage(message: JsonElement)
    fun closeWebSocket(closeCode: Int, reason: String?)
    
    enum class State {
        OPEN,
        RESUMABLE,
        DESTROYED
    }
}
Key Properties:
  • sessionId - Unique session identifier
  • userId - Discord user ID of the client
  • clientName - Optional client name
  • players - All players for this connection (by guild ID)
  • state - Current connection state
State Transitions: Usage Example:
fun handleContext(context: ISocketContext) {
    // Get or create a player
    val player = context.getPlayer(guildId = 123456789)
    
    // Send custom message to client
    val message = buildJsonObject {
        put("op", "customEvent")
        put("data", "value")
    }
    context.sendMessage(message)
    
    // Check state
    if (context.state == ISocketContext.State.OPEN) {
        // Connection is active
    }
}

ISocketServer

Provides access to all WebSocket sessions.
interface ISocketServer {
    val sessions: Map<String, ISocketContext>
    val resumableSessions: Map<String, ISocketContext>
}
Usage Example:
@Service
class MyPlugin @Autowired constructor(
    private val socketServer: ISocketServer
) {
    fun broadcastToAll(message: JsonElement) {
        socketServer.sessions.values.forEach { context ->
            context.sendMessage(message)
        }
    }
    
    fun getActiveSessionCount(): Int {
        return socketServer.sessions.size
    }
}

Plugin Lifecycle

Loading

1

JAR Discovery

Lavalink scans the plugins directory (default: ./plugins) for JAR files.
2

Dependency Resolution

If configured in application.yml, Lavalink downloads plugins from Maven repositories.
3

ClassLoader Creation

Each plugin gets its own ClassLoader with access to Lavalink’s API.
4

Spring Integration

Plugin classes are scanned and registered in the Spring application context.
5

Bean Registration

Plugin beans (services, controllers, etc.) are instantiated and injected.
6

Initialization

Spring calls @PostConstruct methods and initializes beans.

Runtime

Plugins interact with Lavalink through:
  1. Event callbacks - PluginEventHandler methods called by Lavalink
  2. Bean injection - Access to ISocketServer and other Lavalink services
  3. REST endpoints - Handle HTTP requests via Spring controllers
  4. Filter pipeline - Process audio through AudioFilterExtension
  5. Interceptors - Modify requests/responses via RestInterceptor

Shutdown

When Lavalink stops:
  1. Spring calls @PreDestroy methods on plugin beans
  2. Audio source managers’ shutdown() methods are called
  3. Plugin resources are cleaned up
  4. ClassLoaders are released

Dependency Injection

Plugins can inject Lavalink services and other beans:
import org.springframework.stereotype.Service
import org.springframework.beans.factory.annotation.Autowired

@Service
class MyPlugin @Autowired constructor(
    private val socketServer: ISocketServer,
    private val myConfig: MyPluginConfig
) {
    @PostConstruct
    fun initialize() {
        logger.info("Plugin initialized with ${socketServer.sessions.size} sessions")
    }
    
    fun doSomething() {
        // Use injected dependencies
        if (myConfig.enabled) {
            socketServer.sessions.values.forEach { /* ... */ }
        }
    }
}

Thread Safety

When implementing plugins, consider thread safety:
Plugin methods may be called from multiple threads concurrently. Event handlers, REST endpoints, and filter methods should be thread-safe.
Best Practices:
  • Use thread-safe collections (e.g., ConcurrentHashMap)
  • Synchronize access to shared mutable state
  • Prefer immutable data structures
  • Use @Scope("prototype") for stateful beans
@Service
class ThreadSafePlugin {
    private val cache = ConcurrentHashMap<String, String>()
    
    override fun onWebSocketMessageOut(context: ISocketContext, message: String) {
        // This may be called from multiple threads
        cache[context.sessionId] = message
    }
}

Error Handling

Plugins should handle errors gracefully to avoid crashing Lavalink:
@Service
class RobustPlugin : PluginEventHandler() {
    override fun onNewPlayer(context: ISocketContext, player: IPlayer) {
        try {
            // Plugin logic that might fail
            riskyOperation(player)
        } catch (e: Exception) {
            logger.error("Error handling new player", e)
            // Don't rethrow - let Lavalink continue
        }
    }
}

Testing

The Lavalink Gradle plugin provides tasks for testing:
# Run Lavalink with your plugin
./gradlew :runLavalink

# Run with specific configuration
./gradlew :runLavalink -Dlavalink.config=/path/to/application.yml
Testing Checklist:
  • Test WebSocket event handling
  • Verify REST endpoints work correctly
  • Check audio filters produce expected output
  • Test configuration loading
  • Verify error handling
  • Test with multiple concurrent clients

Resources

Plugin API Javadoc

Complete API reference

Spring Boot Docs

Spring Framework documentation

Lavaplayer Docs

Audio processing library

Plugin Examples

Official and community plugins

Next Steps

Plugin Development Guide

Step-by-step plugin creation

Available Plugins

Explore existing plugins

Build docs developers (and LLMs) love