一、webrtc版本接听视频电话-纯js版
先看效果
用户1--拨打

用户2–接听

前端代码

index.html
<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css">
<link rel="stylesheet" href="./assets/style.css">
<title>选择角色</title>
</head>
<body class="index">
<div class="card">
<div class="card-body">
<h5 class="card-title">选择角色</h5>
<p class="card-text">就像微信视频一样,总有一方是发起,另一方是接受。</p>
<a href="./a.html" class="btn btn-primary">我是发起方</a>
<a href="./b.html" class="btn btn-secondary">我是接受方</a>
</div>
</div>
</body> </html>
a.html

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./assets/style.css">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" >
<title>发起方</title>
</head> <body>
<div class="a-wrapp">
<div class="flex-center-wrapp">
<div class="status">
<table class="table">
<thead>
<tr>
<th scope="col">设备</th>
<th scope="col">信令服务器</th>
<th scope="col">webrtc状态</th>
</tr>
</thead>
<tbody>
<tr class="pc1-info">
<td class="name">本机</td>
<td class="websockt">断开</td>
<td class="webrtc">断开</td>
</tr>
<tr class="pc2-info">
<td class="name">远程</td>
<td class="websockt">断开</td>
<td class="webrtc">断开</td>
</tr>
</tbody>
</table>
</div>
<div class="videos">
<video class="local-video video" muted autoplay controls></video>
<video class="remote-video video" autoplay controls></video>
</div>
<div class="btns">
<button type="button" class="btn btn-secondary" onclick="start()">开始</button>
<button type="button" class="btn btn-success" onclick="call()">呼叫</button>
<button type="button" class="btn btn-danger" onclick="hungup()">挂断</button>
</div>
</div>
</div> <script src="./assets/helper.js"></script>
<script>
// 初始化ws
const myWs = initWs('pc1'); // 获取一些dom和定义变量
const pc1Info = document.querySelector('.pc1-info');
let [,pc1Ws, pc1Rtc] = pc1Info.querySelectorAll('td');
const pc2Info = document.querySelector('.pc2-info');
let [,pc2Ws, pc2Rtc] = pc2Info.querySelectorAll('td');
const remoteVideo = document.querySelector('.remote-video');
const localVideo = document.querySelector('.local-video');
let pc1 = null;
let localStram = null; // 开始按钮点击事件
const start = async () => {
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.srcObject = localStream;
} // 呼叫按钮点击事件
const call = async () => {
pc1 = new RTCPeerConnection();
// 核心:ice交换(ice即收集可用链路)
pc1.onicecandidate = (e) => {
e.candidate && myWs.sendIce('pc1',e.candidate);
}
localStream.getTracks().forEach(track => {
pc1.addTrack(track, localStream);
});
pc1.ontrack = async (event) => {
console.log(898989);
remoteVideo.srcObject = event.streams[0];
}; // 核心:sdp交换(spd即会话描述,如编码、stun、本机外网ip等基本信息)
const offer = await pc1.createOffer();
pc1.setLocalDescription(offer);
myWs.sendOffer(offer);
} const hungup = ()=>{ } // ws的onmessage事件
myWs.onmessage = async ({event,data}) => {
console.log(event, data);
if(event === "onlineChange"){
document.querySelectorAll('.websockt').forEach((item)=>{
item.innerHTML = '断开';
})
data.forEach((item)=>{
eval(`${item}Ws`).innerHTML = '已连接';
})
} if (event === "answer") {
await pc1.setRemoteDescription(data);
pc1Ws.innerHTML = '收到对方回应anser类型的sdp';
}else if(event === "ice" && data.id ==='pc2'){
pc1.addIceCandidate(data.ice)
pc1Ws.innerHTML = '收到对方回应ice';
} else if(event === "hello"){
eval(`${data.id}Ws`).innerHTML = '已连接'
}
} </script>
</body> </html>
b.html

