webrtc实现局域网通话(三)

前言

先回顾一下上两篇的内容。第一篇,是调用摄像头采集视频的案例,首先创建了node.js 项目,然后用getUserMedia()这个东西获取MediaStream,然后调用video.srcObject = mediasteam,浏览器就显示采集内容了。第二篇,是单机单个页面呼叫的案例,主要目的是梳理信令转发流程,核心点在于RTCPeerConnection()这个迷人的对象。
本篇将引入socket.io,实现两台电脑之间的呼叫的案例。
说明:我现实情况是笔记本有摄像头,而台式机没有,因此效果是笔记本采集视频,台式机上线后,笔记本呼叫台式机,然后台式机显示笔记本端采集的视频。

好,下面进入实际案例

客户端

创建node.js项目,在项目文件下创建alice.html(呼叫者页面)文件,代码如下

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>alice</title>
        <link rel="stylesheet" href="css/main.css">
    </head>
    <body>
        <div class="container">
            <h1>ailce</h1>
            <hr>
            <div class="video_container" align="center">
                <video id="local_video"  poster="img/video_fill.jpg" autoplay muted></video>
            </div>
            <hr>
            <button id="startButton">获取本地视频</button>
            <button id="callButton">呼叫bob</button>
            <button id="hangupButton">挂断</button>
            <script src="/socket.io/socket.io.js"></script>
            <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
            <script src="js/alice.js"></script>
        </div>
    </body>
</html>

创建js文件夹,在其文件夹下创建alice.js文件,打开文件键入如下代码:

'use strict'

var localVideo = document.getElementById('local_video');

var startButton = document.getElementById('startButton');
var callButton = document.getElementById('callButton');
var hangupButton = document.getElementById('hangupButton');

var pc;
var localStream;
var socket = io.connect();

var config = {
    'iceServers': [{
        'urls': 'stun:stun.l.google.com:19302'
    }]
};

const offerOptions = {
    offerToReceiveVideo: 1,
    offerToReceiveAudio: 1
};

callButton.disabled = true;
hangupButton.disabled = true;

startButton.addEventListener('click', startAction);
callButton.addEventListener('click', callAction);
hangupButton.addEventListener('click', hangupAction);

function startAction() {
    navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(function (mediastream) {
        localStream = mediastream;
        localVideo.srcObject = mediastream;
        startButton.disabled = true;
    }).catch(function (e) {
        console.log(JSON.stringify(e));
    });
}

socket.on('create', function (room, id) {
    console.log('alice创建聊天房间');
    console.log(room + id);
});

socket.on('call', function () {
    callButton.disabled = false;
});

socket.on('signal', function (message) {
    if (pc !== 'undefined') {
        pc.setRemoteDescription(new RTCSessionDescription(message));
        console.log('remote answer');
    }
});

socket.on('ice', function (message) {
    if (pc !== 'undefined') {
        pc.addIceCandidate(new RTCIceCandidate(message));
        console.log('become candidate');
    }
});

socket.emit('create or join', 'room');

function callAction() {
    callButton.disabled = true;
    hangupButton.disabled = false;
    pc = new RTCPeerConnection(config);
    localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
    pc.createOffer(offerOptions).then(function (offer) {
        pc.setLocalDescription(offer);
        socket.emit('signal', offer);
    });
    pc.addEventListener('icecandidate', function (event) {
        var iceCandidate = event.candidate;
        if (iceCandidate) {
            socket.emit('ice', iceCandidate);
        }
    });
}

function hangupAction() {

    localStream.getTracks().forEach(track => track.stop());
    pc.close();
    pc = null;
    hangupButton.disabled = true;
    callButton.disabled = true;
    startButton.disabled = false;

}

创建bob.html文件(被呼叫端页面),编写如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>对方的视频</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="css/main.css">
    </head>
    <body>
        <div class="container">
            <h1>对方的视频</h1>
            <hr>
            <div class="video_container" align="center">
                <video id="remote_video" poster="img/video_fill.jpg" controls autoplay></video>
            </div>
            <hr>
            <script src="/socket.io/socket.io.js"></script>
            <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
            <script src="js/bob.js"></script>
        </div>
    </body>
</html>

在js文件中创建bob.js文件,编写如下代码:

