├── .gitignore ├── README.md ├── mysite ├── chat │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── consumers.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── templates │ │ ├── chat │ │ │ ├── peer.html │ │ │ ├── peer1.html │ │ │ └── peer2.html │ │ ├── main.html │ │ └── trial_main.html │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── manage.py ├── mysite │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── static │ ├── css │ └── main.css │ └── js │ ├── peer.js │ ├── peer1.js │ └── peer2.js └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | db.sqlite3 3 | __pycache__/ 4 | .env 5 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CAUTION: THIS README NEEDS TO BE UPDATED! THIS REPOSITORY IS STILL UNDER DEVELOPMENT AND TESTING! SOME ISSUES STILL NEED TO BE RESOLVED! 2 | 3 | Description: This project was made for learning how to signal WebRTC SDPs using Javascript WebSocket and django-channels to make multi-peer video conferencing systems. 4 | 5 | Installation: Go to your desired folder. 6 | 7 | Run the command: git clone https://github.com/Tauhid-UAP/django-channels-webrtc.git 8 | 9 | Go to the directory with requirements.txt. 10 | 11 | Run the command: python -m venv venv 12 | 13 | After a venv directory is created, 14 | run the command for windows: venv\Scripts\activate.bat 15 | run the command for Unix or MacOS: source venv/bin/activate 16 | 17 | Ensure latest version of pip by running: python -m pip install --upgrade pip 18 | 19 | Install the dependencies by running the command: pip install -r requirements.txt 20 | 21 | We need multiple devices in the same LAN for testing. For that we need to make our localhost public. 22 | For that, download ngrok from https://ngrok.com/download and install it. 23 | 24 | Usage: 25 | From the directory where we have installed venv, go to the mysite directory by running the command: cd mysite 26 | 27 | To start the development server, run the command: python manage.py runserver 28 | 29 | For testing on multiple devices in the same LAN, go to the directory where you have installed ngrok. 30 | Run the command: ngrok.exe http 8000 31 | This will make our localhost public and provide two public URLs. 32 | However, make sure to always use the one that starts with https: and not http: as we will be accessing media devices. 33 | 34 | On local device, go to http://127.0.0.1:8000/ 35 | On other devices, go to the URL from ngrok that starts with https:. 36 | 37 | Once the page is loaded, type a username and click join room from each device. Be sure to use different usernames for now. 38 | 39 | If remote video does not play, click the button that says "Click to play remote video" as some browsers require user gesture to play video. -------------------------------------------------------------------------------- /mysite/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tauhid-UAP/django-channels-webrtc/1c61dfa3bf7523033798290d693f0182a6ff213e/mysite/chat/__init__.py -------------------------------------------------------------------------------- /mysite/chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /mysite/chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | name = 'chat' 6 | -------------------------------------------------------------------------------- /mysite/chat/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from channels.generic.websocket import AsyncWebsocketConsumer 3 | 4 | import asyncio 5 | 6 | class ChatConsumer(AsyncWebsocketConsumer): 7 | async def connect(self): 8 | 9 | self.room_group_name = 'Test-Room' 10 | 11 | await self.channel_layer.group_add( 12 | self.room_group_name, 13 | self.channel_name 14 | ) 15 | 16 | await self.accept() 17 | 18 | async def disconnect(self, close_code): 19 | 20 | await self.channel_layer.group_discard( 21 | self.room_group_name, 22 | self.channel_name 23 | ) 24 | 25 | print('Disconnected!') 26 | 27 | 28 | # Receive message from WebSocket 29 | async def receive(self, text_data): 30 | receive_dict = json.loads(text_data) 31 | peer_username = receive_dict['peer'] 32 | action = receive_dict['action'] 33 | message = receive_dict['message'] 34 | 35 | # print('unanswered_offers: ', self.unanswered_offers) 36 | 37 | print('Message received: ', message) 38 | 39 | print('peer_username: ', peer_username) 40 | print('action: ', action) 41 | print('self.channel_name: ', self.channel_name) 42 | 43 | if(action == 'new-offer') or (action =='new-answer'): 44 | # in case its a new offer or answer 45 | # send it to the new peer or initial offerer respectively 46 | 47 | receiver_channel_name = receive_dict['message']['receiver_channel_name'] 48 | 49 | print('Sending to ', receiver_channel_name) 50 | 51 | # set new receiver as the current sender 52 | receive_dict['message']['receiver_channel_name'] = self.channel_name 53 | 54 | await self.channel_layer.send( 55 | receiver_channel_name, 56 | { 57 | 'type': 'send.sdp', 58 | 'receive_dict': receive_dict, 59 | } 60 | ) 61 | 62 | return 63 | 64 | # set new receiver as the current sender 65 | # so that some messages can be sent 66 | # to this channel specifically 67 | receive_dict['message']['receiver_channel_name'] = self.channel_name 68 | 69 | # send to all peers 70 | await self.channel_layer.group_send( 71 | self.room_group_name, 72 | { 73 | 'type': 'send.sdp', 74 | 'receive_dict': receive_dict, 75 | } 76 | ) 77 | 78 | async def send_sdp(self, event): 79 | receive_dict = event['receive_dict'] 80 | 81 | this_peer = receive_dict['peer'] 82 | action = receive_dict['action'] 83 | message = receive_dict['message'] 84 | 85 | await self.send(text_data=json.dumps({ 86 | 'peer': this_peer, 87 | 'action': action, 88 | 'message': message, 89 | })) -------------------------------------------------------------------------------- /mysite/chat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tauhid-UAP/django-channels-webrtc/1c61dfa3bf7523033798290d693f0182a6ff213e/mysite/chat/migrations/__init__.py -------------------------------------------------------------------------------- /mysite/chat/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /mysite/chat/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import consumers 4 | 5 | websocket_urlpatterns = [ 6 | re_path(r'peer[12]/', consumers.ChatConsumer.as_asgi()), 7 | re_path(r'', consumers.ChatConsumer.as_asgi()), 8 | ] -------------------------------------------------------------------------------- /mysite/chat/templates/chat/peer.html: -------------------------------------------------------------------------------- 1 | {% extends 'main.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block content %} 6 | 10 | 11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /mysite/chat/templates/chat/peer1.html: -------------------------------------------------------------------------------- 1 | {% extends 'trial_main.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 | 10 | {% endblock %} -------------------------------------------------------------------------------- /mysite/chat/templates/chat/peer2.html: -------------------------------------------------------------------------------- 1 | {% extends 'trial_main.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block content %} 6 | 7 | {% endblock %} -------------------------------------------------------------------------------- /mysite/chat/templates/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load static %} 4 | 5 | 6 | 7 | 8 | Django Channels WebRTC 9 | 10 | 11 | 12 |

USERNAME

13 |
14 | 15 |
16 |
17 | 19 |
20 |
21 | 22 | 23 |
24 |
25 |

CHAT

