博主正在担任一款电商app的全栈开发,其中涉及到一个视频通话功能。但是由于业务需求及成本考虑,不能使用第三方提供的SDK进行开发。所以博主选择使用PeerJs+WebSocket来实现这个功能。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebRTC(Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能
PeerJS simplifies WebRTC peer-to-peer data, video, and audio calls.
PeerJS wraps the browser’s WebRTC implementation to provide a complete, configurable, and easy-to-use peer-to-peer connection API. Equipped with nothing but an ID, a peer can create a P2P data or media stream connection to a remote peer.
以上内容来源于PeerJs官网介绍,大概的意思如下(仅供参考,博主英语不好):
PeerJS简化了WebRTC点对点数据、视频和音频呼叫。
PeerJS封装了浏览器的WebRTC实现 提供一个完整 可配置且易于使用的点对点连接API,只需要一个id就能创建到远程的P2P数据或媒体流连接。
PeerJs官网:https://peerjs.com
PeerJs官方文档:https://peerjs.com/docs/
主要用于即使通讯,辅助建立P2P视频通话,WebSocket即时通讯不是本文探讨的重点,有关WebSocket即时通讯的相关内容可以参考博主的另一篇WebSocket即时通讯的博客:https://blog.csdn.net/daiyi666/article/details/124824543?spm=1001.2014.3001.5501
经过上面对WebRTC的官方解释,估计很多小伙伴还是有点懵的状态。简而言之呢,WebRTC就是用于实现端到端这样的一个即时通讯的技术,也就是说无需经过服务器中转(建立连接以后)。相信很多了解过IPV4技术的小伙伴都知道,如果两个终端处在不同的网络下,是无法直接进行通信的,因为经过了NAT,而WebRTC可以借助一个公网的服务器,我们称之为“信令服务器”,以及一个“ICE”服务器进行身份信息交换和打洞,打洞完成之后即可进行P2P通信,且不再需要服务器进行中转
WebRTC架构图

| API | 用途 |
|---|---|
| getUserMedia() | 获取用户的摄像头以及麦克风 |
| RTCPeerConnection() | 端到端连接之间建立音视频通信及 NAT 穿透 |
| RTCDataChannel() | 端到端之间数据共享 |
要实现视频通话,首先应该要能够打开摄像头和麦克风,那么如何在浏览器中打开摄像头和麦克风,还有如何解决浏览器兼容问题呢,请参考以下代码:
function getUserMedia(constrains) {
if (window.navigator.mediaDevices.getUserMedia) {
return window.navigator.mediaDevices.getUserMedia(constrains);
} else if (window.navigator.webkitGetUserMedia) {
return window.navigator.webkitGetUserMedia(constrains);
} else if (window.navigator.mozGetUserMedia) {
return window.navigator.mozGetUserMedia(constrains);
} else if (window.navigator.getUserMedia) {
return window.navigator.getUserMedia(constrains);
}
}
getUserMedia函数将会返回一个Promise对象,这个Promise对象就封装了摄像头和麦克风的流媒体,而参数constrains是作为一个约束出现,通过这个约束对象,可以设置获取到的视频或音频的一些参数,如视频宽高,消除回音等,具体请参考https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia传送门
<template>
<video ref="localCameraVideo"></video>
<video ref="remoteCameraVideo"></video>
</template>
import { onMounted, ref } from "vue";
const localCameraVideo = ref();
const remoteCameraVideo = ref();
onMounted(() => {
getUserMedia().then(userMedia => {
//通过video对象的srcObject 赋值userMedia 就能预览到画面
localCameraVideo.value.srcObject = userMedia;
localCameraVideo.value.play();
});
});
注意事项
getUserMedia需要在localhost或者https环境中才能打开摄像头,否则将会报错,如果要部署测试,又没有https证书,那么可以通过设置谷歌浏览器参数绕过安全检测,具体操作是,右键谷歌浏览器图标->属性->目标
--unsafely-treat-insecure-origin-as-secure="你的服务器地址",示例:"C:\Program Files\Google\Chrome\Application\chrome.exe" --unsafely-treat-insecure-origin-as-secure="http://192.168.50.111:8080"博主使用了reconnecting-websocket库进行WebSocket连接,可根据实际情况选择其他库或者用原生WebSocket进行编码,有关WebSocket即时通讯的相关内容可以参考博主的另一篇WebSocket即时通讯的博客:https://blog.csdn.net/daiyi666/article/details/124824543?spm=1001.2014.3001.5501
function establishWebSocketConnection() {
const webSocketConnection = new ReconnectingWebSocket("你的WebSocket服务器地址");
webSocketConnection.onopen = () => {
//do something
};
webSocketConnection.onmessage = event => {
//do something
};
return webSocketConnection;
}
可能有的小伙伴困惑为什么要用WebSocket,这是因为后面创建了Peer对象之后会有一个ID,这个ID是全局唯一的,代表着一个Peer客户端,我们需要用WebSocket把这个ID发送给服务器,以及客户端离线之后从服务器上移除这个ID,还有从服务器即使更新在线的客户端等,当然也可以用ajax,只是WebSocket更具备即时性
function createPeerSendToWebSocketServer(webSocketConnection) {
const peer = new Peer();
//当peer被打开时被执行,peerId是全局唯一的
peer.on("open", peerId => {
console.log("peer opend, the peerId is:", peerId);
peer.on("close", () => {
console.log("peer close....");
//(呼叫方和接收方的ID都应该保存到服务器)
//此处应该发送一个JSON对象给服务器,方便判断,以下是伪代码,用于演示
webSocketConnection.send(peerId);
});
//(呼叫方和接收方的ID都应该保存到服务器)
//此处应该发送一个JSON对象给服务器,方便判断,以下是伪代码,用于演示
webSocketConnection.send(peerId);
});
return peer;
}
//peerId是对方的peer id,localUserMedia是上面通过getUserMedia获取到的(promise调then)
const call = peer.call(peerId, localUserMedia);
//当得到流时调用,remoteUserMedia 是对方的流媒体,直接赋值给video的srcObject 属性,就可以看到对方的画面了
call.on("stream", remoteUserMedia => {
remoteCameraVideo.value.srcObject = remoteUserMedia;
remoteCameraVideo.value.play();
});
});
//当收到对方的呼叫时调用,mediaConnection 封装了媒体连接
peer.on("call", mediaConnection => {
//通过mediaConnection相应给对方自己的媒体信息
mediaConnection.answer(localUserMedia);
//当得到流时调用,remoteUserMedia 是对方的流媒体
mediaConnection.on("stream", remoteCameraStream => {
remoteCameraVideo.value.srcObject = remoteCameraStream;
remoteCameraVideo.value.play();
});
});
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fenzhimedia</groupId>
<artifactId>video-call</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>video-call</name>
<description>video-call</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.fenzhimedia.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author Yi Dai 484201132@qq.com
* @since 2022/10/18 10:07
*/
@Configuration
public class WebsocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.fenzhimedia.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/**
* @author Yi Dai 484201132@qq.com
* @since 2022/3/7 15:47
*/
@Slf4j
@Component
@ServerEndpoint("/videoCallServerEndpoint")
public class VideoCallServerEndpoint {
@OnMessage
public void onMessage(Session session, String stringMessage) {
log.info("onMessage:the session is:{},the stringMessage is:{}", session, stringMessage);
}
@OnClose
public void onClose(Session session) {
log.info("onClose:the session is:{}", session);
}
@OnError
public void onError(Session session, Throwable e) {
log.info("onError:the session is:{},e:{}", session, e);
e.printStackTrace();
}
}
以上代码为伪代码,用于演示,有很多可以优化的地方;如在vue挂在完成之后立即创建websocket连接,然后创建peer对象注册到服务器中,这样才能保证在线状态
https://gitee.com/daiyi-personal/video-call-web.git
https://gitee.com/daiyi-personal/video-call-java.git
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、
2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p
Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg