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

ASP.NET WebForm中SignalR+WebRTC无法接收其他用户视频流求助

ASP.NET WebForm + SignalR + WebRTC 视频通话接收远程流问题

我正在使用ASP.NET WebForm、SignalR和WebRTC开发视频通话功能,需求为:用户访问Page1.aspx页面时,可互相查看对方摄像头的视频。

目前页面可显示本地视频流,但当其他用户访问Page1.aspx时,我无法接收到新用户的视频流。已编写receiveVideoStream处理接收逻辑,但仍无法正常工作,且控制台无任何报错信息,恳请帮助。

现有代码

Page1.aspx

<ul id="_listvideouser" class="gap15"></ul>
<input id="hdtempIDUser" type="hidden" runat="server" />

<script src="scripts/jquery.signalR-2.4.3.min.js"></script>
<script src="signalr/hubs"></script>
<script>
    var userId = $('#hdtempIDUser').val();;
    $(function () {
        const hub = $.connection.videoHub;

        $.connection.hub.start().done(function () {
            startLocalVideoStream();
        });

        const localVideo = document.createElement('video');
        const remoteVideos = new Map(); //

        // When receiving a video stream from someone else
        hub.client.receiveVideoStream = function (userId, videoStream) {
            if (!remoteVideos[userId]) {
                remoteVideos[userId] = videoStream;
                const li = $('<li data-user-id=' + userId + '></li>');
                const remoteVideo = document.createElement('video');
                remoteVideo.srcObject = videoStream;
                remoteVideo.autoplay = true;
                li.append(remoteVideo);
                $('#_listvideouser').append(li);
            }
        };

    
        // Process video stream from current user
        function startLocalVideoStream() {
            navigator.mediaDevices.getUserMedia({ video: true, audio: true })
                .then(function (stream) {
                    localVideo.srcObject = stream;
                    localVideo.autoplay = true;
                    // Gửi video stream đến hub
                    hub.server.sendVideoStream(userId, stream);

                    const li = $('<li data-user-id=' + userId + '></li>');
                    li.append(localVideo);
                    $('#_listvideouser').append(li);
                    // offer, handle ICE candidates, ...
                })
                .catch(function (error) {
                    console.error('Error accessing webcam:', error);
                });
        }
    });
</script>

VideoHub.cs

public class VideoHub : Hub
{
    public void SendVideoStream(string userId, string videoStream)
    {
        Clients.Others.ReceiveVideoStream(userId, videoStream);
    }
}

问题分析与修复方案

核心问题

  1. WebRTC流无法直接通过SignalR传输:MediaStream是浏览器本地对象,无法序列化成字符串通过SignalR发送,你当前直接传stream到Hub,实际会被转成无效值,导致接收方拿不到有效流。
  2. SignalR回调注册时机错误:hub.client.receiveVideoStream在Hub连接启动后才注册,可能错过早期的回调通知。
  3. Map对象使用错误:声明了remoteVideos为Map,但用了对象的访问方式remoteVideos[userId],不符合Map的API规范。

修复步骤

1. 调整SignalR回调注册顺序

将客户端回调方法移到$.connection.hub.start()之前,确保连接建立前就完成注册:

$(function () {
    const hub = $.connection.videoHub;
    const localVideo = document.createElement('video');
    const remoteVideos = new Map();

    // 先注册所有客户端回调
    hub.client.receiveOffer = function(senderId, offer) { /* 处理逻辑 */ };
    hub.client.receiveAnswer = function(senderId, answer) { /* 处理逻辑 */ };
    hub.client.receiveIceCandidate = function(senderId, candidate) { /* 处理逻辑 */ };

    $.connection.hub.start().done(function () {
        startLocalVideoStream();
    });

    // ... 其他代码
});

2. 改用WebRTC信令流程(SignalR仅传信令)

WebRTC需要通过信令交换SDP会话描述和ICE候选,SignalR作为信令通道,直接建立P2P流传输。修改Page1.aspx的JavaScript:

