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

ASP.NET Core 8 MVC+SignalR一对一直播仅显示本地视频问题求助

一对一直播功能远程视频无法加载问题

我基于ASP.NET Core 8 MVC和SignalR开发一对一直播功能,现状如下:

  • 使用Guid生成直播链接;
  • 两个浏览器标签页打开链接后,发起方(Initiator)和接收方(Receiver)均已成功建立SignalR连接;
  • 页面包含localVideo和remoteVideo两个视频标签,其中remoteVideo用于展示对方视频流;
  • 目前本地视频正常显示,但远程视频无法加载。

完整代码实现

Program.cs 文件

using VideoStreamingApp.Hubs;

var builder = WebApplication.CreateBuilder(args);

// 注册服务
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR(); // 添加SignalR服务

var app = builder.Build();

// 配置HTTP请求管道
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

// 映射SignalR Hub
app.MapHub<StreamingHub>("/streamingHub");

app.Run();

控制器代码

public class AdminController : Controller
{
    private static Dictionary<string, DateTime> _streamLinks = new Dictionary<string, DateTime>();

    public IActionResult GenerateLink()
    {
        string streamId = Guid.NewGuid().ToString();
        DateTime expiryTime = DateTime.UtcNow.AddMinutes(1); // 设置链接有效期1分钟

        // 存储链接及过期时间
        _streamLinks[streamId] = expiryTime;

        string streamLink = $"{Request.Scheme}://{Request.Host}/Stream/Join?streamId={streamId}";
        ViewBag.StreamLink = streamLink;
        ViewBag.ExpiryTime = expiryTime.ToString("yyyy-MM-dd HH:mm:ss UTC");

        return View("GenerateLink");
    }

    public static bool IsLinkValid(string streamId)
    {
        return _streamLinks.ContainsKey(streamId) && _streamLinks[streamId] > DateTime.UtcNow;
    }
}

public class StreamController : Controller
{
    public IActionResult Join(string streamId)
    {
        if (!AdminController.IsLinkValid(streamId))
        {
            return View("ExpiredLink"); // 显示链接过期提示
        }

        ViewBag.StreamId = streamId;
        return View();
    }
}

Hubs/StreamingHub.cs

public class StreamingHub : Hub
{
    private static ConcurrentDictionary<string, string> _initiators = new ConcurrentDictionary<string, string>();

    public async Task JoinStream(string streamId)
    {
        bool isInitiator = !_initiators.ContainsKey(streamId);

        if (isInitiator)
        {
            _initiators[streamId] = Context.ConnectionId;
        }

        await Groups.AddToGroupAsync(Context.ConnectionId, streamId);
        await Clients.Caller.SendAsync("SetInitiator", isInitiator);
    }

    public async Task SendOffer(string streamId, string offer)
    {
        await Clients.OthersInGroup(streamId).SendAsync("ReceiveOffer", offer);
    }

    public async Task SendAnswer(string streamId, string answer)
    {
        await Clients.OthersInGroup(streamId).SendAsync("ReceiveAnswer", answer);
    }

    public async Task SendMuteStatus(string streamId, bool isMuted)
    {
        await Clients.OthersInGroup(streamId).SendAsync("ReceiveMuteStatus", Context.ConnectionId, isMuted);
    }

    public async Task SendVideoStatus(string streamId, bool isVideoOn)
    {
        await Clients.OthersInGroup(streamId).SendAsync("ReceiveVideoStatus", Context.ConnectionId, isVideoOn);
    }
}

Stream/Join.cshtml 视图

<h6>Live Video Stream</h6>

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>

<button id="muteBtn">Mute</button>
<button id="videoToggleBtn">Turn Off Video</button>

<script src="https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.11.1/simplepeer.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.2/signalr.min.js"></script>

