94 Commits

Author SHA1 Message Date
ec69eb2dcb gramarly disable for textarea 2022-09-27 20:34:14 +02:00
882daddb51 hide resize from textarea. 2022-09-27 20:30:06 +02:00
a9fd6ac114 guac remove text compositon. 2022-09-27 20:28:43 +02:00
fd17a282fa switch overlay to textarea to correctly interpret dead keys. 2022-09-26 18:59:42 +02:00
9f26b27d5d fix capture logging. 2022-09-24 14:21:32 +02:00
d06740aa96 auto broadcast started. 2022-09-21 18:59:38 +02:00
da86a0931c pipelineFn returns string. 2022-09-21 18:58:28 +02:00
b6d86aab30 added opera info to docs 2022-09-20 07:50:54 +02:00
03c74c88b9 added opera image to build 2022-09-20 07:50:54 +02:00
ca6c24dee1 add screenshot function. 2022-09-17 18:37:30 +02:00
057ab2d886 codecs from string not bools. 2022-09-17 18:37:30 +02:00
ccbfe93765 fix register. 2022-09-17 18:37:30 +02:00
fd43f84bd0 refactor capture with broadcast. 2022-09-17 18:37:30 +02:00
72da075972 xorg add events. 2022-09-17 18:37:30 +02:00
2e5d3f5624 update changelog. 2022-09-17 11:01:30 +02:00
9394b361bd xevent on clipboard updated. 2022-09-16 00:01:15 +02:00
e9912ea87f add xevent. 2022-09-15 23:55:30 +02:00
2afc356911 capture use SetScreenSize. 2022-09-13 21:40:50 +02:00
4c1c96b163 xorg refactor. 2022-09-13 21:40:40 +02:00
478984e944 change shutdown order. 2022-09-13 20:35:53 +02:00
e045bd8a1e move locks and bans to state. 2022-09-13 20:35:53 +02:00
06e25df962 websocket handler own submodule. 2022-09-13 20:35:53 +02:00
777f7b4c37 use custom pionlog. 2022-09-13 20:35:53 +02:00
deabba80ca move pipelines from gst to capture. 2022-09-13 20:35:53 +02:00
29f67fad06 move config from types. 2022-09-13 20:35:53 +02:00
c0ca073b2d move gst and broadcast under capture. 2022-09-13 20:35:53 +02:00
de4f6b45e5 split remote to desktop and capture. 2022-09-13 20:35:53 +02:00
0bca8c9d02 remote manager split. 2022-09-13 20:35:53 +02:00
e3e3cf9d22 extract clipboard from xorg. 2022-09-13 20:35:53 +02:00
2be071d215 update readme. 2022-09-11 13:42:54 +02:00
5cd09e6c53 Merge branch 'path-prefix' 2022-09-08 20:00:21 +02:00
537d131883 add to chengelog. 2022-09-08 19:58:10 +02:00
a792e4ea87 fix opera openbox. 2022-09-08 19:56:18 +02:00
7312c17f6a removed unnecessary launch options 2022-09-07 20:19:21 -04:00
b0017d134f removed references to vlc 2022-09-07 20:02:36 -04:00
796d05925a removed policies and preferences 2022-09-06 20:43:33 -04:00
86ab5edf4b add path prefix #196. 2022-09-01 18:22:01 +02:00
1edeb717b1 added opera 2022-08-29 21:59:07 -04:00
43fe80c82e update chromium policies. 2022-08-20 23:27:23 +02:00
8c9c348615 fix link to neko-rooms. 2022-08-04 01:19:29 +02:00
32f4dc5f2a npm upgrade. 2022-07-31 12:26:33 +02:00
523289523d go mod upgrade. 2022-07-31 12:21:26 +02:00
45b0657858 update mux docs. 2022-07-31 12:19:05 +02:00
629369487c add funding. 2022-07-31 12:18:59 +02:00
3c17988b24 Updated Readme to include details about url args. (#188)
Please consider adding more info on the main page as the url args are really useful.
2022-07-30 20:50:35 +02:00
2ae54dea64 Merge pull request #186 from m1k1o/vivaldi
New Vivaldi browser
2022-06-18 21:29:29 +02:00
45b1aec1d9 add vivaldi browser. 2022-06-18 21:27:09 +02:00
861f4b02d1 Merge branch 'master' into vivaldi 2022-06-18 21:24:47 +02:00
e91836e9bb add extensions. 2022-06-18 21:24:07 +02:00
6882d102cc Merge branch 'master' of github.com:Xeddius/neko into vivaldi 2022-06-18 21:23:50 +02:00
6114b5b06b Install latest Ungoogled Chromium extensions at build time (#183)
Signed-off-by: Aaron <admin@datahoarder.dev>
2022-06-18 14:20:35 +02:00
934ac9d4e0 docs(configuration): fix typo (#184) 2022-06-18 12:54:39 +02:00
429122574f Update Ungoogled Chromium extensions (#181)
Signed-off-by: Aaron <admin@datahoarder.dev>
2022-06-17 22:58:18 +02:00
fe5a2f9ee7 version 2.6. 2022-06-17 21:15:57 +02:00
7d71fd2bc6 add chinese fonts, fixes #179. 2022-06-17 21:14:45 +02:00
9c4404963b lint fix. 2022-06-16 21:50:36 +02:00
c15daea875 Added simplified chinese as language (#176) 2022-06-08 09:04:39 +02:00
25aab1d7af update preferences.json 2022-06-03 13:58:29 -04:00
c054df072b edit profile 2022-06-03 13:57:51 -04:00
96fd19b178 remove default profile. 2022-06-02 21:03:17 +02:00
7ec0dea7bf edit preferences. 2022-06-02 21:02:44 +02:00
d420c48f5f fix openbox. 2022-06-02 21:02:27 +02:00
2c133599bd Update Dockerfile 2022-06-02 01:50:30 -04:00
b88e3e349a Added Vivaldi Image
Vivaldi browser for n.eko.
It's currently using a dummy "default" profile.
I'm working on contacting a developer to see if we can get some kind of preferences.json support and possibly a link to latest builds for ease of integration/updates.
2022-06-02 01:45:38 -04:00
b4e3bd6d1f github actions disable fail-fast. 2022-06-01 23:04:28 +02:00
676f36c973 upgrade guacamole keyboard, #174. 2022-06-01 22:28:09 +02:00
c48309b648 upgrade to go 1.18. 2022-05-27 23:40:19 +02:00
2aec417fa8 upgrade golang deps. 2022-05-27 23:32:00 +02:00
f97ed8a65b go mod upgrade. 2022-05-14 18:58:09 +02:00
1c1f638be7 build fail if any command fails. 2022-05-14 18:56:51 +02:00
3dce6b1204 fix cgo flags. 2022-05-14 18:56:09 +02:00
58b2812eaa change pulseaudio log level. 2022-05-14 18:54:38 +02:00
5a42e06510 update changelog. 2022-05-14 18:53:22 +02:00
4a9342bc50 upgrade sweetalert2. 2022-05-14 18:51:55 +02:00
c497b7325f npm upgrade + bump fontawesome version. 2022-05-14 18:13:56 +02:00
072d294468 change vp8enc pipeline attributes #156. 2022-04-30 19:45:59 +02:00
0062fc28aa overlay focus only when hosting. 2022-04-30 19:34:12 +02:00
Bad
b963279296 (typo fix) Web RTC -> WebRTC (#168)
how long has this been here 💀
2022-04-07 13:18:14 +02:00
af9289866d update envs in docs, #127. 2022-03-28 20:00:44 +02:00
031a2f0816 Allow webs to replace context menu (#162) 2022-03-28 08:34:09 +02:00
2189e4fd49 opus useinbandfec. 2022-03-24 21:48:03 +01:00
130832177c chromium latest version, fixes #158. 2022-03-21 20:13:38 +01:00
915d050109 remove vncviewer. 2022-03-18 23:14:08 +01:00
e0f921473a add remmina to docs. 2022-03-18 23:11:32 +01:00
d3af6f477c remmina set password already encrypts it. 2022-03-18 23:07:24 +01:00
9550220e01 remmina port optional & fix debug outputs. 2022-03-18 23:07:11 +01:00
c59551da40 fix firefox openbox class, fixes #154, fixes #152. 2022-03-18 21:49:16 +01:00
12c92cb55a Remmina (#155)
* (nits)

* add hardware encoding support for Intel QSV via VAAPI

* automate RENDER_GID env var

* add remmina

* remmina: github CI
2022-03-18 18:25:46 +01:00
87082bb978 Hardware accelerated encoding using Intel QuickSync via VAAPI (#151)
* (nits)

* add hardware encoding support for Intel QSV via VAAPI

* automate RENDER_GID env var
2022-03-17 20:25:17 +01:00
ba3368a3eb Replace nordvpn with sponsorblock. (#144)
* replace nordvpn with sponsorblock.

* update changelog.
2022-03-01 19:32:17 +01:00
d43cf8c58b restore chromium widevine support, fixes #141. 2022-02-27 14:51:33 +01:00
6d23950849 bump chromium version, fixes #134. 2022-02-27 14:50:48 +01:00
807b6b9f7b auto join links to docs, fixes #145. 2022-02-27 14:23:24 +01:00
62cdfdf4fe update docs, fixes #142. 2022-02-06 21:02:38 +01:00
131 changed files with 6234 additions and 2887 deletions

View File

@ -1,7 +1,7 @@
#
# STAGE 1: SERVER
#
FROM golang:1.17-bullseye as server
FROM golang:1.18-bullseye as server
WORKDIR /src
#
@ -13,7 +13,7 @@ RUN set -eux; apt-get update; \
# install libclipboard
set -eux; \
cd /tmp; \
git clone https://github.com/jtanx/libclipboard; \
git clone --depth=1 https://github.com/jtanx/libclipboard; \
cd libclipboard; \
cmake .; \
make -j4; \
@ -60,19 +60,29 @@ ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
#
# install dependencies
RUN set -eux; apt-get update; \
RUN set -eux; \
#
# add non-free repo for intel drivers
echo deb http://deb.debian.org/debian bullseye main contrib non-free > /etc/apt/sources.list; \
echo deb http://deb.debian.org/debian-security/ bullseye-security main contrib non-free >> /etc/apt/sources.list; \
echo deb http://deb.debian.org/debian bullseye-updates main contrib non-free >> /etc/apt/sources.list; \
apt-get update; \
#
# install dependencies
apt-get install -y --no-install-recommends wget ca-certificates supervisor; \
apt-get install -y --no-install-recommends pulseaudio dbus-x11 xserver-xorg-video-dummy; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
#
# gst
# intel driver + vaapi
apt-get install -y --no-install-recommends intel-media-va-driver-non-free libva2 vainfo; \
#
# gst + vaapi plugin
apt-get install -y --no-install-recommends libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio; \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio \
gstreamer1.0-vaapi ;\
#
# fonts
apt-get install -y --no-install-recommends fonts-takao-mincho; \
apt-get install -y --no-install-recommends fonts-takao-mincho fonts-wqy-zenhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -106,6 +116,7 @@ COPY .docker/base/dbus /usr/bin/dbus
COPY .docker/base/default.pa /etc/pulse/default.pa
COPY .docker/base/supervisord.conf /etc/neko/supervisord.conf
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
COPY .docker/base/add-render-group.sh /usr/bin/add-render-group.sh
#
# set default envs
@ -114,6 +125,7 @@ ENV DISPLAY=:99.0
ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080
ENV RENDER_GID=
#
# copy static files from previous stages

View File

@ -1,7 +1,7 @@
#
# STAGE 1: SERVER
#
FROM arm32v7/golang:1.17-buster as server
FROM arm32v7/golang:1.18-buster as server
WORKDIR /src
#
@ -77,7 +77,7 @@ RUN set -eux; apt-get update; \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio gstreamer1.0-omx; \
#
# fonts
apt-get install -y --no-install-recommends fonts-takao-mincho; \
apt-get install -y --no-install-recommends fonts-takao-mincho fonts-wqy-zenhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \

View File

@ -0,0 +1,18 @@
#!/bin/bash
# if no hwenc required, noop
[[ -z "$NEKO_HWENC" ]] && exit 0
if [[ -z "$RENDER_GID" ]]; then
RENDER_GID=$(stat -c "%g" /dev/dri/render* | tail -n 1)
# is /dev/dri passed to the container?
[[ -z "$RENDER_GID" ]] && exit 1
fi
# note that this could conceivably be a security risk...
cnt_group=$(getent group "$RENDER_GID" | cut -d: -f1)
if [[ -z "$cnt_group" ]]; then
groupadd -g "$RENDER_GID" nekorender
cnt_group=nekorender
fi
usermod -a -G "$cnt_group" "$USER"

View File

@ -9,6 +9,19 @@ loglevel=debug
[include]
files=/etc/neko/supervisord/*.conf
[program:rendergroup-init]
environment=RENDER_GID="%(ENV_RENDER_GID)s",USER="%(ENV_USER)s"
command=/usr/bin/add-render-group.sh
startsecs=0
startretries=0
autorestart=false
priority=10
user=root
stdout_logfile=/var/log/neko/rendergroup.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
redirect_stderr=true
[program:dbus]
environment=HOME="/root",USER="root"
command=/usr/bin/dbus
@ -33,7 +46,7 @@ redirect_stderr=true
[program:pulseaudio]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1
command=/usr/bin/pulseaudio --log-level=info --disallow-module-loading --disallow-exit --exit-idle-time=-1
autorestart=true
priority=300
user=%(ENV_USER)s

View File

@ -1,6 +1,4 @@
{
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
@ -20,19 +18,19 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLBlacklist": [
"URLBlocklist": [
"file://*",
"chrome://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallWhitelist": [
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"fjoaledfpmneenckfbpdfhkmimnjocfa"
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlacklist": [
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -1,4 +1,5 @@
#!/bin/sh
set -ex
cd "$(dirname "$0")"
BASE="${PWD}/../"

View File

@ -1,18 +1,23 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
ARG VERSION="90.0.4430.212-1"
#
# install neko chromium
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends unzip chromium=$VERSION chromium-common=$VERSION chromium-sandbox=$VERSION openbox; \
RUN set -eux; \
echo "deb http://ftp.de.debian.org/debian bookworm main" >> /etc/apt/sources.list; \
apt-get update; \
apt-get install -y --no-install-recommends unzip chromium chromium-common chromium-sandbox openbox; \
#
# install widevine module
CHROMIUM_DIR="/usr/lib/chromium"; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/$WIDEVINE_VERSION-linux-x64.zip"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \
unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \
unzip -p /tmp/widevine.zip manifest.json > "${CHROMIUM_DIR}/WidevineCdm/manifest.json"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so"; \
find "${CHROMIUM_DIR}/WidevineCdm" -type d -exec chmod 0755 '{}' \;; \
find "${CHROMIUM_DIR}/WidevineCdm" -type f -exec chmod 0644 '{}' \;; \
rm /tmp/widevine.zip; \
#
# clean up

View File

@ -1,6 +1,4 @@
{
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
@ -20,19 +18,20 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLBlacklist": [
"BrowserLabsEnabled": false,
"URLBlocklist": [
"file://*",
"chrome://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallWhitelist": [
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"fjoaledfpmneenckfbpdfhkmimnjocfa"
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlacklist": [
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -10,7 +10,7 @@ RUN set -eux; apt-get update; \
# install extensions
mkdir -p /usr/lib/firefox-esr/distribution/extensions; \
wget -O '/usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi' https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi; \
wget -O /usr/lib/firefox-esr/distribution/extensions/nordvpnproxy@nordvpn.com.xpi https://addons.mozilla.org/firefox/downloads/latest/nordvpn-proxy-extension/latest.xpi; \
wget -O '/usr/lib/firefox-esr/distribution/extensions/sponsorBlocker@ajay.app.xpi' https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi; \
#
# create a profile directory
mkdir -p /home/neko/.mozilla/firefox/profile.default/extensions; \

View File

@ -13,7 +13,7 @@
<applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="Firefox*" name="Navigator">
<application class="firefox" name="Navigator">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>

View File

@ -56,8 +56,8 @@
"*": {
"installation_mode": "blocked"
},
"nordvpnproxy@nordvpn.com": {
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/nordvpn-proxy-extension/latest.xpi",
"sponsorBlocker@ajay.app": {
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi",
"installation_mode": "force_installed"
},
"uBlock0@raymondhill.net": {
@ -108,7 +108,7 @@
"datareporting.policy.dataSubmissionPolicyBypassNotification": true,
"dom.disable_window_flip": true,
"dom.disable_window_move_resize": true,
"dom.event.contextmenu.enabled": false,
"dom.event.contextmenu.enabled": true,
"extensions.getAddons.showPane": false,
"places.history.enabled": false,
"privacy.file_unique_origin": true,

View File

@ -1,6 +1,4 @@
{
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
@ -20,19 +18,19 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLBlacklist": [
"URLBlocklist": [
"file://*",
"chrome://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallWhitelist": [
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"fjoaledfpmneenckfbpdfhkmimnjocfa"
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlacklist": [
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -1,6 +1,4 @@
{
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
@ -20,19 +18,19 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLBlacklist": [
"URLBlocklist": [
"file://*",
"edge://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallWhitelist": [
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"fjoaledfpmneenckfbpdfhkmimnjocfa"
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlacklist": [
"ExtensionInstallBlocklist": [
"*"
]
}

27
.docker/opera/Dockerfile Normal file
View File

@ -0,0 +1,27 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
ARG SRC_URL="https://download.opera.com/download/get/?id=58545&location=415&nothanks=yes&sub=marine&utm_tryagain=yes"
ARG LIBFFMPEG_URL="https://github.com/nwjs-ffmpeg-prebuilt/nwjs-ffmpeg-prebuilt/releases/download/0.67.1/0.67.1-linux-x64.zip"
#
# install opera
RUN apt-get update
RUN wget -O /tmp/opera.deb $SRC_URL
RUN apt-get install -y --no-install-recommends openbox unzip /tmp/opera.deb
## install libffmpeg
RUN wget -O /tmp/libffmpeg.zip $LIBFFMPEG_URL
RUN unzip -o /tmp/libffmpeg.zip libffmpeg.so -d /usr/lib/x86_64-linux-gnu/opera/lib_extra
RUN echo '[]' > /usr/lib/x86_64-linux-gnu/opera/resources/ffmpeg_preload_config.json
#
# clean up
RUN apt-get clean -y
RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/opera.conf
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -13,7 +13,7 @@
<applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="Vncviewer" name="vncviewer">
<application class="Opera" name="Opera">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>

View File

@ -1,11 +1,11 @@
[program:vncviewer]
[program:opera]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/vncviewer -autopass -x11cursor -nojpeg -quality 9 -compresslevel 9 %(ENV_NEKO_VNC_URL)s
command=/usr/bin/opera --no-sandbox --no-first-run --start-maximized --force-dark-mode --disable-gpu
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/vncviewer.log
stdout_logfile=/var/log/neko/opera.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -0,0 +1,20 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
# install remmina
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends \
remmina-plugin-rdp remmina-plugin-vnc \
# remmina-plugin-x2go # not in bullseye
remmina-plugin-spice remmina-plugin-nx; \
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/remmina.conf
COPY --chown=neko remmina.pref /home/neko/.config/remmina/remmina.pref
COPY --chown=neko rdp.remmina spice.remmina vnc.remmina /home/neko/.local/share/remmina/
COPY run-remmina.sh /usr/bin/run-remmina.sh
ENV REMMINA_URL=
ENV REMMINA_PROFILE=

View File

@ -0,0 +1,90 @@
[remmina]
password=
gateway_username=
notes_text=
vc=
preferipv6=0
ssh_tunnel_loopback=0
serialname=
sound=local
printer_overrides=
name=rdpdefault
console=0
colordepth=99
security=
precommand=
disable_fastpath=0
left-handed=0
postcommand=
multitransport=0
group=
server=
ssh_tunnel_certfile=
glyph-cache=0
ssh_tunnel_enabled=0
disableclipboard=0
audio-output=
parallelpath=
monitorids=
cert_ignore=0
gateway_server=
serialpermissive=0
protocol=RDP
old-license=0
ssh_tunnel_password=
resolution_mode=2
pth=
loadbalanceinfo=
disableautoreconnect=0
clientname=
clientbuild=
resolution_width=0
drive=
relax-order-checks=0
username=
base-cred-for-gw=0
gateway_domain=
network=none
rdp2tcp=
gateway_password=
serialdriver=
domain=
profile-lock=0
rdp_reconnect_attempts=
restricted-admin=0
multimon=0
exec=
smartcardname=
serialpath=
enable-autostart=0
usb=
shareprinter=0
ssh_tunnel_passphrase=
shareparallel=0
disablepasswordstoring=0
quality=0
span=0
parallelname=
ssh_tunnel_auth=0
keymap=
ssh_tunnel_username=
execpath=
shareserial=0
resolution_height=0
timeout=
useproxyenv=0
sharesmartcard=0
freerdp_log_filters=
microphone=
dvc=
ssh_tunnel_privatekey=
gwtransp=http
ssh_tunnel_server=
ignore-tls-errors=1
disable-smooth-scrolling=0
gateway_usage=0
websockets=0
freerdp_log_level=INFO
window_maximize=1
scale=1
viewmode=4

View File

@ -0,0 +1,103 @@
[remmina_pref]
secret=Jsh7BJPwHqLCKi2vdkMAImSgdBVZGF6s8VbUPY3Q9WA=
datadir_path=
remmina_file_name=%G_%P_%N_%h
screenshot_path=
screenshot_name=
deny_screenshot_clipboard=true
save_view_mode=true
use_master_password=false
unlock_timeout=0
unlock_password=
trust_all=true
floating_toolbar_placement=0
toolbar_placement=3
prevent_snap_welcome_message=true
last_quickconnect_protocol=
fullscreen_on_auto=true
always_show_tab=false
hide_connection_toolbar=true
hide_searchbar=true
default_action=0
scale_quality=3
ssh_loglevel=1
ssh_parseconfig=true
hide_toolbar=true
small_toolbutton=true
view_file_mode=0
resolutions=640x480,800x600,1024x768,1152x864,1280x960,1400x1050
keystrokes=
main_width=600
main_height=400
main_maximize=true
main_sort_column_id=1
main_sort_order=0
expanded_group=
toolbar_pin_down=false
sshtunnel_port=4732
ssh_tcp_keepidle=20
ssh_tcp_keepintvl=10
ssh_tcp_keepcnt=3
ssh_tcp_usrtimeout=60000
applet_new_ontop=false
applet_hide_count=true
applet_enable_avahi=false
disable_tray_icon=true
dark_tray_icon=false
recent_maximum=10
default_mode=3
tab_mode=0
fullscreen_toolbar_visibility=2
auto_scroll_step=10
hostkey=65508
shortcutkey_fullscreen=
shortcutkey_autofit=
shortcutkey_nexttab=
shortcutkey_prevtab=
shortcutkey_scale=
shortcutkey_grab=65508
shortcutkey_viewonly=109
shortcutkey_screenshot=
shortcutkey_minimize=65478
shortcutkey_disconnect=65473
shortcutkey_toolbar=116
vte_font=
vte_allow_bold_text=true
vte_lines=512
[ssh_colors]
background=#d5ccba
cursor=#45373c
bold=#45373c
foreground=#45373c
color0=#20111b
color1=#be100e
color2=#858162
color3=#eaa549
color4=#426a79
color5=#97522c
color6=#989a9c
color7=#968c83
color8=#5e5252
color9=#be100e
color10=#858162
color11=#eaa549
color12=#426a79
color13=#97522c
color14=#989a9c
color15=#d5ccba
[usage_stats]
periodic_usage_stats_permitted=false
periodic_usage_stats_last_sent=0
periodic_usage_stats_uuid_prefix=
[remmina_news]
periodic_news_permitted=false
periodic_rmnews_last_get=0
periodic_rmnews_get_count=1
periodic_rmnews_uuid_prefix=
[remmina]
name=
ignore-tls-errors=1

38
.docker/remmina/run-remmina.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/bash
set -u
err() {
echo "ERROR: $*" >&2
exit 1
}
profile_dir="/home/neko/.local/share/remmina"
if [[ -n "$REMMINA_PROFILE" ]]; then
profile=${REMMINA_PROFILE%.remmina}.remmina
file=${profile##/*/}
[[ "$file" = "$profile" ]] && profile="$profile_dir"/"$file"
[[ -f "$profile" ]] || err "Connection profile $profile not found"
echo "Running remmina with connection profile $profile"
exec remmina -c "$profile"
fi
[[ -z "$REMMINA_URL" ]] && err "Neither 'REMMINA_PROFILE' nor 'REMMINA_URL' found in env vars"
readarray -t arr < <( echo -n "$REMMINA_URL" | perl -pe 's|^(\w+)\:\/\/(?:([^:]+)(?::([^@]+))?@)?(.*)$|\1\n\2\n\3\n\4|' )
proto="${arr[0]}"
user="${arr[1]}"
pw="${arr[2]}"
host="${arr[3]}"
echo "Parsed url in 'REMMINA_URL': proto:$proto username:$user host:$host"
[[ "$proto" != "vnc" && "$proto" != "rdp" && "$proto" != "spice" ]] && err "Unsupported protocol $proto in connection url 'REMMINA_URL'"
profile="$profile_dir"/"$proto".remmina
remmina --set-option username="$user" --update-profile "$profile"
remmina --set-option password="$pw" --update-profile "$profile"
remmina --set-option server="$host" --update-profile "$profile"
# remmina --set-option window_maximize=1 --update-profile "$profile"
# remmina --set-option scale=1 --update-profile "$profile"
exec remmina -c "$profile"

View File

@ -0,0 +1,33 @@
[remmina]
disablegstvideooverlay=0
disablepasswordstoring=0
sharesmartcard=0
videocodec=0
ssh_tunnel_password=
postcommand=
server=
name=spicedefault
ssh_tunnel_enabled=0
profile-lock=0
enable-autostart=0
imagecompression=0
password=
precommand=
disableclipboard=0
group=
ssh_tunnel_certfile=
protocol=SPICE
enableaudio=1
viewonly=0
ssh_tunnel_server=
ssh_tunnel_loopback=0
ssh_tunnel_auth=0
ignore-tls-errors=1
ssh_tunnel_username=
ssh_tunnel_passphrase=
ssh_tunnel_privatekey=
notes_text=
usetls=0
window_maximize=1
scale=1
viewmode=4

View File

@ -0,0 +1,12 @@
[program:remmina]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/run-remmina.sh
stopsignal=INT
autorestart=true
priority=500
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/remmina.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -0,0 +1,49 @@
[remmina]
encodings=
videocodec=0
name=temp eos
ssh_tunnel_server=
ssh_tunnel_privatekey=
password=
quality=0
disablesmoothscrolling=0
enableaudio=1
precommand=
disablegstvideooverlay=0
ssh_tunnel_enabled=0
sharesmartcard=0
imagecompression=0
ssh_tunnel_passphrase=
ssh_tunnel_password=
usetls=0
viewonly=0
disableserverinput=0
depth_profile=0
postcommand=
tightencoding=0
disablepasswordstoring=0
lossy_encoding=0
ignore-tls-errors=1
gvncdebug=0
ssh_tunnel_username=
server=
disableclipboard=0
disableserverbell=0
profile-lock=0
enable-autostart=0
window_maximize=1
scale=1
disableencryption=0
ssh_tunnel_auth=0
group=
ssh_tunnel_loopback=0
showcursor=1
viewmode=4
notes_text=
keymap=
colordepth=32
proxy=
protocol=VNC
ssh_tunnel_certfile=
shared=0
username=

View File

@ -5,6 +5,7 @@ ARG API_URL="https://api.github.com/repos/macchrome/linchrome/releases/latest"
#
# install custom chromium build from woolyss with support for hevc/x265
SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends wget unzip libatk1.0-0 libatk-bridge2.0-0 libatomic1 \
libcups2 libgtk-3-0 libnss3 libpci3 libxcomposite1 libxss1 openbox xz-utils jq; \
@ -21,11 +22,27 @@ RUN set -eux; apt-get update; \
#
# install widevine module
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/$WIDEVINE_VERSION-linux-x64.zip"; \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \
rm /tmp/widevine.zip; \
#
# install latest version of uBlock Origin and SponsorBlock for YouTube
CHROMIUM_VERSION="$(wget -O - "${API_URL}" 2>/dev/null | jq -r ".tag_name" | sed -e 's/v//' -e 's/-.*//')"; \
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
EXTENSIONS=( \
cjpalhdlnbpafiamejdnhcphjbkeiagm \
mnjggcdmjocbbbhaepdhchncahnbgone \
); \
mkdir -p "${EXTENSIONS_DIR}"; \
for EXT_ID in "${EXTENSIONS[@]}"; \
do \
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
wget -O "${EXT_PATH}" "${EXT_URL}"; \
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \
echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \
done; \
# clean up
apt-get --purge autoremove -y xz-utils jq; \
apt-get clean -y; \
@ -37,7 +54,3 @@ COPY supervisord.conf /etc/neko/supervisord/ungoogled-chromium.conf
COPY preferences.json /usr/lib/chromium/master_preferences
COPY policies.json /etc/chromium/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml
#
# copy extensions and policy files
COPY extensions /usr/share/chromium/extensions

View File

@ -1,4 +0,0 @@
{
"external_crx": "/usr/share/chromium/extensions/cjpalhdlnbpafiamejdnhcphjbkeiagm.crx",
"external_version": "1.38.6"
}

View File

@ -1,4 +0,0 @@
{
"external_crx": "/usr/share/chromium/extensions/fjoaledfpmneenckfbpdfhkmimnjocfa.crx",
"external_version": "2.32.0"
}

View File

@ -1,6 +1,4 @@
{
"HomepageLocation": "",
"AutoFillEnabled": false,
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
@ -20,15 +18,15 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLBlacklist": [
"URLBlocklist": [
"file://*",
"chrome://policy"
],
"ExtensionInstallWhitelist": [
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"fjoaledfpmneenckfbpdfhkmimnjocfa"
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlacklist": [
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -0,0 +1,42 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
ARG VIVALDI_VERSION="5.3.2679.34-1"
# TODO: Get chromium version from vivaldi
ARG CHROMIUM_VERSION="102.0.5005.72"
#
# install vivaldi
SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \
wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${VIVALDI_VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends wget unzip xz-utils jq openbox /tmp/vivaldi.deb; \
/opt/vivaldi/update-ffmpeg; \
#
# install latest version of uBlock Origin and SponsorBlock for YouTube
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
EXTENSIONS=( \
cjpalhdlnbpafiamejdnhcphjbkeiagm \
mnjggcdmjocbbbhaepdhchncahnbgone \
); \
mkdir -p "${EXTENSIONS_DIR}"; \
for EXT_ID in "${EXTENSIONS[@]}"; \
do \
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
wget -O "${EXT_PATH}" "${EXT_URL}"; \
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \
echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \
done; \
#
# clean up
apt-get --purge autoremove -y xz-utils jq; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/vivaldi-browser.conf
COPY --chown=neko preferences.json /home/neko/.config/vivaldi/Default/Preferences
COPY policies.json /etc/opt/vivaldi/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

763
.docker/vivaldi/openbox.xml Normal file
View File

@ -0,0 +1,763 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Default openbox config but all window decorations are moved
thereby making it harder to accidentally close the virtual browser -->
<openbox_config xmlns="http://openbox.org/3.4/rc"
xmlns:xi="http://www.w3.org/2001/XInclude">
<resistance>
<strength>10</strength>
<screen_edge_strength>20</screen_edge_strength>
</resistance>
<applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="Vivaldi-stable*" name="vivaldi-stable*" role="browser">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>
<layer>normal</layer>
</application>
</applications>
<focus>
<focusNew>yes</focusNew>
<!-- always try to focus new windows when they appear. other rules do
apply -->
<followMouse>no</followMouse>
<!-- move focus to a window when you move the mouse into it -->
<focusLast>yes</focusLast>
<!-- focus the last used window when changing desktops, instead of the one
under the mouse pointer. when followMouse is enabled -->
<underMouse>no</underMouse>
<!-- move focus under the mouse, even when the mouse is not moving -->
<focusDelay>200</focusDelay>
<!-- when followMouse is enabled, the mouse must be inside the window for
this many milliseconds (1000 = 1 sec) before moving focus to it -->
<raiseOnFocus>no</raiseOnFocus>
<!-- when followMouse is enabled, and a window is given focus by moving the
mouse into it, also raise the window -->
</focus>
<placement>
<policy>Smart</policy>
<!-- 'Smart' or 'UnderMouse' -->
<center>yes</center>
<!-- whether to place windows in the center of the free area found or
the top left corner -->
<monitor>Primary</monitor>
<!-- with Smart placement on a multi-monitor system, try to place new windows
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
the active window is, 'Primary' - only on the primary monitor -->
<primaryMonitor>1</primaryMonitor>
<!-- The monitor where Openbox should place popup dialogs such as the
focus cycling popup, or the desktop switch popup. It can be an index
from 1, specifying a particular monitor. Or it can be one of the
following: 'Mouse' - where the mouse is, or
'Active' - where the active window is -->
</placement>
<theme>
<name>Clearlooks</name>
<titleLayout>NLIMC</titleLayout>
<!--
available characters are NDSLIMC, each can occur at most once.
N: window icon
L: window label (AKA title).
I: iconify
M: maximize
C: close
S: shade (roll up/down)
D: omnipresent (on all desktops).
-->
<keepBorder>yes</keepBorder>
<animateIconify>yes</animateIconify>
<font place="ActiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuHeader">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuItem">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="ActiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
</theme>
<desktops>
<!-- this stuff is only used at startup, pagers allow you to change them
during a session
these are default values to use when other ones are not already set
by other applications, or saved in your session
use obconf if you want to change these without having to log out
and back in -->
<number>1</number>
<firstdesk>1</firstdesk>
<names>
<!-- set names up here if you want to, like this:
<name>desktop 1</name>
<name>desktop 2</name>
-->
</names>
<popupTime>875</popupTime>
<!-- The number of milliseconds to show the popup for when switching
desktops. Set this to 0 to disable the popup. -->
</desktops>
<resize>
<drawContents>yes</drawContents>
<popupShow>Nonpixel</popupShow>
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
<popupPosition>Center</popupPosition>
<!-- 'Center', 'Top', or 'Fixed' -->
<popupFixedPosition>
<!-- these are used if popupPosition is set to 'Fixed' -->
<x>10</x>
<!-- positive number for distance from left edge, negative number for
distance from right edge, or 'Center' -->
<y>10</y>
<!-- positive number for distance from top edge, negative number for
distance from bottom edge, or 'Center' -->
</popupFixedPosition>
</resize>
<!-- You can reserve a portion of your screen where windows will not cover when
they are maximized, or when they are initially placed.
Many programs reserve space automatically, but you can use this in other
cases. -->
<margins>
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
</margins>
<dock>
<position>TopLeft</position>
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
<floatingX>0</floatingX>
<floatingY>0</floatingY>
<noStrut>no</noStrut>
<stacking>Above</stacking>
<!-- 'Above', 'Normal', or 'Below' -->
<direction>Vertical</direction>
<!-- 'Vertical' or 'Horizontal' -->
<autoHide>no</autoHide>
<hideDelay>300</hideDelay>
<!-- in milliseconds (1000 = 1 second) -->
<showDelay>300</showDelay>
<!-- in milliseconds (1000 = 1 second) -->
<moveButton>Middle</moveButton>
<!-- 'Left', 'Middle', 'Right' -->
</dock>
<keyboard>
<chainQuitKey>C-g</chainQuitKey>
<!-- Keybindings for desktop switching -->
<keybind key="C-A-Left">
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Right">
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Up">
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Down">
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Left">
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Right">
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Up">
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Down">
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="W-F1">
<action name="GoToDesktop"><to>1</to></action>
</keybind>
<keybind key="W-F2">
<action name="GoToDesktop"><to>2</to></action>
</keybind>
<keybind key="W-F3">
<action name="GoToDesktop"><to>3</to></action>
</keybind>
<keybind key="W-F4">
<action name="GoToDesktop"><to>4</to></action>
</keybind>
<keybind key="W-d">
<action name="ToggleShowDesktop"/>
</keybind>
<!-- Keybindings for windows -->
<keybind key="A-F4">
<action name="Close"/>
</keybind>
<keybind key="A-Escape">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</keybind>
<keybind key="A-space">
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
</keybind>
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
<keybind key="A-Print">
<action name="Execute"><command>scrot -s</command></action>
</keybind>
<!-- Keybindings for window switching -->
<keybind key="A-Tab">
<action name="NextWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="A-S-Tab">
<action name="PreviousWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="C-A-Tab">
<action name="NextWindow">
<panels>yes</panels><desktop>yes</desktop>
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<!-- Keybindings for window switching with the arrow keys -->
<keybind key="W-S-Right">
<action name="DirectionalCycleWindows">
<direction>right</direction>
</action>
</keybind>
<keybind key="W-S-Left">
<action name="DirectionalCycleWindows">
<direction>left</direction>
</action>
</keybind>
<keybind key="W-S-Up">
<action name="DirectionalCycleWindows">
<direction>up</direction>
</action>
</keybind>
<keybind key="W-S-Down">
<action name="DirectionalCycleWindows">
<direction>down</direction>
</action>
</keybind>
<!-- Keybindings for running applications -->
<keybind key="W-e">
<action name="Execute">
<startupnotify>
<enabled>true</enabled>
<name>Konqueror</name>
</startupnotify>
<command>kfmclient openProfile filemanagement</command>
</action>
</keybind>
<!-- Launch scrot when Print is pressed -->
<keybind key="Print">
<action name="Execute"><command>scrot</command></action>
</keybind>
</keyboard>
<mouse>
<dragThreshold>1</dragThreshold>
<!-- number of pixels the mouse must move before a drag begins -->
<doubleClickTime>500</doubleClickTime>
<!-- in milliseconds (1000 = 1 second) -->
<screenEdgeWarpTime>400</screenEdgeWarpTime>
<!-- Time before changing desktops when the pointer touches the edge of the
screen while moving a window, in milliseconds (1000 = 1 second).
Set this to 0 to disable warping -->
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
<!-- Set this to TRUE to move the mouse pointer across the desktop when
switching due to hitting the edge of the screen -->
<context name="Frame">
<mousebind button="A-Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="A-Left" action="Click">
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="A-Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Right" action="Drag">
<action name="Resize"/>
</mousebind>
<mousebind button="A-Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-S-Up" action="Click">
<action name="SendToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-S-Down" action="Click">
<action name="SendToDesktop"><to>next</to></action>
</mousebind>
</context>
<context name="Titlebar">
<mousebind button="Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="Left" action="DoubleClick">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Up" action="Click">
<action name="if">
<shaded>no</shaded>
<then>
<action name="Shade"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
<action name="Lower"/>
</then>
</action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="if">
<shaded>yes</shaded>
<then>
<action name="Unshade"/>
<action name="Raise"/>
</then>
</action>
</mousebind>
</context>
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="Top">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>top</edge></action>
</mousebind>
</context>
<context name="Left">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>left</edge></action>
</mousebind>
</context>
<context name="Right">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>right</edge></action>
</mousebind>
</context>
<context name="Bottom">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>bottom</edge></action>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="TRCorner BRCorner TLCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Drag">
<action name="Resize"/>
</mousebind>
</context>
<context name="Client">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Icon">
<!--mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="AllDesktops">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleOmnipresent"/>
</mousebind>
</context>
<context name="Shade">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleShade"/>
</mousebind>
</context>
<context name="Iconify">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Iconify"/>
</mousebind>
</context>
<context name="Maximize">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Middle" action="Click">
<action name="ToggleMaximize"><direction>vertical</direction></action>
</mousebind>
<mousebind button="Right" action="Click">
<action name="ToggleMaximize"><direction>horizontal</direction></action>
</mousebind>
</context>
<context name="Close">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Close"/>
</mousebind>
</context>
<context name="Desktop">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Root">
<!-- Menus -->
<!--mousebind button="Middle" action="Press">
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="ShowMenu"><menu>root-menu</menu></action>
</mousebind-->
</context>
<context name="MoveResize">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
</context>
</mouse>
<menu>
<!-- You can specify more than one menu file in here and they are all loaded,
just don't make menu ids clash or, well, it'll be kind of pointless -->
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
<!-- system menu files on Debian systems -->
<!--file>/var/lib/openbox/debian-menu.xml</file-->
<file>menu.xml</file>
<hideDelay>200</hideDelay>
<!-- if a press-release lasts longer than this setting (in milliseconds), the
menu is hidden again -->
<middle>no</middle>
<!-- center submenus vertically about the parent entry -->
<submenuShowDelay>100</submenuShowDelay>
<!-- time to delay before showing a submenu after hovering over the parent
entry.
if this is a negative value, then the delay is infinite and the
submenu will not be shown until it is clicked on -->
<submenuHideDelay>400</submenuHideDelay>
<!-- time to delay before hiding a submenu when selecting another
entry in parent menu
if this is a negative value, then the delay is infinite and the
submenu will not be hidden until a different submenu is opened -->
<showIcons>yes</showIcons>
<!-- controls if icons appear in the client-list-(combined-)menu -->
<manageDesktops>yes</manageDesktops>
<!-- show the manage desktops section in the client-list-(combined-)menu -->
</menu>
<applications>
<!--
# this is an example with comments through out. use these to make your
# own rules, but without the comments of course.
# you may use one or more of the name/class/role/title/type rules to specify
# windows to match
<application name="the window's _OB_APP_NAME property (see obxprop)"
class="the window's _OB_APP_CLASS property (see obxprop)"
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
role="the window's _OB_APP_ROLE property (see obxprop)"
title="the window's _OB_APP_TITLE property (see obxprop)"
type="the window's _OB_APP_TYPE property (see obxprob)..
(if unspecified, then it is 'dialog' for child windows)">
# you may set only one of name/class/role/title/type, or you may use more
# than one together to restrict your matches.
# the name, class, role, and title use simple wildcard matching such as those
# used by a shell. you can use * to match any characters and ? to match
# any single character.
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
# or desktop
# when multiple rules match a window, they will all be applied, in the
# order that they appear in this list
# each rule element can be left out or set to 'default' to specify to not
# change that attribute of the window
<decor>yes</decor>
# enable or disable window decorations
<shade>no</shade>
# make the window shaded when it appears, or not
<position force="no">
# the position is only used if both an x and y coordinate are provided
# (and not set to 'default')
# when force is "yes", then the window will be placed here even if it
# says you want it placed elsewhere. this is to override buggy
# applications who refuse to behave
<x>center</x>
# a number like 50, or 'center' to center on screen. use a negative number
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
# the right edge (or bottom). use 'default' to specify using value
# provided by the application, or chosen by openbox, instead.
<y>200</y>
<monitor>1</monitor>
# specifies the monitor in a xinerama setup.
# 1 is the first head, or 'mouse' for wherever the mouse is
</position>
<size>
# the size to make the window.
<width>20</width>
# a number like 20, or 'default' to use the size given by the application.
# you can use fractions such as 1/2 or percentages such as 75% in which
# case the value is relative to the size of the monitor that the window
# appears on.
<height>30%</height>
</size>
<focus>yes</focus>
# if the window should try be given focus when it appears. if this is set
# to yes it doesn't guarantee the window will be given focus. some
# restrictions may apply, but Openbox will try to
<desktop>1</desktop>
# 1 is the first desktop, 'all' for all desktops
<layer>normal</layer>
# 'above', 'normal', or 'below'
<iconic>no</iconic>
# make the window iconified when it appears, or not
<skip_pager>no</skip_pager>
# asks to not be shown in pagers
<skip_taskbar>no</skip_taskbar>
# asks to not be shown in taskbars. window cycling actions will also
# skip past such windows
<fullscreen>yes</fullscreen>
# make the window in fullscreen mode when it appears
<maximized>true</maximized>
# 'Horizontal', 'Vertical' or boolean (yes/no)
</application>
# end of the example
-->
</applications>
</openbox_config>

View File

@ -0,0 +1,36 @@
{
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"BrowserSignin": 0,
"DefaultNotificationsSetting": 2,
"DeveloperToolsAvailability": 2,
"EditBookmarksEnabled": false,
"FullscreenAllowed": true,
"IncognitoModeAvailability": 1,
"SyncDisabled": true,
"AutoplayAllowed": true,
"BrowserAddPersonEnabled": false,
"BrowserGuestModeEnabled": false,
"DefaultPopupsSetting": 2,
"DownloadRestrictions": 3,
"VideoCaptureAllowed": true,
"AllowFileSelectionDialogs": false,
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLBlocklist": [
"file://*",
"chrome://policy"
],
"ExtensionInstallForcelist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
"mnjggcdmjocbbbhaepdhchncahnbgone;https://clients2.google.com/service/update2/crx"
],
"ExtensionInstallAllowlist": [
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
"mnjggcdmjocbbbhaepdhchncahnbgone"
],
"ExtensionInstallBlocklist": [
"*"
]
}

View File

@ -0,0 +1,339 @@
{
"homepage": "http://www.google.com",
"homepage_is_newtabpage": false,
"first_run_tabs": [
"https://www.google.com/_/chrome/newtab?ie=UTF-8"
],
"custom_links": {
"initialized": true,
"list": [
{
"title": "YouTube",
"url": "https://www.youtube.com/"
},
{
"title": "Netflix",
"url": "https://netflix.com"
},
{
"title": "Hulu",
"url": "https://www.hulu.com/"
},
{
"title": "9Anime",
"url": "https://9anime.to/"
},
{
"title": "Crunchy Roll",
"url": "https://www.crunchyroll.com/"
},
{
"title": "Funimation",
"url": "https://www.funimation.com/"
},
{
"title": "Disney+",
"url": "https://www.disneyplus.com/"
},
{
"title": "HBO Now",
"url": "https://play.hbonow.com/"
},
{
"title": "Amazon Video",
"url": "https://www.amazon.com/Amazon-Video/b?node=2858778011"
},
{
"title": "VRV",
"url": "https://vrv.co/"
},
{
"title": "Twitch",
"url": "https://www.twitch.tv/"
},
{
"title": "Mixer",
"url": "https://mixer.com/"
}
]
},
"browser": {
"custom_chrome_frame": false,
"show_home_button": true,
"window_placement": {
"maximized": true
}
},
"bookmark_bar": {
"show_on_all_tabs": false
},
"sync_promo": {
"show_on_first_run_allowed": false
},
"default_search_provider_data": {
"image_template_url_data": {
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://www.google.com/favicon.ico",
"id": "11",
"image_url": "https://www.google.com/searchbyimage/upload",
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "g",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
"preconnect_to_search_url": false,
"prepopulate_id": 1,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "Google",
"side_search_param": "",
"suggestions_url": "",
"suggestions_url_post_params": "",
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
"usage_count": 0
},
"private_template_url_data": {
"alternate_urls": [],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://duckduckgo.com/favicon.ico",
"id": "4",
"image_url": "",
"image_url_post_params": "",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "d",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "Ih3bMkFheVB5WitEdGZmeU9hV0d5K3RGUXRhR3RFPQ==",
"preconnect_to_search_url": false,
"prepopulate_id": 7,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "DuckDuckGo",
"side_search_param": "",
"suggestions_url": "https://duckduckgo.com/ac/?q={searchTerms}&type=list",
"suggestions_url_post_params": "",
"url": "https://duckduckgo.com/?q={searchTerms}&{ddg:Referral}",
"usage_count": 0
},
"speeddials_template_url_data": {
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://www.google.com/favicon.ico",
"id": "11",
"image_url": "https://www.google.com/searchbyimage/upload",
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "g",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
"preconnect_to_search_url": false,
"prepopulate_id": 1,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "Google",
"side_search_param": "",
"suggestions_url": "",
"suggestions_url_post_params": "",
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
"usage_count": 0
},
"template_url_data": {
"alternate_urls": ["https://www.google.com/#q={searchTerms}", "https://www.google.com/search#q={searchTerms}", "https://www.google.com/webhp#q={searchTerms}", "https://www.google.com/s#q={searchTerms}", "https://www.google.com/s?q={searchTerms}"],
"contextual_search_url": "",
"created_by_policy": false,
"created_from_play_api": false,
"date_created": "0",
"doodle_url": "",
"favicon_url": "https://www.google.com/favicon.ico",
"id": "11",
"image_url": "https://www.google.com/searchbyimage/upload",
"image_url_post_params": "encoded_image={google:imageThumbnail},image_url={google:imageURL},sbisrc={google:imageSearchSource},original_width={google:imageOriginalWidth},original_height={google:imageOriginalHeight}",
"input_encodings": ["UTF-8"],
"is_active": 0,
"keyword": "g",
"last_modified": "0",
"last_visited": "0",
"logo_url": "",
"new_tab_url": "",
"originating_url": "",
"position": "IhxEc1dKb3ZJanppM1NCWTZhRHNGN0ljYkRHeVk9",
"preconnect_to_search_url": false,
"prepopulate_id": 1,
"safe_for_autoreplace": true,
"search_url_post_params": "",
"short_name": "Google",
"side_search_param": "",
"suggestions_url": "",
"suggestions_url_post_params": "",
"url": "https://www.google.com/search?q={searchTerms}&{google:originalQueryForSuggestion}{google:prefetchSource}{google:sourceId}{google:contextualSearchVersion}ie={inputEncoding}",
"usage_count": 0
}
},
"distribution": {
"import_bookmarks_from_file": "bookmarks.html",
"import_bookmarks": true,
"import_history": true,
"import_home_page": true,
"import_search_engine": true,
"ping_delay": 60,
"do_not_create_desktop_shortcut": true,
"do_not_create_quick_launch_shortcut": true,
"do_not_create_taskbar_shortcut": true,
"do_not_launch_chrome": true,
"do_not_register_for_update_launch": true,
"make_chrome_default": true,
"make_chrome_default_for_user": true,
"system_level": false,
"verbose_logging": false
},
"enable_do_not_track": true,
"profile": {
"avatar_index": 34,
"default_content_setting_values": {
"clipboard": 2,
"cookies": 4,
"geolocation": 2,
"media_stream_camera": 2,
"media_stream_mic": 2,
"midi_sysex": 2,
"payment_handler": 2,
"usb_guard": 2
},
"name": "Neko",
"using_default_avatar": false,
"using_default_name": false,
"using_gaia_avatar": false
},
"signin": {
"allowed": false
},
"vivaldi": {
"address_bar": {
"autocomplete": {
"enabled": false
},
"omnibox": {
"show_browser_history": false,
"show_search_history": false,
"show_typed_history": false
},
"search": {
"display": 1,
"in_new_tab": false
},
"show_full_url": true,
"show_qr_generator": true,
"visible": true
},
"bookmarks": {
"deleted_partners": [
"f79cd6e8-ebc0-444d-ac96-00da456dcb59",
"d680347f-1073-46b9-a546-ae0238e7b9d9"
],
"language": "en-US",
"version": "24"
},
"downloads": {
"notify_on_complete": false,
"open_panel_on_new": false,
"start_automatically": false,
"update_default_download_when_saving_as": false
},
"history": {
"days_to_keep_visits": 0
},
"homepage": "vivaldi://startpage",
"incognito": {
"show_intro": false
},
"language_at_install": "en-US",
"menu": {
"icon_type": 1
},
"mouse_gestures": {
"enabled": false,
"rocker_gestures": {
"enabled": false
}
},
"popups": {
"show_in_tab": true
},
"settings": {
"in_tab": true
},
"startpage": {
"navigation": 1,
"speed_dial": {
"add_button_visible": false,
"allow_dnd": false,
"columns": 4,
"delete_visible": false,
"display_search": true,
"privacy_stats_show": false,
"tracker_suggestion_show": false,
"width": 170
}
},
"startup": {
"check_is_default": false,
"has_seen_feature": 1
},
"status_bar": {
"display": 0,
"minimized": 0
},
"system": {
"show_exit_confirmation_dialog": true
},
"tabs": {
"new_placement": 3,
"open_new_in_background": false,
"stacking": {
"open_accordions": []
}
},
"theme": {
"schedule": {
"o_s": {
"dark": "Vivaldi2",
"light": "Vivaldi2"
}
},
"use_animation": false
},
"translate": {
"enabled": false
},
"windows": {
"use_native_decoration": true
}
}
}

View File

@ -0,0 +1,22 @@
[program:vivaldi-stable]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/vivaldi-stable --no-sandbox --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/vivaldi --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/vivaldi.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -1,18 +0,0 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
#
# install vncviewer
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends openbox xtightvncviewer; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
ENV NEKO_VNC_URL=""
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/vncviewer.conf
COPY openbox.xml /etc/neko/openbox.xml

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [ m1k1o ]

View File

@ -47,8 +47,10 @@ jobs:
if: github.repository_owner == 'm1k1o'
needs: [ build-base ]
strategy:
# Will build all images even if some fail.
fail-fast: false
matrix:
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, tor-browser, vncviewer, vlc, xfce ]
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce ]
env:
DOCKER_TAG: ${{ matrix.tags }}
steps:

View File

@ -63,6 +63,7 @@ jobs:
if: github.repository_owner == 'm1k1o'
needs: [ build-base ]
strategy:
# Will build all images even if some fail.
matrix:
include:
- tag: firefox
@ -77,10 +78,14 @@ jobs:
platforms: linux/amd64
- tag: brave
platforms: linux/amd64
- tag: vivaldi
platforms: linux/amd64
- tag: opera
platforms: linux/amd64
- tag: tor-browser
platforms: linux/amd64,linux/arm64
- tag: vncviewer
platforms: linux/amd64,linux/arm64
- tag: remmina
platforms: linux/amd64
- tag: vlc
platforms: linux/amd64,linux/arm64
- tag: xfce

104
README.md
View File

@ -1,12 +1,23 @@
<div align="center">
<a href="https://github.com/m1k1o/neko" title="Neko's Github repository.">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="450" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="400" height="auto"/>
</a>
<p align="center">
<img src="https://img.shields.io/github/v/release/m1k1o/neko" alt="release">
<img src="https://img.shields.io/github/license/m1k1o/neko" alt="license">
<img src="https://img.shields.io/docker/pulls/m1k1o/neko" alt="pulls">
<img src="https://img.shields.io/github/issues/m1k1o/neko" alt="issues">
<a href="https://github.com/m1k1o/neko/releases">
<img src="https://img.shields.io/github/v/release/m1k1o/neko" alt="release">
</a>
<a href="https://github.com/m1k1o/neko/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/m1k1o/neko" alt="license">
</a>
<a href="https://hub.docker.com/u/m1k1o/neko">
<img src="https://img.shields.io/docker/pulls/m1k1o/neko" alt="pulls">
</a>
<a href="https://github.com/m1k1o/neko/issues">
<img src="https://img.shields.io/github/issues/m1k1o/neko" alt="issues">
</a>
<a href="https://github.com/sponsors/m1k1o">
<img src="https://img.shields.io/badge/-sponsor-red" alt="issues">
</a>
<a href="https://discord.gg/3U6hWpC">
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
</a>
@ -14,27 +25,86 @@
<img src="https://github.com/m1k1o/neko/actions/workflows/build.yml/badge.svg" alt="build">
</a>
</p>
<br/>
<br/>
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
<br/>
<br/>
</div>
# n.eko
This app uses Web RTC to stream a desktop inside of a docker container, original author made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and his internet could not handle streaming and discord kept crashing when his friend attempted to. He just wanted to watch anime with his friends ლ(ಠ益ಠლ) so he started digging throughout the internet and found a few *kinda* clones, but none of them had the virtual browser, then he found [Turtus](https://github.com/Khauri/Turtus) and he was able to figure out the rest.
This app uses WebRTC to stream a desktop inside of a docker container, original author made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and his internet could not handle streaming and discord kept crashing when his friend attempted to. He just wanted to watch anime with his friends ლ(ಠ益ಠლ) so he started digging throughout the internet and found a few *kinda* clones, but none of them had the virtual browser, then he found [Turtus](https://github.com/Khauri/Turtus) and he was able to figure out the rest.
Then I found [this](https://github.com/nurdism/neko) project and started to dig into it. I really liked the idea of having collaborative browser browsing together with mutliple people, so I created a fork. Initially, I wanted to merge my changes to the upstream repository, but the original author did not have time for this project anymore and it got eventually archived.
## Use-cases and comparison
Neko started as a virtual browser that is streamed using WebRTC to multiple users.
- It is **not only limited to a browser**; it can run anything that runs on linux (e.g. VLC). Browser only happens to be the most popular and widely used use-case.
- In fact, it is not limited to a single program either; you can install a full desktop environment (e.g. XFCE).
- Speaking of limits, it does not need to run in a container; you could install neko on your host, connect to your X server and control your whole VM.
- Theoretically it is not limited to only X server, anything that can be controlled and scraped periodically for images could be used instead.
- Like implementing RDP or VNC protocol, where neko would only act as WebRTC relay server. This is currently only future.
Primary use case is connecting with multiple people, leveraging real time synchronization and interactivity:
- **Watch party** - watching video content together with multiple people and reacting to it (chat, emotes) - open source alternative to [giggl.app](https://giggl.app/).
- **Interactive presentation** - not only screen sharing, but others can control the screen.
- **Collaborative tool** - brainstorming ideas, cobrowsing, code debugging together.
- **Support/Teaching** - interactively guiding people in controlled environment.
- **Embed anything** - embed virtual browser in your web app - open source alternative to [hyperbeam](https://hyperbeam.com/).
- open any third-party website or application, synchronize audio and video flawlessly among multiple participants.
- request rooms using API with [neko-rooms](https://github.com/m1k1o/neko-rooms).
Other use cases that benefit from single-user:
- **Personal workspace** - streaming containerized apps and desktops to end-users - similar to [kasm](https://www.kasmweb.com/).
- **Persistent browser** - own browser with persistent cookies available anywhere - similar to [mightyapp](https://www.mightyapp.com/).
- no state is left on the host browser after terminating the connection.
- sensitive data like cookies are not transferred - only video is shared.
- **Throwaway browser** - a better solution for planning secret parties and buying birthday gifts off the internet.
- use Tor Browser and [VPN](https://github.com/m1k1o/neko-vpn) for additional anonymity.
- mitigates risk of OS fingerprinting and browser vulnerabilities by running in container.
- **Session broadcasting** - broadcast room content using RTMP (to e.g. twitch or youtube...).
- **Session recording** - broadcast RTMP can be saved to a file using e.g. [nginx-rtmp](https://www.nginx.com/products/nginx/modules/rtmp-media-streaming/)
- have clean environment when recording tutorials.
- no need to hide bookmarks or use incognito mode.
- **Jump host** - access your internal applications securely without the need for VPN.
- **Automated browser** - you can install [playwright](https://playwright.dev/) or [puppeteer](https://pptr.dev/) and automate tasks while being able to actively intercept them.
Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://guacamole.apache.org/) or [websockify](https://github.com/novnc/websockify) with [noVNC](https://novnc.com/)), installed with remote desktop server along with desired program (e.g. [linuxserver/firefox](https://docs.linuxserver.io/images/docker-firefox)) provides neko additionally:
- **Smooth video** because it uses WebRTC and not images sent over WebSockets.
- **Built in audio** support, what is not part of Apache Guacamole or noVNC.
- **Multi-participant control**, what is not natively supported by Apache Guacamole or noVNC.
### Supported browsers
<div align="center">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/firefox.svg" title="m1k1o/neko:firefox" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/google-chrome.svg" title="m1k1o/neko:google-chrome" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/chromium.svg" title="m1k1o/neko:chromium" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/microsoft-edge.svg" title="m1k1o/neko:microsoft-edge" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/brave.svg" title="m1k1o/neko:brave" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vivaldi.svg" title="m1k1o/neko:vivaldi" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/opera.svg" title="m1k1o/neko:opera" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/tor-browser.svg" title="m1k1o/neko:tor-browser" width="60" height="auto"/>
</div>
### Other programs
<div align="center">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
... others in <a href="https://github.com/m1k1o/neko-apps">m1k1o/neko-apps</a>
</div>
### Features
* Text Chat (With basic markdown support, discord flavor)
* Admin users (Kick, Ban & Force Give/Release Controls)
* Admin users (Kick, Ban & Force Give/Release Controls, Lock room)
* Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
* Emote overlay
* Ignore user (chat and emotes)
* Persistent settings
* Automatic Login with custom url args. (add `?usr=<your-user-name>&pwd=<room-pass>` to the url.)
* Broadcasting room content using RTMP (to e.g. twitch or youtube...)
### Why n.eko?
@ -42,13 +112,13 @@ I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd.
***But why the cat butt?*** Because cats are *assholes*, but you love them anyways.
# Multiple rooms
## Multiple rooms
For n.eko room management software, visit [neko-rooms](https://github.com/m1k1o/neko-rooms).
It also offers zero-knowledge [installation script](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation).
It also offers zero-knowledge [installation script (with HTTPS and Traefik)](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation-with-https-and-traefik).
# Documentation
## Documentation
* [Getting Started](https://neko.m1k1o.net/#/getting-started/)
* [Quick Start](https://neko.m1k1o.net/#/getting-started/quick-start)
@ -62,6 +132,10 @@ It also offers zero-knowledge [installation script](https://github.com/m1k1o/nek
* [Technologies](https://neko.m1k1o.net/#/technologies)
* [Changelog](https://neko.m1k1o.net/#/changelog)
# How to contribute? How to build?
## How to contribute? How to build?
Navigate to [.docker](.docker) folder for further information.
## Support
If you want to support this project, you can do it [here](https://github.com/sponsors/m1k1o).

View File

@ -1,6 +1,6 @@
{
"name": "neko-client",
"version": "2.0.0",
"version": "2.5.0",
"description": "Client for neko streaming server",
"license": "Apache License 2.0",
"author": "Nurdism <https://github.com/nurdism>",
@ -20,50 +20,50 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@fortawesome/fontawesome-free": "^6.1.2",
"animejs": "^3.2.0",
"axios": "^0.21.4",
"date-fns": "^2.28.0",
"date-fns": "^2.29.1",
"emoji-datasource": "^6.0.1",
"eventemitter3": "^4.0.7",
"resize-observer-polyfill": "^1.5.1",
"simple-markdown": "^0.7.2",
"sweetalert2": "^10.15.7",
"sweetalert2": "^11.4.24",
"typed-vuex": "^0.1.21",
"v-tooltip": "^2.0.3",
"vue": "^2.6.14",
"vue": "^2.7.8",
"vue-class-component": "^7.2.6",
"vue-clickaway": "^2.2.2",
"vue-context": "^5.2.0",
"vue-i18n": "^8.27.0",
"vue-i18n": "^8.27.2",
"vue-notification": "^1.3.20",
"vue-property-decorator": "^9.1.2",
"vuex": "^3.5.1"
},
"devDependencies": {
"@types/animejs": "^3.1.4",
"@types/node": "^14.18.9",
"@types/animejs": "^3.1.5",
"@types/node": "^14.18.23",
"@types/vue": "^2.0.0",
"@types/vue-clickaway": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vue/cli-plugin-babel": "^4.5.15",
"@vue/cli-plugin-eslint": "^4.5.15",
"@vue/cli-plugin-typescript": "^4.5.15",
"@vue/cli-plugin-vuex": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@vue/cli-plugin-babel": "^4.5.19",
"@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-plugin-typescript": "^4.5.19",
"@vue/cli-plugin-vuex": "^4.5.19",
"@vue/cli-service": "^4.5.19",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"core-js": "^3.20.3",
"emojilib": "^3.0.4",
"core-js": "^3.24.1",
"emojilib": "^3.0.7",
"eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.20.0",
"prettier": "^2.5.1",
"sass": "^1.49.0",
"sass-loader": "^10.2.1",
"prettier": "^2.7.1",
"sass": "^1.54.0",
"sass-loader": "^10.3.1",
"ts-node": "^9.1.1",
"typescript": "^4.5.5",
"vue-template-compiler": "^2.6.14"
"typescript": "^4.7.4",
"vue-template-compiler": "^2.7.8"
}
}

View File

@ -2,7 +2,7 @@
@use "sass:math";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
$fa-font-size-base: 16px;
$fa-font-display: auto;
$fa-css-prefix: fa;
@ -13,7 +13,7 @@ $fa-fw-width: math.div(20em, 16);
$fa-primary-opacity: 1;
$fa-secondary-opacity: .4;
$fa-family-default: 'Font Awesome 5 Free';
$fa-family-default: 'Font Awesome 6 Free';
// Import FA source files
@import "~@fortawesome/fontawesome-free/scss/brands";

View File

@ -1,44 +1,16 @@
$swal2-white: #fff;
$swal2-black: #000;
@import '~sweetalert2/src/variables';
$swal2-outline-color: transparent;
// CONTAINER
$swal2-container-padding: .625em;
// BOX MODEL
$swal2-width: 32em;
// POPUP
$swal2-padding: 1.25em;
$swal2-border: none;
$swal2-border-radius: .3125em;
$swal2-box-shadow: #d9d9d9;
// ANIMATIONS
$swal2-show-animation: swal2-show .3s;
$swal2-hide-animation: swal2-hide .15s forwards;
// BACKGROUND
$swal2-background: $background-secondary;
// TYPOGRAPHY
$swal2-font: inherit;
$swal2-font-size: 1rem;
// BACKDROP
$swal2-backdrop: rgba($swal2-black, .4);
$swal2-backdrop-transition: background-color .1s;
// ICONS
$swal2-icon-size: 5em;
$swal2-icon-animations: true;
$swal2-icon-margin: 1.25em auto 1.875em;
$swal2-icon-zoom: null;
$swal2-success: #a5dc86;
$swal2-success-border: rgba($swal2-success, .3);
$swal2-error: #f27474;
$swal2-warning: #f8bb86;
$swal2-info: #3fc3ee;
$swal2-question: #87adbd;
$swal2-icon-font-family: inherit;
// IMAGE
$swal2-image-margin: 1.25em auto;
@ -46,143 +18,53 @@ $swal2-image-margin: 1.25em auto;
// TITLE
$swal2-title-margin: 0 0 .4em;
$swal2-title-color: $interactive-hover;
$swal2-title-font-size: 1.875em;
// CONTENT
$swal2-content-justify-content: center;
$swal2-content-margin: 0;
$swal2-content-pading: 0;
$swal2-content-color: $interactive-hover;
$swal2-content-font-size: 1.125em;
$swal2-content-font-weight: normal;
$swal2-content-line-height: normal;
$swal2-content-text-align: center;
$swal2-content-word-wrap: break-word;
// HTML CONTAINER
$swal2-html-container-margin: 0;
$swal2-html-container-color: $interactive-hover;
// INPUT
$swal2-input-margin: 1em auto;
$swal2-input-width: 100%;
$swal2-input-height: 2.625em;
$swal2-input-padding: 0 .75em;
$swal2-input-border: 1px solid lighten($swal2-black, 85);
$swal2-input-border-radius: .1875em;
$swal2-input-box-shadow: inset 0 1px 1px rgba($swal2-black, .06);
$swal2-input-focus-border: 1px solid #b4dbed;
$swal2-input-focus-outline: none;
$swal2-input-focus-box-shadow: 0 0 3px #c4e6f5;
$swal2-input-font-size: 1.125em;
$swal2-input-background: inherit;
$swal2-input-color: inherit;
$swal2-input-transition: border-color .3s, box-shadow .3s;
// TEXTAREA SPECIFIC VARIABLES
$swal2-textarea-height: 6.75em;
$swal2-textarea-padding: .75em;
// VALIDATION MESSAGE
$swal2-validation-message-justify-content: center;
$swal2-validation-message-padding: .625em;
$swal2-validation-message-background: lighten($swal2-black, 94);
$swal2-validation-message-color: lighten($swal2-black, 40);
$swal2-validation-message-font-size: 1em;
$swal2-validation-message-font-weight: 300;
$swal2-validation-message-icon-background: $swal2-error;
$swal2-validation-message-icon-color: $swal2-white;
$swal2-validation-message-icon-zoom: null;
// PROGRESS STEPS
$swal2-progress-steps-background: inherit;
$swal2-progress-steps-margin: 0 0 1.25em;
$swal2-progress-steps-padding: 0;
$swal2-progress-steps-font-weight: 600;
$swal2-progress-steps-distance: 2.5em;
$swal2-progress-step-width: 2em;
$swal2-progress-step-height: 2em;
$swal2-progress-step-border-radius: 2em;
$swal2-progress-step-background: #add8e6;
$swal2-progress-step-color: $swal2-white;
$swal2-active-step-background: #3085d6;
$swal2-active-step-color: $swal2-white;
// FOOTER
$swal2-footer-margin: 1.25em 0 0;
$swal2-footer-padding: 1em 0 0;
$swal2-footer-border-color: #eee;
$swal2-footer-color: lighten($swal2-black, 33);
$swal2-footer-font-size: 1em;
// TIMER POGRESS BAR
$swal2-timer-progress-bar-height: .25em;
$swal2-timer-progress-bar-background: rgba($swal2-black, .2);
// CLOSE BUTTON
$swal2-close-button-width: 1.2em;
$swal2-close-button-height: 1.2em;
$swal2-close-button-line-height: 1.2;
$swal2-close-button-position: absolute;
$swal2-close-button-gap: 0;
$swal2-close-button-transition: color .1s ease-out;
$swal2-close-button-border: none;
$swal2-close-button-border-radius: 0;
$swal2-close-button-outline: initial;
$swal2-close-button-background: transparent;
$swal2-close-button-color: lighten($swal2-black, 80);
$swal2-close-button-font-family: serif;
$swal2-close-button-font-size: 2.5em;
// CLOSE BUTTON:HOVER
$swal2-close-button-hover-transform: none;
$swal2-close-button-hover-color: $swal2-error;
$swal2-close-button-hover-background: transparent;
// ACTIONS
$swal2-actions-flex-wrap: wrap;
$swal2-actions-align-items: center;
$swal2-actions-justify-content: center;
$swal2-actions-width: 100%;
$swal2-actions-margin: 1.25em auto 0;
// CONFIRM BUTTON
$swal2-confirm-button-border: 0;
$swal2-confirm-button-border-radius: .25em;
$swal2-confirm-button-background-color: $background-tertiary;
$swal2-confirm-button-color: $swal2-white;
$swal2-confirm-button-font-size: 1.0625em;
// CANCEL BUTTON
$swal2-cancel-button-border: 0;
$swal2-cancel-button-border-radius: .25em;
$swal2-cancel-button-background-color: $background-floating;
$swal2-cancel-button-color: $swal2-white;
$swal2-cancel-button-font-size: 1.0625em;
// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS
$swal2-button-darken-hover: rgba($swal2-black, .1);
$swal2-button-darken-active: rgba($swal2-black, .2);
$swal2-button-focus-outline: none;
$swal2-button-focus-background-color: null;
$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
// CONFIRM BUTTON
$swal2-confirm-button-background-color: $background-tertiary;
$swal2-confirm-button-font-size: 1.0625em;
$swal2-confirm-button-focus-box-shadow: 0 0 0 3px rgba($swal2-confirm-button-background-color, .5);
// CANCEL BUTTON
$swal2-cancel-button-background-color: $background-floating;
$swal2-cancel-button-font-size: 1.0625em;
$swal2-cancel-button-focus-box-shadow: 0 0 0 3px rgba($swal2-cancel-button-background-color, .5);
// TOASTS
$swal2-toast-show-animation: swal2-toast-show .5s;
$swal2-toast-hide-animation: swal2-toast-hide .1s forwards;
$swal2-toast-border: none;
$swal2-toast-box-shadow: 0 0 .625em #d9d9d9;
$swal2-toast-background: $swal2-white;
$swal2-toast-close-button-width: .8em;
$swal2-toast-close-button-height: .8em;
$swal2-toast-close-button-line-height: .8;
$swal2-toast-width: auto;
$swal2-toast-padding: .625em;
$swal2-toast-title-margin: 0 .6em;
$swal2-toast-title-font-size: 1em;
$swal2-toast-content-font-size: 1em;
$swal2-toast-input-font-size: 1em;
$swal2-toast-validation-font-size: 1em;
$swal2-toast-buttons-font-size: 1em;
$swal2-toast-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
$swal2-toast-footer-margin: .5em 0 0;
$swal2-toast-footer-padding: .5em 0 0;
$swal2-toast-footer-font-size: .8em;
@import "~sweetalert2/src/sweetalert2.scss";
@import "~sweetalert2/src/sweetalert2.scss";

View File

@ -212,7 +212,7 @@
&:before {
color: $background-tertiary;
font-weight: 900;
font-family: 'Font Awesome 5 Free';
font-family: 'Font Awesome 6 Free';
content: '\f3c1';
font-size: 8px;
line-height: 18px;

View File

@ -90,7 +90,7 @@
&::before {
content: '\f002';
font-weight: 900;
font-family: 'Font Awesome 5 Free';
font-family: 'Font Awesome 6 Free';
position: absolute;
width: 15px;
height: 15px;

View File

@ -77,7 +77,7 @@
&.self {
&::before {
font-family: 'Font Awesome 5 Free';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
content: '\f2bd';
background: $background-floating;
@ -97,7 +97,7 @@
&.admin {
&::before {
display: block;
font-family: 'Font Awesome 5 Free';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
content: '\f3ed';
color: $style-primary;
@ -114,7 +114,7 @@
&.host::after {
display: block;
font-family: 'Font Awesome 5 Free';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
content: '\f521';
background: $style-primary;

View File

@ -8,10 +8,11 @@
<neko-emote :id="index" :key="index" />
</template>
</div>
<div
<textarea
ref="overlay"
class="overlay"
tabindex="0"
data-gramm="false"
@click.stop.prevent
@contextmenu.stop.prevent
@wheel.stop.prevent="onWheel"
@ -173,6 +174,12 @@
bottom: 0;
width: 100%;
height: 100%;
cursor: default;
outline: 0;
border: 0;
color: transparent;
background: transparent;
resize: none;
}
.player-aspect {
@ -209,7 +216,7 @@
export default class extends Vue {
@Ref('component') readonly _component!: HTMLElement
@Ref('container') readonly _container!: HTMLElement
@Ref('overlay') readonly _overlay!: HTMLElement
@Ref('overlay') readonly _overlay!: HTMLTextAreaElement
@Ref('aspect') readonly _aspect!: HTMLElement
@Ref('player') readonly _player!: HTMLElement
@Ref('video') readonly _video!: HTMLVideoElement
@ -451,7 +458,7 @@
/* Initialize Guacamole Keyboard */
this.keyboard.onkeydown = (key: number) => {
if (!this.focused || !this.hosting || this.locked) {
if (!this.hosting || this.locked) {
return true
}
@ -459,7 +466,7 @@
return false
}
this.keyboard.onkeyup = (key: number) => {
if (!this.focused || !this.hosting || this.locked) {
if (!this.hosting || this.locked) {
return
}
@ -700,7 +707,6 @@
this.syncClipboard()
}
this._overlay.focus()
this.focused = true
}
@ -724,5 +730,15 @@
this._container.style.maxWidth = `${(this.horizontal / this.vertical) * offsetHeight}px`
this._aspect.style.paddingBottom = `${(this.vertical / this.horizontal) * 100}%`
}
@Watch('focused')
@Watch('hosting')
@Watch('locked')
onFocus() {
// in order to capture key events, overlay must be focused
if (this.focused && this.hosting && !this.locked) {
this._overlay.focus()
}
}
}
</script>

View File

@ -8,6 +8,7 @@ import * as de from './de-de'
import * as ko from './ko-kr'
import * as fi from './fi-fi'
import * as ru from './ru-ru'
import * as cn from './zh-cn'
export const messages = {
en,
@ -20,4 +21,5 @@ export const messages = {
ko,
fi,
ru,
cn,
}

112
client/src/locale/zh-cn.ts Normal file
View File

@ -0,0 +1,112 @@
export const logout = '登出'
export const unsupported = '你的浏览器不支持 WebRTC'
export const admin_loggedin = '您以管理钻身份登陆'
export const you = '你'
export const somebody = '某人'
export const send_a_message = '发送消息'
export const side = {
chat: '聊天',
settings: '设置',
}
export const connect = {
login_title: '登录',
invitation_title: '你已被邀请到这个房间',
displayname: '您的姓名',
password: '密码',
connect: '连接',
error: '登录错误',
empty_displayname: '显示名称不能为空',
}
export const context = {
ignore: '忽略',
unignore: '取消忽略',
mute: '静音',
unmute: '取消静音',
release: '强制释放控制',
take: '牵制控制',
give: '给予控制',
kick: '踢出',
ban: '禁止 IP',
confirm: {
kick_title: '踢出 {name}?',
kick_text: '你确定你要替 {name}?',
ban_title: '禁止 {name}?',
ban_text: '你是否想禁止 {name}? 你将需要重新启动服务器来撤销这一做法.',
mute_title: '静音 {name}?',
mute_text: '你确定你要精印吗 {name}?',
unmute_title: '取消静音 {name}?',
unmute_text: '你想去下静音吗 {name}?',
button_yes: '是',
button_cancel: '取消',
},
}
export const controls = {
release: '释放控制',
request: '请求控制',
lock: '锁定控制',
unlock: '解锁控制',
has: '你有控制',
hasnot: '你没有控制',
}
export const locks = {
control: {
lock: '对所有用户进行锁定控制',
unlock: '对所有用户进行解锁控制',
locked: '锁定的控制装置',
unlocked: '解锁的控制装置',
notif_locked: '为用户锁定控制',
notif_unlocked: '为用户解锁控制',
},
login: {
lock: '所有用户的锁定室',
unlock: '所有用户的解锁室',
locked: '为所有用户锁定的房间',
unlocked: '为所有用户解锁的房间',
notif_locked: '锁上房间',
notif_unlocked: '解锁房间',
},
}
export const setting = {
scroll: '滚动敏感度',
scroll_invert: '反转滚动敏感度',
autoplay: '自动播放视频',
ignore_emotes: '忽略表情符号',
chat_sound: '播放聊天声音',
keyboard_layout: '键盘布局',
broadcast_title: '现场流媒体',
}
export const connection = {
logged_out: '你已登出',
reconnecting: '正在重新连接',
connected: '已连接',
disconnected: '已断开',
kicked: '你已被踢出',
button_confirm: '好的',
}
export const notifications = {
connected: '{name} 已连接',
disconnected: '{name} 已断开',
controls_taken: '{name} 采取了控制',
controls_taken_force: '被迫接受控制',
controls_taken_steal: '掌握控制权 从 {name}',
controls_released: '{name} 释放控制',
controls_released_force: '强制解除控制',
controls_released_steal: '强制解除控制 从 {name}',
controls_given: '将控制权交给 {name}',
controls_has: '{name} 拥有控制权',
controls_has_alt: '但我让那个人知道你想要它',
controls_requesting: '{name} 正在请求控制',
resolution: '将分辨率改为 {width}x{height}@{rate}',
banned: '被禁止 {name}',
kicked: '被踢的 {name}',
muted: '鸟粪 {name}',
unmuted: '取消静音 {name}',
}

View File

@ -1,4 +1,5 @@
/* eslint-disable */
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@ -35,7 +36,9 @@ Guacamole.Keyboard = function Keyboard(element) {
/**
* Reference to this Guacamole.Keyboard.
*
* @private
* @type {!Guacamole.Keyboard}
*/
var guac_keyboard = this;
@ -44,7 +47,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* instance with respect to other Guacamole.Keyboard instances.
*
* @private
* @type {Number}
* @type {!number}
*/
var guacKeyboardID = Guacamole.Keyboard._nextID++;
@ -54,7 +57,7 @@ Guacamole.Keyboard = function Keyboard(element) {
*
* @private
* @constant
* @type {String}
* @type {!string}
*/
var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;
@ -63,9 +66,12 @@ Guacamole.Keyboard = function Keyboard(element) {
* with this Guacamole.Keyboard in focus.
*
* @event
* @param {Number} keysym The keysym of the key being pressed.
* @return {Boolean} true if the key event should be allowed through to the
* browser, false otherwise.
* @param {!number} keysym
* The keysym of the key being pressed.
*
* @return {!boolean}
* true if the key event should be allowed through to the browser,
* false otherwise.
*/
this.onkeydown = null;
@ -74,7 +80,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* with this Guacamole.Keyboard in focus.
*
* @event
* @param {Number} keysym The keysym of the key being released.
* @param {!number} keysym
* The keysym of the key being released.
*/
this.onkeyup = null;
@ -84,14 +91,14 @@ Guacamole.Keyboard = function Keyboard(element) {
* reliably detect that quirk is to platform/browser-sniff.
*
* @private
* @type {Object.<String, Boolean>}
* @type {!Object.<string, boolean>}
*/
var quirks = {
/**
* Whether keyup events are universally unreliable.
*
* @type {Boolean}
* @type {!boolean}
*/
keyupUnreliable: false,
@ -99,7 +106,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* Whether the Alt key is actually a modifier for typable keys and is
* thus never used for keyboard shortcuts.
*
* @type {Boolean}
* @type {!boolean}
*/
altIsTypableOnly: false,
@ -107,7 +114,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* Whether we can rely on receiving a keyup event for the Caps Lock
* key.
*
* @type {Boolean}
* @type {!boolean}
*/
capsLockKeyupUnreliable: false
@ -137,26 +144,75 @@ Guacamole.Keyboard = function Keyboard(element) {
*
* @private
* @constructor
* @param {KeyboardEvent} [orig]
* The relevant DOM keyboard event.
*/
var KeyEvent = function() {
var KeyEvent = function KeyEvent(orig) {
/**
* Reference to this key event.
*
* @private
* @type {!KeyEvent}
*/
var key_event = this;
/**
* The JavaScript key code of the key pressed. For most events (keydown
* and keyup), this is a scancode-like value related to the position of
* the key on the US English "Qwerty" keyboard. For keypress events,
* this is the Unicode codepoint of the character that would be typed
* by the key pressed.
*
* @type {!number}
*/
this.keyCode = orig ? (orig.which || orig.keyCode) : 0;
/**
* The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
*
* @type {!string}
*/
this.keyIdentifier = orig && orig.keyIdentifier;
/**
* The standard name of the key pressed, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {!string}
*/
this.key = orig && orig.key;
/**
* The location on the keyboard corresponding to the key pressed, as
* defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {!number}
*/
this.location = orig ? getEventLocation(orig) : 0;
/**
* The state of all local keyboard modifiers at the time this event was
* received.
*
* @type {!Guacamole.Keyboard.ModifierState}
*/
this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();
/**
* An arbitrary timestamp in milliseconds, indicating this event's
* position in time relative to other events.
*
* @type {Number}
* @type {!number}
*/
this.timestamp = new Date().getTime();
/**
* Whether the default action of this key event should be prevented.
*
* @type {Boolean}
* @type {!boolean}
*/
this.defaultPrevented = false;
@ -165,7 +221,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* by a best-effort guess using available event properties and keyboard
* state.
*
* @type {Number}
* @type {number}
*/
this.keysym = null;
@ -174,7 +230,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* If false, the keysym may still be valid, but it's only a best guess,
* and future key events may be a better source of information.
*
* @type {Boolean}
* @type {!boolean}
*/
this.reliable = false;
@ -182,8 +238,9 @@ Guacamole.Keyboard = function Keyboard(element) {
* Returns the number of milliseconds elapsed since this event was
* received.
*
* @return {Number} The number of milliseconds elapsed since this
* event was received.
* @return {!number}
* The number of milliseconds elapsed since this event was
* received.
*/
this.getAge = function() {
return new Date().getTime() - key_event.timestamp;
@ -199,62 +256,23 @@ Guacamole.Keyboard = function Keyboard(element) {
* @private
* @constructor
* @augments Guacamole.Keyboard.KeyEvent
* @param {Number} keyCode The JavaScript key code of the key pressed.
* @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
* pressed, as defined at:
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
* @param {String} key The standard name of the key pressed, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
* @param {Number} location The location on the keyboard corresponding to
* the key pressed, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
* @param {!KeyboardEvent} orig
* The relevant DOM "keydown" event.
*/
var KeydownEvent = function(keyCode, keyIdentifier, key, location) {
var KeydownEvent = function KeydownEvent(orig) {
// We extend KeyEvent
KeyEvent.apply(this);
/**
* The JavaScript key code of the key pressed.
*
* @type {Number}
*/
this.keyCode = keyCode;
/**
* The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
*
* @type {String}
*/
this.keyIdentifier = keyIdentifier;
/**
* The standard name of the key pressed, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {String}
*/
this.key = key;
/**
* The location on the keyboard corresponding to the key pressed, as
* defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {Number}
*/
this.location = location;
KeyEvent.call(this, orig);
// If key is known from keyCode or DOM3 alone, use that
this.keysym = keysym_from_key_identifier(key, location)
|| keysym_from_keycode(keyCode, location);
this.keysym = keysym_from_key_identifier(this.key, this.location)
|| keysym_from_keycode(this.keyCode, this.location);
/**
* Whether the keyup following this keydown event is known to be
* reliable. If false, we cannot rely on the keyup event to occur.
*
* @type {Boolean}
* @type {!boolean}
*/
this.keyupReliable = !quirks.keyupUnreliable;
@ -264,12 +282,12 @@ Guacamole.Keyboard = function Keyboard(element) {
this.reliable = true;
// Use legacy keyIdentifier as a last resort, if it looks sane
if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier))
this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift);
if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);
// If a key is pressed while meta is held down, the keyup will
// never be sent in Chrome (bug #108404)
if (guac_keyboard.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
this.keyupReliable = false;
// We cannot rely on receiving keyup for Caps Lock on certain platforms
@ -277,21 +295,21 @@ Guacamole.Keyboard = function Keyboard(element) {
this.keyupReliable = false;
// Determine whether default action for Alt+combinations must be prevented
var prevent_alt = !guac_keyboard.modifiers.ctrl && !quirks.altIsTypableOnly;
var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly;
// Determine whether default action for Ctrl+combinations must be prevented
var prevent_ctrl = !guac_keyboard.modifiers.alt;
var prevent_ctrl = !this.modifiers.alt;
// We must rely on the (potentially buggy) keyIdentifier if preventing
// the default action is important
if ((prevent_ctrl && guac_keyboard.modifiers.ctrl)
|| (prevent_alt && guac_keyboard.modifiers.alt)
|| guac_keyboard.modifiers.meta
|| guac_keyboard.modifiers.hyper)
if ((prevent_ctrl && this.modifiers.ctrl)
|| (prevent_alt && this.modifiers.alt)
|| this.modifiers.meta
|| this.modifiers.hyper)
this.reliable = true;
// Record most recently known keysym by associated key code
recentKeysym[keyCode] = this.keysym;
recentKeysym[this.keyCode] = this.keysym;
};
@ -305,24 +323,16 @@ Guacamole.Keyboard = function Keyboard(element) {
* @private
* @constructor
* @augments Guacamole.Keyboard.KeyEvent
* @param {Number} charCode The Unicode codepoint of the character that
* would be typed by the key pressed.
* @param {!KeyboardEvent} orig
* The relevant DOM "keypress" event.
*/
var KeypressEvent = function(charCode) {
var KeypressEvent = function KeypressEvent(orig) {
// We extend KeyEvent
KeyEvent.apply(this);
/**
* The Unicode codepoint of the character that would be typed by the
* key pressed.
*
* @type {Number}
*/
this.charCode = charCode;
KeyEvent.call(this, orig);
// Pull keysym from char code
this.keysym = keysym_from_charcode(charCode);
this.keysym = keysym_from_charcode(this.keyCode);
// Keypress is always reliable
this.reliable = true;
@ -332,68 +342,30 @@ Guacamole.Keyboard = function Keyboard(element) {
KeypressEvent.prototype = new KeyEvent();
/**
* Information related to the pressing of a key, which need not be a key
* Information related to the releasing of a key, which need not be a key
* associated with a printable character. The presence or absence of any
* information within this object is browser-dependent.
*
* @private
* @constructor
* @augments Guacamole.Keyboard.KeyEvent
* @param {Number} keyCode The JavaScript key code of the key released.
* @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
* released, as defined at:
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
* @param {String} key The standard name of the key released, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
* @param {Number} location The location on the keyboard corresponding to
* the key released, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
* @param {!KeyboardEvent} orig
* The relevant DOM "keyup" event.
*/
var KeyupEvent = function(keyCode, keyIdentifier, key, location) {
var KeyupEvent = function KeyupEvent(orig) {
// We extend KeyEvent
KeyEvent.apply(this);
KeyEvent.call(this, orig);
/**
* The JavaScript key code of the key released.
*
* @type {Number}
*/
this.keyCode = keyCode;
/**
* The legacy DOM3 "keyIdentifier" of the key released, as defined at:
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
*
* @type {String}
*/
this.keyIdentifier = keyIdentifier;
/**
* The standard name of the key released, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {String}
*/
this.key = key;
/**
* The location on the keyboard corresponding to the key released, as
* defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {Number}
*/
this.location = location;
// If key is known from keyCode or DOM3 alone, use that
this.keysym = keysym_from_keycode(keyCode, location)
|| keysym_from_key_identifier(key, location); // keyCode is still more reliable for keyup when dead keys are in use
// If key is known from keyCode or DOM3 alone, use that (keyCode is
// still more reliable for keyup when dead keys are in use)
this.keysym = keysym_from_keycode(this.keyCode, this.location)
|| keysym_from_key_identifier(this.key, this.location);
// Fall back to the most recently pressed keysym associated with the
// keyCode if the inferred key doesn't seem to actually be pressed
if (!guac_keyboard.pressed[this.keysym])
this.keysym = recentKeysym[keyCode] || this.keysym;
this.keysym = recentKeysym[this.keyCode] || this.keysym;
// Keyup is as reliable as it will ever be
this.reliable = true;
@ -407,14 +379,16 @@ Guacamole.Keyboard = function Keyboard(element) {
* KeydownEvent, KeypressEvent, and KeyupEvent classes.
*
* @private
* @type {KeyEvent[]}
* @type {!KeyEvent[]}
*/
var eventLog = [];
/**
* Map of known JavaScript keycodes which do not map to typable characters
* to their X11 keysym equivalents.
*
* @private
* @type {!Object.<number, number[]>}
*/
var keycodeKeysyms = {
8: [0xFF08], // backspace
@ -438,9 +412,9 @@ Guacamole.Keyboard = function Keyboard(element) {
40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
91: [0xFFEB], // left window key (hyper_l)
92: [0xFF67], // right window key (menu key?)
93: null, // select key
91: [0xFFE7], // left windows/command key (meta_l)
92: [0xFFE8], // right window/command key (meta_r)
93: [0xFF67], // menu key
96: [0xFFB0], // KP 0
97: [0xFFB1], // KP 1
98: [0xFFB2], // KP 2
@ -476,7 +450,9 @@ Guacamole.Keyboard = function Keyboard(element) {
/**
* Map of known JavaScript keyidentifiers which do not map to typable
* characters to their unshifted X11 keysym equivalents.
*
* @private
* @type {!Object.<string, number[]>}
*/
var keyidentifier_keysym = {
"Again": [0xFF66],
@ -584,14 +560,16 @@ Guacamole.Keyboard = function Keyboard(element) {
"UIKeyInputUpArrow": [0xFF52],
"Up": [0xFF52],
"Undo": [0xFF65],
"Win": [0xFFEB],
"Win": [0xFFE7, 0xFFE7, 0xFFE8],
"Zenkaku": [0xFF28],
"ZenkakuHankaku": [0xFF2A]
};
/**
* All keysyms which should not repeat when held down.
*
* @private
* @type {!Object.<number, boolean>}
*/
var no_repeat = {
0xFE03: true, // ISO Level 3 Shift (AltGr)
@ -604,12 +582,14 @@ Guacamole.Keyboard = function Keyboard(element) {
0xFFE8: true, // Right meta
0xFFE9: true, // Left alt
0xFFEA: true, // Right alt
0xFFEB: true, // Left hyper
0xFFEC: true // Right hyper
0xFFEB: true, // Left super/hyper
0xFFEC: true // Right super/hyper
};
/**
* All modifiers and their states.
*
* @type {!Guacamole.Keyboard.ModifierState}
*/
this.modifiers = new Guacamole.Keyboard.ModifierState();
@ -617,6 +597,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* The state of every key, indexed by keysym. If a particular key is
* pressed, the value of pressed for that keysym will be true. If a key
* is not currently pressed, it will not be defined.
*
* @type {!Object.<number, boolean>}
*/
this.pressed = {};
@ -629,7 +611,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* of the key is explicitly known), it will not be defined.
*
* @private
* @tyle {Object.<Number, Boolean>}
* @type {!Object.<number, boolean>}
*/
var implicitlyPressed = {};
@ -640,6 +622,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* is (theoretically) still pressed.
*
* @private
* @type {!Object.<number, boolean>}
*/
var last_keydown_result = {};
@ -648,20 +631,24 @@ Guacamole.Keyboard = function Keyboard(element) {
* fired. This object maps keycodes to keysyms.
*
* @private
* @type {Object.<Number, Number>}
* @type {!Object.<number, number>}
*/
var recentKeysym = {};
/**
* Timeout before key repeat starts.
*
* @private
* @type {number}
*/
var key_repeat_timeout = null;
/**
* Interval which presses and releases the last key pressed while that
* key is still being held down.
*
* @private
* @type {number}
*/
var key_repeat_interval = null;
@ -671,11 +658,11 @@ Guacamole.Keyboard = function Keyboard(element) {
* undefined.
*
* @private
* @param {Number[]} keysyms
* @param {number[]} keysyms
* An array of keysyms, where the index of the keysym in the array is
* the location value.
*
* @param {Number} location
* @param {!number} location
* The location on the keyboard corresponding to the key pressed, as
* defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*/
@ -691,10 +678,10 @@ Guacamole.Keyboard = function Keyboard(element) {
* Returns true if the given keysym corresponds to a printable character,
* false otherwise.
*
* @param {Number} keysym
* @param {!number} keysym
* The keysym to check.
*
* @returns {Boolean}
* @returns {!boolean}
* true if the given keysym corresponds to a printable character,
* false otherwise.
*/
@ -773,13 +760,13 @@ Guacamole.Keyboard = function Keyboard(element) {
* correct in all cases.
*
* @private
* @param {Number} keyCode
* @param {!number} keyCode
* The keyCode from a browser keydown/keyup event.
*
* @param {String} keyIdentifier
* @param {string} keyIdentifier
* The legacy keyIdentifier from a browser keydown/keyup event.
*
* @returns {Boolean}
* @returns {!boolean}
* true if the keyIdentifier looks sane, false if the keyIdentifier
* appears incorrectly derived or is missing entirely.
*/
@ -816,8 +803,11 @@ Guacamole.Keyboard = function Keyboard(element) {
* not a modifier. The return value of this function depends on the
* return value of the keydown event handler, if any.
*
* @param {Number} keysym The keysym of the key to press.
* @return {Boolean} true if event should NOT be canceled, false otherwise.
* @param {number} keysym
* The keysym of the key to press.
*
* @return {boolean}
* true if event should NOT be canceled, false otherwise.
*/
this.press = function(keysym) {
@ -860,7 +850,8 @@ Guacamole.Keyboard = function Keyboard(element) {
/**
* Marks a key as released, firing the keyup event if registered.
*
* @param {Number} keysym The keysym of the key to release.
* @param {number} keysym
* The keysym of the key to release.
*/
this.release = function(keysym) {
@ -887,7 +878,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* Presses and releases the keys necessary to type the given string of
* text.
*
* @param {String} str
* @param {!string} str
* The string to type.
*/
this.type = function type(str) {
@ -923,27 +914,28 @@ Guacamole.Keyboard = function Keyboard(element) {
};
/**
* Given the remote and local state of a particular key, resynchronizes the
* remote state of that key with the local state through pressing or
* Resynchronizes the remote state of the given modifier with its
* corresponding local modifier state, as dictated by
* {@link KeyEvent#modifiers} within the given key event, by pressing or
* releasing keysyms.
*
* @private
* @param {Boolean} remoteState
* Whether the key is currently pressed remotely.
* @param {!string} modifier
* The name of the {@link Guacamole.Keyboard.ModifierState} property
* being updated.
*
* @param {Boolean} localState
* Whether the key is currently pressed remotely locally. If the state
* of the key is not known, this may be undefined.
* @param {!number[]} keysyms
* The keysyms which represent the modifier being updated.
*
* @param {Number[]} keysyms
* The keysyms which represent the key being updated.
*
* @param {KeyEvent} keyEvent
* @param {!KeyEvent} keyEvent
* Guacamole's current best interpretation of the key event being
* processed.
*/
var updateModifierState = function updateModifierState(remoteState,
localState, keysyms, keyEvent) {
var updateModifierState = function updateModifierState(modifier,
keysyms, keyEvent) {
var localState = keyEvent.modifiers[modifier];
var remoteState = guac_keyboard.modifiers[modifier];
var i;
@ -988,56 +980,50 @@ Guacamole.Keyboard = function Keyboard(element) {
};
/**
* Given a keyboard event, updates the local modifier state and remote
* key state based on the modifier flags within the event. This function
* pays no attention to keycodes.
* Given a keyboard event, updates the remote key state to match the local
* modifier state and remote based on the modifier flags within the event.
* This function pays no attention to keycodes.
*
* @private
* @param {KeyboardEvent} e
* The keyboard event containing the flags to update.
*
* @param {KeyEvent} keyEvent
* @param {!KeyEvent} keyEvent
* Guacamole's current best interpretation of the key event being
* processed.
*/
var syncModifierStates = function syncModifierStates(e, keyEvent) {
// Get state
var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e);
var syncModifierStates = function syncModifierStates(keyEvent) {
// Resync state of alt
updateModifierState(guac_keyboard.modifiers.alt, state.alt, [
updateModifierState('alt', [
0xFFE9, // Left alt
0xFFEA, // Right alt
0xFE03 // AltGr
], keyEvent);
// Resync state of shift
updateModifierState(guac_keyboard.modifiers.shift, state.shift, [
updateModifierState('shift', [
0xFFE1, // Left shift
0xFFE2 // Right shift
], keyEvent);
// Resync state of ctrl
updateModifierState(guac_keyboard.modifiers.ctrl, state.ctrl, [
updateModifierState('ctrl', [
0xFFE3, // Left ctrl
0xFFE4 // Right ctrl
], keyEvent);
// Resync state of meta
updateModifierState(guac_keyboard.modifiers.meta, state.meta, [
updateModifierState('meta', [
0xFFE7, // Left meta
0xFFE8 // Right meta
], keyEvent);
// Resync state of hyper
updateModifierState(guac_keyboard.modifiers.hyper, state.hyper, [
0xFFEB, // Left hyper
0xFFEC // Right hyper
updateModifierState('hyper', [
0xFFEB, // Left super/hyper
0xFFEC // Right super/hyper
], keyEvent);
// Update state
guac_keyboard.modifiers = state;
guac_keyboard.modifiers = keyEvent.modifiers;
};
@ -1047,7 +1033,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* inspection of other key events.
*
* @private
* @returns {Boolean}
* @returns {!boolean}
* true if all currently pressed keys were implicitly pressed, false
* otherwise.
*/
@ -1068,8 +1054,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* can be).
*
* @private
* @return {Boolean} Whether the default action of the latest event should
* be prevented.
* @return {boolean}
* Whether the default action of the latest event should be prevented.
*/
function interpret_events() {
@ -1099,7 +1085,8 @@ Guacamole.Keyboard = function Keyboard(element) {
* looks like a key that may require AltGr.
*
* @private
* @param {Number} keysym The key that was just pressed.
* @param {!number} keysym
* The key that was just pressed.
*/
var release_simulated_altgr = function release_simulated_altgr(keysym) {
@ -1149,6 +1136,29 @@ Guacamole.Keyboard = function Keyboard(element) {
var keysym = null;
var accepted_events = [];
// Defer handling of Meta until it is known to be functioning as a
// modifier (it may otherwise actually be an alternative method for
// pressing a single key, such as Meta+Left for Home on ChromeOS)
if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) {
// Defer handling until further events exist to provide context
if (eventLog.length === 1)
return null;
// Drop keydown if it turns out Meta does not actually apply
if (eventLog[1].keysym !== first.keysym) {
if (!eventLog[1].modifiers.meta)
return eventLog.shift();
}
// Drop duplicate keydown events while waiting to determine
// whether to acknowledge Meta (browser may repeat keydown
// while the key is held)
else if (eventLog[1] instanceof KeydownEvent)
return eventLog.shift();
}
// If event itself is reliable, no need to wait for other events
if (first.reliable) {
keysym = first.keysym;
@ -1172,6 +1182,8 @@ Guacamole.Keyboard = function Keyboard(element) {
// Fire a key press if valid events were found
if (accepted_events.length > 0) {
syncModifierStates(first);
if (keysym) {
// Fire event
@ -1213,6 +1225,7 @@ Guacamole.Keyboard = function Keyboard(element) {
return first;
}
syncModifierStates(first);
return eventLog.shift();
} // end if keyup
@ -1233,11 +1246,11 @@ Guacamole.Keyboard = function Keyboard(element) {
* have the same keycode, such as left shift vs. right shift.
*
* @private
* @param {KeyboardEvent} e
* @param {!KeyboardEvent} e
* A JavaScript keyboard event, as received through the DOM via a
* "keydown", "keyup", or "keypress" handler.
*
* @returns {Number}
* @returns {!number}
* The location of the key event on the keyboard, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*/
@ -1261,10 +1274,10 @@ Guacamole.Keyboard = function Keyboard(element) {
* Guacamole.Keyboard. If the Event has already been marked as handled,
* false is returned.
*
* @param {Event} e
* @param {!Event} e
* The Event to mark.
*
* @returns {Boolean}
* @returns {!boolean}
* true if the given Event was successfully marked, false if the given
* Event was already marked.
*/
@ -1286,7 +1299,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* events signalled through this Guacamole.Keyboard's onkeydown and
* onkeyup handlers.
*
* @param {Element|Document} element
* @param {!(Element|Document)} element
* The Element to attach event listeners to for the sake of handling
* key or input events.
*/
@ -1301,17 +1314,11 @@ Guacamole.Keyboard = function Keyboard(element) {
// Ignore events which have already been handled
if (!markEvent(e)) return;
var keyCode;
if (window.event) keyCode = window.event.keyCode;
else if (e.which) keyCode = e.which;
// Fix modifier states
var keydownEvent = new KeydownEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));
syncModifierStates(e, keydownEvent);
var keydownEvent = new KeydownEvent(e);
// Ignore (but do not prevent) the "composition" keycode sent by some
// browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html)
if (keyCode === 229)
if (keydownEvent.keyCode === 229)
return;
// Log event
@ -1332,16 +1339,8 @@ Guacamole.Keyboard = function Keyboard(element) {
// Ignore events which have already been handled
if (!markEvent(e)) return;
var charCode;
if (window.event) charCode = window.event.keyCode;
else if (e.which) charCode = e.which;
// Fix modifier states
var keypressEvent = new KeypressEvent(charCode);
syncModifierStates(e, keypressEvent);
// Log event
eventLog.push(keypressEvent);
eventLog.push(new KeypressEvent(e));
// Interpret as many events as possible, prevent default if indicated
if (interpret_events())
@ -1360,74 +1359,13 @@ Guacamole.Keyboard = function Keyboard(element) {
e.preventDefault();
var keyCode;
if (window.event) keyCode = window.event.keyCode;
else if (e.which) keyCode = e.which;
// Fix modifier states
var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e));
syncModifierStates(e, keyupEvent);
// Log event, call for interpretation
eventLog.push(keyupEvent);
eventLog.push(new KeyupEvent(e));
interpret_events();
}, true);
/**
* Handles the given "input" event, typing the data within the input text.
* If the event is complete (text is provided), handling of "compositionend"
* events is suspended, as such events may conflict with input events.
*
* @private
* @param {InputEvent} e
* The "input" event to handle.
*/
var handleInput = function handleInput(e) {
// Only intercept if handler set
if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
// Ignore events which have already been handled
if (!markEvent(e)) return;
// Type all content written
if (e.data && !e.isComposing) {
element.removeEventListener("compositionend", handleComposition, false);
guac_keyboard.type(e.data);
}
};
/**
* Handles the given "compositionend" event, typing the data within the
* composed text. If the event is complete (composed text is provided),
* handling of "input" events is suspended, as such events may conflict
* with composition events.
*
* @private
* @param {CompositionEvent} e
* The "compositionend" event to handle.
*/
var handleComposition = function handleComposition(e) {
// Only intercept if handler set
if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
// Ignore events which have already been handled
if (!markEvent(e)) return;
// Type all content written
if (e.data) {
element.removeEventListener("input", handleInput, false);
guac_keyboard.type(e.data);
}
};
// Automatically type text entered into the wrapped field
element.addEventListener("input", handleInput, false);
element.addEventListener("compositionend", handleComposition, false);
// NEKO: Do not automatically type text entered into the wrapped field
};
@ -1442,7 +1380,7 @@ Guacamole.Keyboard = function Keyboard(element) {
* instance.
*
* @private
* @type {Number}
* @type {!number}
*/
Guacamole.Keyboard._nextID = 0;
@ -1454,42 +1392,49 @@ Guacamole.Keyboard.ModifierState = function() {
/**
* Whether shift is currently pressed.
* @type {Boolean}
*
* @type {!boolean}
*/
this.shift = false;
/**
* Whether ctrl is currently pressed.
* @type {Boolean}
*
* @type {!boolean}
*/
this.ctrl = false;
/**
* Whether alt is currently pressed.
* @type {Boolean}
*
* @type {!boolean}
*/
this.alt = false;
/**
* Whether meta (apple key) is currently pressed.
* @type {Boolean}
*
* @type {!boolean}
*/
this.meta = false;
/**
* Whether hyper (windows key) is currently pressed.
* @type {Boolean}
*
* @type {!boolean}
*/
this.hyper = false;
};
/**
* Returns the modifier state applicable to the keyboard event given.
*
* @param {KeyboardEvent} e The keyboard event to read.
* @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard
* modifiers.
* @param {!KeyboardEvent} e
* The keyboard event to read.
*
* @returns {!Guacamole.Keyboard.ModifierState}
* The current state of keyboard modifiers.
*/
Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {

View File

@ -0,0 +1,4 @@
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#fb542b"/>
<path d="M586.5 266.2c3 0 5.9 1.3 7.8 3.6l38.4 44.3 1.5-.3c9.2-1.6 18.8-2.2 27.9-1.2 11.7 1.4 21.7 5.5 29.1 13 7.8 7.9 15.5 16 22.8 23.8 2.6 2.7 4.9 5.2 7 7.5.7.8 1.4 1.5 1.9 2.1 3.4 3.8 4.2 8.2 2.7 12l-9.9 24.8 13.2 38.4c.7 2 .8 4.2.2 6.2-.1.4-.1.4-.5 2.1-.6 2.4-.6 2.4-1.5 5.9-1.6 6.3-3.5 13.4-5.4 21-5.6 21.8-11.3 43.6-16.5 63.9-13 50.2-21.5 83.3-23.5 91.6-11.2 44.9-20 60.5-48.6 81-25.1 18-77.4 54-87.4 60.4-1 .6-2 1.3-3.4 2.3-.5.4-3.2 2.2-4 2.7-4.9 3.4-8.4 5.5-12.1 7.3-4.8 2.2-9.3 3.5-14 3.5-4.6 0-9.2-1.3-14-3.5-3.8-1.8-7.2-3.9-12.1-7.3-.8-.5-3.4-2.4-4-2.7-1.4-1-2.5-1.7-3.4-2.3-10.1-6.4-62.4-42.5-87.4-60.4-28.6-20.5-37.5-36.1-48.6-81a49038 49038 0 0 0-23.5-91.2c-5.4-20.7-11-42.5-16.6-64.3-2-7.6-3.8-14.7-5.5-21-.9-3.5-.9-3.5-1.5-5.9-.4-1.7-.4-1.7-.5-2.1-.5-2.1-.4-4.2.2-6.2l13.2-38.4-9.9-24.8c-1.5-3.8-.7-8.2 2-11.2 1.2-1.3 1.9-2 2.6-2.8 2.1-2.2 4.4-4.7 7-7.5 7.3-7.8 15-15.9 22.8-23.8 7.4-7.6 17.4-11.6 29.1-13 9.1-1.1 18.7-.5 27.9 1.2l1.5.3 38.4-44.3c2-2.3 4.8-3.6 7.8-3.6h148.8zm-4.7 21.2H442.2l-39.3 45.5c-2.6 3.1-6.8 4.3-10.6 3.2-.2-.1-.7-.2-1.5-.4-1.3-.3-2.9-.6-4.6-.9-7.4-1.3-15-1.8-21.9-1-7.4.9-13.1 3.2-16.8 7-7.6 7.8-15.3 15.7-22.5 23.4-1.7 1.8-3.3 3.5-4.8 5.1l8.9 22.2c1 2.4 1 5.1.2 7.5l-13.4 39.1c.4 1.4.5 1.9 1.3 4.8 1.6 6.3 3.5 13.4 5.4 21 5.6 21.8 11.3 43.6 16.5 63.9 13 50.4 21.5 83.4 23.6 91.7 10 40.4 16.4 51.6 40.4 68.8 24.8 17.8 76.8 53.6 86.5 59.8 1.2.8 2.5 1.6 4.1 2.7.6.4 3.2 2.2 3.9 2.7 4 2.8 6.7 4.4 9.3 5.6 2.2 1.1 4 1.5 5.1 1.5 1.2 0 2.9-.5 5.1-1.5 2.5-1.2 5.2-2.9 9.2-5.6.7-.5 3.4-2.3 4-2.7 1.6-1.1 2.9-2 4.1-2.7 9.7-6.2 61.7-42 86.5-59.8 24-17.2 30.4-28.4 40.4-68.8 2.1-8.3 10.6-41.4 23.5-91.4 5.4-20.7 11-42.5 16.6-64.3 2-7.6 3.8-14.7 5.4-21 .8-2.9.9-3.4 1.3-4.8l-13.4-39c-.8-2.4-.8-5.1.2-7.5l8.9-22.2c-1.5-1.6-3.1-3.3-4.8-5.1-7.2-7.7-14.9-15.6-22.5-23.4-3.7-3.8-9.4-6.1-16.8-7-6.8-.8-14.5-.3-21.9 1-1.7.3-3.2.6-4.6.9-.8.2-1.3.3-1.5.4-3.9 1.1-8-.2-10.6-3.2l-39.3-45.5zM512 584.7c2.8 0 21 6.5 35.6 14.2 14.6 7.7 25.2 13.1 28.5 15.3 3.4 2.1 1.3 6.2-1.8 8.4s-44.5 34.9-48.5 38.5c-4 3.6-9.9 9.6-13.9 9.6s-9.9-6-13.9-9.6c-4-3.6-45.4-36.2-48.5-38.5-3.1-2.2-5.1-6.3-1.8-8.4 3.4-2.2 14-7.6 28.5-15.3 14.8-7.7 33-14.2 35.8-14.2m.1-232.5c.7 0 8.8.2 20.6 4.2 12.4 4.2 25.9 9.5 32.1 9.5s52.3-9 52.3-9 54.6 67.2 54.6 81.6c0 14.4-6.9 18.2-13.8 25.6-6.9 7.5-37.1 40.1-40.9 44.3-3.9 4.2-11.9 10.5-7.2 22 4.7 11.4 11.7 26 4 40.7-7.8 14.8-21.1 24.6-29.7 23-8.5-1.6-28.6-12.3-36-17.2-7.4-4.9-30.8-24.5-30.8-32s24.2-21 28.6-24.1c4.5-3.1 24.8-14.9 25.2-19.6.4-4.7.3-6-5.8-17.5-6-11.5-16.8-26.9-15-37.1 1.8-10.2 19.3-15.5 31.7-20.3 12.4-4.8 36.4-13.8 39.4-15.2 3-1.4 2.2-2.7-6.8-3.6-9.1-.9-34.8-4.4-46.4-1.1-11.6 3.3-31.4 8.3-33.1 11-1.6 2.7-3 2.7-1.4 11.9 1.7 9.2 10.1 53.2 11 61 .8 7.8 2.4 13-5.8 14.9-8.3 1.9-22.2 5.3-27 5.3-4.8 0-18.7-3.4-27-5.3s-6.7-7.1-5.8-14.9c.8-7.8 9.3-51.9 11-61 1.6-9.2.2-9.3-1.4-11.9-1.6-2.7-21.4-7.7-33.1-11-11.6-3.3-37.4.2-46.4 1.1-9.1.9-9.8 2.2-6.8 3.6 3 1.4 27 10.5 39.4 15.2 12.5 4.8 29.9 10.1 31.7 20.3s-9 25.6-15 37.1-6.2 12.9-5.8 17.5 20.8 16.5 25.2 19.6c4.5 3.1 28.6 16.6 28.6 24.1s-23.4 27.2-30.8 32c-7.4 4.9-27.4 15.6-36 17.2-8.5 1.6-21.9-8.2-29.7-23-7.8-14.8-.8-29.3 4-40.7s-3.3-17.8-7.2-22c-3.9-4.2-34-36.8-40.9-44.3-6.9-7.5-13.8-11.3-13.8-25.6 0-14.4 54.6-81.6 54.6-81.6s46.1 9 52.3 9c6.2 0 19.7-5.3 32.1-9.5 12.6-4.2 20.9-4.2 21-4.2z" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1 @@
<svg width="190px" height="190px" viewBox="1 1 190 190" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="c"><circle cx="96" cy="96" r="88"/></clipPath><linearGradient id="d" x1="110.87" x2="52.54" y1="164.5" y2="130.33" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".4"/><stop offset=".33" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="e" x1="30.43" x2="86.54" y1="74.4" y2="41.61" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".2"/><stop offset=".66" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="118.57" x2="54.58" y1="169.11" y2="131.57" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".2"/><stop offset=".33" stop-color="#1a237e" stop-opacity="0"/></linearGradient><clipPath id="g"><use xlink:href="#R"/></clipPath><linearGradient id="a" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".3"/><stop offset=".66" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="121.86" x2="136.55" y1="49.8" y2="114.13" xlink:href="#a"/><linearGradient id="i" x1="121.87" x2="136.29" y1="50.07" y2="113.01" xlink:href="#a"/><clipPath id="j"><use xlink:href="#S"/></clipPath><linearGradient id="k" x1="29.34" x2="81.84" y1="75.02" y2="44.35" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".6"/><stop offset=".66" stop-color="#1a237e" stop-opacity="0"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1a237e" stop-opacity=".2"/><stop offset="1" stop-color="#1a237e" stop-opacity="0"/></linearGradient><radialGradient id="l" cx="92.18" cy="55.95" r="84.08" xlink:href="#b"/><clipPath id="n"><path d="M61.36 116L96 56h88V8H21.97v40.34"/></clipPath><linearGradient id="m" x1="8" x2="130.65" y1="104.24" y2="104.24" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f4b400" stop-opacity=".4"/><stop offset=".09" stop-color="#f2a700" stop-opacity=".3"/><stop offset=".22" stop-color="#f19800" stop-opacity=".13"/><stop offset=".33" stop-color="#f09300" stop-opacity="0"/></linearGradient><radialGradient id="o" cx="21.87" cy="48.52" r="78.04" xlink:href="#b"/><radialGradient id="p" cx="95.84" cy="96.14" r="87.87" xlink:href="#b"/><radialGradient id="q" cx="34.29" cy="32.01" r="176.75" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity=".1"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient><g clip-path="url(#c)"><use fill="#5e97f6" xlink:href="#R"/><use fill="url(#d)" xlink:href="#R"/><use fill="#3367d6" xlink:href="#T"/><use fill="url(#e)" xlink:href="#T"/><path fill="#1a237e" fill-opacity=".15" d="M62.3 115.65l-39.83-68.3-.58 1 39.54 67.8z"/><use fill="#5e97f6" xlink:href="#R"/><use fill="url(#f)" xlink:href="#R"/><path fill="#1a237e" fill-opacity=".15" d="M129.84 117.33l-.83-.48L90.62 184h1.15l38.1-66.64z"/><g clip-path="url(#g)"><use fill="#a1c2fa" xlink:href="#S"/><use fill="url(#h)" xlink:href="#S"/></g><use fill="#a1c2fa" xlink:href="#S"/><use fill="url(#i)" xlink:href="#S"/><g clip-path="url(#j)"><use fill="#3367d6" xlink:href="#T"/><use fill="url(#k)" xlink:href="#T"/></g><path fill="url(#l)" d="M96 56v20.95L174.4 56z"/><use fill="url(#m)" clip-path="url(#n)" xlink:href="#R"/><path fill="url(#o)" d="M21.97 48.45l57.25 57.24L61.36 116z"/><path fill="url(#p)" d="M91.83 183.9l20.96-78.2 17.86 10.3z"/><circle cx="96" cy="96" r="40" fill="#f5f5f5"/><circle cx="96" cy="96" r="32" fill="#4285f4"/><path fill="#1a237e" fill-opacity=".2" d="M96 55a40 40 0 00-40 40v1a40 40 0 0140-40h88v-1z"/><path fill="#fff" fill-opacity=".1" d="M130.6 116A39.96 39.96 0 0196 136a39.97 39.97 0 01-34.61-20h-.04L8 24.48v1L61.36 117h.04a39.94 39.94 0 0069.21 0h.05v-1z"/><path fill="#1a237e" d="M97 56c-.17 0-.33.02-.5.03a39.98 39.98 0 010 79.94c.17 0 .33.03.5.03a40 40 0 000-80z" opacity=".1"/><path fill="#fff" fill-opacity=".2" d="M131 117.33a39.72 39.72 0 003.5-32.05 39.72 39.72 0 01-3.87 30.69l.02.04-38.87 68h1.16l38.1-66.64zM96 9a88 88 0 0187.99 87.5l.01-.5A88 88 0 008 96v.5A88 88 0 0196 9z"/><path fill="#1a237e" fill-opacity=".15" d="M96 183a88 88 0 0087.99-87.5l.01.5A88 88 0 018 96l.01-.5A88 88 0 0096 183z"/></g><circle cx="96" cy="96" r="88" fill="url(#q)"/><defs><path id="R" d="M8 184h83.77l38.88-38.88V116H61.36L8 24.48z"/><path id="S" d="M96 56l34.65 60-38.88 68H184V56z"/><path id="T" d="M21.97 8v108h39.4L96 56h88V8z"/></defs></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="77.42" height="79.97" version="1.1" viewBox="0 0 77.42 79.97" xmlns="http://www.w3.org/2000/svg">
<title>Firefox Browser logo</title>
<defs>
<linearGradient id="a" x1="70.79" x2="6.447" y1="12.39" y2="74.47" gradientTransform="translate(-1.3 -.004086)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".048"/>
<stop stop-color="#ffe847" offset=".111"/>
<stop stop-color="#ffc830" offset=".225"/>
<stop stop-color="#ff980e" offset=".368"/>
<stop stop-color="#ff8b16" offset=".401"/>
<stop stop-color="#ff672a" offset=".462"/>
<stop stop-color="#ff3647" offset=".534"/>
<stop stop-color="#e31587" offset=".705"/>
</linearGradient>
<radialGradient id="b" cx="-7907" cy="-8515" r="80.8" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffbd4f" offset=".129"/>
<stop stop-color="#ffac31" offset=".186"/>
<stop stop-color="#ff9d17" offset=".247"/>
<stop stop-color="#ff980e" offset=".283"/>
<stop stop-color="#ff563b" offset=".403"/>
<stop stop-color="#ff3750" offset=".467"/>
<stop stop-color="#f5156c" offset=".71"/>
<stop stop-color="#eb0878" offset=".782"/>
<stop stop-color="#e50080" offset=".86"/>
</radialGradient>
<radialGradient id="c" cx="-7937" cy="-8482" r="80.8" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#960e18" offset=".3"/>
<stop stop-color="#b11927" stop-opacity=".74" offset=".351"/>
<stop stop-color="#db293d" stop-opacity=".343" offset=".435"/>
<stop stop-color="#f5334b" stop-opacity=".094" offset=".497"/>
<stop stop-color="#ff3750" stop-opacity="0" offset=".53"/>
</radialGradient>
<radialGradient id="d" cx="-7927" cy="-8533" r="58.53" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".132"/>
<stop stop-color="#ffdc3e" offset=".252"/>
<stop stop-color="#ff9d12" offset=".506"/>
<stop stop-color="#ff980e" offset=".526"/>
</radialGradient>
<radialGradient id="e" cx="-7946" cy="-8461" r="38.47" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#3a8ee6" offset=".353"/>
<stop stop-color="#5c79f0" offset=".472"/>
<stop stop-color="#9059ff" offset=".669"/>
<stop stop-color="#c139e6" offset="1"/>
</radialGradient>
<radialGradient id="f" cx="-7936" cy="-8492" r="20.4" gradientTransform="matrix(.972 -.235 .275 1.138 10090 7834)" gradientUnits="userSpaceOnUse">
<stop stop-color="#9059ff" stop-opacity="0" offset=".206"/>
<stop stop-color="#8c4ff3" stop-opacity=".064" offset=".278"/>
<stop stop-color="#7716a8" stop-opacity=".45" offset=".747"/>
<stop stop-color="#6e008b" stop-opacity=".6" offset=".975"/>
</radialGradient>
<radialGradient id="g" cx="-7938" cy="-8518" r="27.68" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffe226" offset="0"/>
<stop stop-color="#ffdb27" offset=".121"/>
<stop stop-color="#ffc82a" offset=".295"/>
<stop stop-color="#ffa930" offset=".502"/>
<stop stop-color="#ff7e37" offset=".732"/>
<stop stop-color="#ff7139" offset=".792"/>
</radialGradient>
<radialGradient id="h" cx="-7916" cy="-8536" r="118.1" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".113"/>
<stop stop-color="#ff980e" offset=".456"/>
<stop stop-color="#ff5634" offset=".622"/>
<stop stop-color="#ff3647" offset=".716"/>
<stop stop-color="#e31587" offset=".904"/>
</radialGradient>
<radialGradient id="i" cx="-7927" cy="-8523" r="86.5" gradientTransform="matrix(.105 .995 -.653 .069 -4685 8470)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset="0"/>
<stop stop-color="#ffe847" offset=".06"/>
<stop stop-color="#ffc830" offset=".168"/>
<stop stop-color="#ff980e" offset=".304"/>
<stop stop-color="#ff8b16" offset=".356"/>
<stop stop-color="#ff672a" offset=".455"/>
<stop stop-color="#ff3647" offset=".57"/>
<stop stop-color="#e31587" offset=".737"/>
</radialGradient>
<radialGradient id="j" cx="-7938" cy="-8508" r="73.72" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".137"/>
<stop stop-color="#ff980e" offset=".48"/>
<stop stop-color="#ff5634" offset=".592"/>
<stop stop-color="#ff3647" offset=".655"/>
<stop stop-color="#e31587" offset=".904"/>
</radialGradient>
<radialGradient id="k" cx="-7919" cy="-8504" r="80.69" gradientTransform="translate(7974 8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".094"/>
<stop stop-color="#ffe141" offset=".231"/>
<stop stop-color="#ffaf1e" offset=".509"/>
<stop stop-color="#ff980e" offset=".626"/>
</radialGradient>
<linearGradient id="l" x1="70.01" x2="15.27" y1="12.06" y2="66.81" gradientTransform="translate(-1.3 -.004086)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" stop-opacity=".8" offset=".167"/>
<stop stop-color="#fff44f" stop-opacity=".634" offset=".266"/>
<stop stop-color="#fff44f" stop-opacity=".217" offset=".489"/>
<stop stop-color="#fff44f" stop-opacity="0" offset=".6"/>
</linearGradient>
</defs>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76l7e-3 0.065c-4.382-10.92-11.81-15.33-17.88-24.92-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015l0.015-0.026c-9.735 5.7-13.04 16.25-13.34 21.53a19.39 19.39 0 0 0-10.67 4.111 11.59 11.59 0 0 0-1-0.758 17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.623 4.347v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74l-0.052 0.256c-0.073 0.341-0.336 2.049-0.381 2.42 0 0.029-6e-3 0.056-9e-3 0.085a36.94 36.94 0 0 0-0.629 5.343v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47zm-44.67 30.34c0.181 0.087 0.351 0.181 0.537 0.264l0.027 0.017q-0.282-0.135-0.564-0.281zm8.878-23.38m31.95-4.934v-0.037l7e-3 0.041z" style="fill:url(#a)"/>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76v0.037l7e-3 0.041a35.1 35.1 0 0 1-1.206 26.16c-4.442 9.531-15.19 19.3-32.02 18.82-18.18-0.515-34.2-14.01-37.19-31.68-0.545-2.787 0-4.2 0.274-6.465a28.88 28.88 0 0 0-0.623 5.348v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47z" style="fill:url(#b)"/>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76v0.037l7e-3 0.041a35.1 35.1 0 0 1-1.206 26.16c-4.442 9.531-15.19 19.3-32.02 18.82-18.18-0.515-34.2-14.01-37.19-31.68-0.545-2.787 0-4.2 0.274-6.465a28.88 28.88 0 0 0-0.623 5.348v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47z" style="fill:url(#c)"/>
<path d="m55.78 31.38c0.084 0.059 0.162 0.118 0.241 0.177a21.1 21.1 0 0 0-3.6-4.695c-12.05-12.05-3.157-26.12-1.658-26.84l0.015-0.022c-9.735 5.7-13.04 16.25-13.34 21.53 0.452-0.031 0.9-0.069 1.362-0.069a19.56 19.56 0 0 1 16.98 9.917z" style="fill:url(#d)"/>
<path d="m38.82 33.79c-0.064 0.964-3.47 4.289-4.661 4.289-11.02 0-12.81 6.667-12.81 6.667 0.488 5.614 4.4 10.24 9.129 12.68 0.216 0.112 0.435 0.213 0.654 0.312q0.569 0.252 1.138 0.466a17.24 17.24 0 0 0 5.043 0.973c19.32 0.906 23.06-23.1 9.119-30.07a13.38 13.38 0 0 1 9.345 2.269 19.56 19.56 0 0 0-16.98-9.917c-0.46 0-0.91 0.038-1.362 0.069a19.39 19.39 0 0 0-10.67 4.111c0.591 0.5 1.258 1.168 2.663 2.553 2.63 2.591 9.375 5.275 9.39 5.59z" style="fill:url(#e)"/>
<path d="m38.82 33.79c-0.064 0.964-3.47 4.289-4.661 4.289-11.02 0-12.81 6.667-12.81 6.667 0.488 5.614 4.4 10.24 9.129 12.68 0.216 0.112 0.435 0.213 0.654 0.312q0.569 0.252 1.138 0.466a17.24 17.24 0 0 0 5.043 0.973c19.32 0.906 23.06-23.1 9.119-30.07a13.38 13.38 0 0 1 9.345 2.269 19.56 19.56 0 0 0-16.98-9.917c-0.46 0-0.91 0.038-1.362 0.069a19.39 19.39 0 0 0-10.67 4.111c0.591 0.5 1.258 1.168 2.663 2.553 2.63 2.591 9.375 5.275 9.39 5.59z" style="fill:url(#f)"/>
<path d="m24.96 24.36c0.314 0.2 0.573 0.374 0.8 0.531a17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21c0.189-5e-3 5.811-0.106 8.638 1.732z" style="fill:url(#g)"/>
<path d="m0.354 42.16c2.991 17.67 19.01 31.17 37.19 31.68 16.83 0.476 27.58-9.294 32.02-18.82a35.1 35.1 0 0 0 1.206-26.16v-0.037c0-0.029-6e-3 -0.046 0-0.037l7e-3 0.065c1.375 8.977-3.191 17.67-10.33 23.56l-0.022 0.05c-13.91 11.33-27.22 6.834-29.91 5q-0.282-0.135-0.564-0.281c-8.109-3.876-11.46-11.26-10.74-17.6a9.953 9.953 0 0 1-9.181-5.775 14.62 14.62 0 0 1 14.25-0.572 19.3 19.3 0 0 0 14.55 0.572c-0.015-0.315-6.76-3-9.39-5.59-1.405-1.385-2.072-2.052-2.663-2.553a11.59 11.59 0 0 0-1-0.758c-0.23-0.157-0.489-0.327-0.8-0.531-2.827-1.838-8.449-1.737-8.635-1.732h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.638 4.337v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74c-0.019 0.079-1.396 6.099-0.717 9.221z" style="fill:url(#h)"/>
<path d="m52.42 26.86a21.1 21.1 0 0 1 3.6 4.7c0.213 0.161 0.412 0.321 0.581 0.476 8.787 8.1 4.183 19.55 3.84 20.36 7.138-5.881 11.7-14.58 10.33-23.56-4.384-10.93-11.82-15.34-17.88-24.93-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015c-1.499 0.711-10.39 14.79 1.66 26.83z" style="fill:url(#i)"/>
<path d="m56.6 32.04c-0.169-0.155-0.368-0.315-0.581-0.476-0.079-0.059-0.157-0.118-0.241-0.177a13.38 13.38 0 0 0-9.345-2.269c13.94 6.97 10.2 30.97-9.119 30.07a17.24 17.24 0 0 1-5.043-0.973q-0.569-0.213-1.138-0.466c-0.219-0.1-0.438-0.2-0.654-0.312l0.027 0.017c2.694 1.839 16 6.332 29.91-5l0.022-0.05c0.347-0.81 4.951-12.26-3.84-20.36z" style="fill:url(#j)"/>
<path d="m21.35 44.74s1.789-6.667 12.81-6.667c1.191 0 4.6-3.325 4.661-4.289a19.3 19.3 0 0 1-14.55-0.572 14.62 14.62 0 0 0-14.25 0.572 9.953 9.953 0 0 0 9.181 5.775c-0.718 6.337 2.632 13.72 10.74 17.6 0.181 0.087 0.351 0.181 0.537 0.264-4.733-2.445-8.641-7.069-9.129-12.68z" style="fill:url(#k)"/>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76l7e-3 0.065c-4.382-10.92-11.81-15.33-17.88-24.92-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015l0.015-0.026c-9.735 5.7-13.04 16.25-13.34 21.53 0.452-0.031 0.9-0.069 1.362-0.069a19.56 19.56 0 0 1 16.98 9.917 13.38 13.38 0 0 0-9.345-2.269c13.94 6.97 10.2 30.97-9.119 30.07a17.24 17.24 0 0 1-5.043-0.973q-0.569-0.213-1.138-0.466c-0.219-0.1-0.438-0.2-0.654-0.312l0.027 0.017q-0.282-0.135-0.564-0.281c0.181 0.087 0.351 0.181 0.537 0.264-4.733-2.446-8.641-7.07-9.129-12.68 0 0 1.789-6.667 12.81-6.667 1.191 0 4.6-3.325 4.661-4.289-0.015-0.315-6.76-3-9.39-5.59-1.405-1.385-2.072-2.052-2.663-2.553a11.59 11.59 0 0 0-1-0.758 17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.623 4.347v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74l-0.052 0.256c-0.073 0.341-0.4 2.073-0.447 2.445v0a45.09 45.09 0 0 0-0.572 5.403v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47zm-3.845 1.991 7e-3 0.041z" style="fill:url(#l)"/>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,8 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path fill="#4285F4" d="M11.9733005,16.4144516 C9.59429509,16.4144516 7.65884342,14.4788121 7.65884342,12.0999945 C7.65884342,9.72075429 9.59429509,7.78530262 11.9733005,7.78530262 C14.3523059,7.78530262 16.2879454,9.72075429 16.2879454,12.0999945 C16.2879924,14.4788121 14.3523529,16.4144516 11.9733005,16.4144516 L11.9733005,16.4144516 Z"/>
<path fill="#4AAE48" d="M13.7910066,17.1810894 C13.1872494,17.4007528 12.5549364,17.5116882 11.9068426,17.5116882 C10.6352186,17.5116882 9.39370022,17.0652225 8.41129728,16.2548117 C7.61431896,15.5972306 7.02033082,14.7318218 6.69297277,13.7500294 L6.691235,13.7442055 L1.93641793,5.50854377 C0.228238232,8.1357683 -0.377867323,11.2684017 0.230163868,14.3493248 C0.849467072,17.4870306 2.65331799,20.1955546 5.30970885,21.9764857 C6.69353637,22.9040785 8.22657716,23.5227711 9.86735961,23.8160783 L13.7910066,17.1810894 L13.7910066,17.1810894 Z"/>
<path fill="#EA3939" d="M22.7599128,6.70666487 C19.7903479,0.731840215 12.5393434,-1.70437065 6.56465968,1.26519432 C5.01832731,2.03375777 3.65958002,3.12347966 2.57441389,4.45616042 L6.59105498,11.413435 C6.85172029,9.39264477 8.25048322,7.60626547 10.3219975,6.95469613 C10.8217704,6.79763941 11.341739,6.71403864 11.865371,6.70666487 L22.7599128,6.70666487 Z"/>
<path fill="#FED14B" d="M11.9264747,24 C14.936431,24 17.8171819,22.8712018 20.0368292,20.8218558 C22.2681243,18.7619893 23.6231612,15.9588274 23.8523118,12.9290041 C23.983349,11.1937716 23.7261592,9.41711443 23.1082181,7.78530262 L15.2520944,7.78530262 C16.5738788,8.83162726 17.3494403,10.4306093 17.3416908,12.1250278 C17.3360548,13.3449884 16.9177692,14.5439079 16.1598672,15.5064909 L11.1518525,23.9751076 C11.4097938,23.9916399 11.6696606,24 11.926052,24 L11.9264747,24 L11.9264747,24 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient-1" gradientUnits="userSpaceOnUse" x1="63.3343" y1="757.83" x2="241.6165" y2="757.83" gradientTransform="matrix(1 0 0 1 -4.63 -580.8098)">
<stop offset="0" style="stop-color:#0C59A4"/>
<stop offset="1" style="stop-color:#114A8B"/>
</linearGradient>
<radialGradient id="radialGradient-1" cx="161.83" cy="788.4008" r="95.38" gradientTransform="matrix(0.9999 0 0 0.9498 -4.6217 -570.3868)" gradientUnits="userSpaceOnUse">
<stop offset="0.72" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="0.95" style="stop-color:#000000;stop-opacity:0.53"/>
<stop offset="1" style="stop-color:#000000"/>
</radialGradient>
<linearGradient id="linearGradient-2" gradientUnits="userSpaceOnUse" x1="157.4013" y1="680.5561" x2="46.0276" y2="801.8683" gradientTransform="matrix(1 0 0 1 -4.63 -580.8098)">
<stop offset="0" style="stop-color:#1B9DE2"/>
<stop offset="0.16" style="stop-color:#1595DF"/>
<stop offset="0.67" style="stop-color:#0680D7"/>
<stop offset="1" style="stop-color:#0078D4"/>
</linearGradient>
<radialGradient id="radialGradient-2" cx="-773.6357" cy="746.7146" r="143.24" gradientTransform="matrix(0.15 -0.9898 0.8 0.12 -410.7182 -656.3412)" gradientUnits="userSpaceOnUse">
<stop offset="0.76" style="stop-color:#000000;stop-opacity:0"/>
<stop offset="0.95" style="stop-color:#000000;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#000000"/>
</radialGradient>
<radialGradient id="radialGradient-3" cx="230.5926" cy="-106.0381" r="202.4299" gradientTransform="matrix(-3.999750e-02 0.9998 -2.1299 -7.998414e-02 -190.7749 -191.6354)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#35C1F1"/>
<stop offset="0.11" style="stop-color:#34C1ED"/>
<stop offset="0.23" style="stop-color:#2FC2DF"/>
<stop offset="0.31" style="stop-color:#2BC3D2"/>
<stop offset="0.67" style="stop-color:#36C752"/>
</radialGradient>
<radialGradient id="radialGradient-4" cx="536.3567" cy="-117.7029" r="97.34" gradientTransform="matrix(0.28 0.9598 -0.78 0.23 -1.9279 -410.3179)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#66EB6E"/>
<stop offset="1" style="stop-color:#66EB6E;stop-opacity:0"/>
</radialGradient>
</defs>
<g>
<path fill="url(#linearGradient-1)" d="M231,190.5c-3.4,1.8-6.9,3.4-10.5,4.7c-11.5,4.3-23.6,6.5-35.9,6.5c-47.3,0-88.5-32.5-88.5-74.3 c0.1-11.4,6.4-21.9,16.4-27.3c-42.8,1.8-53.8,46.4-53.8,72.5c0,73.9,68.1,81.4,82.8,81.4c7.9,0,19.8-2.3,27-4.6l1.3-0.4 c27.6-9.5,51-28.1,66.6-52.8c1.2-1.9,0.6-4.3-1.2-5.5C233.9,189.9,232.3,189.8,231,190.5z"/>
<path opacity="0.35" fill="url(#radialGradient-1)" enable-background="new " d="M231,190.5c-3.4,1.8-6.9,3.4-10.5,4.7 c-11.5,4.3-23.6,6.5-35.9,6.5c-47.3,0-88.5-32.5-88.5-74.3c0.1-11.4,6.4-21.9,16.4-27.3c-42.8,1.8-53.8,46.4-53.8,72.5 c0,73.9,68.1,81.4,82.8,81.4c7.9,0,19.8-2.3,27-4.6l1.3-0.4c27.6-9.5,51-28.1,66.6-52.8c1.2-1.9,0.6-4.3-1.2-5.5 C233.9,189.9,232.3,189.8,231,190.5z"/>
<path fill="url(#linearGradient-2)" d="M105.7,241.4c-8.9-5.5-16.6-12.8-22.7-21.3c-26.3-36-18.4-86.5,17.6-112.8c3.8-2.7,7.7-5.2,11.9-7.2 c3.1-1.5,8.4-4.1,15.5-4c10.1,0.1,19.6,4.9,25.7,13c4,5.4,6.3,11.9,6.4,18.7c0-0.2,24.5-79.6-80-79.6c-43.9,0-80,41.7-80,78.2 c-0.2,19.3,4,38.5,12.1,56c27.6,58.8,94.8,87.6,156.4,67.1C147.5,256.1,124.5,253.2,105.7,241.4L105.7,241.4z"/>
<path opacity="0.41" fill="url(#radialGradient-2)" enable-background="new " d="M105.7,241.4c-8.9-5.5-16.6-12.8-22.7-21.3 c-26.3-36-18.4-86.5,17.6-112.8c3.8-2.7,7.7-5.2,11.9-7.2c3.1-1.5,8.4-4.1,15.5-4c10.1,0.1,19.6,4.9,25.7,13 c4,5.4,6.3,11.9,6.4,18.7c0-0.2,24.5-79.6-80-79.6c-43.9,0-80,41.7-80,78.2c-0.2,19.3,4,38.5,12.1,56 c27.6,58.8,94.8,87.6,156.4,67.1C147.5,256.1,124.5,253.2,105.7,241.4L105.7,241.4z"/>
<path fill="url(#radialGradient-3)" d="M152.3,148.9c-0.8,1-3.3,2.5-3.3,5.7c0,2.6,1.7,5.1,4.7,7.2c14.4,10,41.5,8.7,41.6,8.7 c10.7,0,21.1-2.9,30.3-8.3c18.8-11,30.4-31.1,30.4-52.9c0.3-22.4-8-37.3-11.3-43.9C223.5,23.9,177.7,0,128,0C58,0,1,56.2,0,126.2 c0.5-36.5,36.8-66,80-66c3.5,0,23.5,0.3,42,10.1c16.3,8.6,24.9,18.9,30.8,29.2c6.2,10.7,7.3,24.1,7.3,29.5 C160.1,134.3,157.4,142.3,152.3,148.9z"/>
<path fill="url(#radialGradient-4)" d="M152.3,148.9c-0.8,1-3.3,2.5-3.3,5.7c0,2.6,1.7,5.1,4.7,7.2c14.4,10,41.5,8.7,41.6,8.7 c10.7,0,21.1-2.9,30.3-8.3c18.8-11,30.4-31.1,30.4-52.9c0.3-22.4-8-37.3-11.3-43.9C223.5,23.9,177.7,0,128,0C58,0,1,56.2,0,126.2 c0.5-36.5,36.8-66,80-66c3.5,0,23.5,0.3,42,10.1c16.3,8.6,24.9,18.9,30.8,29.2c6.2,10.7,7.3,24.1,7.3,29.5 C160.1,134.3,157.4,142.3,152.3,148.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<g transform="scale(0.521)">
<g transform="scale(1.8) translate(3,3)">
<defs>
<radialGradient id="RG1" cx="50%" cy="50%" fx="50%" fy="50%" r="50%">
<stop style="stop-color:rgb(255,29,48);stop-opacity:0.75;" offset="0%"/>
<stop style="stop-color:rgb(180,4,15);stop-opacity:1;" offset="100%"/>
</radialGradient>
<linearGradient id="LG1" x1="20%" y1="80%" x2="20%" y2="0%">
<stop style="stop-color:rgb(253,76,86);stop-opacity:0.75;" offset="0%"/>
<stop style="stop-color:rgb(158,4,4);stop-opacity:1;" offset="100%"/>
</linearGradient>
</defs>
<path style="fill:url(#RG1)" d="m 81,11 a 50,50 1 1 0 0,78 a 36.9,45 0 1 1 0,-78"/>
<path style="fill:url(#LG1)" d="m 36,19 a 36.9,45 1 0 1 45,-8 a 50,50 1 0 1 0,78 a 36.9,45 1 0 1 -45,-8 a 26.5,35 1 1 0 0,-62"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="50%" y1="100%" x2="50%" y2="0%" id="linearGradient-1">
<stop stop-color="#420C5D" offset="0%"/>
<stop stop-color="#951AD1" offset="100%"/>
</linearGradient>
<path d="M25,29 C152.577777,29 256,131.974508 256,259 C256,386.025492 152.577777,489 25,489 L25,29 Z" id="path-2"/>
<filter x="-18.2%" y="-7.4%" width="129.4%" height="114.8%" filterUnits="objectBoundingBox" id="filter-3">
<feOffset dx="-8" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"/>
<feColorMatrix values="0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"/>
</filter>
</defs>
<g id="tor-browser-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon_512x512">
<g id="Group">
<g id="tb_icon/Stable">
<g id="Stable">
<circle id="background" fill="#F2E4FF" fill-rule="nonzero" cx="256" cy="256" r="246"/>
<path d="M256.525143,465.439707 L256.525143,434.406609 C354.826191,434.122748 434.420802,354.364917 434.420802,255.992903 C434.420802,157.627987 354.826191,77.8701558 256.525143,77.5862948 L256.525143,46.5531962 C371.964296,46.8441537 465.446804,140.489882 465.446804,255.992903 C465.446804,371.503022 371.964296,465.155846 256.525143,465.439707 Z M256.525143,356.820314 C311.970283,356.529356 356.8487,311.516106 356.8487,255.992903 C356.8487,200.476798 311.970283,155.463547 256.525143,155.17259 L256.525143,124.146588 C329.115485,124.430449 387.881799,183.338693 387.881799,255.992903 C387.881799,328.654211 329.115485,387.562455 256.525143,387.846316 L256.525143,356.820314 Z M256.525143,201.718689 C286.266674,202.00255 310.3026,226.180407 310.3026,255.992903 C310.3026,285.812497 286.266674,309.990353 256.525143,310.274214 L256.525143,201.718689 Z M0,255.992903 C0,397.384044 114.60886,512 256,512 C397.384044,512 512,397.384044 512,255.992903 C512,114.60886 397.384044,0 256,0 C114.60886,0 0,114.60886 0,255.992903 Z" id="center" fill="url(#linearGradient-1)"/>
<g id="half" transform="translate(140.500000, 259.000000) scale(-1, 1) translate(-140.500000, -259.000000) ">
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"/>
<use fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1 @@
<svg width="1900px" height="1900px" viewBox="0 0 1900 1900" xmlns="http://www.w3.org/2000/svg"><path fill="#ef3939" d="M944 1830c386 0 600 0 740-140s140-354 140-740 0-600-140-740S1330 70 944 70s-600 0-740 140S64 564 64 950s0 600 140 740 354 140 740 140z"/><linearGradient id="a" x1="61.24" x2="145.33" y1="37.94" y2="183.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity=".2"/><stop offset=".79" stop-opacity=".05"/></linearGradient><path fill="url(#a)" d="M151.6 62.4A66 66 0 0030.5 78a65.57 65.57 0 006.8 50.4c.1.2.2.4.4.6l31 53.8 25.5.2c17.1 0 30.9 0 42.2-1.2 14-1.5 24.1-5 31.9-12.8 11.3-11.3 13.5-27.6 13.9-53.8l-30.6-52.8z" transform="scale(10)"/><path fill="#fff" d="M1407 484a657.9 657.9 0 00-932 0 660.9 660.9 0 000 933 657.9 657.9 0 00932 0 660.9 660.9 0 000-933zm-39 304l-326 567c-20 35-49 56-90 59-45 3-80-16-103-55L519 786c-42-73 5-162 89-166 44-2 78 18 101 57 31 52 61 105 91 158l66 114c33 55 80 85 144 89 90 5 174-60 185-156 1-7 1-14 2-18 0-31-6-57-19-82-34-68 2-143 75-160 60-13 121 31 129 91 4 27-1 52-14 75z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

107
docs/_media/icons/vlc.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 237 KiB

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="760" height="760" id="svg586">
<title id="title3704">XFCE 4 Logo</title>
<metadata id="metadata12">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>XFCE 4 Logo</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>Savvas Radevic</dc:title>
</cc:Agent>
</dc:creator>
<dc:source>http://www.xfce.org/about/artwork</dc:source>
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/"/>
<dc:description>XFCE logo
* Based on xfce_logo.svg from http://www.xfce.org/about/artwork
* Optimized colours
* Added "X" and "XFCE" text.
</dc:description>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/by-sa/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
</cc:License>
</rdf:RDF>
</metadata>
<defs id="defs588">
<linearGradient id="linearGradient4380">
<stop id="stop4382" style="stop-color:#000000" offset="0"/>
<stop id="stop4384" style="stop-color:#000000;stop-opacity:0" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient3694">
<stop id="stop3702" style="stop-color:#b7b7b7" offset="0"/>
<stop id="stop3698" style="stop-color:#000000" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient3743">
<stop id="stop3745" style="stop-color:#ffffff" offset="0"/>
<stop id="stop3751" style="stop-color:#7fd4ee" offset="0.5"/>
<stop id="stop3747" style="stop-color:#00aade" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient3652">
<stop id="stop3654" style="stop-color:#ffffff" offset="0"/>
<stop id="stop3662" style="stop-color:#d5e2ec" offset="0.49903482"/>
<stop id="stop3656" style="stop-color:#d5e2ec" offset="1"/>
</linearGradient>
<filter color-interpolation-filters="sRGB" id="filter3729">
<feGaussianBlur stdDeviation="6.4154088" id="feGaussianBlur3731"/>
</filter>
<linearGradient x1="965.02625" y1="17.489901" x2="1557.8665" y2="663.80927" id="linearGradient3737" xlink:href="#linearGradient3652" gradientUnits="userSpaceOnUse" gradientTransform="translate(-1581.6355,147.78233)"/>
<linearGradient x1="33.526711" y1="441.98093" x2="159.24117" y2="581.41302" id="linearGradient3966" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse"/>
<linearGradient x1="245.51741" y1="426.95151" x2="305.39166" y2="531.48969" id="linearGradient3968" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.38239847,2.6767893)"/>
<linearGradient x1="601.53467" y1="434.94836" x2="668.81775" y2="543.35834" id="linearGradient3970" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse"/>
<linearGradient x1="440.52084" y1="455.93307" x2="486.95523" y2="535.30469" id="linearGradient3972" xlink:href="#linearGradient3743" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.50986462,6.1183754)"/>
<linearGradient x1="118.25153" y1="254.24648" x2="291.87143" y2="254.24648" id="linearGradient4386" xlink:href="#linearGradient4380" gradientUnits="userSpaceOnUse"/>
<filter color-interpolation-filters="sRGB" id="filter4631">
<feGaussianBlur id="feGaussianBlur4633" stdDeviation="2.3336003"/>
</filter>
<linearGradient x1="174.28104" y1="164.16707" x2="236.40213" y2="253.70575" id="linearGradient4162" xlink:href="#linearGradient3694" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.7524926,0,0,3.7524926,-424.92228,-686.03042)"/>
<filter color-interpolation-filters="sRGB" id="filter4223">
<feGaussianBlur id="feGaussianBlur4225" stdDeviation="4.6672006"/>
</filter>
<filter color-interpolation-filters="sRGB" id="filter4266">
<feGaussianBlur id="feGaussianBlur4268" stdDeviation="1.308628"/>
</filter>
</defs>
<g transform="translate(754.35152,-70.104999)" id="g3733">
<path d="M -688.36669,231.76247 -533.89627,88.530764 -359.32377,303.30745 -170.75037,88.081185 -25.665766,221.31553 -234.39997,435.40777 -20.542666,663.49077 -175.64877,807.89088 -372.21127,579.61811 -569.06737,810.83624 -709.17754,672.17796 -494.29357,442.77473 -688.36669,231.76247 z" id="path2870-8" style="filter:url(#filter3729)"/>
<path d="M -692.09302,232.01783 -537.6226,88.786116 -363.0501,303.5628 -174.4767,88.336537 -29.392112,221.57089 -238.1263,435.66312 -24.269012,663.74612 -179.3751,808.14623 -375.9376,579.87346 -572.7937,811.09159 -712.90387,672.43331 -498.0199,443.03008 -692.09302,232.01783 z" id="path2870" style="fill:url(#linearGradient3737)"/>
</g>
<g transform="translate(20.600025,32.751608)" id="g3915">
<g transform="translate(6,4.5)" id="g3803-6" style="opacity:0.8;filter:url(#filter4223)">
<path d="m 58.345585,417.63579 56.432265,69.38376 56.45069,-66.66449 40.49963,40.30749 -61.78078,65.91662 65.45855,68.01565 -42.74601,42.50269 L 108.5111,564.35998 48.268494,638.64971 7.1899623,597.32826 74.708237,531.54185 13.029005,462.45552 z" id="path3912" style="stroke:#000000;stroke-width:3"/>
<path d="m 245.32235,432.76045 c 0,0 117.20479,-1.21951 117.20479,-0.74369 0,0.47308 -0.46199,46.74243 -0.46199,46.74243 l -56.15161,2.88037 -0.17056,36.17 53.92068,0.57225 0.17335,44.40991 -53.06171,1.1226 -0.0416,75.28312 -62.11058,-0.88382 c 0,0 -0.24879,-205.55317 0.69921,-205.55317 z" id="path3914" style="stroke:#000000;stroke-width:3"/>
<path d="m 542.33906,431.37943 c 3.7919,18.02364 7.581,36.04727 11.373,54.07091 -36.2734,12.22845 -88.71272,15.00605 -91.24882,68.09506 6.6716,52.01081 70.35438,34.37103 98.68398,36.76942 0.4726,17.23426 0.43804,33.70097 0.91074,50.93523 C 435.29236,657.93973 400.36214,596.11929 398.22454,573.7307 385.72484,472.89955 486.80926,443.52538 542.33906,431.37943 z" id="path3916" style="stroke:#000000;stroke-width:3"/>
<path d="m 597.26819,428.94061 116.39026,-0.42207 -1.34547,46.26995 -56.4993,0.0539 0.79926,34.59126 48.82809,-0.68549 -0.56651,45.70916 -48.37488,0.80727 0.4754,32.7275 57.10463,0.0964 0.11331,50.46219 -118.30011,-3.34717 z" id="path3918" style="stroke:#000000;stroke-width:3"/>
</g>
<g id="g4621">
<path d="m 58.345585,417.63579 56.432265,69.38376 56.45069,-66.66449 40.49963,40.30749 -61.78078,65.91662 65.45855,68.01565 -42.74601,42.50269 L 108.5111,564.35998 48.268494,638.64971 7.1899623,597.32826 74.708237,531.54185 13.029005,462.45552 z" id="path4623" style="fill:url(#linearGradient3966);stroke:#00aade;stroke-width:3"/>
<path d="m 245.32235,432.76045 c 0,0 117.20479,-1.21951 117.20479,-0.74369 0,0.47308 -0.46199,46.74243 -0.46199,46.74243 l -56.15161,2.88037 -0.17056,36.17 53.92068,0.57225 0.17335,44.40991 -53.06171,1.1226 -0.0416,75.28312 -62.11058,-0.88382 c 0,0 -0.24879,-205.55317 0.69921,-205.55317 z" id="path4625" style="fill:url(#linearGradient3968);stroke:#00aade;stroke-width:3"/>
<path d="m 542.33906,431.37943 c 3.7919,18.02364 7.581,36.04727 11.373,54.07091 -36.2734,12.22845 -88.71272,15.00605 -91.24882,68.09506 6.6716,52.01081 70.35438,34.37103 98.68398,36.76942 0.4726,17.23426 0.43804,33.70097 0.91074,50.93523 C 435.29236,657.93973 400.36214,596.11929 398.22454,573.7307 385.72484,472.89955 486.80926,443.52538 542.33906,431.37943 z" id="path4627" style="fill:url(#linearGradient3972);stroke:#00aade;stroke-width:3"/>
<path d="m 597.26819,428.94061 116.39026,-0.42207 -1.34547,46.26995 -56.4993,0.0539 0.79926,34.59126 48.82809,-0.68549 -0.56651,45.70916 -48.37488,0.80727 0.4754,32.7275 57.10463,0.0964 0.11331,50.46219 -118.30011,-3.34717 z" id="path4629" style="fill:url(#linearGradient3970);stroke:#00aade;stroke-width:3"/>
</g>
</g>
<g transform="matrix(3.7524926,0,0,3.7524926,-424.92228,-686.03042)" id="g23" style="fill:url(#linearGradient4386);stroke:#000000;stroke-width:0.26814505">
<use transform="translate(1.8654516,1.1992028)" id="use4246" style="opacity:0.8;filter:url(#filter4266)" x="0" y="0" width="760" height="760" xlink:href="#path14"/>
<path d="m 118.94557,209.56947 c -0.915,0.501 7.2975,5.88275 22.6875,12.21875 15.391,6.336 36.667,22.37675 35,24.96875 -4.999,7.041 -7.94125,15.48225 -5.90625,29.90625 0.367,2.539 -7.54275,6.95025 -6.96875,10.78125 0.48,2.915 8.838,-2.896 9.75,-1.75 0.558,2.057 -4.4665,12.47475 -2.4375,13.21875 1.484,0.441 14.52325,-12.4015 15.28125,-12.1875 21.514,6.504 49.5205,1.61225 50.0625,1.40625 -0.051,-0.113 20.12,9.49175 21.875,8.34375 4.427,-4.794 -8.387,-11.551 -6.25,-14.25 0.937,-1.413 15.62075,5.30275 16.34375,3.46875 1.052,-3.175 -6.817,-9.8375 -8,-11.8125 -1.029,-1.821 -1.23275,-4.75375 1.40625,-6.59375 2.64,-1.84 29.632,-12.16625 28.5,-20.15625 -1.957,-14.518 -34.26025,-15.4655 -34.78125,-16.6875 -0.245,-0.574 1.44825,-20.76025 -7.96875,-17.03125 -5.798,2.158 -1.09375,15.0245 -0.71875,16.6875 0.346,1.531 -2.574,1.3595 -3.125,0.6875 -1.23,-1.502 -5.2725,-14.92525 -10.0625,-12.53125 -6.718,3.704 -0.41,15.28125 -1.75,15.65625 -2.489,1.391 -2.34875,4.7945 -4.34375,7.5625 -3.29,4.715 -27.917,-2.2055 -35.875,-2.6875 -5.826,-0.344 -11.38025,4.4205 -11.90625,3.8125 -7.094,-7.104 -21.76475,-19.48475 -37.84375,-23.59375 -9.017,-2.447 -22.43075,-9.5275 -22.96875,-9.4375 z m 164.21875,13 c -0.53716,0.23183 -2.70444,11.39669 -1.65625,11.90625 0.929,0.695 2.5355,-11.257 1.6875,-11.875 -0.0138,-0.016 -0.0139,-0.0387 -0.0313,-0.0313 z m 7.96875,1.53125 c -1.21841,-0.0224 -7.71547,10.3605 -6.21875,11.46875 0.929,0.695 6.7105,-11.0445 6.3125,-11.4375 -0.0278,-0.023 -0.0544,-0.0305 -0.0937,-0.0313 z m -31.5,12.65625 c 1.53924,0 2.78125,1.40374 2.78125,3.15625 0,1.75251 -1.24201,3.1875 -2.78125,3.1875 -1.53924,0 -2.78125,-1.43499 -2.78125,-3.1875 0,-1.75251 1.24201,-3.15625 2.78125,-3.15625 z m -6.25,1.90625 c 1.184,5.111 3.0165,6.7255 8.5625,6.5625 -7.059,6.473 -11.3335,-4.6035 -8.5625,-6.5625 z m 14.9375,13.375 c 0.43934,-0.0356 0.77562,0.0333 0.96875,0.21875 2.781,1.008 -11.93325,6.3115 -12.03125,5.5625 -0.14787,-0.51625 7.98709,-5.53182 11.0625,-5.78125 z m 1.8125,3.375 c 0.15222,-0.0118 0.29388,0.0358 0.40625,0.0937 2.625,2.608 -8.3105,8.3905 -8.1875,8.1875 -0.88125,-0.5325 5.498,-8.1041 7.78125,-8.28125 z" id="path14" style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.26814505"/>
<path d="m 21.40625,100.375 c -3.433531,1.88 27.405389,22.06796 85.15625,45.84375 57.75461,23.77579 137.59916,83.99229 131.34375,93.71875 -18.75871,26.4213 -29.82382,58.0928 -22.1875,112.21875 1.37716,9.52758 -28.27893,26.0617 -26.125,40.4375 1.62157,9.84768 27.16679,-6.83583 34.75,-7.125 C 211.59347,288.40086 380.71993,233.48854 524.375,211.125 c 0.42249,-0.61852 0.9417,-1.14571 1.53125,-1.5625 0.10346,0.44662 0.20625,0.84878 0.3125,1.28125 4.64119,-0.71462 9.26483,-1.41562 13.84375,-2.0625 1.73622,-3.79693 5.24284,-6.375 9.28125,-6.375 3.18263,0 6.02524,1.58871 7.9375,4.125 23.87521,-2.99523 46.50573,-5.06548 66.84375,-6.21875 -37.95486,-16.11541 -88.98683,-18.63101 -90.25,-21.59375 -0.91936,-2.15393 5.43097,-77.89929 -29.90625,-63.90625 -21.75695,8.09788 -4.09468,56.35335 -2.6875,62.59375 1.29836,5.74507 -9.68238,5.11543 -11.75,2.59375 -4.61557,-5.63624 -19.77556,-56.01472 -37.75,-47.03125 -25.20925,13.89923 -1.53416,57.34282 -6.5625,58.75 -9.33995,5.21972 -8.82628,17.9881 -16.3125,28.375 -12.3457,17.693 -104.76266,-8.2538 -134.625,-10.0625 -21.86202,-1.29086 -42.68244,16.56277 -44.65625,14.28125 -26.62018,-26.65771 -81.66367,-73.11226 -142,-88.53125 C 73.788774,126.5989 23.425091,100.03728 21.40625,100.375 z" transform="matrix(0.26648953,0,0,0.26648953,113.23734,182.81993)" id="path4083" style="fill:url(#linearGradient4162)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -2,6 +2,19 @@
## master branch
### New Features
- Added `m1k1o/neko:vivaldi` tag (thanks @Xeddius).
- Added `m1k1o/neko:opera` tag (thanks @prophetofxenu).
- Added `NEKO_PATH_PREFIX`.
- Added screenshot function `/screenshot.jpg?pwd=<admin>`, works only for unlocked rooms.
### Misc
- Server: Split `remote` to `desktop` and `capture`.
- Server: Refactored `xorg` - added `xevent` and clipboard is handled as event (no looped polling anymore).
- Introduced `NEKO_AUDIO_CODEC=` and `NEKO_VIDEO_CODEC=` as a new way of setting codecs.
## [n.eko v2.6](https://github.com/m1k1o/neko/releases/tag/v2.6)
### Bugs
- Fixed fullscreen incompatibility for Safari [#121](https://github.com/m1k1o/neko/issues/121).
- Fixed bad emoji matching for e.g. `:+1:` and `:100:` with new regex `/^:([^:\s]+):/`.
@ -11,11 +24,17 @@
- Fixed clipboard sync in chromium based browsers.
- Added support for implicit control (using `NEKO_IMPLICITCONTROL=1`). That means, users do not need to request control prior usage.
- Automatically start broadcasting using `NEKO_BROADCAST_URL=rtmp://your-rtmp-endpoint/live` (thanks @konsti).
- Added `m1k1o/neko:remmina` tag (by @lowne).
### Misc
- Automatic WebRTC SDP negotiation using onnegotiationneeded handlers. This allows adding/removing track on demand in a session.
- Added UDP and TCP mux for WebRTC connection. It should handle multiple peers.
- Broadcast status change is sent to all admins now.
- NordVPN replaced with Sponsorblock extension in default configuration #144.
- Removed `vncviewer` image, as its functionality is replaced and extended by remmina.
- Opus uses `useinbandfec=1` from now on, hopefully fixes minor audio loss issues.
- Font Awesome and Sweetalert2 upgraded to newest major version.
- Add chinese characters support.
## [n.eko v2.5](https://github.com/m1k1o/neko/releases/tag/v2.5)

View File

@ -1,5 +1,19 @@
# Getting started & FAQ
<div align="center">
<img src="../_media/icons/firefox.svg" title="m1k1o/neko:firefox" width="60" height="auto"/>
<img src="../_media/icons/google-chrome.svg" title="m1k1o/neko:google-chrome" width="60" height="auto"/>
<img src="../_media/icons/chromium.svg" title="m1k1o/neko:chromium" width="60" height="auto"/>
<img src="../_media/icons/microsoft-edge.svg" title="m1k1o/neko:microsoft-edge" width="60" height="auto"/>
<img src="../_media/icons/brave.svg" title="m1k1o/neko:brave" width="60" height="auto"/>
<img src="../_media/icons/vivaldi.svg" title="m1k1o/neko:vivaldi" width="60" height="auto"/>
<img src="../_media/icons/opera.svg" title="m1k1o/neko:opera" width="60" height="auto"/>
<img src="../_media/icons/tor-browser.svg" title="m1k1o/neko:tor-browser" width="60" height="auto"/>
<img src="../_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/>
<img src="../_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/>
<img src="../_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
</div>
Use the following docker images:
- `m1k1o/neko:latest` or `m1k1o/neko:firefox` - for Firefox.
- `m1k1o/neko:chromium` - for Chromium (needs `--cap-add=SYS_ADMIN`, see the [security implications](https://www.redhat.com/en/blog/container-tidbits-adding-capabilities-container)).
@ -7,8 +21,12 @@ Use the following docker images:
- `m1k1o/neko:ungoogled-chromium` - for [Ungoogled Chromium](https://github.com/Eloston/ungoogled-chromium) (needs `--cap-add=SYS_ADMIN`, see the [security implications](https://www.redhat.com/en/blog/container-tidbits-adding-capabilities-container)) (by @whalehub).
- `m1k1o/neko:microsoft-edge` - for Microsoft Edge (needs `--cap-add=SYS_ADMIN`, see the [security implications](https://www.redhat.com/en/blog/container-tidbits-adding-capabilities-container)).
- `m1k1o/neko:brave` - for [Brave Browser](https://brave.com) (needs `--cap-add=SYS_ADMIN`, see the [security implications](https://www.redhat.com/en/blog/container-tidbits-adding-capabilities-container)).
- `m1k1o/neko:vivaldi` - for [Vivaldi Browser](https://vivaldi.com) (needs `--cap-add=SYS_ADMIN`, see the [security implications](https://www.redhat.com/en/blog/container-tidbits-adding-capabilities-container)) (by @Xeddius).
- `m1k1o/neko:opera` for [Opera Browser](https://opera.com) (requires extra steps to enable DRM, see instructions [here](https://www.reddit.com/r/operabrowser/wiki/opera/linux_widevine_config/). libffmpeg is already configured.) (by @prophetofxenu)
- `m1k1o/neko:tor-browser` - for Tor Browser.
- `m1k1o/neko:vncviewer` - for simple VNC viewer (specify `NEKO_VNC_URL` to your VNC target).
- `m1k1o/neko:remmina` - for remote desktop connection (by @lowne).
- Pass env var `REMMINA_URL=<proto>://[<username>[:<password>]@]server[:port]` (proto being `vnc`, `rdp` or `spice`).
- Or create your custom configuration with remmina locally (it's saved in `~/.local/share/remmina/path_to_profile.remmina`) and bind-mount it, then pass env var `REMMINA_PROFILE=<path_to_profile.remmina>`.
- `m1k1o/neko:vlc` - for VLC Video player (needs volume mounted to `/media` with local video files, or setting `VLC_MEDIA=/media` path).
- `m1k1o/neko:xfce` - for a shared desktop / installing shared software.
- `m1k1o/neko:base` - for custom base.
@ -35,10 +53,9 @@ Images (except `arm-`) are built using GitHub actions on every push and on weekl
- You can change API port (8080).
- This **WILL** work: `3000:8080`
#### But there is a hope!
There has been an attempt to implement [single port ice using tcp and udp mux](https://github.com/m1k1o/neko/commit/c97b1fc4541caabf6b00331d081b02d2f9c58751) ([#106](https://github.com/m1k1o/neko/pull/106)), that allows using one port instead (each for TCP and/or UDP). This feature is not properly tested yet and only experimental.
### Using mux instead of epr
We can use TCP mux and/or UDP mux, example:
When using a mux, not so many ports are needed.
```yaml
version: "3.4"
@ -60,6 +77,13 @@ services:
NEKO_ICELITE: 1
```
- When using mux, `NEKO_EPR` is ignored.
- You only need to expose maximum two ports for WebRTC on your router/firewall and have many users connected.
- It can even be the same port number, so e.g. `NEKO_TCPMUX: 8081` and `NEKO_UDPMUX: 8081`.
- You can use them alone (either TCP or UDP) when needed.
- UDP is generally better for latency. But some networks block UDP so it is good to have TCP available as fallback.
- Still, using `NEKO_ICELITE=true` is recommended.
### Want to customize and install own add-ons, set custom bookmarks?
- You would need to modify the existing policy file and mount it to your container.
- For Firefox, copy [this](https://github.com/m1k1o/neko/blob/master/.docker/firefox/policies.json) file, modify and mount it as: ` -v '${PWD}/policies.json:/usr/lib/firefox/distribution/policies.json'`
@ -79,6 +103,11 @@ services:
- There are no accounts, display name (a.k.a. username) can be freely chosen. Only password needs to match. Depending on which password matches, the visitor gets its privilege:
- Anyone, who enters with `NEKO_PASSWORD` will be **user**.
- Anyone, who enters with `NEKO_PASSWORD_ADMIN` will be **admin**.
- Disabling passwords is not possible. However, you can use following query parameters to create auto-join links:
- Adding `?pwd=<password>` will prefill password.
- Adding `?usr=<display-name>` will prefill username.
- Adding `?cast=1` will hide all control and show only video.
- e.g. `http(s)://<URL:Port>/?pwd=neko&usr=guest&cast=1`
### Screen size
- Only admins can change screen size.

View File

@ -1,81 +1,141 @@
# Configuration
Config values can be set using three methods, sorted on this page by priority.
Example, setting `nat1to1` variable:
- As env variable: `NEKO_NAT1TO1=<ip>`
- As argument: `--nat1to1=<ip>`
- In YAML config file:
```yaml
nat1to1: <ip>
```
## Environment variables
```
NEKO_SCREEN:
#### `NEKO_SCREEN`:
- Resolution after startup. Only Admins can change this later.
- e.g. '1920x1080@30'
NEKO_PASSWORD:
- Password for the user login
- e.g. 'user_password'
NEKO_PASSWORD_ADMIN:
- Password for the admin login
- e.g. 'admin_password'
NEKO_EPR:
- For WebRTC needed range of ports
- e.g. 52000-52100
NEKO_VP8:
- If vp8 should be used as video encoder for the stream (default encoder)
- e.g. 'true'
NEKO_VP9:
- If vp9 should be used as video encoder for the stream (Parameter not optimized yet)
- e.g. 'false'
NEKO_H264:
- If h264 should be used as video encoder for the stream (second best option)
- e.g. 'false'
NEKO_VIDEO_BITRATE:
- Bitrate of the video stream in kb/s
- e.g. 3500
NEKO_VIDEO:
- Makes it possible to create custom gstreamer pipelines. With this you could find the best quality for your CPU
- Installed are gstreamer1.0-plugins-base / gstreamer1.0-plugins-good / gstreamer1.0-plugins-bad / gstreamer1.0-plugins-ugly
- e.g. ' ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! video/x-raw,format=NV12 ! x264enc threads=4 bitrate=3500 key-int-max=60 vbv-buf-capacity=4000 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream '
NEKO_MAX_FPS:
- The resulting stream frames per seconds should be capped (0 for uncapped)
- e.g. 0
NEKO_OPUS:
- If opus should be used as audio encoder for the stream (default encoder)
- e.g. 'true'
NEKO_G722:
- If g722 should be used as audio encoder for the stream
- e.g. 'false'
NEKO_PCMU:
- If pcmu should be used as audio encoder for the stream
- e.g. 'false'
NEKO_PCMA:
- If pcma should be used as audio encoder for the stream
- e.g. 'false'
NEKO_AUDIO_BITRATE:
- Bitrate of the audio stream in kb/s
- e.g. 196
NEKO_CERT:
- Path to the SSL-Certificate
- e.g. '/certs/cert.pem'
NEKO_KEY:
- Path to the SSL-Certificate private key
- e.g. '/certs/key.pem'
NEKO_ICELITE:
- Use the ice lite protocol
- e.g. false
NEKO_ICESERVER:
- Describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer (simple usage for server without authentication)
- e.g. 'stun:stun.l.google.com:19302'
NEKO_ICESERVERS:
- Describes multiple STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer
- e.g. '[{"urls": ["turn:turn.example.com:19302", "stun:stun.example.com:19302"], "username": "name", "credential": "password"}, {"urls": ["stun:stun.example2.com:19302"]}]'
- [More information](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer)
NEKO_LOCKS:
- Resources, that will be locked when starting, separated by whitespace.
- Currently supported: control login
NEKO_CONTROL_PROTECTION:
- e.g. `1920x1080@30`
#### `NEKO_PASSWORD`:
- Password for the user login.
- e.g. `user_password`
#### `NEKO_PASSWORD_ADMIN`:
- Password for the admin login.
- e.g. `admin_password`
#### `NEKO_CONTROL_PROTECTION`:
- Control protection means, users can gain control only if at least one admin is in the room.
- e.g. false
NEKO_BROADCAST_URL:
- Set a default URL for broadcast streams. Setting this value will automatically enable broadcasting when n.eko starts. It can be disabled/changed later in GUI.
```
- e.g. `false`
#### `NEKO_IMPLICIT_CONTROL`:
- If enabled members can gain control implicitly, they don't needd to request control.
- e.g. `false`
#### `NEKO_LOCKS`:
- Resources, that will be locked when starting, separated by whitespace.
- Currently supported:
- `control`
- `login`
- e.g. `control`
## Agruments
### WebRTC
#### `NEKO_EPR`:
- For WebRTC needed range of UDP ports.
- e.g. `52000-52099`
#### `NEKO_UDPMUX`:
- Alternative to epr with only one UDP port.
- e.g. `52100`
#### `NEKO_TCPMUX`:
- Use TCP connection, meant as fallback for UDP.
- e.g. `52100`
#### `NEKO_NAT1TO1`:
- IP of the server that will be sent to client, if not specified, public IP is automatically resolved.
- e.g. `10.0.0.1`
#### `NEKO_IPFETCH`:
- Automatically fetch IP address from given URL when `nat1to1` is not specified.
- e.g. `http://checkip.amazonaws.com`
#### `NEKO_ICELITE`:
- Use the ice lite protocol.
- e.g. `false`
#### `NEKO_ICESERVER`:
- Describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer (simple usage for server without authentication).
- e.g. `stun:stun.l.google.com:19302`
#### `NEKO_ICESERVERS`:
- Describes multiple STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer.
- e.g. `[{"urls": ["turn:turn.example.com:19302", "stun:stun.example.com:19302"], "username": "name", "credential": "password"}, {"urls": ["stun:stun.example2.com:19302"]}]`
- [More information](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer)
### Video
#### `NEKO_VIDEO_CODEC`:
- vp8 *(default encoder)*
- vp9 *(parameter not optimized yet)*
- h264 *(second best option)*
#### `NEKO_VIDEO_BITRATE`:
- Bitrate of the video stream in kb/s.
- e.g. 3500
#### `NEKO_VIDEO`:
- Makes it possible to create custom gstreamer video pipeline. With this you could find the best quality for your CPU.
- Installed are
- `gstreamer1.0-plugins-base`
- `gstreamer1.0-plugins-good`
- `gstreamer1.0-plugins-bad`
- `gstreamer1.0-plugins-ugly`
- e.g. `ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! video/x-raw,format=NV12 ! x264enc threads=4 bitrate=3500 key-int-max=60 vbv-buf-capacity=4000 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream`
#### `NEKO_MAX_FPS`:
- The resulting stream frames per seconds should be capped *(0 for uncapped)*.
- e.g. `0`
#### `NEKO_HWENC`:
- Use hardware accelerated encoding, for now supported only `VAAPI`.
- e.g. `VAAPI`
### Audio
#### `NEKO_AUDIO_CODEC`:
- opus *(default encoder)*
- g722
- pcmu
- pcma
#### `NEKO_AUDIO_BITRATE`:
- Bitrate of the audio stream in kb/s.
- e.g. `196`
#### `NEKO_AUDIO`:
- Makes it possible to create custom gstreamer audio pipeline, same as for video.
e.g. `pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! opusenc bitrate=128000`
### Broadcast
#### `NEKO_BROADCAST_PIPELINE`:
- Makes it possible to create custom gstreamer pipeline used for broadcasting, strings `{url}`, `{device}` and `{display}` will be replaced.
#### `NEKO_BROADCAST_URL`:
- Set a default URL for broadcast streams. Setting this value will automatically enable broadcasting when n.eko starts. It can be disabled/changed later by admins in the GUI.
- e.g. `rtmp://<your-server>:1935/ingest/<stream-key>`
### Server
#### `NEKO_BIND`:
- Address/port/socket where neko binds to *(default 127.0.0.1:8080)*.
- e.g. `:8080`
#### `NEKO_CERT`:
- Path to the SSL-Certificate.
- e.g. `/certs/cert.pem`
#### `NEKO_KEY`:
- Path to the SSL-Certificate private key.
- e.g. `/certs/key.pem`
#### `NEKO_PROXY`:
- Enable reverse proxy mode, so that neko trusts `X-Forwarded-For` headers.
- e.g. `false`
#### `NEKO_PATH_PREFIX`:
- Path prefix for HTTP requests.
- e.g. `/neko/`
### Expert settings
#### `NEKO_DISPLAY`:
- XDisplay to capture.
#### `NEKO_DEVICE`:
- Audio device be to captured.
#### `NEKO_STATIC`:
- Path to neko client files to serve.
## Arguments
You can execute `neko serve --help` to see available arguments.
@ -86,6 +146,7 @@ Usage:
Flags:
--audio string audio codec parameters to use for streaming
--audio_bitrate int audio bitrate in kbit/s (default 128)
--audio_codec string audio codec to be used (default "opus")
--bind string address/port/socket to serve neko (default "127.0.0.1:8080")
--broadcast_pipeline string custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced
--broadcast_url string URL for broadcasting, setting this value will automatically enable broadcasting
@ -94,9 +155,10 @@ Flags:
--device string audio device to capture (default "auto_null.monitor")
--display string XDisplay to capture (default ":99.0")
--epr string limits the pool of ephemeral ports that ICE UDP connections can allocate from (default "59000-59100")
--g722 use G722 audio codec
--h264 use H264 video codec
--g722 DEPRECATED: use audio_codec
--h264 DEPRECATED: use video_codec
-h, --help help for serve
--hwenc string use hardware accelerated encoding
--icelite configures whether or not the ice agent should be a lite agent
--iceserver strings describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer (default [stun:stun.l.google.com:19302])
--iceservers string describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer
@ -106,11 +168,12 @@ Flags:
--locks strings resources, that will be locked when starting (control, login)
--max_fps int maximum fps delivered via WebRTC, 0 is for no maximum (default 25)
--nat1to1 strings sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used
--opus use Opus audio codec
--opus DEPRECATED: use audio_codec
--password string password for connecting to stream (default "neko")
--password_admin string admin password for connecting to stream (default "admin")
--pcma use PCMA audio codec
--pcmu use PCMU audio codec
--path_prefix string path prefix for HTTP requests (default "/")
--pcma DEPRECATED: use audio_codec
--pcmu DEPRECATED: use audio_codec
--proxy enable reverse proxy mode
--screen string default screen resolution and framerate (default "1280x720@30")
--static string path to neko client files to serve (default "./www")
@ -118,8 +181,9 @@ Flags:
--udpmux int single UDP mux port for all peers
--video string video codec parameters to use for streaming
--video_bitrate int video bitrate in kbit/s (default 3072)
--vp8 use VP8 video codec
--vp9 use VP9 video codec
--video_codec string video codec to be used (default "vp8")
--vp8 DEPRECATED: use video_codec
--vp9 DEPRECATED: use video_codec
Global Flags:
--config string configuration file path
@ -130,3 +194,30 @@ Global Flags:
## Config file
You can mount YAML config file to docker container on this path `/etc/neko/neko.yaml` and store your configuration there.
Config uses the keys from arguments, that can be viewed in program's help output.
Example (with just some of the available arguments):
```yaml
# audio bitrate in kbit/s
audio_bitrate: 128
# video bitrate in kbit/s
video_bitrate: 3072
# maximum fps delivered via WebRTC, 0 is for no maximum
max_fps: 25
# password for connecting to stream
password: "neko"
# admin password for connecting to stream
password_admin: "admin"
# default screen resolution and framerate
screen: "1280x720@30"
# limits the pool of ephemeral ports that ICE UDP connections can allocate from
epr: "59000-59100"
```

View File

@ -98,7 +98,7 @@ services:
! v4l2h264enc extra-controls="controls,h264_profile=0,video_bitrate=1250000;"
! h264parse config-interval=3
! video/x-h264,profile=baseline,stream-format=byte-stream
NEKO_H264: 1
NEKO_VIDEO_CODEC: h264
```
## Not using docker?

View File

@ -5,7 +5,7 @@ import (
"github.com/spf13/cobra"
"m1k1o/neko"
"m1k1o/neko/internal/types/config"
"m1k1o/neko/internal/config"
)
func init() {
@ -19,8 +19,8 @@ func init() {
configs := []config.Config{
neko.Service.Server,
neko.Service.WebRTC,
neko.Service.Remote,
neko.Service.Broadcast,
neko.Service.Capture,
neko.Service.Desktop,
neko.Service.WebSocket,
}

View File

@ -1,27 +1,26 @@
module m1k1o/neko
go 1.17
go 1.18
require (
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-chi/chi v4.1.2+incompatible
github.com/gorilla/websocket v1.4.2
github.com/kataras/go-events v0.0.2
github.com/kr/text v0.2.0 // indirect
github.com/pion/ice/v2 v2.1.19 // indirect
github.com/pion/interceptor v0.1.7
github.com/gorilla/websocket v1.5.0
github.com/kataras/go-events v0.0.3
github.com/pion/ice/v2 v2.2.7 // indirect
github.com/pion/interceptor v0.1.12
github.com/pion/logging v0.2.2
github.com/pion/rtp v1.7.4 // indirect
github.com/pion/srtp/v2 v2.0.5 // indirect
github.com/pion/webrtc/v3 v3.1.17
github.com/pion/rtp v1.7.13 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect
github.com/pion/webrtc/v3 v3.1.43
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.26.1
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect
github.com/rs/zerolog v1.27.0
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 // indirect
golang.org/x/sys v0.0.0-20220730100132-1609e554cd39 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
@ -30,25 +29,29 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.0 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.9 // indirect
github.com/pion/rtcp v1.2.10 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/sdp/v3 v3.0.4 // indirect
github.com/pion/sdp/v3 v3.0.5 // indirect
github.com/pion/stun v0.3.5 // indirect
github.com/pion/transport v0.13.0 // indirect
github.com/pion/turn/v2 v2.0.6 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/spf13/afero v1.8.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/ini.v1 v1.66.3 // indirect
github.com/subosito/gotenv v1.4.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -17,18 +17,6 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -37,7 +25,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -51,47 +38,16 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -100,39 +56,23 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -140,8 +80,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -157,9 +95,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -170,15 +106,12 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@ -189,114 +122,44 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kataras/go-events v0.0.2 h1:fhyUPXvUbrjIPmH4vRdrAAGoNzdcwJPQmjhg47m1nMU=
github.com/kataras/go-events v0.0.2/go.mod h1:6IxMW59VJdEIqj3bjFGJvGLRdb0WHtrlxPZy9qXctcg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4=
github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -306,152 +169,112 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.0.13/go.mod h1:OaE7eTM+ppaUhJ99OTO4aHl9uY6vPrT1gPY27uNTxRY=
github.com/pion/dtls/v2 v2.1.0 h1:g6gtKVNLp6URDkv9OijFJl16kqGHzVzZG+Fa4A38GTY=
github.com/pion/dtls/v2 v2.1.0/go.mod h1:qG3gA7ZPZemBqpEFqRKyURYdKEwFZQCGb7gv9T3ON3Y=
github.com/pion/ice/v2 v2.1.18/go.mod h1:9jDr0iIUg8P6+0Jq8QJ/eFSkX3JnsPd293TjCdkfpTs=
github.com/pion/ice/v2 v2.1.19 h1:z7iVx/fHlqvPILUbvcj1xjuz/6eVKgEFOM8h1AuLbF8=
github.com/pion/ice/v2 v2.1.19/go.mod h1:E5frMpIJ3zzcQiRo+XyT7z1IiAsGc1hDURcVJQUzGWA=
github.com/pion/interceptor v0.1.6/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo=
github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/ice/v2 v2.2.7 h1:kG9tux3WdYUSqqqnf+O5zKlpy41PdlvLUBlYJeV2emQ=
github.com/pion/ice/v2 v2.2.7/go.mod h1:Ckj7cWZ717rtU01YoDQA9ntGWCk95D42uVZ8sI0EL+8=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA=
github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/turn/v2 v2.0.6 h1:AsXjSPR6Im15DMTB39NlfdTY9BQfieANPBjdg/aVNwY=
github.com/pion/turn/v2 v2.0.6/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v3 v3.1.17 h1:6V4Yf5wnvJZKs86401EcpsKmB5Px5pfF1ICXdPIRsC0=
github.com/pion/webrtc/v3 v3.1.17/go.mod h1:kHunUx6HPCbCvGy/HdWQNwtT9LJ2XMS/sBmLwB1A4rs=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60=
github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs=
github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA=
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -475,7 +298,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@ -486,13 +308,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -500,11 +318,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -522,24 +338,21 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 h1:UreQrH7DbFXSi9ZFox6FNT3WBooWmdANpU+IfkT1T4I=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -549,14 +362,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -567,37 +372,24 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -616,35 +408,19 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220730100132-1609e554cd39 h1:aNCnH+Fiqs7ZDTFH6oEFjIfbX2HvgQXJ6uQuUbTobjk=
golang.org/x/sys v0.0.0-20220730100132-1609e554cd39/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -653,7 +429,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -673,7 +448,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -698,7 +472,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
@ -708,19 +481,11 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -741,20 +506,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -785,7 +536,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
@ -798,35 +548,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -840,22 +562,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -868,8 +577,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -877,22 +584,17 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.3 h1:jRskFVxYaMGAMUbN0UZ7niA9gzL9B49DOqE78vg0k3w=
gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,104 +0,0 @@
package broadcast
import (
"sync"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/gst"
"m1k1o/neko/internal/types/config"
)
type BroadcastManager struct {
mu sync.Mutex
logger zerolog.Logger
pipeline *gst.Pipeline
remote *config.Remote
config *config.Broadcast
enabled bool
url string
}
func New(remote *config.Remote, config *config.Broadcast) *BroadcastManager {
return &BroadcastManager{
logger: log.With().Str("module", "remote").Logger(),
remote: remote,
config: config,
enabled: config.Enabled,
url: config.URL,
}
}
func (manager *BroadcastManager) Shutdown() error {
manager.Destroy()
return nil
}
func (manager *BroadcastManager) Start() error {
if !manager.enabled || manager.IsActive() {
return nil
}
var err error
manager.pipeline, err = gst.CreateRTMPPipeline(
manager.remote.Device,
manager.remote.Display,
manager.config.Pipeline,
manager.url,
)
if err != nil {
manager.pipeline = nil
return err
}
manager.logger.Info().
Str("audio_device", manager.remote.Device).
Str("video_display", manager.remote.Display).
Str("rtmp_pipeline_src", manager.pipeline.Src).
Msgf("RTMP pipeline is starting...")
manager.pipeline.Play()
return nil
}
func (manager *BroadcastManager) Stop() {
if !manager.IsActive() {
return
}
manager.pipeline.Stop()
manager.pipeline = nil
}
func (manager *BroadcastManager) IsActive() bool {
return manager.pipeline != nil
}
func (manager *BroadcastManager) Create(url string) error {
manager.mu.Lock()
defer manager.mu.Unlock()
manager.url = url
manager.enabled = true
err := manager.Start()
if err != nil {
manager.enabled = false
}
return err
}
func (manager *BroadcastManager) Destroy() {
manager.mu.Lock()
defer manager.mu.Unlock()
manager.Stop()
manager.enabled = false
}
func (manager *BroadcastManager) GetUrl() string {
return manager.url
}

View File

@ -0,0 +1,121 @@
package capture
import (
"sync"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/capture/gst"
"m1k1o/neko/internal/types"
)
type BroacastManagerCtx struct {
logger zerolog.Logger
mu sync.Mutex
pipeline *gst.Pipeline
pipelineMu sync.Mutex
pipelineFn func(url string) (string, error)
url string
started bool
}
func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string) *BroacastManagerCtx {
logger := log.With().
Str("module", "capture").
Str("submodule", "broadcast").
Logger()
return &BroacastManagerCtx{
logger: logger,
pipelineFn: pipelineFn,
url: defaultUrl,
started: defaultUrl != "",
}
}
func (manager *BroacastManagerCtx) shutdown() {
manager.logger.Info().Msgf("shutdown")
manager.destroyPipeline()
}
func (manager *BroacastManagerCtx) Start(url string) error {
manager.mu.Lock()
defer manager.mu.Unlock()
err := manager.createPipeline()
if err != nil {
return err
}
manager.url = url
manager.started = true
return nil
}
func (manager *BroacastManagerCtx) Stop() {
manager.mu.Lock()
defer manager.mu.Unlock()
manager.started = false
manager.destroyPipeline()
}
func (manager *BroacastManagerCtx) Started() bool {
manager.mu.Lock()
defer manager.mu.Unlock()
return manager.started
}
func (manager *BroacastManagerCtx) Url() string {
manager.mu.Lock()
defer manager.mu.Unlock()
return manager.url
}
func (manager *BroacastManagerCtx) createPipeline() error {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline != nil {
return types.ErrCapturePipelineAlreadyExists
}
var err error
pipelineStr, err := manager.pipelineFn(manager.url)
if err != nil {
return err
}
manager.logger.Info().
Str("url", manager.url).
Str("src", pipelineStr).
Msgf("starting pipeline")
manager.pipeline, err = gst.CreatePipeline(pipelineStr)
if err != nil {
return err
}
manager.pipeline.Play()
return nil
}
func (manager *BroacastManagerCtx) destroyPipeline() {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline == nil {
return
}
manager.pipeline.Destroy()
manager.logger.Info().Msgf("destroying pipeline")
manager.pipeline = nil
}

View File

@ -0,0 +1,202 @@
#include "gst.h"
static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) {
va_list argptr;
va_start(argptr, format);
char buffer[100];
vsprintf(buffer, format, argptr);
va_end(argptr);
goPipelineLog(level, buffer, ctx->pipelineId);
}
static gboolean gstreamer_bus_call(GstBus *bus, GstMessage *msg, gpointer user_data) {
GstPipelineCtx *ctx = (GstPipelineCtx *)user_data;
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS: {
gstreamer_pipeline_log(ctx, "fatal", "end of stream");
break;
}
case GST_MESSAGE_STATE_CHANGED: {
GstState old_state, new_state;
gst_message_parse_state_changed(msg, &old_state, &new_state, NULL);
gstreamer_pipeline_log(ctx, "debug",
"element %s changed state from %s to %s",
GST_OBJECT_NAME(msg->src),
gst_element_state_get_name(old_state),
gst_element_state_get_name(new_state));
break;
}
case GST_MESSAGE_TAG: {
GstTagList *tags = NULL;
gst_message_parse_tag(msg, &tags);
gstreamer_pipeline_log(ctx, "debug",
"got tags from element %s",
GST_OBJECT_NAME(msg->src));
gst_tag_list_unref(tags);
break;
}
case GST_MESSAGE_ERROR: {
GError *err = NULL;
gchar *dbg_info = NULL;
gst_message_parse_error(msg, &err, &dbg_info);
gstreamer_pipeline_log(ctx, "error",
"error from element %s: %s",
GST_OBJECT_NAME(msg->src), err->message);
gstreamer_pipeline_log(ctx, "warn",
"debugging info: %s",
(dbg_info) ? dbg_info : "none");
g_error_free(err);
g_free(dbg_info);
break;
}
default:
gstreamer_pipeline_log(ctx, "trace", "unknown message");
break;
}
return TRUE;
}
GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error) {
GstElement *pipeline = gst_parse_launch(pipelineStr, error);
if (pipeline == NULL) return NULL;
// create gstreamer pipeline context
GstPipelineCtx *ctx = calloc(1, sizeof(GstPipelineCtx));
ctx->pipelineId = pipelineId;
ctx->pipeline = pipeline;
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
gst_bus_add_watch(bus, gstreamer_bus_call, ctx);
gst_object_unref(bus);
return ctx;
}
static GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
GstPipelineCtx *ctx = (GstPipelineCtx *)user_data;
GstSample *sample = NULL;
GstBuffer *buffer = NULL;
gpointer copy = NULL;
gsize copy_size = 0;
g_signal_emit_by_name(object, "pull-sample", &sample);
if (sample) {
buffer = gst_sample_get_buffer(sample);
if (buffer) {
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), &copy, &copy_size);
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), ctx->pipelineId);
}
gst_sample_unref(sample);
}
return GST_FLOW_OK;
}
void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName) {
ctx->appsink = gst_bin_get_by_name(GST_BIN(ctx->pipeline), sinkName);
g_object_set(ctx->appsink, "emit-signals", TRUE, NULL);
g_signal_connect(ctx->appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), ctx);
}
void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName) {
ctx->appsrc = gst_bin_get_by_name(GST_BIN(ctx->pipeline), srcName);
}
void gstreamer_pipeline_play(GstPipelineCtx *ctx) {
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PLAYING);
}
void gstreamer_pipeline_pause(GstPipelineCtx *ctx) {
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PAUSED);
}
void gstreamer_pipeline_destory(GstPipelineCtx *ctx) {
// end appsrc, if exists
if (ctx->appsrc) {
gst_app_src_end_of_stream(GST_APP_SRC(ctx->appsrc));
}
// send pipeline eos
gst_element_send_event(GST_ELEMENT(ctx->pipeline), gst_event_new_eos());
// set null state
gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_NULL);
if (ctx->appsink) {
gst_object_unref(ctx->appsink);
ctx->appsink = NULL;
}
if (ctx->appsrc) {
gst_object_unref(ctx->appsrc);
ctx->appsrc = NULL;
}
gst_object_unref(ctx->pipeline);
}
void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen) {
if (ctx->appsrc != NULL) {
gpointer p = g_memdup(buffer, bufferLen);
GstBuffer *buffer = gst_buffer_new_wrapped(p, bufferLen);
gst_app_src_push_buffer(GST_APP_SRC(ctx->appsrc), buffer);
}
}
gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value) {
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
if (el == NULL) return FALSE;
g_object_set(G_OBJECT(el),
prop, value,
NULL);
gst_object_unref(el);
return TRUE;
}
gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator) {
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
if (el == NULL) return FALSE;
GstCaps *caps = gst_caps_new_simple("video/x-raw",
"framerate", GST_TYPE_FRACTION, numerator, denominator,
NULL);
g_object_set(G_OBJECT(el),
"caps", caps,
NULL);
gst_caps_unref(caps);
gst_object_unref(el);
return TRUE;
}
gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height) {
GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName);
if (el == NULL) return FALSE;
GstCaps *caps = gst_caps_new_simple("video/x-raw",
"width", G_TYPE_INT, width,
"height", G_TYPE_INT, height,
NULL);
g_object_set(G_OBJECT(el),
"caps", caps,
NULL);
gst_caps_unref(caps);
gst_object_unref(el);
return TRUE;
}

View File

@ -0,0 +1,202 @@
package gst
/*
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
#include "gst.h"
*/
import "C"
import (
"fmt"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/types"
)
type Pipeline struct {
id int
logger zerolog.Logger
Src string
Ctx *C.GstPipelineCtx
Sample chan types.Sample
}
var pSerial int32
var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex
var registry *C.GstRegistry
func init() {
C.gst_init(nil, nil)
registry = C.gst_registry_get()
}
func CreatePipeline(pipelineStr string) (*Pipeline, error) {
id := atomic.AddInt32(&pSerial, 1)
pipelineStrUnsafe := C.CString(pipelineStr)
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
pipelinesLock.Lock()
defer pipelinesLock.Unlock()
var gstError *C.GError
ctx := C.gstreamer_pipeline_create(pipelineStrUnsafe, C.int(id), &gstError)
if gstError != nil {
defer C.g_error_free(gstError)
return nil, fmt.Errorf("(pipeline error) %s", C.GoString(gstError.message))
}
p := &Pipeline{
id: int(id),
logger: log.With().
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(id)).Logger(),
Src: pipelineStr,
Ctx: ctx,
Sample: make(chan types.Sample),
}
pipelines[p.id] = p
return p, nil
}
func (p *Pipeline) AttachAppsink(sinkName string) {
sinkNameUnsafe := C.CString(sinkName)
defer C.free(unsafe.Pointer(sinkNameUnsafe))
C.gstreamer_pipeline_attach_appsink(p.Ctx, sinkNameUnsafe)
}
func (p *Pipeline) AttachAppsrc(srcName string) {
srcNameUnsafe := C.CString(srcName)
defer C.free(unsafe.Pointer(srcNameUnsafe))
C.gstreamer_pipeline_attach_appsrc(p.Ctx, srcNameUnsafe)
}
func (p *Pipeline) Play() {
C.gstreamer_pipeline_play(p.Ctx)
}
func (p *Pipeline) Pause() {
C.gstreamer_pipeline_pause(p.Ctx)
}
func (p *Pipeline) Destroy() {
C.gstreamer_pipeline_destory(p.Ctx)
pipelinesLock.Lock()
delete(pipelines, p.id)
pipelinesLock.Unlock()
close(p.Sample)
C.free(unsafe.Pointer(p.Ctx))
p = nil
}
func (p *Pipeline) Push(buffer []byte) {
bytes := C.CBytes(buffer)
defer C.free(bytes)
C.gstreamer_pipeline_push(p.Ctx, bytes, C.int(len(buffer)))
}
func (p *Pipeline) SetPropInt(binName string, prop string, value int) bool {
cBinName := C.CString(binName)
defer C.free(unsafe.Pointer(cBinName))
cProp := C.CString(prop)
defer C.free(unsafe.Pointer(cProp))
cValue := C.int(value)
p.logger.Debug().Msgf("setting prop %s of %s to %d", prop, binName, value)
ok := C.gstreamer_pipeline_set_prop_int(p.Ctx, cBinName, cProp, cValue)
return ok == C.TRUE
}
func (p *Pipeline) SetCapsFramerate(binName string, numerator, denominator int) bool {
cBinName := C.CString(binName)
cNumerator := C.int(numerator)
cDenominator := C.int(denominator)
defer C.free(unsafe.Pointer(cBinName))
p.logger.Debug().Msgf("setting caps framerate of %s to %d/%d", binName, numerator, denominator)
ok := C.gstreamer_pipeline_set_caps_framerate(p.Ctx, cBinName, cNumerator, cDenominator)
return ok == C.TRUE
}
func (p *Pipeline) SetCapsResolution(binName string, width, height int) bool {
cBinName := C.CString(binName)
cWidth := C.int(width)
cHeight := C.int(height)
defer C.free(unsafe.Pointer(cBinName))
p.logger.Debug().Msgf("setting caps resolution of %s to %dx%d", binName, width, height)
ok := C.gstreamer_pipeline_set_caps_resolution(p.Ctx, cBinName, cWidth, cHeight)
return ok == C.TRUE
}
// gst-inspect-1.0
func CheckPlugins(plugins []string) error {
var plugin *C.GstPlugin
for _, pluginstr := range plugins {
plugincstr := C.CString(pluginstr)
plugin = C.gst_registry_find_plugin(registry, plugincstr)
C.free(unsafe.Pointer(plugincstr))
if plugin == nil {
return fmt.Errorf("required gstreamer plugin %s not found", pluginstr)
}
}
return nil
}
//export goHandlePipelineBuffer
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
defer C.free(buffer)
pipelinesLock.Lock()
pipeline, ok := pipelines[int(pipelineID)]
pipelinesLock.Unlock()
if ok {
pipeline.Sample <- types.Sample{
Data: C.GoBytes(buffer, bufferLen),
Duration: time.Duration(duration),
}
} else {
log.Warn().
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(pipelineID)).
Msgf("discarding sample, pipeline not found")
}
}
//export goPipelineLog
func goPipelineLog(levelUnsafe *C.char, msgUnsafe *C.char, pipelineID C.int) {
levelStr := C.GoString(levelUnsafe)
msg := C.GoString(msgUnsafe)
level, _ := zerolog.ParseLevel(levelStr)
log.WithLevel(level).
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(pipelineID)).
Msg(msg)
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <stdio.h>
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
typedef struct GstPipelineCtx {
int pipelineId;
GstElement *pipeline;
GstElement *appsink;
GstElement *appsrc;
} GstPipelineCtx;
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
extern void goPipelineLog(char *level, char *msg, int pipelineId);
GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error);
void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName);
void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName);
void gstreamer_pipeline_play(GstPipelineCtx *ctx);
void gstreamer_pipeline_pause(GstPipelineCtx *ctx);
void gstreamer_pipeline_destory(GstPipelineCtx *ctx);
void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen);
gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value);
gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator);
gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height);

View File

@ -0,0 +1,98 @@
package capture
import (
"errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/config"
"m1k1o/neko/internal/types"
)
type CaptureManagerCtx struct {
logger zerolog.Logger
desktop types.DesktopManager
// sinks
broadcast *BroacastManagerCtx
audio *StreamSinkManagerCtx
video *StreamSinkManagerCtx
}
func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx {
logger := log.With().Str("module", "capture").Logger()
return &CaptureManagerCtx{
logger: logger,
desktop: desktop,
// sinks
broadcast: broadcastNew(func(url string) (string, error) {
return NewBroadcastPipeline(config.AudioDevice, config.Display, config.BroadcastPipeline, url)
}, config.BroadcastUrl),
audio: streamSinkNew(config.AudioCodec, func() (string, error) {
return NewAudioPipeline(config.AudioCodec, config.AudioDevice, config.AudioPipeline, config.AudioBitrate)
}, "audio"),
video: streamSinkNew(config.VideoCodec, func() (string, error) {
return NewVideoPipeline(config.VideoCodec, config.Display, config.VideoPipeline, config.VideoMaxFPS, config.VideoBitrate, config.VideoHWEnc)
}, "video"),
}
}
func (manager *CaptureManagerCtx) Start() {
if manager.broadcast.Started() {
if err := manager.broadcast.createPipeline(); err != nil {
manager.logger.Panic().Err(err).Msg("unable to create broadcast pipeline")
}
}
manager.desktop.OnBeforeScreenSizeChange(func() {
if manager.video.Started() {
manager.video.destroyPipeline()
}
if manager.broadcast.Started() {
manager.broadcast.destroyPipeline()
}
})
manager.desktop.OnAfterScreenSizeChange(func() {
if manager.video.Started() {
err := manager.video.createPipeline()
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
manager.logger.Panic().Err(err).Msg("unable to recreate video pipeline")
}
}
if manager.broadcast.Started() {
err := manager.broadcast.createPipeline()
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline")
}
}
})
}
func (manager *CaptureManagerCtx) Shutdown() error {
manager.logger.Info().Msgf("shutdown")
manager.broadcast.shutdown()
manager.audio.shutdown()
manager.video.shutdown()
return nil
}
func (manager *CaptureManagerCtx) Broadcast() types.BroadcastManager {
return manager.broadcast
}
func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager {
return manager.audio
}
func (manager *CaptureManagerCtx) Video() types.StreamSinkManager {
return manager.video
}

View File

@ -0,0 +1,202 @@
package capture
import (
"fmt"
"strings"
"m1k1o/neko/internal/capture/gst"
"m1k1o/neko/internal/types/codec"
)
/*
apt-get install \
libgstreamer1.0-0 \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly\
gstreamer1.0-libav \
gstreamer1.0-doc \
gstreamer1.0-tools \
gstreamer1.0-x \
gstreamer1.0-alsa \
gstreamer1.0-pulseaudio
gst-inspect-1.0 --version
gst-inspect-1.0 plugin
gst-launch-1.0 ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! autovideosink
gst-launch-1.0 pulsesrc ! audioconvert ! opusenc ! autoaudiosink
*/
const (
videoSrc = "ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=%d/1 ! videoconvert ! queue ! "
audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! "
)
func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) (string, error) {
video := fmt.Sprintf(videoSrc, display, 25)
audio := fmt.Sprintf(audioSrc, device)
var pipelineStr string
if pipelineSrc != "" {
// replace RTMP url
pipelineStr = strings.Replace(pipelineSrc, "{url}", url, -1)
// replace audio device
pipelineStr = strings.Replace(pipelineStr, "{device}", device, -1)
// replace display
pipelineStr = strings.Replace(pipelineStr, "{display}", display, -1)
} else {
pipelineStr = fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s audio/x-raw,channels=2 ! audioconvert ! voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", url, audio, video)
}
return pipelineStr, nil
}
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc string) (string, error) {
pipelineStr := " ! appsink name=appsink"
// if using custom pipeline
if pipelineSrc != "" {
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, display)
return pipelineStr, nil
}
switch rtpCodec.Name {
case codec.VP8().Name:
if hwenc == "VAAPI" {
if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil {
return "", err
}
// vp8 encode is missing from gstreamer.freedesktop.org/documentation
// note that it was removed from some recent intel CPUs: https://trac.ffmpeg.org/wiki/Hardware/QuickSync
// https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer-vaapi-plugins/html/gstreamer-vaapi-plugins-vaapivp8enc.html
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapivp8enc rate-control=vbr bitrate=%d keyframe-period=180"+pipelineStr, display, fps, bitrate)
} else {
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c
// gstreamer1.0-plugins-good
// vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1
if err := gst.CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
return "", err
}
pipelineStr = strings.Join([]string{
fmt.Sprintf(videoSrc, display, fps),
"vp8enc",
fmt.Sprintf("target-bitrate=%d", bitrate*650),
"cpu-used=4",
"end-usage=cbr",
"threads=4",
"deadline=1",
"undershoot=95",
fmt.Sprintf("buffer-size=%d", bitrate*4),
fmt.Sprintf("buffer-initial-size=%d", bitrate*2),
fmt.Sprintf("buffer-optimal-size=%d", bitrate*3),
"keyframe-max-dist=25",
"min-quantizer=4",
"max-quantizer=20",
pipelineStr,
}, " ")
}
case codec.VP9().Name:
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c
// gstreamer1.0-plugins-good
// vp9enc
if err := gst.CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
return "", err
}
pipelineStr = fmt.Sprintf(videoSrc+"vp9enc target-bitrate=%d cpu-used=-5 threads=4 deadline=1 keyframe-max-dist=30 auto-alt-ref=true"+pipelineStr, display, fps, bitrate*1000)
case codec.H264().Name:
if err := gst.CheckPlugins([]string{"ximagesrc"}); err != nil {
return "", err
}
if hwenc == "VAAPI" {
if err := gst.CheckPlugins([]string{"vaapi"}); err != nil {
return "", err
}
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate)
} else {
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
// gstreamer1.0-plugins-bad
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
if err := gst.CheckPlugins([]string{"openh264"}); err == nil {
pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=%d max-bitrate=%d ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate*1000, (bitrate+1024)*1000)
break
}
// https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
// gstreamer1.0-plugins-ugly
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream
if err := gst.CheckPlugins([]string{"x264"}); err != nil {
return "", err
}
vbvbuf := uint(1000)
if bitrate > 1000 {
vbvbuf = bitrate
}
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate, vbvbuf)
}
default:
return "", fmt.Errorf("unknown codec %s", rtpCodec.Name)
}
return pipelineStr, nil
}
func NewAudioPipeline(rtpCodec codec.RTPCodec, device string, pipelineSrc string, bitrate uint) (string, error) {
pipelineStr := " ! appsink name=appsink"
// if using custom pipeline
if pipelineSrc != "" {
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, device)
return pipelineStr, nil
}
switch rtpCodec.Name {
case codec.Opus().Name:
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
// opusenc
if err := gst.CheckPlugins([]string{"pulseaudio", "opus"}); err != nil {
return "", err
}
pipelineStr = fmt.Sprintf(audioSrc+"opusenc inband-fec=true bitrate=%d"+pipelineStr, device, bitrate*1000)
case codec.G722().Name:
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c
// gstreamer1.0-libav
// avenc_g722
if err := gst.CheckPlugins([]string{"pulseaudio", "libav"}); err != nil {
return "", err
}
pipelineStr = fmt.Sprintf(audioSrc+"avenc_g722 bitrate=%d"+pipelineStr, device, bitrate*1000)
case codec.PCMU().Name:
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c
// gstreamer1.0-plugins-good
// audio/x-raw, rate=8000 ! mulawenc
if err := gst.CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil {
return "", err
}
pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! mulawenc"+pipelineStr, device)
case codec.PCMA().Name:
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c
// gstreamer1.0-plugins-good
// audio/x-raw, rate=8000 ! alawenc
if err := gst.CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil {
return "", err
}
pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! alawenc"+pipelineStr, device)
default:
return "", fmt.Errorf("unknown codec %s", rtpCodec.Name)
}
return pipelineStr, nil
}

View File

@ -0,0 +1,190 @@
package capture
import (
"errors"
"sync"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/capture/gst"
"m1k1o/neko/internal/types"
"m1k1o/neko/internal/types/codec"
)
type StreamSinkManagerCtx struct {
logger zerolog.Logger
mu sync.Mutex
wg sync.WaitGroup
codec codec.RTPCodec
pipeline *gst.Pipeline
pipelineMu sync.Mutex
pipelineFn func() (string, error)
listeners int
listenersMu sync.Mutex
sampleFn func(sample types.Sample)
}
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), video_id string) *StreamSinkManagerCtx {
logger := log.With().
Str("module", "capture").
Str("submodule", "stream-sink").
Str("video_id", video_id).Logger()
manager := &StreamSinkManagerCtx{
logger: logger,
codec: codec,
pipelineFn: pipelineFn,
}
return manager
}
func (manager *StreamSinkManagerCtx) shutdown() {
manager.logger.Info().Msgf("shutdown")
manager.destroyPipeline()
manager.wg.Wait()
}
func (manager *StreamSinkManagerCtx) OnSample(listener func(sample types.Sample)) {
manager.sampleFn = listener
}
func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec {
return manager.codec
}
func (manager *StreamSinkManagerCtx) start() error {
if manager.listeners == 0 {
err := manager.createPipeline()
if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) {
return err
}
manager.logger.Info().Msgf("first listener, starting")
}
return nil
}
func (manager *StreamSinkManagerCtx) stop() {
if manager.listeners == 0 {
manager.destroyPipeline()
manager.logger.Info().Msgf("last listener, stopping")
}
}
func (manager *StreamSinkManagerCtx) addListener() {
manager.listenersMu.Lock()
manager.listeners++
manager.listenersMu.Unlock()
}
func (manager *StreamSinkManagerCtx) removeListener() {
manager.listenersMu.Lock()
manager.listeners--
manager.listenersMu.Unlock()
}
func (manager *StreamSinkManagerCtx) AddListener() error {
manager.mu.Lock()
defer manager.mu.Unlock()
// start if stopped
if err := manager.start(); err != nil {
return err
}
// add listener
manager.addListener()
return nil
}
func (manager *StreamSinkManagerCtx) RemoveListener() error {
manager.mu.Lock()
defer manager.mu.Unlock()
// remove listener
manager.removeListener()
// stop if started
manager.stop()
return nil
}
func (manager *StreamSinkManagerCtx) ListenersCount() int {
manager.listenersMu.Lock()
defer manager.listenersMu.Unlock()
return manager.listeners
}
func (manager *StreamSinkManagerCtx) Started() bool {
return manager.ListenersCount() > 0
}
func (manager *StreamSinkManagerCtx) createPipeline() error {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline != nil {
return types.ErrCapturePipelineAlreadyExists
}
pipelineStr, err := manager.pipelineFn()
if err != nil {
return err
}
manager.logger.Info().
Str("codec", manager.codec.Name).
Str("src", pipelineStr).
Msgf("creating pipeline")
manager.pipeline, err = gst.CreatePipeline(pipelineStr)
if err != nil {
return err
}
manager.pipeline.AttachAppsink("appsink")
manager.pipeline.Play()
manager.wg.Add(1)
pipeline := manager.pipeline
go func() {
manager.logger.Debug().Msg("started emitting samples")
defer manager.wg.Done()
for {
sample, ok := <-pipeline.Sample
if !ok {
manager.logger.Debug().Msg("stopped emitting samples")
return
}
manager.sampleFn(sample)
}
}()
return nil
}
func (manager *StreamSinkManagerCtx) destroyPipeline() {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline == nil {
return
}
manager.pipeline.Destroy()
manager.logger.Info().Msgf("destroying pipeline")
manager.pipeline = nil
}

View File

@ -0,0 +1,224 @@
package config
import (
"m1k1o/neko/internal/types/codec"
"github.com/pion/webrtc/v3"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Capture struct {
// video
Display string
VideoCodec codec.RTPCodec
VideoHWEnc string // TODO: Pipeline builder.
VideoBitrate uint // TODO: Pipeline builder.
VideoMaxFPS int16 // TODO: Pipeline builder.
VideoPipeline string
// audio
AudioDevice string
AudioCodec codec.RTPCodec
AudioBitrate uint // TODO: Pipeline builder.
AudioPipeline string
// broadcast
BroadcastPipeline string
BroadcastUrl string
}
func (Capture) Init(cmd *cobra.Command) error {
//
// video
//
cmd.PersistentFlags().String("display", ":99.0", "XDisplay to capture")
if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil {
return err
}
cmd.PersistentFlags().String("video_codec", "vp8", "video codec to be used")
if err := viper.BindPFlag("video_codec", cmd.PersistentFlags().Lookup("video_codec")); err != nil {
return err
}
// DEPRECATED: video codec
cmd.PersistentFlags().Bool("vp8", false, "DEPRECATED: use video_codec")
if err := viper.BindPFlag("vp8", cmd.PersistentFlags().Lookup("vp8")); err != nil {
return err
}
// DEPRECATED: video codec
cmd.PersistentFlags().Bool("vp9", false, "DEPRECATED: use video_codec")
if err := viper.BindPFlag("vp9", cmd.PersistentFlags().Lookup("vp9")); err != nil {
return err
}
// DEPRECATED: video codec
cmd.PersistentFlags().Bool("h264", false, "DEPRECATED: use video_codec")
if err := viper.BindPFlag("h264", cmd.PersistentFlags().Lookup("h264")); err != nil {
return err
}
cmd.PersistentFlags().String("hwenc", "", "use hardware accelerated encoding")
if err := viper.BindPFlag("hwenc", cmd.PersistentFlags().Lookup("hwenc")); err != nil {
return err
}
cmd.PersistentFlags().Int("video_bitrate", 3072, "video bitrate in kbit/s")
if err := viper.BindPFlag("video_bitrate", cmd.PersistentFlags().Lookup("video_bitrate")); err != nil {
return err
}
cmd.PersistentFlags().Int("max_fps", 25, "maximum fps delivered via WebRTC, 0 is for no maximum")
if err := viper.BindPFlag("max_fps", cmd.PersistentFlags().Lookup("max_fps")); err != nil {
return err
}
cmd.PersistentFlags().String("video", "", "video codec parameters to use for streaming")
if err := viper.BindPFlag("video", cmd.PersistentFlags().Lookup("video")); err != nil {
return err
}
//
// audio
//
cmd.PersistentFlags().String("device", "auto_null.monitor", "audio device to capture")
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
return err
}
cmd.PersistentFlags().String("audio_codec", "opus", "audio codec to be used")
if err := viper.BindPFlag("audio_codec", cmd.PersistentFlags().Lookup("audio_codec")); err != nil {
return err
}
// DEPRECATED: audio codec
cmd.PersistentFlags().Bool("opus", false, "DEPRECATED: use audio_codec")
if err := viper.BindPFlag("opus", cmd.PersistentFlags().Lookup("opus")); err != nil {
return err
}
// DEPRECATED: audio codec
cmd.PersistentFlags().Bool("g722", false, "DEPRECATED: use audio_codec")
if err := viper.BindPFlag("g722", cmd.PersistentFlags().Lookup("g722")); err != nil {
return err
}
// DEPRECATED: audio codec
cmd.PersistentFlags().Bool("pcmu", false, "DEPRECATED: use audio_codec")
if err := viper.BindPFlag("pcmu", cmd.PersistentFlags().Lookup("pcmu")); err != nil {
return err
}
// DEPRECATED: audio codec
cmd.PersistentFlags().Bool("pcma", false, "DEPRECATED: use audio_codec")
if err := viper.BindPFlag("pcma", cmd.PersistentFlags().Lookup("pcma")); err != nil {
return err
}
// audio codecs
cmd.PersistentFlags().Int("audio_bitrate", 128, "audio bitrate in kbit/s")
if err := viper.BindPFlag("audio_bitrate", cmd.PersistentFlags().Lookup("audio_bitrate")); err != nil {
return err
}
cmd.PersistentFlags().String("audio", "", "audio codec parameters to use for streaming")
if err := viper.BindPFlag("audio", cmd.PersistentFlags().Lookup("audio")); err != nil {
return err
}
//
// broadcast
//
cmd.PersistentFlags().String("broadcast_pipeline", "", "custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced")
if err := viper.BindPFlag("broadcast_pipeline", cmd.PersistentFlags().Lookup("broadcast_pipeline")); err != nil {
return err
}
cmd.PersistentFlags().String("broadcast_url", "", "URL for broadcasting, setting this value will automatically enable broadcasting")
if err := viper.BindPFlag("broadcast_url", cmd.PersistentFlags().Lookup("broadcast_url")); err != nil {
return err
}
return nil
}
func (s *Capture) Set() {
var ok bool
//
// video
//
s.Display = viper.GetString("display")
videoCodec := viper.GetString("video_codec")
s.VideoCodec, ok = codec.ParseStr(videoCodec)
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8")
s.VideoCodec = codec.VP8()
}
if viper.GetBool("vp8") {
s.VideoCodec = codec.VP8()
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_VIDEO_CODEC=vp8' instead")
} else if viper.GetBool("vp9") {
s.VideoCodec = codec.VP9()
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_VIDEO_CODEC=vp9' instead")
} else if viper.GetBool("h264") {
s.VideoCodec = codec.H264()
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_VIDEO_CODEC=h264' instead")
}
videoHWEnc := ""
if viper.GetString("hwenc") == "VAAPI" {
videoHWEnc = "VAAPI"
}
s.VideoHWEnc = videoHWEnc
s.VideoBitrate = viper.GetUint("video_bitrate")
s.VideoMaxFPS = int16(viper.GetInt("max_fps"))
s.VideoPipeline = viper.GetString("video")
//
// audio
//
s.AudioDevice = viper.GetString("device")
audioCodec := viper.GetString("audio_codec")
s.AudioCodec, ok = codec.ParseStr(audioCodec)
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus")
s.AudioCodec = codec.Opus()
}
if viper.GetBool("opus") {
s.AudioCodec = codec.Opus()
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_VIDEO_CODEC=opus' instead")
} else if viper.GetBool("g722") {
s.AudioCodec = codec.G722()
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_VIDEO_CODEC=g722' instead")
} else if viper.GetBool("pcmu") {
s.AudioCodec = codec.PCMU()
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_VIDEO_CODEC=pcmu' instead")
} else if viper.GetBool("pcma") {
s.AudioCodec = codec.PCMA()
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_VIDEO_CODEC=pcma' instead")
}
s.AudioBitrate = viper.GetUint("audio_bitrate")
s.AudioPipeline = viper.GetString("audio")
//
// broadcast
//
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
s.BroadcastUrl = viper.GetString("broadcast_url")
}

View File

@ -0,0 +1,51 @@
package config
import (
"os"
"regexp"
"strconv"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Desktop struct {
Display string
ScreenWidth int
ScreenHeight int
ScreenRate int16
}
func (Desktop) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().String("screen", "1280x720@30", "default screen resolution and framerate")
if err := viper.BindPFlag("screen", cmd.PersistentFlags().Lookup("screen")); err != nil {
return err
}
return nil
}
func (s *Desktop) Set() {
// Display is provided by env variable
s.Display = os.Getenv("DISPLAY")
s.ScreenWidth = 1280
s.ScreenHeight = 720
s.ScreenRate = 30
r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`)
res := r.FindStringSubmatch(viper.GetString("screen"))
if len(res) > 0 {
width, err1 := strconv.ParseInt(res[1], 10, 64)
height, err2 := strconv.ParseInt(res[2], 10, 64)
rate, err3 := strconv.ParseInt(res[3], 10, 64)
if err1 == nil && err2 == nil && err3 == nil {
s.ScreenWidth = int(width)
s.ScreenHeight = int(height)
s.ScreenRate = int16(rate)
}
}
}

View File

@ -1,15 +1,18 @@
package config
import (
"path"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Server struct {
Cert string
Key string
Bind string
Static string
Cert string
Key string
Bind string
Static string
PathPrefix string
}
func (Server) Init(cmd *cobra.Command) error {
@ -33,6 +36,11 @@ func (Server) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().String("path_prefix", "/", "path prefix for HTTP requests")
if err := viper.BindPFlag("path_prefix", cmd.PersistentFlags().Lookup("path_prefix")); err != nil {
return err
}
return nil
}
@ -41,4 +49,5 @@ func (s *Server) Set() {
s.Key = viper.GetString("key")
s.Bind = viper.GetString("bind")
s.Static = viper.GetString("static")
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
}

View File

@ -0,0 +1,11 @@
package desktop
import "m1k1o/neko/internal/desktop/clipboard"
func (manager *DesktopManagerCtx) ReadClipboard() string {
return clipboard.Read()
}
func (manager *DesktopManagerCtx) WriteClipboard(data string) {
clipboard.Write(data)
}

View File

@ -0,0 +1,21 @@
#include "clipboard.h"
static clipboard_c *CLIPBOARD = NULL;
clipboard_c *getClipboard(void) {
if (CLIPBOARD == NULL) {
CLIPBOARD = clipboard_new(NULL);
}
return CLIPBOARD;
}
void ClipboardSet(char *src) {
clipboard_c *cb = getClipboard();
clipboard_set_text_ex(cb, src, strlen(src), 0);
}
char *ClipboardGet() {
clipboard_c *cb = getClipboard();
return clipboard_text_ex(cb, NULL, 0);
}

View File

@ -0,0 +1,35 @@
package clipboard
/*
#cgo linux LDFLAGS: /usr/local/lib/libclipboard.a -lxcb
#include "clipboard.h"
*/
import "C"
import (
"sync"
"unsafe"
)
var mu = sync.Mutex{}
func Read() string {
mu.Lock()
defer mu.Unlock()
clipboardUnsafe := C.ClipboardGet()
defer C.free(unsafe.Pointer(clipboardUnsafe))
return C.GoString(clipboardUnsafe)
}
func Write(data string) {
mu.Lock()
defer mu.Unlock()
clipboardUnsafe := C.CString(data)
defer C.free(unsafe.Pointer(clipboardUnsafe))
C.ClipboardSet(clipboardUnsafe)
}

View File

@ -0,0 +1,9 @@
#pragma once
#include <libclipboard.h>
#include <string.h>
clipboard_c *getClipboard(void);
void ClipboardSet(char *src);
char *ClipboardGet();

View File

@ -0,0 +1,98 @@
package desktop
import (
"fmt"
"sync"
"time"
"m1k1o/neko/internal/config"
"m1k1o/neko/internal/desktop/xevent"
"m1k1o/neko/internal/desktop/xorg"
"github.com/kataras/go-events"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var mu = sync.Mutex{}
type DesktopManagerCtx struct {
logger zerolog.Logger
wg sync.WaitGroup
shutdown chan struct{}
emmiter events.EventEmmiter
config *config.Desktop
}
func New(config *config.Desktop) *DesktopManagerCtx {
return &DesktopManagerCtx{
logger: log.With().Str("module", "desktop").Logger(),
shutdown: make(chan struct{}),
emmiter: events.New(),
config: config,
}
}
func (manager *DesktopManagerCtx) Start() {
if xorg.DisplayOpen(manager.config.Display) {
manager.logger.Panic().Str("display", manager.config.Display).Msg("unable to open display")
}
xorg.GetScreenConfigurations()
err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)
manager.logger.Err(err).
Str("screen_size", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)).
Msgf("setting initial screen size")
go xevent.EventLoop(manager.config.Display)
manager.OnEventError(func(error_code uint8, message string, request_code uint8, minor_code uint8) {
manager.logger.Warn().
Uint8("error_code", error_code).
Str("message", message).
Uint8("request_code", request_code).
Uint8("minor_code", minor_code).
Msg("X event error occurred")
})
manager.wg.Add(1)
go func() {
defer manager.wg.Done()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-manager.shutdown:
return
case <-ticker.C:
xorg.CheckKeys(time.Second * 10)
}
}
}()
}
func (manager *DesktopManagerCtx) OnBeforeScreenSizeChange(listener func()) {
manager.emmiter.On("before_screen_size_change", func(payload ...any) {
listener()
})
}
func (manager *DesktopManagerCtx) OnAfterScreenSizeChange(listener func()) {
manager.emmiter.On("after_screen_size_change", func(payload ...any) {
listener()
})
}
func (manager *DesktopManagerCtx) Shutdown() error {
manager.logger.Info().Msgf("desktop shutting down")
close(manager.shutdown)
manager.wg.Wait()
xorg.DisplayClose()
return nil
}

View File

@ -0,0 +1,33 @@
package desktop
import "m1k1o/neko/internal/desktop/xevent"
func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) {
xevent.Emmiter.On("cursor-changed", func(payload ...any) {
listener(payload[0].(uint64))
})
}
func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) {
xevent.Emmiter.On("clipboard-updated", func(payload ...any) {
listener()
})
}
func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) {
xevent.Emmiter.On("file-chooser-dialog-opened", func(payload ...any) {
listener()
})
}
func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) {
xevent.Emmiter.On("file-chooser-dialog-closed", func(payload ...any) {
listener()
})
}
func (manager *DesktopManagerCtx) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) {
xevent.Emmiter.On("event-error", func(payload ...any) {
listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8))
})
}

View File

@ -0,0 +1,123 @@
#include "xevent.h"
static int XEventError(Display *display, XErrorEvent *event) {
char message[100];
int error;
error = XGetErrorText(display, event->error_code, message, sizeof(message));
if (error) {
goXEventError(event, "Could not get error message.");
} else {
goXEventError(event, message);
}
return 1;
}
void XEventLoop(char *name) {
Display *display = XOpenDisplay(name);
Window root = RootWindow(display, 0);
int xfixes_event_base, xfixes_error_base;
if (!XFixesQueryExtension(display, &xfixes_event_base, &xfixes_error_base)) {
return;
}
Atom WM_WINDOW_ROLE = XInternAtom(display, "WM_WINDOW_ROLE", 1);
Atom XA_CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0);
XFixesSelectSelectionInput(display, root, XA_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask);
XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask);
XSelectInput(display, root, SubstructureNotifyMask);
XSync(display, 0);
XSetErrorHandler(XEventError);
while (goXEventActive()) {
XEvent event;
XNextEvent(display, &event);
// XFixesDisplayCursorNotify
if (event.type == xfixes_event_base + 1) {
XFixesCursorNotifyEvent notifyEvent = *((XFixesCursorNotifyEvent *) &event);
if (notifyEvent.subtype == XFixesDisplayCursorNotify) {
goXEventCursorChanged(notifyEvent);
continue;
}
}
// XFixesSelectionNotifyEvent
if (event.type == xfixes_event_base + XFixesSelectionNotify) {
XFixesSelectionNotifyEvent notifyEvent = *((XFixesSelectionNotifyEvent *) &event);
if (notifyEvent.subtype == XFixesSetSelectionOwnerNotify && notifyEvent.selection == XA_CLIPBOARD) {
goXEventClipboardUpdated();
continue;
}
}
// ConfigureNotify
if (event.type == ConfigureNotify) {
Window window = event.xconfigure.window;
char *name;
XFetchName(display, window, &name);
XTextProperty role;
XGetTextProperty(display, window, &role, WM_WINDOW_ROLE);
goXEventConfigureNotify(display, window, name, role.value);
XFree(name);
continue;
}
// UnmapNotify
if (event.type == UnmapNotify) {
Window window = event.xunmap.window;
goXEventUnmapNotify(window);
continue;
}
}
XCloseDisplay(display);
}
void XFileChooserHide(Display *display, Window window) {
Window root = RootWindow(display, 0);
// The WM_TRANSIENT_FOR property is defined by the [ICCCM] for managed windows.
// This specification extends the use of the property to override-redirect windows.
// If an override-redirect is a pop-up on behalf of another window, then the Client
// SHOULD set WM_TRANSIENT_FOR on the override-redirect to this other window.
//
// As an example, a Client should set WM_TRANSIENT_FOR on dropdown menus to the
// toplevel application window that contains the menubar.
// Remove WM_TRANSIENT_FOR
Atom WM_TRANSIENT_FOR = XInternAtom(display, "WM_TRANSIENT_FOR", 0);
XDeleteProperty(display, window, WM_TRANSIENT_FOR);
// Add _NET_WM_STATE_BELOW
XClientMessageEvent clientMessageEvent;
memset(&clientMessageEvent, 0, sizeof(clientMessageEvent));
// window = the respective client window
// message_type = _NET_WM_STATE
// format = 32
// data.l[0] = the action, as listed below
// _NET_WM_STATE_REMOVE 0 // remove/unset property
// _NET_WM_STATE_ADD 1 // add/set property
// _NET_WM_STATE_TOGGLE 2 // toggle property
// data.l[1] = first property to alter
// data.l[2] = second property to alter
// data.l[3] = source indication
// other data.l[] elements = 0
clientMessageEvent.type = ClientMessage;
clientMessageEvent.window = window;
clientMessageEvent.message_type = XInternAtom(display, "_NET_WM_STATE", 0);
clientMessageEvent.format = 32;
clientMessageEvent.data.l[0] = 1;
clientMessageEvent.data.l[1] = XInternAtom(display, "_NET_WM_STATE_BELOW", 0);
clientMessageEvent.data.l[3] = 1;
XSendEvent(display, root, 0, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent *)&clientMessageEvent);
}

View File

@ -0,0 +1,84 @@
package xevent
/*
#cgo LDFLAGS: -lX11 -lXfixes
#include "xevent.h"
*/
import "C"
import (
"strings"
"time"
"unsafe"
"github.com/kataras/go-events"
)
var Emmiter events.EventEmmiter
var file_chooser_dialog_window uint32 = 0
func init() {
Emmiter = events.New()
}
func EventLoop(display string) {
displayUnsafe := C.CString(display)
defer C.free(unsafe.Pointer(displayUnsafe))
C.XEventLoop(displayUnsafe)
}
//export goXEventCursorChanged
func goXEventCursorChanged(event C.XFixesCursorNotifyEvent) {
Emmiter.Emit("cursor-changed", uint64(event.cursor_serial))
}
//export goXEventClipboardUpdated
func goXEventClipboardUpdated() {
Emmiter.Emit("clipboard-updated")
}
//export goXEventConfigureNotify
func goXEventConfigureNotify(display *C.Display, window C.Window, name *C.char, role *C.char) {
if C.GoString(role) != "GtkFileChooserDialog" {
return
}
// TODO: Refactor. Right now processing of this dialog relies on identifying
// via its name. When that changes to role, this condition should be removed.
if !strings.HasPrefix(C.GoString(name), "Open File") {
return
}
C.XFileChooserHide(display, window)
// Because first dialog is not put properly to background
time.Sleep(10 * time.Millisecond)
C.XFileChooserHide(display, window)
if file_chooser_dialog_window == 0 {
file_chooser_dialog_window = uint32(window)
Emmiter.Emit("file-chooser-dialog-opened")
}
}
//export goXEventUnmapNotify
func goXEventUnmapNotify(window C.Window) {
if uint32(window) != file_chooser_dialog_window {
return
}
file_chooser_dialog_window = 0
Emmiter.Emit("file-chooser-dialog-closed")
}
//export goXEventError
func goXEventError(event *C.XErrorEvent, message *C.char) {
Emmiter.Emit("event-error", uint8(event.error_code), C.GoString(message), uint8(event.request_code), uint8(event.minor_code))
}
//export goXEventActive
func goXEventActive() C.int {
return C.int(1)
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <stdlib.h>
#include <string.h>
extern void goXEventCursorChanged(XFixesCursorNotifyEvent event);
extern void goXEventClipboardUpdated();
extern void goXEventConfigureNotify(Display *display, Window window, char *name, char *role);
extern void goXEventUnmapNotify(Window window);
extern void goXEventError(XErrorEvent *event, char *message);
extern int goXEventActive();
static int XEventError(Display *display, XErrorEvent *event);
void XEventLoop(char *display);
void XFileChooserHide(Display *display, Window window);

View File

@ -0,0 +1,149 @@
package desktop
import (
"image"
"os/exec"
"regexp"
"time"
"m1k1o/neko/internal/desktop/xorg"
"m1k1o/neko/internal/types"
)
func (manager *DesktopManagerCtx) Move(x, y int) {
xorg.Move(x, y)
}
func (manager *DesktopManagerCtx) GetCursorPosition() (int, int) {
return xorg.GetCursorPosition()
}
func (manager *DesktopManagerCtx) Scroll(x, y int) {
xorg.Scroll(x, y)
}
func (manager *DesktopManagerCtx) ButtonDown(code uint32) error {
return xorg.ButtonDown(code)
}
func (manager *DesktopManagerCtx) KeyDown(code uint32) error {
return xorg.KeyDown(code)
}
func (manager *DesktopManagerCtx) ButtonUp(code uint32) error {
return xorg.ButtonUp(code)
}
func (manager *DesktopManagerCtx) KeyUp(code uint32) error {
return xorg.KeyUp(code)
}
func (manager *DesktopManagerCtx) ButtonPress(code uint32) error {
xorg.ResetKeys()
defer xorg.ResetKeys()
return xorg.ButtonDown(code)
}
func (manager *DesktopManagerCtx) KeyPress(codes ...uint32) error {
xorg.ResetKeys()
defer xorg.ResetKeys()
for _, code := range codes {
if err := xorg.KeyDown(code); err != nil {
return err
}
}
if len(codes) > 1 {
time.Sleep(10 * time.Millisecond)
}
return nil
}
func (manager *DesktopManagerCtx) ResetKeys() {
xorg.ResetKeys()
}
func (manager *DesktopManagerCtx) ScreenConfigurations() map[int]types.ScreenConfiguration {
return xorg.ScreenConfigurations
}
func (manager *DesktopManagerCtx) SetScreenSize(size types.ScreenSize) error {
mu.Lock()
manager.emmiter.Emit("before_screen_size_change")
defer func() {
manager.emmiter.Emit("after_screen_size_change")
mu.Unlock()
}()
return xorg.ChangeScreenSize(size.Width, size.Height, size.Rate)
}
func (manager *DesktopManagerCtx) GetScreenSize() *types.ScreenSize {
return xorg.GetScreenSize()
}
func (manager *DesktopManagerCtx) SetKeyboardMap(kbd types.KeyboardMap) error {
// TOOD: Use native API.
cmd := exec.Command("setxkbmap", "-layout", kbd.Layout, "-variant", kbd.Variant)
_, err := cmd.Output()
return err
}
func (manager *DesktopManagerCtx) GetKeyboardMap() (*types.KeyboardMap, error) {
// TOOD: Use native API.
cmd := exec.Command("setxkbmap", "-query")
res, err := cmd.Output()
if err != nil {
return nil, err
}
kbd := types.KeyboardMap{}
re := regexp.MustCompile(`layout:\s+(.*)\n`)
arr := re.FindStringSubmatch(string(res))
if len(arr) > 1 {
kbd.Layout = arr[1]
}
re = regexp.MustCompile(`variant:\s+(.*)\n`)
arr = re.FindStringSubmatch(string(res))
if len(arr) > 1 {
kbd.Variant = arr[1]
}
return &kbd, nil
}
func (manager *DesktopManagerCtx) SetKeyboardModifiers(mod types.KeyboardModifiers) {
if mod.NumLock != nil {
xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock)
}
if mod.CapsLock != nil {
xorg.SetKeyboardModifier(xorg.KbdModCapsLock, *mod.CapsLock)
}
}
func (manager *DesktopManagerCtx) GetKeyboardModifiers() types.KeyboardModifiers {
modifiers := xorg.GetKeyboardModifiers()
NumLock := (modifiers & xorg.KbdModNumLock) != 0
CapsLock := (modifiers & xorg.KbdModCapsLock) != 0
return types.KeyboardModifiers{
NumLock: &NumLock,
CapsLock: &CapsLock,
}
}
func (manager *DesktopManagerCtx) GetCursorImage() *types.CursorImage {
return xorg.GetCursorImage()
}
func (manager *DesktopManagerCtx) GetScreenshotImage() *image.RGBA {
return xorg.GetScreenshotImage()
}

View File

@ -0,0 +1,266 @@
#include "xorg.h"
static Display *DISPLAY = NULL;
Display *getXDisplay(void) {
return DISPLAY;
}
int XDisplayOpen(char *name) {
DISPLAY = XOpenDisplay(name);
return DISPLAY == NULL;
}
void XDisplayClose(void) {
XCloseDisplay(DISPLAY);
}
void XMove(int x, int y) {
Display *display = getXDisplay();
XWarpPointer(display, None, DefaultRootWindow(display), 0, 0, 0, 0, x, y);
XSync(display, 0);
}
void XCursorPosition(int *x, int *y) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
Window window;
int i;
unsigned mask;
XQueryPointer(display, root, &root, &window, x, y, &i, &i, &mask);
}
void XScroll(int x, int y) {
int ydir = 4; /* Button 4 is up, 5 is down. */
int xdir = 6;
Display *display = getXDisplay();
if (y < 0) {
ydir = 5;
}
if (x < 0) {
xdir = 7;
}
int xi;
int yi;
for (xi = 0; xi < abs(x); xi++) {
XTestFakeButtonEvent(display, xdir, 1, CurrentTime);
XTestFakeButtonEvent(display, xdir, 0, CurrentTime);
}
for (yi = 0; yi < abs(y); yi++) {
XTestFakeButtonEvent(display, ydir, 1, CurrentTime);
XTestFakeButtonEvent(display, ydir, 0, CurrentTime);
}
XSync(display, 0);
}
void XButton(unsigned int button, int down) {
if (button == 0)
return;
Display *display = getXDisplay();
XTestFakeButtonEvent(display, button, down, CurrentTime);
XSync(display, 0);
}
static xkeyentry_t *xKeysHead = NULL;
void XKeyEntryAdd(KeySym keysym, KeyCode keycode) {
xkeyentry_t *entry = (xkeyentry_t *) malloc(sizeof(xkeyentry_t));
if (entry == NULL)
return;
entry->keysym = keysym;
entry->keycode = keycode;
entry->next = xKeysHead;
xKeysHead = entry;
}
KeyCode XKeyEntryGet(KeySym keysym) {
xkeyentry_t *prev = NULL;
xkeyentry_t *curr = xKeysHead;
KeyCode keycode = 0;
while (curr != NULL) {
if (curr->keysym == keysym) {
keycode = curr->keycode;
if (prev == NULL) {
xKeysHead = curr->next;
} else {
prev->next = curr->next;
}
free(curr);
return keycode;
}
prev = curr;
curr = curr->next;
}
return 0;
}
// From https://github.com/TigerVNC/tigervnc/blob/0946e298075f8f7b6d63e552297a787c5f84d27c/unix/x0vncserver/XDesktop.cxx#L343-L379
KeyCode XkbKeysymToKeycode(Display* dpy, KeySym keysym) {
XkbDescPtr xkb;
XkbStateRec state;
unsigned int mods;
unsigned keycode;
xkb = XkbGetMap(dpy, XkbAllComponentsMask, XkbUseCoreKbd);
if (!xkb)
return 0;
XkbGetState(dpy, XkbUseCoreKbd, &state);
// XkbStateFieldFromRec() doesn't work properly because
// state.lookup_mods isn't properly updated, so we do this manually
mods = XkbBuildCoreState(XkbStateMods(&state), state.group);
for (keycode = xkb->min_key_code;
keycode <= xkb->max_key_code;
keycode++) {
KeySym cursym;
unsigned int out_mods;
XkbTranslateKeyCode(xkb, keycode, mods, &out_mods, &cursym);
if (cursym == keysym)
break;
}
if (keycode > xkb->max_key_code)
keycode = 0;
XkbFreeKeyboard(xkb, XkbAllComponentsMask, True);
// Shift+Tab is usually ISO_Left_Tab, but RFB hides this fact. Do
// another attempt if we failed the initial lookup
if ((keycode == 0) && (keysym == XK_Tab) && (mods & ShiftMask))
return XkbKeysymToKeycode(dpy, XK_ISO_Left_Tab);
return keycode;
}
void XKey(KeySym keysym, int down) {
if (keysym == 0)
return;
Display *display = getXDisplay();
KeyCode keycode = 0;
if (!down)
keycode = XKeyEntryGet(keysym);
if (keycode == 0)
keycode = XkbKeysymToKeycode(display, keysym);
// Map non-existing keysyms to new keycodes
if (keycode == 0) {
int min, max, numcodes;
XDisplayKeycodes(display, &min, &max);
XGetKeyboardMapping(display, min, max-min, &numcodes);
keycode = (max-min+1)*numcodes;
KeySym keysym_list[numcodes];
for(int i=0;i<numcodes;i++) keysym_list[i] = keysym;
XChangeKeyboardMapping(display, keycode, numcodes, keysym_list, 1);
}
if (down)
XKeyEntryAdd(keysym, keycode);
XTestFakeKeyEvent(display, keycode, down, CurrentTime);
XSync(display, 0);
}
void XGetScreenConfigurations() {
Display *display = getXDisplay();
Window root = RootWindow(display, 0);
XRRScreenSize *xrrs;
int num_sizes;
xrrs = XRRSizes(display, 0, &num_sizes);
for (int i = 0; i < num_sizes; i++) {
short *rates;
int num_rates;
goCreateScreenSize(i, xrrs[i].width, xrrs[i].height, xrrs[i].mwidth, xrrs[i].mheight);
rates = XRRRates(display, 0, i, &num_rates);
for (int j = 0; j < num_rates; j++) {
goSetScreenRates(i, j, rates[j]);
}
}
}
void XSetScreenConfiguration(int index, short rate) {
Display *display = getXDisplay();
Window root = RootWindow(display, 0);
XRRSetScreenConfigAndRate(display, XRRGetScreenInfo(display, root), root, index, RR_Rotate_0, rate, CurrentTime);
}
int XGetScreenSize() {
Display *display = getXDisplay();
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0));
Rotation original_rotation;
return XRRConfigCurrentConfiguration(conf, &original_rotation);
}
short XGetScreenRate() {
Display *display = getXDisplay();
XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0));
return XRRConfigCurrentRate(conf);
}
void XSetKeyboardModifier(int mod, int on) {
Display *display = getXDisplay();
XkbLockModifiers(display, XkbUseCoreKbd, mod, on ? mod : 0);
XFlush(display);
}
char XGetKeyboardModifiers() {
XkbStateRec xkbState;
Display *display = getXDisplay();
XkbGetState(display, XkbUseCoreKbd, &xkbState);
return xkbState.locked_mods;
}
XFixesCursorImage *XGetCursorImage(void) {
Display *display = getXDisplay();
return XFixesGetCursorImage(display);
}
char *XGetScreenshot(int *w, int *h) {
Display *display = getXDisplay();
Window root = DefaultRootWindow(display);
XWindowAttributes attr;
XGetWindowAttributes(display, root, &attr);
int width = attr.width;
int height = attr.height;
XImage *ximage = XGetImage(display, root, 0, 0, width, height, AllPlanes, ZPixmap);
*w = width;
*h = height;
char *pixels = (char *)malloc(width * height * 3);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int pos = ((row * width) + col) * 3;
unsigned long pixel = XGetPixel(ximage, col, row);
pixels[pos] = (pixel & ximage->red_mask) >> 16;
pixels[pos+1] = (pixel & ximage->green_mask) >> 8;
pixels[pos+2] = pixel & ximage->blue_mask;
}
}
XDestroyImage(ximage);
return pixels;
}

View File

@ -0,0 +1,320 @@
package xorg
/*
#cgo LDFLAGS: -lX11 -lXrandr -lXtst -lXfixes
#include "xorg.h"
*/
import "C"
import (
"fmt"
"image"
"image/color"
"sync"
"time"
"unsafe"
"m1k1o/neko/internal/types"
)
//go:generate ./keysymdef.sh
type KbdMod uint8
const (
KbdModCapsLock KbdMod = 2
KbdModNumLock KbdMod = 16
)
var ScreenConfigurations = make(map[int]types.ScreenConfiguration)
var debounce_button = make(map[uint32]time.Time)
var debounce_key = make(map[uint32]time.Time)
var mu = sync.Mutex{}
func GetScreenConfigurations() {
mu.Lock()
defer mu.Unlock()
C.XGetScreenConfigurations()
}
func DisplayOpen(display string) bool {
mu.Lock()
defer mu.Unlock()
displayUnsafe := C.CString(display)
defer C.free(unsafe.Pointer(displayUnsafe))
ok := C.XDisplayOpen(displayUnsafe)
return int(ok) == 1
}
func DisplayClose() {
mu.Lock()
defer mu.Unlock()
C.XDisplayClose()
}
func Move(x, y int) {
mu.Lock()
defer mu.Unlock()
C.XMove(C.int(x), C.int(y))
}
func GetCursorPosition() (int, int) {
mu.Lock()
defer mu.Unlock()
var x C.int
var y C.int
C.XCursorPosition(&x, &y)
return int(x), int(y)
}
func Scroll(x, y int) {
mu.Lock()
defer mu.Unlock()
C.XScroll(C.int(x), C.int(y))
}
func ButtonDown(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_button[code]; ok {
return fmt.Errorf("debounced button %v", code)
}
debounce_button[code] = time.Now()
C.XButton(C.uint(code), C.int(1))
return nil
}
func KeyDown(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_key[code]; ok {
return fmt.Errorf("debounced key %v", code)
}
debounce_key[code] = time.Now()
C.XKey(C.KeySym(code), C.int(1))
return nil
}
func ButtonUp(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_button[code]; !ok {
return fmt.Errorf("debounced button %v", code)
}
delete(debounce_button, code)
C.XButton(C.uint(code), C.int(0))
return nil
}
func KeyUp(code uint32) error {
mu.Lock()
defer mu.Unlock()
if _, ok := debounce_key[code]; !ok {
return fmt.Errorf("debounced key %v", code)
}
delete(debounce_key, code)
C.XKey(C.KeySym(code), C.int(0))
return nil
}
func ResetKeys() {
mu.Lock()
defer mu.Unlock()
for code := range debounce_button {
C.XButton(C.uint(code), C.int(0))
delete(debounce_button, code)
}
for code := range debounce_key {
C.XKey(C.KeySym(code), C.int(0))
delete(debounce_key, code)
}
}
func CheckKeys(duration time.Duration) {
mu.Lock()
defer mu.Unlock()
t := time.Now()
for code, start := range debounce_button {
if t.Sub(start) < duration {
continue
}
C.XButton(C.uint(code), C.int(0))
delete(debounce_button, code)
}
for code, start := range debounce_key {
if t.Sub(start) < duration {
continue
}
C.XKey(C.KeySym(code), C.int(0))
delete(debounce_key, code)
}
}
func ChangeScreenSize(width int, height int, rate int16) error {
mu.Lock()
defer mu.Unlock()
for index, size := range ScreenConfigurations {
if size.Width == width && size.Height == height {
for _, fps := range size.Rates {
if rate == fps {
C.XSetScreenConfiguration(C.int(index), C.short(fps))
return nil
}
}
}
}
return fmt.Errorf("unknown screen configuration %dx%d@%d", width, height, rate)
}
func GetScreenSize() *types.ScreenSize {
mu.Lock()
defer mu.Unlock()
index := int(C.XGetScreenSize())
rate := int16(C.XGetScreenRate())
if conf, ok := ScreenConfigurations[index]; ok {
return &types.ScreenSize{
Width: conf.Width,
Height: conf.Height,
Rate: rate,
}
}
return nil
}
func SetKeyboardModifier(mod KbdMod, active bool) {
mu.Lock()
defer mu.Unlock()
num := C.int(0)
if active {
num = C.int(1)
}
C.XSetKeyboardModifier(C.int(mod), num)
}
func GetKeyboardModifiers() KbdMod {
mu.Lock()
defer mu.Unlock()
return KbdMod(C.XGetKeyboardModifiers())
}
func GetCursorImage() *types.CursorImage {
mu.Lock()
defer mu.Unlock()
cur := C.XGetCursorImage()
defer C.XFree(unsafe.Pointer(cur))
width := int(cur.width)
height := int(cur.height)
// Xlib stores 32-bit data in longs, even if longs are 64-bits long.
pixels := C.GoBytes(unsafe.Pointer(cur.pixels), C.int(width*height*8))
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
pos := ((y * width) + x) * 8
img.SetRGBA(x, y, color.RGBA{
A: pixels[pos+3],
R: pixels[pos+2],
G: pixels[pos+1],
B: pixels[pos+0],
})
}
}
return &types.CursorImage{
Width: uint16(width),
Height: uint16(height),
Xhot: uint16(cur.xhot),
Yhot: uint16(cur.yhot),
Serial: uint64(cur.cursor_serial),
Image: img,
}
}
func GetScreenshotImage() *image.RGBA {
mu.Lock()
defer mu.Unlock()
var w, h C.int
pixelsUnsafe := C.XGetScreenshot(&w, &h)
pixels := C.GoBytes(unsafe.Pointer(pixelsUnsafe), w*h*3)
defer C.free(unsafe.Pointer(pixelsUnsafe))
width := int(w)
height := int(h)
img := image.NewRGBA(image.Rect(0, 0, width, height))
for row := 0; row < height; row++ {
for col := 0; col < width; col++ {
pos := ((row * width) + col) * 3
img.SetRGBA(col, row, color.RGBA{
R: uint8(pixels[pos]),
G: uint8(pixels[pos+1]),
B: uint8(pixels[pos+2]),
A: 0xFF,
})
}
}
return img
}
//export goCreateScreenSize
func goCreateScreenSize(index C.int, width C.int, height C.int, mwidth C.int, mheight C.int) {
ScreenConfigurations[int(index)] = types.ScreenConfiguration{
Width: int(width),
Height: int(height),
Rates: make(map[int]int16),
}
}
//export goSetScreenRates
func goSetScreenRates(index C.int, rate_index C.int, rateC C.short) {
rate := int16(rateC)
// filter out all irrelevant rates
if rate > 60 || (rate > 30 && rate%10 != 0) {
return
}
ScreenConfigurations[int(index)].Rates[int(rate_index)] = rate
}

View File

@ -0,0 +1,43 @@
#pragma once
#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xrandr.h>
#include <X11/extensions/XTest.h>
#include <X11/extensions/Xfixes.h>
#include <stdlib.h>
extern void goCreateScreenSize(int index, int width, int height, int mwidth, int mheight);
extern void goSetScreenRates(int index, int rate_index, short rate);
Display *getXDisplay(void);
int XDisplayOpen(char *input);
void XDisplayClose(void);
void XMove(int x, int y);
void XCursorPosition(int *x, int *y);
void XScroll(int x, int y);
void XButton(unsigned int button, int down);
typedef struct xkeyentry_t {
KeySym keysym;
KeyCode keycode;
struct xkeyentry_t *next;
} xkeyentry_t;
static void XKeyEntryAdd(KeySym keysym, KeyCode keycode);
static KeyCode XKeyEntryGet(KeySym keysym);
static KeyCode XkbKeysymToKeycode(Display *dpy, KeySym keysym);
void XKey(KeySym keysym, int down);
void XGetScreenConfigurations();
void XSetScreenConfiguration(int index, short rate);
int XGetScreenSize();
short XGetScreenRate();
void XSetKeyboardModifier(int mod, int on);
char XGetKeyboardModifiers();
XFixesCursorImage *XGetCursorImage(void);
char *XGetScreenshot(int *w, int *h);

View File

@ -1,83 +0,0 @@
#include "gst.h"
typedef struct SampleHandlerUserData {
int pipelineId;
} SampleHandlerUserData;
void gstreamer_init(void) {
gst_init(NULL, NULL);
}
static gboolean gstreamer_send_bus_call(GstBus *bus, GstMessage *msg, gpointer data) {
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
g_print("End of stream\n");
exit(1);
break;
case GST_MESSAGE_ERROR: {
gchar *debug;
GError *error;
gst_message_parse_error(msg, &error, &debug);
g_free(debug);
g_printerr("Error: %s\n", error->message);
g_error_free(error);
exit(1);
}
default:
break;
}
return TRUE;
}
GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
GstSample *sample = NULL;
GstBuffer *buffer = NULL;
gpointer copy = NULL;
gsize copy_size = 0;
SampleHandlerUserData *s = (SampleHandlerUserData *)user_data;
g_signal_emit_by_name (object, "pull-sample", &sample);
if (sample) {
buffer = gst_sample_get_buffer(sample);
if (buffer) {
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), &copy, &copy_size);
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), s->pipelineId);
}
gst_sample_unref (sample);
}
return GST_FLOW_OK;
}
GstElement *gstreamer_send_create_pipeline(char *pipeline, GError **error) {
return gst_parse_launch(pipeline, error);
}
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId) {
SampleHandlerUserData *s = calloc(1, sizeof(SampleHandlerUserData));
s->pipelineId = pipelineId;
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
gst_bus_add_watch(bus, gstreamer_send_bus_call, NULL);
gst_object_unref(bus);
GstElement *appsink = gst_bin_get_by_name(GST_BIN(pipeline), "appsink");
g_object_set(appsink, "emit-signals", TRUE, NULL);
g_signal_connect(appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), s);
gst_object_unref(appsink);
gst_element_set_state(pipeline, GST_STATE_PLAYING);
}
void gstreamer_send_play_pipeline(GstElement *pipeline) {
gst_element_set_state(pipeline, GST_STATE_PLAYING);
}
void gstreamer_send_stop_pipeline(GstElement *pipeline) {
gst_element_set_state(pipeline, GST_STATE_NULL);
}

View File

@ -1,247 +0,0 @@
package gst
/*
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
#include "gst.h"
*/
import "C"
import (
"fmt"
"strings"
"sync"
"time"
"unsafe"
"m1k1o/neko/internal/types"
)
/*
apt-get install \
libgstreamer1.0-0 \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly\
gstreamer1.0-libav \
gstreamer1.0-doc \
gstreamer1.0-tools \
gstreamer1.0-x \
gstreamer1.0-alsa \
gstreamer1.0-pulseaudio
gst-inspect-1.0 --version
gst-inspect-1.0 plugin
gst-launch-1.0 ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! autovideosink
gst-launch-1.0 pulsesrc ! audioconvert ! opusenc ! autoaudiosink
*/
// Pipeline is a wrapper for a GStreamer Pipeline
type Pipeline struct {
Pipeline *C.GstElement
Sample chan types.Sample
Src string
id int
}
var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex
var registry *C.GstRegistry
const (
videoSrc = "ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=%d/1 ! videoconvert ! queue ! "
audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! "
)
func init() {
C.gstreamer_init()
registry = C.gst_registry_get()
}
// CreateRTMPPipeline creates a GStreamer Pipeline
func CreateRTMPPipeline(pipelineDevice string, pipelineDisplay string, pipelineSrc string, pipelineRTMP string) (*Pipeline, error) {
video := fmt.Sprintf(videoSrc, pipelineDisplay, 25)
audio := fmt.Sprintf(audioSrc, pipelineDevice)
var pipelineStr string
if pipelineSrc != "" {
// replace RTMP url
pipelineStr = strings.Replace(pipelineSrc, "{url}", pipelineRTMP, -1)
// replace audio device
pipelineStr = strings.Replace(pipelineStr, "{device}", pipelineDevice, -1)
// replace display
pipelineStr = strings.Replace(pipelineStr, "{display}", pipelineDisplay, -1)
} else {
pipelineStr = fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s audio/x-raw,channels=2 ! audioconvert ! voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", pipelineRTMP, audio, video)
}
return CreatePipeline(pipelineStr)
}
// CreateAppPipeline creates a GStreamer Pipeline
func CreateAppPipeline(codecName string, pipelineDevice string, pipelineSrc string, fps int, bitrate uint) (*Pipeline, error) {
pipelineStr := " ! appsink name=appsink"
// if using custom pipeline
if pipelineSrc != "" {
pipelineStr = fmt.Sprintf(pipelineSrc+pipelineStr, pipelineDevice)
return CreatePipeline(pipelineStr)
}
switch codecName {
case "VP8":
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c
// gstreamer1.0-plugins-good
// vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
return nil, err
}
pipelineStr = fmt.Sprintf(videoSrc+"vp8enc target-bitrate=%d cpu-used=4 end-usage=cbr threads=4 deadline=1 undershoot=95 buffer-size=%d buffer-initial-size=%d buffer-optimal-size=%d keyframe-max-dist=180 min-quantizer=3 max-quantizer=40"+pipelineStr, pipelineDevice, fps, bitrate*1000, bitrate*6, bitrate*4, bitrate*5)
case "VP9":
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c
// gstreamer1.0-plugins-good
// vp9enc
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
return nil, err
}
pipelineStr = fmt.Sprintf(videoSrc+"vp9enc target-bitrate=%d cpu-used=-5 threads=4 deadline=1 keyframe-max-dist=30 auto-alt-ref=true"+pipelineStr, pipelineDevice, fps, bitrate*1000)
case "H264":
if err := CheckPlugins([]string{"ximagesrc"}); err != nil {
return nil, err
}
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
// gstreamer1.0-plugins-bad
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
if err := CheckPlugins([]string{"openh264"}); err == nil {
pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=%d max-bitrate=%d ! video/x-h264,stream-format=byte-stream"+pipelineStr, pipelineDevice, fps, bitrate*1000, (bitrate+1024)*1000)
break
}
// https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
// gstreamer1.0-plugins-ugly
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream
if err := CheckPlugins([]string{"x264"}); err != nil {
return nil, err
}
vbvbuf := uint(1000)
if bitrate > 1000 {
vbvbuf = bitrate
}
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream"+pipelineStr, pipelineDevice, fps, bitrate, vbvbuf)
case "Opus":
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
// opusenc
if err := CheckPlugins([]string{"pulseaudio", "opus"}); err != nil {
return nil, err
}
pipelineStr = fmt.Sprintf(audioSrc+"opusenc bitrate=%d"+pipelineStr, pipelineDevice, bitrate*1000)
case "G722":
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c
// gstreamer1.0-libav
// avenc_g722
if err := CheckPlugins([]string{"pulseaudio", "libav"}); err != nil {
return nil, err
}
pipelineStr = fmt.Sprintf(audioSrc+"avenc_g722 bitrate=%d"+pipelineStr, pipelineDevice, bitrate*1000)
case "PCMU":
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c
// gstreamer1.0-plugins-good
// audio/x-raw, rate=8000 ! mulawenc
if err := CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil {
return nil, err
}
pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! mulawenc"+pipelineStr, pipelineDevice)
case "PCMA":
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c
// gstreamer1.0-plugins-good
// audio/x-raw, rate=8000 ! alawenc
if err := CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil {
return nil, err
}
pipelineStr = fmt.Sprintf(audioSrc+"audio/x-raw, rate=8000 ! alawenc"+pipelineStr, pipelineDevice)
default:
return nil, fmt.Errorf("unknown codec %s", codecName)
}
return CreatePipeline(pipelineStr)
}
// CreatePipeline creates a GStreamer Pipeline
func CreatePipeline(pipelineStr string) (*Pipeline, error) {
pipelineStrUnsafe := C.CString(pipelineStr)
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
pipelinesLock.Lock()
defer pipelinesLock.Unlock()
var err *C.GError
gstPipeline := C.gstreamer_send_create_pipeline(pipelineStrUnsafe, &err)
if err != nil {
defer C.g_error_free(err)
return nil, fmt.Errorf("%s", C.GoString(err.message))
}
p := &Pipeline{
Pipeline: gstPipeline,
Sample: make(chan types.Sample),
Src: pipelineStr,
id: len(pipelines),
}
pipelines[p.id] = p
return p, nil
}
// Start starts the GStreamer Pipeline
func (p *Pipeline) Start() {
C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id))
}
// Play starts the GStreamer Pipeline
func (p *Pipeline) Play() {
C.gstreamer_send_play_pipeline(p.Pipeline)
}
// Stop stops the GStreamer Pipeline
func (p *Pipeline) Stop() {
C.gstreamer_send_stop_pipeline(p.Pipeline)
}
// gst-inspect-1.0
func CheckPlugins(plugins []string) error {
var plugin *C.GstPlugin
for _, pluginstr := range plugins {
plugincstr := C.CString(pluginstr)
plugin = C.gst_registry_find_plugin(registry, plugincstr)
C.free(unsafe.Pointer(plugincstr))
if plugin == nil {
return fmt.Errorf("required gstreamer plugin %s not found", pluginstr)
}
}
return nil
}
//export goHandlePipelineBuffer
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
pipelinesLock.Lock()
pipeline, ok := pipelines[int(pipelineID)]
pipelinesLock.Unlock()
if ok {
pipeline.Sample <- types.Sample{Data: C.GoBytes(buffer, bufferLen), Timestamp: time.Now(), Duration: time.Duration(duration)}
} else {
fmt.Printf("discarding buffer, no pipeline with id %d", int(pipelineID))
}
C.free(buffer)
}

View File

@ -1,13 +0,0 @@
#pragma once
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
GstElement *gstreamer_send_create_pipeline(char *pipeline, GError **error);
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
void gstreamer_send_play_pipeline(GstElement *pipeline);
void gstreamer_send_stop_pipeline(GstElement *pipeline);
void gstreamer_init(void);

View File

@ -3,16 +3,18 @@ package http
import (
"context"
"encoding/json"
"image/jpeg"
"net/http"
"os"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/config"
"m1k1o/neko/internal/types"
"m1k1o/neko/internal/types/config"
)
type Server struct {
@ -22,7 +24,7 @@ type Server struct {
conf *config.Server
}
func New(conf *config.Server, webSocketHandler types.WebSocketHandler) *Server {
func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop types.DesktopManager) *Server {
logger := log.With().Str("module", "http").Logger()
router := chi.NewRouter()
@ -30,6 +32,12 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler) *Server {
router.Use(middleware.RequestLogger(&logformatter{logger}))
router.Use(middleware.Recoverer) // Recover from panics without crashing server
if conf.PathPrefix != "/" {
router.Use(func(h http.Handler) http.Handler {
return http.StripPrefix(conf.PathPrefix, h)
})
}
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
err := webSocketHandler.Upgrade(w, r)
if err != nil {
@ -58,6 +66,39 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler) *Server {
}
})
router.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAdmin, err := webSocketHandler.IsAdmin(password)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !isAdmin {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
if webSocketHandler.IsLocked("login") {
http.Error(w, "room is locked", http.StatusLocked)
return
}
quality, err := strconv.Atoi(r.URL.Query().Get("quality"))
if err != nil {
quality = 90
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Type", "image/jpeg")
img := desktop.GetScreenshotImage()
if err := jpeg.Encode(w, img, &jpeg.Options{Quality: quality}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("true"))
})

View File

@ -1,258 +0,0 @@
package remote
import (
"fmt"
"os/exec"
"time"
"m1k1o/neko/internal/gst"
"m1k1o/neko/internal/types"
"m1k1o/neko/internal/types/config"
"m1k1o/neko/internal/xorg"
"github.com/kataras/go-events"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type RemoteManager struct {
logger zerolog.Logger
video *gst.Pipeline
audio *gst.Pipeline
config *config.Remote
broadcast types.BroadcastManager
cleanup *time.Ticker
shutdown chan bool
emmiter events.EventEmmiter
streaming bool
}
func New(config *config.Remote, broadcast types.BroadcastManager) *RemoteManager {
return &RemoteManager{
logger: log.With().Str("module", "remote").Logger(),
cleanup: time.NewTicker(1 * time.Second),
shutdown: make(chan bool),
emmiter: events.New(),
config: config,
broadcast: broadcast,
streaming: false,
}
}
func (manager *RemoteManager) VideoCodec() string {
return manager.config.VideoCodec
}
func (manager *RemoteManager) AudioCodec() string {
return manager.config.AudioCodec
}
func (manager *RemoteManager) Start() {
xorg.Display(manager.config.Display)
if !xorg.ValidScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) {
manager.logger.Warn().Msgf("invalid screen option %dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)
} else if err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate); err != nil {
manager.logger.Warn().Err(err).Msg("unable to change screen size")
}
manager.createPipelines()
if err := manager.broadcast.Start(); err != nil {
manager.logger.Panic().Err(err).Msg("unable to create rtmp pipeline")
}
go func() {
defer func() {
manager.logger.Info().Msg("shutdown")
}()
for {
select {
case <-manager.shutdown:
return
case sample := <-manager.video.Sample:
manager.emmiter.Emit("video", sample)
case sample := <-manager.audio.Sample:
manager.emmiter.Emit("audio", sample)
case <-manager.cleanup.C:
xorg.CheckKeys(time.Second * 10)
}
}
}()
}
func (manager *RemoteManager) Shutdown() error {
manager.logger.Info().Msgf("remote shutting down")
manager.video.Stop()
manager.audio.Stop()
manager.broadcast.Stop()
manager.cleanup.Stop()
manager.shutdown <- true
return nil
}
func (manager *RemoteManager) OnVideoFrame(listener func(sample types.Sample)) {
manager.emmiter.On("video", func(payload ...interface{}) {
listener(payload[0].(types.Sample))
})
}
func (manager *RemoteManager) OnAudioFrame(listener func(sample types.Sample)) {
manager.emmiter.On("audio", func(payload ...interface{}) {
listener(payload[0].(types.Sample))
})
}
func (manager *RemoteManager) StartStream() {
manager.createPipelines()
manager.logger.Info().
Str("video_display", manager.config.Display).
Str("video_codec", manager.config.VideoCodec).
Str("audio_device", manager.config.Device).
Str("audio_codec", manager.config.AudioCodec).
Str("audio_pipeline_src", manager.audio.Src).
Str("video_pipeline_src", manager.video.Src).
Str("screen_resolution", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)).
Msgf("Pipelines starting...")
manager.video.Start()
manager.audio.Start()
manager.streaming = true
}
func (manager *RemoteManager) StopStream() {
manager.logger.Info().Msgf("Pipelines shutting down...")
manager.video.Stop()
manager.audio.Stop()
manager.streaming = false
}
func (manager *RemoteManager) Streaming() bool {
return manager.streaming
}
func (manager *RemoteManager) createPipelines() {
// handle maximum fps
rate := manager.config.ScreenRate
if manager.config.MaxFPS != 0 && manager.config.MaxFPS < manager.config.ScreenRate {
rate = manager.config.MaxFPS
}
var err error
manager.video, err = gst.CreateAppPipeline(
manager.config.VideoCodec,
manager.config.Display,
manager.config.VideoParams,
rate,
manager.config.VideoBitrate,
)
if err != nil {
manager.logger.Panic().Err(err).Msg("unable to create video pipeline")
}
manager.audio, err = gst.CreateAppPipeline(
manager.config.AudioCodec,
manager.config.Device,
manager.config.AudioParams,
0, // fps: n/a for audio
manager.config.AudioBitrate,
)
if err != nil {
manager.logger.Panic().Err(err).Msg("unable to create audio pipeline")
}
}
func (manager *RemoteManager) ChangeResolution(width int, height int, rate int) error {
if !xorg.ValidScreenSize(width, height, rate) {
return fmt.Errorf("unknown configuration")
}
manager.video.Stop()
manager.broadcast.Stop()
defer func() {
manager.video.Start()
if err := manager.broadcast.Start(); err != nil {
manager.logger.Panic().Err(err).Msg("unable to create rtmp pipeline")
}
manager.logger.Info().Msg("starting video pipeline...")
}()
if err := xorg.ChangeScreenSize(width, height, rate); err != nil {
return err
}
// handle maximum fps
if manager.config.MaxFPS != 0 && manager.config.MaxFPS < rate {
rate = manager.config.MaxFPS
}
var err error
manager.video, err = gst.CreateAppPipeline(
manager.config.VideoCodec,
manager.config.Display,
manager.config.VideoParams,
rate,
manager.config.VideoBitrate,
)
if err != nil {
manager.logger.Panic().Err(err).Msg("unable to create new video pipeline")
}
return nil
}
func (manager *RemoteManager) Move(x, y int) {
xorg.Move(x, y)
}
func (manager *RemoteManager) Scroll(x, y int) {
xorg.Scroll(x, y)
}
func (manager *RemoteManager) ButtonDown(code int) error {
return xorg.ButtonDown(code)
}
func (manager *RemoteManager) KeyDown(code uint64) error {
return xorg.KeyDown(code)
}
func (manager *RemoteManager) ButtonUp(code int) error {
return xorg.ButtonUp(code)
}
func (manager *RemoteManager) KeyUp(code uint64) error {
return xorg.KeyUp(code)
}
func (manager *RemoteManager) ReadClipboard() string {
return xorg.ReadClipboard()
}
func (manager *RemoteManager) WriteClipboard(data string) {
xorg.WriteClipboard(data)
}
func (manager *RemoteManager) ResetKeys() {
xorg.ResetKeys()
}
func (manager *RemoteManager) ScreenConfigurations() map[int]types.ScreenConfiguration {
return xorg.ScreenConfigurations
}
func (manager *RemoteManager) GetScreenSize() *types.ScreenSize {
return xorg.GetScreenSize()
}
func (manager *RemoteManager) SetKeyboardLayout(layout string) {
_ = exec.Command("setxkbmap", layout).Run()
}
func (manager *RemoteManager) SetKeyboardModifiers(NumLock int, CapsLock int, ScrollLock int) {
xorg.SetKeyboardModifiers(NumLock, CapsLock, ScrollLock)
}

Some files were not shown because too many files have changed in this diff Show More