gitframes 1.3.0 → 1.3.1

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 (35) hide show
  1. package/dist/audio/index.mjs +4 -4
  2. package/dist/{audio-C4kiPbrV.mjs → audio-DvL7gQQ8.mjs} +3 -3
  3. package/dist/{audio-C4kiPbrV.mjs.map → audio-DvL7gQQ8.mjs.map} +1 -1
  4. package/dist/{audio-CiXUMGyk.mjs → audio-sZdRh_qg.mjs} +3 -3
  5. package/dist/{audio-CiXUMGyk.mjs.map → audio-sZdRh_qg.mjs.map} +1 -1
  6. package/dist/{dist-C37594c6.mjs → dist-8vbhZYqX.mjs} +471 -152
  7. package/dist/dist-8vbhZYqX.mjs.map +1 -0
  8. package/dist/{dist-CMF9eNLB.mjs → dist-B0vCkDzJ.mjs} +37 -3
  9. package/dist/dist-B0vCkDzJ.mjs.map +1 -0
  10. package/dist/effects/index.mjs +2 -2
  11. package/dist/{effects-C9connag.mjs → effects-C3nV-k4T.mjs} +4 -4
  12. package/dist/{effects-C9connag.mjs.map → effects-C3nV-k4T.mjs.map} +1 -1
  13. package/dist/fonts/index.mjs +1 -1
  14. package/dist/{fonts-Dn18r_ST.mjs → fonts-dn-pl4Pe.mjs} +3 -3
  15. package/dist/{fonts-Dn18r_ST.mjs.map → fonts-dn-pl4Pe.mjs.map} +1 -1
  16. package/dist/{index-YZsTjqut.d.mts → index-D-O2kwLx.d.mts} +3 -3
  17. package/dist/{index-YZsTjqut.d.mts.map → index-D-O2kwLx.d.mts.map} +1 -1
  18. package/dist/index.d.mts +67 -49
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +96 -52
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/react/index.mjs +2 -2
  23. package/dist/renderer/index.d.mts +2 -2
  24. package/dist/renderer/index.mjs +3 -3
  25. package/dist/{renderer-DgY7XBYE.mjs → renderer-CMDIVxhv.mjs} +315 -12
  26. package/dist/renderer-CMDIVxhv.mjs.map +1 -0
  27. package/dist/{renderers-CLJQRPVU-CMZnHNK0.mjs → renderers-CLJQRPVU-DuQ7-PaC.mjs} +3 -3
  28. package/dist/{renderers-CLJQRPVU-CMZnHNK0.mjs.map → renderers-CLJQRPVU-DuQ7-PaC.mjs.map} +1 -1
  29. package/dist/signals/index.mjs +5 -5
  30. package/dist/{signals-BB_kdndF.mjs → signals-CLaCq092.mjs} +2 -2
  31. package/dist/{signals-BB_kdndF.mjs.map → signals-CLaCq092.mjs.map} +1 -1
  32. package/package.json +52 -52
  33. package/dist/dist-C37594c6.mjs.map +0 -1
  34. package/dist/dist-CMF9eNLB.mjs.map +0 -1
  35. package/dist/renderer-DgY7XBYE.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { i as drawCompositionTree, o as useCompositionState, t as CanvasComposition } from "../dist-CMF9eNLB.mjs";
2
- import { g as RenderProvider, it as useRenderContext } from "../dist-C37594c6.mjs";
1
+ import { i as drawCompositionTree, o as useCompositionState, t as CanvasComposition } from "../dist-B0vCkDzJ.mjs";
2
+ import { at as useRenderContext, g as RenderProvider } from "../dist-8vbhZYqX.mjs";
3
3
 
4
4
  export { CanvasComposition, CanvasComposition as GitframesPlayer, RenderProvider, drawCompositionTree, useCompositionState, useRenderContext };
