57 Commits
v2.7 ... v2.7.7

Author SHA1 Message Date
0dd9597519 create pulseaudio sink, fixes #267. 2023-03-26 18:59:10 +02:00
334fcef407 update changelog. 2023-03-25 22:21:00 +01:00
e868ad4061 Merge branch 'pu/autoplay' 2023-03-25 22:19:43 +01:00
b41d0bf956 lint fix. 2023-03-25 22:19:01 +01:00
b62fa6ab8b kde disable autolock, fixes #266. 2023-03-25 22:09:17 +01:00
8dba9cff44 revert extra controls usecase. 2023-03-25 21:53:17 +01:00
217cc451ea add autoplay audio if possible 2023-03-24 21:33:32 +01:00
1f81bd3efc hide buttons in cast mode 2023-03-24 21:15:51 +01:00
50e5483661 turn on vaapi hwenc in intel images by default. 2023-03-19 19:13:11 +01:00
Bad
d2765c30fd readd -> read 2023-03-19 18:13:06 +01:00
76fc892823 fix video state sync, fixes #250. 2023-03-18 13:31:40 +01:00
ea99ce7753 fix log. 2023-03-18 13:11:42 +01:00
646eaced29 fix candidate error. 2023-03-18 13:11:02 +01:00
9104953ad9 upgrade node version in serve script. 2023-03-18 12:59:44 +01:00
c40243635f improved chinese and korean characters support, fixes #252. 2023-03-18 12:58:19 +01:00
8059002475 forgotten mobile styles for cast, fixes #254. 2023-03-18 12:48:18 +01:00
9daf83cc52 fix webrtc client gathering, #259. 2023-03-18 00:49:25 +01:00
0cebe465a2 fix build. 2023-03-17 20:47:53 +01:00
d9403d9c14 add gpu acceleration to docs. 2023-03-17 19:04:57 +01:00
8604a30744 add gpu flag to start server. 2023-03-17 18:28:13 +01:00
957e893cf6 fix nvidia chromium. 2023-03-17 18:27:34 +01:00
55005a6f8d Add nvidia docker gpu acceleration (#238)
* add nvidia dockerfile.

* add nvidia docker to build.

* remove vaapi.

* add google chrome and brave.

* upgrade to virtualgl 3.1.

* add disable-seccomp-filter-sandbox to chrome.

* use vgl display in vglrun.

* Revert "use vgl display in vglrun."

This reverts commit 0cd556b5d8.

* update chrome params.

* update changelog.

* update brave.

* update CI.
2023-03-17 01:12:35 +01:00
bc7aae0401 add kde image. 2023-03-08 17:49:31 +01:00
Bad
addb6b6d1b docs - fix typos 2023-03-02 19:47:39 +01:00
Bad
d2ddae8f45 docs typo fixes 2023-03-02 19:05:17 +01:00
724f5fe384 show some controls for cast #254. 2023-02-23 18:43:00 +01:00
b0e3e29658 upgrade & change to go 1.20. 2023-02-23 18:23:34 +01:00
373dcb4f32 Fix typo in README.md 2023-02-03 07:37: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
98 changed files with 23536 additions and 607 deletions

View File

@ -1,7 +1,7 @@
#
# STAGE 1: SERVER
#
FROM golang:1.18-bullseye as server
FROM golang:1.20-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,13 +68,9 @@ 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; \
#
# install fonts
apt-get install -y --no-install-recommends \
@ -88,7 +79,9 @@ RUN set -eux; \
# Japanese fonts
fonts-takao-mincho \
# Chinese fonts
fonts-wqy-zenhei; \
fonts-wqy-zenhei xfonts-intl-chinese xfonts-wqy \
# Korean fonts
fonts-wqy-microhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -122,7 +115,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
@ -131,7 +123,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.20-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,29 @@ 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 xfonts-intl-chinese xfonts-wqy \
# Korean fonts
fonts-wqy-microhei; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
@ -131,4 +141,3 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
#
# run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View File

@ -0,0 +1,149 @@
#
# STAGE 1: SERVER
#
FROM golang:1.20-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 xfonts-intl-chinese xfonts-wqy \
# Korean fonts
fonts-wqy-microhei; \
#
# 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 NEKO_HWENC=VAAPI
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,197 @@
ARG UBUNTU_RELEASE=20.04
ARG CUDA_VERSION=11.2.2
#
# STAGE 1: SERVER
#
FROM golang:1.18-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:14-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 nvcr.io/nvidia/cudagl:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_RELEASE} as runtime
ARG UBUNTU_RELEASE
ARG CUDA_VERSION
# Make all NVIDIA GPUs visible by default
ENV NVIDIA_VISIBLE_DEVICES=all
# All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work
ENV NVIDIA_DRIVER_CAPABILITIES all
#
# set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container
ENV VGL_DISPLAY egl
#
# 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; \
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; \
#
# hardware acclerations utilities
apt-get install -y --no-install-recommends libgtk-3-bin mesa-utils mesa-utils-extra mesa-va-drivers mesa-vulkan-drivers libvulkan-dev libvulkan-dev:i386 vdpauinfo; \
#
# 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; \
#
# 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 xfonts-intl-chinese xfonts-wqy \
# Korean fonts
fonts-wqy-microhei; \
#
# 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/*
#
# install and configure Vulkan manually
RUN set -eux; \
apt-get update; \
if [ "${UBUNTU_RELEASE}" = "18.04" ]; then apt-get install -y --no-install-recommends vulkan-utils; else apt-get install -y --no-install-recommends vulkan-tools; fi; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*; \
#
# configure vulkan
VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)'); \
mkdir -p /etc/vulkan/icd.d/; \
echo "{\n\
\"file_format_version\" : \"1.0.0\",\n\
\"ICD\": {\n\
\"library_path\": \"libGLX_nvidia.so.0\",\n\
\"api_version\" : \"${VULKAN_API_VERSION}\"\n\
}\n\
}" > /etc/vulkan/icd.d/nvidia_icd.json
#
# install VirtualGL and make libraries available for preload
ARG VIRTUALGL_VERSION=3.1
RUN set -eux; \
apt-get update; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends ./virtualgl_${VIRTUALGL_VERSION}_amd64.deb ./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb; \
rm -f "virtualgl_${VIRTUALGL_VERSION}_amd64.deb" "virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
chmod u+s /usr/lib/libvglfaker.so; \
chmod u+s /usr/lib/libdlfaker.so; \
chmod u+s /usr/lib32/libvglfaker.so; \
chmod u+s /usr/lib32/libdlfaker.so; \
chmod u+s /usr/lib/i386-linux-gnu/libvglfaker.so; \
chmod u+s /usr/lib/i386-linux-gnu/libdlfaker.so; \
#
# 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/supervisord.conf /etc/neko/supervisord.conf
COPY .docker/base/xorg.conf /etc/neko/xorg.conf
COPY .docker/base/nvidia/entrypoint.sh /bin/entrypoint.sh
#
# set default envs
ENV USER=$USERNAME
ENV DISPLAY=:99.0
ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080
#
# 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

@ -1,5 +1,8 @@
#!/usr/bin/pulseaudio -nF
### Create virtual output device sink
load-module module-null-sink sink_name=audio_output sink_properties=device.description="Virtual\ Audio\ Output"
# Allow pulse audio to be accessed via TCP (from localhost only), to allow other users to access the virtual devices
load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1

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

@ -0,0 +1,12 @@
#!/bin/bash -e
# Add VirtualGL directories to path
export PATH="${PATH}:/opt/VirtualGL/bin"
# Use VirtualGL to run wine with OpenGL if the GPU is available, otherwise use barebone wine
if [ -n "$(nvidia-smi --query-gpu=uuid --format=csv | sed -n 2p)" ]; then
exec vglrun "$@"
else
echo "No GPU detected"
exec "$@"
fi

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

