diff --git a/Docker-Compose.yml b/Docker-Compose.yml new file mode 100644 index 0000000..57f49cc --- /dev/null +++ b/Docker-Compose.yml @@ -0,0 +1,26 @@ +version: "3" + +services: + redis: + restart: always + image: redis:latest + networks: + - main + ports: + - "6379:6379/tcp" + web: + restart: always + build: ./web + image: django_video + command: python manage.py runserver 0.0.0.0:8000 + ports: + - "8000:8000" + networks: + - main + volumes: + - ./web/mysite:/usr/src/app/ + env_file: .env + environment: + - DEBUG=true +networks: + main: \ No newline at end of file diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..0de2170 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3 + +ARG DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + + +RUN mkdir /usr/src/app + +COPY requirements.txt /usr/src/app +COPY mysite/ /usr/src/app + +WORKDIR /usr/src/app + +RUN python3 -m pip install --upgrade pip \ + && python3 -m pip install --upgrade pip setuptools wheel \ + && python3 -m pip install -r requirements.txt \ + && apt-get clean autoclean \ + && apt-get autoremove --yes + diff --git a/web/mysite/chat/__init__.py b/web/mysite/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/mysite/chat/admin.py b/web/mysite/chat/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/web/mysite/chat/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/web/mysite/chat/apps.py b/web/mysite/chat/apps.py new file mode 100644 index 0000000..8ebb9f0 --- /dev/null +++ b/web/mysite/chat/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + name = 'chat' diff --git a/web/mysite/chat/consumers.py b/web/mysite/chat/consumers.py new file mode 100644 index 0000000..87ebd3a --- /dev/null +++ b/web/mysite/chat/consumers.py @@ -0,0 +1,90 @@ +import json +from channels.generic.websocket import AsyncWebsocketConsumer + +import asyncio + +class ChatConsumer(AsyncWebsocketConsumer): + async def connect(self): + + self.room_name = self.scope['url_route']['kwargs']['room_name'] + self.room_group_name = 'chat_%s' % self.room_name + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + + await self.accept() + + async def disconnect(self, close_code): + + await self.channel_layer.group_discard( + self.room_group_name, + self.channel_name + ) + + print('Disconnected!') + + + # Receive message from WebSocket + async def receive(self, text_data): + receive_dict = json.loads(text_data) + print(receive_dict) + peer_username = receive_dict['peer'] + action = receive_dict['action'] + message = receive_dict['message'] + + # print('unanswered_offers: ', self.unanswered_offers) + + print('Message received: ', message) + + print('peer_username: ', peer_username) + print('action: ', action) + print('self.channel_name: ', self.channel_name) + + if(action == 'new-offer') or (action =='new-answer'): + # in case its a new offer or answer + # send it to the new peer or initial offerer respectively + + receiver_channel_name = receive_dict['message']['receiver_channel_name'] + + print('Sending to ', receiver_channel_name) + + # set new receiver as the current sender + receive_dict['message']['receiver_channel_name'] = self.channel_name + + await self.channel_layer.send( + receiver_channel_name, + { + 'type': 'send.sdp', + 'receive_dict': receive_dict, + } + ) + + return + + # set new receiver as the current sender + # so that some messages can be sent + # to this channel specifically + receive_dict['message']['receiver_channel_name'] = self.channel_name + + # send to all peers + await self.channel_layer.group_send( + self.room_group_name, + { + 'type': 'send.sdp', + 'receive_dict': receive_dict, + } + ) + + async def send_sdp(self, event): + receive_dict = event['receive_dict'] + + this_peer = receive_dict['peer'] + action = receive_dict['action'] + message = receive_dict['message'] + + await self.send(text_data=json.dumps({ + 'peer': this_peer, + 'action': action, + 'message': message, + })) \ No newline at end of file diff --git a/web/mysite/chat/migrations/__init__.py b/web/mysite/chat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/mysite/chat/models.py b/web/mysite/chat/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/web/mysite/chat/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/web/mysite/chat/routing.py b/web/mysite/chat/routing.py new file mode 100644 index 0000000..1118bd0 --- /dev/null +++ b/web/mysite/chat/routing.py @@ -0,0 +1,9 @@ +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path(r'chat/(?P\w+)/$', consumers.ChatConsumer.as_asgi()), + re_path(r'peer[12]/', consumers.ChatConsumer.as_asgi()), + re_path(r'peer', consumers.ChatConsumer.as_asgi()), +] \ No newline at end of file diff --git a/web/mysite/chat/templates/chat/index.html b/web/mysite/chat/templates/chat/index.html new file mode 100644 index 0000000..e826230 --- /dev/null +++ b/web/mysite/chat/templates/chat/index.html @@ -0,0 +1,27 @@ + + + + + + Chat Rooms + + + What chat room would you like to enter?
+
+ + + + + \ No newline at end of file diff --git a/web/mysite/chat/templates/chat/peer.html b/web/mysite/chat/templates/chat/peer.html new file mode 100644 index 0000000..377f223 --- /dev/null +++ b/web/mysite/chat/templates/chat/peer.html @@ -0,0 +1,12 @@ +{% extends 'main.html' %} + +{% load static %} + +{% block content %} + + + +{% endblock %} \ No newline at end of file diff --git a/web/mysite/chat/templates/chat/peer1.html b/web/mysite/chat/templates/chat/peer1.html new file mode 100644 index 0000000..5457d19 --- /dev/null +++ b/web/mysite/chat/templates/chat/peer1.html @@ -0,0 +1,10 @@ +{% extends 'trial_main.html' %} + +{% load static %} + +{% block content %} +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/web/mysite/chat/templates/chat/peer2.html b/web/mysite/chat/templates/chat/peer2.html new file mode 100644 index 0000000..65c30e2 --- /dev/null +++ b/web/mysite/chat/templates/chat/peer2.html @@ -0,0 +1,7 @@ +{% extends 'trial_main.html' %} + +{% load static %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/web/mysite/chat/templates/chat/room.html b/web/mysite/chat/templates/chat/room.html new file mode 100644 index 0000000..67d7e44 --- /dev/null +++ b/web/mysite/chat/templates/chat/room.html @@ -0,0 +1,52 @@ + + + + + + Chat Room + + +
+
+ + {{ room_name|json_script:"room-name" }} + + + \ No newline at end of file diff --git a/web/mysite/chat/templates/main.html b/web/mysite/chat/templates/main.html new file mode 100644 index 0000000..b0d6c68 --- /dev/null +++ b/web/mysite/chat/templates/main.html @@ -0,0 +1,52 @@ + + +{% load static %} + + + + + Django Channels WebRTC + + + +

USERNAME

+
+ +
+
+ +
+
+ + +
+
+

CHAT

+
+
    +
    +
    + + + + + + +
    +
    + {% block content %} + {% endblock %} + + \ No newline at end of file diff --git a/web/mysite/chat/templates/trial_main.html b/web/mysite/chat/templates/trial_main.html new file mode 100644 index 0000000..db096af --- /dev/null +++ b/web/mysite/chat/templates/trial_main.html @@ -0,0 +1,38 @@ + + +{% load static %} + + + + + Django Channels WebRTC + + + +

    USERNAME

    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    +

    CHAT

    +
    +
      +
      +
      + +
      +
      + {% block content %} + {% endblock %} + + \ No newline at end of file diff --git a/web/mysite/chat/tests.py b/web/mysite/chat/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/web/mysite/chat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/web/mysite/chat/urls.py b/web/mysite/chat/urls.py new file mode 100644 index 0000000..4f930af --- /dev/null +++ b/web/mysite/chat/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.index, name='index'), + path('peer', views.peer, name='peer'), + path('chat//', views.peer, name='room') +] \ No newline at end of file diff --git a/web/mysite/chat/utils.py b/web/mysite/chat/utils.py new file mode 100644 index 0000000..68d521b --- /dev/null +++ b/web/mysite/chat/utils.py @@ -0,0 +1,8 @@ +from django.conf import settings + +# returns numb turn credential and username +def get_turn_info(): + return { + 'numb_turn_credential': settings.NUMB_TURN_CREDENTIAL, + 'numb_turn_username': settings.NUMB_TURN_USERNAME, + } \ No newline at end of file diff --git a/web/mysite/chat/views.py b/web/mysite/chat/views.py new file mode 100644 index 0000000..4f4b4fb --- /dev/null +++ b/web/mysite/chat/views.py @@ -0,0 +1,24 @@ +from django.shortcuts import render + +from .utils import get_turn_info + +# Create your views here. +def index(request): + # get numb turn info + context = get_turn_info() + + return render(request, 'chat/index.html', context=context) +# Create your views here. +def room(request, room_name): + return render(request, 'chat/room.html', { + 'room_name': room_name + }) + +def peer(request, room_name): + # get numb turn info + context = get_turn_info() + print('context: ', context) + context.update({'room_name':room_name}) + return render(request, 'chat/peer.html', { + 'room_name': room_name + }) \ No newline at end of file diff --git a/web/mysite/manage.py b/web/mysite/manage.py new file mode 100644 index 0000000..a7da667 --- /dev/null +++ b/web/mysite/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/web/mysite/mysite/__init__.py b/web/mysite/mysite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/mysite/mysite/asgi.py b/web/mysite/mysite/asgi.py new file mode 100644 index 0000000..903404e --- /dev/null +++ b/web/mysite/mysite/asgi.py @@ -0,0 +1,27 @@ +""" +ASGI config for mysite project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ +""" + +import os + +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application +from channels.auth import AuthMiddlewareStack +import chat.routing + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = ProtocolTypeRouter({ + "http": get_asgi_application(), + # Just HTTP for now. (We can add other protocols later.) + "websocket": AuthMiddlewareStack( + URLRouter( + chat.routing.websocket_urlpatterns + ) + ), +}) \ No newline at end of file diff --git a/web/mysite/mysite/settings.py b/web/mysite/mysite/settings.py new file mode 100644 index 0000000..38d4514 --- /dev/null +++ b/web/mysite/mysite/settings.py @@ -0,0 +1,148 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 3.1. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.1/ref/settings/ +""" + +from pathlib import Path + +import os + +from decouple import config + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve(strict=True).parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + +NUMB_TURN_CREDENTIAL = config('NUMB_TURN_CREDENTIAL', default=None) +NUMB_TURN_USERNAME = config('NUMB_TURN_USERNAME', default=None) + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'chat.apps.ChatConfig', + 'channels', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.1/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] + +# Channels +ASGI_APPLICATION = 'mysite.asgi.application' + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer" + } +} + +# CHANNEL_LAYERS = { +# 'default': { +# 'BACKEND': 'channels_redis.core.RedisChannelLayer', +# 'CONFIG': { +# "hosts": [('127.0.0.1', 6379)], +# }, +# }, +# } \ No newline at end of file diff --git a/web/mysite/mysite/urls.py b/web/mysite/mysite/urls.py new file mode 100644 index 0000000..a9e7442 --- /dev/null +++ b/web/mysite/mysite/urls.py @@ -0,0 +1,22 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('chat.urls')), +] diff --git a/web/mysite/mysite/wsgi.py b/web/mysite/mysite/wsgi.py new file mode 100644 index 0000000..d04765f --- /dev/null +++ b/web/mysite/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_wsgi_application() diff --git a/web/mysite/static/css/main.css b/web/mysite/static/css/main.css new file mode 100644 index 0000000..aa7e3c1 --- /dev/null +++ b/web/mysite/static/css/main.css @@ -0,0 +1,15 @@ +.video-grid-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + background-color: black; +} + +.main-grid-container { + display: grid; + grid-template-columns: 70% 20%; +} + +video { + border-radius: 5%; + background-color: black; +} \ No newline at end of file diff --git a/web/mysite/static/js/peer.js b/web/mysite/static/js/peer.js new file mode 100644 index 0000000..bc0b5c8 --- /dev/null +++ b/web/mysite/static/js/peer.js @@ -0,0 +1,820 @@ +// map peer usernames to corresponding RTCPeerConnections +// as key value pairs +var mapPeers = {}; + +// peers that stream own screen +// to remote peers +var mapScreenPeers = {}; + +// true if screen is being shared +// false otherwise +var screenShared = false; + +const localVideo = document.querySelector('#local-video'); + +// button to start or stop screen sharing +var btnShareScreen = document.querySelector('#btn-share-screen'); + +// local video stream +var localStream = new MediaStream(); + +// local screen stream +// for screen sharing +var localDisplayStream = new MediaStream(); + +// buttons to toggle self audio and video +btnToggleAudio = document.querySelector("#btn-toggle-audio"); +btnToggleVideo = document.querySelector("#btn-toggle-video"); + +var messageInput = document.querySelector('#msg'); +var btnSendMsg = document.querySelector('#btn-send-msg'); + +// button to start or stop screen recording +var btnRecordScreen = document.querySelector('#btn-record-screen'); +// object that will start or stop screen recording +var recorder; +// true of currently recording, false otherwise +var recording = false; + +var file; + +document.getElementById('share-file-button').addEventListener('click', () => { + document.getElementById('select-file-dialog').style.display = 'block'; +}); + +document.getElementById('cancel-button').addEventListener('click', () => { + document.getElementById('select-file-input').value = ''; + document.getElementById('select-file-dialog').style.display = 'none'; + document.getElementById('ok-button').disabled = true; +}); + +document.getElementById('select-file-input').addEventListener('change', (event) => { + file = event.target.files[0]; + document.getElementById('ok-button').disabled = !file; +}); + +// ul of messages +var ul = document.querySelector("#message-list"); + +var loc = window.location; + +var endPoint = ''; +var wsStart = 'ws://'; + +if(loc.protocol == 'https:'){ + wsStart = 'wss://'; +} + +var endPoint = wsStart + loc.host + loc.pathname; + +var webSocket; + +var usernameInput = document.querySelector('#username'); +var username; + +var btnJoin = document.querySelector('#btn-join'); + +// set username +// join room (initiate websocket connection) +// upon button click +btnJoin.onclick = () => { + username = usernameInput.value; + + if(username == ''){ + // ignore if username is empty + return; + } + + // clear input + usernameInput.value = ''; + // disable and vanish input + btnJoin.disabled = true; + usernameInput.style.visibility = 'hidden'; + // disable and vanish join button + btnJoin.disabled = true; + btnJoin.style.visibility = 'hidden'; + + document.querySelector('#label-username').innerHTML = username; + + webSocket = new WebSocket(endPoint); + + webSocket.onopen = function(e){ + console.log('Connection opened! ', e); + + // notify other peers + sendSignal('new-peer', { + 'local_screen_sharing': false, + }); + } + + webSocket.onmessage = webSocketOnMessage; + + webSocket.onclose = function(e){ + console.log('Connection closed! ', e); + } + + webSocket.onerror = function(e){ + console.log('Error occured! ', e); + } + + btnSendMsg.disabled = false; + messageInput.disabled = false; +} + +function webSocketOnMessage(event){ + var parsedData = JSON.parse(event.data); + + var action = parsedData['action']; + // username of other peer + var peerUsername = parsedData['peer']; + + console.log('peerUsername: ', peerUsername); + console.log('action: ', action); + + if(peerUsername == username){ + // ignore all messages from oneself + return; + } + + // boolean value specified by other peer + // indicates whether the other peer is sharing screen + var remoteScreenSharing = parsedData['message']['local_screen_sharing']; + console.log('remoteScreenSharing: ', remoteScreenSharing); + + // channel name of the sender of this message + // used to send messages back to that sender + // hence, receiver_channel_name + var receiver_channel_name = parsedData['message']['receiver_channel_name']; + console.log('receiver_channel_name: ', receiver_channel_name); + + // in case of new peer + if(action == 'new-peer'){ + console.log('New peer: ', peerUsername); + + // create new RTCPeerConnection + createOfferer(peerUsername, false, remoteScreenSharing, receiver_channel_name); + + if(screenShared && !remoteScreenSharing){ + // if local screen is being shared + // and remote peer is not sharing screen + // send offer from screen sharing peer + console.log('Creating screen sharing offer.'); + createOfferer(peerUsername, true, remoteScreenSharing, receiver_channel_name); + } + + return; + } + + // remote_screen_sharing from the remote peer + // will be local screen sharing info for this peer + var localScreenSharing = parsedData['message']['remote_screen_sharing']; + + if(action == 'new-offer'){ + console.log('Got new offer from ', peerUsername); + + // create new RTCPeerConnection + // set offer as remote description + var offer = parsedData['message']['sdp']; + console.log('Offer: ', offer); + var peer = createAnswerer(offer, peerUsername, localScreenSharing, remoteScreenSharing, receiver_channel_name); + + return; + } + + + if(action == 'new-answer'){ + // in case of answer to previous offer + // get the corresponding RTCPeerConnection + var peer = null; + + if(remoteScreenSharing){ + // if answerer is screen sharer + peer = mapPeers[peerUsername + ' Screen'][0]; + }else if(localScreenSharing){ + // if offerer was screen sharer + peer = mapScreenPeers[peerUsername][0]; + }else{ + // if both are non-screen sharers + peer = mapPeers[peerUsername][0]; + } + + // get the answer + var answer = parsedData['message']['sdp']; + + console.log('mapPeers:'); + for(key in mapPeers){ + console.log(key, ': ', mapPeers[key]); + } + + console.log('peer: ', peer); + console.log('answer: ', answer); + + // set remote description of the RTCPeerConnection + peer.setRemoteDescription(answer); + + return; + } +} + +messageInput.addEventListener('keyup', function(event){ + if(event.keyCode == 13){ + // prevent from putting 'Enter' as input + event.preventDefault(); + + // click send message button + btnSendMsg.click(); + } +}); + +btnSendMsg.onclick = btnSendMsgOnClick; + +function btnSendMsgOnClick(){ + var message = messageInput.value; + + var li = document.createElement("li"); + li.appendChild(document.createTextNode("Me: " + message)); + ul.appendChild(li); + + var dataChannels = getDataChannels(); + + console.log('Sending: ', message); + + // send to all data channels + for(index in dataChannels){ + dataChannels[index].send(username + ': ' + message); + } + + messageInput.value = ''; +} + +const constraints = { + 'video': true, + 'audio': true +} + +// const iceConfiguration = { +// iceServers: [ +// { +// urls: ['turn:numb.viagenie.ca'], +// credential: numbTurnCredential, +// username: numbTurnUsername +// } +// ] +// }; + +userMedia = navigator.mediaDevices.getUserMedia(constraints) + .then(stream => { + localStream = stream; + console.log('Got MediaStream:', stream); + var mediaTracks = stream.getTracks(); + + for(i=0; i < mediaTracks.length; i++){ + console.log(mediaTracks[i]); + } + + localVideo.srcObject = localStream; + localVideo.muted = true; + + window.stream = stream; // make variable available to browser console + + audioTracks = stream.getAudioTracks(); + videoTracks = stream.getVideoTracks(); + + // unmute audio and video by default + audioTracks[0].enabled = true; + videoTracks[0].enabled = true; + + btnToggleAudio.onclick = function(){ + audioTracks[0].enabled = !audioTracks[0].enabled; + if(audioTracks[0].enabled){ + btnToggleAudio.innerHTML = 'Audio Mute'; + return; + } + + btnToggleAudio.innerHTML = 'Audio Unmute'; + }; + + btnToggleVideo.onclick = function(){ + videoTracks[0].enabled = !videoTracks[0].enabled; + if(videoTracks[0].enabled){ + btnToggleVideo.innerHTML = 'Video Off'; + return; + } + + btnToggleVideo.innerHTML = 'Video On'; + }; + }) + .then(e => { + btnShareScreen.onclick = event => { + if(screenShared){ + // toggle screenShared + screenShared = !screenShared; + + // set to own video + // if screen already shared + localVideo.srcObject = localStream; + btnShareScreen.innerHTML = 'Share screen'; + + // get screen sharing video element + var localScreen = document.querySelector('#my-screen-video'); + // remove it + removeVideo(localScreen); + + // close all screen share peer connections + var screenPeers = getPeers(mapScreenPeers); + for(index in screenPeers){ + screenPeers[index].close(); + } + // empty the screen sharing peer storage object + mapScreenPeers = {}; + + return; + } + + // toggle screenShared + screenShared = !screenShared; + + navigator.mediaDevices.getDisplayMedia(constraints) + .then(stream => { + localDisplayStream = stream; + + var mediaTracks = stream.getTracks(); + for(i=0; i < mediaTracks.length; i++){ + console.log(mediaTracks[i]); + } + + var localScreen = createVideo('my-screen'); + // set to display stream + // if screen not shared + localScreen.srcObject = localDisplayStream; + + // notify other peers + // of screen sharing peer + sendSignal('new-peer', { + 'local_screen_sharing': true, + }); + }) + .catch(error => { + console.log('Error accessing display media.', error); + }); + + btnShareScreen.innerHTML = 'Stop sharing'; + } + }) + .then(e => { + btnRecordScreen.addEventListener('click', () => { + if(recording){ + // toggle recording + recording = !recording; + + btnRecordScreen.innerHTML = 'Record Screen'; + + recorder.stopRecording(function() { + var blob = recorder.getBlob(); + invokeSaveAsDialog(blob); + }); + + return; + } + + // toggle recording + recording = !recording; + + navigator.mediaDevices.getDisplayMedia(constraints) + .then(stream => { + recorder = RecordRTC(stream, { + type: 'video', + MimeType: 'video/mp4' + }); + recorder.startRecording(); + + var mediaTracks = stream.getTracks(); + for(i=0; i < mediaTracks.length; i++){ + console.log(mediaTracks[i]); + } + + }) + .catch(error => { + console.log('Error accessing display media.', error); + }); + + btnRecordScreen.innerHTML = 'Stop Recording'; + }); + }) + .catch(error => { + console.error('Error accessing media devices.', error); + }); + +// send the given action and message +// over the websocket connection +function sendSignal(action, message){ + webSocket.send( + JSON.stringify( + { + 'peer': username, + 'action': action, + 'message': message, + } + ) + ) +} + +// create RTCPeerConnection as offerer +// and store it and its datachannel +// send sdp to remote peer after gathering is complete +function createOfferer(peerUsername, localScreenSharing, remoteScreenSharing, receiver_channel_name){ + var peer = new RTCPeerConnection(null); + + // add local user media stream tracks + addLocalTracks(peer, localScreenSharing); + + // create and manage an RTCDataChannel + var dc = peer.createDataChannel("channel"); + dc.onopen = () => { + console.log("Connection opened."); + }; + var remoteVideo = null; + if(!localScreenSharing && !remoteScreenSharing){ + // none of the peers are sharing screen (normal operation) + + dc.onmessage = dcOnMessage; + + remoteVideo = createVideo(peerUsername); + setOnTrack(peer, remoteVideo); + console.log('Remote video source: ', remoteVideo.srcObject); + + // store the RTCPeerConnection + // and the corresponding RTCDataChannel + mapPeers[peerUsername] = [peer, dc]; + + peer.oniceconnectionstatechange = () => { + var iceConnectionState = peer.iceConnectionState; + if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ + console.log('Deleting peer'); + delete mapPeers[peerUsername]; + if(iceConnectionState != 'closed'){ + peer.close(); + } + removeVideo(remoteVideo); + } + }; + }else if(!localScreenSharing && remoteScreenSharing){ + // answerer is screen sharing + + dc.onmessage = (e) => { + console.log('New message from %s\'s screen: ', peerUsername, e.data); + }; + + remoteVideo = createVideo(peerUsername + '-screen'); + setOnTrack(peer, remoteVideo); + console.log('Remote video source: ', remoteVideo.srcObject); + + // if offer is not for screen sharing peer + mapPeers[peerUsername + ' Screen'] = [peer, dc]; + + peer.oniceconnectionstatechange = () => { + var iceConnectionState = peer.iceConnectionState; + if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ + delete mapPeers[peerUsername + ' Screen']; + if(iceConnectionState != 'closed'){ + peer.close(); + } + removeVideo(remoteVideo); + } + }; + }else{ + // offerer itself is sharing screen + + dc.onmessage = (e) => { + console.log('New message from %s: ', peerUsername, e.data); + }; + + mapScreenPeers[peerUsername] = [peer, dc]; + + peer.oniceconnectionstatechange = () => { + var iceConnectionState = peer.iceConnectionState; + if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ + delete mapScreenPeers[peerUsername]; + if(iceConnectionState != 'closed'){ + peer.close(); + } + } + }; + } + + peer.onicecandidate = (event) => { + if(event.candidate){ + console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer.localDescription)); + return; + } + + // event.candidate == null indicates that gathering is complete + + console.log('Gathering finished! Sending offer SDP to ', peerUsername, '.'); + console.log('receiverChannelName: ', receiver_channel_name); + + // send offer to new peer + // after ice candidate gathering is complete + sendSignal('new-offer', { + 'sdp': peer.localDescription, + 'receiver_channel_name': receiver_channel_name, + 'local_screen_sharing': localScreenSharing, + 'remote_screen_sharing': remoteScreenSharing, + }); + } + + peer.createOffer() + .then(o => peer.setLocalDescription(o)) + .then(function(event){ + console.log("Local Description Set successfully."); + }); + + console.log('mapPeers[', peerUsername, ']: ', mapPeers[peerUsername]); + + return peer; +} + +// create RTCPeerConnection as answerer +// and store it and its datachannel +// send sdp to remote peer after gathering is complete +function createAnswerer(offer, peerUsername, localScreenSharing, remoteScreenSharing, receiver_channel_name){ + var peer = new RTCPeerConnection(null); + + addLocalTracks(peer, localScreenSharing); + + if(!localScreenSharing && !remoteScreenSharing){ + // if none are sharing screens (normal operation) + + // set remote video + var remoteVideo = createVideo(peerUsername); + + // and add tracks to remote video + setOnTrack(peer, remoteVideo); + + // it will have an RTCDataChannel + peer.ondatachannel = e => { + console.log('e.channel.label: ', e.channel.label); + peer.dc = e.channel; + peer.dc.onmessage = dcOnMessage; + peer.dc.onopen = () => { + console.log("Connection opened."); + } + + // store the RTCPeerConnection + // and the corresponding RTCDataChannel + // after the RTCDataChannel is ready + // otherwise, peer.dc may be undefined + // as peer.ondatachannel would not be called yet + mapPeers[peerUsername] = [peer, peer.dc]; + } + + peer.oniceconnectionstatechange = () => { + var iceConnectionState = peer.iceConnectionState; + if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ + delete mapPeers[peerUsername]; + if(iceConnectionState != 'closed'){ + peer.close(); + } + removeVideo(remoteVideo); + } + }; + }else if(localScreenSharing && !remoteScreenSharing){ + // answerer itself is sharing screen + + // it will have an RTCDataChannel + peer.ondatachannel = e => { + peer.dc = e.channel; + peer.dc.onmessage = (evt) => { + console.log('New message from %s: ', peerUsername, evt.data); + } + peer.dc.onopen = () => { + console.log("Connection opened."); + } + + // this peer is a screen sharer + // so its connections will be stored in mapScreenPeers + // store the RTCPeerConnection + // and the corresponding RTCDataChannel + // after the RTCDataChannel is ready + // otherwise, peer.dc may be undefined + // as peer.ondatachannel would not be called yet + mapScreenPeers[peerUsername] = [peer, peer.dc]; + + peer.oniceconnectionstatechange = () => { + var iceConnectionState = peer.iceConnectionState; + if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ + delete mapScreenPeers[peerUsername]; + if(iceConnectionState != 'closed'){ + peer.close(); + } + } + }; + } + }else{ + // offerer is sharing screen + + // set remote video + var remoteVideo = createVideo(peerUsername + '-screen'); + // and add tracks to remote video + setOnTrack(peer, remoteVideo); + + // it will have an RTCDataChannel + peer.ondatachannel = e => { + peer.dc = e.channel; + peer.dc.onmessage = evt => { + console.log('New message from %s\'s screen: ', peerUsername, evt.data); + } + peer.dc.onopen = () => { + console.log("Connection opened."); + } + + // store the RTCPeerConnection + // and the corresponding RTCDataChannel + // after the RTCDataChannel is ready + // otherwise, peer.dc may be undefined + // as peer.ondatachannel would not be called yet + mapPeers[peerUsername + ' Screen'] = [peer, peer.dc]; + + } + peer.oniceconnectionstatechange = () => { + var iceConnectionState = peer.iceConnectionState; + if (iceConnectionState === "failed" || iceConnectionState === "disconnected" || iceConnectionState === "closed"){ + delete mapPeers[peerUsername + ' Screen']; + if(iceConnectionState != 'closed'){ + peer.close(); + } + removeVideo(remoteVideo); + } + }; + } + + peer.onicecandidate = (event) => { + if(event.candidate){ + console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer.localDescription)); + return; + } + + // event.candidate == null indicates that gathering is complete + + console.log('Gathering finished! Sending answer SDP to ', peerUsername, '.'); + console.log('receiverChannelName: ', receiver_channel_name); + + // send answer to offering peer + // after ice candidate gathering is complete + // answer needs to send two types of screen sharing data + // local and remote so that offerer can understand + // to which RTCPeerConnection this answer belongs + sendSignal('new-answer', { + 'sdp': peer.localDescription, + 'receiver_channel_name': receiver_channel_name, + 'local_screen_sharing': localScreenSharing, + 'remote_screen_sharing': remoteScreenSharing, + }); + } + + peer.setRemoteDescription(offer) + .then(() => { + console.log('Set offer from %s.', peerUsername); + return peer.createAnswer(); + }) + .then(a => { + console.log('Setting local answer for %s.', peerUsername); + return peer.setLocalDescription(a); + }) + .then(() => { + console.log('Answer created for %s.', peerUsername); + console.log('localDescription: ', peer.localDescription); + console.log('remoteDescription: ', peer.remoteDescription); + }) + .catch(error => { + console.log('Error creating answer for %s.', peerUsername); + console.log(error); + }); + + return peer +} + +function dcOnMessage(event){ + var message = event.data; + + var li = document.createElement("li"); + li.appendChild(document.createTextNode(message)); + ul.appendChild(li); +} + +// get all stored data channels +function getDataChannels(){ + var dataChannels = []; + + for(peerUsername in mapPeers){ + console.log('mapPeers[', peerUsername, ']: ', mapPeers[peerUsername]); + var dataChannel = mapPeers[peerUsername][1]; + console.log('dataChannel: ', dataChannel); + + dataChannels.push(dataChannel); + } + + return dataChannels; +} + +// get all stored RTCPeerConnections +// peerStorageObj is an object (either mapPeers or mapScreenPeers) +function getPeers(peerStorageObj){ + var peers = []; + + for(peerUsername in peerStorageObj){ + var peer = peerStorageObj[peerUsername][0]; + console.log('peer: ', peer); + + peers.push(peer); + } + + return peers; +} + +// for every new peer +// create a new video element +// and its corresponding user gesture button +// assign ids corresponding to the username of the remote peer +function createVideo(peerUsername){ + var videoContainer = document.querySelector('#video-container'); + + // create the new video element + // and corresponding user gesture button + var remoteVideo = document.createElement('video'); + // var btnPlayRemoteVideo = document.createElement('button'); + + remoteVideo.id = peerUsername + '-video'; + remoteVideo.autoplay = true; + remoteVideo.playsinline = true; + // btnPlayRemoteVideo.id = peerUsername + '-btn-play-remote-video'; + // btnPlayRemoteVideo.innerHTML = 'Click here if remote video does not play'; + + // wrapper for the video and button elements + var videoWrapper = document.createElement('div'); + + // add the wrapper to the video container + videoContainer.appendChild(videoWrapper); + + // add the video to the wrapper + videoWrapper.appendChild(remoteVideo); + // videoWrapper.appendChild(btnPlayRemoteVideo); + + // as user gesture + // video is played by button press + // otherwise, some browsers might block video + // btnPlayRemoteVideo.addEventListener("click", function (){ + // remoteVideo.play(); + // btnPlayRemoteVideo.style.visibility = 'hidden'; + // }); + + return remoteVideo; +} + +// set onTrack for RTCPeerConnection +// to add remote tracks to remote stream +// to show video through corresponding remote video element +function setOnTrack(peer, remoteVideo){ + console.log('Setting ontrack:'); + // create new MediaStream for remote tracks + var remoteStream = new MediaStream(); + + // assign remoteStream as the source for remoteVideo + remoteVideo.srcObject = remoteStream; + + console.log('remoteVideo: ', remoteVideo.id); + + peer.addEventListener('track', async (event) => { + console.log('Adding track: ', event.track); + remoteStream.addTrack(event.track, remoteStream); + }); +} + +// called to add appropriate tracks +// to peer +function addLocalTracks(peer, localScreenSharing){ + if(!localScreenSharing){ + // if it is not a screen sharing peer + // add user media tracks + localStream.getTracks().forEach(track => { + console.log('Adding localStream tracks.'); + peer.addTrack(track, localStream); + }); + + return; + } + + // if it is a screen sharing peer + // add display media tracks + localDisplayStream.getTracks().forEach(track => { + console.log('Adding localDisplayStream tracks.'); + peer.addTrack(track, localDisplayStream); + }); +} + +function removeVideo(video){ + // get the video wrapper + var videoWrapper = video.parentNode; + // remove it + videoWrapper.parentNode.removeChild(videoWrapper); +} \ No newline at end of file diff --git a/web/mysite/static/js/peer1.js b/web/mysite/static/js/peer1.js new file mode 100644 index 0000000..aeea0b4 --- /dev/null +++ b/web/mysite/static/js/peer1.js @@ -0,0 +1,264 @@ +var loc = window.location; + +var endPoint = ''; +var wsStart = 'ws://'; + +console.log('protocol: ', loc.protocol); +if(loc.protocol == 'https:'){ + wsStart = 'wss://'; +} + +var endPoint = wsStart + loc.host + loc.pathname; + +var webSocket = new WebSocket(endPoint); + +webSocket.onopen = function(e){ + console.log('Connection opened! ', e); +} + +webSocket.onmessage = webSocketOnMessage; + +webSocket.onclose = function(e){ + console.log('Connection closed! ', e); + + peer1.close(); +} + +webSocket.onerror = function(e){ + console.log('Error occured! ', e); +} + +var btnSendOffer = document.querySelector('#btn-send-offer'); + +btnSendOffer.onclick = btnSendOfferOnClick; + +function webSocketOnMessage(event){ + var parsed_data = JSON.parse(event.data); + + var action = parsed_data['action']; + + if(parsed_data['peer'] == 'peer1'){ + // ignore all messages from oneself + return; + }else if(action == 'peer2-candidate'){ + // probably unreachable + // since peer2 will not send new ice candidates + // but send answer after gathering is complete + + // console.log('Adding new ice candidate.') + peer1.addIceCandidate(parsed_data['message']); + return; + } + + const thisPeer = parsed_data['peer']; + const answer = parsed_data['message']; + console.log('thisPeer: ', thisPeer); + console.log('Answer received: ', answer); + + peer1.setRemoteDescription(answer); +} + +// as user gesture +// video is played by button press +// otherwise, some browsers might block video +btnPlayRemoteVideo = document.querySelector('#btn-play-remote-video'); +btnPlayRemoteVideo.addEventListener("click", function (){ + remoteVideo.play(); + btnPlayRemoteVideo.style.visibility = 'hidden'; +}); + +// send the given action and message strings +// over the websocket connection +function sendSignal(thisPeer, action, message){ + webSocket.send( + JSON.stringify( + { + 'peer': thisPeer, + 'action': action, + 'message': message, + } + ) + ) +} + +function btnSendOfferOnClick(event){ + sendSignal('peer1', 'send-offer', peer1.localDescription); + + btnSendOffer.style.visibility = 'hidden'; +} + +var btnSendMsg = document.querySelector('#btn-send-msg'); +btnSendMsg.onclick = btnSendMsgOnClick; + +function btnSendMsgOnClick(){ + var messageInput = document.querySelector('#msg'); + var message = messageInput.value; + + var li = document.createElement("li"); + li.appendChild(document.createTextNode("Me: " + message)); + ul.appendChild(li); + + console.log('Sending: ', message); + + // send to all data channels + // in multi peer environment + dc.send(message); + + messageInput.value = ''; +} + +const constraints = { + 'video': true, + 'audio': true +} + +const iceConfiguration = { + iceServers: [ + { + urls: ['turn:numb.viagenie.ca'], + credential: '{{numb_turn_credential}}', + username: '{{numb_turn_username}}' + } + ] +}; + +// later assign an RTCPeerConnection +var peer1; +// later assign the datachannel +var dc; + +// true if screen is being shared +// false otherwise +var screenShared = false; + +const localVideo = document.querySelector('#local-video'); +var remoteVideo; + +// later assign button +// to play remote video +// in case browser blocks it +var btnPlayRemoteVideo; + +// button to start or stop screen sharing +var btnShareScreen = document.querySelector('#btn-share-screen'); + +// local video stream +var localStream = new MediaStream(); +// remote video stream; +var remoteStream; + +// local screen stream +// for screen sharing +var localDisplayStream = new MediaStream(); + +// ul of messages +var ul = document.querySelector("#message-list"); + +userMedia = navigator.mediaDevices.getUserMedia(constraints) + .then(stream => { + localStream = stream; + console.log('Got MediaStream:', stream); + mediaTracks = stream.getTracks(); + + for(i=0; i < mediaTracks.length; i++){ + console.log(mediaTracks[i]); + } + + localVideo.srcObject = localStream; + localVideo.muted = true; + + window.stream = stream; // make variable available to browser console + }) + .then(e => { + btnShareScreen.onclick = event => { + if(screenShared){ + // toggle screenShared + screenShared = !screenShared; + + // set to own video + // if screen already shared + localVideo.srcObject = localStream; + btnShareScreen.innerHTML = 'Share screen'; + + return; + } + + // toggle screenShared + screenShared = !screenShared; + + navigator.mediaDevices.getDisplayMedia(constraints) + .then(stream => { + localDisplayStream = stream; + + // set to display stream + // if screen not shared + localVideo.srcObject = localDisplayStream; + }); + + btnShareScreen.innerHTML = 'Stop sharing'; + } + }) + .then(e => { + // perform all offerer activities + createOfferer(); + }) + .catch(error => { + console.error('Error accessing media devices.', error); + }); + +function createOfferer(){ + peer1 = new RTCPeerConnection(null); + + localStream.getTracks().forEach(track => { + peer1.addTrack(track, localStream); + }); + + localDisplayStream.getTracks().forEach(track => { + peer1.addTrack(track, localDisplayStream); + }); + + dc = peer1.createDataChannel("channel"); + dc.onmessage = dcOnMessage + dc.onopen = () => { + console.log("Connection opened."); + + // make play button visible + // upon connection + // to play video in case + // browser blocks it + btnPlayRemoteVideo.style.visibility = 'visible'; + } + + peer1.onicecandidate = (event) => { + if(event.candidate){ + console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer1.localDescription)); + }else{ + console.log('Gathering finished!'); + + // send offer in multi peer environment + // sendSignal('peer1', 'send-offer', peer1.localDescription); + } + } + + remoteStream = new MediaStream(); + remoteVideo = document.querySelector('#remote-video'); + remoteVideo.srcObject = remoteStream; + + peer1.addEventListener('track', async (event) => { + remoteStream.addTrack(event.track, remoteStream); + }); + + peer1.createOffer() + .then(o => peer1.setLocalDescription(o)) + .then(function(event){ + console.log("Local Description Set successfully."); + }); + + function dcOnMessage(event){ + var message = event.data; + + var li = document.createElement("li"); + li.appendChild(document.createTextNode("Other: " + message)); + ul.appendChild(li); + } +} \ No newline at end of file diff --git a/web/mysite/static/js/peer2.js b/web/mysite/static/js/peer2.js new file mode 100644 index 0000000..60a59e4 --- /dev/null +++ b/web/mysite/static/js/peer2.js @@ -0,0 +1,220 @@ +var loc = window.location; + +var endPoint = ''; +var wsStart = 'ws://'; + +if(loc.protocol == 'https:'){ + wsStart = 'wss://'; +} + +var endPoint = wsStart + loc.host + loc.pathname; + +var webSocket = new WebSocket(endPoint); + +webSocket.onopen = function(e){ + console.log('Connection opened! ', e); +} + +webSocket.onmessage = webSocketOnMessage; + +webSocket.onclose = function(e){ + console.log('Connection closed! ', e); +} + +webSocket.onerror = function(e){ + console.log('Error occured! ', e); +} + +function webSocketOnMessage(event){ + var parsed_data = JSON.parse(event.data); + + var action = parsed_data['action']; + + if(parsed_data['peer'] == 'peer2'){ + // ignore all messages from oneself + return; + }else if(action == 'peer1-candidate'){ + peer2.addIceCandidate(parsed_data['message']); + return; + } + + const offer = parsed_data['message']; + // perform all answerer activities + createAnswerer(); + peer2.setRemoteDescription(offer) + .then(function(event){ + console.log('Offer set.'); + + peer2.createAnswer() + .then(a => peer2.setLocalDescription(a)) + .then(function(event){ + console.log('Answer created.'); + // sendSignal('peer2', 'send-answer', peer2.localDescription); + }); + }); + + +} + +// send the given action and message strings +// over the websocket connection +function sendSignal(thisPeer, action, message){ + webSocket.send( + JSON.stringify( + { + 'peer': thisPeer, + 'action': action, + 'message': message, + } + ) + ) +} + +var btnSendMsg = document.querySelector('#btn-send-msg'); +btnSendMsg.onclick = btnSendMsgOnClick; + +var ul = document.querySelector("#message-list"); + +function btnSendMsgOnClick(){ + var messageInput = document.querySelector('#msg'); + var message = messageInput.value; + + var li = document.createElement("li"); + li.appendChild(document.createTextNode("Me: " + message)); + ul.appendChild(li); + + console.log('Sending: ', message); + + peer2.dc.send(message); + + messageInput.value = ''; +} + +function dcOnMessage(event){ + var message = event.data; + + var li = document.createElement("li"); + li.appendChild(document.createTextNode("Other: " + message)); + ul.appendChild(li); +} + +const constraints = { + 'video': true, + 'audio': true +} + +const iceConfiguration = { + iceServers: [ + { + urls: ['turn:numb.viagenie.ca'], + credential: '{{numb_turn_credential}}', + username: '{{numb_turn_username}}' + } + ] +}; + + +// later assign an RTCPeerConnection +var peer2; +// later assign the datachannel +var dc; + +// true if screen is being shared +// false otherwise +var screenShared = false; + +const localVideo = document.querySelector('#local-video'); +var remoteVideo; + +// later assign button +// to play remote video +// in case browser blocks it +var btnPlayRemoteVideo; + +// button to start or stop screen sharing +var btnShareScreen = document.querySelector('#btn-share-screen'); + +// local video stream +var localStream = new MediaStream(); +// remote video stream; +var remoteStream; + +// local screen stream +// for screen sharing +var localDisplayStream = new MediaStream(); + +userMedia = navigator.mediaDevices.getUserMedia(constraints) + .then(stream => { + localStream = stream; + console.log('Got MediaStream:', stream); + mediaTracks = stream.getTracks(); + + for(i=0; i < mediaTracks.length; i++){ + console.log(mediaTracks[i]); + } + + localVideo.srcObject = localStream; + localVideo.muted = true; + + window.stream = stream; // make variable available to browser console + }) + .catch(error => { + console.error('Error accessing media devices.', error); + }); + +function createAnswerer(){ + peer2 = new RTCPeerConnection(null); + + localStream.getTracks().forEach(track => { + peer2.addTrack(track, localStream); + }); + + remoteStream = new MediaStream(); + remoteVideo = document.querySelector('#remote-video'); + remoteVideo.srcObject = remoteStream; + + window.stream = remoteStream; + + peer2.addEventListener('track', async (event) => { + console.log('Adding track: ', event.track); + remoteStream.addTrack(event.track, remoteStream); + }); + + // as user gesture + // video is played by button press + // otherwise, some browsers might block video + btnPlayRemoteVideo = document.querySelector('#btn-play-remote-video'); + btnPlayRemoteVideo.addEventListener("click", function (){ + remoteVideo.play(); + btnPlayRemoteVideo.style.visibility = 'hidden'; + }); + + peer2.onicecandidate = (event) => { + if(event.candidate){ + console.log("New Ice Candidate! Reprinting SDP" + JSON.stringify(peer2.localDescription)); + + // following statement not required anymore + // since answer will be sent after gathering is complete + // sendSignal('peer2', 'peer2-candidate', event.candidate); + }else{ + console.log('Gathering finished!'); + + // send answer in multi peer environment + sendSignal('peer2', 'send-answer', peer2.localDescription); + } + } + + peer2.ondatachannel = e => { + peer2.dc = e.channel; + peer2.dc.onmessage = dcOnMessage; + peer2.dc.onopen = () => { + console.log("Connection opened."); + + // make play button visible + // upon connection + // to play video in case + // browser blocks it + btnPlayRemoteVideo.style.visibility = 'visible'; + } + } +} \ No newline at end of file diff --git a/web/requirements.txt b/web/requirements.txt new file mode 100644 index 0000000..ecda654 --- /dev/null +++ b/web/requirements.txt @@ -0,0 +1,26 @@ +asgiref==3.2.10 +attrs==20.3.0 +autobahn==21.3.1 +Automat==20.2.0 +cffi==1.14.5 +channels==3.0.3 +constantly==15.1.0 +cryptography==3.4.6 +daphne==3.0.1 +Django==3.1 +hyperlink==21.0.0 +idna==3.1 +incremental==21.3.0 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.20 +pyOpenSSL==20.0.1 +python-decouple==3.4 +pytz==2021.1 +service-identity==18.1.0 +six==1.15.0 +sqlparse==0.4.1 +Twisted==21.2.0 +# twisted-iocpsupport==1.0.1 +txaio==21.2.1 +zope.interface==5.2.0