This commit is contained in:
root
2026-05-18 03:02:15 +00:00
parent 00953ad831
commit ab45c6c329
12 changed files with 233 additions and 56 deletions

View File

@@ -66,13 +66,17 @@ NEKO_HTTP_PORT_3=9202
NEKO_START_URL=https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com/
NEKO_IMAGE_X86=ghcr.io/m1k1o/neko/google-chrome:3.0.0
NEKO_IMAGE_ARM64=ghcr.io/m1k1o/neko/chromium:3.0.0
NEKO_INSTANCE_COUNT=2
# Neko 浏览器登录:显示昵称可填 live / admin密码与全站策略一致默认 12345678
NEKO_USER_PASS=12345678
NEKO_ADMIN_PASS=12345678
# 持久化 Chromium 配置油猴、YouTube 登录、脚本、开发者数据);可从参考机 rsync 整个目录覆盖以迁移
NEKO_PROFILE_HOST_DIR=
NEKO_SHM_SIZE=2gb
NEKO_DESKTOP_SCREEN=1920x1080@30
NEKO_SHM_SIZE=512mb
NEKO_DESKTOP_SCREEN=1280x720@15
NEKO_MAX_FPS=15
NEKO_VIDEO_BITRATE=1200
NEKO_BROWSER_RENDER_MODE=auto
NEKO_WEBRTC_EPR=52000-52099
NEKO_WEBRTC_EPR_1=52000-52099
NEKO_WEBRTC_EPR_2=52101-52199

View File