@ -0,0 +1,23 @@
ARG BASE_IMAGE=m1k1o/neko:nvidia-base
FROM $BASE_IMAGE
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends apt-transport-https curl openbox; \
#
# install brave browser
curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg; \
echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg arch=amd64] https://brave-browser-apt-release.s3.brave.com/ stable main" \
| tee /etc/apt/sources.list.d/brave-browser-release.list; \
apt-get update; \
apt-get install -y --no-install-recommends brave-browser; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.nvidia.conf /etc/neko/supervisord/brave.conf
COPY --chown=neko preferences.json /home/neko/.config/brave/Default/Preferences
COPY policies.json /etc/brave/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -1,6 +1,17 @@
[program:brave]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/brave-browser --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/brave --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
command=/usr/bin/brave-browser
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/brave
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--disable-gpu
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800

View File

@ -0,0 +1,36 @@
[program:brave]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/bin/entrypoint.sh /usr/bin/brave-browser
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/brave
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/brave.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -77,6 +77,36 @@ 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
}
build_nvidia() {
if [ "$1" = "base" ]
then
# build nvidia base
docker build -t "${BUILD_IMAGE}:nvidia-base" -f base/Dockerfile.nvidia "${BASE}"
elif [ -f "$1/Dockerfile.nvidia" ]
then
# build dedicated nvidia image
docker build -t "${BUILD_IMAGE}:nvidia-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:nvidia-base" -f "$1/Dockerfile.nvidia" "$1/"
else
# try to build nvidia image with common Dockerfile
docker build -t "${BUILD_IMAGE}:nvidia-$1" --build-arg="BASE_IMAGE=${BUILD_IMAGE}:nvidia-base" -f "$1/Dockerfile" "$1/"
fi
}
case $1 in
client) build_client;;
server) build_server;;
@ -84,6 +114,12 @@ case $1 in
# build arm- images
arm-*) build_arm "${1#arm-}";;
# build intel- images
intel-*) build_intel "${1#intel-}";;
# build nvidia- images
nvidia-*) build_nvidia "${1#nvidia-}";;
# 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

@ -0,0 +1,37 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
#
# install neko chromium
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends software-properties-common; \
# chromium-browser from default repo needs snap to be installed
# and nvidia base is ubuntu not debian
add-apt-repository ppa:system76/pop; \
apt-get update; \
apt-get install -y --no-install-recommends unzip chromium openbox; \
#
# install widevine module
CHROMIUM_DIR="/usr/lib/chromium"; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \
unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \
unzip -p /tmp/widevine.zip manifest.json > "${CHROMIUM_DIR}/WidevineCdm/manifest.json"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so"; \
find "${CHROMIUM_DIR}/WidevineCdm" -type d -exec chmod 0755 '{}' \;; \
find "${CHROMIUM_DIR}/WidevineCdm" -type f -exec chmod 0644 '{}' \;; \
rm /tmp/widevine.zip; \
#
# clean up
apt-get --purge autoremove -y unzip; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.nvidia.conf /etc/neko/supervisord/chromium.conf
COPY --chown=neko preferences.json /home/neko/.config/chromium/Default/Preferences
COPY policies.json /etc/chromium/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -1,6 +1,17 @@
[program:chromium]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/chromium --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/chromium --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
command=/usr/bin/chromium
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/chromium
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--disable-gpu
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800

View File

@ -0,0 +1,36 @@
[program:chromium]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/bin/entrypoint.sh /usr/bin/chromium
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/chromium
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/chromium.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -0,0 +1,21 @@
ARG BASE_IMAGE=m1k1o/neko:nvidia-base
FROM $BASE_IMAGE
ARG SRC_URL="https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb"
#
# install google chrome
RUN set -eux; apt-get update; \
wget -O /tmp/google-chrome.deb "${SRC_URL}"; \
apt-get install -y --no-install-recommends openbox /tmp/google-chrome.deb; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.nvidia.conf /etc/neko/supervisord/google-chrome.conf
COPY --chown=neko preferences.json /home/neko/.config/google-chrome/Default/Preferences
COPY policies.json /etc/opt/chrome/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -1,6 +1,17 @@
[program:google-chrome]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/google-chrome --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/google-chrome --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
command=/usr/bin/google-chrome
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/google-chrome
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--disable-gpu
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800

View File

@ -0,0 +1,36 @@
[program:google-chrome]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/bin/entrypoint.sh /usr/bin/google-chrome
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/chromium
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/google-chrome.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

26
.docker/kde/Dockerfile Normal file
View File

@ -0,0 +1,26 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
#
# install kde
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends kde-full kwin-x11 sudo; \
#
# add user to sudoers
usermod -aG sudo neko; \
echo "neko:neko" | chpasswd; \
echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
# clean up
apt remove xserver-xorg-legacy -y; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# disable autolock
RUN kwriteconfig5 --file /home/neko/.config/kscreenlockerrc --group Daemon --key Autolock false; \
chown neko:neko /home/neko/.config/kscreenlockerrc
#
# copy configuation files
COPY supervisord.conf /etc/neko/supervisord/kde.conf

View File

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

View File

@ -0,0 +1,24 @@
ARG BASE_IMAGE=m1k1o/neko:base
FROM $BASE_IMAGE
ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/"
#
# install microsoft edge
RUN set -eux; apt-get update; \
#
# fetch latest release
SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | tail -1)"; \
wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \
apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY supervisord.nvidia.conf /etc/neko/supervisord/microsoft-edge.conf
COPY --chown=neko preferences.json /home/neko/.config/microsoft-edge/Default/Preferences
COPY policies.json /etc/opt/edge/policies/managed/policies.json
COPY openbox.xml /etc/neko/openbox.xml

View File

@ -1,6 +1,17 @@
[program:microsoft-edge]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/microsoft-edge --window-position=0,0 --display=%(ENV_DISPLAY)s --user-data-dir=/home/neko/.config/microsoft-edge --no-first-run --start-maximized --bwsi --force-dark-mode --disable-file-system --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage
command=/usr/bin/microsoft-edge
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/microsoft-edge
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--disable-gpu
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800

View File

@ -0,0 +1,36 @@
[program:microsoft-edge]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/bin/entrypoint.sh /usr/bin/microsoft-edge
--window-position=0,0
--display=%(ENV_DISPLAY)s
--user-data-dir=/home/neko/.config/microsoft-edge
--no-first-run
--start-maximized
--bwsi
--force-dark-mode
--disable-file-system
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--disable-software-rasterizer
--disable-dev-shm-usage
stopsignal=INT
autorestart=true
priority=800
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/microsoft-edge.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true
[program:openbox]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
autorestart=true
priority=300
user=%(ENV_USER)s
stdout_logfile=/var/log/neko/openbox.log
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
redirect_stderr=true

View File

@ -17,5 +17,9 @@ fi
docker run --rm -it \
-v "${PWD}/../server:/src" \
--entrypoint="go" \
neko_dev_server build -o "bin/neko" "cmd/neko/main.go"
-e GIT_COMMIT=`git rev-parse --short HEAD` \
-e GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` \
-e GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"` \
--entrypoint="bash" \
--workdir="/src" \
neko_dev_server ./build

View File

@ -16,7 +16,7 @@ if [ ! -d "${PWD}/../client/node_modules" ] || [ "$1" == "-i" ]; then
-v "${PWD}/../client:/app" \
--workdir="/app" \
--entrypoint="npm" \
node:14-buster-slim install
node:18-bullseye-slim install
fi
docker run --rm -it \
@ -25,5 +25,5 @@ docker run --rm -it \
-e "VUE_APP_SERVER_PORT=${SERVER_PORT}" \
--workdir="/app" \
--entrypoint="npm" \
node:14-buster-slim run serve
node:18-bullseye-slim run serve

View File

