mickle026 650 Posted September 27, 2025 Posted September 27, 2025 (edited) I have created a web socket , working in client (server - client - server routing) as a proof of concept. Took me absolutely ages (approx. 4 years) to finally get this working! Here is the code See Github: WebSocket Demo and Code Sources Emby WebSocket Demo Plugin (4.8.11) This tutorial walks you through creating a minimal WebSocket demo plugin for Emby Server 4.8.11.0. It adds a custom channel named websocketdemo, a dashboard test page, and demonstrates sending JSON messages from the client to the server and back. What It Does Dashboard button → sends a ping JSON payload over Emby’s WebSocket. Server handler (WebSocketHandler) → deserializes, logs, and replies with a pong JSON payload. Dashboard page → logs both the sent request and the server’s reply in real time. Plugin File Structure WebSocketDemo/ ├─ Plugin.cs ├─ WebSocket/ │ └─ WebSocketHandler.cs ├─ WebUI/ │ ├─ WebSocketTest.html │ └─ WebSocketTest.js ├─ Properties/ │ └─ AssemblyInfo.cs └─ WebSocketDemo.csproj 🛠 Step 1 — Plugin.cs Registers the dashboard test page and the JS controller. Notice how each page has a distinct Name (HTML vs JS). public override IEnumerable<PluginPageInfo> GetPages() { return new[] { new PluginPageInfo { Name = "WebSocketTest", EmbeddedResourcePath = GetType().Namespace + ".WebUI.WebSocketTest.html" }, new PluginPageInfo { Name = "WebSocketTestjs", EmbeddedResourcePath = GetType().Namespace + ".WebUI.WebSocketTest.js" } }; } 🛠 Step 2 — WebSocketHandler.cs Implements IWebSocketListener. Emby automatically discovers and hooks it into the WebSocket system. public class WebSocketHandler : IWebSocketListener { private readonly IJsonSerializer _json; private readonly ILogger _logger; public string Name => "websocketdemo"; public WebSocketHandler(IJsonSerializer json, ILogger logger) { _json = json; _logger = logger; } public async Task ProcessMessage(WebSocketMessageInfo message) { _logger.Info("[websocketdemo] Incoming raw: {0}", message.Data); JsonMessage msg; try { msg = _json.DeserializeFromString<JsonMessage>(message.Data); } catch (Exception ex) { _logger.ErrorException("[websocketdemo] Failed to parse JSON", ex); return; } if (msg?.Type == "ping") { var response = new JsonMessage { Type = "pong", UserId = msg.UserId, Payload = "Server time: " + DateTime.UtcNow.ToString("HH:mm:ss") }; var reply = new WebSocketMessage<string> { MessageType = Name, Data = _json.SerializeToString(response) }; await message.Connection.SendAsync(reply, CancellationToken.None); _logger.Info("[websocketdemo] Sent PONG back to client {0}", message.Connection.Id); } } } 🛠 Step 3 — WebSocketTest.html Defines a dashboard page with Emby’s data-controller attribute pointing to our JS file. <div id="WebSocketTestPage" data-role="page" class="page type-interior pluginConfigurationPage" data-controller="__plugin/WebSocketTestjs"> <div class="content-primary"> <h1> WebSocket Test</h1> <p>This page tests the demo WebSocket channel.</p> <button id="btnPing" is="emby-button" class="raised button-submit block">Send Ping</button> <textarea id="logBox" style="width:100%;height:200px;"></textarea> </div> </div> 🛠 Step 4 — WebSocketTest.js The correct Emby way is to subscribe to the generic "message" event and filter on MessageType. define([], function () { return function (view) { var btn, logBox; function log(msg) { var ts = new Date().toLocaleTimeString(); logBox.value += "[" + ts + "] " + msg + "\n"; logBox.scrollTop = logBox.scrollHeight; } function onWebSocketMessage(e, msg) { if (msg && msg.MessageType === "websocketdemo") { try { var data = JSON.parse(msg.Data || "{}"); log(" Response: " + JSON.stringify(data)); } catch (ex) { log(" Raw: " + (msg.Data || "")); } } } view.addEventListener("viewshow", function () { btn = view.querySelector("#btnPing"); logBox = view.querySelector("#logBox"); log("WebSocketTest loaded, ready."); Events.on(ApiClient, "message", onWebSocketMessage); btn.addEventListener("click", function () { log("Sending ping → websocketdemo…"); var payload = { Type: "ping", UserId: ApiClient.getCurrentUserId() || "", Payload: "Hello from dashboard" }; ApiClient.sendMessage("websocketdemo", JSON.stringify(payload)); }); }); view.addEventListener("viewdestroy", function () { Events.off(ApiClient, "message", onWebSocketMessage); }); }; }); Flowchart — Message Cycle digraph G { rankdir=LR; node [shape=box, style=rounded, fillcolor="#E3F2FD", color="#90CAF9", fontcolor="#000000", fontname="Arial", penwidth=1.2]; ClientJS [label="Dashboard JS\n(ApiClient.sendMessage)"]; EmbyWS [label="Emby WebSocket\nServer Core"]; PluginHandler [label="WebSocketHandler\n(ProcessMessage)"]; Response [label="WebSocketMessage<string>\n(pong JSON)"]; ClientLog [label="Dashboard Log\n(JSON parsed)"]; ClientJS -> EmbyWS [label="send {Type:'ping'}"]; EmbyWS -> PluginHandler [label="dispatch websocketdemo"]; PluginHandler -> Response [label="serialize pong"]; Response -> EmbyWS [label="Connection.SendAsync()"]; EmbyWS -> ClientLog [label="deliver {Type:'pong'}"]; } Pitfalls & Mistakes to Avoid Wrong event: ApiClient.on("websocketdemo", …) → not in Emby. Use Events.on(ApiClient, "message", …) and filter on MessageType. Wrong server calls: methods like SendMessageToUser appear in online docs but don’t exist in Emby. Use message.Connection.SendAsync(WebSocketMessage<T>, CancellationToken). Wrong plugin overrides: Init, GetServices are not valid in Emby 4.8.11 plugins. JS/HTML mismatch: make sure HTML page data-controller="__plugin/WebSocketTestjs" points to the JS file, not the HTML. Serialization errors: always send stringified JSON from JS, not raw objects. Conclusion This minimal plugin proves: Emby plugins can register their own WebSocket channels. You can send arbitrary JSON between dashboard pages and the server. Edited September 27, 2025 by mickle026
Amything 122 Posted September 27, 2025 Posted September 27, 2025 Looks interesting! What kind of use case are you thinking for this?
mickle026 650 Posted September 27, 2025 Author Posted September 27, 2025 The intended purpose was real-time updates of tasks running in some of my plugins, so now I can send any data, you can send images, prebuilt html and insert it into pages, thus providing feature rich dashboard pages, not relying on polling the sever and temperamental timers that have to be stopped and restarted on page navigations, or changed tabs. I have several other uses in development , where i can receive TV remote or Gamepad controls. Honestly the possibilities are only limited by your mind, or if Luke shuts this down. Its much easier in Jellyfin though ....
mickle026 650 Posted September 28, 2025 Author Posted September 28, 2025 1 hour ago, Luke said: Why would we shut this down? I hope you don't, but I can see potential for it to be abused and was wondering if that is why its been so hard to get this information. Its central to my main projects, so I really want it to stay.
Luke 42077 Posted September 28, 2025 Posted September 28, 2025 AutoOrganize has similar sample code where code on the server notifies the client-side javascript: https://github.com/MediaBrowser/Emby.AutoOrganize
mickle026 650 Posted September 29, 2025 Author Posted September 29, 2025 (edited) New demo up, no endpoints solely sockets This fully demonstrates , how to get values out of the json reply from the websocket and use them for a html progress bar. Corrupt Image Repair (Demo Teaching Plugin) This Emby plugin demonstrates how to perform a non-destructive image corruption scan using WebSockets for real-time progress updates. It was adapted from the WebSocketDemo scaffold and extended to scan library images (Primary, Backdrop, Logo, Thumb, etc.) without deleting or altering them. Features Real-time WebSocket communication (progress + status updates) Dummy (non-destructive) scan mode – images are checked but never modified Progress bar + log output UI in Emby Dashboard Buttons to Ping, Start Scan, and Stop Scan Teaching scaffold for learning Emby plugin development with WebSockets Screenshot Here’s what the plugin looks like inside the Emby Dashboard: Project Structure CorruptImageRepair/ ├── Controllers/ │ └── ImageRepairController.cs ├── Services/ │ └── ImageRepairScanService.cs ├── Dashboard/ │ ├── corruptionrepair.html │ └── corruptionrepair.js ├── ImageFunctions.cs ├── WebSocket.cs └── Plugin.cs How to Build Open solution in Visual Studio 2022 (Emby SDK 4.8.11). Build the project in Release mode. Copy the .dll output into your Emby plugins/ directory. Restart Emby Server. Usage Navigate to Dashboard → Plugins → Corrupt Image Repair. Click Start Scan to run a dummy image validation scan. Watch the progress bar and live log output update in real time. Stop anytime with Stop Scan. 🛠 Teaching Notes Uses Emby’s built-in ISessionManager.SendMessage for WebSocket events. Demonstrates how to build bi-directional communication between server & client. Great starting scaffold for plugins that need live feedback. License MIT License – free to use, modify, and learn from. Mickle026/Emby-Demo-CorruptImageRepair: Demo Using Websockets without using Endpoints - Non Destructive Edited September 29, 2025 by mickle026 1 1
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 (edited) @mickle026I have the plugin up and running and it works great. You are a really good C# programmer. This plugin is a great teaching tool. However, I do have a few questions that will sharpen my modest programming skills. 1) in myTV when I click on the myTV tuner it gives me the view for the plugins HTML page. In your case when you click on the plugin icon itself you get the plugins HTML page. When I click the icon for myTV I get a message saying that no settings are defined. Is it possible to do both? When someone clicks the myTV icon I would like to explain how to bring up the plugin. 2) you say the plugin "Uses Emby’s built-in ISessionManager.SendMessage for WebSocket events." I don't directly see you using "ISessionManage." Instead, I see you using "Event.on and Event.off" to attach "onWebSocketMessage" to the event. Did I miss Event being defined? This is so exciting for me I have learned so much from this plugin and in the days to come I will learn more. Thank you for sharing it with me. Vic Edited September 30, 2025 by VicMoore
mickle026 650 Posted September 30, 2025 Author Posted September 30, 2025 (edited) 1 hour ago, VicMoore said: @mickle026I have the plugin up and running and it works great. You are a really good C# programmer. This plugin is a great teaching tool. However, I do have a few questions that will sharpen my modest programming skills. 1) in myTV when I click on the myTV tuner it gives me the view for the plugins HTML page. In your case when you click on the plugin icon itself you get the plugins HTML page. When I click the icon for myTV I get a message saying that no settings are defined. Is it possible to do both? When someone clicks the myTV icon I would like to explain how to bring up the plugin. 2) you say the plugin "Uses Emby’s built-in ISessionManager.SendMessage for WebSocket events." I don't directly see you using "ISessionManage." Instead, I see you using "Event.on and Event.off" to attach "onWebSocketMessage" to the event. Did I miss Event being defined? This is so exciting for me I have learned so much from this plugin and in the days to come I will learn more. Thank you for sharing it with me. Vic Using ISessionManager to send back to the user that sent the message public class ImageRepairController : IService { private readonly ISessionManager _sessionManager; public ImageRepairController(ISessionManager sessionManager) { _sessionManager = sessionManager; } public async Task<object> Post(ImageRepairRequest request) { if (string.Equals(request.Name, "ping", StringComparison.OrdinalIgnoreCase)) { await _sessionManager.SendMessageToUserDeviceSessions( request.DeviceId, "imagecorruptrepair", "PONG from server", CancellationToken.None ); } else if (string.Equals(request.Name, "startscan", StringComparison.OrdinalIgnoreCase)) { await _sessionManager.SendMessageToUserDeviceSessions( request.DeviceId, "imagecorruptrepair", "Scan requested (stub)", CancellationToken.None ); } return "ok"; } } Yes you can add dashboard UI pages I have uploaded a basic plugin template with dashboard UI page here Mickle026/Emby-Demo-WebUI-BasicPluginStructure: A Plugin Scaffold - Basic Emby Plugin Structure Study this demo scaffold too , because incorrect linking of the pages and namespaces is by far the biggest headache in plugin development of dashboard ui pages. Note the html file refers to a js controller page , the div id can be anything but the __plugin/<controller-page-name> must match the names defined in plugin page info in your plugin definition page // HTML page // defined in the html ! <div id="corruptionrepairjs" data-role="page" class="page type-interior pluginConfigurationPage page-windowScroll" data-require="emby-button,emby-toggle" <-- thes are also defined in js start deinition, they are the emby styled css components data-controller="__plugin/corruptionrepairjs"> // <-- must Match! note the page is directly in --plugin root! // plugin.cs public PluginPageInfo[] GetPages() { return new[] { new PluginPageInfo { Name = "corruptionrepair", EmbeddedResourcePath = GetType().Namespace + ".Dashboard.corruptionrepair.html" }, new PluginPageInfo { Name = "corruptionrepairjs", // <-------------- MUST MATCH !!! EmbeddedResourcePath = GetType().Namespace + ".Dashboard.corruptionrepair.js" } }; } IEnumerable<PluginPageInfo> IHasWebPages.GetPages() { return GetPages(); } the url /Dashboard/corrupimagerepair.js is constructed in c# namespace convention with full stops instead of / slashes Pages must be added as embeded resource ! In your MyTV IsMainConfigPage = true; Edited September 30, 2025 by mickle026
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 @mickle026Every time I examine your code, I discover how much I have to learn. I appreciate your help. Vic
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 (edited) @mickle026in the plugin a reply is sent to the client via the code below. It uses the "SendAsync method in "message.Connection." async Task Reply(WebSocketMessageInfo message, string txtreply, JsonMessage msg,string Payload="") { _logger.Info("[WebSocket] Got PING from {0}", msg.UserId); if (string.IsNullOrEmpty(Payload)) Payload = "Server time: " + DateTime.UtcNow.ToString("HH:mm:ss"); var response = new JsonMessage { Type = txtreply, UserId = msg.UserId, Payload = Payload }; var json = _json.SerializeToString(response); var reply = new WebSocketMessage<string> { MessageType = Name, Data = json }; await message.Connection.SendAsync(reply, CancellationToken.None); _logger.Info("[WebSocket] Sent {0} back to client {1}", txtreply, message.Connection.Id); } For the code above, I don't understand how the POST in the code below is being used. public async Task<object> Post(ImageRepairRequest request) { if (string.Equals(request.Name, "ping", StringComparison.OrdinalIgnoreCase)) { await _sessionManager.SendMessageToUserDeviceSessions( request.DeviceId, "imagecorruptrepair", "PONG from server", CancellationToken.None ); } else if (string.Equals(request.Name, "startscan", StringComparison.OrdinalIgnoreCase)) { await _sessionManager.SendMessageToUserDeviceSessions( request.DeviceId, "imagecorruptrepair", "Scan requested (stub)", CancellationToken.None ); } return "ok"; } I see the rout defined, but I am not sure on how it's used. This is of course caused by my lack of understanding about C#. I also don't understand how and where the POST is evoked. I am trying to put together the life cycle of messages flowing between the server and client and back again. I appreciate you're mentoring. I know I am a difficult student. Vic Edited September 30, 2025 by VicMoore
mickle026 650 Posted September 30, 2025 Author Posted September 30, 2025 (edited) Ah yes, that is left there for learning purposes. That POST method is not being used. There are no references (or calls) to it in the code. When there are calls to it , in visual studio you can click this, see a list of calls, then click the call in the list and it will jump to that code. I am processing the message server side, choosing how to reply and what to do first. Then replying directly, this is my reply payload builder ending with the sendAsync build json payload ping instantly replies with pong. startscan enters the startscan function passing the message and client id via msg, then calls the reply from within there. Visual Flow Client JS → Emby Server WS → Plugin Handler → Build Response → Emby Server WS → Client JS Webpage→ ProcessReply→ Reply Send → Decide Action → Build Json Payload and Send You can of course split it into seperate functions, see the startscan function, just pass in the parameters await startscan(message, msg); then recieve them at the function async Task startscan(WebSocketMessageInfo message, JsonMessage msg) variables message and msg are passed into the startscan function Edited September 30, 2025 by mickle026
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 @mickle026This is a great help. I am studying the code and having a great time doing it. It's great to learn. Thanks again... Vic
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 (edited) @mickle026I tried to add your code to the myTV code to create an environment where clicking on the myTV plugin icon brings up the DemoWebUI.html and clicking on the myTV Tuner brings up myTV. I get the Emby log error below. I think the problem is trivial but I just don't see it. Don't put a lot of time on this, but if you see the problem let me know. Vic Log Error Message ** Error Report *** Version: 4.8.11.0 Command line: C:\Users\vic\AppData\Roaming\Emby-Server\system\EmbyServer.dll -noautorunwebapp Operating system: Microsoft Windows 10.0.22621 Framework: .NET 6.0.36 OS/Process: x64/x64 Runtime: C:/Users/vic/AppData/Roaming/Emby-Server/system/System.Private.CoreLib.dll Processor count: 4 Data path: C:\Users\vic\AppData\Roaming\Emby-Server\programdata Application path: C:\Users\vic\AppData\Roaming\Emby-Server\system System.IO.FileNotFoundException: System.IO.FileNotFoundException: File not found: DemoWebUI File name: 'DemoWebUI' at Emby.Web.Api.WebAppService.Get(GetDashboardConfigurationPage request) at Emby.Server.Implementations.Services.ServiceController.Execute(HttpListenerHost appHost, Object requestDto, IRequest req, Type serviceType) at Emby.Server.Implementations.Services.ServiceHandler.ProcessRequestAsync(HttpListenerHost httpHost, IServerApplicationHost appHost, IRequest httpReq, IResponse httpRes, IStreamHelper streamHelper, RestPath restPath, String responseContentType, CancellationToken cancellationToken) at Emby.Server.Implementations.HttpServer.HttpListenerHost.RequestHandler(IRequest httpReq, ReadOnlyMemory`1 urlString, ReadOnlyMemory`1 localPath, CancellationToken cancellationToken) Source: Emby.Web TargetSite: System.Threading.Tasks.Task`1[System.Object] Get(Emby.Web.Api.GetDashboardConfigurationPage) myTV.cspro <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <Version>1.0.3.503</Version> <AssemblyVersion>$(Version)</AssemblyVersion> <FileVersion>$(Version)</FileVersion> </PropertyGroup> <ItemGroup> <None Remove="config\mytvconfig.html" /> <None Remove="config\mytvconfig.js" /> <None Remove="config\mytvapps.html" /> <None Remove="config\mytvapps.js" /> <None Remove="DemoWebUI.html" /> <None Remove="DemoWebUI.js" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="thumb.jpg" /> <EmbeddedResource Include="config\mytvconfig.html" /> <EmbeddedResource Include="config\mytvconfig.js" /> <EmbeddedResource Include="config\mytvapps.html" /> <EmbeddedResource Include="config\mytvapps.js" /> <EmbeddedResource Include="config\DemoWebUI.html" /> <EmbeddedResource Include="config\DemoWebUI.js" /> </ItemGroup> <ItemGroup> <PackageReference Include="mediabrowser.server.core" Version="4.7.9" /> <PackageReference Include="System.Memory" Version="4.5.5" /> </ItemGroup> <Target Name="PostBuild" AfterTargets="PostBuildEvent"> <!-- <Exec Command="xcopy "$(TargetPath)" "\\192.168.188.32\appdata\emby-48-beta\plugins" /Y" /> --> <Exec Command="xcopy "$(TargetPath)" "%25AppData%25\Emby-Server\programdata\plugins" /Y" /> </Target> </Project> Note: in the myTV.cspro code I tried putting "config\" before the DemoWebUI.html and the DemoWebUI.js entries above. This did not resolve the problem myTV Files JS code DemoWebUI.js define(["emby-button","emby-toggle"], function () { return function (page, params) { page.addEventListener('viewshow', () => { }); } } ); HTML code DemoWebUI.html <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <div id="Demo_WebUI" data-role="page" class="page type-interior pluginConfigurationPage page-windowScroll" data-require="emby-button,emby-toggle" data-controller="__plugin/DemoWebUIjs"> <div data-role="content"> <h1>Demo Web UI</h1> </div> </div> Edited September 30, 2025 by VicMoore
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 I got it fixed myself--- YEA!!!! Vic
mickle026 650 Posted September 30, 2025 Author Posted September 30, 2025 Always namespace error's, usually plugin info .Dashboard. To .config. ?
VicMoore 754 Posted September 30, 2025 Posted September 30, 2025 @mickle026 I put your files back in the WebUI folder and everything worked fine. This is a great feature because new users always try to bring myTV up by clicking the plugin icon. Now I can explain to them what they need to do. I never knew how to do this until you showed me how to do it with your demo. Vic
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now