@@ -126,11 +126,11 @@ def _resolve_youtube_push_config_file(config_file: str | None = None, *, base_di
def _load_youtube_push_settings(config_file: str) -> dict[str, str | bool]:
settings: dict[str, str | bool] = {
"use_rtmps": True,
"b_v": "4000k",
"maxrate": "4500k",
"bufsize": "8000k",
"b_v": "2800k",
"maxrate": "3200k",
"bufsize": "5600k",
"stream_key": "",
"fast_audio": False,
"fast_audio": True,
}
if not os.path.exists(config_file):
return settings
@@ -183,9 +183,9 @@ def start_douyin_youtube_ffplay(
rtmp_base = "rtmp://a.rtmp.youtube.com/live2/"
use_rtmps = True
# 720p 竖屏 YouTube 建议 46 Mbps偏低易触发「数据不足/卡顿」提示
b_v, maxrate, bufsize = "4000k", "4500k", "8000k"
b_v, maxrate, bufsize = "2800k", "3200k", "5600k"
stream_key = None
fast_audio = False # config: fast_audio=1 可跳过 arnndn+长 EQ显著减轻 CPU
fast_audio = True # 默认轻量音频链,必要时可在配置里关闭
if os.path.exists(config_file):
config = configparser.ConfigParser()
@@ -343,7 +343,7 @@ def start_douyin_youtube_ffplay(
print(f"[WARN] NVENC 检测失败,回退软件编码: {e}")
_cpu_n = os.cpu_count() or 4
_filter_threads = str(min(4, max(1, _cpu_n // 2)))
_filter_threads = str(min(2, max(1, _cpu_n // 2)))
# ====================== 开源音频处理:背景音乐弱化 + 防版权 ======================
_script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -372,7 +372,7 @@ def start_douyin_youtube_ffplay(
print("[INFO] 未找到 arnndn 模型,使用 afftdn 降噪(可克隆 richardpl/arnndn-models 到 arnndn-models/")
_af_denoise = "afftdn=nf=-26:tn=1,"
# 强化版:收紧带通 + 频率偏移 + 音高微调 + 多段EQ + 相位扰动(纯 FFmpeg 理论上限 ~70%
# 仅在明确需要时才启用重音频链,默认优先保 CPU
_af_chain = (
_af_denoise
+ "highpass=f=200,lowpass=f=3800,"
@@ -389,14 +389,14 @@ def start_douyin_youtube_ffplay(
+ "aresample=async=1:first_pts=0"
)
# 720p 竖屏,符合 YouTube 推荐46 Mbps、2s 关键帧、AAC 128k
_video_mid = ["-vsync", "cfr", "-g", "60", "-keyint_min", "60", "-r", "30",
# 720p 竖屏低耗档:更低帧率和码率,优先保证 2 路并发可用
_video_mid = ["-vsync", "cfr", "-g", "30", "-keyint_min", "30", "-r", "15",
"-bf", "0", "-sc_threshold", "0"]
if codec_v == "libx264":
_video_mid += ["-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0"]
# -re按源时间戳节奏拉流避免突发过快不用 realtime/arealtime输入抖动时易让 YouTube 判为数据不足)
_vf_pad = (
"fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=bilinear,"
"fps=15,scale=720:1280:force_original_aspect_ratio=decrease:flags=bilinear,"
"pad=720:1280:(ow-iw)/2:(oh-ih)/2:black"
)
_ffmpeg_pre_input = []

View File

@@ -6,11 +6,17 @@
"ports": { "http": 9200 },
"env": {
"ENABLE_NEKO": "1",
"NEKO_INSTANCE_COUNT": "2",
"NEKO_HTTP_PORT": "9200",
"NEKO_HTTP_PORT_1": "9200",
"NEKO_HTTP_PORT_2": "9201",
"NEKO_HTTP_PORT_3": "9202",
"NEKO_PROFILE_HOST_DIR": "/opt/live-modules/neko-profile",
"NEKO_SHM_SIZE": "512mb",
"NEKO_DESKTOP_SCREEN": "1280x720@15",
"NEKO_MAX_FPS": "15",
"NEKO_VIDEO_BITRATE": "1200",
"NEKO_BROWSER_RENDER_MODE": "auto",
"NEKO_WEBRTC_UDP_RANGE": "52000-52099",
"NEKO_WEBRTC_UDP_RANGE_1": "52000-52099",
"NEKO_WEBRTC_UDP_RANGE_2": "52101-52199",

View File

@@ -242,6 +242,7 @@ load_env() {
NEKO_HTTP_PORT_1="${NEKO_HTTP_PORT_1:-$NEKO_HTTP_PORT}"
NEKO_HTTP_PORT_2="${NEKO_HTTP_PORT_2:-9201}"
NEKO_HTTP_PORT_3="${NEKO_HTTP_PORT_3:-9202}"
NEKO_INSTANCE_COUNT="${NEKO_INSTANCE_COUNT:-2}"
NEKO_WEBRTC_TCPMUX="${NEKO_WEBRTC_TCPMUX:-52300}"
NEKO_WEBRTC_UDPMUX="${NEKO_WEBRTC_UDPMUX:-52300}"
NEKO_WEBRTC_TCPMUX_1="${NEKO_WEBRTC_TCPMUX_1:-$NEKO_WEBRTC_TCPMUX}"
@@ -266,6 +267,11 @@ load_env() {
case "$NEKO_IMAGE_ARM64" in
ghcr.io/m1k1o/neko/chromium:latest) NEKO_IMAGE_ARM64="ghcr.io/m1k1o/neko/chromium:3.0.0" ;;
esac
NEKO_SHM_SIZE="${NEKO_SHM_SIZE:-512mb}"
NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"
NEKO_MAX_FPS="${NEKO_MAX_FPS:-15}"
NEKO_VIDEO_BITRATE="${NEKO_VIDEO_BITRATE:-1200}"
NEKO_BROWSER_RENDER_MODE="${NEKO_BROWSER_RENDER_MODE:-auto}"
ENABLE_REDROID="${ENABLE_REDROID:-1}"
REDROID_DEFAULT_PORT="${REDROID_DEFAULT_PORT:-5555}"
REDROID_INSTANCE_PREFIX="${REDROID_INSTANCE_PREFIX:-redroid13}"
@@ -364,6 +370,54 @@ resolve_neko_profile_dir_for_instance() {
esac
}
resolve_neko_instance_count() {
local requested="${NEKO_INSTANCE_COUNT:-2}"
local cpu count
cpu="$(nproc 2>/dev/null || echo 4)"
case "$requested" in
1|2|3) count="$requested" ;;
auto|"")
if [ "$cpu" -le 4 ]; then
count=2
else
count=3
fi
;;
*)
count=2
;;
esac
if [ "$count" -lt 1 ]; then
count=1
fi
if [ "$count" -gt 3 ]; then
count=3
fi
printf '%s' "$count"
}
resolve_neko_service_names() {
local count name
count="$(resolve_neko_instance_count)"
for name in 1 2 3; do
[ "$name" -le "$count" ] || break
printf 'neko%s\n' "$name"
done
}
resolve_neko_instances_csv() {
resolve_neko_service_names | paste -sd, -
}
resolve_neko_http_port_for_instance() {
case "${1:-1}" in
1) printf '%s' "${NEKO_HTTP_PORT_1:-${NEKO_HTTP_PORT:-9200}}" ;;
2) printf '%s' "${NEKO_HTTP_PORT_2:-9201}" ;;
3) printf '%s' "${NEKO_HTTP_PORT_3:-9202}" ;;
*) printf '%s' "${NEKO_HTTP_PORT_1:-${NEKO_HTTP_PORT:-9200}}" ;;
esac
}
remove_legacy_neko_container() {
if have docker && docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx 'live-neko-chromium'; then
docker stop live-neko-chromium >/dev/null 2>&1 || true

View File

@@ -49,6 +49,7 @@ NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}"
NEKO_HTTP_PORT_1="${NEKO_HTTP_PORT_1:-$NEKO_HTTP_PORT}"
NEKO_HTTP_PORT_2="${NEKO_HTTP_PORT_2:-9201}"
NEKO_HTTP_PORT_3="${NEKO_HTTP_PORT_3:-9202}"
NEKO_INSTANCE_COUNT="${NEKO_INSTANCE_COUNT:-2}"
NEKO_WEBRTC_TCPMUX="${NEKO_WEBRTC_TCPMUX:-52300}"
NEKO_WEBRTC_UDPMUX="${NEKO_WEBRTC_UDPMUX:-52300}"
NEKO_WEBRTC_TCPMUX_1="${NEKO_WEBRTC_TCPMUX_1:-$NEKO_WEBRTC_TCPMUX}"
@@ -74,6 +75,11 @@ esac
case "$NEKO_IMAGE_ARM64" in
ghcr.io/m1k1o/neko/chromium:latest) NEKO_IMAGE_ARM64="ghcr.io/m1k1o/neko/chromium:3.0.0" ;;
esac
NEKO_SHM_SIZE="${NEKO_SHM_SIZE:-512mb}"
NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"
NEKO_MAX_FPS="${NEKO_MAX_FPS:-15}"
NEKO_VIDEO_BITRATE="${NEKO_VIDEO_BITRATE:-1200}"
NEKO_BROWSER_RENDER_MODE="${NEKO_BROWSER_RENDER_MODE:-auto}"
ENABLE_NEKO="${ENABLE_NEKO:-1}"
ENABLE_MITMPROXY="${ENABLE_MITMPROXY:-1}"
MITMPROXY_WEB_PORT="${MITMPROXY_WEB_PORT:-8086}"
@@ -292,6 +298,32 @@ resolve_neko_profile_dir_for_instance() {
esac
}
resolve_neko_instance_count() {
local requested="${NEKO_INSTANCE_COUNT:-2}"
local cpu count
cpu="$(nproc 2>/dev/null || echo 4)"
case "$requested" in
1|2|3) count="$requested" ;;
auto|"")
if [ "$cpu" -le 4 ]; then
count=2
else
count=3
fi
;;
*)
count=2
;;
esac
if [ "$count" -lt 1 ]; then
count=1
fi
if [ "$count" -gt 3 ]; then
count=3
fi
printf '%s' "$count"
}
remove_legacy_neko_container() {
if have docker && docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx 'live-neko-chromium'; then
docker stop live-neko-chromium >/dev/null 2>&1 || true
@@ -810,8 +842,15 @@ service_neko_status() {
fi
kv available 1
local state
local running_count=0 total_count=3 states="" name
local running_count=0 total_count=0 states="" name instance_count
instance_count="$(resolve_neko_instance_count)"
total_count="$instance_count"
for name in live-neko-1 live-neko-2 live-neko-3; do
case "$name" in
live-neko-1) [ "$instance_count" -ge 1 ] || continue ;;
live-neko-2) [ "$instance_count" -ge 2 ] || continue ;;
live-neko-3) [ "$instance_count" -ge 3 ] || continue ;;
esac
state="$(docker_container_status "$name" || true)"
if echo "$state" | grep -qi running; then
running_count=$((running_count + 1))
@@ -834,7 +873,7 @@ service_neko_status() {
if ! neko_nat1to1_is_usable "${NEKO_WEBRTC_NAT1TO1:-}"; then
NEKO_WEBRTC_NAT1TO1="$(detect_preferred_lan_ip || true)"
fi
kv detail "image=$(resolve_neko_image); supervisor_conf=$(resolve_neko_supervisor_conf); start_url=${NEKO_START_URL}; nat1to1=${NEKO_WEBRTC_NAT1TO1:-unset}; instances=${running_count}/${total_count}; ports=${NEKO_HTTP_PORT_1},${NEKO_HTTP_PORT_2},${NEKO_HTTP_PORT_3}; profiles=$(resolve_neko_profile_dir_for_instance 1),$(resolve_neko_profile_dir_for_instance 2),$(resolve_neko_profile_dir_for_instance 3); udp_ranges=${NEKO_WEBRTC_UDP_RANGE_1},${NEKO_WEBRTC_UDP_RANGE_2},${NEKO_WEBRTC_UDP_RANGE_3}; tcpmux=${NEKO_WEBRTC_TCPMUX_1},${NEKO_WEBRTC_TCPMUX_2},${NEKO_WEBRTC_TCPMUX_3}; udpmux=${NEKO_WEBRTC_UDPMUX_1},${NEKO_WEBRTC_UDPMUX_2},${NEKO_WEBRTC_UDPMUX_3}; containers=${states}"
kv detail "image=$(resolve_neko_image); supervisor_conf=$(resolve_neko_supervisor_conf); start_url=${NEKO_START_URL}; nat1to1=${NEKO_WEBRTC_NAT1TO1:-unset}; instances=${running_count}/${total_count}; ports=${NEKO_HTTP_PORT_1},${NEKO_HTTP_PORT_2},${NEKO_HTTP_PORT_3}; profiles=$(resolve_neko_profile_dir_for_instance 1),$(resolve_neko_profile_dir_for_instance 2),$(resolve_neko_profile_dir_for_instance 3); screen=${NEKO_DESKTOP_SCREEN}; shm=${NEKO_SHM_SIZE}; fps=${NEKO_MAX_FPS}; bitrate=${NEKO_VIDEO_BITRATE}; render=${NEKO_BROWSER_RENDER_MODE}; udp_ranges=${NEKO_WEBRTC_UDP_RANGE_1},${NEKO_WEBRTC_UDP_RANGE_2},${NEKO_WEBRTC_UDP_RANGE_3}; tcpmux=${NEKO_WEBRTC_TCPMUX_1},${NEKO_WEBRTC_TCPMUX_2},${NEKO_WEBRTC_TCPMUX_3}; udpmux=${NEKO_WEBRTC_UDPMUX_1},${NEKO_WEBRTC_UDPMUX_2},${NEKO_WEBRTC_UDPMUX_3}; containers=${states}"
}
service_neko_start() {
@@ -847,23 +886,73 @@ service_neko_start() {
return 0
fi
local prof
local instance_count
local service_names="" extra_service_names="" idx_name
instance_count="$(resolve_neko_instance_count)"
for idx_name in 1 2 3; do
case "$idx_name" in
1)
if [ "$instance_count" -ge 1 ]; then
service_names="${service_names} neko1"
else
extra_service_names="${extra_service_names} neko1"
fi
;;
2)
if [ "$instance_count" -ge 2 ]; then
service_names="${service_names} neko2"
else
extra_service_names="${extra_service_names} neko2"
fi
;;
3)
if [ "$instance_count" -ge 3 ]; then
service_names="${service_names} neko3"
else
extra_service_names="${extra_service_names} neko3"
fi
;;
esac
done
for idx in 1 2 3; do
[ "$idx" -le "$instance_count" ] || break
prof="$(resolve_neko_profile_dir_for_instance "$idx")"
prune_neko_profile "$prof"
done
export_neko_env
for port in "$NEKO_HTTP_PORT_1" "$NEKO_HTTP_PORT_2" "$NEKO_HTTP_PORT_3" "$NEKO_WEBRTC_TCPMUX_1" "$NEKO_WEBRTC_TCPMUX_2" "$NEKO_WEBRTC_TCPMUX_3"; do
case "$port" in
"$NEKO_HTTP_PORT_1"|"$NEKO_WEBRTC_TCPMUX_1") [ "$instance_count" -ge 1 ] || continue ;;
"$NEKO_HTTP_PORT_2"|"$NEKO_WEBRTC_TCPMUX_2") [ "$instance_count" -ge 2 ] || continue ;;
"$NEKO_HTTP_PORT_3"|"$NEKO_WEBRTC_TCPMUX_3") [ "$instance_count" -ge 3 ] || continue ;;
esac
allow_tcp_port_if_firewall_active "$port"
done
for port in "$NEKO_WEBRTC_UDPMUX_1" "$NEKO_WEBRTC_UDPMUX_2" "$NEKO_WEBRTC_UDPMUX_3"; do
case "$port" in
"$NEKO_WEBRTC_UDPMUX_1") [ "$instance_count" -ge 1 ] || continue ;;
"$NEKO_WEBRTC_UDPMUX_2") [ "$instance_count" -ge 2 ] || continue ;;
"$NEKO_WEBRTC_UDPMUX_3") [ "$instance_count" -ge 3 ] || continue ;;
esac
allow_udp_port_if_firewall_active "$port"
done
for port_range in "$NEKO_WEBRTC_UDP_RANGE_1" "$NEKO_WEBRTC_UDP_RANGE_2" "$NEKO_WEBRTC_UDP_RANGE_3"; do
case "$port_range" in
"$NEKO_WEBRTC_UDP_RANGE_1") [ "$instance_count" -ge 1 ] || continue ;;
"$NEKO_WEBRTC_UDP_RANGE_2") [ "$instance_count" -ge 2 ] || continue ;;
"$NEKO_WEBRTC_UDP_RANGE_3") [ "$instance_count" -ge 3 ] || continue ;;
esac
allow_udp_range_if_firewall_active "$port_range"
done
remove_legacy_neko_container
compose_in_service_dir "$NEKO_COMPOSE" up -d
kv message "Neko stack started (3 browser instances)"
# 只启动实际需要的实例,避免多余的 Chrome/Xorg 进程继续吃 CPU
# shellcheck disable=SC2086
compose_in_service_dir "$NEKO_COMPOSE" up -d $service_names
if [ -n "${extra_service_names# }" ]; then
# shellcheck disable=SC2086
compose_in_service_dir "$NEKO_COMPOSE" stop $extra_service_names >/dev/null 2>&1 || true
fi
kv message "Neko stack started (${instance_count} browser instances)"
}
export_neko_env() {
@@ -871,6 +960,7 @@ export_neko_env() {
export NEKO_HTTP_PORT_1="${NEKO_HTTP_PORT_1:-$NEKO_HTTP_PORT}"
export NEKO_HTTP_PORT_2="${NEKO_HTTP_PORT_2:-9201}"
export NEKO_HTTP_PORT_3="${NEKO_HTTP_PORT_3:-9202}"
export NEKO_INSTANCE_COUNT="${NEKO_INSTANCE_COUNT:-2}"
export NEKO_USER_PASS="${NEKO_USER_PASS:-12345678}"
export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-12345678}"
export NEKO_PROFILE_HOST_DIR="${NEKO_PROFILE_HOST_DIR:-$(resolve_neko_profile_dir)}"
@@ -880,7 +970,11 @@ export_neko_env() {
export NEKO_START_URL="${NEKO_START_URL:-https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com/}"
export NEKO_IMAGE="$(resolve_neko_image)"
export NEKO_BROWSER_SUPERVISOR_CONF="$(resolve_neko_supervisor_conf)"
export NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1920x1080@30}"
export NEKO_SHM_SIZE="${NEKO_SHM_SIZE:-512mb}"
export NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"
export NEKO_MAX_FPS="${NEKO_MAX_FPS:-15}"
export NEKO_VIDEO_BITRATE="${NEKO_VIDEO_BITRATE:-1200}"
export NEKO_BROWSER_RENDER_MODE="${NEKO_BROWSER_RENDER_MODE:-auto}"
export NEKO_WEBRTC_UDP_RANGE="${NEKO_WEBRTC_UDP_RANGE:-52000-52099}"
export NEKO_WEBRTC_UDP_RANGE_1="${NEKO_WEBRTC_UDP_RANGE_1:-$NEKO_WEBRTC_UDP_RANGE}"
export NEKO_WEBRTC_UDP_RANGE_2="${NEKO_WEBRTC_UDP_RANGE_2:-52101-52199}"

View File

@@ -36,7 +36,7 @@ case "$render_mode" in
auto)
case "$arch:$family" in
x86_64:google-chrome|amd64:google-chrome)
render_flags="--disable-gpu --disable-software-rasterizer"
render_flags="--disable-gpu --disable-software-rasterizer --disable-features=UseSkiaRenderer"
;;
esac
;;
@@ -44,10 +44,10 @@ case "$render_mode" in
render_flags=""
;;
legacy|x11|disable-gpu)
render_flags="--disable-gpu --disable-software-rasterizer"
render_flags="--disable-gpu --disable-software-rasterizer --disable-features=UseSkiaRenderer"
;;
software|swiftshader)
render_flags="--disable-gpu --use-gl=swiftshader --enable-unsafe-swiftshader"
render_flags="--disable-gpu --use-gl=swiftshader --enable-unsafe-swiftshader --disable-features=UseSkiaRenderer"
;;
esac
@@ -75,8 +75,17 @@ exec "$browser_bin" \
--user-data-dir="$profile_dir" \
--password-store=basic \
--no-first-run \
--start-maximized \
--start-minimized \
--force-dark-mode \
--disable-file-system \
--disable-dev-shm-usage \
--disable-background-networking \
--disable-breakpad \
--disable-default-apps \
--disable-extensions \
--disable-sync \
--disable-translate \
--metrics-recording-only \
--no-default-browser-check \
--no-first-run \
"$start_url"