@@ -1,2 +1,2 @@
1
- import { a as FrameGridOptions, c as computeGridLayout, i as renderSemaphore, l as renderFrameGrid, n as HeadlessWebGPURenderer, o as FrameGridRenderable, r as initHeadlessWebGPU, s as FrameSamplePoint, t as HeadlessMediaRenderer, u as resolveSamplePoints } from "../index-YZsTjqut.mjs";
2
- export { FrameGridOptions, FrameGridRenderable, FrameSamplePoint, HeadlessMediaRenderer, HeadlessWebGPURenderer, computeGridLayout, initHeadlessWebGPU, renderFrameGrid, renderSemaphore, resolveSamplePoints };
1
+ import { _ as resolveSamplePoints, a as QaSegment, c as VideoQaStats, d as renderSemaphore, f as FrameGridOptions, g as renderFrameGrid, h as computeGridLayout, i as QaIssue, l as formatQaReport, m as FrameSamplePoint, n as HeadlessMediaRenderer, o as VideoQaOptions, p as FrameGridRenderable, r as HeadlessWebGPURenderer, s as VideoQaReport, t as AudioQaStats, u as initHeadlessWebGPU } from "../index-D-O2kwLx.mjs";
2
+ export { AudioQaStats, FrameGridOptions, FrameGridRenderable, FrameSamplePoint, HeadlessMediaRenderer, HeadlessWebGPURenderer, QaIssue, QaSegment, VideoQaOptions, VideoQaReport, VideoQaStats, computeGridLayout, formatQaReport, initHeadlessWebGPU, renderFrameGrid, renderSemaphore, resolveSamplePoints };
@@ -1,6 +1,6 @@
1
1
  import "../chunk-GMkBE123.mjs";
2
- import { U as initHeadlessWebGPU } from "../dist-C37594c6.mjs";
3
- import { a as HeadlessWebGPURenderer, i as HeadlessMediaRenderer, n as renderFrameGrid, o as renderSemaphore, r as resolveSamplePoints, t as computeGridLayout } from "../renderer-DgY7XBYE.mjs";
2
+ import { W as initHeadlessWebGPU } from "../dist-8vbhZYqX.mjs";
3
+ import { a as HeadlessWebGPURenderer, i as HeadlessMediaRenderer, n as renderFrameGrid, o as formatQaReport, r as resolveSamplePoints, s as renderSemaphore, t as computeGridLayout } from "../renderer-CMDIVxhv.mjs";
4
4
  import "../dist-cjs-Bsz2eGzl.mjs";
5
5
 
6
- export { HeadlessMediaRenderer, HeadlessWebGPURenderer, computeGridLayout, initHeadlessWebGPU, renderFrameGrid, renderSemaphore, resolveSamplePoints };
6
+ export { HeadlessMediaRenderer, HeadlessWebGPURenderer, computeGridLayout, formatQaReport, initHeadlessWebGPU, renderFrameGrid, renderSemaphore, resolveSamplePoints };
@@ -1,6 +1,6 @@
1
1
  import { a as __toCommonJS, i as __require, o as __toESM, t as __commonJSMin } from "./chunk-GMkBE123.mjs";
2
- import { C as preloadFont, I as registerWebGPURenderer, P as audioRegistry, Sn as updateClockSignals, a as mixAudioTracks, i as drawCompositionTree, n as compositionStateStore, on as getMediaType } from "./dist-CMF9eNLB.mjs";
3
- import { H as getRenderer2D, J as lutStore, S as SlugFontCache, T as WebGPUAudioProcessor, U as initHeadlessWebGPU, V as getHeadlessFontPath, W as inputStore, Y as mediaDecoderCache, at as GetFontAssetUrl, m as NodeSurfaceProvider, ot as R2_CUSTOM_DOMAIN, rt as textureCache, tt as shaderStore, z as ensureDevice } from "./dist-C37594c6.mjs";
2
+ import { C as preloadFont, I as registerWebGPURenderer, P as audioRegistry, Sn as takeRenderDiagnostics, a as mixAudioTracks, i as drawCompositionTree, n as compositionStateStore, on as getMediaType, wn as updateClockSignals } from "./dist-B0vCkDzJ.mjs";
3
+ import { B as ensureDevice, G as inputStore, H as getHeadlessFontPath, O as clearAllVideoCache, S as SlugFontCache, T as WebGPUAudioProcessor, U as getRenderer2D, W as initHeadlessWebGPU, X as mediaDecoderCache, Y as lutStore, it as textureCache, m as NodeSurfaceProvider, nt as shaderStore, ot as GetFontAssetUrl, st as R2_CUSTOM_DOMAIN } from "./dist-8vbhZYqX.mjs";
4
4
  import { t as require_dist_cjs$20 } from "./dist-cjs-CslF8Dju.mjs";