@ -17,8 +17,22 @@ if [ ! -f "${BINARY_PATH}" ] || [ "$1" == "-r" ]; then
./rebuild-server
fi
# if image starts with nvidia- add --gpus all
if [[ "${SERVER_TAG}" == "nvidia-"* ]]; then
GPU_FLAG="--gpus all"
echo "Nvidia GPU acceleration enabled"
fi
# if image starts with intel- add --device /dev/dri
if [[ "${SERVER_TAG}" == "intel-"* ]]; then
GPU_FLAG="--device /dev/dri"
echo "Intel GPU acceleration enabled"
fi
# use --gpus all to enable GPU acceleration
docker run --rm -it \
--name "neko_dev" \
$GPU_FLAG \
-p "${SERVER_PORT}:8080" \
-p "${SERVER_EPR}:${SERVER_EPR}/udp" \
-e "NEKO_SCREEN=1920x1080@60" \

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

@ -1,4 +1,4 @@
name: "CI for builds"
name: "build and push amd64 images to Docker Hub"
on:
push:
@ -50,7 +50,7 @@ jobs:
# Will build all images even if some fail.
fail-fast: false
matrix:
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce ]
tags: [ firefox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce, kde ]
env:
DOCKER_TAG: ${{ matrix.tags }}
steps:

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,20 @@ 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
- tag: kde
env:
TAG_NAME: ${{ matrix.tag }}
PLATFORMS: ${{ matrix.platforms }}
steps:
-
name: Checkout
@ -108,11 +99,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 +122,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 }}

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

@ -0,0 +1,126 @@
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
- tag: kde
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 }}

122
.github/workflows/ghcr-nvidia.yml vendored Normal file
View File

@ -0,0 +1,122 @@
name: "nvidia gpu supported images"
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: m1k1o/neko
TAG_PREFIX: nvidia-
BASE_DOCKERFILE: Dockerfile.nvidia
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: brave
dockerfile: Dockerfile.nvidia
- tag: chromium
dockerfile: Dockerfile.nvidia
- tag: google-chrome
dockerfile: Dockerfile.nvidia
- tag: microsoft-edge
dockerfile: Dockerfile.nvidia
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 }}

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,7 +22,7 @@
<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://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/>
@ -30,15 +30,23 @@
# 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.
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 multiple people, so I created a fork. Initially, I wanted to merge my changes to the upstream repository, but the original author did not have time for this project anymore and it got eventually archived.
## Use-cases and comparison
Neko started as a virtual browser that is streamed using WebRTC to multiple users.
- It is **not only limited to a browser**; it can run anything that runs on linux (e.g. VLC). Browser only happens to be the most popular and widely used use-case.
- In fact, it is not limited to a single program either; you can install a full desktop environment (e.g. XFCE).
- In fact, it is not limited to a single program either; you can install a full desktop environment (e.g. XFCE, KDE).
- Speaking of limits, it does not need to run in a container; you could install neko on your host, connect to your X server and control your whole VM.
- Theoretically it is not limited to only X server, anything that can be controlled and scraped periodically for images could be used instead.
- Like implementing RDP or VNC protocol, where neko would only act as WebRTC relay server. This is currently only future.
@ -91,6 +99,7 @@ Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://g
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/kde.svg" title="m1k1o/neko:kde" width="60" height="auto"/>
... others in <a href="https://github.com/m1k1o/neko-apps">m1k1o/neko-apps</a>
</div>

