当前位置: 首页 > news >正文

从小白到入门webrtc音视频通话

0. 写在前面

先会骑车,再研究为什么这么骑,才是我认为学习技术的思路,底部付了demo例子,根据例子上面的介绍即可运行。

1. 音视频通话要用到的技术简介

  1. websocket
    • 介绍:1. 服务器可以向浏览器推送信息;2. 一次握手成功,可持续互相发送信息
    • 在音视频通话钟的作用:1. 作为音视频两个通话终端的桥梁,传递彼此上下线、网络环境等消息,因此他们都叫websocket为“信令服务器”
  2. coturn
    • 介绍:1. 包含stun服务和turn服务,stun可实现两个终端点对点语音通话;turn服务在无法点对点通话时,用作中转音视频流。
  3. webrtc
    • 介绍:1. 开源项目;2. 用于音视频实时互动、游戏、即时通讯、文件传输。

2. webrtc音视频通话开发思路

2.1. webrtc调用时序图

  1. 下图简化了B客户端创建PeerConnection,具体流程要看下面“调用时序图介绍”
    webrtc调用时序图

2.2. 调用时序图介绍

  1. 上图名词介绍
    1. client A:客户端A
    2. Stun Server:穿透服务器,也就是coturn服务器中的Stun
    3. Signal Server:信令服务器,也就是web socket搭建的服务器
    4. client B:客户端B
    5. PeerConnection(WebRtc的接口)
  2. 流程介绍
    1. A客户端先发送信息到信令服务器,信令服务器存储A客户端信息,等待其他客户端加入。
    2. B客户端再发送信息到信令服务器,信令服务器存储B客户端信息,并告知A已有新客户端加入。
    3. A客户端创建 PeerConnection(WebRtc的接口),用于获取本地的网络信息、以及保存对方的网络信息、传递音视频流、监听通话过程状态。(B客户端后面也需要创建PeerConnection)
    4. AddStreams:A客户端添加本地音视频流到PeerConnection
    5. CreateOffer:A客户端创建Offer并发送给信令服务器,由信令服务器转给B客户端。Offer中包含本地的网络信息(SDP)。
    6. CreateAnswer:B客户端收到Offer后,创建放到自己的PeerConnection中,并获取自己的网络信息(SDP),通过发送给信令服务器,由信令服务器转发给A客户端。
    7. 上面步骤进行完毕后,开始通过信令服务器(coturn),双方客户端获取自己的地址作为候选人“candidate”,然后通过websocket发送给对方。彼此拿到候选地址后,互相进行访问测试,建立链接。
    8. OnAddStream:获取对方音视频流,PeerConnection有ontrack监听器能拿到对方视频流数据。

2. 搭建WebSocket服务器

看例子中代码,使用nodejs启动

3. 搭建Coturn音视频穿透服务器

公司内网虚拟机中穿透服务器Coturn的搭建

4. 遇到的问题

后面再慢慢补吧,问题有点多

5. 例子

  1. 客户端代码使用html+js编写
  2. WebSocket代码使用js编写使用nodejs运行
  3. android端代码请下载:WebRtcAndroidDemo

5.1 客户端代码

  1. 引入adapter-latest.js文件,此文件如果过期了就自己百度找找吧。
  2. 将ws://192.168.1.60:9001/ws改为自己websocket服务所在电脑的ip地址,在本地启动则是本机地址
  3. 将iceServers中的ip改为coturn服务器所在ip地址