var userId = $('#hdtempIDUser').val();
$(function () {
    const hub = $.connection.videoHub;
    const localVideo = document.createElement('video');
    const remoteVideos = new Map();
    const peerConnections = new Map(); // 存储每个用户的PeerConnection

    // 接收新用户加入通知
    hub.client.userJoined = function(newUserId) {
        // 主动向新用户发起连接
        createPeerConnection(newUserId);
        peerConnections.get(newUserId).createOffer()
            .then(offer => peerConnections.get(newUserId).setLocalDescription(offer))
            .then(() => hub.server.sendOffer(newUserId, userId, peerConnections.get(newUserId).localDescription));
    };

    // 接收SDP Offer
    hub.client.receiveOffer = function(senderId, offer) {
        createPeerConnection(senderId);
        peerConnections.get(senderId).setRemoteDescription(new RTCSessionDescription(offer))
            .then(() => peerConnections.get(senderId).createAnswer())
            .then(answer => peerConnections.get(senderId).setLocalDescription(answer))
            .then(() => hub.server.sendAnswer(senderId, userId, peerConnections.get(senderId).localDescription));
    };

    // 接收SDP Answer
    hub.client.receiveAnswer = function(senderId, answer) {
        peerConnections.get(senderId).setRemoteDescription(new RTCSessionDescription(answer));
    };

    // 接收ICE候选
    hub.client.receiveIceCandidate = function(senderId, candidate) {
        if (peerConnections.has(senderId)) {
            peerConnections.get(senderId).addIceCandidate(new RTCIceCandidate(candidate));
        }
    };

    $.connection.hub.start().done(function () {
        startLocalVideoStream();
        // 通知服务器当前用户上线
        hub.server.userJoined(userId);
    });

    // 创建PeerConnection实例
    function createPeerConnection(targetUserId) {
        const pc = new RTCPeerConnection({
            iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // 公共STUN服务器,用于NAT穿透
        });

        // 添加本地流到连接
        localVideo.srcObject.getTracks().forEach(track => pc.addTrack(track, localVideo.srcObject));

        // 接收远程流并渲染
        pc.ontrack = function(event) {
            if (!remoteVideos.has(targetUserId)) {
                const remoteVideo = document.createElement('video');
                remoteVideo.srcObject = event.streams[0];
                remoteVideo.autoplay = true;
                remoteVideo.playsInline = true;
                const li = $(`<li data-user-id="${targetUserId}"></li>`);
                li.append(remoteVideo);
                $('#_listvideouser').append(li);
                remoteVideos.set(targetUserId, remoteVideo);
            }
        };

        // 发送ICE候选到对方
        pc.onicecandidate = function(event) {
            if (event.candidate) {
                hub.server.sendIceCandidate(targetUserId, event.candidate);
            }
        };

        peerConnections.set(targetUserId, pc);
        return pc;
    }

    function startLocalVideoStream() {
        navigator.mediaDevices.getUserMedia({ video: true, audio: true })
            .then(function(stream) {
                localVideo.srcObject = stream;
                localVideo.autoplay = true;
                localVideo.playsInline = true;
                const li = $(`<li data-user-id="${userId}"></li>`);
                li.append(localVideo);
                $('#_listvideouser').append(li);
            })
            .catch(function(error) {
                console.error('Error accessing webcam:', error);
            });
    }
});

3. 更新VideoHub.cs的信令转发逻辑

修改Hub类,处理信令消息的转发和在线用户管理:

using System.Collections.Generic;
using Microsoft.AspNet.SignalR;

public class VideoHub : Hub
{
    private static readonly HashSet<string> OnlineUsers = new HashSet<string>();

    public void UserJoined(string userId)
    {
        OnlineUsers.Add(userId);
        // 通知所有在线用户新用户加入
        Clients.Others.UserJoined(userId);
        // 可选:通知新用户已在线的其他用户
        Clients.Caller.ReceiveOnlineUsers(OnlineUsers.Where(u => u != userId).ToList());
    }

    public void SendOffer(string targetUserId, string senderUserId, dynamic offer)
    {
        Clients.User(targetUserId).ReceiveOffer(senderUserId, offer);
    }

    public void SendAnswer(string targetUserId, string senderUserId, dynamic answer)
    {
        Clients.User(targetUserId).ReceiveAnswer(senderUserId, answer);
    }

    public void SendIceCandidate(string targetUserId, dynamic candidate)
    {
        Clients.User(targetUserId).ReceiveIceCandidate(Context.ConnectionId, candidate);
    }

    public override Task OnDisconnected(bool stopCalled)
    {
        var userId = Context.QueryString["userId"]; // 或者用你传递的用户标识
        if (!string.IsNullOrEmpty(userId) && OnlineUsers.Contains(userId))
        {
            OnlineUsers.Remove(userId);
            Clients.Others.UserLeft(userId);
        }
        return base.OnDisconnected(stopCalled);
    }
}

4. 关键注意事项

  • HTTPS要求:WebRTC在非localhost环境下必须使用HTTPS,否则getUserMedia会被浏览器阻止。
  • STUN/TURN服务器:如果用户处于不同网络环境,需要配置STUN/TURN服务器穿透NAT,示例中用了Google的公共STUN,生产环境建议自行部署或使用付费服务。
  • 用户ID唯一性:确保hdtempIDUser的值全局唯一,避免信令混乱。

内容的提问来源于stack exchange,提问作者Chim Di Tru

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 10:22:37