webrtc角とasp .ネットコア


こんにちは、皆さん、このポストで、我々はWebRTC、角度とASPを使っている基本的なビデオ呼び出しウェブを構築します.ネットコア.なぜ?この技術は読まれた肉とmalbecのようであるので、彼らのちょうど対井戸(私はこれについてのポストを書きます).これは、改善のためのポイントの多くは非常に基本的なアプリケーションになるだろうが、私は基本に集中します.
WebRecは、ピア間で送られるビデオ、音声、および一般的なデータをサポートしていますが、どんなP 2 Pシステムにおいても、シグナリング通信チャンネルを必要とするように、ユーザーは互いを発見することができます.
git reposはポストの終わりにあります.
バックエンドから始める
我々は、唯一の作成サーバーを必要とする.NETコアWeb APIプロジェクト
dotnet new webapp -o signalRtc
cd signalRtc
dotnet add package Microsoft.AspNetCore.SignalR.Core
作成されたコントローラフォルダを削除できます.
今、私たちはハブを必要とします、そして、ハブは'クライアントとサーバーが互いにメソッドを呼ぶのを許します.Hubsフォルダとハブを拡張するクラスを作成します.
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using signalRtc.models;

namespace signalRtc.hubs
{
    public class SignalRtcHub : Hub
    {
    }
}

