P2Pビデオチャットのテクニカル分析

P2P ビデオ プロセス全体で、メディアの種類、ストリーム、および両当事者の候補を知る必要があるため、ここでは次のテクノロジが使用されます。

シグナリングサーバー socket.io

ステートマシン

ICE サーバー

WebRTC フレームワーク

メディア相談

シグナリングサーバー Socket.io

端的に言えば、シグナリングサーバーはメッセージを送信するための転送ステーションとして機能し、A はシグナリングサーバーにメッセージを送信し、シグナリングサーバーはメッセージを B に送信します。

Socket.IO は、クライアントとサーバー間の低遅延双方向イベントベースの通信を可能にするライブラリです。サーバーとクライアント間の通信の図

WebSocketプロトコルの上に構築され、HTTP ロング ポーリングや自動再接続へのフォールバックなどの追加の保証を提供します。

WebSocket は、サーバーとブラウザ間に全二重で低遅延のチャネルを提供する通信プロトコルです。詳細については、こちらをご覧ください。

利用可能な Socket.IO サーバー実装がいくつかあります。

  • JavaScript (ドキュメントはこのサイトにあります)
  • Java: https://github.com/mrniko/netty-socketio
  • Java: https://github.com/trinopoty/socket.io-server-java
  • パイソン: https://github.com/miguelgrinberg/python-socketio

ほとんどの主要言語のクライアント実装:

  • JavaScript (ブラウザー、Node.js、または React Native で実行可能)
  • Java: https://github.com/socketio/socket.io-client-java
  • C++: https://github.com/socketio/socket.io-client-cpp
  • スウィフト: https://github.com/socketio/socket.io-client-swift
  • ダーツ: https://github.com/rikulo/socket.io-client-dart
  • パイソン: https://github.com/miguelgrinberg/python-socketio
  • .Net: https://github.com/doghappy/socket.io-client-csharp
  • ゴラン: https://github.com/googollee/go-socket.io
  • さび: https://github.com/1c3t3a/rust-socketio
  • コトリン: https://github.com/icerockdev/moko-socket-io

プレーンな WebSocket を使用した基本的な例を次に示します。

サーバー( wsに基づく)

import {
    
     WebSocketServer } from "ws";

const server = new WebSocketServer({
    
     port: 3000 });

server.on("connection", (socket) => {
    
    
  // 向客户端发送消息
  socket.send(JSON.stringify({
    
    
    type: "hello from server",
    content: [ 1, "2" ]
  }));

  // 从客户端接收消息
  socket.on("message", (data) => {
    
    
    const packet = JSON.parse(data);

    switch (packet.type) {
    
    
      case "hello from server":
        // ...
        break;
    }
  });
});

クライアント

const socket = new WebSocket("ws://localhost:3000");

socket.addEventListener("open", () => {
    
    
  // 向服务器发送消息
  socket.send(JSON.stringify({
    
    
    type: "hello from server",
    content: [ 3, "4" ]
  }));
});

// 从服务器接收消息
socket.addEventListener("message", ({
    
     data }) => {
    
    
  const packet = JSON.parse(data);

  switch (packet.type) {
    
    
    case "hello from server":
      // ...
      break;
  }
});

Socket.IO を使用した同じ例を次に示します。

サーバ

import {
    
     Server } from "socket.io";

const io = new Server(3000);

io.on("connection", (socket) => {
    
    
  // 向客户端发送消息
  socket.emit("hello from server", 1, "2", {
    
     3: Buffer.from([4]) });

  // 从客户端接收消息
  socket.on("hello from server", (...args) => {
    
    
    // ...
  });
});

クライアント

import {
    
     io } from "socket.io-client";

const socket = io("ws://localhost:3000");

// 向服务器发送消息
socket.emit("hello from server", 5, "6", {
    
     7: Uint8Array.from([8]) });

// 从服务器接收消息
socket.on("hello from server", (...args) => {
    
    
  // ...
});