View File

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

View File

@@ -1,7 +1,7 @@
x-neko-common: &neko-common
image: "${NEKO_IMAGE:-ghcr.io/m1k1o/neko/chromium:3.0.0}"
restart: unless-stopped
shm_size: "${NEKO_SHM_SIZE:-2gb}"
shm_size: "${NEKO_SHM_SIZE:-512mb}"
cap_add:
- SYS_ADMIN
@@ -14,9 +14,12 @@ x-neko-env: &neko-env
NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD: "${NEKO_ADMIN_PASS}"
NEKO_WEBRTC_ICELITE: "${NEKO_WEBRTC_ICELITE}"
NEKO_WEBRTC_NAT1TO1: "${NEKO_WEBRTC_NAT1TO1}"
NEKO_MAX_FPS: "${NEKO_MAX_FPS}"
NEKO_VIDEO_BITRATE: "${NEKO_VIDEO_BITRATE}"
NEKO_PLUGINS_ENABLED: "${NEKO_PLUGINS_ENABLED}"
NEKO_PLUGINS_DIR: "/etc/neko/plugins/"
NEKO_START_URL: "${NEKO_START_URL}"
NEKO_BROWSER_RENDER_MODE: "${NEKO_BROWSER_RENDER_MODE}"
services:
neko1:
@@ -37,6 +40,7 @@ services:
- "${NEKO_PROFILE_HOST_DIR_1}:/home/neko/.config/google-chrome"
- "./policies:/etc/chromium/policies/managed:ro"
- "./policies:/etc/opt/chrome/policies/managed:ro"
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro"
neko2:
@@ -57,6 +61,7 @@ services:
- "${NEKO_PROFILE_HOST_DIR_2}:/home/neko/.config/google-chrome"
- "./policies:/etc/chromium/policies/managed:ro"
- "./policies:/etc/opt/chrome/policies/managed:ro"
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro"
neko3:
@@ -77,4 +82,5 @@ services:
- "${NEKO_PROFILE_HOST_DIR_3}:/home/neko/.config/google-chrome"
- "./policies:/etc/chromium/policies/managed:ro"
- "./policies:/etc/opt/chrome/policies/managed:ro"
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro"

