mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
janus: Add new app for lightweight WebRTC server
- Add basic video room based on demo. - Set port range to use for RTP. - coturn: Add component for time-limited TURN configuration. Signed-off-by: James Valleroy <jvalleroy@mailbox.org> [sunil: Don't error out when coturn is not installed/configured] [sunil: Prepend data- to custom attribute in HTML] [sunil: Convert SVG with embedded bitmap to vector graphics] [sunil: Hide Javascript license information in footer] [sunil: Minor changes to comments for styling] Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
4e5835f92a
commit
b7a1d4bf8f
46
actions/janus
Executable file
46
actions/janus
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Configuration helper for Janus server.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
from plinth import action_utils
|
||||
|
||||
JANUS_CONF_PATH = '/etc/janus/janus.jcfg'
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Return parsed command line arguments as dictionary"""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||
subparsers.add_parser('setup', help='Configure Janus server')
|
||||
subparsers.required = True
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def subcommand_setup(_):
|
||||
"""Configure Janus server."""
|
||||
with open(JANUS_CONF_PATH, 'r') as config_file:
|
||||
config_lines = config_file.readlines()
|
||||
|
||||
with open(JANUS_CONF_PATH, 'w') as config_file:
|
||||
for line in config_lines:
|
||||
if '#rtp_port_range' in line:
|
||||
config_file.write("\trtp_port_range = \"50176-51199\"\n")
|
||||
else:
|
||||
config_file.write(line)
|
||||
|
||||
action_utils.service_try_restart('janus')
|
||||
|
||||
|
||||
def main():
|
||||
arguments = parse_arguments()
|
||||
sub_command = arguments.subcommand.replace('-', '_')
|
||||
sub_command_method = globals()['subcommand_' + sub_command]
|
||||
sub_command_method(arguments)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
29
debian/copyright
vendored
29
debian/copyright
vendored
@ -123,6 +123,11 @@ Files: static/themes/default/icons/infinoted.png
|
||||
Copyright: 2008-2014 Armin Burgmeier <armin@arbur.net>
|
||||
License: ISC
|
||||
|
||||
Files: static/themes/default/icons/janus.png
|
||||
static/themes/default/icons/janus.svg
|
||||
Copyright: 2014-2022 Meetecho
|
||||
License: GPL-3 with OpenSSL exception
|
||||
|
||||
Files: static/themes/default/icons/macos.png
|
||||
static/themes/default/icons/macos.svg
|
||||
Copyright: Vectors Market (https://thenounproject.com/vectorsmarket/)
|
||||
@ -2655,6 +2660,30 @@ License: GPL-3
|
||||
On Debian systems you will find a copy of the GPL (version 3) at
|
||||
/usr/share/common-licenses/GPL-3.
|
||||
|
||||
License: GPL-3 with OpenSSL exception
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, version 3.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems you will find a copy of the GPL (version 3) at
|
||||
/usr/share/common-licenses/GPL-3.
|
||||
.
|
||||
If you modify this Program, or any covered work, by linking or
|
||||
combining it with OpenSSL (or a modified version of that library),
|
||||
containing parts covered by the terms of OpenSSL License, the
|
||||
licensors of this Program grant you additional permission to convey
|
||||
the resulting work. Corresponding Source for a non-source form of
|
||||
such a combination shall include the source code for the parts of
|
||||
openssl used as well as that of the covered work.
|
||||
|
||||
License: GPL-3+
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@ -4,12 +4,20 @@
|
||||
|
||||
from __future__ import annotations # Can be removed in Python 3.10
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from time import time
|
||||
|
||||
from plinth import app
|
||||
|
||||
TURN_REST_TTL = 24 * 3600
|
||||
|
||||
TURN_REST_USER = 'fbxturnuser'
|
||||
|
||||
TURN_URI_REGEX = r'(stun|turn):(.*):([0-9]{4})\?transport=(tcp|udp)'
|
||||
|
||||
|
||||
@ -55,6 +63,32 @@ class TurnConfiguration:
|
||||
return all(map(pattern.match, turn_uris))
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserTurnConfiguration(TurnConfiguration):
|
||||
"""Data class to hold per-user TURN server configuration.
|
||||
|
||||
username is a string to identify a specific user and is related to
|
||||
the credential.
|
||||
|
||||
credential is a string generated for this user that must be used
|
||||
by a server to be accepted by Coturn server. It is generated for
|
||||
each user separately upon request and will expire after a set
|
||||
time.
|
||||
|
||||
"""
|
||||
username: str = None
|
||||
credential: str = None
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Return a JSON representation of the configuration."""
|
||||
return json.dumps({
|
||||
'domain': self.domain,
|
||||
'uris': self.uris,
|
||||
'username': self.username,
|
||||
'credential': self.credential
|
||||
})
|
||||
|
||||
|
||||
class TurnConsumer(app.FollowerComponent):
|
||||
"""Component to manage coturn configuration.
|
||||
|
||||
@ -96,3 +130,29 @@ class TurnConsumer(app.FollowerComponent):
|
||||
"""Return current coturn configuration."""
|
||||
from plinth.modules import coturn
|
||||
return coturn.get_config()
|
||||
|
||||
|
||||
class TurnTimeLimitedConsumer(TurnConsumer):
|
||||
"""Component to manage coturn configuration with time-limited
|
||||
credential.
|
||||
|
||||
This component will generate a new credential upon each request,
|
||||
which will expire after 1 day. The shared secret is used to
|
||||
generate the credential, but is not provided in the configuration.
|
||||
|
||||
"""
|
||||
|
||||
def get_configuration(self) -> UserTurnConfiguration:
|
||||
"""Return user coturn configuration."""
|
||||
from plinth.modules import coturn
|
||||
static_config = coturn.get_config()
|
||||
timestamp = int(time()) + TURN_REST_TTL
|
||||
username = str(timestamp) + ':' + TURN_REST_USER
|
||||
credential = None
|
||||
if static_config.shared_secret:
|
||||
digest = hmac.new(bytes(static_config.shared_secret, 'utf-8'),
|
||||
bytes(username, 'utf-8'), hashlib.sha1).digest()
|
||||
credential = base64.b64encode(digest).decode()
|
||||
|
||||
return UserTurnConfiguration(static_config.domain, static_config.uris,
|
||||
None, username, credential)
|
||||
|
||||
@ -10,7 +10,8 @@ import pytest
|
||||
from plinth.utils import random_string
|
||||
|
||||
from .. import notify_configuration_change
|
||||
from ..components import TurnConfiguration, TurnConsumer
|
||||
from ..components import (TurnConfiguration, TurnConsumer,
|
||||
TurnTimeLimitedConsumer, UserTurnConfiguration)
|
||||
|
||||
|
||||
@pytest.fixture(name='turn_configuration')
|
||||
@ -48,13 +49,23 @@ def test_configuration_init():
|
||||
assert config.domain == 'test-domain.example'
|
||||
assert config.uris == ['test-uri1', 'test-uri2']
|
||||
|
||||
config = UserTurnConfiguration('test-domain.example',
|
||||
['test-uri1', 'test-uri2'], None,
|
||||
'test-username', 'test-credential')
|
||||
assert config.domain == 'test-domain.example'
|
||||
assert config.uris == ['test-uri1', 'test-uri2']
|
||||
assert config.shared_secret is None
|
||||
assert config.username == 'test-username'
|
||||
assert config.credential == 'test-credential'
|
||||
|
||||
|
||||
def test_component_init_and_list():
|
||||
"""Test initializing and listing all the components."""
|
||||
component1 = TurnConsumer('component1')
|
||||
component2 = TurnConsumer('component2')
|
||||
component3 = TurnTimeLimitedConsumer('component3')
|
||||
assert component1.component_id == 'component1'
|
||||
assert [component1, component2] == list(TurnConsumer.list())
|
||||
assert [component1, component2, component3] == list(TurnConsumer.list())
|
||||
|
||||
|
||||
@patch('plinth.modules.coturn.get_config')
|
||||
@ -73,3 +84,16 @@ def test_get_configuration(get_config, turn_configuration):
|
||||
get_config.return_value = turn_configuration
|
||||
component = TurnConsumer('component')
|
||||
assert component.get_configuration() == turn_configuration
|
||||
|
||||
|
||||
@patch('plinth.modules.coturn.get_config')
|
||||
def test_get_user_configuration(get_config, turn_configuration):
|
||||
"""Test coturn user configuration retrieval using component."""
|
||||
get_config.return_value = turn_configuration
|
||||
component = TurnTimeLimitedConsumer('component')
|
||||
user_config = component.get_configuration()
|
||||
assert user_config.domain == turn_configuration.domain
|
||||
assert user_config.uris == turn_configuration.uris
|
||||
assert user_config.shared_secret is None
|
||||
assert user_config.username is not None
|
||||
assert user_config.credential is not None
|
||||
|
||||
94
plinth/modules/janus/__init__.py
Normal file
94
plinth/modules/janus/__init__.py
Normal file
@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
FreedomBox app for janus.
|
||||
"""
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import actions
|
||||
from plinth import app as app_module
|
||||
from plinth import frontpage, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.coturn.components import TurnTimeLimitedConsumer
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import manifest
|
||||
|
||||
_description = [
|
||||
_('Janus is a lightweight WebRTC server.'),
|
||||
_('A simple video conference room is included.'),
|
||||
_('A STUN/TURN server (such as Coturn) is required to use Janus.'),
|
||||
]
|
||||
|
||||
app = None
|
||||
|
||||
|
||||
class JanusApp(app_module.App):
|
||||
"""FreedomBox app for janus."""
|
||||
|
||||
app_id = 'janus'
|
||||
|
||||
_version = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(self.app_id, self._version, name=_('Janus'),
|
||||
icon_filename='janus',
|
||||
short_description=_('WebRTC server'),
|
||||
description=_description, manual_page='Janus',
|
||||
clients=manifest.clients)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-janus', info.name, info.short_description,
|
||||
info.icon_filename, 'janus:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-janus', info.name,
|
||||
info.short_description,
|
||||
info.icon_filename,
|
||||
reverse_lazy('janus:room'),
|
||||
clients=manifest.clients)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-janus', [
|
||||
'janus', 'libjs-bootbox', 'libjs-bootstrap', 'libjs-bootswatch',
|
||||
'libjs-janus-gateway', 'libjs-jquery-blockui', 'libjs-spin.js',
|
||||
'libjs-toastr', 'libjs-webrtc-adapter'
|
||||
])
|
||||
self.add(packages)
|
||||
|
||||
firewall = Firewall('firewall-janus', info.name,
|
||||
ports=['http', 'https',
|
||||
'janus-freedombox'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
webserver = Webserver('webserver-janus', 'janus-freedombox')
|
||||
self.add(webserver)
|
||||
|
||||
daemon = Daemon(
|
||||
'daemon-janus', 'janus', listen_ports=[(8088, 'tcp4'),
|
||||
(8088, 'tcp6'),
|
||||
(8188, 'tcp4'),
|
||||
(8188, 'tcp6')])
|
||||
self.add(daemon)
|
||||
|
||||
turn = TurnTimeLimitedConsumer('turn-janus')
|
||||
self.add(turn)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-janus',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the app."""
|
||||
app.setup(old_version)
|
||||
actions.superuser_run('janus', ['setup'])
|
||||
helper.call('post', app.enable)
|
||||
@ -0,0 +1,13 @@
|
||||
##
|
||||
## On all sites, provide Janus web and websockets APIs.
|
||||
##
|
||||
|
||||
<Location /janus>
|
||||
ProxyPass http://127.0.0.1:8088/janus retry=0
|
||||
ProxyPassReverse http://127.0.0.1:8088/janus
|
||||
</Location>
|
||||
|
||||
<Location /janus-ws>
|
||||
ProxyPass ws://127.0.0.1:8188 retry=0
|
||||
ProxyPassReverse ws://127.0.0.1:8188
|
||||
</Location>
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.janus
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<service>
|
||||
<short>Janus WebRTC server/gateway</short>
|
||||
<description>Janus is a minimal, general purpose WebRTC server/gateway. It provides the means to set up a WebRTC media communication with a browser or application, exchanging JSON messages with it over different transports, and relaying RTP/RTCP and messages between clients and the server-side application logic they're attached to. Any specific feature/application is provided by server side plugins.</description>
|
||||
<port protocol="udp" port="50176-51199"/>
|
||||
</service>
|
||||
14
plinth/modules/janus/manifest.py
Normal file
14
plinth/modules/janus/manifest.py
Normal file
@ -0,0 +1,14 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
clients = [{
|
||||
'name': _('Janus Video Room'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': reverse_lazy('janus:room')
|
||||
}]
|
||||
}]
|
||||
|
||||
backup = {}
|
||||
32
plinth/modules/janus/static/janus-freedombox-config.js
Normal file
32
plinth/modules/janus/static/janus-freedombox-config.js
Normal file
@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*
|
||||
#
|
||||
# This file is part of FreedomBox.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
*/
|
||||
|
||||
var server = "/janus";
|
||||
var iceServers = null;
|
||||
var token = "";
|
||||
var apisecret = "";
|
||||
|
||||
$(document).ready(function() {
|
||||
const body = document.querySelector('body');
|
||||
const config = JSON.parse(body.getAttribute('data-user-turn-config'));
|
||||
iceServers = [{urls: config['uris'],
|
||||
username: config['username'],
|
||||
credential: config['credential']}];
|
||||
});
|
||||
170
plinth/modules/janus/static/janus-video-room.css
Normal file
170
plinth/modules/janus/static/janus-video-room.css
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# This file based on example code from Janus Demos which is licensed as
|
||||
# follows.
|
||||
#
|
||||
# 2014-2022 Meetecho
|
||||
#
|
||||
# GPL-3 with OpenSSL exception
|
||||
# If you modify this Program, or any covered work,
|
||||
# by linking or combining it with OpenSSL
|
||||
# (or a modified version of that library),
|
||||
# containing parts covered by the terms of OpenSSL License,
|
||||
# the licensors of this Program grant you
|
||||
# additional permission to convey the resulting work.
|
||||
# Corresponding Source for a non-source form of such a combination
|
||||
# shall include the source code for the parts of openssl used
|
||||
# as well as that of the covered work.
|
||||
*/
|
||||
|
||||
.footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
.navbar-default {
|
||||
-webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);
|
||||
-moz-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);
|
||||
box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.margin-sm {
|
||||
margin: 5px !important;
|
||||
}
|
||||
.margin-md {
|
||||
margin: 10px !important;
|
||||
}
|
||||
.margin-xl {
|
||||
margin: 20px !important;
|
||||
}
|
||||
.margin-bottom-sm {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.margin-bottom-md {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
.margin-bottom-xl {
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.divider hr {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
div.no-video-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.no-video-icon {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-video-text {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.no-video-text-sm {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.meetecho-logo {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.meetecho-logo > img {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.januscon {
|
||||
font-weight: bold;
|
||||
animation: pulsating 1s infinite;
|
||||
}
|
||||
@keyframes pulsating {
|
||||
30% {
|
||||
color: #FFD700;
|
||||
}
|
||||
}
|
||||
|
||||
.mute-button {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.unpublish-button {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.resolution-label {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.bitrate-label {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.simulcast-button-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.simulcast-button {
|
||||
width: 33%;
|
||||
}
|
||||
1015
plinth/modules/janus/static/janus-video-room.js
Normal file
1015
plinth/modules/janus/static/janus-video-room.js
Normal file
File diff suppressed because it is too large
Load Diff
209
plinth/modules/janus/templates/janus_video_room.html
Normal file
209
plinth/modules/janus/templates/janus_video_room.html
Normal file
@ -0,0 +1,209 @@
|
||||
{% comment %}
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# This file based on example code from Janus which is
|
||||
# licensed as follows.
|
||||
#
|
||||
# 2014-2022 Meetecho
|
||||
#
|
||||
# GPL-3 with OpenSSL exception
|
||||
# If you modify this Program, or any covered work,
|
||||
# by linking or combining it with OpenSSL
|
||||
# (or a modified version of that library),
|
||||
# containing parts covered by the terms of OpenSSL License,
|
||||
# the licensors of this Program grant you
|
||||
# additional permission to convey the resulting work.
|
||||
# Corresponding Source for a non-source form of such a combination
|
||||
# shall include the source code for the parts of openssl used
|
||||
# as well as that of the covered work.
|
||||
{% endcomment %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Janus Video Room</title>
|
||||
|
||||
<script type="text/javascript"
|
||||
src="/javascript/webrtc-adapter/adapter.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/jquery/jquery.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/jquery-blockui/jquery.blockUI.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/bootstrap/js/bootstrap.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/bootbox/bootbox.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/spin.js/spin.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/toastr/toastr.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="{% static 'janus/janus-freedombox-config.js' %}" ></script>
|
||||
<script type="text/javascript"
|
||||
src="/javascript/janus-gateway/janus.min.js" ></script>
|
||||
<script type="text/javascript"
|
||||
src="{% static 'janus/janus-video-room.js' %}" ></script>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="/javascript/bootswatch/cerulean/bootstrap.min.css"
|
||||
type="text/css"/>
|
||||
<link rel="stylesheet" href="/javascript/toastr/toastr.min.css"
|
||||
type="text/css"/>
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'janus/janus-video-room.css' %}" type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body data-user-turn-config="{{ user_turn_config }}">
|
||||
<div class="container" id="content" role="main">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Janus Video Room
|
||||
<button class="btn btn-default" autocomplete="off"
|
||||
id="start">Start</button>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="container" id="details">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Details</h3>
|
||||
<p>To use the video room, just insert a username to join
|
||||
the default room that is configured. This will add you
|
||||
to the list of participants, and allow you to
|
||||
automatically send your audio/video frames and receive
|
||||
the other participants' feeds. The other participants
|
||||
will appear in separate panels, whose title will be
|
||||
the names they chose when registering at the video room.</p>
|
||||
<p>Press the <code>Start</code> button above to launch
|
||||
the video room.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container hide" id="videojoin">
|
||||
<div class="row">
|
||||
<span class="label label-info" id="you"></span>
|
||||
<div class="col-md-12" id="controls">
|
||||
<div class="input-group margin-bottom-md hide" id="registernow">
|
||||
<span class="input-group-addon">@</span>
|
||||
<input autocomplete="off" class="form-control"
|
||||
autocomplete="off" type="text"
|
||||
placeholder="Choose a display name"
|
||||
id="username"
|
||||
onkeypress="return checkEnter(this, event);"></input>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-success" autocomplete="off"
|
||||
id="register">Join the room</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container hide" id="videos">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Local Video <span class="label label-primary hide"
|
||||
id="publisher"></span>
|
||||
<div class="btn-group btn-group-xs pull-right hide">
|
||||
<div class="btn-group btn-group-xs">
|
||||
<button id="bitrateset" autocomplete="off"
|
||||
class="btn btn-primary
|
||||
dropdown-toggle"
|
||||
data-toggle="dropdown">
|
||||
Bandwidth<span class="caret"></span>
|
||||
</button>
|
||||
<ul id="bitrate" class="dropdown-menu" role="menu">
|
||||
<li><a href="#" id="0">No limit</a></li>
|
||||
<li><a href="#" id="128">Cap to 128kbit</a></li>
|
||||
<li><a href="#" id="256">Cap to 256kbit</a></li>
|
||||
<li><a href="#" id="512">Cap to 512kbit</a></li>
|
||||
<li><a href="#" id="1024">Cap to 1mbit</a></li>
|
||||
<li><a href="#" id="1500">Cap to 1.5mbit</a></li>
|
||||
<li><a href="#" id="2000">Cap to 2mbit</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body" id="videolocal"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Remote Video #1 <span class="label label-info hide"
|
||||
id="remote1"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body relative" id="videoremote1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Remote Video #2 <span class="label label-info hide"
|
||||
id="remote2"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body relative" id="videoremote2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Remote Video #3 <span class="label label-info hide"
|
||||
id="remote3"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body relative" id="videoremote3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Remote Video #4 <span class="label label-info hide"
|
||||
id="remote4"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body relative" id="videoremote4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Remote Video #5 <span class="label label-info hide"
|
||||
id="remote5"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body relative" id="videoremote5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a href="{% static 'jslicense.html' %}" data-jslicense="1">
|
||||
{% trans "JavaScript license information" %}</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
0
plinth/modules/janus/tests/__init__.py
Normal file
0
plinth/modules/janus/tests/__init__.py
Normal file
16
plinth/modules/janus/tests/test_functional.py
Normal file
16
plinth/modules/janus/tests/test_functional.py
Normal file
@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Functional, browser based tests for Janus app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from plinth.tests import functional
|
||||
|
||||
pytestmark = [pytest.mark.apps, pytest.mark.janus]
|
||||
|
||||
|
||||
class TestJanusApp(functional.BaseAppTests):
|
||||
app_name = 'janus'
|
||||
has_service = True
|
||||
has_web = True
|
||||
17
plinth/modules/janus/urls.py
Normal file
17
plinth/modules/janus/urls.py
Normal file
@ -0,0 +1,17 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
URLs for the janus app.
|
||||
"""
|
||||
|
||||
from django.urls import re_path
|
||||
from stronghold.decorators import public
|
||||
|
||||
from plinth.views import AppView
|
||||
|
||||
from .views import JanusRoomView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^apps/janus/$', AppView.as_view(app_id='janus'), name='index'),
|
||||
re_path(r'^apps/janus/room/$', public(JanusRoomView.as_view()),
|
||||
name='room')
|
||||
]
|
||||
20
plinth/modules/janus/views.py
Normal file
20
plinth/modules/janus/views.py
Normal file
@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Views for the Janus app.
|
||||
"""
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from plinth.modules import janus
|
||||
|
||||
|
||||
class JanusRoomView(TemplateView):
|
||||
"""A simple page to host Janus video room."""
|
||||
template_name = 'janus_video_room.html'
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
"""Add user's TURN server information to view context."""
|
||||
config = janus.app.get_component('turn-janus').get_configuration()
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['user_turn_config'] = config.to_json()
|
||||
return context
|
||||
@ -36,6 +36,7 @@ markers = [
|
||||
"i2p",
|
||||
"ikiwiki",
|
||||
"infinoted",
|
||||
"janus",
|
||||
"jsxc",
|
||||
"matrixsynapse",
|
||||
"mediawiki",
|
||||
|
||||
BIN
static/themes/default/icons/janus.png
Normal file
BIN
static/themes/default/icons/janus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
87
static/themes/default/icons/janus.svg
Normal file
87
static/themes/default/icons/janus.svg
Normal file
@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg60"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512.00001 512.00001"
|
||||
sodipodi:docname="janus.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata66">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs64" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1846"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview62"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.2118871"
|
||||
inkscape:cx="217.01691"
|
||||
inkscape:cy="305.72154"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g68"
|
||||
inkscape:pagecheckerboard="0" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Image"
|
||||
id="g68">
|
||||
<path
|
||||
id="rect1364"
|
||||
style="display:inline;fill:#0071bc;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:7;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 167.07289,203.07798 v 26.49811 l -57.58467,39.36696 v 84.41487 l 57.58467,-36.89065 v 113.79689 h 81.6724 V 203.07798 Z"
|
||||
sodipodi:nodetypes="ccccccccc" />
|
||||
<path
|
||||
id="path13100"
|
||||
style="display:inline;fill:#00ccff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:7;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 344.2499,202.90079 v 26.49811 l 56.34207,-26.32092 -4e-5,81.6377 -56.34203,31.57441 V 430.08698 H 262.57749 V 202.90079 Z"
|
||||
sodipodi:nodetypes="ccccccccc" />
|
||||
<ellipse
|
||||
style="display:inline;fill:#00ccff;fill-opacity:1;stroke:#ffffff;stroke-width:7;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path16994"
|
||||
cx="304.2576"
|
||||
cy="137.60036"
|
||||
rx="55.329285"
|
||||
ry="55.610538" />
|
||||
<ellipse
|
||||
style="display:inline;fill:#0071bc;fill-opacity:1;stroke:#ffffff;stroke-width:7;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="ellipse17076"
|
||||
cx="208.16225"
|
||||
cy="136.78899"
|
||||
rx="55.329285"
|
||||
ry="55.610538" />
|
||||
<ellipse
|
||||
style="display:inline;fill:none;fill-opacity:1;stroke:#0071bc;stroke-width:27.7001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path19871"
|
||||
cy="256.00003"
|
||||
cx="256"
|
||||
rx="242.14993"
|
||||
ry="242.14996" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
Loading…
x
Reference in New Issue
Block a user