You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 20:35:28