View File

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

View File

@@ -497,11 +497,12 @@ def _env_int(env: dict[str, str], key: str, default: int) -> int:
def _neko_http_ports(env: dict[str, str]) -> list[int]:
count = max(1, min(3, _env_int(env, "NEKO_INSTANCE_COUNT", 2)))
return [
_env_int(env, "NEKO_HTTP_PORT_1", _env_int(env, "NEKO_HTTP_PORT", 9200)),
_env_int(env, "NEKO_HTTP_PORT_2", 9201),
_env_int(env, "NEKO_HTTP_PORT_3", 9202),
]
][:count]
def _neko_instances(mdns_host: str, env: dict[str, str]) -> list[dict[str, Any]]:
@@ -877,13 +878,14 @@ async def stack_stream_probes() -> dict[str, Any]:
http = _probe_http_get(f"http://127.0.0.1:{port}/", timeout=2.5)
instances.append({"id": idx, "port": port, "tcp": tcp, "http": http})
primary = instances[0] if instances else {"port": 0, "tcp": {}, "http": {}}
required_count = max(1, min(3, _env_int(env, "NEKO_INSTANCE_COUNT", 2)))
return {
"port": primary["port"],
"tcp": primary["tcp"],
"http": primary["http"],
"instances": instances,
"running_count": sum(1 for item in instances if item["tcp"].get("ok")),
"required_count": 3,
"required_count": required_count,
}
srs_d, neko_d = await asyncio.gather(

View File

@@ -15,6 +15,8 @@ class NekoConfigTests(unittest.TestCase):
self.assertIn("--disable-software-rasterizer", script)
self.assertIn("--use-gl=swiftshader", script)
self.assertIn("NEKO_BROWSER_RENDER_MODE", script)
self.assertIn("--start-minimized", script)
self.assertIn("--disable-background-networking", script)
self.assertIn('prune_path "$profile_dir/BrowserMetrics"', script)
self.assertIn('prune_path "$profile_dir/Crash Reports"', script)
self.assertIn('prune_path "$profile_dir/SingletonLock"', script)
@@ -30,28 +32,29 @@ class NekoConfigTests(unittest.TestCase):
def test_google_chrome_supervisor_uses_direct_stable_command(self) -> None:
conf = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
self.assertIn("command=/usr/bin/google-chrome", conf)
self.assertNotIn("browser-launch.sh", conf)
self.assertIn("--user-data-dir=/home/neko/.config/google-chrome", conf)
self.assertIn("command=/etc/neko/browser-launch.sh google-chrome", conf)
self.assertNotIn("--bwsi", conf)
self.assertIn("--disable-software-rasterizer", conf)
self.assertIn("autorestart=true", conf)
self.assertIn("[program:openbox]", conf)
self.assertIn("/usr/bin/openbox --config-file /etc/neko/openbox.xml", conf)
script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
self.assertIn('profile_dir="/home/neko/.config/google-chrome"', script)
self.assertIn('--user-data-dir="$profile_dir"', script)
def test_chromium_supervisor_uses_direct_stable_command(self) -> None:
conf = (ROOT / "services" / "neko" / "chromium.conf").read_text(encoding="utf-8")
self.assertIn("command=/usr/bin/chromium", conf)
self.assertNotIn("browser-launch.sh", conf)
self.assertIn("--user-data-dir=/home/neko/.config/chromium", conf)
self.assertIn("command=/etc/neko/browser-launch.sh chromium", conf)
self.assertNotIn("--bwsi", conf)
self.assertIn("--disable-software-rasterizer", conf)
self.assertIn("autorestart=true", conf)
self.assertIn("[program:openbox]", conf)
script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
self.assertIn('profile_dir="/home/neko/.config/chromium"', script)
self.assertIn('--user-data-dir="$profile_dir"', script)
def test_neko_compose_mounts_only_selected_supervisor_conf(self) -> None:
compose = (ROOT / "services" / "neko" / "docker-compose.yml").read_text(encoding="utf-8")
self.assertIn("./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro", compose)
self.assertIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose)
self.assertIn("container_name: live-neko-1", compose)
self.assertIn("container_name: live-neko-2", compose)
self.assertIn("container_name: live-neko-3", compose)
@@ -60,7 +63,14 @@ class NekoConfigTests(unittest.TestCase):
self.assertIn("${NEKO_HTTP_PORT_3}:8080", compose)
self.assertNotIn("./chromium.conf:/etc/neko/supervisord/chromium.conf:ro", compose)
self.assertNotIn("./google-chrome.conf:/etc/neko/supervisord/google-chrome.conf:ro", compose)
self.assertNotIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose)
def test_supervisor_uses_browser_launch_script(self) -> None:
chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
chromium = (ROOT / "services" / "neko" / "chromium.conf").read_text(encoding="utf-8")
self.assertIn("command=/etc/neko/browser-launch.sh google-chrome", chrome)
self.assertIn("command=/etc/neko/browser-launch.sh chromium", chromium)
self.assertNotIn("--start-maximized", chrome)
self.assertNotIn("--start-maximized", chromium)
def test_neko_scripts_use_family_specific_profile_dirs(self) -> None:
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")

View File

@@ -18,6 +18,18 @@ class YoutubeRuntimeTests(unittest.TestCase):
self.assertEqual(normalize_youtube_video_save_type("ts", "tiktok"), "TS")
self.assertEqual(normalize_youtube_video_save_type("unknown", "tiktok"), "TS")
def test_push_defaults_use_lighter_profile(self) -> None:
from pathlib import Path
from tempfile import TemporaryDirectory
from douyin_youtube_ffplay import _load_youtube_push_settings
with TemporaryDirectory() as tmp:
cfg = Path(tmp) / "youtube.ini"
cfg.write_text("[youtube]\nkey = abc\n", encoding="utf-8")
settings = _load_youtube_push_settings(str(cfg))
self.assertEqual(settings["b_v"], "2800k")
self.assertTrue(settings["fast_audio"])
def test_arm_falls_back_from_vaapi_to_v4l2m2m(self) -> None:
enc, note = resolve_live_video_encoder(
"h264_vaapi",