├── chat ├── __init__.py ├── migrations │ └── __init__.py ├── apps.py ├── context_processors.py ├── routing.py ├── urls.py ├── views.py ├── templates │ └── chat │ │ ├── index.html │ │ └── room.html └── consumers.py ├── videocall ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── apps.py ├── views.py ├── urls.py ├── routing.py ├── templates │ └── videocall │ │ └── video_call.html ├── consumers.py └── static │ └── videocall │ └── js │ └── video_call.js ├── django_video ├── __init__.py ├── asgi.py ├── wsgi.py ├── routing.py ├── urls.py └── settings.py ├── main.py ├── .gitignore ├── app.yaml ├── requirements.txt ├── manage.py ├── .gcloudignore ├── LICENSE.md └── Readme.md /chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /videocall/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_video/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /videocall/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from django_video.wsgi import application 2 | 3 | app = application 4 | -------------------------------------------------------------------------------- /videocall/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /videocall/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /videocall/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | name = 'chat' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /static 3 | 4 | .idea/ 5 | 6 | __pycache__/ 7 | 8 | migrations 9 | 10 | *.sqlite3 11 | 12 | 13 | -------------------------------------------------------------------------------- /videocall/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VideocallConfig(AppConfig): 5 | name = 'videocall' 6 | -------------------------------------------------------------------------------- /chat/context_processors.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def redishost_processor(request): 5 | return {'redishost': os.environ.get('REDISHOST', '127.0.0.1:8000')} 6 | -------------------------------------------------------------------------------- /videocall/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class VideoCall(TemplateView): 5 | template_name = 'videocall/video_call.html' 6 | -------------------------------------------------------------------------------- /videocall/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from videocall.views import * 4 | 5 | urlpatterns = [ 6 | path('video_call/', VideoCall.as_view(), name='video_call'), 7 | ] 8 | -------------------------------------------------------------------------------- /chat/routing.py: -------------------------------------------------------------------------------- 1 | # chat/routing.py 2 | from django.urls import re_path 3 | 4 | from . import consumers 5 | 6 | websocket_urlpatterns = [ 7 | re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer), 8 | ] -------------------------------------------------------------------------------- /chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from chat.views import * 4 | 5 | urlpatterns = [ 6 | path('', IndexView.as_view(), name='index'), 7 | path('/',RoomView.as_view(),name='room'), 8 | ] 9 | -------------------------------------------------------------------------------- /videocall/routing.py: -------------------------------------------------------------------------------- 1 | # chat/routing.py 2 | from django.urls import path 3 | 4 | from videocall.consumers import * 5 | 6 | websocket_urlpatterns = [ 7 | path(r'ws/video_call/signal/', VideoCallSignalConsumer), 8 | ] 9 | -------------------------------------------------------------------------------- /django_video/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import os 7 | 8 | import django 9 | from channels.routing import get_default_application 10 | 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_video.settings") 12 | django.setup() 13 | application = get_default_application() 14 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python 2 | env: flex 3 | entrypoint: daphne django_video.asgi:application --port $PORT --bind 0.0.0.0 4 | 5 | 6 | runtime_config: 7 | python_version: 3 8 | 9 | automatic_scaling: 10 | min_num_instances: 1 11 | max_num_instances: 1 12 | 13 | 14 | # Update with Redis instance details 15 | env_variables: 16 | REDISHOST: '10.25.45.251' 17 | REDISPORT: '6379' 18 | 19 | # Update with Redis instance network name 20 | network: 21 | name: default 22 | 23 | 24 | -------------------------------------------------------------------------------- /django_video/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_video project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_video.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /django_video/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | import chat.routing 4 | import videocall.routing 5 | 6 | application = ProtocolTypeRouter({ 7 | # (http->django views is added by default) 8 | 'websocket': AuthMiddlewareStack( 9 | URLRouter( 10 | chat.routing.websocket_urlpatterns + 11 | videocall.routing.websocket_urlpatterns 12 | ) 13 | ), 14 | }) 15 | -------------------------------------------------------------------------------- /chat/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic import TemplateView 3 | 4 | 5 | class IndexView(TemplateView): 6 | template_name = 'chat/index.html' 7 | 8 | 9 | class RoomView(TemplateView): 10 | template_name = 'chat/room.html' 11 | 12 | def get_context_data(self, **kwargs): 13 | context = super(RoomView, self).get_context_data(**kwargs) 14 | context['room_name'] = self.request.GET.get('room_name') 15 | return context 16 | 17 | 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.3.1 2 | asgiref==3.2.10 3 | async-timeout==3.0.1 4 | attrs==20.2.0 5 | autobahn==20.7.1 6 | Automat==20.2.0 7 | cffi==1.14.3 8 | channels==2.4.0 9 | channels-redis==3.1.0 10 | constantly==15.1.0 11 | cryptography==3.1.1 12 | daphne==2.5.0 13 | Django==3.1.1 14 | hiredis==1.1.0 15 | hyperlink==20.0.1 16 | idna==2.10 17 | incremental==17.5.0 18 | msgpack==1.0.0 19 | pyasn1==0.4.8 20 | pyasn1-modules==0.2.8 21 | pycparser==2.20 22 | PyHamcrest==2.0.2 23 | pyOpenSSL==19.1.0 24 | pytz==2020.1 25 | service-identity==18.1.0 26 | six==1.15.0 27 | sqlparse==0.3.1 28 | Twisted==20.3.0 29 | txaio==20.4.1 30 | zope.interface==5.1.0 31 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_video.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # This file specifies files that are *not* uploaded to Google Cloud Platform 4 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 5 | # "#!include" directives (which insert the entries of the given .gitignore-style 6 | # file at that point). 7 | # 8 | # For more information, run: 9 | # $ gcloud topic gcloudignore 10 | # 11 | .gcloudignore 12 | # If you would like to upload your .git directory, .gitignore file or files 13 | # from your .gitignore file, remove the corresponding line 14 | # below: 15 | .git 16 | .gitignore 17 | 18 | # Python pycache: 19 | __pycache__/ 20 | # Ignored by the build system 21 | /setup.cfg 22 | 23 | .idea 24 | /myvenv 25 | /README.md 26 | bootstrap-4.4.1-dist 27 | scss 28 | *.scss 29 | *.css.map 30 | db.sqlite3 31 | /fixtures 32 | /static 33 | -------------------------------------------------------------------------------- /django_video/urls.py: -------------------------------------------------------------------------------- 1 | """django_video URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('chat/', include('chat.urls')), 22 | path('video_call/',include('videocall.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /chat/templates/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Rooms 7 | 8 | 9 | What chat room would you like to enter?
10 |
11 | 12 | 13 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020 Aseem 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /videocall/templates/videocall/video_call.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | Video calling 8 | 11 | 12 | 13 |

