1

I'm using Blazor WebAssembly with .NET 5 and I have a MultipleCameraLive component which embeds 4 SingleCameraLive components. Each component represents the streaming of frames coming from a given camera. By clicking on the frame belonging to a given camera, the user will jump to a page (let's call it SingleCameraFocus) where only the streaming from that camera is showed.

Images are sent by the server by means of SignalR as soon as they are acquired from the cameras (server-initiated communication).

Now, for modularity reasons I think the best approach would be to have a single Hub class and have like "multiple instances" of it. In other words in the server I'd have something like this

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        [...]

        app.UseEndpoints(endpoints => {
            endpoints.MapRazorPages();
            endpoints.MapControllers();
            endpoints.MapFallbackToFile("index.html");
            endpoints.MapHub<CameraLiveModeHub>("/cameraLiveHub_0");
            endpoints.MapHub<CameraLiveModeHub>("/cameraLiveHub_1");
            endpoints.MapHub<CameraLiveModeHub>("/cameraLiveHub_2");
            endpoints.MapHub<CameraLiveModeHub>("/cameraLiveHub_3");
        });

        CameraLiveModeHub.Context = app.ApplicationServices.GetService<IHubContext<CameraLiveModeHub>>();

        [...]

Client side there are no problems since I can use the Uri above to connect to the given Hub. However when I send a message from server by means of Hub Context I have no such possibility. In fact, from what I understand it's not like there are 4 Hub: there is just one, with four Uri "pointing" to that Hub. In fact here you can read

Hubs are transient: Don't store state in a property on the hub class. Every hub method call is executed on a new hub instance.

This seems to suggest that there is no way of accomplishing what I'd like to. If I were able to accomplish the above-mentioned modular design I would have had advantages such as: in order to design the SingleCameraFocus component, I'd just need to re-use SingleCameraLive component, passing-in the correct parameters (such as the Hub Uri).

So the other way would be to have a single Hub for MultipleCameraLive component, and everytime it receives a frame from the server it needs to act as "multiplexer", passing the frame to the correct child component. For what concerns the SingleCameraFocus component, I would need a separate Hub for it. Furthermore, since there are 4 cameras, I'd probably need 4 Hubs, 4 different Classes having almost exactly the same code. This is because a client should receive the frames only from a single Camera, i.e. from a single Hub, since a Hub have no means of selecting which client to send data to (if communication is server-initiated like in this case).

What is in your opinion the best way to approach this problem?

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
  • So you want something like "groups" or "rooms", am I right? If so, maybe [this](https://stackoverflow.com/questions/11175173/signalr-multiple-chat-rooms) helps? – Ergis Sep 17 '21 at 12:59
  • I think your best approach might be to write this system yourself, using ordinary sockets. SignalR doesn't seem to be designed for what you want to do, and if I'm honest, the functionality that SignalR provides does not justify the weight of its complexity. – Robert Harvey Sep 17 '21 at 13:05
  • That said, it's been awhile since I've used SignalR, and maybe Microsoft has improved it over the last few years. – Robert Harvey Sep 17 '21 at 13:07
  • @Ergis I'll take a look, thanks! The other option I was thinking about was to have client-initiated communication: every 1/25 of second a single component ask for the last frame acquired, and the hub can answer to that client only. I just have concerns performance-wise... – Alessandro Martinelli Sep 17 '21 at 13:17
  • @RobertHarvey that's a viable way too, I'll try to understand If I'm able to do something like that. – Alessandro Martinelli Sep 17 '21 at 13:17
  • As for the concern of the Hub being transient, you can have a static property, like *Hubs* or *Groups* (whatever) in the form of `Dictionary`, where the key can be the indentifier (in your case, *cameraLiveHub_0*, *cameraLiveHub_1* etc.) and the value can be the `ConnectionId's` of the connections ... when one joins, you assign him to one of them groups/rooms etc. etc. You get what I mean about the rest, I guess... – Ergis Sep 17 '21 at 13:27

1 Answers1

2

Here is a sample:

    public class MyHub : Hub
    {
        private new static readonly ConcurrentDictionary<string, IList<string>> Groups = new ConcurrentDictionary<string, IList<string>>(
            new Dictionary<string, IList<string>>()
            {
                { "group_1", new List<string>() },
                { "group_2", new List<string>() },
                { "group_3", new List<string>() },
                { "group_4", new List<string>() }
            }
        );

        public bool Join(string groupName)
        {
            var connId = Context.ConnectionId;
            // Check if I already belong to this group
            if(Groups.TryGetValue(groupName, out var groupClients))
            {
                if (groupClients.Contains(connId) == false)
                    groupClients.Add(connId);

                return true;
            }
            return false;
        }

        public async Task Send(string groupName, string message)
        {
            var connId = Context.ConnectionId;
            // Check if I belong to this group
            if(Groups.TryGetValue(groupName, out var groupClients))
            {
                if(groupClients.Contains(Context.ConnectionId))
                {
                    await Clients.Users((IReadOnlyList<string>)groupClients.Where(c => c != connId)).SendAsync(message);
                }
            }
        }

        public void Disconnect()
        {
            var myGroups = GetMyGroups();

            foreach(var groupName in myGroups)
                if (Groups.TryGetValue(groupName, out var groupClients))
                    groupClients.Remove(Context.ConnectionId);
        }

        // In case you can belong to many groups at the same time
        private IList<string> GetMyGroups()
        {
            var connId = Context.ConnectionId;
            var output = new List<string>();

            foreach(var item in Groups)
            {
                if (item.Value.Contains(connId))
                    output.Add(item.Key);
            }
            return output;
        }
    }
Ergis
  • 1,105
  • 1
  • 7
  • 18