Browse Source

Initial working version.

tags/blog-initial
Daniel Perelman 1 month ago
commit
5c6ca82c13
20 changed files with 488 additions and 0 deletions
  1. +61
    -0
      .gitignore
  2. +0
    -0
      camera/__init__.py
  3. +3
    -0
      camera/admin.py
  4. +5
    -0
      camera/apps.py
  5. +42
    -0
      camera/consumers.py
  6. +0
    -0
      camera/migrations/__init__.py
  7. +3
    -0
      camera/models.py
  8. +8
    -0
      camera/routing.py
  9. +173
    -0
      camera/templates/index.html
  10. +3
    -0
      camera/tests.py
  11. +8
    -0
      camera/urls.py
  12. +19
    -0
      camera/views.py
  13. +0
    -0
      camera_site/__init__.py
  14. +12
    -0
      camera_site/asgi.py
  15. +9
    -0
      camera_site/routing.py
  16. +94
    -0
      camera_site/settings.py
  17. +21
    -0
      camera_site/urls.py
  18. +21
    -0
      manage.py
  19. +5
    -0
      run_daphne.sh
  20. +1
    -0
      run_viewer.sh

+ 61
- 0
.gitignore View File

@@ -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
camera/__init__.py View File


+ 3
- 0
camera/admin.py View File

@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.

+ 5
- 0
camera/apps.py View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CameraConfig(AppConfig):
name = 'camera'

+ 42
- 0
camera/consumers.py View File

@@ -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
camera/migrations/__init__.py View File


+ 3
- 0
camera/models.py View File

@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.

+ 8
- 0
camera/routing.py View File

@@ -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),
]

+ 173
- 0
camera/templates/index.html View File

@@ -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>

+ 3
- 0
camera/tests.py View File

@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.

+ 8
- 0
camera/urls.py View File

@@ -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'),
]

+ 19
- 0
camera/views.py View File

@@ -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
camera_site/__init__.py View File


+ 12
- 0
camera_site/asgi.py View File

@@ -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()

+ 9
- 0
camera_site/routing.py View File

@@ -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
),
})

+ 94
- 0
camera_site/settings.py View File

@@ -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')

+ 21
- 0
camera_site/urls.py View File

@@ -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')),
]

+ 21
- 0
manage.py View File

@@ -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()

+ 5
- 0
run_daphne.sh View File

@@ -0,0 +1,5 @@
#!/bin/sh

export DJANGO_SETTINGS_MODULE=camera_site.local_settings
daphne -u site.sock camera_site.asgi:application


+ 1
- 0
run_viewer.sh View File

@@ -0,0 +1 @@
chromium --incognito --app=https://localhost/viewer/lobby/

Loading…
Cancel
Save