2
2
mirror of https://github.com/m1k1o/neko.git synced 2024-07-24 14:40:50 +12:00

28 Commits

Author SHA1 Message Date
c1360d3abc ensure that paths are writable by neko user, . 2023-04-06 00:00:59 +02:00
950095d6d8 update Dockerfile.nvidia, . 2023-04-03 20:06:52 +02:00
009bc20969 update docs, . 2023-04-02 12:35:06 +02:00
395db0fd54 version 2.8.0. 2023-04-01 23:01:10 +02:00
13fa86d543 update changelog. 2023-04-01 22:59:39 +02:00
8308c13382 update nvenc pipeline. 2023-04-01 22:59:39 +02:00
ec175909a3 improve quality a little bit 2023-04-01 22:59:39 +02:00
d2f51fa10f add h264parse to nvidia pipeline 2023-04-01 22:59:39 +02:00
70325e0277 add nvenc pipeline. 2023-04-01 22:59:39 +02:00
bdff0841ec add nvenc support for gstreamer. 2023-04-01 22:59:39 +02:00
3c17dbe282 h264 encoding profile constrained-baseline, . 2023-03-31 21:43:20 +02:00
887413d536 update changelog. 2023-03-27 21:55:52 +02:00
f9228e653d Merge branch 'pu/touch' 2023-03-27 21:47:49 +02:00
df634be1c5 lint. 2023-03-27 21:47:41 +02:00
2130daa02d add volume to docs. 2023-03-27 18:22:31 +02:00
aee7650d47 add touch events on touch monitor 2023-03-27 14:28:42 +02:00
98ba32c574 add url param for audio volume. 2023-03-27 13:04:11 +02:00
d08d3eccef configure pulseaudio from ENV, . 2023-03-26 20:13:35 +02:00
0dd9597519 create pulseaudio sink, fixes . 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 . 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
15 changed files with 302 additions and 98 deletions

@ -90,17 +90,13 @@ RUN set -eux; \
adduser $USERNAME video; \ adduser $USERNAME video; \
adduser $USERNAME pulse; \ 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 # workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
mkdir /tmp/.X11-unix; \ mkdir /tmp/.X11-unix; \
chmod 1777 /tmp/.X11-unix; \ chmod 1777 /tmp/.X11-unix; \
chown $USERNAME /tmp/.X11-unix/; \ chown $USERNAME /tmp/.X11-unix/; \
# #
# make directories for neko # make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \ mkdir -p /etc/neko /var/www /var/log/neko /home/$USERNAME/.config/pulse /home/$USERNAME/.local/share/xorg; \
chmod 1777 /var/log/neko; \ chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/; \ chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \ chown -R $USERNAME:$USERNAME /home/$USERNAME; \
@ -120,6 +116,7 @@ COPY .docker/base/xorg.conf /etc/neko/xorg.conf
# set default envs # set default envs
ENV USER=$USERNAME ENV USER=$USERNAME
ENV DISPLAY=:99.0 ENV DISPLAY=:99.0
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
ENV NEKO_PASSWORD=neko ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080 ENV NEKO_BIND=:8080

@ -96,17 +96,13 @@ RUN set -eux; \
adduser $USERNAME video; \ adduser $USERNAME video; \
adduser $USERNAME pulse; \ 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 # workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
mkdir /tmp/.X11-unix; \ mkdir /tmp/.X11-unix; \
chmod 1777 /tmp/.X11-unix; \ chmod 1777 /tmp/.X11-unix; \
chown $USERNAME /tmp/.X11-unix/; \ chown $USERNAME /tmp/.X11-unix/; \
# #
# make directories for neko # make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \ mkdir -p /etc/neko /var/www /var/log/neko /home/$USERNAME/.config/pulse /home/$USERNAME/.local/share/xorg; \
chmod 1777 /var/log/neko; \ chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/; \ chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \ chown -R $USERNAME:$USERNAME /home/$USERNAME; \
@ -126,6 +122,7 @@ COPY .docker/base/xorg.conf /etc/neko/xorg.conf
# set default envs # set default envs
ENV USER=$USERNAME ENV USER=$USERNAME
ENV DISPLAY=:99.0 ENV DISPLAY=:99.0
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
ENV NEKO_PASSWORD=neko ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080 ENV NEKO_BIND=:8080