これら 2 つの例は非常に似ていますが、実際には Socket.IO は、実稼働環境で WebSocket ベースのアプリケーションを実行する際の複雑さを隠す追加機能を提供します。これらの関数を以下に示します。

しかし、最初に、Socket.IO ではないものについて明確にしましょう。

Socket.IO ではないもの:

Socket.IO はWebSocket の実装ではありません。

Socket.IO は可能な限り転送に WebSocket を使用しますが、各パケットに追加のメタデータを追加します。そのため、WebSocket クライアントは Socket.IO サーバーに正常に接続できず、Socket.IO クライアントはプレーンな WebSocket サーバーに接続できません。

// 警告:客户端将无法连接!
const socket = io("ws://echo.websocket.org");

プレーンな WebSocket サーバーを探している場合は、wsまたはµWebSockets.jsを調べてください。

Node.js コアに WebSocket サーバーを含めることについても議論があります。クライアント側では、robust-websocketに関心があるかもしれません。

Socket.IO は、モバイル アプリケーションのバックグラウンド サービスとして使用するためのものではありません

Socket.IO ライブラリは、サーバーへの開いた TCP 接続を維持します。これにより、ユーザーのバッテリが大幅に消耗する可能性があります。このユース ケースでは、FCMなどの専用メッセージング プラットフォームを使用してください。

特徴

Socket.IO がプレーンな WebSocket で提供するものは次のとおりです。

HTTP ロング ポーリングのフォールバック

WebSocket 接続を確立できない場合、接続は HTTP ロング ポーリングにフォールバックします。

10 年以上前 (!) にプロジェクトを作成したときに人々が Socket.IO を使用したのは、この機能が理由です。これは、WebSocket に対するブラウザーのサポートがまだ初期段階にあったためです。

現在、ほとんどのブラウザーが WebSocket をサポートしていますが ( 97%以上)、WebSocket 接続を確立できないユーザーから、誤った構成の動作を使用しているという報告が依然として寄せられているため、これは依然として優れた機能です。

自動再接続

いくつかの特定のケースでは、サーバーとクライアント間の WebSocket 接続が、どちらの当事者もリンクの切断状態を認識せずに切断されることがあります。

そのため、Socket.IO には、接続の状態を定期的にチェックするハートビート メカニズムが含まれています。

クライアントが最終的に切断されると、サーバーを圧倒しないように指数関数的なバックオフ遅延で自動的に再接続します。

パケット バッファ

クライアントが切断すると、パケットは自動的にバッファリングされ、再接続すると送信されます。

詳細はこちら

受信後のコールバック

Socket.IO は、イベントを送信して応答を受信する便利な方法を提供します。

差出人

socket.emit("hello", "world", (response) => {
    
    
  console.log(response); // "got it"
});

受信機

socket.on("hello", (arg, callback) => {
    
    
  console.log(arg); // "world"
  callback("got it!");
});

タイムアウトを追加することもできます:

socket.timeout(5000).emit("hello", "world", (err, response) => {
    
    
  if (err) {
    
    
    // 另一方未在给定延迟内确认事件
  } else {
    
    
    console.log(response); // "got it"
  }
});

ブロードキャスト

サーバー側では、接続されているすべてのクライアントまたはクライアントのサブセットにイベントを送信できます。

// 到所有连接的客户端
io.emit("hello");

// 致“news”房间中的所有连接客户端
io.to("news").emit("hello");

これは、複数のノードにスケーリングする場合にも機能します。

多重化

名前空間を使用すると、アプリケーションのロジックを単一の共有接続に分割できます。これは、たとえば、許可されたユーザーのみが参加できる「管理者」チャネルを作成する場合に役立ちます。

io.on("connection", (socket) => {
    
    
  // 普通用户
});

io.of("/admin").on("connection", (socket) => {
    
    
  // 管理员用户
});