<html><head><title>Voice WebRTC demo</title></head><h1>WebRTC demo 1v1</h1><div id="buttons"><input id="zero-roomId" type="text" placeholder="请输入房间ID" maxlength="40"/><button id="joinBtn" type="button">加入</button><button id="leaveBtn" type="button">离开</button>    </div><div id="videos"><video id="localVideo" autoplay muted playsinline>本地窗口</video><video id="remoteVideo" autoplay playsinline>远端窗口</video></div><script src="js/main.js"></script><!-- 可直接引入在线js:https://webrtc.github.io/adapter/adapter-latest.js  --><script src="js/adapter-latest.js"></script>
</html>
'use strict';// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";var localUserId = Math.random().toString(36).substr(2); // 本地uid
var remoteUserId = -1;      // 对端
var roomId = 0;var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var localStream = null;
var remoteStream = null;
var pc = null;var zeroRTCEngine;function handleIceCandidate(event) {console.info("handleIceCandidate");if (event.candidate) {var candidateJson = {'label': event.candidate.sdpMLineIndex,'id': event.candidate.sdpMid,'candidate': event.candidate.candidate};var jsonMsg = {'cmd': SIGNAL_TYPE_CANDIDATE,'roomId': roomId,'uid': localUserId,'remoteUid':remoteUserId,'msg': JSON.stringify(candidateJson) };var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);console.info("handleIceCandidate message: " + message);console.info("send candidate message");} else {console.warn("End of candidates");}
}function handleRemoteStreamAdd(event) {console.info("handleRemoteStreamAdd");remoteStream = event.streams[0];// 视频轨道// let videoTracks = remoteStream.getVideoTracks()// 音频轨道// let audioTracks = remoteStream.getAudioTracks()remoteVideo.srcObject = remoteStream;
}function handleConnectionStateChange() {if(pc != null) {console.info("ConnectionState -> " + pc.connectionState);}
}function handleIceConnectionStateChange() {if(pc != null) {console.info("IceConnectionState -> " + pc.iceConnectionState);}
}function createPeerConnection() {var defaultConfiguration = {  bundlePolicy: "max-bundle",rtcpMuxPolicy: "require",iceTransportPolicy:"all",//relay 或者 all// 修改ice数组测试效果,需要进行封装iceServers: [{"urls": ["turn:192.168.1.173:3478?transport=udp","turn:192.168.1.173:3478?transport=tcp"       // 可以插入多个进行备选],"username": "lqf","credential": "123456"},{"urls": ["stun:192.168.1.173:3478"]}]};pc = new RTCPeerConnection(defaultConfiguration); // 音视频通话的核心类pc.onicecandidate = handleIceCandidate;pc.ontrack = handleRemoteStreamAdd;pc.onconnectionstatechange = handleConnectionStateChange;pc.oniceconnectionstatechange = handleIceConnectionStateChangelocalStream.getTracks().forEach((track) => pc.addTrack(track, localStream)); // 把本地流设置给RTCPeerConnection
}function createOfferAndSendMessage(session) {pc.setLocalDescription(session).then(function () {var jsonMsg = {'cmd': 'offer','roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(session)};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);// console.info("send offer message: " + message);console.info("send offer message");}).catch(function (error) {console.error("offer setLocalDescription failed: " + error);});}function handleCreateOfferError(error) {console.error("handleCreateOfferError: " + error);
}function createAnswerAndSendMessage(session) {pc.setLocalDescription(session).then(function () {var jsonMsg = {'cmd': 'answer','roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(session)};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);// console.info("send answer message: " + message);console.info("send answer message");}).catch(function (error) {console.error("answer setLocalDescription failed: " + error);});}function handleCreateAnswerError(error) {console.error("handleCreateAnswerError: " + error);
}var ZeroRTCEngine = function (wsUrl) {this.init(wsUrl);zeroRTCEngine = this;return this;
}ZeroRTCEngine.prototype.init = function (wsUrl) {// 设置websocket  urlthis.wsUrl = wsUrl;/** websocket对象 */this.signaling = null;
}ZeroRTCEngine.prototype.createWebsocket = function () {zeroRTCEngine = this;zeroRTCEngine.signaling = new WebSocket(this.wsUrl);zeroRTCEngine.signaling.onopen = function () {zeroRTCEngine.onOpen();}zeroRTCEngine.signaling.onmessage = function (ev) {zeroRTCEngine.onMessage(ev);}zeroRTCEngine.signaling.onerror = function (ev) {zeroRTCEngine.onError(ev);}zeroRTCEngine.signaling.onclose = function (ev) {zeroRTCEngine.onClose(ev);}
}ZeroRTCEngine.prototype.onOpen = function () {console.log("websocket打开");
}
ZeroRTCEngine.prototype.onMessage = function (event) {console.log("websocket收到信息: " + event.data);var jsonMsg = null;try {jsonMsg = JSON.parse(event.data);} catch(e) {console.warn("onMessage parse Json failed:" + e);return;}switch (jsonMsg.cmd) {case SIGNAL_TYPE_NEW_PEER:handleRemoteNewPeer(jsonMsg);break;case SIGNAL_TYPE_RESP_JOIN:handleResponseJoin(jsonMsg);break;case SIGNAL_TYPE_PEER_LEAVE:handleRemotePeerLeave(jsonMsg);break;case SIGNAL_TYPE_OFFER:handleRemoteOffer(jsonMsg);break;case SIGNAL_TYPE_ANSWER:handleRemoteAnswer(jsonMsg);break;case SIGNAL_TYPE_CANDIDATE:handleRemoteCandidate(jsonMsg);break;}
}ZeroRTCEngine.prototype.onError = function (event) {console.log("onError: " + event.data);
}ZeroRTCEngine.prototype.onClose = function (event) {console.log("onClose -> code: " + event.code + ", reason:" + EventTarget.reason);
}ZeroRTCEngine.prototype.sendMessage = function (message) {this.signaling.send(message);
}function handleResponseJoin(message) {console.info("handleResponseJoin, remoteUid: " + message.remoteUid);remoteUserId = message.remoteUid;// doOffer();
}function handleRemotePeerLeave(message) {console.info("handleRemotePeerLeave, remoteUid: " + message.remoteUid);remoteVideo.srcObject = null;if(pc != null) {pc.close();pc = null;}
}function handleRemoteNewPeer(message) {console.info("处理远端新加入链接,并发送offer, remoteUid: " + message.remoteUid);remoteUserId = message.remoteUid;doOffer();
}function handleRemoteOffer(message) {console.info("handleRemoteOffer");if(pc == null) {createPeerConnection();}var desc = JSON.parse(message.msg);pc.setRemoteDescription(desc);doAnswer();
}function handleRemoteAnswer(message) {console.info("handleRemoteAnswer");var desc = JSON.parse(message.msg);pc.setRemoteDescription(desc);
}function handleRemoteCandidate(message) {console.info("handleRemoteCandidate");var jsonMsg = message.msg;if(typeof message.msg === "string"){jsonMsg = JSON.parse(message.msg);}var candidateMsg = {'sdpMLineIndex': jsonMsg.label,'sdpMid': jsonMsg.id,'candidate': jsonMsg.candidate};var candidate = new RTCIceCandidate(candidateMsg);pc.addIceCandidate(candidate).catch(e => {console.error("addIceCandidate failed:" + e.name);});
}function doOffer() {// 创建RTCPeerConnectionif (pc == null) {createPeerConnection();}// let options = {offerToReceiveVideo:true}// pc.createOffer(options).then(createOfferAndSendMessage).catch(handleCreateOfferError);pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);
}function doAnswer() {pc.createAnswer().then(createAnswerAndSendMessage).catch(handleCreateAnswerError);
}function doJoin(roomId) {var jsonMsg = {'cmd': 'join','roomId': roomId,'uid': localUserId,};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);console.info("doJoin message: " + message);
}function doLeave() {var jsonMsg = {'cmd': 'leave','roomId': roomId,'uid': localUserId,};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);console.info("doLeave message: " + message);hangup();
}function hangup() {localVideo.srcObject = null; // 0.关闭自己的本地显示remoteVideo.srcObject = null; // 1.不显示对方closeLocalStream(); // 2. 关闭本地流if(pc != null) {pc.close(); // 3.关闭RTCPeerConnectionpc = null;}
}function closeLocalStream() {if(localStream != null) {localStream.getTracks().forEach((track) => {track.stop();});}
}function openLocalStream(stream) {console.log('Open local stream');doJoin(roomId);localVideo.srcObject = stream;      // 显示画面localStream = stream;   // 保存本地流的句柄
}function initLocalStream() {navigator.mediaDevices.getUserMedia({audio: true,video: true}).then(openLocalStream).catch(function (e) {alert("getUserMedia() error: " + e.name);});
}
// zeroRTCEngine = new ZeroRTCEngine("wss://192.168.1.60:80/ws");
zeroRTCEngine = new ZeroRTCEngine("ws://192.168.1.60:9001/ws");
zeroRTCEngine.createWebsocket();document.getElementById('joinBtn').onclick = function () {roomId = document.getElementById('zero-roomId').value;if (roomId == "" || roomId == "请输入房间ID") {alert("请输入房间ID");return;}console.log("第一步:加入按钮被点击, roomId: " + roomId);// 初始化本地码流initLocalStream();
}document.getElementById('leaveBtn').onclick = function () {console.log("离开按钮被点击");doLeave();
}

5.2. 编写websocket服务

  1. 使用nodejs启动
var ws = require("nodejs-websocket")
var prort = 9001;// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";/** ----- ZeroRTCMap ----- */
var ZeroRTCMap = function () {this._entrys = new Array();this.put = function (key, value) {if (key == null || key == undefined) {return;}var index = this._getIndex(key);if (index == -1) {var entry = new Object();entry.key = key;entry.value = value;this._entrys[this._entrys.length] = entry;} else {this._entrys[index].value = value;}};this.get = function (key) {var index = this._getIndex(key);return (index != -1) ? this._entrys[index].value : null;};this.remove = function (key) {var index = this._getIndex(key);if (index != -1) {this._entrys.splice(index, 1);}};this.clear = function () {this._entrys.length = 0;};this.contains = function (key) {var index = this._getIndex(key);return (index != -1) ? true : false;};this.size = function () {return this._entrys.length;};this.getEntrys = function () {return this._entrys;};this._getIndex = function (key) {if (key == null || key == undefined) {return -1;}var _length = this._entrys.length;for (var i = 0; i < _length; i++) {var entry = this._entrys[i];if (entry == null || entry == undefined) {continue;}if (entry.key === key) {// equalreturn i;}}return -1;};
}var roomTableMap = new ZeroRTCMap();function Client(uid, conn, roomId) {this.uid = uid;     // 用户所属的idthis.conn = conn;   // uid对应的websocket连接this.roomId = roomId;
}function handleJoin(message, conn) {var roomId = message.roomId;var uid = message.uid;console.info("uid: " + uid + "try to join room " + roomId);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {roomMap = new  ZeroRTCMap();        // 如果房间没有创建,则新创建一个房间roomTableMap.put(roomId, roomMap);}if(roomMap.size() >= 2) {console.error("roomId:" + roomId + " 已经有两人存在,请使用其他房间");// 加信令通知客户端,房间已满return null;}var client = new Client(uid, conn, roomId);roomMap.put(uid, client);if(roomMap.size() > 1) {// 房间里面已经有人了,加上新进来的人,那就是>=2了,所以要通知对方var clients = roomMap.getEntrys();for(var i in clients) {var remoteUid = clients[i].key;if (remoteUid != uid) {var jsonMsg = {'cmd': SIGNAL_TYPE_NEW_PEER,'remoteUid': uid};var msg = JSON.stringify(jsonMsg);var remoteClient =roomMap.get(remoteUid);console.info("new-peer: " + msg);remoteClient.conn.sendText(msg);jsonMsg = {'cmd':SIGNAL_TYPE_RESP_JOIN,'remoteUid': remoteUid};msg = JSON.stringify(jsonMsg);console.info("resp-join: " + msg);conn.sendText(msg);}}}return client;
}function handleLeave(message) {var roomId = message.roomId;var uid = message.uid;var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleLeave can't find then roomId " + roomId);return;}if (!roomMap.contains(uid)) {console.info("uid: " + uid +" have leave roomId " + roomId);return;}console.info("uid: " + uid + " leave room " + roomId);roomMap.remove(uid);        // 删除发送者if(roomMap.size() >= 1) {var clients = roomMap.getEntrys();for(var i in clients) {var jsonMsg = {'cmd': 'peer-leave','remoteUid': uid // 谁离开就填写谁};var msg = JSON.stringify(jsonMsg);var remoteUid = clients[i].key;var remoteClient = roomMap.get(remoteUid);if(remoteClient) {console.info("notify peer:" + remoteClient.uid + ", uid:" + uid + " leave");remoteClient.conn.sendText(msg);}}}
}function handleForceLeave(client) {var roomId = client.roomId;var uid = client.uid;// 1. 先查找房间号var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.warn("handleForceLeave can't find then roomId " + roomId);return;}// 2. 判别uid是否在房间if (!roomMap.contains(uid)) {console.info("uid: " + uid +" have leave roomId " + roomId);return;}// 3.走到这一步,说明客户端没有正常离开,所以我们要执行离开程序console.info("uid: " + uid + " force leave room " + roomId);roomMap.remove(uid);        // 删除发送者if(roomMap.size() >= 1) {var clients = roomMap.getEntrys();for(var i in clients) {var jsonMsg = {'cmd': 'peer-leave','remoteUid': uid // 谁离开就填写谁};var msg = JSON.stringify(jsonMsg);var remoteUid = clients[i].key;var remoteClient = roomMap.get(remoteUid);if(remoteClient) {console.info("notify peer:" + remoteClient.uid + ", uid:" + uid + " leave");remoteClient.conn.sendText(msg);}}}
}function handleOffer(message) {var roomId = message.roomId;var uid = message.uid;var remoteUid = message.remoteUid;console.info("handleOffer uid: " + uid + "transfer  offer  to remoteUid" + remoteUid);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleOffer can't find then roomId " + roomId);return;}if(roomMap.get(uid) == null) {console.error("handleOffer can't find then uid " + uid);return;}var remoteClient = roomMap.get(remoteUid);if(remoteClient) {var msg = JSON.stringify(message);remoteClient.conn.sendText(msg);    //把数据发送给对方} else {console.error("can't find remoteUid: " + remoteUid);}
}function handleAnswer(message) {var roomId = message.roomId;var uid = message.uid;var remoteUid = message.remoteUid;console.info("handleAnswer uid: " + uid + "transfer answer  to remoteUid" + remoteUid);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleAnswer can't find then roomId " + roomId);return;}if(roomMap.get(uid) == null) {console.error("handleAnswer can't find then uid " + uid);return;}var remoteClient = roomMap.get(remoteUid);if(remoteClient) {var msg = JSON.stringify(message);remoteClient.conn.sendText(msg);} else {console.error("can't find remoteUid: " + remoteUid);}
}function handleCandidate(message) {var roomId = message.roomId;var uid = message.uid;var remoteUid = message.remoteUid;console.info("处理Candidate uid: " + uid + "transfer candidate  to remoteUid" + remoteUid);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleCandidate can't find then roomId " + roomId);return;}if(roomMap.get(uid) == null) {console.error("handleCandidate can't find then uid " + uid);return;}var remoteClient = roomMap.get(remoteUid);if(remoteClient) {var msg = JSON.stringify(message);remoteClient.conn.sendText(msg);} else {console.error("can't find remoteUid: " + remoteUid);}
}
// 创建监听9001端口webSocket服务
var server = ws.createServer(function(conn){console.log("创建一个新的连接--------")conn.client = null; // 对应的客户端信息// conn.sendText("我收到你的连接了....");conn.on("text", function(str) {// console.info("recv msg:" + str);var jsonMsg = JSON.parse(str);switch (jsonMsg.cmd) {case SIGNAL_TYPE_JOIN:conn.client = handleJoin(jsonMsg, conn);break;case SIGNAL_TYPE_LEAVE:handleLeave(jsonMsg);break;case SIGNAL_TYPE_OFFER:handleOffer(jsonMsg);break;   case SIGNAL_TYPE_ANSWER:handleAnswer(jsonMsg);break; case SIGNAL_TYPE_CANDIDATE:handleCandidate(jsonMsg);break;      }});conn.on("close", function(code, reason) {console.info("连接关闭 code: " + code + ", reason: " + reason);if(conn.client != null) {// 强制让客户端从房间退出handleForceLeave(conn.client);}});conn.on("error", function(err) {console.info("监听到错误:" + err);});
}).listen(prort);

6. 参考文档

  1. WebRtc接口参考
  2. WebRTC 传输协议详解
  3. WebRTC的学习(java版本信令服务)
  4. Android webrtc实战(一)录制本地视频并播放,附带详细的基础知识讲解
  5. webSocket(wss)出现连接失败的问题解决方法
  6. 最重要的是这个,完整听了课程:2023最新Webrtc基础教程合集,涵盖所有核心内容(Nodejs+vscode+coturn

相关文章:

从小白到入门webrtc音视频通话

0. 写在前面 先会骑车&#xff0c;再研究为什么这么骑&#xff0c;才是我认为学习技术的思路&#xff0c;底部付了demo例子&#xff0c;根据例子上面的介绍即可运行。 1. 音视频通话要用到的技术简介 websocket 介绍&#xff1a;1. 服务器可以向浏览器推送信息&#xff1b;2…...

Qt之漂亮的地球

这个画的是一个东西围绕着中心的地球不停的旋转&#xff0c;可以放在界面的中部&#xff0c;增加美感。 展示 界面展示 设计过程 标题在之前的博客有写过&#xff0c;这里不再重复 下面是关于地球旋转的相关 1.资源文件添加 先将相关的资源文件添加&#xff0c;三个图片 2…...

FPGA解码MIPI视频:Xilinx Artix7-35T低端FPGA,基于MIPI CSI-2 RX Subsystem架构实现,提供工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐我这里已有的 MIPI 编解码方案本方案在Xilinx Artix7-100T上解码MIPI视频的应用本方案在Xilinx Kintex7上解码MIPI视频的应用本方案在Xilinx Zynq7000上解码MIPI视频的应用本方案在Xilinx Zynq UltraScale上解码MIPI视频的应用纯VHDL代码解…...

使用docker部署Kafka(MAC Apple M2 Pro)

前置准备 下载适用于Apple M2 Pro的Zookeeper和Kafka Docker镜像 docker pull zookeeper:3.6 docker pull cppla/kafka-docker:arm 下载成功后确认镜像无误 docker images 部署Zookeeper 执行部署命令后查看容器是否启动 docker run -d --name zookeeper -p 2181:2181 -…...

车位检测,YOLOV8,OPENCV调用

车位检测YOLOV8NANO,opencv调用 车位检测&#xff0c;YOLOV8NANO&#xff0c;训练得到PT模型&#xff0c;然后转换成ONNX&#xff0c;OPENCV的DNN调用&#xff0c;支持C,PYTHON,ANDROID...

FCIS 2023:洞悉网络安全新态势,引领创新防护未来

随着网络技术的飞速发展&#xff0c;网络安全问题日益凸显&#xff0c;成为全球共同关注的焦点。在这样的背景下&#xff0c;FCIS 2023网络安全创新大会应运而生&#xff0c;旨在汇聚业界精英&#xff0c;共同探讨网络安全领域的最新动态、创新技术和解决方案。 本文将从大会的…...

前端工程化之:webpack2-1(常用扩展)

目录 前言 一、CleanWebpackPlugin 二、HtmlWebpackPlugin 三、CopyPlugin 四、webpack-dev-server 五 、file-loader 六、url-loader 七、路径问题 前言 由于 webpack 、 webpack-cli 、 webpack-dev-server 会存在版本不兼容问题&#xff0c;所以这里使用的版本如下&…...

Python学习路线 - Python高阶技巧 - PySpark案例实战

Python学习路线 - Python高阶技巧 - PySpark案例实战 前言介绍Spark是什么Python On SparkPySparkWhy PySpark 基础准备PySpark库的安装构建PySpark执行环境入口对象PySpark的编程模型 数据输入RDD对象Python数据容器转RDD对象读取文件转RDD对象 数据计算map方法flatMap方法red…...

【TCP】高频面试题

前言 在IT行业的求职过程中&#xff0c;传输控制协议&#xff08;TCP&#xff09;作为网络通信的核心协议之一&#xff0c;其相关面试题常常出现在各大公司面试中。TCP的稳定性和可靠性是支撑互联网数据传输的基石&#xff0c;因此&#xff0c;对TCP有深入理解不仅能够帮助求职…...

Python||五城P.M.2.5数据分析与可视化_使用华夫图分析各个城市的情况(中)

目录 1.上海市的空气质量 2.成都市的空气质量 【沈阳市空气质量情况详见下期】 五城P.M.2.5数据分析与可视化——北京市、上海市、广州市、沈阳市、成都市&#xff0c;使用华夫图和柱状图分析各个城市的情况 1.上海市的空气质量 import numpy as np import pandas as pd impor…...

使用PDFBox实现pdf转其他图片格式

最近在做一个小项目&#xff0c;项目中有一个功能要把pdf格式的图片转换为其它格式&#xff0c;接下来看看用pdfbox来如何实现吧。 首先导入pdfbox相关依赖&#xff1a; <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</a…...

【技术预研】StarRocks官方文档浅析(4)

背景说明 基于starRocks官方文档&#xff0c;对其内容进行一定解析&#xff0c;方便大家理解和使用。 若无特殊标注&#xff0c;startRocks版本是3.2。 下面的章节和官方文档保持一致。 参考文档 产品简介 | StarRocks StarRocks StarRocks 是一款高性能分析型数据仓库&…...

时序数据库 Tdengine 执行命令能够查看执行的sql语句

curl是 访问6041端口&#xff0c;在windows系统里没有linux里的curl命令&#xff0c;需要用别的工具实现。我在cmd里是访问6030端口 第一步 在安装是时序数据库的服务器上也就是数据库服务端 进入命令窗口 执行 taos 第二步 执行 show queries\G;...

LeetCode、746. 使用最小花费爬楼梯【简单,动态规划 线性DP】

文章目录 前言LeetCode、746. 使用最小花费爬楼梯【简单&#xff0c;动态规划 线性DP】题目与分类思路 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。…...

[香橙派开发系列]使用蓝牙和手机进行信息的交换

文章目录 前言一、HC05蓝牙模块1.HC05概述2.HC05的连接图3.进入HC05的命令模式4.常用的AT指令4.1 检查AT是否上线4.2 重启模块4.3 获取软件版本号4.4 恢复默认状态4.5 获取蓝牙的名称4.6 设置蓝牙模块的波特率4.7 查询蓝牙的连接模式4.8 查询模块角色 5.连接电脑6.通过HC05发送…...

Jmeter 01 -概述线程组

1、Jmeter:概述 1.1 是什么&#xff1f; Jmeter是Apache公司使用Java 开发的一款测试工具 1.2 为什么&#xff1f; 高效、功能强大 模拟一些高并发或多次循环等特殊场景 1.3 怎么用&#xff1f; 下载安装 1、下载jmeter&#xff0c;解压缩2、安装Java环境&#xff08;jmet…...

大数据Zookeeper--案例

文章目录 服务器动态上下线监听案例需求需求分析具体实现测试 Zookeeper分布式锁案例原生Zookeeper实现分布式锁Curator框架实现分布式锁 Zookeeper面试重点选举机制生产集群安装多少zk合适zk常用命令 服务器动态上下线监听案例 需求 某分布式系统中&#xff0c;主节点可以有…...

VS编译器对scanf函数不安全报错的解决办法(详细步骤)

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有…...

vscode连接ssh报错

关于vscode更新版本至1.86后&#xff0c;导致无法连接服务器问题的记录 原因&#xff1a;vscode1.86更新了对glibc的要求&#xff0c;需要最低2.28版本&#xff0c;导致各种旧版本的linux发行版&#xff08;比如最常见的centos 7&#xff09;都无法用remote-ssh来连接了&#…...

C++ 哈希+unordered_map+unordered_set+位图+布隆过滤器(深度剖析)

文章目录 1. 前言2. unordered 系列关联式容器2.1 unordered_map2.1.1 unordered_map 的概念2.1.2 unordered_map 的使用 2.2 unordered_set2.2.1 unordered_set 的概念2.2.2 unordered_set 的使用 3. 底层结构3.1 哈希的概念3.2 哈希冲突3.3 哈希函数3.4 哈希冲突的解决3.4.1 …...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例

文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

Cinnamon修改面板小工具图标

Cinnamon开始菜单-CSDN博客 设置模块都是做好的&#xff0c;比GNOME简单得多&#xff01; 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...

NFT模式:数字资产确权与链游经济系统构建

NFT模式&#xff1a;数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新&#xff1a;构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议&#xff1a;基于LayerZero协议实现以太坊、Solana等公链资产互通&#xff0c;通过零知…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…...

Web 架构之 CDN 加速原理与落地实践

文章目录 一、思维导图二、正文内容&#xff08;一&#xff09;CDN 基础概念1. 定义2. 组成部分 &#xff08;二&#xff09;CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 &#xff08;三&#xff09;CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 &#xf…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...