我们在视讯通话中很常用的一个功能就是关闭镜头或是麦克风,今天我们将实作此功能
<div>
<button id="startBtn">开始</button>
<button id="leaveBtn">离开</button>
<button name="audio" id="audioBtn"></button>
<button name="video" id="VideoBtn"></button>
</div>
按钮标签
const audioBtn = document.querySelector('button#audioBtn')
const VideoBtn = document.querySelector('button#VideoBtn')
串流的开关状态
let streamOutput = { audio: true, video: true, }
设定按钮文字
function setBtnText() {
audioBtn.textContent = streamOutput.audio ? '关闭麦克风' : '开启麦克风'
VideoBtn.textContent = streamOutput.video ? '关闭视讯镜头' : '开启视讯镜头'
}
更新本地串流输出状态
function setSelfStream() {
localStream.getAudioTracks().forEach((item) => {
item.enabled = streamOutput.audio
})
localStream.getVideoTracks().forEach((item) => {
item.enabled = streamOutput.video
})
}
设定本地串流开关状态
function handleStreamOutput(e) {
const { name } = e.target
streamOutput = {
...streamOutput,
[name]: !streamOutput[name],
}
setBtnText()
setSelfStream()
}
监听按钮点击
audioBtn.onclick = handleStreamOutput
VideoBtn.onclick = handleStreamOutput
<!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>
<label>切换麦克风:</label>
<select id="audioSource"></select>
</div>
<div>
<label>切换摄影机:</label>
<select id="videoSource"></select>
</div>
<div>
<button id="startBtn">开始</button>
<button id="leaveBtn">离开</button>
<button name="audio" id="audioBtn">关闭麦克风</button>
<button name="video" id="VideoBtn">关闭镜头</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>
// 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')
const audioBtn = document.querySelector('button#audioBtn')
const VideoBtn = document.querySelector('button#VideoBtn')
// 切换设备
const audioInputSelect = document.querySelector('select#audioSource')
const videoSelect = document.querySelector('select#videoSource')
const selectors = [audioInputSelect, videoSelect]
let localStream
let peerConn
let socket
const room = 'room1'
// ===================== 连线相关 =====================
/**
* 连线 socket.io
*/
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 {
const audioSource = audioInputSelect.value
const videoSource = videoSelect.value
const constraints = {
audio: { deviceId: audioSource ? { exact: audioSource } : undefined },
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
}
// 取得影音的Stream
const stream = await navigator.mediaDevices.getUserMedia(constraints)
// 提升作用域
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
}
// ===================== 切换设备 =====================
/**
* 将读取到的设备加入到 select 标签中
* @param {*} deviceInfos
*/
function gotDevices(deviceInfos) {
const values = selectors.map((select) => select.value)
selectors.forEach((select) => {
while (select.firstChild) {
select.removeChild(select.firstChild)
}
})
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i]
const option = document.createElement('option')
option.value = deviceInfo.deviceId
if (deviceInfo.kind === 'audioinput') {
option.text =
deviceInfo.label || `microphone ${audioInputSelect.length + 1}`
audioInputSelect.appendChild(option)
} else if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`
videoSelect.appendChild(option)
} else {
console.log('Some other kind of source/device: ', deviceInfo)
}
}
selectors.forEach((select, selectorIndex) => {
if (
Array.prototype.slice
.call(select.childNodes)
.some((n) => n.value === values[selectorIndex])
) {
select.value = values[selectorIndex]
}
})
}
/**
* 读取设备
*/
navigator.mediaDevices
.enumerateDevices()
.then(gotDevices)
.catch((err) => {
console.error('Error happens:', err)
})
/**
* 切换设备
* @param {*} isAudio
* @returns
*/
async function switchDevice(isAudio) {
if (!peerConn) return
const audioSource = audioInputSelect.value
const videoSource = videoSelect.value
const constraints = {
audio: { deviceId: audioSource ? { exact: audioSource } : undefined },
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
}
const stream = await navigator.mediaDevices.getUserMedia(constraints)
let track = stream[isAudio ? 'getAudioTracks' : 'getVideoTracks']()[0]
let sender = peerConn.getSenders().find(function (s) {
return s.track.kind == track.kind
})
console.log('found sender:', sender)
sender.replaceTrack(track)
localStream = stream
localVideo.srcObject = stream
}
// ===================== 关闭镜头或麦克风 =====================
// 串流开关状态
let streamOutput = { audio: true, video: true, }
/**
* 设定按钮文字
*/
function setBtnText() {
audioBtn.textContent = streamOutput.audio ? '关闭麦克风' : '开启麦克风'
VideoBtn.textContent = streamOutput.video ? '关闭镜头' : '开启镜头'
}
/**
* 更新本地串流输出状态
*/
function setSelfStream() {
localStream.getAudioTracks().forEach((item) => {
item.enabled = streamOutput.audio
})
localStream.getVideoTracks().forEach((item) => {
item.enabled = streamOutput.video
})
}
/**
* 设定本地串流开关状态
* @param {Object} e
*/
function handleStreamOutput(e) {
const { name } = e.target
streamOutput = {
...streamOutput,
[name]: !streamOutput[name],
}
setBtnText()
setSelfStream()
}
// ===================== 监听事件 =====================
/**
* 监听按钮点击
*/
audioBtn.onclick = handleStreamOutput
VideoBtn.onclick = handleStreamOutput
startBtn.onclick = init
leaveBtn.onclick = () => {
if (socket) {
socket.emit('leave', room)
}
hangup()
startBtn.disabled = false
leaveBtn.disabled = true
}
/**
* 监听 select 改变状态
*/
audioInputSelect.onchange = () => {
switchDevice(true)
}
videoSelect.onchange = () => {
switchDevice(false)
}
<<: #25 No-code 之旅 — 实作 Notion 部落格 Pagination (分页) 功能 ft. SWR
Django的Template(样板) 在Django专案刚建立的时候, 我们可以从views.py...
或许你想问我 狗狗币有突出的技术吗? 它不是只是照抄程序码而诞生的加密货币吗? 事实并非如此, 我认...
intent可以使一个Activity切换至另一个Activity,而一个application可能...
Abstract 随者Spring各种嗨到爆的注解模式来势汹汹,满足所有开发者配置各类型的元件注解,...
Youtube 频道:https://www.youtube.com/c/kaochenlong ...