* server: generated video pipelines keep at most one raw frame queued
GetPipeline used plain `queue` elements, which buffer up to 1s / 200
buffers. When the encoder cannot keep up, frames pile up there and the
stream (and every reaction to input) falls behind by up to that much.
The queues now hold only the newest frame (leaky=downstream): a slow
encoder drops frames instead of adding latency.
Measured input-to-display latency (Alt+F2/Escape opens/closes a dialog,
time to the first changed received frame; xfce image built with ./build,
1920x1080, default VP8 pipeline, 30 trials per run, runs interleaved):
before p50/p90 ms after p50/p90 ms
no CPU limit 268/303, 188/225 183/217, 157/226
--cpus=1 270/422, 362/673 155/237, 200/428
fps unchanged without a limit (25), slightly higher with one (22.5 ->
23.5-24). Pipelines given as raw gst strings are unchanged.
Tests: TestVideoConfigGetPipelineQueues; client e2e on the built image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* server: session ids no longer carry the login name verbatim
The multiuser and noauth providers built the session id as
"<username>-<token>". The v3 API addresses sessions and members by id
in the URL path (DELETE /api/sessions/{id}, POST /api/members/{id},
POST /api/room/control/give/{id}), so a login name like
"x/../../logout?" produced an id that sent an admin's unencoded
request to another endpoint, and even when the client encodes it, an
id with "/" never matches the chi route: such a member could not be
kicked, muted or banned.
utils.SafeID maps a name to letters and digits of any script plus
".", "_" and "-": every run of other characters becomes one "-",
leading and trailing "-" and "." are trimmed, the result is cut to 64
characters, and an empty result becomes "user". Nothing with a meaning
in a URL path survives; letters outside ASCII are kept so names such as
"Müller" stay recognisable (clients percent-encode ids in paths). Both
providers now use SafeID(username) + "-" + token; the profile name
keeps the name as typed, so nothing changes on screen.
The file and object providers use the username itself as the member
id. Members there are created explicitly by an admin, so
POST /api/members now rejects a name SafeID would change with a 400
instead of silently altering it. The oauth provider derives ids from
the IdP subject ("oauth:" + sub) and is unchanged.
Tests: TestSafeID (pkg/utils).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* server: chat plugin limits message length (chat.max_length)
The chat plugin accepted text of any size, over the websocket
(chat/message) and over REST (POST /api/chat), and broadcast it to
every session. The web client caps its input box at 512 characters,
but any client can send more, and one large message stalls every
viewer's renderer.
New option chat.max_length (default 512, counted in characters with
utf8.RuneCountInString, not bytes; 0 disables the limit). Both paths
go through one check: REST answers 400 with the length and the limit,
the websocket handler logs at warn level and drops the message, as it
already does for a sender without permission. The default never
rejects what the web client can type: its maxlength counts UTF-16
units, which is never fewer than runes.
Docs: plugins.md and release notes. help.txt/help.json list no plugin
options (no chat.*, filetransfer.*, openinapp.* entries), so nothing
was added there.
Tests: TestCheckLength (internal/plugins/chat).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* server: limit the size of websocket frames a client may send
There was no read limit on the websocket, so a client could push frames
of any size; they were read and decoded in full before a handler (such
as the chat length check) could reject them. Frames above 1 MiB now end
the connection, as gorilla's SetReadLimit does. The largest legitimate
messages, an SDP answer and pasted clipboard text, stay far below that.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* server: video RTP timestamps follow the time between captured frames
ximagesrc gives a frame it captures late a buffer duration of only the
time left until the next frame slot ("Frame duration is from now until
the next expected capture time"), and frames dropped before the encoder
leave gaps. pion advances the RTP clock by Sample.Duration, so whenever
the encoder falls behind, the stream's clock runs slower than real
time. Browsers grow their jitter buffer to make up for it and lip sync
drifts. This happens with any queue in front of the encoder.
Each video sample's duration is now the gap since the previous sample's
PTS. The gap is known only when the next frame arrives, so it is
applied one frame late; the clock itself stays right. The sink also
counts frames in neko_capture_streamsink_frames: a rate below the
configured fps means the encoder falls behind.
Measured on the firefox image built with ./build, 1080p25 VP8, the
container limited to 0.5 CPU: RTP clock 0.2-0.9x real time before,
1.00x after; browser video jitter buffer 0.24-1.9 s before, 60-220 ms
after; audio up to 0.4 s (default queue) or 0.74 s (one-frame leaky
queue) ahead of the video before, within -70..+150 ms after.
Tests: TestSampleDuration; go test -race ./internal/capture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
import { Def, Opt } from '@site/src/components/Anchor'; import { ConfigurationTab } from '@site/src/components/Configuration'; import configOptions from './help.json';
Configuration
Neko uses the Viper library to manage configuration. The configuration file is optional and is not required for Neko to run. If a configuration file is present, it will be read in and merged with the default configuration values.
The merge order is as follows:
- Default configuration values
- Configuration file
- Environment variables
- Command-line arguments
Example merging order
# Default Value: 127.0.0.1:8080
# Config File
cat config.yaml <<EOF
server:
bind: "127.0.0.1:8081"
EOF
# Environment Variable
export NEKO_SERVER_BIND=127.0.0.1:8082
# Command-line Argument
./neko -config=config.yaml -server.bind=127.0.0.1:8083
The final value of server.bind will be 127.0.0.1:8083.
Configuration File
You have multiple ways to specify the configuration file for the neko server:
- Command-line argument:
-config=/path/to/config.yaml - Environment variable:
NEKO_CONFIG=/path/to/config.yaml - Place the
neko.yamlfile in the same directory as the neko binary. - Place the
neko.yamlfile to/etc/neko/neko.yaml(ideal for Docker containers).
The configuration file can be specified in YAML, JSON, TOML, HCL, envfile, and Java properties format. Throughout the documentation, we will use the YAML format.
Example configuration files
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
```yaml title="config.yaml"
capture:
screencast:
enabled: false
server:
pprof: true
desktop:
screen: "1920x1080@60"
member:
provider: "multiuser"
multiuser:
admin_password: "admin"
user_password: "neko"
session:
merciful_reconnect: true
implicit_hosting: false
inactive_cursors: true
cookie:
enabled: false
webrtc:
icelite: true
iceservers:
# Backend servers are ignored if icelite is true.
backend:
- urls: [ stun:stun.l.google.com:19302 ]
frontend:
- urls: [ stun:stun.l.google.com:19305 ]
```
```json title="config.json"
{
"capture": {
"screencast": {
"enabled": false
}
},
"server": {
"pprof": true
},
"desktop": {
"screen": "1920x1080@60"
},
"member": {
"provider": "multiuser",
"multiuser": {
"admin_password": "admin",
"user_password": "neko"
}
},
"session": {
"merciful_reconnect": true,
"implicit_hosting": false,
"inactive_cursors": true,
"cookie": {
"enabled": false
}
},
"webrtc": {
"icelite": true,
"iceservers": {
"backend": [
{
"urls": [ "stun:stun.l.google.com:19302" ]
}
],
"frontend": [
{
"urls": [ "stun:stun.l.google.com:19305" ]
}
]
}
}
}
```
```toml title="config.toml"
[capture.screencast]
enabled = false
[server]
pprof = true
[desktop]
screen = "1920x1080@60"
[member]
provider = "multiuser"
[member.multiuser]
admin_password = "admin"
user_password = "neko"
[session]
merciful_reconnect = true
implicit_hosting = false
inactive_cursors = true
[session.cookie]
enabled = false
[webrtc]
icelite = true
[[webrtc.iceservers.backend]]
urls = [ "stun:stun.l.google.com:19302" ]
[[webrtc.iceservers.frontend]]
urls = [ "stun:stun.l.google.com:19305" ]
```
```hcl title="config.hcl"
capture {
screencast {
enabled = false
}
}
server {
pprof = true
}
desktop {
screen = "1920x1080@60"
}
member {
provider = "multiuser"
multiuser {
admin_password = "admin"
user_password = "neko"
}
}
session {
merciful_reconnect = true
implicit_hosting = false
inactive_cursors = true
cookie {
enabled = false
}
}
webrtc {
icelite = true
iceservers {
backend {
urls = [ "stun:stun.l.google.com:19302" ]
}
frontend {
urls = [ "stun:stun.l.google.com:19305" ]
}
}
}
```
```env title=".env"
CAPTURE_SCREENCAST_ENABLED=false
SERVER_PPROF=true
DESKTOP_SCREEN=1920x1080@60
MEMBER_PROVIDER=multiuser
MEMBER_MULTIUSER_ADMIN_PASSWORD=admin
MEMBER_MULTIUSER_USER_PASSWORD=neko
SESSION_MERCIFUL_RECONNECT=true
SESSION_IMPLICIT_HOSTING=false
SESSION_INACTIVE_CURSORS=true
SESSION_COOKIE_ENABLED=false
WEBRTC_ICELITE=true
WEBRTC_ICESERVERS_BACKEND="[{"urls":["stun:stun.l.google.com:19302"]}]"
WEBRTC_ICESERVERS_FRONTEND="[{"urls":["stun:stun.l.google.com:19305"]}]"
```
```properties title="config.properties"
capture.screencast.enabled = false
server.pprof = true
desktop.screen = 1920x1080@60
member.provider = multiuser
member.multiuser.admin_password = admin
member.multiuser.user_password = neko
session.merciful_reconnect = true
session.implicit_hosting = false
session.inactive_cursors = true
session.cookie.enabled = false
webrtc.icelite = true
webrtc.iceservers.backend[0].urls[0] = stun:stun.l.google.com:19302
webrtc.iceservers.frontend[0].urls[0] = stun:stun.l.google.com:19305
```
Room Configuration
This is the initial configuration of the room that can be modified by an admin in real-time.
<ConfigurationTab options={configOptions} filter={[ 'session.private_mode', 'session.locked_logins', 'session.locked_controls', 'session.control_protection', 'session.implicit_hosting', 'session.inactive_cursors', 'session.merciful_reconnect', 'session.heartbeat_interval', ]} comments={false} />
- whether private mode is enabled, users do not receive the room video or audio.
- whether logins are locked for users, admins can still login.
- whether controls are locked for users, admins can still control.
- users can gain control only if at least one admin is in the room.
- automatically grants control to a user when they click on the screen, unless an admin has locked the controls.
- whether to show inactive cursors server-wide (only for users that have it enabled in their profile).
- whether to allow reconnecting to the websocket even if the previous connection was not closed. This means that a new login can kick out the previous one.
- interval in seconds for sending a heartbeat message to the server. This is used to keep the connection alive and to detect when the connection is lost.
Server Configuration
This is the configuration of the neko server.
<ConfigurationTab options={configOptions} filter={[ 'server.bind', 'server.cert', 'server.key', 'server.cors', 'server.metrics', 'server.path_prefix', 'server.pprof', 'server.proxy', 'server.static', ]} comments={false} />
- address/port/socket to serve neko. For docker you might want to bind to
0.0.0.0to allow connections from outside the container. - and paths to the SSL cert and key used to secure the neko server. If both are empty, the server will run in plain HTTP.
- is a list of allowed origins for CORS.
- If empty, CORS is disabled, and only same-origin requests are allowed.
- If
*is present, all origins are allowed. Neko will respond always with the requested origin, not with*since credentials are not allowed with wildcard. - If a list of origins is present, only those origins are allowed for CORS.
- when true, prometheus metrics are available at
/metrics. - is the prefix for all HTTP requests. This is useful when running neko behind a reverse proxy and you want to serve neko under a subpath, e.g.
/neko. - when true, the pprof endpoint is available at
/debug/pproffor debugging and profiling. This should be disabled in production. - when true, neko will trust the
X-Forwarded-ForandX-Real-IPheaders from the reverse proxy. Make sure your reverse proxy is configured to set these headers and never trust them when not behind a reverse proxy. See Reverse Proxy Setup for more information. - path to the directory containing the neko client files to serve. This is useful if you want to serve the client files on the same domain as the server.
Logging Configuration
This is the configuration of the logging system.
<ConfigurationTab options={configOptions} filter={[ 'log.dir', 'log.json', 'log.level', 'log.nocolor', 'log.time', ]} comments={false} />
- directory to store logs. If empty, logs are written to stdout. This is useful when running neko in a container.
- when true, logs are written in JSON format.
- log level to set. Available levels are
trace,debug,info,warn,error,fatal,panic, anddisabled. - when true, ANSI colors are disabled in non-JSON output. Accepts as well
NO_COLORenvironment variable. - time format used in logs. Available formats are
unix,unixms, andunixmicro.
:::tip
Shortcut environment variable to enable DEBUG mode: NEKO_DEBUG=true
:::
Full Configuration Reference
Here is a full configuration with default values as shown in the help command. Please refer to the sub-sections for more details.
Next Steps
import DocCardList from '@theme/DocCardList';