@camstack/addon-pipeline 1.2.47 → 1.2.49

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.
Files changed (37) hide show
  1. package/dist/{model-download-service-D8B-4ktF-7-YxE9Wx.js → addon-utils-DveoBR6G.js} +219 -3
  2. package/dist/{model-download-service-D8B-4ktF-BB7oZL3y.mjs → addon-utils-QuxtOdK1.mjs} +219 -3
  3. package/dist/audio-analyzer/index.js +4 -4
  4. package/dist/audio-analyzer/index.mjs +2 -2
  5. package/dist/detection-pipeline/index.js +9 -9
  6. package/dist/detection-pipeline/index.mjs +3 -3
  7. package/dist/{dist-CfxC7XoI.mjs → dist-CLJKoYEP.mjs} +1301 -1284
  8. package/dist/{dist-C9Q1t0nT.js → dist-J2DeRZAN.js} +1301 -1284
  9. package/dist/{event-loop-stall-monitor-B_XUPTwp.mjs → event-loop-stall-monitor-B6dqQjiX.mjs} +1 -1
  10. package/dist/{event-loop-stall-monitor-Luq9AX4t.js → event-loop-stall-monitor-Bkbiw20z.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/recorder/index.js +5 -5
  16. package/dist/recorder/index.mjs +2 -2
  17. package/dist/session-decode/decode-worker-child.js +1 -1
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-SKj0Zdz2.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DZikMEgD.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cj2xMupA.mjs +26 -0
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czrsozsg.mjs +26 -0
  23. package/dist/stream-broker/{hostInit-D8gyxL0j.mjs → hostInit-i24FVz-3.mjs} +2 -2
  24. package/dist/stream-broker/index.js +198 -132
  25. package/dist/stream-broker/index.mjs +197 -131
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/dist/{worker-protocol-rqbyKlm9.mjs → worker-protocol-BxPNiino.mjs} +1 -1
  28. package/dist/{worker-protocol-BUJpm7l3.js → worker-protocol-CNi5srgV.js} +1 -1
  29. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DwXIyaHw.js → MaskShapeCanvas-DI4BY7W2-m7YpaY98.js} +1 -1
  30. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Cf8EtWEG.js → MotionZonesSettings-NcxxQN8r-619YTOjy.js} +1 -1
  31. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BvsVXya9.js → PrivacyMaskSettings-APgPLF7p-B-k8v0O5.js} +1 -1
  32. package/embed-dist/assets/index-CYO4OY8_.js +112 -0
  33. package/embed-dist/index.html +1 -1
  34. package/package.json +1 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-KpixNEma.mjs +0 -26
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-rd7UaqfV.mjs +0 -26
  37. package/embed-dist/assets/index-BuAeIGv8.js +0 -112
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-C9Q1t0nT.js");
6
+ const require_dist = require("../dist-J2DeRZAN.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
- const require_model_download_service_D8B_4ktF = require("../model-download-service-D8B-4ktF-7-YxE9Wx.js");
8
+ const require_addon_utils = require("../addon-utils-DveoBR6G.js");
9
9
  let node_crypto = require("node:crypto");
10
10
  node_crypto = require_chunk.__toESM(node_crypto, 1);
11
11
  let node_child_process = require("node:child_process");
@@ -37,7 +37,7 @@ let node_events = require("node:events");
37
37
  * of the "live stream takes ~10s to start" cold-start cost (worse on 4G). */
38
38
  var IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
39
39
  function createEmbedSpaHandler(root) {
40
- const fileHandler = require_model_download_service_D8B_4ktF.createFileDataPlaneHandler({
40
+ const fileHandler = require_addon_utils.createFileDataPlaneHandler({
41
41
  getRoots: () => [root],
42
42
  cacheControl: (rel) => rel.startsWith("assets/") ? IMMUTABLE_CACHE : void 0
43
43
  });
@@ -5167,127 +5167,6 @@ function defaultId() {
5167
5167
  counter += 1;
5168
5168
  return `${Date.now().toString(36)}${counter.toString(36)}`;
5169
5169
  }
5170
- /** A derive that has not finished by now will not finish inside the drain. */
5171
- var GIF_RENDER_TIMEOUT_MS = 2e4;
5172
- /**
5173
- * The filter chain. Extracted so a test can assert the palette pair without
5174
- * spawning ffmpeg — an argv test cannot see quality, but it can see a filter
5175
- * that somebody deleted.
5176
- */
5177
- function gifFilterComplex(input) {
5178
- const setpts = input.speed !== 1 ? `,setpts=PTS/${input.speed}` : "";
5179
- return `[0:v]fps=${input.fps}${setpts},scale=${input.maxWidth}:-2:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`;
5180
- }
5181
- /** The full argv, for the same reason. */
5182
- function gifFromMp4Args(input) {
5183
- return [
5184
- "-y",
5185
- "-v",
5186
- "error",
5187
- "-f",
5188
- "mp4",
5189
- "-i",
5190
- "pipe:0",
5191
- "-filter_complex",
5192
- gifFilterComplex(input),
5193
- "-loop",
5194
- "0",
5195
- "-f",
5196
- "gif",
5197
- "pipe:1"
5198
- ];
5199
- }
5200
- /**
5201
- * Derive the gif. `null` on every failure — a notification that lost its gif is
5202
- * a notification, and one that failed is not.
5203
- *
5204
- * The child is guaranteed dead before this settles: these run on every
5205
- * notification that asks for a gif, and a leaked ffmpeg accumulates on the node
5206
- * until somebody notices the host is slow.
5207
- */
5208
- async function gifFromMp4(input) {
5209
- const args = gifFromMp4Args({
5210
- maxWidth: input.maxWidth ?? 640,
5211
- fps: input.fps ?? 8,
5212
- speed: input.speed ?? 1
5213
- });
5214
- const startedAt = Date.now();
5215
- try {
5216
- const bytes = await runGif(input, args);
5217
- input.logger?.info("nc gif: derived from the notification video", {
5218
- tags: { deviceId: input.deviceId },
5219
- meta: {
5220
- sourceBytes: input.mp4.length,
5221
- gifBytes: bytes.length,
5222
- width: input.maxWidth ?? 640,
5223
- fps: input.fps ?? 8,
5224
- encodeMs: Date.now() - startedAt
5225
- }
5226
- });
5227
- return bytes;
5228
- } catch (err) {
5229
- input.logger?.warn("nc gif: the derive failed — this notification ships without a gif", {
5230
- tags: { deviceId: input.deviceId },
5231
- meta: {
5232
- sourceBytes: input.mp4.length,
5233
- error: err instanceof Error ? err.message : String(err)
5234
- }
5235
- });
5236
- return null;
5237
- }
5238
- }
5239
- function runGif(input, args) {
5240
- return new Promise((resolve, reject) => {
5241
- const proc = input.spawnFn(input.ffmpegPath ?? "ffmpeg", [...args], { stdio: [
5242
- "pipe",
5243
- "pipe",
5244
- "pipe"
5245
- ] });
5246
- const chunks = [];
5247
- let stderrTail = "";
5248
- let done = false;
5249
- let timer = null;
5250
- const settle = (err, out) => {
5251
- if (done) return;
5252
- done = true;
5253
- if (timer !== null) clearTimeout(timer);
5254
- if (proc.exitCode === null && proc.signalCode === null) try {
5255
- proc.kill("SIGKILL");
5256
- } catch {}
5257
- try {
5258
- proc.stdout?.removeAllListeners();
5259
- proc.stderr?.removeAllListeners();
5260
- proc.stdin?.destroy();
5261
- } catch {}
5262
- if (err !== null) reject(err);
5263
- else resolve(out ?? new Uint8Array(0));
5264
- };
5265
- timer = setTimeout(() => settle(/* @__PURE__ */ new Error("gif derive timed out")), GIF_RENDER_TIMEOUT_MS);
5266
- timer.unref?.();
5267
- proc.stdout?.on("data", (c) => chunks.push(c));
5268
- proc.stderr?.on("data", (c) => {
5269
- stderrTail = (stderrTail + c.toString()).slice(-400);
5270
- });
5271
- proc.on("error", (err) => settle(err instanceof Error ? err : new Error(String(err))));
5272
- proc.on("close", (code) => {
5273
- if (code !== 0) {
5274
- settle(/* @__PURE__ */ new Error(`ffmpeg gif derive exited ${code}: ${stderrTail}`));
5275
- return;
5276
- }
5277
- const out = Buffer.concat(chunks);
5278
- if (out.byteLength === 0) {
5279
- settle(/* @__PURE__ */ new Error("gif derive produced no bytes"));
5280
- return;
5281
- }
5282
- const bytes = new Uint8Array(out.byteLength);
5283
- bytes.set(out);
5284
- settle(null, bytes);
5285
- });
5286
- proc.stdin?.on("error", () => {});
5287
- proc.stdin?.write(Buffer.from(input.mp4));
5288
- proc.stdin?.end();
5289
- });
5290
- }
5291
5170
  //#endregion
5292
5171
  //#region src/stream-broker/webrtc/nal-utils.ts
5293
5172
  var NAL_START_CODE_4B = Buffer.from([
@@ -5968,6 +5847,41 @@ function toElementaryStream(window, sdpParameterSets) {
5968
5847
  }
5969
5848
  var RENDER_TIMEOUT_MS = 2e4;
5970
5849
  /**
5850
+ * The widest an event clip is RE-ENCODED at — 720p.
5851
+ *
5852
+ * Only ever reached when the render already forced a decode (a speed other than
5853
+ * real time, or an H.265 source). At real time the width must stay the source's
5854
+ * own, because an identity scale is precisely what `clipCanCopy` looks for.
5855
+ *
5856
+ * Measured on a real 615 720p window, `-preset veryfast`: crf 23 at 1280 is
5857
+ * 254 KB and 0.23 s of encode, against the 922 KB the copy of the same window
5858
+ * carried. Capping wider renditions here costs nothing an operator can see on a
5859
+ * phone and keeps the burst bounded on a hub that is already busy.
5860
+ */
5861
+ var EVENT_CLIP_ENCODE_MAX_WIDTH = 1280;
5862
+ /**
5863
+ * The width to render an event clip at, given what the source is and whether
5864
+ * the caller asked for real time.
5865
+ *
5866
+ * One function because two call sites have to agree: the mux and the
5867
+ * `copy`/`encode` the production REPORTS. They disagreed once already — a
5868
+ * reported mode that does not match the render is a log line that lies.
5869
+ */
5870
+ function eventClipEncodeWidth(sourceWidth, speed) {
5871
+ const source = Math.max(1, Math.round(sourceWidth));
5872
+ if (speed === 1) return source;
5873
+ return Math.min(source, EVENT_CLIP_ENCODE_MAX_WIDTH);
5874
+ }
5875
+ /**
5876
+ * Format a frame rate for an ffmpeg filter argument.
5877
+ *
5878
+ * `12 / 4` must reach the filter as `3` and `15 / 4` as `3.75`, not as a float
5879
+ * tail that `av_parse_video_rate` then has to guess at.
5880
+ */
5881
+ function ffmpegRate(rate) {
5882
+ return String(Math.round(rate * 1e3) / 1e3);
5883
+ }
5884
+ /**
5971
5885
  * Whether this render can COPY the camera's own H.264 instead of re-encoding it.
5972
5886
  *
5973
5887
  * The ring already holds the camera's elementary stream. Re-encoding it is only
@@ -6003,6 +5917,7 @@ function clipFfmpegArgs(input) {
6003
5917
  const demuxer = input.hevc === true ? "hevc" : "h264";
6004
5918
  const speed = input.speed ?? 2;
6005
5919
  const setpts = speed !== 1 ? `,setpts=PTS/${speed}` : "";
5920
+ const gifSetpts = speed !== 1 ? `${setpts},fps=${ffmpegRate(input.fps * speed)}` : setpts;
6006
5921
  if (input.format === "gif") return [
6007
5922
  "-y",
6008
5923
  "-f",
@@ -6010,7 +5925,7 @@ function clipFfmpegArgs(input) {
6010
5925
  "-i",
6011
5926
  "pipe:0",
6012
5927
  "-filter_complex",
6013
- `[0:v]fps=${input.fps}${setpts},${scale},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`,
5928
+ `[0:v]fps=${ffmpegRate(input.fps)}${gifSetpts},${scale},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`,
6014
5929
  "-loop",
6015
5930
  "0",
6016
5931
  "-f",
@@ -6045,7 +5960,7 @@ function clipFfmpegArgs(input) {
6045
5960
  "-preset",
6046
5961
  "veryfast",
6047
5962
  "-crf",
6048
- "28",
5963
+ "23",
6049
5964
  "-pix_fmt",
6050
5965
  "yuv420p",
6051
5966
  "-movflags",
@@ -6121,6 +6036,142 @@ async function renderPreBufferClip(input) {
6121
6036
  proc.stdin?.end();
6122
6037
  });
6123
6038
  }
6039
+ /**
6040
+ * The factor the derive must apply ON TOP of the speed the mp4 already carries.
6041
+ *
6042
+ * The gif is derived from the muxed mp4, so if that mux already ran `setpts` at
6043
+ * the caller's `speed`, applying the absolute gif rate again would compound
6044
+ * them — a 4× gif beside a 2× video would come out at 8×. Discounting here is
6045
+ * what keeps {@link GIF_SPEED} an ABSOLUTE rate against real time whatever the
6046
+ * video does.
6047
+ */
6048
+ function gifDeriveSpeed(gifSpeed, videoSpeed) {
6049
+ if (!Number.isFinite(videoSpeed) || videoSpeed <= 0) return gifSpeed;
6050
+ return gifSpeed / videoSpeed;
6051
+ }
6052
+ /** A derive that has not finished by now will not finish inside the drain. */
6053
+ var GIF_RENDER_TIMEOUT_MS = 2e4;
6054
+ /**
6055
+ * The filter chain. Extracted so a test can assert the palette pair without
6056
+ * spawning ffmpeg — an argv test cannot see quality, but it can see a filter
6057
+ * that somebody deleted.
6058
+ */
6059
+ function gifFilterComplex(input) {
6060
+ const speed = input.speed > 0 ? input.speed : 1;
6061
+ if (speed === 1) return `[0:v]fps=${ffmpegRate(input.fps)},scale=${input.maxWidth}:-2:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`;
6062
+ return `[0:v]fps=${ffmpegRate(input.fps / speed)},setpts=PTS/${speed},fps=${ffmpegRate(input.fps)},scale=${input.maxWidth}:-2:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`;
6063
+ }
6064
+ /** The full argv, for the same reason. */
6065
+ function gifFromMp4Args(input) {
6066
+ return [
6067
+ "-y",
6068
+ "-v",
6069
+ "error",
6070
+ "-f",
6071
+ "mp4",
6072
+ "-i",
6073
+ "pipe:0",
6074
+ "-filter_complex",
6075
+ gifFilterComplex(input),
6076
+ "-loop",
6077
+ "0",
6078
+ "-f",
6079
+ "gif",
6080
+ "pipe:1"
6081
+ ];
6082
+ }
6083
+ /**
6084
+ * Derive the gif. `null` on every failure — a notification that lost its gif is
6085
+ * a notification, and one that failed is not.
6086
+ *
6087
+ * The child is guaranteed dead before this settles: these run on every
6088
+ * notification that asks for a gif, and a leaked ffmpeg accumulates on the node
6089
+ * until somebody notices the host is slow.
6090
+ */
6091
+ async function gifFromMp4(input) {
6092
+ const args = gifFromMp4Args({
6093
+ maxWidth: input.maxWidth ?? 640,
6094
+ fps: input.fps ?? 12,
6095
+ speed: input.speed ?? 4
6096
+ });
6097
+ const startedAt = Date.now();
6098
+ try {
6099
+ const bytes = await runGif(input, args);
6100
+ input.logger?.info("nc gif: derived from the notification video", {
6101
+ tags: { deviceId: input.deviceId },
6102
+ meta: {
6103
+ sourceBytes: input.mp4.length,
6104
+ gifBytes: bytes.length,
6105
+ width: input.maxWidth ?? 640,
6106
+ fps: input.fps ?? 12,
6107
+ speed: input.speed ?? 4,
6108
+ encodeMs: Date.now() - startedAt
6109
+ }
6110
+ });
6111
+ return bytes;
6112
+ } catch (err) {
6113
+ input.logger?.warn("nc gif: the derive failed — this notification ships without a gif", {
6114
+ tags: { deviceId: input.deviceId },
6115
+ meta: {
6116
+ sourceBytes: input.mp4.length,
6117
+ error: err instanceof Error ? err.message : String(err)
6118
+ }
6119
+ });
6120
+ return null;
6121
+ }
6122
+ }
6123
+ function runGif(input, args) {
6124
+ return new Promise((resolve, reject) => {
6125
+ const proc = input.spawnFn(input.ffmpegPath ?? "ffmpeg", [...args], { stdio: [
6126
+ "pipe",
6127
+ "pipe",
6128
+ "pipe"
6129
+ ] });
6130
+ const chunks = [];
6131
+ let stderrTail = "";
6132
+ let done = false;
6133
+ let timer = null;
6134
+ const settle = (err, out) => {
6135
+ if (done) return;
6136
+ done = true;
6137
+ if (timer !== null) clearTimeout(timer);
6138
+ if (proc.exitCode === null && proc.signalCode === null) try {
6139
+ proc.kill("SIGKILL");
6140
+ } catch {}
6141
+ try {
6142
+ proc.stdout?.removeAllListeners();
6143
+ proc.stderr?.removeAllListeners();
6144
+ proc.stdin?.destroy();
6145
+ } catch {}
6146
+ if (err !== null) reject(err);
6147
+ else resolve(out ?? new Uint8Array(0));
6148
+ };
6149
+ timer = setTimeout(() => settle(/* @__PURE__ */ new Error("gif derive timed out")), GIF_RENDER_TIMEOUT_MS);
6150
+ timer.unref?.();
6151
+ proc.stdout?.on("data", (c) => chunks.push(c));
6152
+ proc.stderr?.on("data", (c) => {
6153
+ stderrTail = (stderrTail + c.toString()).slice(-400);
6154
+ });
6155
+ proc.on("error", (err) => settle(err instanceof Error ? err : new Error(String(err))));
6156
+ proc.on("close", (code) => {
6157
+ if (code !== 0) {
6158
+ settle(/* @__PURE__ */ new Error(`ffmpeg gif derive exited ${code}: ${stderrTail}`));
6159
+ return;
6160
+ }
6161
+ const out = Buffer.concat(chunks);
6162
+ if (out.byteLength === 0) {
6163
+ settle(/* @__PURE__ */ new Error("gif derive produced no bytes"));
6164
+ return;
6165
+ }
6166
+ const bytes = new Uint8Array(out.byteLength);
6167
+ bytes.set(out);
6168
+ settle(null, bytes);
6169
+ });
6170
+ proc.stdin?.on("error", () => {});
6171
+ proc.stdin?.write(Buffer.from(input.mp4));
6172
+ proc.stdin?.end();
6173
+ });
6174
+ }
6124
6175
  //#endregion
6125
6176
  //#region src/stream-broker/stream-broker/profile-restreamers.ts
6126
6177
  /**
@@ -15251,9 +15302,10 @@ var StreamBrokerManager = class StreamBrokerManager {
15251
15302
  postRollSec: input.postSeconds,
15252
15303
  ...preferred !== void 0 ? { profile: preferred } : {}
15253
15304
  });
15305
+ const clipWidth = eventClipEncodeWidth(cut.sourceWidth, input.speed);
15254
15306
  const mp4 = await this.muxClipWindow(cut, {
15255
15307
  format: "mp4",
15256
- maxWidth: Math.max(1, cut.sourceWidth),
15308
+ maxWidth: clipWidth,
15257
15309
  fps: 0,
15258
15310
  speed: input.speed
15259
15311
  });
@@ -15269,22 +15321,23 @@ var StreamBrokerManager = class StreamBrokerManager {
15269
15321
  nodeId: this.localNodeId ?? "",
15270
15322
  mime: "video/mp4",
15271
15323
  bytes: mp4.byteLength,
15272
- width: cut.sourceWidth,
15273
- height: cut.sourceHeight
15324
+ width: clipWidth,
15325
+ height: scaledHeight(cut, clipWidth)
15274
15326
  });
15327
+ const gifSpeed = input.gifSpeed ?? 4;
15275
15328
  if (input.kinds.includes("gif")) {
15276
15329
  const gif = await gifFromMp4({
15277
15330
  mp4,
15278
15331
  deviceId: input.deviceId,
15279
15332
  maxWidth: input.gifMaxWidth,
15280
15333
  fps: input.gifFps,
15281
- speed: 1,
15334
+ speed: gifDeriveSpeed(gifSpeed, input.speed),
15282
15335
  ...this.logger !== void 0 ? { logger: this.logger } : {},
15283
15336
  spawnFn: node_child_process.spawn,
15284
15337
  ...this.ffmpegConfig.binaryPath !== void 0 ? { ffmpegPath: this.ffmpegConfig.binaryPath } : {}
15285
15338
  });
15286
15339
  if (gif !== null) {
15287
- const height = cut.sourceWidth > 0 ? Math.round(input.gifMaxWidth / cut.sourceWidth * cut.sourceHeight) : 0;
15340
+ const height = scaledHeight(cut, input.gifMaxWidth);
15288
15341
  media.push({
15289
15342
  kind: "gif",
15290
15343
  handle: this.eventMedia.put({
@@ -15303,7 +15356,7 @@ var StreamBrokerManager = class StreamBrokerManager {
15303
15356
  }
15304
15357
  const video = this.clipVideoMode(cut, {
15305
15358
  format: "mp4",
15306
- maxWidth: Math.max(1, cut.sourceWidth),
15359
+ maxWidth: clipWidth,
15307
15360
  speed: input.speed
15308
15361
  });
15309
15362
  this.logger?.info("event media produced — one window, every container", {
@@ -15315,6 +15368,8 @@ var StreamBrokerManager = class StreamBrokerManager {
15315
15368
  packets: cut.window.length,
15316
15369
  kinds: [...input.kinds].join(","),
15317
15370
  artifacts: media.map((m) => `${m.kind}:${m.bytes}`).join(" "),
15371
+ speed: input.speed,
15372
+ gifSpeed,
15318
15373
  startOffsetMs: cut.coverage.fromTs - input.aroundMs,
15319
15374
  endOffsetMs: cut.coverage.toTs - input.aroundMs
15320
15375
  }
@@ -17572,6 +17627,17 @@ var StreamBrokerManager = class StreamBrokerManager {
17572
17627
  }
17573
17628
  }
17574
17629
  };
17630
+ /**
17631
+ * The height a render lands at, once `scale=W:-2` has picked it.
17632
+ *
17633
+ * `-2` means "keep the aspect, round to an EVEN number" — libx264 rejects an
17634
+ * odd dimension in yuv420p. Reporting the arithmetic without the rounding is
17635
+ * how an artifact ends up one pixel taller than the file it describes.
17636
+ */
17637
+ function scaledHeight(cut, width) {
17638
+ if (cut.sourceWidth <= 0 || cut.sourceHeight <= 0) return 0;
17639
+ return 2 * Math.round(width / cut.sourceWidth * cut.sourceHeight / 2);
17640
+ }
17575
17641
  //#endregion
17576
17642
  //#region src/stream-broker/encode-profile-resolver.ts
17577
17643
  /**