homebridge-plugin-utils 1.35.0 → 2.0.0

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 (198) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +94 -45
  20. package/dist/backpressure.js +229 -93
  21. package/dist/backpressure.js.map +1 -1
  22. package/dist/cli/index.d.ts +95 -0
  23. package/dist/cli/index.js +355 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/eslint-plugin/config.d.mts +193 -0
  32. package/dist/eslint-plugin/index.d.mts +2 -0
  33. package/dist/eslint-plugin/plugin.d.mts +87 -0
  34. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  35. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  36. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  37. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  38. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  39. package/dist/featureOptions-docs.d.ts +96 -0
  40. package/dist/featureOptions-docs.js +193 -0
  41. package/dist/featureOptions-docs.js.map +1 -0
  42. package/dist/featureOptions.d.ts +674 -0
  43. package/dist/featureOptions.js +870 -0
  44. package/dist/featureOptions.js.map +1 -0
  45. package/dist/ffmpeg/codecs.d.ts +256 -72
  46. package/dist/ffmpeg/codecs.js +477 -262
  47. package/dist/ffmpeg/codecs.js.map +1 -1
  48. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  49. package/dist/ffmpeg/dgram-util.js +38 -0
  50. package/dist/ffmpeg/dgram-util.js.map +1 -0
  51. package/dist/ffmpeg/exec.d.ts +83 -64
  52. package/dist/ffmpeg/exec.js +77 -86
  53. package/dist/ffmpeg/exec.js.map +1 -1
  54. package/dist/ffmpeg/fmp4.d.ts +49 -2
  55. package/dist/ffmpeg/fmp4.js +47 -11
  56. package/dist/ffmpeg/fmp4.js.map +1 -1
  57. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  58. package/dist/ffmpeg/hap-enums.js +92 -0
  59. package/dist/ffmpeg/hap-enums.js.map +1 -0
  60. package/dist/ffmpeg/index.d.ts +16 -9
  61. package/dist/ffmpeg/index.js +6 -0
  62. package/dist/ffmpeg/index.js.map +1 -1
  63. package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
  64. package/dist/ffmpeg/mp4-assembler.js +387 -0
  65. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  66. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  67. package/dist/ffmpeg/mp4-parser.js +130 -0
  68. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  69. package/dist/ffmpeg/options.d.ts +62 -149
  70. package/dist/ffmpeg/options.js +608 -499
  71. package/dist/ffmpeg/options.js.map +1 -1
  72. package/dist/ffmpeg/process.d.ts +142 -96
  73. package/dist/ffmpeg/process.js +406 -278
  74. package/dist/ffmpeg/process.js.map +1 -1
  75. package/dist/ffmpeg/record.d.ts +302 -188
  76. package/dist/ffmpeg/record.js +408 -567
  77. package/dist/ffmpeg/record.js.map +1 -1
  78. package/dist/ffmpeg/recording-process-double.d.ts +144 -0
  79. package/dist/ffmpeg/recording-process-double.js +168 -0
  80. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  81. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  82. package/dist/ffmpeg/rtp-parser.js +77 -0
  83. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  84. package/dist/ffmpeg/rtp.d.ts +198 -141
  85. package/dist/ffmpeg/rtp.js +473 -251
  86. package/dist/ffmpeg/rtp.js.map +1 -1
  87. package/dist/ffmpeg/settings.d.ts +5 -2
  88. package/dist/ffmpeg/settings.js +20 -5
  89. package/dist/ffmpeg/settings.js.map +1 -1
  90. package/dist/ffmpeg/stream.d.ts +57 -107
  91. package/dist/ffmpeg/stream.js +121 -150
  92. package/dist/ffmpeg/stream.js.map +1 -1
  93. package/dist/formatters.d.ts +106 -0
  94. package/dist/formatters.js +174 -0
  95. package/dist/formatters.js.map +1 -0
  96. package/dist/homebridge-enums.d.ts +30 -0
  97. package/dist/homebridge-enums.js +17 -0
  98. package/dist/homebridge-enums.js.map +1 -0
  99. package/dist/index.d.ts +12 -6
  100. package/dist/index.js +7 -2
  101. package/dist/index.js.map +1 -1
  102. package/dist/logclient/auth.d.ts +114 -0
  103. package/dist/logclient/auth.js +199 -0
  104. package/dist/logclient/auth.js.map +1 -0
  105. package/dist/logclient/cli-run.d.ts +76 -0
  106. package/dist/logclient/cli-run.js +639 -0
  107. package/dist/logclient/cli-run.js.map +1 -0
  108. package/dist/logclient/cli.d.ts +3 -0
  109. package/dist/logclient/cli.js +97 -0
  110. package/dist/logclient/cli.js.map +1 -0
  111. package/dist/logclient/client.d.ts +145 -0
  112. package/dist/logclient/client.js +600 -0
  113. package/dist/logclient/client.js.map +1 -0
  114. package/dist/logclient/config.d.ts +173 -0
  115. package/dist/logclient/config.js +199 -0
  116. package/dist/logclient/config.js.map +1 -0
  117. package/dist/logclient/endpoints.d.ts +54 -0
  118. package/dist/logclient/endpoints.js +73 -0
  119. package/dist/logclient/endpoints.js.map +1 -0
  120. package/dist/logclient/filter.d.ts +45 -0
  121. package/dist/logclient/filter.js +51 -0
  122. package/dist/logclient/filter.js.map +1 -0
  123. package/dist/logclient/frame.d.ts +93 -0
  124. package/dist/logclient/frame.js +203 -0
  125. package/dist/logclient/frame.js.map +1 -0
  126. package/dist/logclient/index.d.ts +31 -0
  127. package/dist/logclient/index.js +12 -0
  128. package/dist/logclient/index.js.map +1 -0
  129. package/dist/logclient/parser.d.ts +211 -0
  130. package/dist/logclient/parser.js +393 -0
  131. package/dist/logclient/parser.js.map +1 -0
  132. package/dist/logclient/rest.d.ts +41 -0
  133. package/dist/logclient/rest.js +111 -0
  134. package/dist/logclient/rest.js.map +1 -0
  135. package/dist/logclient/settings.d.ts +15 -0
  136. package/dist/logclient/settings.js +64 -0
  137. package/dist/logclient/settings.js.map +1 -0
  138. package/dist/logclient/socket-double.d.ts +201 -0
  139. package/dist/logclient/socket-double.js +384 -0
  140. package/dist/logclient/socket-double.js.map +1 -0
  141. package/dist/logclient/socket.d.ts +257 -0
  142. package/dist/logclient/socket.js +620 -0
  143. package/dist/logclient/socket.js.map +1 -0
  144. package/dist/logclient/stitch.d.ts +83 -0
  145. package/dist/logclient/stitch.js +146 -0
  146. package/dist/logclient/stitch.js.map +1 -0
  147. package/dist/logclient/time-expression.d.ts +42 -0
  148. package/dist/logclient/time-expression.js +181 -0
  149. package/dist/logclient/time-expression.js.map +1 -0
  150. package/dist/logclient/time-window.d.ts +38 -0
  151. package/dist/logclient/time-window.js +53 -0
  152. package/dist/logclient/time-window.js.map +1 -0
  153. package/dist/logclient/types.d.ts +107 -0
  154. package/dist/logclient/types.js +6 -0
  155. package/dist/logclient/types.js.map +1 -0
  156. package/dist/mqttClient.d.ts +287 -0
  157. package/dist/mqttClient.js +433 -0
  158. package/dist/mqttClient.js.map +1 -0
  159. package/dist/service.d.ts +64 -15
  160. package/dist/service.js +93 -66
  161. package/dist/service.js.map +1 -1
  162. package/dist/ui/featureOptions.js +870 -0
  163. package/dist/ui/featureOptions.js.map +1 -0
  164. package/dist/ui/formatters.js +174 -0
  165. package/dist/ui/formatters.js.map +1 -0
  166. package/dist/ui/pluginConfigSession.mjs +141 -0
  167. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  168. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  169. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  170. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  171. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  172. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  173. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  174. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  175. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  176. package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
  177. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  179. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  180. package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
  181. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  182. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  183. package/dist/ui/webUi-featureOptions.mjs +677 -0
  184. package/dist/ui/webUi.mjs +192 -87
  185. package/dist/util.d.ts +602 -45
  186. package/dist/util.js +783 -78
  187. package/dist/util.js.map +1 -1
  188. package/package.json +33 -15
  189. package/build/eslint-rules.mjs +0 -511
  190. package/dist/featureoptions.d.ts +0 -264
  191. package/dist/featureoptions.js +0 -480
  192. package/dist/featureoptions.js.map +0 -1
  193. package/dist/mqttclient.d.ts +0 -178
  194. package/dist/mqttclient.js +0 -310
  195. package/dist/mqttclient.js.map +0 -1
  196. package/dist/ui/featureoptions.js +0 -480
  197. package/dist/ui/featureoptions.js.map +0 -1
  198. package/dist/ui/webUi-featureoptions.mjs +0 -3765
