EmbyTranscoder is a lightweight Go reverse proxy that adds local FFmpeg HLS transcoding fallback for Emby and Jellyfin clients.
It is intentionally narrow: normal API traffic is forwarded to the upstream server, while selected clients can receive a proxy-provided HLS TranscodingUrl when they request PlaybackInfo.
- Native Linux-friendly Go binary.
- Transparent reverse proxy for ordinary Emby/Jellyfin requests.
- Client profile matching by
User-AgentandX-Emby-Authorization. - PlaybackInfo rewriting for matched profiles.
- Local FFmpeg HLS sessions under
/streambridge/transcode/. - Audio track selection through Emby
AudioStreamIndex, with local transcode restart on audio changes. - Playback lifecycle tracking through Emby
/Sessions/Playing*check-ins plus HLS access. - Conservative output target: H.264 video, AAC audio, HLS MPEG-TS segments.
- Video output is capped at 1920x1080 and keeps aspect ratio.
- PlaybackInfo rewrite prewarms the transcode session before the first playlist request.
- FFmpeg uses low-latency startup and GOP settings to cut first-segment delay.
Not included: virtual libraries, RSS, cover generation, scraping, database storage, or a management UI.
go run ./cmd/emby-transcoder -config config.example.jsonPoint a client at the proxy listen address, for example http://linux-host:8097.
The published linux/amd64 image is available as tces1/emby_transcoder:latest.
cd docker
mkdir -p data/transcode
cp config/config.json config/config.local.jsonEdit docker/config/config.local.json before starting:
- set
upstream.urlto your Emby or Jellyfin server - set
server.public_urlif clients reach the proxy through another reverse proxy - leave
server.debugasfalsefor concise logs, or set it totruefor detailed diagnostics - set
transcode.hardware_decodetovaapion Linux hosts with Intel or AMD/dev/driVAAPI support - VAAPI mode first tries
vaapi-full(scale_vaapiGPU scaling plush264_vaapiencoding), then falls back tovaapi-encode(CPU scaling plush264_vaapi) when GPU scaling is unsupported - startup will probe VAAPI availability, including device initialization and
h264_vaapi, and fail startup if the device, driver, or ffmpeg support is missing
Update docker/docker-compose.yml to mount the local config file if you use config.local.json:
volumes:
- ./config/config.local.json:/app/config/config.json:ro
- ./data/transcode:/var/lib/emby-transcoder/transcodeStart or update the service:
docker compose pull
docker compose up -d
docker compose logs -fStop the service:
docker compose downgo build ./cmd/emby-transcoderCopy config.example.json and change the upstream URL:
{
"server": {
"listen": ":8097",
"public_url": "",
"debug": false
},
"upstream": {
"url": "http://127.0.0.1:8096"
},
"transcode": {
"enabled": true,
"ffmpeg_path": "/usr/bin/ffmpeg",
"temp_dir": "/var/lib/emby-transcoder/transcode",
"hardware_decode": "",
"hardware_device": "/dev/dri/renderD128",
"max_sessions": 2,
"buffer_pause_seconds": 300,
"buffer_resume_seconds": 120,
"segment_seconds": 2,
"segment_retention_seconds": 300,
"idle_timeout_seconds": 60
}
}Leave public_url empty when clients connect directly to EmbyTranscoder. Set it when EmbyTranscoder sits behind another reverse proxy.
Leave debug as false for concise action-level logs. Set it to true when you want detailed TRACE_SWITCH and request-level diagnostics.
Set hardware_decode to vaapi to enable VAAPI hardware transcoding. The default hardware_device is /dev/dri/renderD128.
Startup prefers vaapi-full; if scale_vaapi fails, it falls back to vaapi-encode so H.264 encoding still stays on the GPU. If the device, driver, or h264_vaapi probe fails, startup stops with an error.
EmbyTranscoder keeps local FFmpeg sessions tied to Emby playback check-ins:
POST /Sessions/Playingand/Sessions/Playing/Progressupdate local playback state.POST /Sessions/Playing/Stoppedimmediately stops the matching local FFmpeg session.- HLS playlist and segment requests refresh media activity.
segment_secondscontrols HLS segment duration; default2balances startup latency with segment count, while1is fastest and higher values reduce disk churn.- When transcoded media gets more than
buffer_pause_secondsahead of playback, FFmpeg is paused. - When buffered media falls back under
buffer_resume_seconds, FFmpeg resumes. - Segments older than
segment_retention_secondsbehind the current playback position are deleted from the local cache. - If neither playback activity nor HLS access arrives before
idle_timeout_seconds, the idle reaper stops the session. - A new
master.m3u8request with a different upstream stream URL, such as a seek with a differentStartTimeTicks, restarts the local session.