@ -99,17 +99,13 @@ RUN set -eux; \
adduser $USERNAME video; \ adduser $USERNAME video; \
adduser $USERNAME pulse; \ 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 # workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
mkdir /tmp/.X11-unix; \ mkdir /tmp/.X11-unix; \
chmod 1777 /tmp/.X11-unix; \ chmod 1777 /tmp/.X11-unix; \
chown $USERNAME /tmp/.X11-unix/; \ chown $USERNAME /tmp/.X11-unix/; \
# #
# make directories for neko # make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \ mkdir -p /etc/neko /var/www /var/log/neko /home/$USERNAME/.config/pulse /home/$USERNAME/.local/share/xorg; \
chmod 1777 /var/log/neko; \ chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/; \ chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \ chown -R $USERNAME:$USERNAME /home/$USERNAME; \
@ -130,9 +126,11 @@ COPY .docker/base/intel/add-render-group.sh /usr/bin/add-render-group.sh
# set default envs # set default envs
ENV USER=$USERNAME ENV USER=$USERNAME
ENV DISPLAY=:99.0 ENV DISPLAY=:99.0
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
ENV NEKO_PASSWORD=neko ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080 ENV NEKO_BIND=:8080
ENV NEKO_HWENC=VAAPI
ENV RENDER_GID= ENV RENDER_GID=
# #