5
5
  import { D as require_dist_cjs$21, O as dist_es_exports$1, _ as require_dist_cjs$33, a as require_dist_cjs$26, b as init_protocols, c as require_dist_cjs$37, d as require_dist_cjs$32, f as require_dist_cjs$23, g as require_dist_cjs$34, h as require_dist_cjs$35, i as require_dist_cjs$28, k as init_dist_es$1, l as require_dist_cjs$27, m as require_dist_cjs$22, n as require_dist_cjs$31, o as require_dist_cjs$29, p as require_dist_cjs$24, r as require_dist_cjs$30, s as require_dist_cjs$25, t as require_dist_cjs$38, u as require_dist_cjs$36, v as dist_es_exports, x as protocols_exports, y as init_dist_es } from "./dist-cjs-Bsz2eGzl.mjs";
6
6
  import { F as require_dist_cjs$42, I as require_dist_cjs$44, P as require_dist_cjs$39, R as require_dist_cjs$43, T as schema_exports, t as require_dist_cjs$41, v as init_tslib_es6, w as init_schema, y as tslib_es6_exports, z as require_dist_cjs$40 } from "./dist-cjs-DssYqayv.mjs";
@@ -21747,7 +21747,7 @@ S3StorageService = __decorate([
21747
21747
  ], S3StorageService);
21748
21748
 
21749
21749
  //#endregion
21750
- //#region ../renderer/dist/bootstrap-mediabunny-DrIDlSKH.mjs
21750
+ //#region ../renderer/dist/bootstrap-mediabunny-CMP9YRF6.mjs
21751
21751
  /**
21752
21752
  * Recursively scans and preloads only the required font assets inside a VirtualMediaData tree.
21753
21753
  * This ensures typography renders correctly on Node.js without pre-loading or memory bloating from large media assets.
@@ -21919,7 +21919,7 @@ var DmaStagingRing = class {
21919
21919
  * bypassing redundant intermediate JS allocations when row pitches align,
21920
21920
  * and immediately unmaps the buffer.
21921
21921
  */
