Event Reference
The plugin exposes one streaming endpoint per server. Everything below applies to both Jellyfin and Emby unless a difference is called out.
The Endpoint
| Server | URL | Auth |
|---|---|---|
| Jellyfin | GET http://your-jellyfin:8096/api/sse/events | Any token Jellyfin accepts for its REST API (see below) |
| Emby | GET http://your-emby:8096/emby/sse/events | X-Emby-Token: YOUR_API_KEY header |
Jellyfin accepts the token four ways, same as the rest of its API: an Authorization: MediaBrowser Token="..." header, an X-Emby-Token header, an X-MediaBrowser-Token header, or an api_key query parameter. The query parameter matters for browser EventSource, which can’t set headers. But tokens in URLs end up in access logs, so prefer a header anywhere you control the client.
Wire Format
Standard Server-Sent Events: a named event, a single-line JSON data payload, a blank line between events. Null fields are omitted from the JSON.
event: playing
data: {"sessionId":"abc123","itemId":"def456","userId":"user1","state":"playing","positionTicks":0}
event: ping
data: {}positionTicks is in .NET ticks (100-nanosecond units, 10,000,000 per second), matching what the Jellyfin and Emby APIs report elsewhere.
Connection Events
hello
Sent once, immediately on connect. Confirms you’re talking to the plugin and tells you which server and plugin version.
event: hello
data: {"version":"0.4.0.0","server":"jellyfin"}| Field | Meaning |
|---|---|
version | Plugin version |
server | jellyfin or emby |
ping
Keepalive, sent every 30 seconds with an empty {} payload. Useful for detecting a dead connection and for keeping intermediaries (Cloudflare, reverse proxies) from timing out an idle stream.
Playback Events
| Event | When |
|---|---|
playing | Playback started |
progress | Position update during playback |
paused | Playback paused |
stopped | Playback stopped |
All four carry the same fields:
| Field | Meaning |
|---|---|
sessionId | Device session ID; matches what /Sessions returns, not the per-playback PlaySessionId |
itemId | ID of the item being played |
userId | User who is playing it |
state | playing, paused, or stopped |
positionTicks | Playback position in ticks |
playedToCompletion | stopped only; whether the item finished |
Notes:
progressevents pass through at whatever rate the media server reports them, typically every 5–10 seconds. The plugin does not throttle them.- Theme music and local trailer playback are filtered out, so you won’t get
playingevents every time someone browses a menu. - A pause is its own
pausedevent. You don’t need to watchprogresspayloads for a state flag.
Session Events
| Event | Fields | When |
|---|---|---|
session.start | sessionId, userId | A device session connected |
session.end | sessionId, userId | A device session disconnected |
Sessions are device connections, not playback. A client can hold a session open for hours without playing anything.
Library Events
| Event | Fields | When |
|---|---|---|
library.item.added | itemId, itemType, parentId | An item finished being added to a library |
library.item.removed | itemId, itemType, parentId | An item was removed |
Events fire once per changed item, as it happens: no batching, no scheduled-task delay. Theme media and virtual placeholder items (missing episodes, for example) are filtered out.
parentId is the item’s immediate parent: a season for an episode, a library folder for a top-level movie or series. It is not necessarily the library root.
Platform differences worth knowing:
- IDs differ by server. Library event IDs match what each server’s own REST API reports: the 32-character GUID form on Jellyfin, the numeric internal ID on Emby. Emby playback events keep the GUID
itemIdthey have always had. - Emby also raises folder events. A new movie’s directory arrives as
itemType: "Folder"alongside the movie itself, and deleting a directory fires one removed event for the folder, not one per child. Don’t assume added/removed pairs match one-to-one on Emby; treat a removal as a cue to re-query the library.
Scheduled Task Events
| Event | Fields | When |
|---|---|---|
task.started | taskId, taskName, taskCategory | A scheduled task began running |
task.progress | taskId, taskName, progress | Progress update; throttled to 1% or 2 seconds per task, whichever comes first |
task.completed | taskId, taskName, state | Task finished; state is the completion status |
This covers library scans, metadata refreshes, and every other task in the server’s scheduled task list, including a live progress percentage for a running library scan.
Server Stats
server.stats
A CPU and memory sample, sent every 6 seconds while at least one client is connected.
event: server.stats
data: {"at":1786151199,"hostCpuUtilization":3.257,"processCpuUtilization":0.622,"hostMemoryUtilization":30.042,"processMemoryUtilization":0.548}| Field | Meaning |
|---|---|
at | Unix timestamp (seconds) of the sample |
hostCpuUtilization | Whole-machine CPU use, percent |
processCpuUtilization | The media server process’s CPU use, percent |
hostMemoryUtilization | Whole-machine memory use, percent |
processMemoryUtilization | The media server process’s memory use, percent |
Host values are read from /proc, so they appear on Linux hosts only and are omitted elsewhere. Inside a container, /proc reports the host machine, which is usually what you want. This event is what powers Tracearr’s Server Resources charts for Jellyfin and Emby; neither server exposes these numbers through its own API.
Delivery and Buffering
- Events broadcast to every connected client. There is no per-connection filtering.
- Each client gets a bounded buffer of 512 events. A client that falls behind is disconnected rather than silently losing events. Reconnect and resync from
/Sessions(and your library, if you track it) to catch up. - Any disconnect means missed events. Treat the stream as a live signal, not a durable queue: on reconnect, poll once to re-establish state, then go back to listening.
New event types get added over time (the changelog lists them per release). Unknown event names are safe to ignore, so a client that only handles the events it knows about keeps working across plugin updates.