<script>
    const hubConnection = new signalR.HubConnectionBuilder()
        .withUrl("/streamingHub")
        .configureLogging(signalR.LogLevel.Information)
        .build();

    let peer;
    let isInitiator = false;
    const streamId = "@ViewBag.StreamId";

    hubConnection.start().then(async () => {
        console.log(`✅ Connected to SignalR. with StreamId: ${streamId}`);

        try {
            console.log("🔹 Sending JoinStream request...");
            await hubConnection.invoke("JoinStream", streamId);
            console.log("✅ Successfully invoked JoinStream.");
        } catch (error) {
            console.error("❌ Error invoking JoinStream:", error);
        }
    });

    hubConnection.on("SetInitiator", (role) => {
        console.log("🔹 Received SetInitiator event:", role);
        isInitiator = role;
        console.log(`🔹 Role assigned: ${isInitiator ? "Initiator" : "Receiver"}`);
        startStreaming();
    });

    async function startStreaming() {
        console.log("🔹 Requesting user media...");

        try {
            const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
            console.log("✅ Local video stream received:", stream);
            document.getElementById("localVideo").srcObject = stream;

            // 创建新连接前关闭旧连接
            if (peer) {
                console.log("🔹 Closing existing peer connection...");
                peer.destroy();
                peer = null;
            }

            peer = new SimplePeer({
                initiator: isInitiator,
                trickle: false,
                stream: stream // 直接传入本地流
            });

            // 当Peer生成信令时发送给对方
            peer.on("signal", (data) => {
                console.log("🔹 [Receiver] Generating ANSWER:", JSON.stringify(data));

                hubConnection.invoke("SendAnswer", streamId, JSON.stringify(data))
                    .then(() => console.log("✅ [Receiver] Answer sent successfully."))
                    .catch((err) => console.error("❌ [Receiver] Error sending Answer:", err));
            });

            peer.on("stream", (remoteStream) => {
                console.log("✅ Remote stream received:", remoteStream);
                const remoteVideoElement = document.getElementById("remoteVideo");

                if (remoteVideoElement) {
                    remoteVideoElement.srcObject = remoteStream;
                    console.log("🎥 Remote video assigned successfully.");
                } else {
                    console.error("❌ remoteVideo element not found.");
                }
            });

            peer.on("error", (err) => {
                console.error("❌ WebRTC Error:", err);
            });

            peer.on("connect", () => {
                console.log("✅ Peer connection established.");
            });

        } catch (error) {
            console.error("❌ Error accessing media devices:", error);
        }
    }

    // 接收方监听OFFER
    hubConnection.on("ReceiveOffer", (offer) => {
        console.log("🔹 [Receiver] Received OFFER:", offer);

        if (!peer) {
            console.log("🔹 [Receiver] Initializing peer connection...");
            startStreaming();
        }

        if (peer) {
            console.log("🔹 [Receiver] Applying OFFER...");
            peer.signal(JSON.parse(offer));
        } else {
            console.error("❌ [Receiver] Peer object is NULL while processing offer.");
        }
    });

    // 发起方监听ANSWER
    hubConnection.on("ReceiveAnswer", (answer) => {
        console.log("🔹 [Initiator] Received ANSWER:", answer);

        if (peer) {
            console.log("🔹 [Initiator] Applying ANSWER...");
            peer.signal(JSON.parse(answer));
        } else {
            console.error("❌ [Initiator] Peer object is NULL while processing answer.");
        }
    });

</script>

控制台日志

发起方(第一标签页)

✅ Connected to SignalR. with StreamId: 2acc13b3-9c48-423f-aa3e-1e497494f886
🔹 Sending JoinStream request...
🔹 Received SetInitiator event: true
🔹 Role assigned: Initiator
🔹 Requesting user media...
✅ Successfully invoked JoinStream.
🔹 [Receiver] Generating ANSWER:
✅ Local video stream received: 
✅ [Receiver] Answer sent successfully.

接收方(第二标签页)

✅ Connected to SignalR. with StreamId: 2acc13b3-9c48-423f-aa3e-1e497494f886
🔹 Sending JoinStream request...
🔹 Received SetInitiator event: false
🔹 Role assigned: Receiver
🔹 Requesting user media...
✅ Successfully invoked JoinStream.
✅ Local video stream received: 

问题分析与修复方案

核心问题

从代码和日志可以看出,WebRTC信令逻辑存在错误:

  1. 发起方未发送Offer:当前代码中peer.on("signal")统一调用SendAnswer方法,但发起方的signal事件生成的是Offer,导致接收方永远收不到Offer,无法触发后续连接流程。
  2. 信令交互断裂:接收方未收到Offer,ReceiveOffer事件从未触发,无法生成Answer并返回给发起方,WebRTC连接无法建立。

修复步骤

1. 修正前端信令发送逻辑

在startStreaming函数中,根据isInitiator角色判断发送Offer还是Answer:

peer.on("signal", (data) => {
    if (isInitiator) {
        console.log("🔹 [Initiator] Generating OFFER:", JSON.stringify(data));
        hubConnection.invoke("SendOffer", streamId, JSON.stringify(data))
            .then(() => console.log("✅ [Initiator] Offer sent successfully."))
            .catch((err) => console.error("❌ [Initiator] Error sending Offer:", err));
    } else {
        console.log("🔹 [Receiver] Generating ANSWER:", JSON.stringify(data));
        hubConnection.invoke("SendAnswer", streamId, JSON.stringify(data))
            .then(() => console.log("✅ [Receiver] Answer sent successfully."))
            .catch((err) => console.error("❌ [Receiver] Error sending Answer:", err));
    }
});

2. 补充Hub连接清理逻辑(可选但重要)

当前_initiators字典未处理连接断开的情况,会导致同一streamId无法重复使用,添加OnDisconnectedAsync方法:

public override async Task OnDisconnectedAsync(Exception exception)
{
    // 查找当前连接对应的streamId
    var streamId = _initiators.FirstOrDefault(kv => kv.Value == Context.ConnectionId).Key;
    if (!string.IsNullOrEmpty(streamId))
    {
        _initiators.TryRemove(streamId, out _);
    }
    await base.OnDisconnectedAsync(exception);
}

3. 优化SimplePeer配置(可选)

测试环境中trickle: false可以简化流程,但生产环境建议开启trickle: true以优化连接速度。

修复后,发起方会正确发送Offer,接收方收到Offer后生成Answer并返回,双方完成WebRTC握手,远程视频流即可正常显示。


内容的提问来源于stack exchange,提问作者Sayed Asad

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 12:57:02