结合前两篇我们已经实现了 MVP
(Minimum Viable Product;最小可行产品),完成了最简单的一对一通话,後续我们会慢慢强化他的功能,今天我们会加上通话及挂断机制,运作方法如下图。
收到 leave
消息後向对方发送bye 对自己发送 leaved
// 离开房间
socket.on('leave', (room) => {
socket.to(room).emit('bye')
socket.emit('leaved')
})
index.html 中增加挂电话按钮
<div>
<button id="leaveBtn">Leave</button>
</div>
main.js 中增加 挂断电话
function hangup() {
if (peerConn) {
peerConn.close()
peerConn = null
}
}
main.js 中增加 bye
及 leaved
两个订阅
// 收到 leaved 把 socket 中断连线
socket.on('leaved', (room, id) => {
console.log('收到 leaved')
// 中断 socket
socket.disconnect()
})
socket.on('bye', (room, id) => {
console.log('收到 bye')
// 对方要挂掉电话
hunghp()
})
main.js 中增加 按钮绑定
const leaveBtn = document.querySelector('button#leaveBtn')
// 点击後发送离开的讯息并挂断电话
leaveBtn.onclick = () => {
if (socket) {
socket.emit('leave', room)
}
hangup()
}
index.html 中增加启动通话按钮
<div>
<button id="startBtn">Start</button>
<button id="leaveBtn">Leave</button>
</div>
把 socket.io 相关程序码使用function 包起来
function connectIO() {
// socket
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)
})
socket.on('leaved', () => {
console.log('收到 leaved')
socket.disconnect()
})
socket.on('bye', () => {
console.log('收到 bye')
hangup()
})
socket.emit('join', room)
}
替换初始化方法
const startBtn = document.querySelector('button#startBtn')
...
// window.onload = init()
startBtn.onclick = init
加上按钮控制
async function init() {
await createStream()
initPeerConnection()
connectIO()
startBtn.disabled = true
leaveBtn.disabled = false
}
leaveBtn.onclick = () => {
if (socket) {
socket.emit('leave', room)
}
hangup()
startBtn.disabled = false
leaveBtn.disabled = true
}
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>
<button id="startBtn">Start</button>
<button id="leaveBtn">Leave</button>
</div>
<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>
main.js
// video 标签
const localVideo = document.querySelector('video#localVideo')
const remoteVideo = document.querySelector('video#remoteVideo')
// button 标签
const startBtn = document.querySelector('button#startBtn')
const leaveBtn = document.querySelector('button#leaveBtn')
let localStream
let peerConn
let socket
const room = 'room1'
function connectIO() {
// socket
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)
})
socket.on('leaved', () => {
console.log('收到 leaved')
socket.disconnect()
closeLocalMedia()
})
socket.on('bye', () => {
console.log('收到 bye')
hangup()
})
socket.emit('join', room)
}
/**
* 取得本地串流
*/
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) {
initPeerConnection()
}
// 创建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
}
}
/**
* 关闭自己的视讯
*/
function closeLocalMedia() {
if (localStream && localStream.getTracks()) {
localStream.getTracks().forEach((track) => {
track.stop()
})
}
localStream = null
}
/**
* 挂掉电话
*/
function hangup() {
if (peerConn) {
peerConn.close()
peerConn = null
}
}
/**
* 初始化
*/
async function init() {
await createStream()
initPeerConnection()
connectIO()
startBtn.disabled = true
leaveBtn.disabled = false
}
// window.onload = init()
leaveBtn.onclick = () => {
if (socket) {
socket.emit('leave', room)
}
hangup()
startBtn.disabled = false
leaveBtn.disabled = true
}
startBtn.onclick = init
<<: [Day 24] Reactive Programming - Spring WebFlux(Router)
>>: 【设计+切版30天实作】|Day24 - Steps区块 - 如何做出渐层背景?
通常我们在写module的时候,会需要一些初始资料或是固定需要的资料,我们可以定义资料在创立Mode...
昨天我们做了一个不能点的 neuomorphic-button 今天我们把他可以点击 & 加...
Celery 提供任务链结的功能,字面上的意思,就是将任务一个一个串联在一起,下面的叙述 or 范例...
前一篇文章提到了Azure DevOps上的Repos可以分成Git Repo和TFVC Repo,...
上一篇我们介绍了 interfaces/operations.ts 里定义的 Operation ...