<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./assets/style.css">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" >
<title>接收方</title>
</head> <body>
<div class="a-wrapp">
<div class="flex-center-wrapp">
<div class="status">
<table class="table">
<thead>
<tr>
<th scope="col">设备</th>
<th scope="col">信令服务器</th>
<th scope="col">webrtc状态</th>
</tr>
</thead>
<tbody>
<tr class="pc1-info">
<td class="name">本机</td>
<td class="websockt">断开</td>
<td class="webrtc">已就绪</td>
</tr>
<tr class="pc2-info">
<td class="name">远程</td>
<td class="websockt">断开</td>
<td class="webrtc">已就绪</td>
</tr>
</tbody>
</table>
</div>
<div class="videos">
<video class="local-video video" muted autoplay controls></video>
<video class="remote-video video" autoplay controls></video>
</div>
<div class="btns">
<div>接收方禁用以下功能,是给发送方用的</div>
<button type="button" disabled class="btn btn-secondary" onclick="start()">开始</button>
<button type="button" disabled class="btn btn-success" onclick="call()">呼叫</button>
<button type="button" disabled class="btn btn-danger" onclick="hungup()">挂断</button>
</div>
</div>
</div> <div class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">呼入</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>您有新的来电.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary">取消</button>
<button type="button" class="btn btn-primary">接听</button>
</div>
</div>
</div>
</div>
<script src="./assets/helper.js"></script>
<script>
// 初始化ws
const myWs = initWs('pc2'); // 获取一些dom和定义变量
const modal = document.querySelector('.modal');
const pc1Info = document.querySelector('.pc1-info');
let [,pc1Ws, pc1Rtc] = pc1Info.querySelectorAll('td');
const pc2Info = document.querySelector('.pc2-info');
let [,pc2Ws, pc2Rtc] = pc2Info.querySelectorAll('td');
const remoteVideo = document.querySelector('.remote-video');
const localVideo = document.querySelector('.local-video');
let pc2 = new RTCPeerConnection(); // 本地媒体展示和peer加入流监听
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((localStream)=>{
localVideo.srcObject = localStream;
localStream.getTracks().forEach(track => {
pc2.addTrack(track, localStream);
});
}); // 核心:ice交换(ice即收集可用链路)
pc2.onicecandidate = (e) => {
if (e.candidate) {
myWs.sendIce('pc2', e.candidate)
}
} pc2.ontrack = async (event) => {
remoteVideo.srcObject = event.streams[0];
// remoteVideo.play()
}; let offerSdp = null; // ws的onmessage事件
myWs.onmessage = async ({event,data}) => {
if(event === "onlineChange"){
document.querySelectorAll('.websockt').forEach((item)=>{
item.innerHTML = '断开';
})
data.forEach((item)=>{
eval(`${item}Ws`).innerHTML = '已连接';
})
}
if (event === "offer") {
pc1Ws.innerHTML = '收到对方回应的offer类型的sdp';
offerSdp = data; modal.style.display="block";
} else if (event === "ice" && data.id === 'pc1') {
pc1Ws.innerHTML = '收到对方回应ice';
pc2.addIceCandidate(data.ice)
} else if(event === "hello"){
eval(`${data.id}Ws`).innerHTML = '已连接'
}
} // 有来电弹窗,点击接听的时候按钮开始交换sdp
document.querySelector('.btn-primary').onclick = async ()=>{
await pc2.setRemoteDescription(offerSdp);
const answer = await pc2.createAnswer();
pc2.setLocalDescription(answer);
myWs.sendAnswer(answer);
modal.style.display="none"; } document.querySelector('.btn-secondary').onclick = ()=>{
modal.style.display="none";
}
</script>
</body> </html>
helper.js

