{ 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; rateControlArgs = if streamCfg.bitrateKbps != null then "-b:v ${toString streamCfg.bitrateKbps}k -maxrate ${toString maxrateKbps}k -bufsize ${toString bufsizeKbps}k" else ""; videoCodecArgs = if streamCfg.useVaapi then "-vaapi_device ${streamCfg.vaapiDevice} -vf format=nv12,hwupload -c:v h264_vaapi -qp ${toString streamCfg.vaapiQp}" else "-vcodec libx264 -tune zerolatency -preset ${streamCfg.preset}"; 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} ${rateControlArgs} -f rtsp ${streamCfg.rtspUrl}"; }; } ) effectiveStreams; }; }