@apocaliss92/scrypted-reolink-native 0.5.42 → 0.5.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.5.42",
3
+ "version": "0.5.44",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
@@ -44,7 +44,7 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@apocaliss92/nodelink-js": "^0.5.2",
47
+ "@apocaliss92/nodelink-js": "^0.6.0",
48
48
  "@scrypted/common": "file:../../scrypted/common",
49
49
  "@scrypted/rtsp": "file:../../scrypted/plugins/rtsp",
50
50
  "@scrypted/sdk": "^0.3.118"
package/src/camera.ts CHANGED
@@ -43,7 +43,43 @@ import sdk, {
43
43
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
44
44
  import crypto from "crypto";
45
45
  import fs from "fs";
46
+ import os from "os";
46
47
  import path from "path";
48
+
49
+ /**
50
+ * Per-platform list of ffmpeg H.264 encoders we offer in the composite
51
+ * settings dropdown. Mirrors `@scrypted/common/src/ffmpeg-hardware-
52
+ * acceleration.ts`'s `getH264EncoderArgs()` but replicated here so we
53
+ * don't take a build dependency on Scrypted's internal common package
54
+ * (the subpath import doesn't resolve cleanly through the file-symlink
55
+ * Scrypted plugins use). The shape mirrors the upstream helper exactly
56
+ * so we can swap to the real one as soon as the resolution issue is
57
+ * addressed upstream.
58
+ *
59
+ * Each value is the full `-c:v <encoder> [extra-args...]` arg list. The
60
+ * code below pulls element [1] for the `-c:v` identifier and propagates
61
+ * the rest through `extraOutputArgs`.
62
+ */
63
+ function getCompositeH264EncoderArgs(): { [label: string]: string[] } {
64
+ const args: { [label: string]: string[] } = {};
65
+ const platform = os.platform();
66
+ if (platform === "darwin") {
67
+ args["VideoToolbox"] = ["-c:v", "h264_videotoolbox"];
68
+ } else if (platform === "win32") {
69
+ args["Intel QuickSync"] = ["-c:v", "h264_qsv"];
70
+ args["AMD"] = ["-c:v", "h264_amf"];
71
+ args["Nvidia"] = ["-c:v", "h264_nvenc"];
72
+ } else if (platform === "linux") {
73
+ args["V4L2 (Raspberry Pi)"] = [
74
+ "-pix_fmt", "yuv420p",
75
+ "-c:v", "h264_v4l2m2m",
76
+ ];
77
+ args["VAAPI"] = ["-c:v", "h264_vaapi"];
78
+ args["Nvidia"] = ["-c:v", "h264_nvenc"];
79
+ }
80
+ args["libx264 (Software)"] = ["-c:v", "libx264"];
81
+ return args;
82
+ }
47
83
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
48
84
  import {
49
85
  ReolinkCameraAutotracking,
@@ -683,6 +719,109 @@ export class ReolinkCamera
683
719
  this.scheduleStreamManagerRestart("compositeDisableTranscode changed");
684
720
  },
685
721
  },
