Reputation: 31
I am creating a one-on-one live streaming functionality.
Program.cs
file:
using VideoStreamingApp.Hubs;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR(); // Adding SignalR
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// **Map SignalR Hub**
app.MapHub<StreamingHub>("/streamingHub");
app.Run();
Controller:
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); // Set expiry time to 1 minutes
// Store the link with expiration time
_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"); // Show expiration message
}
ViewBag.StreamId = streamId;
return View();
}
}
Inside Hubs folder > StreamingHub
:
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);
}
}
Join.cshtml
of StreamController
:
<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;
// Close existing peer connection before creating a new one
if (peer) {
console.log("πΉ Closing existing peer connection...");
peer.destroy();
peer = null;
}
peer = new SimplePeer({
initiator: isInitiator,
trickle: false,
stream: stream // Directly pass the stream
});
// 2οΈβ£ When peer connection generates an ANSWER, send it back
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);
}
}
// 1οΈβ£ Receiver listens for 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.");
}
});
// 3οΈβ£ Initiator listens for the 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>
Console logs:
First Tab:
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.
Second Tab:
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:
Upvotes: 2
Views: 78
Reputation: 22029
After carefully reviewing the documentation of the three-party simple-peer
you use, I found that the problem is that every time you create a SimplePeer
object, the role always is initiator
. You need to modify the logic yourself. I just manually set isInitiator=false
(debugging in vs2022) when establishing the signalr connection to bypass this problem.
In addition, Clients.OthersInGroup
does not seem to be effective, please check the official documentation. I use Clients.All.SendAsync
instead here, just for testing.
Logs
[2025-02-24T07:29:12.793Z] Information: Normalizing '/streamingHub' to 'http://localhost:5179/streamingHub'.
signalr.min.js:1 [2025-02-24T07:29:12.864Z] Information: WebSocket connected to ws://localhost:5179/streamingHub?id=3y5EqBOdjMBaO2lsTRk6aA.
signalr.min.js:1 [2025-02-24T07:29:12.865Z] Information: Using HubProtocol 'json'.
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:62 β
Connected to SignalR. with StreamId: f2d6b973-f5c9-420f-8d43-40867adea1e9
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:65 πΉ Sending JoinStream request...
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:75 πΉ Received SetInitiator event: false
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:77 πΉ Role assigned: Receiver
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:87 πΉ Requesting user media...
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:67 β
Successfully invoked JoinStream.
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:91 β
Local video stream received: MediaStreamΒ {id: '9226ded7-a11d-4f97-bfed-0beca1b4abc6', active: true, onaddtrack: null, onremovetrack: null, onactive: null,Β β¦}
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:143 πΉ Processing pending signals...
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:81 πΉ Peer connection ready as: Receiver
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:154 πΉ [Receiver] Received OFFER: {"type":"offer","sdp":"v=0\r\no=- 3232030228750443743 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS 26cb8b55-ae56-42fd-b329-705a9df***ize:262144\r\n"}
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:167 πΉ [Receiver] Applying OFFER to existing peer...
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:130 β
Remote stream received: MediaStreamΒ {id: '26cb8b55-ae56-42fd-b329-705a9df9ce5f', active: true, onaddtrack: null, onremovetrack: null, onactive: null,Β β¦}
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:107 πΉ Peer generated signal data: {type: 'answer', sdp: 'v=0\r\no=- 5343498561161681530 2 IN IP4 127.0.0.1\r\nsβ¦:2\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n'}
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:174 πΉ [Initiator] Received ANSWER: {"type":"answer","sdp":"v=0\r\no=- 5343498561161681530 2 **C:F6:E3:50:79:FA:47:63:76:4F:32:46:46:EF\r\na=setup:active\r\na=mid:2\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n"}
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:179 β οΈ [Initiator] Already in 'stable' state, ignoring duplicate Answer.
(anonymous) @ Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:179
(anonymous) @ signalr.min.js:1
ut @ signalr.min.js:1
I @ signalr.min.js:1
A.connection.onreceive @ signalr.min.js:1
i.onmessage @ signalr.min.js:1
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:120 β
[Receiver] Answer sent successfully.
Join?streamId=f2d6b973-f5c9-420f-8d43-40867adea1e9:139 β
Peer connection established.
Here is my test code
Join.cshtml
<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>
<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>
const hubConnection = new signalR.HubConnectionBuilder()
.withUrl("/streamingHub")
.configureLogging(signalR.LogLevel.Information)
.build();
let peer;
let isInitiator = false;
const streamId = "@ViewBag.StreamId";
let pendingSignals = [];
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().then(() => {
console.log("πΉ Peer connection ready as:", isInitiator ? "Initiator" : "Receiver");
});
});
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) {
peer.destroy();
}
peer = new SimplePeer({
initiator: isInitiator,
trickle: false,
stream: stream
});
peer.on("signal", (data) => {
console.log("πΉ Peer generated signal data:", data);
if (isInitiator) {
// if (peer._pc.signalingState === "have-local-offer") {
// console.warn("β οΈ [Initiator] Already have local offer, skipping duplicate send.");
// return;
// }
hubConnection.invoke("SendOffer", streamId, JSON.stringify(data))
.then(() => console.log("β
[Initiator] Offer sent successfully."))
.catch((err) => console.error("β [Initiator] Error sending Offer:", err));
} else {
if (peer._pc.signalingState === "stable") {
hubConnection.invoke("SendAnswer", streamId, JSON.stringify(data))
.then(() => console.log("β
[Receiver] Answer sent successfully."))
.catch((err) => console.error("β [Receiver] Error sending Answer:", err));
} else {
console.warn("β οΈ [Receiver] Not in 'stable' state, skipping Answer send.");
}
}
});
peer.on("stream", (remoteStream) => {
console.log("β
Remote stream received:", remoteStream);
document.getElementById("remoteVideo").srcObject = remoteStream;
});
peer.on("error", (err) => {
console.error("β WebRTC Error:", err);
});
peer.on("connect", () => {
console.log("β
Peer connection established.");
});
console.log("πΉ Processing pending signals...");
pendingSignals.forEach((signal) => peer.signal(signal));
pendingSignals = [];
} catch (error) {
console.error("β Error accessing media devices:", error);
}
}
hubConnection.on("ReceiveOffer", (offer) => {
console.log("πΉ [Receiver] Received OFFER:", offer);
const parsedOffer = JSON.parse(offer);
if (!peer) {
console.log("πΉ [Receiver] Peer not initialized. Adding to pending signals...");
pendingSignals.push(parsedOffer);
startStreaming();
} else {
if (peer._pc.signalingState !== "stable") {
console.warn("β οΈ [Receiver] Peer connection not in stable state, ignoring OFFER.");
return;
}
console.log("πΉ [Receiver] Applying OFFER to existing peer...");
peer.signal(parsedOffer);
}
});
hubConnection.on("ReceiveAnswer", (answer) => {
console.log("πΉ [Initiator] Received ANSWER:", answer);
const parsedAnswer = JSON.parse(answer);
if (peer) {
if (peer._pc.signalingState === "stable") {
console.warn("β οΈ [Initiator] Already in 'stable' state, ignoring duplicate Answer.");
return;
}
console.log("πΉ [Initiator] Applying ANSWER...");
peer.signal(parsedAnswer);
} else {
console.error("β [Initiator] Peer object is NULL while processing answer.");
}
});
</script>
StreamingHub.cs
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace VideoStreamingApp.Hubs
{
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);
await Clients.All.SendAsync("ReceiveOffer", offer);
}
public async Task SendAnswer(string streamId, string answer)
{
//await Clients.OthersInGroup(streamId).SendAsync("ReceiveAnswer", answer);
await Clients.All.SendAsync("ReceiveAnswer", answer);
}
public async Task SendMuteStatus(string streamId, bool isMuted)
{
//await Clients.OthersInGroup(streamId).SendAsync("ReceiveMuteStatus", Context.ConnectionId, isMuted);
await Clients.All.SendAsync("ReceiveMuteStatus", Context.ConnectionId, isMuted);
}
public async Task SendVideoStatus(string streamId, bool isVideoOn)
{
//await Clients.OthersInGroup(streamId).SendAsync("ReceiveVideoStatus", Context.ConnectionId, isVideoOn);
await Clients.All.SendAsync("ReceiveVideoStatus", Context.ConnectionId, isVideoOn);
}
}
}
Upvotes: 0