21271
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.0",
"animejs": "^3.2.0",
"axios": "^0.24.0",
"axios": "^1.2.3",
"date-fns": "^2.29.3",
"emoji-datasource": "^6.0.1",
"eventemitter3": "^4.0.7",
@ -42,23 +42,23 @@
},
"devDependencies": {
"@types/animejs": "^3.1.6",
"@types/node": "^14.18.32",
"@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",
"@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.55.0",
"sass-loader": "^10.3.1",

View File

@ -1,17 +1,22 @@
<template>
<div id="neko" :class="[side ? 'expanded' : '']">
<div id="neko" :class="[!videoOnly && side ? 'expanded' : '']">
<template v-if="!$client.supported">
<neko-unsupported />
</template>
<template v-else>
<main class="neko-main">
<div v-if="!hideControls" class="header-container">
<div v-if="!videoOnly" class="header-container">
<neko-header />
</div>
<div class="video-container">
<neko-video ref="video" :hideControls="hideControls" @control-attempt="controlAttempt" />
<neko-video
ref="video"
:hideControls="hideControls"
:extraControls="isEmbedMode"
@control-attempt="controlAttempt"
/>
</div>
<div v-if="!hideControls" class="room-container">
<div v-if="!videoOnly" class="room-container">
<neko-members />
<div class="room-menu">
<div class="settings">
@ -26,11 +31,11 @@
</div>
</div>
</main>
<neko-side v-if="!hideControls && side" />
<neko-side v-if="!videoOnly && side" />
<neko-connect v-if="!connected" />
<neko-about v-if="about" />
<notifications
v-if="!hideControls"
v-if="!videoOnly"
group="neko"
position="top left"
style="top: 50px; pointer-events: none"
@ -176,10 +181,22 @@
shakeKbd = false
get hideControls() {
get isCastMode() {
return !!new URL(location.href).searchParams.get('cast')
}
get isEmbedMode() {
return !!new URL(location.href).searchParams.get('embed')
}
get hideControls() {
return this.isCastMode
}
get videoOnly() {
return this.isCastMode || this.isEmbedMode
}
@Watch('hideControls', { immediate: true })
onHideControls(enabled: boolean) {
if (enabled) {

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

@ -334,7 +334,7 @@
onDownloadProgress: (x) => {
transfer.progress = x.loaded
if (x.lengthComputable && transfer.size !== x.total) {
if (x.total && transfer.size !== x.total) {
transfer.size = x.total
}
if (transfer.progress === transfer.size) {

View File

@ -157,7 +157,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' })

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

@ -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

@ -22,10 +22,10 @@
@mouseenter.stop.prevent="onMouseEnter"
@mouseleave.stop.prevent="onMouseLeave"
/>
<div v-if="!playing && playable" class="player-overlay" @click.stop.prevent="toggle">
<div v-if="!playing && playable" class="player-overlay" @click.stop.prevent="playAndUnmute">
<i class="fas fa-play-circle" />
</div>
<div v-if="mutedOverlay && muted" class="player-overlay" @click.stop.prevent="unmute">
<div v-else-if="mutedOverlay && muted" class="player-overlay" @click.stop.prevent="unmute">
<i class="fas fa-volume-up" />
</div>
<div ref="aspect" class="player-aspect" />
@ -33,7 +33,7 @@
<ul v-if="!fullscreen && !hideControls" class="video-menu top">
<li><i @click.stop.prevent="requestFullscreen" class="fas fa-expand"></i></li>
<li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
<li class="request-control">
<li v-if="!implicitHosting" :class="extraControls || 'extra-control'">
<i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
@click.stop.prevent="toggleControl"
@ -106,12 +106,12 @@
}
}
&.request-control {
/* usually extra controls are only shown on mobile */
&.extra-control {
display: none;
}
@media (max-width: 768px) {
&.request-control {
&.extra-control {
display: inline-block;
}
}
@ -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'
@ -223,13 +223,15 @@
@Ref('resolution') readonly _resolution!: Resolution
@Ref('clipboard') readonly _clipboard!: Clipboard
// all controls are hidden (e.g. for cast mode)
@Prop(Boolean) readonly hideControls!: boolean
// extra controls are shown (e.g. for embed mode)
@Prop(Boolean) readonly extraControls!: boolean
private keyboard = GuacamoleKeyboard()
private observer = new ResizeObserver(this.onResize.bind(this))
private focused = false
private fullscreen = false
private startsMuted = true
private mutedOverlay = true
get admin() {
@ -339,12 +341,12 @@
}
@Watch('width')
onWidthChanged(width: number) {
onWidthChanged() {
this.onResize()
}
@Watch('height')
onHeightChanged(height: number) {
onHeightChanged() {
this.onResize()
}
@ -361,7 +363,6 @@
onMutedChanged(muted: boolean) {
if (this._video && this._video.muted != muted) {
this._video.muted = muted
this.startsMuted = muted
if (!muted) {
this.mutedOverlay = false
@ -384,9 +385,29 @@
}
@Watch('playing')
onPlayingChanged(playing: boolean) {
async onPlayingChanged(playing: boolean) {
if (this._video && this._video.paused && playing) {
this.play()
// if autoplay is disabled, play() will throw an error
// and we need to properly save the state otherwise we
// would be thinking we're playing when we're not
try {
await this._video.play()
} catch (err: any) {
if (!this._video.muted) {
// video.play() can fail if audio is set due restrictive
// browsers autoplay policy -> retry with muted audio
try {
this.$accessor.video.setMuted(true)
this._video.muted = true
await this._video.play()
} catch (err: any) {
// if it still fails, we're not playing anything
this.$accessor.video.pause()
}
} else {
this.$accessor.video.pause()
}
}
}
if (this._video && !this._video.paused && !playing) {
@ -417,17 +438,13 @@
onFullscreenChange(this._player, () => {
this.fullscreen = isFullscreen()
this.fullscreen ? lockKeyboard() : unlockKeyboard()
this.onResize()
})
this._video.addEventListener('canplaythrough', () => {
this.$accessor.video.setPlayable(true)
if (this.autoplay) {
// start as muted due to restrictive browsers autoplay policy
if (this.startsMuted && (!document.hasFocus() || !this.$accessor.active)) {
this.$accessor.video.setMuted(true)
}
this.$nextTick(() => {
this.$accessor.video.play()
})
@ -443,7 +460,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)
})
@ -559,6 +576,11 @@
}
}
playAndUnmute() {
this.$accessor.video.play()
this.$accessor.video.setMuted(false)
}
unmute() {
this.$accessor.video.setMuted(false)
}

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)
@ -203,22 +203,23 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
return
}
this._peer = new RTCPeerConnection()
if (lite !== true) {
this._peer = new RTCPeerConnection({
iceServers: servers,
})
} else {
this._peer = new RTCPeerConnection()
}
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}`)
@ -251,11 +252,28 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._peer.ontrack = this.onTrack.bind(this)
this._peer.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
if (!event.candidate) {
this.emit('debug', `sent all local ICE candidates`)
return
}
const init = event.candidate.toJSON()
this.emit('debug', `sending local ICE candidate`, init)
this._ws!.send(
JSON.stringify({
event: EVENT.SIGNAL.CANDIDATE,
data: JSON.stringify(init),
}),
)
}
this._peer.onnegotiationneeded = async () => {
this.emit('warn', `negotiation is needed`)
const d = await this._peer!.createOffer()
this._peer!.setLocalDescription(d)
await this._peer!.setLocalDescription(d)
this._ws!.send(
JSON.stringify({
@ -277,15 +295,19 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
return
}
this._peer.setRemoteDescription({ type: 'offer', sdp })
await this._peer.setRemoteDescription({ type: 'offer', sdp })
for (const candidate of this._candidates) {
this._peer.addIceCandidate(candidate)
await this._peer.addIceCandidate(candidate)
}
this._candidates = []
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(
@ -306,7 +328,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
return
}
this._peer.setRemoteDescription({ type: 'answer', sdp })
await this._peer.setRemoteDescription({ type: 'answer', sdp })
}
private async onMessage(e: MessageEvent) {

View File

@ -7,7 +7,6 @@ import { accessor } from '~/store'
import {
SystemMessagePayload,
SignalProvidePayload,
MemberListPayload,
MemberDisconnectPayload,
MemberPayload,
@ -19,7 +18,6 @@ import {
ScreenConfigurationsPayload,
ScreenResolutionPayload,
BroadcastStatusPayload,
AdminPayload,
AdminTargetPayload,
AdminLockMessage,
SystemInitPayload,
@ -131,7 +129,7 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
this.$accessor.video.setStream(0)
}
protected [EVENT.DATA](data: any) {}
protected [EVENT.DATA]() {}
/////////////////////////////
// System Events

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)
}

View File

@ -53,7 +53,7 @@ export const actions = actionTree(
accessor.files._removeTransfer(transfer)
},
cancelAllTransfers(store) {
cancelAllTransfers() {
for (const t of accessor.files.transfers) {
if (t.status !== 'completed') {
t.abortController?.abort()
@ -62,7 +62,7 @@ export const actions = actionTree(
}
},
refresh(store) {
refresh() {
if (!accessor.connected) {
return
}

View File

@ -63,7 +63,7 @@ export const getters = getterTree(state, {
export const actions = actionTree(
{ state, getters, mutations },
{
initialise(store) {
initialise() {
accessor.emoji.initialise()
accessor.settings.initialise()
},
@ -92,12 +92,12 @@ export const actions = actionTree(
}
},
login({ state }, { displayname, password }: { displayname: string; password: string }) {
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', '')

View File

@ -21,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) => {
@ -136,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
}
@ -160,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

@ -13,21 +13,32 @@ With `NEKO_FILE_TRANSFER_ENABLED=true`:
# 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.
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.
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 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 multiple 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?
I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd.
***But why the cat butt?*** Because cats are *assholes*, but you love them anyways.
***But why the cat butt?*** Because cats are *assholes*, but you love them anyway.

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128" viewBox="0 0 33.8667 33.8667" width="128" xmlns="http://www.w3.org/2000/svg">
<metadata/>
<g transform="translate(0 -263.13)">
<path d="m0 263.13h33.87v33.87h-33.87z" fill="#1d99f3"/>
<path d="m18.8061 267.477-4.30242.41187v17.7156l4.25685-.64088v-7.55326l5.72205 8.37759 4.48586-1.41935-5.85934-8.05685 5.90521-7.59912-4.57729-1.05245-5.67649 7.59883zm-9.75254 4.31712c-.04858.005-.09551.0265-.13199.0632l-1.68863 1.68833c-.071.0712-.08437.18161-.03203.26782l1.97702 3.25614c-.35065.5895-.63169 1.22509-.83255 1.89559l-3.6295.75494c-.10098.0209-.17374.11039-.17374.21402v2.38772c0 .10098.06909.18844.16639.21196l3.52278.86107c.18786.77655.47897 1.51268.86372 2.18928l-2.03906 3.10944c-.05689.0869-.045.20138.0285.27458l1.68804 1.68834c.071.0708.18183.0847.26841.0326l3.19528-1.94057c.62765.36219 1.30454.64721 2.01995.8405l.74553 3.58451c.02098.10157.11076.17375.21373.17375h2.38802c.10039 0 .188-.0685.21196-.16699l.87812-3.59186c.73745-.19902 1.43492-.49565 2.07786-.8743l3.14883 2.06463c.08658.057.20109.0456.27458-.0279l1.68863-1.68834c.07143-.0714.08422-.18182.03174-.26781l-1.14947-1.89442-.37189.11759c-.05421.0171-.11333-.003-.14522-.0503 0 0-.73319-1.07332-1.68011-2.45915-1.13197 2.21544-3.43502 3.73297-6.09453 3.73297-3.77847 0-6.84183-3.06343-6.84183-6.84212 0-2.77974 1.65799-5.17032 4.03813-6.24122v-1.76506c-.43318.15154-.85196.33425-1.24972.55092-.00029-.00027-.00058-.001-.0017-.002l-3.22292-2.11384c-.04341-.0284-.09371-.0397-.14229-.0347z" fill="#fcfcfc" stroke-width=".265"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -2,6 +2,28 @@
## master branch
### New Features
- Added AV1 tag, metadata and pipeline. Unfortunately does not work yet, since the encoding is way too slow (by @mbattista).
- Added `m1k1o/neko:kde` tag as an alternative to `m1k1o/neko:xfce`.
- New VirtualGL version 3.1 was released, adding support for Chromium browsers to use Nvidia GPU acceleration!
- Added `?embed=1` parameter to the URL, which will hide the sidebar and the top bar, so that it can be embedded in other websites.
### 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.
- Fixed client side webrtc ICE gathering, so that neko can be used without exposed ports, only with STUN and TURN servers.
- Fixed play state synchronization, when autoplay is disabled.
### 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.
- Improved chinese and korean characters support.
- Disabled autolock for kde, so that it does not lock the screen when you are not using it.
- Refactored autoplay, so that it will start playing audio, if it's allowed by the browser (by @urbanekpj).
- Renamed pulseaudio sink from `auto_null` to `audio_output`, because it was ignored by KDE.
## [n.eko v2.7](https://github.com/m1k1o/neko/releases/tag/v2.7)
### New Features

View File

@ -12,9 +12,10 @@
<img src="../_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/>
<img src="../_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/>
<img src="../_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
<img src="../_media/icons/kde.svg" title="m1k1o/neko:kde" 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)).
@ -28,15 +29,59 @@ Use the following docker images:
- Pass env var `REMMINA_URL=<proto>://[<username>[:<password>]@]server[:port]` (proto being `vnc`, `rdp` or `spice`).
- Or create your custom configuration with remmina locally (it's saved in `~/.local/share/remmina/path_to_profile.remmina`) and bind-mount it, then pass env var `REMMINA_PROFILE=<path_to_profile.remmina>`.
- `m1k1o/neko:vlc` - for VLC Video player (needs volume mounted to `/media` with local video files, or setting `VLC_MEDIA=/media` path).
- `m1k1o/neko:xfce` - for a shared desktop / installing shared software.
- `m1k1o/neko:xfce` or `m1k1o/neko:kde` - 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`
- `ghcr.io/m1k1o/neko/kde: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.io/m1k1o/neko/intel-kde:latest`
For images with Nvidia GPU hardware acceleration using EGL use:
- `ghcr.io/m1k1o/neko/nvidia-chromium:latest`
- `ghcr.io/m1k1o/neko/nvidia-google-chrome:latest`
- `ghcr.io/m1k1o/neko/nvidia-microsoft-edge:latest`
- `ghcr.io/m1k1o/neko/nvidia-brave: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.
@ -93,7 +138,7 @@ services:
- 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.
- From security perspective, browser is not enabled to access local file data.
- If you want to enable this, you need to modify following policies:
```json
"DownloadRestrictions": 0,
@ -110,13 +155,64 @@ services:
- 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.
- From security perspective, browser is set up to forget all cookies and browsing history when its closed.
- If you want to enable this, you need to modify following policies:
```json
"DefaultCookiesSetting": 1,
"RestoreOnStartup": 1,
```
### Nvidia GPU acceleration
You need to have nvidia-docker installed, start the container with `--gpus all` flag and use images built for nvidia (see above).
```bash
docker run -d --gpus all \
-p 8080:8080 \
-p 56000-56100:56000-56100/udp \
-e NEKO_SCREEN=1920x1080@30 \
-e NEKO_PASSWORD=neko \
-e NEKO_PASSWORD_ADMIN=admin \
-e NEKO_EPR=56000-56100 \
-e NEKO_NAT1TO1=192.168.1.10 \
-e NEKO_ICELITE=1 \
--shm-size=2gb \
--cap-add=SYS_ADMIN \
--name neko \
ghcr.io/m1k1o/neko/nvidia-google-chrome:latest
```
If you want to use docker-compose, you can use this example:
```yaml
version: "3.4"
services:
neko:
image: "ghcr.io/m1k1o/neko/nvidia-google-chrome:latest"
restart: "unless-stopped"
shm_size: "2gb"
ports:
- "8080:8080"
- "56000-56100:56000-56100/udp"
cap_add:
- SYS_ADMIN
environment:
NEKO_SCREEN: '1920x1080@30'
NEKO_PASSWORD: neko
NEKO_PASSWORD_ADMIN: admin
NEKO_EPR: 56000-56100
NEKO_NAT1TO1: 192.168.1.10
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
```
Note, currently only browser GPU acceleration is supported, not encoding.
### Want to use VPN for your n.eko browsing?
- Check this out: https://github.com/m1k1o/neko-vpn
@ -134,6 +230,7 @@ services:
- Adding `?pwd=<password>` will prefill password.
- Adding `?usr=<display-name>` will prefill username.
- Adding `?cast=1` will hide all control and show only video.
- Adding `?embed=1` will hide most additional components and show only video.
- e.g. `http(s)://<URL:Port>/?pwd=neko&usr=guest&cast=1`
### Screen size

View File

@ -25,7 +25,7 @@ nat1to1: <ip>
- Control protection means, users can gain control only if at least one admin is in the room.
- e.g. `false`
#### `NEKO_IMPLICIT_CONTROL`:
- If enabled members can gain control implicitly, they don't needd to request control.
- If enabled members can gain control implicitly, they don't need to request control.
- e.g. `false`
#### `NEKO_LOCKS`:
- Resources, that will be locked when starting, separated by whitespace.
@ -93,7 +93,7 @@ nat1to1: <ip>
- opus *(default encoder)*
- g722
- pcmu
- pcma
- pcma
#### `NEKO_AUDIO_BITRATE`:
- Bitrate of the audio stream in kb/s.
- e.g. `196`
@ -136,7 +136,7 @@ nat1to1: <ip>
- 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
- 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`
@ -167,7 +167,7 @@ Flags:
--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")
--device string audio device to capture (default "audio_output.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)

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

@ -42,9 +42,9 @@ server {
After successfully installing and running neko, you might want to get rid of the port in the url, use DNS instead of IP address and also having SSL.
This will remove the port from the URL and also enables HTTPS.
To do this, you have to get running apache server. Now you can go into the `/etc/apache2/sites-available` folder and create new config file for example `neko.conf`
After creating new config file, you can use this example config and paste it in. Some thing might vary on your machine so read through and modify if needed.
Bear in mind that your neko server doesn't have to run on the same computer as apache. They just have to be on the same network and then you replace localhost with correct internal IP.
To do this, you have to get running Apache server. Now you can go into the `/etc/apache2/sites-available` folder and create new config file for example `neko.conf`
After creating new config file, you can use this example config and paste it in. Some things may vary on your machine so read through and modify if needed.
Bear in mind that your neko server doesn't have to run on the same computer as Apache. They just have to be on the same network, and then you replace localhost with correct internal IP.
```xml
<VirtualHost *:80>

View File

@ -1,9 +1,13 @@
# Troubleshooting
Neko UI loads but you don't see the screen and it gives you `connection timeout` or `disconnected` error?
Neko UI loads, but you don't see the screen, and it gives you `connection timeout` or `disconnected` error?
## 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
@ -38,7 +42,7 @@ services:
Ensure, that your ports are reachable through your external IP.
To validate UDP connection the simpliest way, run this on your server:
To validate UDP connection the simplest way, run this on your server:
```shell
nc -ul 52101
@ -57,7 +61,7 @@ If it does not work for you, then most likely your port forwarding is not workin
### Check if your external IP was determined correctly
One of the first logs, when the server starts, writes down your external IP that will be sent to your clients to conenct to.
One of the first logs, when the server starts, writes down your external IP that will be sent to your clients to connect to.
```shell
docker-compose logs neko | grep nat_ips
@ -69,7 +73,7 @@ You should see this:
11:11AM INF webrtc starting ephemeral_port_range=52000-52100 ice_lite=true ice_servers="[{URLs:[stun:stun.l.google.com:19302] Username: Credential:<nil> CredentialType:password}]" module=webrtc nat_ips=<your-IP>
```
If your IP is not correct, you can specify own IP resover using `NEKO_IPFETCH`. It needs to return IP address that will be used.
If your IP is not correct, you can specify own IP resolver using `NEKO_IPFETCH`. It needs to return IP address that will be used.
```diff
version: "3.4"
@ -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 probably 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 correctly (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`.
@ -178,7 +201,7 @@ Check if your TCP port is exposed correctly and your reverse proxy is correctly
Getting black screen with a cursor, but no browser.
```
Most likely you forgot to add `-cap-add=SYS_ADMIN` when using chromium-based brwosers.
Most likely you forgot to add `-cap-add=SYS_ADMIN` when using chromium-based browsers.
### Unrelated server errors

View File

@ -3,8 +3,22 @@
set -ex
BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
GIT_COMMIT=`git rev-parse --short HEAD`
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
go build -o bin/neko -ldflags "-s -X 'm1k1o/neko.buildDate=${BUILD_TIME}' -X 'm1k1o/neko.gitCommit=${GIT_DIRTY}${GIT_COMMIT}' -X 'm1k1o/neko.gitBranch=${GIT_BRANCH}'" -i cmd/neko/main.go
#
# set git build variables if git exists
if git status > /dev/null 2>&1 && [ -z $GIT_COMMIT ] && [ -z $GIT_BRANCH ] && [ -z $GIT_DIRTY ];
then
GIT_COMMIT=`git rev-parse --short HEAD`
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
fi
go build \
-o bin/neko \
-ldflags "
-s -w
-X 'm1k1o/neko.buildDate=${BUILD_TIME}'
-X 'm1k1o/neko.gitCommit=${GIT_DIRTY}${GIT_COMMIT}'
-X 'm1k1o/neko.gitBranch=${GIT_BRANCH}'
" \
cmd/neko/main.go;

View File

@ -1,58 +1,55 @@
module m1k1o/neko
go 1.18
go 1.20
require (
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.11 // indirect
github.com/pion/ice/v2 v2.3.0
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.47
github.com/pion/srtp/v2 v2.0.12 // indirect
github.com/pion/webrtc/v3 v3.1.55
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.28.0
github.com/rs/zerolog v1.29.0
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.6.0
github.com/spf13/viper v1.13.0
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.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.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/magiconair/properties v1.8.6 // 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.16 // 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.5 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/mdns v0.0.5 // 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.2.6 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.10 // indirect
github.com/pion/sctp v1.8.3 // 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/spf13/afero v1.9.2 // indirect
github.com/pion/stun v0.4.0 // indirect
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/turn/v2 v2.1.0 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/spf13/afero v1.9.4 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -111,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=
@ -141,12 +141,11 @@ 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.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
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=
@ -157,14 +156,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
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-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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
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=
@ -176,51 +176,49 @@ 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.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
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.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.11 h1:wiAy7TSrVZ4KdyjC0CcNTkwltz9ywetbe4wbHLKUbIg=
github.com/pion/ice/v2 v2.2.11/go.mod h1:NqUDUao6SjSs1+4jrqpexDmFlptlVhGxQjcymXLaVvE=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
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.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
github.com/pion/ice/v2 v2.3.0 h1:G+ysriabk1p9wbySDpdsnlD+6ZspLlDLagRduRfzJPk=
github.com/pion/ice/v2 v2.3.0/go.mod h1:+xO/cXVnnVUr6D2ZJcCT5g9LngucUkkTvfnTMqUxKRM=
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
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/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.3 h1:LWcciN2ptLkw9Ugp/Ks2E76fiWy7yk3Wm79D6oFbFNo=
github.com/pion/sctp v1.8.3/go.mod h1:OHbDjdk7kg+L+7TJim9q/qGVefdEJohuA2SZyihccgI=
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 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0/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/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v3 v3.1.47 h1:2dFEKRI1rzFvehXDq43hK9OGGyTGJSusUi3j6QKHC5s=
github.com/pion/webrtc/v3 v3.1.47/go.mod h1:8U39MYZCLVV4sIBn01htASVNkWQN2zDa/rx5xisEXWs=
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
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/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.0.1/go.mod h1:93OYg91+mrGxKW+Jrgzmqr80kgXqD7J0yybOrdr7w0Y=
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pion/webrtc/v3 v3.1.55 h1:jQt98hZ8DUi/l/s/rtogthBdsKKvKekFgZCX9hMEqRo=
github.com/pion/webrtc/v3 v3.1.55/go.mod h1:M1gU5mnvvo4e1nnLvF23esYz0nZAFOtbU/wq44MSfbc=
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=
@ -232,38 +230,40 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
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/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.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.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
github.com/spf13/afero v1.9.4/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.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
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.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
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.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
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=
@ -276,11 +276,11 @@ 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-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
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=
@ -314,6 +314,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=
@ -344,20 +345,16 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-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-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-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
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.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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=
@ -377,6 +374,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=
@ -419,18 +417,20 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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-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-20220728004956-3c1f35247d10/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.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
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.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.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.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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=
@ -439,8 +439,10 @@ 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/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.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=
@ -492,6 +494,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=
@ -599,7 +602,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
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

@ -35,7 +35,13 @@ 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"),
}
}
@ -47,31 +53,43 @@ 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 {

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

@ -56,6 +56,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 {
@ -86,7 +92,7 @@ func (Capture) Init(cmd *cobra.Command) error {
// audio
//
cmd.PersistentFlags().String("device", "auto_null.monitor", "audio device to capture")
cmd.PersistentFlags().String("device", "audio_output.monitor", "audio device to capture")
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
return err
}
@ -173,6 +179,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 := ""

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

@ -10,13 +10,24 @@ import "C"
import (
"unsafe"
"github.com/kataras/go-events"
"m1k1o/neko/internal/types"
)
var Emmiter events.EventEmmiter
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) {
@ -26,14 +37,19 @@ 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
@ -48,7 +64,12 @@ func goXEventUnmapNotify(window C.Window) {
//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

@ -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
}
@ -127,6 +131,17 @@ func (session *Session) SignalLocalAnswer(sdp string) error {
})
}
func (session *Session) SignalLocalCandidate(data string) error {
if session.socket == nil {
return nil
}
session.logger.Info().Msg("signal update - LocalCandidate")
return session.socket.Send(&message.SignalCandidate{
Event: event.SIGNAL_CANDIDATE,
Data: data,
})
}
func (session *Session) SignalRemoteOffer(sdp string) error {
if session.peer == nil {
return nil
@ -150,14 +165,12 @@ func (session *Session) SignalRemoteAnswer(sdp string) error {
return session.peer.SetAnswer(sdp)
}
func (session *Session) SignalCandidate(data string) error {
func (session *Session) SignalRemoteCandidate(data string) error {
if session.socket == nil {
return nil
}
return session.socket.Send(&message.SignalCandidate{
Event: event.SIGNAL_CANDIDATE,
Data: data,
})
session.logger.Info().Msg("signal update - RemoteCandidate")
return session.peer.SetCandidate(data)
}
func (session *Session) destroy() error {

View File

@ -19,13 +19,13 @@ type BroadcastManager interface {
type StreamSinkManager interface {
Codec() codec.RTPCodec
OnSample(listener func(sample Sample))
AddListener() error
RemoveListener() error
ListenersCount() int
Started() bool
GetSampleChannel() chan Sample
}
type CaptureManager interface {

View File

@ -31,6 +31,8 @@ func ParseStr(codecName string) (codec RTPCodec, ok bool) {
codec = VP8()
case VP9().Name:
codec = VP9()
case AV1().Name:
codec = AV1()
case H264().Name:
codec = H264()
case Opus().Name:
@ -62,6 +64,14 @@ func (codec RTPCodec) Register(engine *webrtc.MediaEngine) error {
}, codec.Type)
}
func (codec RTPCodec) IsVideo() bool {
return codec.Type == webrtc.RTPCodecTypeVideo
}
func (codec RTPCodec) IsAudio() bool {
return codec.Type == webrtc.RTPCodecTypeAudio
}
func VP8() RTPCodec {
return RTPCodec{
Name: "vp8",
@ -109,6 +119,22 @@ func H264() RTPCodec {
}
}
// TODO: Profile ID.
func AV1() RTPCodec {
return RTPCodec{
Name: "av1",
PayloadType: 96,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: RTCPFeedback,
},
}
}
func Opus() RTPCodec {
return RTPCodec{
Name: "opus",
@ -118,7 +144,7 @@ func Opus() RTPCodec {
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "useinbandfec=1",
SDPFmtpLine: "useinbandfec=1;stereo=1",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
}

View File

@ -33,11 +33,17 @@ type KeyboardMap struct {
Variant string
}
type DesktopErrorMessage struct {
Error_code uint8
Message string
Request_code uint8
Minor_code uint8
}
type DesktopManager interface {
Start()
Shutdown() error
OnBeforeScreenSizeChange(listener func())
OnAfterScreenSizeChange(listener func())
GetScreenSizeChangeChannel() (before chan bool) // true - before, false - after
// clipboard
ReadClipboard() string
@ -65,9 +71,7 @@ type DesktopManager interface {
GetScreenshotImage() *image.RGBA
// xevent
OnCursorChanged(listener func(serial uint64))
OnClipboardUpdated(listener func())
OnFileChooserDialogOpened(listener func())
OnFileChooserDialogClosed(listener func())
OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8))
GetCursorChangedChannel() chan uint64
GetClipboardUpdatedChannel() chan struct{}
GetEventErrorChannel() chan DesktopErrorMessage
}

View File

@ -7,6 +7,22 @@ type Member struct {
Muted bool `json:"muted"`
}
type SessionEventType int
const (
SESSION_CREATED SessionEventType = iota
SESSION_CONNECTED
SESSION_DESTROYED
SESSION_HOST_SET
SESSION_HOST_CLEARED
)
type SessionEvent struct {
Type SessionEventType
Id string
Session Session
}
type Session interface {
ID() string
Name() string
@ -24,9 +40,10 @@ type Session interface {
Send(v interface{}) error
SignalLocalOffer(sdp string) error
SignalLocalAnswer(sdp string) error
SignalLocalCandidate(data string) error
SignalRemoteOffer(sdp string) error
SignalRemoteAnswer(sdp string) error
SignalCandidate(data string) error
SignalRemoteCandidate(data string) error
}
type SessionManager interface {
@ -46,9 +63,5 @@ type SessionManager interface {
Clear() error
Broadcast(v interface{}, exclude interface{}) error
AdminBroadcast(v interface{}, exclude interface{}) error
OnHost(listener func(id string))
OnHostCleared(listener func(id string))
OnDestroy(listener func(id string, session Session))
OnCreated(listener func(id string, session Session))
OnConnected(listener func(id string, session Session))
GetEventsChannel() chan SessionEvent
}

View File

@ -21,6 +21,7 @@ type Peer interface {
CreateAnswer() (string, error)
SetOffer(sdp string) error
SetAnswer(sdp string) error
SetCandidate(candidateString string) error
WriteData(v interface{}) error
Destroy() error
}

View File

@ -1,6 +1,7 @@
package webrtc
import (
"encoding/json"
"sync"
"github.com/pion/webrtc/v3"
@ -49,6 +50,16 @@ func (peer *Peer) SetAnswer(sdp string) error {
return peer.connection.SetRemoteDescription(webrtc.SessionDescription{SDP: sdp, Type: webrtc.SDPTypeAnswer})
}
func (peer *Peer) SetCandidate(candidateString string) error {
var candidate webrtc.ICECandidateInit
err := json.Unmarshal([]byte(candidateString), &candidate)
if err != nil {
return err
}
return peer.connection.AddICECandidate(candidate)
}
func (peer *Peer) WriteData(v interface{}) error {
peer.mu.Lock()
defer peer.mu.Unlock()

View File

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/pion/ice/v2"
"github.com/pion/interceptor"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media"
@ -54,12 +55,20 @@ func (manager *WebRTCManager) Start() {
manager.logger.Panic().Err(err).Msg("unable to create audio track")
}
manager.capture.Audio().OnSample(func(sample types.Sample) {
err := manager.audioTrack.WriteSample(media.Sample(sample))
if err != nil && errors.Is(err, io.ErrClosedPipe) {
manager.logger.Warn().Err(err).Msg("audio pipeline failed to write")
go func() {
for {
sample, ok := <-manager.capture.Audio().GetSampleChannel()
if !ok {
manager.logger.Debug().Msg("audio capture channel is closed")
continue
}
err := manager.audioTrack.WriteSample(media.Sample(sample))
if err != nil && errors.Is(err, io.ErrClosedPipe) {
manager.logger.Warn().Err(err).Msg("audio pipeline failed to write")
}
}
})
}()
//
// video
@ -71,12 +80,20 @@ func (manager *WebRTCManager) Start() {
manager.logger.Panic().Err(err).Msg("unable to create video track")
}
manager.capture.Video().OnSample(func(sample types.Sample) {
err := manager.videoTrack.WriteSample(media.Sample(sample))
if err != nil && errors.Is(err, io.ErrClosedPipe) {
manager.logger.Warn().Err(err).Msg("video pipeline failed to write")
go func() {
for {
sample, ok := <-manager.capture.Video().GetSampleChannel()
if !ok {
manager.logger.Debug().Msg("video capture channel is closed")
continue
}
err := manager.videoTrack.WriteSample(media.Sample(sample))
if err != nil && errors.Is(err, io.ErrClosedPipe) {
manager.logger.Warn().Err(err).Msg("video pipeline failed to write")
}
}
})
}()
//
// api
@ -106,7 +123,6 @@ func (manager *WebRTCManager) initAPI() error {
LoggerFactory: logger,
}
_ = settings.SetEphemeralUDPPortRange(manager.config.EphemeralMin, manager.config.EphemeralMax)
settings.SetNAT1To1IPs(manager.config.NAT1To1IPs, webrtc.ICECandidateTypeHost)
settings.SetICETimeouts(6*time.Second, 6*time.Second, 3*time.Second)
settings.SetSRTPReplayProtectionWindow(512)
@ -125,7 +141,12 @@ func (manager *WebRTCManager) initAPI() error {
return err
}
tcpMux := webrtc.NewICETCPMux(logger.NewLogger("ice-tcp"), tcpListener, 32)
tcpMux := ice.NewTCPMuxDefault(ice.TCPMuxParams{
Listener: tcpListener,
Logger: logger.NewLogger("ice-tcp"),
ReadBufferSize: 32, // receiving channel size
WriteBufferSize: 4 * 1024 * 1024, // write buffer size, 4MB
})
settings.SetICETCPMux(tcpMux)
networkType = append(networkType, webrtc.NetworkTypeTCP4)
@ -134,26 +155,27 @@ func (manager *WebRTCManager) initAPI() error {
// Add UDP Mux
if manager.config.UDPMUX > 0 {
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IP{0, 0, 0, 0},
Port: manager.config.UDPMUX,
})
udpMux, err := ice.NewMultiUDPMuxFromPort(manager.config.UDPMUX,
ice.UDPMuxFromPortWithLogger(logger.NewLogger("ice-udp")),
)
if err != nil {
return err
}
udpMux := webrtc.NewICEUDPMux(logger.NewLogger("ice-udp"), udpListener)
settings.SetICEUDPMux(udpMux)
networkType = append(networkType, webrtc.NetworkTypeUDP4)
manager.logger.Info().Str("listener", udpListener.LocalAddr().String()).Msg("using UDP MUX")
manager.logger.Info().Int("port", manager.config.UDPMUX).Msg("using UDP MUX")
} else if manager.config.EphemeralMax != 0 {
_ = settings.SetEphemeralUDPPortRange(manager.config.EphemeralMin, manager.config.EphemeralMax)
networkType = append(networkType,
webrtc.NetworkTypeUDP4,
webrtc.NetworkTypeUDP6,
)
}
// Enable support for TCP and UDP ICE candidates
if len(networkType) > 0 {
settings.SetNetworkTypes(networkType)
}
settings.SetNetworkTypes(networkType)
// Create MediaEngine with selected codecs
engine := webrtc.MediaEngine{}
@ -279,7 +301,7 @@ func (manager *WebRTCManager) CreatePeer(id string, session types.Session) (type
return
}
if err := session.SignalCandidate(string(candidateString)); err != nil {
if err := session.SignalLocalCandidate(string(candidateString)); err != nil {
manager.logger.Warn().Err(err).Msg("sending SignalCandidate failed")
return
}

View File

@ -87,6 +87,12 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
utils.Unmarshal(payload, raw, func() error {
return h.signalRemoteAnswer(id, session, payload)
}), "%s failed", header.Event)
case event.SIGNAL_CANDIDATE:
payload := &message.SignalCandidate{}
return errors.Wrapf(
utils.Unmarshal(payload, raw, func() error {
return h.signalRemoteCandidate(id, session, payload)
}), "%s failed", header.Event)
// Control Events
case event.CONTROL_RELEASE:

View File

@ -45,3 +45,7 @@ func (h *MessageHandler) signalRemoteAnswer(id string, session types.Session, pa
return nil
}
func (h *MessageHandler) signalRemoteCandidate(id string, session types.Session, payload *message.SignalCandidate) error {
return session.SignalRemoteCandidate(payload.Data)
}

View File

@ -101,102 +101,121 @@ type WebSocketHandler struct {
}
func (ws *WebSocketHandler) Start() {
ws.sessions.OnCreated(func(id string, session types.Session) {
if err := ws.handler.SessionCreated(id, session); err != nil {
ws.logger.Warn().Str("id", id).Err(err).Msg("session created with and error")
} else {
ws.logger.Debug().Str("id", id).Msg("session created")
}
})
go func() {
for {
e, ok := <-ws.sessions.GetEventsChannel()
if !ok {
ws.logger.Info().Msg("session channel was closed")
return
}
ws.sessions.OnConnected(func(id string, session types.Session) {
if err := ws.handler.SessionConnected(id, session); err != nil {
ws.logger.Warn().Str("id", id).Err(err).Msg("session connected with and error")
} else {
ws.logger.Debug().Str("id", id).Msg("session connected")
}
switch e.Type {
case types.SESSION_CREATED:
if err := ws.handler.SessionCreated(e.Id, e.Session); err != nil {
ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session created with and error")
} else {
ws.logger.Debug().Str("id", e.Id).Msg("session created")
}
case types.SESSION_CONNECTED:
if err := ws.handler.SessionConnected(e.Id, e.Session); err != nil {
ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session connected with and error")
} else {
ws.logger.Debug().Str("id", e.Id).Msg("session connected")
}
// if control protection is enabled and at least one admin
// and if room was locked on behalf control protection, unlock
sess, ok := ws.state.GetLocked("control")
if ok && ws.conf.ControlProtection && sess == CONTROL_PROTECTION_SESSION && len(ws.sessions.Admins()) > 0 {
ws.state.Unlock("control")
ws.sessions.SetControlLocked(false) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control unlocked on behalf of control protection")
// if control protection is enabled and at least one admin
// and if room was locked on behalf control protection, unlock
sess, ok := ws.state.GetLocked("control")
if ok && ws.conf.ControlProtection && sess == CONTROL_PROTECTION_SESSION && len(ws.sessions.Admins()) > 0 {
ws.state.Unlock("control")
ws.sessions.SetControlLocked(false) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control unlocked on behalf of control protection")
if err := ws.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_UNLOCK,
ID: id,
Resource: "control",
}, nil); err != nil {
ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK)
if err := ws.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_UNLOCK,
ID: e.Id,
Resource: "control",
}, nil); err != nil {
ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK)
}
}
// remove outdated stats
if e.Session.Admin() {
ws.lastAdminLeftAt = nil
} else {
ws.lastUserLeftAt = nil
}
case types.SESSION_DESTROYED:
if err := ws.handler.SessionDestroyed(e.Id); err != nil {
ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session destroyed with and error")
} else {
ws.logger.Debug().Str("id", e.Id).Msg("session destroyed")
}
membersCount := len(ws.sessions.Members())
adminCount := len(ws.sessions.Admins())
// if control protection is enabled and no admin
// and room is not locked, lock
ok := ws.state.IsLocked("control")
if !ok && ws.conf.ControlProtection && adminCount == 0 {
ws.state.Lock("control", CONTROL_PROTECTION_SESSION)
ws.sessions.SetControlLocked(true) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control locked and released on behalf of control protection")
ws.handler.AdminRelease(e.Id, e.Session)
if err := ws.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_LOCK,
ID: e.Id,
Resource: "control",
}, nil); err != nil {
ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK)
}
}
// if this was the last admin
if e.Session.Admin() && adminCount == 0 {
now := time.Now()
ws.lastAdminLeftAt = &now
}
// if this was the last user
if !e.Session.Admin() && membersCount-adminCount == 0 {
now := time.Now()
ws.lastUserLeftAt = &now
}
case types.SESSION_HOST_SET:
// TODO: Unused.
case types.SESSION_HOST_CLEARED:
// TODO: Unused.
}
}
}()
// remove outdated stats
if session.Admin() {
ws.lastAdminLeftAt = nil
} else {
ws.lastUserLeftAt = nil
}
})
ws.sessions.OnDestroy(func(id string, session types.Session) {
if err := ws.handler.SessionDestroyed(id); err != nil {
ws.logger.Warn().Str("id", id).Err(err).Msg("session destroyed with and error")
} else {
ws.logger.Debug().Str("id", id).Msg("session destroyed")
}
membersCount := len(ws.sessions.Members())
adminCount := len(ws.sessions.Admins())
// if control protection is enabled and no admin
// and room is not locked, lock
ok := ws.state.IsLocked("control")
if !ok && ws.conf.ControlProtection && adminCount == 0 {
ws.state.Lock("control", CONTROL_PROTECTION_SESSION)
ws.sessions.SetControlLocked(true) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control locked and released on behalf of control protection")
ws.handler.AdminRelease(id, session)
if err := ws.sessions.Broadcast(
message.AdminLock{
Event: event.ADMIN_LOCK,
ID: id,
Resource: "control",
}, nil); err != nil {
ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK)
go func() {
for {
_, ok := <-ws.desktop.GetClipboardUpdatedChannel()
if !ok {
ws.logger.Info().Msg("clipboard update channel closed")
return
}
session, ok := ws.sessions.GetHost()
if !ok {
return
}
err := session.Send(message.Clipboard{
Event: event.CONTROL_CLIPBOARD,
Text: ws.desktop.ReadClipboard(),
})
ws.logger.Err(err).Msg("sync clipboard")
}
// if this was the last admin
if session.Admin() && adminCount == 0 {
now := time.Now()
ws.lastAdminLeftAt = &now
}
// if this was the last user
if !session.Admin() && membersCount-adminCount == 0 {
now := time.Now()
ws.lastUserLeftAt = &now
}
})
ws.desktop.OnClipboardUpdated(func() {
session, ok := ws.sessions.GetHost()
if !ok {
return
}
err := session.Send(message.Clipboard{
Event: event.CONTROL_CLIPBOARD,
Text: ws.desktop.ReadClipboard(),
})
ws.logger.Err(err).Msg("sync clipboard")
})
}()
// watch for file changes and send file list if file transfer is enabled
if ws.conf.FileTransferEnabled {