如何用WebRTC+SignalR跨页面投屏?黑屏报错求助
问题分析与解决思路
核心问题
你遇到的错误根源是**MediaStream对象无法通过SignalR直接传输**。SignalR会自动将参数序列化为JSON格式,但MediaStream是浏览器原生的复杂对象,无法被JSON序列化,所以学生端收到的stream只是一个空的普通对象,而非真正的MediaStream,自然无法设置给video的srcObject。
WebRTC的正确用法是:SignalR仅作为信令服务器传递连接协商数据(SDP、ICE候选),媒体流由教师端和学生端浏览器直接通过P2P连接传输,不需要经过服务器转发。
具体解决方案
1. 修改SignalR Hub(仅处理信令)
将原Hub方法替换为处理WebRTC信令的方法:
// 发送SDP Offer/Answer public async Task SendSdp(int classCode, string type, string sdp) { await Clients.Others.SendAsync("ReceiveSdp", classCode, type, sdp); } // 发送ICE候选 public async Task SendIceCandidate(int classCode, string candidate, string sdpMid, int sdpMLineIndex) { await Clients.Others.SendAsync("ReceiveIceCandidate", classCode, candidate, sdpMid, sdpMLineIndex); }
2. 教师端JS代码(发起投屏)
修改投屏逻辑,使用WebRTC建立P2P连接:
let peerConnection; let localStream; $('#share-screen-btn').click(function () { const options = { audio: true, video: true }; navigator.mediaDevices.getDisplayMedia(options) .then(handleSuccess) .catch(handleError); }); async function handleSuccess(stream) { localStream = stream; // 初始化RTCPeerConnection,使用公共STUN服务器穿透NAT peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); // 将本地屏幕流添加到连接 localStream.getTracks().forEach(track => { peerConnection.addTrack(track, localStream); }); // 监听ICE候选,发送给学生端 peerConnection.onicecandidate = (event) => { if (event.candidate) { connection.invoke("SendIceCandidate", clsCode, event.candidate.candidate, event.candidate.sdpMid, event.candidate.sdpMLineIndex) .catch(err => console.error(err)); } }; // 生成SDP Offer并发送给学生端 const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); connection.invoke("SendSdp", clsCode, "offer", JSON.stringify(offer)) .catch(err => console.error(err)); } // 接收学生端返回的Answer connection.on("ReceiveSdp", async (classCode2, type, sdp) => { if (classCode2 !== clsCode) return; if (type === "answer") { await peerConnection.setRemoteDescription(JSON.parse(sdp)); } }); // 接收学生端的ICE候选 connection.on("ReceiveIceCandidate", (classCode2, candidate, sdpMid, sdpMLineIndex) => { if (classCode2 !== clsCode) return; peerConnection.addIceCandidate(new RTCIceCandidate({ candidate, sdpMid, sdpMLineIndex })).catch(err => console.error(err)); });
3. 学生端JS代码(接收投屏)
修改监听逻辑,建立WebRTC连接并接收流:
let peerConnection; // 初始化RTCPeerConnection function initPeerConnection() { peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); // 监听远程流,设置到video元素 peerConnection.ontrack = (event) => { const video = document.querySelector('#share-screen-container'); video.srcObject = event.streams[0]; }; // 监听ICE候选,发送给教师端 peerConnection.onicecandidate = (event) => { if (event.candidate) { connection.invoke("SendIceCandidate", classCode, event.candidate.candidate, event.candidate.sdpMid, event.candidate.sdpMLineIndex) .catch(err => console.error(err)); } }; } // 接收教师端的Offer并返回Answer connection.on("ReceiveSdp", async (classCode2, type, sdp) => { if (classCode2 !== classCode) return; if (type === "offer") { if (!peerConnection) initPeerConnection(); await peerConnection.setRemoteDescription(JSON.parse(sdp)); // 生成Answer并发送给教师端 const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); connection.invoke("SendSdp", classCode, "answer", JSON.stringify(answer)) .catch(err => console.error(err)); } }); // 接收教师端的ICE候选 connection.on("ReceiveIceCandidate", (classCode2, candidate, sdpMid, sdpMLineIndex) => { if (classCode2 !== classCode || !peerConnection) return; peerConnection.addIceCandidate(new RTCIceCandidate({ candidate, sdpMid, sdpMLineIndex })).catch(err => console.error(err)); });
4. 额外注意事项
- 确保测试环境使用HTTPS(localhost除外),浏览器对非安全上下文限制
getDisplayMedia等WebRTC相关API - 如果需要穿透复杂NAT网络,需配置TURN服务器(可使用开源的coturn)
- 可添加流断开、重连的处理逻辑,提升稳定性
内容的提问来源于stack exchange,提问作者timmack
相关产品推荐
相关产品推荐

