├── .dockerignore ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE.txt ├── README-de.md ├── README.md ├── SECURITY.md ├── docker-compose.development.yml ├── docker-compose.yml ├── docker-package.json ├── docker-zerva.env ├── docs ├── README.md ├── assets │ ├── connection.drawio │ ├── connection.png │ └── connection.svg ├── blog │ ├── version2-de.md │ ├── version2-en.md │ ├── version3-de.md │ └── version3-en.md ├── configuration.md ├── development.md ├── examples │ ├── docker-compose.briefing-proxy.yml │ └── docker-compose.nginx-proxy.yml └── installation │ ├── README.md │ ├── coturn.md │ ├── docker.md │ ├── fly.io.md │ ├── render.com.md │ └── website.md ├── icon.png ├── index.html ├── locales ├── README.md ├── de.json ├── en.json ├── es.json ├── fr.json ├── id.json ├── it.json ├── kr.json ├── pl.json ├── pt.json ├── ro.json ├── ru.json ├── tr.json └── zh.json ├── package.json ├── public ├── .well-known │ └── apple-app-site-association ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── briefing-config.js ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── manifest.json ├── mstile-150x150.png ├── og-image.png ├── pristine.mp3 ├── robots.txt ├── sample.jpg ├── sample.png └── service-worker.js ├── scripts ├── app.js └── make-docker.sh ├── src ├── app.vue ├── bugs │ ├── README-BUGTRACKER.md │ ├── index.ts │ └── lazy-sentry.ts ├── components │ ├── app-chat.scss │ ├── app-chat.vue │ ├── app-main.vue │ ├── app-settings.vue │ ├── app-share.scss │ ├── app-share.vue │ ├── app-video.vue │ ├── app-whitelabel.scss │ └── app-whitelabel.vue ├── config.spec.ts ├── config.ts ├── css │ ├── _html.scss │ ├── _macros.scss │ ├── _reset.scss │ ├── _units.scss │ ├── _variables.scss │ ├── app.scss │ ├── briefing.scss │ ├── forms.scss │ ├── index.scss │ ├── layout-default.scss │ ├── layout-maximized.scss │ ├── sea-button.scss │ ├── stack.scss │ └── text.scss ├── i18n.ts ├── lib │ ├── base.ts │ ├── base64.ts │ ├── history.ts │ ├── iframe.ts │ ├── link-external.ts │ ├── local.spec.ts │ ├── local.ts │ ├── messages.ts │ ├── names-const.ts │ ├── names.spec.ts │ ├── names.ts │ ├── qrcode.ts │ └── share.ts ├── logic │ ├── connection.ts │ ├── fingerprint.spec.ts │ ├── fingerprint.ts │ ├── in-browser-test.ts │ ├── sdp-manipulation.ts │ ├── simple-peer.ts │ ├── stream.ts │ ├── webrtc-peer.ts │ └── webrtc.ts ├── main.ts ├── product │ ├── app-embed.scss │ ├── app-embed.vue │ ├── app-help.scss │ ├── app-help.vue │ ├── app-welcome.scss │ ├── app-welcome.vue │ └── external-links.ts ├── shim-window.d.ts ├── shim.d.ts ├── state.ts ├── ui │ ├── helpers.ts │ ├── sea-modal.scss │ ├── sea-modal.vue │ ├── sea-symbol.vue │ └── trapFocus.ts └── zerva │ ├── config.ts │ ├── index.ts │ ├── room.ts │ └── stun.ts ├── tsconfig.json ├── vite.config.ts ├── vitest-setup.ts └── vitest.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .out* 3 | .pnpm* 4 | *.local 5 | *.log 6 | data 7 | dist 8 | docker 9 | docker* 10 | docs 11 | node_modules 12 | pnpm* 13 | tmp 14 | www 15 | scripts 16 | src 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Make a copy of this file named .env.local and add your values 2 | # All settings are optional. 3 | 4 | # URL used in the share dialog. Default is the current location/ 5 | # BRIEFING_ROOM_URL = https://example.com/room/ 6 | 7 | # The path to prefix the room names, default is `/` 8 | # BRIEFING_ROOM_PATH = /room/ 9 | 10 | # If you use the public signaling server, you should choose a unique domain to not get in conflict with brie.fi/ng's rooms 11 | # BRIEFING_ROOM_DOMAIN = default 12 | 13 | # Signal server, default is the same as the web site URL 14 | # BRIEFING_SIGNAL_URL = wss://example.com 15 | 16 | # TURN server to be used. Please set up your own, if you expect heavy traffic. See /docs/coturn.md 17 | # BRIEFING_STUN_URL=stun:turn01.brie.fi:5349 18 | 19 | # STUN server to be used. Please set up your own, if you expect heavy traffic. See /docs/coturn.md 20 | # BRIEFING_TURN_URL=turn:turn01.brie.fi:5349 21 | # BRIEFING_TURN_USER=brie 22 | # BRIEFING_TURN_PASSWORD=fi 23 | 24 | # Lazy error message handling requires a sentry.io DSN 25 | # BRIEFING_SENTRY_DSN= 26 | 27 | # IFrame embed defaults 28 | # BRIEFING_SHOW_FULLSCREEN = 1 29 | # BRIEFING_SHOW_INVITATION = 1 30 | # BRIEFING_SHOW_INVITATION_HINT = 1 31 | # BRIEFING_SHOW_SETTINGS = 1 32 | # BRIEFING_SHOW_SHARE = 1 33 | # BRIEFING_SHOW_CHAT = 1 34 | 35 | # When entering a room... 36 | 37 | # ... set video muted 38 | # BRIEFING_MUTE_VIDEO = 0 39 | 40 | # ... set audio muted 41 | # BRIEFING_MUTE_AUDIO = 1 42 | 43 | # For development it is nice to always have the same room preset, otherwise it is randomly generated 44 | # BRIEFING_DEFAULT_ROOM=development -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | _archive 2 | _* 3 | .DS_* 4 | .env.* 5 | .htaccess 6 | .htaccess* 7 | .idea 8 | .out.* 9 | .rsync-ignore 10 | *.local 11 | *.log 12 | *.spec.* 13 | bg.jpg 14 | build 15 | data 16 | dist 17 | dist_electron 18 | docker 19 | internal 20 | legacy 21 | logs 22 | node_modules 23 | npm-debug.log 24 | package-lock.json 25 | pnpm-lock.yaml 26 | tmp 27 | video 28 | www 29 | yarn-error.log 30 | vite.config.ts 31 | vitest* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu", 3 | "rules": { 4 | "unused-imports/no-unused-vars": 0, 5 | "@typescript-eslint/no-unsafe-argument": 0, 6 | "@typescript-eslint/no-unsafe-assignment": 0, 7 | "@typescript-eslint/no-unsafe-call": 0, 8 | "@typescript-eslint/no-unsafe-member-access": 0, 9 | "@typescript-eslint/no-unsafe-return": 0, 10 | "@typescript-eslint/require-await": 0, 11 | "@typescript-eslint/restrict-template-expressions": 0, 12 | "@typescript-eslint/no-misused-promises": 0 13 | } 14 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [holtwick] 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | # on: 12 | # push: 13 | # branches: 14 | # - main 15 | # - master 16 | # release: 17 | # types: [created] 18 | # # workflow_dispatch: 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | permissions: 25 | contents: write 26 | packages: write 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: 18.x 35 | registry-url: 'https://registry.npmjs.org' 36 | - run: npm version 37 | - run: npx changelogithub 38 | env: 39 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_* 2 | .env.* 3 | .htaccess 4 | .htaccess* 5 | .idea 6 | .out.* 7 | .rsync-ignore 8 | *.local 9 | *.log 10 | dist 11 | dist_electron 12 | docker 13 | internal 14 | legacy 15 | logs 16 | node_modules 17 | npm-debug.log 18 | package-lock.json 19 | pnpm-lock.yaml 20 | tmp 21 | video 22 | www 23 | yarn-error.log 24 | production 25 | data 26 | _archive 27 | upload-free.sh 28 | scripts/add-plausible.tsx 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "i18n-ally.localesPaths": [ 8 | "locales", 9 | "legacy/app/src/locales", 10 | "legacy/electron/dist_electron/win-unpacked/locales", 11 | "legacy/internal/briefing-internal/src/locales" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Please read https://github.com/holtwick/briefing/blob/master/docs/installation/docker.md for proper use of Docker 2 | 3 | FROM node:20-alpine 4 | COPY . /app 5 | # COPY docker-package.json /app/package.json 6 | WORKDIR /app 7 | # RUN cd /app && npm install --omit=dev 8 | CMD ["node", "dist/main.cjs"] 9 | EXPOSE 8080 10 | 11 | # STUN 12 | EXPOSE 3478 13 | -------------------------------------------------------------------------------- /README-de.md: -------------------------------------------------------------------------------- 1 | # Brie.fi/ng 2 | 3 | **Sicherer direkter Video-Gruppenchat** 4 | 5 | > Wähle deine Sprache: [en](README.md) | [de](README-de.md) 6 | 7 | Der Datenschutz ist die zentrale Idee hinter diesem Projekt. Es werden ausschließlich offene Technologien (wie [WebRTC](https://webrtc-security.github.io/)) verwendet, die in allen modernen Browsern funktionieren. Somit ist keine Installation von zusätzlicher Software notwendig und der Ansatz bleibt zukunftssicher. 8 | 9 | Über [Brie.fi/ng](https://brie.fi/ng) kann sofort gratis eine Unterhaltung begonnen werden. Eine [kostenlose iOS-App](https://apps.apple.com/app/briefing-video-chat/id1510803601) steht ebenfalls zur Verfügung. 10 | 11 | Features wie Desktop-Sharing, Text Chat und einfache Einladungslinks sind vorhanden. Weitere können leicht mit Kenntnissen in Javascript und Vue hinzugefügt werden. 12 | 13 | ## Schnellstart für Entwickler 14 | 15 | Lade oder klone das Projekt auf deinen lokalen Rechner und schon geht es los: 16 | 17 | ```sh 18 | npm install 19 | npm run start 20 | ``` 21 | 22 | ## Alles enthalten 23 | 24 | Eine WebRTC-Videochat-Anwendung benötigt mehrere Komponenten, um richtig zu funktionieren. Briefing enthält alles, was Sie für den Start benötigen: Benutzeroberfläche, Signalisierung und STUN. 25 | 26 | Erfahre mehr darüber in der [**→ Dokumentation**](./docs/README.md). 27 | 28 | ![Verbindung](./docs/assets/connection.svg) 29 | 30 | ## Anpassung / Whitelabeling 31 | 32 | Um Briefing für Ihr eigenes Projekt zu nutzen, stehen Ihnen die folgenden Optionen für alle Erfahrungsstufen zur Verfügung: 33 | 34 | 1. **Erstelle einen Raum** über [Brie.fi/ng](https://brie.fi/ng) und kopiere den Link in deine Website. 35 | 2. Briefing per `iframe` **in deine eigene Seite einbetten**. Verwende dazu den [praktischen Konfigurator](https://brie.fi/ng/embed-demo). 36 | 3. Starte Briefing auf deinem **eigenen Server** z.B. [via Docker](docs/installation/docker.md) 37 | 4. **Source Code anpassen**, genau nach deinen Bedürfnissen. Details dazu in der [Dokumentation](docs/README.md). 38 | 39 | Erfahre mehr untert [**→ Installation**](./docs/installation/README.md). 40 | 41 | ## Lizenzen 42 | 43 | Generell ist Briefing frei, allerdings steht auch eine [kommerzielle Lizenz](#commercial-license) zur Verfügung, um deren Erwerb ich, insbesondere bei "White Labeling" Anwendungen, bitte. Ansonsten freue ich mich über die Unterstützung des Projektes durch [Sponsoring via GitHub](https://github.com/sponsors/holtwick), um die Weiterentwicklung zu fördern. Danke. 44 | 45 | ### Öffentliche Lizenz 46 | 47 | Es gelten die Bedingungen der AGPL 3.0 oder später (GNU Affero General Public License v3.0): "Die Erlaubnis dieser stärksten Copyleft-Lizenz ist an die Bedingung geknüpft, dass der vollständige Quellcode der lizenzierten Werke und Modifikationen, die größere Werke unter Verwendung eines lizenzierten Werks umfassen, unter derselben Lizenz zur Verfügung gestellt werden. Copyright- und Lizenzvermerke müssen erhalten bleiben. Die Mitwirkenden gewähren ausdrücklich die Patentrechte. Wenn eine modifizierte Version verwendet wird, um einen Dienst über ein Netzwerk anzubieten, muss der vollständige Quellcode der modifizierten Version zur Verfügung gestellt werden." . 48 | 49 | ### Kommerzielle Lizenz 50 | 51 | Für die kommerzielle Nutzung oder Closed-Source-Projekte / "White Labeling" biete ich eine Lizenz mit folgenden Bedingungen an: 52 | 53 | > Weltweite, nicht-exklusive, nicht-übertragbare und nicht unterlizenzierbare Lizenz von Briefing, wie es auf https:// github.com/holtwick/briefing/ zu finden ist, zur Verwendung in Produkten des Käufers, solange die resultierende Software nicht in Konkurrenz zu Briefing selbst steht. Jegliche Haftung ist ausgeschlossen. Es gilt ausschließlich das Recht der Bundesrepublik Deutschland. 54 | 55 | Die einmalige Gebühr beträgt 499 EUR netto. Bitte wenden dich an [license@holtwick.de](mailto:license@holtwick.de) oder [kaufe direkt via Paddle](https://buy.paddle.com/product/650756). 56 | 57 | ## Autor 58 | 59 | Mein Name ist Dirk Holtwick. Ich bin ein unabhängiger Softwareentwickler mit Sitz in Deutschland. Erfahren mehr über meine Arbeit unter [holtwick.de](https://holtwick.de/about). 60 | 61 | ## Beitragen 62 | 63 | Beiträge sind immer willkommen. Am besten fängst du an [Issues](https://github.com/holtwick/briefing/issues) hinzuzufügen oder darauf zu antworten. 64 | 65 | Um Übersetzungen hinzuzufügen oder zu korrigieren, starte hier: [→ locales](locales/). 66 | 67 | 68 | ## Hauptversionen 69 | 70 | ### Version 3.0 71 | 72 | Vollständige Migration auf Vue3 und Typescript. Lokalisierung mit vue-i18n Standardbibliothek. Modernisierung von vielen Teilen des Projekts. Siehe [→ Artikel](docs/blog/version3-de.md) ([en](docs/blog/version3-en.md)) 73 | 74 | ### Version 2.0 75 | 76 | Einführung von Typescript-Unterstützung. Umstellung auf Vite. Ersetzen von socket.io durch zuverlässiges [Zerva](https://github.com/holtwick/zerva). Docker-Images. Reduktion auf die wesentlichen Teile des Projekts. Siehe [→ Artikel](docs/blog/version2-de.md) ([en](docs/blog/version2-en.md)) 77 | 78 | ### Version 1.0 79 | 80 | Basierte auf Webpack und hatte Beispiele für iOS, Android, Windows und Electron sowie einen separaten Signal-Server. Sie bot auch Hintergrundunschärfe und Bildhintergründe über Unsplash. Alle diese Implementierungen sind mittlerweile veraltet. Der Code ist jedoch weiterhin über den [legacy branch](https://github.com/holtwick/briefing/tree/legacy) zugänglich, wird aber nicht mehr gepflegt oder unterstützt. 81 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brie.fi/ng 2 | 3 | **Secure direct video group chat** 4 | 5 | > Choose your language: [en](README.md) | [de](README-de.md) 6 | 7 | Privacy is the central idea behind this project. Only open technologies (such as [WebRTC](https://webrtc-security.github.io/)) are used, which work with all modern browsers. Thus, no installation of additional software is required, and the approach remains future-proof. 8 | 9 | A conversation can be started immediately for free via [Brie.fi/ng](https://brie.fi/ng). A [free iOS app](https://apps.apple.com/app/briefing-video-chat/id1510803601) is also available. 10 | 11 | Features such as desktop sharing, text chat and simple invitation links are available. More can be easily added with knowledge of Javascript and Vue. 12 | 13 | ## Quick start for developers 14 | 15 | Download or clone the project to your local machine, and you are ready to go: 16 | 17 | ```sh 18 | npm install 19 | npm run start 20 | ``` 21 | 22 | ## Batteries included 23 | 24 | A WebRTC video chat application requires multiple components to work properly. Briefing includes everything you need to get started: User Interface, Signaling and STUN. 25 | 26 | Learn more about it in the [**→ documentation**](./docs/README.md). 27 | 28 | ![connection](./docs/assets/connection.svg) 29 | 30 | ## Customization / Whitelabeling 31 | 32 | To use Briefing for your own project, the following options for all levels of experience are available: 33 | 34 | 1. **create a room** via [Brie.fi/ng](https://brie.fi/ng) and copy the link into your website. 35 | 2. **embed** Briefing via 'frame' into your own site. Use the [handy configurator](https://brie.fi/ng/embed-demo). 36 | 3. start Briefing on your **own server** e.g. [via Docker](docs/installation/docker.md) or use a service like [fly.io](docs/fly.io.md), [render.com](docs/render.com.md), Google Cloud, AWS, Azure ... you name it. 37 | 4. **customize** the source code according to your needs. See the [documentation](docs/README.md) for details. 38 | 39 | Learn more at [**→ installation**](./docs/installation/README.md). 40 | 41 | ## Licenses 42 | 43 | In general Briefing is free, however a **[commercial license](#commercial-license) is also available**, which I ask you to purchase, especially for "white labeling" applications. Otherwise, I appreciate support for the project through [sponsorship via GitHub](https://github.com/sponsors/holtwick) to support further development. Thanks. 44 | 45 | ### Public License 46 | 47 | The terms of the AGPL 3.0 or later (GNU Affero General Public License v3.0) apply: "Permissions of this strongest copyleft license are conditioned on making available complete source code of licensed works and modifications, which include larger works using a licensed work, under the same license. Copyright and license notices must be preserved. Contributors provide an express grant of patent rights. When a modified version is used to provide a service over a network, the complete source code of the modified version must be made available." . 48 | 49 | ### Commercial license 50 | 51 | For commercial use or closed source projects / "white labeling" I offer a license with the following conditions: 52 | 53 | > Worldwide, non-exclusive, non-transferable, non-sublicensable license of Briefing, as found at https:// github.com/holtwick/briefing/, for use in purchaser's products, as long as the resulting software is not in competition with Briefing itself. Any liability is excluded. The law of the Federal Republic of Germany applies exclusively. 54 | 55 | The one-time fee is 499 EUR net. Please contact [license@holtwick.de](mailto:license@holtwick.de) or [buy directly via Paddle](https://buy.paddle.com/product/650756). 56 | 57 | ## Author 58 | 59 | My name is Dirk Holtwick. I am an independent software developer based in Germany. Learn more about my work at [holtwick.de](https://holtwick.de/about). 60 | 61 | ## Contribute 62 | 63 | Contributions are always welcome. The best place to start is to add or respond to [Issues](https://github.com/holtwick/briefing/issues). 64 | 65 | To add or correct translations, start here: [→ locales](locales/). 66 | 67 | ## Major Releases 68 | 69 | ### Version 3.0 70 | 71 | Full migration to Vue3 and Typescript. Localization using vue-i18n standard library. Modernization of many parts of the project. See [→ blog post](docs/blog/version3-en.md) ([de](docs/blog/version3-de.md)) 72 | 73 | ### Version 2.0 74 | 75 | Introduced Typescript support. Migration to Vite. Replacing socket.io with reliable [Zerva](https://github.com/holtwick/zerva). Docker images. Reduction to the essential parts of the project. See [→ blog post](docs/blog/version2-en.md) ([de](docs/blog/version2-de.md)) 76 | 77 | ### Version 1.0 78 | 79 | Based on Webpack and had examples for iOS, Android, Windows, and Electron, as well as a separate signal server. It also offered background blur and image backgrounds via Unsplash. All of these implementations were deprecated. However, the code is still accessible via the [legacy branch](https://github.com/holtwick/briefing/tree/legacy), but is no longer maintained or supported. 80 | 81 | --- 82 | 83 | This document is also available in [German language](README-de.md). 84 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## General Info 4 | 5 | See [wiki](https://github.com/holtwick/briefing/wiki/) in general and [Security Discussion](https://github.com/holtwick/briefing/wiki/Security-Discussion) in particular 6 | for details about security considerations. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Create an [issue](https://github.com/holtwick/briefing/issues) or get in [contact with me directly](https://holtwick.de/imprint). 11 | -------------------------------------------------------------------------------- /docker-compose.development.yml: -------------------------------------------------------------------------------- 1 | # Please read https://github.com/holtwick/briefing/blob/master/docs/docker.md for proper use of Docker 2 | 3 | version: '3' 4 | 5 | services: 6 | briefing: 7 | build: . 8 | restart: always 9 | volumes: 10 | - ./dist:/app/dist 11 | - ./www:/app/www 12 | - ./data:/app/data 13 | env_file: 14 | - docker-zerva.env 15 | 16 | networks: 17 | default: 18 | external: 19 | name: proxy 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Please read https://github.com/holtwick/briefing/blob/master/docs/docker.md for proper use of Docker 2 | 3 | version: '3' 4 | 5 | services: 6 | briefing: 7 | image: holtwick/briefing 8 | restart: always 9 | volumes: 10 | - ./data:/app/data 11 | ports: 12 | - 8080:8080 13 | 14 | # networks: 15 | # default: 16 | # external: 17 | # name: proxy 18 | -------------------------------------------------------------------------------- /docker-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "briefing-docker", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": { 6 | "name": "Dirk Holtwick", 7 | "email": "dirk.holtwick@gmail.com", 8 | "url": "https://holtwick.de" 9 | }, 10 | "type": "module", 11 | "scripts": { 12 | "start": "node dist/main.cjs" 13 | }, 14 | "dependencies": {}, 15 | "engines": { 16 | "node": ">=18.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docker-zerva.env: -------------------------------------------------------------------------------- 1 | # Please read https://github.com/holtwick/briefing/blob/master/docs/installation/docker.md for proper use of Docker 2 | 3 | NODE_MODE=production 4 | LOG=/app/data/zerva.log 5 | ZEED=* 6 | ZEED_LEVEL=i 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Brie.fi/ng Docs 2 | 3 | ## Installation 4 | 5 | You can clone Briefing locally and run it with: 6 | 7 | ``` 8 | npm install 9 | npm start 10 | ``` 11 | 12 | To adopt the look and feel to your needs, follow this guide: 13 | 14 | - [**Development, Installation, Customization and White-Label**](installation/README.md) 15 | 16 | ## Posts 17 | 18 | - [Briefing 3.0](blog/version3-en.md) ([de](blog/version3-de.md)) 19 | - [Briefing 2.0](blog/version2-en.md) ([de](blog/version2-de.md)) 20 | 21 | ## Other Resources for Maintainers 22 | 23 | - [Marketing](internal/marketing-en.md) ([de](internal/marketing-de.md)) - Describes Briefing in a few words. 24 | - [Internal](internal/internal.md) - Details for repository maintainers. 25 | -------------------------------------------------------------------------------- /docs/assets/connection.drawio: -------------------------------------------------------------------------------- 1 | 7VrZcuI6EP2aVN17qzJlW5jlkSUEp0iGAAkJb7IlbDGy5bLFYr5+JC/sZMhAYm4qD0lZLakld/c5ajW+AnV3fhtA37lnCNMrTUHzK9C40rRSqST+S0GUCAqakgjsgKBEpK4EPbLAqTAbNiEIhxsDOWOUE39TaDHPwxbfkMEgYLPNYSNGN1f1oY13BD0L0l3pgCDuJNKyVlrJW5jYTrayWqwkPS7MBqdvEjoQsdmaCNxcgXrAGE+e3HkdU2m7zC7JvOaB3uXGAuzxYyZMh+PI7Pde6s0baj3X29jX4HU53RuPshfGSLx/2mQBd5jNPEhvVtJawCYewlKrIlqrMW3GfCFUhXCMOY9SZ8IJZ0LkcJemvWLDQfQi5//Qs+Zrqi5uNOYbrShthb8wt5xUSbJxuduD9khFIZsEFn7DCFlcwcDG/I1x+tJrItoxc7HYnJgXYAo5mW7uA6ZxZy/HrVwjHlLvvMNTqp4onkI6SZe60opU7LeGyFQ82jy2SiIyM0GPY1/MUrMOsba5MzjYlohRe5SuiZ5CLCYphsdxMILCugcm7gmuNjQFRWwEBKTE9sSzhaU+IZjigBMBwmra4RKEktjDIVlAM9YnQ8JnxOOxqfXald5YrikV4Pk+hkgnr3C5HjGHMbLr9lS78qNUAikbZByXtI6Oi1R3R77LSvF1YUPpNdjSwEajUITrdlwtd3hCqGl5sELe6Aa5ortwEN3bUNbeB+U1ODaJh4QCxp0YvCfAfyLgH35lzCcQeAv0agGcBvMM11rx03BdzAPXOUC5kCuUS6cc1OAtdP8RlynC4dETZlAahjvCy7ZzAiFQJpAr82sS4BmURvrC3FD8EzcoemXz7FbPkhGoWwkB+DTiyOWaEAqc86q8w8lQoDAMiZWJm4SuhgXs1/Jypl9AJnHg7Pgc+snC+O/op3AS/TQE+i2+whZh3tFzTcxnGHtnyUuUf3wsHv49et6UCMpUBtjs9uvHL/8lyOzt28218kPb5DLtPJcbbUNpdmZ+ApVV8qAyBEMnnr9z0TmS5HJgsQOn3Cex2PH3ocJ/f30h6gRsLt8JQQ5PIx4yEvM9JnVlHHiCtnX6VMjBi9Y+VoeuiMiaZ4Z+TCAinCQlfGUCq/whG9PVUvHjGew6K1h/AoW964B/f/TlM7WD46JE9chYnTmE454PY6qbBdDfitnQT34NGJG5JN5N0h0JYq0zyoJYF0AQl0fWMpVc6ylaZWyO3orbHQY+GIkFXdk887IQn639rpCKnLWfFMrK4dA8iWNzOQkP1f6XfZ9c+9eOPA1zrf0v3+bbVZdeyM1Ol29XXXyhTgP/J1dd0hXiWPfme4X4ymnSsTWJc6ZJWEU6Lu1LkyrFEoDFj0mTgJ5zmgR24qgnrj3iUuTZu/xBKfFDyRNnzU91XEaFfYYvayYonsnwy09usmtSKWfD75YAev2nhy9lc3BpNt/9yGSAzVCY9UuZXb00sxePOav6T92HYys5H+Kc0QgXrb33ZFSqmIpyHufo5QtzDthXcNkyeOhAXz4SN/6ecFkii8tpHSYQJIt5oGEyzpkrBlDZUYPWLzvOKDNbIjyCk9jD20U2LvPLXX/FC1YzqZJJpCpZ1ATVpKk1w6k4rmpz4Xmt3mk9aMOoNjZvmwtroZCh++ygW4dapOYOB00F3j5H7ZfnxfDFIEbrQbHcSjDsqSEc6BS6Fd8cK0TIVNN93Ol/1ZqzTu8uMsHzZFg3bAt0I1PjtP2C/GGryzo9IzJIbYFad1OoPVUM90G1vDtqet2Gqc2nFjHsoUtDs6EQ061Mhj3DRi6lSLmbYiG7r1dnRsNQ4j9SY8MB9WDrsWKM4zaAg64C5bix3JvDzVt9IfbDUKs7+0nKUwQQaHvWou1WomFUju4b1VkbSHsYwj6dVk1YwraHQkO/b8iV5ogo0Wv/0X6IV67a93VFafe7xs/bG7GLRzvZUZWLfgWNDdJeFMqvGo1etTk1WobYmdjJ7YMu3hyIHY7jt+gn4+KVFwUZRKnnVjEhhB7z8LKDBUiWXzPhEqlnwl2htFWfAru4K+zBXeHDcLcvg//G3Tfuzo27DwAPKH4YeERz9dF48pvC6st7cPMb -------------------------------------------------------------------------------- /docs/assets/connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/docs/assets/connection.png -------------------------------------------------------------------------------- /docs/blog/version2-de.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-09-30 3 | title: Briefing 2.0 4 | --- 5 | 6 | # Briefing 2.0 7 | 8 | Briefing entstand vor zwei Jahren zu Beginn der Corona-Pandemie. Danke seines einfachen, offener Aufbaus wurde es schnell beliebt und hat nun auch auf Github fast die tausender Marke an Sternen erreicht. In dieser Zeit hat sich technisch viel getan, daher habe ich den Code modernisiert und an die aktuellen technischen Standards angepasst. 9 | 10 | ## Vite statt Webpack 11 | 12 | Zunächst wurde [Webpack](https://webpack.js.org/) durch [Vite](https://vitejs.dev/) ersetzt. Vite ist das Build-Tool der Wahl für heutige Web-Projekte. Es ist rasend schnell und erlaubt eine ganz neue Art des Entwickelns, bei der das Ergebnis im Moment des Speicherns des Codes schon im Browser sichtbar ist. Es unterstützt auch [Typescript](https://www.typescriptlang.org/) out-of-the-box und somit kann Briefing nun auch unkompliziert zu dieser Sprache migriert werden. 13 | 14 | ## Zerva als Server Unterbau 15 | 16 | Der Signaling-Server war bisher getrennt von dem Code der Clientseite von Briefing. In den letzten Jahren habe ich das Open Source Projekt [Zerva](https://github.com/holtwick/zerva) vorangetrieben, dass die Entwicklung auf der Serverseite stark vereinfacht. 17 | 18 | Erstens, weil es mit "Events" arbeitet, sodass jede Komponente an der Stelle einsetzen kann, an der sie benötigt wird. Zweitens weil das Zusammenspiel mit Vite und Vue optimiert wurde. Zerva integriert Vite in seinen Prozess und nur ein Aufruf startet sowohl den Server als auch die Website. Das Beste aus beiden Welten inklusive Hot-Reload auf beiden Seiten. 19 | 20 | So ist es logisch, dass der Signaling Server jetzt in Zerva realisiert wurde. Auch [socket.io](https://socket.io/) konnte so endlich auf Altenteil geschickt werden, weil Zerva seine eigene Websocket Implementierung mitbringt, die sich durch ein sehr robustes Polling auszeichnet, sodass Verbindungen kaum verloren gehen kann. 21 | 22 | ## Ausblick 23 | 24 | Technisch bleibt nun noch auf der Agenda die Modernisierung von Vue 2 zu 3 und eine umfangreichere Verwendung von Typescript, zur Qualitätssicherung des Codes. 25 | 26 | **Der Code steht als Open Source wie zuvor unter zur Verfügung und die App zur freien Benutzung unter .** 27 | -------------------------------------------------------------------------------- /docs/blog/version2-en.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-09-30 3 | title: Briefing 2.0 4 | --- 5 | 6 | # Briefing 2.0 7 | 8 | Briefing was created two years ago at the beginning of the Corona pandemic. Thanks to its simple, open structure, it quickly became popular and has now almost reached the thousand star mark on Github. A lot has happened technically in that time, so I've modernized the code and brought it up to current technical standards. 9 | 10 | ## Vite instead of Webpack 11 | 12 | First, [Webpack](https://webpack.js.org/) was replaced by [Vite](https://vitejs.dev/). Vite is the build tool of choice for today's web projects. It is blazingly fast and allows a whole new way of developing, where the result is already visible in the browser the moment the code is saved. It also supports [Typescript](https://www.typescriptlang.org/) out-of-the-box and thus Briefing can now be easily migrated to this language. 13 | 14 | ## Zerva as server substructure 15 | 16 | The signing server was previously separate from the client side code of Briefing. In recent years I have been pushing the open source project [Zerva](https://github.com/holtwick/zerva), which greatly simplifies development on the server side. 17 | 18 | First, because it works with "events" so that each component can deploy where it is needed. Second, because the interaction with Vite and Vue has been optimized. Zerva integrates Vite into its process and just one call launches both the server and the website. The best of both worlds including hot reload on both sides. 19 | 20 | So it is logical that the Signaling Server has now been implemented in Zerva. Also [socket.io](https://socket.io/) could finally be put to rest, because Zerva brings its own websocket implementation, which is characterized by a very robust polling, so that connections can hardly be lost. 21 | 22 | ## Outlook 23 | 24 | Technically, the modernization of Vue 2 to 3 and a more extensive use of Typescript, for the quality assurance of the code, remains on the agenda. 25 | 26 | **The code is available as open source as before at and the app for free use at .** 27 | -------------------------------------------------------------------------------- /docs/blog/version3-de.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-02-27 3 | title: Briefing 3.0 4 | lang: de 5 | --- 6 | 7 | # Briefing 3.0 8 | 9 | Mit diesem neuen Major-Release wird die Code Basis auf den aktuellen Stand der Technik gehoben und die Weichen für weitere Features gestellt. 10 | 11 | ## Vue3 und Typescript 12 | 13 | Eine Modernisierung des Codes bringt nun [Vue3](https://vuejs.org/) Unterstützung und die vollständige Migration auf [Typescript](https://www.typescriptlang.org/). Durch die Typsicherheit wird die Zuverlässigkeit des Sourcecodes verbessert. 14 | 15 | ## Lokalisierung 16 | 17 | Für die Übersetzungen wird nun die Standardbibliothek [vue-i18n](https://vue-i18n.intlify.dev/) verwendet, wodurch die Türen geöffnet werden für eine bessere Unterstützung von entsprechenden Hilfsmitteln. Bisher ist Briefing in 13 Sprachen verfügbar, bei Interesse gibt es hier eine [Anleitung zur Erweiterung](../locales/README.md). 18 | 19 | ## Sonstiges 20 | 21 | - Die Docker Images wurden auf node 18 aktualisiert. 22 | - Überarbeitung der Dokumentation 23 | - QRCode Fullscreen durch Klick 24 | 25 | ## Ausblick 26 | 27 | Die Basis von Briefing ist solide. In den nächsten Schritten soll es um den Ausbau häufig angefragter Funktionalitäten gehen. 28 | 29 | **Der Code steht als Open Source wie zuvor unter zur Verfügung und die App zur freien Benutzung unter .** 30 | -------------------------------------------------------------------------------- /docs/blog/version3-en.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-02-27 3 | title: Briefing 3.0 4 | lang: en 5 | --- 6 | 7 | # Briefing 3.0 8 | 9 | This new major release brings the code base up to the current state of the art and sets the course for further features. 10 | 11 | ## Vue3 and Typescript 12 | 13 | A modernization of the code now brings [Vue3](https://vuejs.org/) support and the complete migration to [Typescript](https://www.typescriptlang.org/). Typescript security improves the reliability of the source code. 14 | 15 | ## Localization 16 | 17 | The standard library [vue-i18n](https://vue-i18n.intlify.dev/) is now used for translations, opening the doors for better support of appropriate tools. So far Briefing is available in 13 languages, if you are interested, here is a [manual for extension](../locales/README.md). 18 | 19 | ## Other 20 | 21 | - Docker images have been updated to node 18. 22 | - Revision of the documentation 23 | - QRCode fullscreen by click 24 | 25 | ## Outlook 26 | 27 | The base of Briefing is solid. The next steps will be to expand frequently requested functionalities. 28 | 29 | **The code is available as open source as before at and the app for free use at .** 30 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # User Interface Configuration 2 | 3 | All settings are **optional**! By default standard values are used. 4 | 5 | Environment variables can be set on build or runtime, e.g. by using [.env](../.env) files. But then these need to be prefixed by `BRIEFING_`. So for example `ROOM_PATH` becomes `BRIEIFING_ROOM_PATH`. 6 | 7 | If used in a [static website](installation/website.md) these values go into the `briefing-config.js` file. 8 | 9 | ### Rooms 10 | 11 | - `ROOM_URL`: URL used in the share dialog. Default is the current location. Example: `https://example.com/room/` 12 | - `ROOM_PATH`: The path to prefix the room names, default is `/`. Example: `/room/` 13 | - `ROOM_DOMAIN`: If you use the public signaling server, you can choose a unique domain to not get in conflict with brie.fi/ng's rooms. By default the hostname is used here. 14 | 15 | ### Server Bindings 16 | 17 | By default Briefing comes with STUN build in. But if also TURN is required, the installation of [coturn.md](installation/coturn.md) is recommended. 18 | 19 | - `SIGNAL_URL`: Signal server, default is the same as the web site URL. Example: `wss://example.com` 20 | - `STUN_URL`: TURN server to be used. Please set up your own, if you expect heavy traffic. Example: `stun:turn01.brie.fi:5349` 21 | - `TURN_URL`: STUN server to be used. Please set up your own, if you expect heavy traffic. See [coturn.md](installation/coturn.md). Example: `turn:turn01.brie.fi:5349` 22 | - `TURN_USER`: Username 23 | - `TURN_PASSWORD`: Password 24 | 25 | ### UI Defaults 26 | 27 | Boolean values may be set as `0` or `1` 28 | 29 | - `MUTE_VIDEO`: Start with video off. 30 | - `MUTE_AUDIO`: Start with audio muted. 31 | - `SHOW_FULLSCREEN`: Show fullscreen button. 32 | - `SHOW_INVITATION`: Show invitation button. 33 | - `SHOW_INVITATION_HINT`: Show invitation hint if only one peer is in the room. 34 | - `SHOW_SETTINGS`: Show settings button. 35 | - `SHOW_SHARE`: Show share button. 36 | - `SHOW_CHAT`: Show chat button. 37 | 38 | ### Debugging 39 | 40 | - `DEFAULT_ROOM`: For development it is nice to always have a static room name preset, otherwise it is randomly generated. 41 | - `SENTRY_DSN`: Lazy error message handling using a service. Enter your projects DSN here. -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To make contributions to this project or customize it for your own needs, start by cloning / downloading the repository to your local machine. The development mode can be started like this: 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Access the local server via [http://localhost:8080](http://localhost:8080). 11 | 12 | ## Customization 13 | 14 | Most customization can be done by setting env variables. See [**→ configuration**](configuration.md) 15 | 16 | To get started with changing the code directly you might want to begin with the landing page at [→ src/components/app-whitelabel.vue](../src/components/app-whitelabel.vue). 17 | 18 | ## Debugging 19 | 20 | Enable logging by entering the following in the browsers console: 21 | 22 | ```js 23 | localStorage.zeed = '*' 24 | ``` 25 | 26 | In production it is possible to connect to via [**→ configuration**](configuration.md). 27 | 28 | ## Technology stack 29 | 30 | Just the most up to date frameworks: 31 | 32 | - [vite](https://vitejs.dev/) 33 | - [vue3](https://vuejs.org/) 34 | - [vue-i18n](https://vue-i18n.intlify.dev/) 35 | - [zerva](https://www.npmjs.com/package/@zerva/core) 36 | - [zeed](https://www.npmjs.com/package/zeed) 37 | - [Feather Icons](https://feathericons.com) 38 | -------------------------------------------------------------------------------- /docs/examples/docker-compose.briefing-proxy.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | briefing: 5 | image: holtwick/briefing 6 | restart: always 7 | volumes: 8 | - ./data:/app/data 9 | 10 | networks: 11 | default: 12 | external: 13 | name: proxy 14 | -------------------------------------------------------------------------------- /docs/examples/docker-compose.nginx-proxy.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | image: 'jc21/nginx-proxy-manager:latest' 6 | restart: unless-stopped 7 | ports: 8 | - '80:80' 9 | - '81:81' 10 | - '443:443' 11 | volumes: 12 | - ./data:/data 13 | - ./letsencrypt:/etc/letsencrypt 14 | 15 | networks: 16 | default: 17 | external: 18 | name: proxy 19 | -------------------------------------------------------------------------------- /docs/installation/README.md: -------------------------------------------------------------------------------- 1 | # Customization / White Label 2 | 3 | You can take this project as a basis and modify it to be used on your own website, as long as you respect the conditions of the [AGPL like licenese](../LICENSE.txt). We kindly ask you to consider [purchasing a commercial license](../README.md#commercial-license) to support the project and benefit from a personal license. 4 | 5 | There are diffent stages of customization: 6 | 7 | 1. **Embedding:** Put brie.fi/ng itself into your website using an IFRAME. Use the [Embed Configurator](https://brie.fi/ng/embed-demo) to find the right settings. No installation required! 8 | 2. **Website:** Just use the HTML, CSS and JS from Briefing and put it on your own hosting. You can easily customize. Signaling and STUN are used from brie.fi/ng or you point to your own services. [→ Details](website.md) 9 | 3. **Docker:** Use a container with everything prepared. Just some modifications are required. It is also possible to use other hosting options. This way you have your own Signaling and STUN running quickly. [→ Details](./docker.md) 10 | 4. **Source Code:** Modify the project directly. You've got the full scale of possibilities. [→ Details](../development.md) 11 | 12 | Additionally: 13 | 14 | - **TURN Server**: In case you need a TURN server, learn [how to install "coturn"](./coturn.md) 15 | 16 | Service providers: 17 | 18 | - [Installation on fly.io](./fly.io.md) 19 | - [Installation on render.com](./render.com.md) 20 | - [Installation on repl.it](https://replit.com/@holtwick/briefing?v=1) 21 | 22 | This illustration shows the different functional parts of Briefing: 23 | 24 | ![connection](../assets/connection.svg) 25 | -------------------------------------------------------------------------------- /docs/installation/coturn.md: -------------------------------------------------------------------------------- 1 | # Installation of STUN / TURN server: "coturn" 2 | 3 | > This is only required if you want to set up your own COTURN server. 4 | > Briefing already is connected to an instance of such a server, which is located in Germany at [Hetzner](https://hetzner.cloud/?ref=thK9VpOJK5Sg). 5 | > Alternatively you can also use free STUN servers from Google or others. 6 | 7 | Follow these steps: https://community.hetzner.com/tutorials/install-turn-stun-server-on-debian-ubuntu-with-coturn 8 | 9 | Additionally: 10 | 11 | 1. Create a user via `turnadmin -a -u brie -r stun.brie.fi -p fi` 12 | 2. Set `/etc/turnserver.conf` to the following: 13 | 14 | ```ini 15 | listening-port=3478 16 | tls-listening-port=5349 17 | 18 | fingerprint 19 | lt-cred-mech 20 | 21 | user=brie:fi 22 | 23 | server-name:stun.brie.fi 24 | realm=stun.brie.fi 25 | 26 | total-quota=100 27 | stale-nonce=600 28 | 29 | cert=/etc/letsencrypt/live/stun.brie.fi/cert.pem 30 | pkey=/etc/letsencrypt/live/stun.brie.fi/privkey.pem 31 | cipher-list="ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384" 32 | 33 | no-stdout-log 34 | 35 | proc-user=turnserver 36 | proc-group=turnserver 37 | ``` 38 | 39 | --- 40 | 41 | Docker might be an alternative, but I didn't try by myself: 42 | 43 | - https://hub.docker.com/r/coturn/coturn 44 | - https://gabrieltanner.org/blog/turn-server 45 | 46 | --- 47 | 48 | Test if it is working: 49 | 50 | - https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 51 | - https://icetest.info/ 52 | -------------------------------------------------------------------------------- /docs/installation/docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | > [!NOTE] Docker Image 4 | > A public docker image is available at [hub.docker.com/r/holtwick/briefing](https://hub.docker.com/r/holtwick/briefing). 5 | 6 | With a [Docker Engine installed](https://docs.docker.com/engine/install/) use it like this: 7 | 8 | ```sh 9 | docker run -d -p 8080:8080 holtwick/briefing 10 | ``` 11 | 12 | If running locally, you can now access it via . 13 | 14 | For production installation I recommend using a [proxy](#proxy) and consider making a `docker-compose.yml`. 15 | 16 | **Important! If not running locally in order to get camera and audio access working, an [SSL connection is required](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#privacy_and_security)!** 17 | 18 | Video with a quick walkthrough: 19 | 20 | [![](https://img.youtube.com/vi/YFwlnkRVmPc/0.jpg)](https://www.youtube.com/watch?v=YFwlnkRVmPc) 21 | 22 | ## Configuration 23 | 24 | Fine tuning by setting environment variables. See [**→ configuration**](../configuration.md) 25 | 26 | ## Alternative: Build locally 27 | 28 | The easiest way to install Briefing is by using Docker. 29 | 30 | 1. Run `npm run build:docker` to create all files needed. 31 | 2. Copy the newly generated folder `docker` to the server. 32 | 3. On the server go inside the `docker` folder 33 | 4. Run `docker compose up -d --build` 34 | 35 | ## Debug 36 | 37 | To stop run `docker compose down`. 38 | 39 | In case of problems try to start without the `-d` flag to see logs, like `docker compose up --build`. 40 | 41 | Once built, you can also leave out the `--build` flag. 42 | 43 | All dynamic data goes into the `data` folder. Currently, only a log file of the signal server is available. 44 | 45 | ## Proxy 46 | 47 | We recommend using a proxy to easily support safe `https`, which is required to get the camera and audio working on client side. 48 | 49 | A good proxy is [nginxproxymanager.com](https://nginxproxymanager.com/). 50 | 51 | You can find the required `docker-compose.yml` files in the [examples](../examples) folder. 52 | 53 | See this video for details step by step: 54 | 55 | [![](https://img.youtube.com/vi/KIpB6rlxRsE/0.jpg)](https://www.youtube.com/watch?v=KIpB6rlxRsE) 56 | -------------------------------------------------------------------------------- /docs/installation/fly.io.md: -------------------------------------------------------------------------------- 1 | # Installation on fly.io 2 | 3 | To install Briefing on [fly.io](https://fly.io/?ref=briefing), follow the steps to install `flyctl` on your machine and the enter this command: 4 | 5 | ```sh 6 | flyctl launch --image holtwick/briefing 7 | ``` 8 | 9 | Here is a running demo instance: 10 | -------------------------------------------------------------------------------- /docs/installation/render.com.md: -------------------------------------------------------------------------------- 1 | # Installation on render.com 2 | 3 | To install Briefing on [render.com](https://render.com/?ref=briefing), please follow these steps: 4 | 5 | 1. Go to dashboard and create new "Web Service" 6 | 2. Connect to the public repository 7 | 3. Fill in the form, leave the presets except: 8 | 1. Choose a "Name" you like 9 | 2. For "Environment" choose "Node" 10 | 4. Done :) 11 | 12 | Here is a running demo instance: 13 | -------------------------------------------------------------------------------- /docs/installation/website.md: -------------------------------------------------------------------------------- 1 | # Static Website 2 | 3 | If you are fine with using brie.fi/ng Signaling and STUN you may also just host the HTML, CSS and JS files of the project. 4 | 5 | Run the following to build the website data: 6 | 7 | ``` 8 | npm install 9 | npm run build 10 | ``` 11 | 12 | You will find the result in the folder `www`. 13 | 14 | Modify the file `briefing-config.js` according to these [**→ configuration options**](../configuration.md). 15 | 16 | Example 17 | 18 | ```js 19 | window.briefingConfig = { 20 | ROOM_URL: 'https://example.com/room/', 21 | ROOM_PATH: '/room/' 22 | } 23 | ``` 24 | 25 | Now copy this folder to your hosting service. 26 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Briefing - Secure direct video conferencing 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /locales/README.md: -------------------------------------------------------------------------------- 1 | You can easily contribute translations via GitHub. 2 | 3 | There is also an integration available for POEditor, that might help to get started quicker with a new language: 4 | https://poeditor.com/join/project?hash=o4xqgp9kr9 5 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "ask_to_send_error": "Ein Fehler ist aufgetreten. Bitte hilf uns, das Problem zu beheben, indem du uns die Details zusenden. Diese Option ist auch in den Einstellungen verfügbar. Danke!", 4 | "send_allow": "Erlauben", 5 | "send_deny": "Ablehnen" 6 | }, 7 | "main": { 8 | "action_restart_video": "Klicke hier, um das Video zu starten", 9 | "connection_wait": "Auf Verbindung warten", 10 | "security_info": "Wenn die Person, die du hier siehst, bestätigt, dieselbe ID zu sehen, bist du sicher verbunden:", 11 | "video_muted": "Du hast das Video ausgeschaltet" 12 | }, 13 | "settings": { 14 | "audio": "Audio Geräte", 15 | "background": "Hintergrund", 16 | "bandwidth": "Bandbreitenoptimierungen", 17 | "bandwidth_info": "Experimentell: Bei dieser Einstellung versucht Briefing, die Bandbreite durch geringer Qualität der Video- und Audiodaten zu reduzieren.", 18 | "blur": "Hintergrund verschwommen", 19 | "blur_info": "Experimentell: Ein intelligenter Algorithmus der künstlichen Intelligenz ist in der Lage, die Konturen von Personen zu identifizieren und den verbleibenden Hintergrund zu verwischen. Dies fügt deinem Anruf etwas visuelle Privatsphäre hinzu. Aber Achtung, dies ist eine sehr energieintensive Funktion und wird auf mobilen Geräten sehr wahrscheinlich nicht funktionieren!", 20 | "blurred_background": "Verschwommener Hintergrund", 21 | "desktop": "Bildschirm oder Fenster teilen", 22 | "fill": "Video ausfüllend", 23 | "fill_info": "Briefing versucht, den verfügbaren Bildschirmplatz so weit wie möglich auszunutzen, indem das Video so skaliert wird, dass es in den ganzen sichtbaren Rahmen passt. Ansonsten siehst du stattdessen das gesamte Video, jedoch mit einem Rahmen ums Video.", 24 | "image_background": "Hintergrundbild", 25 | "image_tip": "Ziehe ein Bild auf dieses Fenster, um deinen eigenen Hintergrund zu verwenden", 26 | "original_background": "Echter Hintergrund", 27 | "persist_settings": "Einstellungen speichern", 28 | "random_image": "Klicke um ein zufälliges Bild zu erhalten.", 29 | "sentry": "Fehleranalyse erlauben", 30 | "sentry_confirm": "Danke, dass du die Fehleranalyse erlaubst. Bitte bestätige, dass die Seite jetzt neu zu geladen werden darf.", 31 | "sentry_info": "Wenn wir auf einen Programmierfehler oder andere relevante Informationen stoßen, die zur Verbesserung der Anwendung nützlich sind, senden wir Debug-Daten an einen Dienst namens sentry.io.", 32 | "subscribe": "Diesen Raum abonnieren", 33 | "subscribe_info": "Experimentell: Wenn du den Raum abonnierst, erhältst du eine Benachrichtigung, wenn jemand anderes diesen Raum betritt. Du kannst dann mit einem Klick in das Gespräch einsteigen. Die Benachrichtigungen werden nur angezeigt, wenn der Browser läuft.", 34 | "title": "Einstellungen", 35 | "video": "Video Geräte" 36 | }, 37 | "share": { 38 | "button_copy": "Teilen", 39 | "feedback": "Feedback bitte an {link}", 40 | "link_info": "Gib diesen Link weiter an jeden der an dieser Sitzung teilnehmen soll:", 41 | "message": "Einladungs-Link verschicken durch Klick auf das {symbol}-Symbol.", 42 | "qr_info": "Oder scanne diesen QR-Code mit einer Kamera:", 43 | "title": "Einladen" 44 | }, 45 | "welcome": { 46 | "abstract": "Sicherer direkter Video-Chat für Gruppen", 47 | "created": "Erstellt von", 48 | "help": "Mehr Informationen", 49 | "history": "Zuvor besuchte Räume", 50 | "start": "Starte Chat" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "ask_to_send_error": "An error occurred. Please help us fixing it by allowing to send the details to us. This option is also available in the settings. Thanks!", 4 | "send_allow": "Allow", 5 | "send_deny": "Deny" 6 | }, 7 | "main": { 8 | "action_restart_video": "Click to start video", 9 | "connection_wait": "Waiting for connection", 10 | "security_info": "If the person you see here confirms to see the same ID, you are securely connected:", 11 | "video_muted": "You turned the video off" 12 | }, 13 | "settings": { 14 | "audio": "Audio Source", 15 | "background": "Background", 16 | "bandwidth": "Apply bandwidth optimizations", 17 | "bandwidth_info": "Experimental: With this setting Briefing tries to reduce bandwith by thinning video and audio data.", 18 | "blur": "Blur background", 19 | "blur_info": "Experimental: A smart artifical intelligence algorithm is able to indetify the shapes of persons and will blur out the remaining background. This adds some visual privacy to your call. But attention, this is a very power consuming feature and will very likely not work on mobile devices! ", 20 | "blurred_background": "Blurred background", 21 | "desktop": "Share screen or window", 22 | "fill": "Scale up video", 23 | "fill_info": "Briefing tries to use up the available screen space as much as possible by scaling up the video in a way that makes it fit in its visual frame. When turned off you will see the whole video instead but with borders around it.", 24 | "image_background": "Image background", 25 | "image_tip": "You can upload your own background by dragging an image file on this window.", 26 | "original_background": "Original background", 27 | "persist_settings": "Persist Settings", 28 | "random_image": "Click to get another random image.", 29 | "sentry": "Allow bug tracking", 30 | "sentry_confirm": "Thanks for allowing bug tracking. Please confirm to reload the page now.", 31 | "sentry_info": "When encounting a programming error or other relevant information that is useful improving the app, we will send debug data to a service called sentry.io.", 32 | "subscribe": "Subscribe to this room", 33 | "subscribe_info": "Experimental: By subscribing you will receive a notification when somebody else enters this room. You can then join the conversation with one click. Notifications will only be shown if the browser is running.", 34 | "title": "Settings", 35 | "video": "Video Source" 36 | }, 37 | "share": { 38 | "button_copy": "Share", 39 | "feedback": "For feedback write to {link}", 40 | "link_info": "Please share this link with everyone you want to invite to this session:", 41 | "message": "Invite by sending the link via pressing on the {symbol} symbol.", 42 | "qr_info": "You can also scan this QR Code with a mobile device camera:", 43 | "title": "Invite" 44 | }, 45 | "welcome": { 46 | "abstract": "Secure direct group video chat", 47 | "created": "Created by", 48 | "help": "Learn More", 49 | "history": "Previously visited rooms", 50 | "start": "Start Chat" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Fuente de audio", 4 | "background": "Antecedentes", 5 | "bandwidth": "Aplicar optimizaciones de ancho de banda", 6 | "bandwidth_info": "Experimental: Con esta configuración, Briefing intenta reducir el ancho de banda disminuyendo los datos de vídeo y audio.", 7 | "blur": "Fondo borroso", 8 | "blur_info": "Experimental: Un algoritmo de inteligencia artificial inteligente es capaz de indetificar las formas de las personas y difuminar el fondo restante. Esto añade algo de privacidad visual a tu llamada. Pero atención, esta función consume mucha energía y es muy probable que no funcione en los dispositivos móviles. ", 9 | "blurred_background": "Fondo borroso", 10 | "desktop": "Compartir pantalla o ventana", 11 | "fill": "Vídeo de ampliación", 12 | "fill_info": "Briefing trata de utilizar el espacio disponible en la pantalla lo máximo posible, escalando el vídeo de forma que quepa en su marco visual. Si se desactiva, se verá el vídeo completo pero con bordes alrededor.", 13 | "image_background": "Fondo de la imagen", 14 | "image_tip": "Puede cargar su propio fondo arrastrando un archivo de imagen en esta ventana.", 15 | "original_background": "Fondo original", 16 | "persist_settings": "Persistir en la configuración", 17 | "random_image": "Haga clic para obtener otra imagen al azar.", 18 | "sentry": "Permitir el seguimiento de errores", 19 | "sentry_confirm": "Gracias por permitir el seguimiento de errores. Por favor, confirme la recarga de la página ahora.", 20 | "sentry_info": "Cuando encontremos un error de programación u otra información relevante que sea útil para mejorar la app, enviaremos datos de depuración a un servicio llamado sentry.io.", 21 | "subscribe": "Suscríbase a esta sala", 22 | "subscribe_info": "Experimental: Al suscribirte recibirás una notificación cuando alguien entre en esta sala. Entonces podrás unirte a la conversación con un solo clic. Las notificaciones sólo se mostrarán si el navegador está en marcha.", 23 | "title": "Ajustes", 24 | "video": "Fuente de vídeo" 25 | }, 26 | "share": { 27 | "button_copy": "Compartir", 28 | "feedback": "Si desea recibir comentarios, escriba a {link}", 29 | "link_info": "Por favor, comparte este enlace con todas las personas que quieras invitar a esta sesión:", 30 | "message": "Invite enviando el enlace mediante la pulsación del símbolo {symbol}.", 31 | "qr_info": "También puede escanear este código QR con la cámara de un dispositivo móvil:", 32 | "title": "Invite a" 33 | }, 34 | "welcome": { 35 | "abstract": "Videochat directo y seguro en grupo", 36 | "created": "Creado por", 37 | "help": "Más información", 38 | "history": "Salas visitadas anteriormente", 39 | "start": "Iniciar el chat" 40 | } 41 | } -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Source audio", 4 | "background": "Contexte", 5 | "bandwidth": "Appliquer des optimisations de la bande passante", 6 | "bandwidth_info": "Expérimental : avec ce réglage, Briefing tente de réduire la bande passante en réduisant les données vidéo et audio.", 7 | "blur": "Fond flou", 8 | "blur_info": "Expérimental : Un algorithme intelligent d'intelligence artificielle est capable d'indexer les formes des personnes et de brouiller l'arrière-plan restant. Cela ajoute une certaine intimité visuelle à votre appel. Mais attention, cette fonction est très gourmande en énergie et ne fonctionnera très probablement pas sur les appareils mobiles ! ", 9 | "blurred_background": "Fond flou", 10 | "desktop": "Partager l'écran ou la fenêtre", 11 | "fill": "Agrandir la vidéo", 12 | "fill_info": "Briefing essaie d'utiliser au maximum l'espace disponible sur l'écran en augmentant l'échelle de la vidéo de manière à ce qu'elle s'inscrive dans son cadre visuel. Lorsqu'elle est éteinte, vous verrez la vidéo en entier, mais avec des bordures autour.", 13 | "image_background": "Fond d'image", 14 | "image_tip": "Vous pouvez télécharger votre propre arrière-plan en faisant glisser un fichier image sur cette fenêtre.", 15 | "original_background": "Contexte original", 16 | "persist_settings": "Paramètres persistants", 17 | "random_image": "Cliquez pour obtenir une autre image aléatoire.", 18 | "sentry": "Permettre le suivi des bogues", 19 | "sentry_confirm": "Merci de permettre le suivi des bogues. Veuillez confirmer pour recharger la page maintenant.", 20 | "sentry_info": "En cas d'erreur de programmation ou d'autres informations utiles pour améliorer l'application, nous envoyons les données de débogage à un service appelé sentry.io.", 21 | "subscribe": "Abonnez-vous à cette salle", 22 | "subscribe_info": "Expérimental : en vous inscrivant, vous recevrez une notification lorsque quelqu'un d'autre entrera dans cette salle. Vous pouvez alors vous joindre à la conversation en un seul clic. Les notifications ne seront affichées que si le navigateur est en cours d'exécution.", 23 | "title": "Paramètres", 24 | "video": "Source vidéo" 25 | }, 26 | "share": { 27 | "button_copy": "Partager", 28 | "feedback": "Pour tout commentaire, écrivez à {link}", 29 | "link_info": "Veuillez partager ce lien avec toutes les personnes que vous souhaitez inviter à cette session :", 30 | "message": "Inviter en envoyant le lien en appuyant sur le bouton {symbol} symbole.", 31 | "qr_info": "Vous pouvez également scanner ce QR Code avec un appareil photo mobile :", 32 | "title": "Inviter" 33 | }, 34 | "welcome": { 35 | "abstract": "Chat vidéo de groupe direct et sécurisé", 36 | "created": "Créé par", 37 | "help": "En savoir plus", 38 | "history": "Salles déjà visitées", 39 | "start": "Démarrer le chat" 40 | } 41 | } -------------------------------------------------------------------------------- /locales/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Sumber Audio", 4 | "background": "Latar belakang", 5 | "bandwidth": "Terapkan optimalisasi bandwidth", 6 | "bandwidth_info": "Eksperimen: Dengan mengaktifkan pengaturan ini, Briefing akan mengurangi bandwidth dengan mengecilkan data video dan audio.", 7 | "blur": "Latar belakang Buram", 8 | "blur_info": "Eksperimental: Sebuah algoritma kecerdasan buatan dapat mengenali bentuk orang dan mengaburkan latar belakang yang ada. Ini akan menambahkan sedikit privasi secara visual pada panggilan anda. Tetapi, fitur ini menggunakan banyak tenaga dan kemungkinan tidak akan berjalan pada perangkat mobile!", 9 | "blurred_background": "Latar belakang blur", 10 | "desktop": "Bagikan layar atau jendela", 11 | "fill": "Perbesar skala video", 12 | "fill_info": "Briefing menggunakan sisa ruang layar sebanyak mungkin dengan memperbesar skala video dengan membuatnya pas dalam bingkai tampilan. Ketika dimatikan anda akan melihat video secara keseluruhan tetapi dengan pembatas di sekelilingnya.", 13 | "image_background": "Latar belakang gambar", 14 | "image_tip": "Anda dapat mengunggah latar belakang anda sendiri dengan menarik file gambar ke dalam window ini.", 15 | "original_background": "Latar belakang bawaan", 16 | "persist_settings": "Pertahankan pengaturan", 17 | "random_image": "Klik untuk mendapatkan gambar acak lainnya.", 18 | "sentry": "Izinkan pelacakan bug", 19 | "sentry_confirm": "Terima kasih terlah mengizinkan pelacakan bug. Mohon segarkan laman sekarang.", 20 | "sentry_info": "Ketika menghadapi kegagalan pemograman atau informasi lain yang relevan serta berguna bagi pengemabangan aplikasi, kami akan mengirim data debug ke layanan bernama sentry.io.", 21 | "subscribe": "Berlangganan ke ruang ini", 22 | "subscribe_info": "Eksperimental : Dengan berlangganan anda akan menerima sebuah notifikasi ketika seseorang memasuki ruangan ini. Anda dapat kemudian bergabung dengan percakapan hanya dengan satu klik. Notifikasi hanya akan muncul ketika peramban sedang berjalan.", 23 | "title": "Pengaturan", 24 | "video": "Sumber Video" 25 | }, 26 | "share": { 27 | "button_copy": "Bagikan", 28 | "feedback": "Beri masukan dengan menulis ke {link}", 29 | "link_info": "Silahkan bagikan tautan ini kepada orang yang ingin anda undang:", 30 | "message": "Undang melalui tautan dengan memencet pada {symbol} symbol.", 31 | "qr_info": "Anda juga dapat memindai Kode QR dengan ponsel pintar:", 32 | "title": "Undang" 33 | }, 34 | "welcome": { 35 | "abstract": "Video chat berkelompok langsung aman", 36 | "created": "Diciptakan oleh", 37 | "help": "Pelajari Lagi", 38 | "start": "Mulai chat" 39 | } 40 | } -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Sorgente audio", 4 | "background": "Sfondo", 5 | "bandwidth": "Applica ottimizzazioni della larghezza di banda", 6 | "bandwidth_info": "Sperimentale: con questa impostazione,briefing cerca di ridurre la larghezza di banda assottigliando i dati video e audio.", 7 | "blur": "Sfocatura dello sfondo", 8 | "blur_info": "Sperimentale: un algoritmo di intelligenza artificiale è in grado di individuare le forme delle persone e sfocare lo sfondo rimanente. Questo aggiunge un po di privacy visiva alla tua chiamata. Ma attenzione, questa è una funzione che consuma molto energia e molto probabilmente non funzionerà su dispositivi mobili! ", 9 | "blurred_background": "Sfondo sfocato", 10 | "desktop": "Condividi schermo o finestra", 11 | "fill": "Scala video", 12 | "fill_info": "Il briefing cerca di sfruttare il più possibile lo spazio disponibile sullo schermo ridimensionando il video in modo da adattarlo alla sua cornice visiva. Quando è spento, vedrai invece l'intero video ma con bordi attorno.", 13 | "image_background": "Sfondo dell'immagine", 14 | "image_tip": "Puoi caricare il tuo sfondo trascinando un file immagine in questa finestra.", 15 | "original_background": "Sfondo originale", 16 | "persist_settings": "Impostazioni persistenti", 17 | "random_image": "Fare clic per ottenere un'altra immagine casuale.", 18 | "sentry": "Consenti rilevamento bug", 19 | "sentry_confirm": "Grazie per aver permesso il tracciamento dei bug. Conferma per ricaricare la pagina ora.", 20 | "sentry_info": "Quando riscontriamo un errore di programmazione o altre informazioni utili utili per migliorare l'app,invieremo i dati di debug a una chiamata di servizio sentry.io.", 21 | "subscribe": "Iscriviti a questa stanza", 22 | "subscribe_info": "Sperimentale: iscrivendoti riceverai una notifica quando qualcun altro entra in questa stanza. È quindi possibile partecipare alla conversazione con un clic. Le notifiche verranno visualizzate solo se il browser è in esecuzione.", 23 | "title": "Impostazioni", 24 | "video": "Sorgente video" 25 | }, 26 | "share": { 27 | "button_copy": "Condividi", 28 | "feedback": "Per feedback scrivere a {link}", 29 | "link_info": "Si prega di inviare questo link a tutti i partecipanti:", 30 | "message": "Invita inviando il link premendo sul tasto {symbol} symbol.", 31 | "qr_info": "Puoi anche scansionare questo codice QR con una fotocamera del dispositivo mobile:", 32 | "title": "Condividi" 33 | }, 34 | "welcome": { 35 | "abstract": "Video chat di gruppo sicura e diretta", 36 | "created": "Creato da", 37 | "help": "Per saperne di più", 38 | "history": "Stanze visitate in precedenza", 39 | "start": "Avvia chat" 40 | } 41 | } -------------------------------------------------------------------------------- /locales/kr.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "오디오 소스", 4 | "background": "배경", 5 | "bandwidth": "대역폭 최적화 적용", 6 | "bandwidth_info": "실험 기능: 이 설정을 통해 Briefing은 비디오 및 오디오 데이터를 정제해 대역폭을 줄이려고 합니다.", 7 | "blur": "흐린 배경화면", 8 | "blur_info": "실험 기능 : AI 알고리즘이 사람의 형체를 인식해 배경을 흐리게 합니다. 해당 기능을 사용하려면 당신의 시각적 개인정보 동의가 필요합니다. 하지만, 전력 소모량이 많아 모바일 기기에서는 원활히 기능하지 않을 가능성이 큽니다!", 9 | "blurred_background": "블러처리 된 배경", 10 | "desktop": "전체화면 또는 앱 화면 공유", 11 | "fill": "비디오 스케일 업", 12 | "fill_info": "Briefing은 영상을 비주얼 프레임에 맞게 스케일 업 해 사용 가능한 화면 공간을 다 사용합니다. 해제하면 비디오를 전체 화면으로 볼 수 있는 대신 테두리가 있는 상태로 표시됩니다.", 13 | "image_background": "이미지 배경", 14 | "image_tip": "이미지 파일을 이 위치로 드래그해 이미지 파일을 업로드 할 수 있습니다.", 15 | "original_background": "기본 배경", 16 | "persist_settings": "지속 설정", 17 | "random_image": "랜덤으로 이미지 고르기.", 18 | "sentry": "버그 트래킹 허용", 19 | "sentry_confirm": "버그 트래킹을 허용해주셔서 감사합니다. 페이지를 새로고침해 확인해주세요.", 20 | "sentry_info": "프로그램 오류나 앱을 개선할 수 있는 유용한 정보를 마주했을 때, sentry.io로 디버그 데이터를 보냅니다.", 21 | "subscribe": "방 구독하기", 22 | "subscribe_info": "실험 기능: 구독하기 기능으로 누군가 이 방에 들어온 다면 알림을 받을 수 있습니다. 한 번의 클릭으로 참여자와 소통이 가능합니다. 알림은 브라우저가 작동하고 있을 때만 보여집니다.", 23 | "title": "설정", 24 | "video": "비디오 소스" 25 | }, 26 | "share": { 27 | "button_copy": "공유하기", 28 | "feedback": "피드백 {link}", 29 | "link_info": "모두에게 링크를 공유해 초대해보세요:", 30 | "message": "{symbol} 아이콘을 클릭하여 링크를 보내 초대해보세요.", 31 | "qr_info": "모바일 기기의 카메라를 이용해 QR 코드를 스캔할 수도 있습니다:", 32 | "title": "초대하기" 33 | }, 34 | "welcome": { 35 | "abstract": "안전하게 보호되는 그룹 영상 통화", 36 | "created": "개발자", 37 | "help": "알아보기", 38 | "start": "시작하기" 39 | } 40 | } -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Źródło dźwięku", 4 | "background": "Tło", 5 | "bandwidth": "Zastosuj optymalizacje przepustowości", 6 | "bandwidth_info": "Eksperymentalne: przy tym ustawieniu Briefing próbuje zmniejszyć przepustowość, zmniejszając ilość danych wideo i audio.", 7 | "blur": "Rozmycie tła", 8 | "blur_info": "Eksperymentalny: inteligentny algorytm sztucznej inteligencji jest w stanie zidentyfikować kształty osób i rozmyć pozostałe tło. To dodaje wizualnej prywatności do twojego połączenia. Ale uwaga, jest to bardzo energochłonna funkcja i najprawdopodobniej nie będzie działać na urządzeniach mobilnych! ", 9 | "blurred_background": "Rozmazane tło", 10 | "desktop": "Udostępnij ekran lub okno", 11 | "fill": "Skaluj wideo", 12 | "fill_info": "Briefing stara się maksymalnie wykorzystać dostępną przestrzeń ekranu, skalując wideo w taki sposób, aby mieściło się w ramce wizualnej. Po wyłączeniu zamiast tego zobaczysz cały film, ale z ramkami wokół niego.", 13 | "image_background": "Obraz tła", 14 | "image_tip": "Możesz przesłać własne tło, przeciągając plik obrazu w tym oknie.", 15 | "original_background": "Oryginalne tło", 16 | "persist_settings": "Utrwal ustawienia", 17 | "random_image": "Kliknij, aby uzyskać inny losowy obraz.", 18 | "sentry": "Zezwól na śledzenie błędów", 19 | "sentry_confirm": "Dziękujemy za umożliwienie śledzenia błędów. Potwierdź teraz ponowne załadowanie strony.", 20 | "sentry_info": "W przypadku napotkania błędu programistycznego lub innych istotnych informacji, które są przydatne do ulepszenia aplikacji, wyślemy dane debugowania do usługi o nazwie sentry.io.", 21 | "subscribe": "Zapisz się do tego pokoju", 22 | "subscribe_info": "Eksperymentalne: Subskrybując będziesz otrzymywać powiadomienia, gdy ktoś inny wejdzie do tego pokoju. Następnie możesz dołączyć do rozmowy jednym kliknięciem. Powiadomienia będą wyświetlane tylko wtedy, gdy przeglądarka jest uruchomiona.", 23 | "title": "Ustawienia", 24 | "video": "Źródło wideo" 25 | }, 26 | "share": { 27 | "button_copy": "Udostępnij", 28 | "feedback": "Napisz opinię do {link}", 29 | "link_info": "Proszę podziel się tym linkiem ze wszystkimi, których chcesz zaprosić do tej sesji:", 30 | "message": "Zaproś, wysyłając link, naciskając przycisk {symbol}.", 31 | "qr_info": "Możesz także zeskanować ten kod QR aparatem telefonu:", 32 | "title": "Zaproś" 33 | }, 34 | "welcome": { 35 | "abstract": "Bezpieczny chat grupowy", 36 | "created": "Stworzone przez", 37 | "help": "Zobacz więcej", 38 | "start": "Rozpocznij" 39 | } 40 | } -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Fonte de áudio", 4 | "background": "Antecedentes", 5 | "bandwidth": "Aplicar optimizações de largura de banda", 6 | "bandwidth_info": "Experimental: Com este cenário, o Briefing tenta reduzir a largura de banda através do desbaste de dados de vídeo e áudio.", 7 | "blur": "Fundo desfocado", 8 | "blur_info": "Experimental: Um algoritmo inteligente de inteligência artificial é capaz de indetificar as formas das pessoas e irá desfocar o fundo restante. Isto adiciona alguma privacidade visual à sua chamada. Mas atenção, este é um recurso que consome muita energia e muito provavelmente não funcionará em dispositivos móveis! ", 9 | "blurred_background": "Fundo desfocado", 10 | "desktop": "Partilhar ecrã ou janela", 11 | "fill": "Ampliar vídeo", 12 | "fill_info": "O Briefing tenta utilizar ao máximo o espaço de tela disponível, ampliando o vídeo de uma forma que o faça caber no seu quadro visual. Quando desligado, você verá o vídeo inteiro em vez disso, mas com bordas ao redor dele.", 13 | "image_background": "Imagem de fundo", 14 | "image_tip": "Você pode carregar seu próprio fundo arrastando um arquivo de imagem nesta janela.", 15 | "original_background": "Fundo original", 16 | "persist_settings": "Configurações Persistentes", 17 | "random_image": "Clique para obter outra imagem aleatória.", 18 | "sentry": "Permitir o acompanhamento de bugs", 19 | "sentry_confirm": "Obrigado por permitir o acompanhamento de bugs. Por favor, confirme para recarregar a página agora.", 20 | "sentry_info": "Ao contar um erro de programação ou outra informação relevante que seja útil para melhorar o aplicativo, enviaremos dados de debug para um serviço chamado sentry.io.", 21 | "subscribe": "Subscreva esta sala", 22 | "subscribe_info": "Experimental: Ao subscrever você receberá uma notificação quando outra pessoa entrar nesta sala. Você pode então juntar-se à conversa com um clique. As notificações só serão mostradas se o navegador estiver em execução.", 23 | "title": "Configurações", 24 | "video": "Fonte de vídeo" 25 | }, 26 | "share": { 27 | "button_copy": "Compartilhe", 28 | "feedback": "Para feedback escreva para {link}", 29 | "link_info": "Por favor, partilhe este link com todas as pessoas que queira convidar para esta sessão:", 30 | "message": "Convide enviando o link pressionando o símbolo {symbol}.", 31 | "qr_info": "Você também pode digitalizar este Código QR com uma câmera de dispositivo móvel:", 32 | "title": "Convide" 33 | }, 34 | "welcome": { 35 | "abstract": "Chat de vídeo seguro direto em grupo", 36 | "created": "Criado por", 37 | "help": "Saiba mais", 38 | "history": "Salas anteriormente visitadas", 39 | "start": "Iniciar Chat" 40 | } 41 | } -------------------------------------------------------------------------------- /locales/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Sursa Audio", 4 | "background": "Fundal", 5 | "bandwidth": "Aplica optimizarile pentru viteza", 6 | "bandwidth_info": "Experimental: Cu aceasta setare Briefing incearca sa reduca viteza folosita prin comprimarea video si audio.", 7 | "blur": "Blureaza fundalul", 8 | "blur_info": "Experimental: Un algoritm inteligent de inteligență artificială este capabil să indetifice formele persoanelor și va estompa fundalul rămas. Acest lucru adaugă o anumită confidențialitate vizuală apelului dvs. Dar atenție, aceasta este o caracteristică foarte consumatoare de energie și foarte probabil nu va funcționa pe dispozitivele mobile! ", 9 | "blurred_background": "Fundal estompat", 10 | "desktop": "Partajeaza ecranul sau o fereastra", 11 | "fill": "Umple video pe tot ecranul", 12 | "fill_info": "Briefing încearcă să utilizeze spațiul disponibil pe ecran cât mai mult posibil, mărind videoclipul într-un mod care îl face să se încadreze în cadrul său vizual. Când este dezactivat, veți vedea întregul videoclip, dar cu margini în jurul acestuia.", 13 | "image_background": "Imagine de fundal", 14 | "image_tip": "Poti urca propriul tau fundal prin tragerea unei imagini peste aceasta fereastra.", 15 | "original_background": "Fundal original", 16 | "persist_settings": "Setari persistente", 17 | "random_image": "Apasa pentru a vedea o imagine aleatorie.", 18 | "sentry": "Permite raportarea de bug-uri", 19 | "sentry_confirm": "Multumesc pentru ca permiti raportarea de bug-uri. Te rog confirma prin reincarcarea paginii.", 20 | "sentry_info": "Când întâmpinăm o eroare de programare sau alte informații relevante care sunt utile pentru îmbunătățirea aplicației, vom trimite date de depanare către un serviciu numit sentry.io.", 21 | "subscribe": "Inscrie-te in aceasta camera", 22 | "subscribe_info": "Experimental: Prin abonare veți primi o notificare atunci când altcineva intră în această cameră. Puteți apoi să vă alăturați conversației cu un singur clic. Notificările vor fi afișate numai dacă browserul rulează.", 23 | "title": "Setari", 24 | "video": "Sursa Video" 25 | }, 26 | "share": { 27 | "button_copy": "Partajeaza", 28 | "feedback": "Pentru sugestii scrieti la {link}", 29 | "link_info": "Va rugam partajati acest link cu toti participantii pe care ii vreti in sesiune:", 30 | "message": "Invita prin trimiterea link-ului apasand pe simbolul {symbol}.", 31 | "qr_info": "Puteti deasemenea scana acest cod QR cu un telefon mobil:", 32 | "title": "Invita" 33 | }, 34 | "welcome": { 35 | "abstract": "Conversatie video de grup securizata", 36 | "created": "Creat de", 37 | "help": "Afla mai mult", 38 | "history": "Camere vizitate anterior", 39 | "start": "Incepe conversatia" 40 | } 41 | } -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "ask_to_send_error": "Произошла ошибка. Пожалуйста, помогите нам исправить это, разрешив отправку нам подробной информации. Эта опция также доступна в настройках. Спасибо!", 4 | "send_allow": "Разрешить", 5 | "send_deny": "Запретить" 6 | }, 7 | "main": { 8 | "action_restart_video": "Нажмите, чтобы включить видео", 9 | "connection_wait": "Ожидаем подключения", 10 | "security_info": "Если человек, которого вы здесь видите, подтвердит, что видит тот же идентификатор, ваше подключение надежно защищено:", 11 | "video_muted": "Вы выключили видео" 12 | }, 13 | "settings": { 14 | "audio": "Источник звука", 15 | "background": "Фон", 16 | "bandwidth": "Применить оптимизацию трафика", 17 | "bandwidth_info": "Экспериментально: С помощью этой настройки Briefing пытается уменьшить трафик за счет прореживания видео и аудио данных.", 18 | "blur": "Размыть фон", 19 | "blur_info": "Экспериментально: Умный алгоритм искусственного интеллекта способен распознавать силуэты людей и размывать оставшийся фон. Это добавит визуальной конфиденциальности вашему звонку. Но внимание, это очень энергозатратная функция и на мобильных устройствах она, скорее всего, работать не будет!", 20 | "blurred_background": "Размытый фон", 21 | "desktop": "Поделиться экраном или окном", 22 | "fill": "Масштабировать видео", 23 | "fill_info": "Briefing пытается максимально использовать доступное пространство экрана, масштабируя видео таким образом, чтобы оно вписывалось в визуальный кадр. Если этот параметр отключен, вы увидите все видео, но с рамками вокруг него.", 24 | "image_background": "Изображение фона", 25 | "image_tip": "Вы можете загрузить свой собственный фон, перетащив файл изображения в это окно.", 26 | "original_background": "Оригинальный фон", 27 | "persist_settings": "Сохранить настройки", 28 | "random_image": "Нажмите, чтобы получить другое случайное изображение.", 29 | "sentry": "Разрешить отслеживание ошибок", 30 | "sentry_confirm": "Спасибо, что разрешили отслеживать ошибки. Пожалуйста, подтвердите перезагрузку страницы сейчас.", 31 | "sentry_info": "При обнаружении программной ошибки или другой важной информации, полезной для улучшения приложения, мы отправляем данные отладки в службу под названием sentry.io.", 32 | "subscribe": "Подписаться на эту комнату", 33 | "subscribe_info": "Экспериментально: Подписавшись, вы будете получать уведомления, когда кто-то еще войдет в эту комнату. После этого вы сможете присоединиться к беседе одним щелчком мыши. Уведомления будут отображаться только в том случае, если браузер запущен.", 34 | "title": "Настройки", 35 | "video": "Источник видео" 36 | }, 37 | "share": { 38 | "button_copy": "Поделиться", 39 | "feedback": "Для обратной связи пишите на {link}", 40 | "link_info": "Пожалуйста, поделитесь этой ссылкой со всеми, кого вы хотите пригласить на эту сессию:", 41 | "message": "Нажмите на иконку {symbol}, чтобы отправить приглашение.", 42 | "qr_info": "Вы также можете отсканировать этот QR-код с помощью камеры мобильного устройства:", 43 | "title": "Пригласить" 44 | }, 45 | "welcome": { 46 | "abstract": "Защищенный прямой групповой видеочат", 47 | "created": "Создано", 48 | "help": "Узнать больше", 49 | "history": "Ранее посещенные комнаты", 50 | "start": "Начать Чат" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "Ses Kaynağı", 4 | "background": "Arkaplan", 5 | "bandwidth": "Bant genişliği optimizasyonunu uygula", 6 | "bandwidth_info": "Deneysel: Briefing video ve ses datalarını küçülterek bant genişliği sağlamaya çalışır", 7 | "blur": "Bulanık arkaplan", 8 | "blur_info": "Deneysel: Bir yapay zeka uygulaması kişileri algılayabilir ve geri kalan kısmı bulanıklaştırabilir. Bu bir nevi gizlilik katar. Yalnız bu özellik çok güç tüketen bir sistemdir mobil cihazlarda çalışmalabilir.", 9 | "blurred_background": "Bulanıklandırılmış arkaplan", 10 | "desktop": "Ekran yada pencere paylaş", 11 | "fill": "Videoyu ölçeklendirin", 12 | "fill_info": "Brifing, videoyu çerçeveye sığacak şekilde ölçeklendirerek mevcut ekran tamamını kullanmaya çalışır. Kapatıldığında ancak etrafında kenarlıklar olacak şekilde göreceksiniz.", 13 | "image_background": "Resim arkaplanı", 14 | "image_tip": "Bir resim yükleyerek kendi arkaplanınızı oluşturabilirsiniz.", 15 | "original_background": "Orjinal arkaplan", 16 | "persist_settings": "Kalıcı Ayarlar", 17 | "random_image": "Rasgele başka bir resim almak için tıklayınız.", 18 | "sentry": "Hata izlemeye izin ver", 19 | "sentry_confirm": "Hata izlemeye onay verdiğiniz için teşekkürler. Şimdi sayfayı lütfen yeniden yükleyin.", 20 | "sentry_info": "Bir hata ile karşılaşılır ise veya uygulamanın iyileştirilmesi için yararlı olan diğer ilgili bilgilerle karşılaşıldığında, hata ayıklama verilerini sentry.io adlı bir hizmete göndereceğiz.", 21 | "subscribe": "Bu odaya abone ol", 22 | "subscribe_info": "Deneysel: Abone olduğunu kanala bir kişi katıldığında bildirim alacaksınız. Sonra tek bir tıklama ile sohbete katılabilirsiniz. Bildirimler yalnızca tarayıcı açıksa gösterilecektir.", 23 | "title": "Ayarlar", 24 | "video": "Video Kaynağı" 25 | }, 26 | "share": { 27 | "button_copy": "Paylaş", 28 | "feedback": "Geri bildirim için {link}", 29 | "link_info": "Herkesi oturuma davet etmek için lütfen bu bağlantıyı paylaşın:", 30 | "message": "Butonu kullanarak davetiye gönderin {symbol} symbol.", 31 | "qr_info": "QR Kodu telefon kamerası ile tarayabilirsiniz:", 32 | "title": "Davet Et" 33 | }, 34 | "welcome": { 35 | "abstract": "Doğrudan güvenli görüntülü grup sohbet", 36 | "created": "Volkan Koç", 37 | "help": "Daha Fazla Öğren", 38 | "history": "Daha önce ziyaret edilen odalar", 39 | "start": "Sohbete Başla" 40 | } 41 | } -------------------------------------------------------------------------------- /locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "audio": "音频来源", 4 | "background": "背景", 5 | "bandwidth": "应用带宽优化 ", 6 | "bandwidth_info": "实验性:使用此设置,“简报”尝试通过稀疏视频和音频数据来减少带宽。 ", 7 | "blur": "模糊背景", 8 | "blur_info": "实验性的:一种智能的人工智能算法能够识别人的形状,并模糊掉其余的背景。这为您的通话增加了一些视觉隐私。但是请注意,这是一个非常耗电的功能,很可能无法在移动设备上使用! ", 9 | "blurred_background": "模糊背景", 10 | "desktop": "共享屏幕或窗口", 11 | "fill": "放大视频", 12 | "fill_info": "简报试图通过以使视频适合其视觉框架的方式缩放视频来尽可能地利用可用的屏幕空间。关闭后,您会看到整个视频,但周围带有边框。", 13 | "image_background": "图片背景", 14 | "image_tip": "您可以通过在此窗口上拖动图像文件来上传自己的背景。", 15 | "original_background": "原始背景", 16 | "persist_settings": "持续设置", 17 | "random_image": "单击以获取另一个随机图像。", 18 | "sentry": "允许错误跟踪", 19 | "sentry_confirm": "感谢您允许错误跟踪。请确认立即重新加载页面。", 20 | "sentry_info": "当遇到编程错误或其他对改善应用程序有用的相关信息时,我们会将调试数据发送到sentry.io.", 21 | "subscribe": "订阅这个房间", 22 | "subscribe_info": "实验性的:通过订阅,当其他人进入这个房间时,您会收到通知。然后,您可以一键加入对话。仅在浏览器正在运行时显示通知。 ", 23 | "title": "设置", 24 | "video": "视频来源" 25 | }, 26 | "share": { 27 | "button_copy": "分享", 28 | "feedback": "有问题请反馈到{link}", 29 | "link_info": "请您通过此链接邀请参加此会议的所有人:", 30 | "message": "通过按下 {symbol} 来发送邀请朋友加入群聊", 31 | "qr_info": "您也可以使用移动设备摄像头扫描此二维码:", 32 | "title": "邀请" 33 | }, 34 | "welcome": { 35 | "abstract": "安全的点对点的群组聊天工具", 36 | "created": "作者:", 37 | "help": "了解更多", 38 | "history": "以前访问过的房间", 39 | "start": "开始聊天" 40 | } 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "briefing", 3 | "version": "3.1.11", 4 | "private": true, 5 | "description": "Secure direct video chat", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/holtwick/briefing.git" 9 | }, 10 | "funding": { 11 | "type": "GitHub Sponsors ❤", 12 | "url": "https://github.com/sponsors/holtwick" 13 | }, 14 | "license": "AGPL-3.0-or-later", 15 | "author": { 16 | "name": "Dirk Holtwick", 17 | "email": "dirk.holtwick@gmail.com", 18 | "url": "https://holtwick.de" 19 | }, 20 | "type": "module", 21 | "scripts": { 22 | "build": "cross-env MODE=production nr build:basis", 23 | "build:basis": "nr clean && zerva build src/zerva/index.ts && vite build --mode $MODE", 24 | "build:docker": "cross-env MODE=production-docker nr build:basis && ./scripts/make-docker.sh", 25 | "check": "vue-tsc --noEmit -p ./tsconfig.json", 26 | "clean": "rm -rf dist www", 27 | "lint": "eslint .", 28 | "lint:fix": "eslint . --fix", 29 | "preview": "vite preview", 30 | "reset": "rm -rf node_modules pnpm-lock.yaml dist dist_www www", 31 | "serve": "cross-env ZEED=* LEVEL=i node dist/main.cjs", 32 | "start": "cross-env DEBUG=1 ZEED=* LEVEL=a zerva src/zerva/index.ts", 33 | "test": "vitest", 34 | "upload:dockerhub": "nr build:docker && (cd docker && docker login -u holtwick && docker buildx build --platform linux/arm64,linux/amd64,linux/s390x,linux/arm/v7,linux/arm/v6 -t holtwick/briefing:$npm_package_version -t holtwick/briefing:latest --push .)" 35 | }, 36 | "dependencies": { 37 | "@sentry/browser": "^7.60.0", 38 | "@sentry/tracing": "^7.60.0", 39 | "@vueuse/core": "^10.2.1", 40 | "@zerva/core": "^0.32.0", 41 | "@zerva/http": "^0.32.0", 42 | "@zerva/websocket": "^0.32.1", 43 | "clipboard-copy": "^4.0.1", 44 | "lodash": "^4.17.21", 45 | "vue": "^3.3.4", 46 | "vue-i18n": "^9.2.2", 47 | "zeed": "^0.11.3" 48 | }, 49 | "devDependencies": { 50 | "@antfu/eslint-config": "^0.39.8", 51 | "@antfu/ni": "^0.21.5", 52 | "@types/node": "^20.4.5", 53 | "@vitejs/plugin-vue": "^4.2.3", 54 | "@zerva/bin": "^0.32.0", 55 | "@zerva/vite": "^0.32.0", 56 | "cross-env": "^7.0.3", 57 | "eslint": "^8.45.0", 58 | "ministun": "^1.0.6", 59 | "sass": "^1.64.1", 60 | "tsc": "^2.0.4", 61 | "typescript": "^5.1.6", 62 | "vite": "^4.4.7", 63 | "vitest": "^0.33.0", 64 | "vue-tsc": "^1.8.6", 65 | "zeed-dom": "^0.12.5" 66 | }, 67 | "engines": { 68 | "node": ">=18.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/.well-known/apple-app-site-association: -------------------------------------------------------------------------------- 1 | { 2 | "applinks": { 3 | "details": [ 4 | { 5 | "appIDs": [ "8SS3YPUJH9.de.holtwick.Briefing" ], 6 | "components": [ 7 | { 8 | "/": "/ng/*", 9 | "comment": "Enter a room https://brie.fi/ng/example-room-name" 10 | }, 11 | { 12 | "/": "/", 13 | "comment": "Open Briefing app" 14 | } 15 | ] 16 | } 17 | ] 18 | }, 19 | "webcredentials": { 20 | "apps": [ "8SS3YPUJH9.de.holtwick.Briefing" ] 21 | }, 22 | "appclips": { 23 | "apps": ["8SS3YPUJH9.de.holtwick.Briefing.BriefingAppClip"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/briefing-config.js: -------------------------------------------------------------------------------- 1 | // Don't remove this file! 2 | 3 | // Visit https://github.com/holtwick/briefing/ for more details. 4 | 5 | window.briefingConfig = {} 6 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #272727 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Briefing", 3 | "short_name": "Briefing", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "scope": "/", 18 | "display": "standalone", 19 | "background_color": "#272727", 20 | "theme_color": "#272727", 21 | "author": "Dirk Holtwick", 22 | "description": "Secure direct video chat", 23 | "homepage_url": "https://brie.fi/", 24 | "url": "https://brie.fi/", 25 | "lang": "EN", 26 | "screenshots": [ 27 | { 28 | "src": "https://brie.fi/sample.jpg", 29 | "type": "image/png", 30 | "size": "1478x1988", 31 | "description": "Screenshot Briefing App" 32 | } 33 | ], 34 | "features": [ 35 | "Direct secure group video chat", 36 | "Verify integrity", 37 | "Blur background" 38 | ], 39 | "orientation": "any", 40 | "prefer_related_applications": false 41 | } -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/og-image.png -------------------------------------------------------------------------------- /public/pristine.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/pristine.mp3 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /public/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/sample.jpg -------------------------------------------------------------------------------- /public/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtwick/briefing/8be876f3c3043cbefbb57898d494629f6329e99f/public/sample.png -------------------------------------------------------------------------------- /public/service-worker.js: -------------------------------------------------------------------------------- 1 | console.info('Unregister previous service workers') 2 | 3 | try { 4 | self.addEventListener('install', () => { 5 | // Skip over the "waiting" lifecycle state, to ensure that our 6 | // new service worker is activated immediately, even if there's 7 | // another tab open controlled by our older service worker code. 8 | self.skipWaiting() 9 | }) 10 | 11 | self.addEventListener('activate', () => { 12 | // Optional: Get a list of all the current open windows/tabs under 13 | // our service worker's control, and force them to reload. 14 | // This can "unbreak" any open windows/tabs as soon as the new 15 | // service worker activates, rather than users having to manually reload. 16 | self.clients 17 | .matchAll({ 18 | type: 'window', 19 | }) 20 | .then((windowClients) => { 21 | windowClients.forEach((windowClient) => { 22 | windowClient.navigate(windowClient.url) 23 | }) 24 | }) 25 | }) 26 | 27 | self.addEventListener('activate', function (e) { 28 | self.registration 29 | .unregister() 30 | .then(function () { 31 | return self.clients.matchAll() 32 | }) 33 | .then(function (clients) { 34 | clients.forEach((client) => client.navigate(client.url)) 35 | }) 36 | }) 37 | } catch (err) { 38 | console.error('Activate failed', err) 39 | } 40 | 41 | try { 42 | caches.keys().then(function (names) { 43 | for (let name of names) caches.delete(name) 44 | }) 45 | } catch (err) { 46 | console.error('Cache delete failed', err) 47 | } 48 | 49 | try { 50 | navigator.serviceWorker.getRegistrations().then(function (registrations) { 51 | for (let registration of registrations) { 52 | registration.unregister() 53 | } 54 | }) 55 | } catch (err) { 56 | console.error('Unregistering failed', err) 57 | } 58 | -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | // Some services prefer to start with app.js 2 | 3 | import './dist/main.cjs' 4 | -------------------------------------------------------------------------------- /scripts/make-docker.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | rm -rf docker 4 | 5 | mkdir -p docker 6 | mkdir -p docker/data 7 | 8 | cp -a Dockerfile docker 9 | cp -a docker-package.json docker/package.json 10 | cp -a dist docker 11 | cp -a www docker 12 | cp docs/installation/docker.md docker/README.md 13 | cp scripts/app.js docker 14 | 15 | echo "" 16 | echo "**********************************************" 17 | echo "Find the Docker files in the ./docker/ folder." 18 | echo "**********************************************" -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/bugs/README-BUGTRACKER.md: -------------------------------------------------------------------------------- 1 | # Bug Tracking 2 | 3 | Knowing which bugs occur is crucial for the maturity of a project. And it is a complex business, where services like [sentry.io](https://sentry.io/) provide great infrastructure to learn the most out of the bugs and be able to fix them. Since privacy in general, and in particular for this project, is important, I tried to find a balance between code quality and privacy concerns. 4 | 5 | ### Opt-in 6 | 7 | It would be sad to lose any bug information if the user is willing to help, but not aware of the feature. A solution to this problem is, to ask the user for permission to track errors at the moment they occur. This will not catch all possible errors, but hopefully most of them. The user still has the possibility to opt-out in the settings. 8 | 9 | ### Lazy Loading 10 | 11 | To make sure no 3rd party code is doing anything in the background without the permission of the user, the related code is only loaded when allowed to. It will not be loaded immediately but "lazily" on demand. 12 | 13 | --- 14 | 15 | If you still have concerns or have ideas for improvement, please write to 16 | -------------------------------------------------------------------------------- /src/bugs/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { Logger } from 'zeed' 4 | import { SENTRY_DSN } from '../config' 5 | import { messages } from '../lib/messages' 6 | 7 | const log = Logger('app:bugs') 8 | 9 | // Lazy loading of bug tracker 10 | export function setupBugTracker(done?: Function) { 11 | if (SENTRY_DSN && isAllowedBugTracking()) { 12 | console.log('Sentry bug tracking is allowed') 13 | import('./lazy-sentry').then(({ setupSentry }) => { 14 | setupSentry({ 15 | dsn: SENTRY_DSN, 16 | }) 17 | console.log('Did init Sentry bug tracking') 18 | if (done) 19 | done() 20 | }) 21 | } 22 | } 23 | 24 | // Send bugs if user allowed to do so 25 | 26 | const collectedErrors = [] 27 | 28 | export function isAllowedBugTracking() { 29 | return localStorage?.allowSentry === '1' 30 | } 31 | 32 | export function setAllowedBugTracking( 33 | allowed = true, 34 | reloadMessage = 'Reload to activate changes', 35 | ) { 36 | log('setAllowedBugTracking', allowed) 37 | if (allowed) { 38 | localStorage.allowSentry = '1' 39 | setupBugTracker(() => { 40 | log('setupBugTracker', collectedErrors) 41 | let err 42 | // eslint-disable-next-line no-cond-assign 43 | while ((err = collectedErrors.pop())) { 44 | log('send error', err) 45 | trackException(err) 46 | } 47 | }) 48 | } 49 | else { 50 | localStorage.allowSentry = '0' 51 | // eslint-disable-next-line no-alert 52 | if (confirm(reloadMessage)) 53 | location.reload() 54 | } 55 | } 56 | 57 | export function trackException(e, silent = false) { 58 | if (!silent) 59 | log.error('Exception:', e) 60 | 61 | if (window.sentry) { 62 | log('sentry exception', e) 63 | window.sentry.captureException(e) 64 | } 65 | else { 66 | collectedErrors.push(e) 67 | messages.emit('requestBugTracking') 68 | } 69 | } 70 | 71 | export function trackSilentException(e) { 72 | log.error(e) 73 | trackException(e, true) 74 | } 75 | -------------------------------------------------------------------------------- /src/bugs/lazy-sentry.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser' 2 | import { BrowserTracing } from '@sentry/tracing' 3 | import { RELEASE } from '../config' 4 | 5 | window.sentry = Sentry 6 | 7 | export function setupSentry({ dsn }: any = {}) { 8 | Sentry.init({ 9 | dsn, 10 | release: RELEASE, 11 | integrations: [new BrowserTracing()], 12 | 13 | // beforeBreadcrumb(breadcrumb) { 14 | // // console.log("bc", breadcrumb) 15 | // return breadcrumb.category === "console" ? null : breadcrumb 16 | // }, 17 | // async beforeSend(event) { 18 | // // console.log("ev", event) 19 | // return event 20 | // }, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/app-chat.scss: -------------------------------------------------------------------------------- 1 | // Chat Added by Yash Govekar 2 | 3 | @import '../css/variables'; 4 | 5 | .unread-msg { 6 | border-radius: 50%; 7 | background-color: red; 8 | height: 0.5em; 9 | width: 0.5em; 10 | position: relative; 11 | right: 0.25em; 12 | top: -0.4em; 13 | } 14 | 15 | .chat-container { 16 | height: 100%; 17 | 18 | .messages-container { 19 | flex-direction: column; 20 | overflow-y: scroll; 21 | margin-bottom: 1em; 22 | padding-right: 0.2em; 23 | padding-left: 0.2em; 24 | } 25 | 26 | input { 27 | border: 1px solid gray; 28 | background: white; 29 | padding: 0.5rem; 30 | border-radius: 0.25rem; 31 | width: 100%; 32 | color: #1e89f6; 33 | margin-right: 0.5rem; 34 | } 35 | 36 | p { 37 | color: #212529; 38 | padding: 10px 15px !important; 39 | margin-bottom: 5px !important; 40 | display: inline-block !important; 41 | border-radius: 0.25rem !important; 42 | line-height: 1.6; 43 | } 44 | 45 | .bg-light { 46 | background-color: #c8e2f8 !important; 47 | } 48 | 49 | .bg-dark { 50 | background-color: #e4e7ed !important; 51 | } 52 | 53 | .save-btn { 54 | display: inline-block; 55 | margin-top: 0.5em; 56 | font-weight: 400; 57 | text-align: center; 58 | white-space: nowrap; 59 | vertical-align: middle; 60 | cursor: pointer; 61 | -webkit-user-select: none; 62 | -moz-user-select: none; 63 | -ms-user-select: none; 64 | user-select: none; 65 | padding: 0.375rem 0.75rem; 66 | font-size: 1rem; 67 | line-height: 1.5; 68 | border-radius: 0.25rem; 69 | color: #fff; 70 | background-color: $primary-color; 71 | border-color: $primary-color; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/app-chat.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 113 | -------------------------------------------------------------------------------- /src/components/app-share.scss: -------------------------------------------------------------------------------- 1 | .share-container { 2 | p, 3 | .p { 4 | margin-bottom: 1rem; 5 | } 6 | 7 | .input { 8 | border: 1px solid gray; 9 | background: white; 10 | padding: 0.5rem; 11 | border-radius: 0.25rem; 12 | width: 100%; 13 | color: #1e89f6; 14 | margin-right: 0.5rem; 15 | } 16 | 17 | 18 | } 19 | 20 | .qrcode { 21 | display: block; 22 | align-items: center; 23 | justify-content: center; 24 | margin: auto; 25 | background-color: white; 26 | cursor: pointer; 27 | text-align: center; 28 | 29 | svg { 30 | display: inline-block; 31 | max-height: 100vh; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/components/app-share.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 74 | -------------------------------------------------------------------------------- /src/components/app-whitelabel.scss: -------------------------------------------------------------------------------- 1 | .page1 { 2 | text-align: center; 3 | flex-shrink: 0; 4 | min-height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | line-height: 1.5; 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: inherit; 12 | } 13 | 14 | .main { 15 | width: 100%; 16 | display: flex; 17 | flex-grow: 1; 18 | align-content: center; 19 | justify-content: center; 20 | 21 | &-body { 22 | width: 90%; 23 | display: flex; 24 | flex-direction: column; 25 | align-self: center; 26 | justify-self: center; 27 | } 28 | } 29 | 30 | .logo { 31 | flex: auto; 32 | flex-direction: column; 33 | justify-content: center; 34 | display: flex; 35 | align-items: center; 36 | font-variant-ligatures: common-ligatures; 37 | padding: 1rem; 38 | // padding-top: 5rem; 39 | font-size: 4rem; 40 | } 41 | 42 | .dot, 43 | .slash { 44 | color: #0090c9; 45 | } 46 | 47 | .button-container { 48 | margin-top: 0rem; 49 | } 50 | 51 | .button { 52 | border: none; 53 | background: #00a2e4; 54 | color: white; 55 | font-weight: 400; 56 | font-size: 2rem; 57 | border-radius: 0.25rem; 58 | padding: 1rem 1.5rem; 59 | text-decoration: none; 60 | 61 | &:hover { 62 | background: #00b5ff; 63 | } 64 | 65 | &:active { 66 | background: #0088c0; 67 | } 68 | } 69 | 70 | .history { 71 | display: flex; 72 | flex-direction: column; 73 | 74 | &-intro { 75 | margin-top: 4rem; 76 | } 77 | 78 | &-list { 79 | align-self: center; 80 | margin-top: 0.5rem; 81 | max-width: min(48ch, 80%); 82 | } 83 | 84 | a { 85 | display: inline-block; 86 | opacity: 0.9; 87 | border: none; 88 | background: #0090c9; 89 | color: white; 90 | font-weight: 500; 91 | border-radius: 4px; 92 | padding: 2px 6px; 93 | text-decoration: none; 94 | white-space: nowrap; 95 | margin: 0.25rem; 96 | 97 | &:hover { 98 | opacity: 1; 99 | background: #00b5ff; 100 | } 101 | 102 | &:active { 103 | opacity: 1; 104 | background: #0088c0; 105 | } 106 | } 107 | } 108 | 109 | .footer { 110 | flex-grow: 0; 111 | font-size: 0.9rem; 112 | padding: 1rem; 113 | font-weight: 500; 114 | color: rgba(255, 255, 255, 0.75); 115 | 116 | p { 117 | margin: 0; 118 | } 119 | 120 | a { 121 | opacity: 0.9; 122 | color: #99e2ff; 123 | text-decoration: none; 124 | } 125 | 126 | a:hover { 127 | opacity: 1; 128 | // color: #00b5ff; 129 | } 130 | 131 | a:active { 132 | opacity: 1; 133 | // color: #0088c0; 134 | } 135 | } 136 | 137 | input, 138 | input::placeholder { 139 | appearance: none; 140 | border: none; 141 | background: transparent; 142 | color: #99e2ff !important; 143 | font-size: inherit; 144 | } 145 | 146 | input { 147 | max-width: 90vw !important; 148 | width: 1px; 149 | padding: 0; 150 | margin: 0; 151 | outline: 0; 152 | } 153 | 154 | input::placeholder { 155 | opacity: 0.5; 156 | } 157 | 158 | @media only screen and (max-width: 799px) { 159 | .logo { 160 | font-size: 8vw; 161 | } 162 | 163 | .link { 164 | font-size: 12vw; 165 | display: block; 166 | } 167 | 168 | .button-container { 169 | /*margin-top: 4vw;*/ 170 | 171 | margin-top: 2rem; 172 | } 173 | 174 | .button { 175 | font-size: max(1.5rem, 4vw) !important; 176 | } 177 | } 178 | 179 | .brand-icon { 180 | margin-left: 0.5rem; 181 | display: inline-block; 182 | vertical-align: middle; 183 | 184 | svg { 185 | fill: currentColor; 186 | color: inherit; 187 | width: 1rem; // auto !important; 188 | height: 1rem; 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/components/app-whitelabel.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 144 | -------------------------------------------------------------------------------- /src/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "./config" 2 | 3 | describe("config.spec", () => { 4 | it("should handle values", async () => { 5 | 6 | expect(getConfig('test abc', 'a')).toEqual('a') 7 | 8 | import.meta.env['briefing test xyz'] = 'b' 9 | expect(getConfig('TEST xyz', undefined, true)).toEqual('b') 10 | 11 | window.briefingConfig = { 12 | 'test-XYZ': 'c' 13 | } 14 | expect(getConfig('TEST xyz', undefined, true)).toEqual('c') 15 | }) 16 | }) -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { getWebsocketUrlFromLocation } from '@zerva/websocket' 2 | import { Logger, isValue } from 'zeed' 3 | import { isTrue } from './lib/base' 4 | 5 | const log = Logger('config') 6 | 7 | export const iOS = navigator?.platform?.match(/(iPhone|iPod|iPad)/i) != null 8 | // export const iPhone = navigator?.platform?.match(/(iPhone|iPod)/i) != null // Identified Phone of a native app 9 | 10 | let configNormalized: any 11 | 12 | const normalizeConfigName = (name: string) => name.toUpperCase().replace(/[-\ .]/gim, '_') 13 | 14 | /** 15 | * There are multiple ways for configuration, listed by priority: 16 | * 17 | * 1. The server evaluates its ENV variables an passes them via 18 | * URL `/briefing-config.js` to the server (loaded by `index.html` before 19 | * all other code). Accessible through `window.briefingConfig` 20 | * 2. On build the ENV variables are passed via `import.meta.env` 21 | * 3. Default values 22 | */ 23 | export function getConfig(name: string, defaultValue?: string, forceUpdate = false) { 24 | if (forceUpdate || configNormalized == null) { 25 | const configEnv = Object.entries(import.meta.env).map(([key, value]) => { 26 | key = normalizeConfigName(key) 27 | if (key.startsWith('BRIEFING_')) 28 | return [key, value] 29 | return undefined 30 | }).filter(isValue) 31 | 32 | const configWindow = Object.entries(window.briefingConfig ?? {}).map(([key, value]) => ([`BRIEFING_${normalizeConfigName(key)}`, value])) 33 | 34 | configNormalized = Object.fromEntries([ 35 | ...configEnv, 36 | ...configWindow, 37 | ]) 38 | } 39 | 40 | return (configNormalized[`BRIEFING_${normalizeConfigName(name)}`] ?? defaultValue) 41 | } 42 | 43 | // See https://github.com/holtwick/briefing-signal 44 | export const SIGNAL_SERVER_URL = getConfig( 45 | 'SIGNAL_URL', 46 | getWebsocketUrlFromLocation(), 47 | ) 48 | 49 | // getConfig('STUN_URL', 'stun:turn01.brie.fi:5349') 50 | // iceServers: [ 51 | // { 52 | // urls: [ 53 | // 'stun:stun.l.google.com:19302', 54 | // 'stun:global.stun.twilio.com:3478' 55 | // ] 56 | // } 57 | // ], 58 | // sdpSemantics: 'unified-plan' 59 | const stun = getConfig('STUN_URL', `stun:${location.hostname}:3478`) 60 | 61 | const iceServers: any = [{ urls: stun }] 62 | 63 | const turn = getConfig('TURN_URL') // , 'turn:turn01.brie.fi:5349') 64 | if (turn) { 65 | iceServers.push({ 66 | urls: turn, 67 | username: getConfig('TURN_USER', 'brie'), 68 | credential: getConfig('TURN_PASSWORD', 'fi'), 69 | }) 70 | } 71 | 72 | // See https://github.com/feross/simple-peer#peer--new-peeropts 73 | export const ICE_CONFIG = { 74 | iceTransportPolicy: 'all', 75 | reconnectTimer: 3000, 76 | 77 | // These settings are no secret, since they are readable from the client side anyway 78 | iceServers, 79 | } 80 | 81 | export const RELEASE = import.meta.env.BRIEFING_RELEASE 82 | 83 | export const SENTRY_DSN = getConfig('SENTRY_DSN') 84 | 85 | export const ROOM_PATH = getConfig('ROOM_PATH', '/') 86 | export const ROOM_URL = getConfig('ROOM_URL', `${location.protocol}//${location.host}${ROOM_PATH}`) 87 | export const ROOM_DOMAIN = getConfig('ROOM_DOMAIN', location.hostname) 88 | 89 | export const SHOW_FULLSCREEN = isTrue(getConfig('SHOW_FULLSCREEN'), true) 90 | export const SHOW_INVITATION = isTrue(getConfig('SHOW_INVITATION'), true) 91 | export const SHOW_INVITATION_HINT = isTrue(getConfig('SHOW_INVITATION_HINT'), true) 92 | export const SHOW_SETTINGS = isTrue(getConfig('SHOW_SETTINGS'), true) 93 | export const SHOW_SHARE = isTrue(getConfig('SHOW_SHARE'), true) 94 | export const SHOW_CHAT = isTrue(getConfig('SHOW_CHAT'), true) 95 | 96 | export const MUTE_AUDIO = isTrue(getConfig('MUTE_AUDIO'), false) 97 | export const MUTE_VIDEO = isTrue(getConfig('MUTE_VIDEO'), false) 98 | 99 | export const DEFAULT_ROOM = getConfig('DEFAULT_ROOM') 100 | 101 | export const LICENSE = getConfig('LICENSE', '') 102 | 103 | log.info( 104 | `Config: ${JSON.stringify( 105 | { 106 | RELEASE, 107 | ROOM_URL, 108 | ROOM_PATH, 109 | ROOM_DOMAIN, 110 | DEFAULT_ROOM, 111 | SIGNAL_SERVER_URL, 112 | ICE_CONFIG, 113 | SENTRY_DSN, 114 | SHOW_FULLSCREEN, 115 | SHOW_CHAT, 116 | SHOW_INVITATION, 117 | SHOW_INVITATION_HINT, 118 | SHOW_SETTINGS, 119 | SHOW_SHARE, 120 | LICENSE, 121 | }, 122 | null, 123 | 2, 124 | )}`, 125 | ) 126 | -------------------------------------------------------------------------------- /src/css/_html.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | @import 'variables'; 4 | @import 'reset'; 5 | 6 | html { 7 | -webkit-tap-highlight-color: transparent; 8 | } 9 | 10 | html body * { 11 | box-sizing: border-box; 12 | -webkit-text-size-adjust: none; 13 | } 14 | 15 | html *:not(input, textarea, [contenteditable]) { 16 | // Be careful with * because WebKit is sensitive for input fields 17 | user-select: none; 18 | -webkit-touch-callout: none; 19 | } 20 | 21 | html, 22 | body { 23 | height: 100%; // Required for UI layout 24 | overflow: hidden; // Do not bounce on scroll macOS 25 | margin: 0; 26 | padding: 0; 27 | // font-size: 16px; 28 | // font-family: var(--font-family); 29 | // -ms-overflow-style: -ms-autohiding-scrollbar; 30 | } 31 | 32 | body, 33 | .theme-element { 34 | font-family: var(--font-family, $body-font-family); 35 | font-size: var(--font-size, $font-size); 36 | 37 | background: var(--background); 38 | color: var(--text-color); 39 | } 40 | 41 | body { 42 | overflow-x: hidden; 43 | font-variant-ligatures: common-ligatures; // https://twitter.com/rsms/status/1094379965452185600 44 | text-rendering: optimizeLegibility; 45 | -webkit-font-smoothing: antialiased; 46 | -moz-osx-font-smoothing: grayscale; 47 | } 48 | 49 | // Optimized for East Asian CJK 50 | 51 | html:lang(zh), 52 | html:lang(zh-Hans), 53 | .lang-zh, 54 | .lang-zh-hans { 55 | font-family: $cjk-zh-hans-font-family; 56 | } 57 | 58 | html:lang(zh-Hant), 59 | .lang-zh-hant { 60 | font-family: $cjk-zh-hant-font-family; 61 | } 62 | 63 | html:lang(ja), 64 | .lang-ja { 65 | font-family: $cjk-jp-font-family; 66 | } 67 | 68 | html:lang(ko), 69 | .lang-ko { 70 | font-family: $cjk-ko-font-family; 71 | } 72 | 73 | :lang(zh), 74 | :lang(ja), 75 | .lang-cjk { 76 | ins, 77 | u { 78 | border-bottom: $border-width solid; 79 | text-decoration: none; 80 | } 81 | 82 | del + del, 83 | del + s, 84 | ins + ins, 85 | ins + u, 86 | s + del, 87 | s + s, 88 | u + ins, 89 | u + u { 90 | margin-left: 0.125em; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/css/_macros.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | // Mixins 4 | 5 | @import 'units'; 6 | 7 | @mixin selectable { 8 | // !important needs to be there because otherwise it does not propagate in WebKit! 9 | user-select: text !important; 10 | -webkit-touch-callout: initial !important; 11 | } 12 | 13 | //@mixin vscroll { 14 | // overflow: auto; 15 | // overflow-x: hidden; 16 | // overflow-y: auto; 17 | // -webkit-overflow-scrolling: touch; 18 | //} 19 | 20 | // Button variant mixin 21 | @mixin button-variant($color: $primary-color) { 22 | background: $color; 23 | border-color: darken($color, 3%); 24 | color: $light-color; 25 | &:focus { 26 | @include control-shadow($color); 27 | } 28 | &:focus, 29 | &:hover { 30 | background: darken($color, 2%); 31 | border-color: darken($color, 5%); 32 | color: $light-color; 33 | } 34 | &:active, 35 | &.active { 36 | background: darken($color, 7%); 37 | border-color: darken($color, 10%); 38 | color: $light-color; 39 | } 40 | &.loading { 41 | &::after { 42 | border-bottom-color: $light-color; 43 | border-left-color: $light-color; 44 | } 45 | } 46 | } 47 | 48 | @mixin button-outline-variant($color: $primary-color) { 49 | background: $light-color; 50 | border-color: $color; 51 | color: $color; 52 | &:focus { 53 | @include control-shadow($color); 54 | } 55 | &:focus, 56 | &:hover { 57 | background: lighten($color, 50%); 58 | border-color: darken($color, 2%); 59 | color: $color; 60 | } 61 | &:active, 62 | &.active { 63 | background: $color; 64 | border-color: darken($color, 5%); 65 | color: $light-color; 66 | } 67 | &.loading { 68 | &::after { 69 | border-bottom-color: $color; 70 | border-left-color: $color; 71 | } 72 | } 73 | } 74 | 75 | // Component focus shadow 76 | @mixin control-shadow($color: $shadow-color) { 77 | // http://help.dottoro.com/lanifqvh.php 78 | //box-shadow: 0 0 1px 3px rgba(59, 153, 252, .7); 79 | //box-shadow: 0 0 0 3px activeborder; // Blink 80 | //box-shadow: 0 0 0 3px -moz-mac-focusring; // Firefox 81 | //outline: auto 0 -webkit-focus-ring-color; // Webkit 82 | 83 | // border-color: $color; 84 | // box-shadow: 0 0 0 4px rgba($color, 0.25); 85 | // box-shadow: 0 0 0 $px-2 rgba($color, 0.25); 86 | 87 | box-shadow: var(--focus-shadow); 88 | } 89 | 90 | // Shadow mixin 91 | @mixin shadow-variant($offset) { 92 | box-shadow: 0 $offset ($offset + 0.05rem) * 2 rgba($dark-color, 0.3); 93 | } 94 | 95 | @mixin hiddenButSelectable { 96 | // Due to scroll issues on WebKit and Mozilla it should not be hidden completely, otherwise unexpected things 97 | // will happen when tabbing around 98 | 99 | // position: fixed; 100 | // width: 1px; 101 | // height: 1px; 102 | padding: 0; 103 | margin: 0; 104 | // overflow: hidden; 105 | clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ 106 | //clip: rect(1px, 1px, 1px, 1px); 107 | clip: rect(0, 0, 0, 0); 108 | border: 0; 109 | display: block; 110 | visibility: visible; 111 | white-space: nowrap; 112 | } 113 | 114 | // https://a11yproject.com/posts/how-to-hide-content/ 115 | //.visually-hidden { 116 | // position: absolute !important; 117 | // height: 1px; 118 | // width: 1px; 119 | // overflow: hidden; 120 | // clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ 121 | // clip: rect(1px, 1px, 1px, 1px); 122 | // white-space: nowrap; 123 | //} 124 | 125 | // Label base style 126 | @mixin label-base() { 127 | border-radius: $border-radius; 128 | line-height: 1.25; 129 | padding: 0.1rem 0.2rem; 130 | } 131 | 132 | @mixin label-variant($color: $light-color, $bg-color: $primary-color) { 133 | background: $bg-color; 134 | color: $color; 135 | } 136 | 137 | @mixin hyphens { 138 | -ms-word-break: break-all; 139 | word-break: break-all; 140 | word-break: break-word; 141 | -webkit-hyphens: auto; 142 | -moz-hyphens: auto; 143 | hyphens: auto; 144 | } 145 | 146 | @mixin centerContent { 147 | // display: flex; 148 | align-items: center; 149 | justify-content: center; 150 | } 151 | 152 | // Mega awesome idea https://css-tricks.com/css-custom-properties-theming/ 153 | @mixin backgroundVarLighter($name, $perc: 0.25) { 154 | background: linear-gradient( 155 | to top, 156 | rgba(255, 255, 255, $perc), 157 | rgba(255, 255, 255, $perc) 158 | ) 159 | var($name); 160 | } 161 | 162 | @mixin backgroundVarDarker($name, $perc: 0.25) { 163 | background: linear-gradient( 164 | to top, 165 | rgba(0, 0, 0, $perc), 166 | rgba(0, 0, 0, $perc) 167 | ) 168 | var($name); 169 | } 170 | 171 | @mixin backgroundVar($name, $perc: 0) { 172 | @if $perc < 0 { 173 | @include backgroundVarLighter($name, $perc * -1); 174 | } @else if $perc > 0 { 175 | @include backgroundVarDarker($name, $perc); 176 | } @else { 177 | background: var($name); 178 | } 179 | } 180 | 181 | @mixin hstack { 182 | display: flex; 183 | flex-direction: row; 184 | } 185 | 186 | @mixin vstack { 187 | display: flex; 188 | flex-direction: column; 189 | } 190 | 191 | @mixin fix { 192 | } 193 | 194 | @mixin fit { 195 | flex: auto; 196 | } 197 | 198 | $tablet-width: 768px; 199 | $desktop-width: 1024px; 200 | 201 | @mixin mediaMobile { 202 | @media (max-width: #{$tablet-width - 1px}) { 203 | @content; 204 | } 205 | } 206 | 207 | @mixin notMediaMobile { 208 | @media (min-width: #{$tablet-width}) { 209 | @content; 210 | } 211 | } 212 | 213 | @mixin mediaTablet { 214 | @media (min-width: #{$tablet-width}) and (max-width: #{$desktop-width - 1px}) { 215 | @content; 216 | } 217 | } 218 | 219 | @mixin mediaDesktop { 220 | @media (min-width: #{$desktop-width}) { 221 | @content; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/css/_reset.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | // Inspired by https://github.com/jgthms/minireset.css/blob/master/minireset.css 4 | 5 | html, 6 | body, 7 | p, 8 | ol, 9 | ul, 10 | li, 11 | dl, 12 | dt, 13 | dd, 14 | blockquote, 15 | figure, 16 | fieldset, 17 | legend, 18 | textarea, 19 | pre, 20 | iframe, 21 | hr, 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6 { 28 | margin: 0; 29 | padding: 0; 30 | } 31 | 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | font-size: 100%; 39 | font-weight: normal; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | button, 47 | input, 48 | select, 49 | textarea { 50 | font-size: inherit; 51 | font-family: inherit; 52 | margin: 0; 53 | padding: 0; 54 | outline: none; 55 | appearance: none; 56 | border: none; 57 | width: auto; 58 | min-width: 0; 59 | background: inherit; 60 | color: inherit; 61 | // display: inline-block; 62 | 63 | //&:focus { 64 | // border: inherit; // Fix a jitter in Firefox 65 | //} 66 | } 67 | 68 | a { 69 | outline: none; 70 | } 71 | 72 | [contenteditable] { 73 | outline: none; 74 | } 75 | 76 | html { 77 | box-sizing: border-box; 78 | } 79 | 80 | *, 81 | *::before, 82 | *::after { 83 | box-sizing: inherit; 84 | } 85 | 86 | img, 87 | video { 88 | height: auto; 89 | max-width: 100%; 90 | // display: inline-block; 91 | } 92 | 93 | iframe { 94 | border: 0; 95 | } 96 | 97 | table { 98 | border-collapse: collapse; 99 | border-spacing: 0; 100 | } 101 | 102 | td, 103 | th { 104 | padding: 0; 105 | } 106 | 107 | td:not([align]), 108 | th:not([align]) { 109 | text-align: left; 110 | } 111 | 112 | // WebKit search field 113 | 114 | input[type='search']::-webkit-search-decoration, 115 | input[type='search']::-webkit-search-cancel-button, 116 | input[type='search']::-webkit-search-results-button, 117 | input[type='search']::-webkit-search-results-decoration { 118 | -webkit-appearance: none; 119 | } 120 | 121 | // Mozilla dotted focus 122 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Firefox 123 | 124 | *::-moz-focus-inner { 125 | border: 0 !important; 126 | } 127 | -------------------------------------------------------------------------------- /src/css/_units.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | // Unit sizes 4 | 5 | @use 'sass:math'; 6 | 7 | $base-font-size: 16; // Only for calculations! Never set it in ! 8 | $base-font-size-px: (1px * $base-font-size); 9 | 10 | @function rem($px) { 11 | @return math.div($px, $base-font-size-px) * 1rem; 12 | } 13 | 14 | @function px($px) { 15 | @return math.div($px, $base-font-size) * 1rem; 16 | } 17 | 18 | $px-1: rem(1px); // 1/8 19 | $px-2: rem(2px); // 1/4 20 | $px-4: rem(4px); // 1/2 21 | $px-8: rem(8px); // 1 22 | $px-16: rem(16px); // 2 23 | $px-24: rem(24px); // 3 24 | $px-32: rem(32px); // 4 25 | $px-40: rem(40px); // 5 26 | $px-48: rem(48px); // 6 27 | $px-64: rem(64px); // 8 28 | $px-128: rem(16px); // 16 29 | 30 | // Unit sizes 31 | $unit-o: rem(1px); // 1px 32 | $unit-h: rem(2px); // 2px 33 | $unit-1: rem(4px); 34 | $unit-2: rem(8px); 35 | $unit-3: rem(12px); 36 | $unit-4: rem(16px); 37 | $unit-5: rem(20px); 38 | $unit-6: rem(24px); 39 | $unit-7: rem(28px); 40 | $unit-8: rem(32px); 41 | $unit-9: rem(36px); 42 | $unit-10: rem(40px); 43 | $unit-12: rem(48px); 44 | $unit-16: rem(64px); 45 | 46 | $control-width-xs: 180px !default; 47 | $control-width-sm: 320px !default; 48 | $control-width-md: 640px !default; 49 | $control-width-lg: 960px !default; 50 | $control-width-xl: 1280px !default; 51 | 52 | // Responsive breakpoints 53 | 54 | $size-xs: rem(480px) !default; 55 | $size-sm: rem(600px) !default; 56 | $size-md: rem(840px) !default; 57 | $size-lg: rem(960px) !default; 58 | $size-xl: rem(1280px) !default; 59 | $size-2x: rem(1440px) !default; 60 | 61 | $responsive-breakpoint: $size-xs !default; 62 | 63 | // z-index 64 | $z-index-0: 1 !default; 65 | $z-index-1: 100 !default; 66 | $z-index-2: 200 !default; 67 | $z-index-3: 300 !default; 68 | $z-index-4: 400 !default; 69 | $z-index-5: 500 !default; 70 | $z-index-6: 600 !default; 71 | $z-index-7: 700 !default; 72 | $z-index-8: 800 !default; 73 | $z-index-9: 900 !default; 74 | 75 | // Content > Floating > Overlay > Modal > Popover > Tooltip 76 | 77 | $z-index-floating: $z-index-1 !default; // separator-handle, knobs 78 | $z-index-overlay: $z-index-2 !default; // modal backgrounds 79 | $z-index-modal: $z-index-3 !default; // modal, dialog, page (mobile) 80 | $z-index-popover: $z-index-4 !default; // menu, dropdown, popover 81 | $z-index-tooltip: $z-index-1 !default; 82 | 83 | // font-weight 84 | $font-weight-thin: 100 !default; 85 | $font-weight-extra-light: 200 !default; 86 | $font-weight-light: 300 !default; 87 | $font-weight-normal: 400 !default; 88 | $font-weight-medium: 500 !default; 89 | $font-weight-semi-bold: 600 !default; 90 | $font-weight-bold: 700 !default; 91 | $font-weight-extra-bold: 800 !default; 92 | $font-weight-black: 900 !default; 93 | $font-weight-extra-black: 950 !default; 94 | 95 | // https://www.smashingmagazine.com/2019/12/ui-design-tips-speed-up-workflow/ 96 | 97 | $font-size-sm-2: px(12) !default; 98 | $font-size-sm-1: px(14) !default; 99 | $font-size: px(16) !default; 100 | $font-size-lg-1: px(18) !default; 101 | $font-size-lg-2: px(20) !default; 102 | $font-size-lg-3: px(24) !default; 103 | $font-size-lg-4: px(30) !default; 104 | $font-size-lg-5: px(36) !default; 105 | $font-size-lg-6: px(48) !default; 106 | $font-size-lg-7: px(60) !default; 107 | $font-size-lg-8: px(72) !default; 108 | 109 | $line-height: px(24) !default; 110 | $line-height-bigger: px(28) !default; 111 | 112 | $safe-top: env(safe-area-inset-top, 0); 113 | $safe-right: env(safe-area-inset-right, 0); 114 | $safe-bottom: env(safe-area-inset-bottom, 0); 115 | $safe-left: env(safe-area-inset-left, 0); 116 | -------------------------------------------------------------------------------- /src/css/_variables.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | @import 'units'; 4 | @import 'macros'; 5 | 6 | // Core features 7 | 8 | $rtl: false !default; 9 | 10 | $blueSecondary: rgba(43, 184, 255, 1); 11 | $blue: rgba(10, 124, 179, 1); 12 | $orange: rgba(255, 161, 43, 1); 13 | $orangeSecondary: rgba(255, 172, 69, 1); 14 | 15 | $primary-color: $blue !default; // #5755d9 #FFBD33 16 | $alt-color: $orange; 17 | 18 | $dark-color: #333333 !default; 19 | $light-color: #ffffff !default; 20 | 21 | // https://css-tricks.com/snippets/sass/tint-shade-functions/ 22 | 23 | $shade-dark: black; 24 | $tint-light: white; 25 | 26 | @function tint($color, $percentage) { 27 | @return mix($tint-light, $color, $percentage); 28 | } 29 | 30 | @function shade($color, $percentage) { 31 | @return mix($shade-dark, $color, $percentage); 32 | } 33 | 34 | @function swatch($color, $h) { 35 | @if $h >= 900 { 36 | @return mix($shade-dark, $color, 80%); 37 | } @else if $h >= 800 { 38 | @return mix($shade-dark, $color, 60%); 39 | } @else if $h >= 700 { 40 | @return mix($shade-dark, $color, 35%); // 40 41 | } @else if $h >= 600 { 42 | @return mix($shade-dark, $color, 15%); // 20 43 | } @else if $h >= 500 { 44 | @return $color; 45 | } @else if $h >= 400 { 46 | @return mix($tint-light, $color, 20%); 47 | } @else if $h >= 300 { 48 | @return mix($tint-light, $color, 50%); // 40 49 | } @else if $h >= 200 { 50 | @return mix($tint-light, $color, 80%); 51 | } @else if $h >= 100 { 52 | @return mix($tint-light, $color, 90%); // 90 53 | } 54 | } 55 | 56 | // Gray colors 57 | 58 | $gray-color: lighten($dark-color, 55%) !default; 59 | $gray-color-dark: darken($gray-color, 30%) !default; 60 | $gray-color-light: lighten($gray-color, 20%) !default; 61 | 62 | $border-color: lighten($dark-color, 65%) !default; 63 | $border-color-dark: darken($border-color, 10%) !default; 64 | $border-color-light: lighten($border-color, 8%) !default; 65 | 66 | $bg-color: lighten($dark-color, 75%) !default; 67 | $bg-color-dark: darken($bg-color, 3%) !default; 68 | $bg-color-light: $light-color !default; 69 | 70 | // Control colors https://colors.eva.design/ 71 | 72 | $success-color: #32b643 !default; 73 | $warning-color: #ffb700 !default; 74 | $error-color: #e85600 !default; 75 | 76 | // Other colors 77 | 78 | $code-color: #d73e48 !default; 79 | $highlight-color: #ffe9b3 !default; 80 | 81 | $body-bg: #272727 !default; 82 | $body-font-color: #eee !default; 83 | 84 | $link-color: $primary-color !default; 85 | $link-color-dark: darken($link-color, 10%) !default; 86 | $link-color-light: lighten($link-color, 10%) !default; 87 | 88 | // Sizes 89 | 90 | $layout-spacing: $px-8 !default; 91 | $layout-spacing-sm: $px-4 !default; 92 | $layout-spacing-lg: $px-16 !default; 93 | 94 | $border-radius: 0 !default; 95 | $border-width: $px-1 !default; 96 | $border-width-lg: $px-2 !default; 97 | 98 | $control-size: $px-40 !default; 99 | $control-size-sm: $px-24 !default; 100 | $control-size-lg: $px-64 !default; 101 | 102 | $control-spacing: $control-size * 0.2 * 3; 103 | 104 | $control-padding-x: $px-8 !default; 105 | $control-padding-x-sm: $px-4 !default; 106 | $control-padding-x-lg: $px-16 !default; 107 | $control-padding-y: ($control-size - $line-height) * 0.5 - $border-width !default; 108 | $control-padding-y-sm: ($control-size-sm - $line-height) * 0.5 - $border-width !default; 109 | $control-padding-y-lg: ($control-size-lg - $line-height) * 0.5 - $border-width !default; 110 | $control-icon-size: 0.8rem !default; 111 | 112 | // sea 113 | 114 | $shadow-color: $primary-color; 115 | $control-spacing: $px-24; // $control-size / 5 * 3; 116 | 117 | // --------------------- 118 | 119 | // Fonts 120 | 121 | // Credit: https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/ 122 | $base-font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 123 | Roboto !default; 124 | $mono-font-family: 'SF Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Courier, 125 | monospace !default; 126 | $fallback-font-family: 'Helvetica Neue', sans-serif !default; 127 | $cjk-zh-hans-font-family: $base-font-family, 'PingFang SC', 'Hiragino Sans GB', 128 | 'Microsoft YaHei', $fallback-font-family !default; 129 | $cjk-zh-hant-font-family: $base-font-family, 'PingFang TC', 'Hiragino Sans CNS', 130 | 'Microsoft JhengHei', $fallback-font-family !default; 131 | $cjk-jp-font-family: $base-font-family, 'Hiragino Sans', 132 | 'Hiragino Kaku Gothic Pro', 'Yu Gothic', YuGothic, Meiryo, 133 | $fallback-font-family !default; 134 | $cjk-ko-font-family: $base-font-family, 'Malgun Gothic', $fallback-font-family !default; 135 | $body-font-family: $base-font-family, $fallback-font-family !default; 136 | 137 | // Font sizes 138 | 139 | //$html-font-size: 16px !default; 140 | $html-line-height: 1.5 !default; 141 | 142 | $font-size: 1rem !default; 143 | $font-size-sm: px(14) !default; 144 | $font-size-lg: px(24) !default; 145 | 146 | $line-height: px(20) !default; 147 | 148 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .app { 4 | color: var(--text-color); 5 | background: var(--background); 6 | 7 | padding: $safe-top $safe-right $safe-bottom $safe-left; 8 | 9 | &:before { 10 | position: fixed; 11 | z-index: 99999; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | height: $safe-top; 16 | background: var(--statusbar-color, rgba(255, 255, 255, 0.25)); 17 | content: ''; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/css/index.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | // units, macros 4 | @import 'variables'; 5 | 6 | // reset 7 | @import 'html'; 8 | 9 | @import 'app'; 10 | @import 'stack'; 11 | @import 'text'; 12 | @import 'forms'; 13 | 14 | @import 'sea-button'; 15 | 16 | @import 'briefing'; 17 | 18 | #app { 19 | height: 100%; 20 | } 21 | 22 | .debug { 23 | position: fixed; 24 | width: 2rem; 25 | height: 2rem; 26 | bottom: 1rem; 27 | right: 1rem; 28 | background-color: red; 29 | overflow: hidden; 30 | border: 0.5rem red solid; 31 | z-index: 99999; 32 | 33 | .debug-content { 34 | display: none; 35 | padding: 1rem; 36 | } 37 | 38 | &:hover, 39 | &:active, 40 | &.-show { 41 | top: 1rem; 42 | left: 1rem; 43 | width: auto; // calc(100% - 2rem); 44 | height: auto; // calc(100% - 2rem); 45 | bottom: 1rem; 46 | right: 1rem; 47 | overflow: auto; 48 | background: white; 49 | 50 | .debug-content { 51 | display: block; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/css/layout-default.scss: -------------------------------------------------------------------------------- 1 | @media (orientation: landscape) { 2 | .videos { 3 | flex-direction: row !important; 4 | 5 | // https://stackoverflow.com/a/12198561/140927 6 | .item:first-child:nth-last-child(1) { 7 | min-height: 100%; 8 | width: 100%; 9 | } 10 | 11 | .item:first-child:nth-last-child(2), 12 | .item:first-child:nth-last-child(2) ~ .item { 13 | min-height: 100%; 14 | width: 50%; 15 | } 16 | 17 | .item:first-child:nth-last-child(3), 18 | .item:first-child:nth-last-child(3) ~ .item { 19 | min-height: 100%; 20 | width: 33.3333%; 21 | } 22 | 23 | .item:first-child:nth-last-child(4), 24 | .item:first-child:nth-last-child(4) ~ .item { 25 | min-height: 50%; 26 | width: 50%; 27 | } 28 | 29 | .item:first-child:nth-last-child(5), 30 | .item:first-child:nth-last-child(5) ~ .item, 31 | .item:first-child:nth-last-child(6), 32 | .item:first-child:nth-last-child(6) ~ .item { 33 | min-height: 50%; 34 | width: 33.3333%; 35 | } 36 | 37 | .item:first-child:nth-last-child(7), 38 | .item:first-child:nth-last-child(7) ~ .item, 39 | .item:first-child:nth-last-child(8), 40 | .item:first-child:nth-last-child(8) ~ .item { 41 | min-height: 50%; 42 | width: 25%; 43 | } 44 | 45 | .item:first-child:nth-last-child(9), 46 | .item:first-child:nth-last-child(9) ~ .item { 47 | min-height: 33.3333%; 48 | width: 33.3333%; 49 | } 50 | 51 | .item:first-child:nth-last-child(10), 52 | .item:first-child:nth-last-child(10) ~ .item, 53 | .item:first-child:nth-last-child(11), 54 | .item:first-child:nth-last-child(11) ~ .item, 55 | .item:first-child:nth-last-child(12), 56 | .item:first-child:nth-last-child(12) ~ .item { 57 | min-height: 33.3333%; 58 | width: 25%; 59 | } 60 | } 61 | } 62 | 63 | @media (orientation: portrait) { 64 | .videos { 65 | flex-direction: column !important; 66 | 67 | .item:first-child:nth-last-child(1) { 68 | width: 100%; 69 | height: 100%; 70 | } 71 | 72 | .item:first-child:nth-last-child(2), 73 | .item:first-child:nth-last-child(2) ~ .item { 74 | width: 100%; 75 | height: 50%; 76 | } 77 | 78 | .item:first-child:nth-last-child(3), 79 | .item:first-child:nth-last-child(3) ~ .item { 80 | width: 100%; 81 | height: 33.3333%; 82 | } 83 | 84 | .item:first-child:nth-last-child(4), 85 | .item:first-child:nth-last-child(4) ~ .item { 86 | width: 50%; 87 | height: 50%; 88 | } 89 | 90 | .item:first-child:nth-last-child(5), 91 | .item:first-child:nth-last-child(5) ~ .item, 92 | .item:first-child:nth-last-child(6), 93 | .item:first-child:nth-last-child(6) ~ .item { 94 | width: 50%; 95 | height: 33.3333%; 96 | } 97 | 98 | .item:first-child:nth-last-child(7), 99 | .item:first-child:nth-last-child(7) ~ .item, 100 | .item:first-child:nth-last-child(8), 101 | .item:first-child:nth-last-child(8) ~ .item { 102 | width: 50%; 103 | height: 25%; 104 | } 105 | 106 | .item:first-child:nth-last-child(9), 107 | .item:first-child:nth-last-child(9) ~ .item { 108 | width: 33.3333%; 109 | height: 33.3333%; 110 | } 111 | 112 | .item:first-child:nth-last-child(10), 113 | .item:first-child:nth-last-child(10) ~ .item, 114 | .item:first-child:nth-last-child(11), 115 | .item:first-child:nth-last-child(11) ~ .item, 116 | .item:first-child:nth-last-child(12), 117 | .item:first-child:nth-last-child(12) ~ .item { 118 | width: 33.3333%; 119 | height: 25%; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/css/layout-maximized.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .videos { 4 | position: relative; 5 | display: flex; 6 | flex-wrap: nowrap; 7 | align-items: flex-end; 8 | justify-content: flex-end; 9 | padding: 1rem; 10 | 11 | .peer { 12 | z-index: 2; 13 | 14 | &.-maximized { 15 | position: absolute; 16 | top: 0.25rem; 17 | left: 0.25rem; 18 | bottom: 0; 19 | right: 0.25rem; 20 | z-index: 1; 21 | } 22 | 23 | &:not(.-maximized) { 24 | height: 6rem; 25 | width: 6rem; 26 | flex-grow: 0; 27 | flex-shrink: 0; 28 | margin: 0; 29 | 30 | .video { 31 | border: 0.5px solid #272727; 32 | background: #272727; 33 | } 34 | } 35 | } 36 | } 37 | 38 | @media (orientation: landscape) { 39 | .videos { 40 | flex-direction: column; 41 | //align-items: flex-end; 42 | //justify-content: flex-end; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/css/stack.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | @import 'variables'; 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | margin: 0; 9 | } 10 | 11 | * { 12 | // flex:auto is usually the default, but for our purposes `none` fits better 13 | flex: none; 14 | } 15 | 16 | .app { 17 | // If a first level child of body is a stack it has to consume the 18 | // full height, otherwise content will grow outside of view 19 | height: 100%; 20 | } 21 | 22 | .-relative { 23 | position: relative; 24 | } 25 | 26 | .-absolute { 27 | position: absolute; 28 | } 29 | 30 | // https://css-tricks.com/snippets/css/a-guide-to-flexbox/ 31 | .stack, 32 | .hstack, 33 | .vstack { 34 | display: flex; 35 | 36 | // Layout should not be affected by too big elements 37 | // overflow: hidden; 38 | 39 | // &, > * {} 40 | 41 | // > .-collapsed {} 42 | // > .-fix {} 43 | 44 | > .-fit, 45 | > .-fill, 46 | > .-grow { 47 | flex: auto; 48 | overflow: hidden; // Important to get the scrollable exceeding contents to work 49 | } 50 | 51 | // A thin line, can be used for resizing as well with sea-separator 52 | > .stack-separator, 53 | > .-separator { 54 | background: var(--separator-color); 55 | 56 | &.handle { 57 | position: relative; 58 | cursor: col-resize; 59 | overflow: visible; 60 | 61 | $sepHandleSize: $px-8; 62 | 63 | &:after { 64 | display: flex; 65 | position: absolute; 66 | opacity: 0.5; 67 | top: 0; 68 | left: -($sepHandleSize * 0.5); 69 | width: $sepHandleSize + $px-1; 70 | height: 100%; 71 | z-index: $z-index-floating; 72 | content: ' '; 73 | cursor: col-resize; 74 | } 75 | 76 | &:hover:after { 77 | background: var(--separator-focus-color); 78 | } 79 | 80 | &.invisible { 81 | width: 0; 82 | min-width: 0; 83 | 84 | &:after { 85 | width: $sepHandleSize; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | .-scrollable, 93 | .-scroll { 94 | // https://css-tricks.com/popping-hidden-overflow/ 95 | position: static !important; 96 | overflow: auto !important; 97 | overflow-x: hidden !important; 98 | overflow-y: auto !important; 99 | // -webkit-overflow-scrolling: touch !important; 100 | // !important is required to override -fix overflow:hidden 101 | } 102 | 103 | .-content { 104 | padding: $px-16; 105 | } 106 | 107 | .hstack, 108 | .stack.-horizontal, 109 | .stack.-orientation-horizontal { 110 | flex-direction: row; 111 | 112 | > .-collapsed { 113 | width: 0; 114 | max-width: 0; 115 | // height: 100%; 116 | } 117 | 118 | > .stack-separator, 119 | > .-separator { 120 | min-width: 1px; 121 | width: $px-1; 122 | } 123 | 124 | &.-space > * { 125 | margin-right: $px-8; 126 | 127 | &:last-child { 128 | margin-right: 0; 129 | } 130 | } 131 | } 132 | 133 | .vstack, 134 | .stack.-vertical, 135 | .stack.-orientation-vertical { 136 | flex-direction: column; 137 | 138 | > .-collapsed { 139 | height: 0; 140 | max-height: 0; 141 | } 142 | 143 | > .stack-separator, 144 | > .-separator { 145 | min-height: 1px; 146 | height: $px-1; 147 | } 148 | 149 | &.-space > * { 150 | margin-bottom: $px-8; 151 | 152 | &:last-child { 153 | margin-bottom: 0; 154 | } 155 | } 156 | } 157 | 158 | .-content-center { 159 | @include centerContent; 160 | } 161 | 162 | // https://stackoverflow.com/a/22218694/140927 163 | .placeholder, 164 | .-content-placeholder { 165 | display: flex; 166 | flex-direction: column; 167 | width: 100%; 168 | height: 100%; 169 | @include centerContent; 170 | color: var(--secondary-color, #cccccc); 171 | text-align: center; 172 | padding: $px-16; 173 | } 174 | 175 | .-selectable { 176 | user-select: text; 177 | } 178 | 179 | .text { 180 | flex-shrink: 0; 181 | // padding: px(16); 182 | } 183 | -------------------------------------------------------------------------------- /src/css/text.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | @import 'variables'; 4 | 5 | .text { 6 | // Typography 7 | // Headings 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | color: inherit; 15 | font-weight: 500; 16 | line-height: 1.2; 17 | margin-bottom: 0.5em; 18 | margin-top: 0; 19 | } 20 | 21 | .h1, 22 | .h2, 23 | .h3, 24 | .h4, 25 | .h5, 26 | .h6 { 27 | font-weight: 500; 28 | } 29 | 30 | h1, 31 | .h1 { 32 | font-size: 2rem; 33 | } 34 | 35 | h2, 36 | .h2 { 37 | font-size: 1.6rem; 38 | } 39 | 40 | h3, 41 | .h3 { 42 | font-size: 1.4rem; 43 | } 44 | 45 | h4, 46 | .h4 { 47 | font-size: 1.2rem; 48 | } 49 | 50 | h5, 51 | .h5 { 52 | font-size: 1rem; 53 | } 54 | 55 | h6, 56 | .h6 { 57 | font-size: 0.8rem; 58 | } 59 | 60 | // Paragraphs 61 | p, 62 | .-text-p { 63 | margin: 0 0 $line-height; 64 | } 65 | 66 | // Semantic text elements 67 | a, 68 | ins, 69 | u { 70 | text-decoration-skip: ink edges; 71 | } 72 | 73 | a { 74 | color: var(--link-color); 75 | 76 | &:hover { 77 | color: var(--link-hover-color, var(--alt-color)); 78 | } 79 | 80 | &:active { 81 | color: var(--link-active-color, var(--link-color)); 82 | } 83 | } 84 | 85 | abbr[title] { 86 | border-bottom: $border-width dotted; 87 | cursor: help; 88 | text-decoration: none; 89 | } 90 | 91 | kbd, 92 | tt { 93 | color: var(--code-foreground); 94 | background: var(--code-background, var(--secondary-background)); 95 | //font-size: $font-size-sm; 96 | padding-top: 0; 97 | padding-right: $px-2; 98 | padding-left: $px-2; 99 | padding-bottom: $px-1; 100 | border-radius: $px-2; 101 | } 102 | 103 | mark { 104 | color: var(--highlight-foreground); 105 | background: var(--highlight-background); 106 | padding-top: 0; 107 | padding-right: $px-2; 108 | padding-left: $px-2; 109 | padding-bottom: $px-1; 110 | border-radius: $px-2; 111 | } 112 | 113 | // Blockquote 114 | blockquote { 115 | border-left: $border-width-lg solid $border-color; 116 | margin-left: 0; 117 | padding: $unit-2 $unit-4; 118 | 119 | p:last-child { 120 | margin-bottom: 0; 121 | } 122 | } 123 | 124 | // Lists 125 | ul, 126 | ol { 127 | margin: $unit-4 0 $unit-4 $unit-4; 128 | padding: 0; 129 | 130 | ul, 131 | ol { 132 | margin: $unit-4 0 $unit-4 $unit-4; 133 | } 134 | 135 | li { 136 | margin-top: $unit-2; 137 | } 138 | } 139 | 140 | ul { 141 | list-style: disc inside; 142 | 143 | ul { 144 | list-style-type: circle; 145 | } 146 | } 147 | 148 | ol { 149 | list-style: decimal inside; 150 | 151 | ol { 152 | list-style-type: lower-alpha; 153 | } 154 | } 155 | 156 | dl { 157 | dt { 158 | font-weight: bold; 159 | } 160 | 161 | dd { 162 | margin: $unit-2 0 $unit-4 0; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | import { computed, ref } from 'vue' 3 | import { createI18n } from 'vue-i18n' 4 | import { Logger, arraySorted, cloneObject, objectMap } from 'zeed' 5 | 6 | const log = Logger('i18n') 7 | 8 | const initialLocale = globalThis.isNodeTestEnv ? 'en' : (localStorage.getItem('locale') ?? '') 9 | 10 | const messages = objectMap( 11 | import.meta.glob('../locales/*.json', { eager: true }), 12 | (path, component) => { 13 | const name = path.split('/').pop()?.slice(0, -5) 14 | return [name, cloneObject((component as any).default)] 15 | }, 16 | ) 17 | 18 | export const supportedLocales = arraySorted(Object.keys(messages)) 19 | 20 | export const i18n = createI18n({ 21 | locale: initialLocale || navigator?.language?.slice(0, 2), 22 | // globalInjection: true, 23 | fallbackLocale: 'en', 24 | messages, 25 | legacy: false, 26 | }) as any 27 | 28 | log(`locale=${initialLocale || navigator?.language?.slice(0, 2)}, supportedLocales=${supportedLocales}`) 29 | 30 | /** Shortcut to $t */ 31 | export const t = i18n?.global?.t ? i18n.global.t.bind(i18n.global) : (s: string) => s 32 | 33 | // Locale 34 | 35 | function setLocale(locale: string) { 36 | log('setLocale to', locale) 37 | i18n.global.locale.value = locale || navigator?.language?.slice(0, 2) 38 | localStorage.setItem('locale', locale) 39 | } 40 | 41 | export const locale = computed({ 42 | get: () => i18n.global.locale.value, 43 | set(value) { 44 | setLocale(value) 45 | }, 46 | }) 47 | 48 | export const _languageTag = globalThis.isNodeTestEnv ? ref('') : useLocalStorage('languageTag', '') 49 | 50 | export const languageTag = computed({ 51 | get: () => _languageTag.value, 52 | set(value) { 53 | try { 54 | 'a'.localeCompare('b', value) 55 | _languageTag.value = value 56 | } 57 | catch (err) { 58 | log.warn(`invalid languageTag: ${value}`, err) 59 | _languageTag.value = '' 60 | } 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /src/lib/base.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'zeed' 2 | import { trackSilentException } from '../bugs' 3 | 4 | const log = Logger('base') 5 | 6 | const replacer = (key, value) => 7 | (value instanceof Object && !(Array.isArray(value))) 8 | ? Object.keys(value) 9 | .sort() 10 | .filter(key => value[key] != null) // Remove null and undefined 11 | .reduce((sorted, key) => { 12 | // Sorted copy 13 | sorted[key] = value[key] 14 | return sorted 15 | }, {}) 16 | : value 17 | 18 | // https://gist.github.com/davidfurlong/463a83a33b70a3b6618e97ec9679e490 19 | export function JSONSortedStringify(obj: any, indent = 2) { 20 | return JSON.stringify(obj, replacer, indent) 21 | } 22 | 23 | export function objectSnapshot(obj: any) { 24 | return JSON.stringify(obj, replacer) 25 | } 26 | 27 | export function cloneObject(obj) { 28 | try { 29 | if (typeof obj === 'object') 30 | return JSON.parse(JSON.stringify(obj)) 31 | 32 | return obj 33 | } 34 | catch (err) { 35 | trackSilentException(err) 36 | log('cloneObject error:', err) 37 | } 38 | return null 39 | } 40 | 41 | export function mergeDeep(target: any, source: any) { 42 | const isObject = obj => obj && typeof obj === 'object' 43 | 44 | if (!isObject(target) || !isObject(source)) 45 | return source 46 | 47 | Object.keys(source).forEach((key) => { 48 | const targetValue = target[key] 49 | const sourceValue = source[key] 50 | 51 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) 52 | target[key] = targetValue.concat(sourceValue) 53 | else if (isObject(targetValue) && isObject(sourceValue)) 54 | target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue) 55 | else 56 | target[key] = sourceValue 57 | }) 58 | 59 | return target 60 | } 61 | 62 | export function isTrue(value: any, dflt = false) { 63 | if (value == null) 64 | return dflt 65 | return ['1', 'true', 'yes'].includes(value.toString().toLocaleLowerCase()) 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/base64.ts: -------------------------------------------------------------------------------- 1 | export function urlBase64ToUint8Array(base64String) { 2 | try { 3 | const padding = '='.repeat((4 - (base64String.length % 4)) % 4) 4 | const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') 5 | 6 | const rawData = window.atob(base64) 7 | const outputArray = new Uint8Array(rawData.length) 8 | 9 | for (let i = 0; i < rawData.length; ++i) 10 | outputArray[i] = rawData.charCodeAt(i) 11 | 12 | return outputArray 13 | } 14 | catch (err) { 15 | console.error('Exception:', err, base64String) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/history.ts: -------------------------------------------------------------------------------- 1 | import { Logger, arrayRemoveElement } from 'zeed' 2 | const log = Logger('history') 3 | 4 | const storageKeyHistory = 'briefingHistory' 5 | 6 | /** Get all previously visited rooms */ 7 | export function historyAllRooms(): string[] { 8 | let rooms = [] 9 | try { 10 | const roomsString = localStorage.getItem(storageKeyHistory) 11 | if (roomsString) 12 | rooms = JSON.parse(roomsString) 13 | } 14 | catch (err) { 15 | log.warn('Failed to get room history') 16 | } 17 | return rooms 18 | } 19 | 20 | /** Add room to history */ 21 | export function historyAddRoom(room: string) { 22 | let rooms = historyAllRooms() 23 | rooms = arrayRemoveElement(rooms, room) 24 | rooms.unshift(room) 25 | localStorage.setItem(storageKeyHistory, JSON.stringify(rooms)) 26 | } 27 | 28 | export function historyClearRooms() { 29 | localStorage.removeItem(storageKeyHistory) 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/iframe.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'zeed' 2 | 3 | const log = Logger('app:iframe') 4 | 5 | const source = 'briefing' 6 | 7 | export function postMessageToParent(name: string, data = {}) { 8 | try { 9 | const info = { 10 | source, 11 | name, 12 | data, 13 | } 14 | log('postMessageToParent', info) 15 | window.parent.postMessage(info) 16 | } 17 | catch (err) { 18 | log('postMessageToParent error', err) 19 | } 20 | } 21 | 22 | export function onMessageFromFrame(name: string, fn: (data: any) => void) { 23 | window.addEventListener('message', (e) => { 24 | const info = e.data 25 | log('onMessageFromFrame', info) 26 | if (info.source === source && info.name === name) 27 | fn(info.data) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/link-external.ts: -------------------------------------------------------------------------------- 1 | export function openExternalLink(event: any) { 2 | if (window.electron) { 3 | let href: string 4 | if (typeof event === 'string') { 5 | href = event 6 | } 7 | else { 8 | let target = event?.target 9 | while (target && target?.href == null) 10 | target = target.parentElement 11 | 12 | href = target?.href 13 | } 14 | // log.info('Open external link', event.target) 15 | if (href) 16 | window.electron.shell.openExternal(href) 17 | 18 | event.preventDefault() 19 | return false 20 | } 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/local.spec.ts: -------------------------------------------------------------------------------- 1 | import { clearLocal, getLocal } from './local' 2 | 3 | describe('Local', () => { 4 | it('should store data', () => { 5 | clearLocal() 6 | expect(getLocal('a') == null).toBe(true) 7 | expect(getLocal('a', (name) => name)).toEqual('a') 8 | expect(getLocal('a')).toEqual('a') 9 | expect(getLocal('a', 'b')).toEqual('a') 10 | expect(getLocal('b', 'b')).toEqual('b') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/lib/local.ts: -------------------------------------------------------------------------------- 1 | export function getLocal(name: string, dflt?: string | ((name: string) => string)) { 2 | let value = localStorage?.getItem(name) 3 | if (value == null) { 4 | if (dflt != null) { 5 | if (dflt instanceof Function) 6 | value = dflt(name) 7 | else 8 | value = dflt 9 | } 10 | if (value != null) 11 | localStorage?.setItem(name, value) 12 | } 13 | return value 14 | } 15 | 16 | export function setLocal(name, value) { 17 | localStorage?.setItem(name, value) 18 | } 19 | 20 | export function clearLocal() { 21 | localStorage?.clear() 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/messages.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from 'zeed' 2 | 3 | export const messages = new Emitter() 4 | -------------------------------------------------------------------------------- /src/lib/names.spec.ts: -------------------------------------------------------------------------------- 1 | import { normalizeName } from './names' 2 | 3 | describe('Names', () => { 4 | it('should normalize', () => { 5 | let sample = '  ärger--macht/nur der 123Casanova!==' 6 | expect(normalizeName(sample)).toEqual('arger-macht-nur-der-123casanova') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/lib/names.ts: -------------------------------------------------------------------------------- 1 | // Thanks to Thomas Konings for this wonderful name generator 2 | // See https://gist.github.com/tkon99/4c98af713acc73bed74c 3 | 4 | import { deburr } from 'lodash' 5 | import { ADJECTIVES, NOUNS } from './names-const' 6 | 7 | // Alternative solutions https://stackoverflow.com/a/37511463/140927 8 | export function normalizeName(name: string) { 9 | return deburr(name) 10 | .toLowerCase() 11 | .split(/[^a-z0-9]+/gim) 12 | .filter((s: string) => s.length > 0) 13 | .join('-') 14 | } 15 | 16 | export function generateName() { 17 | // function capFirst(string) { 18 | // return string.charAt(0).toUpperCase() + string.slice(1) 19 | // } 20 | 21 | function getRandomInt(min: number, max: number) { 22 | return Math.floor(Math.random() * (max - min)) + min 23 | } 24 | 25 | return (`${ADJECTIVES[getRandomInt(0, ADJECTIVES.length - 1)]}-${NOUNS[getRandomInt(0, NOUNS.length - 1)]}-${getRandomInt(1, 99)}`) 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/share.ts: -------------------------------------------------------------------------------- 1 | import clipboardCopy from 'clipboard-copy' 2 | import { Logger } from 'zeed' 3 | import { ROOM_URL } from '../config' 4 | 5 | const log = Logger('share') 6 | 7 | export function createLinkForRoom(room: string) { 8 | return `${ROOM_URL}${room}#${room}` 9 | } 10 | 11 | export const canShare = navigator.share != null 12 | export const canCopy = !canShare 13 | 14 | export async function shareLink( 15 | url: string, 16 | { 17 | title = 'Briefing URL', 18 | text = 'Please open the link in your browser to join the video conference', 19 | } = {}, 20 | ) { 21 | if (navigator.share) { 22 | log('nav share') 23 | try { 24 | await navigator.share({ 25 | title, 26 | text, 27 | url, 28 | }) 29 | return true 30 | } 31 | catch (err) { 32 | log.warn(err) 33 | // trackException(err) 34 | } 35 | } 36 | else if (window.electron) { 37 | log('electron') 38 | try { 39 | // https://electronjs.org/docs/api/clipboard 40 | await window.electron.clipboard.writeText(url) 41 | // eslint-disable-next-line no-alert 42 | alert('The URL has been copied to your clipboard.') 43 | return true 44 | } 45 | catch (err) { 46 | log.warn(err) 47 | // trackException(err) 48 | } 49 | } 50 | else { 51 | log('copy clipboard') 52 | try { 53 | await clipboardCopy(url) 54 | // eslint-disable-next-line no-alert 55 | alert('The URL has been copied to your clipboard.') 56 | return true 57 | } 58 | catch (err) { 59 | // eslint-disable-next-line no-alert 60 | alert(`Cannot copy ${url}. Please do by hand.`) 61 | // trackException(err) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/logic/connection.ts: -------------------------------------------------------------------------------- 1 | // https://webrtchacks.com/limit-webrtc-bandwidth-sdp/ 2 | 3 | import { Logger, useDispose } from 'zeed' 4 | import { ICE_CONFIG } from '../config' 5 | import { cloneObject } from '../lib/base' 6 | import { messages } from '../lib/messages' 7 | import { WebRTC } from './webrtc' 8 | 9 | // import { removeBandwidthRestriction, setMediaBitrate } from './sdp-manipulation' 10 | 11 | const log = Logger('app:connection') 12 | 13 | export async function setupWebRTC(state) { 14 | if (!WebRTC.isSupported()) 15 | return null 16 | 17 | const dispose = useDispose(log) 18 | 19 | const config = ICE_CONFIG 20 | 21 | const webrtc = new WebRTC({ 22 | room: state.room, 23 | peerSettings: { 24 | trickle: true, 25 | // sdpTransform: (sdp) => { 26 | // log('sdpTransform', state.bandwidth) // , sdp) 27 | // let newSDP = sdp 28 | // if (state.bandwidth) { 29 | // // newSDP = updateBandwidthRestriction(sdp, 10) 30 | // // log('Old SDP', newSDP) 31 | // newSDP = setMediaBitrate(newSDP, 'video', 233) 32 | // newSDP = setMediaBitrate(newSDP, 'audio', 80) 33 | // // log('New SDP', newSDP) 34 | // } 35 | // else { 36 | // newSDP = removeBandwidthRestriction(sdp) 37 | // } 38 | // return newSDP 39 | // }, 40 | config, 41 | }, 42 | }) 43 | 44 | dispose.add(webrtc) 45 | 46 | webrtc.on('status', (info) => { 47 | log('status', info.status) 48 | // hack somehow Vue doesn't like the real WebRtcPeer object any more 49 | const status = info.status.map((p) => { 50 | const pp = cloneObject(p) 51 | pp.peer.stream = p.peer.stream 52 | return pp 53 | }) 54 | state.status = status 55 | }) 56 | 57 | webrtc.on('connected', ({ peer }) => { 58 | log('connected', peer) 59 | if (state.stream) 60 | peer.setStream(state.stream) 61 | 62 | messages.emit('requestUserInfo') 63 | }) 64 | 65 | // Getting Client's Info with Local Peer Info 66 | webrtc.on('userInfoWithPeer', ({ peer, data }) => { 67 | webrtc.send('userInfoUpdate', { peer, data }) 68 | }) 69 | 70 | // Listening to Remote Client's Info with its Local Peer Info and 71 | // emitting to Local Client 72 | webrtc.on('userInfoUpdate', ({ peer, data }) => { 73 | messages.emit('userInfoUpdate', { peer, data }) 74 | }) 75 | 76 | // Listening to new messages from Remote Client and emitting to Local client 77 | webrtc.on('chatMessage', (info) => { 78 | messages.emit('newMessage', info) 79 | }) 80 | 81 | dispose.add(messages.on('setLocalStream', (stream) => { 82 | webrtc.forEachPeer((peer) => { 83 | peer.setStream(stream) 84 | }) 85 | })) 86 | 87 | dispose.add(messages.on('negotiateBandwidth', (_stream) => { 88 | webrtc.forEachPeer((peer) => { 89 | peer.peer.negotiate() 90 | }) 91 | })) 92 | 93 | // Send a new message to all peers 94 | dispose.add(messages.on('chatMessage', ({ name, message, time }) => { 95 | webrtc.send('chatMessage', { name, message, time }) 96 | })) 97 | 98 | // Listen to local userInfo and emit to webrtc for getting peer info 99 | dispose.add(messages.on('userInfo', (data) => { 100 | webrtc.emit('userInfo', { data }) 101 | })) 102 | 103 | // dispose.add(messages.on('subscribePush', async (_on) => { 104 | // const add = state.subscription 105 | // const registration = await navigator.serviceWorker.getRegistration() 106 | // let subscription = await registration.pushManager.getSubscription() 107 | // const vapidPublicKey = state.vapidPublicKey 108 | // if (!subscription && vapidPublicKey) { 109 | // const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey) 110 | // // @ts-expect-error todo 111 | // subscription = registration.pushManager.subscribe({ 112 | // userVisibleOnly: true, 113 | // applicationServerKey, 114 | // }) 115 | // } 116 | 117 | // webrtc.io.emit('registerPush', { // ??? 118 | // add, 119 | // room: state.room, 120 | // subscription, 121 | // }) 122 | // })) 123 | 124 | // async function getStats(peer) { 125 | // let bytes = 0 126 | // let timestamp = 0 127 | // return new Promise((resolve) => { 128 | // peer?.peer?.getStats((_, reports) => { 129 | // reports.forEach((report) => { 130 | // if (report.type === 'outbound-rtp') { 131 | // if (report.isRemote) 132 | // return 133 | // bytes += report.bytesSent 134 | // timestamp = report.timestamp 135 | // // log('bb', bytes, prevBytes, timestamp, prevTimestamp) 136 | // resolve({ bytes, timestamp }) 137 | // } 138 | // }) 139 | // }) 140 | // }) 141 | // } 142 | 143 | // // https://github.com/webrtc/samples/blob/gh-pages/src/content/peerconnection/bandwidth/js/main.ts#L253 144 | // let prevTimestamp = 0 145 | // let prevBytes = 0 146 | 147 | // if (!!localStorage?.debug) { 148 | // let el = document.createElement("div") 149 | // el.className = "bandwidth" 150 | // document.body.appendChild(el) 151 | 152 | // setInterval(async () => { 153 | // // const now = performance.now() 154 | // let results = await Promise.all( 155 | // Object.values(webrtc.peerConnections).map((p) => getStats(p)) 156 | // ) 157 | // let bytes = results.reduce((acc, curr) => curr.bytes + acc, 0) 158 | // let timestamp = results?.[0]?.timestamp 159 | // const bitrate = (8 * (bytes - prevBytes)) / (timestamp - prevTimestamp) 160 | // el.textContent = bitrate.toFixed(2) + " Bit/s" 161 | // prevBytes = bytes 162 | // prevTimestamp = timestamp 163 | // }, 1000) 164 | // } 165 | 166 | return { 167 | webrtc, 168 | dispose, 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/logic/fingerprint.spec.ts: -------------------------------------------------------------------------------- 1 | import { getCompactChecksum, getFingerprint} from './fingerprint' 2 | 3 | describe('Fingerprint', () => { 4 | const sample = 5 | 'v=0\r\no=- 307404001895177377 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=msid-semantic: WMS w2zFUW5ETCVD3fSwsQPWihmgq1QtZugeID8D\r\nm=application 50041 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 169.254.123.65\r\nb=AS:30\r\na=candidate:3575507499 1 udp 2122260223 169.254.123.65 50041 typ host generation 0 network-id 3\r\na=candidate:2479012131 1 udp 2122197247 2a02:908:89b:60:988:92ae:a532:559d 50042 typ host generation 0 network-id 2 network-cost 10\r\na=candidate:3030907853 1 udp 2122129151 192.168.0.241 61982 typ host generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:zQTh\r\na=ice-pwd:dL3JIT+IjMc5lqeKvRqZCdaa\r\na=ice-options:trickle\r\na=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC\r\na=setup:active\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\nm=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zQTh\r\na=ice-pwd:dL3JIT+IjMc5lqeKvRqZCdaa\r\na=ice-options:trickle\r\na=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC\r\na=setup:active\r\na=mid:1\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:w2zFUW5ETCVD3fSwsQPWihmgq1QtZugeID8D 0408a99e-c357-4f60-85a9-299459678f24\r\na=rtcp-mux\r\na=rtpmap:109 opus/48000/2\r\na=fmtp:109 minptime=10;useinbandfec=1\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=ssrc:1185745839 cname:Sq7x5BjdM1thhRzt\r\nm=video 9 UDP/TLS/RTP/SAVPF 120 121\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zQTh\r\na=ice-pwd:dL3JIT+IjMc5lqeKvRqZCdaa\r\na=ice-options:trickle\r\na=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC\r\na=setup:active\r\na=mid:2\r\na=extmap:5 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:4 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:w2zFUW5ETCVD3fSwsQPWihmgq1QtZugeID8D 188a1334-5f04-422e-82f0-e5bb6102f9c4\r\na=rtcp-mux\r\na=rtpmap:120 VP8/90000\r\na=rtcp-fb:120 goog-remb\r\na=rtcp-fb:120 ccm fir\r\na=rtcp-fb:120 nack\r\na=rtcp-fb:120 nack pli\r\na=rtpmap:121 VP9/90000\r\na=rtcp-fb:121 goog-remb\r\na=rtcp-fb:121 ccm fir\r\na=rtcp-fb:121 nack\r\na=rtcp-fb:121 nack pli\r\na=fmtp:121 profile-id=0\r\na=ssrc:3405577348 cname:Sq7x5BjdM1thhRzt\r\n' 6 | 7 | it('should extract fingerprints', () => { 8 | let fp = getFingerprint(sample) 9 | expect(fp).toEqual([ 10 | 108, 93, 243, 15, 114, 18, 118, 1, 211, 237, 50, 157, 238, 97, 132, 30, 11 | 214, 156, 195, 23, 56, 189, 164, 145, 252, 67, 252, 135, 3, 160, 203, 172, 12 | ]) 13 | expect(fp.length).toBe(32) 14 | expect(fp[0]).toBe(0x6c) 15 | expect(fp[31]).toBe(0xac) 16 | }) 17 | 18 | const fp1 = [ 19 | 196, 63, 157, 83, 90, 81, 216, 2, 61, 173, 111, 209, 137, 47, 209, 98, 15, 20 | 75, 225, 185, 174, 170, 36, 22, 3, 128, 144, 55, 41, 185, 35, 135, 21 | ] 22 | const fp2 = [ 23 | 31, 164, 75, 163, 111, 6, 101, 24, 183, 133, 185, 32, 172, 146, 3, 194, 192, 24 | 106, 134, 93, 187, 49, 70, 53, 146, 65, 156, 119, 226, 86, 16, 136, 25 | ] 26 | 27 | it('should calc checksum', () => { 28 | // Always 4 digits 29 | expect('123'.padStart(4, '0')).toBe('0123') 30 | 31 | // Single tests 32 | expect(getCompactChecksum(fp1)).toBe('397c') 33 | expect(getCompactChecksum(fp2)).toBe('c264') 34 | 35 | // Multiple, random order 36 | expect(getCompactChecksum(fp1, fp2)).toBe('fbe0') 37 | expect(getCompactChecksum(fp2, fp1)).toBe('fbe0') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/logic/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase32 } from 'zeed' 2 | 3 | export function getCompactChecksum(...args) { 4 | args.sort() 5 | const values = Array.prototype.concat.apply([], args) // join 6 | if (values.length % 2) 7 | values.push(0) // even length 8 | let checksum = 0 9 | for (let j = 0; j < values.length; j += 2) { 10 | const left = values[j] 11 | const right = values[j + 1] 12 | checksum += left * 0xFF + right 13 | } 14 | return (checksum % 0xFFFF).toString(16).padStart(4, '0') 15 | } 16 | 17 | export function splitByNChars(value, splitN = 3, join = '-') { 18 | const strings = [] 19 | while (value?.length) { 20 | strings.push(value.substr(0, splitN)) 21 | value = value.substr(splitN) 22 | } 23 | return strings.join(join) 24 | } 25 | 26 | export async function digestMessage(message) { 27 | const msgUint8 = new TextEncoder().encode(message) 28 | const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) 29 | return encodeBase32(hashBuffer, 9) 30 | } 31 | 32 | export async function digestMessages(...messages) { 33 | // log('messages', messages) 34 | messages = messages.map(m => m.toString().toLowerCase().trim()) 35 | messages.sort() 36 | return digestMessage(messages.join('\n')) 37 | } 38 | 39 | export async function sha256Messages(...messages) { 40 | // log('messages', messages) 41 | messages = messages.map(m => m.toString().toLowerCase().trim()) 42 | messages.sort() 43 | const message = messages.join('\n') 44 | const msgUint8 = new TextEncoder().encode(message) 45 | return await crypto.subtle.digest('SHA-256', msgUint8) 46 | } 47 | 48 | function getFingerprintArray(fp) { 49 | if (!fp) 50 | return null 51 | return fp.split(':').map(v => parseInt(v.toLowerCase(), 16)) 52 | // return Uint8Array.from(fp.split(':').map(v => parseInt(v.toLowerCase(), 16))) 53 | } 54 | 55 | export function getFingerprint(sdp) { 56 | if (sdp) { 57 | const m = /^a=fingerprint(:[\w-]+)?\s+(.*)$/gm.exec(sdp) 58 | if (m.length) { 59 | // log(sdp, m) 60 | return getFingerprintArray(m[2]) 61 | } 62 | } 63 | return null 64 | } 65 | 66 | export function getFingerprintString(sdp) { 67 | if (sdp) { 68 | const m = /^a=fingerprint.*$/gm.exec(sdp) 69 | if (m && m.length) 70 | return m[0] 71 | } 72 | return null 73 | } 74 | -------------------------------------------------------------------------------- /src/logic/in-browser-test.ts: -------------------------------------------------------------------------------- 1 | import type { LoggerInterface } from 'zeed' 2 | import { Logger } from 'zeed' 3 | import { 4 | digestMessage, 5 | digestMessages, 6 | getFingerprintString, 7 | splitByNChars, 8 | } from './fingerprint' 9 | 10 | const log: LoggerInterface = Logger('test') 11 | 12 | const sample 13 | = 'v=0\r\no=- 307404001895177377 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=msid-semantic: WMS w2zFUW5ETCVD3fSwsQPWihmgq1QtZugeID8D\r\nm=application 50041 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 169.254.123.65\r\nb=AS:30\r\na=candidate:3575507499 1 udp 2122260223 169.254.123.65 50041 typ host generation 0 network-id 3\r\na=candidate:2479012131 1 udp 2122197247 2a02:908:89b:60:988:92ae:a532:559d 50042 typ host generation 0 network-id 2 network-cost 10\r\na=candidate:3030907853 1 udp 2122129151 192.168.0.241 61982 typ host generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:zQTh\r\na=ice-pwd:dL3JIT+IjMc5lqeKvRqZCdaa\r\na=ice-options:trickle\r\na=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC\r\na=setup:active\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\nm=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zQTh\r\na=ice-pwd:dL3JIT+IjMc5lqeKvRqZCdaa\r\na=ice-options:trickle\r\na=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC\r\na=setup:active\r\na=mid:1\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:w2zFUW5ETCVD3fSwsQPWihmgq1QtZugeID8D 0408a99e-c357-4f60-85a9-299459678f24\r\na=rtcp-mux\r\na=rtpmap:109 opus/48000/2\r\na=fmtp:109 minptime=10;useinbandfec=1\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=ssrc:1185745839 cname:Sq7x5BjdM1thhRzt\r\nm=video 9 UDP/TLS/RTP/SAVPF 120 121\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zQTh\r\na=ice-pwd:dL3JIT+IjMc5lqeKvRqZCdaa\r\na=ice-options:trickle\r\na=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC\r\na=setup:active\r\na=mid:2\r\na=extmap:5 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:4 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:w2zFUW5ETCVD3fSwsQPWihmgq1QtZugeID8D 188a1334-5f04-422e-82f0-e5bb6102f9c4\r\na=rtcp-mux\r\na=rtpmap:120 VP8/90000\r\na=rtcp-fb:120 goog-remb\r\na=rtcp-fb:120 ccm fir\r\na=rtcp-fb:120 nack\r\na=rtcp-fb:120 nack pli\r\na=rtpmap:121 VP9/90000\r\na=rtcp-fb:121 goog-remb\r\na=rtcp-fb:121 ccm fir\r\na=rtcp-fb:121 nack\r\na=rtcp-fb:121 nack pli\r\na=fmtp:121 profile-id=0\r\na=ssrc:3405577348 cname:Sq7x5BjdM1thhRzt\r\n' 14 | 15 | async function inBrowserTest() { 16 | const fp = getFingerprintString(sample) 17 | log.assert(fp === 'a=fingerprint:sha-256 6C:5D:F3:0F:72:12:76:01:D3:ED:32:9D:EE:61:84:1E:D6:9C:C3:17:38:BD:A4:91:FC:43:FC:87:03:A0:CB:AC') 18 | log.assert(await digestMessage(fp) === 'MT2AATY56') 19 | log.assert(splitByNChars('MT2AATY56') === 'MT2-AAT-Y56') 20 | 21 | const r1 = await digestMessages('abc', fp) 22 | const r2 = await digestMessages(fp, 'abc') 23 | log.assert(r1 === r2) 24 | } 25 | 26 | inBrowserTest() 27 | -------------------------------------------------------------------------------- /src/logic/sdp-manipulation.ts: -------------------------------------------------------------------------------- 1 | export function setMediaBitrate(sdp, media, bitrate) { 2 | const lines = sdp.split('\n') 3 | let line = -1 4 | for (let i = 0; i < lines.length; i++) { 5 | if (lines[i].indexOf(`m=${media}`) === 0) { 6 | line = i 7 | break 8 | } 9 | } 10 | if (line === -1) { 11 | // log('Could not find the m line for', media) 12 | return sdp 13 | } 14 | // log('Found the m line for', media, 'at line', line) 15 | 16 | // Pass the m line 17 | line++ 18 | 19 | // Skip i and c lines 20 | while (lines[line].indexOf('i=') === 0 || lines[line].indexOf('c=') === 0) 21 | line++ 22 | 23 | // If we're on a b line, replace it 24 | if (lines[line].indexOf('b') === 0) { 25 | // log('Replaced b line at line', line) 26 | lines[line] = `b=AS:${bitrate}` 27 | return lines.join('\n') 28 | } 29 | 30 | // Add a new b line 31 | // log('Adding new b line before line', line) 32 | let newLines = lines.slice(0, line) 33 | newLines.push(`b=AS:${bitrate}`) 34 | newLines = newLines.concat(lines.slice(line, lines.length)) 35 | return newLines.join('\n') 36 | } 37 | 38 | export function removeBandwidthRestriction(sdp) { 39 | return sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '') 40 | } 41 | 42 | // function isFirefox() { 43 | // return navigator?.userAgent?.includes('Firefox/') 44 | // } 45 | // 46 | // function updateBandwidthRestriction(sdp, bandwidth) { 47 | // let modifier = 'AS' 48 | // if (isFirefox()) { 49 | // bandwidth = (bandwidth >>> 0) * 1000 50 | // modifier = 'TIAS' 51 | // } 52 | // if (sdp.indexOf('b=' + modifier + ':') === -1) { 53 | // // insert b= after c= line. 54 | // sdp = sdp.replace(/c=IN (.*)\r\n/, 'c=IN $1\r\nb=' + modifier + ':' + bandwidth + '\r\n') 55 | // } else { 56 | // sdp = sdp.replace(new RegExp('b=' + modifier + ':.*\r\n'), 'b=' + modifier + ':' + bandwidth + '\r\n') 57 | // } 58 | // return sdp 59 | // } 60 | -------------------------------------------------------------------------------- /src/logic/stream.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'zeed' 2 | import { trackException, trackSilentException } from '../bugs' 3 | 4 | const log = Logger('app:stream') 5 | 6 | export async function getDevices() { 7 | try { 8 | return navigator.mediaDevices.enumerateDevices() 9 | } 10 | catch (err) { 11 | trackSilentException(err) 12 | } 13 | return [] 14 | } 15 | 16 | export const bandwidthVideoConstraints = { 17 | // video: { 18 | // width: { ideal: 320 }, 19 | // height: { ideal: 240 }, 20 | // }, 21 | // width: { ideal: 320 }, 22 | // height: { ideal: 240 }, 23 | } 24 | 25 | export const defaultVideoConstraints: MediaTrackConstraints = { 26 | // frameRate: { 27 | // min: 1, 28 | // ideal: 15, 29 | // }, 30 | } 31 | 32 | export const defaultAudioConstraints: MediaTrackConstraints = { 33 | // echoCancellation: true, 34 | // noiseSuppression: true, 35 | // autoGainControl: true, 36 | } 37 | 38 | function __getUserMedia(constraints) { 39 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) 40 | return navigator.mediaDevices.getUserMedia(constraints) 41 | 42 | // @ts-expect-error vendor specific 43 | const _getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia 44 | return new Promise((resolve, reject) => { 45 | if (!_getUserMedia) { 46 | reject( 47 | new Error( 48 | 'Video and audio cannot be accessed. Please try again with another browser or check your browser\'s settings.', 49 | ), 50 | ) 51 | } 52 | else { 53 | _getUserMedia.call(navigator, constraints, resolve, reject) 54 | } 55 | }) 56 | } 57 | 58 | export async function getUserMedia( 59 | constraints: MediaStreamConstraints = { 60 | audio: { 61 | ...defaultAudioConstraints, 62 | }, 63 | video: { 64 | ...defaultVideoConstraints, 65 | facingMode: 'user', 66 | }, 67 | }, 68 | ) { 69 | try { 70 | // Solution via https://stackoverflow.com/a/47958949/140927 71 | // Only available for HTTPS! See https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Security 72 | log('getUserMedia constraints', constraints) 73 | const stream = await __getUserMedia(constraints) 74 | return { stream } 75 | } 76 | catch (err) { 77 | const name = err?.name || err?.toString() 78 | if (name === 'NotAllowedError') { 79 | return { 80 | error: 81 | 'You denied access to your camera and microphone. Please check your setup.', 82 | } 83 | } 84 | else if (name === 'NotFoundError') { 85 | return { 86 | error: 'No camera or microphone has been found!', 87 | } 88 | } 89 | trackException(err) 90 | return { 91 | error: err?.message || err?.name || err.toString(), 92 | } 93 | } 94 | } 95 | 96 | // export async function getUserMedia(constraints = { 97 | // audio: { 98 | // ...defaultAudioConstraints, 99 | // }, 100 | // video: { 101 | // ...defaultVideoConstraints, 102 | // facingMode: 'user', 103 | // }, 104 | // }) { 105 | // let audioStream = await _getUserMedia({ audio: constraints.audio, video: false }) 106 | // let videoStream = await _getUserMedia({ video: constraints.video, audio: false }) 107 | // if (audioStream?.stream && videoStream?.stream) { 108 | // videoStream.stream.addTrack(audioStream.stream.getAudioTracks()[0]) 109 | // } 110 | // return videoStream || audioStream 111 | // } 112 | 113 | export async function getDisplayMedia( 114 | constraints: DisplayMediaStreamOptions = { 115 | video: { 116 | cursor: 'always', 117 | } as any, 118 | }, 119 | ) { 120 | try { 121 | if (!navigator?.mediaDevices?.getDisplayMedia) { 122 | return { 123 | error: 'Accessing the desktop is not available.', 124 | } 125 | } 126 | // Solution via https://stackoverflow.com/a/47958949/140927 127 | // Only available for HTTPS! See https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Security 128 | log('getDisplayMedia constraints', constraints) 129 | const stream = await navigator.mediaDevices.getDisplayMedia(constraints) 130 | return { stream } 131 | } 132 | catch (err) { 133 | const name = err?.name || err?.toString() 134 | if (name === 'NotAllowedError') { 135 | return { 136 | error: 137 | 'You denied access to your camera and microphone. Please check your setup.', 138 | } 139 | } 140 | else if (name === 'NotFoundError') { 141 | return { 142 | error: 'No camera or microphone has been found!', 143 | } 144 | } 145 | trackException(err) 146 | return { 147 | error: err?.message || err?.name || err.toString(), 148 | } 149 | } 150 | } 151 | 152 | export function setAudioTracks(stream, audioTracks) { 153 | Array.from(stream.getAudioTracks()).forEach(t => stream.removeTrack(t)) 154 | audioTracks.forEach((t) => { 155 | try { 156 | stream.addTrack(t) 157 | } 158 | catch (err) { 159 | if (err?.message !== 'Track has already been added to that stream.') 160 | trackSilentException(err) 161 | } 162 | }) 163 | return stream 164 | } 165 | -------------------------------------------------------------------------------- /src/logic/webrtc-peer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2023 Dirk Holtwick. All rights reserved. https://holtwick.de/copyright 2 | 3 | import { Emitter, Logger, encodeBase32 } from 'zeed' 4 | import { trackException } from '../bugs' 5 | import { cloneObject } from '../lib/base' 6 | import { getFingerprintString, sha256Messages, splitByNChars } from './fingerprint' 7 | import { Peer } from './simple-peer' 8 | 9 | const log = Logger('app:webrtc-peer') 10 | 11 | let ctr = 1 12 | 13 | export class WebRTCPeer extends Emitter { 14 | remote: any 15 | local: any 16 | initiator: any 17 | room: any 18 | id: string 19 | fingerprint: string 20 | name: string 21 | error: any 22 | active: boolean 23 | stream: any 24 | peer: Peer 25 | 26 | static isSupported() { 27 | return Peer.WEBRTC_SUPPORT 28 | } 29 | 30 | constructor({ remote, local, ...opt }: any = {}) { 31 | super() 32 | 33 | this.remote = remote 34 | this.local = local 35 | this.initiator = opt.initiator 36 | this.room = opt.room || '' 37 | this.id = `webrtc-peer${ctr++}` 38 | this.fingerprint = '' 39 | this.name = '' 40 | 41 | log('peer', this.id) 42 | this.setupPeer(opt) 43 | } 44 | 45 | setupPeer(opt) { 46 | this.error = null 47 | this.active = false 48 | this.stream = null 49 | 50 | const opts = cloneObject({ 51 | ...opt, 52 | // Allow the peer to receive video, even if it's not sending stream: 53 | // https://github.com/feross/simple-peer/issues/95 54 | offerConstraints: { 55 | offerToReceiveAudio: true, 56 | offerToReceiveVideo: true, 57 | }, 58 | }) 59 | 60 | log('Peer opts:', opts) 61 | 62 | // https://github.com/feross/simple-peer/blob/master/README.md 63 | this.peer = new Peer(opts) 64 | 65 | this.peer.on('close', this.close.bind(this)) 66 | 67 | // We receive a connection error 68 | this.peer.on('error', (err) => { 69 | log(`${this.id} | error`, err) 70 | this.error = err 71 | this.emit('error', err) 72 | this.close() 73 | setTimeout(() => { 74 | this.setupPeer(opt) // ??? 75 | }, 1000) 76 | }) 77 | 78 | // This means, we received network details (signal) we need to provide 79 | // the remote peer, so he can set up a connection to us. Usually we will 80 | // send this over a separate channel like the web socket signaling server 81 | this.peer.on('signal', (data) => { 82 | // log(`${this.id} | signal`, this.initiator) 83 | this.emit('signal', data) 84 | }) 85 | 86 | this.peer.on('signalingStateChange', async (_) => { 87 | const fpl 88 | = getFingerprintString(this.peer?._pc?.currentLocalDescription?.sdp) || '' 89 | const fpr 90 | = getFingerprintString(this.peer?._pc?.currentRemoteDescription?.sdp) 91 | || '' 92 | if (fpl && fpr) { 93 | const digest = await sha256Messages(this.room, fpl, fpr) 94 | this.fingerprint = splitByNChars(encodeBase32(digest), 4) 95 | } 96 | else { 97 | this.fingerprint = '' 98 | } 99 | }) 100 | 101 | // We received data from the peer 102 | this.peer.on('data', (data) => { 103 | log(`${this.id} | data`) 104 | this.emit('data', data) 105 | this.emit('message', { data }) // Channel compat 106 | }) 107 | 108 | // Connection succeeded 109 | this.peer.on('connect', () => { 110 | log(`${this.id} | connect`) 111 | this.active = true 112 | // p.send('whatever' + Math.random()) 113 | this.emit('connect') 114 | }) 115 | 116 | this.peer.on('stream', (stream) => { 117 | log('new stream', stream) 118 | this.stream = stream 119 | this.emit('stream', stream) 120 | }) 121 | } 122 | 123 | setStream(stream) { 124 | try { 125 | this.peer.setStream(stream) 126 | } 127 | catch (err) { 128 | trackException(err) 129 | } 130 | } 131 | 132 | // We got a signal from the remote peer and will use it now to establish the connection 133 | signal(data) { 134 | if (this.peer && !this.peer.destroyed) { 135 | // To prove that manipulated fingerprints will result in refusing connection 136 | // if (data?.sdp) { 137 | // data.sdp = data.sdp.replace(/(fingerprint:.*?):(\w\w):/, '$1:00:') 138 | // } 139 | this.peer.signal(data) 140 | } 141 | else { 142 | log('Tried to set signal on destroyed peer', this.peer, data) 143 | } 144 | } 145 | 146 | postMessage(data) { 147 | // Channel compat 148 | this.peer.send(data) 149 | } 150 | 151 | close() { 152 | this.emit('close') 153 | this.active = false 154 | this.peer?.destroy() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { Logger } from 'zeed' 3 | import appComponent from './app.vue' 4 | import { i18n } from './i18n' 5 | 6 | // import "./logic/registerServiceWorker" 7 | 8 | const log = Logger('main') 9 | 10 | log(`env = ${JSON.stringify(import.meta.env, null, 2)}`) 11 | 12 | // Force removal of 1.0 service-workers 13 | try { 14 | log('try removal of service workers') 15 | navigator.serviceWorker.getRegistrations().then((registrations) => { 16 | for (const registration of registrations) 17 | registration.unregister() 18 | }) 19 | } 20 | catch (err) { 21 | log.error('Unregistering failed', err) 22 | } 23 | 24 | const app = createApp(appComponent) 25 | app.use(i18n) 26 | app.mount('#app') 27 | -------------------------------------------------------------------------------- /src/product/app-embed.scss: -------------------------------------------------------------------------------- 1 | .app-welcome { 2 | margin: 2rem; 3 | padding-bottom: 4rem; 4 | text-align: center; 5 | 6 | h1 { 7 | font-size: 2rem; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | a { 12 | color: rgba(43, 184, 255, 1); 13 | user-select: text; 14 | } 15 | 16 | .url { 17 | margin-bottom: 1rem; 18 | } 19 | 20 | label { 21 | display: block; 22 | margin-bottom: 0.5rem; 23 | text-align: left; 24 | } 25 | 26 | .options { 27 | display: inline-block; 28 | margin-bottom: 1rem; 29 | 30 | .form-group { 31 | margin-bottom: 0.5rem; 32 | } 33 | } 34 | 35 | .code { 36 | display: inline-block; 37 | text-align: left; 38 | user-select: text; 39 | width: 75vw; 40 | 41 | background: #ccc; 42 | color: black; 43 | padding: 1rem; 44 | border-radius: 0.25rem; 45 | } 46 | } 47 | 48 | .iframe { 49 | border: 1px solid #ccc; 50 | width: Min(90vh, 90vw); 51 | height: Calc(0.75 * Min(90vh, 90vw)); 52 | margin-bottom: 1rem; 53 | } -------------------------------------------------------------------------------- /src/product/app-embed.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 |