├── .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 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Select a file:
39 |
40 |
41 |
45 |
46 |
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 |
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 |
--------------------------------------------------------------------------------