722
+ // ─── Composite ffmpeg tuning ─────────────────────────────────
723
+ // Encoder + quality knobs. Defaults preserve the previous behavior
724
+ // (libx264 / ultrafast / crf 23 / 1s GOP). Switching the encoder
725
+ // away from libx264 is the single biggest perf win — try the HW
726
+ // encoder for your host: h264_videotoolbox (macOS), h264_qsv or
727
+ // h264_vaapi (Intel/AMD), h264_nvenc (NVIDIA), h264_v4l2m2m
728
+ // (Raspberry Pi). When you change the encoder the libx264-specific
729
+ // preset/CRF knobs are silently dropped — use "Composite: extra
730
+ // output args" to pass the encoder's own options.
731
+ // Encoder choices come straight from Scrypted's per-platform
732
+ // hardware-acceleration helper, so the dropdown only ever offers
733
+ // encoders that ffmpeg on this host actually supports — Mac sees
734
+ // VideoToolbox, Windows sees QuickSync/AMD/Nvidia, Linux sees
735
+ // V4L2/VAAPI/Nvidia, Raspberry Pi sees its dedicated V4L2 path.
736
+ // The displayed label is human-readable ("VideoToolbox",
737
+ // "Intel QuickSync", "libx264 (Software)") and the actual ffmpeg
738
+ // `-c:v` identifier is resolved at use time via
739
+ // `resolveCompositeVideoEncoder()` below.
740
+ compositeVideoEncoder: {
741
+ title: "Composite: ffmpeg encoder",
742
+ description:
743
+ "Output video encoder. Choices are limited to what ffmpeg supports on this host. Hardware encoders typically reduce CPU 5-10× vs libx264.",
744
+ type: "string",
745
+ defaultValue: "libx264 (Software)",
746
+ group: "Composite stream",
747
+ hide: true,
748
+ choices: Object.keys(getCompositeH264EncoderArgs()),
749
+ onPut: async () => {
750
+ this.scheduleStreamManagerRestart("compositeVideoEncoder changed");
751
+ },
752
+ },
753
+ compositeEncoderPreset: {
754
+ title: "Composite: libx264 preset",
755
+ description:
756
+ "Only used when encoder is libx264. ultrafast = least CPU / worst compression; placebo = the opposite. Default ultrafast.",
757
+ type: "string",
758
+ defaultValue: "ultrafast",
759
+ group: "Composite stream",
760
+ hide: true,
761
+ choices: [
762
+ "ultrafast",
763
+ "superfast",
764
+ "veryfast",
765
+ "faster",
766
+ "fast",
767
+ "medium",
768
+ "slow",
769
+ "slower",
770
+ "veryslow",
771
+ "placebo",
772
+ ],
773
+ onPut: async () => {
774
+ this.scheduleStreamManagerRestart("compositeEncoderPreset changed");
775
+ },
776
+ },
777
+ compositeCrf: {
778
+ title: "Composite: libx264 CRF (quality)",
779
+ description:
780
+ "Only used when encoder is libx264. 0 = lossless (huge), 23 = visually lossless, 51 = worst. Drop by 2 to roughly halve bitrate at the same quality.",
781
+ type: "number",
782
+ defaultValue: 23,
783
+ group: "Composite stream",
784
+ hide: true,
785
+ onPut: async () => {
786
+ this.scheduleStreamManagerRestart("compositeCrf changed");
787
+ },
788
+ },
789
+ compositeGopSeconds: {
790
+ title: "Composite: keyframe interval (seconds)",
791
+ description:
792
+ "Lower = faster mid-stream join + larger stream; higher = better compression + slower join. Default 1s.",
793
+ type: "number",
794
+ defaultValue: 1,
795
+ group: "Composite stream",
796
+ hide: true,
797
+ onPut: async () => {
798
+ this.scheduleStreamManagerRestart("compositeGopSeconds changed");
799
+ },
800
+ },
801
+ compositeExtraGlobalArgs: {
802
+ title: "Composite: extra global args",
803
+ description:
804
+ "Free-form ffmpeg args inserted BEFORE the inputs. Use for hardware decode hints, e.g. \"-hwaccel videotoolbox\" or \"-hwaccel qsv -qsv_device /dev/dri/renderD128\". Whitespace-separated. Invalid args WILL crash ffmpeg.",
805
+ type: "string",
806
+ defaultValue: "",
807
+ group: "Composite stream",
808
+ hide: true,
809
+ onPut: async () => {
810
+ this.scheduleStreamManagerRestart("compositeExtraGlobalArgs changed");
811
+ },
812
+ },
813
+ compositeExtraOutputArgs: {
814
+ title: "Composite: extra output args",
815
+ description:
816
+ "Free-form ffmpeg args inserted JUST BEFORE the output. Use for encoder-specific options when encoder ≠ libx264, e.g. \"-q:v 23\" (qsv) or \"-preset fast -rc cbr\" (nvenc). Whitespace-separated. Invalid args WILL crash ffmpeg.",
817
+ type: "string",
818
+ defaultValue: "",
819
+ group: "Composite stream",
820
+ hide: true,
821
+ onPut: async () => {
822
+ this.scheduleStreamManagerRestart("compositeExtraOutputArgs changed");
823
+ },
824
+ },
686
825
  // ─── E-mail Push ─────────────────────────────────────────────
687
826
  // Per-camera knobs that pair with the singleton
688
827
  // `Reolink E-mail Push Server` device. Hidden by default and
@@ -1889,10 +2028,10 @@ export class ReolinkCamera
1889
2028
  /**
1890
2029
  * Initialize or recreate the StreamManager, taking into account multifocal composite options.
1891
2030
  */
