mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
0dd9597519 | |||
334fcef407 | |||
e868ad4061 | |||
b41d0bf956 | |||
b62fa6ab8b | |||
8dba9cff44 | |||
217cc451ea | |||
1f81bd3efc | |||
50e5483661 | |||
d2765c30fd | |||
76fc892823 | |||
ea99ce7753 | |||
646eaced29 | |||
9104953ad9 | |||
c40243635f | |||
8059002475 | |||
9daf83cc52 | |||
0cebe465a2 | |||
d9403d9c14 |
@ -79,7 +79,9 @@ RUN set -eux; \
|
|||||||
# Japanese fonts
|
# Japanese fonts
|
||||||
fonts-takao-mincho \
|
fonts-takao-mincho \
|
||||||
# Chinese fonts
|
# Chinese fonts
|
||||||
fonts-wqy-zenhei; \
|
fonts-wqy-zenhei xfonts-intl-chinese xfonts-wqy \
|
||||||
|
# Korean fonts
|
||||||
|
fonts-wqy-microhei; \
|
||||||
#
|
#
|
||||||
# create a non-root user
|
# create a non-root user
|
||||||
groupadd --gid $USER_GID $USERNAME; \
|
groupadd --gid $USER_GID $USERNAME; \
|
||||||
|
@ -85,7 +85,9 @@ RUN set -eux; \
|
|||||||
# Japanese fonts
|
# Japanese fonts
|
||||||
fonts-takao-mincho \
|
fonts-takao-mincho \
|
||||||
# Chinese fonts
|
# Chinese fonts
|
||||||
fonts-wqy-zenhei; \
|
fonts-wqy-zenhei xfonts-intl-chinese xfonts-wqy \
|
||||||
|
# Korean fonts
|
||||||
|
fonts-wqy-microhei; \
|
||||||
#
|
#
|
||||||
# create a non-root user
|
# create a non-root user
|
||||||
groupadd --gid $USER_GID $USERNAME; \
|
groupadd --gid $USER_GID $USERNAME; \
|
||||||
|
@ -88,7 +88,9 @@ RUN set -eux; \
|
|||||||
# Japanese fonts
|
# Japanese fonts
|
||||||
fonts-takao-mincho \
|
fonts-takao-mincho \
|
||||||
# Chinese fonts
|
# Chinese fonts
|
||||||
fonts-wqy-zenhei; \
|
fonts-wqy-zenhei xfonts-intl-chinese xfonts-wqy \
|
||||||
|
# Korean fonts
|
||||||
|
fonts-wqy-microhei; \
|
||||||
#
|
#
|
||||||
# create a non-root user
|
# create a non-root user
|
||||||
groupadd --gid $USER_GID $USERNAME; \
|
groupadd --gid $USER_GID $USERNAME; \
|
||||||
@ -131,6 +133,7 @@ ENV DISPLAY=:99.0
|
|||||||
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=
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -97,7 +97,9 @@ RUN set -eux; \
|
|||||||
# Japanese fonts
|
# Japanese fonts
|
||||||
fonts-takao-mincho \
|
fonts-takao-mincho \
|
||||||
# Chinese fonts
|
# Chinese fonts
|
||||||
fonts-wqy-zenhei; \
|
fonts-wqy-zenhei xfonts-intl-chinese xfonts-wqy \
|
||||||
|
# Korean fonts
|
||||||
|
fonts-wqy-microhei; \
|
||||||
#
|
#
|
||||||
# create a non-root user
|
# create a non-root user
|
||||||
groupadd --gid $USER_GID $USERNAME; \
|
groupadd --gid $USER_GID $USERNAME; \
|
||||||
|
@ -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
|
||||||
|
@ -17,5 +17,9 @@ fi
|
|||||||
|
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-v "${PWD}/../server:/src" \
|
-v "${PWD}/../server:/src" \
|
||||||
--entrypoint="go" \
|
-e GIT_COMMIT=`git rev-parse --short HEAD` \
|
||||||
neko_dev_server build -o "bin/neko" "cmd/neko/main.go"
|
-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" \
|
-v "${PWD}/../client:/app" \
|
||||||
--workdir="/app" \
|
--workdir="/app" \
|
||||||
--entrypoint="npm" \
|
--entrypoint="npm" \
|
||||||
node:14-buster-slim install
|
node:18-bullseye-slim install
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
@ -25,5 +25,5 @@ docker run --rm -it \
|
|||||||
-e "VUE_APP_SERVER_PORT=${SERVER_PORT}" \
|
-e "VUE_APP_SERVER_PORT=${SERVER_PORT}" \
|
||||||
--workdir="/app" \
|
--workdir="/app" \
|
||||||
--entrypoint="npm" \
|
--entrypoint="npm" \
|
||||||
node:14-buster-slim run serve
|
node:18-bullseye-slim run serve
|
||||||
|
|
@ -1,17 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="neko" :class="[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,22 @@
|
|||||||
|
|
||||||
shakeKbd = false
|
shakeKbd = false
|
||||||
|
|
||||||
get hideControls() {
|
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('hideControls', { immediate: true })
|
@Watch('hideControls', { immediate: true })
|
||||||
onHideControls(enabled: boolean) {
|
onHideControls(enabled: boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
@ -22,25 +22,25 @@
|
|||||||
@mouseenter.stop.prevent="onMouseEnter"
|
@mouseenter.stop.prevent="onMouseEnter"
|
||||||
@mouseleave.stop.prevent="onMouseLeave"
|
@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" />
|
<i class="fas fa-play-circle" />
|
||||||
</div>
|
</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" />
|
<i class="fas fa-volume-up" />
|
||||||
</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 +106,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 +223,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 +363,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
|
||||||
@ -384,9 +385,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Watch('playing')
|
@Watch('playing')
|
||||||
onPlayingChanged(playing: boolean) {
|
async onPlayingChanged(playing: boolean) {
|
||||||
if (this._video && this._video.paused && playing) {
|
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) {
|
if (this._video && !this._video.paused && !playing) {
|
||||||
@ -424,11 +445,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()
|
||||||
})
|
})
|
||||||
@ -560,6 +576,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playAndUnmute() {
|
||||||
|
this.$accessor.video.play()
|
||||||
|
this.$accessor.video.setMuted(false)
|
||||||
|
}
|
||||||
|
|
||||||
unmute() {
|
unmute() {
|
||||||
this.$accessor.video.setMuted(false)
|
this.$accessor.video.setMuted(false)
|
||||||
}
|
}
|
||||||
|
@ -203,11 +203,12 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._peer = new RTCPeerConnection()
|
|
||||||
if (lite !== true) {
|
if (lite !== true) {
|
||||||
this._peer = new RTCPeerConnection({
|
this._peer = new RTCPeerConnection({
|
||||||
iceServers: servers,
|
iceServers: servers,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this._peer = new RTCPeerConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
this._peer.onconnectionstatechange = () => {
|
this._peer.onconnectionstatechange = () => {
|
||||||
@ -251,11 +252,28 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
|
|
||||||
this._peer.ontrack = this.onTrack.bind(this)
|
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._peer.onnegotiationneeded = async () => {
|
||||||
this.emit('warn', `negotiation is needed`)
|
this.emit('warn', `negotiation is needed`)
|
||||||
|
|
||||||
const d = await this._peer!.createOffer()
|
const d = await this._peer!.createOffer()
|
||||||
this._peer!.setLocalDescription(d)
|
await this._peer!.setLocalDescription(d)
|
||||||
|
|
||||||
this._ws!.send(
|
this._ws!.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -277,10 +295,10 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._peer.setRemoteDescription({ type: 'offer', sdp })
|
await this._peer.setRemoteDescription({ type: 'offer', sdp })
|
||||||
|
|
||||||
for (const candidate of this._candidates) {
|
for (const candidate of this._candidates) {
|
||||||
this._peer.addIceCandidate(candidate)
|
await this._peer.addIceCandidate(candidate)
|
||||||
}
|
}
|
||||||
this._candidates = []
|
this._candidates = []
|
||||||
|
|
||||||
@ -310,7 +328,7 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._peer.setRemoteDescription({ type: 'answer', sdp })
|
await this._peer.setRemoteDescription({ type: 'answer', sdp })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onMessage(e: MessageEvent) {
|
private async onMessage(e: MessageEvent) {
|
||||||
|
@ -6,16 +6,23 @@
|
|||||||
- 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.
|
||||||
|
|
||||||
### Bugs
|
### Bugs
|
||||||
- Fixed TCP mux occasional freeze by adding write buffer to it.
|
- 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 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 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 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
|
### Misc
|
||||||
- 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.
|
||||||
|
- 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)
|
## [n.eko v2.7](https://github.com/m1k1o/neko/releases/tag/v2.7)
|
||||||
|
|
||||||
|
@ -162,6 +162,57 @@ services:
|
|||||||
"RestoreOnStartup": 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?
|
### 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
|
||||||
|
|
||||||
@ -179,6 +230,7 @@ services:
|
|||||||
- 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.
|
||||||
- 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.
|
||||||
@ -167,7 +167,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)
|
||||||
|
22
server/build
22
server/build
@ -3,8 +3,22 @@
|
|||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
|
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;
|
||||||
|
@ -92,7 +92,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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
func (session *Session) SignalRemoteOffer(sdp string) error {
|
||||||
if session.peer == nil {
|
if session.peer == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -154,14 +165,12 @@ func (session *Session) SignalRemoteAnswer(sdp string) error {
|
|||||||
return session.peer.SetAnswer(sdp)
|
return session.peer.SetAnswer(sdp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) SignalCandidate(data string) error {
|
func (session *Session) SignalRemoteCandidate(data string) error {
|
||||||
if session.socket == nil {
|
if session.socket == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return session.socket.Send(&message.SignalCandidate{
|
session.logger.Info().Msg("signal update - RemoteCandidate")
|
||||||
Event: event.SIGNAL_CANDIDATE,
|
return session.peer.SetCandidate(data)
|
||||||
Data: data,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) destroy() error {
|
func (session *Session) destroy() error {
|
||||||
|
@ -40,9 +40,10 @@ type Session interface {
|
|||||||
Send(v interface{}) error
|
Send(v interface{}) error
|
||||||
SignalLocalOffer(sdp string) error
|
SignalLocalOffer(sdp string) error
|
||||||
SignalLocalAnswer(sdp string) error
|
SignalLocalAnswer(sdp string) error
|
||||||
|
SignalLocalCandidate(data string) error
|
||||||
SignalRemoteOffer(sdp string) error
|
SignalRemoteOffer(sdp string) error
|
||||||
SignalRemoteAnswer(sdp string) error
|
SignalRemoteAnswer(sdp string) error
|
||||||
SignalCandidate(data string) error
|
SignalRemoteCandidate(data string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionManager interface {
|
type SessionManager interface {
|
||||||
|
@ -21,6 +21,7 @@ type Peer interface {
|
|||||||
CreateAnswer() (string, error)
|
CreateAnswer() (string, error)
|
||||||
SetOffer(sdp string) error
|
SetOffer(sdp string) error
|
||||||
SetAnswer(sdp string) error
|
SetAnswer(sdp string) error
|
||||||
|
SetCandidate(candidateString string) error
|
||||||
WriteData(v interface{}) error
|
WriteData(v interface{}) error
|
||||||
Destroy() error
|
Destroy() error
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v3"
|
"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})
|
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 {
|
func (peer *Peer) WriteData(v interface{}) error {
|
||||||
peer.mu.Lock()
|
peer.mu.Lock()
|
||||||
defer peer.mu.Unlock()
|
defer peer.mu.Unlock()
|
||||||
|
@ -123,7 +123,6 @@ func (manager *WebRTCManager) initAPI() error {
|
|||||||
LoggerFactory: logger,
|
LoggerFactory: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = settings.SetEphemeralUDPPortRange(manager.config.EphemeralMin, manager.config.EphemeralMax)
|
|
||||||
settings.SetNAT1To1IPs(manager.config.NAT1To1IPs, webrtc.ICECandidateTypeHost)
|
settings.SetNAT1To1IPs(manager.config.NAT1To1IPs, webrtc.ICECandidateTypeHost)
|
||||||
settings.SetICETimeouts(6*time.Second, 6*time.Second, 3*time.Second)
|
settings.SetICETimeouts(6*time.Second, 6*time.Second, 3*time.Second)
|
||||||
settings.SetSRTPReplayProtectionWindow(512)
|
settings.SetSRTPReplayProtectionWindow(512)
|
||||||
@ -168,12 +167,15 @@ func (manager *WebRTCManager) initAPI() error {
|
|||||||
|
|
||||||
networkType = append(networkType, webrtc.NetworkTypeUDP4)
|
networkType = append(networkType, webrtc.NetworkTypeUDP4)
|
||||||
manager.logger.Info().Int("port", manager.config.UDPMUX).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
|
settings.SetNetworkTypes(networkType)
|
||||||
if len(networkType) > 0 {
|
|
||||||
settings.SetNetworkTypes(networkType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create MediaEngine with selected codecs
|
// Create MediaEngine with selected codecs
|
||||||
engine := webrtc.MediaEngine{}
|
engine := webrtc.MediaEngine{}
|
||||||
@ -299,7 +301,7 @@ func (manager *WebRTCManager) CreatePeer(id string, session types.Session) (type
|
|||||||
return
|
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")
|
manager.logger.Warn().Err(err).Msg("sending SignalCandidate failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,12 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
|
|||||||
utils.Unmarshal(payload, raw, func() error {
|
utils.Unmarshal(payload, raw, func() error {
|
||||||
return h.signalRemoteAnswer(id, session, payload)
|
return h.signalRemoteAnswer(id, session, payload)
|
||||||
}), "%s failed", header.Event)
|
}), "%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
|
// Control Events
|
||||||
case event.CONTROL_RELEASE:
|
case event.CONTROL_RELEASE:
|
||||||
|
@ -45,3 +45,7 @@ func (h *MessageHandler) signalRemoteAnswer(id string, session types.Session, pa
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MessageHandler) signalRemoteCandidate(id string, session types.Session, payload *message.SignalCandidate) error {
|
||||||
|
return session.SignalRemoteCandidate(payload.Data)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user