75 Commits

Author SHA1 Message Date
c873d4d344 screenshare POC. 2023-01-29 21:29:16 +01:00
72c0070a3a update changelog. 2023-01-29 20:31:00 +01:00
009ceddbd3 gst: move sample channel to AttachAppsink. 2023-01-29 20:31:00 +01:00
fdf17cfe77 channel should not close on destroy 2023-01-29 20:31:00 +01:00
628c6a1b77 remove wait timer from goroutine 2023-01-29 20:31:00 +01:00
79a1c41938 ensure fps is not 0. 2023-01-29 20:31:00 +01:00
64b79f4579 update. 2023-01-29 20:31:00 +01:00
8d0468ea62 codec is video & audio. 2023-01-29 20:31:00 +01:00
89737dd4ce codec is video & audio. 2023-01-29 20:31:00 +01:00
2649594c2e strongly typed session events channel. 2023-01-29 20:31:00 +01:00
f3080713ce join GetScreenSizeChangeChannel. 2023-01-29 20:31:00 +01:00
6e62b796fc remove unused channels. 2023-01-29 20:31:00 +01:00
4094639ea9 adaptive fps moved to pipeline creation. 2023-01-29 20:31:00 +01:00
c45a315d9b removed adaptive framerate tag and react to closed channels 2023-01-29 20:31:00 +01:00
ee13e40d4c go fmt. 2023-01-29 20:31:00 +01:00
dfe8b8b57d npm lint. 2023-01-29 20:31:00 +01:00
12623866b3 disableHostCheck -> allowedHosts. 2023-01-29 20:31:00 +01:00
161d121e59 channel direct from the pipeline 2023-01-29 20:31:00 +01:00
5690a849e2 remove go-events 2023-01-29 20:31:00 +01:00
cfc6bd417f Update README.md 2023-01-15 22:21:44 +01:00
32472a70bc Fix docker build (#237)
* include package-lock.

* extract intel gpu support from dockerfile.

* update arm support.

* new workflows.

* build intel images.

* add to docs.
2023-01-15 20:38:29 +01:00
cd9ac70cb9 update XkbAddKeyKeysym. 2023-01-15 16:23:48 +01:00
a02f47f140 update xorg bindings. 2023-01-14 21:15:52 +01:00
ccc1df936d fix stereo for chormium browsers. 2023-01-12 20:53:36 +01:00
1509521129 Merge pull request #232 from Dishwasha/master
Use Keyboard API to lock screen for supported browsers
2023-01-07 11:58:36 +01:00
10e2fbd499 Use Keyboard API to lock screen for supported browsers
When using fullscreen mode, hitting the escape key will exit out of
fullscreen mode.  To stay in fullscreen mode but pass the escape
key through, we can use the Keyboard API to require the escape key
be held down https://wicg.github.io/keyboard-lock/#escape-key.

Tested in Chrome which supports Keyboard API and Firefox which does
not https://caniuse.com/?search=navigator.keyboard with no errors
in either case.
2023-01-06 20:10:34 -05:00
1c228a7daa update troubleshooting docs. 2023-01-04 23:16:17 +01:00
e63e9d5b2f update readme. 2022-12-22 21:40:31 +01:00
4789b7704f fix tor browser folder name. 2022-12-13 23:53:50 +01:00
bd73dfae9d Fix WebRTC mux issues. 2022-12-13 23:40:22 +01:00
c8c39b0f83 version 2.7. 2022-11-23 00:04:00 +01:00
61ea7b2940 allow downloads folder in chrome policies. 2022-11-23 00:03:51 +01:00
4d4e6eaeb7 update readme. 2022-11-22 23:54:08 +01:00
2debd4c3f3 upgrade intro gif. 2022-11-22 23:45:15 +01:00
c7885e3a90 update docs. 2022-11-20 14:53:46 +01:00
8c54585f2d add persistent data to docs #223. 2022-11-20 14:50:13 +01:00
e067894e02 opera fetch latest. 2022-11-20 14:41:36 +01:00
407a85fffb add role browser to openbox. 2022-11-20 14:40:35 +01:00
e0366cf1ef do not hide file chooser dialogs. 2022-11-20 14:12:08 +01:00
04a0ce17de fix file tranfser access control. 2022-11-19 23:05:34 +01:00
db87229f16 Merge pull request #221 from prophetofxenu/master
In app file transfer
2022-11-19 22:29:16 +01:00
3df319e071 line height. 2022-11-19 20:55:50 +01:00
ad5abb6054 css fix long file names. 2022-11-19 20:53:27 +01:00
ac822a2531 update docs. 2022-11-19 20:45:38 +01:00
e25e9c2b24 lint. 2022-11-19 20:33:08 +01:00
1666693c25 add cors. 2022-11-19 20:33:03 +01:00
d17a7e8d82 move filetransfer to locks. 2022-11-19 20:26:45 +01:00
cdb9b185f2 filepath clean. 2022-11-19 18:29:21 +01:00
76b44b949c add status failed + error. 2022-11-19 17:35:02 +01:00
950cb118cc remove unused property. 2022-11-19 16:57:00 +01:00
3703d2b73d lint fix. 2022-11-19 16:53:13 +01:00
fac8700192 lint fix. 2022-11-19 16:03:49 +01:00
04cd943eb4 move refresh ws to store. 2022-11-19 15:46:27 +01:00
472a3c3355 Merge branch 'master' of github.com:prophetofxenu/neko 2022-11-19 15:46:11 +01:00
5d41048695 update docs. 2022-11-18 19:41:15 +01:00
42ee7477a0 updated configuration 2022-11-16 22:30:50 -05:00
bbfa0d5834 added translations 2022-11-16 22:03:25 -05:00
4885c2d69e frontend improvements 2022-11-16 20:58:39 -05:00
b65df3e3bf more efficient file upload/download 2022-11-16 20:06:36 -05:00
57e89bb1cc file transfer permission state management 2022-11-15 20:39:06 -05:00
19af921913 added file uploads to frontend 2022-11-13 22:06:24 -05:00
5ba4019e36 abort in progress transfers on cancel (updated axios too) 2022-11-13 11:27:04 -05:00
e3963736ad Merge branch 'm1k1o:master' into master 2022-11-11 20:27:59 -05:00
b9f31cc19c added file downloads to frontend 2022-11-11 20:27:15 -05:00
7c6029aa99 watch file transfer dir for changes 2022-11-04 22:43:18 -04:00
cfc7b15211 manual refresh function 2022-11-03 21:54:05 -04:00
70e84c5840 listing of files on connect 2022-11-02 22:20:32 -04:00
1505abb703 http endpoints for transferring files 2022-10-30 21:06:05 -04:00
758879ef84 fix sweetalert2 version #211. 2022-10-27 20:23:45 +02:00
afd0925cfc upgrade client deps. 2022-10-24 22:29:26 +02:00
7d3a888c79 upgrade go deps. 2022-10-24 21:40:59 +02:00
cfbda6ec97 update changelog. 2022-10-24 19:38:24 +02:00
44e891331e join fonts install. 2022-10-24 19:37:42 +02:00
Bad
8ab2943126 Emoji support
Fixes emojis by adding Google's Noto Emoji.
2022-10-24 20:32:14 +03:00
fa45619dbb added basic UI for file transfer 2022-10-16 20:59:01 -04:00
116 changed files with 24278 additions and 669 deletions

View File

@ -1,7 +1,7 @@
#
# STAGE 1: SERVER
#
FROM golang:1.18-bullseye as server
FROM golang:1.19-bullseye as server
WORKDIR /src
#
@ -32,7 +32,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
#
# STAGE 2: CLIENT
#
FROM node:14-bullseye-slim as client
FROM node:18-bullseye-slim as client
WORKDIR /src
#
@ -61,11 +61,6 @@ ARG USER_UID=1000
ARG USER_GID=$USER_UID
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
@ -73,16 +68,18 @@ RUN set -eux; \
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; \
#
# intel driver + vaapi
apt-get install -y --no-install-recommends intel-media-va-driver-non-free libva2 vainfo; \
#
# gst + vaapi plugin
# gst
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-vaapi ;\
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio; \
#
# fonts
apt-get install -y --no-install-recommends fonts-takao-mincho fonts-wqy-zenhei; \
# install fonts
apt-get install -y --no-install-recommends \
# Google emojis
fonts-noto-color-emoji \
# Japanese fonts
fonts-takao-mincho \
# Chinese fonts
fonts-wqy-zenhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -116,7 +113,6 @@ 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
@ -125,7 +121,6 @@ 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.18-buster as server
FROM golang:1.19-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; \
@ -32,7 +32,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
#
# STAGE 2: CLIENT
#
FROM node:14-buster-slim as client
FROM node:18-bullseye-slim as client
# install dependencies
RUN set -eux; apt-get update; \
@ -53,7 +53,7 @@ RUN npm run build
#
# STAGE 3: RUNTIME
#
FROM arm32v7/debian:buster-slim
FROM debian:bullseye-slim
#
# avoid warnings by switching to noninteractive
@ -65,19 +65,27 @@ ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
#
# install dependencies
RUN set -eux; apt-get update; \
RUN set -eux; \
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 libvpx5; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
#
# gst
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-omx; \
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 fonts-wqy-zenhei; \
# install fonts
apt-get install -y --no-install-recommends \
# Google emojis
fonts-noto-color-emoji \
# Japanese fonts
fonts-takao-mincho \
# Chinese fonts
fonts-wqy-zenhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -131,4 +139,3 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
#
# run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View File

@ -0,0 +1,146 @@
#
# STAGE 1: SERVER
#
FROM golang:1.19-bullseye as server
WORKDIR /src
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends git cmake make libx11-dev libxrandr-dev libxtst-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly; \
#
# install libclipboard
set -eux; \
cd /tmp; \
git clone --depth=1 https://github.com/jtanx/libclipboard; \
cd libclipboard; \
cmake .; \
make -j4; \
make install; \
rm -rf /tmp/libclipboard; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# build server
COPY server/ .
RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
#
# STAGE 2: CLIENT
#
FROM node:18-bullseye-slim as client
WORKDIR /src
#
# install dependencies
COPY client/package*.json ./
RUN npm install
#
# build client
COPY client/ .
RUN npm run build
#
# STAGE 3: RUNTIME
#
FROM debian:bullseye-slim
#
# avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive
#
# set custom user
ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
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; \
#
# 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-vaapi; \
#
# install fonts
apt-get install -y --no-install-recommends \
# Google emojis
fonts-noto-color-emoji \
# Japanese fonts
fonts-takao-mincho \
# Chinese fonts
fonts-wqy-zenhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
adduser $USERNAME audio; \
adduser $USERNAME video; \
adduser $USERNAME pulse; \
#
# setup pulseaudio
mkdir -p /home/$USERNAME/.config/pulse/; \
echo "default-server=unix:/tmp/pulseaudio.socket" > /home/$USERNAME/.config/pulse/client.conf; \
#
# workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
mkdir /tmp/.X11-unix; \
chmod 1777 /tmp/.X11-unix; \
chown $USERNAME /tmp/.X11-unix/; \
#
# make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \
chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy config files
COPY .docker/base/dbus /usr/bin/dbus
COPY .docker/base/default.pa /etc/pulse/default.pa
COPY .docker/base/intel/supervisord.conf /etc/neko/supervisord.conf
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
COPY .docker/base/intel/add-render-group.sh /usr/bin/add-render-group.sh
#
# set default envs
ENV USER=$USERNAME
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
COPY --from=server /src/bin/neko /usr/bin/neko
COPY --from=client /src/dist/ /var/www
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_BIND#*:}/health || exit 1
#
# run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View File

@ -0,0 +1,69 @@
[supervisord]
nodaemon=true
user=root
pidfile=/var/run/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
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
autorestart=true
priority=100
user=root
stdout_logfile=/var/log/neko/dbus.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:x-server]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s"
command=/usr/bin/X -config /etc/neko/xorg.conf %(ENV_DISPLAY)s
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/xorg.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:pulseaudio]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/pulseaudio --log-level=info --disallow-module-loading --disallow-exit --exit-idle-time=-1
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/pulseaudio.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:neko]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/neko serve --static "/var/www"
stopsignal=INT
stopwaitsecs=5
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/neko.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -9,19 +9,6 @@ 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

View File

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

View File

@ -18,6 +18,9 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"chrome://policy"

View File

@ -77,6 +77,21 @@ build_arm() {
fi
}
build_intel() {
if [ "$1" = "base" ]
then
# build intel base
docker build -t "${BUILD_IMAGE}:intel-base" -f base/Dockerfile.intel "${BASE}"
elif [ -f "$1/Dockerfile.intel" ]
then
# build dedicated intel image
docker build -t "${BUILD_IMAGE}:intel-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:intel-base" -f "$1/Dockerfile.intel" "$1/"
else
# try to build intel image with common Dockerfile
docker build -t "${BUILD_IMAGE}:intel-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:intel-base" -f "$1/Dockerfile" "$1/"
fi
}
case $1 in
client) build_client;;
server) build_server;;
@ -84,6 +99,9 @@ case $1 in
# build arm- images
arm-*) build_arm "${1#arm-}";;
# build intel- images
intel-*) build_intel "${1#intel-}";;
# build images
*) build "$1";;
esac

View File

@ -4,7 +4,7 @@ FROM $BASE_IMAGE
#
# install neko chromium
RUN set -eux; apt-get update; \
# TODO: Bring back DRM support with arm32v7/debian:buster-slim image.
# TODO: Bring back DRM support.
apt-get install -y --no-install-recommends chromium openbox; \
#
# clean up

View File

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

View File

@ -19,6 +19,9 @@
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"BrowserLabsEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"chrome://policy"

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" role="browser">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>

View File

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

View File

