WebRTC类Zoom应用开发:屏幕共享无法同步至房间成员求助
问题描述
我正在基于WebRTC开发类Zoom的网页应用,目前已在本地成功实现屏幕共享,但无法将共享画面同步至同一房间内的其他对等节点,希望获取实现全员屏幕共享的解决方案,相关JavaScript代码如下:
const socket = io('/') const videoGrid = document.getElementById('video-grid') const myPeer = new Peer(undefined, { host: '/', port: '8000' }) const myVideo = document.createElement('video') myVideo.muted = true const peers = {} let videoStream navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => { videoStream = stream addVideoStream(myVideo, stream) myPeer.on('call', call => { call.answer(stream) const video = document.createElement('video') call.on('stream', userVideoStream => { addVideoStream(video, userVideoStream) }) }) socket.on('user-connected', userId => { connectToNewUser(userId, stream) }) socket.on('user-disconnected', userId => { if (peers[userId]) peers[userId].close() }) }) myPeer.on('open', id => { socket.emit('join-room', ROOM_ID, id) }) function connectToNewUser(userId, stream) { const call = myPeer.call(userId, stream) const video = document.createElement('video') call.on('stream', userVideoStream => { addVideoStream(video, userVideoStream) }) call.on('close', () => { video.remove() }) peers[userId] = call } function addVideoStream(video, stream) { video.srcObject = stream video.addEventListener('loadedmetadata', () => { video.play() }) videoGrid.append(video) } //====================================== Front-end styling logics ================================= const audio = document.getElementById('audio') const audio_mute = document.getElementById('audio-mute') const video = document.getElementById('video') const video_mute = document.getElementById('video-mute') const screen_share = document.getElementById('screen-share') const record = document.getElementById('record') const record_stop = document.getElementById('record-stop') const leave_btn = document.getElementById('leave-btn') const message_view_box = document.getElementById('message-view-box') audio.addEventListener('click', function () { const track = videoStream.getAudioTracks()[0].enabled console.log(videoStream.getAudioTracks()); if (track) { videoStream.getAudioTracks()[0].enabled = false } else { videoStream.getAudioTracks()[0].enabled = true } audio.style.display = 'none'; audio_mute.style.display = 'inline-block'; }) audio_mute.addEventListener('click', function () { const track = videoStream.getAudioTracks()[0].enabled console.log(videoStream.getAudioTracks()); if (track) { videoStream.getAudioTracks()[0].enabled = false } else { videoStream.getAudioTracks()[0].enabled = true } audio_mute.style.display = 'none'; audio.style.display = 'inline-block'; }) video.addEventListener('click', function () { const track = videoStream.getVideoTracks()[0].enabled console.log(videoStream.getVideoTracks()[0].enabled); if (track) { videoStream.getVideoTracks()[0].enabled = false } else { videoStream.getVideoTracks()[0].enabled = true } video.style.display = 'none'; video_mute.style.display = 'inline-block'; }) video_mute.addEventListener('click', function () { const track = videoStream.getVideoTracks()[0].enabled console.log(videoStream.getVideoTracks()[0].enabled); if (track) { videoStream.getVideoTracks()[0].enabled = false } else { videoStream.getVideoTracks()[0].enabled = true } video_mute.style.display = 'none'; video.style.display = 'inline-block'; }) // ============================= Chat box logics =============================== let chat_box = document.getElementById('chat_box'); let chat_box_input = document.getElementById('chat_box_input'); let send_icon = document.getElementById('send-icon'); chat_box.addEventListener('submit', function (e) { e.preventDefault() if (chat_box_input.value) { socket.emit('chat message', chat_box_input.value); chat_box_input.value = ''; } }) socket.on('chat message', function (msg) { const item = document.createElement('li'); item.textContent = msg; message_view_box.appendChild(item); message_view_box.scrollTop = message_view_box.scrollHeight - message_view_box.clientHeight; }); // ============================================================================= //================================== Screen share logics ======================= const videoElem = document.getElementById('screen') screen_share.addEventListener('click',async function () { startCapture(); }) // Options for getDisplayMedia() const displayMediaOptions = { video: { cursor: "always" }, audio: true }; async function startCapture() { try { videoElem.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); dumpOptionsInfo(); } catch (err) { console.error(`Error: ${err}`); } } function dumpOptionsInfo() { const videoTrack = videoElem.srcObject.getVideoTracks()[0]; } //============================================================================== //============================================================================== record.addEventListener('click', async function () { record.style.display = 'none'; record_stop.style.display = 'inline-block'; let stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }) const mime = MediaRecorder.isTypeSupported("video/webm; codecs=vp9") ? "video/webm; codecs=vp9" : "video/webm" let mediaRecorder = new MediaRecorder(stream, { mimeType: mime }) let chunks = [] mediaRecorder.addEventListener('dataavailable', function(e) { chunks.push(e.data) }) mediaRecorder.addEventListener('stop', function(){ let blob = new Blob(chunks, { type: chunks[0].type }) let url = URL.createObjectURL(blob) let a = document.createElement('a') a.href = url a.download = 'video.webm' a.click() }) mediaRecorder.start() }) record_stop.addEventListener('click', function () { record_stop.style.display = 'none'; record.style.display = 'inline-block'; mediaRecorder.stop(); }) //==============================================================================
解决方案
核心逻辑是:启动屏幕共享时,替换所有已建立Peer连接的视频轨道为屏幕共享轨道,同时确保新加入用户能直接获取共享流;停止共享时恢复原始摄像头轨道。以下是分步修改方案:
1. 全局变量补充
在代码顶部添加两个全局变量,用于保存原始摄像头轨道和屏幕共享流:
let originalVideoTrack; // 保存原始摄像头视频轨道 let screenShareStream; // 保存屏幕共享流实例
2. 保存原始摄像头轨道
在获取摄像头和麦克风流的回调中,保存原始视频轨道:
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => { videoStream = stream originalVideoTrack = stream.getVideoTracks()[0]; // 新增:保存原始摄像头轨道 addVideoStream(myVideo, stream) // ... 原有代码不变 })
3. 修改屏幕共享启动逻辑,同步至所有对等节点
替换startCapture函数,获取屏幕共享流后更新本地流,并同步所有已连接的Peer:
async function startCapture() { try { screenShareStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); videoElem.srcObject = screenShareStream; // 本地预览共享画面 // 更新本地流:替换摄像头轨道为屏幕共享轨道 videoStream.removeTrack(videoStream.getVideoTracks()[0]); videoStream.addTrack(screenShareStream.getVideoTracks()[0]); // 遍历所有已建立的Peer连接,替换视频轨道 Object.values(peers).forEach(call => { const videoSender = call.peerConnection.getSenders().find(sender => sender.track.kind === 'video'); if (videoSender) { videoSender.replaceTrack(screenShareStream.getVideoTracks()[0]); } }); // 监听屏幕共享自动结束事件(比如用户手动关闭共享窗口) screenShareStream.getVideoTracks()[0].addEventListener('ended', () => { stopScreenShare(); // 切换按钮状态,恢复显示开始共享按钮 screen_share.style.display = 'inline-block'; document.getElementById('screen-share-stop').style.display = 'none'; }); dumpOptionsInfo(); } catch (err) { console.error(`Error: ${err}`); } }
4. 处理新用户加入时的共享同步
修改connectToNewUser和myPeer.on('call')逻辑,确保新用户加入时直接获取当前的共享流(如果正在共享):
// 修改connectToNewUser函数 function connectToNewUser(userId, stream) { // 如果正在共享屏幕,发送包含共享轨道的流;否则发送原始摄像头流 const sendStream = screenShareStream ? videoStream : stream; const call = myPeer.call(userId, sendStream); const video = document.createElement('video') call.on('stream', userVideoStream => { addVideoStream(video, userVideoStream) }) call.on('close', () => { video.remove() }) peers[userId] = call } // 修改myPeer.on('call')回调 myPeer.on('call', call => { // 使用当前的流响应呼叫(自动适配共享/摄像头状态) const answerStream = screenShareStream ? videoStream : stream; call.answer(answerStream) const video = document.createElement('video') call.on('stream', userVideoStream => { addVideoStream(video, userVideoStream) }) })
5. 添加停止屏幕共享功能
首先在页面中添加停止共享按钮(示例ID为screen-share-stop),然后实现停止逻辑:
// 新增停止共享按钮监听 const screenShareStop = document.getElementById('screen-share-stop'); screenShareStop.addEventListener('click', function () { stopScreenShare(); // 切换按钮显示状态 screen_share.style.display = 'inline-block'; screenShareStop.style.display = 'none'; }); // 实现停止共享核心逻辑 function stopScreenShare() { // 停止屏幕共享轨道 if (screenShareStream) { screenShareStream.getTracks().forEach(track => track.stop()); videoElem.srcObject = null; } // 恢复本地流的原始摄像头轨道 videoStream.removeTrack(videoStream.getVideoTracks()[0]); videoStream.addTrack(originalVideoTrack); // 遍历所有Peer连接,替换回摄像头轨道 Object.values(peers).forEach(call => { const videoSender = call.peerConnection.getSenders().find(sender => sender.track.kind === 'video'); if (videoSender) { videoSender.replaceTrack(originalVideoTrack); } }); screenShareStream = null; // 清空共享流引用 }
6. 调整共享按钮的显示逻辑
修改屏幕共享按钮的点击事件,切换按钮显示状态:
screen_share.addEventListener('click',async function () { startCapture(); // 切换按钮显示:隐藏开始共享,显示停止共享 screen_share.style.display = 'none'; document.getElementById('screen-share-stop').style.display = 'inline-block'; })
内容的提问来源于stack exchange,提问作者Mehedi Hasan
相关产品推荐
相关产品推荐

