I am encountering an issue with my WebRTC implementation in Django using Django Channels. The error I am facing is:
"Uncaught (in promise) DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable"
After some research, I found a possible explanation for this error on Stack Overflow. It seems to be related to the fact that when a third user joins, it sends an offer to the two previously connected users, resulting in two answers. As a single RTCPeerConnection can only establish one peer-to-peer connection, attempting to setRemoteDescription on the second answer fails.
The suggested solution is to instantiate a new RTCPeerConnection for every remote peer. However, I'm unsure how to implement this in my existing code. Below is the relevant portion of my consumers.py and JavaScript code.
consumers.py and JavaScript code
import json
from typing import Text
from django.contrib.auth import authenticate
from Program.models import ChatRoom, Message
from channels.generic.websocket import AsyncWebsocketConsumer
from .service import add_remove_online_user, updateLocationList, add_remove_room_user
from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async
class ChatRoomConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_code']
self.room_group_name = self.room_name
print(self.room_group_name)
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
await self.authenticate_user()
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'chatroom_message',
'message' : "add",
'to' : "all",
'from' : self.user,
"event" : "online_traffic",
}
)
async def disconnect(self, close_code):
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'chatroom_message',
'to' : "all",
'from' : self.user,
'event' : 'online_traffic',
'message' : "remove"
}
)
await self.authenticate_user(add=False)
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
@sync_to_async
def get_user_profile(self):
user = self.scope['user']
return user.user_profile
@database_sync_to_async
def authenticate_user(self, add=True):
if self.scope['user'].is_authenticated:
self.room = ChatRoom.objects.get(room_id=self.room_group_name)
user = self.scope["user"]
profile = user.user_profile
self.user = {"id": user.user_profile.unique_id, "name": user.first_name if user.first_name else user.username, "profile_pic": user.user_profile.profile_pic.url}
if add:
profile.active = True
profile.save()
self.room.online.add(user)
add_remove_online_user(self.room, user) # Update the list of active users
else:
profile.active = False
profile.save()
self.room.online.remove(user)
add_remove_online_user(self.room, user) # Update the list of active users
self.room.save()
async def receive(self, text_data):
text_data_json = json.loads(text_data)
print(text_data_json)
msgType = text_data_json.get("type")
to = text_data_json.get("to")
from_ = text_data_json.get("from")
# user_profile = await self.get_user_profile()
if msgType == "login":
print(f"[ {from_['id']} logged in. ]")
elif msgType == "offer":
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'offer',
'offer' : text_data_json["offer"],
'to' : to,
'from' : from_
}
)
elif msgType == "answer":
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'answer',
'answer' : text_data_json["answer"],
'to' : to,
'from' : from_
}
)
elif msgType == "candidate":
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'candidate',
'candidate' : text_data_json["candidate"],
'to' : to,
'from' : from_
}
)
elif msgType == "joiner":
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'joiner',
"to" : "all",
"from" : from_
}
)
elif msgType == "success_join":
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'success_join',
"to" : to,
"from" : from_
}
)
elif msgType == "chat_message":
await self.save_message(text_data_json["message"])
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'chatroom_message',
'message' : text_data_json["message"],
'to' : from_,
'from' : from_,
"event" : "chat_message"
}
)
elif msgType == "join_request":
if self.room.admin.user_profile.unique_id == self.user.id:
from_ = text_data_json.get("user")
await self.channel_layer.group_send(
self.room_group_name,
{
'type' : 'join_request',
'to' : self.user,
'from' : from_
}
)
async def chatroom_message(self, event):
message = event['message']
await self.send(text_data=json.dumps({
'type' : event["event"],
'message': message,
'to': event["to"],
'from': event['from']
}))
async def offer(self, event):
await self.send(text_data=json.dumps({
'type' : 'offer',
'offer': event['offer'],
'to': event["to"],
'from': event['from']
}))
async def answer(self, event):
await self.send(text_data=json.dumps({
'type' : 'answer',
'answer': event['answer'],
'to': event["to"],
'from': event['from']
}))
async def candidate(self, event):
await self.send(text_data=json.dumps({
'type' : 'candidate',
'candidate': event['candidate'],
'to': event["to"],
'from': event['from']
}))
async def joiner(self, event):
await self.send(text_data=json.dumps({
'type' : 'joiner',
'to': event["to"],
'from': event['from']
}))
async def success_join(self, event):
await self.send(text_data=json.dumps({
'type' : 'success_join',
'to': event["to"],
'from': event['from']
}))
async def join_request(self, event):
if event["to"]== self.user["id"]:
from_ = event.get("user")
await self.send(text_data=json.dumps({
'type' : 'join_request',
'to': self.user,
'from': from_
}))
@database_sync_to_async
def save_message(self, message):
msg = Message(user=self.scope['user'], to=self.room, text=message)
msg.save()
JavaScript
<script>
function updateTime() {
document.getElementById('date-time').innerText = moment(Date.now()).format('Do MMMM, YYYY h:mm a');
}
setInterval(updateTime, 1000);
let myDetails = {
"id": '{{request.user.user_profile.unique_id}}',
"name": '{% if user.first_name %}{{user.first_name}}{% else %}{{user.username}}{% endif %}',
"profile_pic":'{{request.user.user_profile.profile_pic.url}}',
};
let chatSocket;
let onlineList = document.getElementById("participantsList");
let chatList = document.getElementById("messages");
let audioBtn = document.getElementById("audioBtn");
let videoBtn = document.getElementById("videoBtn");
let screenBtn = document.getElementById("screen-btn");
let localVideoStream;
let localScreenStream;
let localStream;
let call_to;
let peerConnection = {};
let remoteOffer;
let numVideos = 0;
let showVideo = true;
let playAudio = true;
let videoDialog = document.getElementById("video-div");
let videoElement;
let waitList = [];
let showScreen = false;
screenBtn.style.color = "red";
function getLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(showPosition);
} else {
myPosition.innerHTML = "Geolocation is not supported by this browser.";
}
}
// document.getElementById("location").style.display = "block";
function showPosition(position) {
myPosition.style.display = "block";
myPosition.innerHTML = "<i class='fas fa-map-marker-alt'></i> " + position.coords.latitude +
", " + position.coords.longitude;
}
let ICEconfig = {
iceServers: [{
urls: ["stun:bn-turn1.xirsys.com"]
},
{
username: "WtJcHNTgNpN90FSvMVmZtWWVztiHEhbFfsdRyMS1f_PoMfbjWJSW_rVXiivC4VCOAAAAAGGZQvNhc2hpc2hzYXNtYWwx",
credential: "6e26a8e4-4a32-11ec-8f0c-0242ac140004",
urls: [
"turn:bn-turn1.xirsys.com:80?transport=udp",
"turn:bn-turn1.xirsys.com:3478?transport=udp",
"turn:bn-turn1.xirsys.com:80?transport=tcp",
"turn:bn-turn1.xirsys.com:3478?transport=tcp",
"turns:bn-turn1.xirsys.com:443?transport=tcp",
"turns:bn-turn1.xirsys.com:5349?transport=tcp"
]
}
]
}
{% if request.user == room.admin %}
const mediaConstraints = {
"video": true,
audio: true
}
{% else %}
const mediaConstraints = {
"video": false,
audio: false
}
{% endif %}
const screenConstraints = {
"video": {
mediaSource: "screen"
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
}
let iceCandidatesList = {};
let conn = [];
function play() {
var audio = new Audio('https://2u039f-a.akamaihd.net/downloads/ringtones/files/mp3/xperia-artic-54206.mp3');
audio.play();
}
function playEnterRoom() {
var audio = new Audio('https://www.setasringtones.com/storage/ringtones/9/aa925f907affb2e0998254d360689a2f.mp3');
audio.play();
}
function accessMedia(user, screen = false) {
console.log("access media " + myDetails.id)
console.log(mediaConstraints)
return navigator.mediaDevices.getUserMedia(mediaConstraints)
.then(stream => {
localVideoStream = stream;
localStream = stream;
muteAudio();
muteVideo();
onVideoAdd(user, localVideoStream);
}).catch(function(error) {
console.log(error)
});
}
async function accessScreen(user) {
console.log("access media " + myDetails.id)
console.log(mediaConstraints)
return navigator.mediaDevices.getDisplayMedia(screenConstraints)
.then(stream => {
localScreenStream = stream;
localStream = stream;
muteAudio(screen = false);
muteVideo(screen = true);
console.log(localScreenStream);
}).catch(function(error) {
console.log(error)
});
}
function connectWebSocket() {
var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
chatSocket = new WebSocket(ws_scheme + '://' + window.location.host + '/ws/chat/{{room.room_id}}/');
console.log("Websocket connected");
chatSocket.onopen = event => {
chatSocket.send(JSON.stringify({
"type": "login",
"to": myDetails,
"from": myDetails
}));
accessMedia(myDetails).then(
bool => {
playEnterRoom();
chatSocket.send(JSON.stringify({
"type": "joiner",
"to": "all",
"from": myDetails,
}));
})
}
chatSocket.onmessage = async (e) => {
const data = JSON.parse(e.data);
console.log(data);
let type = data.type;
let user = data.from;
if (data.to == "all") {
if (type == "online_traffic") {
let action = data.message;
if (action == "remove") {
var elem = document.getElementById(user.id);
if (elem) {
elem.parentNode.removeChild(elem);
}
} else if (action == "add") {
if (!document.getElementById(user.id)){
onlineList.innerHTML +=`
<li id='${user.id}'>
<img src="${user.profile_pic}" alt="${user.name}">
<span class="participant-name">${user.name}</span>
</li>`;
}
}
}
if (type == "joiner" && user.id != myDetails.id) {
iceCandidatesList[user.id] = [];
await call(user);
}
} else if (data.to.id == myDetails.id) {
if (type == "success_join") {
iceCandidatesList[user.id] = [];
call(user);
} else if (type == "offer") {
iceCandidatesList[user.id] = [];
console.log("Receiving call from " + user);
await handleIncomingcall(user, data);
} else if (type == "answer") {
console.log("Call answered");
peerConnection[user.id].pc.setRemoteDescription(data.answer);
} else if (type == "candidate") {
console.log("ICE candidate received");
handleIncomingICEcandidate(user, data);
}
} else {
if (type == "chat_message") {
chatList.innerHTML +=
`<div class="message">
<div class="user-info">
<img
src="${user.profile_pic}"
alt="User">
<span class="user-name">${user.name}</span>
</div>
<div class="message-content">${data.message}</div>
</div>`
}
}
}
}
if (chatSocket) {
chatSocket.onclose = (e) => {
alert("Socket disconnected");
connectWebSocket();
}
}
function sendSocket(data, to, from) {
if (chatSocket) {
data.to = to;
data.from = from;
chatSocket.send(JSON.stringify(
data
));
} else {
console.log("Chat socket not connected")
}
}
connectWebSocket();
function handleIncomingICEcandidate(user, data) {
if (peerConnection[user.id] && data.candidate) {
peerConnection[user.id].pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} else {
console.log("RTC peer connection not set");
iceCandidatesList[user.id].push(data.candidate);
}
}
async function handleIncomingcall(user, data) {
await createRTCPeerConnection(user);
await createAndSendAnswer(user, data.offer);
}
function manageSocket() {
if (activeBtn.checked) {
connectWebSocket();
} else {
if (chatSocket) {
chatSocket.close();
console.log("Socket closed")
} else {
console.log("Socket not connected");
}
}
}
function createAndSendAnswer(user, remoteOffer) {
console.log("hello")
peerConnection[user.id].pc.setRemoteDescription(new RTCSessionDescription(remoteOffer));
peerConnection[user.id].pc.createAnswer((answer) => {
peerConnection[user.id].pc.setLocalDescription(new RTCSessionDescription(answer));
sendSocket({
type: "answer",
answer: answer
}, user, myDetails)
}, error => {
console.log(error);
})
}
async function createAndSendOffer(user) {
console.log("offer true")
peerConnection[user.id].pc.createOffer((offer) => {
console.log("hello")
peerConnection[user.id].pc.setLocalDescription(new RTCSessionDescription(offer));
sendSocket({
type: "offer",
offer: offer
}, user, myDetails)
}, (error) => {
console.log("Error");
});
}
async function call(user) {
await createRTCPeerConnection(user);
await createAndSendOffer(user);
}
async function createRTCPeerConnection(user, offer = false) {
console.log("RTCPeerConnection connected");
peerConnection[user.id] = {
"name": user.name,
"id": user.id,
"pc": new RTCPeerConnection(ICEconfig)
};
// peerConnection = new RTCPeerConnection(null);
if (localVideoStream) {
localVideoStream.getTracks().forEach((track) => {
peerConnection[user.id].pc.addTrack(track, localVideoStream);
});
}
if (offer) {
console.log("Creating Offer");
peerConnection[user.id].pc.onnegotiationneeded = async (event) => createAndSendOffer(user);
}
peerConnection[user.id].pc.onicecandidate = (event) => handleICEcandidate(user, event);
peerConnection[user.id].pc.ontrack = event => handleAddStream(user, event);
return;
}
function handleAddStream(user, event) {
console.log("track received");
let stream = event.streams[0];
onVideoAdd(user, stream);
}
function handleICEcandidate(user, event) {
if (event.candidate == null)
return;
sendSocket({
type: "candidate",
candidate: event.candidate
}, user, myDetails)
}
function muteAudio(screen = false) {
if (localStream) {
playAudio = !playAudio;
if (playAudio) {
const audiobtncolour = document.getElementById('audiobtncolour');
audiobtncolour.style.fill = "#2870de";
} else {
audiobtncolour.style.fill = "red";
}
if (localStream.getAudioTracks()[0])
localStream.getAudioTracks()[0].enabled = playAudio;
}
}
function muteVideo(screen = false) {
if (localStream) {
showVideo = !showVideo;
console.log(showVideo)
if (showVideo) {
if (showScreen){
showScreen = false;
localStream = localVideoStream;
broadcast(localStream);
}
const videobtncolour = document.getElementById('videobtncolour');
videobtncolour.style.fill = "#2870de";
onVideoAdd(myDetails, localVideoStream);
} else {
videobtncolour.style.fill = "red";
}
localStream.getVideoTracks()[0].enabled = showVideo;
} else {
alert("Please allow video permission.");
}
}
function broadcast(stream){
let track = stream.getVideoTracks()[0];
for (let p in peerConnection) {
let sender = peerConnection[p].pc.getSenders ? peerConnection[p].pc.getSenders().find(s => s.track && s.track.kind === track.kind) : false;
if (sender) {
console.log("hello " + track)
sender.replaceTrack(track);
}
}
}
if (localScreenStream){
localScreenStream.getVideoTracks()[0].onended = function () {
screenBtn.style.color = "red";
};
}
async function shareScreen() {
if (showScreen){
localScreenStream = await navigator.mediaDevices.getDisplayMedia();
localStream = localScreenStream;
}
else if (!localScreenStream) {
localScreenStream = await navigator.mediaDevices.getDisplayMedia();
localStream = localScreenStream;
}
muteVideo();
localStream.getVideoTracks()[0].enabled = true;
console.log(peerConnection);
broadcast(localScreenStream);
onVideoAdd(myDetails, localScreenStream);
// senders.find(sender => sender.track.kind === 'video').replaceTrack(localScreenStream.getTracks()[0]);
showScreen = true;
screenBtn.style.color = "#2870de";
}
function replaceStreamTrack(stream) {
for (let p in peerConnection) {
if (p.pc) {
p.pc.getSenders().forEach(function(sender) {
console.log(sender);
stream.getTracks.forEach(function(track) {
if (track == sender.track) {
p.pc.removeTrack(sender);
}
})
});
createAndSendOffer({
"name": pc.name,
"id": pc.id
});
}
}
}
function onVideoAdd(user, stream) {
if (document.getElementById(`vid-${user.id}`)) {
document.getElementById(`vid-${user.id}`).srcObject = stream;
} else {
var vidElement = document.createElement("video");
vidElement.setAttribute('autoplay', '');
vidElement.setAttribute('muted', '');
vidElement.setAttribute('class', 'video-container video-js');
vidElement.setAttribute('id', `vid-${user.id}`);
vidElement.srcObject = stream;
var videoContainer = document.querySelector(".video-container");
videoContainer.innerHTML = ''; // Clear existing content
videoContainer.appendChild(vidElement);
}
if (user.id == myDetails.id) {
document.getElementById(`vid-${user.id}`).volume = 0;
}
}
function sendMessage() {
msg = document.getElementById("inputMsg").value;
if (msg) {
chatList.innerHTML +=
`<div class="message">
<div class="user-info">
<img src="${myDetails.profile_pic}" alt="User">
<span class="user-name">${myDetails.name}</span>
</div>
<div class="message-content">${msg}</div>
</div>`
sendSocket({
"type": "chat_message",
"message": msg
}, "all", myDetails)
}
document.getElementById("inputMsg").value = "";
}
// Store connection information before refresh
function storeConnectionInfo() {
// Store relevant connection information in local storage
const connectionInfo = {
peerConnections: peerConnection,
localStream: localStream,
showVideo: showVideo,
playAudio: playAudio,
showScreen: showScreen,
localScreenStream: localScreenStream
};
localStorage.setItem('connectionInfo', JSON.stringify(connectionInfo));
}
// Event listener for page refresh
window.addEventListener('beforeunload', storeConnectionInfo);
// Event listener for page load
var input = document.getElementById("inputMsg");
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
sendMessage();
}
});
let recorder; // Global variable to store the recorder instance
let isRecording = false;
function toggleRecording() {
const recordIcon = document.getElementById('recordIcon');
if (!isRecording) {
startRecording();
recordIcon.style.fill = 'red'; // Change the fill color to indicate recording
} else {
stopRecording();
recordIcon.style.fill = '#292d32'; // Change the fill color back to default
}
isRecording = !isRecording;
}
function startRecording() {
// Create a media stream that combines both video and audio
const combinedStream = new MediaStream();
localStream.getTracks().forEach(track => combinedStream.addTrack(track));
// Create a recorder instance
recorder = new RecordRTC(combinedStream, {
type: 'video', // or 'audio' for audio-only recording
});
// Start recording
recorder.startRecording();
}
function stopRecording() {
if (recorder) {
// Stop recording
recorder.stopRecording(function() {
const recordedBlob = recorder.getBlob();
// Create a download link for the recorded file
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(recordedBlob);
downloadLink.download = 'recorded-meeting.mp4'; // Change the filename as needed
downloadLink.innerHTML = '<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg"><g style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-width:2"><path d="m36.0851 17.1466v18.8699"/><path d="m42.24 29.8951-6.14 6.14"/><path d="m29.91 29.8951 6.14 6.14"/><path d="m56.8961 54.9782h-41.8193c-1.65 0-3-1.35-3-3v-7.5762c0-1.65 1.35-3 3-3h41.8194c1.65 0 3 1.35 3 3v7.5762c-.0001 1.65-1.3501 3-3.0001 3z"/><circle cx="19.0173" cy="48.2886" r="2"/></g>Download </svg>';
// Add the download link to the page
const downloadContainer = document.getElementById('download-container');
downloadContainer.appendChild(downloadLink);
recorder = null;
});
}
}
</script>
I would appreciate any guidance or code examples on how to modify my implementation to create a new RTCPeerConnection for each remote peer and avoid the mentioned error. Here's the github repo https://github.com/Codewithshagbaor/Extra/tree/main Thank you!