320 lines
13 KiB
Markdown
320 lines
13 KiB
Markdown
# JellyLMS
|
|
|
|
A Jellyfin plugin that bridges audio playback to Logitech Media Server (LMS) for multi-room synchronized playback.
|
|
|
|
## Quick Install
|
|
|
|
Add the following repository URL to your Jellyfin server to install JellyLMS directly from the plugin catalog:
|
|
|
|
```
|
|
https://gitea.tourolle.paris/dtourolle/jellyLMS/raw/branch/master/manifest.json
|
|
```
|
|
|
|
**Steps:**
|
|
1. Go to **Dashboard** → **Plugins** → **Repositories**
|
|
2. Click **Add** and paste the URL above
|
|
3. Go to **Catalog** and find "JellyLMS"
|
|
4. Click **Install** and restart Jellyfin
|
|
|
|
## Overview
|
|
|
|
JellyLMS enables Jellyfin to stream audio to LMS, which acts as a multi-room speaker system. The architecture is:
|
|
|
|
- **Jellyfin** owns the library, queue, and playback intent
|
|
- **LMS** owns synchronized audio delivery to players (Squeezebox, piCorePlayer, etc.)
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ Jellyfin │ │ JellyLMS │ │ LMS │
|
|
│ │ │ Plugin │ │ │
|
|
│ ┌───────────┐ │ │ │ │ ┌───────────┐ │
|
|
│ │ Library │──┼────────►│ LmsApiClient │────────►│ │ Players │ │
|
|
│ │ (Audio) │ │ │ │ │ │ (Zones) │ │
|
|
│ └───────────┘ │ │ ┌───────────┐ │ │ └───────────┘ │
|
|
│ │ │ │ Session │ │ │ │
|
|
│ ┌───────────┐ │ │ │Controller │ │ │ ┌───────────┐ │
|
|
│ │ Queue │──┼────────►│ │ (State │ │────────►│ │ Sync │ │
|
|
│ │ │ │ │ │ Machine) │ │ │ │ Groups │ │
|
|
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
|
|
│ │ │ │ │ │
|
|
│ ┌───────────┐ │ │ ┌───────────┐ │ │ │
|
|
│ │ Playback │──┼────────►│ │ REST API │ │ │ │
|
|
│ │ Controls │ │ │ └───────────┘ │ │ │
|
|
│ └───────────┘ │ └─────────────────┘ └─────────────────┘
|
|
└─────────────────┘
|
|
```
|
|
|
|
## Features
|
|
|
|
- **Player Discovery**: Automatically discovers all LMS players/zones
|
|
- **Multi-Room Sync**: Create and manage sync groups for synchronized playback across multiple rooms
|
|
- **Playback Control**: Play, pause, stop, seek, and volume control via Jellyfin's "Play On" (cast) interface
|
|
- **Stream Bridging**: Generates audio stream URLs from Jellyfin for LMS to consume
|
|
- **Robust State Machine**: Ensures proper sequencing of playback operations with automatic retry and timeout handling
|
|
|
|
## Screenshots
|
|
|
|
### Cast to LMS Players
|
|
Select any LMS player directly from Jellyfin's "Play On" menu:
|
|
|
|

|
|
|
|
### Plugin Configuration
|
|
Configure LMS and Jellyfin server connections:
|
|
|
|

|
|
|
|
### Player Discovery
|
|
View discovered LMS players with their status and volume levels:
|
|
|
|

|
|
|
|
### Sync Group Management
|
|
Create and manage synchronized playback groups for multi-room audio:
|
|
|
|

|
|
|
|
## Requirements
|
|
|
|
- Jellyfin Server 10.10.0 or later
|
|
- .NET 9.0 Runtime
|
|
- Logitech Media Server (LMS) with JSON-RPC API enabled (default on port 9000)
|
|
|
|
## Playback Architecture
|
|
|
|
JellyLMS uses Jellyfin's native "Play On" (cast) interface to control LMS players. When you select an LMS player from Jellyfin's cast menu, playback is managed through a robust state machine that ensures reliable operation.
|
|
|
|
### State Machine
|
|
|
|
The playback controller uses a state machine to ensure proper sequencing of operations:
|
|
|
|
```
|
|
┌────────┐
|
|
│ Idle │ (device connected, no media)
|
|
└───┬────┘
|
|
│ Play command
|
|
▼
|
|
┌────────┐
|
|
┌────►│Loading │◄────┐
|
|
│ └───┬────┘ │
|
|
│ │ │ Seek (HTTP streaming
|
|
│ LMS confirms │ restarts stream)
|
|
│ mode="play" │
|
|
│ ▼ │
|
|
┌───────┐ │ ┌────────┐ │
|
|
│ Error │◄────┼─────│Playing │─────┘
|
|
└───┬───┘ │ └───┬────┘
|
|
│ │ │ Pause
|
|
retry │ ▼
|
|
│ │ ┌────────┐
|
|
└─────────┼─────│ Paused │
|
|
│ └───┬────┘
|
|
│ │ Seek (native LMS)
|
|
│ ▼
|
|
│ ┌────────┐
|
|
└─────│Seeking │
|
|
└────────┘
|
|
|
|
From any state: Stop → Stopped
|
|
```
|
|
|
|
### How Playback Works
|
|
|
|
1. **Cast Request**: User selects an LMS player from Jellyfin's "Play On" menu
|
|
2. **Loading**: Plugin sends play command to LMS and transitions to Loading state
|
|
3. **Confirmation**: Plugin polls LMS until playback is confirmed (mode="play")
|
|
4. **Playing**: Playback is active; progress is synced between Jellyfin and LMS
|
|
5. **Controls**: Play, pause, seek, and volume commands are forwarded to LMS
|
|
|
|
### Error Handling
|
|
|
|
The state machine includes automatic retry with exponential backoff:
|
|
- **Timeout errors**: Auto-retry up to 2 times (500ms → 1s delay)
|
|
- **Network errors**: Auto-retry up to 2 times
|
|
- **LMS errors**: No retry, transition to Error state
|
|
|
|
## Installation
|
|
|
|
### Manual Installation
|
|
|
|
1. Download the latest release or build from source
|
|
2. Copy `Jellyfin.Plugin.JellyLMS.dll` to your Jellyfin plugins directory:
|
|
- **Linux**: `~/.local/share/jellyfin/plugins/JellyLMS/`
|
|
- **Windows**: `%APPDATA%\jellyfin\plugins\JellyLMS\`
|
|
- **Docker**: `/config/plugins/JellyLMS/`
|
|
3. Restart Jellyfin
|
|
|
|
### Building from Source
|
|
|
|
```bash
|
|
# Clone the repository
|
|
git clone https://gitea.tourolle.paris/dtourolle/jellyLMS.git
|
|
cd jellyLMS
|
|
|
|
# Build
|
|
dotnet build Jellyfin.Plugin.JellyLMS.sln -c Release
|
|
|
|
# The DLL will be in:
|
|
# Jellyfin.Plugin.JellyLMS/bin/Release/net9.0/
|
|
```
|
|
|
|
## Configuration
|
|
|
|
1. Navigate to Jellyfin Dashboard → Plugins → JellyLMS
|
|
2. Configure the following settings:
|
|
|
|
| Setting | Description | Default |
|
|
|---------|-------------|---------|
|
|
| LMS Server URL | Full URL to your LMS server | `http://localhost:9000` |
|
|
| Jellyfin Server URL | URL where LMS can reach Jellyfin | `http://localhost:8096` |
|
|
| Connection Timeout | Timeout for LMS API calls (seconds) | `10` |
|
|
| Enable Auto Sync | Automatically sync players when creating groups | `true` |
|
|
| Default Player | MAC address of the default player | (none) |
|
|
| Use Direct File Path | Enable direct file access instead of HTTP streaming | `false` |
|
|
|
|
### Advanced Settings (State Machine)
|
|
|
|
| Setting | Description | Default |
|
|
|---------|-------------|---------|
|
|
| Loading Timeout | Max time to wait for LMS to start playback (seconds) | `5` |
|
|
| Seek Timeout | Max time to wait for seek to complete (seconds) | `3` |
|
|
| Transition Poll Interval | How often to poll LMS during state transitions (ms) | `300` |
|
|
| Max Auto Retries | Number of automatic retries for transient failures | `2` |
|
|
|
|
3. Click "Test Connection" to verify connectivity to LMS
|
|
4. Use "Discover Players" to see available LMS players
|
|
|
|
## API Endpoints
|
|
|
|
The plugin exposes REST API endpoints under `/JellyLms/` for player and sync group management.
|
|
|
|
**Note:** Playback control (play, pause, seek, volume) is handled through Jellyfin's native "Play On" (cast) interface, not through REST endpoints.
|
|
|
|
### Players
|
|
|
|
- `GET /JellyLms/Players` - List all LMS players
|
|
- `GET /JellyLms/Players/{mac}` - Get specific player details
|
|
- `POST /JellyLms/Players/{mac}/PowerOn` - Power on a player
|
|
- `POST /JellyLms/Players/{mac}/PowerOff` - Power off a player
|
|
- `POST /JellyLms/Players/{mac}/Volume` - Set player volume
|
|
|
|
### Sync Groups
|
|
|
|
- `GET /JellyLms/SyncGroups` - List all sync groups
|
|
- `POST /JellyLms/SyncGroups` - Create a new sync group
|
|
- `DELETE /JellyLms/SyncGroups/{masterMac}` - Dissolve a sync group
|
|
- `DELETE /JellyLms/SyncGroups/Players/{mac}` - Remove player from its sync group
|
|
|
|
### Utilities
|
|
|
|
- `POST /JellyLms/TestConnection` - Test LMS connectivity
|
|
- `GET /JellyLms/DiscoverPaths` - Discover file paths for direct file access configuration
|
|
|
|
## LMS Setup
|
|
|
|
Ensure your LMS server has the JSON-RPC API available. This is enabled by default and accessible at:
|
|
|
|
```
|
|
http://<lms-server>:9000/jsonrpc.js
|
|
```
|
|
|
|
The plugin communicates with LMS using the `slim.request` JSON-RPC method.
|
|
|
|
## Troubleshooting
|
|
|
|
### Cannot connect to LMS
|
|
|
|
1. Verify LMS is running and accessible at the configured URL
|
|
2. Check that the JSON-RPC endpoint responds: `curl http://localhost:9000/jsonrpc.js`
|
|
3. Ensure no firewall is blocking connections between Jellyfin and LMS
|
|
|
|
### Players not appearing
|
|
|
|
1. Ensure players are powered on and connected to LMS
|
|
2. Click "Discover Players" to refresh the player list
|
|
3. Check LMS web interface to verify players are visible there
|
|
|
|
### Audio not playing
|
|
|
|
1. Verify Jellyfin server URL is accessible from LMS server
|
|
2. Check that audio files are in a format supported by your LMS players
|
|
3. Ensure players are powered on (plugin can auto-power-on if configured)
|
|
|
|
## Known Limitations
|
|
|
|
### Seeking with HTTP Streaming
|
|
|
|
When using HTTP streaming (the default), LMS cannot seek within audio streams. To work around this, when you seek or cast from a specific position, JellyLMS restarts playback with a new transcoded stream that begins at the requested position. This means:
|
|
|
|
- **Seeking triggers a brief audio restart** rather than a smooth jump
|
|
- **Starting playback mid-track** uses transcoding (MP3 320kbps) instead of direct streaming
|
|
- **Playback from the beginning** uses direct/static streaming for best quality
|
|
|
|
This is a fundamental limitation of how LMS handles HTTP streams.
|
|
|
|
### Solution: Direct File Access
|
|
|
|
If your Jellyfin and LMS servers can both access the same storage (e.g., a NAS), you can enable **Direct File Access** mode in the plugin settings. This allows LMS to read files directly from disk, enabling:
|
|
|
|
- **Native smooth seeking** - no audio restart when scrubbing
|
|
- **Full quality playback** - no transcoding needed
|
|
- **Better performance** - no HTTP overhead
|
|
|
|
To configure, set the path mappings in the plugin settings:
|
|
- **Jellyfin Media Path**: The path prefix as Jellyfin sees your library (e.g., `/media/music`)
|
|
- **LMS Media Path**: The same location as LMS sees it (e.g., `/mnt/music` or `//nas/music`)
|
|
|
|
## Development
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
Jellyfin.Plugin.JellyLMS/
|
|
├── Plugin.cs # Main plugin entry point
|
|
├── PluginServiceRegistrator.cs # DI service registration
|
|
├── Configuration/
|
|
│ ├── PluginConfiguration.cs # Plugin settings
|
|
│ └── configPage.html # Dashboard configuration UI
|
|
├── Api/
|
|
│ └── JellyLmsController.cs # REST API endpoints (players, sync groups)
|
|
├── Services/
|
|
│ ├── ILmsApiClient.cs # LMS API interface
|
|
│ ├── LmsApiClient.cs # LMS JSON-RPC client
|
|
│ ├── LmsPlayerManager.cs # Player discovery & sync
|
|
│ ├── LmsSessionController.cs # Playback control (ISessionController)
|
|
│ ├── PlaybackStateMachine.cs # State machine for playback lifecycle
|
|
│ └── LmsStatusPoller.cs # Polls LMS to confirm state transitions
|
|
└── Models/
|
|
├── LmsPlayer.cs # Player model
|
|
├── LmsPlaybackSession.cs # Session state (incl. PlaybackState enum)
|
|
└── LmsApiModels.cs # JSON-RPC DTOs
|
|
```
|
|
|
|
### Building for Development
|
|
|
|
```bash
|
|
# Build in debug mode
|
|
dotnet build Jellyfin.Plugin.JellyLMS.sln
|
|
|
|
# Copy to Jellyfin plugins directory
|
|
cp Jellyfin.Plugin.JellyLMS/bin/Debug/net9.0/Jellyfin.Plugin.JellyLMS.dll \
|
|
~/.local/share/jellyfin/plugins/JellyLMS/
|
|
|
|
# Restart Jellyfin to load the plugin
|
|
```
|
|
|
|
## License
|
|
|
|
This plugin is licensed under the GPLv3. See [LICENSE](LICENSE) for details.
|
|
|
|
Due to how Jellyfin plugins work, when compiled into a binary, it links against Jellyfin's GPLv3-licensed NuGet packages, making the resulting binary GPLv3 licensed.
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome! Please open an issue or submit a pull request.
|
|
|
|
## Acknowledgments
|
|
|
|
- [Jellyfin](https://jellyfin.org/) - The Free Software Media System
|
|
- [Logitech Media Server](https://github.com/Logitech/slimserver) - Open source server for Squeezebox players
|