3(または4)の基本的な行動は、順序でビデオ通話アプリを作るために必要です.
  • すべての新しいユーザーが到着通知する必要があります.
  • 既存のユーザーは、彼らの存在を新しいユーザーに知らせる必要があります.
  • は、WebBTCのために同輩の間で信号発信を共有します.
  • 誰かが切断するとき、
  • ユーザーに通知してください.
  • 私が言ったように、我々は角度プロジェクトから実行されることができて、若干のデータをフロントエンドに送ることができるハブ・クラスでタスクを書くつもりです.
    最初にUserInfoクラスを使用してユーザーモデルをカプセル化するフォルダモデルを作成する
    namespace signalRtc.models
    {
        public class UserInfo
        {
            public string userName { get; set; }
            public string connectionId { get; set; }
        }
    }
    
    ConnectionIDは、Siglarrが各接続されたユーザに与えるユニークな識別子であり、特定のユーザにメッセージを送信する必要があります.
    点1
    public async Task NewUser(string username)
    {
        var userInfo = new UserInfo() { userName = username, connectionId = Context.ConnectionId };
        await Clients.Others.SendAsync("NewUserArrived", JsonSerializer.Serialize(userInfo));
    }
    
    コードのこの短い部分では、signalr素晴らしい機能の多くを使用します.タスクはフロントエンドから呼び出すことができるアクションですが、コンテキストから取得する現在のユーザーIDを持つユーザーオブジェクトを作成します.私たちは“こんにちは私は新しいユーザー”を受信する必要はありませんので、我々は“他人”にメッセージを送信すると、それはすべてのユーザーがハブに接続されていないが、私にとっては、signalr私たちのためのユーザーのリストを保持することを意味します.“NewuserGrowding”文字列は、このメッセージを受け取るためにフロントエンドで後で使用する識別子です.
    私たちが「Newuser」メッセージを得るとき、私たちは私たちの存在をHelloで新しいユーザに知らせる必要があります.
    public async Task HelloUser(string userName, string user)
    {
        var userInfo = new UserInfo() { userName = userName, connectionId = Context.ConnectionId };
        await Clients.Client(user).SendAsync("UserSaidHello", JsonSerializer.Serialize(userInfo));
    }
    
    タスクは2つのパラメータを受け取ります.helloと言うユーザのユーザ名と、私たちが敬礼するユーザです.つのユーザーにメッセージを送信するには、クライアントを使用します.クライアント(' connectionID ').この場合、フロントエンドに「usersaidhello」イベントを送ります.
    ポイントは、3は簡単です、我々はP 2 P Videocallを開始するためにユーザーに我々の信号を送りたいです
    public async Task SendSignal(string signal, string user)
    {
        await Clients.Client(user).SendAsync("SendSignal", Context.ConnectionId, signal);
    }
    
    最後に必須ではありませんが、ユーザーが切断したときにグループに通知したい場合は、OnDisconnectタスクをオーバーライドします.
    public override async Task OnDisconnectedAsync(System.Exception exception)
    {
        await Clients.All.SendAsync("UserDisconnect", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
    
    我々はほとんどバックエンドで行われているので、我々は起動時にSinglarを使用していることをNetcoreに示す必要があります.SingleServiceのためのConfigureService(角度ポートのためのCors)と終点のcs add singalr
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(MyAllowSpecificOrigins,
                builder => builder.WithOrigins("http://localhost:4200", "https://localhost:4200")
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
        });
    
        services.AddSignalR();
    }
    
    readonly string MyAllowSpecificOrigins = "AllowOrigins";
    
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseRouting();
        app.UseCors(MyAllowSpecificOrigins);
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<SignalRtcHub>("/signalrtc");
        });
    
        app.Run(async(context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
    
    
    角度に移動する
    最初に新しい角度プロジェクトを作成し、いくつかの依存関係をインストールします.
    単純な同輩(そして、そのタイプは、私たちが静的な型チェックが好きであるので).
    bootstap、私はあなたがプレーンCSSとflexまたはグリッドですべてをすることができるということを知っているので、私はbootstrapが好きです.
    signalr、メソッドを呼び出すには、メッセージを書いて受け取ります.
    CLIが尋ねるとき、SCSSを選ぶのを忘れないでください.
    ng new signalRtc
    cd signalRtc
    npm install simple-peer
    npm install @types/simple-peer
    npm install bootstrap
    npm install @aspnet/signalr
    
    シチュー、オープンスタイルを追加します.& quot ; @ import '~ブートストラップ/dist/css/bootstrap . min . css ';を追加します.
    まず、singalrメッセージを管理するサービスを作成します.
    cd src/app
    ng g service signalr
    
    ハブ接続を維持するためにプライベートプロパティが必要です.また、signalrライブラリからイベントをカプセル化するための被験者とオブザーバブルも必要です.私は、個人の被験者に、メソッドとパブリックオブザーバブルを介して、コンポーネント内のデータを取得するために値を出力する必要があります.
    private hubConnection: signalR.HubConnection;
    
    private newPeer = new Subject<UserInfo>();
    public newPeer$ = this.newPeer.asObservable();
    
    private helloAnswer = new Subject<UserInfo>();
    public helloAnswer$ = this.helloAnswer.asObservable();
    
    private disconnectedPeer = new Subject<UserInfo>();
    public disconnectedPeer$ = this.disconnectedPeer.asObservable();
    
    private signal = new Subject<SignalInfo>();
    public signal$ = this.signal.asObservable();
    
    ご覧のように、すべてのタスクといくつかのインターフェイスで、別のファイルで書くことができます
    export interface PeerData {
      id: string;
      data: any;
    }
    
    export interface UserInfo {
      userName: string;
      connectionId: string;
    }
    
    export interface SignalInfo {
      user: string;
      signal: any;
    }
    
    今、我々はいくつかのメソッドを書きます、我々は接続を始める1つを必要とします、新しいユーザーメッセージへの応答とP 2 P信号を送る1つ
    接続を開始する
      public async startConnection(currentUser: string): Promise<void> {
    
        this.hubConnection = new signalR.HubConnectionBuilder()
          .withUrl('https://localhost:5001/signalrtc')
          .build();
    
        await this.hubConnection.start();
        console.log('Connection started');
    
        this.hubConnection.on('NewUserArrived', (data) => {
          this.newPeer.next(JSON.parse(data));
        });
    
        this.hubConnection.on('UserSaidHello', (data) => {
          this.helloAnswer.next(JSON.parse(data));
        });
    
        this.hubConnection.on('UserDisconnect', (data) => {
          this.disconnectedPeer.next(JSON.parse(data));
        });
    
        this.hubConnection.on('SendSignal', (user, signal) => {
          this.signal.next({ user, signal });
        });
    
        this.hubConnection.invoke('NewUser', currentUser);
      }
    
    コードから分かるように、接続を開始し、メッセージイベントをObservablesにカプセル化します.また、接続を開始するときには、「hello I new new user」メッセージを送ります.これはバックエンドとの双方向通信です.
    こんにちは新しいユーザと信号データを送信するには、ハブメソッドを呼び出す必要があります
    public sendSignalToUser(signal: string, user: string) {
      this.hubConnection.invoke('SendSignal', signal, user);
    }
    
    public sayHello(userName: string, user: string): void {
      this.hubConnection.invoke('HelloUser', userName, user);
    }
    
    
    今、私たちはSimplee同輩イベントをカプセル化するためにsignalr 1に類似したRTCサービスを書くつもりです.また、私たちが話している会話と現在の同輩を始めるために、接続されたユーザーのリストを維持する必要があります.
    import { Injectable } from '@angular/core';
    import { Observable, Subject, BehaviorSubject } from 'rxjs';
    import { Instance } from 'simple-peer';
    
    declare var SimplePeer: any;
    
    @Injectable({
      providedIn: 'root'
    })
    export class RtcService {
    
      private users: BehaviorSubject<Array<UserInfo>>;
      public users$: Observable<Array<UserInfo>>;
    
      private onSignalToSend = new Subject<PeerData>();
      public onSignalToSend$ = this.onSignalToSend.asObservable();
    
      private onStream = new Subject<PeerData>();
      public onStream$ = this.onStream.asObservable();
    
      private onConnect = new Subject<PeerData>();
      public onConnect$ = this.onConnect.asObservable();
    
      private onData = new Subject<PeerData>();
      public onData$ = this.onData.asObservable();
    
      public currentPeer: Instance;
    
      constructor() {
        this.users = new BehaviorSubject([]);
        this.users$ = this.users.asObservable();
      }
    
    }
    
    ユーザーが到着すると、リストに追加され、それらが切断されると、私たちはそれを削除します
     public newUser(user: UserInfo): void {
      this.users.next([...this.users.getValue(), user]);
    }
    
    public disconnectedUser(user: UserInfo): void {
      const filteredUsers = this.users.getValue().filter(x => x.connectionId === user.connectionId);
      this.users.next(filteredUsers);
    }
    
    会話を始めるとき、ピアインスタンスを作成する必要があります.単純なピアライブラリは、P 2 P接続を開始しているかどうかを知る必要があります.また、我々はカプセル化するいくつかのイベントを提供します.最も重要な出来事は' signal 'であり、ライブラリの記述を引用しています.ピアがリモートのピアに信号を送信したいときに発行されます.
    これは、アプリケーション開発者の責任ですこのデータを他のピア'に取得するには
      public createPeer(stream, userId: string, initiator: boolean): Instance {
        const peer = new SimplePeer({ initiator, stream });
    
        peer.on('signal', data => {
          const stringData = JSON.stringify(data);
          this.onSignalToSend.next({ id: userId, data: stringData });
        });
    
        peer.on('stream', data => {
          console.log('on stream', data);
          this.onStream.next({ id: userId, data });
        });
    
        peer.on('connect', () => {
          this.onConnect.next({ id: userId, data: null });
        });
    
        peer.on('data', data => {
          this.onData.next({ id: userId, data });
        });
    
        return peer;
      }
    
    最後に、ピアインスタンスにシグナルを送る方法があります.このアクションは単純なピアで必要です.現在の同輩が存在するかどうかチェックしなければなりません、それがそうしないならば、我々がイニシエータでないということを意味します、そして、誰かが我々と一緒のVideoCallを見つめているので、我々はそれをつくらなければなりません
    public signalPeer(userId: string, signal: string, stream: any) {
        const signalObject = JSON.parse(signal);
        if (this.currentPeer) {
          this.currentPeer.signal(signalObject);
        } else {
          this.currentPeer = this.createPeer(stream, userId, false);
          this.currentPeer.signal(signalObject);
        }
      }
    
    public sendMessage(message: string) {
      this.currentPeer.send(message);
    }
    
    また、webrtcがピア間でバイナリデータを送ることができるので、チャット部分のsendmessageを作成します.私たちはsignalrを使用することができました、しかし、我々はそれをシグナリング・サーバーとして欲しいだけです.
    今すぐアプリを変更しましょう.コンポーネント.HTMは、我々のユーザー名とチャットの場所の入力を追加します.良い練習の後、我々はそのための場所を保つために接続されたユーザーリストを管理するための特別なコンポーネントを作成します.
    <div class="container-fluid">
      <h1>SignalRTC</h1>
      <div class="row">
        <div class="col-5">
          <div class="row">
            <div class="col">
              <input [(ngModel)]="currentUser" required placeholder="UserName" type="text">
            </div>
            <div class="col">
              <div class="btn-group" role="group" aria-label="button group">
                <button [disabled]="!currentUser" (click)="saveUsername()" type="button"
                  class="btn btn-sm btn-primary">Save</button>
              </div>
            </div>
          </div>
          <div class="row">
            <div class="col">
              <!-- user list component -->
            </div>
          </div>
        </div>
        <div class="col-7">
          <div class="row">
            <div class="col-8">
              <input [(ngModel)]="dataString" required placeholder="Write a message" type="text">
            </div>
            <div class="col-4">
              <button (click)="sendMessage()" type="button" class="btn btn-sm btn-secondary">Send</button>
            </div>
          </div>
        </div>
      </div>
    </div>
    
    
    これは、リストコンポーネント、ちょうどVideoCallを開始するためにユーザーをクリックすることができる単純なリストです、コンポーネントはイベントコンポーネントを生成するので、アプリケーションコンポーネントで反応することができます.
    <ul class="list-group mt-4">
      <li class="list-group-item" (click)="userClicked(user)" *ngFor="let user of users$ | async">
        {{user.userName}}
      </li>
    </ul>
    
     @Output() userSelected: EventEmitter<UserInfo> = new EventEmitter();
    
      public users$: Observable<Array<UserInfo>>;
    
    
      constructor(private rtcService: RtcService) { }
    
      ngOnInit() {
        this.users$ = this.rtcService.users$;
      }
    
      public userClicked(user: UserInfo) {
        this.userSelected.emit(user);
      }
    
    
    今、あなたはアプリを変更することができます.コンポーネント.HTMLと実際のリストコンポーネントのコメントを置き換えます.
    <app-user-list (userSelected)="onUserSelected($event)"></app-user-list>
    
    このコンポーネントでは、oninitメソッドを使用します.
    ngOnInit() {
        this.subscriptions.add(this.signalR.newPeer$.subscribe((user: UserInfo) => {
          this.rtcService.newUser(user);
          this.signalR.sayHello(this.currentUser, user.connectionId);
        }));
    
        this.subscriptions.add(this.signalR.helloAnswer$.subscribe((user: UserInfo) => {
          this.rtcService.newUser(user);
        }));
    
        this.subscriptions.add(this.signalR.disconnectedPeer$.subscribe((user: UserInfo) => {
          this.rtcService.disconnectedUser(user);
        }));
    
        this.subscriptions.add(this.signalR.signal$.subscribe((signalData: SignalInfo) => {
          this.rtcService.signalPeer(signalData.user, signalData.signal, this.stream);
        }));
    
        this.subscriptions.add(this.rtcService.onSignalToSend$.subscribe((data: PeerData) => {
          this.signalR.sendSignalToUser(data.data, data.id);
        }));
    
        this.subscriptions.add(this.rtcService.onData$.subscribe((data: PeerData) => {
          console.log(`Data from user ${data.id}: ${data.data}`);
        }));
    
        this.subscriptions.add(this.rtcService.onStream$.subscribe((data: PeerData) => {
          this.userVideo = data.id;
          this.videoPlayer.nativeElement.srcObject = data.data;
          this.videoPlayer.nativeElement.load();
          this.videoPlayer.nativeElement.play();
        }));
      }
    
    
    各ステップの簡単な説明:
  • ユーザーが到着すると、新しいユーザーを作成し、我々の地獄のメッセージを送信します.
  • Helloを受け取ると、リストにユーザーを追加します.
  • ユーザーが切断すると、リストから削除されます.
  • 信号を受信するとき、我々は機能を実行します.シグナルが単純なピアドキュメントで示されていることを示します.
  • 単純なピアがWERTC信号が準備ができていることを示すとき、我々はそれをユーザーに送ります.
  • 我々がメッセージを受け取るとき、我々はそれを示します.
  • 最終的に、ビデオストリームが準備ができているとき、我々はビデオタグでそれを示します.最後に、ユーザーをクリックすると、新しいピアが作成され、最終的にシグナルイベントが発生します!
    また、ユーザー名を保存し、データチャネルを介してメッセージを送信する機能があります.また、OnDestroy LifeCycleフックの上で常に登録してください、このケースでは、我々は1つの構成要素だけを持っています、しかし、この実行に続いて、我々はメモリリークを防ぎます.
    public onUserSelected(userInfo: UserInfo) {
        const peer = this.rtcService.createPeer(this.stream, userInfo.connectionId, true);
        this.rtcService.currentPeer = peer;
      }
    
      public async saveUsername(): Promise<void> {
        try {
          await this.signalR.startConnection(this.currentUser);
          this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        } catch (error) {
          console.error(`Can't join room, error ${error}`);
        }
      }
    
      public sendMessage() {
        this.rtcService.sendMessage(this.dataString);
        this.messages = [...this.messages, { own: true, message: this.dataString }];
        this.dataString = null;
      }
      ngOnDestroy() {
        this.subscriptions.unsubscribe();
      }
    
    Okey、それはすべての人々です.私が最初に言ったように、これは非常に簡単な出発点です.我々は多くの改善するために、例えば、彼らが呼び出しを受け入れるようにしたい場合は、他のユーザーに尋ねるsignalrを使用することができます!シンプルにしたかった.
    フロントエンド

    セーバ / signalrtcフロントエンド


    クライアントの例


    バックエンド

    セーバ / signalrtcバックエンド


    Webbrcシグナリングのためのsignalrバックエンドサーバ