nix/moduler/services/webcam-rtsp/default.nix
2026-03-29 15:28:46 +02:00

238 lines
8.3 KiB
Nix

{
lib,
config,
pkgs,
...
}:
with lib;
let
cfg = config.webcam-rtsp;
effectiveStreams =
if cfg.streams != { } then
cfg.streams
else
{
main = {
device = cfg.device;
rtspUrl = cfg.rtspUrl;
framerate = cfg.framerate;
videoSize = cfg.videoSize;
preset = cfg.preset;
bitrateKbps = cfg.bitrateKbps;
maxrateKbps = cfg.maxrateKbps;
bufsizeKbps = cfg.bufsizeKbps;
useVaapi = cfg.useVaapi;
vaapiDevice = cfg.vaapiDevice;
vaapiQp = cfg.vaapiQp;
vaapiDriver = cfg.vaapiDriver;
};
};
sanitizeName = name: replaceStrings [ "/" " " ] [ "-" "-" ] name;
in
{
options = {
webcam-rtsp = {
enable = mkEnableOption "enables webcam RTSP publisher";
device = mkOption {
type = types.str;
default = "/dev/v4l/by-id/usb-GENERAL_GENERAL_WEBCAM-video-index0";
description = "V4L2 device used as input for ffmpeg.";
};
rtspUrl = mkOption {
type = types.str;
default = "rtsp://192.168.1.143:8554/laptop";
description = "Destination RTSP URL where ffmpeg publishes the stream.";
};
framerate = mkOption {
type = types.int;
default = 30;
description = "Input framerate for the webcam stream.";
};
videoSize = mkOption {
type = types.str;
default = "1280x720";
description = "Input video size for the webcam stream.";
};
preset = mkOption {
type = types.enum [
"ultrafast"
"superfast"
"veryfast"
"faster"
"fast"
"medium"
"slow"
"slower"
"veryslow"
];
default = "veryfast";
description = "x264 preset used for software encoding.";
};
bitrateKbps = mkOption {
type = types.nullOr types.int;
default = null;
description = "Target bitrate in kbps. Set to null for CRF-like unconstrained output.";
};
maxrateKbps = mkOption {
type = types.nullOr types.int;
default = null;
description = "Maximum bitrate in kbps. Defaults to bitrateKbps when unset.";
};
bufsizeKbps = mkOption {
type = types.nullOr types.int;
default = null;
description = "Rate-control buffer in kbps. Defaults to 2x bitrateKbps when unset.";
};
useVaapi = mkOption {
type = types.bool;
default = false;
description = "Use VAAPI hardware encoding (h264_vaapi) instead of libx264.";
};
vaapiDevice = mkOption {
type = types.str;
default = "/dev/dri/renderD128";
description = "VAAPI render device path used when useVaapi = true.";
};
vaapiQp = mkOption {
type = types.int;
default = 24;
description = "VAAPI quality parameter (lower means better quality, higher CPU/bitrate).";
};
vaapiDriver = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional LIBVA_DRIVER_NAME value (for example iHD).";
};
streams = mkOption {
type = types.attrsOf (
types.submodule {
options = {
device = mkOption {
type = types.str;
description = "V4L2 device used as input for ffmpeg.";
};
rtspUrl = mkOption {
type = types.str;
description = "Destination RTSP URL where ffmpeg publishes the stream.";
};
framerate = mkOption {
type = types.int;
default = 30;
description = "Input framerate for this stream.";
};
videoSize = mkOption {
type = types.str;
default = "1280x720";
description = "Input video size for this stream.";
};
preset = mkOption {
type = types.enum [
"ultrafast"
"superfast"
"veryfast"
"faster"
"fast"
"medium"
"slow"
"slower"
"veryslow"
];
default = "veryfast";
description = "x264 preset used for software encoding.";
};
bitrateKbps = mkOption {
type = types.nullOr types.int;
default = null;
description = "Target bitrate in kbps. Set to null for unconstrained output.";
};
maxrateKbps = mkOption {
type = types.nullOr types.int;
default = null;
description = "Maximum bitrate in kbps. Defaults to bitrateKbps when unset.";
};
bufsizeKbps = mkOption {
type = types.nullOr types.int;
default = null;
description = "Rate-control buffer in kbps. Defaults to 2x bitrateKbps when unset.";
};
useVaapi = mkOption {
type = types.bool;
default = false;
description = "Use VAAPI hardware encoding (h264_vaapi) instead of libx264.";
};
vaapiDevice = mkOption {
type = types.str;
default = "/dev/dri/renderD128";
description = "VAAPI render device path used when useVaapi = true.";
};
vaapiQp = mkOption {
type = types.int;
default = 24;
description = "VAAPI quality parameter for h264_vaapi.";
};
vaapiDriver = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional LIBVA_DRIVER_NAME value (for example iHD).";
};
};
}
);
default = { };
description = "Named stream definitions. One systemd service is created per stream.";
};
};
};
config = mkIf cfg.enable {
systemd.services = mapAttrs' (
streamName: streamCfg:
let
maxrateKbps =
if streamCfg.maxrateKbps != null then streamCfg.maxrateKbps else streamCfg.bitrateKbps;
bufsizeKbps =
if streamCfg.bufsizeKbps != null then
streamCfg.bufsizeKbps
else if streamCfg.bitrateKbps != null then
streamCfg.bitrateKbps * 2
else
null;
x264RateControlArgs =
if streamCfg.bitrateKbps != null then
"-b:v ${toString streamCfg.bitrateKbps}k -maxrate ${toString maxrateKbps}k -bufsize ${toString bufsizeKbps}k"
else
"";
vaapiRateControlArgs =
if streamCfg.bitrateKbps != null then
"-rc_mode VBR -b:v ${toString streamCfg.bitrateKbps}k -maxrate ${toString maxrateKbps}k -bufsize ${toString bufsizeKbps}k"
else
"-rc_mode CQP -qp ${toString streamCfg.vaapiQp}";
videoCodecArgs =
if streamCfg.useVaapi then
"-vaapi_device ${streamCfg.vaapiDevice} -vf format=nv12,hwupload -c:v h264_vaapi -profile:v main -level:v 4.1 ${vaapiRateControlArgs} -g ${
toString (streamCfg.framerate * 2)
} -bf 0"
else
"-vcodec libx264 -pix_fmt yuv420p -profile:v main -level:v 4.1 -tune zerolatency -preset ${streamCfg.preset} ${x264RateControlArgs} -g ${
toString (streamCfg.framerate * 2)
} -bf 0";
in
nameValuePair "webcam-rtsp-publisher-${sanitizeName streamName}" {
description = "Publish webcam stream '${streamName}' to MediaMTX over RTSP";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
Restart = "always";
RestartSec = "2";
Environment = optional (streamCfg.vaapiDriver != null) "LIBVA_DRIVER_NAME=${streamCfg.vaapiDriver}";
ExecStart = "${pkgs.ffmpeg}/bin/ffmpeg -hide_banner -loglevel warning -f v4l2 -framerate ${toString streamCfg.framerate} -video_size ${streamCfg.videoSize} -i ${streamCfg.device} ${videoCodecArgs} -f rtsp -rtsp_transport tcp ${streamCfg.rtspUrl}";
};
}
) effectiveStreams;
};
}