skip to Main Content

I have tried to make a simple video calling app using Django and WebRTC. I tried running this on localhost and it works well, but I tried hosting it on a website hosting platform and now it doesn’t work. I surfed through many websites to correct this, but failed. I haven’t used Redis server.

Here are some required files:

asgi.py

import os

from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
import chat.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myProject.settings')

application = ProtocolTypeRouter({
    "https": get_asgi_application(),
    # Just HTTP for now. (We can add other protocols later.)
    "websocket": AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    )
})

settings.py


import os
import django_heroku

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '****'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['https://myProject.herokuapp.com/']


# Application definition

INSTALLED_APPS = [
    'channels',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'chat',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myProject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'myProject.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/

STATIC_URL = '/static/'

STATIC_ROOT = '/home/myProject/static/'


ASGI_APPLICATION = 'myProject.asgi.application'

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [('https://myProject.herokuapp.com/'),
         ('redis://<my_secret_password_given_by_redislabs>@<my_redislabs_endpoint>')],
        },
    },
}
django_heroku.settings(locals())

This is my main.js file that consists code for VideoCalling functionality.

console.log('in main.js')

var ICE_config = {
  'iceServers': [
    {
      'url': 'stun:stun.l.google.com:19302'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=udp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=tcp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    }
  ]
}

var mapPeers = {};
var usernameInput = document.querySelector('#username');
var btnJoin = document.querySelector('#btn-join');

var username;
var webSocket;

function webSocketOnMessage(event)
{
    var parsedData = JSON.parse(event.data);
    var peerUsername = parsedData['peer'];
    var action = parsedData['action'];

    if(username == peerUsername)
    {
        return;
    }
    var receiver_channel_name = parsedData['message']['receiver_channel_name'];

    if(action == 'new-peer'){
        createOfferer(peerUsername, receiver_channel_name);
        return;
    }

    if(action == 'new-offer'){
        var offer = parsedData['message']['sdp'];

        createAnswerer(offer, peerUsername, receiver_channel_name);
        return;
    }

    if(action == 'new-answer'){
        var answer = parsedData['message']['sdp'];

        var peer = mapPeers[peerUsername][0];

        peer.setRemoteDescription(answer);

        return;
    }

}

btnJoin.addEventListener('click', () =>{
    username= usernameInput.value;

    console.log(username);

    if(username =='')
    {
        return;
    }
    usernameInput.value='';
    usernameInput.disabled=true;
    usernameInput.style.visibility = 'hidden';

    btnJoin.disabled=true;
    btnJoin.style.visibility = 'hidden';

    var labelUsername = document.querySelector('#label-username');
    labelUsername.innerHTML = username;

    var loc = window.location;
    var wsStart = 'ws://';

    if(loc.protocol == 'https:')
    {
        wsStart = 'wss://';
    }

    var endpoint = wsStart + loc.host + loc.pathname;

    console.log(endpoint);

line 95 --> webSocket = new WebSocket(endpoint);

    webSocket.addEventListener('open', (e) =>{
        console.log('Connection opened');

       sendSignal('new-peer',{});
    webSocket.addEventListener('message', webSocketOnMessage);
    webSocket.addEventListener('close', (e) =>{
        console.log('Connection closed');
    });
    webSocket.addEventListener('error', (e) =>{
        console.log('Error occurred');
    });
});
});


var 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');

var userMedia = navigator.mediaDevices.getUserMedia(constraints)
    .then(stream => {
        localStream = stream;
        localVideo.srcObject = localStream;
        localVideo.muted = true;

        var audioTracks = stream.getAudioTracks();
        var 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 = '<i class="fas fa-microphone" style="font-size:20px;"></i>';
                return;
            }
            btnToggleAudio.innerHTML = '<i class="fas fa-microphone-slash" style="font-size:20px;"></i>';
        });
        btnToggleVideo.addEventListener('click', () =>{
            videoTracks[0].enabled = !videoTracks[0].enabled;

            if(videoTracks[0].enabled){
                btnToggleVideo.innerHTML = '<i class="fas fa-video" style="font-size:20px;"></i>';
                return;
            }
            btnToggleVideo.innerHTML = '<i class="fas fa-video-slash" style="font-size:20px;"></i>';
        });

    })
    .catch(error =>{
        console.log('Error accessing media devices!', error);
    });
    var btnSendMsg = document.querySelector('#btn-send-msg');
    var messageList = document.querySelector('#message-list');
    var messageInput = document.querySelector('#msg');
    btnSendMsg.addEventListener('click', sendMsgOnClick);

