mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-13 10:30:16 +00:00
Compare commits
12 Commits
91de3e6e3b
...
6c3b2e1f82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c3b2e1f82 | ||
|
|
b6bade7d06 | ||
|
|
94c344573b | ||
|
|
32520c7c89 | ||
|
|
2467d6a033 | ||
|
|
3eef1d9324 | ||
|
|
7d38f49dd8 | ||
|
|
601de6d0e3 | ||
|
|
cdfdacabad | ||
|
|
d4c4900b1d | ||
|
|
8f87d658a6 | ||
|
|
117932e66f |
@ -47,48 +47,6 @@
|
|||||||
RedirectMatch "^/$" "/plinth"
|
RedirectMatch "^/$" "/plinth"
|
||||||
</IfFile>
|
</IfFile>
|
||||||
|
|
||||||
##
|
|
||||||
## Disable sending Referer (sic) header from FreedomBox web interface to
|
|
||||||
## external websites. This improves privacy by not disclosing FreedomBox
|
|
||||||
## domains/URLs to external domains. Apps such as blogs which want to popularize
|
|
||||||
## themselves with referrer header may still do so.
|
|
||||||
##
|
|
||||||
## A strict Content Security Policy.
|
|
||||||
## - @fonts are allowed only from FreedomBox itself.
|
|
||||||
## - <frame>/<iframe> sources are disabled.
|
|
||||||
## - <img> sources are allowed only from FreedomBox itself.
|
|
||||||
## - Manifest file is not allowed as there is none yet.
|
|
||||||
## - <audio>, <video>, <track> tags are not allowed yet.
|
|
||||||
## - <object>, <embed>, <applet> tags are not allowed yet.
|
|
||||||
## - Allow JS from FreedomBox itself (no inline and attribute scripts).
|
|
||||||
## - Allow inline CSS and CSS files from Freedombox itself.
|
|
||||||
## - Web worker sources are allowed only from FreedomBox itself (for JSXC).
|
|
||||||
## - All other fetch sources including Ajax are not allowed from FreedomBox
|
|
||||||
## itself.
|
|
||||||
## - <base> tag is not allowed.
|
|
||||||
## - No plugins types are alllowed since object-src is 'none'.
|
|
||||||
## - Form action should be to FreedomBox itself.
|
|
||||||
## - This interface may be not embedded in <frame>, <iframe>, etc. tags.
|
|
||||||
## - When serving HTTPS, don't allow HTTP assets.
|
|
||||||
##
|
|
||||||
## Enable strict sandboxing enabled with some exceptions:
|
|
||||||
## - Allow running Javascript.
|
|
||||||
## - Allow popups as sometimes we use <a target=_blank>
|
|
||||||
## - Allow popups to have different sandbox requirements as we launch apps' web
|
|
||||||
## clients.
|
|
||||||
## - Allow forms to support configuration forms.
|
|
||||||
## - Allow policies to treat same origin differently from other origins
|
|
||||||
## - Allow downloads such as backup tarballs.
|
|
||||||
##
|
|
||||||
## Disable browser guessing of MIME types. FreedoBox already sets good content
|
|
||||||
## types for all the common file types.
|
|
||||||
##
|
|
||||||
<LocationMatch "^/(plinth|freedombox)">
|
|
||||||
Header set Referrer-Policy 'same-origin'
|
|
||||||
Header set Content-Security-Policy "font-src 'self'; frame-src 'none'; img-src 'self' data:; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; default-src 'self'; base-uri 'none'; sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-same-origin allow-downloads; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content;"
|
|
||||||
Header set X-Content-Type-Options 'nosniff'
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## On all sites, provide FreedomBox on a default path: /plinth
|
## On all sites, provide FreedomBox on a default path: /plinth
|
||||||
##
|
##
|
||||||
|
|||||||
5
debian/copyright
vendored
5
debian/copyright
vendored
@ -143,7 +143,10 @@ License: ISC
|
|||||||
|
|
||||||
Files: plinth/modules/janus/static/icons/janus.png
|
Files: plinth/modules/janus/static/icons/janus.png
|
||||||
plinth/modules/janus/static/icons/janus.svg
|
plinth/modules/janus/static/icons/janus.svg
|
||||||
Copyright: 2014-2022 Meetecho
|
plinth/modules/janus/static/janus-video-room.css
|
||||||
|
plinth/modules/janus/static/janus-video-room.js
|
||||||
|
plinth/modules/janus/templates/janus_video_room.html
|
||||||
|
Copyright: 2014-2025 Meetecho
|
||||||
License: GPL-3 with OpenSSL exception
|
License: GPL-3 with OpenSSL exception
|
||||||
|
|
||||||
Files: plinth/modules/kiwix/static/icons/kiwix.png
|
Files: plinth/modules/kiwix/static/icons/kiwix.png
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-28 07:02+0000\n"
|
"PO-Revision-Date: 2025-11-12 07:51+0000\n"
|
||||||
"Last-Translator: Jiří Podhorecký <j.podhorecky@volny.cz>\n"
|
"Last-Translator: Jiří Podhorecký <j.podhorecky@volny.cz>\n"
|
||||||
"Language-Team: Czech <https://hosted.weblate.org/projects/freedombox/"
|
"Language-Team: Czech <https://hosted.weblate.org/projects/freedombox/"
|
||||||
"freedombox/cs/>\n"
|
"freedombox/cs/>\n"
|
||||||
@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
|
||||||
"X-Generator: Weblate 5.14-dev\n"
|
"X-Generator: Weblate 5.15-dev\n"
|
||||||
|
|
||||||
#: plinth/config.py:103
|
#: plinth/config.py:103
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@ -4360,20 +4360,16 @@ msgstr ""
|
|||||||
"href=\"%(config_url)s\">nastavte</a> alespoň jednu doménu."
|
"href=\"%(config_url)s\">nastavte</a> alespoň jednu doménu."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
"requires uninstalling and reinstalling the app which will wipe app's data."
|
"requires uninstalling and reinstalling the app which will wipe app's data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Doména Matrix serveru je nastavená na <em>%(domain_name)s</em>. "
|
"Doména serveru Matrix je nastavena na <em>%(domain_name)s</em>. ID uživatelů "
|
||||||
"Identifikátory uživatelů budou mít podobu <em>@uzivatelske_jmeno:"
|
"budou vypadat takto: <em>@username:%(domain_name)s</em>. Změna názvu domény "
|
||||||
"%(domain_name)s</em>. Změna doménového názvu po úvodním nastavení není v "
|
"vyžaduje odinstalování a opětovnou instalaci aplikace, což vymaže data "
|
||||||
"současnosti podporována."
|
"aplikace."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -4416,10 +4412,8 @@ msgid "Unlimited"
|
|||||||
msgstr "Neomezený"
|
msgstr "Neomezený"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Feed generator"
|
|
||||||
msgid "Federation"
|
msgid "Federation"
|
||||||
msgstr "Generátor kanálů"
|
msgstr "Federace"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -4430,6 +4424,12 @@ msgid ""
|
|||||||
"hosted here. If you face problems with federation, try the <a "
|
"hosted here. If you face problems with federation, try the <a "
|
||||||
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Matrix Synapse je nakonfigurován tak, aby spolupracoval s ostatními servery "
|
||||||
|
"Matrix na internetu. To umožňuje uživatelům %(box_name)s účastnit se "
|
||||||
|
"chatovacích místností hostovaných jinde a uživatelům Matrix na jiných "
|
||||||
|
"serverech účastnit se chatovacích místností hostovaných zde. Pokud máte "
|
||||||
|
"problémy s federací, vyzkoušejte <a href=\"%(tester_url)s#%(domain_name)s\""
|
||||||
|
">nástroj pro testování federace</a>."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -7275,19 +7275,15 @@ msgstr ""
|
|||||||
"href=\"{users_url}\">každý uživatel </a> patřící do skupiny feed-reader."
|
"href=\"{users_url}\">každý uživatel </a> patřící do skupiny feed-reader."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:28
|
#: plinth/modules/rssbridge/__init__.py:28
|
||||||
#, fuzzy, python-brace-format
|
#, python-brace-format
|
||||||
#| msgid ""
|
|
||||||
#| "You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
|
||||||
#| "href=\"{ttrss_url}\">Tiny Tiny RSS</a> to follow various websites. When "
|
|
||||||
#| "adding a feed, enable authentication and use your {box_name} credentials."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
||||||
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
||||||
"adding a feed, enable authentication and use your {box_name} credentials."
|
"adding a feed, enable authentication and use your {box_name} credentials."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"RSS-Bridge můžete používat s <a href=\"{miniflux_url}\">Miniflux</a> nebo <a "
|
"RSS-Bridge můžete použít s <a href=\"{miniflux_url}\">Miniflux</a> nebo <a "
|
||||||
"href=\"{ttrss_url}\">Tiny Tiny RSS</a> pro sledování různých webových "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> k sledování různých webových "
|
||||||
"stránek. Při přidávání kanálu povolte ověřování a použijte své přihlašovací "
|
"stránek. Při přidávání zdroje povolte ověřování a použijte své přihlašovací "
|
||||||
"údaje {box_name}."
|
"údaje {box_name}."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:49
|
#: plinth/modules/rssbridge/__init__.py:49
|
||||||
|
|||||||
@ -10,7 +10,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: FreedomBox UI\n"
|
"Project-Id-Version: FreedomBox UI\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-11-11 01:13+0000\n"
|
"PO-Revision-Date: 2025-11-13 13:51+0000\n"
|
||||||
"Last-Translator: Dietmar <sagen@permondes.de>\n"
|
"Last-Translator: Dietmar <sagen@permondes.de>\n"
|
||||||
"Language-Team: German <https://hosted.weblate.org/projects/freedombox/"
|
"Language-Team: German <https://hosted.weblate.org/projects/freedombox/"
|
||||||
"freedombox/de/>\n"
|
"freedombox/de/>\n"
|
||||||
@ -4451,19 +4451,16 @@ msgstr ""
|
|||||||
"Matrix Synapse nutzen zu können."
|
"Matrix Synapse nutzen zu können."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
"requires uninstalling and reinstalling the app which will wipe app's data."
|
"requires uninstalling and reinstalling the app which will wipe app's data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ihre Matrix-Server-Domain ist auf <em>%(domain_name)s</em> gesetzt. Benutzer-"
|
"Die Matrix-Serverdomäne ist auf <em>%(domain_name)s</em> eingestellt. "
|
||||||
"IDs erscheinen als <em>Nutzername:%(domain_name)s</em>. Änderungen der "
|
"Benutzer-IDs sehen wie folgt aus: <em>@username:%(domain_name)s</em>. Um den "
|
||||||
"Domain nach der ersten Konfiguration werden derzeit nicht unterstützt."
|
"Domänennamen zu ändern, muss die App deinstalliert und neu installiert "
|
||||||
|
"werden, wodurch die Daten der App gelöscht werden."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -4520,6 +4517,12 @@ msgid ""
|
|||||||
"hosted here. If you face problems with federation, try the <a "
|
"hosted here. If you face problems with federation, try the <a "
|
||||||
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Matrix Synapse ist so konfiguriert, dass es mit anderen Matrix-Servern im "
|
||||||
|
"Internet zusammenarbeitet. Dadurch können %(box_name)s-Benutzer an Räumen "
|
||||||
|
"teilnehmen, die woanders gehostet werden, und Matrix-Benutzer auf anderen "
|
||||||
|
"Servern können an Räumen teilnehmen, die hier gehostet werden. Wenn Sie "
|
||||||
|
"Probleme mit der Föderation haben, probieren Sie das <a href="
|
||||||
|
"\"%(tester_url)s#%(domain_name)s\">Föderationstest-Tool</a> ."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -7418,20 +7421,16 @@ msgstr ""
|
|||||||
"a> der zur Feed-Reader-Gruppe gehört, aufgerufen werden."
|
"a> der zur Feed-Reader-Gruppe gehört, aufgerufen werden."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:28
|
#: plinth/modules/rssbridge/__init__.py:28
|
||||||
#, fuzzy, python-brace-format
|
#, python-brace-format
|
||||||
#| msgid ""
|
|
||||||
#| "You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
|
||||||
#| "href=\"{ttrss_url}\">Tiny Tiny RSS</a> to follow various websites. When "
|
|
||||||
#| "adding a feed, enable authentication and use your {box_name} credentials."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
||||||
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
||||||
"adding a feed, enable authentication and use your {box_name} credentials."
|
"adding a feed, enable authentication and use your {box_name} credentials."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Sie können RSS-Bridge mit <a href=\"{miniflux_url}\">Miniflux</a> oder <a "
|
"Sie können RSS-Bridge mit <a href=\"{miniflux_url}\">Miniflux</a> oder <a "
|
||||||
"href=\"{ttrss_url}\">Tiny Tiny RSS</a> verwenden, um verschiedenen "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> verwenden, um verschiedene "
|
||||||
"Internetseiten zu folgen. Aktivieren Sie beim Hinzufügen eines Feeds die "
|
"Websites zu verfolgen. Wenn Sie einen Feed hinzufügen, aktivieren Sie die "
|
||||||
"Authentifizierung und verwenden Sie Ihre {box_name}-Anmeldeinformationen."
|
"Authentifizierung und verwenden Sie Ihre {box_name}-Anmeldedaten."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:49
|
#: plinth/modules/rssbridge/__init__.py:49
|
||||||
msgid "Read and subscribe to news feeds"
|
msgid "Read and subscribe to news feeds"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-11-11 01:13+0000\n"
|
"PO-Revision-Date: 2025-11-13 13:51+0000\n"
|
||||||
"Last-Translator: Dietmar <sagen@permondes.de>\n"
|
"Last-Translator: Dietmar <sagen@permondes.de>\n"
|
||||||
"Language-Team: Italian <https://hosted.weblate.org/projects/freedombox/"
|
"Language-Team: Italian <https://hosted.weblate.org/projects/freedombox/"
|
||||||
"freedombox/it/>\n"
|
"freedombox/it/>\n"
|
||||||
@ -4213,20 +4213,16 @@ msgstr ""
|
|||||||
"almeno un dominio per poter usare Matrix Synapse."
|
"almeno un dominio per poter usare Matrix Synapse."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
"requires uninstalling and reinstalling the app which will wipe app's data."
|
"requires uninstalling and reinstalling the app which will wipe app's data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Il dominio del server Matrix è impostato su <em> %(domain_name)s </em>. Gli "
|
"Il dominio del server Matrix è impostato su <em> %(domain_name)s </em>. Gli "
|
||||||
"ID utente assomiglieranno a <em>@username:%(domain_name)s</em>. Il cambio "
|
"ID utente assomiglieranno a <em>@nome_utente:%(domain_name)s</em>. La "
|
||||||
"del nome di dominio dopo la prima configurazione, attualmente, non è "
|
"modifica del nome di dominio richiede la disinstallazione e la "
|
||||||
"supportato."
|
"reinstallazione dell'app, che cancellerà i dati dell'app."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
@ -4282,6 +4278,12 @@ msgid ""
|
|||||||
"hosted here. If you face problems with federation, try the <a "
|
"hosted here. If you face problems with federation, try the <a "
|
||||||
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Matrix Synapse è configurato per funzionare con altri server Matrix su "
|
||||||
|
"Internet. Ciò consente agli utenti di %(box_name)s di partecipare a stanze "
|
||||||
|
"ospitate altrove e agli utenti Matrix su altri server di partecipare alle "
|
||||||
|
"stanze ospitate qui. Se riscontri problemi con la federazione, prova lo <a "
|
||||||
|
"href=\"%(tester_url)s#%(domain_name)s\"> strumento di verifica della "
|
||||||
|
"federazione</a>."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -6930,6 +6932,10 @@ msgid ""
|
|||||||
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
||||||
"adding a feed, enable authentication and use your {box_name} credentials."
|
"adding a feed, enable authentication and use your {box_name} credentials."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"È possibile utilizzare RSS-Bridge con <a href=\"{miniflux_url}\">Miniflux</"
|
||||||
|
"a> o <a href=\"{nextcloud_url}\">Nextcloud News</a> per seguire vari siti "
|
||||||
|
"web. Quando si aggiunge un feed, abilitare l'autenticazione e utilizzare le "
|
||||||
|
"credenziali {box_name}."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:49
|
#: plinth/modules/rssbridge/__init__.py:49
|
||||||
msgid "Read and subscribe to news feeds"
|
msgid "Read and subscribe to news feeds"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 10:07+0000\n"
|
"PO-Revision-Date: 2025-11-12 07:51+0000\n"
|
||||||
"Last-Translator: Besnik Bleta <besnik@programeshqip.org>\n"
|
"Last-Translator: Besnik Bleta <besnik@programeshqip.org>\n"
|
||||||
"Language-Team: Albanian <https://hosted.weblate.org/projects/freedombox/"
|
"Language-Team: Albanian <https://hosted.weblate.org/projects/freedombox/"
|
||||||
"freedombox/sq/>\n"
|
"freedombox/sq/>\n"
|
||||||
@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.14-dev\n"
|
"X-Generator: Weblate 5.15-dev\n"
|
||||||
|
|
||||||
#: plinth/config.py:103
|
#: plinth/config.py:103
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@ -3665,6 +3665,10 @@ msgid ""
|
|||||||
"alarms, presence sensors, door bells, thermostats, irrigation timers, energy "
|
"alarms, presence sensors, door bells, thermostats, irrigation timers, energy "
|
||||||
"monitors, etc."
|
"monitors, etc."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Home Assistant është një qendër automatizimi shtëpie, me theksin te "
|
||||||
|
"kontrolli vendor dhe privatësia. Integrohet me mijëra pajisje, përfshi "
|
||||||
|
"llamba të mençura, alarme, ndijues pranie, zile dyersh, termostate, "
|
||||||
|
"kohëmatës ujitjeje, mbikëqyrës energjie, etj."
|
||||||
|
|
||||||
#: plinth/modules/homeassistant/__init__.py:35
|
#: plinth/modules/homeassistant/__init__.py:35
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -3673,6 +3677,11 @@ msgid ""
|
|||||||
"requires additional hardware such as a ZigBee USB dongle. You need to re-run "
|
"requires additional hardware such as a ZigBee USB dongle. You need to re-run "
|
||||||
"setup if such hardware is added or removed."
|
"setup if such hardware is added or removed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Home Assistant mund të pikasë, formësojë dhe përdor pajisje të ndryshme në "
|
||||||
|
"rrjetin vendor. Për pajisje që përdorin protokolle të tjera, b.f. ZigBee, "
|
||||||
|
"zakonisht duhet hardware shtesë, si, fjala vjen, një marifet ZigBee USB. "
|
||||||
|
"Është e nevojshme të rikryeni ujdisjen, nëse vihet ose hiqet hardware i "
|
||||||
|
"tillë."
|
||||||
|
|
||||||
#: plinth/modules/homeassistant/__init__.py:39
|
#: plinth/modules/homeassistant/__init__.py:39
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -3680,6 +3689,9 @@ msgid ""
|
|||||||
"An administrator account is created at this time. Home Assistant maintains "
|
"An administrator account is created at this time. Home Assistant maintains "
|
||||||
"its own user accounts."
|
"its own user accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Ndërfaqja web e Home Assistant-it duhet ujdisur fill pasi të jetë instaluar "
|
||||||
|
"aplikacioni. Në atë kohë krijohet një llogari përgjegjësi. Home Assistant "
|
||||||
|
"mban llogaritë e veta të përdoruesve."
|
||||||
|
|
||||||
#: plinth/modules/homeassistant/__init__.py:43
|
#: plinth/modules/homeassistant/__init__.py:43
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@ -4395,20 +4407,16 @@ msgstr ""
|
|||||||
"përkatësi, që të jeni në gjendje të përdorni Matrix Synapse."
|
"përkatësi, që të jeni në gjendje të përdorni Matrix Synapse."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
"requires uninstalling and reinstalling the app which will wipe app's data."
|
"requires uninstalling and reinstalling the app which will wipe app's data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Si përkatësi shërbyesi Matrix është caktuar <em>%(domain_name)s</em>. ID-të "
|
"Si përkatësi shërbyesi Matrix është vënër <em>%(domain_name)s</em>. ID-të e "
|
||||||
"e përdoruesve do të jenë të trajtës <em>@emërpërdoruesi:%(domain_name)s</"
|
"përdoruesve do të jenë të trajtës <em>@emërpërdoruesi:%(domain_name)s</em>. "
|
||||||
"em>. Ndryshimi i emrit të përkatësisë, pas ujdisjes fillestare, aktualisht "
|
"Ndryshimi i emrit të përkatësisë lyp çinstalimin dhe riinstalimin e "
|
||||||
"s’mbulohet."
|
"aplikacionit, çka do të spastrojë të dhënat e aplikacionit."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -4451,10 +4459,8 @@ msgid "Unlimited"
|
|||||||
msgstr "E pakufizuar"
|
msgstr "E pakufizuar"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Feed generator"
|
|
||||||
msgid "Federation"
|
msgid "Federation"
|
||||||
msgstr "Prodhues prurjesh"
|
msgstr "Federim"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -4465,6 +4471,11 @@ msgid ""
|
|||||||
"hosted here. If you face problems with federation, try the <a "
|
"hosted here. If you face problems with federation, try the <a "
|
||||||
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Matrix Synapse është formësuar të punojë me të tjerë shërbyes Matrix në "
|
||||||
|
"internet. Kjo u lejon përdoruesve të %(box_name)s të marrin pjesë në dhoma "
|
||||||
|
"që strehohen gjetkë dhe përdoruesve Matrix në shërbyes të tjerë të marrin "
|
||||||
|
"pjesë në dhoma të strehuara këtu. Nëse hasni probleme me federimin, provoni "
|
||||||
|
"<a href=\"%(tester_url)s#%(domain_name)s\"> mjetin testues të federimit</a>."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -7346,18 +7357,14 @@ msgstr ""
|
|||||||
"\">cilido përdorues</a> pjesë e grupit të leximit të prurjeve."
|
"\">cilido përdorues</a> pjesë e grupit të leximit të prurjeve."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:28
|
#: plinth/modules/rssbridge/__init__.py:28
|
||||||
#, fuzzy, python-brace-format
|
#, python-brace-format
|
||||||
#| msgid ""
|
|
||||||
#| "You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
|
||||||
#| "href=\"{ttrss_url}\">Tiny Tiny RSS</a> to follow various websites. When "
|
|
||||||
#| "adding a feed, enable authentication and use your {box_name} credentials."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
||||||
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
||||||
"adding a feed, enable authentication and use your {box_name} credentials."
|
"adding a feed, enable authentication and use your {box_name} credentials."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"RSS-Bridge-in mund ta përdorni me <a href=\"{miniflux_url}\">Miniflux</a>, "
|
"RSS-Bridge-in mund ta përdorni me <a href=\"{miniflux_url}\">Miniflux</a> "
|
||||||
"ose <a href=\"{ttrss_url}\">Tiny Tiny RSS</a> për të ndjekur sajte të "
|
"ose <a href=\"{nextcloud_url}\">Nextcloud News</a> për të ndjekur sajte të "
|
||||||
"ndryshëm. Kur shtohet një prurje, aktivizoni mirëfilltësimin dhe përdorni "
|
"ndryshëm. Kur shtohet një prurje, aktivizoni mirëfilltësimin dhe përdorni "
|
||||||
"kredencialet tuaj për {box_name}."
|
"kredencialet tuaj për {box_name}."
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-24 03:01+0000\n"
|
"PO-Revision-Date: 2025-11-12 07:51+0000\n"
|
||||||
"Last-Translator: Burak Yavuz <hitowerdigit@hotmail.com>\n"
|
"Last-Translator: Burak Yavuz <hitowerdigit@hotmail.com>\n"
|
||||||
"Language-Team: Turkish <https://hosted.weblate.org/projects/freedombox/"
|
"Language-Team: Turkish <https://hosted.weblate.org/projects/freedombox/"
|
||||||
"freedombox/tr/>\n"
|
"freedombox/tr/>\n"
|
||||||
@ -16,7 +16,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||||
"X-Generator: Weblate 5.14-dev\n"
|
"X-Generator: Weblate 5.15-dev\n"
|
||||||
|
|
||||||
#: plinth/config.py:103
|
#: plinth/config.py:103
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@ -4378,11 +4378,7 @@ msgstr ""
|
|||||||
"az bir etki alanı <a href=\"%(config_url)s\">yapılandırın</a>."
|
"az bir etki alanı <a href=\"%(config_url)s\">yapılandırın</a>."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
@ -4390,8 +4386,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Matrix sunucusu etki alanı <em>%(domain_name)s</em> olarak ayarlandı. "
|
"Matrix sunucusu etki alanı <em>%(domain_name)s</em> olarak ayarlandı. "
|
||||||
"Kullanıcı kimlikleri <em>@kullanıcıadı:%(domain_name)s</em> şeklinde "
|
"Kullanıcı kimlikleri <em>@kullanıcıadı:%(domain_name)s</em> şeklinde "
|
||||||
"görünecek. İlk ayarlamadan sonra etki alanı adının değiştirilmesi şu anda "
|
"görünecek. Etki alanı adını değiştirmek, uygulamanın kaldırılmasını ve "
|
||||||
"desteklenmiyor."
|
"yeniden yüklenmesini gerektirir; bu da uygulamanın verilerini siler."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -4434,10 +4430,8 @@ msgid "Unlimited"
|
|||||||
msgstr "Sınırsız"
|
msgstr "Sınırsız"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Feed generator"
|
|
||||||
msgid "Federation"
|
msgid "Federation"
|
||||||
msgstr "Bildirim oluşturucu"
|
msgstr "Federasyon"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -4448,6 +4442,12 @@ msgid ""
|
|||||||
"hosted here. If you face problems with federation, try the <a "
|
"hosted here. If you face problems with federation, try the <a "
|
||||||
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Matrix Synapse, internet'teki diğer Matrix sunucularıyla çalışacak şekilde "
|
||||||
|
"yapılandırılmıştır. Bu, %(box_name)s kullanıcılarının başka bir yerde "
|
||||||
|
"barındırılan odalara katılmasını ve diğer sunuculardaki Matrix "
|
||||||
|
"kullanıcılarının burada barındırılan odalara katılmasını sağlar. Eğer "
|
||||||
|
"federasyonla ilgili sorunlarla karşılaşırsanız, <a href="
|
||||||
|
"\"%(tester_url)s#%(domain_name)s\">federasyon deneyici aracını</a> deneyin."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -7307,18 +7307,14 @@ msgstr ""
|
|||||||
"href=\"{users_url}\">herhangi bir kullanıcı</a> tarafından erişilebilir."
|
"href=\"{users_url}\">herhangi bir kullanıcı</a> tarafından erişilebilir."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:28
|
#: plinth/modules/rssbridge/__init__.py:28
|
||||||
#, fuzzy, python-brace-format
|
#, python-brace-format
|
||||||
#| msgid ""
|
|
||||||
#| "You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
|
||||||
#| "href=\"{ttrss_url}\">Tiny Tiny RSS</a> to follow various websites. When "
|
|
||||||
#| "adding a feed, enable authentication and use your {box_name} credentials."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
||||||
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
||||||
"adding a feed, enable authentication and use your {box_name} credentials."
|
"adding a feed, enable authentication and use your {box_name} credentials."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Çeşitli web sitelerini takip etmek için <a href=\"{miniflux_url}\">Miniflux</"
|
"Çeşitli web sitelerini takip etmek için <a href=\"{miniflux_url}\">Miniflux</"
|
||||||
"a> veya <a href=\"{ttrss_url}\">Tiny Tiny RSS</a> ile RSS-Bridge'i "
|
"a> veya <a href=\"{nextcloud_url}\">Nextcloud News</a> ile RSS-Bridge'i "
|
||||||
"kullanabilirsiniz. Bir bildirim eklerken, kimlik doğrulamayı etkinleştirin "
|
"kullanabilirsiniz. Bir bildirim eklerken, kimlik doğrulamayı etkinleştirin "
|
||||||
"ve {box_name} kimlik bilgilerinizi kullanın."
|
"ve {box_name} kimlik bilgilerinizi kullanın."
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-24 03:02+0000\n"
|
"PO-Revision-Date: 2025-11-12 07:51+0000\n"
|
||||||
"Last-Translator: Максим Горпиніч <gorpinicmaksim0@gmail.com>\n"
|
"Last-Translator: Максим Горпиніч <gorpinicmaksim0@gmail.com>\n"
|
||||||
"Language-Team: Ukrainian <https://hosted.weblate.org/projects/freedombox/"
|
"Language-Team: Ukrainian <https://hosted.weblate.org/projects/freedombox/"
|
||||||
"freedombox/uk/>\n"
|
"freedombox/uk/>\n"
|
||||||
@ -16,9 +16,10 @@ msgstr ""
|
|||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
"Plural-Forms: nplurals=3; plural="
|
||||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
"(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? "
|
||||||
"X-Generator: Weblate 5.14-dev\n"
|
"1 : 2);\n"
|
||||||
|
"X-Generator: Weblate 5.15-dev\n"
|
||||||
|
|
||||||
#: plinth/config.py:103
|
#: plinth/config.py:103
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@ -4385,20 +4386,16 @@ msgstr ""
|
|||||||
"щонайменше один домен для використання Matrix Synapse."
|
"щонайменше один домен для використання Matrix Synapse."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
"requires uninstalling and reinstalling the app which will wipe app's data."
|
"requires uninstalling and reinstalling the app which will wipe app's data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Домен сервера Matrix встановлюється на <em>%(domain_name)s</em>. "
|
"Домен сервера Matrix встановлений на <em>%(domain_name)s</em>. "
|
||||||
"Ідентифікатори користувачів будуть виглядати як <em>@ім'я користувача:"
|
"Ідентифікатори користувача будуть схожі на <em>@username:%(domain_name)s</"
|
||||||
"%(domain_name)s</em>. Зміна доменного імені після початкового налаштування "
|
"em>. Зміна доменного ім'я вимагає видалення та повторної установки програми, "
|
||||||
"наразі не підтримується."
|
"яка витримає дані додатка."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -4441,10 +4438,8 @@ msgid "Unlimited"
|
|||||||
msgstr "Необмежено"
|
msgstr "Необмежено"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Feed generator"
|
|
||||||
msgid "Federation"
|
msgid "Federation"
|
||||||
msgstr "Генератор корму"
|
msgstr "Федерація"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -4455,6 +4450,12 @@ msgid ""
|
|||||||
"hosted here. If you face problems with federation, try the <a "
|
"hosted here. If you face problems with federation, try the <a "
|
||||||
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
"href=\"%(tester_url)s#%(domain_name)s\"> federation tester tool</a>."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Matrix Synapse налаштований для роботи з іншими серверами Matrix в "
|
||||||
|
"Інтернеті. Це дозволяє користувачам %(box_name)s брати участь у чатах, що "
|
||||||
|
"розміщені в інших місцях, а користувачам Matrix на інших серверах — брати "
|
||||||
|
"участь у чатах, розміщених тут. Якщо у вас виникли проблеми з федерацією, "
|
||||||
|
"спробуйте <a href=\"%(tester_url)s#%(domain_name)s\">інструмент для "
|
||||||
|
"тестування федерації</a>."
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -7311,20 +7312,16 @@ msgstr ""
|
|||||||
"\">будь-якого користувача</a>, який належить до групи feed-reader."
|
"\">будь-якого користувача</a>, який належить до групи feed-reader."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:28
|
#: plinth/modules/rssbridge/__init__.py:28
|
||||||
#, fuzzy, python-brace-format
|
#, python-brace-format
|
||||||
#| msgid ""
|
|
||||||
#| "You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
|
||||||
#| "href=\"{ttrss_url}\">Tiny Tiny RSS</a> to follow various websites. When "
|
|
||||||
#| "adding a feed, enable authentication and use your {box_name} credentials."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
"You can use RSS-Bridge with <a href=\"{miniflux_url}\">Miniflux</a> or <a "
|
||||||
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
"href=\"{nextcloud_url}\">Nextcloud News</a> to follow various websites. When "
|
||||||
"adding a feed, enable authentication and use your {box_name} credentials."
|
"adding a feed, enable authentication and use your {box_name} credentials."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ви можете використовувати RSS-Bridge із <a href=\"{miniflux_url}\">Miniflux</"
|
"Ви можете використовувати RSS-Bridge з <a href=\"{miniflux_url}\">Miniflux</"
|
||||||
"a> або <a href=\"{ttrss_url}\">Tiny Tiny RSS</a>, щоб стежити за різними веб-"
|
"a> або <a>a href=\"{nextcloud_url}: >Nextcloud News</a> для перегляду "
|
||||||
"сайтами. Додаючи канал, увімкніть автентифікацію та використовуйте свої "
|
"різних веб-сайтів. Під час додавання фід, дозвольте аутентифікації та "
|
||||||
"облікові дані {box_name}."
|
"використовуйте свої акредитації {box_name}."
|
||||||
|
|
||||||
#: plinth/modules/rssbridge/__init__.py:49
|
#: plinth/modules/rssbridge/__init__.py:49
|
||||||
msgid "Read and subscribe to news feeds"
|
msgid "Read and subscribe to news feeds"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: Plinth\n"
|
"Project-Id-Version: Plinth\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
"POT-Creation-Date: 2025-11-11 01:20+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-24 03:02+0000\n"
|
"PO-Revision-Date: 2025-11-12 07:51+0000\n"
|
||||||
"Last-Translator: 大王叫我来巡山 "
|
"Last-Translator: 大王叫我来巡山 "
|
||||||
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
|
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
|
||||||
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
|
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
|
||||||
@ -18,7 +18,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Generator: Weblate 5.14-dev\n"
|
"X-Generator: Weblate 5.15-dev\n"
|
||||||
|
|
||||||
#: plinth/config.py:103
|
#: plinth/config.py:103
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@ -4038,18 +4038,15 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:20
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs "
|
|
||||||
#| "will look like <em>@username:%(domain_name)s</em>. Changing the domain "
|
|
||||||
#| "name after the initial setup is currently not supported."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
"The Matrix server domain is set to <em>%(domain_name)s</em>. User IDs will "
|
||||||
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
"look like <em>@username:%(domain_name)s</em>. Changing the domain name "
|
||||||
"requires uninstalling and reinstalling the app which will wipe app's data."
|
"requires uninstalling and reinstalling the app which will wipe app's data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Matrix 服务器域名已设置为 <em>%(domain_name)s <em>。用户 ID 看起来像是这样 "
|
"Matrix 服务器域名已设置为 <em>%(domain_name)s <em>。用户 ID 看起来像是这样 "
|
||||||
"<em>@username:%(domain_name)s</em>。尚不支持在初始设置后更改域名。</em></em>"
|
"<em>@username:%(domain_name)s</em>。更改域名需要卸载并重新安装该应用,这会抹"
|
||||||
|
"去应用的数据。</em></em>"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:28
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -4088,10 +4085,8 @@ msgid "Unlimited"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:78
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Conversations"
|
|
||||||
msgid "Federation"
|
msgid "Federation"
|
||||||
msgstr "Conversations"
|
msgstr "联邦"
|
||||||
|
|
||||||
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
#: plinth/modules/matrixsynapse/templates/matrix-synapse.html:81
|
||||||
#, python-format
|
#, python-format
|
||||||
|
|||||||
@ -188,3 +188,93 @@ class CommonErrorMiddleware(MiddlewareMixin):
|
|||||||
breadcrumbs = views.get_breadcrumbs(request)
|
breadcrumbs = views.get_breadcrumbs(request)
|
||||||
parent_index = 1 if len(breadcrumbs) > 1 else 0
|
parent_index = 1 if len(breadcrumbs) > 1 else 0
|
||||||
return list(breadcrumbs.keys())[parent_index]
|
return list(breadcrumbs.keys())[parent_index]
|
||||||
|
|
||||||
|
|
||||||
|
class CSPDict(dict):
|
||||||
|
"""A dictionary to store Content Security Policy.
|
||||||
|
|
||||||
|
And return a full value of the HTTP header.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_header_value(self) -> str:
|
||||||
|
"""Return the string header value for the policy stored."""
|
||||||
|
return ' '.join([f'{key} {value};' for key, value in self.items()])
|
||||||
|
|
||||||
|
|
||||||
|
CONTENT_SECURITY_POLICY = CSPDict({
|
||||||
|
# @fonts are allowed only from FreedomBox itself.
|
||||||
|
'font-src': "'self'",
|
||||||
|
# <frame>/<iframe> sources are disabled.
|
||||||
|
'frame-src': "'none'",
|
||||||
|
# <img> sources are allowed only from FreedomBox itself. Allow
|
||||||
|
# data: URLs for SVGs in CSS.
|
||||||
|
'img-src': "'self' data:",
|
||||||
|
# Manifest file is not allowed as there is none yet.
|
||||||
|
'manifest-src': "'none'",
|
||||||
|
# <audio>, <video>, <track> tags are not allowed yet.
|
||||||
|
'media-src': "'none'",
|
||||||
|
# <object>, <embed>, <applet> tags are not allowed yet. No plugins
|
||||||
|
# types are alllowed since object-src is 'none'.
|
||||||
|
'object-src': "'none'",
|
||||||
|
# Allow JS from FreedomBox itself (no inline and attribute
|
||||||
|
# scripts).
|
||||||
|
'script-src': "'self'",
|
||||||
|
# Allow inline CSS and CSS files from Freedombox itself.
|
||||||
|
'style-src': "'self'",
|
||||||
|
# Web worker sources are allowed only from FreedomBox itself (for
|
||||||
|
# JSXC).
|
||||||
|
'worker-src': "'self'",
|
||||||
|
# All other fetch sources including Ajax are not allowed from
|
||||||
|
# FreedomBox itself.
|
||||||
|
'default-src': "'self'",
|
||||||
|
# <base> tag is not allowed.
|
||||||
|
'base-uri': "'none'",
|
||||||
|
# Enable strict sandboxing enabled with some exceptions:
|
||||||
|
# - Allow running Javascript.
|
||||||
|
# - Allow popups as sometimes we use <a target=_blank>
|
||||||
|
# - Allow popups to have different sandbox requirements as we
|
||||||
|
# launch apps' web clients.
|
||||||
|
# - Allow forms to support configuration forms.
|
||||||
|
# - Allow policies to treat same origin differently from other
|
||||||
|
# - origins
|
||||||
|
# - Allow downloads such as backup tarballs.
|
||||||
|
'sandbox': 'allow-scripts allow-popups '
|
||||||
|
'allow-popups-to-escape-sandbox allow-forms '
|
||||||
|
'allow-same-origin allow-downloads',
|
||||||
|
# Form action should be to FreedomBox itself.
|
||||||
|
'form-action': "'self'",
|
||||||
|
# This interface may be not embedded in <frame>, <iframe>, etc.
|
||||||
|
# tags.
|
||||||
|
'frame-ancestors': "'none'",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class CommonHeadersMiddleware:
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
"""Initialize the middleware object."""
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
"""Add common security middleware."""
|
||||||
|
# Disable sending Referer (sic) header from FreedomBox web interface to
|
||||||
|
# external websites. This improves privacy by not disclosing FreedomBox
|
||||||
|
# domains/URLs to external domains. Apps such as blogs which want to
|
||||||
|
# popularize themselves with referrer header may still do so.
|
||||||
|
response = self.get_response(request)
|
||||||
|
if not response.get('Referrer-Policy'):
|
||||||
|
response['Referrer-Policy'] = 'same-origin'
|
||||||
|
|
||||||
|
# Disable browser guessing of MIME types. FreedoBox already sets good
|
||||||
|
# content types for all the common file types.
|
||||||
|
if not response.get('X-Content-Type-Options'):
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
|
||||||
|
csp = ' '.join([
|
||||||
|
f'{key} {value};'
|
||||||
|
for key, value in CONTENT_SECURITY_POLICY.items()
|
||||||
|
])
|
||||||
|
if not response.get('Content-Security-Policy'):
|
||||||
|
response['Content-Security-Policy'] = csp
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@ -25,6 +25,11 @@ _description = [
|
|||||||
format_lazy(
|
format_lazy(
|
||||||
_('<a href="{coturn_url}">Coturn</a> is required to '
|
_('<a href="{coturn_url}">Coturn</a> is required to '
|
||||||
'use Janus.'), coturn_url=reverse_lazy('coturn:index')),
|
'use Janus.'), coturn_url=reverse_lazy('coturn:index')),
|
||||||
|
format_lazy(
|
||||||
|
_('<strong>Note:</strong> This app receives frequent feature updates. '
|
||||||
|
'It can only be installed if frequent feature updates is enabled in '
|
||||||
|
'the <a href="{upgrades_url}">Software Update</a> app.'),
|
||||||
|
upgrades_url=reverse_lazy('upgrades:index')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -56,9 +61,9 @@ class JanusApp(app_module.App):
|
|||||||
self.add(shortcut)
|
self.add(shortcut)
|
||||||
|
|
||||||
packages = Packages('packages-janus', [
|
packages = Packages('packages-janus', [
|
||||||
'janus', 'libjs-jquery', 'libjs-bootbox', 'libjs-bootstrap',
|
'janus', 'libjs-jquery', 'libjs-bootbox', 'libjs-bootstrap5',
|
||||||
'libjs-bootswatch', 'libjs-janus-gateway', 'libjs-jquery-blockui',
|
'libjs-janus-gateway', 'libjs-jquery-blockui', 'libjs-toastr',
|
||||||
'libjs-spin.js', 'libjs-toastr', 'libjs-webrtc-adapter'
|
'libjs-webrtc-adapter', 'node-popper2'
|
||||||
])
|
])
|
||||||
self.add(packages)
|
self.add(packages)
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
# This file based on example code from Janus Demos which is licensed as
|
# This file based on example code from Janus Demos which is licensed as
|
||||||
# follows.
|
# follows.
|
||||||
#
|
#
|
||||||
# 2014-2022 Meetecho
|
# 2014-2025 Meetecho
|
||||||
#
|
#
|
||||||
# GPL-3 with OpenSSL exception
|
# GPL-3 with OpenSSL exception
|
||||||
# If you modify this Program, or any covered work,
|
# If you modify this Program, or any covered work,
|
||||||
@ -18,10 +18,36 @@
|
|||||||
# as well as that of the covered work.
|
# as well as that of the covered work.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Default style removed in bootstrap 4 */
|
||||||
|
.btn-default {
|
||||||
|
--bs-btn-color: #333;
|
||||||
|
--bs-btn-bg: #fff;
|
||||||
|
--bs-btn-border-color: #ccc;
|
||||||
|
--bs-btn-hover-color: #333;
|
||||||
|
--bs-btn-hover-bg: #e6e6e6;
|
||||||
|
--bs-btn-hover-border-color: #adadad;
|
||||||
|
--bs-btn-focus-shadow-rgb: 192, 192, 192;
|
||||||
|
--bs-btn-active-color: #333;
|
||||||
|
--bs-btn-active-bg: #e6e6e6;
|
||||||
|
--bs-btn-active-border-color: #adadad;
|
||||||
|
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
--bs-btn-disabled-color: #333;
|
||||||
|
--bs-btn-disabled-bg: #e6e6e6;
|
||||||
|
--bs-btn-disabled-border-color: #adadad;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-2 {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
@ -35,11 +61,35 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-left {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-right {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-left {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-right {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
margin-left: 0px !important;
|
margin-left: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-default {
|
.navbar {
|
||||||
-webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);
|
-webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);
|
||||||
-moz-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);
|
box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);
|
||||||
@ -49,23 +99,11 @@
|
|||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-sm {
|
.btn-group-xs > .btn, .btn-xs {
|
||||||
margin: 5px !important;
|
padding: 1px 5px;
|
||||||
}
|
font-size: 12px;
|
||||||
.margin-md {
|
line-height: 1.5;
|
||||||
margin: 10px !important;
|
border-radius: 3px;
|
||||||
}
|
|
||||||
.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 {
|
.divider {
|
||||||
@ -87,6 +125,7 @@ div.no-video-container {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 240px;
|
height: 240px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding-top: 5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-video-text {
|
.no-video-text {
|
||||||
@ -121,6 +160,11 @@ pre {
|
|||||||
white-space: -pre-wrap;
|
white-space: -pre-wrap;
|
||||||
white-space: -o-pre-wrap;
|
white-space: -o-pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray {
|
||||||
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.januscon {
|
.januscon {
|
||||||
@ -164,7 +208,3 @@ pre {
|
|||||||
.simulcast-button-group {
|
.simulcast-button-group {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simulcast-button {
|
|
||||||
width: 33%;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
# This file based on example code from Janus Demos which is licensed as
|
# This file based on example code from Janus Demos which is licensed as
|
||||||
# follows.
|
# follows.
|
||||||
#
|
#
|
||||||
# 2014-2022 Meetecho
|
# 2014-2025 Meetecho
|
||||||
#
|
#
|
||||||
# GPL-3 with OpenSSL exception
|
# GPL-3 with OpenSSL exception
|
||||||
# If you modify this Program, or any covered work,
|
# If you modify this Program, or any covered work,
|
||||||
@ -50,12 +50,16 @@ var mypvtid = null;
|
|||||||
var remoteFeed = null;
|
var remoteFeed = null;
|
||||||
var feeds = {}, feedStreams = {}, subStreams = {}, slots = {}, mids = {}, subscriptions = {};
|
var feeds = {}, feedStreams = {}, subStreams = {}, slots = {}, mids = {}, subscriptions = {};
|
||||||
var localTracks = {}, localVideos = 0, remoteTracks = {};
|
var localTracks = {}, localVideos = 0, remoteTracks = {};
|
||||||
var bitrateTimer = [], simulcastStarted = {};
|
var bitrateTimer = [], simulcastStarted = {}, svcStarted = {};
|
||||||
|
|
||||||
var doSimulcast = (getQueryStringValue("simulcast") === "yes" || getQueryStringValue("simulcast") === "true");
|
var doSimulcast = (getQueryStringValue("simulcast") === "yes" || getQueryStringValue("simulcast") === "true");
|
||||||
|
var doSvc = getQueryStringValue("svc");
|
||||||
|
if(doSvc === "")
|
||||||
|
doSvc = null;
|
||||||
var acodec = (getQueryStringValue("acodec") !== "" ? getQueryStringValue("acodec") : null);
|
var acodec = (getQueryStringValue("acodec") !== "" ? getQueryStringValue("acodec") : null);
|
||||||
var vcodec = (getQueryStringValue("vcodec") !== "" ? getQueryStringValue("vcodec") : null);
|
var vcodec = (getQueryStringValue("vcodec") !== "" ? getQueryStringValue("vcodec") : null);
|
||||||
var subscriber_mode = (getQueryStringValue("subscriber-mode") === "yes" || getQueryStringValue("subscriber-mode") === "true");
|
var subscriber_mode = (getQueryStringValue("subscriber-mode") === "yes" || getQueryStringValue("subscriber-mode") === "true");
|
||||||
|
var use_msid = (getQueryStringValue("msid") === "yes" || getQueryStringValue("msid") === "true");
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize the library (all console debuggers enabled)
|
// Initialize the library (all console debuggers enabled)
|
||||||
@ -87,8 +91,8 @@ $(document).ready(function() {
|
|||||||
Janus.log("Plugin attached! (" + sfutest.getPlugin() + ", id=" + sfutest.getId() + ")");
|
Janus.log("Plugin attached! (" + sfutest.getPlugin() + ", id=" + sfutest.getId() + ")");
|
||||||
Janus.log(" -- This is a publisher/manager");
|
Janus.log(" -- This is a publisher/manager");
|
||||||
// Prepare the username registration
|
// Prepare the username registration
|
||||||
$('#videojoin').removeClass('hide').show();
|
$('#videojoin').removeClass('hide');
|
||||||
$('#registernow').removeClass('hide').show();
|
$('#registernow').removeClass('hide');
|
||||||
$('#register').click(registerUsername);
|
$('#register').click(registerUsername);
|
||||||
$('#username').focus();
|
$('#username').focus();
|
||||||
$('#start').removeAttr('disabled').html("Stop")
|
$('#start').removeAttr('disabled').html("Stop")
|
||||||
@ -117,16 +121,17 @@ $(document).ready(function() {
|
|||||||
return;
|
return;
|
||||||
$('#publish').remove();
|
$('#publish').remove();
|
||||||
// This controls allows us to override the global room bitrate cap
|
// This controls allows us to override the global room bitrate cap
|
||||||
$('#bitrate').parent().parent().removeClass('hide').show();
|
$('#bitrate').parent().parent().removeClass('hide');
|
||||||
$('#bitrate a').click(function() {
|
$('#bitrate a').click(function() {
|
||||||
var id = $(this).attr("id");
|
$('.dropdown-toggle').dropdown('hide');
|
||||||
var bitrate = parseInt(id)*1000;
|
let id = $(this).attr("id");
|
||||||
|
let bitrate = parseInt(id)*1000;
|
||||||
if(bitrate === 0) {
|
if(bitrate === 0) {
|
||||||
Janus.log("Not limiting bandwidth via REMB");
|
Janus.log("Not limiting bandwidth via REMB");
|
||||||
} else {
|
} else {
|
||||||
Janus.log("Capping bandwidth to " + bitrate + " via REMB");
|
Janus.log("Capping bandwidth to " + bitrate + " via REMB");
|
||||||
}
|
}
|
||||||
$('#bitrateset').html($(this).html() + '<span class="caret"></span>').parent().removeClass('open');
|
$('#bitrateset').text($(this).text()).parent().removeClass('open');
|
||||||
sfutest.send({ message: { request: "configure", bitrate: bitrate }});
|
sfutest.send({ message: { request: "configure", bitrate: bitrate }});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@ -137,7 +142,7 @@ $(document).ready(function() {
|
|||||||
},
|
},
|
||||||
onmessage: function(msg, jsep) {
|
onmessage: function(msg, jsep) {
|
||||||
Janus.debug(" ::: Got a message (publisher) :::", msg);
|
Janus.debug(" ::: Got a message (publisher) :::", msg);
|
||||||
var event = msg["videoroom"];
|
let event = msg["videoroom"];
|
||||||
Janus.debug("Event: " + event);
|
Janus.debug("Event: " + event);
|
||||||
if(event != undefined && event != null) {
|
if(event != undefined && event != null) {
|
||||||
if(event === "joined") {
|
if(event === "joined") {
|
||||||
@ -146,29 +151,35 @@ $(document).ready(function() {
|
|||||||
mypvtid = msg["private_id"];
|
mypvtid = msg["private_id"];
|
||||||
Janus.log("Successfully joined room " + msg["room"] + " with ID " + myid);
|
Janus.log("Successfully joined room " + msg["room"] + " with ID " + myid);
|
||||||
if(subscriber_mode) {
|
if(subscriber_mode) {
|
||||||
$('#videojoin').hide();
|
$('#videojoin').addClass('hide');
|
||||||
$('#videos').removeClass('hide').show();
|
$('#videos').removeClass('hide');
|
||||||
} else {
|
} else {
|
||||||
publishOwnFeed(true);
|
publishOwnFeed(true);
|
||||||
}
|
}
|
||||||
// Any new feed to attach to?
|
// Any new feed to attach to?
|
||||||
if(msg["publishers"]) {
|
if(msg["publishers"]) {
|
||||||
var list = msg["publishers"];
|
let list = msg["publishers"];
|
||||||
Janus.debug("Got a list of available publishers/feeds:", list);
|
Janus.debug("Got a list of available publishers/feeds:", list);
|
||||||
var sources = null;
|
let sources = null;
|
||||||
for(var f in list) {
|
for(let f in list) {
|
||||||
var id = list[f]["id"];
|
if(list[f]["dummy"])
|
||||||
var display = list[f]["display"];
|
continue;
|
||||||
var streams = list[f]["streams"];
|
let id = list[f]["id"];
|
||||||
for(var i in streams) {
|
let display = list[f]["display"];
|
||||||
var stream = streams[i];
|
let streams = list[f]["streams"];
|
||||||
|
for(let i in streams) {
|
||||||
|
let stream = streams[i];
|
||||||
stream["id"] = id;
|
stream["id"] = id;
|
||||||
stream["display"] = display;
|
stream["display"] = display;
|
||||||
}
|
}
|
||||||
|
let slot = feedStreams[id] ? feedStreams[id].slot : null;
|
||||||
|
let remoteVideos = feedStreams[id] ? feedStreams[id].remoteVideos : 0;
|
||||||
feedStreams[id] = {
|
feedStreams[id] = {
|
||||||
id: id,
|
id: id,
|
||||||
display: display,
|
display: display,
|
||||||
streams: streams
|
streams: streams,
|
||||||
|
slot: slot,
|
||||||
|
remoteVideos: remoteVideos
|
||||||
}
|
}
|
||||||
Janus.debug(" >> [" + id + "] " + display + ":", streams);
|
Janus.debug(" >> [" + id + "] " + display + ":", streams);
|
||||||
if(!sources)
|
if(!sources)
|
||||||
@ -187,9 +198,9 @@ $(document).ready(function() {
|
|||||||
} else if(event === "event") {
|
} else if(event === "event") {
|
||||||
// Any info on our streams or a new feed to attach to?
|
// Any info on our streams or a new feed to attach to?
|
||||||
if(msg["streams"]) {
|
if(msg["streams"]) {
|
||||||
var streams = msg["streams"];
|
let streams = msg["streams"];
|
||||||
for(var i in streams) {
|
for(let i in streams) {
|
||||||
var stream = streams[i];
|
let stream = streams[i];
|
||||||
stream["id"] = myid;
|
stream["id"] = myid;
|
||||||
stream["display"] = myusername;
|
stream["display"] = myusername;
|
||||||
}
|
}
|
||||||
@ -199,22 +210,28 @@ $(document).ready(function() {
|
|||||||
streams: streams
|
streams: streams
|
||||||
}
|
}
|
||||||
} else if(msg["publishers"]) {
|
} else if(msg["publishers"]) {
|
||||||
var list = msg["publishers"];
|
let list = msg["publishers"];
|
||||||
Janus.debug("Got a list of available publishers/feeds:", list);
|
Janus.debug("Got a list of available publishers/feeds:", list);
|
||||||
var sources = null;
|
let sources = null;
|
||||||
for(var f in list) {
|
for(let f in list) {
|
||||||
var id = list[f]["id"];
|
if(list[f]["dummy"])
|
||||||
var display = list[f]["display"];
|
continue;
|
||||||
var streams = list[f]["streams"];
|
let id = list[f]["id"];
|
||||||
for(var i in streams) {
|
let display = list[f]["display"];
|
||||||
var stream = streams[i];
|
let streams = list[f]["streams"];
|
||||||
|
for(let i in streams) {
|
||||||
|
let stream = streams[i];
|
||||||
stream["id"] = id;
|
stream["id"] = id;
|
||||||
stream["display"] = display;
|
stream["display"] = display;
|
||||||
}
|
}
|
||||||
|
let slot = feedStreams[id] ? feedStreams[id].slot : null;
|
||||||
|
let remoteVideos = feedStreams[id] ? feedStreams[id].remoteVideos : 0;
|
||||||
feedStreams[id] = {
|
feedStreams[id] = {
|
||||||
id: id,
|
id: id,
|
||||||
display: display,
|
display: display,
|
||||||
streams: streams
|
streams: streams,
|
||||||
|
slot: slot,
|
||||||
|
remoteVideos: remoteVideos
|
||||||
}
|
}
|
||||||
Janus.debug(" >> [" + id + "] " + display + ":", streams);
|
Janus.debug(" >> [" + id + "] " + display + ":", streams);
|
||||||
if(!sources)
|
if(!sources)
|
||||||
@ -225,12 +242,12 @@ $(document).ready(function() {
|
|||||||
subscribeTo(sources);
|
subscribeTo(sources);
|
||||||
} else if(msg["leaving"]) {
|
} else if(msg["leaving"]) {
|
||||||
// One of the publishers has gone away?
|
// One of the publishers has gone away?
|
||||||
var leaving = msg["leaving"];
|
let leaving = msg["leaving"];
|
||||||
Janus.log("Publisher left: " + leaving);
|
Janus.log("Publisher left: " + leaving);
|
||||||
unsubscribeFrom(leaving);
|
unsubscribeFrom(leaving);
|
||||||
} else if(msg["unpublished"]) {
|
} else if(msg["unpublished"]) {
|
||||||
// One of the publishers has unpublished?
|
// One of the publishers has unpublished?
|
||||||
var unpublished = msg["unpublished"];
|
let unpublished = msg["unpublished"];
|
||||||
Janus.log("Publisher left: " + unpublished);
|
Janus.log("Publisher left: " + unpublished);
|
||||||
if(unpublished === 'ok') {
|
if(unpublished === 'ok') {
|
||||||
// That's us
|
// That's us
|
||||||
@ -258,21 +275,21 @@ $(document).ready(function() {
|
|||||||
sfutest.handleRemoteJsep({ jsep: jsep });
|
sfutest.handleRemoteJsep({ jsep: jsep });
|
||||||
// Check if any of the media we wanted to publish has
|
// Check if any of the media we wanted to publish has
|
||||||
// been rejected (e.g., wrong or unsupported codec)
|
// been rejected (e.g., wrong or unsupported codec)
|
||||||
var audio = msg["audio_codec"];
|
let audio = msg["audio_codec"];
|
||||||
if(mystream && mystream.getAudioTracks() && mystream.getAudioTracks().length > 0 && !audio) {
|
if(mystream && mystream.getAudioTracks() && mystream.getAudioTracks().length > 0 && !audio) {
|
||||||
// Audio has been rejected
|
// Audio has been rejected
|
||||||
toastr.warning("Our audio stream has been rejected, viewers won't hear us");
|
toastr.warning("Our audio stream has been rejected, viewers won't hear us");
|
||||||
}
|
}
|
||||||
var video = msg["video_codec"];
|
let video = msg["video_codec"];
|
||||||
if(mystream && mystream.getVideoTracks() && mystream.getVideoTracks().length > 0 && !video) {
|
if(mystream && mystream.getVideoTracks() && mystream.getVideoTracks().length > 0 && !video) {
|
||||||
// Video has been rejected
|
// Video has been rejected
|
||||||
toastr.warning("Our video stream has been rejected, viewers won't see us");
|
toastr.warning("Our video stream has been rejected, viewers won't see us");
|
||||||
// Hide the webcam video
|
// Hide the webcam video
|
||||||
$('#myvideo').hide();
|
$('#myvideo').addClass('hide');
|
||||||
$('#videolocal').append(
|
$('#videolocal').prepend(
|
||||||
'<div class="no-video-container">' +
|
'<div class="no-video-container">' +
|
||||||
'<span class="no-video-text-sm">Video rejected, no webcam</span>' +
|
'<span class="no-video-text-sm">Video rejected, no webcam</span>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -280,18 +297,19 @@ $(document).ready(function() {
|
|||||||
Janus.debug(" ::: Got a local track event :::");
|
Janus.debug(" ::: Got a local track event :::");
|
||||||
Janus.debug("Local track " + (on ? "added" : "removed") + ":", track);
|
Janus.debug("Local track " + (on ? "added" : "removed") + ":", track);
|
||||||
// We use the track ID as name of the element, but it may contain invalid characters
|
// We use the track ID as name of the element, but it may contain invalid characters
|
||||||
var trackId = track.id.replace(/[{}]/g, "");
|
let trackId = track.id.replace(/[{}]/g, "");
|
||||||
if(!on) {
|
if(!on) {
|
||||||
// Track removed, get rid of the stream and the rendering
|
// Track removed, get rid of the stream and the rendering
|
||||||
var stream = localTracks[trackId];
|
let stream = localTracks[trackId];
|
||||||
if(stream) {
|
if(stream) {
|
||||||
try {
|
try {
|
||||||
var tracks = stream.getTracks();
|
let tracks = stream.getTracks();
|
||||||
for(var i in tracks) {
|
for(let i in tracks) {
|
||||||
var mst = tracks[i];
|
let mst = tracks[i];
|
||||||
if(mst)
|
if(mst)
|
||||||
mst.stop();
|
mst.stop();
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
if(track.kind === "video") {
|
if(track.kind === "video") {
|
||||||
@ -300,10 +318,10 @@ $(document).ready(function() {
|
|||||||
if(localVideos === 0) {
|
if(localVideos === 0) {
|
||||||
// No video, at least for now: show a placeholder
|
// No video, at least for now: show a placeholder
|
||||||
if($('#videolocal .no-video-container').length === 0) {
|
if($('#videolocal .no-video-container').length === 0) {
|
||||||
$('#videolocal').append(
|
$('#videolocal').prepend(
|
||||||
'<div class="no-video-container">' +
|
'<div class="no-video-container">' +
|
||||||
'<span class="no-video-text">No webcam available</span>' +
|
'<span class="no-video-text">No webcam available</span>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,12 +329,12 @@ $(document).ready(function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If we're here, a new track was added
|
// If we're here, a new track was added
|
||||||
var stream = localTracks[trackId];
|
let stream = localTracks[trackId];
|
||||||
if(stream) {
|
if(stream) {
|
||||||
// We've been here already
|
// We've been here already
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$('#videos').removeClass('hide').show();
|
$('#videos').removeClass('hide');
|
||||||
if($('#mute').length === 0) {
|
if($('#mute').length === 0) {
|
||||||
// Add a 'mute' button
|
// Add a 'mute' button
|
||||||
$('#videolocal').append('<button class="btn btn-warning btn-xs mute-button" id="mute">Mute</button>');
|
$('#videolocal').append('<button class="btn btn-warning btn-xs mute-button" id="mute">Mute</button>');
|
||||||
@ -330,23 +348,22 @@ $(document).ready(function() {
|
|||||||
if(localVideos === 0) {
|
if(localVideos === 0) {
|
||||||
// No video, at least for now: show a placeholder
|
// No video, at least for now: show a placeholder
|
||||||
if($('#videolocal .no-video-container').length === 0) {
|
if($('#videolocal .no-video-container').length === 0) {
|
||||||
$('#videolocal').append(
|
$('#videolocal').prepend(
|
||||||
'<div class="no-video-container">' +
|
'<div class="no-video-container">' +
|
||||||
'<span class="no-video-text">No webcam available</span>' +
|
'<span class="no-video-text">No webcam available</span>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// New video track: create a stream out of it
|
// New video track: create a stream out of it
|
||||||
localVideos++;
|
localVideos++;
|
||||||
$('#videolocal .no-video-container').remove();
|
$('#videolocal .no-video-container').remove();
|
||||||
stream = new MediaStream();
|
let stream = new MediaStream([track]);
|
||||||
stream.addTrack(track.clone());
|
|
||||||
localTracks[trackId] = stream;
|
localTracks[trackId] = stream;
|
||||||
Janus.log("Created local stream:", stream);
|
Janus.log("Created local stream:", stream);
|
||||||
Janus.log(stream.getTracks());
|
Janus.log(stream.getTracks());
|
||||||
Janus.log(stream.getVideoTracks());
|
Janus.log(stream.getVideoTracks());
|
||||||
$('#videolocal').append('<video class="rounded centered" id="myvideo' + trackId + '" width=100% autoplay playsinline muted="muted"/>');
|
$('#videolocal').prepend('<video class="rounded centered" id="myvideo' + trackId + '" width=100% autoplay playsinline muted="muted"/>');
|
||||||
Janus.attachMediaStream($('#myvideo' + trackId).get(0), stream);
|
Janus.attachMediaStream($('#myvideo' + trackId).get(0), stream);
|
||||||
}
|
}
|
||||||
if(sfutest.webrtcStuff.pc.iceConnectionState !== "completed" &&
|
if(sfutest.webrtcStuff.pc.iceConnectionState !== "completed" &&
|
||||||
@ -361,6 +378,7 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onremotetrack: function(track, mid, on) {
|
onremotetrack: function(track, mid, on) {
|
||||||
// The publisher stream is sendonly, we don't expect anything here
|
// The publisher stream is sendonly, we don't expect anything here
|
||||||
},
|
},
|
||||||
@ -392,8 +410,9 @@ $(document).ready(function() {
|
|||||||
}});
|
}});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function checkEnter(field, event) {
|
function checkEnter(field, event) {
|
||||||
var theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
|
let theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
|
||||||
if(theCode == 13) {
|
if(theCode == 13) {
|
||||||
registerUsername();
|
registerUsername();
|
||||||
return false;
|
return false;
|
||||||
@ -411,10 +430,10 @@ function registerUsername() {
|
|||||||
// Try a registration
|
// Try a registration
|
||||||
$('#username').attr('disabled', true);
|
$('#username').attr('disabled', true);
|
||||||
$('#register').attr('disabled', true).unbind('click');
|
$('#register').attr('disabled', true).unbind('click');
|
||||||
var username = $('#username').val();
|
let username = $('#username').val();
|
||||||
if(username === "") {
|
if(username === "") {
|
||||||
$('#you')
|
$('#you')
|
||||||
.removeClass().addClass('label label-warning')
|
.removeClass().addClass('badge bg-warning')
|
||||||
.html("Insert your display name (e.g., pippo)");
|
.html("Insert your display name (e.g., pippo)");
|
||||||
$('#username').removeAttr('disabled');
|
$('#username').removeAttr('disabled');
|
||||||
$('#register').removeAttr('disabled').click(registerUsername);
|
$('#register').removeAttr('disabled').click(registerUsername);
|
||||||
@ -422,13 +441,13 @@ function registerUsername() {
|
|||||||
}
|
}
|
||||||
if(/[^a-zA-Z0-9]/.test(username)) {
|
if(/[^a-zA-Z0-9]/.test(username)) {
|
||||||
$('#you')
|
$('#you')
|
||||||
.removeClass().addClass('label label-warning')
|
.removeClass().addClass('badge bg-warning')
|
||||||
.html('Input is not alphanumeric');
|
.html('Input is not alphanumeric');
|
||||||
$('#username').removeAttr('disabled').val("");
|
$('#username').removeAttr('disabled').val("");
|
||||||
$('#register').removeAttr('disabled').click(registerUsername);
|
$('#register').removeAttr('disabled').click(registerUsername);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var register = {
|
let register = {
|
||||||
request: "join",
|
request: "join",
|
||||||
room: myroom,
|
room: myroom,
|
||||||
ptype: "publisher",
|
ptype: "publisher",
|
||||||
@ -442,23 +461,32 @@ function registerUsername() {
|
|||||||
function publishOwnFeed(useAudio) {
|
function publishOwnFeed(useAudio) {
|
||||||
// Publish our stream
|
// Publish our stream
|
||||||
$('#publish').attr('disabled', true).unbind('click');
|
$('#publish').attr('disabled', true).unbind('click');
|
||||||
|
|
||||||
|
// We want sendonly audio and video (uncomment the data track
|
||||||
|
// too if you want to publish via datachannels as well)
|
||||||
|
let tracks = [];
|
||||||
|
if(useAudio)
|
||||||
|
tracks.push({ type: 'audio', capture: true, recv: false });
|
||||||
|
tracks.push({ type: 'video', capture: true, recv: false,
|
||||||
|
// We may need to enable simulcast or SVC on the video track
|
||||||
|
simulcast: doSimulcast,
|
||||||
|
// We only support SVC for VP9 and (still WIP) AV1
|
||||||
|
svc: ((vcodec === 'vp9' || vcodec === 'av1') && doSvc) ? doSvc : null
|
||||||
|
});
|
||||||
|
//~ tracks.push({ type: 'data' });
|
||||||
|
|
||||||
sfutest.createOffer(
|
sfutest.createOffer(
|
||||||
{
|
{
|
||||||
// Add data:true here if you want to publish datachannels as well
|
tracks: tracks,
|
||||||
media: { audioRecv: false, videoRecv: false, audioSend: useAudio, videoSend: true }, // Publishers are sendonly
|
|
||||||
// If you want to test simulcasting (Chrome and Firefox only), then
|
|
||||||
// pass a ?simulcast=true when opening this demo page: it will turn
|
|
||||||
// the following 'simulcast' property to pass to janus.js to true
|
|
||||||
simulcast: doSimulcast,
|
|
||||||
success: function(jsep) {
|
success: function(jsep) {
|
||||||
Janus.debug("Got publisher SDP!");
|
Janus.debug("Got publisher SDP!");
|
||||||
Janus.debug(jsep);
|
Janus.debug(jsep);
|
||||||
var publish = { request: "configure", audio: useAudio, video: true };
|
let publish = { request: "configure", audio: useAudio, video: true };
|
||||||
// You can force a specific codec to use when publishing by using the
|
// You can force a specific codec to use when publishing by using the
|
||||||
// audiocodec and videocodec properties, for instance:
|
// audiocodec and videocodec properties, for instance:
|
||||||
// publish["audiocodec"] = "opus"
|
// publish["audiocodec"] = "opus"
|
||||||
// to force Opus as the audio codec to use, or:
|
// to force Opus as the audio codec to use, or:
|
||||||
// publish["videocodec"] = "vp9"
|
// publish["videocodec"] = "vp9"
|
||||||
// to force VP9 as the videocodec to use. In both case, though, forcing
|
// to force VP9 as the videocodec to use. In both case, though, forcing
|
||||||
// a codec will only work if: (1) the codec is actually in the SDP (and
|
// a codec will only work if: (1) the codec is actually in the SDP (and
|
||||||
// so the browser supports it), and (2) the codec is in the list of
|
// so the browser supports it), and (2) the codec is in the list of
|
||||||
@ -483,7 +511,7 @@ function publishOwnFeed(useAudio) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
var muted = sfutest.isAudioMuted();
|
let muted = sfutest.isAudioMuted();
|
||||||
Janus.log((muted ? "Unmuting" : "Muting") + " local stream...");
|
Janus.log((muted ? "Unmuting" : "Muting") + " local stream...");
|
||||||
if(muted)
|
if(muted)
|
||||||
sfutest.unmuteAudio();
|
sfutest.unmuteAudio();
|
||||||
@ -496,7 +524,7 @@ function toggleMute() {
|
|||||||
function unpublishOwnFeed() {
|
function unpublishOwnFeed() {
|
||||||
// Unpublish our stream
|
// Unpublish our stream
|
||||||
$('#unpublish').attr('disabled', true).unbind('click');
|
$('#unpublish').attr('disabled', true).unbind('click');
|
||||||
var unpublish = { request: "unpublish" };
|
let unpublish = { request: "unpublish" };
|
||||||
sfutest.send({ message: unpublish });
|
sfutest.send({ message: unpublish });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,21 +542,28 @@ function subscribeTo(sources) {
|
|||||||
if(remoteFeed) {
|
if(remoteFeed) {
|
||||||
// Prepare the streams to subscribe to, as an array: we have the list of
|
// Prepare the streams to subscribe to, as an array: we have the list of
|
||||||
// streams the feeds are publishing, so we can choose what to pick or skip
|
// streams the feeds are publishing, so we can choose what to pick or skip
|
||||||
var subscription = [];
|
let added = null, removed = null;
|
||||||
for(var s in sources) {
|
for(let s in sources) {
|
||||||
var streams = sources[s];
|
let streams = sources[s];
|
||||||
for(var i in streams) {
|
for(let i in streams) {
|
||||||
var stream = streams[i];
|
let stream = streams[i];
|
||||||
// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video
|
// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video
|
||||||
if(stream.type === "video" && Janus.webRTCAdapter.browserDetails.browser === "safari" &&
|
if(stream.type === "video" && Janus.webRTCAdapter.browserDetails.browser === "safari" &&
|
||||||
(stream.codec === "vp9" || (stream.codec === "vp8" && !Janus.safariVp8))) {
|
((stream.codec === "vp9" && !Janus.safariVp9) || (stream.codec === "vp8" && !Janus.safariVp8))) {
|
||||||
toastr.warning("Publisher is using " + stream.codec.toUpperCase +
|
toastr.warning("Publisher is using " + stream.codec.toUpperCase +
|
||||||
", but Safari doesn't support it: disabling video stream #" + stream.mindex);
|
", but Safari doesn't support it: disabling video stream #" + stream.mindex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(stream.disabled) {
|
if(stream.disabled) {
|
||||||
Janus.log("Disabled stream:", stream);
|
Janus.log("Disabled stream:", stream);
|
||||||
// TODO Skipping for now, we should unsubscribe
|
// Unsubscribe
|
||||||
|
if(!removed)
|
||||||
|
removed = [];
|
||||||
|
removed.push({
|
||||||
|
feed: stream.id, // This is mandatory
|
||||||
|
mid: stream.mid // This is optional (all streams, if missing)
|
||||||
|
});
|
||||||
|
delete subscriptions[stream.id][stream.mid];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(subscriptions[stream.id] && subscriptions[stream.id][stream.mid]) {
|
if(subscriptions[stream.id] && subscriptions[stream.id][stream.mid]) {
|
||||||
@ -537,35 +572,40 @@ function subscribeTo(sources) {
|
|||||||
}
|
}
|
||||||
// Find an empty slot in the UI for each new source
|
// Find an empty slot in the UI for each new source
|
||||||
if(!feedStreams[stream.id].slot) {
|
if(!feedStreams[stream.id].slot) {
|
||||||
var slot;
|
let slot;
|
||||||
for(var i=1;i<6;i++) {
|
for(let i=1;i<6;i++) {
|
||||||
if(!feeds[i]) {
|
if(!feeds[i]) {
|
||||||
slot = i;
|
slot = i;
|
||||||
feeds[slot] = stream.id;
|
feeds[slot] = stream.id;
|
||||||
feedStreams[stream.id].slot = slot;
|
feedStreams[stream.id].slot = slot;
|
||||||
feedStreams[stream.id].remoteVideos = 0;
|
feedStreams[stream.id].remoteVideos = 0;
|
||||||
$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).show();
|
$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).removeClass('hide');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subscription.push({
|
// Subscribe
|
||||||
|
if(!added)
|
||||||
|
added = [];
|
||||||
|
added.push({
|
||||||
feed: stream.id, // This is mandatory
|
feed: stream.id, // This is mandatory
|
||||||
mid: stream.mid // This is optional (all streams, if missing)
|
mid: stream.mid // This is optional (all streams, if missing)
|
||||||
});
|
});
|
||||||
if(!subscriptions[stream.id])
|
if(!subscriptions[stream.id])
|
||||||
subscriptions[stream.id] = {};
|
subscriptions[stream.id] = {};
|
||||||
subscriptions[stream.id][stream.mid] = true;
|
subscriptions[stream.id][stream.mid] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(subscription.length === 0) {
|
if((!added || added.length === 0) && (!removed || removed.length === 0)) {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
remoteFeed.send({ message: {
|
let update = { request: 'update' };
|
||||||
request: "subscribe",
|
if(added)
|
||||||
streams: subscription
|
update.subscribe = added;
|
||||||
}});
|
if(removed)
|
||||||
|
update.unsubscribe = removed;
|
||||||
|
remoteFeed.send({ message: update });
|
||||||
// Nothing else we need to do
|
// Nothing else we need to do
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -582,14 +622,14 @@ function subscribeTo(sources) {
|
|||||||
Janus.log(" -- This is a multistream subscriber");
|
Janus.log(" -- This is a multistream subscriber");
|
||||||
// Prepare the streams to subscribe to, as an array: we have the list of
|
// Prepare the streams to subscribe to, as an array: we have the list of
|
||||||
// streams the feed is publishing, so we can choose what to pick or skip
|
// streams the feed is publishing, so we can choose what to pick or skip
|
||||||
var subscription = [];
|
let subscription = [];
|
||||||
for(var s in sources) {
|
for(let s in sources) {
|
||||||
var streams = sources[s];
|
let streams = sources[s];
|
||||||
for(var i in streams) {
|
for(let i in streams) {
|
||||||
var stream = streams[i];
|
let stream = streams[i];
|
||||||
// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video
|
// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video
|
||||||
if(stream.type === "video" && Janus.webRTCAdapter.browserDetails.browser === "safari" &&
|
if(stream.type === "video" && Janus.webRTCAdapter.browserDetails.browser === "safari" &&
|
||||||
(stream.codec === "vp9" || (stream.codec === "vp8" && !Janus.safariVp8))) {
|
((stream.codec === "vp9" && !Janus.safariVp9) || (stream.codec === "vp8" && !Janus.safariVp8))) {
|
||||||
toastr.warning("Publisher is using " + stream.codec.toUpperCase +
|
toastr.warning("Publisher is using " + stream.codec.toUpperCase +
|
||||||
", but Safari doesn't support it: disabling video stream #" + stream.mindex);
|
", but Safari doesn't support it: disabling video stream #" + stream.mindex);
|
||||||
continue;
|
continue;
|
||||||
@ -606,21 +646,21 @@ function subscribeTo(sources) {
|
|||||||
}
|
}
|
||||||
// Find an empty slot in the UI for each new source
|
// Find an empty slot in the UI for each new source
|
||||||
if(!feedStreams[stream.id].slot) {
|
if(!feedStreams[stream.id].slot) {
|
||||||
var slot;
|
let slot;
|
||||||
for(var i=1;i<6;i++) {
|
for(let i=1;i<6;i++) {
|
||||||
if(!feeds[i]) {
|
if(!feeds[i]) {
|
||||||
slot = i;
|
slot = i;
|
||||||
feeds[slot] = stream.id;
|
feeds[slot] = stream.id;
|
||||||
feedStreams[stream.id].slot = slot;
|
feedStreams[stream.id].slot = slot;
|
||||||
feedStreams[stream.id].remoteVideos = 0;
|
feedStreams[stream.id].remoteVideos = 0;
|
||||||
$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).show();
|
$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).removeClass('hide');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subscription.push({
|
subscription.push({
|
||||||
feed: stream.id, // This is mandatory
|
feed: stream.id, // This is mandatory
|
||||||
mid: stream.mid // This is optional (all streams, if missing)
|
mid: stream.mid // This is optional (all streams, if missing)
|
||||||
});
|
});
|
||||||
if(!subscriptions[stream.id])
|
if(!subscriptions[stream.id])
|
||||||
subscriptions[stream.id] = {};
|
subscriptions[stream.id] = {};
|
||||||
@ -628,11 +668,12 @@ function subscribeTo(sources) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We wait for the plugin to send us an offer
|
// We wait for the plugin to send us an offer
|
||||||
var subscribe = {
|
let subscribe = {
|
||||||
request: "join",
|
request: "join",
|
||||||
room: myroom,
|
room: myroom,
|
||||||
ptype: "subscriber",
|
ptype: "subscriber",
|
||||||
streams: subscription,
|
streams: subscription,
|
||||||
|
use_msid: use_msid,
|
||||||
private_id: mypvtid
|
private_id: mypvtid
|
||||||
};
|
};
|
||||||
remoteFeed.send({ message: subscribe });
|
remoteFeed.send({ message: subscribe });
|
||||||
@ -653,7 +694,7 @@ function subscribeTo(sources) {
|
|||||||
},
|
},
|
||||||
onmessage: function(msg, jsep) {
|
onmessage: function(msg, jsep) {
|
||||||
Janus.debug(" ::: Got a message (subscriber) :::", msg);
|
Janus.debug(" ::: Got a message (subscriber) :::", msg);
|
||||||
var event = msg["videoroom"];
|
let event = msg["videoroom"];
|
||||||
Janus.debug("Event: " + event);
|
Janus.debug("Event: " + event);
|
||||||
if(msg["error"]) {
|
if(msg["error"]) {
|
||||||
bootbox.alert(msg["error"]);
|
bootbox.alert(msg["error"]);
|
||||||
@ -664,21 +705,32 @@ function subscribeTo(sources) {
|
|||||||
Janus.log("Successfully attached to feed in room " + msg["room"]);
|
Janus.log("Successfully attached to feed in room " + msg["room"]);
|
||||||
} else if(event === "event") {
|
} else if(event === "event") {
|
||||||
// Check if we got an event on a simulcast-related event from this publisher
|
// Check if we got an event on a simulcast-related event from this publisher
|
||||||
var mid = msg["mid"];
|
let mid = msg["mid"];
|
||||||
var substream = msg["substream"];
|
let substream = msg["substream"];
|
||||||
var temporal = msg["temporal"];
|
let temporal = msg["temporal"];
|
||||||
if((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {
|
if((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {
|
||||||
// Check which this feed this refers to
|
// Check which this feed this refers to
|
||||||
var sub = subStreams[mid];
|
let slot = slots[mid];
|
||||||
var feed = feedStreams[sub.feed_id];
|
|
||||||
var slot = slots[mid];
|
|
||||||
if(!simulcastStarted[slot]) {
|
if(!simulcastStarted[slot]) {
|
||||||
simulcastStarted[slot] = true;
|
simulcastStarted[slot] = true;
|
||||||
// Add some new buttons
|
// Add some new buttons
|
||||||
addSimulcastButtons(slot, true);
|
addSimulcastSvcButtons(slot, true);
|
||||||
}
|
}
|
||||||
// We just received notice that there's been a switch, update the buttons
|
// We just received notice that there's been a switch, update the buttons
|
||||||
updateSimulcastButtons(slot, substream, temporal);
|
updateSimulcastSvcButtons(slot, substream, temporal);
|
||||||
|
}
|
||||||
|
// Or maybe SVC?
|
||||||
|
let spatial = msg["spatial_layer"];
|
||||||
|
temporal = msg["temporal_layer"];
|
||||||
|
if((spatial !== null && spatial !== undefined) || (temporal !== null && temporal !== undefined)) {
|
||||||
|
let slot = slots[mid];
|
||||||
|
if(!svcStarted[slot]) {
|
||||||
|
svcStarted[slot] = true;
|
||||||
|
// Add some new buttons
|
||||||
|
addSimulcastSvcButtons(slot, true);
|
||||||
|
}
|
||||||
|
// We just received notice that there's been a switch, update the buttons
|
||||||
|
updateSimulcastSvcButtons(slot, spatial, temporal);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// What has just happened?
|
// What has just happened?
|
||||||
@ -686,10 +738,10 @@ function subscribeTo(sources) {
|
|||||||
}
|
}
|
||||||
if(msg["streams"]) {
|
if(msg["streams"]) {
|
||||||
// Update map of subscriptions by mid
|
// Update map of subscriptions by mid
|
||||||
for(var i in msg["streams"]) {
|
for(let i in msg["streams"]) {
|
||||||
var mid = msg["streams"][i]["mid"];
|
let mid = msg["streams"][i]["mid"];
|
||||||
subStreams[mid] = msg["streams"][i];
|
subStreams[mid] = msg["streams"][i];
|
||||||
var feed = feedStreams[msg["streams"][i]["feed_id"]];
|
let feed = feedStreams[msg["streams"][i]["feed_id"]];
|
||||||
if(feed && feed.slot) {
|
if(feed && feed.slot) {
|
||||||
slots[mid] = feed.slot;
|
slots[mid] = feed.slot;
|
||||||
mids[feed.slot] = mid;
|
mids[feed.slot] = mid;
|
||||||
@ -702,13 +754,17 @@ function subscribeTo(sources) {
|
|||||||
remoteFeed.createAnswer(
|
remoteFeed.createAnswer(
|
||||||
{
|
{
|
||||||
jsep: jsep,
|
jsep: jsep,
|
||||||
// Add data:true here if you want to subscribe to datachannels as well
|
// We only specify data channels here, as this way in
|
||||||
// (obviously only works if the publisher offered them in the first place)
|
// case they were offered we'll enable them. Since we
|
||||||
media: { audioSend: false, videoSend: false }, // We want recvonly audio/video
|
// don't mention audio or video tracks, we autoaccept them
|
||||||
|
// as recvonly (since we won't capture anything ourselves)
|
||||||
|
tracks: [
|
||||||
|
{ type: 'data' }
|
||||||
|
],
|
||||||
success: function(jsep) {
|
success: function(jsep) {
|
||||||
Janus.debug("Got SDP!");
|
Janus.debug("Got SDP!");
|
||||||
Janus.debug(jsep);
|
Janus.debug(jsep);
|
||||||
var body = { request: "start", room: myroom };
|
let body = { request: "start", room: myroom };
|
||||||
remoteFeed.send({ message: body, jsep: jsep });
|
remoteFeed.send({ message: body, jsep: jsep });
|
||||||
},
|
},
|
||||||
error: function(error) {
|
error: function(error) {
|
||||||
@ -718,16 +774,21 @@ function subscribeTo(sources) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onlocaltrack: function(track, on) {
|
onlocaltrack: function(track, on) {
|
||||||
// The subscriber stream is recvonly, we don't expect anything here
|
// The subscriber stream is recvonly, we don't expect anything here
|
||||||
},
|
},
|
||||||
onremotetrack: function(track, mid, on) {
|
onremotetrack: function(track, mid, on, metadata) {
|
||||||
Janus.debug("Remote track (mid=" + mid + ") " + (on ? "added" : "removed") + ":", track);
|
Janus.debug(
|
||||||
|
"Remote track (mid=" + mid + ") " +
|
||||||
|
(on ? "added" : "removed") +
|
||||||
|
(metadata ? " (" + metadata.reason + ") ": "") + ":", track
|
||||||
|
);
|
||||||
// Which publisher are we getting on this mid?
|
// Which publisher are we getting on this mid?
|
||||||
var sub = subStreams[mid];
|
let sub = subStreams[mid];
|
||||||
var feed = feedStreams[sub.feed_id];
|
let feed = feedStreams[sub.feed_id];
|
||||||
Janus.debug(" >> This track is coming from feed " + sub.feed_id + ":", feed);
|
Janus.debug(" >> This track is coming from feed " + sub.feed_id + ":", feed);
|
||||||
var slot = slots[mid];
|
let slot = slots[mid];
|
||||||
if(feed && !slot) {
|
if(feed && !slot) {
|
||||||
slot = feed.slot;
|
slot = feed.slot;
|
||||||
slots[mid] = feed.slot;
|
slots[mid] = feed.slot;
|
||||||
@ -736,17 +797,6 @@ function subscribeTo(sources) {
|
|||||||
Janus.debug(" >> mid " + mid + " is in slot " + slot);
|
Janus.debug(" >> mid " + mid + " is in slot " + slot);
|
||||||
if(!on) {
|
if(!on) {
|
||||||
// Track removed, get rid of the stream and the rendering
|
// Track removed, get rid of the stream and the rendering
|
||||||
var stream = remoteTracks[mid];
|
|
||||||
if(stream) {
|
|
||||||
try {
|
|
||||||
var tracks = stream.getTracks();
|
|
||||||
for(var i in tracks) {
|
|
||||||
var mst = tracks[i];
|
|
||||||
if(mst)
|
|
||||||
mst.stop();
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
$('#remotevideo' + slot + '-' + mid).remove();
|
$('#remotevideo' + slot + '-' + mid).remove();
|
||||||
if(track.kind === "video" && feed) {
|
if(track.kind === "video" && feed) {
|
||||||
feed.remoteVideos--;
|
feed.remoteVideos--;
|
||||||
@ -766,16 +816,11 @@ function subscribeTo(sources) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If we're here, a new track was added
|
// If we're here, a new track was added
|
||||||
if(feed.spinner) {
|
|
||||||
feed.spinner.stop();
|
|
||||||
feed.spinner = null;
|
|
||||||
}
|
|
||||||
if($('#remotevideo' + slot + '-' + mid).length > 0)
|
if($('#remotevideo' + slot + '-' + mid).length > 0)
|
||||||
return;
|
return;
|
||||||
if(track.kind === "audio") {
|
if(track.kind === "audio") {
|
||||||
// New audio track: create a stream out of it, and use a hidden <audio> element
|
// New audio track: create a stream out of it, and use a hidden <audio> element
|
||||||
stream = new MediaStream();
|
let stream = new MediaStream([track]);
|
||||||
stream.addTrack(track.clone());
|
|
||||||
remoteTracks[mid] = stream;
|
remoteTracks[mid] = stream;
|
||||||
Janus.log("Created remote audio stream:", stream);
|
Janus.log("Created remote audio stream:", stream);
|
||||||
$('#videoremote' + slot).append('<audio class="hide" id="remotevideo' + slot + '-' + mid + '" autoplay playsinline/>');
|
$('#videoremote' + slot).append('<audio class="hide" id="remotevideo' + slot + '-' + mid + '" autoplay playsinline/>');
|
||||||
@ -793,45 +838,47 @@ function subscribeTo(sources) {
|
|||||||
// New video track: create a stream out of it
|
// New video track: create a stream out of it
|
||||||
feed.remoteVideos++;
|
feed.remoteVideos++;
|
||||||
$('#videoremote' + slot + ' .no-video-container').remove();
|
$('#videoremote' + slot + ' .no-video-container').remove();
|
||||||
stream = new MediaStream();
|
let stream = new MediaStream([track]);
|
||||||
stream.addTrack(track.clone());
|
|
||||||
remoteTracks[mid] = stream;
|
remoteTracks[mid] = stream;
|
||||||
Janus.log("Created remote video stream:", stream);
|
Janus.log("Created remote video stream:", stream);
|
||||||
$('#videoremote' + slot).append('<video class="rounded centered" id="remotevideo' + slot + '-' + mid + '" width=100% autoplay playsinline/>');
|
$('#videoremote' + slot).append('<video class="rounded centered" id="remotevideo' + slot + '-' + mid + '" width=100% autoplay playsinline/>');
|
||||||
$('#videoremote' + slot).append(
|
$('#videoremote' + slot).append(
|
||||||
'<span class="label label-primary hide resolution-label" id="curres'+slot+'"></span>' +
|
'<span class="badge bg-primary hide resolution-label" id="curres'+slot+'"></span>' +
|
||||||
'<span class="label label-info hide bitrate-label" id="curbitrate'+slot+'"></span>');
|
'<span class="badge bg-info hide bitrate-label" id="curbitrate'+slot+'"></span>');
|
||||||
Janus.attachMediaStream($('#remotevideo' + slot + '-' + mid).get(0), stream);
|
Janus.attachMediaStream($('#remotevideo' + slot + '-' + mid).get(0), stream);
|
||||||
// Note: we'll need this for additional videos too
|
// Note: we'll need this for additional videos too
|
||||||
if(!bitrateTimer[slot]) {
|
if(!bitrateTimer[slot]) {
|
||||||
$('#curbitrate' + slot).removeClass('hide').show();
|
$('#curbitrate' + slot).removeClass('hide');
|
||||||
bitrateTimer[slot] = setInterval(function() {
|
bitrateTimer[slot] = setInterval(function() {
|
||||||
if(!$("#videoremote" + slot + ' video').get(0))
|
if(!$("#videoremote" + slot + ' video').get(0))
|
||||||
return;
|
return;
|
||||||
// Display updated bitrate, if supported
|
// Display updated bitrate, if supported
|
||||||
var bitrate = remoteFeed.getBitrate(mid);
|
let bitrate = remoteFeed.getBitrate(mid);
|
||||||
$('#curbitrate' + slot).text(bitrate);
|
$('#curbitrate' + slot).text(bitrate);
|
||||||
// Check if the resolution changed too
|
// Check if the resolution changed too
|
||||||
var width = $("#videoremote" + slot + ' video').get(0).videoWidth;
|
let width = $("#videoremote" + slot + ' video').get(0).videoWidth;
|
||||||
var height = $("#videoremote" + slot + ' video').get(0).videoHeight;
|
let height = $("#videoremote" + slot + ' video').get(0).videoHeight;
|
||||||
if(width > 0 && height > 0)
|
if(width > 0 && height > 0) {
|
||||||
$('#curres' + slot).removeClass('hide').text(width+'x'+height).show();
|
let res = width + 'x' + height;
|
||||||
|
if(simulcastStarted[slot])
|
||||||
|
res += ' (simulcast)';
|
||||||
|
else if(svcStarted[slot])
|
||||||
|
res += ' (SVC)';
|
||||||
|
$('#curres' + slot).removeClass('hide').text(res).removeClass('hide');
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
oncleanup: function() {
|
oncleanup: function() {
|
||||||
Janus.log(" ::: Got a cleanup notification (remote feed) :::");
|
Janus.log(" ::: Got a cleanup notification (remote feed) :::");
|
||||||
for(var i=1;i<6;i++) {
|
for(let i=1;i<6;i++) {
|
||||||
$('#remotevideo'+i).remove();
|
$('#videoremote'+i).empty();
|
||||||
$('#waitingvideo'+i).remove();
|
|
||||||
$('#novideo'+i).remove();
|
|
||||||
$('#curbitrate'+i).remove();
|
|
||||||
$('#curres'+i).remove();
|
|
||||||
if(bitrateTimer[i])
|
if(bitrateTimer[i])
|
||||||
clearInterval(bitrateTimer[i]);
|
clearInterval(bitrateTimer[i]);
|
||||||
bitrateTimer[i] = null;
|
bitrateTimer[i] = null;
|
||||||
feedStreams[i].simulcastStarted = false;
|
feedStreams[i].simulcastStarted = false;
|
||||||
|
feedStreams[i].svcStarted = false;
|
||||||
feedStreams[i].remoteVideos = 0;
|
feedStreams[i].remoteVideos = 0;
|
||||||
$('#simulcast'+i).remove();
|
$('#simulcast'+i).remove();
|
||||||
}
|
}
|
||||||
@ -842,22 +889,23 @@ function subscribeTo(sources) {
|
|||||||
|
|
||||||
function unsubscribeFrom(id) {
|
function unsubscribeFrom(id) {
|
||||||
// Unsubscribe from this publisher
|
// Unsubscribe from this publisher
|
||||||
var feed = feedStreams[id];
|
let feed = feedStreams[id];
|
||||||
if(!feed)
|
if(!feed)
|
||||||
return;
|
return;
|
||||||
Janus.debug("Feed " + id + " (" + feed.display + ") has left the room, detaching");
|
Janus.debug("Feed " + id + " (" + feed.display + ") has left the room, detaching");
|
||||||
if(bitrateTimer[feed.slot])
|
if(bitrateTimer[feed.slot])
|
||||||
clearInterval(bitrateTimer[feed.slot]);
|
clearInterval(bitrateTimer[feed.slot]);
|
||||||
bitrateTimer[feed.slot] = null;
|
bitrateTimer[feed.slot] = null;
|
||||||
$('#remote' + feed.slot).empty().hide();
|
$('#remote' + feed.slot).empty().addClass('hide');
|
||||||
$('#videoremote' + feed.slot).empty();
|
$('#videoremote' + feed.slot).empty();
|
||||||
delete simulcastStarted[feed.slot];
|
delete simulcastStarted[feed.slot];
|
||||||
|
delete svcStarted[feed.slot];
|
||||||
$('#simulcast' + feed.slot).remove();
|
$('#simulcast' + feed.slot).remove();
|
||||||
delete feeds[feed.slot];
|
delete feeds[feed.slot];
|
||||||
feeds.slot = 0;
|
feeds.slot = 0;
|
||||||
delete feedStreams[id];
|
delete feedStreams[id];
|
||||||
// Send an unsubscribe request
|
// Send an unsubscribe request
|
||||||
var unsubscribe = {
|
let unsubscribe = {
|
||||||
request: "unsubscribe",
|
request: "unsubscribe",
|
||||||
streams: [{ feed: id }]
|
streams: [{ feed: id }]
|
||||||
};
|
};
|
||||||
@ -868,8 +916,8 @@ function unsubscribeFrom(id) {
|
|||||||
|
|
||||||
// Helper to parse query string
|
// Helper to parse query string
|
||||||
function getQueryStringValue(name) {
|
function getQueryStringValue(name) {
|
||||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
name = name.replace(/[[]/, "\\[").replace(/[\]]/, "\\]");
|
||||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
let regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||||
results = regex.exec(location.search);
|
results = regex.exec(location.search);
|
||||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||||
}
|
}
|
||||||
@ -877,137 +925,161 @@ function getQueryStringValue(name) {
|
|||||||
// Helper to escape XML tags
|
// Helper to escape XML tags
|
||||||
function escapeXmlTags(value) {
|
function escapeXmlTags(value) {
|
||||||
if(value) {
|
if(value) {
|
||||||
var escapedValue = value.replace(new RegExp('<', 'g'), '<');
|
let escapedValue = value.replace(new RegExp('<', 'g'), '<');
|
||||||
escapedValue = escapedValue.replace(new RegExp('>', 'g'), '>');
|
escapedValue = escapedValue.replace(new RegExp('>', 'g'), '>');
|
||||||
return escapedValue;
|
return escapedValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers to create Simulcast-related UI, if enabled
|
// Helpers to create Simulcast- or SVC-related UI, if enabled
|
||||||
function addSimulcastButtons(feed, temporal) {
|
function addSimulcastSvcButtons(feed, temporal) {
|
||||||
var index = feed;
|
let index = feed;
|
||||||
|
let simulcast = simulcastStarted[index];
|
||||||
|
let what = (simulcast ? 'simulcast' : 'SVC');
|
||||||
|
let layer = (simulcast ? 'substream' : 'layer');
|
||||||
$('#remote'+index).parent().append(
|
$('#remote'+index).parent().append(
|
||||||
'<div id="simulcast'+index+'" class="btn-group-vertical btn-group-vertical-xs pull-right">' +
|
'<div id="simulcast'+index+'" class="btn-group-vertical btn-group-xs top-right">' +
|
||||||
' <div class"row">' +
|
' <div class="btn-group btn-group-xs d-flex simulcast-button-group">' +
|
||||||
' <div class="btn-group btn-group-xs simulcast-button-group">' +
|
' <button id="sl'+index+'-2" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Switch to higher quality">SL 2</button>' +
|
||||||
' <button id="sl'+index+'-2" type="button" class="btn btn-primary simulcast-button" data-toggle="tooltip" title="Switch to higher quality" style="width: 33%">SL 2</button>' +
|
' <button id="sl'+index+'-1" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Switch to normal quality">SL 1</button>' +
|
||||||
' <button id="sl'+index+'-1" type="button" class="btn btn-primary simulcast-button" data-toggle="tooltip" title="Switch to normal quality" style="width: 33%">SL 1</button>' +
|
' <button id="sl'+index+'-0" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Switch to lower quality">SL 0</button>' +
|
||||||
' <button id="sl'+index+'-0" type="button" class="btn btn-primary simulcast-button" data-toggle="tooltip" title="Switch to lower quality" style="width: 34%">SL 0</button>' +
|
' </div>' +
|
||||||
' </div>' +
|
' <div class="btn-group btn-group-xs d-flex hide simulcast-button-group">' +
|
||||||
' </div>' +
|
' <button id="tl'+index+'-2" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Cap to temporal layer 2">TL 2</button>' +
|
||||||
' <div class"row">' +
|
' <button id="tl'+index+'-1" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Cap to temporal layer 1">TL 1</button>' +
|
||||||
' <div class="btn-group btn-group-xs hide simulcast-button-group">' +
|
' <button id="tl'+index+'-0" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Cap to temporal layer 0">TL 0</button>' +
|
||||||
' <button id="tl'+index+'-2" type="button" class="btn btn-primary simulcast-button" data-toggle="tooltip" title="Cap to temporal layer 2">TL 2</button>' +
|
' </div>' +
|
||||||
' <button id="tl'+index+'-1" type="button" class="btn btn-primary simulcast-button" data-toggle="tooltip" title="Cap to temporal layer 1">TL 1</button>' +
|
'</div>'
|
||||||
' <button id="tl'+index+'-0" type="button" class="btn btn-primary simulcast-button" data-toggle="tooltip" title="Cap to temporal layer 0">TL 0</button>' +
|
|
||||||
' </div>' +
|
|
||||||
' </div>' +
|
|
||||||
'</div>'
|
|
||||||
);
|
);
|
||||||
|
if(simulcast && Janus.webRTCAdapter.browserDetails.browser !== "firefox") {
|
||||||
|
// Chromium-based browsers only have two temporal layers, when doing simulcast
|
||||||
|
$('#tl'+index+'-2').remove();
|
||||||
|
}
|
||||||
// Enable the simulcast selection buttons
|
// Enable the simulcast selection buttons
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')
|
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')
|
||||||
.unbind('click').click(function() {
|
.unbind('click').click(function() {
|
||||||
var index = $(this).attr('id').split('sl')[1].split('-')[0];
|
let index = $(this).attr('id').split('sl')[1].split('-')[0];
|
||||||
toastr.info("Switching simulcast substream (mid=" + mids[index] + "), wait for it... (lower quality)", null, {timeOut: 2000});
|
toastr.info("Switching " + what + " " + layer + " (mid=" + mids[index] + "), wait for it... (lower quality)", null, {timeOut: 2000});
|
||||||
if(!$('#sl' + index + '-2').hasClass('btn-success'))
|
if(!$('#sl' + index + '-2').hasClass('btn-success'))
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
if(!$('#sl' + index + '-1').hasClass('btn-success'))
|
if(!$('#sl' + index + '-1').hasClass('btn-success'))
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
||||||
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 0 }});
|
if(simulcastStarted[index])
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 0 }});
|
||||||
|
else
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], spatial_layer: 0 }});
|
||||||
});
|
});
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')
|
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')
|
||||||
.unbind('click').click(function() {
|
.unbind('click').click(function() {
|
||||||
var index = $(this).attr('id').split('sl')[1].split('-')[0];
|
let index = $(this).attr('id').split('sl')[1].split('-')[0];
|
||||||
toastr.info("Switching simulcast substream (mid=" + mids[index] + "), wait for it... (normal quality)", null, {timeOut: 2000});
|
toastr.info("Switching " + what + " " + layer + " (mid=" + mids[index] + "), wait for it... (normal quality)", null, {timeOut: 2000});
|
||||||
if(!$('#sl' + index + '-2').hasClass('btn-success'))
|
if(!$('#sl' + index + '-2').hasClass('btn-success'))
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
||||||
if(!$('#sl' + index + '-0').hasClass('btn-success'))
|
if(!$('#sl' + index + '-0').hasClass('btn-success'))
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 1 }});
|
if(simulcastStarted[index])
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 1 }});
|
||||||
|
else
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], spatial_layer: 1 }});
|
||||||
});
|
});
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')
|
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')
|
||||||
.unbind('click').click(function() {
|
.unbind('click').click(function() {
|
||||||
var index = $(this).attr('id').split('sl')[1].split('-')[0];
|
let index = $(this).attr('id').split('sl')[1].split('-')[0];
|
||||||
toastr.info("Switching simulcast substream (mid=" + mids[index] + "), wait for it... (higher quality)", null, {timeOut: 2000});
|
toastr.info("Switching " + what + " " + layer + " (mid=" + mids[index] + "), wait for it... (higher quality)", null, {timeOut: 2000});
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
||||||
if(!$('#sl' + index + '-1').hasClass('btn-success'))
|
if(!$('#sl' + index + '-1').hasClass('btn-success'))
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
if(!$('#sl' + index + '-0').hasClass('btn-success'))
|
if(!$('#sl' + index + '-0').hasClass('btn-success'))
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 2 }});
|
if(simulcastStarted[index])
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 2 }});
|
||||||
|
else
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], spatial_layer: 2 }});
|
||||||
});
|
});
|
||||||
if(!temporal) // No temporal layer support
|
if(!temporal) // No temporal layer support
|
||||||
return;
|
return;
|
||||||
$('#tl' + index + '-0').parent().removeClass('hide');
|
$('#tl' + index + '-0').parent().removeClass('hide');
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')
|
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')
|
||||||
.unbind('click').click(function() {
|
.unbind('click').click(function() {
|
||||||
var index = $(this).attr('id').split('tl')[1].split('-')[0];
|
let index = $(this).attr('id').split('tl')[1].split('-')[0];
|
||||||
toastr.info("Capping simulcast temporal layer (mid=" + mids[index] + "), wait for it... (lowest FPS)", null, {timeOut: 2000});
|
toastr.info("Capping " + what + " temporal layer (mid=" + mids[index] + "), wait for it... (lowest FPS)", null, {timeOut: 2000});
|
||||||
if(!$('#tl' + index + '-2').hasClass('btn-success'))
|
if(!$('#tl' + index + '-2').hasClass('btn-success'))
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
if(!$('#tl' + index + '-1').hasClass('btn-success'))
|
if(!$('#tl' + index + '-1').hasClass('btn-success'))
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
||||||
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 0 }});
|
if(simulcastStarted[index])
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 0 }});
|
||||||
|
else
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal_layer: 0 }});
|
||||||
});
|
});
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')
|
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')
|
||||||
.unbind('click').click(function() {
|
.unbind('click').click(function() {
|
||||||
var index = $(this).attr('id').split('tl')[1].split('-')[0];
|
let index = $(this).attr('id').split('tl')[1].split('-')[0];
|
||||||
toastr.info("Capping simulcast temporal layer (mid=" + mids[index] + "), wait for it... (medium FPS)", null, {timeOut: 2000});
|
toastr.info("Capping " + what + " temporal layer (mid=" + mids[index] + "), wait for it... (medium FPS)", null, {timeOut: 2000});
|
||||||
if(!$('#tl' + index + '-2').hasClass('btn-success'))
|
if(!$('#tl' + index + '-2').hasClass('btn-success'))
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-info');
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-info');
|
||||||
if(!$('#tl' + index + '-0').hasClass('btn-success'))
|
if(!$('#tl' + index + '-0').hasClass('btn-success'))
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 1 }});
|
if(simulcastStarted[index])
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 1 }});
|
||||||
|
else
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal_layer: 1 }});
|
||||||
});
|
});
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')
|
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')
|
||||||
.unbind('click').click(function() {
|
.unbind('click').click(function() {
|
||||||
var index = $(this).attr('id').split('tl')[1].split('-')[0];
|
let index = $(this).attr('id').split('tl')[1].split('-')[0];
|
||||||
toastr.info("Capping simulcast temporal layer (mid=" + mids[index] + "), wait for it... (highest FPS)", null, {timeOut: 2000});
|
toastr.info("Capping " + what + " temporal layer (mid=" + mids[index] + "), wait for it... (highest FPS)", null, {timeOut: 2000});
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
||||||
if(!$('#tl' + index + '-1').hasClass('btn-success'))
|
if(!$('#tl' + index + '-1').hasClass('btn-success'))
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
if(!$('#tl' + index + '-0').hasClass('btn-success'))
|
if(!$('#tl' + index + '-0').hasClass('btn-success'))
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
||||||
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 2 }});
|
if(simulcastStarted[index])
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 2 }});
|
||||||
|
else
|
||||||
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal_layer: 2 }});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSimulcastButtons(feed, substream, temporal) {
|
function updateSimulcastSvcButtons(feed, substream, temporal) {
|
||||||
// Check the substream
|
// Check the substream
|
||||||
var index = feed;
|
let index = feed;
|
||||||
|
let simulcast = simulcastStarted[index];
|
||||||
|
let what = (simulcast ? 'simulcast' : 'SVC');
|
||||||
|
let layer = (simulcast ? 'substream' : 'layer');
|
||||||
if(substream === 0) {
|
if(substream === 0) {
|
||||||
toastr.success("Switched simulcast substream! (lower quality)", null, {timeOut: 2000});
|
toastr.success("Switched " + what + " " + layer + "! (lower quality)", null, {timeOut: 2000});
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
||||||
} else if(substream === 1) {
|
} else if(substream === 1) {
|
||||||
toastr.success("Switched simulcast substream! (normal quality)", null, {timeOut: 2000});
|
toastr.success("Switched " + what + " " + layer + "! (normal quality)", null, {timeOut: 2000});
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
} else if(substream === 2) {
|
} else if(substream === 2) {
|
||||||
toastr.success("Switched simulcast substream! (higher quality)", null, {timeOut: 2000});
|
toastr.success("Switched " + what + " " + layer + "! (higher quality)", null, {timeOut: 2000});
|
||||||
$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
||||||
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
}
|
}
|
||||||
// Check the temporal layer
|
// Check the temporal layer
|
||||||
if(temporal === 0) {
|
if(temporal === 0) {
|
||||||
toastr.success("Capped simulcast temporal layer! (lowest FPS)", null, {timeOut: 2000});
|
toastr.success("Capped " + what + " temporal layer! (lowest FPS)", null, {timeOut: 2000});
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
||||||
} else if(temporal === 1) {
|
} else if(temporal === 1) {
|
||||||
toastr.success("Capped simulcast temporal layer! (medium FPS)", null, {timeOut: 2000});
|
toastr.success("Capped " + what + " temporal layer! (medium FPS)", null, {timeOut: 2000});
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
} else if(temporal === 2) {
|
} else if(temporal === 2) {
|
||||||
toastr.success("Capped simulcast temporal layer! (highest FPS)", null, {timeOut: 2000});
|
toastr.success("Capped " + what + " temporal layer! (highest FPS)", null, {timeOut: 2000});
|
||||||
$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
||||||
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
# This file based on example code from Janus which is
|
# This file based on example code from Janus which is
|
||||||
# licensed as follows.
|
# licensed as follows.
|
||||||
#
|
#
|
||||||
# 2014-2022 Meetecho
|
# 2014-2025 Meetecho
|
||||||
#
|
#
|
||||||
# GPL-3 with OpenSSL exception
|
# GPL-3 with OpenSSL exception
|
||||||
# If you modify this Program, or any covered work,
|
# If you modify this Program, or any covered work,
|
||||||
@ -21,7 +21,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<!DOCTYPE HTML>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -34,13 +34,13 @@
|
|||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/javascript/jquery/jquery.min.js" ></script>
|
src="/javascript/jquery/jquery.min.js" ></script>
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/javascript/jquery-blockui/jquery.blockUI.js" ></script>
|
src="/javascript/popperjs2/popper.min.js"></script>
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/javascript/bootstrap/js/bootstrap.min.js" ></script>
|
src="/javascript/jquery-blockui/jquery.blockUI.min.js" ></script>
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="/javascript/bootstrap5/js/bootstrap.min.js" ></script>
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/javascript/bootbox/bootbox.min.js" ></script>
|
src="/javascript/bootbox/bootbox.min.js" ></script>
|
||||||
<script type="text/javascript"
|
|
||||||
src="/javascript/spin.js/spin.min.js" ></script>
|
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/javascript/toastr/toastr.min.js" ></script>
|
src="/javascript/toastr/toastr.min.js" ></script>
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
@ -51,22 +51,26 @@
|
|||||||
src="{% static 'janus/janus-video-room.js' %}" ></script>
|
src="{% static 'janus/janus-video-room.js' %}" ></script>
|
||||||
|
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="/javascript/bootswatch/cerulean/bootstrap.min.css"
|
href="/javascript/bootstrap5/css/bootstrap.css"
|
||||||
type="text/css"/>
|
type="text/css"/>
|
||||||
<link rel="stylesheet" href="/javascript/toastr/toastr.min.css"
|
<link rel="stylesheet" href="/javascript/toastr/toastr.min.css"
|
||||||
type="text/css"/>
|
type="text/css"/>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{% static 'janus/janus-video-room.css' %}" type="text/css"/>
|
href="{% static 'janus/janus-video-room.css' %}" type="text/css"/>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="/javascript/font-awesome/css/font-awesome.min.css" type="text/css"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-user-turn-config="{{ user_turn_config }}">
|
<body data-user-turn-config="{{ user_turn_config }}">
|
||||||
<div class="container" id="content" role="main">
|
<div class="container" id="content" role="main">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1>Janus Video Room
|
<div class="pb-2 mt-4 mb-2 border-bottom">
|
||||||
<button class="btn btn-default" autocomplete="off"
|
<h1>Janus Video Room
|
||||||
id="start">Start</button>
|
<button class="btn btn-default" autocomplete="off"
|
||||||
</h1>
|
id="start">Start</button>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" id="details">
|
<div class="container" id="details">
|
||||||
@ -86,12 +90,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container hide" id="videojoin">
|
<div class="container mt-4 hide" id="videojoin">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="label label-info" id="you"></span>
|
<span class="badge bg-info" id="you"></span>
|
||||||
<div class="col-md-12" id="controls">
|
<div class="col-md-12" id="controls">
|
||||||
<div class="input-group margin-bottom-md hide" id="registernow">
|
<div class="input-group mt-3 mb-1 hide" id="registernow">
|
||||||
<span class="input-group-addon">@</span>
|
<span class="input-group-text">👤</span>
|
||||||
<input autocomplete="off" class="form-control"
|
<input autocomplete="off" class="form-control"
|
||||||
autocomplete="off" type="text"
|
autocomplete="off" type="text"
|
||||||
placeholder="Choose a display name"
|
placeholder="Choose a display name"
|
||||||
@ -106,93 +110,88 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container hide" id="videos">
|
<div class="container mt-4 hide" id="videos">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
<h3 class="panel-title">
|
<span class="card-title">
|
||||||
Local Video <span class="label label-primary hide"
|
Local Video <span class="badge bg-primary hide"
|
||||||
id="publisher"></span>
|
id="publisher"></span>
|
||||||
<div class="btn-group btn-group-xs pull-right hide">
|
<div class="btn-group btn-group-sm top-right hide">
|
||||||
<div class="btn-group btn-group-xs">
|
<div class="btn-group btn-group-sm">
|
||||||
<button id="bitrateset" autocomplete="off"
|
<button id="bitrateset" autocomplete="off"
|
||||||
class="btn btn-primary
|
class="btn btn-primary
|
||||||
dropdown-toggle"
|
dropdown-toggle"
|
||||||
data-toggle="dropdown">
|
data-bs-toggle="dropdown">
|
||||||
Bandwidth<span class="caret"></span>
|
Bandwidth
|
||||||
</button>
|
</button>
|
||||||
<ul id="bitrate" class="dropdown-menu" role="menu">
|
<ul id="bitrate" class="dropdown-menu" role="menu">
|
||||||
<li><a href="#" id="0">No limit</a></li>
|
<a class="dropdown-item" href="#" id="0">No limit</a>
|
||||||
<li><a href="#" id="128">Cap to 128kbit</a></li>
|
<a class="dropdown-item" href="#" id="128">Cap to 128kbit</a>
|
||||||
<li><a href="#" id="256">Cap to 256kbit</a></li>
|
<a class="dropdown-item" href="#" id="256">Cap to 256kbit</a>
|
||||||
<li><a href="#" id="512">Cap to 512kbit</a></li>
|
<a class="dropdown-item" href="#" id="512">Cap to 512kbit</a>
|
||||||
<li><a href="#" id="1024">Cap to 1mbit</a></li>
|
<a class="dropdown-item" href="#" id="1024">Cap to 1mbit</a>
|
||||||
<li><a href="#" id="1500">Cap to 1.5mbit</a></li>
|
<a class="dropdown-item" href="#" id="1500">Cap to 1.5mbit</a>
|
||||||
<li><a href="#" id="2000">Cap to 2mbit</a></li>
|
<a class="dropdown-item" href="#" id="2000">Cap to 2mbit</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" id="videolocal"></div>
|
<div class="card-body" id="videolocal"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
<h3 class="panel-title">
|
<span class="card-title">
|
||||||
Remote Video #1 <span class="label label-info hide"
|
Remote Video #1 <span class="badge bg-info hide"
|
||||||
id="remote1"></span>
|
id="remote1"></span></span>
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body relative" id="videoremote1"></div>
|
<div class="card-body relative" id="videoremote1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
<h3 class="panel-title">
|
<span class="card-title">
|
||||||
Remote Video #2 <span class="label label-info hide"
|
Remote Video #2 <span class="badge bg-info hide"
|
||||||
id="remote2"></span>
|
id="remote2"></span></span>
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body relative" id="videoremote2"></div>
|
<div class="card-body relative" id="videoremote2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
<h3 class="panel-title">
|
<span class="card-title">
|
||||||
Remote Video #3 <span class="label label-info hide"
|
Remote Video #3 <span class="badge bg-info hide"
|
||||||
id="remote3"></span>
|
id="remote3"></span></span>
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body relative" id="videoremote3"></div>
|
<div class="card-body relative" id="videoremote3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
<h3 class="panel-title">
|
<span class="card-title">
|
||||||
Remote Video #4 <span class="label label-info hide"
|
Remote Video #4 <span class="badge bg-info hide"
|
||||||
id="remote4"></span>
|
id="remote4"></span></span>
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body relative" id="videoremote4"></div>
|
<div class="card-body relative" id="videoremote4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
<h3 class="panel-title">
|
<span class="card-title">
|
||||||
Remote Video #5 <span class="label label-info hide"
|
Remote Video #5 <span class="badge bg-info hide"
|
||||||
id="remote5"></span>
|
id="remote5"></span></span>
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body relative" id="videoremote5"></div>
|
<div class="card-body relative" id="videoremote5"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,14 +3,26 @@
|
|||||||
Views for the Janus app.
|
Views for the Janus app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
|
from plinth.middleware import CONTENT_SECURITY_POLICY
|
||||||
|
|
||||||
|
|
||||||
class JanusRoomView(TemplateView):
|
class JanusRoomView(TemplateView):
|
||||||
"""A simple page to host Janus video room."""
|
"""A simple page to host Janus video room."""
|
||||||
template_name = 'janus_video_room.html'
|
template_name = 'janus_video_room.html'
|
||||||
|
headers: dict[str, str] = {}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize the view and set CSP."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
csp = copy.copy(CONTENT_SECURITY_POLICY)
|
||||||
|
csp['script-src'] = "'self' 'unsafe-inline'"
|
||||||
|
csp['style-src'] = "'self' 'unsafe-inline'"
|
||||||
|
self.headers['Content-Security-Policy'] = csp.get_header_value()
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
"""Add user's TURN server information to view context."""
|
"""Add user's TURN server information to view context."""
|
||||||
@ -19,3 +31,8 @@ class JanusRoomView(TemplateView):
|
|||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
context['user_turn_config'] = config.to_json()
|
context['user_turn_config'] = config.to_json()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Handle GET request and return a response object."""
|
||||||
|
context = self.get_context_data(**kwargs)
|
||||||
|
return self.render_to_response(context, headers=self.headers)
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class UpgradesApp(app_module.App):
|
|||||||
|
|
||||||
app_id = 'upgrades'
|
app_id = 'upgrades'
|
||||||
|
|
||||||
_version = 20
|
_version = 21
|
||||||
|
|
||||||
can_be_disabled = False
|
can_be_disabled = False
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,17 @@ Explanation: matrix-synapse recommends python3-pympler
|
|||||||
Package: python3-pympler
|
Package: python3-pympler
|
||||||
Pin: release n=sid
|
Pin: release n=sid
|
||||||
Pin-Priority: 200
|
Pin-Priority: 200
|
||||||
|
|
||||||
|
Explanation: Make janus package and its dependencies installable from Debian
|
||||||
|
Explanation: 'unstable' distribution.
|
||||||
|
Package: janus
|
||||||
|
Pin: release n=sid
|
||||||
|
Pin-Priority: 200
|
||||||
|
|
||||||
|
Explanation: Janus app in FreedomBox needs the package for web UI.
|
||||||
|
Package: libjs-janus-gateway
|
||||||
|
Pin: release n=sid
|
||||||
|
Pin-Priority: 200
|
||||||
'''
|
'''
|
||||||
|
|
||||||
APT_PREFERENCES_UNSTABLE = \
|
APT_PREFERENCES_UNSTABLE = \
|
||||||
|
|||||||
@ -308,9 +308,9 @@ class Packages(app_module.FollowerComponent):
|
|||||||
"""
|
"""
|
||||||
packages_set: set[str] = set(packages)
|
packages_set: set[str] = set(packages)
|
||||||
|
|
||||||
# Get list of packages needed by other installed apps (packages to
|
# Get list of packages needed by other installed apps and by freedombox
|
||||||
# keep).
|
# itself (packages to keep).
|
||||||
keep_packages: set[str] = set()
|
keep_packages: set[str] = {'freedombox'}
|
||||||
for app in app_module.App.list():
|
for app in app_module.App.list():
|
||||||
# uninstall() will be called on Packages of this app separately
|
# uninstall() will be called on Packages of this app separately
|
||||||
# for uninstalling this app.
|
# for uninstalling this app.
|
||||||
|
|||||||
@ -149,6 +149,7 @@ LOGIN_REDIRECT_URL = 'index'
|
|||||||
MESSAGE_TAGS: dict = {}
|
MESSAGE_TAGS: dict = {}
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
|
'plinth.middleware.CommonHeadersMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
|
|||||||
@ -178,11 +178,11 @@ def test_packages_uninstall(uninstall, _refresh_package_lists):
|
|||||||
"""Test app"""
|
"""Test app"""
|
||||||
app_id = 'test-app'
|
app_id = 'test-app'
|
||||||
|
|
||||||
component = Packages('test-component', ['python3', 'bash'])
|
component = Packages('test-component', ['bash', 'dash'])
|
||||||
app = TestApp()
|
app = TestApp()
|
||||||
app.add(component)
|
app.add(component)
|
||||||
app.uninstall()
|
app.uninstall()
|
||||||
uninstall.assert_has_calls([call(['python3', 'bash'], purge=True)])
|
uninstall.assert_has_calls([call(['bash', 'dash'], purge=True)])
|
||||||
|
|
||||||
|
|
||||||
@patch('plinth.package.refresh_package_lists')
|
@patch('plinth.package.refresh_package_lists')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user