I developed using django channels to implement video chat and message chat.(I was referring to a YouTuber's course.)
When I first completed the development, I noticed that webrtc works only in the local network, and I found out that a stun/turn server was needed.
So, I created a separate EC2 and built the stun/turn server and set this to the RTCPeerconnection in the web server.
Stun/turn server test in Trickle ICE is good.
But My video call still works only within the local network. And even within the same network, the connection was very slow.
Server overall configuration.(Webserver with SSL application loadbalancer)

Coturn config
# /etc/default/coturn
TURNSERVER_ENABLED=1
# /etc/turnserver.conf
# STUN server port is 3478 for UDP and TCP, and 5349 for TLS.
# Allow connection on the UDP port 3478
listening-port=3478
# and 5349 for TLS (secure)
tls-listening-port=5349
# Require authentication
fingerprint
lt-cred-mech
server-name=mysite.com
realm=mysite.com
# Important:
# Create a test user if you want
# You can remove this user after testing
user=myuser:userpassword
total-quota=100
stale-nonce=600
# Path to the SSL certificate and private key. In this example we will use
# the letsencrypt generated certificate files.
cert=/etc/letsencrypt/live/stun.mysite.com/cert.pem
pkey=/etc/letsencrypt/live/stun.mysite.com/privkey.pem
# Specify the allowed OpenSSL cipher list for TLS/DTLS connections
cipher-list="~~~~~-SHA512:~~~~~SHA512:~~~~~-SHA384:~~~~~SHA384:~~~-AES256-SHA384"
# Specify the process user and group
proc-user=turnserver
proc-group=turnserver
main.js in Web Server
let mapPeers = {};
let usernameInput = document.querySelector('#username');
let btnJoin = document.querySelector('#btn-join');
let username;
let webSocket;
const iceConfiguration = {
iceServers: [
{ urls:'stun:stun.mysite.com' },
{
username: 'myuser',
credential: 'userpassword',
urls: 'turn:turn.mysite.com'
},
]
}
function webSocketOnMessage(event) {
let parsedData = JSON.parse(event.data);
let peerUsername = parsedData['peer'];
let action = parsedData['action'];
if (username === peerUsername){
return;
}
let receiver_channel_name = parsedData['message']['receiver_channel_name'];
if (action === 'new-peer'){
createOfferer(peerUsername, receiver_channel_name);
return;
}
if (action === 'new-offer'){
let offer = parsedData['message']['sdp']
createAnswerer(offer, peerUsername, receiver_channel_name);
return;
}
if (action === 'new-answer'){
let answer = parsedData['message']['sdp'];
let peer = mapPeers[peerUsername][0];
peer.setRemoteDescription(answer);
return;
}
// console.log('message : ', message)
}
btnJoin.addEventListener('click', () => {
username = usernameInput.value;
console.log('username : ', username);
if (username === ''){
return;
}
usernameInput.value = '';
usernameInput.disabled = true;
usernameInput.style.visibility = 'hidden';
btnJoin.disabled = true;
btnJoin.style.visibility = 'hidden';
let labelUsername = document.querySelector('#label-username');
labelUsername.innerHTML = username;
let loc = window.location;
let wsStart = 'ws://';
if (loc.protocol === 'https:'){
wsStart = 'wss://';
}
let endpoint = wsStart + loc.host + loc.pathname + 'ws/';
console.log(loc.host)
console.log(loc.pathname)
console.log('endpoint: ', endpoint);
webSocket = new WebSocket(endpoint);
console.log('--------', webSocket)
webSocket.addEventListener('open', (e) => {
console.log('Connection opened!');
sendSignal('new-peer', {});
});
webSocket.addEventListener('message', webSocketOnMessage);
webSocket.addEventListener('close', (e) => {
console.log('Connection closed!', e)
});
})
// Media
let localStream = new MediaStream();
const constraints = {
'video': true,
'audio': true
}
const localVideo = document.querySelector('#local-video');
const btnToggleAudio = document.querySelector('#btn-toggle-audio');
const btnToggleVideo = document.querySelector('#btn-toggle-video');
let userMedia = navigator.mediaDevices.getUserMedia(constraints)
.then(stream => {
localStream = stream;
localVideo.srcObject = localStream;
localVideo.muted = true;
let audioTracks = stream.getAudioTracks();
let videoTracks = stream.getVideoTracks();
audioTracks[0].enabled = true;
videoTracks[0].enabled = true;
btnToggleAudio.addEventListener('click', () => {
audioTracks[0].enabled = !audioTracks[0].enabled;
if (audioTracks[0].enabled){
btnToggleAudio.innerHTML = 'Audio mute'
return;
}
btnToggleAudio.innerHTML = 'Audio unmute'
});
btnToggleVideo.addEventListener('click', () => {
videoTracks[0].enabled = !videoTracks[0].enabled;
if (videoTracks[0].enabled){
btnToggleVideo.innerHTML = 'Video off'
return;
}
btnToggleVideo.innerHTML = 'Video on'
});
})
.catch(error => {
console.log('Error accessing media devices', error);
});
// Message
let btnSendMsg = document.querySelector('#btn-send-msd');
let messageList = document.querySelector('#message-list');
let messageInput = document.querySelector('#msg');
btnSendMsg.addEventListener('click', sendMsgOnclick);
function sendMsgOnclick() {
let message = messageInput.value;
let li = document.createElement('li');
li.appendChild(document.createTextNode('Me: ' + message));
messageList.appendChild(li);
let dataChannels = getDataChannels();
message = username + ': ' + message;
console.log("---------console.log(dataChannels)----------")
console.log(dataChannels)
for (index in dataChannels){
console.log("---------console.log(index)----------")
console.log(index)
dataChannels[index].send(message);
}
messageInput.value = '';
}
function sendSignal(action, message){
let jsonStr = JSON.stringify({
'peer': username,
'action': action,
"message": message,
});
webSocket.send(jsonStr);
}
function createOfferer(peerUsername, receiver_channel_name) {
let peer = new RTCPeerConnection(iceConfiguration);
console.log('=================================')
console.log(peer)
console.log('=================================')
addLocalTracks(peer);
let dc = peer.createDataChannel('channel');
dc.addEventListener('open', () => {
console.log('connection opened!')
})
dc.addEventListener('message', dcOnMessage);
let remoteVideo = createVideo(peerUsername);
setOnTrack(peer, remoteVideo);
mapPeers[peerUsername] = [peer, dc];
peer.addEventListener('iceconnectionstatechange', () => {
let iceConnectionState = peer.iceConnectionState;
if (iceConnectionState === 'failed' || iceConnectionState === 'disconnected' || iceConnectionState === 'closed'){
delete mapPeers[peerUsername];
if (iceConnectionState !== 'closed'){
peer.close();
}
removeVideo(remoteVideo);
}
})
peer.addEventListener('icecandidate', (event) => {
if (event.candidate){
console.log('new ice candidate', JSON.stringify(peer.localDescription))
return;
}
sendSignal('new-offer', {
'sdp': peer.localDescription,
'receiver_channel_name': receiver_channel_name
});
});
peer.createOffer()
.then(o => peer.setLocalDescription(o))
.then(() => {
console.log('Local description set successfully!');
});
}
function createAnswerer(offer, peerUsername, receiver_channel_name) {
let peer = new RTCPeerConnection(iceConfiguration);
console.log('=================================')
console.log(peer)
console.log('=================================')
// let peer = new RTCPeerConnection(null);
addLocalTracks(peer);
let remoteVideo = createVideo(peerUsername);
setOnTrack(peer, remoteVideo);
peer.addEventListener('datachannel', e => {
peer.dc = e.channel;
peer.dc.addEventListener('open', () => {
console.log('connection opened!')
})
peer.dc.addEventListener('message', dcOnMessage);
mapPeers[peerUsername] = [peer, peer.dc];
});
peer.addEventListener('iceconnectionstatechange', () => {
let iceConnectionState = peer.iceConnectionState;
if (iceConnectionState === 'failed' || iceConnectionState === 'disconnected' || iceConnectionState === 'closed'){
delete mapPeers[peerUsername];
if (iceConnectionState !== 'closed'){
peer.close();
}
removeVideo(remoteVideo);
}
})
peer.addEventListener('icecandidate', (event) => {
if (event.candidate){
console.log('new ice candidate', JSON.stringify(peer.localDescription))
return;
}
sendSignal('new-answer', {
'sdp': peer.localDescription,
'receiver_channel_name': receiver_channel_name
});
});
peer.setRemoteDescription(offer)
.then(() => {
console.log('Remote description set successfully for %s.', peerUsername);
return peer.createAnswer();
})
.then(a => {
console.log('Answer created!')
peer.setLocalDescription(a);
})
// peer.createOffer()
// .then(o => peer.setLocalDescription(o))
// .then(() => {
// console.log('Local description set successfully!');
// });
}
function addLocalTracks(peer) {
localStream.getTracks().forEach(track => {
peer.addTrack(track, localStream);
return;
});
}
function dcOnMessage(event) {
let message = event.data;
let li = document.createElement('li')
li.appendChild(document.createTextNode(message));
messageList.appendChild(li);
}
function createVideo(peerUsername) {
let videoContainer = document.querySelector('#video-container');
let remoteVideo = document.createElement('video');
remoteVideo.id = peerUsername + '-video';
remoteVideo.autoplay = true;
remoteVideo.playsInline = true;
let videoWrapper = document.createElement('div');
videoContainer.appendChild(videoWrapper);
videoWrapper.appendChild(remoteVideo);
return remoteVideo;
}
function setOnTrack(peer, remoteVideo) {
let remoteStream = new MediaStream();
remoteVideo.srcObject = remoteStream;
peer.addEventListener('track', async (event) => {
remoteStream.addTrack(event.track, remoteStream);
});
}
function removeVideo(video) {
let videoWrapper = video.parentNode;
videoWrapper.parentNode.removeChild(videoWrapper);
}
function getDataChannels() {
let dataChannels = []
for (peerUsername in mapPeers){
let dataChannel = mapPeers[peerUsername][1];
dataChannels.push(dataChannel);
}
return dataChannels;
}
django channels code in webserver
# Consumer.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
print('connect!!')
self.room_group_name = 'test_room'
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
print(f"room_group_name : {self.room_group_name} and channel_name : {self.channel_name}")
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
print('disconnected!')
async def receive(self, text_data):
receive_dict = json.loads(text_data)
print(f"receive_data : {receive_dict}")
message = receive_dict['message']
action = receive_dict['action']
if (action == 'new-offer') or (action == 'new-answer'):
receiver_channel_name = receive_dict['message']['receiver_channel_name']
receive_dict['message']['receiver_channel_name'] = self.channel_name
await self.channel_layer.send(
receiver_channel_name,
{
'type': 'send.sdp',
'receive_dict': receive_dict
}
)
return
receive_dict['message']['receiver_channel_name'] = self.channel_name
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'send.sdp',
'receive_dict': receive_dict
}
)
async def send_sdp(self, event):
print('send_sdp!!')
receive_dict = event['receive_dict']
await self.send(text_data=json.dumps(receive_dict))
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Project.settings')
django_asgi_app = get_asgi_application()
from channels.auth import AuthMiddlewareStack
import video_app.routing
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(
URLRouter(
video_app.routing.websocket_urlpatterns
)
),
})
#routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"^ws/$", consumers.ChatConsumer.as_asgi()),
]
# settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("myredis.cache.amazonaws.com", 6379)]
},
},
}
What is my fault?