function sendMsgOnClick(){
    var message = messageInput.value;
    var li = document.createElement('li');
    li.appendChild(document.createTextNode('Me: '+ message));
    messageList.append(li);

    var dataChannels = getDataChannels();

    message = username + ': ' + message;

    for(index in dataChannels){
        dataChannels[index].send(message);
    }
    messageInput.value = '';
}

function sendSignal(action, message)
{

     var jsonStr = JSON.stringify({
           'peer': username,
           'action': action,
           'message': message,
         });
     webSocket.send(jsonStr);
}

function createOfferer(peerUsername, receiver_channel_name)
{
    
    var peer = new RTCPeerConnection(ICE_config);
    addLocalTracks(peer);

    var dc = peer.createDataChannel('channel');
    dc.addEventListener('open',() =>{
        console.log('Connection opened!');
    });
    dc.addEventListener('message', dcOnMessage);

    var remoteVideo = createVideo(peerUsername);
    setOnTrack(peer, remoteVideo);

    mapPeers[peerUsername] = [peer,dc];

    peer.addEventListener('iceconnectionstatechange', ()=>{
        var 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 addLocalTracks(peer){
    localStream.getTracks().forEach(track => {
        peer.addTrack(track, localStream);
    });
    return;
}


function dcOnMessage(event){
    var message = event.data;

    var li = document.createElement('li');
    li.appendChild(document.createTextNode(message));
    messageList.appendChild(li);
}


function createAnswerer(offer, peerUsername, receiver_channel_name)
{
    var peer = new RTCPeerConnection(ICE_config);
    addLocalTracks(peer);

    var 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', () => {
        var 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);
        });
}


function createVideo(peerUsername){
    var videoContainer = document.querySelector('#video-container');
    var remoteVideo = document.createElement('video');
    remoteVideo.id = peerUsername + '-video';
    remoteVideo.autoplay = true;
    remoteVideo.playsInline = true;

    var videoWrapper = document.createElement('div');
    videoContainer.appendChild(videoWrapper);
    videoWrapper.appendChild(remoteVideo);
    return remoteVideo;
}

function setOnTrack(peer, remoteVideo){
    var remoteStream = new MediaStream()

    remoteVideo.srcObject = remoteStream;
    peer.addEventListener('track', async(event) =>{
        remoteStream.addTrack(event.track, remoteStream);
    });
}

function removeVideo(video){
    var videoWrapper = video.parentNode;
    videoWrapper.parentNode.removeChild(videoWrapper);
}

function getDataChannels(){
    var dataChannels =[];

    for(peerUsername in mapPeers){
        var dataChannel = mapPeers[peerUsername][1];

        dataChannels.push(dataChannel);
    }

    return dataChannels;
}

I’m also attaching main.html for reference:

<!DOCTYPE html>

{% load static %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>TClone</title>
    <link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
</head>
<body>

        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                  <ul class="navbar-nav mr-auto">
                    <li>
                        <div style="float:right;" class="nav-link">
                            <button class="bg-dark" id="btn-toggle-audio"><i class="fas fa-microphone" style="font-size:20px;"></i></button>
                            <button class="bg-dark" id="btn-toggle-video"><i class="fas fa-video" style="font-size:20px;"></i></button>
                            <button class="bg-dark" id="main" onclick="openNav()"><i class="far fa-comments" style="font-size:20px;"></i></button>
                        </div>
                    </li>
                  </ul>

                </div>
        </nav>
<h3 id="label-username">USERNAME</h3>
<div>
    <input id="username"><button id="btn-join" class="bg-dark">Join Room</button>
</div>
<div>
    <div id="video-container">

        <div>
            <video id="local-video" style="float:left;" poster="{% static 'images/profile.png' %}" controls  width="590px" height="480px" autoplay playsinline></video>
        </div>

    </div>
        <div class="sidenav bg-dark" id="mySidebar">
            <h3 style="color:white">CHAT</h3>
             <button class="closebtn bg-dark"">×</button>

            <div id="messages">
                <ul id="message-list" class="navbar-nav mr-auto"></ul>
            </div>
            <div>
                <input id="msg"><button id="btn-send-msg" class="bg-dark">Send Message</button>
            </div>
            <button id="btn-share-screen" class="bg-dark">Share screen</button>
        </div>
</div>

<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

EDIT:

consumers.py

import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer


class ChatConsumer(AsyncJsonWebsocketConsumer):

    async def connect(self):
        self.room_group_name = 'Test-Room'

        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )

        await self.accept()

    async def disconnect(self, 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)
        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):
        receive_dict = event['receive_dict']

        await self.send(text_data=json.dumps(receive_dict))

Also adding Procfile.

