如何在WebRTC中实现局域网视频连接?
去年TSINGSEE青犀视频研发团队基于WEBRTC架构开了网页音视频通话平台EasyRTC,EasyRTC支持微信小程序、H5页面、APP、PC客户端等接入方式之间互通,快速从零开始搭建实时音视频通信;支持多人至百万人视频通话,满足语音视频社交。
如果大家对EasyRTC感兴趣,可以联系我们进行了解或试用。今年我们仍然没有停止对WEBRTC技术的探索。本文和大家分享一下通过webrtc实现局域网进行音视频连接的步骤。
1、首先要创建服务代码,来确保服务器启动
const Koa = require('koa'); const path = require('path'); const koaSend = require('koa-send'); const static = require('koa-static'); const socket = require('koa-socket'); const users = {}; // 保存用户 const sockS = {}; // 保存客户端对应的socket const io = new socket({ ioOptions: { pingTimeout: 10000, pingInterval: 5000, } }); const https = require("https"); const fs = require("fs"); // 创建一个Koa对象表示web app本身: const app = new Koa(); // socket注入应用 io.attach(app); app.use(static( path.join( __dirname, './public') )); // 对于任何请求,app将调用该异步函数处理请求: app.use(async (ctx, next) => { if (!/\./.test(ctx.request.url)) { await koaSend( ctx, 'index.html', { root: path.join(__dirname, './'), maxage: 1000 * 60 * 60 * 24 * 7, gzip: true, } // eslint-disable-line ); } else { await next(); } }); // io.on('join', ctx=>{ // event data socket.id // }); app._io.on('connection', sock => { sock.on('join', data=>{ console.log("join:", data); sock.join(data.roomid, () => { if (!users[data.roomid]) { users[data.roomid] = []; } let obj = { account: data.account, id: sock.id }; let arr = users[data.roomid].filter(v => v.account === data.account); if (!arr.length) { users[data.roomid].push(obj); } sockS[data.account] = sock; app._io.in(data.roomid).emit('joined', users[data.roomid], data.account, sock.id); // 发给房间内所有人 // sock.to(data.roomid).emit('joined',data.account); }); }); sock.on('offer', data=>{ console.log('offer', data); sock.to(data.roomid).emit('offer',data); }); sock.on('answer', data=>{ console.log('answer', data); sock.to(data.roomid).emit('answer',data); }); sock.on('__ice_candidate', data=>{ console.log('__ice_candidate', data); sock.to(data.roomid).emit('__ice_candidate',data); }); // 1 v 1 sock.on('apply', data=>{ // 转发申请 // console.log("apply:", data); sockS[data.account].emit('apply', data); }); sock.on('reply', data=>{ // 转发回复 console.log("reply:", data); sockS[data.account].emit('reply', data); }); sock.on('1v1answer', data=>{ // 转发 answer sockS[data.account].emit('1v1answer', data); }); sock.on('1v1ICE', data=>{ // 转发 ICE sockS[data.account].emit('1v1ICE', data); }); sock.on('1v1offer', data=>{ // 转发 Offer sockS[data.account].emit('1v1offer', data); }); sock.on('1v1hangup', data=>{ // 转发 hangup sockS[data.account].emit('1v1hangup', data); }); }); app._io.on('disconnect', (sock) => { for (let k in users) { users[k] = users[k].filter(v => v.id !== sock.id); } console.log(`disconnect id => ${users}`); }); // 在端口3001监听: let port = 3001; app.listen(port, _ => { console.log('app started at port ...' + port); });
2、在进行音视频通信是,必修要本地端口或者https服务。
(1)网上下载OpenSSL软件
(2)创建后缀名(.Key和.cert)证书;
查看网址教程:https://www.cnblogs.com/tugenhua0707/p/10927722.html
(3)在js服务把证书添加进去
const options = { key: fs.readFileSync("./server.key", "utf8"), cert: fs.readFileSync("./server.cert", "utf8") }; let server = https.createServer(options, app.callback()).listen(3002); app._io.listen(server);
3、客户端代码采用VUE,如下(重要部分代码):
<template> <div class="remote1" v-loading="loading" :element-loading-text="loadingText" element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)" > <div class="shade" v-if="!isJoin"> <div class="input-container"> <input type="text" v-model="account" placeholder="请输入你的昵称" @keyup.enter="join"> <button @click="join">确定</button> </div> </div> <div class="userList"> <h5>在线用户:{{userList.length}}</h5> <p v-for="v in userList" :key="v.account"> {{v.account}} <i v-if="v.account === account || v.account === isCall"> {{v.account === account ? 'me' : ''}} {{v.account === isCall ? 'calling' : ''}} </i> <span @click="apply(v.account)" v-if="v.account !== account && v.account !== isCall">呼叫 {{v.account}}</span> </p> </div> <div class="video-container" v-show="isToPeer"> <div> <video src="" id="rtcA" controls autoplay></video> <h5>{{account}}</h5> <button @click="hangup">hangup</button> </div> <div> <video src="" id="rtcB" controls autoplay></video> <h5>{{isCall}}</h5> </div> </div> </div> </template> <script> import socket from '../../utils/socket'; export default{ name: 'remote1', data() { return { account: window.sessionStorage.account || '', isJoin: false, userList: [], roomid: 'webrtc_1v1', // 指定房间ID isCall: false, // 正在通话的对象 loading: false, loadingText: '呼叫中', isToPeer: false, // 是否建立了 P2P 连接 peer: null, offerOption: { offerToReceiveAudio: 1, offerToReceiveVideo: 1 } }; }, methods: { join() { if (!this.account) return; this.isJoin = true; window.sessionStorage.account = this.account; socket.emit('join', {roomid: this.roomid, account: this.account}); }, initSocket() { socket.on('joined', (data) => { this.userList = data; }); socket.on('reply', async data =>{ // 收到回复 this.loading = false; console.log(data); switch (data.type) { case '1': // 同意 this.isCall = data.self; // 对方同意之后创建自己的 peer await this.createP2P(data); // 并给对方发送 offer this.createOffer(data); break; case '2': //拒绝 this.$message({ message: '对方拒绝了你的请求!', type: 'warning' }); break; case '3': // 正在通话中 this.$message({ message: '对方正在通话中!', type: 'warning' }); break; } }); socket.on('apply', (data) => { // 收到请求 if (this.isCall) { this.reply(data.self, '3'); return; } this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', { confirmButtonText: '同意', cancelButtonText: '拒绝', type: 'warning' }).then(async () => { await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer this.isCall = data.self; this.reply(data.self, '1'); }).catch(() => { this.reply(data.self, '2'); }); }); socket.on('1v1answer', (data) =>{ // 接收到 answer this.onAnswer(data); }); socket.on('1v1ICE', (data) =>{ // 接收到 ICE this.onIce(data); }); socket.on('1v1offer', (data) =>{ // 接收到 offer this.onOffer(data); }); socket.on('1v1hangup', _ =>{ // 通话挂断 this.$message({ message: '对方已断开连接!', type: 'warning' }); this.peer.close(); this.peer = null; this.isToPeer = false; this.isCall = false; }); }, hangup() { // 挂断通话 socket.emit('1v1hangup', {account: this.isCall, self: this.account}); this.peer.close(); this.peer = null; this.isToPeer = false; this.isCall = false; }, apply(account) { // account 对方account self 是自己的account this.loading = true; this.loadingText = '呼叫中'; socket.emit('apply', {account: account, self: this.account}); }, reply(account, type) { socket.emit('reply', {account: account, self: this.account, type: type}); }, async createP2P(data) { this.loading = true; this.loadingText = '正在建立通话连接'; await this.createMedia(data); }, async createMedia(data) { // 保存本地流到全局 try { this.localstream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); let video = document.querySelector('#rtcA'); video.srcObject = this.localstream; console.log("发送端:", Date.now()); } catch (e) { console.log('getUserMedia: ', e) } this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection }, initPeer(data) { // 创建输出端 PeerConnection let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; this.peer = new PeerConnection(); this.peer.addStream(this.localstream); // 添加本地流 // 监听ICE候选信息 如果收集到,就发送给对方 this.peer.onicecandidate = (event) => { if (event.candidate) { socket.emit('1v1ICE', {account: data.self, self: this.account, sdp: event.candidate}); } }; this.peer.onaddstream = (event) => { // 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src this.isToPeer = true; this.loading = false; let video = document.querySelector('#rtcB'); video.srcObject = event.stream; console.log("接送端:", Date.now()); }; }, async createOffer(data) { // 创建并发送 offer try { // 创建offer let offer = await this.peer.createOffer(this.offerOption); // 呼叫端设置本地 offer 描述 await this.peer.setLocalDescription(offer); // 给对方发送 offer socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer}); } catch (e) { console.log('createOffer: ', e); } }, async onOffer(data) { // 接收offer并发送 answer try { // 接收端设置远程 offer 描述 await this.peer.setRemoteDescription(data.sdp); // 接收端创建 answer let answer = await this.peer.createAnswer(); // 接收端设置本地 answer 描述 await this.peer.setLocalDescription(answer); // 给对方发送 answer socket.emit('1v1answer', {account: data.self, self: this.account, sdp: answer}); } catch (e) { console.log('onOffer: ', e); } }, async onAnswer(data) { // 接收answer try { await this.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述 } catch (e) { console.log('onAnswer: ', e); } }, async onIce(data) { // 接收 ICE 候选 try { await this.peer.addIceCandidate(data.sdp); // 设置远程 ICE } catch (e) { console.log('onAnswer: ', e); } } }, mounted() { this.initSocket(); if (this.account) { this.join(); } } } </script> <style lang="scss" scoped> .remote1{ width: 100%; height: 100%; display: flex; justify-content: flex-start; } .shade{ position: fixed; width:100vw; height: 100vh; left: 0; top:0; z-index: 100; background-color: rgba(0,0,0,0.9); .input-container{ position: absolute; left:50%; top:30%; transform: translate(-50%, 50%); display: flex; justify-content: space-between; align-items: center; input{ margin: 0; } } } .userList{ border: 1px solid #ddd; margin-right: 50px; h5{ text-align: left; margin-bottom: 5px; } p{ border-bottom: 1px solid #ddd; line-height: 32px; width:200px; position: relative; overflow: hidden; cursor: pointer; span{ position: absolute; left:0; top:100%; background-color: #1fbeca; color: #fff; height: 100%; transition: top 0.2s; display: block; width: 100%; } i{ font-style: normal; font-size: 11px; border: 1px solid #1fbeca; color: #27cac7; border-radius: 2px; line-height: 1; display: block; position: absolute; padding: 1px 2px; right: 5px; top: 5px; } } p:last-child{ border-bottom: none; } p:hover span{ top:0; } } .video-container{ // display: flex; // justify-content: center; video{ width: 400px; height: 300px; margin-left: 20px; background-color: #ddd; } } </style>
说明
首先必修启动服务端建立连接,在通过呼叫对方id进行识别;建立PeerConnection视频连接,并把SDP的ICE候选者传递candidate给远程对等体

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
在SpringBoot中使用RedisTemplate重新消费Redis Stream中未ACK的消息
消费组从stream中获取到消息后,会分配给自己组中其中的一个消费者进行消费,消费者消费完毕,需要给消费组返回ACK,表示这条消息已经消费完毕了。 当消费者从消费组获取到消息的时候,会先把消息添加到自己的pending消息列表,当消费者给消费组返回ACK的时候,就会把这条消息从pending队列删除。(每个消费者都有自己的pending消息队列) 消费者可能没有及时的返回ACK。例如消费者消费完毕后,宕机,没有及时返回ACK,此时就会导致这条消息占用2倍的内存(stream中保存一份, 消费者的的pending消息列表中保存一份) 关于Stream的基础姿势,可以先看看这篇贴帖子 如何在Springboot中使用Redis5的Stream 开始之前,通过Redis客户端模拟一点数据 1,新打开Redis客户端(我们称之为:生产端), 创建streamm,名称叫做:my_stream XADD my_stream * hello world 随便添加一条消息,目的是为了初始化stream 2,创建一个消费组,名称叫做:my_group XGROUP CREATE my_stream my...
- 下一篇
多线程问的太深入不知道怎么回答,从volatile开始给你讲清楚
volatile的用途 1.线程可见性 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。 package com.msb.testvolatile; public class T01_ThreadVisibility { private static volatile boolean fla...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS关闭SELinux安全模块
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Hadoop3单机部署,实现最简伪集群