我在使用 Django Channels 在 Django 中实现 WebRTC 时遇到问题。我面临的错误是:
“未捕获(承诺中)DOMException:无法在‘RTCPeerConnection’上执行‘setRemoteDescription’:无法设置远程应答 sdp:在错误状态下调用:稳定”
经过一番研究,我在 Stack Overflow 上找到了这个错误的可能解释。这似乎与以下事实有关:当第三个用户加入时,它会向之前连接的两个用户发送报价,从而产生两个答案。由于单个 RTCPeerConnection 只能建立一个点对点连接,因此尝试在第二个答案上设置RemoteDescription 会失败。
建议的解决方案是为每个远程对等点实例化一个新的 RTCPeerConnection。但是,我不确定如何在现有代码中实现这一点。下面是我的consumers.py 和JavaScript 代码的相关部分。
consumers.py 和 JavaScript 代码
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>
我将不胜感激任何有关如何修改我的实现以便为每个远程对等点创建新的 RTCPeerConnection 并避免提到的错误的指导或代码示例。这是 github 存储库https://github.com/Codewithshagbaor/Extra/tree/main谢谢!
嘿!!!呵呵!!!你好!!!!!哈啰!!!!!!