1892
- protected initStreamManager(
2031
+ protected async initStreamManager(
1893
2032
  logger?: Console,
1894
2033
  forceRecreate: boolean = false,
1895
- ): void {
2034
+ ): Promise<void> {
1896
2035
  const { username, password } = this.storageSettings.values;
1897
2036
  // Ensure logger is always valid - use provided logger or get from device, fallback to console
1898
2037
  const validLogger = logger || this.getBaichuanLogger() || console;
@@ -1917,7 +2056,73 @@ export class ReolinkCamera
1917
2056
  rtspChannel,
1918
2057
  compositeAssumeH264,
1919
2058
  compositeDisableTranscode,
2059
+ compositeVideoEncoder,
2060
+ compositeEncoderPreset,
2061
+ compositeCrf,
2062
+ compositeGopSeconds,
2063
+ compositeExtraGlobalArgs,
2064
+ compositeExtraOutputArgs,
1920
2065
  } = this.storageSettings.values;
2066
+ // Get the path to the ffmpeg binary Scrypted ships with. The
2067
+ // composite stream spawns ffmpeg directly; on Windows / Electron
2068
+ // the host has a stripped PATH so a bare `ffmpeg` ENOENTs — we
2069
+ // MUST pass the absolute path the SDK knows about.
2070
+ let ffmpegPath: string | undefined;
2071
+ try {
2072
+ ffmpegPath = await sdk.mediaManager.getFFmpegPath();
2073
+ } catch {
2074
+ // Older Scrypted runtimes / weird environments may not expose
2075
+ // this. Fall through to the library's default ("ffmpeg" via PATH);
2076
+ // worst case the composite fails to spawn and the user sees a
2077
+ // friendly error.
2078
+ ffmpegPath = undefined;
2079
+ }
2080
+ // Whitespace-split the free-form extra args. Empty string -> [].
2081
+ const splitArgs = (s: string | undefined): string[] | undefined => {
2082
+ if (typeof s !== "string") return undefined;
2083
+ const arr = s
2084
+ .trim()
2085
+ .split(/\s+/)
2086
+ .filter((a) => a.length > 0);
2087
+ return arr.length > 0 ? arr : undefined;
2088
+ };
2089
+ const extraGlobalArgs = splitArgs(compositeExtraGlobalArgs);
2090
+ const extraOutputArgs = splitArgs(compositeExtraOutputArgs);
2091
+
2092
+ // Resolve the human-readable encoder label the user picked back
2093
+ // into the actual ffmpeg `-c:v` identifier. The helper returns
2094
+ // arrays shaped like ['-c:v', 'h264_videotoolbox', ...extras] —
2095
+ // we pull element [1] for the encoder name and propagate any
2096
+ // additional pix-fmt / pix-encoder args via extraOutputArgs so
2097
+ // they end up just before the output pipe.
2098
+ let resolvedVideoEncoder: string | undefined;
2099
+ let encoderImpliedOutputArgs: string[] = [];
2100
+ if (compositeVideoEncoder) {
2101
+ try {
2102
+ const encoderArgs = getCompositeH264EncoderArgs()[compositeVideoEncoder];
2103
+ // Some entries (V4L2/Raspberry Pi) carry pre-`-c:v` pixel-format
2104
+ // args. Find where `-c:v` is in the array — everything before
2105
+ // becomes encoderImpliedOutputArgs (pixfmt), the element after
2106
+ // `-c:v` is the encoder identifier, and anything after that is
2107
+ // also part of encoderImpliedOutputArgs.
2108
+ const cvIdx = encoderArgs?.indexOf("-c:v") ?? -1;
2109
+ if (encoderArgs && cvIdx >= 0 && encoderArgs[cvIdx + 1]) {
2110
+ resolvedVideoEncoder = encoderArgs[cvIdx + 1]!;
2111
+ // Everything except the `-c:v <encoder>` pair becomes extra
2112
+ // output args, preserving any pixel-format hints the upstream
2113
+ // helper baked in (e.g. yuv420p for V4L2).
2114
+ encoderImpliedOutputArgs = [
2115
+ ...encoderArgs.slice(0, cvIdx),
2116
+ ...encoderArgs.slice(cvIdx + 2),
2117
+ ];
2118
+ }
2119
+ } catch {
2120
+ // ignore; fall back to library default below
2121
+ }
2122
+ }
2123
+ const finalExtraOutputArgs = encoderImpliedOutputArgs.length || extraOutputArgs
2124
+ ? [...encoderImpliedOutputArgs, ...(extraOutputArgs ?? [])]
2125
+ : undefined;
1921
2126
 
1922
2127
  // On NVR/Hub, TrackMix lenses are selected via stream variant, not via a separate channel.
1923
2128
  // Use rtspChannel for BOTH wide and tele so the library can request tele via streamType/variant.
@@ -1967,6 +2172,20 @@ export class ReolinkCamera
1967
2172
  forceH264: true,
1968
2173
  assumeH264Inputs: compositeAssumeH264 ?? true,
1969
2174
  disableTranscode: compositeDisableTranscode ?? false,
2175
+ // ffmpeg knobs — only include when set so the library defaults stay live.
2176
+ ...(ffmpegPath ? { ffmpegPath } : {}),
2177
+ ...(resolvedVideoEncoder
2178
+ ? { videoEncoder: resolvedVideoEncoder }
2179
+ : {}),
2180
+ ...(compositeEncoderPreset
2181
+ ? { encoderPreset: compositeEncoderPreset }
2182
+ : {}),
2183
+ ...(typeof compositeCrf === "number" ? { crf: compositeCrf } : {}),
2184
+ ...(typeof compositeGopSeconds === "number"
2185
+ ? { gopSeconds: compositeGopSeconds }
2186
+ : {}),
2187
+ ...(extraGlobalArgs ? { extraGlobalArgs } : {}),
2188
+ ...(finalExtraOutputArgs ? { extraOutputArgs: finalExtraOutputArgs } : {}),
1970
2189
  };