よくある問題

今でも Socket.IO が必要ですか?

最近では WebSocket がほぼすべての場所でサポートされているため、これは良い質問です。

そうは言っても、アプリケーションでプレーンな WebSocket を使用している場合、reconnection acknowledgement または放送

Socket.IO プロトコルのデータ テーブル サイズは?

socket.emit("hello", "world")以下を含む単一の WebSocket フレームとして送信されます42["hello","world"]

  • 4Engine.IO の「メッセージ」パケット タイプです。
  • 2Socket.IO「メッセージ」パケットタイプです
  • ["hello","world"]JSON.stringify()引数配列の -ed バージョンです

そのため、メッセージごとに数バイトが追加されますが、カスタム パーサーを使用することでさらに削減できます。

& ブラウザ バンドル自体のサイズは10.4 kB(縮小および圧縮) です。

始める

免責事項: socket.io のバージョンによって使用方法が異なります。最新の使用方法は次のとおりです。

最初のステップ: Express フレームワークを使用してサーバー ルーティングを構築する

const express = require('express'); //引入express模块
const app = express();
app.get('/', (req, res) => {
    
    
  res.sendFile(__dirname + '/login.html');
});
/http/
const http = require('http');//此处引用的是http,但是我们的目的是创建P2P视频,所以要开启https,所以需要拥有ssl证书,这个可以在各大云服务商免费申请(推荐华为云和阿里云)
const httpServer = http.createServer(app)
				.listen(8888, "0.0.0.0");
https
const https = require('https');
var fs = require("fs");
var opt = {
    
    
    key:fs.readFileSync("./cert/ssl_server.key"),
    cert:fs.readFileSync("./cert/ssl_server.pem")
}//引入ssl证书
const httpsServer = http.createServer(app,opt)
 				.listen(8008, "0.0.0.0");
//这样一个服务器就搭好了,直接node xx.js就可以启动

ステップ 2: Socket.io サーバーを使用する

サーバーは直接 npm install socket.io できます

老版本///
var io = socketIo.listen(httpsServer);
/新版本///
const io = new Server(httpsServer);

io.sockets.on("connection", (socket)=>{
    
    
    /操作内容///
})

ステップ 3: Socket.io クライアントを使用する

クライアントは CDN を使用して、関連する js ライブラリをインポートまたはダウンロードできます

<script src="/socket.io/socket.io.js"></script>
<script>
  var url = "120.78.xxx.xx:8008"
  var socket = io(url);
  });
</script>

このようなクライアントはすでにサーバーに接続しており、次のステップは関連する操作です

ステップ4:関連操作(略)

P2P ルームに参加するプロセス

下の図では、3 つのクライアント ABC があり、それらは同時にシグナリング サーバーに接続されています. 最初に、A がルームに参加する信号の参加を開始し、次にシグナリング サーバーが A をルームに追加し、参加した信号をに返信しますA; 次に、B が部屋に参加する信号を開始し、次に信号サーバーが B を部屋に参加させ、参加した信号を B に返信し、同時に A に他の部屋に参加する信号 otherjoin を与えます。その後、C はルームに参加するためにシグナル ジョインを開始しますが、シグナル サーバーはルーム内の人数を制御します。これは、部屋がいっぱいで、あなたが追加されていないことを示します。このとき、AさんとBさんは同じ部屋にいて、部屋の中でコミュニケーションが取れます。
ここに画像の説明を挿入

ステートマシン

状態の変換と判断にステート マシンを使用する

ステート マシンがある理由:

まず、クライアントがチャット ルームでいくつかの状態を持つことを考慮してください。

入室前または退室後(Init/Leave)

入室後(入室)

2 番目のおしゃべりが参加した後 (joined_conn)

2 番目のおしゃべりが去った後 (joined_unbind)

