📜 ⬆️ ⬇️

Implementing a simple video chat on ASP.NET MVC


Good day, ladies and gentlemen!
In this topic, I will tell you how you can make a simple video chat on ASP.NET MVC.

But for the beginning of the background. We run a video consultation service with a doctor via the Internet. There will definitely be a separate article about it, and now we want to find out how much load servers and channels can withstand.
To do this, we wrote a small web application, the source code and description of which I am glad to share with you.
The main idea is borrowed from chatroulette : you go into the general chat, choose any interlocutor and communicate on video.
The source code of the project is published on codeplex.com under a free license, I will be glad for comments / comments / suggestions.

So. I chose RTMP as the most common protocol. Why not RTMFP? Just using RTMFP is difficult to achieve a stable connection between clients, which is necessary for rendering paid video consultations, and the server implementation of distribution of IT managers is not available for stable use. As a server - Wowza Media Server , because as opposed to free Red5 (may his supporters forgive me) he has clear documentation with examples, and unlike the FMS, a trial period of 30 days and an acceptable pricing policy. And we will check the quality of work in practice, as far as I can imagine, there is no strong difference between all three in performance. As an alternative, we are considering erlyvideo , but so far it has not been possible to look at it in detail and try it out.

Everything is written under ASP.NET MVC 4. And for the implementation of text chat and communication between clients, the SignalR library is used .
')
Next, the points.

The implementation of the chat.
The main thing here is the two classes ChatMessage and Chat.
The class Chat is inherited from SignalR.Hubs.Hub and implements the basic methods of working with clients:

//      . public void JoinRoom(string roomKey, string userName) { //          if (roomKey == C.MAIN_CHAT_GROUP) Store.Add(new User(Context.ConnectionId, userName)); //    id Clients[Context.ConnectionId].OnJoinRoom(Context.ConnectionId); //     Groups.Add(Context.ConnectionId, roomKey); //       UpdateUsers(); } //      public void Send(ChatMessage message) { // -       if (message.Content.Length > 0) { //   message.Date = DateTime.Now; //   message.SenderKey = Context.ConnectionId; //       message.Content = HttpUtility.HtmlEncode(message.Content); message.SenderName = HttpUtility.HtmlEncode(message.SenderName); //      Clients[message.RoomKey].OnSend(message); Store.SaveMessage(message); } } 


Store here is a static collection of users, which, if desired, can be easily replaced with its own implementation.
In our demo, it is saved to the database instead of a static variable.

