ASP.NET MVCでのシンプルなビデオチャットの実装



こんにちは、紳士、habrayuzery!

このトピックでは、ASP.NET MVCで簡単なビデオチャットを行う方法を説明します。



しかし、まず第一に、背景。 インターネットを介して医師とのビデオ相談サービスを開始します。 これについては別の記事が必ずありますが、サーバーとチャネルがどれだけの負荷に耐えられるかを調べたいと思います。

これを行うために、小さなWebアプリケーションを作成しました。ソースコードとその説明を喜んで共有します。

基本的なアイデアはチャットルーレットから借りたものです。一般的なチャットに入り、話し相手を選択して、ビデオでコミュニケーションを取ります。

プロジェクトのソースコードは無料のライセンスの下でcodeplex.comに公開されています。コメント/コメント/提案を歓迎します。



だから。 プロトコルとして、最も一般的なものとしてRTMPを選択しました。 なぜRTMFPではないのですか? RTMFPを使用するだけでは、クライアント間の安定した接続を実現することは難しく、これは有料のビデオコンサルティングを提供するために必要であり、IDを配布するサーバー実装は安定して使用できません。 サーバーはWowza Media Serverです 。 無料のRed5 (サポーターはご容赦ください)とは異なり、明確なドキュメントと例があり、 FMSとは異なり試用期間は30日間であり、許容可能な価格設定ポリシーです。 そして、私が想像する限り、パフォーマンスの3つすべてに大きな違いはありません。 別の方法として、 erlyvideoを検討しますが 、まだ詳細に見て試してみる可能性はありません。



すべてがASP.NET MVC 4で記述されています。また、テキストチャットの実装とクライアント間の通信には、 SignalRライブラリが使用されます。



さらにポイント。



チャットの実装。

主なものは、2つのChatMessageクラスとChatクラスです。

ChatクラスはSignalR.Hubs.Hubから継承され、クライアントを操作する基本的なメソッドを実装します。



//      . 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); } }
      
      







ここのストアはユーザーの静的なコレクションであり、必要に応じて、独自の実装に簡単に置き換えることができます。

デモでは、静的変数の代わりにデータベースに保存されます。



クライアントで適切なメソッドを作成します。 簡潔にするために、特定の実装を隠しました



 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 */ }
      
      







次に、「呼び出し」の機能を提供する必要があります。 これを行うために、チャットの開始、コールの拒否と受け入れを処理するメソッドをチャットに追加します。



 //   ( ) 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); }
      
      







作業スキームは次のとおりです。あるサブスクライバー(たとえば、アンジェリーナ)が別のサブスクライバー(たとえば、ピート)を呼び出したい場合、アンジェリーナはCallメソッドを呼び出して、ペティアのキー、彼女のキー、および彼女の名前を渡します。 ピート、OnCall通知を送信し、クライアントで処理して、アンジェリーナからの呼び出しに関するメッセージを表示します。 Petyaが呼び出しを拒否することにした場合、彼はRejectCallメソッドを呼び出し、呼び出し元のキー、キー、および名前を返します。 AngelinaにOnRejectCall通知を送信し、そのハンドラーでAngelinaにコール拒否通知を表示します。

Petyaが電話を受けると、彼はAcceptCallメソッドを呼び出します。このメソッドでは、両方のサブスクライバーのパーソナルチャットルームの新しい識別子とキーが生成されます。 次に、両方のOnAcceptCall通知を送信し、必要なキーを渡します。 通知ハンドラーのクライアントで、PetyaとAngelinaの両方をパーソナルチャットページにリダイレクトします。



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







パーソナルチャットページで、転送されたキーを使用して、USBフラッシュドライブとテキストチャットを初期化します。 ルームページでのテキストチャットでは、同じChatオブジェクトを使用しますが、クライアントのユーザーと通話のリストを更新するイベントを処理するだけではありません。



次に、フラッシュドライブに移動します。

通信を整理するには、サーバーに「公開」するストリームを作成し、対話者によって公開されたストリームをサブスクライブする必要があります。 サーバー上のストリームは、公開の開始時にサーバーに送信されるキーによって識別されます。

フラッシュドライブが初期化されると、ページからキーを取得し、ローカル変数に保存してタイマーを開始します。タイマーは通信セッションの開始と進行を監視します。 サーバー接続を作成し、ストリームを発行およびサブスクライブするには、次の3つの方法が使用されます。



 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; }
      
      







タイマーがトリガーされると、サーバーに接続されているかどうかと、パブリケーションおよびサブスクリプションストリームのステータスを確認します。 そして、すべてのチェックが成功した場合、通話時間を考慮します



 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(); } } }
      
      







それが事実上すべてです。



最後のコンポーネントはMedia Serverです。

Wowza Media Serverは、インストールと構成に特別な問題を引き起こすことはありませんでした。 公式Webサイトから配布キットをダウンロードし、インストールして、マシンの1935番目のポートを開き、USBフラッシュドライブにサーバーアドレスを書き込みます。 必要に応じて、RTMPをサポートする他のサーバー(Red5、Adobe FlashMediaServer、erlyvideo)を使用できます。 クライアントの実装はサーバーに依存しません。



このテストの目標:

1.品質を損なうことなく耐えられる同時通信ユーザー数を確認します。

2.実装を改善するためのヒントを入手する

3.おそらくセキュリティホールを見つける



UPD:テストは終了しました。オンラインデモへのリンクは投稿から削除されました。

その結果、habraeffectは通り過ぎたと言わざるを得ません。 サーバーは最大負荷の半分で動作しました。

いくつかの数字:

1.単位時間あたりのビデオチャットの最大数-一度に5つのセッションが1分の精度で開始され、そのうち4つは1分以上続きました

2.合計コール試行-361

1)これらの試行のうち、30秒以上続く-174

2)2分以上続く-38

3)誤って完了(完了時間を設定せずに)-62

3.チャットメッセージの総数-12347

1)これらのうち、主に-11256

2)個人的に-1125



負荷テストに参加したすべての人に感謝します!



All Articles