上記の状態から、ユーザーの状態が入退室でない限り、ユーザーがルームに出入りするとどうなるかがわかります。ここで、ユーザーがチャット ルームに入室し、チャット ルームから退出するとどうなるかを考える必要があります。チャット ルームには少なくとも 1 人がいて、この人が開始者です。現在のユーザーが開始者であることをどのように知ることができますか。ご利用者様の状態により決定いたします。次に、下の写真を見てください.最初は現在のユーザーはルームに参加していない、つまり退出状態ですが、ルームに参加した後、彼は参加しました.2番目の人が参加すると、彼はjoined_connになりますJoined_conn 状態が現れるので、現在のユーザーが最初のユーザー、つまりイニシエーター (イニシエーターの役割にはメディア ネゴシエーションが含まれます) であるかどうかを判断できます。最後に、2 番目のユーザーが離れると、joined_unbind 状態になります。
ここに画像の説明を挿入

ICE フレームワーク

まず、2 つのクライアントがポイントツーポイントで通信する方法を理解しましょう。

最初のもの:ホストIPを自分で知っている

2番目の方法:STUNサーバーを使用します。AとBの両方がSTUNサーバーにアクセスして、相手のパブリックネットワークIPを取得し、シグナリングサーバーを使用してアクセスできます。クライアントは、シグナリングサーバーを介してシグナリングサーバーに参加しています情報を交換することで、NAT の侵入の効果を得ることができます

3 番目のタイプ: 中継サーバーを使用する 中継サーバー (TURN サーバー)

この 3 つの方法が 3 つの候補です. なぜ 3 つの通信方法があるのか​​というと、シグナリング サーバーはできるだけ通信に参加することを避けたいか、またはシグナリング サーバーは単純な情報通信のみを行いたいためです。したがって、オーディオとビデオの通信の可能性が高いのは、リレー サーバーと STUN サーバーであることがわかります。
ここに画像の説明を挿入

これで、ホスト IP、リレー サーバー、および STUN サーバーがすべて 1 つの ICE サーバー プロジェクトに統合され、あとはこのサーバーを構築するだけで済みます。

スタン/ターン サーバーを構築する手順:

最初に依存ライブラリをインストールします。

ubuntu:
apt(-get) install build-essential
apt(-get) install openssl libssl-dev
centos:
yum install libevent-devel openssl-devel

バージョン 4.5 のソース コードをダウンロード

wget https://github.com/coturn/coturn/archive/4.5.1.1.tar.ge
连不上github的查下资料改hosts

解凍する

tar -zxvf 4.5.1.1.tar.gz

プロジェクト ディレクトリに移動します。

cd coturn-4.5.1.1

ソースコードのインストール3回

./configure
make
make install

構成ファイルのコピー

cp examoles/etc/turnserver.conf bin/turnserver.conf

構成ファイルを変更する

#服务器监听端口,3478为默认端口,可修改为自己想要的端口号
listening-port=3478
#想要监听的ip地址,云服务器则是云服务器的内网ip
listening-ip=xxx.xxx.xxx.xxx
#扩展ip地址,云服务器则是云服务器的外网ip
extenal-ip=xxx.xxx.xxx.xxx
#可以设置访问的用户及密码
user=demon:123

サービス開始

turnserver -v -r 外网ip:监听端口 -a -o

確認:

https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

ホストローカル接続

srflx nat マッピング後の結果

中継中継サーバー
ここに画像の説明を挿入

WebRTC フレームワーク

RTCPeerConnection 関連の原則

pc = 新しい RTCPeerConnection([構成])

PCの関連能力:

メディア相談

ストリームとトラックの追加と停止

輸送関連機能

統計関連機能

メディア相談

各クライアントは、独自のサポート メディア フォーマット情報を持っているため、両者のメディア サポート フォーマットを統一するためには、PC の機能であるメディア ネゴシエーションが必要です。

プロセスは次のとおりです。

ここに画像の説明を挿入

