mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
950095d6d8 | |||
009bc20969 | |||
395db0fd54 | |||
13fa86d543 | |||
8308c13382 | |||
ec175909a3 | |||
d2f51fa10f | |||
70325e0277 | |||
bdff0841ec | |||
3c17dbe282 | |||
887413d536 | |||
f9228e653d | |||
df634be1c5 | |||
2130daa02d | |||
aee7650d47 | |||
98ba32c574 | |||
d08d3eccef | |||
0dd9597519 | |||
334fcef407 | |||
e868ad4061 | |||
b41d0bf956 | |||
b62fa6ab8b | |||
8dba9cff44 | |||
217cc451ea | |||
1f81bd3efc | |||
50e5483661 | |||
d2765c30fd | |||
76fc892823 | |||
ea99ce7753 | |||
646eaced29 | |||
9104953ad9 | |||
c40243635f | |||
8059002475 | |||
9daf83cc52 | |||
0cebe465a2 | |||
d9403d9c14 | |||
8604a30744 | |||
957e893cf6 |
@ -79,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; \
|
||||
@ -88,10 +90,6 @@ RUN set -eux; \
|
||||
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; \
|
||||
@ -118,6 +116,7 @@ COPY .docker/base/xorg.conf /etc/neko/xorg.conf
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
|
@ -85,7 +85,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; \
|
||||
@ -94,10 +96,6 @@ RUN set -eux; \
|
||||
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; \
|
||||
@ -124,6 +122,7 @@ COPY .docker/base/xorg.conf /etc/neko/xorg.conf
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
|
@ -88,7 +88,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; \
|
||||
@ -97,10 +99,6 @@ RUN set -eux; \
|
||||
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; \
|
||||
@ -128,9 +126,11 @@ 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 PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
ENV NEKO_HWENC=VAAPI
|
||||
ENV RENDER_GID=
|
||||
|
||||
#
|
||||
|
@ -1,10 +1,62 @@
|
||||
ARG UBUNTU_RELEASE=20.04
|
||||
ARG CUDA_VERSION=11.2.2
|
||||
ARG CUDA_VERSION=11.4.3
|
||||
ARG VIRTUALGL_VERSION=3.1
|
||||
ARG GSTREAMER_VERSION=1.20
|
||||
|
||||
#
|
||||
# STAGE 0: Build gstreamer with nvidia plugins.
|
||||
#
|
||||
FROM ubuntu:${UBUNTU_RELEASE} AS gstreamer
|
||||
ARG GSTREAMER_VERSION
|
||||
|
||||
#
|
||||
# install dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Install essentials
|
||||
curl build-essential ca-certificates git \
|
||||
# Install pip and ninja
|
||||
python3-pip python-gi-dev ninja-build \
|
||||
# Install build deps
|
||||
autopoint autoconf automake autotools-dev libtool gettext bison flex gtk-doc-tools \
|
||||
# Install libraries
|
||||
librtmp-dev \
|
||||
libvo-aacenc-dev \
|
||||
libtool-bin \
|
||||
libgtk2.0-dev \
|
||||
libgl1-mesa-dev \
|
||||
libopus-dev \
|
||||
libpulse-dev \
|
||||
libssl-dev \
|
||||
libx264-dev \
|
||||
libvpx-dev; \
|
||||
# Install meson
|
||||
pip3 install meson; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# build gstreamer
|
||||
RUN set -eux; \
|
||||
git clone --depth 1 --branch $GSTREAMER_VERSION https://gitlab.freedesktop.org/gstreamer/gstreamer.git /gstreamer/src; \
|
||||
cd /gstreamer/src; \
|
||||
mkdir -p /opt/gstreamer; \
|
||||
meson --prefix /opt/gstreamer \
|
||||
-Dgpl=enabled \
|
||||
-Dugly=enabled \
|
||||
-Dgst-plugins-ugly:x264=enabled \
|
||||
build; \
|
||||
ninja -C build; \
|
||||
meson install -C build;
|
||||
|
||||
#
|
||||
# STAGE 1: SERVER
|
||||
#
|
||||
FROM golang:1.18-bullseye as server
|
||||
FROM golang:1.20-bullseye as server
|
||||
WORKDIR /src
|
||||
|
||||
#
|
||||
@ -35,7 +87,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
|
||||
|
||||
#
|
||||
@ -51,13 +103,13 @@ RUN npm run build
|
||||
#
|
||||
# STAGE 3: RUNTIME
|
||||
#
|
||||
FROM nvcr.io/nvidia/cudagl:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_RELEASE} as runtime
|
||||
FROM nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${UBUNTU_RELEASE} as runtime
|
||||
|
||||
ARG UBUNTU_RELEASE
|
||||
ARG CUDA_VERSION
|
||||
ARG VIRTUALGL_VERSION
|
||||
|
||||
# Make all NVIDIA GPUs visible by default
|
||||
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||
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
|
||||
|
||||
@ -75,20 +127,55 @@ ARG USERNAME=neko
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
RUN set -eux; \
|
||||
dpkg --add-architecture i386; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# opengl base: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/base/Dockerfile
|
||||
libxau6 libxau6:i386 \
|
||||
libxdmcp6 libxdmcp6:i386 \
|
||||
libxcb1 libxcb1:i386 \
|
||||
libxext6 libxext6:i386 \
|
||||
libx11-6 libx11-6:i386 \
|
||||
# opengl runtime: https://gitlab.com/nvidia/container-images/opengl/-/blob/ubuntu20.04/glvnd/runtime/Dockerfile
|
||||
libglvnd0 libglvnd0:i386 \
|
||||
libgl1 libgl1:i386 \
|
||||
libglx0 libglx0:i386 \
|
||||
libegl1 libegl1:i386 \
|
||||
libgles2 libgles2:i386 \
|
||||
# hardware accleration utilities
|
||||
libglu1 libglu1:i386 \
|
||||
libvulkan-dev libvulkan-dev:i386 \
|
||||
mesa-utils mesa-utils-extra \
|
||||
mesa-va-drivers mesa-vulkan-drivers \
|
||||
vainfo vdpauinfo; \
|
||||
#
|
||||
# install vulkan-utils or vulkan-tools depending on ubuntu release
|
||||
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; \
|
||||
#
|
||||
# create symlink for libnvrtc.so (needed for cudaconvert)
|
||||
find /usr/local/cuda/lib64/ -maxdepth 1 -type l -name "*libnvrtc.so.*" -exec sh -c 'ln -sf {} /usr/local/cuda/lib64/libnvrtc.so' \;; \
|
||||
#
|
||||
# clean up
|
||||
apt-get clean -y; \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
|
||||
#
|
||||
# add cuda to ld path, for gstreamer cuda plugins
|
||||
ENV LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}:/usr/local/cuda/lib:/usr/local/cuda/lib64"
|
||||
|
||||
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; \
|
||||
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6 libx264-155 libvo-aacenc0 librtmp1; \
|
||||
apt-get install -y --no-install-recommends libgtk-3-bin software-properties-common cabextract aptitude vim curl; \
|
||||
#
|
||||
# install fonts
|
||||
apt-get install -y --no-install-recommends \
|
||||
@ -97,7 +184,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; \
|
||||
@ -106,10 +195,6 @@ RUN set -eux; \
|
||||
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; \
|
||||
@ -126,18 +211,18 @@ RUN set -eux; \
|
||||
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/; \
|
||||
# configure EGL and Vulkan manually
|
||||
RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)') && \
|
||||
# Configure EGL manually
|
||||
mkdir -p /usr/share/glvnd/egl_vendor.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
\"library_path\": \"libEGL_nvidia.so.0\"\n\
|
||||
}\n\
|
||||
}" > /usr/share/glvnd/egl_vendor.d/10_nvidia.json && \
|
||||
# Configure Vulkan manually
|
||||
mkdir -p /etc/vulkan/icd.d/ && \
|
||||
echo "{\n\
|
||||
\"file_format_version\" : \"1.0.0\",\n\
|
||||
\"ICD\": {\n\
|
||||
@ -148,7 +233,6 @@ RUN set -eux; \
|
||||
|
||||
#
|
||||
# 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"; \
|
||||
@ -178,10 +262,21 @@ COPY .docker/base/nvidia/entrypoint.sh /bin/entrypoint.sh
|
||||
# set default envs
|
||||
ENV USER=$USERNAME
|
||||
ENV DISPLAY=:99.0
|
||||
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
ENV NEKO_PASSWORD=neko
|
||||
ENV NEKO_PASSWORD_ADMIN=admin
|
||||
ENV NEKO_BIND=:8080
|
||||
|
||||
#
|
||||
# set gstreamer envs
|
||||
ENV PATH="/opt/gstreamer/bin:${PATH}"
|
||||
ENV LD_LIBRARY_PATH="/opt/gstreamer/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
ENV PKG_CONFIG_PATH="/opt/gstreamer/lib/x86_64-linux-gnu/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"
|
||||
|
||||
#
|
||||
# copy gstreamer from previous stage
|
||||
COPY --from=gstreamer /opt/gstreamer /opt/gstreamer
|
||||
|
||||
#
|
||||
# copy static files from previous stages
|
||||
COPY --from=server /src/bin/neko /usr/bin/neko
|
||||
|
@ -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
|
||||
|
||||
|
@ -4,9 +4,13 @@ FROM $BASE_IMAGE
|
||||
#
|
||||
# install neko chromium
|
||||
RUN set -eux; \
|
||||
echo "deb http://ftp.de.debian.org/debian bookworm main" >> /etc/apt/sources.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends unzip chromium chromium-common chromium-sandbox openbox; \
|
||||
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"; \
|
||||
|
@ -15,6 +15,11 @@ RUN set -eux; apt-get update; \
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -17,9 +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" \
|
||||
|
@ -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,32 @@
|
||||
|
||||
shakeKbd = false
|
||||
|
||||
get hideControls() {
|
||||
get volume() {
|
||||
const numberParam = parseFloat(new URL(location.href).searchParams.get('volume') || '1.0')
|
||||
return Math.max(0.0, Math.min(!isNaN(numberParam) ? numberParam * 100 : 100, 100))
|
||||
}
|
||||
|
||||
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('volume', { immediate: true })
|
||||
onVolume(volume: number) {
|
||||
this.$accessor.video.setVolume(volume)
|
||||
}
|
||||
|
||||
@Watch('hideControls', { immediate: true })
|
||||
onHideControls(enabled: boolean) {
|
||||
if (enabled) {
|
||||
|
@ -21,26 +21,29 @@
|
||||
@mouseup.stop.prevent="onMouseUp"
|
||||
@mouseenter.stop.prevent="onMouseEnter"
|
||||
@mouseleave.stop.prevent="onMouseLeave"
|
||||
@touchmove.stop.prevent="onTouchHandler"
|
||||
@touchstart.stop.prevent="onTouchHandler"
|
||||
@touchend.stop.prevent="onTouchHandler"
|
||||
/>
|
||||
<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" />
|
||||
</div>
|
||||
<ul v-if="!fullscreen" class="video-menu top">
|
||||
<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="hideControls || '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"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="!fullscreen" class="video-menu bottom">
|
||||
<ul v-if="!fullscreen && !hideControls" class="video-menu bottom">
|
||||
<li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)">
|
||||
<i @click.stop.prevent="openClipboard" class="fas fa-clipboard"></i>
|
||||
</li>
|
||||
@ -106,12 +109,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;
|
||||
}
|
||||
}
|
||||
@ -223,13 +226,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() {
|
||||
@ -361,7 +366,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 +388,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) {
|
||||
@ -424,11 +448,6 @@
|
||||
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()
|
||||
})
|
||||
@ -560,6 +579,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
playAndUnmute() {
|
||||
this.$accessor.video.play()
|
||||
this.$accessor.video.setMuted(false)
|
||||
}
|
||||
|
||||
unmute() {
|
||||
this.$accessor.video.setMuted(false)
|
||||
}
|
||||
@ -667,6 +691,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
onTouchHandler(e: TouchEvent) {
|
||||
let first = e.changedTouches[0]
|
||||
let type = ''
|
||||
switch (e.type) {
|
||||
case 'touchstart':
|
||||
type = 'mousedown'
|
||||
break
|
||||
case 'touchmove':
|
||||
type = 'mousemove'
|
||||
break
|
||||
case 'touchend':
|
||||
type = 'mouseup'
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const simulatedEvent = new MouseEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
screenX: first.screenX,
|
||||
screenY: first.screenY,
|
||||
clientX: first.clientX,
|
||||
clientY: first.clientY,
|
||||
})
|
||||
first.target.dispatchEvent(simulatedEvent)
|
||||
}
|
||||
|
||||
onMouseDown(e: MouseEvent) {
|
||||
if (!this.hosting) {
|
||||
this.$emit('control-attempt', e)
|
||||
|
@ -203,11 +203,12 @@ 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 = () => {
|
||||
@ -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,10 +295,10 @@ 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 = []
|
||||
|
||||
@ -310,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) {
|
||||
|
@ -2,20 +2,34 @@
|
||||
|
||||
## master branch
|
||||
|
||||
## [n.eko v2.8.0](https://github.com/m1k1o/neko/releases/tag/v2.8.0)
|
||||
|
||||
### 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.
|
||||
- Added `?volume=<0-1>` parameter to the URL, which will set the inital volume of the player (by @urbanekpj).
|
||||
- Touch events are now supported on mobile devices (by @urbanekpj).
|
||||
- Added NVENC support, hardware h264 encoding for Nvidia GPUs!
|
||||
- Fixed an issue where `nvh264enc` did not send SPS and PPS NAL units (by @mbattista).
|
||||
|
||||
### Bugs
|
||||
- Fixed TCP mux occasional freeze by adding write buffer to it.
|
||||
- Fixed stereo problem in chromium-based browsers, where it was only as mono by adding `stereo=1` to opus SDP to clients answer.
|
||||
- Fixed keysym mapping for unknown keycodes, which was causing some key combinations to not work on some keyboards.
|
||||
- Fixed a bug where `max_fps=0` would lead to an invalid pipeline.
|
||||
- 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.
|
||||
- Pulseaudio is now configured using environment variables, so that users can mount `/home/neko` without losing audio configuration.
|
||||
|
||||
## [n.eko v2.7](https://github.com/m1k1o/neko/releases/tag/v2.7)
|
||||
|
||||
|
@ -162,6 +162,63 @@ services:
|
||||
"RestoreOnStartup": 1,
|
||||
```
|
||||
|
||||
### Nvidia GPU acceleration
|
||||
|
||||
You need to have [nvidia-docker](https://github.com/NVIDIA/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 \
|
||||
-e NEKO_VIDEO_CODEC=h264 \
|
||||
-e NEKO_HWENC=nvenc \
|
||||
--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
|
||||
NEKO_VIDEO_CODEC: h264
|
||||
NEKO_HWENC: nvenc
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
- You can verify that GPU is available inside the container by running `docker exec -it neko nvidia-smi` command.
|
||||
- You can verify that GPU is used for encoding by searching for `nvh264enc` in `docker logs neko` output.
|
||||
- If you don'ŧ specify `NEKO_HWENC: nvenc` environment variable, CPU encoding will be used but GPU will still be available for browser rendering.
|
||||
|
||||
### Want to use VPN for your n.eko browsing?
|
||||
- Check this out: https://github.com/m1k1o/neko-vpn
|
||||
|
||||
@ -179,6 +236,8 @@ 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.
|
||||
- Adding `?volume=<0-1>` will set volume to given value.
|
||||
- e.g. `http(s)://<URL:Port>/?pwd=neko&usr=guest&cast=1`
|
||||
|
||||
### Screen size
|
||||
|
@ -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.
|
||||
@ -79,13 +79,14 @@ nat1to1: <ip>
|
||||
- `gstreamer1.0-plugins-good`
|
||||
- `gstreamer1.0-plugins-bad`
|
||||
- `gstreamer1.0-plugins-ugly`
|
||||
- e.g. `ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! video/x-raw,format=NV12 ! x264enc threads=4 bitrate=3500 key-int-max=60 vbv-buf-capacity=4000 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream`
|
||||
- e.g. `ximagesrc display-name=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue ! video/x-raw,format=NV12 ! x264enc threads=4 bitrate=3500 key-int-max=60 vbv-buf-capacity=4000 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline`
|
||||
#### `NEKO_MAX_FPS`:
|
||||
- The resulting stream frames per seconds should be capped *(0 for uncapped)*.
|
||||
- e.g. `0`
|
||||
#### `NEKO_HWENC`:
|
||||
- Use hardware accelerated encoding, for now supported only `VAAPI`.
|
||||
- e.g. `VAAPI`
|
||||
- none *(default CPU encoding)*
|
||||
- vaapi
|
||||
- nvenc
|
||||
|
||||
### Audio
|
||||
|
||||
@ -167,7 +168,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)
|
||||
|
@ -95,9 +95,9 @@ services:
|
||||
! videoconvert
|
||||
! queue
|
||||
! video/x-raw,framerate=30/1,format=NV12
|
||||
! v4l2h264enc extra-controls="controls,h264_profile=0,video_bitrate=1250000;"
|
||||
! v4l2h264enc extra-controls="controls,h264_profile=1,video_bitrate=1250000;"
|
||||
! h264parse config-interval=3
|
||||
! video/x-h264,profile=baseline,stream-format=byte-stream
|
||||
! video/x-h264,stream-format=byte-stream,profile=constrained-baseline
|
||||
NEKO_VIDEO_CODEC: h264
|
||||
```
|
||||
|
||||
|
22
server/build
22
server/build
@ -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;
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"m1k1o/neko/internal/capture/gst"
|
||||
"m1k1o/neko/internal/config"
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
)
|
||||
|
||||
@ -52,7 +53,7 @@ func NewBroadcastPipeline(device string, display string, pipelineSrc string, url
|
||||
return pipelineStr, nil
|
||||
}
|
||||
|
||||
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc string) (string, error) {
|
||||
func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc config.HwEnc) (string, error) {
|
||||
pipelineStr := " ! appsink name=appsinkvideo"
|
||||
|
||||
// if using custom pipeline
|
||||
@ -68,7 +69,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
|
||||
|
||||
switch rtpCodec.Name {
|
||||
case codec.VP8().Name:
|
||||
if hwenc == "VAAPI" {
|
||||
if hwenc == config.HwEncVAAPI {
|
||||
if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -138,35 +139,40 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
if hwenc == "VAAPI" {
|
||||
vbvbuf := uint(1000)
|
||||
if bitrate > 1000 {
|
||||
vbvbuf = bitrate
|
||||
}
|
||||
|
||||
if hwenc == config.HwEncVAAPI {
|
||||
if err := gst.CheckPlugins([]string{"vaapi"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate)
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate)
|
||||
} else if hwenc == config.HwEncNVENC {
|
||||
if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! nvh264enc name=encoder preset=2 gop-size=25 spatial-aq=true temporal-aq=true bitrate=%d vbv-buffer-size=%d rc-mode=6 ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf)
|
||||
} else {
|
||||
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
|
||||
// gstreamer1.0-plugins-bad
|
||||
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
|
||||
if err := gst.CheckPlugins([]string{"openh264"}); err == nil {
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=%d max-bitrate=%d ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate*1000, (bitrate+1024)*1000)
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"openh264enc multi-thread=4 complexity=high bitrate=%d max-bitrate=%d ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate*1000, (bitrate+1024)*1000)
|
||||
break
|
||||
}
|
||||
|
||||
// https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
|
||||
// gstreamer1.0-plugins-ugly
|
||||
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream
|
||||
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline
|
||||
if err := gst.CheckPlugins([]string{"x264"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vbvbuf := uint(1000)
|
||||
if bitrate > 1000 {
|
||||
vbvbuf = bitrate
|
||||
}
|
||||
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate, vbvbuf)
|
||||
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate, vbvbuf)
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unknown codec %s", rtpCodec.Name)
|
||||
|
@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"m1k1o/neko/internal/types/codec"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -9,13 +10,21 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type HwEnc int
|
||||
|
||||
const (
|
||||
HwEncNone HwEnc = iota
|
||||
HwEncVAAPI
|
||||
HwEncNVENC
|
||||
)
|
||||
|
||||
type Capture struct {
|
||||
// video
|
||||
Display string
|
||||
VideoCodec codec.RTPCodec
|
||||
VideoHWEnc string // TODO: Pipeline builder.
|
||||
VideoBitrate uint // TODO: Pipeline builder.
|
||||
VideoMaxFPS int16 // TODO: Pipeline builder.
|
||||
VideoHWEnc HwEnc // TODO: Pipeline builder.
|
||||
VideoBitrate uint // TODO: Pipeline builder.
|
||||
VideoMaxFPS int16 // TODO: Pipeline builder.
|
||||
VideoPipeline string
|
||||
|
||||
// audio
|
||||
@ -92,7 +101,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
|
||||
}
|
||||
@ -184,11 +193,19 @@ func (s *Capture) Set() {
|
||||
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_VIDEO_CODEC=av1' instead")
|
||||
}
|
||||
|
||||
videoHWEnc := ""
|
||||
if viper.GetString("hwenc") == "VAAPI" {
|
||||
videoHWEnc = "VAAPI"
|
||||
videoHWEnc := strings.ToLower(viper.GetString("hwenc"))
|
||||
switch videoHWEnc {
|
||||
case "":
|
||||
fallthrough
|
||||
case "none":
|
||||
s.VideoHWEnc = HwEncNone
|
||||
case "vaapi":
|
||||
s.VideoHWEnc = HwEncVAAPI
|
||||
case "nvenc":
|
||||
s.VideoHWEnc = HwEncNVENC
|
||||
default:
|
||||
log.Warn().Str("hwenc", videoHWEnc).Msgf("unknown video hw encoder, using CPU")
|
||||
}
|
||||
s.VideoHWEnc = videoHWEnc
|
||||
|
||||
s.VideoBitrate = viper.GetUint("video_bitrate")
|
||||
s.VideoMaxFPS = int16(viper.GetInt("max_fps"))
|
||||
|
@ -131,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
|
||||
@ -154,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 {
|
||||
|
@ -40,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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -123,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)
|
||||
@ -168,12 +167,15 @@ func (manager *WebRTCManager) initAPI() error {
|
||||
|
||||
networkType = append(networkType, webrtc.NetworkTypeUDP4)
|
||||
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{}
|
||||
@ -299,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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ var (
|
||||
// Major version when you make incompatible API changes,
|
||||
major = "2"
|
||||
// Minor version when you add functionality in a backwards-compatible manner, and
|
||||
minor = "7"
|
||||
minor = "8"
|
||||
// Patch version when you make backwards-compatible bug fixes.
|
||||
patch = "0"
|
||||
)
|
||||
|
Reference in New Issue
Block a user