// 判断是不是json字符串
const isJsonStr = (str) => {
if (typeof str == 'string') {
try {
var obj = JSON.parse(str);
if (typeof obj == 'object' && obj) {
return true;
} else {
return false;
}
} catch (e) {
console.log('error:' + str + '!!!' + e);
return false;
}
}
}; // 判断是不是json
const isJson = (data) => {
const typeofRes = typeof (data) == "object";
const toStringRes = Object.prototype.toString.call(data).toLowerCase() == "[object object]";
const isLen = !data?.length;
return typeofRes && toStringRes && isLen;
} const initWs = (id) => {
const ws = new WebSocket(`wss://dshvv.com:8888/my_ws/${id}`); // 重写ws,便于传参和接参数--主要是json序列化和反序列化
const myWs = new Proxy(ws, {
get(obj, prop) {
const value = obj[prop];
if (!typeof value === "function") { return obj[prop]; }
//如果不这么做会出现this指向问题:https://juejin.cn/post/6844903730987401230
return (...args) => {
//处理ws上传消息的json格式转换成字符串
if (isJson(args[0]) && prop === 'send') {
args[0] = JSON.stringify(args[0]);
}
return value.apply(obj, args)
}
},
set(obj, prop, value) {
if (prop !== 'onmessage') {
obj[prop] = value
} else {
obj[prop] = function (e) {
const res = null;
if (isJsonStr(e.data)) {
value({
...e,
...JSON.parse(e.data)
})
} else {
value(e)
}
}
}
return true;
}
}); myWs.sendSdp = function (event, data) {
myWs.send({ event, data })
}
myWs.sendOffer = function (sdp) {
myWs.sendSdp('offer', sdp)
}
myWs.sendAnswer = function (sdp) {
myWs.sendSdp('answer', sdp)
}
myWs.sendIce = function (id, ice) {
myWs.send({
event: 'ice',
data:{
ice,
id
}
})
} return myWs;
}
style.css

