- Server (Node.js/Koa)
- /src/server/
- api.ts - Main server entry point
- routes/ - API routes
- services/ - Core services
- db/ - Database interactions
- /src/server/
-
PlayerService
- Manages video player instances
- Handles IPC communication with MPV
- Manages stream lifecycle
-
StreamManager
- Orchestrates multiple streams
- Manages screen configurations
- Handles queue management
-
QueueService
- Manages stream queues per screen
- Tracks watched streams
- Handles queue updates
- API Request → Koa Router → StreamManager → PlayerService
- PlayerService ↔ MPV Player (via IPC)
- StreamManager ↔ QueueService (via EventEmitter)
- Services:
camelCase.ts(e.g., playerService.ts) - Types:
camelCase.ts(e.g., streamTypes.ts) - Components:
PascalCase.svelte
- Services: camelCase
- Event handlers: handleEventName
- Callbacks: onEventName
- Private methods: _methodName
- Constants: UPPER_SNAKE_CASE
- Base types: PascalCase (e.g., StreamOptions)
- Service interfaces: IPascalCase (e.g., IPlayerService)
- Event types: EventPascalCase (e.g., StreamEvent)
- koa - Web framework
- winston - Logging
- node-fetch - HTTP client
- @twurple - Twitch API client
- holodex.js - Holodex API client
- Node.js built-ins
- External packages
- Project types
- Project services
- Project utilities
import { EventEmitter } from 'events'; // Node.js built-in
import Koa from 'koa'; // External package
import type { StreamOptions } from '../types/stream'; // Project types
import { logger } from './services/logger'; // Project services
import { formatTime } from './utils'; // Project utilities
4. **Design Patterns**
```markdown
# Design Patterns
## Service Pattern
- Services are singletons
- Services extend EventEmitter for pub/sub
- Services handle one specific domain
## Event-Driven Architecture
- Use EventEmitter for cross-service communication
- Define strict event types
- Document event payloads
## Error Handling
- Use typed errors
- Log errors with context
- Propagate errors to appropriate handlers
## Example Service Pattern
```typescript
export class ServiceName extends EventEmitter {
private static instance: ServiceName;
private constructor() {
super();
// initialization
}
public static getInstance(): ServiceName {
if (!ServiceName.instance) {
ServiceName.instance = new ServiceName();
}
return ServiceName.instance;
}
}
5. **API Structure**
```markdown
# API Structure
## RESTful Endpoints
Base URL: /api
### Streams
- GET /streams/active - List active streams
- POST /streams/start - Start a stream
- DELETE /streams/:screen - Stop stream on screen
- GET /streams/queue/:screen - Get queue for screen
### Screens
- POST /screens/:screen/enable - Enable screen
- POST /screens/:screen/disable - Disable screen
- GET /screens/:screen - Get screen info
### Player
- POST /player/command/:screen - Send command to player
- POST /player/volume/:target - Set volume
- POST /player/seek/:target - Seek in stream
## Response Format
```typescript
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
6. **Configuration**
```markdown
# Configuration
## Environment Variables
- HOLODEX_API_KEY - Holodex API key
- TWITCH_CLIENT_ID - Twitch client ID
- TWITCH_CLIENT_SECRET - Twitch client secret
- PORT - Server port (default: 3001)
## Config Files
- config/
- player.json - Player settings
- streams.json - Stream configurations
- favorites.json - Favorite channels
- mpv.json - MPV player settings
- streamlink.json - Streamlink settings
## Example Config
```json
{
"player": {
"preferStreamlink": false,
"defaultQuality": "best",
"defaultVolume": 0,
"maxStreams": 2,
"screens": []
}
}
7. **Tiling Window Manager & Wayland Support**
```markdown
# Tiling Window Manager & Wayland Support
LiveLink automatically detects your display server (X11/Wayland) and window manager type to optimize MPV player behavior.
## Automatic Detection
The PlayerService detects:
- **Display Server**: X11 or Wayland
- **Window Manager Type**: Tiling (i3, sway, hyprland, etc.) or Floating
- **GPU Context**: Automatically sets `--gpu-api` and `--gpu-context` based on display server
## Behavior on Different WMs
### Tiling Window Managers (i3, sway, hyprland, bspwm, etc.)
On tiling WMs, LiveLink:
- Uses `--class=livelink-screen-{N}` instead of geometry positioning
- Sets `--title=LiveLink-Screen-{N}` for window identification
- Applies window size hints (may be respected by some WMs)
- **Does not use** `--geometry=+x+y` positioning (ignored by tiling WMs)
#### i3/i3-gaps Configuration
Add to your `~/.config/i3/config`:
```bash
# Float LiveLink windows
for_window [class="livelink-screen-*"] floating enable
# Set specific geometry for each screen
for_window [class="livelink-screen-1"] resize set 1280 720, move position 0 0
for_window [class="livelink-screen-2"] resize set 1280 720, move position 1280 0
Add to your ~/.config/sway/config:
# Float LiveLink windows
for_window [app_id="livelink-screen-*"] floating enable
# Set specific geometry for each screen
for_window [app_id="livelink-screen-1"] resize set 1280 720, move position 0 0
for_window [app_id="livelink-screen-2"] resize set 1280 720, move position 1280 0Add to your ~/.config/hypr/hyprland.conf:
# LiveLink Auto-Generated Configuration
# Float and position LiveLink windows
# Screen 1
windowrule {
name = livelink-screen-1
match:class = livelink-screen-1
float = on
size = 1920 1080
move = 1366 0
}
# Screen 2
windowrule {
name = livelink-screen-2
match:class = livelink-screen-2
float = on
size = 1366 768
move = 0 312
}
# Or use anonymous rule syntax for all screens:
# windowrule = float on, match:class livelink-screen-.*
# windowrule = size 1280 720, match:class livelink-screen-.*
# windowrule = move 0 0, match:class livelink-screen-1
# windowrule = move 1280 0, match:class livelink-screen-2Note: For Hyprland versions < 0.45, use the legacy windowrulev2 syntax:
windowrulev2 = float, class:livelink-screen-1
windowrulev2 = size 1920 1080, class:livelink-screen-1
windowrulev2 = move 1366 0, class:livelink-screen-1Tips:
- Use
hyprctl clientsto see window class/title information - Rules are evaluated top to bottom
- Named rules take precedence over anonymous rules
- Use
match:classfor RegEx matching
Add to your ~/.config/bspwm/bspwmrc:
# Float LiveLink windows
bspc rule -a livelink-screen-* state=floating
# Set specific geometry for each screen
bspc rule -a livelink-screen-1 state=floating rectangle=1280x720+0+0
bspc rule -a livelink-screen-2 state=floating rectangle=1280x720+1280+0Add to your ~/.xmonad/xmonad.hs:
-- Float LiveLink windows
manageHook = composeAll
[ className =? "livelink-screen-1" --> doFloat (W.RationalRect l t w h)
, className =? "livelink-screen-2" --> doFloat (W.RationalRect l t w h)
]
where
l = 0 -- left (0 = 0% from left)
t = 0 -- top (0 = 0% from top)
w = 1280 -- width in pixels
h = 720 -- height in pixelsAdd to your ~/.config/awesome/rc.lua:
-- Float LiveLink windows and set geometry
awful.rules.rules = {
{
rule = { class = "livelink-screen-.*" },
properties = { floating = true },
callback = function(c)
if c.class == "livelink-screen-1" then
c:geometry({ x = 0, y = 0, width = 1280, height = 720 })
elseif c.class == "livelink-screen-2" then
c:geometry({ x = 1280, y = 0, width = 1280, height = 720 })
end
end
}
}Add to your ~/.config/qtile/config.py:
from libqtile import hook
from libqtile.backend.x11 import window
@hook.subscribe.client_new
def float_livelink(client):
if "livelink-screen-" in client.name or "livelink-screen-" in client.get_wm_class():
client.floating = True
if "livelink-screen-1" in client.name:
client.cmd_set_position(0, 0)
client.cmd_set_size(1280, 720)
elif "livelink-screen-2" in client.name:
client.cmd_set_position(1280, 0)
client.cmd_set_size(1280, 720)Add to your config.h and recompile:
static const Rule rules[] = {
// class instance title tag mask isfloating monitor
{ "livelink-screen-1", NULL, NULL, 0, True, -1 },
{ "livelink-screen-2", NULL, NULL, 0, True, -1 },
};
// Then use xdotool or similar to position windows after they appearAdd to your ~/.config/openbox/rc.xml:
<applications>
<application class="livelink-screen-1">
<position force="yes">
<x>0</x>
<y>0</y>
</position>
<size>
<width>1280</width>
<height>720</height>
</size>
</application>
<application class="livelink-screen-2">
<position force="yes">
<x>1280</x>
<y>0</y>
</position>
<size>
<width>1280</width>
<height>720</height>
</size>
</application>
</applications>Create a KWin script at ~/.local/share/kwin/scripts/livelink-float/metadata.desktop:
[Desktop Entry]
Name=LiveLink Float
Comment=Float LiveLink windows
Type=Service
X-KDE-ServiceTypes=KWin/Script
X-KDE-PluginInfo-Author=YourName
X-KDE-PluginInfo-Name=livelink-float
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-EnabledByDefault=trueAnd ~/.local/share/kwin/scripts/livelink-float/code/main.js:
function init() {
workspace.clientAdded.connect(function(client) {
if (client.resourceClass.includes("livelink-screen-")) {
client.keepAbove = true;
// Note: KWin doesn't support geometry setting via script
// Use window rules instead
}
});
}Or use KDE Window Rules (System Settings → Window Management → Window Rules):
- Add rule for class
livelink-screen-* - Set "Size & Position" → "Force" → specify geometry
Instead of manually writing WM rules, use the included config generator:
# Using npm scripts (recommended)
npm run wm:config hyprland # Generate for specific WM
npm run wm:config:all # Generate for all WMs
npm run wm:config i3 -- --dry-run # Preview without saving
# Or run the script directly
node scripts/generate-wm-config.js hyprland
node scripts/generate-wm-config.js --allThe script reads your config/player.json and generates appropriate WM rules.
Generated configs are saved to generated-wm-configs/ directory.
Supported Window Managers:
- i3 / i3-gaps
- Sway
- Hyprland (v0.45+ with
windowrule, older versions usewindowrulev2) - BSPWM
- Xmonad
- AwesomeWM
- Qtile
- Openbox
- KWin (KDE)
- Wayfire
See generated-wm-configs/README.md for detailed usage instructions.
- Uses
--gpu-api=wayland --gpu-context=wayland - Applies full geometry positioning
- Uses
--class=livelink-screen-{N}for identification
On X11 with compositing WMs (GNOME, KDE, XFCE, etc.):
- Uses
--gpu-api=x11 --gpu-context=x11 - Applies full geometry positioning with
--geometry=WxH+x+y
LiveLink reads configuration from JSON files in the config/ directory:
This file contains global MPV player settings. All MPV options can be specified here.
{
"vo": "gpu",
"hwdec": "auto-copy-safe",
"priority": "high",
"cache": true,
"cache-secs": 60,
"demuxer-max-bytes": "800M",
"ytdl-format": "bestvideo[height<=?1080]+bestaudio/best"
}Important: gpu-context is set automatically based on your display server:
- On Wayland: Always uses
--gpu-context=wayland - On X11: Always uses
--gpu-context=x11 - If
vo=gpuis set with X11 context on Wayland, it's automatically changed tovo=gpu-next - Note:
gpu-apioption is not used for better MPV version compatibility
This file contains Streamlink settings and can also override MPV settings when using streamlink.
{
"path": "streamlink",
"options": {
"twitch-disable-hosting": true,
"twitch-disable-ads": true,
"stream-timeout": 60,
"hls-live-edge": 3
},
"http_header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept-Language": "en-US,en;q=0.9"
},
"mpv": {
"vo": "gpu",
"hwdec": "auto"
},
"args": ["--low-latency"]
}Structure:
path: Path to streamlink executableoptions: Streamlink configuration options (key-value pairs)http_header: HTTP headers to send with requestsmpv: MPV-specific settings when launched by streamlink (merged with mpv.json)args: Additional command-line arguments for streamlink
When both mpv.json and streamlink.json have MPV settings:
mpv.jsonsettings take precedence (global config)streamlink.json mpvsettings are applied only if not in mpv.json- Display server overrides (
gpu-api,gpu-context) always take final precedence
If automatic detection doesn't work, you can override settings in config/mpv.json:
{
"gpu-api": "wayland",
"gpu-context": "wayland",
"vo": "gpu",
"hwdec": "auto-copy-safe"
}Or for X11:
{
"gpu-api": "x11",
"gpu-context": "x11",
"vo": "gpu",
"hwdec": "auto-copy-safe"
}- Tiling WM: Configure your WM rules as shown above
- Wayland: Some compositors may ignore positioning - check compositor settings
- X11: Ensure your WM supports geometry hints
- Ensure you have
mpvbuilt with Wayland support - Check that
WAYLAND_DISPLAYenvironment variable is set - Try setting
SDL_VIDEODRIVER=waylandin your environment - If using
vo=gpuwith X11 context in mpv.json, it will be automatically changed tovo=gpu-next
- Verify WM class with
xprop(X11) orwayland-info(Wayland) - Check your WM configuration syntax
- Reload WM configuration after changes
- Enable VSync in your compositor settings
- For Hyprland:
general:gaps_in = 0can help - Consider using
vo=gpuwithgpu-context=wayland
- Check that mpv.json is in the
config/directory - Verify JSON syntax is valid (use a JSON validator)
- Note that
gpu-apiandgpu-contextare always overridden for compatibility - Check logs for "MPV args" to see what arguments are actually being used
- Ensure streamlink.json is in the
config/directory - Check that options are in the correct format (boolean vs string)
- HTTP headers should be in the
http_headerobject - Additional CLI args go in the
argsarray
If settings from mpv.json and streamlink.json conflict:
- mpv.json takes precedence for global MPV settings
- streamlink.json mpv settings only apply when not in mpv.json
- Display server detection always overrides gpu-api/gpu-context
LiveLink sets these automatically based on detection:
DISPLAY- X11 display (X11 only)XAUTHORITY- X11 authentication (X11 only)WAYLAND_DISPLAY- Wayland display (Wayland only)DBUS_SESSION_BUS_ADDRESS- D-Bus session (both)SDL_VIDEODRIVER-x11orwaylandbased on detection