昨天我们把 Signaling server 完成了,今天我们要继续完成 Client 端:
上一篇为了测试我们在 index.html 中写入 hello,今天我们要把他替换掉,我们要在画面中呈现两个 video ,一个显示自己的画面,另一个显示通话对方的画面
<!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" />
<title>1on1 webrtc</title>
</head>
<body>
<div>
<video muted="false" width="320" autoplay playsinline id="localVideo" ></video>
<video width="320" autoplay playsinline id="remoteVideo"></video>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="./js/main.js"></script>
</body>
</html>
在 public 资料夹内建立一个 js 的资料夹,并在内部建立 main.js
// video 标签
const localVideo = document.querySelector('video#localVideo')
const remoteVideo = document.querySelector('video#remoteVideo')
let localStream
let peerConn
const room = 'room1'
// socket
const socket = io('ws://0.0.0.0:8080')
socket.on('ready', async (msg) => {
// TODO:- 收到 ready 代表对方已经连线,可以建立offer 发过去
})
socket.on('offer', async (desc) => {
// TODO:- 收到 offer 後,设定对方的配置,并建立 answer 发送到对端
})
socket.on('answer', async (desc) => {
// TODO:- 收到 answer 後,设定对方的配置
})
socket.on('ice_candidate', async (data) => {
// TODO:- 加入新取得的 ICE candidate
})
function init() {
// 加入房间
socket.emit('join', room)
}
window.onload = init()
建立一个用来处理信令的 function
/**
* 处理信令
* @param {Boolean} isOffer 是 offer 还是 answer
*/
async function sendSDP(isOffer) {
try {
if (!peerConn) {
return
}
// 建立 SDP
const localSDP = await peerConn[isOffer ? 'createOffer' : 'createAnswer']({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
})
// 设定本地 SDP 信令
await peerConn.setLocalDescription(localSDP)
// 寄出SDP信令
let e = isOffer ? 'offer' : 'answer'
socket.emit(e, room, peerConn.localDescription)
} catch (err) {
throw err
}
}
在ready中加入
socket.on('ready', async (msg) => {
// 发送 offer
await sendSDP(true)
})
socket.on('offer', async (desc) => {
// 设定对方的配置
await peerConn.setRemoteDescription(desc)
// 发送 answer
await sendSDP(false)
})
socket.on('answer', async (desc) => {
// 设定对方的配置
await peerConn.setRemoteDescription(desc)
})
socket.on('ice_candidate', async (data) => {
const candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate,
})
await peerConn.addIceCandidate(candidate)
})
/**
* 取得本地串流
*/
async function createStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
})
localStream = stream
localVideo.srcObject = stream
} catch (err) {
throw err
}
}
/**
* 初始化Peer连结
*/
function initPeerConnection() {
const configuration = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
],
}
peerConn = new RTCPeerConnection(configuration)
// 增加本地串流
localStream.getTracks().forEach((track) => {
peerConn.addTrack(track, localStream)
})
// 找寻到 ICE 候选位置後,送去 Server 与另一位配对
peerConn.onicecandidate = (e) => {
if (e.candidate) {
console.log('发送 ICE')
// 发送 ICE
socket.emit('ice_candidate', room, {
label: e.candidate.sdpMLineIndex,
id: e.candidate.sdpMid,
candidate: e.candidate.candidate,
})
}
}
// 监听 ICE 连接状态
peerConn.oniceconnectionstatechange = (e) => {
if (e.target.iceConnectionState === 'disconnected') {
remoteVideo.srcObject = null
}
}
// 监听是否有流传入,如果有的话就显示影像
peerConn.onaddstream = ({ stream }) => {
// 接收流并显示远端视讯
remoteVideo.srcObject = stream
}
}
async function init() {
await createStream()
initPeerConnection()
socket.emit('join', room)
}
public/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" />
<title>1on1 webrtc</title>
</head>
<body>
<div>
<video muted width="320" autoplay playsinline id="localVideo" ></video>
<video width="320" autoplay playsinline id="remoteVideo"></video>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="./js/main.js"></script>
</body>
</html>
pubilc/js/main.js
// video 标签
const localVideo = document.querySelector('video#localVideo')
const remoteVideo = document.querySelector('video#remoteVideo')
let localStream
let peerConn
const room = 'room1'
// socket
const socket = io('ws://0.0.0.0:8080')
socket.on('ready', async (msg) => {
console.log(msg)
// 发送 offer
console.log('发送 offer ')
await sendSDP(true)
})
socket.on('ice_candidate', async (data) => {
console.log('收到 ice_candidate')
const candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate,
})
await peerConn.addIceCandidate(candidate)
})
socket.on('offer', async (desc) => {
console.log('收到 offer')
// 设定对方的配置
await peerConn.setRemoteDescription(desc)
// 发送 answer
await sendSDP(false)
})
socket.on('answer', async (desc) => {
console.log('收到 answer')
// 设定对方的配置
await peerConn.setRemoteDescription(desc)
})
/**
* 取得本地串流
*/
async function createStream() {
try {
// 取得影音的Stream
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
})
// 提升作用域
localStream = stream
// 导入<video>
localVideo.srcObject = stream
} catch (err) {
throw err
}
}
/**
* 初始化Peer连结
*/
function initPeerConnection() {
const configuration = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
],
}
peerConn = new RTCPeerConnection(configuration)
// 增加本地串流
localStream.getTracks().forEach((track) => {
peerConn.addTrack(track, localStream)
})
// 找寻到 ICE 候选位置後,送去 Server 与另一位配对
peerConn.onicecandidate = (e) => {
if (e.candidate) {
console.log('发送 ICE')
// 发送 ICE
socket.emit('ice_candidate', room, {
label: e.candidate.sdpMLineIndex,
id: e.candidate.sdpMid,
candidate: e.candidate.candidate,
})
}
}
// 监听 ICE 连接状态
peerConn.oniceconnectionstatechange = (e) => {
if (e.target.iceConnectionState === 'disconnected') {
remoteVideo.srcObject = null
}
}
// 监听是否有流传入,如果有的话就显示影像
peerConn.onaddstream = ({ stream }) => {
// 接收流并显示远端视讯
remoteVideo.srcObject = stream
}
}
/**
* 处理信令
* @param {Boolean} isOffer 是 offer 还是 answer
*/
async function sendSDP(isOffer) {
try {
if (!peerConn) {
console.log('尚未开启视讯')
return
}
// 创建SDP信令
const localSDP = await peerConn[isOffer ? 'createOffer' : 'createAnswer']({
offerToReceiveAudio: true, // 是否传送声音流给对方
offerToReceiveVideo: true, // 是否传送影像流给对方
})
// 设定本地SDP信令
await peerConn.setLocalDescription(localSDP)
// 寄出SDP信令
let e = isOffer ? 'offer' : 'answer'
socket.emit(e, room, peerConn.localDescription)
} catch (err) {
throw err
}
}
/**
* 初始化
*/
async function init() {
await createStream()
initPeerConnection()
socket.emit('join', room)
}
window.onload = init()
确认结构
❯ tree -I 'node_modules'
.
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ ├── index.js
│ └── js
│ └── main.js
└── server.js
2 directories, 6 files
进入 1-on-1-webrtc
资料夹内部
node server.js
连线开启两个视窗 PC_A 及 PC_B
http://localhost:8080/
<<: Day 22. slate × Operation × transform
range(start=0, stop, step=1) 用来产生 整数等差数列 的函式,常和今天要...
包含基础设施、架构设计、资料采集(ETL)、主资料管理(MDM)、即时计算、资资料储存和作业排程等。...
如果有用过 HE 提供的 Tunnel Broker 服务的话,应该对 SIT 隧道不陌生。 但是,...
状况 最近的经验是要把公司的程序码翻新 但由於旧有的程序码技术债实在太过庞大,没办法像以前以往接手到...
从传统的接案甲乙方关系我们发现,因为利益的冲突,甲方也不可能得到乙方 100% 的专业协助,因为乙方...