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信令逻辑存在错误:
- 发起方未发送Offer:当前代码中
peer.on("signal")统一调用SendAnswer方法,但发起方的signal事件生成的是Offer,导致接收方永远收不到Offer,无法触发后续连接流程。 - 信令交互断裂:接收方未收到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
相关产品推荐
相关产品推荐

