homebridge-plugin-utils 1.35.0 → 2.1.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.
- package/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +94 -45
- package/dist/backpressure.js +229 -93
- package/dist/backpressure.js.map +1 -1
- package/dist/cli/index.d.ts +166 -0
- package/dist/cli/index.js +551 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/disposable-stack.d.ts +59 -0
- package/dist/disposable-stack.js +155 -0
- package/dist/disposable-stack.js.map +1 -0
- package/dist/docChrome.d.ts +260 -0
- package/dist/docChrome.js +361 -0
- package/dist/docChrome.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -262
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +49 -2
- package/dist/ffmpeg/fmp4.js +47 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +161 -0
- package/dist/ffmpeg/mp4-assembler.js +424 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +142 -96
- package/dist/ffmpeg/process.js +406 -278
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +325 -186
- package/dist/ffmpeg/record.js +418 -565
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +157 -0
- package/dist/ffmpeg/recording-process-double.js +190 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +474 -251
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -2
- package/dist/ffmpeg/settings.js +20 -5
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -150
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +213 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +341 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +702 -0
- package/dist/ui/webUi.mjs +225 -88
- package/dist/util.d.ts +679 -45
- package/dist/util.js +830 -77
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* Construction-time options for {@link Mp4SegmentAssembler}.
|
|
4
|
+
*
|
|
5
|
+
* @property segmentTimeout - Optional watchdog window, in milliseconds. The timer arms when the initialization segment resolves (we begin expecting media segments)
|
|
6
|
+
* and re-arms on each completed media segment. If no segment arrives within the window, the assembler aborts with
|
|
7
|
+
* `HbpuAbortError("timeout")` and the generator terminates cleanly. Typical value for HKSV is a little under five seconds.
|
|
8
|
+
* @property signal - Optional parent {@link AbortSignal} to compose with the assembler's internal controller. When the parent aborts, the assembler tears
|
|
9
|
+
* down and the segment generator exits.
|
|
10
|
+
*
|
|
11
|
+
* @category FFmpeg
|
|
12
|
+
*/
|
|
13
|
+
export interface Mp4SegmentAssemblerInit {
|
|
14
|
+
segmentTimeout?: number;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The kind of a segment yielded by {@link Mp4SegmentAssembler.stream}. `"init"` is the one-shot initialization segment; `"media"` is a continuous media fragment. A
|
|
19
|
+
* consumer that paces or forwards every item uniformly can read {@link Mp4Segment.bytes} without branching; a consumer that must treat the init segment specially
|
|
20
|
+
* branches on this field.
|
|
21
|
+
*
|
|
22
|
+
* @category FFmpeg
|
|
23
|
+
*/
|
|
24
|
+
export type Mp4SegmentKind = "init" | "media";
|
|
25
|
+
/**
|
|
26
|
+
* A single fMP4 segment yielded by {@link Mp4SegmentAssembler.stream}, tagged with its kind so a consumer can tell the one-shot initialization segment apart from the
|
|
27
|
+
* media fragments that follow it without relying on positional ordering.
|
|
28
|
+
*
|
|
29
|
+
* @property bytes - The complete segment bytes: for `"init"`, the concatenated initialization boxes (typically `ftyp` + `moov`); for `"media"`, a concatenated
|
|
30
|
+
* `moof` + `mdat` pair.
|
|
31
|
+
* @property kind - `"init"` for the single leading initialization segment, `"media"` for each subsequent media fragment.
|
|
32
|
+
*
|
|
33
|
+
* @category FFmpeg
|
|
34
|
+
*/
|
|
35
|
+
export interface Mp4Segment {
|
|
36
|
+
bytes: Buffer;
|
|
37
|
+
kind: Mp4SegmentKind;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* AsyncDisposable fMP4 segment assembler that converts a Readable byte source into an init segment promise and a media-segment async generator.
|
|
41
|
+
*
|
|
42
|
+
* 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
|
|
43
|
+
* machine: everything before the first `moof` accumulates into the initialization segment; from the first `moof` onward, boxes accumulate into the current media
|
|
44
|
+
* segment until an `mdat` flushes the accumulated pair to the output queue.
|
|
45
|
+
*
|
|
46
|
+
* The single public teardown verb is {@link Mp4SegmentAssembler.abort}, mirroring `AbortController.abort()`. `Symbol.asyncDispose` is implemented in terms of it and
|
|
47
|
+
* awaits the drain loop's completion before returning, so `await using` guarantees the assembler has fully unwound by the time the surrounding scope exits.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { Mp4SegmentAssembler } from "homebridge-plugin-utils";
|
|
53
|
+
*
|
|
54
|
+
* await using assembler = new Mp4SegmentAssembler(ffmpegStdout, { segmentTimeout: 4500, signal: session.signal });
|
|
55
|
+
*
|
|
56
|
+
* const initSegment = await assembler.initSegment;
|
|
57
|
+
*
|
|
58
|
+
* for await (const segment of assembler.segments()) {
|
|
59
|
+
*
|
|
60
|
+
* // Forward segment bytes to HomeKit.
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @see Mp4BoxParser
|
|
65
|
+
*
|
|
66
|
+
* @category FFmpeg
|
|
67
|
+
*/
|
|
68
|
+
export declare class Mp4SegmentAssembler implements AsyncDisposable {
|
|
69
|
+
#private;
|
|
70
|
+
/**
|
|
71
|
+
* The composed abort signal representing this assembler's lifetime. Aborts exactly once when the source ends, the source errors, the parent signal fires, the
|
|
72
|
+
* watchdog timeout expires, or {@link Mp4SegmentAssembler.abort} is called; the reason encoded on `signal.reason` names the cause.
|
|
73
|
+
*/
|
|
74
|
+
readonly signal: AbortSignal;
|
|
75
|
+
/**
|
|
76
|
+
* Promise that resolves with the concatenated initialization-segment bytes (typically `ftyp` + `moov`) once the first `moof` box arrives on the source. Rejects with
|
|
77
|
+
* `this.signal.reason` if the assembler is aborted before the initialization segment completes.
|
|
78
|
+
*/
|
|
79
|
+
readonly initSegment: Promise<Buffer>;
|
|
80
|
+
/**
|
|
81
|
+
* Construct and start a new fMP4 segment assembler.
|
|
82
|
+
*
|
|
83
|
+
* 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
|
|
84
|
+
* ready to emit boxes. There is no separate `start()` step.
|
|
85
|
+
*
|
|
86
|
+
* @param source - Any {@link Readable} producing fMP4 byte chunks. Typically an FFmpeg process's stdout; any Readable works, which keeps the class testable in
|
|
87
|
+
* isolation with in-memory fixture streams.
|
|
88
|
+
* @param init - Optional init options. See {@link Mp4SegmentAssemblerInit}.
|
|
89
|
+
*/
|
|
90
|
+
constructor(source: Readable, init?: Mp4SegmentAssemblerInit);
|
|
91
|
+
/**
|
|
92
|
+
* Abort the assembler and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged.
|
|
93
|
+
*
|
|
94
|
+
* 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
|
|
95
|
+
* for the same reason.
|
|
96
|
+
*
|
|
97
|
+
* @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors (`TimeoutError`, `AbortError`) also interoperate by convention.
|
|
98
|
+
*/
|
|
99
|
+
abort(reason?: unknown): void;
|
|
100
|
+
/**
|
|
101
|
+
* `AsyncDisposable` implementation. Aborts the assembler (defaulting to `"shutdown"`) and awaits actual drain-loop completion before returning, so callers using
|
|
102
|
+
* `await using` are guaranteed every internal listener has been detached by the time the block exits.
|
|
103
|
+
*
|
|
104
|
+
* @returns A promise that resolves once the drain loop has fully exited.
|
|
105
|
+
*/
|
|
106
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* `true` once `this.signal` has aborted. Derived from the signal; no independent state.
|
|
109
|
+
*/
|
|
110
|
+
get aborted(): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* `true` when the abort reason indicates a timeout. Matches both the canonical `HbpuAbortError("timeout")` emitted by the inter-segment watchdog and the platform
|
|
113
|
+
* `TimeoutError` emitted by `AbortSignal.timeout()`. The branching lives in {@link isTimeoutReason} so this getter stays a one-line delegation and every
|
|
114
|
+
* resource class in the library shares one definition of "timeout."
|
|
115
|
+
*/
|
|
116
|
+
get isTimedOut(): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* The number of completed media segments buffered between the drain loop and the {@link Mp4SegmentAssembler.segments} consumer - the segments the producer has
|
|
119
|
+
* 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
|
|
120
|
+
* 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.
|
|
121
|
+
*/
|
|
122
|
+
get bufferedSegments(): number;
|
|
123
|
+
/**
|
|
124
|
+
* Async generator yielding each completed media segment (concatenated `moof` + `mdat` pair) as a single Buffer.
|
|
125
|
+
*
|
|
126
|
+
* Yields only after {@link Mp4SegmentAssembler.initSegment} has resolved - the init segment is not surfaced through this stream. Terminates cleanly when the source
|
|
127
|
+
* 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
|
|
128
|
+
* segment that was already assembled before teardown.
|
|
129
|
+
*
|
|
130
|
+
* **Single-consumer only.** The internal parked-waiter slot is single-writer; calling `segments()` concurrently with another consumer on the same assembler - including
|
|
131
|
+
* the {@link Mp4SegmentAssembler.stream} view, which drives this generator internally - is unsupported and will hang one of the consumers when the producer's wake-up
|
|
132
|
+
* resolves only the later parker. If fan-out is needed, tee at the consumer side by replicating each yielded Buffer into per-consumer queues external to the assembler.
|
|
133
|
+
*
|
|
134
|
+
* @param init - Optional init options. `signal` composes with the assembler's own signal - aborting it terminates only this generator call, not the assembler.
|
|
135
|
+
*
|
|
136
|
+
* @returns An async generator yielding concatenated `moof` + `mdat` pair buffers in stream order.
|
|
137
|
+
*/
|
|
138
|
+
segments(init?: {
|
|
139
|
+
signal?: AbortSignal;
|
|
140
|
+
}): AsyncGenerator<Buffer>;
|
|
141
|
+
/**
|
|
142
|
+
* Async generator yielding the whole segment stream as a kind-tagged sequence: exactly one {@link Mp4Segment} of kind `"init"` carrying the initialization bytes,
|
|
143
|
+
* followed by one of kind `"media"` per completed media fragment. This is a third view over the same single-pass pipeline, composed from {@link initSegment} and
|
|
144
|
+
* {@link segments} - it lets a consumer forward the init segment and the media segments through a single loop without tracking which item is which by position.
|
|
145
|
+
*
|
|
146
|
+
* Terminates cleanly on the same conditions as {@link segments}: the source ends, the assembler aborts, or the optional caller signal aborts; queued media drains
|
|
147
|
+
* before the generator returns, so no assembled segment is lost. If the assembler is aborted before the initialization segment arrives, the generator returns without
|
|
148
|
+
* yielding anything.
|
|
149
|
+
*
|
|
150
|
+
* **Single-consumer only.** `stream()` drives {@link segments} internally, so it shares the one parked-waiter slot. Use `stream()` OR the {@link initSegment} /
|
|
151
|
+
* {@link segments} pair on a single assembler, never both concurrently - mixing them competes for the same drain and hangs one consumer.
|
|
152
|
+
*
|
|
153
|
+
* @param init - Optional init options. `signal` composes with the assembler's own signal - aborting it terminates only this generator call, not the assembler.
|
|
154
|
+
*
|
|
155
|
+
* @returns An async generator yielding one `"init"` segment followed by `"media"` segments in stream order.
|
|
156
|
+
*/
|
|
157
|
+
stream(init?: {
|
|
158
|
+
signal?: AbortSignal;
|
|
159
|
+
}): AsyncGenerator<Mp4Segment>;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=mp4-assembler.d.ts.map
|
|
@@ -0,0 +1,424 @@
|
|
|
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 complementary 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
|
+
* - `stream(): AsyncGenerator<Mp4Segment>` - yields the whole sequence tagged by kind: one `"init"` item carrying the initialization bytes, then a `"media"`
|
|
66
|
+
* item per fragment, so a caller can forward init and media through one loop.
|
|
67
|
+
*
|
|
68
|
+
* One-shot artifacts (the init segment) are promises; continuous streams (media segments) are async generators. Lifetime is governed by
|
|
69
|
+
* a composed {@link AbortSignal}: external abort, parent signal propagation, source error, source end, or an optional inter-segment watchdog timeout all converge on the
|
|
70
|
+
* same signal. The class is single-consumer by design - these views share one internal drain loop, not independent subscriptions, so a caller uses `stream()` OR the
|
|
71
|
+
* `initSegment` / `segments()` pair, never both concurrently.
|
|
72
|
+
*
|
|
73
|
+
* @module
|
|
74
|
+
*/
|
|
75
|
+
import { BOX_TYPE_MDAT, BOX_TYPE_MOOF, Mp4BoxParser } from "./mp4-parser.js";
|
|
76
|
+
import { HbpuAbortError, Watchdog, composeSignals, isTimeoutReason, markHandled, onAbort, waitWithSignal } from "../util.js";
|
|
77
|
+
import { on } from "node:events";
|
|
78
|
+
/**
|
|
79
|
+
* AsyncDisposable fMP4 segment assembler that converts a Readable byte source into an init segment promise and a media-segment async generator.
|
|
80
|
+
*
|
|
81
|
+
* 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
|
|
82
|
+
* machine: everything before the first `moof` accumulates into the initialization segment; from the first `moof` onward, boxes accumulate into the current media
|
|
83
|
+
* segment until an `mdat` flushes the accumulated pair to the output queue.
|
|
84
|
+
*
|
|
85
|
+
* The single public teardown verb is {@link Mp4SegmentAssembler.abort}, mirroring `AbortController.abort()`. `Symbol.asyncDispose` is implemented in terms of it and
|
|
86
|
+
* awaits the drain loop's completion before returning, so `await using` guarantees the assembler has fully unwound by the time the surrounding scope exits.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { Mp4SegmentAssembler } from "homebridge-plugin-utils";
|
|
92
|
+
*
|
|
93
|
+
* await using assembler = new Mp4SegmentAssembler(ffmpegStdout, { segmentTimeout: 4500, signal: session.signal });
|
|
94
|
+
*
|
|
95
|
+
* const initSegment = await assembler.initSegment;
|
|
96
|
+
*
|
|
97
|
+
* for await (const segment of assembler.segments()) {
|
|
98
|
+
*
|
|
99
|
+
* // Forward segment bytes to HomeKit.
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @see Mp4BoxParser
|
|
104
|
+
*
|
|
105
|
+
* @category FFmpeg
|
|
106
|
+
*/
|
|
107
|
+
export class Mp4SegmentAssembler {
|
|
108
|
+
/**
|
|
109
|
+
* The composed abort signal representing this assembler's lifetime. Aborts exactly once when the source ends, the source errors, the parent signal fires, the
|
|
110
|
+
* watchdog timeout expires, or {@link Mp4SegmentAssembler.abort} is called; the reason encoded on `signal.reason` names the cause.
|
|
111
|
+
*/
|
|
112
|
+
signal;
|
|
113
|
+
/**
|
|
114
|
+
* Promise that resolves with the concatenated initialization-segment bytes (typically `ftyp` + `moov`) once the first `moof` box arrives on the source. Rejects with
|
|
115
|
+
* `this.signal.reason` if the assembler is aborted before the initialization segment completes.
|
|
116
|
+
*/
|
|
117
|
+
initSegment;
|
|
118
|
+
// The private AbortController whose signal is composed into `this.signal`. Owning the controller internally keeps teardown reachable from any handler - the parser,
|
|
119
|
+
// the source error listener, the watchdog - without giving callers a handle to the raw controller.
|
|
120
|
+
#controller;
|
|
121
|
+
// The byte source. Held so drain-path listeners (`end`, `error`) can be attached and detached in one place.
|
|
122
|
+
#source;
|
|
123
|
+
// The box parser driving the drain loop. Stateful: carries residual bytes across chunks.
|
|
124
|
+
#parser;
|
|
125
|
+
// 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
|
|
126
|
+
// completed media segment; fires the composed controller with `HbpuAbortError("timeout")` on lapse. Self-cleans when the signal aborts for any other reason.
|
|
127
|
+
#watchdog;
|
|
128
|
+
// Resolver pair for {@link initSegment}. Resolved when the first `moof` arrives (with the accumulated init parts); rejected from the teardown path if still pending.
|
|
129
|
+
#initResolvers;
|
|
130
|
+
// Accumulated box bytes for the initialization segment. Flipped to empty and concatenated once the first `moof` flushes them into `initSegment`.
|
|
131
|
+
#initParts = [];
|
|
132
|
+
// Set `true` once the initialization segment has resolved. The phase flag for the drain state machine: `false` means "collecting init boxes"; `true` means "collecting
|
|
133
|
+
// media boxes."
|
|
134
|
+
#initResolved = false;
|
|
135
|
+
// Accumulated box bytes for the media segment currently being built. Reset to empty each time an `mdat` flushes the pair into the output queue.
|
|
136
|
+
#segmentParts = [];
|
|
137
|
+
// Completed media segments waiting to be yielded from `segments()`. A FIFO queue decouples the drain loop (producer) from the generator (consumer), which lets the
|
|
138
|
+
// consumer fall behind momentarily without dropping data.
|
|
139
|
+
#segmentQueue = [];
|
|
140
|
+
// 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
|
|
141
|
+
// single-consumer by design.
|
|
142
|
+
#segmentWaiter;
|
|
143
|
+
// The drain loop's promise. Held so `[Symbol.asyncDispose]` can await actual completion before returning, so callers using `await using` are guaranteed all drain
|
|
144
|
+
// listeners have been detached by the time the block exits.
|
|
145
|
+
#drainTask;
|
|
146
|
+
/**
|
|
147
|
+
* Construct and start a new fMP4 segment assembler.
|
|
148
|
+
*
|
|
149
|
+
* 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
|
|
150
|
+
* ready to emit boxes. There is no separate `start()` step.
|
|
151
|
+
*
|
|
152
|
+
* @param source - Any {@link Readable} producing fMP4 byte chunks. Typically an FFmpeg process's stdout; any Readable works, which keeps the class testable in
|
|
153
|
+
* isolation with in-memory fixture streams.
|
|
154
|
+
* @param init - Optional init options. See {@link Mp4SegmentAssemblerInit}.
|
|
155
|
+
*/
|
|
156
|
+
constructor(source, init = {}) {
|
|
157
|
+
const { signal: parentSignal, segmentTimeout } = init;
|
|
158
|
+
this.#controller = new AbortController();
|
|
159
|
+
this.signal = composeSignals(parentSignal, this.#controller.signal);
|
|
160
|
+
this.#source = source;
|
|
161
|
+
this.#parser = new Mp4BoxParser();
|
|
162
|
+
// Instantiate the inter-segment watchdog only when the caller opted into timeout enforcement. When undefined, no watchdog is constructed and every `arm()` site
|
|
163
|
+
// 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.
|
|
164
|
+
this.#watchdog = (segmentTimeout !== undefined) ? new Watchdog({
|
|
165
|
+
onFire: () => {
|
|
166
|
+
this.#controller.abort(new HbpuAbortError("timeout"));
|
|
167
|
+
},
|
|
168
|
+
signal: this.signal,
|
|
169
|
+
timeoutMs: segmentTimeout
|
|
170
|
+
}) : undefined;
|
|
171
|
+
// 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
|
|
172
|
+
// 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
|
|
173
|
+
// consumer uses only `segments()` and never awaits the init segment directly.
|
|
174
|
+
this.#initResolvers = Promise.withResolvers();
|
|
175
|
+
this.initSegment = markHandled(this.#initResolvers.promise);
|
|
176
|
+
// Attach a permanent source `"error"` absorber. Node's EventEmitter crashes the host process when `"error"` is emitted with no listeners, and the
|
|
177
|
+
// `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
|
|
178
|
+
// 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
|
|
179
|
+
// 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`
|
|
180
|
+
// still rejects the iterator; the absorber just backstops the cases `events.on` does not cover. Released with the assembler instance.
|
|
181
|
+
this.#source.on("error", () => { });
|
|
182
|
+
// Single teardown convergence point. `onAbort` registers the one-shot teardown listener for the normal abort path AND handles the "pre-aborted signal" edge case
|
|
183
|
+
// where `addEventListener("abort", ...)` would otherwise silently skip the handler (the AbortSignal spec does not re-dispatch historical events). Pairing it with
|
|
184
|
+
// the `signal.aborted` short-circuit below means `initSegment` settles consistently regardless of whether the parent signal was live at construction time.
|
|
185
|
+
onAbort(this.signal, () => this.#teardown());
|
|
186
|
+
if (this.signal.aborted) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Start the drain pipeline. The returned promise is retained so `[Symbol.asyncDispose]` can await the drain's completion before returning. `#drain` is written to
|
|
190
|
+
// always resolve - its body catches and classifies errors inline - so nothing can reject out of it, and no `markHandled` wrapper is needed here.
|
|
191
|
+
this.#drainTask = this.#drain();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Abort the assembler and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged.
|
|
195
|
+
*
|
|
196
|
+
* 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
|
|
197
|
+
* for the same reason.
|
|
198
|
+
*
|
|
199
|
+
* @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors (`TimeoutError`, `AbortError`) also interoperate by convention.
|
|
200
|
+
*/
|
|
201
|
+
abort(reason) {
|
|
202
|
+
if (this.aborted) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.#controller.abort(reason ?? new HbpuAbortError("shutdown"));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `AsyncDisposable` implementation. Aborts the assembler (defaulting to `"shutdown"`) and awaits actual drain-loop completion before returning, so callers using
|
|
209
|
+
* `await using` are guaranteed every internal listener has been detached by the time the block exits.
|
|
210
|
+
*
|
|
211
|
+
* @returns A promise that resolves once the drain loop has fully exited.
|
|
212
|
+
*/
|
|
213
|
+
async [Symbol.asyncDispose]() {
|
|
214
|
+
this.abort();
|
|
215
|
+
if (this.#drainTask) {
|
|
216
|
+
// Drain failures are already observed internally; swallow here so `await using` does not surface cleanup-side errors the caller cannot react to.
|
|
217
|
+
await this.#drainTask.catch(() => { });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `true` once `this.signal` has aborted. Derived from the signal; no independent state.
|
|
222
|
+
*/
|
|
223
|
+
get aborted() {
|
|
224
|
+
return this.signal.aborted;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* `true` when the abort reason indicates a timeout. Matches both the canonical `HbpuAbortError("timeout")` emitted by the inter-segment watchdog and the platform
|
|
228
|
+
* `TimeoutError` emitted by `AbortSignal.timeout()`. The branching lives in {@link isTimeoutReason} so this getter stays a one-line delegation and every
|
|
229
|
+
* resource class in the library shares one definition of "timeout."
|
|
230
|
+
*/
|
|
231
|
+
get isTimedOut() {
|
|
232
|
+
return isTimeoutReason(this.signal.reason);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The number of completed media segments buffered between the drain loop and the {@link Mp4SegmentAssembler.segments} consumer - the segments the producer has
|
|
236
|
+
* 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
|
|
237
|
+
* 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.
|
|
238
|
+
*/
|
|
239
|
+
get bufferedSegments() {
|
|
240
|
+
return this.#segmentQueue.length;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Async generator yielding each completed media segment (concatenated `moof` + `mdat` pair) as a single Buffer.
|
|
244
|
+
*
|
|
245
|
+
* Yields only after {@link Mp4SegmentAssembler.initSegment} has resolved - the init segment is not surfaced through this stream. Terminates cleanly when the source
|
|
246
|
+
* 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
|
|
247
|
+
* segment that was already assembled before teardown.
|
|
248
|
+
*
|
|
249
|
+
* **Single-consumer only.** The internal parked-waiter slot is single-writer; calling `segments()` concurrently with another consumer on the same assembler - including
|
|
250
|
+
* the {@link Mp4SegmentAssembler.stream} view, which drives this generator internally - is unsupported and will hang one of the consumers when the producer's wake-up
|
|
251
|
+
* resolves only the later parker. If fan-out is needed, tee at the consumer side by replicating each yielded Buffer into per-consumer queues external to the assembler.
|
|
252
|
+
*
|
|
253
|
+
* @param init - Optional init options. `signal` composes with the assembler's own signal - aborting it terminates only this generator call, not the assembler.
|
|
254
|
+
*
|
|
255
|
+
* @returns An async generator yielding concatenated `moof` + `mdat` pair buffers in stream order.
|
|
256
|
+
*/
|
|
257
|
+
async *segments(init = {}) {
|
|
258
|
+
// 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
|
|
259
|
+
// `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
|
|
260
|
+
// assembler's signal still governs the underlying pipeline.
|
|
261
|
+
const composed = composeSignals(this.signal, init.signal);
|
|
262
|
+
// 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
|
|
263
|
+
// wait terminates the iterator immediately rather than hanging until the assembler's own signal settles init.
|
|
264
|
+
try {
|
|
265
|
+
await waitWithSignal(this.initSegment, composed);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
for (;;) {
|
|
271
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
272
|
+
try {
|
|
273
|
+
// 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
|
|
274
|
+
// 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
|
|
275
|
+
// unconditionally before checking abort - "no bytes lost" is the rule for segments already assembled before teardown.
|
|
276
|
+
while (this.#segmentQueue.length > 0) {
|
|
277
|
+
const drained = this.#segmentQueue;
|
|
278
|
+
this.#segmentQueue = [];
|
|
279
|
+
for (const segment of drained) {
|
|
280
|
+
yield segment;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (composed.aborted) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// 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
|
|
287
|
+
// always fresh; sharing a resolver across iterations would require manual reset logic.
|
|
288
|
+
const waiter = Promise.withResolvers();
|
|
289
|
+
this.#segmentWaiter = waiter;
|
|
290
|
+
// `onAbort` handles listener registration, the pre-aborted-signal pitfall, and the one-shot `{ once: true }` semantic through one primitive; the returned
|
|
291
|
+
// `Disposable` is handed to `using` so the listener is deterministically removed on every scope-exit path (segment push via `#handleBox`, caller signal abort,
|
|
292
|
+
// assembler abort, thrown error in the generator). The single unified primitive matches the shape `waitWithSignal` uses for exactly the same kind of parked wait.
|
|
293
|
+
const _abortRegistration = __addDisposableResource(env_1, onAbort(composed, () => waiter.resolve()), false);
|
|
294
|
+
try {
|
|
295
|
+
// eslint-disable-next-line no-await-in-loop
|
|
296
|
+
await waiter.promise;
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
this.#segmentWaiter = undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (e_1) {
|
|
303
|
+
env_1.error = e_1;
|
|
304
|
+
env_1.hasError = true;
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
__disposeResources(env_1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Async generator yielding the whole segment stream as a kind-tagged sequence: exactly one {@link Mp4Segment} of kind `"init"` carrying the initialization bytes,
|
|
313
|
+
* followed by one of kind `"media"` per completed media fragment. This is a third view over the same single-pass pipeline, composed from {@link initSegment} and
|
|
314
|
+
* {@link segments} - it lets a consumer forward the init segment and the media segments through a single loop without tracking which item is which by position.
|
|
315
|
+
*
|
|
316
|
+
* Terminates cleanly on the same conditions as {@link segments}: the source ends, the assembler aborts, or the optional caller signal aborts; queued media drains
|
|
317
|
+
* before the generator returns, so no assembled segment is lost. If the assembler is aborted before the initialization segment arrives, the generator returns without
|
|
318
|
+
* yielding anything.
|
|
319
|
+
*
|
|
320
|
+
* **Single-consumer only.** `stream()` drives {@link segments} internally, so it shares the one parked-waiter slot. Use `stream()` OR the {@link initSegment} /
|
|
321
|
+
* {@link segments} pair on a single assembler, never both concurrently - mixing them competes for the same drain and hangs one consumer.
|
|
322
|
+
*
|
|
323
|
+
* @param init - Optional init options. `signal` composes with the assembler's own signal - aborting it terminates only this generator call, not the assembler.
|
|
324
|
+
*
|
|
325
|
+
* @returns An async generator yielding one `"init"` segment followed by `"media"` segments in stream order.
|
|
326
|
+
*/
|
|
327
|
+
async *stream(init = {}) {
|
|
328
|
+
// Wait for the initialization segment first, racing the caller signal exactly as segments() does, so a caller abort during the init wait ends this stream at once
|
|
329
|
+
// rather than hanging until the assembler's own signal settles init. A rejection here (aborted before the first moof) ends the stream with nothing yielded.
|
|
330
|
+
const composed = composeSignals(this.signal, init.signal);
|
|
331
|
+
let initBytes;
|
|
332
|
+
try {
|
|
333
|
+
initBytes = await waitWithSignal(this.initSegment, composed);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// The one init item, then every media segment relabeled. The media loop delegates to segments() so the swap-drain, the parked wait, and the no-bytes-lost teardown
|
|
339
|
+
// stay in exactly one place rather than being reimplemented here.
|
|
340
|
+
yield { bytes: initBytes, kind: "init" };
|
|
341
|
+
for await (const segment of this.segments(init)) {
|
|
342
|
+
yield { bytes: segment, kind: "media" };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Drain loop. Attaches the source's `end` listener, then iterates `events.on(source, "data", { signal })` until the composed signal aborts. For each chunk, the
|
|
346
|
+
// parser is fed and every complete box is dispatched through `#handleBox`. Every exit path - source end, source error, external abort - converges on the signal
|
|
347
|
+
// being aborted, which lets the generator's park loop unwind uniformly via `composed.aborted`.
|
|
348
|
+
async #drain() {
|
|
349
|
+
// 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
|
|
350
|
+
// 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
|
|
351
|
+
// source and produced both `end` and our own abort).
|
|
352
|
+
const onEnd = () => {
|
|
353
|
+
if (this.aborted) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.#controller.abort(new HbpuAbortError("closed"));
|
|
357
|
+
};
|
|
358
|
+
this.#source.once("end", onEnd);
|
|
359
|
+
try {
|
|
360
|
+
// `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
|
|
361
|
+
// typing stays explicit. The `{ signal }` option wires abort-driven termination into the iterator directly - when the signal fires, the iterator rejects its next
|
|
362
|
+
// 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
|
|
363
|
+
// block below classifies both exit paths.
|
|
364
|
+
for await (const eventArgs of on(this.#source, "data", { signal: this.signal })) {
|
|
365
|
+
const [chunk] = eventArgs;
|
|
366
|
+
for (const box of this.#parser.consume(chunk)) {
|
|
367
|
+
this.#handleBox(box);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
// Single classification point. Every rejection this loop can produce lands here, classified into one of two reasons today: our composed signal aborted
|
|
373
|
+
// (already carries a structured `signal.reason`, `this.aborted` is true, pass through unchanged) or the source emitted an `"error"` event (the emitted error
|
|
374
|
+
// 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,
|
|
375
|
+
// which today it does not - `#handleBox` is synchronous and side-effect-only.
|
|
376
|
+
if (!this.aborted) {
|
|
377
|
+
this.#controller.abort(new HbpuAbortError("failed", { cause: error }));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
this.#source.off("end", onEnd);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Single-box state machine. Before init resolves, boxes accumulate into `#initParts`; the first `moof` flushes them into the init promise, transitions to media
|
|
385
|
+
// 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
|
|
386
|
+
// and pushes it to the output queue.
|
|
387
|
+
#handleBox(box) {
|
|
388
|
+
if (!this.#initResolved) {
|
|
389
|
+
if (box.type === BOX_TYPE_MOOF) {
|
|
390
|
+
// First `moof` - the preceding boxes (typically ftyp + moov) are the complete init segment. Concatenate them once, resolve the promise, and transition into
|
|
391
|
+
// media-collection mode with this `moof` starting the first segment.
|
|
392
|
+
const init = Buffer.concat(this.#initParts);
|
|
393
|
+
this.#initParts = [];
|
|
394
|
+
this.#initResolved = true;
|
|
395
|
+
this.#initResolvers.resolve(init);
|
|
396
|
+
this.#segmentParts.push(box.bytes);
|
|
397
|
+
this.#watchdog?.arm();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.#initParts.push(box.bytes);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// 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
|
|
404
|
+
// possible) are appended to the current pair so the downstream consumer sees the stream verbatim.
|
|
405
|
+
this.#segmentParts.push(box.bytes);
|
|
406
|
+
if (box.type === BOX_TYPE_MDAT) {
|
|
407
|
+
const segment = Buffer.concat(this.#segmentParts);
|
|
408
|
+
this.#segmentParts = [];
|
|
409
|
+
this.#segmentQueue.push(segment);
|
|
410
|
+
this.#segmentWaiter?.resolve();
|
|
411
|
+
this.#watchdog?.arm();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// 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
|
|
415
|
+
// 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
|
|
416
|
+
// exit surface and it wakes up via the resolved waiter.
|
|
417
|
+
#teardown() {
|
|
418
|
+
// Promise resolvers are inert after first settlement, so calling reject on an already-resolved init promise is a safe no-op. This lets us keep the teardown
|
|
419
|
+
// path uniform regardless of whether init arrived before the abort or not.
|
|
420
|
+
this.#initResolvers.reject(this.signal.reason);
|
|
421
|
+
this.#segmentWaiter?.resolve();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
//# 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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"}
|