VIdeo shown

14 |
15 | 16 |
17 |
18 | Video: 19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |
Message output:
28 |
Signaling Message:
29 |
30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /chat/consumers.py: -------------------------------------------------------------------------------- 1 | # chat/consumers.py 2 | import json 3 | 4 | from channels.generic.websocket import AsyncWebsocketConsumer 5 | 6 | 7 | class ChatConsumer(AsyncWebsocketConsumer): 8 | async def connect(self): 9 | self.room_name = self.scope['url_route']['kwargs']['room_name'] 10 | self.room_group_name = 'chat_%s' % self.room_name 11 | 12 | # Join room group 13 | await self.channel_layer.group_add( 14 | self.room_group_name, 15 | self.channel_name 16 | ) 17 | 18 | await self.accept() 19 | 20 | async def disconnect(self, close_code): 21 | # Leave room group 22 | await self.channel_layer.group_discard( 23 | self.room_group_name, 24 | self.channel_name 25 | ) 26 | 27 | # Receive message from WebSocket 28 | async def receive(self, text_data=None, bytes_data=None): 29 | text_data_json = json.loads(text_data) 30 | message = text_data_json['message'] 31 | 32 | # Send message to room group 33 | await self.channel_layer.group_send( 34 | self.room_group_name, 35 | { 36 | 'type': 'chat_message', 37 | 'message': message 38 | } 39 | ) 40 | 41 | # Receive message from room group 42 | async def chat_message(self, event): 43 | message = event['message'] 44 | 45 | # Send message to WebSocket 46 | await self.send(text_data=json.dumps({ 47 | 'message': message 48 | })) 49 | -------------------------------------------------------------------------------- /chat/templates/chat/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Room 7 | 10 | 11 | 12 |
13 |
14 | 15 | {{ room_name|json_script:"room-name" }} 16 | 60 | 61 | -------------------------------------------------------------------------------- /videocall/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from channels.generic.websocket import AsyncWebsocketConsumer 4 | 5 | 6 | class VideoCallSignalConsumer(AsyncWebsocketConsumer): 7 | def __init__(self, *args, **kwargs): 8 | super(VideoCallSignalConsumer, self).__init__(*args, **kwargs) 9 | 10 | async def connect(self): 11 | """ 12 | join user to a general group and accept the connection 13 | """ 14 | print('Signal connect') 15 | self.room_group_name = 'general_group' 16 | # join room group 17 | await self.channel_layer.group_add( 18 | self.room_group_name, 19 | self.channel_name 20 | ) 21 | await self.accept() 22 | 23 | async def disconnect(self, code): 24 | print('Signal disconnect') 25 | await self.channel_layer.group_discard( 26 | self.room_group_name, 27 | self.channel_name 28 | ) 29 | 30 | async def receive(self, text_data=None, bytes_data=None): 31 | """ 32 | its called when UI websocket sends(). So its called only once 33 | irrespective of number of users in a group 34 | """ 35 | print(' Signal receive') 36 | text_data_json = json.loads(text_data) 37 | 38 | # Send message to room group. its high level app-to-app communication 39 | await self.channel_layer.group_send( 40 | self.room_group_name, 41 | { 42 | # func that will receive this data and send data to socket 43 | 'type': 'signal_message', 44 | 'data': text_data_json, 45 | 'sender_channel_name': self.channel_name 46 | } 47 | ) 48 | 49 | async def signal_message(self, event): 50 | """ 51 | its not called directly from UI websocket. Its called from 52 | django receive() func. 53 | if 2 users (each user has a unique channel_name) in a group, 54 | this func will be called 2 times. 55 | """ 56 | data = event['data'] 57 | 58 | # Send message to all channels except parent channel 59 | if self.channel_name != event['sender_channel_name']: 60 | print('channel name != ') 61 | await self.send(text_data=json.dumps({ 62 | 'type': data['type'], 63 | 'message': data['message'] 64 | })) 65 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Video call and chat in Django

