一、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 ...
随机推荐
- CH9121替换注意事项
CH9121A 基于前版CH9121(无后缀字母)升级,引脚基本兼容,替换时需调整外围电路. 升级内容: 精简供电方式由3.3&1.8v双电源供电改为3.3v单电源供电: I/O 口支持3.3 ...
- QUBO建模
技术背景 QUBO(Quadratic Unconstrained Binary Optimization)模型是一种常用于求解组合优化问题的一种技术,它所能够求解的问题是这样定义的:给定一个布尔类型 ...
- Cocos Creator3.x小白常见问题笔记&官方视频教程合集收藏分享
小白常见问题 为什么会有这篇笔记? 这篇笔记旨在答疑解惑官方文档或视频教程里忽略掉的细节.对于小白来说这些细节没人提醒或浪费很多时间,但在熟悉的人眼里这都是些什么问题,回都懒得回. (别问我怎么知道的 ...
- cmd /k 解决cmd命令闪退问题
cmd /k 的含义是执行后面的命令,并且执行完毕后保留窗口. & 是连接多条命令.PAUSE 表示运行结束后暂停,等待一个任意按键.EXIT 表示关闭命令行窗口.如果使用 cmd /c 就可 ...
- Servlet创建的三种方式
目录 1 实现Servlet接口 2 继承GenericServlet 3 继承HttpServlet 4 web.xml配置 关于servlet的创建,我们有三种方式. 实现Servlet接口 继承 ...
- K8s Service 示例详解
Kubernetes 官方文档:Services-Networking Service介绍 在kubernetes中,pod是应用程序的载体,我们可以通过pod的ip来访问应用程序,但是pod的ip地 ...
- 关于正点原子input子系统,驱动中按键中断只检测了上升或下降沿却可以实现连按(EV_REP)的原因
问题 在学习到Linux内核input子系统时,产生了一个疑惑.可以看到,我们改造按键中断驱动程序(请见keyinputdriver.c(内核驱动代码)),通过检测按键的上升沿和下降沿,在中断处理函数 ...
- vue3 基础-表单元素双向绑定
通常是在 form 表单相关的场景中会用到双向绑定相关, 核心是 v-model 的应用. input 输入框 <!DOCTYPE html> <html lang="en ...
- MCP 实践系列:看热点、蹭热点,创作与摸鱼两手抓!
连续工作累死人,身心疲惫时,总得有那么一点时间给自己松口气.每当这个时候,我总喜欢偷偷摸摸地看看新闻,整理一下逐渐疯狂的思维.毕竟,谁说程序员就只能埋头写代码?谁规定了只能死磕在堆积如山的bug中? ...
- Google I/O 详细解读
AI创业失败,可私聊经验教训分享... 当前我唯一每个月付费的模型是ChatGPT,但昨天看了Google I/O 后,默默的打开了支付界面,国外做模型基建的大厂真的足够卷! PS:本来这篇文章是懒得 ...