まず最初に、A はイニシエーター (ステート マシンを介して認識) であり、最初にオファーを取得します。この Get プロセスは、A 自身のメディア情報と候補情報 (ICE を介して認識) を収集し、次にこの情報を setLocalDescription し、メディアを配置します。情報はシグナリング サーバに送信され、シグナリング サーバはそれをルーム内の別のクライアント B に送信します。次に、B はこのメッセージを受信した後、このメッセージの setRemoteDescription を設定します. 同時に、B は自身のメディア情報と候補情報も収集します. setLocalDescription の後、応答をシグナリング サーバーに送信し、シグナリング サーバーはそれを A に転送します.回答後にRemoteDescriptionも設定します。このようにして、両当事者は、メディア情報と、反対側が支持する候補者を知ることができます。

ここに画像の説明を挿入

コードは以下のように表示されます:

function mediaNegociate() {
    
    
    if (status === "joined_conn") {
    
    //joined_conn代表我是连接的发起人
        if (pc) {
    
    
            var options = {
    
    //要协商的内容,如音频、视频...
                offerToReceiveVideo:true,
                offerToReceiveAudio:true
            }

            pc.createOffer(options)
                .then(getOffer)
                .catch(handleErr);
        }
    }
}


socked.on("vgetdata", (room, data)=>{
    
    
        console.log("vgetdata:", data);
        if (!data) {
    
    
            return ;
        }
        if (data.type === "candidata") {
    
    //拿到对方传过来的候选者信息
           //
        } else if(data.type === "offer") {
    
    //媒体协商默认有type值为offer
            console.log("get offer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来

            //查询自己的媒体格式信息并且应答给对方
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleErr);
        } else if(data.type === "answer") {
    
    //媒体协商默认回应有type值为answer
            console.log("get answer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方,对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端,第一个进来的会发offer,第二个进来的会发answer。把对方的媒体格式设置进来
        }
    });

P2Pコード

html コード:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>视频聊天</title>
    <link rel="icon" href="./2.jpg" type="image/x-icon">
</head>
<body>

    <div align="center">
        <table>
            <tr><td colspan="2">
                <h1 id="welcom">欢迎来到1v1的聊天室</h1>
                <input id = "room">
                <button id = "enterRoom">进入房间</button>
                <button id="leaveRoom" >离开房间</button>
            </td></tr>
            <tr>
                <td><lable>本地视频</lable></td>
                <td><label>远端视频</label></td>
            </tr>
            <tr>
                <td><video id="localVideo" autoplay playsinline></video></td>
                <td><video id="remoteVideo" autoplay playsinline></video></td>
            </tr>
        </table>
    </div>

    <script src="js/socket.io.js"></script>
    <script src="js/videoRoom.js"></script>

</body>
</html>

シグナリング サーバー コード:

"use strict";

var http = require("http");
var https = require("https");
var fs = require("fs");

//自己安装的模块
var express = require("express");
var serveIndex = require("serve-index");//文件目录
var sqlite3 = require("sqlite3");
var log4js = require("log4js");
var socketIo = require("socket.io");

var logger = log4js.getLogger();
logger.level = "info";

var app=express();
app.use(serveIndex("./zhangyangsong"));
app.use(express.static("./zhangyangsong"));

var opt = {
    
    
    key:fs.readFileSync("./cert/ssl_server.key"),
    cert:fs.readFileSync("./cert/ssl_server.pem")
}

// var httpServer=http.createServer(app)
//     .listen(8888, "0.0.0.0");
var httpsServer=https.createServer(opt, app)
    .listen(8008, "0.0.0.0");

var db = null;
var sql = "";
// var io = socketIo.listen(httpServer);
var io = socketIo.listen(httpsServer);
io.sockets.on("connection", (socket)=>{
    
    
    logger.info("connection:", socket.id);

    //处理1v1聊天室的消息
    socket.on("vjoin", (room, uname)=>{
    
    
        logger.info("vjoin", room, uname);
        socket.join(room);

        var myRoom = io.sockets.adapter.rooms[room];
        var users = Object.keys(myRoom.sockets).length;
        logger.info(room + "user=" + users);
        if (users > 2) {
    
    
            socket.leave(room);
            socket.emit("vfull", room);
        } else {
    
    
            socket.emit("vjoined", room);
            if (users > 1) {
    
    
                socket.to(room).emit("votherjoined", room, uname);
            }
        }
    });
    socket.on("vdata", (room, data)=>{
    
    
        logger.info("vdata", data);
        socket.to(room).emit("vgetdata", room, data);
    });

    socket.on("vleave", (room, uname)=>{
    
    
        if (room === "") {
    
    
            logger.info("room is empty string");
        } else if (room === undefined) {
    
    
            logger.info("room is undefine");
        } else if (room === null) {
    
    
            logger.info("room is null");
        }

        var myRoom = io.sockets.adapter.rooms[room];
        var users = Object.keys(myRoom.sockets).length;

        logger.info("vleave users=" + (users - 1));
        socket.leave(room);
        socket.emit("vleft", room);
        socket.to(room).emit("votherleft", room, uname);
    });
});

function handleErr(e) {
    
    
    logger.info(e);
}

特定の操作 js コード:

"use strict"
//整个P2P过程需要知道双方的媒体类型、流和候选者
var hWelcom = document.querySelector("h1#welcom");

var url = location.href;
var uname = url.split("?")[1].split("=")[1];

hWelcom.textContent = "欢迎来到1v1视频聊天室:" + uname;

var iptRoom = document.querySelector("input#room");
var btnEnterRoom = document.querySelector("button#enterRoom");
var btnLeaveRoom = document.querySelector("button#leaveRoom");

var videoLocal = document.querySelector("video#localVideo");
var videoRemote = document.querySelector("video#remoteVideo");

var localStream = null;
var remoteStream = null;

var socked = null;
var room = null;
var status = "init";
var pc = null;
var url = "120.78.130.50:8008"
function getMedia(stream) {
    
    
    localStream = stream;
    videoLocal.srcObject = stream;
}

function  start() {
    
    
    var constraints = {
    
    
        video:true,
        audio:true
    };

    //打开摄像头
    navigator.mediaDevices.getUserMedia(constraints)
        .then(getMedia)
        .catch(handleErr);
    conn();
}

function conn() {
    
    
    socked = io.connect(url);
    //监听来自服务器的信号
    socked.on("vfull", (room)=>{
    
    
        status = "leaved";
        alert("房间已满:" + room);
        console.log("vfull", status);
    });

    socked.on("vjoined", (room)=>{
    
    
        //创建视频连接类
        alert("成功加入房间:" + room);
        createPeerConnection();

        status = "joined";
        console.log("vjoined:", status);
    });

    socked.on("votherjoined", (room, uname)=>{
    
    
        //建立视频连接
        alert("有人进来了:" + uname);

        if (status === "joined_unbind") {
    
    
            createPeerConnection();
        }
        status = "joined_conn";

        //当第二个人进来就要发起媒体协商了:媒体协商就是双方互相知道和设置对方的媒体格式
        mediaNegociate();

        console.log("votherjoined:", status);
    });

    socked.on("vgetdata", (room, data)=>{
    
    
        console.log("vgetdata:", data);
        if (!data) {
    
    
            return ;
        }
        if (data.type === "candidata") {
    
    //拿到对方传过来的候选者信息
            console.log("get other candidata");

            //候选者信息
            var cddt = new RTCIceCandidate({
    
    
                sdpMLineIndex:data.label,
                candidate:data.candidate
            });
            pc.addIceCandidate(cddt);//把候选者对象加入pc

        } else if(data.type === "offer") {
    
    //媒体协商默认有type值为offer
            console.log("get offer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来

            //查询自己的媒体格式信息并且应答给对方
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleErr);
        } else if(data.type === "answer") {
    
    //媒体协商默认回应有type值为answer
            console.log("get answer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方,对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端,第一个进来的会发offer,第二个进来的会发answer。把对方的媒体格式设置进来
        }
    });

    socked.on("vleft", (room)=>{
    
    
        status = "leaved";
        console.log("vleft:", status);
    });

    socked.on("votherleft", (room, uname)=>{
    
    
        status = "joined_unbind";
        closePeerConnection();
        console.log("votherleft:", status);
    });
}

function getAnswer(decs) {
    
    
    pc.setLocalDescription(decs);//设置一下本地的媒体格式信息
    sendMessage(decs);
}

function mediaNegociate() {
    
    
    if (status === "joined_conn") {
    
    //joined_conn代表我是连接的发起人
        if (pc) {
    
    
            var options = {
    
    //要协商的内容,如音频、视频...
                offerToReceiveVideo:true,
                offerToReceiveAudio:true
            }

            pc.createOffer(options)
                .then(getOffer)
                .catch(handleErr);
        }
    }
}

function getOffer(desc) {
    
    //收到的媒体格式
    pc.setLocalDescription(desc);
    sendMessage(desc);//把我需要的媒体格式发给对方
}

function createPeerConnection() {
    
    
    if (!pc) {
    
    
        var pcConfig = {
    
    //ICE服务器
            "iceServers":[{
    
    
                "urls":"turn:120.78.130.xx:3478", //指定中继服务器turn
                "username":"zhangyangsong",
                "credential":"123"
            }]
        }

        pc = new RTCPeerConnection(pcConfig); //pc作用:媒体协商,流和轨的添加和停止,传输相关功能,统计相关功能

        pc.onicecandidate = (e)=>{
    
     //得到了ICE服务器选择的候选者返回的事件
            if (e.candidate) {
    
    //先判断是不是候选者事件回来的
                console.log("CANDIDATE", e.candidate);
                sendMessage({
    
    //把候选者信息发给对方(会发给信令服务器然后转发给对方)
                    type:"candidata",
                    label:e.candidate.sdpMLineIndex,//候选者标签
                    id:e.candidate.sdpMid,//候选者id
                    candidate:e.candidate.candidate//候选者数据
                });
            }
        }

        //当媒体到达的时候,做什么
        pc.ontrack = (e)=>{
    
    //ontrack收到远程音视频轨e时
            //alert("连接成功")
            remoteStream = e.streams[0];
            videoRemote.srcObject = remoteStream;//把远程媒体流放到远程音频标签里面显示出来
        }
    }

    if (localStream) {
    
    
        localStream.getTracks().forEach((track)=>{
    
    
            pc.addTrack(track, localStream);//将本地的媒体流轨加到pc里面
        })
    }
}

start();

function sendMessage(data) {
    
    
    if (socked) {
    
    
        socked.emit("vdata", room, data);
    }
}

function handleErr(e) {
    
    
    console.log(e);
}

function enterRoom() {
    
    
    room = iptRoom.value.trim();
    if (room === "") {
    
    
        alert("请输入房间号");
        return;
    }
    socked.emit("vjoin", room, uname);
}

function  leaveRoom() {
    
    
    socked.emit("vleave", room, uname);
    closePeerConnection();
}

function closePeerConnection() {
    
    
    console.log("close RTCPeerConnection");
    if (pc) {
    
    
        pc.close();
        pc = null;
    }
}

btnEnterRoom.onclick = enterRoom;
btnLeaveRoom.onclick = leaveRoom;

この時点で WebRTCP2P チャット ルーム全体が完成します. WebRTC が開発できる機能はまだたくさんありますが、基本的な原則はこれらのいくつかのコンテンツです.

注: chrome://flags/ の Web サイトを開いてプラットフォームを検索し、開いてください。

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/qq_58360406/article/details/129111151