4 |
5 | Using Django, Django-channels, js, js - websockets, Daphne ASGI server ( built specially for django channels), 6 | redis server (installed locally using docker or on GCP using redis memory cache service) 7 |
8 | 9 |

Deploying this app on google app engine flex

10 | 11 | * We dont use/need any sql db. This app has default sqlite db in settings.py. 12 | 13 | * Clone this repository to your local machine. cd into this repo- `cd django_video/` 14 | 15 | * create a python virtual env `python3.8 -m venv venv` 16 | 17 | * Install all dependencies required: `pip install -r requirements.txt` 18 | 19 | * Create a GCP (google cloud platform lol) project. 20 | 21 | * Deploy a test app on gcp app engine flex to get an idea - 22 | Link 23 | 24 | * You can ignore the cloud sql creation step as we dont need sql in our app. 25 | 26 | * In settings.py update STATIC_URL with your gcp public bucket name that you created 27 | when deploying test app to GAE flex 28 | 29 | * Create a redis instance on default VPC network in same gcp project - 30 | Link 31 | * This Redis instance should be in same region as app engine flex. 32 | 33 | * Perhaps every gcp project comes with a default VPC network, hence we choose default and dont have to create 34 | another VPC. 35 | 36 | * In app.yaml file update env variables - REDISHOST,REDISPORT from your redis instance. 37 | 38 | 39 |

You can run this project locally through your venv that you created above. I run it on Ubuntu. 40 | if you are using windows, god save you.

41 | 42 | * we will need a redis server locally: Hoping you have docker installed on your machine. 43 | if not then install it on your own. 44 | * `docker run -p 6379:6379 -d redis:5` 45 | * `daphne django_video.asgi:application` 46 | * Once you are done using this app, you can stop the redis container: 47 | 48 | * `sudo systemctl restart docker.socket docker.service` 49 | 50 | * `docker ps` -- shows which containers are running 51 | 52 | * `docker rm ` -- stop the container 53 | 54 |

Deploy your project to google app engine flex:

55 | 56 | * `gcloud app deploy` -- call this from root dir that contains app.yaml file. 57 | 58 | * If this is your first time deploying app to GAE, you will be asked to select a region and app engine type. 59 | * Pick the same region as your redis instance. 60 | * Pick flexible app engine option 61 | 62 | GAE Flex deployment takes lot of time compared to GAE standard deployment, but GAE standard doesnt support websockets 63 | (which are required for peer to peer connections for video and chat functionality.) 64 | 65 | Video calling doesnt work on localhost if your VPN is on. -------------------------------------------------------------------------------- /django_video/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_video project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | import os 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '3pd87(#wgihdf09234kj(09slkwner2398sad##$$(*&asd7yn23,fpwieuh928xjch!' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'chat', 34 | 'videocall', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | 'channels', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'django_video.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 61 | , 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | 70 | 'chat.context_processors.redishost_processor' 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'django_video.wsgi.application' 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': BASE_DIR / 'db.sqlite3', 85 | } 86 | } 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 121 | 122 | STATIC_URL = 'https://storage.googleapis.com/static-public-bucket/static/' 123 | STATIC_ROOT = 'static' 124 | 125 | ASGI_APPLICATION = "django_video.routing.application" 126 | CHANNEL_LAYERS = { 127 | 'default': { 128 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 129 | 'CONFIG': { 130 | "hosts": [( 131 | # '127.0.0.1', 6379 132 | os.environ.get('REDISHOST', '127.0.0.1'), 133 | int(os.environ.get('REDISPORT', 6379)) 134 | )], 135 | }, 136 | }, 137 | } 138 | -------------------------------------------------------------------------------- /videocall/static/videocall/js/video_call.js: -------------------------------------------------------------------------------- 1 | // Frontend io we have { io.on as input, io.emit as op) 2 | let width = 250; 3 | let height = 250; 4 | var myVideoArea = document.querySelector('#idMyVideoTag'); 5 | var theirVideoArea = document.querySelector('#idTheirVideoTag'); 6 | 7 | var cameraSelect = document.querySelector('#camera'); 8 | var devices = navigator.mediaDevices.enumerateDevices(); 9 | 10 | var canvas = document.querySelector('#idCanvas'); 11 | var profileImage = document.querySelector('#idImage'); 12 | var takePicButton = document.querySelector('#idTakePictureBtn'); 13 | 14 | var myName = document.querySelector('#idMyName'); 15 | var myMessage = document.querySelector('#idMessage'); 16 | var sendMessage = document.querySelector('#idSendMessage'); 17 | var chatArea = document.querySelector('#idChatArea'); 18 | var ROOM = "chat"; 19 | var SIGNAL_ROOM = 'SIGNAL_ROOM'; 20 | 21 | var websocketProtocol; 22 | console.log('redishost=', redishost); 23 | console.log('host = ', window.location.host); 24 | console.log('protocol=', location.protocol); 25 | if (location.protocol === 'http:') { 26 | websocketProtocol = 'ws'; 27 | } else { 28 | websocketProtocol = 'wss'; 29 | } 30 | 31 | var websocketBaseUrl = websocketProtocol + '://' + window.location.host + '/ws/'; 32 | 33 | // using test stun server 34 | var configuration = { 35 | 'iceServers': [{ 36 | 'url': 'stun:stun1.l.google.com:19302' //'stun:stun.l.google.com:19302' 37 | }] 38 | }; 39 | var rtcPeerConn; 40 | 41 | var signalingArea = document.querySelector('#idSignalingArea'); 42 | 43 | takePicButton.addEventListener('click', function () { 44 | console.log('take picture'); 45 | takeProfilePic(); 46 | }); 47 | 48 | getCameras(); 49 | 50 | // WebSocket -> .send, .onmessage, .onclose 51 | 52 | 53 | const videoSignalSocket = new WebSocket( 54 | websocketBaseUrl + 'video_call/signal/' 55 | ); 56 | 57 | videoSignalSocket.onmessage = function (e) { 58 | const data = JSON.parse(e.data); 59 | 60 | displaySignalMessage('Signal received:' + data.type); 61 | 62 | // setup RTC peer connection object 63 | if (!rtcPeerConn) { 64 | console.log('rtc Peer conn doesnt exists yet'); 65 | startSignaling(); 66 | } 67 | 68 | // we are sending some bogus signal on load with type='user_here'. we call below 69 | // code only for real signal message 70 | if (data.type != 'user_here') { 71 | console.log('data type != user_here'); 72 | var message = JSON.parse(data.message); // parse json from message 73 | 74 | // sdp message means remote party made us an offer 75 | if (message.sdp) { 76 | rtcPeerConn.setRemoteDescription( 77 | new RTCSessionDescription(message.sdp), function () { 78 | // if we received an offer, we need to answer 79 | if (rtcPeerConn.remoteDescription.type == 'offer') { 80 | rtcPeerConn.createAnswer(sendLocalDesc, logError); 81 | } 82 | }, logError); 83 | } else { 84 | rtcPeerConn.addIceCandidate(new RTCIceCandidate(message.candidate)); 85 | } 86 | } 87 | 88 | }; 89 | 90 | videoSignalSocket.onclose = function (e) { 91 | console.error('video signal socket closed unexpectedly'); 92 | }; 93 | 94 | // send ready state signal 95 | // this one also should only be told to other user, not both user 96 | var videoSignalSocketReady = setInterval(function () { 97 | // keep checking if socket is ready at certain intervals 98 | // once ready, send a signal and exit loop 99 | console.log('ready state=', videoSignalSocket.readyState); 100 | if (videoSignalSocket.readyState === 1) { 101 | videoSignalSocket.send(JSON.stringify({ 102 | 'type': 'user_here', 103 | 'message': 'Are you ready for a call?', 104 | 'room': SIGNAL_ROOM 105 | })); 106 | clearInterval(videoSignalSocketReady); 107 | } 108 | 109 | }, 1000); 110 | 111 | function displaySignalMessage(message) { 112 | signalingArea.innerHTML = signalingArea.innerHTML + "
" + message; 113 | } 114 | 115 | function startSignaling() { 116 | console.log('startSignaling'); 117 | displaySignalMessage('starting signaling...'); 118 | rtcPeerConn = new RTCPeerConnection(configuration); 119 | 120 | // send any ice candidates to other peer 121 | rtcPeerConn.onicecandidate = function (evt) { 122 | if (evt.candidate) { 123 | videoSignalSocket.send(JSON.stringify({ 124 | 'type': 'ice candidate', 125 | 'message': JSON.stringify({ 126 | 'candidate': evt.candidate 127 | }), 128 | 'room': SIGNAL_ROOM 129 | })); 130 | } 131 | displaySignalMessage('completed that ice candidate...'); 132 | } 133 | 134 | // when we receive an offer, we return our offer 135 | // let the 'negotiationneeded' event trigger offer generation 136 | rtcPeerConn.onnegotiationneeded = function () { 137 | displaySignalMessage(' on negotiation called'); 138 | rtcPeerConn.createOffer(sendLocalDesc, logError); 139 | } 140 | 141 | // once remote stream arrives, show it in remote video element 142 | rtcPeerConn.onaddstream = function (evt) { 143 | displaySignalMessage('going to add their stream...'); 144 | theirVideoArea.srcObject = evt.stream; 145 | } 146 | 147 | // get a local stream, show it in our video tag and add it to be sent 148 | startStream(); 149 | 150 | } 151 | 152 | // new 153 | function startStream() { 154 | console.log('startStream'); 155 | let constraints = { 156 | video: { 157 | width: width, 158 | height: height, 159 | deviceId: cameraSelect.value // if wrong device id, then it goes with default 160 | }, 161 | audio: true 162 | }; 163 | navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { 164 | console.log('Stream connected successfully'); 165 | myVideoArea.srcObject = stream; 166 | rtcPeerConn.addStream(stream); // this triggers event our peer needs to get our stream 167 | }).catch(function (error) { 168 | console.log('error in stream:', error); 169 | }); 170 | } 171 | 172 | function sendLocalDesc(desc) { 173 | rtcPeerConn.setLocalDescription(desc, function () { 174 | displaySignalMessage('sending local description'); 175 | videoSignalSocket.send(JSON.stringify({ 176 | 'type': 'SDP', 177 | 'message': JSON.stringify({ 178 | 'sdp': rtcPeerConn.localDescription 179 | }), 180 | 'room': SIGNAL_ROOM 181 | })); 182 | }, logError); 183 | } 184 | 185 | function logError(error) { 186 | displaySignalMessage(error.name + ':' + error.message); 187 | } 188 | 189 | function getCameras() { 190 | if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { 191 | console.log("enumerateDevices() not supported."); 192 | return; 193 | } 194 | 195 | // List cameras and microphones. 196 | navigator.mediaDevices.enumerateDevices() 197 | .then(function (devices) { 198 | devices.forEach(function (device) { 199 | console.log(device.kind + ": " + device.label + 200 | " id = " + device.deviceId); 201 | 202 | // add different camera options to a select tag 203 | if (device.kind === 'videoinput') { 204 | let option = document.createElement('option'); 205 | option.value = device.deviceId; 206 | option.text = device.label; 207 | cameraSelect.append(option); 208 | } 209 | }); 210 | 211 | }) 212 | .catch(function (err) { 213 | console.log(err.name + ": " + err.message); 214 | }); 215 | } 216 | 217 | 218 | function takeProfilePic() { 219 | var context = canvas.getContext('2d'); 220 | 221 | canvas.width = width; 222 | canvas.height = height; 223 | context.drawImage(myVideoArea, 0, 0, width, height); 224 | 225 | var data = canvas.toDataURL('image/png'); 226 | profileImage.setAttribute('src', data); 227 | } 228 | 229 | 230 | --------------------------------------------------------------------------------