26 |
27 |
    28 |
    29 |
    30 | 31 | 32 | 33 | 34 | 35 | 47 |
    48 |
    49 | {% block content %} 50 | {% endblock %} 51 | 52 | -------------------------------------------------------------------------------- /mysite/chat/templates/trial_main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load static %} 4 | 5 | 6 | 7 | 8 | Django Channels WebRTC 9 | 10 | 11 | 12 |

    USERNAME

    13 |
    14 | 15 |
    16 |
    17 | 19 |
    20 |
    21 |
    22 | 23 | 24 |
    25 |
    26 |
    27 |

    CHAT

    28 |
    29 |
      30 |
      31 |
      32 | 33 |
      34 |
      35 | {% block content %} 36 | {% endblock %} 37 | 38 | -------------------------------------------------------------------------------- /mysite/chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /mysite/chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import peer1, peer2, peer 3 | 4 | urlpatterns = [ 5 | path('', peer, name='peer'), 6 | path('peer1/', peer1, name='peer1'), 7 | path('peer2/', peer2, name='peer2'), 8 | ] -------------------------------------------------------------------------------- /mysite/chat/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | # returns numb turn credential and username 4 | def get_turn_info(): 5 | return { 6 | 'numb_turn_credential': settings.NUMB_TURN_CREDENTIAL, 7 | 'numb_turn_username': settings.NUMB_TURN_USERNAME, 8 | } -------------------------------------------------------------------------------- /mysite/chat/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from .utils import get_turn_info 4 | 5 | # Create your views here. 6 | 7 | def peer1(request): 8 | # get numb turn info 9 | context = get_turn_info() 10 | 11 | return render(request, 'chat/peer1.html', context=context) 12 | 13 | def peer2(request): 14 | # get numb turn info 15 | context = get_turn_info() 16 | 17 | return render(request, 'chat/peer2.html', context=context) 18 | 19 | def peer(request): 20 | # get numb turn info 21 | context = get_turn_info() 22 | print('context: ', context) 23 | 24 | return render(request, 'chat/peer.html', context=context) -------------------------------------------------------------------------------- /mysite/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', 'mysite.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 | -------------------------------------------------------------------------------- /mysite/mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tauhid-UAP/django-channels-webrtc/1c61dfa3bf7523033798290d693f0182a6ff213e/mysite/mysite/__init__.py -------------------------------------------------------------------------------- /mysite/mysite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for mysite project. 3 | 4 | It exposes the ASGI 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/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from channels.routing import ProtocolTypeRouter, URLRouter 13 | from django.core.asgi import get_asgi_application 14 | from channels.auth import AuthMiddlewareStack 15 | import chat.routing 16 | 17 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 18 | 19 | application = ProtocolTypeRouter({ 20 | "http": get_asgi_application(), 21 | # Just HTTP for now. (We can add other protocols later.) 22 | "websocket": AuthMiddlewareStack( 23 | URLRouter( 24 | chat.routing.websocket_urlpatterns 25 | ) 26 | ), 27 | }) -------------------------------------------------------------------------------- /mysite/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.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 | 15 | import os 16 | 17 | from decouple import config 18 | 19 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 20 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 21 | 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = config('SECRET_KEY') 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | 32 | ALLOWED_HOSTS = ['*'] 33 | 34 | NUMB_TURN_CREDENTIAL = config('NUMB_TURN_CREDENTIAL', default=None) 35 | NUMB_TURN_USERNAME = config('NUMB_TURN_USERNAME', default=None) 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 47 | 'chat.apps.ChatConfig', 48 | 49 | 'channels', 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | 'django.middleware.security.SecurityMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'mysite.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'mysite.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.sqlite3', 89 | 'NAME': BASE_DIR / 'db.sqlite3', 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | 132 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] 133 | 134 | # Channels 135 | ASGI_APPLICATION = 'mysite.asgi.application' 136 | 137 | CHANNEL_LAYERS = { 138 | "default": { 139 | "BACKEND": "channels.layers.InMemoryChannelLayer" 140 | } 141 | } 142 | 143 | # CHANNEL_LAYERS = { 144 | # 'default': { 145 | # 'BACKEND': 'channels_redis.core.RedisChannelLayer', 146 | # 'CONFIG': { 147 | # "hosts": [('127.0.0.1', 6379)], 148 | # }, 149 | # }, 150 | # } -------------------------------------------------------------------------------- /mysite/mysite/urls.py: -------------------------------------------------------------------------------- 1 | """mysite 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('', include('chat.urls')), 22 | ] 23 | -------------------------------------------------------------------------------- /mysite/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite 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', 'mysite.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /mysite/static/css/main.css: -------------------------------------------------------------------------------- 1 | .video-grid-container { 2 | display: grid; 3 | grid-template-columns: repeat(3, 1fr); 4 | background-color: black; 5 | } 6 | 7 | .main-grid-container { 8 | display: grid; 9 | grid-template-columns: 60% 20%; 10 | } 11 | 12 | video { 13 | border-radius: 25%; 14 | background-color: black; 15 | } -------------------------------------------------------------------------------- /mysite/static/js/peer.js: -------------------------------------------------------------------------------- 1 | // map peer usernames to corresponding RTCPeerConnections 2 | // as key value pairs 3 | var mapPeers = {}; 4 | 5 | // peers that stream own screen 6 | // to remote peers 7 | var mapScreenPeers = {}; 8 | 9 | // true if screen is being shared 10 | // false otherwise 11 | var screenShared = false; 12 | 13 | const localVideo = document.querySelector('#local-video'); 14 | 15 | // button to start or stop screen sharing 16 | var btnShareScreen = document.querySelector('#btn-share-screen'); 17 | 18 | // local video stream 19 | var localStream = new MediaStream(); 20 | 21 | // local screen stream 22 | // for screen sharing 23 | var localDisplayStream = new MediaStream(); 24 | 25 | // buttons to toggle self audio and video 26 | btnToggleAudio = document.querySelector("#btn-toggle-audio"); 27 | btnToggleVideo = document.querySelector("#btn-toggle-video"); 28 | 29 | var messageInput = document.querySelector('#msg'); 30 | var btnSendMsg = document.querySelector('#btn-send-msg'); 31 | 32 | // button to start or stop screen recording 33 | var btnRecordScreen = document.querySelector('#btn-record-screen'); 34 | // object that will start or stop screen recording 35 | var recorder; 36 | // true of currently recording, false otherwise 37 | var recording = false; 38 | 39 | var file; 40 | 41 | document.getElementById('share-file-button').addEventListener('click', () => { 42 | document.getElementById('select-file-dialog').style.display = 'block'; 43 | }); 44 | 45 | document.getElementById('cancel-button').addEventListener('click', () => { 46 | document.getElementById('select-file-input').value = ''; 47 | document.getElementById('select-file-dialog').style.display = 'none'; 48 | document.getElementById('ok-button').disabled = true; 49 | }); 50 | 51 | document.getElementById('select-file-input').addEventListener('change', (event) => { 52 | file = event.target.files[0]; 53 | document.getElementById('ok-button').disabled = !file; 54 | }); 55 | 56 | // ul of messages 57 | var ul = document.querySelector("#message-list"); 58 | 59 | var loc = window.location; 60 | 61 | var endPoint = ''; 62 | var wsStart = 'ws://'; 63 | 64 | if(loc.protocol == 'https:'){ 65 | wsStart = 'wss://'; 66 | } 67 | 68 | var endPoint = wsStart + loc.host + loc.pathname; 69 | 70 | var webSocket; 71 | 72 | var usernameInput = document.querySelector('#username'); 73 | var username; 74 | 75 | var btnJoin = document.querySelector('#btn-join'); 76 | 77 | // set username 78 | // join room (initiate websocket connection) 79 | // upon button click 80 | btnJoin.onclick = () => { 81 | username = usernameInput.value; 82 | 83 | if(username == ''){ 84 | // ignore if username is empty 85 | return; 86 | } 87 | 88 | // clear input 89 | usernameInput.value = ''; 90 | // disable and vanish input 91 | btnJoin.disabled = true; 92 | usernameInput.style.visibility = 'hidden'; 93 | // disable and vanish join button 94 | btnJoin.disabled = true; 95 | btnJoin.style.visibility = 'hidden'; 96 | 97 | document.querySelector('#label-username').innerHTML = username; 98 | 99 | webSocket = new WebSocket(endPoint); 100 | 101 | webSocket.onopen = function(e){ 102 | console.log('Connection opened! ', e); 103 | 104 | // notify other peers 105 | sendSignal('new-peer', { 106 | 'local_screen_sharing': false, 107 | }); 108 | } 109 | 110 | webSocket.onmessage = webSocketOnMessage; 111 | 112 | webSocket.onclose = function(e){ 113 | console.log('Connection closed! ', e); 114 | } 115 | 116 | webSocket.onerror = function(e){ 117 | console.log('Error occured! ', e); 118 | } 119 | 120 | btnSendMsg.disabled = false; 121 | messageInput.disabled = false; 122 | } 123 | 124 | function webSocketOnMessage(event){ 125 | var parsedData = JSON.parse(event.data); 126 | 127 | var action = parsedData['action']; 128 | // username of other peer 129 | var peerUsername = parsedData['peer']; 130 | 131 | console.log('peerUsername: ', peerUsername); 132 | console.log('action: ', action); 133 | 134 | if(peerUsername == username){ 135 | // ignore all messages from oneself 136 | return; 137 | } 138 | 139 | // boolean value specified by other peer 140 | // indicates whether the other peer is sharing screen 141 | var remoteScreenSharing = parsedData['message']['local_screen_sharing']; 142 | console.log('remoteScreenSharing: ', remoteScreenSharing); 143 | 144 | // channel name of the sender of this message 145 | // used to send messages back to that sender 146 | // hence, receiver_channel_name 147 | var receiver_channel_name = parsedData['message']['receiver_channel_name']; 148 | console.log('receiver_channel_name: ', receiver_channel_name); 149 | 150 | // in case of new peer 151 | if(action == 'new-peer'){ 152 | console.log('New peer: ', peerUsername); 153 | 154 | // create new RTCPeerConnection 155 | createOfferer(peerUsername, false, remoteScreenSharing, receiver_channel_name); 156 | 157 | if(screenShared && !remoteScreenSharing){ 158 | // if local screen is being shared 159 | // and remote peer is not sharing screen 160 | // send offer from screen sharing peer 161 | console.log('Creating screen sharing offer.'); 162 | createOfferer(peerUsername, true, remoteScreenSharing, receiver_channel_name); 163 | } 164 | 165 | return; 166 | } 167 | 168 | // remote_screen_sharing from the remote peer 169 | // will be local screen sharing info for this peer 170 | var localScreenSharing = parsedData['message']['remote_screen_sharing']; 171 | 172 | if(action == 'new-offer'){ 173 | console.log('Got new offer from ', peerUsername); 174 | 175 | // create new RTCPeerConnection 176 | // set offer as remote description 177 | var offer = parsedData['message']['sdp']; 178 | console.log('Offer: ', offer); 179 | var peer = createAnswerer(offer, peerUsername, localScreenSharing, remoteScreenSharing, receiver_channel_name); 180 | 181 | return; 182 | } 183 | 184 | 185 | if(action == 'new-answer'){ 186 | // in case of answer to previous offer 187 | // get the corresponding RTCPeerConnection 188 | var peer = null; 189 | 190 | if(remoteScreenSharing){ 191 | // if answerer is screen sharer 192 | peer = mapPeers[peerUsername + ' Screen'][0]; 193 | }else if(localScreenSharing){ 194 | // if offerer was screen sharer 195 | peer = mapScreenPeers[peerUsername][0]; 196 | }else{ 197 | // if both are non-screen sharers 198 | peer = mapPeers[peerUsername][0]; 199 | } 200 | 201 | // get the answer 202 | var answer = parsedData['message']['sdp']; 203 | 204 | console.log('mapPeers:'); 205 | for(key in mapPeers){ 206 | console.log(key, ': ', mapPeers[key]); 207 | } 208 | 209 | console.log('peer: ', peer); 210 | console.log('answer: ', answer); 211 | 212 | // set remote description of the RTCPeerConnection 213 | peer.setRemoteDescription(answer); 214 | 215 | return; 216 | } 217 | } 218 | 219 | messageInput.addEventListener('keyup', function(event){ 220 | if(event.keyCode == 13){ 221 | // prevent from putting 'Enter' as input 222 | event.preventDefault(); 223 | 224 | // click send message button 225 | btnSendMsg.click(); 226 | } 227 | }); 228 | 229 | btnSendMsg.onclick = btnSendMsgOnClick; 230 | 231 | function btnSendMsgOnClick(){ 232 | var message = messageInput.value; 233 | 234 | var li = document.createElement("li"); 235 | li.appendChild(document.createTextNode("Me: " + message)); 236 | ul.appendChild(li); 237 | 238 | var dataChannels = getDataChannels(); 239 | 240 | console.log('Sending: ', message); 241 | 242 | // send to all data channels 243 | for(index in dataChannels){ 244 | dataChannels[index].send(username + ': ' + message); 245 | } 246 | 247 | messageInput.value = ''; 248 | } 249 | 250 | const constraints = { 251 | 'video': true, 252 | 'audio': true 253 | } 254 | 255 | // const iceConfiguration = { 256 | // iceServers: [ 257 | // { 258 | // urls: ['turn:numb.viagenie.ca'], 259 | // credential: numbTurnCredential, 260 | // username: numbTurnUsername 261 | // } 262 | // ] 263 | // }; 264 | 265 | userMedia = navigator.mediaDevices.getUserMedia(constraints) 266 | .then(stream => { 267 | localStream = stream; 268 | console.log('Got MediaStream:', stream); 269 | var mediaTracks = stream.getTracks(); 270 | 271 | for(i=0; i < mediaTracks.length; i++){ 272 | console.log(mediaTracks[i]); 273 | } 274 | 275 | localVideo.srcObject = localStream; 276 | localVideo.muted = true; 277 | 278 | window.stream = stream; // make variable available to browser console 279 | 280 | audioTracks = stream.getAudioTracks(); 281 | videoTracks = stream.getVideoTracks(); 282 | 283 | // unmute audio and video by default 284 | audioTracks[0].enabled = true; 285 | videoTracks[0].enabled = true; 286 | 287 | btnToggleAudio.onclick = function(){ 288 | audioTracks[0].enabled = !audioTracks[0].enabled; 289 | if(audioTracks[0].enabled){ 290 | btnToggleAudio.innerHTML = 'Audio Mute'; 291 | return; 292 | } 293 | 294 | btnToggleAudio.innerHTML = 'Audio Unmute'; 295 | }; 296 | 297 | btnToggleVideo.onclick = function(){ 298 | videoTracks[0].enabled = !videoTracks[0].enabled; 299 | if(videoTracks[0].enabled){ 300 | btnToggleVideo.innerHTML = 'Video Off'; 301 | return; 302 | } 303 | 304 | btnToggleVideo.innerHTML = 'Video On'; 305 | }; 306 | }) 307 | .then(e => { 308 | btnShareScreen.onclick = event => { 309 | if(screenShared){ 310 | // toggle screenShared 311 | screenShared = !screenShared; 312 | 313 | // set to own video 314 | // if screen already shared 315 | localVideo.srcObject = localStream; 316 | btnShareScreen.innerHTML = 'Share screen'; 317 | 318 | // get screen sharing video element 319 | var localScreen = document.querySelector('#my-screen-video'); 320 | // remove it 321 | removeVideo(localScreen); 322 | 323 | // close all screen share peer connections 324 | var screenPeers = getPeers(mapScreenPeers); 325 | for(index in screenPeers){ 326 | screenPeers[index].close(); 327 | } 328 | // empty the screen sharing peer storage object 329 | mapScreenPeers = {}; 330 | 331 | return; 332 | } 333 | 334 | // toggle screenShared 335 | screenShared = !screenShared; 336 | 337 | navigator.mediaDevices.getDisplayMedia(constraints) 338 | .then(stream => { 339 | localDisplayStream = stream; 340 | 341 | var mediaTracks = stream.getTracks(); 342 | for(i=0; i < mediaTracks.length; i++){ 343 | console.log(mediaTracks[i]); 344 | } 345 | 346 | var localScreen = createVideo('my-screen'); 347 | // set to display stream 348 | // if screen not shared 349 | localScreen.srcObject = localDisplayStream; 350 | 351 | // notify other peers 352 | // of screen sharing peer 353 | sendSignal('new-peer', { 354 | 'local_screen_sharing': true, 355 | }); 356 | }) 357 | .catch(error => { 358 | console.log('Error accessing display media.', error); 359 | }); 360 | 361 | btnShareScreen.innerHTML = 'Stop sharing'; 362 | } 363 | }) 364 | .then(e => { 365 | btnRecordScreen.addEventListener('click', () => { 366 | if(recording){ 367 | // toggle recording 368 | recording = !recording; 369 | 370 | btnRecordScreen.innerHTML = 'Record Screen'; 371 | 372 | recorder.stopRecording(function() { 373 | var blob = recorder.getBlob(); 374 | invokeSaveAsDialog(blob); 375 | }); 376 | 377 | return; 378 | } 379 | 380 | // toggle recording 381 | recording = !recording; 382 | 383 | navigator.mediaDevices.getDisplayMedia(constraints) 384 | .then(stream => { 385 | recorder = RecordRTC(stream, { 386 | type: 'video', 387 | MimeType: 'video/mp4' 388 | }); 389 | recorder.startRecording(); 390 | 391 | var mediaTracks = stream.getTracks(); 392 | for(i=0; i < mediaTracks.length; i++){ 393 | console.log(mediaTracks[i]); 394 | } 395 | 396 | }) 397 | .catch(error => { 398 | console.log('Error accessing display media.', error); 399 | }); 400 | 401 | btnRecordScreen.innerHTML = 'Stop Recording'; 402 | }); 403 | }) 404 | .catch(error => { 405 | console.error('Error accessing media devices.', error); 406 | }); 407 | 408 | // send the given action and message 409 | // over the websocket connection 410 | function sendSignal(action, message){ 411 | webSocket.send( 412 | JSON.stringify( 413 | { 414 | 'peer': username, 415 | 'action': action, 416 | 'message': message, 417 | } 418 | ) 419 | ) 420 | } 421 | 422 | // create RTCPeerConnection as offerer 423 | // and store it and its datachannel 424 | // send sdp to remote peer after gathering is complete 425 | function createOfferer(peerUsername, localScreenSharing, remoteScreenSharing, receiver_channel_name){ 426 | var peer = new RTCPeerConnection(null); 427 | 428 | // add local user media stream tracks 429 | addLocalTracks(peer, localScreenSharing); 430 | 431 | // create and manage an RTCDataChannel 432 | var dc = peer.createDataChannel("channel"); 433 | dc.onopen = () => { 434 | console.log("Connection opened."); 435 | }; 436 | var remoteVideo = null; 437 | if(!localScreenSharing && !remoteScreenSharing){ 438 | // none of the peers are sharing screen (normal operation) 439 | 440 | dc.onmessage = dcOnMessage; 441 | 442 | remoteVideo = createVideo(peerUsername); 443 | setOnTrack(peer, remoteVideo); 444 | console.log('Remote video source: ', remoteVideo.srcObject); 445 | 446 | // store the RTCPeerConnection 447 | // and the corresponding RTCDataChannel 448 | mapPeers[peerUsername] = [peer, dc]; 449 | 450 | peer.oniceconnectionstatechange = () => { 451 | var iceConnectionState = peer.iceConnectionState; 452 | if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ 453 | console.log('Deleting peer'); 454 | delete mapPeers[peerUsername]; 455 | if(iceConnectionState != 'closed'){ 456 | peer.close(); 457 | } 458 | removeVideo(remoteVideo); 459 | } 460 | }; 461 | }else if(!localScreenSharing && remoteScreenSharing){ 462 | // answerer is screen sharing 463 | 464 | dc.onmessage = (e) => { 465 | console.log('New message from %s\'s screen: ', peerUsername, e.data); 466 | }; 467 | 468 | remoteVideo = createVideo(peerUsername + '-screen'); 469 | setOnTrack(peer, remoteVideo); 470 | console.log('Remote video source: ', remoteVideo.srcObject); 471 | 472 | // if offer is not for screen sharing peer 473 | mapPeers[peerUsername + ' Screen'] = [peer, dc]; 474 | 475 | peer.oniceconnectionstatechange = () => { 476 | var iceConnectionState = peer.iceConnectionState; 477 | if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ 478 | delete mapPeers[peerUsername + ' Screen']; 479 | if(iceConnectionState != 'closed'){ 480 | peer.close(); 481 | } 482 | removeVideo(remoteVideo); 483 | } 484 | }; 485 | }else{ 486 | // offerer itself is sharing screen 487 | 488 | dc.onmessage = (e) => { 489 | console.log('New message from %s: ', peerUsername, e.data); 490 | }; 491 | 492 | mapScreenPeers[peerUsername] = [peer, dc]; 493 | 494 | peer.oniceconnectionstatechange = () => { 495 | var iceConnectionState = peer.iceConnectionState; 496 | if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ 497 | delete mapScreenPeers[peerUsername]; 498 | if(iceConnectionState != 'closed'){ 499 | peer.close(); 500 | } 501 | } 502 | }; 503 | } 504 | 505 | peer.onicecandidate = (event) => { 506 | if(event.candidate){ 507 | console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer.localDescription)); 508 | return; 509 | } 510 | 511 | // event.candidate == null indicates that gathering is complete 512 | 513 | console.log('Gathering finished! Sending offer SDP to ', peerUsername, '.'); 514 | console.log('receiverChannelName: ', receiver_channel_name); 515 | 516 | // send offer to new peer 517 | // after ice candidate gathering is complete 518 | sendSignal('new-offer', { 519 | 'sdp': peer.localDescription, 520 | 'receiver_channel_name': receiver_channel_name, 521 | 'local_screen_sharing': localScreenSharing, 522 | 'remote_screen_sharing': remoteScreenSharing, 523 | }); 524 | } 525 | 526 | peer.createOffer() 527 | .then(o => peer.setLocalDescription(o)) 528 | .then(function(event){ 529 | console.log("Local Description Set successfully."); 530 | }); 531 | 532 | console.log('mapPeers[', peerUsername, ']: ', mapPeers[peerUsername]); 533 | 534 | return peer; 535 | } 536 | 537 | // create RTCPeerConnection as answerer 538 | // and store it and its datachannel 539 | // send sdp to remote peer after gathering is complete 540 | function createAnswerer(offer, peerUsername, localScreenSharing, remoteScreenSharing, receiver_channel_name){ 541 | var peer = new RTCPeerConnection(null); 542 | 543 | addLocalTracks(peer, localScreenSharing); 544 | 545 | if(!localScreenSharing && !remoteScreenSharing){ 546 | // if none are sharing screens (normal operation) 547 | 548 | // set remote video 549 | var remoteVideo = createVideo(peerUsername); 550 | 551 | // and add tracks to remote video 552 | setOnTrack(peer, remoteVideo); 553 | 554 | // it will have an RTCDataChannel 555 | peer.ondatachannel = e => { 556 | console.log('e.channel.label: ', e.channel.label); 557 | peer.dc = e.channel; 558 | peer.dc.onmessage = dcOnMessage; 559 | peer.dc.onopen = () => { 560 | console.log("Connection opened."); 561 | } 562 | 563 | // store the RTCPeerConnection 564 | // and the corresponding RTCDataChannel 565 | // after the RTCDataChannel is ready 566 | // otherwise, peer.dc may be undefined 567 | // as peer.ondatachannel would not be called yet 568 | mapPeers[peerUsername] = [peer, peer.dc]; 569 | } 570 | 571 | peer.oniceconnectionstatechange = () => { 572 | var iceConnectionState = peer.iceConnectionState; 573 | if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ 574 | delete mapPeers[peerUsername]; 575 | if(iceConnectionState != 'closed'){ 576 | peer.close(); 577 | } 578 | removeVideo(remoteVideo); 579 | } 580 | }; 581 | }else if(localScreenSharing && !remoteScreenSharing){ 582 | // answerer itself is sharing screen 583 | 584 | // it will have an RTCDataChannel 585 | peer.ondatachannel = e => { 586 | peer.dc = e.channel; 587 | peer.dc.onmessage = (evt) => { 588 | console.log('New message from %s: ', peerUsername, evt.data); 589 | } 590 | peer.dc.onopen = () => { 591 | console.log("Connection opened."); 592 | } 593 | 594 | // this peer is a screen sharer 595 | // so its connections will be stored in mapScreenPeers 596 | // store the RTCPeerConnection 597 | // and the corresponding RTCDataChannel 598 | // after the RTCDataChannel is ready 599 | // otherwise, peer.dc may be undefined 600 | // as peer.ondatachannel would not be called yet 601 | mapScreenPeers[peerUsername] = [peer, peer.dc]; 602 | 603 | peer.oniceconnectionstatechange = () => { 604 | var iceConnectionState = peer.iceConnectionState; 605 | if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ 606 | delete mapScreenPeers[peerUsername]; 607 | if(iceConnectionState != 'closed'){ 608 | peer.close(); 609 | } 610 | } 611 | }; 612 | } 613 | }else{ 614 | // offerer is sharing screen 615 | 616 | // set remote video 617 | var remoteVideo = createVideo(peerUsername + '-screen'); 618 | // and add tracks to remote video 619 | setOnTrack(peer, remoteVideo); 620 | 621 | // it will have an RTCDataChannel 622 | peer.ondatachannel = e => { 623 | peer.dc = e.channel; 624 | peer.dc.onmessage = evt => { 625 | console.log('New message from %s\'s screen: ', peerUsername, evt.data); 626 | } 627 | peer.dc.onopen = () => { 628 | console.log("Connection opened."); 629 | } 630 | 631 | // store the RTCPeerConnection 632 | // and the corresponding RTCDataChannel 633 | // after the RTCDataChannel is ready 634 | // otherwise, peer.dc may be undefined 635 | // as peer.ondatachannel would not be called yet 636 | mapPeers[peerUsername + ' Screen'] = [peer, peer.dc]; 637 | 638 | } 639 | peer.oniceconnectionstatechange = () => { 640 | var iceConnectionState = peer.iceConnectionState; 641 | if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ 642 | delete mapPeers[peerUsername + ' Screen']; 643 | if(iceConnectionState != 'closed'){ 644 | peer.close(); 645 | } 646 | removeVideo(remoteVideo); 647 | } 648 | }; 649 | } 650 | 651 | peer.onicecandidate = (event) => { 652 | if(event.candidate){ 653 | console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer.localDescription)); 654 | return; 655 | } 656 | 657 | // event.candidate == null indicates that gathering is complete 658 | 659 | console.log('Gathering finished! Sending answer SDP to ', peerUsername, '.'); 660 | console.log('receiverChannelName: ', receiver_channel_name); 661 | 662 | // send answer to offering peer 663 | // after ice candidate gathering is complete 664 | // answer needs to send two types of screen sharing data 665 | // local and remote so that offerer can understand 666 | // to which RTCPeerConnection this answer belongs 667 | sendSignal('new-answer', { 668 | 'sdp': peer.localDescription, 669 | 'receiver_channel_name': receiver_channel_name, 670 | 'local_screen_sharing': localScreenSharing, 671 | 'remote_screen_sharing': remoteScreenSharing, 672 | }); 673 | } 674 | 675 | peer.setRemoteDescription(offer) 676 | .then(() => { 677 | console.log('Set offer from %s.', peerUsername); 678 | return peer.createAnswer(); 679 | }) 680 | .then(a => { 681 | console.log('Setting local answer for %s.', peerUsername); 682 | return peer.setLocalDescription(a); 683 | }) 684 | .then(() => { 685 | console.log('Answer created for %s.', peerUsername); 686 | console.log('localDescription: ', peer.localDescription); 687 | console.log('remoteDescription: ', peer.remoteDescription); 688 | }) 689 | .catch(error => { 690 | console.log('Error creating answer for %s.', peerUsername); 691 | console.log(error); 692 | }); 693 | 694 | return peer 695 | } 696 | 697 | function dcOnMessage(event){ 698 | var message = event.data; 699 | 700 | var li = document.createElement("li"); 701 | li.appendChild(document.createTextNode(message)); 702 | ul.appendChild(li); 703 | } 704 | 705 | // get all stored data channels 706 | function getDataChannels(){ 707 | var dataChannels = []; 708 | 709 | for(peerUsername in mapPeers){ 710 | console.log('mapPeers[', peerUsername, ']: ', mapPeers[peerUsername]); 711 | var dataChannel = mapPeers[peerUsername][1]; 712 | console.log('dataChannel: ', dataChannel); 713 | 714 | dataChannels.push(dataChannel); 715 | } 716 | 717 | return dataChannels; 718 | } 719 | 720 | // get all stored RTCPeerConnections 721 | // peerStorageObj is an object (either mapPeers or mapScreenPeers) 722 | function getPeers(peerStorageObj){ 723 | var peers = []; 724 | 725 | for(peerUsername in peerStorageObj){ 726 | var peer = peerStorageObj[peerUsername][0]; 727 | console.log('peer: ', peer); 728 | 729 | peers.push(peer); 730 | } 731 | 732 | return peers; 733 | } 734 | 735 | // for every new peer 736 | // create a new video element 737 | // and its corresponding user gesture button 738 | // assign ids corresponding to the username of the remote peer 739 | function createVideo(peerUsername){ 740 | var videoContainer = document.querySelector('#video-container'); 741 | 742 | // create the new video element 743 | // and corresponding user gesture button 744 | var remoteVideo = document.createElement('video'); 745 | // var btnPlayRemoteVideo = document.createElement('button'); 746 | 747 | remoteVideo.id = peerUsername + '-video'; 748 | remoteVideo.autoplay = true; 749 | remoteVideo.playsinline = true; 750 | // btnPlayRemoteVideo.id = peerUsername + '-btn-play-remote-video'; 751 | // btnPlayRemoteVideo.innerHTML = 'Click here if remote video does not play'; 752 | 753 | // wrapper for the video and button elements 754 | var videoWrapper = document.createElement('div'); 755 | 756 | // add the wrapper to the video container 757 | videoContainer.appendChild(videoWrapper); 758 | 759 | // add the video to the wrapper 760 | videoWrapper.appendChild(remoteVideo); 761 | // videoWrapper.appendChild(btnPlayRemoteVideo); 762 | 763 | // as user gesture 764 | // video is played by button press 765 | // otherwise, some browsers might block video 766 | // btnPlayRemoteVideo.addEventListener("click", function (){ 767 | // remoteVideo.play(); 768 | // btnPlayRemoteVideo.style.visibility = 'hidden'; 769 | // }); 770 | 771 | return remoteVideo; 772 | } 773 | 774 | // set onTrack for RTCPeerConnection 775 | // to add remote tracks to remote stream 776 | // to show video through corresponding remote video element 777 | function setOnTrack(peer, remoteVideo){ 778 | console.log('Setting ontrack:'); 779 | // create new MediaStream for remote tracks 780 | var remoteStream = new MediaStream(); 781 | 782 | // assign remoteStream as the source for remoteVideo 783 | remoteVideo.srcObject = remoteStream; 784 | 785 | console.log('remoteVideo: ', remoteVideo.id); 786 | 787 | peer.addEventListener('track', async (event) => { 788 | console.log('Adding track: ', event.track); 789 | remoteStream.addTrack(event.track, remoteStream); 790 | }); 791 | } 792 | 793 | // called to add appropriate tracks 794 | // to peer 795 | function addLocalTracks(peer, localScreenSharing){ 796 | if(!localScreenSharing){ 797 | // if it is not a screen sharing peer 798 | // add user media tracks 799 | localStream.getTracks().forEach(track => { 800 | console.log('Adding localStream tracks.'); 801 | peer.addTrack(track, localStream); 802 | }); 803 | 804 | return; 805 | } 806 | 807 | // if it is a screen sharing peer 808 | // add display media tracks 809 | localDisplayStream.getTracks().forEach(track => { 810 | console.log('Adding localDisplayStream tracks.'); 811 | peer.addTrack(track, localDisplayStream); 812 | }); 813 | } 814 | 815 | function removeVideo(video){ 816 | // get the video wrapper 817 | var videoWrapper = video.parentNode; 818 | // remove it 819 | videoWrapper.parentNode.removeChild(videoWrapper); 820 | } -------------------------------------------------------------------------------- /mysite/static/js/peer1.js: -------------------------------------------------------------------------------- 1 | var loc = window.location; 2 | 3 | var endPoint = ''; 4 | var wsStart = 'ws://'; 5 | 6 | console.log('protocol: ', loc.protocol); 7 | if(loc.protocol == 'https:'){ 8 | wsStart = 'wss://'; 9 | } 10 | 11 | var endPoint = wsStart + loc.host + loc.pathname; 12 | 13 | var webSocket = new WebSocket(endPoint); 14 | 15 | webSocket.onopen = function(e){ 16 | console.log('Connection opened! ', e); 17 | } 18 | 19 | webSocket.onmessage = webSocketOnMessage; 20 | 21 | webSocket.onclose = function(e){ 22 | console.log('Connection closed! ', e); 23 | 24 | peer1.close(); 25 | } 26 | 27 | webSocket.onerror = function(e){ 28 | console.log('Error occured! ', e); 29 | } 30 | 31 | var btnSendOffer = document.querySelector('#btn-send-offer'); 32 | 33 | btnSendOffer.onclick = btnSendOfferOnClick; 34 | 35 | function webSocketOnMessage(event){ 36 | var parsed_data = JSON.parse(event.data); 37 | 38 | var action = parsed_data['action']; 39 | 40 | if(parsed_data['peer'] == 'peer1'){ 41 | // ignore all messages from oneself 42 | return; 43 | }else if(action == 'peer2-candidate'){ 44 | // probably unreachable 45 | // since peer2 will not send new ice candidates 46 | // but send answer after gathering is complete 47 | 48 | // console.log('Adding new ice candidate.') 49 | peer1.addIceCandidate(parsed_data['message']); 50 | return; 51 | } 52 | 53 | const thisPeer = parsed_data['peer']; 54 | const answer = parsed_data['message']; 55 | console.log('thisPeer: ', thisPeer); 56 | console.log('Answer received: ', answer); 57 | 58 | peer1.setRemoteDescription(answer); 59 | } 60 | 61 | // as user gesture 62 | // video is played by button press 63 | // otherwise, some browsers might block video 64 | btnPlayRemoteVideo = document.querySelector('#btn-play-remote-video'); 65 | btnPlayRemoteVideo.addEventListener("click", function (){ 66 | remoteVideo.play(); 67 | btnPlayRemoteVideo.style.visibility = 'hidden'; 68 | }); 69 | 70 | // send the given action and message strings 71 | // over the websocket connection 72 | function sendSignal(thisPeer, action, message){ 73 | webSocket.send( 74 | JSON.stringify( 75 | { 76 | 'peer': thisPeer, 77 | 'action': action, 78 | 'message': message, 79 | } 80 | ) 81 | ) 82 | } 83 | 84 | function btnSendOfferOnClick(event){ 85 | sendSignal('peer1', 'send-offer', peer1.localDescription); 86 | 87 | btnSendOffer.style.visibility = 'hidden'; 88 | } 89 | 90 | var btnSendMsg = document.querySelector('#btn-send-msg'); 91 | btnSendMsg.onclick = btnSendMsgOnClick; 92 | 93 | function btnSendMsgOnClick(){ 94 | var messageInput = document.querySelector('#msg'); 95 | var message = messageInput.value; 96 | 97 | var li = document.createElement("li"); 98 | li.appendChild(document.createTextNode("Me: " + message)); 99 | ul.appendChild(li); 100 | 101 | console.log('Sending: ', message); 102 | 103 | // send to all data channels 104 | // in multi peer environment 105 | dc.send(message); 106 | 107 | messageInput.value = ''; 108 | } 109 | 110 | const constraints = { 111 | 'video': true, 112 | 'audio': true 113 | } 114 | 115 | const iceConfiguration = { 116 | iceServers: [ 117 | { 118 | urls: ['turn:numb.viagenie.ca'], 119 | credential: '{{numb_turn_credential}}', 120 | username: '{{numb_turn_username}}' 121 | } 122 | ] 123 | }; 124 | 125 | // later assign an RTCPeerConnection 126 | var peer1; 127 | // later assign the datachannel 128 | var dc; 129 | 130 | // true if screen is being shared 131 | // false otherwise 132 | var screenShared = false; 133 | 134 | const localVideo = document.querySelector('#local-video'); 135 | var remoteVideo; 136 | 137 | // later assign button 138 | // to play remote video 139 | // in case browser blocks it 140 | var btnPlayRemoteVideo; 141 | 142 | // button to start or stop screen sharing 143 | var btnShareScreen = document.querySelector('#btn-share-screen'); 144 | 145 | // local video stream 146 | var localStream = new MediaStream(); 147 | // remote video stream; 148 | var remoteStream; 149 | 150 | // local screen stream 151 | // for screen sharing 152 | var localDisplayStream = new MediaStream(); 153 | 154 | // ul of messages 155 | var ul = document.querySelector("#message-list"); 156 | 157 | userMedia = navigator.mediaDevices.getUserMedia(constraints) 158 | .then(stream => { 159 | localStream = stream; 160 | console.log('Got MediaStream:', stream); 161 | mediaTracks = stream.getTracks(); 162 | 163 | for(i=0; i < mediaTracks.length; i++){ 164 | console.log(mediaTracks[i]); 165 | } 166 | 167 | localVideo.srcObject = localStream; 168 | localVideo.muted = true; 169 | 170 | window.stream = stream; // make variable available to browser console 171 | }) 172 | .then(e => { 173 | btnShareScreen.onclick = event => { 174 | if(screenShared){ 175 | // toggle screenShared 176 | screenShared = !screenShared; 177 | 178 | // set to own video 179 | // if screen already shared 180 | localVideo.srcObject = localStream; 181 | btnShareScreen.innerHTML = 'Share screen'; 182 | 183 | return; 184 | } 185 | 186 | // toggle screenShared 187 | screenShared = !screenShared; 188 | 189 | navigator.mediaDevices.getDisplayMedia(constraints) 190 | .then(stream => { 191 | localDisplayStream = stream; 192 | 193 | // set to display stream 194 | // if screen not shared 195 | localVideo.srcObject = localDisplayStream; 196 | }); 197 | 198 | btnShareScreen.innerHTML = 'Stop sharing'; 199 | } 200 | }) 201 | .then(e => { 202 | // perform all offerer activities 203 | createOfferer(); 204 | }) 205 | .catch(error => { 206 | console.error('Error accessing media devices.', error); 207 | }); 208 | 209 | function createOfferer(){ 210 | peer1 = new RTCPeerConnection(null); 211 | 212 | localStream.getTracks().forEach(track => { 213 | peer1.addTrack(track, localStream); 214 | }); 215 | 216 | localDisplayStream.getTracks().forEach(track => { 217 | peer1.addTrack(track, localDisplayStream); 218 | }); 219 | 220 | dc = peer1.createDataChannel("channel"); 221 | dc.onmessage = dcOnMessage 222 | dc.onopen = () => { 223 | console.log("Connection opened."); 224 | 225 | // make play button visible 226 | // upon connection 227 | // to play video in case 228 | // browser blocks it 229 | btnPlayRemoteVideo.style.visibility = 'visible'; 230 | } 231 | 232 | peer1.onicecandidate = (event) => { 233 | if(event.candidate){ 234 | console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer1.localDescription)); 235 | }else{ 236 | console.log('Gathering finished!'); 237 | 238 | // send offer in multi peer environment 239 | // sendSignal('peer1', 'send-offer', peer1.localDescription); 240 | } 241 | } 242 | 243 | remoteStream = new MediaStream(); 244 | remoteVideo = document.querySelector('#remote-video'); 245 | remoteVideo.srcObject = remoteStream; 246 | 247 | peer1.addEventListener('track', async (event) => { 248 | remoteStream.addTrack(event.track, remoteStream); 249 | }); 250 | 251 | peer1.createOffer() 252 | .then(o => peer1.setLocalDescription(o)) 253 | .then(function(event){ 254 | console.log("Local Description Set successfully."); 255 | }); 256 | 257 | function dcOnMessage(event){ 258 | var message = event.data; 259 | 260 | var li = document.createElement("li"); 261 | li.appendChild(document.createTextNode("Other: " + message)); 262 | ul.appendChild(li); 263 | } 264 | } -------------------------------------------------------------------------------- /mysite/static/js/peer2.js: -------------------------------------------------------------------------------- 1 | var loc = window.location; 2 | 3 | var endPoint = ''; 4 | var wsStart = 'ws://'; 5 | 6 | if(loc.protocol == 'https:'){ 7 | wsStart = 'wss://'; 8 | } 9 | 10 | var endPoint = wsStart + loc.host + loc.pathname; 11 | 12 | var webSocket = new WebSocket(endPoint); 13 | 14 | webSocket.onopen = function(e){ 15 | console.log('Connection opened! ', e); 16 | } 17 | 18 | webSocket.onmessage = webSocketOnMessage; 19 | 20 | webSocket.onclose = function(e){ 21 | console.log('Connection closed! ', e); 22 | } 23 | 24 | webSocket.onerror = function(e){ 25 | console.log('Error occured! ', e); 26 | } 27 | 28 | function webSocketOnMessage(event){ 29 | var parsed_data = JSON.parse(event.data); 30 | 31 | var action = parsed_data['action']; 32 | 33 | if(parsed_data['peer'] == 'peer2'){ 34 | // ignore all messages from oneself 35 | return; 36 | }else if(action == 'peer1-candidate'){ 37 | peer2.addIceCandidate(parsed_data['message']); 38 | return; 39 | } 40 | 41 | const offer = parsed_data['message']; 42 | // perform all answerer activities 43 | createAnswerer(); 44 | peer2.setRemoteDescription(offer) 45 | .then(function(event){ 46 | console.log('Offer set.'); 47 | 48 | peer2.createAnswer() 49 | .then(a => peer2.setLocalDescription(a)) 50 | .then(function(event){ 51 | console.log('Answer created.'); 52 | // sendSignal('peer2', 'send-answer', peer2.localDescription); 53 | }); 54 | }); 55 | 56 | 57 | } 58 | 59 | // send the given action and message strings 60 | // over the websocket connection 61 | function sendSignal(thisPeer, action, message){ 62 | webSocket.send( 63 | JSON.stringify( 64 | { 65 | 'peer': thisPeer, 66 | 'action': action, 67 | 'message': message, 68 | } 69 | ) 70 | ) 71 | } 72 | 73 | var btnSendMsg = document.querySelector('#btn-send-msg'); 74 | btnSendMsg.onclick = btnSendMsgOnClick; 75 | 76 | var ul = document.querySelector("#message-list"); 77 | 78 | function btnSendMsgOnClick(){ 79 | var messageInput = document.querySelector('#msg'); 80 | var message = messageInput.value; 81 | 82 | var li = document.createElement("li"); 83 | li.appendChild(document.createTextNode("Me: " + message)); 84 | ul.appendChild(li); 85 | 86 | console.log('Sending: ', message); 87 | 88 | peer2.dc.send(message); 89 | 90 | messageInput.value = ''; 91 | } 92 | 93 | function dcOnMessage(event){ 94 | var message = event.data; 95 | 96 | var li = document.createElement("li"); 97 | li.appendChild(document.createTextNode("Other: " + message)); 98 | ul.appendChild(li); 99 | } 100 | 101 | const constraints = { 102 | 'video': true, 103 | 'audio': true 104 | } 105 | 106 | const iceConfiguration = { 107 | iceServers: [ 108 | { 109 | urls: ['turn:numb.viagenie.ca'], 110 | credential: '{{numb_turn_credential}}', 111 | username: '{{numb_turn_username}}' 112 | } 113 | ] 114 | }; 115 | 116 | 117 | // later assign an RTCPeerConnection 118 | var peer2; 119 | // later assign the datachannel 120 | var dc; 121 | 122 | // true if screen is being shared 123 | // false otherwise 124 | var screenShared = false; 125 | 126 | const localVideo = document.querySelector('#local-video'); 127 | var remoteVideo; 128 | 129 | // later assign button 130 | // to play remote video 131 | // in case browser blocks it 132 | var btnPlayRemoteVideo; 133 | 134 | // button to start or stop screen sharing 135 | var btnShareScreen = document.querySelector('#btn-share-screen'); 136 | 137 | // local video stream 138 | var localStream = new MediaStream(); 139 | // remote video stream; 140 | var remoteStream; 141 | 142 | // local screen stream 143 | // for screen sharing 144 | var localDisplayStream = new MediaStream(); 145 | 146 | userMedia = navigator.mediaDevices.getUserMedia(constraints) 147 | .then(stream => { 148 | localStream = stream; 149 | console.log('Got MediaStream:', stream); 150 | mediaTracks = stream.getTracks(); 151 | 152 | for(i=0; i < mediaTracks.length; i++){ 153 | console.log(mediaTracks[i]); 154 | } 155 | 156 | localVideo.srcObject = localStream; 157 | localVideo.muted = true; 158 | 159 | window.stream = stream; // make variable available to browser console 160 | }) 161 | .catch(error => { 162 | console.error('Error accessing media devices.', error); 163 | }); 164 | 165 | function createAnswerer(){ 166 | peer2 = new RTCPeerConnection(null); 167 | 168 | localStream.getTracks().forEach(track => { 169 | peer2.addTrack(track, localStream); 170 | }); 171 | 172 | remoteStream = new MediaStream(); 173 | remoteVideo = document.querySelector('#remote-video'); 174 | remoteVideo.srcObject = remoteStream; 175 | 176 | window.stream = remoteStream; 177 | 178 | peer2.addEventListener('track', async (event) => { 179 | console.log('Adding track: ', event.track); 180 | remoteStream.addTrack(event.track, remoteStream); 181 | }); 182 | 183 | // as user gesture 184 | // video is played by button press 185 | // otherwise, some browsers might block video 186 | btnPlayRemoteVideo = document.querySelector('#btn-play-remote-video'); 187 | btnPlayRemoteVideo.addEventListener("click", function (){ 188 | remoteVideo.play(); 189 | btnPlayRemoteVideo.style.visibility = 'hidden'; 190 | }); 191 | 192 | peer2.onicecandidate = (event) => { 193 | if(event.candidate){ 194 | console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer2.localDescription)); 195 | 196 | // following statement not required anymore 197 | // since answer will be sent after gathering is complete 198 | // sendSignal('peer2', 'peer2-candidate', event.candidate); 199 | }else{ 200 | console.log('Gathering finished!'); 201 | 202 | // send answer in multi peer environment 203 | sendSignal('peer2', 'send-answer', peer2.localDescription); 204 | } 205 | } 206 | 207 | peer2.ondatachannel = e => { 208 | peer2.dc = e.channel; 209 | peer2.dc.onmessage = dcOnMessage; 210 | peer2.dc.onopen = () => { 211 | console.log("Connection opened."); 212 | 213 | // make play button visible 214 | // upon connection 215 | // to play video in case 216 | // browser blocks it 217 | btnPlayRemoteVideo.style.visibility = 'visible'; 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | attrs==20.3.0 3 | autobahn==21.3.1 4 | Automat==20.2.0 5 | cffi==1.14.5 6 | channels==3.0.3 7 | constantly==15.1.0 8 | cryptography==3.4.6 9 | daphne==3.0.1 10 | Django==3.1 11 | hyperlink==21.0.0 12 | idna==3.1 13 | incremental==21.3.0 14 | pyasn1==0.4.8 15 | pyasn1-modules==0.2.8 16 | pycparser==2.20 17 | pyOpenSSL==20.0.1 18 | python-decouple==3.4 19 | pytz==2021.1 20 | service-identity==18.1.0 21 | six==1.15.0 22 | sqlparse==0.4.1 23 | Twisted==21.2.0 24 | twisted-iocpsupport==1.0.1 25 | txaio==21.2.1 26 | zope.interface==5.2.0 27 | --------------------------------------------------------------------------------