web: gunicorn myProject.wsgi:application --log-file - --log-level debug
python manage.py collectstatic --noinput
python manage.py migrate
worker: python manage.py runworker channel_layer
web: python myProject/manage.py runserver 0.0.0.0:$PORT

requirements.txt

django

django-channels

gunicorn

django_heroku

And when I run this app, I get this error:

in main.js line 95:WebSocket connection to ‘wss://(name of website).com/’ failed:

I tried checking the security of the website hosting service, and it is https. Now I am really NOT able to identify where my error is and I really need help in correcting this. Thanks in advance!

2

Answers


  1. The problem lies in your Procfile. Gunicorn is a WSGI HTTP Server. It doesn’t support ASGI. This is what you have to do.

    But first things first, in requirements.txt that’s not how you’re supposed to add packages. Create a virtual environment(if you haven’t already) with either by pipenv, virtualenv or if you don’t want to download additional packages, then you can use venv. (Note: There is no major difference among the three mentioned packages.).

    In your working directory, run this on the root level of your django-channels project: python -m venv venv. Then activate the venv:

    venvScriptsactivate – if you are on Windows

    source venv/bin/activate – if you are on Mac or Linux.

    Then install django, channels, channels_redis with pip.

    Now run: pip freeze > requirements.txt. This will add all the required packages correctly.

    Now, in your Procfile delete everything and add these lines:

    web: daphne <your_project>.asgi:application --port $PORT --bind 0.0.0.0 -v2
    worker: python manage.py runworker channels --settings=<your_project>.settings -v2
    

    Deploy again with these settings.

    EDIT:

    Change your Procfile to this:

    release: python manage.py migrate
    web: daphne <your_project>.asgi:application --port $PORT --bind 0.0.0.0 -v2
    worker: python manage.py runworker channels --settings=<your_project>.settings -v2
    

    And I see that you have changed your Redis configuration to this:

    "CONFIG": {
                "hosts": [('https://myProject.herokuapp.com/'), // <--- I don't think this line is necessary, so change it back to what it was
             ('redis://<my_secret_password_given_by_redislabs>@<my_redislabs_endpoint>')],
            },
    
    Login or Signup to reply.
  2. You have to use daphne.

    https://github.com/django/daphne

    I will try to show you the minimum configuration.

    app
    ├── app
    │   ├── asgi.py
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── db.sqlite3
    ├── main
    │   ├── admin.py
    │   ├── apps.py
    │   ├── __init__.py
    │   ├── migrations
    │   ├── models.py
    │   ├── routing.py
    │   ├── consumers.py
    │   ├── tests.py
    │   ├── urls.py
    │   ├── views.py
    │   └── templates
    │       │
    │       └── chat
    │            └── room.html
    ├── manage.py
    

    asgi.py

    import os
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
    import django
    django.setup()
    
    from channels.auth import AuthMiddlewareStack
    from channels.routing import ProtocolTypeRouter, URLRouter
    from django.core.asgi import get_asgi_application
    import main.routing
    
    application = ProtocolTypeRouter({
        "http": get_asgi_application(),
        "websocket": AuthMiddlewareStack(
            URLRouter(
                main.routing.websocket_urlpatterns
            )
        ),
    })
    

    routing.py

    from django.urls import re_path
    import main.consumers
    websocket_urlpatterns = [
        re_path(r"ws/chat/(?P<root_name>w+)/$", main.consumers.ChatConsumer.as_asgi()),
    ]
    

    consumers.py

    from channels.generic.websocket import AsyncWebsocketConsumer
    from channels.db import database_sync_to_async
    class ChatConsumer(AsyncWebsocketConsumer): 
        async def connect(self):
            self.room_name  = self.scope["url_route"]["kwargs"]["room_name"]
            self.room_group_name = "chat_%s" % self.room_name
            await self.channel_layer.group_add(
                self.room_group_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
            )
    
        async def receive(self, text_data):
            print("receive")
    

    settings.py

    ASGI_APPLICATION = 'main.asgi.application'
    CHANNEL_LAYERS = {
        'default': {
            'BACKEND': 'channels_redis.core.RedisChannelLayer',
            'CONFIG': {
                "hosts": [('127.0.0.1', 6379)],
            },
        },
    }
    

    views.py

    def room(request, room_name):
        return render(request, "chat/room.html", {"room_name": room_name})
    

    room.html

    {{ room_name|json_script:"roomName" }}
    <script>
        roomName = JSON.parse(document.getElementById("roomName").textContent);
            chatSocket = new WebSocket(
                "ws://"
                + window.location.host
                + "/ws/chat/"
                + roomName
                + "/"
            );
    </script>
    

    Run

    ~/app$ daphne app.asgi:application
    

    Open

    http://127.0.0.1:8000/room_1/
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search