.index{
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.index>.card{
width:90% ;
max-width:600px ;
}
html,body{
height:100%;
width: 100%;
padding: 0;
margin: 0;
width: 100%;
}
.a-wrapp {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.flex-center-wrapp{
width: 90%;
max-width: 400px;
}
.btns{
margin-top: 10px;
text-align: center;
}
.videos{
border: 1px solid gainsboro;
padding: 10px;
border-radius: 10px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
}
.video{
width: 160px;
height: 140px;
background-color: gainsboro;
}
.local-video{
width: 80px;
height: 70px;
}
后端代码
主要是ws服务

package com.dshvv.myblogserver.websocket; import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet; /**
* 前后端交互的类实现消息的接收推送(自己发送给自己)
* 参考:https://www.cnblogs.com/xuwenjin/p/12664650.html
* @ServerEndpoint(value = "/my_ws") 前端通过此URI和后端交互,建立连接
*/
@Slf4j
@ServerEndpoint(value = "/my_ws/{id}")
@Component
public class MyWebSocket { // 当前组测的用户id
private static HashSet onlineIds = new HashSet<>(); //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session; /**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(@PathParam("id") String id, Session session){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
this.onlineChange(id, "onOpen");
System.out.println(session.getId()+"有新连接加入!当前在线人数为" + getOnlineCount());
} /**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("id") String id){
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
this.onlineChange(id, "onClose");
System.out.println(session.getId()+"有一连接关闭!当前在线人数为" + getOnlineCount());
} /**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(session.getId()+"来自客户端的消息:" + message);
//群发消息
for(MyWebSocket item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
} /**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
} /**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
} public static synchronized int getOnlineCount() {
return onlineCount;
} public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
} public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount--;
} public void onlineChange(String id, String type) {
System.out.println("898989898989");
if(type.equals("onOpen")){
onlineIds.add(id);
}else {
onlineIds.remove(id);
}
Map<String, Object> initMsg = new HashMap<>();
initMsg.put("event","onlineChange");
initMsg.put("data",onlineIds);
//群发消息
for(MyWebSocket item: webSocketSet){
try {
item.sendMessage(JSONObject.toJSONString(initMsg));
} catch (IOException e) {
e.printStackTrace();
continue;
}
} }
}
注意里边用到了两个mvn包
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- map转json的包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
后话
接下来我将用框架编写webrtc-demo,比如vue或react。原生的操作dom有点麻烦,相同代码不能抽离成公共组件复用
一、webrtc版本接听视频电话-纯js版的更多相关文章
- 黑客帝国纯js版
明天就回家过年了,今天没什么心思上班,看了下博客,发现一个黑客帝国额js版本,地址:https://blog.csdn.net/zhongyi_yang/article/details/5384180 ...
- jQuery下实现等待指定元素加载完毕(可改成纯js版)
http://www.poluoluo.com/jzxy/201307/233374.html 代码如下: jQuery.fn.wait = function (func, times, interv ...
- 纯JS文本在线HTML编辑器KindEditor
KindEditor(http://www.kindsoft.net)是一款比较专业,主流,好用的在线HTML编辑器. 它除了可以将文本进行编辑.将Word中的内容复制进来外,本身还可以拖动缩放(右下 ...
- Ajax,纯Js+Jquery
AJAX:Asynchronous Javascript and xml 异步,Js和Xml 交互式网页开发 不刷新页面,与服务器交互 详情请参照Jquery工具指南用在浏览器端的技术,无刷新,通过X ...
- 纯css和js版下拉菜单
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...
- 【干货】JS版汉字与拼音互转终极方案,附简单的JS拼音输入法
前言 网上关于JS实现汉字和拼音互转的文章很多,但是比较杂乱,都是互相抄来抄去,而且有的不支持多音字,有的不支持声调,有的字典文件太大,还比如有时候我仅仅是需要获取汉字拼音首字母却要引入200kb的字 ...
- 解决jQuery多个版本,与其他js库冲突方法
jQuery多个版本或和其他js库冲突主要是常用的$符号的问题,这个问题 jquery早早就有给我们预留处理方法了,下面一起来看看解决办法. 1.同一页面jQuery多个版本或冲突解决方法. < ...
- javascript日历控件——纯javascript版
平时只有下班时间能code,闲来写了个纯javascript版.引用该calendar.js文件,然后给要设置成日历控件的input的id设置成calendar,该input就会变成日历控件. < ...
- JS版百度地图API
地图的构建非常简单,官方的API文档也写得很清晰,我只做一总结: 一起jquery,17jquery 一.引入JS :这个很容易理解,既然是调用JS版的百度地图,肯定得引用外部的JS文件了,而这个文件 ...
- 纯JS单页面赛车游戏代码分享
分享一个以前写的小游戏,纯js游戏,代码很简单.欢迎大家来喷呦! 效果图: 代码展示://直接复制到html文件即可 支持IE9+版本 <!DOCTYPE html> <html&g ...
随机推荐
- 从零开始构建智能聊天机器人:Rasa与ChatGPT API实战教程
引言:AI对话系统的时代机遇 在数字化转型浪潮中,聊天机器人已成为连接用户与服务的关键纽带.无论是客服系统中的7×24小时即时响应,还是智能家居中的语音交互,聊天机器人正在重塑人机交互方式.本文将通过 ...
- 使用SymPy求解矩阵微分方程
引言 在数学.物理.工程等领域,微分方程常常被用来描述系统的变化和动态过程.对于多变量系统或者多方程系统,矩阵微分方程是非常常见的,它可以用来描述如电路.控制系统.振动系统等复杂的动态行为.今天,我们 ...
- 探秘Transformer系列之(27)--- MQA & GQA
探秘Transformer系列之(27)--- MQA & GQA 目录 探秘Transformer系列之(27)--- MQA & GQA 0x00 概述 0x01 MHA 1.1 ...
- eolinker校验规则之 Json Path定位:返回值每一项数组内值校验
如下图,获取H5首页菜单,验证菜单名是否正确 找到对应的接口,查看返回数据,菜单名字存放在TabBar下的3个数组内 Eolinker传统的JSON参数定位(json结构定位)只能校验第一个数组内的p ...
- 康谋分享 | 在基于场景的AD/ADAS验证过程中,识别挑战性场景!
基于场景的验证是AD/ADAS(自动驾驶和高级驾驶辅助)系统开发过程中的重要步骤,它包括对自动化系统进行一系列预定义场景的测试.测试中包含的场景越多,尤其挑战性场景越多,人们对正在测试的AD/ADAS ...
- 1、HTML常用标签
此文章为学习笔记以下内容为HTML常用标签. 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset=&quo ...
- kali安装docker环境
Docker需要Linux内核大于 3.10 并且是 64位 的,可以用 uname -a 可以查看是否符合要求 uname -a Linux kali 4.17.0-kali1-amd64 #1 S ...
- 移动web开发——flex布局
目录 1.0传统布局和flex布局对比 1.1传统布局 1.2 flex布局 1.3 建议 2.0 flex布局原理 3.0 父项常见属性 3.1 flex-direction设置主轴的方向 3.2 ...
- Ant Design Pro版中后台原型模板及Axure rplib元件库组件
Ant Design Pro版中后台原型模板及Axure rplib元件库组件, Ant Design服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更 ...
- 模板导入_分页_cookie_装饰器_笔记
默认值:url(r'index/', views.index,{'name':"root"})def index(request,name): print(name) ...