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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 14:53:24