@motionscript/browser 0.0.0-stage → 0.1.0-alpha.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 (120) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/README.md +68 -3
  4. package/dist/asset-bytes.d.ts +50 -0
  5. package/dist/asset-bytes.d.ts.map +1 -0
  6. package/dist/asset-bytes.js +60 -0
  7. package/dist/asset-bytes.js.map +1 -0
  8. package/dist/audio/bus-graph.d.ts +43 -0
  9. package/dist/audio/bus-graph.d.ts.map +1 -0
  10. package/dist/audio/bus-graph.js +104 -0
  11. package/dist/audio/bus-graph.js.map +1 -0
  12. package/dist/audio/filter-graph.d.ts +58 -0
  13. package/dist/audio/filter-graph.d.ts.map +1 -0
  14. package/dist/audio/filter-graph.js +235 -0
  15. package/dist/audio/filter-graph.js.map +1 -0
  16. package/dist/audio/index.d.ts +5 -0
  17. package/dist/audio/index.d.ts.map +1 -0
  18. package/dist/audio/index.js +12 -0
  19. package/dist/audio/index.js.map +1 -0
  20. package/dist/audio/mixer.d.ts +59 -0
  21. package/dist/audio/mixer.d.ts.map +1 -0
  22. package/dist/audio/mixer.js +193 -0
  23. package/dist/audio/mixer.js.map +1 -0
  24. package/dist/audio/player.d.ts +49 -0
  25. package/dist/audio/player.d.ts.map +1 -0
  26. package/dist/audio/player.js +226 -0
  27. package/dist/audio/player.js.map +1 -0
  28. package/dist/browser/audio.js +2 -0
  29. package/dist/browser/audio.js.map +7 -0
  30. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +2 -0
  31. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +7 -0
  32. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +2 -0
  33. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js.map +7 -0
  34. package/dist/browser/chunks/chunk-CTLBXZSU.js +4119 -0
  35. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +7 -0
  36. package/dist/browser/chunks/chunk-FQ32PDIU.js +2 -0
  37. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +7 -0
  38. package/dist/browser/chunks/chunk-FY3BTKY4.js +2 -0
  39. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +7 -0
  40. package/dist/browser/chunks/chunk-NSFLRI4P.js +2 -0
  41. package/dist/browser/chunks/chunk-NSFLRI4P.js.map +7 -0
  42. package/dist/browser/chunks/three.module-Z4EOVUWF.js +2 -0
  43. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +7 -0
  44. package/dist/browser/esbuild.wasm +0 -0
  45. package/dist/browser/index.js +2172 -0
  46. package/dist/browser/index.js.map +7 -0
  47. package/dist/browser/manifest.json +15 -0
  48. package/dist/concat.d.ts +95 -0
  49. package/dist/concat.d.ts.map +1 -0
  50. package/dist/concat.js +174 -0
  51. package/dist/concat.js.map +1 -0
  52. package/dist/decode/image-levels.d.ts +137 -0
  53. package/dist/decode/image-levels.d.ts.map +1 -0
  54. package/dist/decode/image-levels.js +477 -0
  55. package/dist/decode/image-levels.js.map +1 -0
  56. package/dist/decode/levels.d.ts +37 -0
  57. package/dist/decode/levels.d.ts.map +1 -0
  58. package/dist/decode/levels.js +68 -0
  59. package/dist/decode/levels.js.map +1 -0
  60. package/dist/decode/video-levels.d.ts +37 -0
  61. package/dist/decode/video-levels.d.ts.map +1 -0
  62. package/dist/decode/video-levels.js +95 -0
  63. package/dist/decode/video-levels.js.map +1 -0
  64. package/dist/dynamic-nodes.d.ts +9 -0
  65. package/dist/dynamic-nodes.d.ts.map +1 -0
  66. package/dist/dynamic-nodes.js +56 -0
  67. package/dist/dynamic-nodes.js.map +1 -0
  68. package/dist/engine.d.ts +59 -0
  69. package/dist/engine.d.ts.map +1 -0
  70. package/dist/engine.js +171 -0
  71. package/dist/engine.js.map +1 -0
  72. package/dist/exporter.d.ts +163 -0
  73. package/dist/exporter.d.ts.map +1 -0
  74. package/dist/exporter.js +263 -0
  75. package/dist/exporter.js.map +1 -0
  76. package/dist/getter.d.ts +10 -0
  77. package/dist/getter.d.ts.map +1 -0
  78. package/dist/getter.js +38 -0
  79. package/dist/getter.js.map +1 -0
  80. package/dist/index.d.ts +22 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +72 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/measure-context.d.ts +12 -0
  85. package/dist/measure-context.d.ts.map +1 -0
  86. package/dist/measure-context.js +12 -0
  87. package/dist/measure-context.js.map +1 -0
  88. package/dist/mix-timeline-audio.d.ts +42 -0
  89. package/dist/mix-timeline-audio.d.ts.map +1 -0
  90. package/dist/mix-timeline-audio.js +63 -0
  91. package/dist/mix-timeline-audio.js.map +1 -0
  92. package/dist/output.d.ts +7 -0
  93. package/dist/output.d.ts.map +1 -0
  94. package/dist/output.js +6 -0
  95. package/dist/output.js.map +1 -0
  96. package/dist/render-context.d.ts +30 -0
  97. package/dist/render-context.d.ts.map +1 -0
  98. package/dist/render-context.js +55 -0
  99. package/dist/render-context.js.map +1 -0
  100. package/dist/screenshot.d.ts +45 -0
  101. package/dist/screenshot.d.ts.map +1 -0
  102. package/dist/screenshot.js +32 -0
  103. package/dist/screenshot.js.map +1 -0
  104. package/dist/still.d.ts +151 -0
  105. package/dist/still.d.ts.map +1 -0
  106. package/dist/still.js +222 -0
  107. package/dist/still.js.map +1 -0
  108. package/dist/storage-adapter.d.ts +367 -0
  109. package/dist/storage-adapter.d.ts.map +1 -0
  110. package/dist/storage-adapter.js +1278 -0
  111. package/dist/storage-adapter.js.map +1 -0
  112. package/dist/three/renderer.d.ts +48 -0
  113. package/dist/three/renderer.d.ts.map +1 -0
  114. package/dist/three/renderer.js +177 -0
  115. package/dist/three/renderer.js.map +1 -0
  116. package/dist/ticker.d.ts +42 -0
  117. package/dist/ticker.d.ts.map +1 -0
  118. package/dist/ticker.js +143 -0
  119. package/dist/ticker.js.map +1 -0
  120. package/package.json +74 -3
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@motionscript/browser",
3
+ "version": "0.1.0-alpha.0",
4
+ "exports": {
5
+ ".": "dist/browser/index.js",
6
+ "./audio": "dist/browser/audio.js"
7
+ },
8
+ "peers": [
9
+ "@motionscript/canvaskit",
10
+ "@motionscript/core"
11
+ ],
12
+ "assets": {
13
+ "esbuild.wasm": "dist/browser/esbuild.wasm"
14
+ }
15
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Join encoded MP4 segments end to end, without re-encoding.
3
+ *
4
+ * The point of this is what it *doesn't* do: no decode, no encode, no pixels.
5
+ * Each segment is demuxed to its encoded packets, the packets are re-stamped onto
6
+ * one continuous timeline, and a single muxer writes them into one container. The
7
+ * bytes of every frame survive untouched, so the result is bit-identical to the
8
+ * segments and the whole thing runs at I/O speed rather than encode speed.
9
+ *
10
+ * It exists so an export can be **split across workers**. Rendering a timeline is
11
+ * embarrassingly parallel per scene — each scene's generator is independent — but
12
+ * the output has to be one file, and re-encoding the parts to join them would give
13
+ * back everything the parallelism won (and lose a generation of quality). Joining
14
+ * at the packet level is what makes splitting worth doing.
15
+ *
16
+ * ## What the caller has to guarantee
17
+ *
18
+ * **Every segment must be encoded with an identical configuration** — same codec,
19
+ * same resolution, same bitrate settings. A container carries exactly one decoder
20
+ * configuration for a track, so a segment whose parameter sets differ from the
21
+ * first would be described by a configuration that isn't its own, and decode as
22
+ * garbage from that point on. That is not a theoretical concern: hardware encoders
23
+ * are free to pick different parameter sets for the same nominal settings. This is
24
+ * checked rather than assumed — see {@link ConcatMismatchError} — because the
25
+ * failure is silent corruption rather than an exception.
26
+ *
27
+ * **Every segment must begin with a key frame.** Otherwise its first packets refer
28
+ * back to frames that lived in the previous segment's GOP and no longer precede
29
+ * them. Encoders start with a key frame by default; a caller choosing an explicit
30
+ * `keyFrameInterval` needs it to divide the segment length.
31
+ *
32
+ * Audio is *muxed* here but never *joined*: the segments' own audio is ignored
33
+ * entirely, and a single already-mixed buffer is encoded once across the whole
34
+ * result (see {@link ConcatOptions.audio}). Concatenating separately-encoded AAC
35
+ * would put the encoder's priming delay at every join — a click at each boundary
36
+ * and a track that drifts further out of sync with every one — so the timeline's
37
+ * audio is mixed in one pass by whoever owns the timeline, and arrives here as
38
+ * one buffer.
39
+ */
40
+ /** Thrown when segments can't be joined losslessly. Carries which segment disagreed. */
41
+ export declare class ConcatMismatchError extends Error {
42
+ /** Index of the offending segment in the input array. */
43
+ readonly segment: number;
44
+ constructor(
45
+ /** Index of the offending segment in the input array. */
46
+ segment: number, message: string);
47
+ }
48
+ export interface ConcatOptions {
49
+ /**
50
+ * One continuous audio track for the whole joined timeline.
51
+ *
52
+ * Mixed once by the caller (see `mixTimelineAudio`) rather than per segment,
53
+ * and that is not a convenience: separately-encoded AAC carries an encoder
54
+ * priming delay at the head of each piece, so joining them puts a click at
55
+ * every scene boundary and drifts the track further out of sync with each
56
+ * one. One buffer in, one encode, no seams.
57
+ */
58
+ audio?: AudioBuffer | null;
59
+ /** AAC bitrate for that track. Defaults to 192 kbps. */
60
+ audioBitrate?: number;
61
+ /**
62
+ * Write the result out as it is muxed instead of returning it.
63
+ *
64
+ * With this the joined file never exists in memory as one buffer, which is
65
+ * the difference between an export the process can hold and one it can't:
66
+ * a 4× 60fps render is hundreds of megabytes, and the default path costs
67
+ * roughly twice that at the tail once the `Blob` copy is counted.
68
+ *
69
+ * **Chunks carry a byte `position` and do not arrive in file order** — a
70
+ * muxer seeks back to patch atom sizes. A sink that appends will produce a
71
+ * file of exactly the right length whose size fields belong to a different
72
+ * one: unplayable, and indistinguishable from success until it is opened.
73
+ * Honour `position`.
74
+ *
75
+ * `concatVideoSegments` resolves to an empty array when this is used; the
76
+ * bytes went to the sink.
77
+ */
78
+ stream?: WritableStream<{
79
+ type: "write";
80
+ data: Uint8Array;
81
+ position: number;
82
+ }>;
83
+ }
84
+ /**
85
+ * Concatenate `segments` (each a complete MP4) into one MP4.
86
+ *
87
+ * Segments are joined in array order. Returns the muxed bytes.
88
+ *
89
+ * @throws {ConcatMismatchError} if a segment carries no video track, or one whose
90
+ * decoder configuration differs from the first segment's. Callers that can
91
+ * fall back to a single-threaded export should catch this and do so — the
92
+ * alternative is shipping a file that decodes into garbage halfway through.
93
+ */
94
+ export declare function concatVideoSegments(segments: Uint8Array[], options?: ConcatOptions): Promise<Uint8Array>;
95
+ //# sourceMappingURL=concat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concat.d.ts","sourceRoot":"","sources":["../src/concat.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,wFAAwF;AACxF,qBAAa,mBAAoB,SAAQ,KAAK;IAEtC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM;;IADxB,yDAAyD;IAChD,OAAO,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM;CAKtB;AA0CD,MAAM,WAAW,aAAa;IAC1B;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClF;AAID;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACrC,QAAQ,EAAE,UAAU,EAAE,EACtB,OAAO,GAAE,aAAkB,GAC5B,OAAO,CAAC,UAAU,CAAC,CA8ErB"}
package/dist/concat.js ADDED
@@ -0,0 +1,174 @@
1
+ import { AudioBufferSource, BufferSource, BufferTarget, EncodedPacketSink, EncodedVideoPacketSource, Input, MP4, Mp4OutputFormat, Output, StreamTarget, } from "mediabunny";
2
+ /**
3
+ * Join encoded MP4 segments end to end, without re-encoding.
4
+ *
5
+ * The point of this is what it *doesn't* do: no decode, no encode, no pixels.
6
+ * Each segment is demuxed to its encoded packets, the packets are re-stamped onto
7
+ * one continuous timeline, and a single muxer writes them into one container. The
8
+ * bytes of every frame survive untouched, so the result is bit-identical to the
9
+ * segments and the whole thing runs at I/O speed rather than encode speed.
10
+ *
11
+ * It exists so an export can be **split across workers**. Rendering a timeline is
12
+ * embarrassingly parallel per scene — each scene's generator is independent — but
13
+ * the output has to be one file, and re-encoding the parts to join them would give
14
+ * back everything the parallelism won (and lose a generation of quality). Joining
15
+ * at the packet level is what makes splitting worth doing.
16
+ *
17
+ * ## What the caller has to guarantee
18
+ *
19
+ * **Every segment must be encoded with an identical configuration** — same codec,
20
+ * same resolution, same bitrate settings. A container carries exactly one decoder
21
+ * configuration for a track, so a segment whose parameter sets differ from the
22
+ * first would be described by a configuration that isn't its own, and decode as
23
+ * garbage from that point on. That is not a theoretical concern: hardware encoders
24
+ * are free to pick different parameter sets for the same nominal settings. This is
25
+ * checked rather than assumed — see {@link ConcatMismatchError} — because the
26
+ * failure is silent corruption rather than an exception.
27
+ *
28
+ * **Every segment must begin with a key frame.** Otherwise its first packets refer
29
+ * back to frames that lived in the previous segment's GOP and no longer precede
30
+ * them. Encoders start with a key frame by default; a caller choosing an explicit
31
+ * `keyFrameInterval` needs it to divide the segment length.
32
+ *
33
+ * Audio is *muxed* here but never *joined*: the segments' own audio is ignored
34
+ * entirely, and a single already-mixed buffer is encoded once across the whole
35
+ * result (see {@link ConcatOptions.audio}). Concatenating separately-encoded AAC
36
+ * would put the encoder's priming delay at every join — a click at each boundary
37
+ * and a track that drifts further out of sync with every one — so the timeline's
38
+ * audio is mixed in one pass by whoever owns the timeline, and arrives here as
39
+ * one buffer.
40
+ */
41
+ /** Thrown when segments can't be joined losslessly. Carries which segment disagreed. */
42
+ export class ConcatMismatchError extends Error {
43
+ segment;
44
+ constructor(
45
+ /** Index of the offending segment in the input array. */
46
+ segment, message) {
47
+ super(message);
48
+ this.segment = segment;
49
+ this.name = "ConcatMismatchError";
50
+ }
51
+ }
52
+ /**
53
+ * Two decoder configurations describe the same bitstream.
54
+ *
55
+ * `description` holds the codec's parameter sets (the `avcC` box for H.264) and is
56
+ * the field that actually decides decodability — a differing one is exactly the
57
+ * silent-corruption case. Compared bytewise; the rest are compared by value.
58
+ */
59
+ function sameDecoderConfig(a, b) {
60
+ if (a.codec !== b.codec)
61
+ return false;
62
+ if (a.codedWidth !== b.codedWidth)
63
+ return false;
64
+ if (a.codedHeight !== b.codedHeight)
65
+ return false;
66
+ const da = toBytes(a.description);
67
+ const db = toBytes(b.description);
68
+ if (da === null || db === null)
69
+ return da === db;
70
+ if (da.length !== db.length)
71
+ return false;
72
+ for (let i = 0; i < da.length; i++) {
73
+ if (da[i] !== db[i])
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ function toBytes(description) {
79
+ if (!description)
80
+ return null;
81
+ // `description` is a BufferSource: an ArrayBuffer, a SharedArrayBuffer, or any
82
+ // view over one. Views carry an offset into a larger buffer, so they must be
83
+ // narrowed rather than wrapped whole.
84
+ if (ArrayBuffer.isView(description)) {
85
+ return new Uint8Array(description.buffer, description.byteOffset, description.byteLength);
86
+ }
87
+ return new Uint8Array(description);
88
+ }
89
+ const DEFAULT_AUDIO_BITRATE = 192_000;
90
+ /**
91
+ * Concatenate `segments` (each a complete MP4) into one MP4.
92
+ *
93
+ * Segments are joined in array order. Returns the muxed bytes.
94
+ *
95
+ * @throws {ConcatMismatchError} if a segment carries no video track, or one whose
96
+ * decoder configuration differs from the first segment's. Callers that can
97
+ * fall back to a single-threaded export should catch this and do so — the
98
+ * alternative is shipping a file that decodes into garbage halfway through.
99
+ */
100
+ export async function concatVideoSegments(segments, options = {}) {
101
+ if (segments.length === 0)
102
+ throw new Error("concatVideoSegments: no segments");
103
+ // A lone segment is already the finished file — unless a track has to be added
104
+ // to it, or it has to go to a sink, either of which means muxing after all.
105
+ if (segments.length === 1 && !options.audio && !options.stream)
106
+ return segments[0];
107
+ const opened = [];
108
+ for (let i = 0; i < segments.length; i++) {
109
+ const input = new Input({ source: new BufferSource(segments[i]), formats: [MP4] });
110
+ const track = await input.getPrimaryVideoTrack();
111
+ if (!track)
112
+ throw new ConcatMismatchError(i, `Segment ${i} has no video track.`);
113
+ const config = await track.getDecoderConfig();
114
+ if (!config)
115
+ throw new ConcatMismatchError(i, `Segment ${i} has no decoder configuration.`);
116
+ if (opened.length > 0 && !sameDecoderConfig(opened[0].config, config)) {
117
+ throw new ConcatMismatchError(i, `Segment ${i} was encoded differently from segment 0 (${opened[0].config.codec} `
118
+ + `${opened[0].config.codedWidth}x${opened[0].config.codedHeight} vs ${config.codec} `
119
+ + `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`);
120
+ }
121
+ opened.push({ track, config, duration: await track.computeDuration() });
122
+ }
123
+ const target = options.stream
124
+ ? new StreamTarget(options.stream)
125
+ : new BufferTarget();
126
+ const output = new Output({ format: new Mp4OutputFormat(), target });
127
+ // `codec` comes off the track rather than the decoder config: the latter's
128
+ // `codec` is the full codec *string* (`avc1.64001f`), while the source wants
129
+ // the family.
130
+ const source = new EncodedVideoPacketSource(opened[0].track.codec);
131
+ output.addVideoTrack(source);
132
+ // Declared before `start()` — a muxer fixes its track list up front, so an
133
+ // audio track added later would have nowhere to go.
134
+ const audioSource = options.audio
135
+ ? new AudioBufferSource({
136
+ codec: "aac",
137
+ bitrate: options.audioBitrate ?? DEFAULT_AUDIO_BITRATE,
138
+ })
139
+ : null;
140
+ if (audioSource)
141
+ output.addAudioTrack(audioSource);
142
+ await output.start();
143
+ let offset = 0;
144
+ for (const segment of opened) {
145
+ // The decoder config is handed over with the first packet of each segment.
146
+ // Only the first one can take effect — a container holds one per track —
147
+ // but they have all been checked equal above, so restating it is harmless
148
+ // and keeps the muxer from having to infer anything.
149
+ let first = true;
150
+ for await (const packet of new EncodedPacketSink(segment.track).packets()) {
151
+ await source.add(packet.clone({ timestamp: packet.timestamp + offset }), first ? { decoderConfig: segment.config } : undefined);
152
+ first = false;
153
+ }
154
+ // Advance by the segment's own duration rather than by the last packet's
155
+ // end: a packet's `duration` is what the container recorded, and summing
156
+ // those would accumulate rounding into a drift across many segments.
157
+ offset += segment.duration;
158
+ }
159
+ // After the video packets, so the encode overlaps nothing it has to wait on.
160
+ if (audioSource && options.audio)
161
+ await audioSource.add(options.audio);
162
+ source.close();
163
+ audioSource?.close();
164
+ await output.finalize();
165
+ // A streamed result has already gone to the sink; there is nothing to hand
166
+ // back, and materialising it here would undo the point of streaming.
167
+ if (options.stream)
168
+ return new Uint8Array(0);
169
+ const buffer = target.buffer;
170
+ if (!buffer)
171
+ throw new Error("concatVideoSegments produced no data");
172
+ return new Uint8Array(buffer);
173
+ }
174
+ //# sourceMappingURL=concat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concat.js","sourceRoot":"","sources":["../src/concat.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AudioBufferSource,\n BufferSource,\n BufferTarget,\n EncodedPacketSink,\n EncodedVideoPacketSource,\n Input,\n MP4,\n Mp4OutputFormat,\n Output,\n StreamTarget,\n type InputVideoTrack,\n type VideoCodec,\n} from \"mediabunny\";\n\n/**\n * Join encoded MP4 segments end to end, without re-encoding.\n *\n * The point of this is what it *doesn't* do: no decode, no encode, no pixels.\n * Each segment is demuxed to its encoded packets, the packets are re-stamped onto\n * one continuous timeline, and a single muxer writes them into one container. The\n * bytes of every frame survive untouched, so the result is bit-identical to the\n * segments and the whole thing runs at I/O speed rather than encode speed.\n *\n * It exists so an export can be **split across workers**. Rendering a timeline is\n * embarrassingly parallel per scene — each scene's generator is independent — but\n * the output has to be one file, and re-encoding the parts to join them would give\n * back everything the parallelism won (and lose a generation of quality). Joining\n * at the packet level is what makes splitting worth doing.\n *\n * ## What the caller has to guarantee\n *\n * **Every segment must be encoded with an identical configuration** — same codec,\n * same resolution, same bitrate settings. A container carries exactly one decoder\n * configuration for a track, so a segment whose parameter sets differ from the\n * first would be described by a configuration that isn't its own, and decode as\n * garbage from that point on. That is not a theoretical concern: hardware encoders\n * are free to pick different parameter sets for the same nominal settings. This is\n * checked rather than assumed — see {@link ConcatMismatchError} — because the\n * failure is silent corruption rather than an exception.\n *\n * **Every segment must begin with a key frame.** Otherwise its first packets refer\n * back to frames that lived in the previous segment's GOP and no longer precede\n * them. Encoders start with a key frame by default; a caller choosing an explicit\n * `keyFrameInterval` needs it to divide the segment length.\n *\n * Audio is *muxed* here but never *joined*: the segments' own audio is ignored\n * entirely, and a single already-mixed buffer is encoded once across the whole\n * result (see {@link ConcatOptions.audio}). Concatenating separately-encoded AAC\n * would put the encoder's priming delay at every join — a click at each boundary\n * and a track that drifts further out of sync with every one — so the timeline's\n * audio is mixed in one pass by whoever owns the timeline, and arrives here as\n * one buffer.\n */\n\n/** Thrown when segments can't be joined losslessly. Carries which segment disagreed. */\nexport class ConcatMismatchError extends Error {\n constructor(\n /** Index of the offending segment in the input array. */\n readonly segment: number,\n message: string,\n ) {\n super(message);\n this.name = \"ConcatMismatchError\";\n }\n}\n\n/** One segment's demuxed video track, plus the config that describes it. */\ninterface Segment {\n track: InputVideoTrack;\n config: VideoDecoderConfig;\n duration: number;\n}\n\n/**\n * Two decoder configurations describe the same bitstream.\n *\n * `description` holds the codec's parameter sets (the `avcC` box for H.264) and is\n * the field that actually decides decodability — a differing one is exactly the\n * silent-corruption case. Compared bytewise; the rest are compared by value.\n */\nfunction sameDecoderConfig(a: VideoDecoderConfig, b: VideoDecoderConfig): boolean {\n if (a.codec !== b.codec) return false;\n if (a.codedWidth !== b.codedWidth) return false;\n if (a.codedHeight !== b.codedHeight) return false;\n\n const da = toBytes(a.description);\n const db = toBytes(b.description);\n if (da === null || db === null) return da === db;\n if (da.length !== db.length) return false;\n for (let i = 0; i < da.length; i++) {\n if (da[i] !== db[i]) return false;\n }\n return true;\n}\n\nfunction toBytes(description: VideoDecoderConfig[\"description\"]): Uint8Array | null {\n if (!description) return null;\n // `description` is a BufferSource: an ArrayBuffer, a SharedArrayBuffer, or any\n // view over one. Views carry an offset into a larger buffer, so they must be\n // narrowed rather than wrapped whole.\n if (ArrayBuffer.isView(description)) {\n return new Uint8Array(description.buffer, description.byteOffset, description.byteLength);\n }\n return new Uint8Array(description);\n}\n\nexport interface ConcatOptions {\n /**\n * One continuous audio track for the whole joined timeline.\n *\n * Mixed once by the caller (see `mixTimelineAudio`) rather than per segment,\n * and that is not a convenience: separately-encoded AAC carries an encoder\n * priming delay at the head of each piece, so joining them puts a click at\n * every scene boundary and drifts the track further out of sync with each\n * one. One buffer in, one encode, no seams.\n */\n audio?: AudioBuffer | null;\n /** AAC bitrate for that track. Defaults to 192 kbps. */\n audioBitrate?: number;\n /**\n * Write the result out as it is muxed instead of returning it.\n *\n * With this the joined file never exists in memory as one buffer, which is\n * the difference between an export the process can hold and one it can't:\n * a 4× 60fps render is hundreds of megabytes, and the default path costs\n * roughly twice that at the tail once the `Blob` copy is counted.\n *\n * **Chunks carry a byte `position` and do not arrive in file order** — a\n * muxer seeks back to patch atom sizes. A sink that appends will produce a\n * file of exactly the right length whose size fields belong to a different\n * one: unplayable, and indistinguishable from success until it is opened.\n * Honour `position`.\n *\n * `concatVideoSegments` resolves to an empty array when this is used; the\n * bytes went to the sink.\n */\n stream?: WritableStream<{ type: \"write\"; data: Uint8Array; position: number }>;\n}\n\nconst DEFAULT_AUDIO_BITRATE = 192_000;\n\n/**\n * Concatenate `segments` (each a complete MP4) into one MP4.\n *\n * Segments are joined in array order. Returns the muxed bytes.\n *\n * @throws {ConcatMismatchError} if a segment carries no video track, or one whose\n * decoder configuration differs from the first segment's. Callers that can\n * fall back to a single-threaded export should catch this and do so — the\n * alternative is shipping a file that decodes into garbage halfway through.\n */\nexport async function concatVideoSegments(\n segments: Uint8Array[],\n options: ConcatOptions = {},\n): Promise<Uint8Array> {\n if (segments.length === 0) throw new Error(\"concatVideoSegments: no segments\");\n // A lone segment is already the finished file — unless a track has to be added\n // to it, or it has to go to a sink, either of which means muxing after all.\n if (segments.length === 1 && !options.audio && !options.stream) return segments[0];\n\n const opened: Segment[] = [];\n for (let i = 0; i < segments.length; i++) {\n const input = new Input({ source: new BufferSource(segments[i]), formats: [MP4] });\n const track = await input.getPrimaryVideoTrack();\n if (!track) throw new ConcatMismatchError(i, `Segment ${i} has no video track.`);\n const config = await track.getDecoderConfig();\n if (!config) throw new ConcatMismatchError(i, `Segment ${i} has no decoder configuration.`);\n if (opened.length > 0 && !sameDecoderConfig(opened[0].config, config)) {\n throw new ConcatMismatchError(\n i,\n `Segment ${i} was encoded differently from segment 0 (${opened[0].config.codec} `\n + `${opened[0].config.codedWidth}x${opened[0].config.codedHeight} vs ${config.codec} `\n + `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`,\n );\n }\n opened.push({ track, config, duration: await track.computeDuration() });\n }\n\n const target = options.stream\n ? new StreamTarget(options.stream as ConstructorParameters<typeof StreamTarget>[0])\n : new BufferTarget();\n const output = new Output({ format: new Mp4OutputFormat(), target });\n // `codec` comes off the track rather than the decoder config: the latter's\n // `codec` is the full codec *string* (`avc1.64001f`), while the source wants\n // the family.\n const source = new EncodedVideoPacketSource(opened[0].track.codec as VideoCodec);\n output.addVideoTrack(source);\n // Declared before `start()` — a muxer fixes its track list up front, so an\n // audio track added later would have nowhere to go.\n const audioSource = options.audio\n ? new AudioBufferSource({\n codec: \"aac\",\n bitrate: options.audioBitrate ?? DEFAULT_AUDIO_BITRATE,\n })\n : null;\n if (audioSource) output.addAudioTrack(audioSource);\n await output.start();\n\n let offset = 0;\n for (const segment of opened) {\n // The decoder config is handed over with the first packet of each segment.\n // Only the first one can take effect — a container holds one per track —\n // but they have all been checked equal above, so restating it is harmless\n // and keeps the muxer from having to infer anything.\n let first = true;\n for await (const packet of new EncodedPacketSink(segment.track).packets()) {\n await source.add(\n packet.clone({ timestamp: packet.timestamp + offset }),\n first ? { decoderConfig: segment.config } : undefined,\n );\n first = false;\n }\n // Advance by the segment's own duration rather than by the last packet's\n // end: a packet's `duration` is what the container recorded, and summing\n // those would accumulate rounding into a drift across many segments.\n offset += segment.duration;\n }\n\n // After the video packets, so the encode overlaps nothing it has to wait on.\n if (audioSource && options.audio) await audioSource.add(options.audio);\n\n source.close();\n audioSource?.close();\n await output.finalize();\n\n // A streamed result has already gone to the sink; there is nothing to hand\n // back, and materialising it here would undo the point of streaming.\n if (options.stream) return new Uint8Array(0);\n\n const buffer = (target as BufferTarget).buffer;\n if (!buffer) throw new Error(\"concatVideoSegments produced no data\");\n return new Uint8Array(buffer);\n}\n"]}
@@ -0,0 +1,137 @@
1
+ import { type PixelSize } from "./levels";
2
+ /**
3
+ * Which level a draw is served, and when levels are decoded and released.
4
+ *
5
+ * Browser-free: decoding, uploading and fetching are the host's, so every
6
+ * decision here runs under a plain test runner with a fake clock.
7
+ */
8
+ /** Mirrors core's `DecodePolicy`, which its barrel does not export yet. */
9
+ export type DecodePolicy = "exact" | "interactive";
10
+ export declare const IMAGE_DECODE_BUDGET_BYTES: number;
11
+ /** Unselected levels older than this go, whatever the budget — bar a {@link RETAINED_LEVEL_BYTES} largest level. */
12
+ export declare const LEVEL_AGE_OUT_MS = 2000;
13
+ /** A preview drawing a level at twice what it needs for this long decodes the smaller one. */
14
+ export declare const DOWNGRADE_AFTER_MS = 5000;
15
+ export declare const IDLE_TO_FLOOR_MS = 30000;
16
+ export declare const IDLE_DROP_SOURCE_MS = 60000;
17
+ export declare const MAX_CONCURRENT_DECODES = 3;
18
+ /** An interactive decode covers this multiple of the demand, so a growing draw re-decodes per octave, not per rung. */
19
+ export declare const UPGRADE_HEADROOM = 2;
20
+ /**
21
+ * A largest level no bigger than this is neither aged out nor downgraded until
22
+ * the budget needs the room: decoding it again the next time a draw grows costs
23
+ * more, in main-thread copies and a blurry frame, than holding it.
24
+ */
25
+ export declare const RETAINED_LEVEL_BYTES: number;
26
+ export interface ImageLevel<G = unknown> {
27
+ /** The ladder rung this level was decoded for. */
28
+ readonly edge: number;
29
+ readonly width: number;
30
+ readonly height: number;
31
+ readonly pixels: Uint8Array;
32
+ /** The host's GPU copy, counted against the budget alongside the pixels. */
33
+ gpu: G | null;
34
+ lastSelected: number;
35
+ /** The pass that last selected it — "unselected" is judged by pass, since several can share a clock tick. */
36
+ selectedPass: number;
37
+ }
38
+ export interface DecodedImage {
39
+ pixels: Uint8Array;
40
+ width: number;
41
+ height: number;
42
+ intrinsic: PixelSize;
43
+ vector: boolean;
44
+ }
45
+ export interface ImageLevelHost<G> {
46
+ now(): number;
47
+ /**
48
+ * Decode `src` at the rung `chooseEdge` picks once the source's intrinsic
49
+ * size is known — the host may have to fetch or even decode the source to
50
+ * learn it, which is why the choice is a callback rather than an argument.
51
+ */
52
+ decode(src: string, chooseEdge: (intrinsic: PixelSize, vector: boolean) => number): Promise<DecodedImage & {
53
+ edge: number;
54
+ }>;
55
+ /** A level left the cache. Free what was built from it. */
56
+ released(src: string, level: ImageLevel<G>): void;
57
+ /** Kept compressed bytes for `src`, so an upgrade needs no refetch; 0 when none. */
58
+ sourceBytes(src: string): number;
59
+ dropSource(src: string): void;
60
+ /** Long edge of the surface in device px, for sizing a first interactive load. */
61
+ surfaceEdge(): number;
62
+ onError?(src: string, error: unknown): void;
63
+ }
64
+ export declare class ImageLevelCache<G = unknown> {
65
+ private readonly _host;
66
+ private readonly _budget;
67
+ private _policy;
68
+ private readonly _entries;
69
+ private _pass;
70
+ private _inflight;
71
+ constructor(_host: ImageLevelHost<G>, _budget?: number);
72
+ get policy(): DecodePolicy;
73
+ setPolicy(policy: DecodePolicy): void;
74
+ has(src: string): boolean;
75
+ intrinsicSize(src: string): PixelSize | null;
76
+ /** Make sure `src` has a level to draw with. First loads pick their rung by policy. */
77
+ load(src: string): Promise<void>;
78
+ beginPass(): void;
79
+ /**
80
+ * The level one draw paints with. `footprint` is the long edge in device px
81
+ * the draw needs; absent means the source's own resolution.
82
+ */
83
+ select(src: string, footprint?: number): ImageLevel<G> | null;
84
+ /** Decide this pass's level changes, now that every draw has reported. */
85
+ endPass(): void;
86
+ /**
87
+ * What a settling caller must wait for before the frame is right: under
88
+ * `exact`, every decode queued or running; under `interactive`, only a source
89
+ * the last pass asked for with nothing resident. An upgrade is {@link hasUpgrades}.
90
+ */
91
+ hasPending(): boolean;
92
+ /** Await what {@link hasPending} reports, and report whether there was any. */
93
+ warm(): Promise<boolean>;
94
+ /** Whether, under `interactive`, a source the last pass drew below demand has a better level on its way. */
95
+ hasUpgrades(): boolean;
96
+ /** Await the upgrades {@link hasUpgrades} reports; whether one landed. */
97
+ warmUpgrades(): Promise<boolean>;
98
+ stats(): {
99
+ sources: number;
100
+ levels: number;
101
+ levelBytes: number;
102
+ sourceBytes: number;
103
+ inflight: number;
104
+ };
105
+ /** Every level held for `src`, ascending — for tests and stats. */
106
+ levelsOf(src: string): readonly ImageLevel<G>[];
107
+ dispose(): void;
108
+ private _entry;
109
+ private _blocks;
110
+ private _upgrading;
111
+ /** The rung an interactive decode for `demand` asks for: with headroom, capped at the source. */
112
+ private _interactiveEdge;
113
+ private _learn;
114
+ private _add;
115
+ /**
116
+ * Queue a decode of `edge`. An interactive request replaces what is queued,
117
+ * since only the latest demand matters; exact requests accumulate, since a
118
+ * frame may need several rungs of one source.
119
+ */
120
+ private _request;
121
+ private _pump;
122
+ /**
123
+ * The next queued rung worth decoding. Under `interactive` never one below the
124
+ * latest demand, and no upgrade once what landed meanwhile already covers it.
125
+ */
126
+ private _nextEdge;
127
+ private _ageOut;
128
+ private _total;
129
+ /**
130
+ * Cheapest loss first: levels nothing drew this pass, then levels a preview
131
+ * holds bigger than it needs, then sources nobody has looked at in a while,
132
+ * then their compressed bytes. A source's last level is never evicted — a
133
+ * draw finding nothing would throw rather than degrade.
134
+ */
135
+ private _enforceBudget;
136
+ }
137
+ //# sourceMappingURL=image-levels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-levels.d.ts","sourceRoot":"","sources":["../../src/decode/image-levels.ts"],"names":[],"mappings":"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"}