On the client we create the corresponding methods. For brevity, I hid a specific implementation.

 var CHAT = {}; var OPTIONS = {}; function Start(data) { //  ,  OPTIONS.SenderName = data.name; OPTIONS.RoomKey = 'MAIN'; CHAT = $.connection.chat; //   ,    CHAT.OnSend = OnSend; CHAT.OnJoinRoom = OnJoinRoom; } //       function OnJoinRoom(key) { OPTIONS.SenderKey = key; } //         function OnUpdateUsers(data) { /* ... ,  data  - User,     IUser */ } //    ,   Chat.Send function Send() { var messageInput = $("#msg"), //  ,      ChatMessage msg = { 'SenderName': OPTIONS.MyName, 'RoomKey': OPTIONS.RoomKey, 'Content': messageInput.val() }; CHAT.send(msg); //  :    -     messageInput.val(""); messageInput.focus(); } // ,       function OnSend(msg) { var chatContent = $(".chat_content"), msgClass = 'chat_message'; /* ...    ,  msg - ,     ChatMessage */ } 


Next, you need to provide the functionality of the "calls". To do this, add to Chat the methods that handle the beginning of the call, the rejection and the acceptance of the call.

 //   ( ) public void Call(string recieverKey, string senderKey, string senderName) { Clients[recieverKey].OnCall(senderKey, senderName); } //    public void RejectCall(string senderKey, string recieverKey, string recieverName) { Clients[senderKey].OnRejectCall(recieverKey, recieverName); } //   public void AcceptCall(string calleePulicKey, string calleeName, string myName) { string myKey = Guid.NewGuid().ToString().Replace("-", ""); string calleeKey = Guid.NewGuid().ToString().Replace("-", ""); string roomKey = Guid.NewGuid().ToString().Replace("-", ""); var model = new RoomModel { MyPublicKey = Context.ConnectionId, MyKey = myKey, MyName = myName, CalleePublicKey = calleePulicKey, CalleeKey = calleeKey, CalleeName = calleeName, RoomKey = roomKey }; //      Store.SaveRoomInfo(model); //   Clients[calleePulicKey].OnAcceptCall(false, roomKey); Clients[Context.ConnectionId].OnAcceptCall(true, roomKey); } 


The scheme of work is as follows: when one subscriber (say, Angelina) wants to call another (for example, Pete), Angelina calls the Call method and gives him the key of Petit, her key and her name. We will send the OnCall notification to Petya, process it on the client and display a message about the call from Angelina. If Petya decides to reject the call, he calls the RejectCall method and returns the caller's key, his key and his name. We send Angelina a notification OnRejectCall, in the handler of which we display Angelina a notification about the rejection of the call.
If Peter accepts the call, he calls the AcceptCall method, in which we generate for both subscribers a new identifier and a key for the personal chat room. Then we send OnAcceptCall notifications to both of them, passing with them the necessary keys. On the client in the notification handler, we redirect both Petya and Angelina to the personal chat page:

 function OnAcceptCall(isMy, roomKey) { document.location = '@Url.Action("Room", "Home")' + '?isMy=' + isMy + '&roomKey=' + roomKey; } 


On the personal chat page using the transferred keys, we initialize the USB flash drive and text chat. For the text chat on the Room page, we use the same Chat object, simply by not processing the events of updating the list of users and calls on the client.

Next, go to the flash drive.
To organize communication, we have to create a stream that we will “publish” to the server and subscribe to the stream published by the interlocutor. The streams on the server are identified by means of keys transmitted to the server when publishing begins.
When initializing the flash drive, we get the keys from the page, save them to local variables and start a timer that will monitor the start and progress of the communication session. There are three methods for creating a connection to the server, publishing and subscribing to a stream:

 private function Connect():void { if (!isConnected && rtmpConnection == null) { //   rtmpConnection = new NetConnection(); rtmpConnection.connect(connectStr); //       rtmpConnection.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus_rtmpConnection); } isConnected = true; } private function StartPublish():void { //     nsPublish = new NetStream(rtmpConnection); nsPublish.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus_nsPublish); //     0 nsPublish.bufferTime = 0; //  nsPublish.publish(publishName); //     nsPublish.attachCamera(camera); nsPublish.attachAudio(microphone); isPublish = true; } private function StartSubscribe():void { // C       nsSubscribe = new NetStream(rtmpConnection); //     nsSubscribe.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus_nsSubscribe); //     0 nsSubscribe.bufferTime = 0; //    var volume:Number = sldrVolume.value / 100; var st:SoundTransform = new SoundTransform(volume); nsSubscribe.soundTransform = st; //    nsSubscribe.play(subscribeName); //     videoRemote.attachNetStream(nsSubscribe); isSubscribe = true; } 


When the timer is triggered, we check whether we are connected to the server and the status of the publication and subscription streams. And if all checks are successful, we consider the talk time.

 private function onTick_Timer(event:TimerEvent):void { if(!isConnected)//   { lblEndTime.text = "..."; Connect(); startTime = new Date(); } else { if(!isPublish && needPublish)//   { lblEndTime.text = "..."; StartPublish(); } if(!isSubscribe)//    { lblEndTime.text = "..."; StartSubscribe(); } if(isPublish && isSubscribe)//   ,    { var now:Date = new Date(); var toStart:TimeSpan = new TimeSpan(now.getTime() - startTime.getTime()); lblEndTime.text = toStart.getTotalMinutes() + ':' + toStart.getSeconds(); } } } 


This is almost everything.

The last component is the Media Server.
Wowza Media Server did not cause any special difficulties in installation and configuration. Download the distribution from the official site, install, open the 1935th port on the machine, and write the server address to the flash drive. If you wish, you can use any other server that supports RTMP: Red5, Adobe FlashMediaServer, erlyvideo. Client implementation does not depend on the server.

Our goals for this test are:
1. Find out how many simultaneously communicating users we can withstand without loss of quality.
2. Get tips for better implementation
3. It is possible to find security holes.

UPD: Testing is over, links to the online demo removed from the post.
According to the results I must say that habraeffekt passed by. The server worked at a maximum of half the load.
Some numbers:
1. How much was maximally in the video chat per unit of time - 5 sessions started at the same time, accurate to a minute, 4 of them lasted more than a minute
2. Total call attempts - 361
1) Of these attempts, which lasted more than 30 seconds - 174
2) lasted more than 2 minutes - 38
3) Incorrectly completed (without expiration) - 62
3. Total number of chat messages - 12347
1) Of these, the main thing is 11256
2) In personal - 1125

I thank everyone who took part in load testing!

Source: https://habr.com/ru/post/154455/


All Articles