Emby: Shared Code, Separate Truths
Why sibling platforms still need their own verification
Emby: Shared Code, Separate Truths
Not in the HomeSeer store quite yet. It’s been running here every day, and the plan is to send it to the store as a beta.
By the time I got to Emby, I had already built the Plex and Jellyfin plugins.
And because Emby and Jellyfin share a common ancestry, this one ought to have been the easy one.
For the most part, it was.
But “for the most part” is doing a fair amount of work in that sentence.
The broad architecture carried over beautifully.
Sessions looked familiar.
The push model looked familiar.
The polling path looked familiar.
The HomeSeer-facing device model was already decided.
The interesting part was not how much stayed the same.
It was the handful of places where it did not.
Because large differences tend to announce themselves.
Small ones sit quietly until somebody assumes two related systems must still behave identically.
That is how afternoons disappear.
What carried over
On the Emby server I tested on August 21, 2026, the same general WebSocket subscription pattern used by Jellyfin worked here too.
The working socket path on that server was:
/socket
And the session subscription was the familiar:
SessionsStart "0,1500"
That tells the server we want session updates.
The returned data contained the full session list in the same useful shape as the ordinary sessions response.
Which meant the architecture from Jellyfin carried over very cleanly.
The WebSocket receives an update.
Polling retrieves an update.
Both converge on the same session model.
Then the same reconciliation logic.
Then the same HomeSeer publishing path.
One parser.
Two transports.
Exactly as intended.
But there is an important reason I am comfortable writing that down:
I tested it against an actual Emby server.
I did not simply inherit the Jellyfin implementation and assume the common ancestry meant the behavior must still match.
That distinction matters.
A shared history is evidence.
A live observation is considerably better evidence.
At least for the version sitting in front of me that day.
Where Emby quietly differs
The interesting pieces are the places where the resemblance stops.
They are small.
Which makes them precisely the kind worth documenting.
No message identifier
The Jellyfin WebSocket messages I was working with included a message identifier.
The Emby frames I captured did not.
They carried the message type and data I actually needed, but not that additional identifier.
Today, that changes absolutely nothing.
The plugin does not use the field.
No behavior depends on it.
So from the HomeSeer side, Plex, Jellyfin, and Emby can still look exactly the way I want them to.
But I wrote the difference down anyway.
Because the dangerous version of this story happens later.
Imagine someone changes the shared WebSocket handling code and starts using that identifier because:
Jellyfin always sends one.
The code works.
The tests against Jellyfin pass.
The change looks perfectly reasonable.
Then Emby quietly stops working.
Not because Emby changed.
Because somewhere along the way, an implementation detail from one sibling was promoted into a requirement for all of them.
That is exactly the sort of bug that is cheap to prevent now and irritating to rediscover later.
So the note stays.
The health check that lies
This was the more interesting difference.
A very obvious way to test whether an endpoint exists is:
Send a normal HTTP GET request.
If something answers sensibly, the endpoint exists.
If it returns 404:
Probably not.
Except that was not true here.
On the Emby server I tested, a plain GET to the socket path returned:
404
Which strongly suggests:
There is nothing here.
But perform an actual WebSocket connection with the proper upgrade handshake and the socket works perfectly.
The endpoint is real.
It simply is not an ordinary HTTP resource.
That means the obvious health check:
“GET the URL and see whether it exists”
produces exactly the wrong conclusion.
And I enjoy this kind of bug because everything about the failed reasoning looks sensible.
The question is reasonable.
The response is unambiguous.
The conclusion is wrong.
So WebSocket support is tested by attempting enough of a real WebSocket handshake to answer the actual question.
Not by asking an HTTP endpoint a question it never promised to answer.
A health check is only useful if it tests the thing you think it tests.
Documentation versus observation
This is also one of those places where the documentation itself reinforces the lesson.
Emby has documented WebSocket support for years, but examples and documentation across versions have not always described the connection URL in exactly the same way.
There have been references to /socket.
Older material also references /embywebsocket.
Current developer documentation describes constructing the WebSocket URL from the server address itself.
That does not make the protocol unusable.
It means I do not want a comment in the plugin saying:
“Emby WebSockets are always at this URL.”
What I can say confidently is:
This is the endpoint and handshake behavior I verified against the server version I tested.
That is stronger documentation than pretending history never happened.
One thing I deliberately did not claim
In the Jellyfin plugin, I measured the update cadence.
The subscription requested a 1.5-second interval.
The Jellyfin server running here consistently delivered session updates every:
2.0 seconds
It appeared to clamp the requested interval to its own minimum.
That was measured.
On Jellyfin.
I did not repeat that timing experiment on Emby.
Emby accepts the same SessionsStart "0,1500" subscription format.
Given the shared history, I would not be surprised if its timing behavior is similar.
But:
“I would not be surprised” is not a measurement.
So the Emby code does not quietly inherit a comment stating:
Updates arrive every two seconds.
Instead, it says that the two-second behavior was measured on Jellyfin and has not been independently timed here.
That sounds almost absurdly fussy.
It is also exactly the kind of distinction that gets lost when comments are copied between related projects.
Once the original context disappears, the copied comment becomes a “fact.”
And then a future developer, quite possibly me, starts debugging against something nobody actually measured.
I would rather leave him fewer traps.
There is not much of a war story here
Emby did not require me to write a tolerant HTTP parser because an embedded switch sends malformed headers.
It did not vanish behind Windows Firewall for half a day.
It did not change a firmware permission and suddenly refuse to press buttons.
It mostly behaved.
Which is excellent for software development and admittedly less useful if you are trying to write an exciting adventure story about a media server.
But I think there is still something important in this one.
The interesting part is the record of what is actually known.
Which behavior was verified directly against Emby?
Which behavior came from Jellyfin?
Which similarities are observed?
Which ones are simply plausible because the projects share history?
Those distinctions are all written down.
Because related systems create a very particular temptation:
Assume the sibling already answered the question.
Sometimes it did.
Sometimes it answered a slightly different question.
Shared architecture should carry knowledge, not folklore
One of the benefits of building a suite is that solutions should carry forward.
The general session model from Plex helped shape Jellyfin.
The push/poll architecture from Jellyfin carried naturally into Emby.
The same identity principles apply.
The same HomeSeer publishing model works.
The same user-facing device structure can be preserved.
That is exactly what shared architecture is supposed to buy.
Solve the general problem once.
Reuse the solution.
But there is an important line between:
sharing knowledge
and:
sharing assumptions.
Knowledge says:
“We verified this behavior on Jellyfin, and then verified the corresponding behavior independently on Emby.”
An assumption says:
“They came from the same codebase once, so I copied the comment.”
Those two things can look almost identical in source code.
Until one of them is wrong.
I already managed to prove the point elsewhere
Part of the reason I am being particularly careful about this distinction is that I was bitten by essentially the same problem recently.
A confident technical note existed in one project.
It propagated into another.
Nothing in its wording suggested uncertainty.
By the time it reached the second repository, the original context was gone.
It had become “true” through repetition.
And it turned out not to be true.
That is an uncomfortable reminder of how easily documentation can manufacture certainty.
Code can copy a bug.
Comments can copy an assumption.
Both can spread quite efficiently.
So I am trying to leave a more useful trail.
If I measured something, I say that I measured it.
If I observed it on a sibling platform, I say where.
If I verified it here, I say that.
If I have not verified it here, I say that too.
It costs almost nothing while the information is fresh.
It may save an afternoon later.
That seems like a good trade.
Deliberately identical where it matters
From HomeSeer’s perspective, Emby is intentionally shaped like its siblings:
Plex
Jellyfin
Emby
Same general concepts.
Same device structure.
Same event model.
Same page layout.
Same interpretation of:
Playing.
Paused.
Stopped.
Player identity.
Now playing.
The APIs underneath can differ as much as they like.
That should not force the user to relearn the plugin.
If someone switches media servers, the house has not suddenly changed what:
“Living Room is playing something”
means.
So the user-facing consistency is deliberate.
The implementation underneath is allowed to remember every place where the platforms disagree.
In fact, it must.
That is how you get a suite that behaves consistently without pretending its components are identical.
What I like about this one
There is no giant technical breakthrough hiding in the Emby plugin.
And I think that is actually the point.
The shared architecture worked.
The parser carried over.
The HomeSeer model carried over.
The push/poll design carried over.
The new work was mostly finding and recording the places where the assumptions stopped carrying over.
That is a healthy outcome.
A good abstraction should make the third implementation somewhat boring.
But “boring” should never become:
“We stopped checking.”
So the rule I take away from this one is simple:
Share solutions aggressively.
Share assumptions cautiously.
A suite gets stronger when its components share what they have learned.
It gets fragile when they merely share what somebody once believed.
And the difference is often nothing more glamorous than a careful note written while the answer is still fresh.