@memberjunction/video-studio-render-worker 0.0.1-seed.1 → 6.2.0-edge.7
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/LICENSE +183 -0
- package/README.md +139 -4
- package/assets/fonts/Inter-OFL.txt +93 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/JetBrainsMono-OFL.txt +93 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/dist/backend.d.ts +201 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +205 -0
- package/dist/backend.js.map +1 -0
- package/dist/bundle.d.ts +112 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +433 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checks/dom-rules.d.ts +57 -0
- package/dist/checks/dom-rules.d.ts.map +1 -0
- package/dist/checks/dom-rules.js +205 -0
- package/dist/checks/dom-rules.js.map +1 -0
- package/dist/checks/hyperframes-check.d.ts +37 -0
- package/dist/checks/hyperframes-check.d.ts.map +1 -0
- package/dist/checks/hyperframes-check.js +161 -0
- package/dist/checks/hyperframes-check.js.map +1 -0
- package/dist/checks/static-checks.d.ts +46 -0
- package/dist/checks/static-checks.d.ts.map +1 -0
- package/dist/checks/static-checks.js +681 -0
- package/dist/checks/static-checks.js.map +1 -0
- package/dist/hyperframes-backend.d.ts +53 -0
- package/dist/hyperframes-backend.d.ts.map +1 -0
- package/dist/hyperframes-backend.js +733 -0
- package/dist/hyperframes-backend.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +88 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +650 -0
- package/dist/jobs.js.map +1 -0
- package/dist/lint-gate.d.ts +21 -0
- package/dist/lint-gate.d.ts.map +1 -0
- package/dist/lint-gate.js +528 -0
- package/dist/lint-gate.js.map +1 -0
- package/dist/main.d.ts +47 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +195 -0
- package/dist/main.js.map +1 -0
- package/dist/one-shot.d.ts +37 -0
- package/dist/one-shot.d.ts.map +1 -0
- package/dist/one-shot.js +228 -0
- package/dist/one-shot.js.map +1 -0
- package/dist/preamble.d.ts +69 -0
- package/dist/preamble.d.ts.map +1 -0
- package/dist/preamble.js +331 -0
- package/dist/preamble.js.map +1 -0
- package/dist/server.d.ts +75 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +526 -0
- package/dist/server.js.map +1 -0
- package/package.json +43 -5
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as fssync from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { normalizeSystemFontPrimaryFamilies } from '@hyperframes/core/fonts/embed';
|
|
10
|
+
import { captureFrameToBuffer, closeCaptureSession, createCaptureSession, drainBrowserPool, extractMediaMetadata, getFfmpegBinary, getFfprobeBinary, initializeSession, resolveHeadlessShellPath, runFfmpeg, } from '@hyperframes/engine';
|
|
11
|
+
import { lintProject, shouldBlockRender, } from '@hyperframes/lint';
|
|
12
|
+
import { createFileServer, createRenderJob, executeRenderJob, injectDeterministicFontFaces, RenderCancelledError, } from '@hyperframes/producer';
|
|
13
|
+
import { RenderWorkerError, } from './backend.js';
|
|
14
|
+
import { EnsureIndexHtmlForServer } from './bundle.js';
|
|
15
|
+
import { BuildCheckCliFailedIssue, BuildReducedCheckCliEnv, MJ_CHECK_CLI_UNREADABLE_CODE, MapHyperFramesCheckOutput, } from './checks/hyperframes-check.js';
|
|
16
|
+
import { BUNDLED_DEFAULT_FONTS } from './preamble.js';
|
|
17
|
+
export { BuildReducedCheckCliEnv, EnsureIndexHtmlForServer, };
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
20
|
+
/** Timeout in milliseconds for `hyperframes check --json` CLI subprocesses (2 minutes). */
|
|
21
|
+
export const CHECK_CLI_TIMEOUT_MS = 120_000;
|
|
22
|
+
const PACKAGE_ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the installed version of `@hyperframes/<subpackage>` from its `package.json` at runtime.
|
|
25
|
+
* Throws if the package's `package.json` cannot be read or lacks a valid `version` field.
|
|
26
|
+
*/
|
|
27
|
+
export function GetInstalledHyperFramesPackageVersion(subpackage = 'producer') {
|
|
28
|
+
const pkgJsonPath = path.join(PACKAGE_ROOT_DIR, 'node_modules', '@hyperframes', subpackage, 'package.json');
|
|
29
|
+
try {
|
|
30
|
+
const raw = fssync.readFileSync(pkgJsonPath, 'utf8');
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (typeof parsed.version === 'string' && parsed.version.trim().length > 0) {
|
|
33
|
+
return parsed.version.trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw new Error(`Unable to resolve installed version for @hyperframes/${subpackage} from ${pkgJsonPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Invalid or missing version for @hyperframes/${subpackage} in ${pkgJsonPath}`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the installed `@hyperframes/producer` version from its `package.json` at runtime.
|
|
43
|
+
*/
|
|
44
|
+
export function GetInstalledHyperFramesVersion() {
|
|
45
|
+
return GetInstalledHyperFramesPackageVersion('producer');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Production `RenderBackend` backed by `@hyperframes/producer`, `@hyperframes/engine`,
|
|
49
|
+
* `@hyperframes/lint`, headless Chromium, and `ffmpeg` / `ffprobe`.
|
|
50
|
+
*
|
|
51
|
+
* This is the ONLY module in `@memberjunction/video-studio-render-worker` that imports
|
|
52
|
+
* `@hyperframes/*` or spawns `ffmpeg` / `ffprobe`.
|
|
53
|
+
*/
|
|
54
|
+
export class HyperFramesRenderBackend {
|
|
55
|
+
GetRendererVersion() {
|
|
56
|
+
return GetInstalledHyperFramesVersion();
|
|
57
|
+
}
|
|
58
|
+
async Lint(dir, entry) {
|
|
59
|
+
const explicitEntry = entry === 'index.html' ? undefined : path.join(dir, entry);
|
|
60
|
+
const report = await lintProject(dir, explicitEntry, { host: 'cli' });
|
|
61
|
+
const projectFindings = report.results.flatMap((r) => r.result.findings);
|
|
62
|
+
const issues = projectFindings
|
|
63
|
+
.filter((diag) => diag.severity === 'error' || diag.severity === 'warning')
|
|
64
|
+
.map((diag) => mapLintFindingToCheckIssue(diag, dir, entry));
|
|
65
|
+
const errorCount = issues.filter((i) => i.Severity === 'Error').length;
|
|
66
|
+
const warningCount = issues.filter((i) => i.Severity === 'Warning').length;
|
|
67
|
+
const block = shouldBlockRender(true, false, errorCount, warningCount);
|
|
68
|
+
return { Issues: issues, Block: block };
|
|
69
|
+
}
|
|
70
|
+
async RunCheckCli(job, signal) {
|
|
71
|
+
throwIfAborted(signal);
|
|
72
|
+
const browserPath = ensureBrowserBinaryEnv();
|
|
73
|
+
await EnsureIndexHtmlForServer(job.RootDir, job.EntryFile);
|
|
74
|
+
await compileOfflineEntryFonts(job.RootDir);
|
|
75
|
+
const cliBinPath = resolveHyperFramesCliBinPath();
|
|
76
|
+
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mj-video-check-home-'));
|
|
77
|
+
const reducedEnv = BuildReducedCheckCliEnv(process.env, homeDir, browserPath);
|
|
78
|
+
try {
|
|
79
|
+
const { stdout } = await execFileAsync(process.execPath, [cliBinPath, 'check', '--json', '--no-proxy', '--no-browser-gpu', job.RootDir], {
|
|
80
|
+
env: reducedEnv,
|
|
81
|
+
timeout: CHECK_CLI_TIMEOUT_MS,
|
|
82
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
83
|
+
signal,
|
|
84
|
+
});
|
|
85
|
+
return MapHyperFramesCheckOutput(stdout);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throwIfAborted(signal);
|
|
89
|
+
const maybeStdout = extractStdoutFromExecError(error);
|
|
90
|
+
if (maybeStdout) {
|
|
91
|
+
const mapped = MapHyperFramesCheckOutput(maybeStdout);
|
|
92
|
+
if (mapped.length !== 1 || mapped[0]?.Code !== MJ_CHECK_CLI_UNREADABLE_CODE) {
|
|
93
|
+
return mapped;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
97
|
+
return [BuildCheckCliFailedIssue(reason)];
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await fs.rm(homeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async MeasureText(job, timestampsSeconds, signal) {
|
|
104
|
+
throwIfAborted(signal);
|
|
105
|
+
const browserBinary = ensureBrowserBinaryEnv();
|
|
106
|
+
const fpsInt = Math.max(1, Math.round(job.Fps ?? 30));
|
|
107
|
+
await EnsureIndexHtmlForServer(job.RootDir, job.EntryFile);
|
|
108
|
+
await compileOfflineEntryFonts(job.RootDir);
|
|
109
|
+
const server = await createFileServer({
|
|
110
|
+
projectDir: job.RootDir,
|
|
111
|
+
fps: { num: fpsInt, den: 1 },
|
|
112
|
+
stripEmbeddedRuntime: false,
|
|
113
|
+
});
|
|
114
|
+
const sessionDir = await fs.mkdtemp(path.join(job.RootDir, 'measure-session-'));
|
|
115
|
+
const session = await createCaptureSession(server.url, sessionDir, {
|
|
116
|
+
width: job.Width,
|
|
117
|
+
height: job.Height,
|
|
118
|
+
fps: { num: fpsInt, den: 1 },
|
|
119
|
+
format: 'png',
|
|
120
|
+
quality: 90,
|
|
121
|
+
}, null, { chromePath: browserBinary, forceScreenshot: true });
|
|
122
|
+
try {
|
|
123
|
+
await configureBundleOnlyRequestInterception(session, server.url);
|
|
124
|
+
await initializeSession(session);
|
|
125
|
+
await warmDocumentFonts(session);
|
|
126
|
+
const results = [];
|
|
127
|
+
for (const rawTime of timestampsSeconds) {
|
|
128
|
+
throwIfAborted(signal);
|
|
129
|
+
const safeTime = Math.max(0, rawTime);
|
|
130
|
+
const frameIndex = Math.max(0, Math.round(safeTime * fpsInt));
|
|
131
|
+
await captureFrameToBuffer(session, frameIndex, safeTime);
|
|
132
|
+
const script = BuildMeasureTextInPageScript(job.Width, job.Height, safeTime);
|
|
133
|
+
const measured = (await session.page.evaluate(script));
|
|
134
|
+
results.push(Array.isArray(measured) ? measured : []);
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await closeCaptureSession(session).catch(() => undefined);
|
|
140
|
+
await Promise.resolve(server.close()).catch(() => undefined);
|
|
141
|
+
await fs.rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async Render(job, onProgress, signal) {
|
|
145
|
+
throwIfAborted(signal);
|
|
146
|
+
ensureBrowserBinaryEnv();
|
|
147
|
+
const rawOutputPath = path.join(job.RootDir, 'hf-raw-output.mp4');
|
|
148
|
+
const finalOutputPath = path.join(job.RootDir, 'output.mp4');
|
|
149
|
+
const startMs = Date.now();
|
|
150
|
+
const hfJob = createRenderJob({
|
|
151
|
+
entryFile: job.EntryFile,
|
|
152
|
+
fps: job.Fps,
|
|
153
|
+
quality: job.Quality ?? 'standard',
|
|
154
|
+
format: 'mp4',
|
|
155
|
+
workers: 1,
|
|
156
|
+
});
|
|
157
|
+
try {
|
|
158
|
+
await executeRenderJob(hfJob, job.RootDir, rawOutputPath, (updatedJob) => {
|
|
159
|
+
onProgress(mapHyperFramesProgress(updatedJob, startMs));
|
|
160
|
+
}, signal);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof RenderCancelledError || signal?.aborted) {
|
|
164
|
+
throw new RenderWorkerError('Cancelled', 'Render job was cancelled.');
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
throwIfAborted(signal);
|
|
169
|
+
await ensureFaststartMp4(rawOutputPath, finalOutputPath, signal);
|
|
170
|
+
const totalFrames = hfJob.totalFrames && hfJob.totalFrames > 0
|
|
171
|
+
? hfJob.totalFrames
|
|
172
|
+
: Math.max(1, Math.round((job.DurationSeconds ?? 1) * job.Fps));
|
|
173
|
+
return { OutputPath: finalOutputPath, FrameCount: totalFrames };
|
|
174
|
+
}
|
|
175
|
+
async CaptureStills(job, signal) {
|
|
176
|
+
throwIfAborted(signal);
|
|
177
|
+
const browserBinary = ensureBrowserBinaryEnv();
|
|
178
|
+
const fpsInt = Math.max(1, Math.round(job.Fps));
|
|
179
|
+
await EnsureIndexHtmlForServer(job.RootDir, job.EntryFile);
|
|
180
|
+
const server = await createFileServer({
|
|
181
|
+
projectDir: job.RootDir,
|
|
182
|
+
fps: { num: fpsInt, den: 1 },
|
|
183
|
+
stripEmbeddedRuntime: false,
|
|
184
|
+
});
|
|
185
|
+
const sessionDir = await fs.mkdtemp(path.join(job.RootDir, 'stills-session-'));
|
|
186
|
+
const session = await createCaptureSession(server.url, sessionDir, {
|
|
187
|
+
width: job.Width,
|
|
188
|
+
height: job.Height,
|
|
189
|
+
fps: { num: fpsInt, den: 1 },
|
|
190
|
+
format: job.ImageFormat,
|
|
191
|
+
quality: job.JpegQuality ?? 90,
|
|
192
|
+
}, null, { chromePath: browserBinary, forceScreenshot: true });
|
|
193
|
+
try {
|
|
194
|
+
await configureBundleOnlyRequestInterception(session, server.url);
|
|
195
|
+
await initializeSession(session);
|
|
196
|
+
const buffers = [];
|
|
197
|
+
for (const timestamp of job.TimestampsSeconds) {
|
|
198
|
+
throwIfAborted(signal);
|
|
199
|
+
const safeTime = Math.max(0, timestamp);
|
|
200
|
+
const frameIndex = Math.max(0, Math.round(safeTime * fpsInt));
|
|
201
|
+
const captured = await captureFrameToBuffer(session, frameIndex, safeTime);
|
|
202
|
+
buffers.push(new Uint8Array(captured.buffer));
|
|
203
|
+
}
|
|
204
|
+
return buffers;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await closeCaptureSession(session).catch(() => undefined);
|
|
208
|
+
await Promise.resolve(server.close()).catch(() => undefined);
|
|
209
|
+
await fs.rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async Probe(videoPath) {
|
|
213
|
+
const ffprobeBin = await resolveFfprobeBinary();
|
|
214
|
+
try {
|
|
215
|
+
const { stdout } = await execFileAsync(ffprobeBin, [
|
|
216
|
+
'-v',
|
|
217
|
+
'quiet',
|
|
218
|
+
'-print_format',
|
|
219
|
+
'json',
|
|
220
|
+
'-show_format',
|
|
221
|
+
'-show_streams',
|
|
222
|
+
videoPath,
|
|
223
|
+
]);
|
|
224
|
+
return parseFfprobeJson(stdout);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
const meta = await extractMediaMetadata(videoPath);
|
|
228
|
+
return {
|
|
229
|
+
DurationSeconds: meta.durationSeconds ?? 0,
|
|
230
|
+
HasAudio: Boolean(meta.hasAudio),
|
|
231
|
+
Width: meta.width ?? 0,
|
|
232
|
+
Height: meta.height ?? 0,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async ExtractKeyframes(videoPath, outDir, max, signal) {
|
|
237
|
+
throwIfAborted(signal);
|
|
238
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
239
|
+
const probe = await this.Probe(videoPath);
|
|
240
|
+
const duration = Math.max(0.1, probe.DurationSeconds || 1);
|
|
241
|
+
const width = probe.Width || 1280;
|
|
242
|
+
const height = probe.Height || 720;
|
|
243
|
+
const maxFrames = Math.max(1, Math.min(max, 24));
|
|
244
|
+
const sceneTimestamps = await detectSceneChangeTimestamps(videoPath, duration, signal);
|
|
245
|
+
const plan = buildKeyframeTimestampPlan(sceneTimestamps, duration, maxFrames);
|
|
246
|
+
const extracted = [];
|
|
247
|
+
for (let i = 0; i < plan.length; i++) {
|
|
248
|
+
throwIfAborted(signal);
|
|
249
|
+
const item = plan[i];
|
|
250
|
+
const framePath = path.join(outDir, `keyframe-${String(i).padStart(3, '0')}.png`);
|
|
251
|
+
await extractSinglePngFrame(videoPath, item.TimestampSeconds, framePath, signal);
|
|
252
|
+
const bytes = await fs.readFile(framePath);
|
|
253
|
+
extracted.push({
|
|
254
|
+
Index: i,
|
|
255
|
+
TimestampSeconds: Number(item.TimestampSeconds.toFixed(3)),
|
|
256
|
+
Reason: item.Reason,
|
|
257
|
+
MimeType: 'image/png',
|
|
258
|
+
Width: width,
|
|
259
|
+
Height: height,
|
|
260
|
+
ImageBase64: bytes.toString('base64'),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return extracted;
|
|
264
|
+
}
|
|
265
|
+
async Health() {
|
|
266
|
+
const errors = [];
|
|
267
|
+
let hyperFramesVersion = '';
|
|
268
|
+
let chromiumVersion;
|
|
269
|
+
let ffmpegVersion;
|
|
270
|
+
try {
|
|
271
|
+
hyperFramesVersion = GetInstalledHyperFramesVersion();
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
errors.push(`HyperFrames check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const browserBin = resolveBrowserBinary();
|
|
278
|
+
if (!browserBin) {
|
|
279
|
+
errors.push('Chromium headless shell binary was not found. Run `pnpm --filter @memberjunction/video-studio-render-worker run browser:install` or set CHROME_PATH.');
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
chromiumVersion = await probeBinaryFirstLine(browserBin, ['--version']);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
errors.push(`Chromium check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const ffmpegBin = await getFfmpegBinary();
|
|
290
|
+
ffmpegVersion = await probeBinaryFirstLine(ffmpegBin, ['-version']);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
errors.push(`ffmpeg check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
Healthy: errors.length === 0,
|
|
297
|
+
NodeVersion: process.version,
|
|
298
|
+
HyperFramesVersion: hyperFramesVersion,
|
|
299
|
+
ChromiumVersion: chromiumVersion,
|
|
300
|
+
FfmpegVersion: ffmpegVersion,
|
|
301
|
+
Errors: errors,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Drains the shared HyperFrames browser pool on worker shutdown.
|
|
306
|
+
*/
|
|
307
|
+
async Shutdown() {
|
|
308
|
+
await drainBrowserPool().catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function mapLintFindingToCheckIssue(diag, dir, defaultFile) {
|
|
312
|
+
const rawFile = diag.file ?? defaultFile;
|
|
313
|
+
const relativeFile = path.isAbsolute(rawFile)
|
|
314
|
+
? path.relative(dir, rawFile).split(path.sep).join('/')
|
|
315
|
+
: rawFile.split(path.sep).join('/');
|
|
316
|
+
return {
|
|
317
|
+
Code: diag.code,
|
|
318
|
+
Category: mapLintCategory(diag.code),
|
|
319
|
+
Severity: diag.severity === 'error' ? 'Error' : 'Warning',
|
|
320
|
+
Message: diag.message,
|
|
321
|
+
File: relativeFile || defaultFile,
|
|
322
|
+
Line: diag.line ?? undefined,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function mapLintCategory(code) {
|
|
326
|
+
const lower = code.toLowerCase();
|
|
327
|
+
if (lower.includes('url') || lower.includes('network') || lower.includes('remote')) {
|
|
328
|
+
return 'RemoteUrl';
|
|
329
|
+
}
|
|
330
|
+
if (lower.includes('asset') || lower.includes('missing') || lower.includes('file')) {
|
|
331
|
+
return 'MissingAsset';
|
|
332
|
+
}
|
|
333
|
+
if (lower.includes('audio')) {
|
|
334
|
+
return 'Audio';
|
|
335
|
+
}
|
|
336
|
+
return 'Lint';
|
|
337
|
+
}
|
|
338
|
+
function mapHyperFramesProgress(job, startMs) {
|
|
339
|
+
let phase = 'Rendering';
|
|
340
|
+
if (job.status === 'queued' || job.status === 'preprocessing') {
|
|
341
|
+
phase = 'Launching';
|
|
342
|
+
}
|
|
343
|
+
else if (job.status === 'encoding' || job.status === 'assembling') {
|
|
344
|
+
phase = 'Encoding';
|
|
345
|
+
}
|
|
346
|
+
else if (job.status === 'complete') {
|
|
347
|
+
phase = 'Complete';
|
|
348
|
+
}
|
|
349
|
+
const total = Math.max(1, job.totalFrames ?? 1);
|
|
350
|
+
const rendered = Math.min(total, Math.max(0, job.framesRendered ?? 0));
|
|
351
|
+
const percent = Math.min(100, Math.max(0, Math.round(job.progress || (rendered / total) * 100)));
|
|
352
|
+
return {
|
|
353
|
+
Phase: phase,
|
|
354
|
+
RenderedFrames: rendered,
|
|
355
|
+
TotalFrames: total,
|
|
356
|
+
Percent: percent,
|
|
357
|
+
ElapsedMs: Math.max(0, Date.now() - startMs),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async function ensureFaststartMp4(rawPath, finalPath, signal) {
|
|
361
|
+
const res = await runFfmpeg([
|
|
362
|
+
'-y',
|
|
363
|
+
'-i',
|
|
364
|
+
rawPath,
|
|
365
|
+
'-c:v',
|
|
366
|
+
'libx264',
|
|
367
|
+
'-pix_fmt',
|
|
368
|
+
'yuv420p',
|
|
369
|
+
'-c:a',
|
|
370
|
+
'aac',
|
|
371
|
+
'-movflags',
|
|
372
|
+
'+faststart',
|
|
373
|
+
finalPath,
|
|
374
|
+
], { signal });
|
|
375
|
+
if (!res.success) {
|
|
376
|
+
if (signal?.aborted) {
|
|
377
|
+
throw new RenderWorkerError('Cancelled', 'Render job was cancelled.');
|
|
378
|
+
}
|
|
379
|
+
throw res.error ?? new Error(`ffmpeg faststart failed: ${res.stderr}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function parseFfprobeJson(stdout) {
|
|
383
|
+
const parsed = JSON.parse(stdout);
|
|
384
|
+
const streams = parsed.streams ?? [];
|
|
385
|
+
const videoStream = streams.find((s) => s.codec_type === 'video');
|
|
386
|
+
const audioStream = streams.find((s) => s.codec_type === 'audio');
|
|
387
|
+
const formatDuration = Number.parseFloat(parsed.format?.duration ?? '0');
|
|
388
|
+
const streamDuration = Number.parseFloat(videoStream?.duration ?? '0');
|
|
389
|
+
const durationSeconds = Number.isFinite(formatDuration) && formatDuration > 0
|
|
390
|
+
? formatDuration
|
|
391
|
+
: Number.isFinite(streamDuration) && streamDuration > 0
|
|
392
|
+
? streamDuration
|
|
393
|
+
: 0;
|
|
394
|
+
return {
|
|
395
|
+
DurationSeconds: durationSeconds,
|
|
396
|
+
HasAudio: Boolean(audioStream),
|
|
397
|
+
Width: videoStream?.width ?? 0,
|
|
398
|
+
Height: videoStream?.height ?? 0,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function detectSceneChangeTimestamps(videoPath, durationSeconds, signal) {
|
|
402
|
+
try {
|
|
403
|
+
const ffmpegBin = await getFfmpegBinary();
|
|
404
|
+
const { stderr } = await execFileAsync(ffmpegBin, [
|
|
405
|
+
'-hide_banner',
|
|
406
|
+
'-i',
|
|
407
|
+
videoPath,
|
|
408
|
+
'-vf',
|
|
409
|
+
"select='gt(scene,0.3)',showinfo",
|
|
410
|
+
'-f',
|
|
411
|
+
'null',
|
|
412
|
+
'-',
|
|
413
|
+
], { signal });
|
|
414
|
+
const matches = [...stderr.matchAll(/pts_time:\s*([0-9.]+)/g)];
|
|
415
|
+
return matches
|
|
416
|
+
.map((m) => Number.parseFloat(m[1] ?? '0'))
|
|
417
|
+
.filter((t) => Number.isFinite(t) && t >= 0 && t < durationSeconds);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (signal?.aborted) {
|
|
421
|
+
throw new RenderWorkerError('Cancelled', 'Keyframe extraction was cancelled.');
|
|
422
|
+
}
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function buildKeyframeTimestampPlan(sceneTimes, durationSeconds, maxFrames) {
|
|
427
|
+
const chosen = [];
|
|
428
|
+
const minGap = Math.min(0.15, durationSeconds / (maxFrames * 2));
|
|
429
|
+
const addIfDistinct = (t, reason) => {
|
|
430
|
+
const clamped = Math.max(0, Math.min(Math.max(0, durationSeconds - 0.05), t));
|
|
431
|
+
if (chosen.some((existing) => Math.abs(existing.TimestampSeconds - clamped) < minGap)) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
chosen.push({ TimestampSeconds: clamped, Reason: reason });
|
|
435
|
+
};
|
|
436
|
+
for (const sceneTime of sceneTimes) {
|
|
437
|
+
if (chosen.length >= maxFrames) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
addIfDistinct(sceneTime, 'SceneChange');
|
|
441
|
+
}
|
|
442
|
+
const uniformCount = Math.max(1, maxFrames);
|
|
443
|
+
for (let i = 0; i < uniformCount && chosen.length < maxFrames; i++) {
|
|
444
|
+
const fraction = uniformCount === 1 ? 0.5 : (i + 0.5) / uniformCount;
|
|
445
|
+
addIfDistinct(fraction * durationSeconds, 'UniformSample');
|
|
446
|
+
}
|
|
447
|
+
if (chosen.length === 0) {
|
|
448
|
+
chosen.push({ TimestampSeconds: 0, Reason: 'UniformSample' });
|
|
449
|
+
}
|
|
450
|
+
return chosen.sort((a, b) => a.TimestampSeconds - b.TimestampSeconds).slice(0, maxFrames);
|
|
451
|
+
}
|
|
452
|
+
async function extractSinglePngFrame(videoPath, timestampSeconds, outputPath, signal) {
|
|
453
|
+
const res = await runFfmpeg([
|
|
454
|
+
'-y',
|
|
455
|
+
'-ss',
|
|
456
|
+
timestampSeconds.toFixed(3),
|
|
457
|
+
'-i',
|
|
458
|
+
videoPath,
|
|
459
|
+
'-frames:v',
|
|
460
|
+
'1',
|
|
461
|
+
'-f',
|
|
462
|
+
'image2',
|
|
463
|
+
outputPath,
|
|
464
|
+
], { signal });
|
|
465
|
+
if (!res.success) {
|
|
466
|
+
if (signal?.aborted) {
|
|
467
|
+
throw new RenderWorkerError('Cancelled', 'Keyframe extraction was cancelled.');
|
|
468
|
+
}
|
|
469
|
+
throw res.error ?? new Error(`ffmpeg frame extract failed: ${res.stderr}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function ensureBrowserBinaryEnv() {
|
|
473
|
+
process.env.DO_NOT_TRACK = '1';
|
|
474
|
+
process.env.HYPERFRAMES_NO_TELEMETRY = '1';
|
|
475
|
+
const resolved = resolveBrowserBinary();
|
|
476
|
+
if (!resolved) {
|
|
477
|
+
throw new RenderWorkerError('DependencyMissing', 'Chromium headless shell binary was not found. Run `pnpm run browser:install` or set CHROME_PATH.');
|
|
478
|
+
}
|
|
479
|
+
if (!process.env.CHROME_PATH) {
|
|
480
|
+
process.env.CHROME_PATH = resolved;
|
|
481
|
+
}
|
|
482
|
+
if (!process.env.PUPPETEER_EXECUTABLE_PATH) {
|
|
483
|
+
process.env.PUPPETEER_EXECUTABLE_PATH = resolved;
|
|
484
|
+
}
|
|
485
|
+
return resolved;
|
|
486
|
+
}
|
|
487
|
+
function resolveBrowserBinary() {
|
|
488
|
+
const envCandidates = [process.env.CHROME_PATH, process.env.PUPPETEER_EXECUTABLE_PATH];
|
|
489
|
+
for (const candidate of envCandidates) {
|
|
490
|
+
if (candidate && fssync.existsSync(candidate)) {
|
|
491
|
+
return candidate;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const fromEngine = resolveHeadlessShellPath();
|
|
495
|
+
if (fromEngine && fssync.existsSync(fromEngine)) {
|
|
496
|
+
return fromEngine;
|
|
497
|
+
}
|
|
498
|
+
if (process.env.PUPPETEER_CACHE_DIR) {
|
|
499
|
+
const fromCustomCache = findFileRecursive(process.env.PUPPETEER_CACHE_DIR, 'chrome-headless-shell');
|
|
500
|
+
if (fromCustomCache) {
|
|
501
|
+
return fromCustomCache;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const puppeteerCache = path.join(os.homedir(), '.cache', 'puppeteer', 'chrome-headless-shell');
|
|
505
|
+
const fromHomeCache = findFileRecursive(puppeteerCache, 'chrome-headless-shell');
|
|
506
|
+
if (fromHomeCache) {
|
|
507
|
+
return fromHomeCache;
|
|
508
|
+
}
|
|
509
|
+
const systemCandidates = [
|
|
510
|
+
'/usr/bin/chromium',
|
|
511
|
+
'/usr/bin/chromium-browser',
|
|
512
|
+
'/usr/bin/google-chrome-stable',
|
|
513
|
+
];
|
|
514
|
+
for (const candidate of systemCandidates) {
|
|
515
|
+
if (fssync.existsSync(candidate)) {
|
|
516
|
+
return candidate;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
function findFileRecursive(dir, targetName) {
|
|
522
|
+
if (!fssync.existsSync(dir)) {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const entries = fssync.readdirSync(dir, { withFileTypes: true });
|
|
527
|
+
for (const entry of entries) {
|
|
528
|
+
const fullPath = path.join(dir, entry.name);
|
|
529
|
+
if (entry.isFile() && entry.name === targetName) {
|
|
530
|
+
return fullPath;
|
|
531
|
+
}
|
|
532
|
+
if (entry.isDirectory()) {
|
|
533
|
+
const nested = findFileRecursive(fullPath, targetName);
|
|
534
|
+
if (nested) {
|
|
535
|
+
return nested;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
async function resolveFfprobeBinary() {
|
|
546
|
+
try {
|
|
547
|
+
return await getFfprobeBinary();
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return 'ffprobe';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function probeBinaryFirstLine(bin, args) {
|
|
554
|
+
const { stdout, stderr } = await execFileAsync(bin, args, { timeout: 5000 });
|
|
555
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
556
|
+
const firstLine = combined.split(/\r?\n/)[0]?.trim();
|
|
557
|
+
return firstLine || path.basename(bin);
|
|
558
|
+
}
|
|
559
|
+
function throwIfAborted(signal) {
|
|
560
|
+
if (signal?.aborted) {
|
|
561
|
+
throw new RenderWorkerError('Cancelled', 'Render job was cancelled.');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function resolveHyperFramesCliBinPath() {
|
|
565
|
+
const pkgJsonPath = nodeRequire.resolve('hyperframes/package.json');
|
|
566
|
+
return path.join(path.dirname(pkgJsonPath), 'bin', 'hyperframes.mjs');
|
|
567
|
+
}
|
|
568
|
+
async function compileOfflineEntryFonts(rootDir) {
|
|
569
|
+
const indexHtmlPath = path.join(rootDir, 'index.html');
|
|
570
|
+
const rawIndexHtml = await fs.readFile(indexHtmlPath, 'utf8');
|
|
571
|
+
const normalizedHtml = normalizeSystemFontPrimaryFamilies(rawIndexHtml);
|
|
572
|
+
const compiledHtml = await injectDeterministicFontFaces(normalizedHtml, {
|
|
573
|
+
fetchImpl: async () => {
|
|
574
|
+
throw new Error('Network disabled in render worker');
|
|
575
|
+
},
|
|
576
|
+
allowSystemFontCapture: false,
|
|
577
|
+
});
|
|
578
|
+
if (compiledHtml !== rawIndexHtml) {
|
|
579
|
+
await fs.writeFile(indexHtmlPath, compiledHtml, 'utf8');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function extractStdoutFromExecError(error) {
|
|
583
|
+
if (!error || typeof error !== 'object') {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
const stdout = error.stdout;
|
|
587
|
+
if (typeof stdout === 'string' && stdout.trim().length > 0) {
|
|
588
|
+
return stdout;
|
|
589
|
+
}
|
|
590
|
+
if (Buffer.isBuffer(stdout) && stdout.length > 0) {
|
|
591
|
+
return stdout.toString('utf8');
|
|
592
|
+
}
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
async function configureBundleOnlyRequestInterception(session, serverUrl) {
|
|
596
|
+
const normalizedBase = serverUrl.endsWith('/') ? serverUrl : `${serverUrl}/`;
|
|
597
|
+
await session.page.setRequestInterception(true);
|
|
598
|
+
session.page.on('request', (request) => {
|
|
599
|
+
const url = request.url();
|
|
600
|
+
const isAllowed = url === serverUrl ||
|
|
601
|
+
url.startsWith(normalizedBase) ||
|
|
602
|
+
url.startsWith('data:') ||
|
|
603
|
+
url.startsWith('blob:') ||
|
|
604
|
+
url === 'about:blank';
|
|
605
|
+
if (isAllowed) {
|
|
606
|
+
request.continue().catch(() => undefined);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
request.abort('blockedbyclient').catch(() => undefined);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async function warmDocumentFonts(session) {
|
|
614
|
+
await session.page.evaluate('document.fonts ? Promise.all(Array.from(document.fonts).map(function(f){return f.load().catch(function(){return null;});})).then(function(){return document.fonts.ready;}) : Promise.resolve()');
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Builds the self-contained in-page JavaScript expression (evaluated as source string) that
|
|
618
|
+
* collects `MeasuredText[]` for all visible elements with their own non-empty text (including
|
|
619
|
+
* SVG `<text>`) whose tight glyph bounding box (`Range.getClientRects()` union over child text
|
|
620
|
+
* nodes for HTML elements, or `el.getBoundingClientRect()` for SVG `<text>`) intersects
|
|
621
|
+
* `[0..width, 0..height]`.
|
|
622
|
+
*/
|
|
623
|
+
export function BuildMeasureTextInPageScript(frameWidth, frameHeight, timestampSeconds) {
|
|
624
|
+
const defaultBundledFamilies = BUNDLED_DEFAULT_FONTS.map((f) => f.toLowerCase());
|
|
625
|
+
return [
|
|
626
|
+
'(function(frameWidth, frameHeight, timestampSeconds, defaultBundled) {',
|
|
627
|
+
' var loadedFamilies = defaultBundled.slice();',
|
|
628
|
+
' if (document.fonts && typeof document.fonts.forEach === "function") {',
|
|
629
|
+
' document.fonts.forEach(function(face) {',
|
|
630
|
+
' if (face && face.status === "loaded" && typeof face.family === "string") {',
|
|
631
|
+
' var fam = face.family.trim().replace(/^["\']+|["\']+$/g, "").trim().toLowerCase();',
|
|
632
|
+
' if (fam && loadedFamilies.indexOf(fam) === -1) loadedFamilies.push(fam);',
|
|
633
|
+
' }',
|
|
634
|
+
' });',
|
|
635
|
+
' }',
|
|
636
|
+
' var skipTags = { script: 1, style: 1, noscript: 1, title: 1, defs: 1, template: 1, head: 1, meta: 1, link: 1 };',
|
|
637
|
+
' function getOwnText(el) {',
|
|
638
|
+
' var parts = [];',
|
|
639
|
+
' var nodes = el.childNodes;',
|
|
640
|
+
' for (var i = 0; i < nodes.length; i++) {',
|
|
641
|
+
' var n = nodes[i];',
|
|
642
|
+
' if (n && n.nodeType === 3 && n.nodeValue) parts.push(n.nodeValue);',
|
|
643
|
+
' }',
|
|
644
|
+
' return parts.join(" ").replace(/\\s+/g, " ").trim();',
|
|
645
|
+
' }',
|
|
646
|
+
' function measureTightTextRect(el, tag) {',
|
|
647
|
+
' var isSvg = tag === "text" || tag === "tspan" || (typeof SVGElement !== "undefined" && el instanceof SVGElement);',
|
|
648
|
+
' if (!isSvg && typeof document.createRange === "function") {',
|
|
649
|
+
' var minLeft = Infinity, minTop = Infinity, maxRight = -Infinity, maxBottom = -Infinity;',
|
|
650
|
+
' var nodes = el.childNodes;',
|
|
651
|
+
' for (var i = 0; i < nodes.length; i++) {',
|
|
652
|
+
' var n = nodes[i];',
|
|
653
|
+
' if (!n || n.nodeType !== 3 || !n.nodeValue || !n.nodeValue.trim()) continue;',
|
|
654
|
+
' var range = document.createRange();',
|
|
655
|
+
' range.selectNodeContents(n);',
|
|
656
|
+
' var rects = range.getClientRects();',
|
|
657
|
+
' for (var j = 0; j < rects.length; j++) {',
|
|
658
|
+
' var r = rects[j];',
|
|
659
|
+
' if (!r || !(r.width > 0 && r.height > 0)) continue;',
|
|
660
|
+
' if (r.left < minLeft) minLeft = r.left;',
|
|
661
|
+
' if (r.top < minTop) minTop = r.top;',
|
|
662
|
+
' if (r.right > maxRight) maxRight = r.right;',
|
|
663
|
+
' if (r.bottom > maxBottom) maxBottom = r.bottom;',
|
|
664
|
+
' }',
|
|
665
|
+
' }',
|
|
666
|
+
' if (minLeft < maxRight && minTop < maxBottom) {',
|
|
667
|
+
' return { left: minLeft, top: minTop, right: maxRight, bottom: maxBottom, width: maxRight - minLeft, height: maxBottom - minTop };',
|
|
668
|
+
' }',
|
|
669
|
+
' }',
|
|
670
|
+
' return el.getBoundingClientRect();',
|
|
671
|
+
' }',
|
|
672
|
+
' function buildSelector(el) {',
|
|
673
|
+
' if (el.id) return "#" + el.id;',
|
|
674
|
+
' var segments = [];',
|
|
675
|
+
' var cur = el;',
|
|
676
|
+
' while (cur && cur.nodeType === 1 && segments.length < 3) {',
|
|
677
|
+
' var tag = (cur.tagName || "div").toLowerCase();',
|
|
678
|
+
' if (cur.id) { segments.unshift("#" + cur.id); break; }',
|
|
679
|
+
' var cls = (typeof cur.className === "string" && cur.className.trim())',
|
|
680
|
+
' ? "." + cur.className.trim().split(/\\s+/)[0]',
|
|
681
|
+
' : "";',
|
|
682
|
+
' segments.unshift(tag + cls);',
|
|
683
|
+
' if (tag === "body" || tag === "html") break;',
|
|
684
|
+
' cur = cur.parentElement;',
|
|
685
|
+
' }',
|
|
686
|
+
' return segments.join(" > ") || "body";',
|
|
687
|
+
' }',
|
|
688
|
+
' var results = [];',
|
|
689
|
+
' var root = document.body || document.documentElement;',
|
|
690
|
+
' if (!root) return results;',
|
|
691
|
+
' var all = root.querySelectorAll("*");',
|
|
692
|
+
' for (var idx = 0; idx < all.length; idx++) {',
|
|
693
|
+
' var el = all[idx];',
|
|
694
|
+
' var tag = (el.tagName || "").toLowerCase();',
|
|
695
|
+
' if (skipTags[tag]) continue;',
|
|
696
|
+
' var text = getOwnText(el);',
|
|
697
|
+
' if (!text) continue;',
|
|
698
|
+
' if (typeof el.checkVisibility === "function") {',
|
|
699
|
+
' if (!el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) continue;',
|
|
700
|
+
' }',
|
|
701
|
+
' var rect = measureTightTextRect(el, tag);',
|
|
702
|
+
' if (!(rect.width > 0 && rect.height > 0)) continue;',
|
|
703
|
+
' if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= frameWidth || rect.top >= frameHeight) continue;',
|
|
704
|
+
' var style = window.getComputedStyle(el);',
|
|
705
|
+
' if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity || "1") <= 0) continue;',
|
|
706
|
+
' var fontSizePx = parseFloat(style.fontSize) || 0;',
|
|
707
|
+
' var rawFirstFamily = ((style.fontFamily || "").split(",")[0] || "").trim();',
|
|
708
|
+
' var fontFamily = rawFirstFamily.replace(/^["\']+|["\']+$/g, "").trim();',
|
|
709
|
+
' var fontIsBundled = Boolean(fontFamily) && loadedFamilies.indexOf(fontFamily.toLowerCase()) !== -1;',
|
|
710
|
+
' var boundEl = typeof el.closest === "function" ? el.closest("[data-mj-bound]") : null;',
|
|
711
|
+
' var isBound = boundEl !== null;',
|
|
712
|
+
' var boundKey = boundEl ? (boundEl.getAttribute("data-mj-bound") || "") : undefined;',
|
|
713
|
+
' var item = {',
|
|
714
|
+
' Text: text,',
|
|
715
|
+
' Left: rect.left,',
|
|
716
|
+
' Top: rect.top,',
|
|
717
|
+
' Width: rect.width,',
|
|
718
|
+
' Height: rect.height,',
|
|
719
|
+
' FontSizePx: fontSizePx,',
|
|
720
|
+
' FontFamily: fontFamily,',
|
|
721
|
+
' FontIsBundled: fontIsBundled,',
|
|
722
|
+
' IsBound: isBound,',
|
|
723
|
+
' Selector: buildSelector(el),',
|
|
724
|
+
' TimestampSeconds: timestampSeconds',
|
|
725
|
+
' };',
|
|
726
|
+
' if (boundKey !== undefined) { item.BoundKey = boundKey; }',
|
|
727
|
+
' results.push(item);',
|
|
728
|
+
' }',
|
|
729
|
+
' return results;',
|
|
730
|
+
`})(${JSON.stringify(frameWidth)}, ${JSON.stringify(frameHeight)}, ${JSON.stringify(timestampSeconds)}, ${JSON.stringify(defaultBundledFamilies)})`,
|
|
731
|
+
].join('\n');
|
|
732
|
+
}
|
|
733
|
+
//# sourceMappingURL=hyperframes-backend.js.map
|