@@ -0,0 +1,387 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * ffmpeg/mp4-assembler.ts: AsyncDisposable fMP4 segment assembler composing Mp4BoxParser against a Readable byte source.
4
+ */
5
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
6
+ if (value !== null && value !== void 0) {
7
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
8
+ var dispose, inner;
9
+ if (async) {
10
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
11
+ dispose = value[Symbol.asyncDispose];
12
+ }
13
+ if (dispose === void 0) {
14
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
15
+ dispose = value[Symbol.dispose];
16
+ if (async) inner = dispose;
17
+ }
18
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
19
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
20
+ env.stack.push({ value: value, dispose: dispose, async: async });
21
+ }
22
+ else if (async) {
23
+ env.stack.push({ async: true });
24
+ }
25
+ return value;
26
+ };
27
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
28
+ return function (env) {
29
+ function fail(e) {
30
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
31
+ env.hasError = true;
32
+ }
33
+ var r, s = 0;
34
+ function next() {
35
+ while (r = env.stack.pop()) {
36
+ try {
37
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
38
+ if (r.dispose) {
39
+ var result = r.dispose.call(r.value);
40
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
41
+ }
42
+ else s |= 1;
43
+ }
44
+ catch (e) {
45
+ fail(e);
46
+ }
47
+ }
48
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
49
+ if (env.hasError) throw env.error;
50
+ }
51
+ return next();
52
+ };
53
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
54
+ var e = new Error(message);
55
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
56
+ });
57
+ /**
58
+ * AsyncDisposable fMP4 segment assembler.
59
+ *
60
+ * The assembler composes a {@link Mp4BoxParser} against an arbitrary Node {@link Readable} source (typically an FFmpeg process's stdout, but any Readable of well-formed
61
+ * fMP4 bytes works - including in-memory fixtures for tests) and exposes two views over the single-pass box pipeline:
62
+ *
63
+ * - `initSegment: Promise<Buffer>` - resolves once with the concatenated bytes of every box that appeared before the first `moof` (typically ftyp + moov).
64
+ * - `segments(): AsyncGenerator<Buffer>` - yields each subsequent `moof` / `mdat` pair concatenated into a single Buffer.
65
+ *
66
+ * One-shot artifacts (the init segment) are promises; continuous streams (media segments) are async generators. Lifetime is governed by
67
+ * a composed {@link AbortSignal}: external abort, parent signal propagation, source error, source end, or an optional inter-segment watchdog timeout all converge on the
68
+ * same signal. The class is single-consumer by design - `initSegment` and `segments()` are two views on one internal drain loop, not independent subscriptions.
69
+ *
70
+ * @module
71
+ */
72
+ import { BOX_TYPE_MDAT, BOX_TYPE_MOOF, Mp4BoxParser } from "./mp4-parser.js";
73
+ import { HbpuAbortError, Watchdog, composeSignals, isTimeoutReason, markHandled, onAbort, waitWithSignal } from "../util.js";
74
+ import { on } from "node:events";
75
+ /**
76
+ * AsyncDisposable fMP4 segment assembler that converts a Readable byte source into an init segment promise and a media-segment async generator.
77
+ *
78
+ * Construction kicks off a background drain loop that feeds {@link Mp4BoxParser} from the source's `data` events and routes each parsed box through a small state
79
+ * machine: everything before the first `moof` accumulates into the initialization segment; from the first `moof` onward, boxes accumulate into the current media
80
+ * segment until an `mdat` flushes the accumulated pair to the output queue.
81
+ *
82
+ * The single public teardown verb is {@link Mp4SegmentAssembler.abort}, mirroring `AbortController.abort()`. `Symbol.asyncDispose` is implemented in terms of it and
83
+ * awaits the drain loop's completion before returning, so `await using` guarantees the assembler has fully unwound by the time the surrounding scope exits.
84
+ *
85
+ * @example
86
+ *
87
+ * ```ts
88
+ * import { Mp4SegmentAssembler } from "homebridge-plugin-utils";
89
+ *
90
+ * await using assembler = new Mp4SegmentAssembler(ffmpegStdout, { segmentTimeout: 4500, signal: session.signal });
91
+ *
92
+ * const initSegment = await assembler.initSegment;
93
+ *
94
+ * for await (const segment of assembler.segments()) {
95
+ *
96
+ * // Forward segment bytes to HomeKit.
97
+ * }
98
+ * ```
99
+ *
100
+ * @see Mp4BoxParser
101
+ *
102
+ * @category FFmpeg
103
+ */
104
+ export class Mp4SegmentAssembler {
105
+ /**
106
+ * The composed abort signal representing this assembler's lifetime. Aborts exactly once when the source ends, the source errors, the parent signal fires, the
107
+ * watchdog timeout expires, or {@link Mp4SegmentAssembler.abort} is called; the reason encoded on `signal.reason` names the cause.
108
+ */
109
+ signal;
110
+ /**
111
+ * Promise that resolves with the concatenated initialization-segment bytes (typically `ftyp` + `moov`) once the first `moof` box arrives on the source. Rejects with
112
+ * `this.signal.reason` if the assembler is aborted before the initialization segment completes.
113
+ */
114
+ initSegment;
115
+ // The private AbortController whose signal is composed into `this.signal`. Owning the controller internally keeps teardown reachable from any handler - the parser,
116
+ // the source error listener, the watchdog - without giving callers a handle to the raw controller.
117
+ #controller;
118
+ // The byte source. Held so drain-path listeners (`end`, `error`) can be attached and detached in one place.
119
+ #source;
120
+ // The box parser driving the drain loop. Stateful: carries residual bytes across chunks.
121
+ #parser;
122
+ // Inter-segment watchdog composed over this.signal, or `undefined` when no `segmentTimeout` was configured. Armed when the init segment resolves and re-armed on each
123
+ // completed media segment; fires the composed controller with `HbpuAbortError("timeout")` on lapse. Self-cleans when the signal aborts for any other reason.
124
+ #watchdog;
125
+ // Resolver pair for {@link initSegment}. Resolved when the first `moof` arrives (with the accumulated init parts); rejected from the teardown path if still pending.
126
+ #initResolvers;
127
+ // Accumulated box bytes for the initialization segment. Flipped to empty and concatenated once the first `moof` flushes them into `initSegment`.
128
+ #initParts = [];
129
+ // Set `true` once the initialization segment has resolved. The phase flag for the drain state machine: `false` means "collecting init boxes"; `true` means "collecting
130
+ // media boxes."
131
+ #initResolved = false;
132
+ // Accumulated box bytes for the media segment currently being built. Reset to empty each time an `mdat` flushes the pair into the output queue.
133
+ #segmentParts = [];
134
+ // Completed media segments waiting to be yielded from `segments()`. A FIFO queue decouples the drain loop (producer) from the generator (consumer), which lets the
135
+ // consumer fall behind momentarily without dropping data.
136
+ #segmentQueue = [];
137
+ // Parked waiter the generator uses to block until a segment is pushed or the signal aborts. A single-slot optional field is enough because the class is
138
+ // single-consumer by design.
139
+ #segmentWaiter;
140
+ // The drain loop's promise. Held so `[Symbol.asyncDispose]` can await actual completion before returning, so callers using `await using` are guaranteed all drain
141
+ // listeners have been detached by the time the block exits.
142
+ #drainTask;
143
+ /**
144
+ * Construct and start a new fMP4 segment assembler.
145
+ *
146
+ * The drain loop starts synchronously as part of construction: by the time the constructor returns, the source's `data` events are being observed and the parser is
147
+ * ready to emit boxes. There is no separate `start()` step.
148
+ *
149
+ * @param source - Any {@link Readable} producing fMP4 byte chunks. Typically an FFmpeg process's stdout; any Readable works, which keeps the class testable in
150
+ * isolation with in-memory fixture streams.
151
+ * @param init - Optional init options. See {@link Mp4SegmentAssemblerInit}.
152
+ */
153
+ constructor(source, init = {}) {
154
+ const { signal: parentSignal, segmentTimeout } = init;
155
+ this.#controller = new AbortController();
156
+ this.signal = composeSignals(parentSignal, this.#controller.signal);
157
+ this.#source = source;
158
+ this.#parser = new Mp4BoxParser();
159
+ // Instantiate the inter-segment watchdog only when the caller opted into timeout enforcement. When undefined, no watchdog is constructed and every `arm()` site
160
+ // becomes a cheap `?.` no-op. The watchdog self-cleans when the composed signal aborts, so nothing else in this class needs to know about it.
161
+ this.#watchdog = (segmentTimeout !== undefined) ? new Watchdog({
162
+ onFire: () => {
163
+ this.#controller.abort(new HbpuAbortError("timeout"));
164
+ },
165
+ signal: this.signal,
166
+ timeoutMs: segmentTimeout
167
+ }) : undefined;
168
+ // Wire up the init resolvers before starting any listeners. Callers reading `initSegment` should see a promise that can be awaited regardless of whether the source
169
+ // has already emitted anything by the time they get the reference. `markHandled` opts the promise out of Node's unhandled-rejection tracker for the case where a
170
+ // consumer uses only `segments()` and never awaits the init segment directly.
171
+ this.#initResolvers = Promise.withResolvers();
172
+ this.initSegment = markHandled(this.#initResolvers.promise);
173
+ // Attach a permanent source `"error"` absorber. Node's EventEmitter crashes the host process when `"error"` is emitted with no listeners, and the
174
+ // `events.on`-driven error handling inside `#drain` only protects while the drain loop is active. A phantom listener that lives for the assembler's lifetime
175
+ // guarantees no stray source error can crash the host - even during the sub-microtask window between drain termination and our teardown, or after teardown if the
176
+ // caller keeps the source alive past the assembler's useful life. During drain, Node dispatches `"error"` to every listener in registration order so `events.on`
177
+ // still rejects the iterator; the absorber just backstops the cases `events.on` does not cover. Released with the assembler instance.
178
+ this.#source.on("error", () => { });
179
+ // Single teardown convergence point. `onAbort` registers the one-shot teardown listener for the normal abort path AND handles the "pre-aborted signal" edge case
180
+ // where `addEventListener("abort", ...)` would otherwise silently skip the handler (the AbortSignal spec does not re-dispatch historical events). Pairing it with
181
+ // the `signal.aborted` short-circuit below means `initSegment` settles consistently regardless of whether the parent signal was live at construction time.
182
+ onAbort(this.signal, () => this.#teardown());
183
+ if (this.signal.aborted) {
184
+ return;
185
+ }
186
+ // Start the drain pipeline. The returned promise is retained so `[Symbol.asyncDispose]` can await the drain's completion before returning. `#drain` is written to
187
+ // always resolve - its body catches and classifies errors inline - so nothing can reject out of it, and no `markHandled` wrapper is needed here.
188
+ this.#drainTask = this.#drain();
189
+ }
190
+ /**
191
+ * Abort the assembler and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged.
192
+ *
193
+ * Safe to call more than once: subsequent calls are no-ops because the underlying signal only aborts once. Calling `abort()` after natural completion is also safe
194
+ * for the same reason.
195
+ *
196
+ * @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors (`TimeoutError`, `AbortError`) also interoperate by convention.
197
+ */
198
+ abort(reason) {
199
+ if (this.aborted) {
200
+ return;
201
+ }
202
+ this.#controller.abort(reason ?? new HbpuAbortError("shutdown"));
203
+ }
204
+ /**
205
+ * `AsyncDisposable` implementation. Aborts the assembler (defaulting to `"shutdown"`) and awaits actual drain-loop completion before returning, so callers using
206
+ * `await using` are guaranteed every internal listener has been detached by the time the block exits.
207
+ *
208
+ * @returns A promise that resolves once the drain loop has fully exited.
209
+ */
210
+ async [Symbol.asyncDispose]() {
211
+ this.abort();
212
+ if (this.#drainTask) {
213
+ // Drain failures are already observed internally; swallow here so `await using` does not surface cleanup-side errors the caller cannot react to.
214
+ await this.#drainTask.catch(() => { });
215
+ }
216
+ }
217
+ /**
218
+ * `true` once `this.signal` has aborted. Derived from the signal; no independent state.
219
+ */
220
+ get aborted() {
221
+ return this.signal.aborted;
222
+ }
223
+ /**
224
+ * `true` when the abort reason indicates a timeout. Matches both the canonical `HbpuAbortError("timeout")` emitted by the inter-segment watchdog and the platform
225
+ * `TimeoutError` emitted by `AbortSignal.timeout()`. The discrimination lives in {@link isTimeoutReason} so this getter stays a one-line delegation and every
226
+ * resource class in the library shares one definition of "timeout."
227
+ */
228
+ get isTimedOut() {
229
+ return isTimeoutReason(this.signal.reason);
230
+ }
231
+ /**
232
+ * The number of completed media segments buffered between the drain loop and the {@link Mp4SegmentAssembler.segments} consumer - the segments the producer has
233
+ * assembled but the consumer has not yet pulled. A consumer pacing its reads slower than the source produces accrues a reserve here, and that reserve is what
234
+ * absorbs an upstream stall: the consumer keeps pulling buffered segments while no new ones arrive. It is zero in steady state when the consumer keeps pace.
235
+ */
236
+ get bufferedSegments() {
237
+ return this.#segmentQueue.length;
238
+ }
239
+ /**
240
+ * Async generator yielding each completed media segment (concatenated `moof` + `mdat` pair) as a single Buffer.
241
+ *
242
+ * Yields only after {@link Mp4SegmentAssembler.initSegment} has resolved - the init segment is not surfaced through this stream. Terminates cleanly when the source
243
+ * ends, the assembler aborts, or the optional caller signal aborts; in every case the queue is drained before the generator returns, so a consumer never loses a
244
+ * segment that was already assembled before teardown.
245
+ *
246
+ * **Single-consumer only.** The internal parked-waiter slot is single-writer; calling `segments()` concurrently with another consumer on the same assembler is
247
+ * unsupported and will hang one of the consumers when the producer's wake-up resolves only the later parker. If fan-out is needed, tee at the consumer side by
248
+ * replicating each yielded Buffer into per-consumer queues external to the assembler.
249
+ *
250
+ * @param init - Optional init options. `signal` composes with the assembler's own signal - aborting it terminates only this generator call, not the assembler.
251
+ *
252
+ * @returns An async generator yielding concatenated `moof` + `mdat` pair buffers in stream order.
253
+ */
254
+ async *segments(init = {}) {
255
+ // Compose the per-call signal with the assembler's own signal upfront so every wait in this method honors caller cancellation uniformly. A caller who passes
256
+ // `init.signal` can terminate just this iterator (e.g., to drop a per-session consumer while the assembler keeps running for another session), while the
257
+ // assembler's signal still governs the underlying pipeline.
258
+ const composed = composeSignals(this.signal, init.signal);
259
+ // Gate media-segment delivery on the init-first contract. `waitWithSignal` races the init promise against the composed signal so a caller abort during the init
260
+ // wait terminates the iterator immediately rather than hanging until the assembler's own signal settles init.
261
+ try {
262
+ await waitWithSignal(this.initSegment, composed);
263
+ }
264
+ catch {
265
+ return;
266
+ }
267
+ for (;;) {
268
+ const env_1 = { stack: [], error: void 0, hasError: false };
269
+ try {
270
+ // Swap-drain the queue: hand ourselves whatever the producer has staged, leave a fresh empty array for the producer to push into, then yield the snapshot. The
271
+ // outer for-loop re-enters on every yielded batch so segments pushed while the consumer awaited a previous yield are picked up on the next pass. Drain
272
+ // unconditionally before checking abort - "no bytes lost" is the invariant for segments already assembled before teardown.
273
+ while (this.#segmentQueue.length > 0) {
274
+ const drained = this.#segmentQueue;
275
+ this.#segmentQueue = [];
276
+ for (const segment of drained) {
277
+ yield segment;
278
+ }
279
+ }
280
+ if (composed.aborted) {
281
+ return;
282
+ }
283
+ // Park until either a new segment is pushed (producer resolves the waiter) or the composed signal aborts. A per-iteration resolver is used so the waiter is
284
+ // always fresh; sharing a resolver across iterations would require manual reset logic.
285
+ const waiter = Promise.withResolvers();
286
+ this.#segmentWaiter = waiter;
287
+ // `onAbort` handles listener registration, the pre-aborted-signal pitfall, and the one-shot `{ once: true }` semantic through one primitive; the returned
288
+ // `Disposable` is handed to `using` so the listener is deterministically removed on every scope-exit path (segment push via `#handleBox`, caller signal abort,
289
+ // assembler abort, thrown error in the generator). The single unified primitive matches the shape `waitWithSignal` uses for exactly the same kind of parked wait.
290
+ const _abortRegistration = __addDisposableResource(env_1, onAbort(composed, () => waiter.resolve()), false);
291
+ try {
292
+ // eslint-disable-next-line no-await-in-loop
293
+ await waiter.promise;
294
+ }
295
+ finally {
296
+ this.#segmentWaiter = undefined;
297
+ }
298
+ }
299
+ catch (e_1) {
300
+ env_1.error = e_1;
301
+ env_1.hasError = true;
302
+ }
303
+ finally {
304
+ __disposeResources(env_1);
305
+ }
306
+ }
307
+ }
308
+ // Drain loop. Attaches the source's `end` listener, then iterates `events.on(source, "data", { signal })` until the composed signal aborts. For each chunk, the
309
+ // parser is fed and every complete box is dispatched through `#handleBox`. Every exit path - source end, source error, external abort - converges on the signal
310
+ // being aborted, which lets the generator's park loop unwind uniformly via `composed.aborted`.
311
+ async #drain() {
312
+ // Source end: the byte producer has nothing more to say. Drive teardown through the signal with reason `"closed"`; the signal's teardown listener will resolve the
313
+ // generator's parked waiter and reject any pending init. Guard against double-abort when the signal already fired (e.g., external teardown that destroyed the
314
+ // source and produced both `end` and our own abort).
315
+ const onEnd = () => {
316
+ if (this.aborted) {
317
+ return;
318
+ }
319
+ this.#controller.abort(new HbpuAbortError("closed"));
320
+ };
321
+ this.#source.once("end", onEnd);
322
+ try {
323
+ // `events.on` yields each emission as an array of event arguments. For Readable's `data` event the array is `[chunk]`; we destructure inside the loop so the
324
+ // typing stays explicit. The `{ signal }` option wires abort-driven termination into the iterator directly - when the signal fires, the iterator rejects its next
325
+ // call with the signal's reason. `events.on` also listens to the source's `"error"` events internally and rejects the iterator with the emitted error; the catch
326
+ // block below classifies both exit paths.
327
+ for await (const eventArgs of on(this.#source, "data", { signal: this.signal })) {
328
+ const [chunk] = eventArgs;
329
+ for (const box of this.#parser.consume(chunk)) {
330
+ this.#handleBox(box);
331
+ }
332
+ }
333
+ }
334
+ catch (error) {
335
+ // Single classification point. Every rejection this loop can produce lands here, classified into one of two reasons today: our composed signal aborted
336
+ // (already carries a structured `signal.reason`, `this.aborted` is true, pass through unchanged) or the source emitted an `"error"` event (the emitted error
337
+ // is our `cause`, wrap into the `"failed"` reason so every consumer sees the same taxonomy). Parser failures would also land here if the parser ever threw,
338
+ // which today it does not - `#handleBox` is synchronous and side-effect-only.
339
+ if (!this.aborted) {
340
+ this.#controller.abort(new HbpuAbortError("failed", { cause: error }));
341
+ }
342
+ }
343
+ finally {
344
+ this.#source.off("end", onEnd);
345
+ }
346
+ }
347
+ // Single-box state machine. Before init resolves, boxes accumulate into `#initParts`; the first `moof` flushes them into the init promise, transitions to media
348
+ // collection, and starts the first media segment with itself as the opening box. From there, each box is appended to the current segment; an `mdat` closes the pair
349
+ // and pushes it to the output queue.
350
+ #handleBox(box) {
351
+ if (!this.#initResolved) {
352
+ if (box.type === BOX_TYPE_MOOF) {
353
+ // First `moof` - the preceding boxes (typically ftyp + moov) are the complete init segment. Concatenate them once, resolve the promise, and transition into
354
+ // media-collection mode with this `moof` starting the first segment.
355
+ const init = Buffer.concat(this.#initParts);
356
+ this.#initParts = [];
357
+ this.#initResolved = true;
358
+ this.#initResolvers.resolve(init);
359
+ this.#segmentParts.push(box.bytes);
360
+ this.#watchdog?.arm();
361
+ return;
362
+ }
363
+ this.#initParts.push(box.bytes);
364
+ return;
365
+ }
366
+ // Media-collection phase: every box contributes to the current pair, and `mdat` is the flush signal. Non-moof/non-mdat boxes (uncommon in fMP4 fragments, but
367
+ // possible) are appended to the current pair so the downstream consumer sees the stream verbatim.
368
+ this.#segmentParts.push(box.bytes);
369
+ if (box.type === BOX_TYPE_MDAT) {
370
+ const segment = Buffer.concat(this.#segmentParts);
371
+ this.#segmentParts = [];
372
+ this.#segmentQueue.push(segment);
373
+ this.#segmentWaiter?.resolve();
374
+ this.#watchdog?.arm();
375
+ }
376
+ }
377
+ // Single teardown convergence point, fired exactly once when `this.signal` aborts. Rejects a pending init promise and unblocks the generator so the drain-and-return
378
+ // sequence can run. The watchdog self-cleans through its own signal listener, and `exited`-style promises are not in this class's contract - the generator is the
379
+ // exit surface and it wakes up via the resolved waiter.
380
+ #teardown() {
381
+ // Promise resolvers are idempotent after first settlement, so calling reject on an already-resolved init promise is a safe no-op. This lets us keep the teardown
382
+ // path uniform regardless of whether init arrived before the abort or not.
383
+ this.#initResolvers.reject(this.signal.reason);
384
+ this.#segmentWaiter?.resolve();
385
+ }
386
+ }
387
+ //# sourceMappingURL=mp4-assembler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mp4-assembler.js","sourceRoot":"","sources":["../../src/ffmpeg/mp4-assembler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ISO BMFF box type code for the `ftyp` (file type) box, encoded as a 32-bit big-endian integer. Useful for discriminating on {@link Mp4Box.type} without re-encoding
3
+ * the 4-character ASCII tag on every comparison.
4
+ *
5
+ * @category FFmpeg
6
+ */
7
+ export declare const BOX_TYPE_FTYP = 1718909296;
8
+ /**
9
+ * ISO BMFF box type code for the `mdat` (media data) box. The mdat box carries the sample payload for the preceding `moof` in an fMP4 fragment and is the end-of-segment
10
+ * marker that {@link ffmpeg/mp4-assembler!Mp4SegmentAssembler | Mp4SegmentAssembler} watches for.
11
+ *
12
+ * @category FFmpeg
13
+ */
14
+ export declare const BOX_TYPE_MDAT = 1835295092;
15
+ /**
16
+ * ISO BMFF box type code for the `moof` (movie fragment) box. Marks the start of a new fMP4 media fragment; the first `moof` in a stream also marks the end of the
17
+ * initialization segment.
18
+ *
19
+ * @category FFmpeg
20
+ */
21
+ export declare const BOX_TYPE_MOOF = 1836019558;
22
+ /**
23
+ * ISO BMFF box type code for the `moov` (movie) box. Part of the initialization segment that precedes the first `moof` in an fMP4 stream.
24
+ *
25
+ * @category FFmpeg
26
+ */
27
+ export declare const BOX_TYPE_MOOV = 1836019574;
28
+ /**
29
+ * A complete parsed ISO BMFF box emitted by {@link Mp4BoxParser}.
30
+ *
31
+ * @property bytes - The complete box bytes, including the 8-byte header. The buffer is typically a zero-copy subarray view over the feed chunk; consumers that need
32
+ * to hold it past the iteration loop should copy with `Buffer.from()` if the upstream chunk lifetime is suspect.
33
+ * @property type - The box type encoded as a 32-bit big-endian integer. Compare against the `BOX_TYPE_*` constants exported from this module; numeric comparison
34
+ * avoids the per-box string allocation that an ASCII tag comparison would incur.
35
+ *
36
+ * @category FFmpeg
37
+ */
38
+ export interface Mp4Box {
39
+ bytes: Buffer;
40
+ type: number;
41
+ }
42
+ /**
43
+ * Pure stateful parser that converts a stream of ISO BMFF byte chunks into discrete {@link Mp4Box} records.
44
+ *
45
+ * Feed each chunk from the byte source through {@link Mp4BoxParser.consume} and iterate the returned values for every complete box now available. The parser carries
46
+ * only the bytes of an incomplete trailing box across calls; chunk boundaries in the middle of a box are handled transparently. A single chunk may produce zero, one, or
47
+ * many boxes depending on how the stream falls on chunk boundaries.
48
+ *
49
+ * The class is intentionally signal-free and event-free. It performs one job, synchronously, and leaves resource lifecycle and async consumption to the composing
50
+ * caller - typically {@link ffmpeg/mp4-assembler!Mp4SegmentAssembler | Mp4SegmentAssembler} for fMP4 streams driven by an FFmpeg process.
51
+ *
52
+ * @remarks **Trust boundary.** The parser does not bound the size field it reads from the wire. A source that claims a multi-gigabyte box size would cause the parser
53
+ * to accumulate incoming chunks into its internal pending buffer until the declared box completes. This is safe for trusted sources like FFmpeg stdout, where the
54
+ * size field reflects the actual payload. Callers feeding untrusted byte streams (e.g., user-uploaded media, network input from arbitrary peers) should impose their
55
+ * own bound - either by truncating chunks that would exceed a budget, or by wrapping the parser in a guard that rejects implausibly large declared sizes before
56
+ * feeding the chunk.
57
+ *
58
+ * @example
59
+ *
60
+ * ```ts
61
+ * import { BOX_TYPE_MDAT, Mp4BoxParser } from "homebridge-plugin-utils";
62
+ *
63
+ * const parser = new Mp4BoxParser();
64
+ *
65
+ * for(const box of parser.consume(chunk)) {
66
+ *
67
+ * if(box.type === BOX_TYPE_MDAT) {
68
+ *
69
+ * // We have just received a complete mdat box.
70
+ * }
71
+ * }
72
+ * ```
73
+ *
74
+ * @see Mp4SegmentAssembler
75
+ *
76
+ * @category FFmpeg
77
+ */
78
+ export declare class Mp4BoxParser {
79
+ #private;
80
+ /**
81
+ * Feed the parser a new byte chunk and yield every complete box now available.
82
+ *
83
+ * The iterable is single-pass: consume it with a `for...of` loop (or spread into an array) before the next call to `consume`. Emitted `bytes` are typically
84
+ * zero-copy subarray views over the input chunk; copy with `Buffer.from()` if you intend to hold a box past the upstream chunk's lifetime.
85
+ *
86
+ * Chunk boundaries in the middle of a box are handled internally: the incomplete trailing bytes are stashed and prepended to the next call's input.
87
+ *
88
+ * @param chunk - A contiguous slice of box-stream bytes from the source.
89
+ *
90
+ * @returns An iterable of every box contained in (or completed by) this chunk, in stream order.
91
+ */
92
+ consume(chunk: Buffer): Iterable<Mp4Box>;
93
+ }
94
+ //# sourceMappingURL=mp4-parser.d.ts.map
@@ -0,0 +1,130 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * ffmpeg/mp4-parser.ts: Pure byte-to-record parser for ISO BMFF (fMP4) box streams.
4
+ */
5
+ /**
6
+ * Pure stateful byte-to-record parser for ISO BMFF (fMP4) box streams.
7
+ *
8
+ * This module ships the streaming counterpart to the static predicates in {@link ffmpeg/fmp4! | fmp4}. The predicates (`findBox`, `isKeyframe`, `hasAudioTrack`,
9
+ * `splitMoofMdat`) answer one-shot questions about a complete Buffer. {@link Mp4BoxParser} solves a different problem: incrementally consuming an unbounded byte stream
10
+ * (typically FFmpeg stdout producing fMP4 fragments) and emitting each complete box as it becomes available.
11
+ *
12
+ * The parser is intentionally signal-free, event-free, and allocation-light. It carries only the bytes of an incomplete trailing box across calls and hands out
13
+ * zero-copy `subarray` views whenever possible. Resource lifetime, async consumption, and cancellation are not this class's job - those belong to the composing
14
+ * caller (see {@link ffmpeg/mp4-assembler!Mp4SegmentAssembler | Mp4SegmentAssembler}).
15
+ *
16
+ * @module
17
+ */
18
+ import { BOX_HEADER_SIZE } from "./fmp4.js";
19
+ /**
20
+ * ISO BMFF box type code for the `ftyp` (file type) box, encoded as a 32-bit big-endian integer. Useful for discriminating on {@link Mp4Box.type} without re-encoding
21
+ * the 4-character ASCII tag on every comparison.
22
+ *
23
+ * @category FFmpeg
24
+ */
25
+ export const BOX_TYPE_FTYP = 0x66747970;
26
+ /**
27
+ * ISO BMFF box type code for the `mdat` (media data) box. The mdat box carries the sample payload for the preceding `moof` in an fMP4 fragment and is the end-of-segment
28
+ * marker that {@link ffmpeg/mp4-assembler!Mp4SegmentAssembler | Mp4SegmentAssembler} watches for.
29
+ *
30
+ * @category FFmpeg
31
+ */
32
+ export const BOX_TYPE_MDAT = 0x6D646174;
33
+ /**
34
+ * ISO BMFF box type code for the `moof` (movie fragment) box. Marks the start of a new fMP4 media fragment; the first `moof` in a stream also marks the end of the
35
+ * initialization segment.
36
+ *
37
+ * @category FFmpeg
38
+ */
39
+ export const BOX_TYPE_MOOF = 0x6D6F6F66;
40
+ /**
41
+ * ISO BMFF box type code for the `moov` (movie) box. Part of the initialization segment that precedes the first `moof` in an fMP4 stream.
42
+ *
43
+ * @category FFmpeg
44
+ */
45
+ export const BOX_TYPE_MOOV = 0x6D6F6F76;
46
+ // Reusable empty buffer sentinel for the parser's pending-bytes state. Avoids repeated zero-byte allocations at the start of every parse cycle where no residual bytes
47
+ // carried over from the previous chunk.
48
+ const EMPTY_BUFFER = Buffer.alloc(0);
49
+ /**
50
+ * Pure stateful parser that converts a stream of ISO BMFF byte chunks into discrete {@link Mp4Box} records.
51
+ *
52
+ * Feed each chunk from the byte source through {@link Mp4BoxParser.consume} and iterate the returned values for every complete box now available. The parser carries
53
+ * only the bytes of an incomplete trailing box across calls; chunk boundaries in the middle of a box are handled transparently. A single chunk may produce zero, one, or
54
+ * many boxes depending on how the stream falls on chunk boundaries.
55
+ *
56
+ * The class is intentionally signal-free and event-free. It performs one job, synchronously, and leaves resource lifecycle and async consumption to the composing
57
+ * caller - typically {@link ffmpeg/mp4-assembler!Mp4SegmentAssembler | Mp4SegmentAssembler} for fMP4 streams driven by an FFmpeg process.
58
+ *
59
+ * @remarks **Trust boundary.** The parser does not bound the size field it reads from the wire. A source that claims a multi-gigabyte box size would cause the parser
60
+ * to accumulate incoming chunks into its internal pending buffer until the declared box completes. This is safe for trusted sources like FFmpeg stdout, where the
61
+ * size field reflects the actual payload. Callers feeding untrusted byte streams (e.g., user-uploaded media, network input from arbitrary peers) should impose their
62
+ * own bound - either by truncating chunks that would exceed a budget, or by wrapping the parser in a guard that rejects implausibly large declared sizes before
63
+ * feeding the chunk.
64
+ *
65
+ * @example
66
+ *
67
+ * ```ts
68
+ * import { BOX_TYPE_MDAT, Mp4BoxParser } from "homebridge-plugin-utils";
69
+ *
70
+ * const parser = new Mp4BoxParser();
71
+ *
72
+ * for(const box of parser.consume(chunk)) {
73
+ *
74
+ * if(box.type === BOX_TYPE_MDAT) {
75
+ *
76
+ * // We have just received a complete mdat box.
77
+ * }
78
+ * }
79
+ * ```
80
+ *
81
+ * @see Mp4SegmentAssembler
82
+ *
83
+ * @category FFmpeg
84
+ */
85
+ export class Mp4BoxParser {
86
+ // Residual bytes from the previous call that did not complete a box. Prepended to the next chunk so a box split across two chunks is reassembled transparently. A
87
+ // zero-length sentinel is used when nothing is pending to avoid per-call allocation.
88
+ #pending = EMPTY_BUFFER;
89
+ /**
90
+ * Feed the parser a new byte chunk and yield every complete box now available.
91
+ *
92
+ * The iterable is single-pass: consume it with a `for...of` loop (or spread into an array) before the next call to `consume`. Emitted `bytes` are typically
93
+ * zero-copy subarray views over the input chunk; copy with `Buffer.from()` if you intend to hold a box past the upstream chunk's lifetime.
94
+ *
95
+ * Chunk boundaries in the middle of a box are handled internally: the incomplete trailing bytes are stashed and prepended to the next call's input.
96
+ *
97
+ * @param chunk - A contiguous slice of box-stream bytes from the source.
98
+ *
99
+ * @returns An iterable of every box contained in (or completed by) this chunk, in stream order.
100
+ */
101
+ *consume(chunk) {
102
+ // Prepend any pending partial box from the previous call. Buffer.concat is only paid when there is actually residual state; the common "chunk carries whole boxes
103
+ // and nothing is pending" path stays zero-allocation.
104
+ const buffer = (this.#pending.length > 0) ? Buffer.concat([this.#pending, chunk]) : chunk;
105
+ let offset = 0;
106
+ // Walk boxes by reading each header: 4 bytes size (big-endian) + 4 bytes type. The 32-bit type code is surfaced directly so consumers can compare against the
107
+ // exported BOX_TYPE_* constants without re-encoding a 4-character ASCII tag on every box.
108
+ while ((offset + BOX_HEADER_SIZE) <= buffer.length) {
109
+ const size = buffer.readUInt32BE(offset);
110
+ // Size must be at least the header size. Smaller values indicate corruption, a misaligned feed, or an unsupported extended-size box (size === 1 signals a 64-bit
111
+ // size field that we do not handle). Either way, the parser cannot make further progress - reset pending state so we do not busy-loop on the same bad byte on
112
+ // every subsequent chunk.
113
+ if (size < BOX_HEADER_SIZE) {
114
+ this.#pending = EMPTY_BUFFER;
115
+ return;
116
+ }
117
+ // Not enough bytes for a complete box yet. Stash the remainder - including the header - for the next call.
118
+ if ((offset + size) > buffer.length) {
119
+ break;
120
+ }
121
+ const type = buffer.readUInt32BE(offset + 4);
122
+ const bytes = buffer.subarray(offset, offset + size);
123
+ offset += size;
124
+ yield { bytes, type };
125
+ }
126
+ // Carry the trailing incomplete box (header + partial payload, or just the unconsumed tail) into the next call.
127
+ this.#pending = (offset === buffer.length) ? EMPTY_BUFFER : buffer.subarray(offset);
128
+ }
129
+ }
130
+ //# sourceMappingURL=mp4-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mp4-parser.js","sourceRoot":"","sources":["../../src/ffmpeg/mp4-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AAExC;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AAExC,uKAAuK;AACvK,wCAAwC;AACxC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAkBrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,OAAO,YAAY;IAEvB,kKAAkK;IAClK,qFAAqF;IACrF,QAAQ,GAAW,YAAY,CAAC;IAEhC;;;;;;;;;;;OAWG;IACI,CAAC,OAAO,CAAC,KAAa;QAE3B,kKAAkK;QAClK,sDAAsD;QACtD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5F,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,8JAA8J;QAC9J,0FAA0F;QAC1F,OAAM,CAAC,MAAM,GAAG,eAAe,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAElD,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAEzC,iKAAiK;YACjK,8JAA8J;YAC9J,0BAA0B;YAC1B,IAAG,IAAI,GAAG,eAAe,EAAE,CAAC;gBAE1B,IAAI,CAAC,QAAQ,GAAG,YAAY,CAAC;gBAE7B,OAAO;YACT,CAAC;YAED,2GAA2G;YAC3G,IAAG,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;gBAEnC,MAAM;YACR,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;YAErD,MAAM,IAAI,IAAI,CAAC;YAEf,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACxB,CAAC;QAED,gHAAgH;QAChH,IAAI,CAAC,QAAQ,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtF,CAAC;CACF"}