I am trying to make a web-livestreaming using webRTC, signaling server using golang to connect between clients. Clients will include broadcasters and viewers. There will be about 5-10 broadcasters that can livestream at the same time. The broadcaster will broadcast the livestream with its own licenseId. Viewers will send the licenseId depending on which broadcaster they want to watch livestream.
After running, no error occurs, but the client viewer cannot see the video of the broadcaster being livestreamed
Server-side (Golang):
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
type Room3 struct {
mu sync.Mutex
broadcaster *Client3
watchers map[*Client3]bool
}
type Client3 struct {
socket *websocket.Conn
send chan []byte
licenseID string
isBroadcaster bool
}
var (
upgrader3 = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
mu sync.Mutex
rooms3 = make(map[string]*Room3)
)
func main() {
http.HandleFunc("/ws", handleWebSocket)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
socket, err := upgrader3.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
client := &Client3{
socket: socket,
send: make(chan []byte),
}
go client.listen()
}
func (c *Client3) listen() {
defer func() {
c.socket.Close()
}()
for {
_, msg, err := c.socket.ReadMessage()
if err != nil {
log.Println("Read error:", err)
break
}
var message map[string]interface{}
err = json.Unmarshal(msg, &message)
if err != nil {
log.Println("JSON unmarshal error:", err)
continue
}
action, ok := message["action"].(string)
if !ok {
log.Println("Invalid action")
continue
}
switch action {
case "broadcaster":
licenseID, ok := message["licenseID"].(string)
if !ok {
log.Println("Invalid licenseID for broadcaster")
continue
}
handleBroadcaster(c, licenseID)
case "viewer":
licenseID, ok := message["licenseID"].(string)
if !ok {
log.Println("Invalid licenseID for viewer")
continue
}
handleViewer(c, licenseID)
case "offer", "answer", "candidate":
licenseID, ok := message["licenseID"].(string)
if !ok {
log.Println("Invalid licenseID for WebRTC message")
continue
}
handleWebRTCMessage(action, licenseID, msg)
default:
log.Println("Unknown action:", action)
}
}
}
func handleBroadcaster(client *Client3, licenseID string) {
mu.Lock()
defer mu.Unlock()
if room, exists := rooms3[licenseID]; exists {
if room.broadcaster != nil {
log.Println("Broadcaster already exists for licenseID:", licenseID)
return
}
client.licenseID = licenseID
client.isBroadcaster = true
room.broadcaster = client
room.watchers = make(map[*Client3]bool)
log.Printf("Broadcaster connected: LicenseID %sn", licenseID)
} else {
room := &Room3{
watchers: make(map[*Client3]bool),
}
client.licenseID = licenseID
client.isBroadcaster = true
room.broadcaster = client
rooms3[licenseID] = room
log.Printf("New room created with Broadcaster: LicenseID %sn", licenseID)
}
}
func handleViewer(client *Client3, licenseID string) {
mu.Lock()
defer mu.Unlock()
if room, exists := rooms3[licenseID]; exists {
if room.broadcaster == nil {
log.Println("No broadcaster available for licenseID:", licenseID)
client.socket.WriteJSON(map[string]string{"error": "No broadcaster available for this licenseID"})
return
}
client.licenseID = licenseID
room.watchers[client] = true
room.broadcaster.socket.WriteJSON(map[string]interface{}{
"action": "viewer",
"viewer": client,
"licenseID": licenseID,
})
log.Printf("Viewer connected: LicenseID %sn", licenseID)
} else {
log.Println("No room exists for licenseID:", licenseID)
client.socket.WriteJSON(map[string]string{"error": "No room exists for this licenseID"})
}
}
func handleWebRTCMessage(action, licenseID string, msg []byte) {
mu.Lock()
defer mu.Unlock()
room, exists := rooms3[licenseID]
if !exists {
log.Println("Room3 does not exist for licenseID:", licenseID)
return
}
switch action {
case "offer":
if room.broadcaster != nil {
room.broadcaster.socket.WriteMessage(websocket.TextMessage, msg)
} else {
log.Println("No broadcaster available for licenseID:", licenseID)
}
case "answer":
for watcher := range room.watchers {
watcher.socket.WriteMessage(websocket.TextMessage, msg)
}
case "candidate":
var candidate map[string]interface{}
err := json.Unmarshal(msg, &candidate)
if err != nil {
log.Println("Error decoding ICE candidate:", err)
return
}
if candidateJSON, err := json.Marshal(candidate); err == nil {
for watcher := range room.watchers {
watcher.socket.WriteMessage(websocket.TextMessage, candidateJSON)
}
} else {
log.Println("Error encoding ICE candidate:", err)
}
}
}
Clients-side (HTML,JS):
Broadcaster
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Broadcaster</title>
</head>
<body>
<h1>Broadcaster</h1>
<input type="text" id="licenseID" placeholder="Enter License ID">
<button id="startBroadcasting">Start Broadcasting</button>
<video id="localVideo" autoplay muted></video>
<script>
const ws = new WebSocket('ws://localhost:8080/ws');
let peerConnection;
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
document.getElementById('startBroadcasting').onclick = async () => {
const licenseID = document.getElementById('licenseID').value;
if (!licenseID) {
alert('Please enter a License ID');
return;
}
ws.send(JSON.stringify({ action: 'broadcaster', licenseID }));
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
document.getElementById('localVideo').srcObject = stream;
peerConnection = new RTCPeerConnection(config);
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
peerConnection.onicecandidate = event => {
if (event.candidate) {
ws.send(JSON.stringify({ action: 'candidate', licenseID, candidate: event.candidate }));
}
};
peerConnection.onnegotiationneeded = async () => {
try {
await peerConnection.setLocalDescription(await peerConnection.createOffer());
ws.send(JSON.stringify({ action: 'offer', licenseID, description: peerConnection.localDescription }));
} catch (err) {
console.error('Error creating offer:', err);
}
};
};
ws.onmessage = async event => {
const message = JSON.parse(event.data);
switch (message.action) {
case 'answer':
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(message.description));
} catch (err) {
console.error('Error setting remote description:', err);
}
break;
case 'candidate':
try {
await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
} catch (err) {
console.error('Error adding ICE candidate:', err);
}
break;
}
};
</script>
</body>
</html>
Viewer:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Viewer</title>
</head>
<body>
<h1>Viewer</h1>
<input type="text" id="licenseID" placeholder="Enter License ID">
<button id="startViewing">Start Viewing</button>
<video id="remoteVideo" autoplay></video>
<script>
const ws = new WebSocket('ws://localhost:8080/ws');
let peerConnection;
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
document.getElementById('startViewing').onclick = async () => {
const licenseID = document.getElementById('licenseID').value;
if (!licenseID) {
alert('Please enter a License ID');
return;
}
ws.send(JSON.stringify({ action: 'viewer', licenseID }));
};
ws.onmessage = async event => {
const message = JSON.parse(event.data);
switch (message.action) {
case 'offer':
try {
peerConnection = new RTCPeerConnection(config);
peerConnection.ontrack = event => {
document.getElementById('remoteVideo').srcObject = event.streams[0];
};
peerConnection.onicecandidate = event => {
if (event.candidate) {
ws.send(JSON.stringify({ action: 'candidate', licenseID: message.licenseID, candidate: event.candidate }));
}
};
await peerConnection.setRemoteDescription(new RTCSessionDescription(message.description));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
ws.send(JSON.stringify({ action: 'answer', licenseID: message.licenseID, description: answer }));
} catch (err) {
console.error('Error handling offer:', err);
}
break;
case 'candidate':
try {
await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
} catch (err) {
console.error('Error adding ICE candidate:', err);
}
break;
}
};
</script>
</body>
</html>