1971
2190
  }
1972
2191
 
@@ -1995,7 +2214,7 @@ export class ReolinkCamera
1995
2214
  logger.log(
1996
2215
  "Restarting StreamManager due to PIP/composite settings change",
1997
2216
  );
1998
- this.initStreamManager(logger, true);
2217
+ await this.initStreamManager(logger, true);
1999
2218
 
2000
2219
  // Invalidate snapshot cache for battery/multifocal-battery so that
2001
2220
  // the next snapshot reflects the new PIP/composite configuration.
@@ -3560,7 +3779,7 @@ export class ReolinkCamera
3560
3779
  const logger = this.getBaichuanLogger();
3561
3780
  logger.warn("StreamManager not initialized, initializing now...");
3562
3781
  try {
3563
- this.initStreamManager(logger);
3782
+ await this.initStreamManager(logger);
3564
3783
  } catch (e) {
3565
3784
  logger.error(
3566
3785
  "Failed to initialize StreamManager in getVideoStream",
@@ -3788,6 +4007,18 @@ export class ReolinkCamera
3788
4007
  this.storageSettings.settings.pipPosition.hide = !this.isMultiFocal;
3789
4008
  this.storageSettings.settings.pipSize.hide = !this.isMultiFocal;
3790
4009
  this.storageSettings.settings.pipMargin.hide = !this.isMultiFocal;
4010
+ this.storageSettings.settings.compositeAssumeH264.hide = !this.isMultiFocal;
4011
+ this.storageSettings.settings.compositeDisableTranscode.hide =
4012
+ !this.isMultiFocal;
4013
+ this.storageSettings.settings.compositeVideoEncoder.hide = !this.isMultiFocal;
4014
+ this.storageSettings.settings.compositeEncoderPreset.hide =
4015
+ !this.isMultiFocal;
4016
+ this.storageSettings.settings.compositeCrf.hide = !this.isMultiFocal;
4017
+ this.storageSettings.settings.compositeGopSeconds.hide = !this.isMultiFocal;
4018
+ this.storageSettings.settings.compositeExtraGlobalArgs.hide =
4019
+ !this.isMultiFocal;
4020
+ this.storageSettings.settings.compositeExtraOutputArgs.hide =
4021
+ !this.isMultiFocal;
3791
4022
 
3792
4023
  const hideUid = !this.isBattery || this.isOnNvr || !!this.multiFocalDevice;
3793
4024
  this.storageSettings.settings.uid.hide = hideUid;
@@ -3844,7 +4075,7 @@ export class ReolinkCamera
3844
4075
  }
3845
4076
 
3846
4077
  try {
3847
- this.initStreamManager();
4078
+ await this.initStreamManager();
3848
4079
  } catch (e) {
3849
4080
  logger.error(
3850
4081
  "Failed to initialize StreamManager in init",