@ -18,6 +18,9 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"chrome://policy"

View File

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

View File

@ -18,6 +18,9 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"edge://policy"

View File

@ -1,24 +1,28 @@
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"
ARG API_URL="https://download5.operacdn.com/pub/opera/desktop/"
ARG LIBFFMPEG_API_URL="https://api.github.com/repos/nwjs-ffmpeg-prebuilt/nwjs-ffmpeg-prebuilt/releases/latest"
#
# 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/*
RUN set -eux; apt-get update; \
#
# fetch latest release
VERSION="$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"/]*\).*/\1/p' | tail -1)"; \
wget -O /tmp/opera.deb "${API_URL}${VERSION}/linux/opera-stable_${VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends openbox jq unzip /tmp/opera.deb; \
#
# install libffmpeg
LIBFFMPEG_URL="$(wget -O - "${LIBFFMPEG_API_URL}" 2>/dev/null | jq -r "[.assets[] | select(.browser_download_url | contains(\"linux-x64.zip\"))][-1] | .browser_download_url")"; \
wget -O /tmp/libffmpeg.zip $LIBFFMPEG_URL; \
unzip -o /tmp/libffmpeg.zip libffmpeg.so -d /usr/lib/x86_64-linux-gnu/opera/lib_extra; \
echo '[]' > /usr/lib/x86_64-linux-gnu/opera/resources/ffmpeg_preload_config.json; \
#
# clean up
apt-get --purge autoremove -y unzip jq; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files

View File

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

View File

@ -11,6 +11,7 @@ RUN set -eux; apt-get update; \
echo "Downloading $DOWNLOAD_URI"; \
curl -sSL -o /tmp/tor.tar.xz "https://www.torproject.org/$DOWNLOAD_URI"; \
tar -xvJf /tmp/tor.tar.xz -C /opt; \
mv /opt/tor-browser* /opt/tor-browser_en-US; \
chown -R neko:neko /opt/tor-browser_en-US/; \
rm -f /tmp/tor.tar.xz; \
#

View File

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

View File

@ -18,6 +18,9 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"chrome://policy"

View File

@ -18,6 +18,9 @@
"PromptForDownloadLocation": false,
"BookmarkBarEnabled": false,
"PasswordManagerEnabled": false,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
"URLBlocklist": [
"file://*",
"chrome://policy"

View File

@ -1,4 +1,4 @@
name: "CI for builds"
name: "build and push amd64 images to Docker Hub"
on:
push:

View File

@ -1,4 +1,4 @@
name: "CI for version tags"
name: "amd64 images"
on:
push:
@ -8,6 +8,9 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: ""
BASE_DOCKERFILE: Dockerfile
PLATFORMS: linux/amd64
jobs:
build-base:
@ -31,7 +34,7 @@ jobs:
uses: docker/metadata-action@v3
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@ -49,8 +52,8 @@ jobs:
uses: docker/build-push-action@v2
with:
context: ./
file: .docker/base/Dockerfile
platforms: linux/amd64,linux/arm64
file: .docker/base/${{ env.BASE_DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@ -67,32 +70,19 @@ jobs:
matrix:
include:
- tag: firefox
platforms: linux/amd64,linux/arm64
- tag: chromium
platforms: linux/amd64,linux/arm64
- tag: google-chrome
platforms: linux/amd64
- tag: ungoogled-chromium
platforms: linux/amd64,linux/arm64
- tag: microsoft-edge
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: remmina
platforms: linux/amd64
- tag: vlc
platforms: linux/amd64,linux/arm64
- tag: xfce
platforms: linux/amd64,linux/arm64
env:
TAG_NAME: ${{ matrix.tag }}
PLATFORMS: ${{ matrix.platforms }}
steps:
-
name: Checkout
@ -108,11 +98,12 @@ jobs:
uses: docker/metadata-action@v3
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_NAME }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}${{ env.TAG_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=long
-
name: Log in to the Container registry
uses: docker/login-action@v1
@ -130,4 +121,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/base:sha-${{ github.sha }}
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base:sha-${{ github.sha }}

126
.github/workflows/ghcr-arm.yml vendored Normal file
View File

@ -0,0 +1,126 @@
name: "arm64v8 and arm32v7 images"
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: arm-
BASE_DOCKERFILE: Dockerfile.arm
PLATFORMS: linux/arm64,linux/arm/v7
jobs:
build-base:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v3
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=long
-
name: Log in to the Container registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: ./
file: .docker/base/${{ env.BASE_DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
needs: [ build-base ]
strategy:
# Will build all images even if some fail.
matrix:
include:
- tag: firefox
dockerfile: Dockerfile.arm
- tag: chromium
dockerfile: Dockerfile.arm
- tag: ungoogled-chromium
dockerfile: Dockerfile
- tag: tor-browser
dockerfile: Dockerfile
- tag: vlc
dockerfile: Dockerfile
- tag: xfce
dockerfile: Dockerfile
env:
TAG_NAME: ${{ matrix.tag }}
DOCKERFILE: ${{ matrix.dockerfile }}
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v3
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}${{ env.TAG_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=long
-
name: Log in to the Container registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .docker/${{ env.TAG_NAME }}
file: .docker/${{ env.TAG_NAME }}/${{ env.DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base:sha-${{ github.sha }}

125
.github/workflows/ghcr-intel.yml vendored Normal file
View File

@ -0,0 +1,125 @@
name: "intel gpu supported images"
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: intel-
BASE_DOCKERFILE: Dockerfile.intel
PLATFORMS: linux/amd64
jobs:
build-base:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v3
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=long
-
name: Log in to the Container registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: ./
file: .docker/base/${{ env.BASE_DOCKERFILE }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
needs: [ build-base ]
strategy:
# Will build all images even if some fail.
matrix:
include:
- tag: firefox
- tag: chromium
- tag: google-chrome
- tag: ungoogled-chromium
- tag: microsoft-edge
- tag: brave
- tag: vivaldi
- tag: opera
- tag: tor-browser
- tag: remmina
- tag: vlc
- tag: xfce
env:
TAG_NAME: ${{ matrix.tag }}
DOCKERFILE: ${{ matrix.dockerfile }}
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v3
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}${{ env.TAG_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,format=long
-
name: Log in to the Container registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .docker/${{ env.TAG_NAME }}
platforms: ${{ env.PLATFORMS }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.TAG_PREFIX }}base:sha-${{ github.sha }}

4
.gitignore vendored
View File

@ -23,10 +23,6 @@ pids
*.seed
*.pid.lock
# Lock files
yarn.lock
package-lock.json
# TypeScript incremental compilation cache
*.tsbuildinfo

View File

@ -22,14 +22,22 @@
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
</a>
<a href="https://github.com/m1k1o/neko/actions">
<img src="https://github.com/m1k1o/neko/actions/workflows/build.yml/badge.svg" alt="build">
<img src="https://github.com/m1k1o/neko/actions/workflows/ghcr-amd.yml/badge.svg" alt="build">
</a>
</p>
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/>
</div>
# n.eko
Welcome to Neko, a self-hosted virtual browser that runs in Docker and uses WebRTC technology. Neko is a powerful tool that allows you to **run a fully-functional browser in a virtual environment**, giving you the ability to **access the internet securely and privately from anywhere**. With Neko, you can browse the web, **run applications**, and perform other tasks just as you would on a regular browser, all within a **secure and isolated environment**. Whether you are a developer looking to test web applications, a **privacy-conscious user seeking a secure browsing experience**, or simply someone who wants to take advantage of the **convenience and flexibility of a virtual browser**, Neko is the perfect solution.
In addition to its security and privacy features, Neko offers the **ability for multiple users to access it simultaneously**. This makes it an ideal solution for teams or organizations that need to share access to a browser, as well as for individuals who want to use **multiple devices to access the same virtual environment**. With Neko, you can **easily and securely share access to a browser with others**, without having to worry about maintaining separate configurations or settings. Whether you need to **collaborate on a project**, access shared resources, or simply want to **share access to a browser with friends or family**, Neko makes it easy to do so.
Neko is also a great tool for **hosting watch parties** and interactive presentations. With its virtual browser capabilities, Neko allows you to host watch parties and presentations that are **accessible from anywhere**, without the need for in-person gatherings. This makes it easy to **stay connected with friends and colleagues**, even when you are unable to meet in person. With Neko, you can easily host a watch party or give an **interactive presentation**, whether it's for leisure or work. Simply invite your guests to join the virtual environment, and you can share the screen and **interact with them in real-time**.
## About
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.
@ -44,11 +52,11 @@ Neko started as a virtual browser that is streamed using WebRTC to multiple user
- 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/).
- **Watch party** - watching video content together with multiple people and reacting to it (chat, emotes) - open source alternative to [giggl.app](https://giggl.app/) or [hyperbeam](https://watch.hyperbeam.com).
- **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/).
- **Embed anything** - embed virtual browser in your web app - open source alternative to [hyperbeam API](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).
@ -105,6 +113,14 @@ Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://g
* 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...)
* Bidirectional file transfer (if enabled)
<div align="center">
With `NEKO_FILE_TRANSFER_ENABLED=true`:
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/file-transfer.gif" width="650" height="auto"/>
</div>
### Why n.eko?

21271
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,18 +20,18 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.1.2",
"@fortawesome/fontawesome-free": "^6.2.0",
"animejs": "^3.2.0",
"axios": "^0.21.4",
"date-fns": "^2.29.1",
"axios": "^1.2.3",
"date-fns": "^2.29.3",
"emoji-datasource": "^6.0.1",
"eventemitter3": "^4.0.7",
"resize-observer-polyfill": "^1.5.1",
"simple-markdown": "^0.7.2",
"sweetalert2": "^11.4.24",
"sweetalert2": "11.4.8",
"typed-vuex": "^0.1.21",
"v-tooltip": "^2.0.3",
"vue": "^2.7.8",
"vue": "^2.7.13",
"vue-class-component": "^7.2.6",
"vue-clickaway": "^2.2.2",
"vue-context": "^5.2.0",
@ -41,29 +41,29 @@
"vuex": "^3.5.1"
},
"devDependencies": {
"@types/animejs": "^3.1.5",
"@types/node": "^14.18.23",
"@types/animejs": "^3.1.6",
"@types/node": "^18.11.18",
"@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.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",
"@typescript-eslint/eslint-plugin": "^5.0.8",
"@typescript-eslint/parser": "^5.0.8",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-plugin-vuex": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"core-js": "^3.24.1",
"@vue/eslint-config-typescript": "^11.0.2",
"core-js": "^3.26.0",
"emojilib": "^3.0.7",
"eslint": "^6.8.0",
"eslint": "^8.32.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.20.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^2.7.1",
"sass": "^1.54.0",
"sass": "^1.55.0",
"sass-loader": "^10.3.1",
"ts-node": "^9.1.1",
"typescript": "^4.7.4",
"vue-template-compiler": "^2.7.8"
"typescript": "^4.8.4",
"vue-template-compiler": "^2.7.13"
}
}

View File

@ -132,8 +132,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import md, { HtmlOutputRule } from 'simple-markdown'
import { Component, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-about' })
export default class extends Vue {

View File

@ -147,8 +147,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { get, set } from '~/utils/localstorage'
import { Component, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-connect' })
export default class extends Vue {

View File

@ -132,7 +132,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Ref, Vue } from 'vue-property-decorator'
import { Member } from '~/neko/types'
// @ts-ignore
@ -229,11 +229,11 @@
}
}
adminRelease(member: Member) {
adminRelease() {
this.$accessor.remote.adminRelease()
}
adminControl(member: Member) {
adminControl() {
this.$accessor.remote.adminControl()
}

View File

@ -287,9 +287,9 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Ref, Vue } from 'vue-property-decorator'
import { directive as onClickaway } from 'vue-clickaway'
import { get, set } from '../utils/localstorage'
import { get } from '../utils/localstorage'
@Component({
name: 'neko-emoji',
@ -356,7 +356,7 @@
this.waitingForPaint = false
let scrollTop = this._scroll.scrollTop
let active = 0
for (const [i, group] of this.groups.entries()) {
for (const [i] of this.groups.entries()) {
let component = this._groups[i]
if (component && component.offsetTop > scrollTop) {
break
@ -368,7 +368,7 @@
}
}
onMouseExit(event: MouseEvent, emoji: string) {
onMouseExit() {
this.hovered = ''
}
@ -382,7 +382,7 @@
this.$emit('picked', emoji)
}
onClickAway(event: MouseEvent) {
onClickAway() {
this.$emit('done')
}
}

View File

@ -0,0 +1,520 @@
<template>
<div class="files">
<div class="files-cwd">
<p>{{ cwd }}</p>
<i class="fas fa-rotate-right refresh" @click="refresh" />
</div>
<div class="files-list">
<div v-for="item in files" :key="item.name" class="files-list-item">
<i :class="fileIcon(item)" />
<p class="file-name" :title="item.name">{{ item.name }}</p>
<p class="file-size">{{ fileSize(item.size) }}</p>
<i v-if="item.type !== 'dir'" class="fas fa-download download" @click="download(item)" />
</div>
</div>
<div class="transfer-area">
<div class="transfers" v-if="transfers.length > 0">
<p v-if="downloads.length > 0" class="transfers-list-header">
<span>{{ $t('files.downloads') }}</span>
<i class="fas fa-xmark remove-transfer" @click="downloads.forEach((t) => removeTransfer(t))"></i>
</p>
<div v-for="download in downloads" :key="download.id" class="transfers-list-item">
<div class="transfer-info">
<i
class="fas transfer-status"
:class="{
'fa-clock': download.status === 'pending',
'fa-arrows-rotate': download.status === 'inprogress',
'fa-check': download.status === 'completed',
'fa-warning': download.status === 'failed',
}"
></i>
<p class="file-name" :title="download.name">{{ download.name }}</p>
<p class="file-size">{{ Math.min(100, Math.round((download.progress / download.size) * 100)) }}%</p>
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(download)"></i>
</div>
<div v-if="download.status === 'failed'" class="transfer-error">{{ download.error }}</div>
<progress
v-else
class="transfer-progress"
:aria-label="download.name + ' progress'"
:value="download.progress"
:max="download.size"
></progress>
</div>
<p v-if="uploads.length > 0" class="transfers-list-header">
<span>{{ $t('files.uploads') }}</span>
<i class="fas fa-xmark remove-transfer" @click="uploads.forEach((t) => removeTransfer(t))"></i>
</p>
<div v-for="upload in uploads" :key="upload.id" class="transfers-list-item">
<div class="transfer-info">
<i
class="fas transfer-status"
:title="upload.status"
:class="{
'fa-clock': upload.status === 'pending',
'fa-arrows-rotate': upload.status === 'inprogress',
'fa-check': upload.status === 'completed',
'fa-warning': upload.status === 'failed',
}"
></i>
<p class="file-name" :title="upload.name">{{ upload.name }}</p>
<p class="file-size">{{ Math.min(100, Math.round((upload.progress / upload.size) * 100)) }}%</p>
<i class="fas fa-xmark remove-transfer" @click="removeTransfer(upload)"></i>
</div>
<div v-if="upload.status === 'failed'" class="transfer-error">{{ upload.error }}</div>
<progress
v-else
class="transfer-progress"
:aria-label="upload.name + ' progress'"
:value="upload.progress"
:max="upload.size"
></progress>
</div>
</div>
<div
class="upload-area"
:class="{ 'upload-area-drag': uploadAreaDrag }"
@dragover.prevent="uploadAreaDrag = true"
@dragleave.prevent="uploadAreaDrag = false"
@drop.prevent="(e) => upload(e.dataTransfer)"
@click="openFileBrowser"
>
<i class="fas fa-file-arrow-up" />
<p>{{ $t('files.upload_here') }}</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.files {
flex: 1;
flex-direction: column;
display: flex;
max-width: 100%;
.files-cwd {
display: flex;
flex-direction: row;
margin: 10px 10px 0px 10px;
padding: 0.5em;
font-weight: 600;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
.files-list {
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
overflow-y: scroll;
scrollbar-width: thin;
scrollbar-color: $background-tertiary transparent;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $background-tertiary;
border: 2px solid $background-primary;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $background-floating;
}
}
.files-list-item {
padding: 0.5em;
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
display: flex;
flex-direction: row;
line-height: 1.2;
}
.transfers-list-header {
display: flex;
justify-content: space-between;
border-bottom: 2px solid rgba($color: #fff, $alpha: 0.1);
}
.file-icon,
.transfer-status {
width: 14px;
margin-right: 0.5em;
}
.transfer-error {
border: 1px solid $style-error;
border-radius: 5px;
padding: 10px;
}
.files-list-item:last-child {
border-bottom: 0px;
}
.refresh {
margin-left: auto;
}
.file-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.file-size {
margin-left: auto;
margin-right: 0.5em;
color: rgba($color: #fff, $alpha: 0.4);
white-space: nowrap;
}
.refresh:hover,
.download:hover,
.remove-transfer:hover {
cursor: pointer;
}
.transfer-area {
margin-top: auto;
}
.transfers {
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
max-height: 50vh;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: $background-tertiary transparent;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $background-tertiary;
border: 2px solid $background-primary;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $background-floating;
}
}
.transfers > p {
padding: 10px;
font-weight: 600;
}
.transfer-info {
display: flex;
flex-direction: row;
max-width: 100%;
padding: 10px;
}
.transfer-progress {
margin: 0px 10px 10px 10px;
width: 95%;
}
.upload-area {
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
margin: 10px 10px 10px 10px;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
}
.upload-area:hover {
cursor: pointer;
}
.upload-area-drag,
.upload-area:hover {
background-color: rgba($color: #fff, $alpha: 0.1);
}
.upload-area > i {
font-size: 4em;
margin: 10px 10px 10px 10px;
}
.upload-area > p {
margin: 0px 10px 10px 10px;
}
}
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Markdown from './markdown'
import Content from './context.vue'
import { FileTransfer, FileListItem } from '~/neko/types'
@Component({
name: 'neko-files',
components: {
'neko-markdown': Markdown,
'neko-context': Content,
},
})
export default class extends Vue {
public uploadAreaDrag: boolean = false
get cwd() {
return this.$accessor.files.cwd
}
get files() {
return this.$accessor.files.files
}
get transfers() {
return this.$accessor.files.transfers
}
get downloads() {
return this.$accessor.files.transfers.filter((t) => t.direction === 'download')
}
get uploads() {
return this.$accessor.files.transfers.filter((t) => t.direction === 'upload')
}
refresh() {
this.$accessor.files.refresh()
}
download(item: FileListItem) {
if (this.downloads.map((t) => t.name).includes(item.name)) {
return
}
const url =
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
const abortController = new AbortController()
let transfer: FileTransfer = {
id: Math.round(Math.random() * 10000),
name: item.name,
direction: 'download',
// this may be smaller than the actual transfer amount, but for large files the
// content length is not sent (chunked transfer)
size: item.size,
progress: 0,
status: 'pending',
abortController: abortController,
}
this.$http
.get(url, {
responseType: 'blob',
signal: abortController.signal,
withCredentials: false,
onDownloadProgress: (x) => {
transfer.progress = x.loaded
if (x.total && transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {
transfer.status = 'completed'
} else if (transfer.status !== 'inprogress') {
transfer.status = 'inprogress'
}
},
})
.then((res) => {
const url = window.URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', item.name)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
transfer.progress = transfer.size
transfer.status = 'completed'
})
.catch((error) => {
this.$log.error(error)
transfer.status = 'failed'
transfer.error = error.message
})
this.$accessor.files.addTransfer(transfer)
}
upload(dt: DataTransfer) {
const url = '/file?pwd=' + encodeURIComponent(this.$accessor.password)
this.uploadAreaDrag = false
for (const file of dt.files) {
const abortController = new AbortController()
const formdata = new FormData()
formdata.append('files', file, file.name)
let transfer: FileTransfer = {
id: Math.round(Math.random() * 10000),
name: file.name,
direction: 'upload',
size: file.size,
progress: 0,
status: 'pending',
abortController: abortController,
}
this.$http
.post(url, formdata, {
signal: abortController.signal,
withCredentials: false,
onUploadProgress: (x: any) => {
transfer.progress = x.loaded
if (transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {
transfer.status = 'completed'
} else if (transfer.status !== 'inprogress') {
transfer.status = 'inprogress'
}
},
})
.catch((error) => {
this.$log.error(error)
transfer.status = 'failed'
transfer.error = error.message
})
this.$accessor.files.addTransfer(transfer)
}
}
openFileBrowser() {
const input = document.createElement('input')
input.type = 'file'
input.setAttribute('multiple', 'true')
input.onchange = (e: Event) => {
if (e === null) return
const dt = new DataTransfer()
const target = e.target as HTMLInputElement
if (target.files === null) return
for (const f of target.files) {
dt.items.add(f)
}
this.upload(dt)
}
input.click()
}
removeTransfer(transfer: FileTransfer) {
if (transfer.status !== 'completed') {
transfer.abortController?.abort()
}
this.$accessor.files.removeTransfer(transfer)
}
fileIcon(file: FileListItem) {
let className = 'file-icon fas '
// if is directory
if (file.type === 'dir') {
className += 'fa-folder'
return className
}
// try to get file extension
const ext = file.name.split('.').pop()
if (ext === undefined) {
className += 'fa-file'
return className
}
// try to find icon
switch (ext.toLowerCase()) {
case 'txt':
case 'md':
className += 'fa-file-text'
break
case 'pdf':
className += 'fa-file-pdf'
break
case 'zip':
case 'rar':
case '7z':
case 'gz':
className += 'fa-archive'
break
case 'aac':
case 'flac':
case 'midi':
case 'mp3':
case 'ogg':
case 'wav':
className += 'fa-music'
break
case 'avi':
case 'mkv':
case 'mov':
case 'mpeg':
case 'mp4':
case 'webm':
className += 'fa-film'
break
case 'bmp':
case 'gif':
case 'jpeg':
case 'jpg':
case 'png':
case 'svg':
case 'tiff':
case 'webp':
className += 'fa-image'
break
default:
className += 'fa-file'
}
return className
}
fileSize(size: number) {
if (size < 1024) {
return size + ' B'
}
if (size < 1024 * 1024) {
return Math.round(size / 1024) + ' KB'
}
if (size < 1024 * 1024 * 1024) {
return Math.round(size / (1024 * 1024)) + ' MB'
}
if (size < 1024 * 1024 * 1024 * 1024) {
return Math.round(size / (1024 * 1024 * 1024)) + ' GB'
}
return Math.round(size / (1024 * 1024 * 1024 * 1024)) + ' TB'
}
}
</script>

View File

@ -5,6 +5,14 @@
<span><b>n</b>.eko</span>
</a>
<ul class="menu">
<li>
<button class="btn" @click="startShareScreen" v-if="!mediaStream">
START SCREEN SHARE
</button>
<button class="btn" @click="stopShareScreen" v-else>
STOP SCREEN SHARE
</button>
</li>
<li>
<i
:class="[{ disabled: !admin }, { locked: isLocked('control') }, 'fas', 'fa-mouse']"
@ -31,6 +39,19 @@
}"
/>
</li>
<li v-if="fileTransfer">
<i
:class="[{ disabled: !admin }, { locked: isLocked('file_transfer') }, 'fas', 'fa-file']"
@click="toggleLock('file_transfer')"
v-tooltip="{
content: lockedTooltip('file_transfer'),
placement: 'bottom',
offset: 5,
boundariesElement: 'body',
delay: { show: 300, hide: 100 },
}"
/>
</li>
<li>
<span v-if="showBadge" class="badge">&bull;</span>
<i class="fas fa-bars toggle" @click="toggleMenu" />
@ -144,7 +165,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Vue } from 'vue-property-decorator'
import { AdminLockResource } from '~/neko/messages'
@Component({ name: 'neko-settings' })
@ -169,26 +190,24 @@
return !this.side && this.readTexts != this.texts
}
get fileTransfer() {
return this.$accessor.remote.fileTransfer
}
toggleLock(resource: AdminLockResource) {
this.$accessor.toggleLock(resource)
}
isLocked(resource: AdminLockResource): boolean {
return this.$accessor.isLocked(resource)
}
readTexts: number = 0
toggleMenu() {
this.$accessor.client.toggleSide()
this.readTexts = this.texts
}
toggleLock(resource: AdminLockResource) {
if (!this.admin) return
if (this.isLocked(resource)) {
this.$accessor.unlock(resource)
} else {
this.$accessor.lock(resource)
}
}
isLocked(resource: AdminLockResource): boolean {
return resource in this.locked && this.locked[resource]
}
lockedTooltip(resource: AdminLockResource) {
if (this.admin) {
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `unlock` : `lock`))
@ -196,5 +215,31 @@
return this.$t(`locks.${resource}.` + (this.isLocked(resource) ? `locked` : `unlocked`))
}
//
// Screen Share
//
mediaStream: MediaStream | null = null
mediaRtcpSender: RTCRtpSender | null = null
async startShareScreen() {
// get media stream from user's browser
this.mediaStream = await navigator.mediaDevices
.getDisplayMedia({
video: true,
audio: false,
})
const mediaTrack = this.mediaStream.getVideoTracks()[0];
this.mediaRtcpSender = this.$client.addTrack(mediaTrack, this.mediaStream)
}
async stopShareScreen() {
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop())
this.mediaStream = null
}
if (this.mediaRtcpSender) {
this.$client.removeTrack(this.mediaRtcpSender)
this.mediaRtcpSender = null
}
}
}
</script>

View File

@ -1,5 +1,5 @@
import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown'
import { Component, Watch, Vue, Prop } from 'vue-property-decorator'
import { Component, Vue, Prop } from 'vue-property-decorator'
const { blockQuote, inlineCode, codeBlock, autolink, newline, escape, strong, text, link, url, em, u, br } =
defaultRules

View File

@ -157,8 +157,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Member } from '~/neko/types'
import { Component, Ref, Vue } from 'vue-property-decorator'
import Content from './context.vue'
import Avatar from './avatar.vue'

View File

@ -60,7 +60,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Vue } from 'vue-property-decorator'
import { messages } from '~/locale'
@Component({ name: 'neko-menu' })

View File

@ -97,7 +97,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Ref, Vue } from 'vue-property-decorator'
import { ScreenResolution } from '~/neko/types'
// @ts-ignore

View File

@ -304,7 +304,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Watch, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-settings' })
export default class extends Vue {

View File

@ -6,6 +6,10 @@
<i class="fas fa-comment-alt" />
<span>{{ $t('side.chat') }}</span>
</li>
<li v-if="filetransferAllowed" :class="{ active: tab === 'files' }" @click.stop.prevent="change('files')">
<i class="fas fa-file" />
<span>{{ $t('side.files') }}</span>
</li>
<li :class="{ active: tab === 'settings' }" @click.stop.prevent="change('settings')">
<i class="fas fa-sliders-h" />
<span>{{ $t('side.settings') }}</span>
@ -14,6 +18,7 @@
</div>
<div class="page-container">
<neko-chat v-if="tab === 'chat'" />
<neko-files v-if="tab === 'files'" />
<neko-settings v-if="tab === 'settings'" />
</div>
</aside>
@ -74,23 +79,47 @@
</style>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { Vue, Component, Watch } from 'vue-property-decorator'
import Settings from '~/components/settings.vue'
import Chat from '~/components/chat.vue'
import Files from '~/components/files.vue'
@Component({
name: 'neko',
components: {
'neko-settings': Settings,
'neko-chat': Chat,
'neko-files': Files,
},
})
export default class extends Vue {
get filetransferAllowed() {
return (
this.$accessor.remote.fileTransfer && (this.$accessor.user.admin || !this.$accessor.isLocked('file_transfer'))
)
}
get tab() {
return this.$accessor.client.tab
}
@Watch('tab', { immediate: true })
@Watch('filetransferAllowed', { immediate: true })
onTabChange() {
// do not show the files tab if file transfer is disabled
if (this.tab === 'files' && !this.filetransferAllowed) {
this.change('chat')
}
}
@Watch('filetransferAllowed')
onFileTransferAllowedChange() {
if (this.filetransferAllowed) {
this.$accessor.files.refresh()
}
}
change(tab: string) {
this.$accessor.client.setTab(tab)
}

View File

@ -68,7 +68,7 @@
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { Component, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-unsupported' })
export default class extends Vue {}

View File

@ -194,7 +194,7 @@
<script lang="ts">
import { Component, Ref, Watch, Vue, Prop } from 'vue-property-decorator'
import ResizeObserver from 'resize-observer-polyfill'
import { elementRequestFullscreen, onFullscreenChange, isFullscreen } from '~/utils'
import { elementRequestFullscreen, onFullscreenChange, isFullscreen, lockKeyboard, unlockKeyboard } from '~/utils'
import Emote from './emote.vue'
import Resolution from './resolution.vue'
@ -339,12 +339,12 @@
}
@Watch('width')
onWidthChanged(width: number) {
onWidthChanged() {
this.onResize()
}
@Watch('height')
onHeightChanged(height: number) {
onHeightChanged() {
this.onResize()
}
@ -417,6 +417,7 @@
onFullscreenChange(this._player, () => {
this.fullscreen = isFullscreen()
this.fullscreen ? lockKeyboard() : unlockKeyboard()
this.onResize()
})
@ -443,7 +444,7 @@
this.$accessor.video.setPlayable(false)
})
this._video.addEventListener('volumechange', (event) => {
this._video.addEventListener('volumechange', () => {
this.$accessor.video.setMuted(this._video.muted)
this.$accessor.video.setVolume(this._video.volume * 100)
})

View File

@ -38,9 +38,9 @@ const exportMixin = {
$accessor() {
return neko
},
$client () {
$client() {
return window.$client
}
},
},
}
@ -52,15 +52,8 @@ const plugini18n: PluginObject<undefined> = {
},
}
function extend (component: any) {
return component
.use(plugini18n)
.use(Logger)
.use(Axios)
.use(Swal)
.use(Anime)
.use(Client)
.extend(exportMixin)
function extend(component: any) {
return component.use(plugini18n).use(Logger).use(Axios).use(Swal).use(Anime).use(Client).extend(exportMixin)
}
export const NekoConnect = extend(Connect)

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Sende eine Nachricht'
export const side = {
chat: 'Chat',
files: 'Dateien',
settings: 'Einstellungen',
}
@ -68,6 +69,14 @@ export const locks = {
notif_locked: 'Raum gesperrt',
notif_unlocked: 'Raum entsperrt',
},
file_transfer: {
lock: 'Dateiübertragung sperren (für Nutzer)',
unlock: 'Dateiübertragung entsperren (für Nutzer)',
locked: 'Dateiübertragung gesperrt (für Nutzer)',
unlocked: 'Dateiübertragung entsperrt (für Nutzer)',
notif_locked: 'Dateiübertragung gesperrt',
notif_unlocked: 'Dateiübertragung entsperrt',
},
}
export const setting = {
@ -108,3 +117,9 @@ export const notifications = {
muted: '{name} stummgeschaltet',
unmuted: '{name} stummschaltung aufgehoben',
}
export const files = {
downloads: 'Herunterladen',
uploads: 'Hochladen',
upload_here: 'Klicken oder ziehen Sie Dateien zum Hochladen hierher',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Send a message'
export const side = {
chat: 'Chat',
files: 'Files',
settings: 'Settings',
}
@ -70,6 +71,14 @@ export const locks = {
notif_locked: 'locked the room',
notif_unlocked: 'unlocked the room',
},
file_transfer: {
lock: 'Lock File Transfer (for users)',
unlock: 'Unlock File Transfer (for users)',
locked: 'File Transfer Locked (for users)',
unlocked: 'File Transfer Unlocked (for users)',
notif_locked: 'locked file transfer',
notif_unlocked: 'unlocked file transfer',
},
}
export const setting = {
@ -110,3 +119,9 @@ export const notifications = {
muted: 'muted {name}',
unmuted: 'unmuted {name}',
}
export const files = {
downloads: 'Downloads',
uploads: 'Uploads',
upload_here: 'Click or drag files here to upload',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Enviar un mensaje'
export const side = {
chat: 'Chat',
files: 'Archivos',
settings: 'Configuración',
}
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'bloqueó la sala',
notif_unlocked: 'desbloqueó la sala',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: '{name} silenciado',
unmuted: '{name} no silenciado',
}
export const files = {
downloads: 'Descargas',
uploads: 'Cargar',
upload_here: 'Haga clic o arrastre los archivos aquí para cargarlos',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Lähetä viesti'
export const side = {
chat: 'Chatti',
files: 'Tiedostot',
settings: 'Asetukset',
}
@ -70,6 +71,15 @@ export const locks = {
notif_locked: 'lukittu huone',
notif_unlocked: 'vapautettu huone',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -110,3 +120,9 @@ export const notifications = {
muted: 'mykistetty {name}',
unmuted: 'poistettu mykistys {name}',
}
export const files = {
downloads: 'Lataukset',
uploads: 'Lataa',
upload_here: 'Klikkaa tai vedä tiedostoja tähän ladataksesi',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Envoyer un message'
export const side = {
chat: 'Chat',
files: 'Fichiers',
settings: 'Paramètres',
}
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'a vérouillé la salle',
notif_unlocked: 'a dévérouillé la salle',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: 'a mute {name}',
unmuted: 'a démute {name}',
}
export const files = {
downloads: 'Téléchargements',
uploads: 'Télécharger',
upload_here: 'Cliquez ou faites glisser les fichiers ici pour les télécharger',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = '메세지 보내기'
export const side = {
chat: '채팅',
files: '파일',
settings: '설정',
}
@ -68,6 +69,15 @@ export const locks = {
notif_locked: '방이 잠겼습니다',
notif_unlocked: '방 잠금이 해제됐습니다',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -108,3 +118,9 @@ export const notifications = {
muted: '{name} 님이 뮤트됐습니다',
unmuted: '{name} 님의 뮤트가 해제됐습니다',
}
export const files = {
downloads: '다운로드',
uploads: '업로드',
upload_here: '업로드할 파일을 여기로 클릭하거나 드래그하세요.',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Send en melding'
export const side = {
chat: 'Sludring',
files: 'Filer',
settings: 'Innstillinger',
}
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'låste rommet',
notif_unlocked: 'låste opp rommet',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: 'forstummet {name}',
unmuted: 'opphevet forstummingen av {name}',
}
export const files = {
downloads: 'Overførsler',
uploads: 'Overfør',
upload_here: 'Klik eller træk filer her for at uploade',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = 'Отправить сообщение'
export const side = {
chat: 'Чат',
files: 'Файлы',
settings: 'Настройки',
}
@ -70,6 +71,15 @@ export const locks = {
notif_locked: 'комната закрыта',
notif_unlocked: 'комната открыта',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -110,3 +120,9 @@ export const notifications = {
muted: 'заглушен {name}',
unmuted: 'не заглушен {name}',
}
export const files = {
downloads: 'Загрузки',
uploads: 'Загрузить',
upload_here: 'Нажмите или перетащите сюда файлы для загрузки',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Odoslať správu'
export const side = {
chat: 'Chat',
files: 'Súbory',
settings: 'Nastavenia',
}
@ -73,6 +74,15 @@ export const locks = {
notif_locked: 'miestnosť bola zamknutá',
notif_unlocked: 'miestnosť bola odomknutá',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -113,3 +123,9 @@ export const notifications = {
muted: 'zakázal chat používateľovi {name}',
unmuted: 'povolil chat používateľovi {name}',
}
export const files = {
downloads: 'Stiahnutia',
uploads: 'Nahrávanie',
upload_here: 'Kliknutím alebo pretiahnutím súborov sem ich môžete nahrať',
}

View File

@ -8,6 +8,7 @@ export const send_a_message = 'Skicka ett meddelande'
export const side = {
chat: 'Chatt',
files: 'Filer',
settings: 'Inställningar',
}
@ -74,6 +75,15 @@ export const locks = {
notif_locked: 'låste rummet',
notif_unlocked: 'låste upp rummet',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -117,3 +127,9 @@ export const notifications = {
muted: 'tystade {name}',
unmuted: 'tog bort tystningen på {name}',
}
export const files = {
downloads: 'Nedladdningar',
uploads: 'Ladda upp',
upload_here: 'Klicka eller dra filer hit för att ladda upp dem',
}

View File

@ -7,6 +7,7 @@ export const send_a_message = '发送消息'
export const side = {
chat: '聊天',
files: '文件',
settings: '设置',
}
@ -70,6 +71,15 @@ export const locks = {
notif_locked: '锁上房间',
notif_unlocked: '解锁房间',
},
// TODO
//file_transfer: {
// lock: 'Lock File Transfer (for users)',
// unlock: 'Unlock File Transfer (for users)',
// locked: 'File Transfer Locked (for users)',
// unlocked: 'File Transfer Unlocked (for users)',
// notif_locked: 'locked file transfer',
// notif_unlocked: 'unlocked file transfer',
//},
}
export const setting = {
@ -110,3 +120,9 @@ export const notifications = {
muted: '鸟粪 {name}',
unmuted: '取消静音 {name}',
}
export const files = {
downloads: '下载',
uploads: '上传',
upload_here: '点击或拖动文件到这里来上传',
}

View File

@ -66,8 +66,8 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._ws = new WebSocket(`${url}?password=${encodeURIComponent(password)}`)
this.emit('debug', `connecting to ${this._ws.url}`)
this._ws.onmessage = this.onMessage.bind(this)
this._ws.onerror = (event) => this.onError.bind(this)
this._ws.onclose = (event) => this.onDisconnected.bind(this, new Error('websocket closed'))
this._ws.onerror = () => this.onError.bind(this)
this._ws.onclose = () => this.onDisconnected.bind(this, new Error('websocket closed'))
this._timeout = window.setTimeout(this.onTimeout.bind(this), 15000)
} catch (err: any) {
this.onDisconnected(err)
@ -210,15 +210,15 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
})
}
this._peer.onconnectionstatechange = (event) => {
this._peer.onconnectionstatechange = () => {
this.emit('debug', `peer connection state changed`, this._peer ? this._peer.connectionState : undefined)
}
this._peer.onsignalingstatechange = (event) => {
this._peer.onsignalingstatechange = () => {
this.emit('debug', `peer signaling state changed`, this._peer ? this._peer.signalingState : undefined)
}
this._peer.oniceconnectionstatechange = (event) => {
this._peer.oniceconnectionstatechange = () => {
this._state = this._peer!.iceConnectionState
this.emit('debug', `peer ice connection state changed: ${this._peer!.iceConnectionState}`)
@ -286,6 +286,10 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
try {
const d = await this._peer.createAnswer()
// add stereo=1 to answer sdp to enable stereo audio for chromium
d.sdp = d.sdp?.replace(/(stereo=1;)?useinbandfec=1/, 'useinbandfec=1;stereo=1')
this._peer!.setLocalDescription(d)
this._ws!.send(
@ -309,6 +313,22 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._peer.setRemoteDescription({ type: 'answer', sdp })
}
public addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
if (!this._peer) {
throw new Error('peer not connected')
}
return this._peer.addTrack(track, ...streams)
}
public removeTrack(sender: RTCRtpSender) {
if (!this._peer) {
throw new Error('peer not connected')
}
this._peer.removeTrack(sender)
}
private async onMessage(e: MessageEvent) {
const { event, ...payload } = JSON.parse(e.data) as WebSocketMessages

View File

@ -38,6 +38,10 @@ export const EVENT = {
MESSAGE: 'chat/message',
EMOTE: 'chat/emote',
},
FILETRANSFER: {
LIST: 'filetransfer/list',
REFRESH: 'filetransfer/refresh',
},
SCREEN: {
CONFIGURATIONS: 'screen/configurations',
RESOLUTION: 'screen/resolution',
@ -69,6 +73,7 @@ export type WebSocketEvents =
| MemberEvents
| SignalEvents
| ChatEvents
| FileTransferEvents
| ScreenEvents
| BroadcastEvents
| AdminEvents
@ -91,6 +96,9 @@ export type SignalEvents =
| typeof EVENT.SIGNAL.CANDIDATE
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
export type FileTransferEvents = typeof EVENT.FILETRANSFER.LIST | typeof EVENT.FILETRANSFER.REFRESH
export type ScreenEvents = typeof EVENT.SCREEN.CONFIGURATIONS | typeof EVENT.SCREEN.RESOLUTION | typeof EVENT.SCREEN.SET
export type BroadcastEvents =

View File

@ -7,7 +7,6 @@ import { accessor } from '~/store'
import {
SystemMessagePayload,
SignalProvidePayload,
MemberListPayload,
MemberDisconnectPayload,
MemberPayload,
@ -19,11 +18,11 @@ import {
ScreenConfigurationsPayload,
ScreenResolutionPayload,
BroadcastStatusPayload,
AdminPayload,
AdminTargetPayload,
AdminLockMessage,
SystemInitPayload,
AdminLockResource,
FileTransferListPayload,
} from './messages'
interface NekoEvents extends BaseEvents {}
@ -46,6 +45,8 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$vue = vue
this.$accessor = vue.$accessor
this.url = url
// convert ws url to http url
this.$vue.$http.defaults.baseURL = url.replace(/^ws/, 'http').replace(/\/ws$/, '')
}
private cleanup() {
@ -128,13 +129,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$accessor.video.setStream(0)
}
protected [EVENT.DATA](data: any) {}
protected [EVENT.DATA]() {}
/////////////////////////////
// System Events
/////////////////////////////
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks }: SystemInitPayload) {
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) {
this.$accessor.remote.setImplicitHosting(implicit_hosting)
this.$accessor.remote.setFileTransfer(file_transfer)
for (const resource in locks) {
this[EVENT.ADMIN.LOCK]({
@ -351,6 +353,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$accessor.chat.newEmote({ type: emote })
}
/////////////////////////////
// File Transfer Events
/////////////////////////////
protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
this.$accessor.files.setCwd(cwd)
this.$accessor.files.setFileList(files)
}
/////////////////////////////
// Screen Events
/////////////////////////////

View File

@ -8,8 +8,9 @@ import {
ChatEvents,
ScreenEvents,
AdminEvents,
FileTransferEvents,
} from './events'
import { Member, ScreenConfigurations, ScreenResolution } from './types'
import { FileListItem, Member, ScreenConfigurations, ScreenResolution } from './types'
export type WebSocketMessages =
| WebSocketMessage
@ -59,6 +60,7 @@ export interface SystemInit extends WebSocketMessage, SystemInitPayload {
export interface SystemInitPayload {
implicit_hosting: boolean
locks: Record<string, string>
file_transfer: boolean
}
// system/disconnect
@ -192,6 +194,18 @@ export interface EmojiSendPayload {
emote: string
}
/*
FILE TRANSFER PAYLOADS
*/
export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
event: FileTransferEvents
}
export interface FileTransferListPayload {
cwd: string
files: FileListItem[]
}
/*
SCREEN PAYLOADS
*/
@ -215,11 +229,11 @@ export interface ScreenConfigurationsPayload {
BROADCAST PAYLOADS
*/
export interface BroadcastCreatePayload {
url: string
url: string
}
export interface BroadcastStatusPayload {
url: string
url: string
isActive: boolean
}
@ -248,7 +262,7 @@ export interface AdminLockMessage extends WebSocketMessage, AdminLockPayload {
id: string
}
export type AdminLockResource = 'login' | 'control'
export type AdminLockResource = 'login' | 'control' | 'file_transfer'
export interface AdminLockPayload {
resource: AdminLockResource

View File

@ -22,3 +22,20 @@ export interface ScreenResolution {
height: number
rate: number
}
export interface FileListItem {
name: string
type: 'file' | 'dir'
size: number
}
export interface FileTransfer {
id: number
name: string
direction: 'upload' | 'download'
size: number
progress: number
status: 'pending' | 'inprogress' | 'completed' | 'failed'
error?: string
abortController?: AbortController
}

View File

@ -10,7 +10,7 @@ declare module 'vue/types/vue' {
$swal: VueSwalInstance
}
interface VueConstructor<V extends Vue = Vue> {
interface VueConstructor {
swal: VueSwalInstance
}
}

View File

@ -73,7 +73,7 @@ export const actions = actionTree(
accessor.chat.addEmote({ id, emote })
},
newMessage({ state }, message: Message) {
newMessage(store, message: Message) {
if (accessor.settings.chat_sound) {
new Audio('chat.mp3').play().catch(console.error)
}

72
client/src/store/files.ts Normal file
View File

@ -0,0 +1,72 @@
import { actionTree, getterTree, mutationTree } from 'typed-vuex'
import { FileListItem, FileTransfer } from '~/neko/types'
import { EVENT } from '~/neko/events'
import { accessor } from '~/store'
export const state = () => ({
cwd: '',
files: [] as FileListItem[],
transfers: [] as FileTransfer[],
})
export const getters = getterTree(state, {
//
})
export const mutations = mutationTree(state, {
_setCwd(state, cwd: string) {
state.cwd = cwd
},
_setFileList(state, files: FileListItem[]) {
state.files = files
},
_addTransfer(state, transfer: FileTransfer) {
state.transfers = [...state.transfers, transfer]
},
_removeTransfer(state, transfer: FileTransfer) {
state.transfers = state.transfers.filter((t) => t.id !== transfer.id)
},
})
export const actions = actionTree(
{ state, getters, mutations },
{
setCwd(store, cwd: string) {
accessor.files._setCwd(cwd)
},
setFileList(store, files: FileListItem[]) {
accessor.files._setFileList(files)
},
addTransfer(store, transfer: FileTransfer) {
if (transfer.status !== 'pending') {
return
}
accessor.files._addTransfer(transfer)
},
removeTransfer(store, transfer: FileTransfer) {
accessor.files._removeTransfer(transfer)
},
cancelAllTransfers() {
for (const t of accessor.files.transfers) {
if (t.status !== 'completed') {
t.abortController?.abort()
}
accessor.files.removeTransfer(t)
}
},
refresh() {
if (!accessor.connected) {
return
}
$client.sendMessage(EVENT.FILETRANSFER.REFRESH)
},
},
)

View File

@ -1,12 +1,13 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { useAccessor, mutationTree, actionTree } from 'typed-vuex'
import { useAccessor, mutationTree, getterTree, actionTree } from 'typed-vuex'
import { EVENT } from '~/neko/events'
import { AdminLockResource } from '~/neko/messages'
import { get, set } from '~/utils/localstorage'
import * as video from './video'
import * as chat from './chat'
import * as files from './files'
import * as remote from './remote'
import * as user from './user'
import * as settings from './settings'
@ -55,10 +56,14 @@ export const mutations = mutationTree(state, {
},
})
export const getters = getterTree(state, {
isLocked: (state) => (resource: AdminLockResource) => resource in state.locked && state.locked[resource],
})
export const actions = actionTree(
{ state, mutations },
{ state, getters, mutations },
{
initialise(store) {
initialise() {
accessor.emoji.initialise()
accessor.settings.initialise()
},
@ -79,12 +84,20 @@ export const actions = actionTree(
$client.sendMessage(EVENT.ADMIN.UNLOCK, { resource })
},
login({ state }, { displayname, password }: { displayname: string; password: string }) {
toggleLock(_, resource: AdminLockResource) {
if (accessor.isLocked(resource)) {
accessor.unlock(resource)
} else {
accessor.lock(resource)
}
},
login(store, { displayname, password }: { displayname: string; password: string }) {
accessor.setLogin({ displayname, password })
$client.login(password, displayname)
},
logout({ state }) {
logout() {
accessor.setLogin({ displayname: '', password: '' })
set('displayname', '')
set('password', '')
@ -97,7 +110,8 @@ export const storePattern = {
state,
mutations,
actions,
modules: { video, chat, user, remote, settings, client, emoji },
getters,
modules: { video, chat, files, user, remote, settings, client, emoji },
}
Vue.use(Vuex)

View File

@ -13,6 +13,7 @@ export const state = () => ({
clipboard: '',
locked: false,
implicitHosting: true,
fileTransfer: true,
keyboardModifierState: -1,
})
@ -20,7 +21,7 @@ export const getters = getterTree(state, {
hosting: (state, getters, root) => {
return root.user.id === state.id || state.implicitHosting
},
hosted: (state, getters, root) => {
hosted: (state) => {
return state.id !== '' || state.implicitHosting
},
host: (state, getters, root) => {
@ -53,6 +54,10 @@ export const mutations = mutationTree(state, {
state.implicitHosting = val
},
setFileTransfer(state, val: boolean) {
state.fileTransfer = val
},
reset(state) {
state.id = ''
state.clipboard = ''
@ -131,7 +136,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.ADMIN.RELEASE)
},
adminGive({ getters }, member: string | Member) {
adminGive(store, member: string | Member) {
if (!accessor.connected) {
return
}
@ -155,7 +160,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.CONTROL.KEYBOARD, { layout: accessor.settings.keyboard_layout })
},
syncKeyboardModifierState({ state, getters }, { capsLock, numLock, scrollLock }) {
syncKeyboardModifierState({ state }, { capsLock, numLock, scrollLock }) {
if (state.keyboardModifierState === keyboardModifierState(capsLock, numLock, scrollLock)) {
return
}

View File

@ -79,13 +79,13 @@ export const actions = actionTree(
}
},
broadcastStatus({ getters }, { url, isActive }) {
broadcastStatus(store, { url, isActive }) {
accessor.settings.setBroadcastStatus({ url, isActive })
},
broadcastCreate({ getters }, url: string) {
broadcastCreate(store, url: string) {
$client.sendMessage(EVENT.BROADCAST.CREATE, { url })
},
broadcastDestroy({ getters }) {
broadcastDestroy() {
$client.sendMessage(EVENT.BROADCAST.DESTROY)
},
},

View File

@ -169,7 +169,7 @@ export const mutations = mutationTree(state, {
export const actions = actionTree(
{ state, getters, mutations },
{
screenConfiguations({ state }) {
screenConfiguations() {
if (!accessor.connected || !accessor.user.admin) {
return
}
@ -177,7 +177,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.SCREEN.CONFIGURATIONS)
},
screenGet({ state }) {
screenGet() {
if (!accessor.connected) {
return
}
@ -185,7 +185,7 @@ export const actions = actionTree(
$client.sendMessage(EVENT.SCREEN.RESOLUTION)
},
screenSet({ state }, resolution: ScreenResolution) {
screenSet(store, resolution: ScreenResolution) {
if (!accessor.connected || !accessor.user.admin) {
return
}

View File

@ -0,0 +1,15 @@
// navigator.keyboard.d.ts
// Type declarations for Keyboard API
// https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API
interface Keyboard {
lock(keyCodes?: array<string>): Promise<void>
unlock(): void
}
interface NavigatorKeyboard {
// Only available in a secure context.
readonly keyboard?: Keyboard
}
interface Navigator extends NavigatorKeyboard {}

View File

@ -8,6 +8,18 @@ export function makeid(length: number) {
return result
}
export function lockKeyboard() {
if (navigator && navigator.keyboard) {
navigator.keyboard.lock()
}
}
export function unlockKeyboard() {
if (navigator && navigator.keyboard) {
navigator.keyboard.unlock()
}
}
export function elementRequestFullscreen(el: HTMLElement) {
if (typeof el.requestFullscreen === 'function') {
el.requestFullscreen()

View File

@ -22,6 +22,6 @@ module.exports = {
},
},
devServer: {
disableHostCheck: true,
}
allowedHosts: 'all',
},
}

View File

@ -1,26 +1,41 @@
<div align="center">
<a href="https://neko.m1k1o.net/#/" ><img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="450" height="auto"/></a>
<a href="https://neko.m1k1o.net/#/" ><img src="./_media/logo.png" width="450" height="auto"/></a>
<br/>
<br/>
<img src="https://i.imgur.com/ZSzbQr7.gif" width="650" height="auto"/>
<img src="./_media/intro.gif" width="650" height="auto"/>
With `NEKO_FILE_TRANSFER_ENABLED=true`:
<img src="./_media/file-transfer.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.
Welcome to Neko, a self-hosted virtual browser that runs in Docker and uses WebRTC technology. Neko is a powerful tool that allows you to **run a fully-functional browser in a virtual environment**, giving you the ability to **access the internet securely and privately from anywhere**. With Neko, you can browse the web, **run applications**, and perform other tasks just as you would on a regular browser, all within a **secure and isolated environment**. Whether you are a developer looking to test web applications, a **privacy-conscious user seeking a secure browsing experience**, or simply someone who wants to take advantage of the **convenience and flexibility of a virtual browser**, Neko is the perfect solution.
In addition to its security and privacy features, Neko offers the **ability for multiple users to access it simultaneously**. This makes it an ideal solution for teams or organizations that need to share access to a browser, as well as for individuals who want to use **multiple devices to access the same virtual environment**. With Neko, you can **easily and securely share access to a browser with others**, without having to worry about maintaining separate configurations or settings. Whether you need to **collaborate on a project**, access shared resources, or simply want to **share access to a browser with friends or family**, Neko makes it easy to do so.
Neko is also a great tool for **hosting watch parties** and interactive presentations. With its virtual browser capabilities, Neko allows you to host watch parties and presentations that are **accessible from anywhere**, without the need for in-person gatherings. This makes it easy to **stay connected with friends and colleagues**, even when you are unable to meet in person. With Neko, you can easily host a watch party or give an **interactive presentation**, whether it's for leisure or work. Simply invite your guests to join the virtual environment, and you can share the screen and **interact with them in real-time**.
## About
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.
### 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...)
* Bidirectional file transfer (if enabled)
### Why n.eko?

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
docs/_media/intro.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@ -2,16 +2,35 @@
## master branch
### New Features
- Added AV1 tag, metadata and pipeline. Unfortunately does not work yet, since the encoding is way too slow (by @mbattista).
### Bugs
- Fixed TCP mux occasional freeze by adding write buffer to it.
- Fixed stereo problem in chromium-based browsers, where it was only as mono by adding `stereo=1` to opus SDP to clients answer.
- Fixed keysym mapping for unknown keycodes, which was causing some key combinations to not work on some keyboards.
- Fixed a bug where `max_fps=0` would lead to an invalid pipeline.
### Misc
- Updated to go 1.19 and Node 18, removed go-events as dependency (by @mbattista).
- Added adaptive framerate which now streams in the framerate you selected from the dropdown.
## [n.eko v2.7](https://github.com/m1k1o/neko/releases/tag/v2.7)
### 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.
- Added emoji support (by @yesBad).
- Added file transfer (by @prophetofxenu).
### 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.
- Added CORS.
- Opera versions are not hardcoded in Dockerfile anymore but automatically are fetch latest.
## [n.eko v2.6](https://github.com/m1k1o/neko/releases/tag/v2.6)

View File

@ -14,7 +14,7 @@
<img src="../_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
</div>
Use the following docker images:
Use the following docker images from [Docker Hub](https://hub.docker.com/r/m1k1o/neko) for x86_64:
- `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)).
- `m1k1o/neko:google-chrome` - for Google Chrome (needs `--cap-add=SYS_ADMIN`, see the [security implications](https://www.redhat.com/en/blog/container-tidbits-adding-capabilities-container)).
@ -31,12 +31,47 @@ Use the following docker images:
- `m1k1o/neko:xfce` - for a shared desktop / installing shared software.
- `m1k1o/neko:base` - for custom base.
For ARM-based devices (like Raspberry Pi, with GPU hardware acceleration):
- `m1k1o/neko:arm-firefox` - for Firefox.
- `m1k1o/neko:arm-chromium` - for Chromium.
- `m1k1o/neko:arm-base` - for custom arm based.
Dockerhub images are built using GitHub actions on every push and on weekly basis to keep all browsers up-to-date.
Images (except `arm-`) are built using GitHub actions on every push and on weekly basis to keep all browsers up-to-date,
All images are also available on [GitHub Container Registry](https://github.com/m1k1o?tab=packages&repo_name=neko) for faster pulls:
- `ghcr.io/m1k1o/neko/firefox:latest`
- `ghcr.io/m1k1o/neko/chromium:latest`
- `ghcr.io/m1k1o/neko/google-chrome:latest`
- `ghcr.io/m1k1o/neko/ungoogled-chromium:latest`
- `ghcr.io/m1k1o/neko/microsoft-edge:latest`
- `ghcr.io/m1k1o/neko/brave:latest`
- `ghcr.io/m1k1o/neko/vivaldi:latest`
- `ghcr.io/m1k1o/neko/opera:latest`
- `ghcr.io/m1k1o/neko/tor-browser:latest`
- `ghcr.io/m1k1o/neko/remmina:latest`
- `ghcr.io/m1k1o/neko/vlc:latest`
- `ghcr.io/m1k1o/neko/xfce:latest`
For ARM-based images (like Raspberry Pi - with GPU hardware acceleration, Oracle Cloud ARM tier). Currently not all images are available for ARM, because not all applications are available for ARM.
- `ghcr.io/m1k1o/neko/arm-firefox:latest`
- `ghcr.io/m1k1o/neko/arm-chromium:latest`
- `ghcr.io/m1k1o/neko/arm-ungoogled-chromium:latest`
- `ghcr.io/m1k1o/neko/arm-vlc:latest`
- `ghcr.io/m1k1o/neko/arm-xfce:latest`
For images with VAAPI GPU hardware acceleration using intel drivers use:
- `ghcr.io/m1k1o/neko/intel-firefox:latest`
- `ghcr.io/m1k1o/neko/intel-chromium:latest`
- `ghcr.io/m1k1o/neko/intel-google-chrome:latest`
- `ghcr.io/m1k1o/neko/intel-ungoogled-chromium:latest`
- `ghcr.io/m1k1o/neko/intel-microsoft-edge:latest`
- `ghcr.io/m1k1o/neko/intel-brave:latest`
- `ghcr.io/m1k1o/neko/intel-vivaldi:latest`
- `ghcr.io/m1k1o/neko/intel-opera:latest`
- `ghcr.io/m1k1o/neko/intel-tor-browser:latest`
- `ghcr.io/m1k1o/neko/intel-remmina:latest`
- `ghcr.io/m1k1o/neko/intel-vlc:latest`
- `ghcr.io/m1k1o/neko/intel-xfce:latest`
GHCR images are built using GitHub actions for every tag.
### Networking:
- If you want to use n.eko in **external** network, you can omit `NEKO_NAT1TO1`. It will automatically get your Public IP.
@ -78,8 +113,10 @@ services:
```
- When using mux, `NEKO_EPR` is ignored.
- Mux accepts only one port, not a range.
- 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`.
- The same port must be exposed from docker container, you can't map them to different ports. So `8082:8082` is OK, but `"5454:8082` will not work.
- 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.
@ -90,6 +127,31 @@ services:
- For Chromium, copy [this](https://github.com/m1k1o/neko/blob/master/.docker/chromium/policies.json) file, modify and mount it as: ` -v '${PWD}/policies.json:/etc/chromium/policies/managed/policies.json'`
- For others, see where existing `policies.json` is placed in their `Dockerfile`.
#### Allow file uploading & downloading
- From security perespective, browser is not enabled to access local file data.
- If you want to enable this, you need to modify following policies:
```json
"DownloadRestrictions": 0,
"AllowFileSelectionDialogs": true,
"URLAllowlist": [
"file:///home/neko/Downloads"
],
```
### Want to preserve browser data between restarts?
- You need to mount browser profile as volume.
- For Firefox, that is this `/home/neko/.mozilla/firefox/profile.default` folder, mount it as: ` -v '${PWD}/data:/home/neko/.mozilla/firefox/profile.default'`
- For Chromium, that is this `/home/neko/.config/chromium` folder, mount it as: ` -v '${PWD}/data:/home/neko/.config/chromium'`
- For other chromium based browsers, see in `supervisord.conf` folder that is specified in `--user-data-dir`.
#### Allow persistent data in policies
- From security perespective, browser is set up to forget all cookies and brwosing history when its closed.
- If you want to enable this, you need to modify following policies:
```json
"DefaultCookiesSetting": 1,
"RestoreOnStartup": 1,
```
### Want to use VPN for your n.eko browsing?
- Check this out: https://github.com/m1k1o/neko-vpn

View File

@ -32,6 +32,7 @@ nat1to1: <ip>
- Currently supported:
- `control`
- `login`
- `file_transfer`
- e.g. `control`
### WebRTC
@ -125,6 +126,19 @@ nat1to1: <ip>
#### `NEKO_PATH_PREFIX`:
- Path prefix for HTTP requests.
- e.g. `/neko/`
#### `NEKO_CORS`:
- Cross origin request sharing, whitespace separated list of allowed hosts, `*` for all.
- e.g. `127.0.0.1 neko.example.com`
### File Transfer
#### `NEKO_FILE_TRANSFER_ENABLED`:
- Enable file transfer feature.
- e.g. `true`
#### `NEKO_FILE_TRANSFER_PATH`:
- Path where files will be transferred between the host and users. By default this is
`/home/neko/Downloads`. If the path doesn't exist, it will be created.
- e.g. `/home/neko/Desktop`
### Expert settings
@ -152,9 +166,12 @@ Flags:
--broadcast_url string URL for broadcasting, setting this value will automatically enable broadcasting
--cert string path to the SSL cert used to secure the neko server
--control_protection control protection means, users can gain control only if at least one admin is in the room
--cors strings list of allowed origins for CORS (default [*])
--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")
--file_transfer_enabled enable file transfer feature (default false)
--file_transfer_path string path to use for file transfer (default "/home/neko/Downloads")
--g722 DEPRECATED: use audio_codec
--h264 DEPRECATED: use video_codec
-h, --help help for serve

View File

@ -1,5 +1,7 @@
# Quick Start
Neko is easy to use and requires no technical expertise to get started. All you need to do is download the Docker image and you're ready to go:
1. Deploy a server or VPS.
**Recommended Specs:**

View File

@ -4,6 +4,10 @@ Neko UI loads but you don't see the screen and it gives you `connection timeout`
## Test your client
Some browser may block WebRTC access by default. You can check if it is enabled by going to `about:webrtc` or `chrome://webrtc-internals` in your browser.
Check if your extensions are not blocking WebRTC access. For example, Privacy Badger or Private Internet Access blocks WebRTC by default.
Test whether your client [supports](https://www.webrtc-experiment.com/DetectRTC/) and can [connect to WebRTC](https://www.webcasts.com/webrtc/).
## Networking
@ -113,6 +117,25 @@ services:
If you want to use n.eko only locally, you must put here your local IP address, otherwise public address will be used.
### Neko works externally, but not locally
You are probabbly missing NAT Loopback (NAT Hairpinning) setting on your router.
Example for pfsense with truecharts docker container:
- First, port forward the relevant ports 8080 and 52000-52100/udp for the container.
- Then turn on `Pure NAT` pfsense (under system > advanced > firewall and nat).
- Make sure to check the two boxes so it works.
- Make sure `NEKO_NAT1TO1` is blank and `NEKO_IPFETCH` address is working correclty (if unset default value is chosen).
- Test externally to confirm it works.
- Internally you have to access it using `<your-public-ip>:port`
### Neko works locally, but not externally
Make sure, that you are exposing your ports correctly.
If you put local ip as `NEKO_NAT1TO1`, external clients try to connect to that ip. But it is unreachable for them, because it is your local IP. You must use your public IP address with port forwarding.
## Debug mode
To see verbose information from n.eko server, you can enable debug mode using `NEKO_DEBUG`.

View File

@ -1,57 +1,55 @@
module m1k1o/neko
go 1.18
go 1.19
require (
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fsnotify/fsnotify v1.6.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.2.1
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/ice/v2 v2.2.13
github.com/pion/interceptor v0.1.12
github.com/pion/logging v0.2.2
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/pion/srtp/v2 v2.0.11 // indirect
github.com/pion/webrtc/v3 v3.1.50
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.27.0
github.com/rs/zerolog v1.28.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
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
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.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // 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/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pion/datachannel v1.5.5 // 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.10 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/sdp/v3 v3.0.5 // indirect
github.com/pion/sctp v1.8.6 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect
github.com/pion/stun v0.3.5 // 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/rogpeppe/go-internal v1.8.1 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/pion/transport v0.14.1 // indirect
github.com/pion/turn/v2 v2.0.9 // indirect
github.com/pion/udp v0.1.2 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // 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
github.com/subosito/gotenv v1.4.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -48,6 +48,7 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@ -60,10 +61,12 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
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.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
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=
@ -108,7 +111,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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=
@ -138,26 +141,30 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
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/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/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/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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=
@ -169,18 +176,15 @@ 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/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.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
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/ice/v2 v2.2.12/go.mod h1:z2KXVFyRkmjetRlaVRgjO9U3ShKwzhlUylvxKfHfd5A=
github.com/pion/ice/v2 v2.2.13 h1:NvLtzwcyob6wXgFqLmVQbGB3s9zzWmOegNMKYig5l9M=
github.com/pion/ice/v2 v2.2.13/go.mod h1:eFO4/1zCI+a3OFVt7l7kP+5jWCuZo8FwU2UwEa3+164=
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=
@ -195,26 +199,29 @@ 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.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/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/srtp/v2 v2.0.11 h1:6cEEgT1oCLWgE+BynbfaSMAxtsqU0M096x9dNH6olY0=
github.com/pion/srtp/v2 v2.0.11/go.mod h1:vzHprzbuVoYJ9NfaRMycnFrkHcLSaLVuBZDOtFQNZjY=
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/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
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/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/turn/v2 v2.0.9 h1:jcDPw0Vfd5I4iTc7s0Upfc2aMnyu2lgJ9vV0SUrNC1o=
github.com/pion/turn/v2 v2.0.9/go.mod h1:DQlwUwx7hL8Xya6TTAabbd9DdKXTNR96Xf5g5Qqso/M=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
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/pion/udp v0.1.2 h1:Bl1ifOcoVYg9gnk1+9yyTX8XgAUORiDvM7UqBb3skhg=
github.com/pion/udp v0.1.2/go.mod h1:CuqU2J4MmF3sjqKfk1SaIhuNXdum5PJRqd2LHuLMQSk=
github.com/pion/webrtc/v3 v3.1.50 h1:wLMo1+re4WMZ9Kun9qcGcY+XoHkE3i0CXrrc0sjhVCk=
github.com/pion/webrtc/v3 v3.1.50/go.mod h1:y9n09weIXB+sjb9mi0GBBewNxo4TKUQm5qdtT5v3/X4=
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=
@ -223,40 +230,44 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
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/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/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/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
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.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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=
@ -269,12 +280,12 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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-20210921155107-089bfa567519/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-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/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
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=
@ -308,6 +319,7 @@ 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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=
@ -346,13 +358,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
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-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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
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=
@ -372,6 +385,7 @@ 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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -415,14 +429,22 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20211216021012-1d35b9e2eb4e/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/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/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/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -430,8 +452,11 @@ 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.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=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -483,6 +508,7 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
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=
@ -584,13 +610,12 @@ 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.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -51,6 +51,7 @@ func CreatePipeline(pipelineStr string) (*Pipeline, error) {
if gstError != nil {
defer C.g_error_free(gstError)
fmt.Printf("(pipeline error) %s", C.GoString(gstError.message))
return nil, fmt.Errorf("(pipeline error) %s", C.GoString(gstError.message))
}
@ -60,19 +61,20 @@ func CreatePipeline(pipelineStr string) (*Pipeline, error) {
Str("module", "capture").
Str("submodule", "gstreamer").
Int("pipeline_id", int(id)).Logger(),
Src: pipelineStr,
Ctx: ctx,
Sample: make(chan types.Sample),
Src: pipelineStr,
Ctx: ctx,
}
pipelines[p.id] = p
return p, nil
}
func (p *Pipeline) AttachAppsink(sinkName string) {
func (p *Pipeline) AttachAppsink(sinkName string, sampleChannel chan types.Sample) {
sinkNameUnsafe := C.CString(sinkName)
defer C.free(unsafe.Pointer(sinkNameUnsafe))
p.Sample = sampleChannel
C.gstreamer_pipeline_attach_appsink(p.Ctx, sinkNameUnsafe)
}
@ -98,7 +100,6 @@ func (p *Pipeline) Destroy() {
delete(pipelines, p.id)
pipelinesLock.Unlock()
close(p.Sample)
C.free(unsafe.Pointer(p.Ctx))
p = nil
}
@ -176,8 +177,9 @@ func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.i
if ok {
pipeline.Sample <- types.Sample{
Data: C.GoBytes(buffer, bufferLen),
Duration: time.Duration(duration),
Data: C.GoBytes(buffer, bufferLen),
Timestamp: time.Now(),
Duration: time.Duration(duration),
}
} else {
log.Warn().

View File

@ -2,12 +2,14 @@ package capture
import (
"errors"
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"m1k1o/neko/internal/config"
"m1k1o/neko/internal/types"
"m1k1o/neko/internal/types/codec"
)
type CaptureManagerCtx struct {
@ -18,6 +20,9 @@ type CaptureManagerCtx struct {
broadcast *BroacastManagerCtx
audio *StreamSinkManagerCtx
video *StreamSinkManagerCtx
// source-sinks
screenshare *StreamSrcSinkManagerCtx
}
func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx {
@ -35,8 +40,23 @@ func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCt
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)
// use screen fps as default
fps := desktop.GetScreenSize().Rate
// if max fps is set, cap it to that value
if config.VideoMaxFPS > 0 && config.VideoMaxFPS < fps {
fps = config.VideoMaxFPS
}
return NewVideoPipeline(config.VideoCodec, config.Display, config.VideoPipeline, fps, config.VideoBitrate, config.VideoHWEnc)
}, "video"),
// source-sinks
screenshare: streamSrcSinkNew(config.ScreenshareEnabled, map[string]string{
codec.VP8().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " +
fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=VP8-DRAFT-IETF-01 ", codec.VP8().PayloadType) +
"! rtpvp8depay " +
"! appsink name=appsink",
// TODO: Add support for more codecs.
}, "webcam"),
}
}
@ -47,36 +67,49 @@ func (manager *CaptureManagerCtx) Start() {
}
}
manager.desktop.OnBeforeScreenSizeChange(func() {
if manager.video.Started() {
manager.video.destroyPipeline()
}
go func() {
for {
before, ok := <-manager.desktop.GetScreenSizeChangeChannel()
if !ok {
manager.logger.Info().Msg("screen size change channel was closed")
return
}
if manager.broadcast.Started() {
manager.broadcast.destroyPipeline()
}
})
if before {
// before screen size change, we need to destroy all pipelines
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.video.Started() {
manager.video.destroyPipeline()
}
if manager.broadcast.Started() {
manager.broadcast.destroyPipeline()
}
} else {
// after screen size change, we need to recreate all pipelines
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")
}
}
}
}
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.screenshare.shutdown()
manager.broadcast.shutdown()
manager.audio.shutdown()
@ -96,3 +129,7 @@ func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager {
func (manager *CaptureManagerCtx) Video() types.StreamSinkManager {
return manager.video
}
func (manager *CaptureManagerCtx) Screenshare() types.StreamSrcSinkManager {
return manager.screenshare
}

View File

@ -53,7 +53,7 @@ func NewBroadcastPipeline(device string, display string, pipelineSrc string, url
}
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc string) (string, error) {
pipelineStr := " ! appsink name=appsink"
pipelineStr := " ! appsink name=appsinkvideo"
// if using custom pipeline
if pipelineSrc != "" {
@ -61,6 +61,11 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
return pipelineStr, nil
}
// use default fps if not set
if fps == 0 {
fps = 25
}
switch rtpCodec.Name {
case codec.VP8().Name:
if hwenc == "VAAPI" {
@ -106,6 +111,28 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
}
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.AV1().Name:
// https://gstreamer.freedesktop.org/documentation/aom/av1enc.html?gi-language=c
// gstreamer1.0-plugins-bad
// av1enc usage-profile=1
// TODO: check for plugin.
if err := gst.CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
return "", err
}
pipelineStr = strings.Join([]string{
fmt.Sprintf(videoSrc, display, fps),
"av1enc",
fmt.Sprintf("target-bitrate=%d", bitrate*650),
"cpu-used=4",
"end-usage=cbr",
// "usage-profile=realtime",
"undershoot=95",
"keyframe-max-dist=25",
"min-quantizer=4",
"max-quantizer=20",
pipelineStr,
}, " ")
case codec.H264().Name:
if err := gst.CheckPlugins([]string{"ximagesrc"}); err != nil {
return "", err
@ -149,7 +176,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
}
func NewAudioPipeline(rtpCodec codec.RTPCodec, device string, pipelineSrc string, bitrate uint) (string, error) {
pipelineStr := " ! appsink name=appsink"
pipelineStr := " ! appsink name=appsinkaudio"
// if using custom pipeline
if pipelineSrc != "" {

View File

@ -13,9 +13,9 @@ import (
)
type StreamSinkManagerCtx struct {
logger zerolog.Logger
mu sync.Mutex
wg sync.WaitGroup
logger zerolog.Logger
mu sync.Mutex
sampleChannel chan types.Sample
codec codec.RTPCodec
pipeline *gst.Pipeline
@ -24,8 +24,6 @@ type StreamSinkManagerCtx struct {
listeners int
listenersMu sync.Mutex
sampleFn func(sample types.Sample)
}
func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), video_id string) *StreamSinkManagerCtx {
@ -35,9 +33,10 @@ func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), vide
Str("video_id", video_id).Logger()
manager := &StreamSinkManagerCtx{
logger: logger,
codec: codec,
pipelineFn: pipelineFn,
logger: logger,
codec: codec,
pipelineFn: pipelineFn,
sampleChannel: make(chan types.Sample),
}
return manager
@ -47,11 +46,6 @@ 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 {
@ -152,27 +146,14 @@ func (manager *StreamSinkManagerCtx) createPipeline() error {
return err
}
manager.pipeline.AttachAppsink("appsink")
appsinkSubfix := "audio"
if manager.codec.IsVideo() {
appsinkSubfix = "video"
}
manager.pipeline.AttachAppsink("appsink"+appsinkSubfix, manager.sampleChannel)
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
}
@ -188,3 +169,7 @@ func (manager *StreamSinkManagerCtx) destroyPipeline() {
manager.logger.Info().Msgf("destroying pipeline")
manager.pipeline = nil
}
func (manager *StreamSinkManagerCtx) GetSampleChannel() chan types.Sample {
return manager.sampleChannel
}

View File

@ -0,0 +1,137 @@
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 StreamSrcSinkManagerCtx struct {
logger zerolog.Logger
sampleChannel chan types.Sample
enabled bool
codecPipeline map[string]string // codec -> pipeline
codec codec.RTPCodec
pipeline *gst.Pipeline
pipelineMu sync.Mutex
pipelineStr string
}
func streamSrcSinkNew(enabled bool, codecPipeline map[string]string, video_id string) *StreamSrcSinkManagerCtx {
logger := log.With().
Str("module", "capture").
Str("submodule", "stream-src-sink").
Str("video_id", video_id).Logger()
return &StreamSrcSinkManagerCtx{
logger: logger,
enabled: enabled,
codecPipeline: codecPipeline,
sampleChannel: make(chan types.Sample),
}
}
func (manager *StreamSrcSinkManagerCtx) shutdown() {
manager.logger.Info().Msgf("shutdown")
manager.Stop()
}
func (manager *StreamSrcSinkManagerCtx) Codec() codec.RTPCodec {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
return manager.codec
}
func (manager *StreamSrcSinkManagerCtx) Start(codec codec.RTPCodec) error {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline != nil {
return types.ErrCapturePipelineAlreadyExists
}
if !manager.enabled {
return errors.New("stream-src-sink not enabled")
}
found := false
for codecName, pipeline := range manager.codecPipeline {
if codecName == codec.Name {
manager.pipelineStr = pipeline
manager.codec = codec
found = true
break
}
}
if !found {
return errors.New("no pipeline found for a codec")
}
var err error
manager.logger.Info().
Str("codec", manager.codec.Name).
Str("src", manager.pipelineStr).
Msgf("creating pipeline")
manager.pipeline, err = gst.CreatePipeline(manager.pipelineStr)
if err != nil {
return err
}
manager.pipeline.AttachAppsrc("appsrc")
manager.pipeline.AttachAppsink("appsink", manager.sampleChannel)
manager.pipeline.Play()
return nil
}
func (manager *StreamSrcSinkManagerCtx) Stop() {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline == nil {
return
}
manager.pipeline.Destroy()
manager.pipeline = nil
manager.logger.Info().
Str("codec", manager.codec.Name).
Str("src", manager.pipelineStr).
Msgf("destroying pipeline")
}
func (manager *StreamSrcSinkManagerCtx) Push(bytes []byte) {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
if manager.pipeline == nil {
return
}
manager.pipeline.Push(bytes)
}
func (manager *StreamSrcSinkManagerCtx) Started() bool {
manager.pipelineMu.Lock()
defer manager.pipelineMu.Unlock()
return manager.pipeline != nil
}
func (manager *StreamSrcSinkManagerCtx) GetSampleChannel() chan types.Sample {
return manager.sampleChannel
}

View File

@ -27,6 +27,9 @@ type Capture struct {
// broadcast
BroadcastPipeline string
BroadcastUrl string
// screenshare
ScreenshareEnabled bool
}
func (Capture) Init(cmd *cobra.Command) error {
@ -56,6 +59,12 @@ func (Capture) Init(cmd *cobra.Command) error {
return err
}
// DEPRECATED: video codec
cmd.PersistentFlags().Bool("av1", false, "DEPRECATED: use video_codec")
if err := viper.BindPFlag("av1", cmd.PersistentFlags().Lookup("av1")); 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 {
@ -145,6 +154,15 @@ func (Capture) Init(cmd *cobra.Command) error {
return err
}
//
// screenshare
//
cmd.PersistentFlags().Bool("screenshare.enabled", true, "enable screenshare")
if err := viper.BindPFlag("screenshare.enabled", cmd.PersistentFlags().Lookup("screenshare.enabled")); err != nil {
return err
}
return nil
}
@ -173,6 +191,9 @@ func (s *Capture) Set() {
} 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")
} else if viper.GetBool("av1") {
s.VideoCodec = codec.AV1()
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_VIDEO_CODEC=av1' instead")
}
videoHWEnc := ""
@ -221,4 +242,10 @@ func (s *Capture) Set() {
s.BroadcastPipeline = viper.GetString("broadcast_pipeline")
s.BroadcastUrl = viper.GetString("broadcast_url")
//
// screenshare
//
s.ScreenshareEnabled = viper.GetBool("screenshare.enabled")
}

View File

@ -1,10 +1,13 @@
package config
import (
"net/http"
"path"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"m1k1o/neko/internal/utils"
)
type Server struct {
@ -13,6 +16,7 @@ type Server struct {
Bind string
Static string
PathPrefix string
CORS []string
}
func (Server) Init(cmd *cobra.Command) error {
@ -41,6 +45,11 @@ func (Server) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().StringSlice("cors", []string{"*"}, "list of allowed origins for CORS")
if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil {
return err
}
return nil
}
@ -50,4 +59,15 @@ func (s *Server) Set() {
s.Bind = viper.GetString("bind")
s.Static = viper.GetString("static")
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
s.CORS = viper.GetStringSlice("cors")
in, _ := utils.ArrayIn("*", s.CORS)
if len(s.CORS) == 0 || in {
s.CORS = []string{"*"}
}
}
func (s *Server) AllowOrigin(r *http.Request, origin string) bool {
in, _ := utils.ArrayIn(origin, s.CORS)
return in || s.CORS[0] == "*"
}

View File

@ -1,6 +1,8 @@
package config
import (
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -12,6 +14,9 @@ type WebSocket struct {
Locks []string
ControlProtection bool
FileTransferEnabled bool
FileTransferPath string
}
func (WebSocket) Init(cmd *cobra.Command) error {
@ -40,6 +45,18 @@ func (WebSocket) Init(cmd *cobra.Command) error {
return err
}
// File transfer
cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature")
if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil {
return err
}
cmd.PersistentFlags().String("file_transfer_path", "/home/neko/Downloads", "path to use for file transfer")
if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil {
return err
}
return nil
}
@ -50,4 +67,8 @@ func (s *WebSocket) Set() {
s.Locks = viper.GetStringSlice("locks")
s.ControlProtection = viper.GetBool("control_protection")
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
s.FileTransferPath = viper.GetString("file_transfer_path")
s.FileTransferPath = filepath.Clean(s.FileTransferPath)
}

View File

@ -9,7 +9,6 @@ import (
"m1k1o/neko/internal/desktop/xevent"
"m1k1o/neko/internal/desktop/xorg"
"github.com/kataras/go-events"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
@ -20,16 +19,18 @@ type DesktopManagerCtx struct {
logger zerolog.Logger
wg sync.WaitGroup
shutdown chan struct{}
emmiter events.EventEmmiter
config *config.Desktop
screenSizeChangeChannel chan bool
}
func New(config *config.Desktop) *DesktopManagerCtx {
return &DesktopManagerCtx{
logger: log.With().Str("module", "desktop").Logger(),
shutdown: make(chan struct{}),
emmiter: events.New(),
config: config,
screenSizeChangeChannel: make(chan bool),
}
}
@ -47,14 +48,22 @@ func (manager *DesktopManagerCtx) Start() {
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")
})
go func() {
for {
msg, ok := <-xevent.EventErrorChannel
if !ok {
manager.logger.Info().Msg("xevent error channel was closed")
return
}
manager.logger.Warn().
Uint8("error_code", msg.Error_code).
Str("message", msg.Message).
Uint8("request_code", msg.Request_code).
Uint8("minor_code", msg.Minor_code).
Msg("X event error occurred")
}
}()
manager.wg.Add(1)
@ -75,22 +84,15 @@ func (manager *DesktopManagerCtx) Start() {
}()
}
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) GetScreenSizeChangeChannel() chan bool {
return manager.screenSizeChangeChannel
}
func (manager *DesktopManagerCtx) Shutdown() error {
manager.logger.Info().Msgf("desktop shutting down")
close(manager.shutdown)
close(manager.screenSizeChangeChannel)
manager.wg.Wait()
xorg.DisplayClose()

View File

@ -1,33 +1,18 @@
package desktop
import "m1k1o/neko/internal/desktop/xevent"
import (
"m1k1o/neko/internal/desktop/xevent"
"m1k1o/neko/internal/types"
)
func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) {
xevent.Emmiter.On("cursor-changed", func(payload ...any) {
listener(payload[0].(uint64))
})
func (manager *DesktopManagerCtx) GetCursorChangedChannel() chan uint64 {
return xevent.CursorChangedChannel
}
func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) {
xevent.Emmiter.On("clipboard-updated", func(payload ...any) {
listener()
})
func (manager *DesktopManagerCtx) GetClipboardUpdatedChannel() chan struct{} {
return xevent.ClipboardUpdatedChannel
}
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))
})
func (manager *DesktopManagerCtx) GetEventErrorChannel() chan types.DesktopErrorMessage {
return xevent.EventErrorChannel
}

View File

@ -79,45 +79,3 @@ void XEventLoop(char *name) {
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

@ -8,18 +8,26 @@ package xevent
import "C"
import (
"strings"
"time"
"unsafe"
"github.com/kataras/go-events"
"m1k1o/neko/internal/types"
)
var Emmiter events.EventEmmiter
var file_chooser_dialog_window uint32 = 0
var CursorChangedChannel chan uint64
var ClipboardUpdatedChannel chan struct{}
var EventErrorChannel chan types.DesktopErrorMessage
func init() {
Emmiter = events.New()
CursorChangedChannel = make(chan uint64)
ClipboardUpdatedChannel = make(chan struct{})
EventErrorChannel = make(chan types.DesktopErrorMessage)
go func() {
for {
// TODO: Reserved for future use.
<-CursorChangedChannel
}
}()
}
func EventLoop(display string) {
@ -29,53 +37,39 @@ func EventLoop(display string) {
C.XEventLoop(displayUnsafe)
}
// TODO: Shutdown function.
//close(CursorChangedChannel)
//close(ClipboardUpdatedChannel)
//close(EventErrorChannel)
//export goXEventCursorChanged
func goXEventCursorChanged(event C.XFixesCursorNotifyEvent) {
Emmiter.Emit("cursor-changed", uint64(event.cursor_serial))
CursorChangedChannel <- uint64(event.cursor_serial)
}
//export goXEventClipboardUpdated
func goXEventClipboardUpdated() {
Emmiter.Emit("clipboard-updated")
ClipboardUpdatedChannel <- struct{}{}
}
//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))
EventErrorChannel <- types.DesktopErrorMessage{
Error_code: uint8(event.error_code),
Message: C.GoString(message),
Request_code: uint8(event.request_code),
Minor_code: uint8(event.minor_code),
}
}
//export goXEventActive

View File

@ -72,10 +72,10 @@ func (manager *DesktopManagerCtx) ScreenConfigurations() map[int]types.ScreenCon
func (manager *DesktopManagerCtx) SetScreenSize(size types.ScreenSize) error {
mu.Lock()
manager.emmiter.Emit("before_screen_size_change")
manager.GetScreenSizeChangeChannel() <- true
defer func() {
manager.emmiter.Emit("after_screen_size_change")
manager.GetScreenSizeChangeChannel() <- false
mu.Unlock()
}()

View File

@ -71,6 +71,7 @@ void XButton(unsigned int button, int down) {
static xkeyentry_t *xKeysHead = NULL;
// add keycode->keysym mapping to list
void XKeyEntryAdd(KeySym keysym, KeyCode keycode) {
xkeyentry_t *entry = (xkeyentry_t *) malloc(sizeof(xkeyentry_t));
if (entry == NULL)
@ -82,6 +83,7 @@ void XKeyEntryAdd(KeySym keysym, KeyCode keycode) {
xKeysHead = entry;
}
// get keycode for keysym from list
KeyCode XKeyEntryGet(KeySym keysym) {
xkeyentry_t *prev = NULL;
xkeyentry_t *curr = xKeysHead;
@ -147,6 +149,61 @@ KeyCode XkbKeysymToKeycode(Display* dpy, KeySym keysym) {
return keycode;
}
// From https://github.com/TigerVNC/tigervnc/blob/a434ef3377943e89165ac13c537cd0f28be97f84/unix/x0vncserver/XDesktop.cxx#L401-L453
KeyCode XkbAddKeyKeysym(Display* dpy, KeySym keysym) {
int types[1];
unsigned int key;
XkbDescPtr xkb;
XkbMapChangesRec changes;
KeySym *syms;
KeySym upper, lower;
xkb = XkbGetMap(dpy, XkbAllComponentsMask, XkbUseCoreKbd);
if (!xkb)
return 0;
for (key = xkb->max_key_code; key >= xkb->min_key_code; key--) {
if (XkbKeyNumGroups(xkb, key) == 0)
break;
}
// no free keycodes
if (key < xkb->min_key_code)
return 0;
// assign empty structure
changes = *(XkbMapChangesRec *) malloc(sizeof(XkbMapChangesRec));
for (int i = 0; i < sizeof(changes); i++) ((char *) &changes)[i] = 0;
XConvertCase(keysym, &lower, &upper);
if (upper == lower)
types[XkbGroup1Index] = XkbOneLevelIndex;
else
types[XkbGroup1Index] = XkbAlphabeticIndex;
XkbChangeTypesOfKey(xkb, key, 1, XkbGroup1Mask, types, &changes);
syms = XkbKeySymsPtr(xkb,key);
if (upper == lower)
syms[0] = keysym;
else {
syms[0] = lower;
syms[1] = upper;
}
changes.changed |= XkbKeySymsMask;
changes.first_key_sym = key;
changes.num_key_syms = 1;
if (XkbChangeMap(dpy, xkb, &changes)) {
return key;
}
return 0;
}
void XKey(KeySym keysym, int down) {
if (keysym == 0)
return;
@ -157,20 +214,13 @@ void XKey(KeySym keysym, int down) {
if (!down)
keycode = XKeyEntryGet(keysym);
// Try to get keysyms from existing keycodes
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 (keycode == 0)
keycode = XkbAddKeyKeysym(display, keysym);
if (down)
XKeyEntryAdd(keysym, keycode);
@ -250,14 +300,14 @@ char *XGetScreenshot(int *w, int *h) {
*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;
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;
pixels[pos] = (pixel & ximage->red_mask) >> 16;
pixels[pos+1] = (pixel & ximage->green_mask) >> 8;
pixels[pos+2] = pixel & ximage->blue_mask;
}
}

View File

@ -26,6 +26,11 @@ typedef struct xkeyentry_t {
struct xkeyentry_t *next;
} xkeyentry_t;
typedef struct xkeycode_t {
KeyCode keycode;
struct xkeycode_t *next;
} xkeycode_t;
static void XKeyEntryAdd(KeySym keysym, KeyCode keycode);
static KeyCode XKeyEntryGet(KeySym keysym);
static KeyCode XkbKeysymToKeycode(Display *dpy, KeySym keysym);

View File

@ -3,13 +3,17 @@ package http
import (
"context"
"encoding/json"
"fmt"
"image/jpeg"
"io"
"net/http"
"os"
"regexp"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -17,6 +21,8 @@ import (
"m1k1o/neko/internal/types"
)
const FILE_UPLOAD_BUF_SIZE = 65000
type Server struct {
logger zerolog.Logger
router *chi.Mux
@ -31,6 +37,16 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
router.Use(middleware.RequestID) // Create a request ID for each request
router.Use(middleware.RequestLogger(&logformatter{logger}))
router.Use(middleware.Recoverer) // Recover from panics without crashing server
router.Use(middleware.Compress(5, "application/octet-stream"))
router.Use(cors.Handler(cors.Options{
AllowOriginFunc: conf.AllowOrigin,
AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}))
if conf.PathPrefix != "/" {
router.Use(func(h http.Handler) http.Handler {
@ -99,6 +115,78 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
}
})
// allow downloading and uploading files
if webSocketHandler.FileTransferEnabled() {
router.Get("/file", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !isAuthorized {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
filename := r.URL.Query().Get("filename")
badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename)
if filename == "" || badChars {
http.Error(w, "bad filename", http.StatusBadRequest)
return
}
filePath := webSocketHandler.FileTransferPath(filename)
f, err := os.Open(filePath)
if err != nil {
http.Error(w, "not found or unable to open", http.StatusNotFound)
return
}
defer f.Close()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
io.Copy(w, f)
})
router.Post("/file", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !isAuthorized {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
r.ParseMultipartForm(32 << 20)
for _, formheader := range r.MultipartForm.File["files"] {
filePath := webSocketHandler.FileTransferPath(formheader.Filename)
formfile, err := formheader.Open()
if err != nil {
logger.Warn().Err(err).Msg("failed to open formdata file")
http.Error(w, "error writing file", http.StatusInternalServerError)
return
}
defer formfile.Close()
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
http.Error(w, "unable to open file for writing", http.StatusInternalServerError)
return
}
defer f.Close()
io.Copy(f, formfile)
}
})
}
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("true"))
})

View File

@ -4,7 +4,6 @@ import (
"fmt"
"sync"
"github.com/kataras/go-events"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -14,21 +13,21 @@ import (
func New(capture types.CaptureManager) *SessionManager {
return &SessionManager{
logger: log.With().Str("module", "session").Logger(),
host: "",
capture: capture,
members: make(map[string]*Session),
emmiter: events.New(),
logger: log.With().Str("module", "session").Logger(),
host: "",
capture: capture,
eventsChannel: make(chan types.SessionEvent, 10),
members: make(map[string]*Session),
}
}
type SessionManager struct {
mu sync.Mutex
logger zerolog.Logger
host string
capture types.CaptureManager
members map[string]*Session
emmiter events.EventEmmiter
mu sync.Mutex
logger zerolog.Logger
host string
capture types.CaptureManager
members map[string]*Session
eventsChannel chan types.SessionEvent
// TODO: Handle locks in sessions as flags.
controlLocked bool
}
@ -49,7 +48,12 @@ func (manager *SessionManager) New(id string, admin bool, socket types.WebSocket
manager.capture.Video().AddListener()
manager.mu.Unlock()
manager.emmiter.Emit("created", id, session)
manager.eventsChannel <- types.SessionEvent{
Type: types.SESSION_CREATED,
Id: id,
Session: session,
}
return session
}
@ -68,7 +72,12 @@ func (manager *SessionManager) SetHost(id string) error {
if ok {
manager.host = id
manager.emmiter.Emit("host", id)
manager.eventsChannel <- types.SessionEvent{
Type: types.SESSION_HOST_SET,
Id: id,
}
return nil
}
@ -86,7 +95,11 @@ func (manager *SessionManager) GetHost() (types.Session, bool) {
func (manager *SessionManager) ClearHost() {
id := manager.host
manager.host = ""
manager.emmiter.Emit("host_cleared", id)
manager.eventsChannel <- types.SessionEvent{
Type: types.SESSION_HOST_CLEARED,
Id: id,
}
}
func (manager *SessionManager) Has(id string) bool {
@ -163,7 +176,11 @@ func (manager *SessionManager) Destroy(id string) {
manager.capture.Video().RemoveListener()
manager.mu.Unlock()
manager.emmiter.Emit("destroyed", id, session)
manager.eventsChannel <- types.SessionEvent{
Type: types.SESSION_DESTROYED,
Id: id,
Session: session,
}
manager.logger.Err(err).Str("session_id", id).Msg("destroying session")
return
}
@ -221,32 +238,6 @@ func (manager *SessionManager) AdminBroadcast(v interface{}, exclude interface{}
return nil
}
func (manager *SessionManager) OnHost(listener func(id string)) {
manager.emmiter.On("host", func(payload ...interface{}) {
listener(payload[0].(string))
})
}
func (manager *SessionManager) OnHostCleared(listener func(id string)) {
manager.emmiter.On("host_cleared", func(payload ...interface{}) {
listener(payload[0].(string))
})
}
func (manager *SessionManager) OnDestroy(listener func(id string, session types.Session)) {
manager.emmiter.On("destroyed", func(payload ...interface{}) {
listener(payload[0].(string), payload[1].(*Session))
})
}
func (manager *SessionManager) OnCreated(listener func(id string, session types.Session)) {
manager.emmiter.On("created", func(payload ...interface{}) {
listener(payload[0].(string), payload[1].(*Session))
})
}
func (manager *SessionManager) OnConnected(listener func(id string, session types.Session)) {
manager.emmiter.On("connected", func(payload ...interface{}) {
listener(payload[0].(string), payload[1].(*Session))
})
func (manager *SessionManager) GetEventsChannel() chan types.SessionEvent {
return manager.eventsChannel
}

View File

@ -78,7 +78,11 @@ func (session *Session) SetPeer(peer types.Peer) error {
func (session *Session) SetConnected(connected bool) error {
session.connected = connected
if connected {
session.manager.emmiter.Emit("connected", session.id, session)
session.manager.eventsChannel <- types.SessionEvent{
Type: types.SESSION_CONNECTED,
Id: session.id,
Session: session,
}
}
return nil
}

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