'use strict'

var remoteVideo = document.getElementById('remote_video');

var socket = io.connect();

var config = {
    'iceServers': [{
        'urls': 'stun:stun.l.google.com:19302'
    }]
};

var pc;

socket.emit('create or join', 'room');

socket.on('join', function (room, id) {
    console.log('bob加入房间');
});

socket.on('signal', function (message) {
    pc = new RTCPeerConnection(config);
    pc.setRemoteDescription(new RTCSessionDescription(message));
    pc.createAnswer().then(function (answer) {
        pc.setLocalDescription(answer);
        socket.emit('signal', answer);
    });

    pc.addEventListener('icecandidate', function (event) {
        var iceCandidate = event.candidate;
        if (iceCandidate) {
            socket.emit('ice', iceCandidate);
        }
    });

    pc.addEventListener('addstream', function (event) {
        remoteVideo.srcObject = event.stream;
    });
});

socket.on('ice', function (message) {
    pc.addIceCandidate(new RTCIceCandidate(message));
});

至此,呼叫端和被呼叫端代码编写完成。

信令转发服务器

引入socket.io模块(sockio.io是一个开源的及时通讯框架)

npm install socket.io

新建index.js文件,编写如下代码:

'use strict'

var express = require('express');
var app = express();

var http  = require('http').createServer(app);
var io = require('socket.io')(http);

app.use('/css',express.static('css'));
app.use('/js',express.static('js'));
app.use('/img',express.static('img'));

app.get('/',function(request,response){
    response.sendFile(__dirname +'/index.html');
});

app.get('/alice',function(request,response){
    response.sendFile(__dirname+"/alice.html")
});

app.get('/bob',function(request,response){
    response.sendFile(__dirname+"/bob.html")
});

io.on('connection',function(socket){
    console.log('有用户加入进来');
    socket.on('signal',function(message){
        socket.to('room').emit('signal',message);
    });

    socket.on('ice',function(message){
        socket.to('room').emit('ice',message);
    });

    socket.on('create or join',function(room){
        var clientsInRoom = io.sockets.adapter.rooms[room];
        var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
        console.log(numClients);
        if(numClients===0){
            socket.join(room);
            socket.emit('create', room, socket.id);
            console.log('caller joined');
        }else if(numClients===1){
            socket.join(room);
            socket.to('room').emit('call');
            console.log('callee joined');
        }
    });
});

var server = http.listen(8080,function(){
    var host = server.address().address;
    var port = server.address().port;
    console.log('listening on:http://s%:s%',host,port);
});

至此,案例代码编写完成

测试结果

命令行 启动项目,打开chrome,地址栏输入localhost:8080/alice,打开alice页面(呼叫者),点击获取本地视频按钮,显示内容如下:

alice.png

打开bob(被呼叫者)页面,显示内容如下:

bob.png

被呼叫者上线后,呼叫者页面呼叫按钮可用,


call.png

点击呼叫bob,显示效果如下:

callee.png

总结

本章完成了局域网不同设备呼叫的案例。引入了socket.io框架,为信令转发提供服务,对信令转发的认识更加清晰了。在建立RTCPeerConnection()对象的时候,配置了iceServer,学名叫stun服务,这是一个协助p2p连接的服务。搭建stun服务,开源项目有coturn。
下面将在局域网打通android手机端和笔记本之间的连接。

推荐阅读更多精彩内容

  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 2,459评论 0 5
  • 单机版视频呼叫 前端代码 1、新建node.js项目,在项目文件夹下新建index.html打开,编写如下代码: ...
    EarthNut阅读 1,608评论 0 3
  • pyspark.sql模块 模块上下文 Spark SQL和DataFrames的重要类: pyspark.sql...
    mpro阅读 6,619评论 0 11
  • 加密货币,特别是比特币,几乎从各个方面都得到了大量关注:规则、管理、税务、技术、产品创新等等,不胜枚举。“点对点(...
    简闻阅读 359评论 0 9
  • 咋天吃洒喝高了,睡的不醒耽误更新了。 今天更新说什么呢?就说吃酒。 昨天吃的是青水花苗尚部的嫁女酒。种在竹子里的竹...
    歪才大白话阅读 207评论 2 9