21922
- async readFrameToVideoFrame(frameIndex, timestampUs, colorSpace) {
21922
+ async readFrameToVideoFrame(frameIndex, timestampUs, colorSpace, inspect) {
21923
21923
  this.checkDestroyed();
21924
21924
  const slot = this.slots[frameIndex % this.capacity];
21925
21925
  if (!slot) throw new Error(`Invalid slot index for frame ${frameIndex}`);
@@ -21937,6 +21937,7 @@ var DmaStagingRing = class {
21937
21937
  pixelData.set(sourceView.subarray(srcOffset, srcOffset + this.unpaddedBytesPerRow), dstOffset);
21938
21938
  }
21939
21939
  }
21940
+ inspect?.(pixelData);
21940
21941
  const vf = new VideoFrame(pixelData, {
21941
21942
  format: "RGBA",
21942
21943
  codedWidth: this.width,
@@ -22180,7 +22181,290 @@ const bootstrapMediabunny = () => {
22180
22181
  };
22181
22182
 
22182
22183
  //#endregion
22183
- //#region ../renderer/dist/headless-webgpu-renderer-DBVU0l8X.mjs
22184
+ //#region ../renderer/dist/headless-webgpu-renderer-C7euEl4e.mjs
22185
+ const DEFAULTS = {
22186
+ blackPixelThreshold: .1,
22187
+ blackFrameRatio: .98,
22188
+ minBlackSec: .5,
22189
+ minFrozenSec: 2,
22190
+ silenceDbfs: -60,
22191
+ minSilenceSec: 2,
22192
+ lufsTolerance: 1
22193
+ };
22194
+ const SAMPLE_STEP = 4;
22195
+ const FROZEN_DIFF = .25;
22196
+ /** Measures frames handed to it in order, as RGBA bytes. */
22197
+ var FrameInspector = class {
22198
+ blackLuma;
22199
+ blackRatio;
22200
+ black = [];
22201
+ sameAsPrevious = [];
22202
+ lumaSum = 0;
22203
+ previous = null;
22204
+ constructor(options = {}) {
22205
+ this.blackLuma = (options.blackPixelThreshold ?? DEFAULTS.blackPixelThreshold) * 255;
22206
+ this.blackRatio = options.blackFrameRatio ?? DEFAULTS.blackFrameRatio;
22207
+ }
22208
+ get frameCount() {
22209
+ return this.black.length;
22210
+ }
22211
+ inspect(rgba, width, height) {
22212
+ const cols = Math.ceil(width / SAMPLE_STEP);
22213
+ const rows = Math.ceil(height / SAMPLE_STEP);
22214
+ const luma = new Uint8Array(cols * rows);
22215
+ let blackCount = 0;
22216
+ let sum = 0;
22217
+ let k$2 = 0;
22218
+ for (let y$1 = 0; y$1 < height; y$1 += SAMPLE_STEP) {
22219
+ let p$1 = y$1 * width * 4;
22220
+ for (let x$2 = 0; x$2 < width; x$2 += SAMPLE_STEP, p$1 += SAMPLE_STEP * 4) {
22221
+ const l$1 = .2126 * rgba[p$1] + .7152 * rgba[p$1 + 1] + .0722 * rgba[p$1 + 2];
22222
+ luma[k$2++] = l$1;
22223
+ sum += l$1;
22224
+ if (l$1 <= this.blackLuma) blackCount++;
22225
+ }
22226
+ }
22227
+ const n$2 = luma.length;
22228
+ this.lumaSum += sum / n$2 / 255;
22229
+ this.black.push(blackCount / n$2 >= this.blackRatio);
22230
+ let same = false;
22231
+ if (this.previous && this.previous.length === n$2) {
22232
+ let diff = 0;
22233
+ for (let i$1 = 0; i$1 < n$2; i$1++) diff += Math.abs(luma[i$1] - this.previous[i$1]);
22234
+ same = diff / n$2 < FROZEN_DIFF;
22235
+ }
22236
+ this.sameAsPrevious.push(same);
22237
+ this.previous = luma;
22238
+ }
22239
+ stats(expectedFrames, encodedFrames, fps, width, height, options = {}) {
22240
+ const minBlack = Math.max(1, Math.round((options.minBlackSec ?? DEFAULTS.minBlackSec) * fps));
22241
+ const minFrozen = Math.max(2, Math.round((options.minFrozenSec ?? DEFAULTS.minFrozenSec) * fps));
22242
+ const blackSegments = runs(this.black, minBlack).map(([a$2, b$1]) => ({
22243
+ startSec: a$2 / fps,
22244
+ endSec: b$1 / fps
22245
+ }));
22246
+ const frozenSegments = runs(this.sameAsPrevious, minFrozen - 1).map(([a$2, b$1]) => ({
22247
+ startSec: (a$2 - 1) / fps,
22248
+ endSec: b$1 / fps
22249
+ }));
22250
+ return {
22251
+ expectedFrames,
22252
+ encodedFrames,
22253
+ fps,
22254
+ width,
22255
+ height,
22256
+ meanLuma: this.frameCount > 0 ? this.lumaSum / this.frameCount : 0,
22257
+ blackSegments,
22258
+ frozenSegments
22259
+ };
22260
+ }
22261
+ };
22262
+ /** [start, end) index ranges where flags hold for at least minLength in a row. */
22263
+ function runs(flags, minLength) {
22264
+ const out = [];
22265
+ let start = -1;
22266
+ for (let i$1 = 0; i$1 <= flags.length; i$1++) if (i$1 < flags.length && flags[i$1]) {
22267
+ if (start < 0) start = i$1;
22268
+ } else if (start >= 0) {
22269
+ if (i$1 - start >= minLength) out.push([start, i$1]);
22270
+ start = -1;
22271
+ }
22272
+ return out;
22273
+ }
22274
+ const toDb = (power) => power > 0 ? 10 * Math.log10(power) : -Infinity;
22275
+ /** Measures a mixed audio track (one Float32Array per channel). */
22276
+ function analyzeAudio(channels, sampleRate, options = {}) {
22277
+ const length = channels[0]?.length ?? 0;
22278
+ let peak = 0;
22279
+ let sumSquares = 0;
22280
+ let clipped = 0;
22281
+ for (const ch of channels) for (let i$1 = 0; i$1 < length; i$1++) {
22282
+ const v$1 = Math.abs(ch[i$1]);
22283
+ if (v$1 > peak) peak = v$1;
22284
+ if (v$1 >= .9999) clipped++;
22285
+ sumSquares += v$1 * v$1;
22286
+ }
22287
+ const total = length * Math.max(1, channels.length);
22288
+ const silenceDbfs = options.silenceDbfs ?? DEFAULTS.silenceDbfs;
22289
+ const window = Math.max(1, Math.round(sampleRate * .1));
22290
+ const silent = [];
22291
+ for (let start = 0; start < length; start += window) {
22292
+ const end = Math.min(length, start + window);
22293
+ let s = 0;
22294
+ for (const ch of channels) for (let i$1 = start; i$1 < end; i$1++) s += ch[i$1] * ch[i$1];
22295
+ silent.push(toDb(s / ((end - start) * channels.length)) < silenceDbfs);
22296
+ }
22297
+ const silentSegments = runs(silent, Math.max(1, Math.round((options.minSilenceSec ?? DEFAULTS.minSilenceSec) / .1))).map(([a$2, b$1]) => ({
22298
+ startSec: a$2 * window / sampleRate,
22299
+ endSec: Math.min(length, b$1 * window) / sampleRate
22300
+ }));
22301
+ const peakDbfs = 20 * Math.log10(Math.max(peak, 1e-12));
22302
+ return {
22303
+ sampleRate,
22304
+ channels: channels.length,
22305
+ durationSec: length / sampleRate,
22306
+ peakDbfs,
22307
+ rmsDbfs: toDb(sumSquares / Math.max(1, total)),
22308
+ integratedLufs: peakDbfs < silenceDbfs ? null : integratedLoudness(channels, sampleRate),
22309
+ clippedSamples: clipped,
22310
+ silentSegments
22311
+ };
22312
+ }
22313
+ /** BS.1770 K-weighting stage 1: +4 dB high shelf around 1.5 kHz (the head). */
22314
+ function highShelf(sampleRate) {
22315
+ const A$2 = 10 ** (4 / 40);
22316
+ const w0 = 2 * Math.PI * 1500 / sampleRate;
22317
+ const cos = Math.cos(w0);
22318
+ const alpha = Math.sin(w0) / (2 / Math.SQRT2);
22319
+ const sq = 2 * Math.sqrt(A$2) * alpha;
22320
+ const a0 = A$2 + 1 - (A$2 - 1) * cos + sq;
22321
+ return {
22322
+ b0: A$2 * (A$2 + 1 + (A$2 - 1) * cos + sq) / a0,
22323
+ b1: -2 * A$2 * (A$2 - 1 + (A$2 + 1) * cos) / a0,
22324
+ b2: A$2 * (A$2 + 1 + (A$2 - 1) * cos - sq) / a0,
22325
+ a1: 2 * (A$2 - 1 - (A$2 + 1) * cos) / a0,
22326
+ a2: (A$2 + 1 - (A$2 - 1) * cos - sq) / a0
22327
+ };
22328
+ }
22329
+ /** BS.1770 K-weighting stage 2: 38 Hz high pass, Q 0.5 (the RLB curve). */
22330
+ function highPass(sampleRate) {
22331
+ const w0 = 2 * Math.PI * 38 / sampleRate;
22332
+ const cos = Math.cos(w0);
22333
+ const alpha = Math.sin(w0) / (2 * .5);
22334
+ const a0 = 1 + alpha;
22335
+ return {
22336
+ b0: (1 + cos) / 2 / a0,
22337
+ b1: -(1 + cos) / a0,
22338
+ b2: (1 + cos) / 2 / a0,
22339
+ a1: -2 * cos / a0,
22340
+ a2: (1 - alpha) / a0
22341
+ };
22342
+ }
22343
+ function filter(input, f$1) {
22344
+ const out = new Float32Array(input.length);
22345
+ let x1 = 0;
22346
+ let x2 = 0;
22347
+ let y1 = 0;
22348
+ let y2 = 0;
22349
+ for (let i$1 = 0; i$1 < input.length; i$1++) {
22350
+ const x$2 = input[i$1];
22351
+ const y$1 = f$1.b0 * x$2 + f$1.b1 * x1 + f$1.b2 * x2 - f$1.a1 * y1 - f$1.a2 * y2;
22352
+ out[i$1] = y$1;
22353
+ x2 = x1;
22354
+ x1 = x$2;
22355
+ y2 = y1;
22356
+ y1 = y$1;
22357
+ }
22358
+ return out;
22359
+ }
22360
+ /**
22361
+ * ITU-R BS.1770-4 integrated loudness (LUFS): K-weighted mean square in
22362
+ * 400 ms blocks with 75% overlap, gated at -70 LUFS and then at 10 LU below
22363
+ * the mean of what passed. Channels weigh 1 (stereo and mono layouts).
22364
+ */
22365
+ function integratedLoudness(channels, sampleRate) {
22366
+ const shelf = highShelf(sampleRate);
22367
+ const hp = highPass(sampleRate);
22368
+ const weighted = channels.map((ch) => filter(filter(ch, shelf), hp));
22369
+ const length = weighted[0]?.length ?? 0;
22370
+ const block = Math.round(.4 * sampleRate);
22371
+ const hop = Math.round(.1 * sampleRate);
22372
+ if (length < block) return null;
22373
+ const blockPower = [];
22374
+ for (let start = 0; start + block <= length; start += hop) {
22375
+ let power = 0;
22376
+ for (const ch of weighted) {
22377
+ let s = 0;
22378
+ for (let i$1 = start; i$1 < start + block; i$1++) s += ch[i$1] * ch[i$1];
22379
+ power += s / block;
22380
+ }
22381
+ blockPower.push(power);
22382
+ }
22383
+ const lufs = (power) => -.691 + toDb(power);
22384
+ const mean = (xs) => xs.reduce((a$2, b$1) => a$2 + b$1, 0) / xs.length;
22385
+ const absolute = blockPower.filter((p$1) => lufs(p$1) > -70);
22386
+ if (absolute.length === 0) return null;
22387
+ const relativeGate = lufs(mean(absolute)) - 10;
22388
+ const gated = absolute.filter((p$1) => lufs(p$1) > relativeGate);
22389
+ return gated.length > 0 ? lufs(mean(gated)) : null;
22390
+ }
22391
+ /** Turn measurements and document diagnostics into a report with issues. */
22392
+ function buildQaReport(parts) {
22393
+ const { video, audio, options = {} } = parts;
22394
+ const issues = [];
22395
+ const at$1 = (s) => `${s.startSec.toFixed(2)}–${s.endSec.toFixed(2)} s`;
22396
+ if (video) {
22397
+ if (video.encodedFrames !== video.expectedFrames) issues.push({
22398
+ severity: "error",
22399
+ code: "frames_missing",
22400
+ message: `${video.encodedFrames} of ${video.expectedFrames} frames were encoded`
22401
+ });
22402
+ for (const s of video.blackSegments) issues.push({
22403
+ severity: "warning",
22404
+ code: "black_frames",
22405
+ message: `Black frames at ${at$1(s)}`,
22406
+ ...s
22407
+ });
22408
+ for (const s of video.frozenSegments) issues.push({
22409
+ severity: "info",
22410
+ code: "frozen_video",
22411
+ message: `Picture does not change at ${at$1(s)}`,
22412
+ ...s
22413
+ });
22414
+ }
22415
+ if (audio) if (audio.integratedLufs === null) issues.push({
22416
+ severity: "info",
22417
+ code: "audio_silent",
22418
+ message: "The audio track is silent"
22419
+ });
22420
+ else {
22421
+ if (audio.clippedSamples > 0) issues.push({
22422
+ severity: "warning",
22423
+ code: "audio_clipping",
22424
+ message: `${audio.clippedSamples} audio sample(s) at or beyond full scale (peak ${audio.peakDbfs.toFixed(1)} dBFS)`
22425
+ });
22426
+ for (const s of audio.silentSegments) issues.push({
22427
+ severity: "info",
22428
+ code: "audio_silence",
22429
+ message: `Silence at ${at$1(s)}`,
22430
+ ...s
22431
+ });
22432
+ if (options.targetLufs !== void 0) {
22433
+ const off = audio.integratedLufs - options.targetLufs;
22434
+ const tolerance = options.lufsTolerance ?? DEFAULTS.lufsTolerance;
22435
+ if (Math.abs(off) > tolerance) issues.push({
22436
+ severity: "warning",
22437
+ code: "loudness_off_target",
22438
+ message: `Integrated loudness ${audio.integratedLufs.toFixed(1)} LUFS is ${Math.abs(off).toFixed(1)} LU ${off > 0 ? "above" : "below"} the ${options.targetLufs} LUFS target`
22439
+ });
22440
+ }
22441
+ }
22442
+ for (const d$1 of parts.diagnostics ?? []) issues.push({
22443
+ severity: d$1.severity,
22444
+ code: d$1.code,
22445
+ message: d$1.message,
22446
+ ...d$1.layerId !== void 0 && { layerId: d$1.layerId }
22447
+ });
22448
+ return {
22449
+ passed: !issues.some((i$1) => i$1.severity !== "info"),
22450
+ ...video && { video },
22451
+ ...audio && { audio },
22452
+ issues
22453
+ };
22454
+ }
22455
+ /** A short human- and agent-readable summary of a report. */
22456
+ function formatQaReport(report) {
22457
+ const lines = [`QA ${report.passed ? "passed" : "FAILED"}`];
22458
+ const v$1 = report.video;
22459
+ if (v$1) lines.push(` video: ${v$1.encodedFrames}/${v$1.expectedFrames} frames, ${v$1.width}x${v$1.height} @ ${v$1.fps} fps, mean luma ${v$1.meanLuma.toFixed(2)}`);
22460
+ const a$2 = report.audio;
22461
+ if (a$2) {
22462
+ const lufs = a$2.integratedLufs === null ? "silent" : `${a$2.integratedLufs.toFixed(1)} LUFS`;
22463
+ lines.push(` audio: ${a$2.durationSec.toFixed(2)} s, ${lufs}, peak ${a$2.peakDbfs.toFixed(1)} dBFS, rms ${a$2.rmsDbfs.toFixed(1)} dBFS`);
22464
+ }
22465
+ for (const i$1 of report.issues) lines.push(` ${i$1.severity.padEnd(7)} ${i$1.code}: ${i$1.message}`);
22466
+ return lines.join("\n");
22467
+ }
22184
22468
  /**
22185
22469
  * High-performance hardware video encoding pipeline integrating WebGPU textures with WebCodecs.
22186
22470
  *
@@ -22199,6 +22483,7 @@ var ZeroCopyWebCodecsPipeline = class {
22199
22483
  stagingRing;
22200
22484
  colorSpace;
22201
22485
  directCanvasSource;
22486
+ onFrame;
22202
22487
  inFlightFrames = [];
22203
22488
  cycleStartTimes = /* @__PURE__ */ new Map();
22204
22489
  totalFramesEnqueued = 0;
@@ -22218,6 +22503,7 @@ var ZeroCopyWebCodecsPipeline = class {
22218
22503
  this.ringCapacity = Math.max(2, options.ringCapacity ?? 2);
22219
22504
  this.colorSpace = options.colorSpace;
22220
22505
  this.directCanvasSource = options.directCanvasSource;
22506
+ this.onFrame = options.onFrame;
22221
22507
  this.stagingRing = new DmaStagingRing(device, {
22222
22508
  width: this.width,
22223
22509
  height: this.height,
@@ -22317,7 +22603,8 @@ var ZeroCopyWebCodecsPipeline = class {
22317
22603
  sample.close();
22318
22604
  }
22319
22605
  } else {
22320
- const vf = await this.stagingRing.readFrameToVideoFrame(frameIndex, timestampUs, this.colorSpace);
22606
+ const onFrame = this.onFrame;
22607
+ const vf = await this.stagingRing.readFrameToVideoFrame(frameIndex, timestampUs, this.colorSpace, onFrame && ((rgba) => onFrame(rgba, frameIndex)));
22321
22608
  try {
22322
22609
  const sample = new VideoSample(vf, {
22323
22610
  duration: 1 / this.fps,
@@ -22600,7 +22887,7 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
22600
22887
  renderer,
22601
22888
  surface
22602
22889
  };
22603
- updateClockSignals(frame, fps, virtualMedia.metadata.durationMs);
22890
+ updateClockSignals(frame, fps, virtualMedia.metadata.durationMs ?? void 0);
22604
22891
  if (frame === 0 || lutStore.hasPending(device) || SlugFontCache.hasPending()) {
22605
22892
  const preEncoder = device.createCommandEncoder();
22606
22893
  const dummyTex = renderer.getTemporaryTexture(width, height);
@@ -22838,6 +23125,10 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
22838
23125
  }
22839
23126
  const totalFrames = Math.max(1, Math.round(durationMs / 1e3 * fps));
22840
23127
  const renderId = `vid-${randomUUID()}`;
23128
+ const qaOptions = options?.qa === true ? {} : options?.qa ? options.qa : void 0;
23129
+ const inspector = qaOptions ? new FrameInspector(qaOptions) : void 0;
23130
+ let videoQa;
23131
+ let audioQa;
22841
23132
  const videoCodec = codecOption === "vp9" ? "vp9" : codecOption === "vp8" ? "vp8" : "avc";
22842
23133
  const audioCodec = options?.audioCodec ?? (isMp3 ? "mp3" : isWebM ? "opus" : "aac");
22843
23134
  const quality = resolveQuality(options?.quality);
@@ -22940,7 +23231,8 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
22940
23231
  height,
22941
23232
  fps,
22942
23233
  videoSource,
22943
- ringCapacity: 2
23234
+ ringCapacity: 2,
23235
+ onFrame: inspector ? (rgba) => inspector.inspect(rgba, width, height) : void 0
22944
23236
  });
22945
23237
  const ctx = surface && renderer ? {
22946
23238
  device,
@@ -23004,6 +23296,7 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
23004
23296
  if (pipeline) {
23005
23297
  await pipeline.drain();
23006
23298
  const pipelineStats = pipeline.getStats();
23299
+ if (inspector && qaOptions) videoQa = inspector.stats(totalFrames, pipelineStats.totalFramesEncoded, fps, width, height, qaOptions);
23007
23300
  rendererLogger.debug({
23008
23301
  renderId,
23009
23302
  totalFrames,
@@ -23014,6 +23307,7 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
23014
23307
  const { channels, sampleRate } = await mixAudioTracks(virtualMedia, fps, 48e3, device, renderId);
23015
23308
  const numChannels = channels.length;
23016
23309
  if (numChannels === 0) throw new Error("mixAudioTracks returned no channels");
23310
+ if (qaOptions) audioQa = analyzeAudio(channels, sampleRate, qaOptions);
23017
23311
  const totalSamples = channels[0].length;
23018
23312
  const chunkSize = Math.round(.1 * sampleRate);
23019
23313
  for (let offset = 0; offset < totalSamples; offset += chunkSize) {
@@ -23035,6 +23329,12 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
23035
23329
  }
23036
23330
  await output.finalize();
23037
23331
  outputFinalized = true;
23332
+ const qa = qaOptions ? buildQaReport({
23333
+ video: videoQa,
23334
+ audio: audioQa,
23335
+ diagnostics: takeRenderDiagnostics(renderId),
23336
+ options: qaOptions
23337
+ }) : void 0;
23038
23338
  const cleanup = async () => {
23039
23339
  try {
23040
23340
  await fs$1.unlink(tempFilePath).catch(() => {});
@@ -23046,12 +23346,14 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
23046
23346
  await convertMp4ToGif(tempFilePath, tempGifPath);
23047
23347
  return {
23048
23348
  filePath: tempGifPath,
23049
- cleanup
23349
+ cleanup,
23350
+ ...qa && { qa }
23050
23351
  };
23051
23352
  }
23052
23353
  return {
23053
23354
  filePath: tempFilePath,
23054
- cleanup
23355
+ cleanup,
23356
+ ...qa && { qa }
23055
23357
  };
23056
23358
  } catch (error) {
23057
23359
  if (outputStarted && !outputFinalized) try {
@@ -23088,6 +23390,7 @@ var HeadlessWebGPURenderer = class HeadlessWebGPURenderer$1 {
23088
23390
  try {
23089
23391
  shaderStore.clear(renderId);
23090
23392
  } catch {}
23393
+ takeRenderDiagnostics(renderId);
23091
23394
  renderSemaphore.release();
23092
23395
  rendererLogger.debug({
23093
23396
  renderId,
@@ -23270,5 +23573,5 @@ async function renderFrameGrid(target, options = {}) {
23270
23573
  }
23271
23574
 
23272
23575
  //#endregion
23273
- export { HeadlessWebGPURenderer as a, HeadlessMediaRenderer as i, renderFrameGrid as n, renderSemaphore as o, resolveSamplePoints as r, computeGridLayout as t };
23274
- //# sourceMappingURL=renderer-DgY7XBYE.mjs.map
23576
+ export { HeadlessWebGPURenderer as a, HeadlessMediaRenderer as i, renderFrameGrid as n, formatQaReport as o, resolveSamplePoints as r, renderSemaphore as s, computeGridLayout as t };
23577
+ //# sourceMappingURL=renderer-CMDIVxhv.mjs.map