@@ -0,0 +1,61 @@ | |||
# Byte-compiled / optimized / DLL files | |||
__pycache__/ | |||
*.py[cod] | |||
# C extensions | |||
*.so | |||
# Distribution / packaging | |||
.Python | |||
env/ | |||
build/ | |||
develop-eggs/ | |||
dist/ | |||
downloads/ | |||
eggs/ | |||
.eggs/ | |||
lib/ | |||
lib64/ | |||
parts/ | |||
sdist/ | |||
var/ | |||
*.egg-info/ | |||
.installed.cfg | |||
*.egg | |||
# PyInstaller | |||
# Usually these files are written by a python script from a template | |||
# before PyInstaller builds the exe, so as to inject date/other infos into it. | |||
*.manifest | |||
*.spec | |||
# Installer logs | |||
pip-log.txt | |||
pip-delete-this-directory.txt | |||
# Unit test / coverage reports | |||
htmlcov/ | |||
.tox/ | |||
.coverage | |||
.coverage.* | |||
.cache | |||
nosetests.xml | |||
coverage.xml | |||
*,cover | |||
# Translations | |||
*.mo | |||
*.pot | |||
# Django stuff: | |||
*.log | |||
local_settings.py | |||
# Sphinx documentation | |||
docs/_build/ | |||
# PyBuilder | |||
target/ | |||
# sqlite database | |||
*.sqlite3 |
@@ -0,0 +1,3 @@ | |||
from django.contrib import admin | |||
# Register your models here. |
@@ -0,0 +1,5 @@ | |||
from django.apps import AppConfig | |||
class CameraConfig(AppConfig): | |||
name = 'camera' |
@@ -0,0 +1,42 @@ | |||
from channels.generic.websocket import AsyncWebsocketConsumer | |||
class VideoConsumer(AsyncWebsocketConsumer): | |||
async def connect(self): | |||
self.room_name = self.scope['url_route']['kwargs']['room_name'] | |||
self.kind = self.scope['url_route']['kwargs']['kind'] | |||
self.listen_group_name = '%s_%s' % (self.kind, self.room_name) | |||
other_kind = 'client' if self.kind == 'host' else 'host' | |||
self.send_group_name = '%s_%s' % (other_kind, self.room_name) | |||
# Join room group | |||
await self.channel_layer.group_add( | |||
self.listen_group_name, | |||
self.channel_name | |||
) | |||
await self.accept() | |||
async def disconnect(self, close_code): | |||
# Leave room group | |||
await self.channel_layer.group_discard( | |||
self.listen_group_name, | |||
self.channel_name | |||
) | |||
# Receive message from WebSocket | |||
async def receive(self, text_data): | |||
await self.channel_layer.group_send( | |||
self.send_group_name, | |||
{ | |||
'type': 'chat_message', | |||
'message': text_data | |||
} | |||
) | |||
# Receive message from room group | |||
async def chat_message(self, event): | |||
message = event['message'] | |||
# Send message to WebSocket | |||
await self.send(text_data=message) |
@@ -0,0 +1,3 @@ | |||
from django.db import models | |||
# Create your models here. |
@@ -0,0 +1,8 @@ | |||
from django.urls import re_path | |||
from . import consumers | |||
websocket_urlpatterns = [ | |||
re_path(r'camera/ws/(?P<kind>host|client)/(?P<room_name>\w+)/$', | |||
consumers.VideoConsumer), | |||
] |
@@ -0,0 +1,173 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="utf-8"/> | |||
<title>Simple WebRTC</title> | |||
<style> | |||
body.justVideo { | |||
margin: 0; | |||
overflow: hidden; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<a id="qrcodelink" style="display:none;"><img id="qrcode" /></a> | |||
<span id="status"></span> | |||
<div id="videos" style="display:none;"> | |||
<video id="remoteView" width="100%" autoplay muted></video> | |||
<video id="selfView" width="200" height="150" autoplay></video> | |||
</div> | |||
<script > | |||
const create = (container, type) => container.appendChild(document.createElement(type)); | |||
const body = document.querySelector("body"); | |||
const out = document.getElementById("status"); | |||
const qrcode = document.getElementById("qrcode"); | |||
const qrcodelink = document.getElementById("qrcodelink"); | |||
const remoteView = document.getElementById("remoteView"); | |||
remoteView.style.display = 'none'; | |||
out.innerText += "Loading...\n"; | |||
function getRoomName() { | |||
JSON.parse(document.getElementById('room-name').textContent); | |||
} | |||
let roomName = window.location.hash; | |||
let isHost = roomName === undefined || !roomName; | |||
if (isHost) { | |||
// From https://stackoverflow.com/a/1349426 | |||
function makeid(length) { | |||
var result = ''; | |||
var characters = 'abcdefghijklmnopqrstuvwxyz'; | |||
var charactersLength = characters.length; | |||
for (var i = 0; i < length; i++) { | |||
result += characters.charAt(Math.floor(Math.random() * charactersLength)); | |||
} | |||
return result; | |||
} | |||
roomName = makeid(8); | |||
qrcodelink.href = window.location.href.split('#')[0] + '#' + roomName; | |||
qrcode.src = window.location.href.split('#')[0] + roomName + '/qr'; | |||
qrcodelink.style.display = ''; | |||
} else { | |||
roomName = roomName.substring(1); | |||
qrcodelink.style.display = 'none'; | |||
} | |||
out.innerText += "Room: " + roomName + "\n"; | |||
var webSocket = undefined; | |||
function sendJson(data) { | |||
const toSend = JSON.stringify(data); | |||
out.innerText += "Sending message...\n"; | |||
create(out, 'pre').innerText = toSend.split('\\r\\n').join('\r\n'); | |||
create(out, 'br'); | |||
webSocket.send(toSend); | |||
} | |||
var pc = undefined; | |||
function createRTCPeerConnection() { | |||
const pc = new RTCPeerConnection(); | |||
out.innerText += "Created RTCPeerConnection.\n"; | |||
pc.onicecandidate = ({candidate}) => sendJson({candidate}); | |||
// let the "negotiationneeded" event trigger offer generation | |||
pc.onnegotiationneeded = async function () { | |||
out.innerText += "In pc.onnegotiationneeded...\n"; | |||
await pc.setLocalDescription(await pc.createOffer()); | |||
sendJson({ | |||
description: pc.localDescription | |||
}); | |||
} | |||
pc.ontrack = ({streams: [stream]}) => { | |||
out.innerText += "In pc.ontrack...\n"; | |||
remoteView.srcObject = stream; | |||
remoteView.style.display = ''; | |||
remoteView.play(); | |||
out.innerText += "Set srcObject\n"; | |||
out.style.display = 'none'; | |||
videos.style.display = ''; | |||
body.classList.add('justVideo'); | |||
}; | |||
return pc; | |||
} | |||
async function receiveMessage(e) { | |||
if (pc === undefined) pc = createRTCPeerConnection(); | |||
qrcode.style.display = 'none'; | |||
out.innerText += "In webSocket.onmessage...\n"; | |||
create(out, 'pre').innerText = e.data.split('\\r\\n').join('\r\n'); | |||
create(out, 'br'); | |||
const data = JSON.parse(e.data); | |||
if (data.description) { | |||
await pc.setRemoteDescription(data.description); | |||
if (data.description.type == "offer") { | |||
out.innerText += "Got an offer...\n"; | |||
await pc.setLocalDescription(await pc.createAnswer()); | |||
sendJson({ | |||
description: pc.localDescription | |||
}); | |||
} | |||
} else if (data.candidate) { | |||
out.innerText += "Adding ice candidate...\n"; | |||
await pc.addIceCandidate(data.candidate); | |||
} | |||
}; | |||
function createWebSocket() { | |||
const webSocket = new WebSocket( | |||
'ws' + (window.location.protocol == 'https:' ? 's' : '') + '://' | |||
+ window.location.host | |||
+ '/camera/ws/' + (isHost ? 'host' : 'client') + '/' | |||
+ roomName | |||
+ '/' | |||
); | |||
out.innerText += "Created WebSocket.\n"; | |||
webSocket.onclose = function(e) { | |||
console.error('Web socket closed unexpectedly'); | |||
}; | |||
webSocket.onmessage = receiveMessage; | |||
return webSocket; | |||
} | |||
webSocket = createWebSocket(); | |||
// get a local stream, show it in a self-view and add it to be sent | |||
async function startStreaming() { | |||
pc = createRTCPeerConnection(); | |||
const videoConstraints = { advanced: [{facingMode: "environment"}] }; | |||
out.innerText += "Created videoConstraints.\n"; | |||
const stream = await navigator.mediaDevices.getUserMedia({ "audio": false, "video": videoConstraints }); | |||
out.innerText += "Created stream.\n"; | |||
selfView.srcObject = stream; | |||
for (const track of stream.getTracks()) { | |||
out.innerText += "Added track.\n"; | |||
pc.addTrack(track, stream); | |||
} | |||
} | |||
if (!isHost) { | |||
startStreaming() | |||
.then(() => { | |||
out.innerText += "startStreaming() finished.\n"; | |||
}) | |||
.catch(e => { | |||
out.innerText += "startStreaming() errored: " + e.message + "\n"; | |||
}) | |||
; | |||
} | |||
out.innerText += "Finished <script> block.\n"; | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,3 @@ | |||
from django.test import TestCase | |||
# Create your tests here. |
@@ -0,0 +1,8 @@ | |||
from django.urls import path | |||
from . import views | |||
urlpatterns = [ | |||
path('camera/', views.camera, name='camera'), | |||
path('camera/<str:room_name>/qr', views.camera_qr, name='camera_qr'), | |||
] |
@@ -0,0 +1,19 @@ | |||
from io import BytesIO | |||
import qrcode | |||
from django.http import HttpResponse | |||
from django.shortcuts import render | |||
from django.urls import reverse | |||
def camera(request): | |||
return render(request, 'index.html') | |||
def camera_qr(request, room_name): | |||
camera_url = reverse('camera') | |||
camera_url = request.build_absolute_uri(camera_url) + '#' + room_name | |||
img = qrcode.make(camera_url) | |||
output = BytesIO() | |||
img.save(output, "PNG") | |||
return HttpResponse(output.getvalue(), content_type='image/png') |
@@ -0,0 +1,12 @@ | |||
""" | |||
ASGI entrypoint. Configures Django and then runs the application | |||
defined in the ASGI_APPLICATION setting. | |||
""" | |||
import os | |||
import django | |||
from channels.routing import get_default_application | |||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "camera_site.settings") | |||
django.setup() | |||
application = get_default_application() |
@@ -0,0 +1,9 @@ | |||
from channels.routing import ProtocolTypeRouter, URLRouter | |||
import camera.routing | |||
application = ProtocolTypeRouter({ | |||
# (http->django views is added by default) | |||
'websocket': URLRouter( | |||
camera.routing.websocket_urlpatterns | |||
), | |||
}) |
@@ -0,0 +1,94 @@ | |||
""" | |||
Django settings for camera_site project. | |||
Generated by 'django-admin startproject' using Django 3.0.4. | |||
For more information on this file, see | |||
https://docs.djangoproject.com/en/3.0/topics/settings/ | |||
For the full list of settings and their values, see | |||
https://docs.djangoproject.com/en/3.0/ref/settings/ | |||
""" | |||
import os | |||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) | |||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||
# Quick-start development settings - unsuitable for production | |||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ | |||
# SECURITY WARNING: keep the secret key used in production secret! | |||
SECRET_KEY = 'xh9dx24&9r8yx!@@qyjn@b7z%b-_mc&@itrv&52z#8@=08sdlr' | |||
# SECURITY WARNING: don't run with debug turned on in production! | |||
DEBUG = False | |||
ALLOWED_HOSTS = [] | |||
# Application definition | |||
INSTALLED_APPS = [ | |||
'channels', | |||
'camera', | |||
'django.contrib.contenttypes', | |||
'django.contrib.staticfiles', | |||
] | |||
MIDDLEWARE = [ | |||
'django.middleware.security.SecurityMiddleware', | |||
'django.middleware.common.CommonMiddleware', | |||
'django.middleware.csrf.CsrfViewMiddleware', | |||
'django.middleware.clickjacking.XFrameOptionsMiddleware', | |||
] | |||
ROOT_URLCONF = 'camera_site.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', | |||
], | |||
}, | |||
}, | |||
] | |||
WSGI_APPLICATION = 'camera_site.wsgi.application' | |||
# Internationalization | |||
# https://docs.djangoproject.com/en/3.0/topics/i18n/ | |||
LANGUAGE_CODE = 'en-us' | |||
TIME_ZONE = 'UTC' | |||
USE_I18N = True | |||
USE_L10N = True | |||
USE_TZ = True | |||
# Static files (CSS, JavaScript, Images) | |||
# https://docs.djangoproject.com/en/3.0/howto/static-files/ | |||
STATIC_URL = '/static/' | |||
ASGI_APPLICATION = 'camera_site.routing.application' | |||
CHANNEL_LAYERS = { | |||
'default': { | |||
'BACKEND': 'channels_redis.core.RedisChannelLayer', | |||
'CONFIG': { | |||
"hosts": [('127.0.0.1', 6379)], | |||
}, | |||
}, | |||
} | |||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') |
@@ -0,0 +1,21 @@ | |||
"""camera_site URL Configuration | |||
The `urlpatterns` list routes URLs to views. For more information please see: | |||
https://docs.djangoproject.com/en/3.0/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.conf.urls import include | |||
from django.urls import path | |||
urlpatterns = [ | |||
path('', include('camera.urls')), | |||
] |
@@ -0,0 +1,21 @@ | |||
#!/usr/bin/env python | |||
"""Django's command-line utility for administrative tasks.""" | |||
import os | |||
import sys | |||
def main(): | |||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'camera_site.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() |
@@ -0,0 +1,5 @@ | |||
#!/bin/sh | |||
export DJANGO_SETTINGS_MODULE=camera_site.local_settings | |||
daphne -u site.sock camera_site.asgi:application | |||
@@ -0,0 +1 @@ | |||
chromium --incognito --app=https://localhost/viewer/lobby/ |