@ -1,10 +1,62 @@
ARG UBUNTU_RELEASE=20.04 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 # STAGE 1: SERVER
# #
FROM golang:1.18-bullseye as server FROM golang:1.20-bullseye as server
WORKDIR /src WORKDIR /src
# #
@ -35,7 +87,7 @@ RUN go get -v -t -d . && go build -o bin/neko cmd/neko/main.go
# #
# STAGE 2: CLIENT # STAGE 2: CLIENT
# #
FROM node:14-bullseye-slim as client FROM node:18-bullseye-slim as client
WORKDIR /src WORKDIR /src
# #
@ -51,13 +103,13 @@ RUN npm run build
# #
# STAGE 3: RUNTIME # 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 UBUNTU_RELEASE
ARG CUDA_VERSION ARG VIRTUALGL_VERSION
# Make all NVIDIA GPUs visible by default # 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 # 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 ENV NVIDIA_DRIVER_CAPABILITIES all
@ -75,20 +127,55 @@ ARG USERNAME=neko
ARG USER_UID=1000 ARG USER_UID=1000
ARG USER_GID=$USER_UID 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; \ RUN set -eux; \
apt-get update; \ apt-get update; \
# #
# install dependencies # install dependencies
apt-get install -y --no-install-recommends wget ca-certificates supervisor; \ 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 pulseaudio dbus-x11 xserver-xorg-video-dummy; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \ 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; \
# 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 # install fonts
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
@ -108,17 +195,13 @@ RUN set -eux; \
adduser $USERNAME video; \ adduser $USERNAME video; \
adduser $USERNAME pulse; \ 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 # workaround for an X11 problem: http://blog.tigerteufel.de/?p=476
mkdir /tmp/.X11-unix; \ mkdir /tmp/.X11-unix; \
chmod 1777 /tmp/.X11-unix; \ chmod 1777 /tmp/.X11-unix; \
chown $USERNAME /tmp/.X11-unix/; \ chown $USERNAME /tmp/.X11-unix/; \
# #
# make directories for neko # make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \ mkdir -p /etc/neko /var/www /var/log/neko /home/$USERNAME/.config/pulse /home/$USERNAME/.local/share/xorg; \
chmod 1777 /var/log/neko; \ chmod 1777 /var/log/neko; \
chown $USERNAME /var/log/neko/; \ chown $USERNAME /var/log/neko/; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \ chown -R $USERNAME:$USERNAME /home/$USERNAME; \
@ -128,18 +211,18 @@ RUN set -eux; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/* rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# #
# install and configure Vulkan manually # configure EGL and Vulkan manually
RUN set -eux; \ RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)') && \
apt-get update; \ # Configure EGL manually
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; \ mkdir -p /usr/share/glvnd/egl_vendor.d/ && \
# echo "{\n\
# clean up \"file_format_version\" : \"1.0.0\",\n\
apt-get clean -y; \ \"ICD\": {\n\
rm -rf /var/lib/apt/lists/* /var/cache/apt/*; \ \"library_path\": \"libEGL_nvidia.so.0\"\n\
# }\n\
# configure vulkan }" > /usr/share/glvnd/egl_vendor.d/10_nvidia.json && \
VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9]+(\.[0-9]+)(\.[0-9]+)'); \ # Configure Vulkan manually
mkdir -p /etc/vulkan/icd.d/; \ mkdir -p /etc/vulkan/icd.d/ && \
echo "{\n\ echo "{\n\
\"file_format_version\" : \"1.0.0\",\n\ \"file_format_version\" : \"1.0.0\",\n\
\"ICD\": {\n\ \"ICD\": {\n\
@ -150,7 +233,6 @@ RUN set -eux; \
# #
# install VirtualGL and make libraries available for preload # install VirtualGL and make libraries available for preload
ARG VIRTUALGL_VERSION=3.1
RUN set -eux; \ RUN set -eux; \
apt-get update; \ apt-get update; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \ wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
@ -180,10 +262,21 @@ COPY .docker/base/nvidia/entrypoint.sh /bin/entrypoint.sh
# set default envs # set default envs
ENV USER=$USERNAME ENV USER=$USERNAME
ENV DISPLAY=:99.0 ENV DISPLAY=:99.0
ENV PULSE_SERVER=unix:/tmp/pulseaudio.socket
ENV NEKO_PASSWORD=neko ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080 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 static files from previous stages
COPY --from=server /src/bin/neko /usr/bin/neko COPY --from=server /src/bin/neko /usr/bin/neko

@ -1,5 +1,8 @@
#!/usr/bin/pulseaudio -nF #!/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 # 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 load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1

@ -15,6 +15,11 @@ RUN set -eux; apt-get update; \
apt-get clean -y; \ apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/* 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 configuation files
COPY supervisord.conf /etc/neko/supervisord/kde.conf COPY supervisord.conf /etc/neko/supervisord/kde.conf

@ -1,17 +1,22 @@
<template> <template>
<div id="neko" :class="[!hideControls && side ? 'expanded' : '']"> <div id="neko" :class="[!videoOnly && side ? 'expanded' : '']">
<template v-if="!$client.supported"> <template v-if="!$client.supported">
<neko-unsupported /> <neko-unsupported />
</template> </template>
<template v-else> <template v-else>
<main class="neko-main"> <main class="neko-main">
<div v-if="!hideControls" class="header-container"> <div v-if="!videoOnly" class="header-container">
<neko-header /> <neko-header />
</div> </div>
<div class="video-container"> <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>
<div v-if="!hideControls" class="room-container"> <div v-if="!videoOnly" class="room-container">
<neko-members /> <neko-members />
<div class="room-menu"> <div class="room-menu">
<div class="settings"> <div class="settings">
@ -26,11 +31,11 @@
</div> </div>
</div> </div>
</main> </main>
<neko-side v-if="!hideControls && side" /> <neko-side v-if="!videoOnly && side" />
<neko-connect v-if="!connected" /> <neko-connect v-if="!connected" />
<neko-about v-if="about" /> <neko-about v-if="about" />
<notifications <notifications
v-if="!hideControls" v-if="!videoOnly"
group="neko" group="neko"
position="top left" position="top left"
style="top: 50px; pointer-events: none" style="top: 50px; pointer-events: none"
@ -176,10 +181,32 @@
shakeKbd = false 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') 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 }) @Watch('hideControls', { immediate: true })
onHideControls(enabled: boolean) { onHideControls(enabled: boolean) {
if (enabled) { if (enabled) {

@ -21,6 +21,9 @@
@mouseup.stop.prevent="onMouseUp" @mouseup.stop.prevent="onMouseUp"
@mouseenter.stop.prevent="onMouseEnter" @mouseenter.stop.prevent="onMouseEnter"
@mouseleave.stop.prevent="onMouseLeave" @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="playAndUnmute"> <div v-if="!playing && playable" class="player-overlay" @click.stop.prevent="playAndUnmute">
<i class="fas fa-play-circle" /> <i class="fas fa-play-circle" />
@ -30,17 +33,17 @@
</div> </div>
<div ref="aspect" class="player-aspect" /> <div ref="aspect" class="player-aspect" />
</div> </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><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 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 <i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']" :class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']"
@click.stop.prevent="toggleControl" @click.stop.prevent="toggleControl"
/> />
</li> </li>
</ul> </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)"> <li v-if="hosting && (!clipboard_read_available || !clipboard_write_available)">
<i @click.stop.prevent="openClipboard" class="fas fa-clipboard"></i> <i @click.stop.prevent="openClipboard" class="fas fa-clipboard"></i>
</li> </li>
@ -106,12 +109,12 @@
} }
} }
&.request-control { /* usually extra controls are only shown on mobile */
&.extra-control {
display: none; display: none;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
&.request-control { &.extra-control {
display: inline-block; display: inline-block;
} }
} }
@ -223,13 +226,15 @@
@Ref('resolution') readonly _resolution!: Resolution @Ref('resolution') readonly _resolution!: Resolution
@Ref('clipboard') readonly _clipboard!: Clipboard @Ref('clipboard') readonly _clipboard!: Clipboard
// all controls are hidden (e.g. for cast mode)
@Prop(Boolean) readonly hideControls!: boolean @Prop(Boolean) readonly hideControls!: boolean
// extra controls are shown (e.g. for embed mode)
@Prop(Boolean) readonly extraControls!: boolean
private keyboard = GuacamoleKeyboard() private keyboard = GuacamoleKeyboard()
private observer = new ResizeObserver(this.onResize.bind(this)) private observer = new ResizeObserver(this.onResize.bind(this))
private focused = false private focused = false
private fullscreen = false private fullscreen = false
private startsMuted = true
private mutedOverlay = true private mutedOverlay = true
get admin() { get admin() {
@ -361,7 +366,6 @@
onMutedChanged(muted: boolean) { onMutedChanged(muted: boolean) {
if (this._video && this._video.muted != muted) { if (this._video && this._video.muted != muted) {
this._video.muted = muted this._video.muted = muted
this.startsMuted = muted
if (!muted) { if (!muted) {
this.mutedOverlay = false this.mutedOverlay = false
@ -392,7 +396,20 @@
try { try {
await this._video.play() await this._video.play()
} catch (err: any) { } catch (err: any) {
this.$accessor.video.pause() 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()
}
} }
} }
@ -431,11 +448,6 @@
this._video.addEventListener('canplaythrough', () => { this._video.addEventListener('canplaythrough', () => {
this.$accessor.video.setPlayable(true) this.$accessor.video.setPlayable(true)
if (this.autoplay) { 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.$nextTick(() => {
this.$accessor.video.play() this.$accessor.video.play()
}) })
@ -679,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) { onMouseDown(e: MouseEvent) {
if (!this.hosting) { if (!this.hosting) {
this.$emit('control-attempt', e) this.$emit('control-attempt', e)

@ -2,10 +2,17 @@
## master branch ## master branch
## [n.eko v2.8.0](https://github.com/m1k1o/neko/releases/tag/v2.8.0)
### New Features ### New Features
- Added AV1 tag, metadata and pipeline. Unfortunately does not work yet, since the encoding is way too slow (by @mbattista). - 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`. - 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! - 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 ### Bugs
- Fixed TCP mux occasional freeze by adding write buffer to it. - Fixed TCP mux occasional freeze by adding write buffer to it.
@ -19,6 +26,10 @@
- Updated to go 1.19 and Node 18, removed go-events as dependency (by @mbattista). - 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. - Added adaptive framerate which now streams in the framerate you selected from the dropdown.
- Improved chinese and korean characters support. - 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) ## [n.eko v2.7](https://github.com/m1k1o/neko/releases/tag/v2.7)

@ -164,7 +164,7 @@ services:
### Nvidia GPU acceleration ### 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). 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 ```bash
docker run -d --gpus all \ docker run -d --gpus all \
@ -176,6 +176,8 @@ docker run -d --gpus all \
-e NEKO_EPR=56000-56100 \ -e NEKO_EPR=56000-56100 \
-e NEKO_NAT1TO1=192.168.1.10 \ -e NEKO_NAT1TO1=192.168.1.10 \
-e NEKO_ICELITE=1 \ -e NEKO_ICELITE=1 \
-e NEKO_VIDEO_CODEC=h264 \
-e NEKO_HWENC=nvenc \
--shm-size=2gb \ --shm-size=2gb \
--cap-add=SYS_ADMIN \ --cap-add=SYS_ADMIN \
--name neko \ --name neko \
@ -202,6 +204,8 @@ services:
NEKO_PASSWORD_ADMIN: admin NEKO_PASSWORD_ADMIN: admin
NEKO_EPR: 56000-56100 NEKO_EPR: 56000-56100
NEKO_NAT1TO1: 192.168.1.10 NEKO_NAT1TO1: 192.168.1.10
NEKO_VIDEO_CODEC: h264
NEKO_HWENC: nvenc
deploy: deploy:
resources: resources:
reservations: reservations:
@ -211,7 +215,9 @@ services:
capabilities: [gpu] capabilities: [gpu]
``` ```
Note, currently only browser GPU acceleration is supported, not encoding. - 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? ### Want to use VPN for your n.eko browsing?
- Check this out: https://github.com/m1k1o/neko-vpn - Check this out: https://github.com/m1k1o/neko-vpn
@ -230,6 +236,8 @@ Note, currently only browser GPU acceleration is supported, not encoding.
- Adding `?pwd=<password>` will prefill password. - Adding `?pwd=<password>` will prefill password.
- Adding `?usr=<display-name>` will prefill username. - Adding `?usr=<display-name>` will prefill username.
- Adding `?cast=1` will hide all control and show only video. - 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` - e.g. `http(s)://<URL:Port>/?pwd=neko&usr=guest&cast=1`
### Screen size ### 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. - Control protection means, users can gain control only if at least one admin is in the room.
- e.g. `false` - e.g. `false`
#### `NEKO_IMPLICIT_CONTROL`: #### `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` - e.g. `false`
#### `NEKO_LOCKS`: #### `NEKO_LOCKS`:
- Resources, that will be locked when starting, separated by whitespace. - Resources, that will be locked when starting, separated by whitespace.
@ -79,13 +79,14 @@ nat1to1: <ip>
- `gstreamer1.0-plugins-good` - `gstreamer1.0-plugins-good`
- `gstreamer1.0-plugins-bad` - `gstreamer1.0-plugins-bad`
- `gstreamer1.0-plugins-ugly` - `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`: #### `NEKO_MAX_FPS`:
- The resulting stream frames per seconds should be capped *(0 for uncapped)*. - The resulting stream frames per seconds should be capped *(0 for uncapped)*.
- e.g. `0` - e.g. `0`
#### `NEKO_HWENC`: #### `NEKO_HWENC`:
- Use hardware accelerated encoding, for now supported only `VAAPI`. - none *(default CPU encoding)*
- e.g. `VAAPI` - vaapi
- nvenc
### Audio ### Audio
@ -167,7 +168,7 @@ Flags:
--cert string path to the SSL cert used to secure the neko server --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 --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 [*]) --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") --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") --epr string limits the pool of ephemeral ports that ICE UDP connections can allocate from (default "59000-59100")
--file_transfer_enabled enable file transfer feature (default false) --file_transfer_enabled enable file transfer feature (default false)

@ -95,9 +95,9 @@ services:
! videoconvert ! videoconvert
! queue ! queue
! video/x-raw,framerate=30/1,format=NV12 ! 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 ! 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 NEKO_VIDEO_CODEC: h264
``` ```

@ -5,6 +5,7 @@ import (
"strings" "strings"
"m1k1o/neko/internal/capture/gst" "m1k1o/neko/internal/capture/gst"
"m1k1o/neko/internal/config"
"m1k1o/neko/internal/types/codec" "m1k1o/neko/internal/types/codec"
) )
@ -52,7 +53,7 @@ func NewBroadcastPipeline(device string, display string, pipelineSrc string, url
return pipelineStr, nil 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" pipelineStr := " ! appsink name=appsinkvideo"
// if using custom pipeline // if using custom pipeline
@ -68,7 +69,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
switch rtpCodec.Name { switch rtpCodec.Name {
case codec.VP8().Name: case codec.VP8().Name:
if hwenc == "VAAPI" { if hwenc == config.HwEncVAAPI {
if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil { if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil {
return "", err return "", err
} }
@ -138,35 +139,40 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin
return "", err 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 { if err := gst.CheckPlugins([]string{"vaapi"}); err != nil {
return "", err 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 { } else {
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc // https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
// gstreamer1.0-plugins-bad // gstreamer1.0-plugins-bad
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 // openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
if err := gst.CheckPlugins([]string{"openh264"}); err == nil { 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 break
} }
// https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c // https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
// gstreamer1.0-plugins-ugly // 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 { if err := gst.CheckPlugins([]string{"x264"}); err != nil {
return "", err return "", err
} }
vbvbuf := uint(1000) 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)
if bitrate > 1000 {
vbvbuf = bitrate
}
pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! x264enc threads=4 bitrate=%d key-int-max=60 vbv-buf-capacity=%d byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream"+pipelineStr, display, fps, bitrate, vbvbuf)
} }
default: default:
return "", fmt.Errorf("unknown codec %s", rtpCodec.Name) return "", fmt.Errorf("unknown codec %s", rtpCodec.Name)

@ -2,6 +2,7 @@ package config
import ( import (
"m1k1o/neko/internal/types/codec" "m1k1o/neko/internal/types/codec"
"strings"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -9,13 +10,21 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type HwEnc int
const (
HwEncNone HwEnc = iota
HwEncVAAPI
HwEncNVENC
)
type Capture struct { type Capture struct {
// video // video
Display string Display string
VideoCodec codec.RTPCodec VideoCodec codec.RTPCodec
VideoHWEnc string // TODO: Pipeline builder. VideoHWEnc HwEnc // TODO: Pipeline builder.
VideoBitrate uint // TODO: Pipeline builder. VideoBitrate uint // TODO: Pipeline builder.
VideoMaxFPS int16 // TODO: Pipeline builder. VideoMaxFPS int16 // TODO: Pipeline builder.
VideoPipeline string VideoPipeline string
// audio // audio
@ -92,7 +101,7 @@ func (Capture) Init(cmd *cobra.Command) error {
// audio // 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 { if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
return err 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") log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_VIDEO_CODEC=av1' instead")
} }
videoHWEnc := "" videoHWEnc := strings.ToLower(viper.GetString("hwenc"))
if viper.GetString("hwenc") == "VAAPI" { switch videoHWEnc {
videoHWEnc = "VAAPI" 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.VideoBitrate = viper.GetUint("video_bitrate")
s.VideoMaxFPS = int16(viper.GetInt("max_fps")) s.VideoMaxFPS = int16(viper.GetInt("max_fps"))

@ -39,7 +39,7 @@ var (
// Major version when you make incompatible API changes, // Major version when you make incompatible API changes,
major = "2" major = "2"
// Minor version when you add functionality in a backwards-compatible manner, and // 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 version when you make backwards-compatible bug fixes.
patch = "0" patch = "0"
) )