polotno-node 3.8.1 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/assets/{html2canvas-Cm_vP64m.js → html2canvas-Dn6bw578.js} +1 -1
- package/dist/assets/index-BB9RRitk.js +649 -0
- package/dist/assets/{index.es-Dg88tupA.js → index.es-BRgisxmP.js} +1 -1
- package/dist/assets/{jspdf.es.min-CTt99xrZ.js → jspdf.es.min-D1i2-kxU.js} +4 -4
- package/dist/assets/lib-BAYK6Nbm.js +1 -0
- package/dist/assets/purify.es-7fJ1DZ6H.js +3 -0
- package/dist/index.html +2 -7
- package/lib/index.d.ts +1 -1
- package/lib/instance.d.ts +12 -9
- package/lib/instance.js +363 -190
- package/lib/types.d.ts +23 -1
- package/lib/video-status.d.ts +7 -0
- package/lib/video-status.js +107 -0
- package/package.json +16 -18
- package/dist/assets/index-Bc1WTi4k.js +0 -648
- package/dist/assets/lib-C3mnf1jX.js +0 -1
- package/dist/assets/purify.es-ChwZkWde.js +0 -3
package/lib/types.d.ts
CHANGED
|
@@ -28,9 +28,31 @@ export interface PDFRenderOptions extends RenderOptions {
|
|
|
28
28
|
unit?: 'pt' | 'mm' | 'cm' | 'in';
|
|
29
29
|
cropMarkSize?: number;
|
|
30
30
|
}
|
|
31
|
+
/** Includes the browser stages from @polotno/video-export and Node I/O stages. */
|
|
32
|
+
export type VideoExportStage = 'prepare-media' | 'load-client' | 'load-design' | 'setup' | 'frames' | 'audio-decode' | 'audio-render' | 'audio-encode' | 'finalize' | 'transfer' | 'postprocess' | 'read-output';
|
|
33
|
+
export interface VideoExportProgress {
|
|
34
|
+
stage: VideoExportStage;
|
|
35
|
+
/** Completed media sources, pages, frames, audio sources, or bytes for this stage. */
|
|
36
|
+
completed?: number;
|
|
37
|
+
total?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface VideoExportStatus extends VideoExportProgress {
|
|
40
|
+
state: 'running' | 'stalled' | 'completed' | 'failed';
|
|
41
|
+
elapsedMs: number;
|
|
42
|
+
/** Time since the last stage change, counter increase, or fraction increase. */
|
|
43
|
+
idleMs: number;
|
|
44
|
+
/** Time since the last tab heartbeat. Absent until the first heartbeat arrives. */
|
|
45
|
+
heartbeatAgeMs?: number;
|
|
46
|
+
}
|
|
31
47
|
export interface VideoRenderOptions extends RenderOptions {
|
|
32
48
|
fps?: number;
|
|
33
|
-
onProgress?: (progress: number, frameTime?: number) => void;
|
|
49
|
+
onProgress?: (progress: number, frameTime?: number, status?: VideoExportStatus) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Inactivity budget in ms, or overrides per stage (default: 60000).
|
|
52
|
+
* 0 disables stall notifications. Only used with onProgress.
|
|
53
|
+
* A stall notification never cancels, retries, or changes protocolTimeout.
|
|
54
|
+
*/
|
|
55
|
+
stallTimeoutMs?: number | Partial<Record<VideoExportStage, number>>;
|
|
34
56
|
/**
|
|
35
57
|
* When using the default local client (no `createInstance({ url })`), polotno-node will
|
|
36
58
|
* pre-download remote video/audio sources into a temp folder and serve them to the
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VideoExportProgress, VideoRenderOptions } from './types.js';
|
|
2
|
+
export declare function createVideoStatusMonitor({ onProgress, stallTimeoutMs, }: Pick<VideoRenderOptions, 'onProgress' | 'stallTimeoutMs'>): {
|
|
3
|
+
update: (next: VideoExportProgress, nextFraction?: number, nextFrameTime?: number) => void;
|
|
4
|
+
progress(fraction: number, frameTime?: number, details?: VideoExportProgress): void;
|
|
5
|
+
heartbeat(): void;
|
|
6
|
+
finish(result: "completed" | "failed"): void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { polotnoError } from './errors.js';
|
|
2
|
+
// Node owns this clock: a renderer that cannot execute JavaScript cannot run
|
|
3
|
+
// its own stall detector. Heartbeats describe responsiveness, not useful work.
|
|
4
|
+
export function createVideoStatusMonitor({ onProgress, stallTimeoutMs = 60000, }) {
|
|
5
|
+
const timeouts = typeof stallTimeoutMs === 'object' && stallTimeoutMs !== null
|
|
6
|
+
? Object.values(stallTimeoutMs).filter((timeout) => timeout !== undefined)
|
|
7
|
+
: [stallTimeoutMs];
|
|
8
|
+
for (const timeout of timeouts) {
|
|
9
|
+
if (typeof timeout !== 'number' ||
|
|
10
|
+
!Number.isFinite(timeout) ||
|
|
11
|
+
timeout < 0 ||
|
|
12
|
+
timeout > 2147483647) {
|
|
13
|
+
throw polotnoError('EXPORT_FAILED', 'Invalid video stall timeout.', {
|
|
14
|
+
reason: 'invalid-options',
|
|
15
|
+
option: 'stallTimeoutMs',
|
|
16
|
+
value: String(timeout),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const startedAt = performance.now();
|
|
21
|
+
let advancedAt = startedAt;
|
|
22
|
+
let heartbeatAt;
|
|
23
|
+
let progress = { stage: 'prepare-media' };
|
|
24
|
+
let state = 'running';
|
|
25
|
+
let fraction = 0;
|
|
26
|
+
let frameTime;
|
|
27
|
+
let finished = false;
|
|
28
|
+
let timer;
|
|
29
|
+
async function emit() {
|
|
30
|
+
if (!onProgress)
|
|
31
|
+
return;
|
|
32
|
+
const now = performance.now();
|
|
33
|
+
const status = {
|
|
34
|
+
...progress,
|
|
35
|
+
state,
|
|
36
|
+
elapsedMs: now - startedAt,
|
|
37
|
+
idleMs: now - advancedAt,
|
|
38
|
+
...(heartbeatAt === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { heartbeatAgeMs: now - heartbeatAt }),
|
|
41
|
+
};
|
|
42
|
+
// Observers cannot turn a successful render into a failed one, including
|
|
43
|
+
// when an async observer rejects. Never await user I/O in the render path.
|
|
44
|
+
try {
|
|
45
|
+
await onProgress(fraction, frameTime, status);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error('Video status callback failed:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function arm() {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
const timeout = typeof stallTimeoutMs === 'number'
|
|
54
|
+
? stallTimeoutMs
|
|
55
|
+
: (stallTimeoutMs[progress.stage] ?? 60000);
|
|
56
|
+
if (!onProgress || timeout === 0)
|
|
57
|
+
return;
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
state = 'stalled';
|
|
60
|
+
emit();
|
|
61
|
+
}, timeout);
|
|
62
|
+
timer.unref();
|
|
63
|
+
}
|
|
64
|
+
emit();
|
|
65
|
+
arm();
|
|
66
|
+
function update(next, nextFraction = fraction, nextFrameTime) {
|
|
67
|
+
if (finished || !onProgress)
|
|
68
|
+
return;
|
|
69
|
+
const fractionAdvanced = Number.isFinite(nextFraction) && nextFraction > fraction;
|
|
70
|
+
// Stage changes, counters, and legacy percentage advances are useful work.
|
|
71
|
+
// Repeated values and heartbeats must not keep a stuck job alive.
|
|
72
|
+
const advanced = next.stage !== progress.stage ||
|
|
73
|
+
fractionAdvanced ||
|
|
74
|
+
(next.completed !== undefined &&
|
|
75
|
+
next.completed > (progress.completed ?? -1));
|
|
76
|
+
if (!advanced)
|
|
77
|
+
return;
|
|
78
|
+
progress = { ...next };
|
|
79
|
+
if (fractionAdvanced)
|
|
80
|
+
fraction = Math.min(1, nextFraction);
|
|
81
|
+
frameTime = nextFrameTime;
|
|
82
|
+
advancedAt = performance.now();
|
|
83
|
+
state = 'running';
|
|
84
|
+
emit();
|
|
85
|
+
arm();
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
update,
|
|
89
|
+
progress(fraction, frameTime, details) {
|
|
90
|
+
update(details ?? progress, fraction, frameTime);
|
|
91
|
+
},
|
|
92
|
+
heartbeat() {
|
|
93
|
+
if (!finished)
|
|
94
|
+
heartbeatAt = performance.now();
|
|
95
|
+
},
|
|
96
|
+
finish(result) {
|
|
97
|
+
if (finished)
|
|
98
|
+
return;
|
|
99
|
+
finished = true;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
state = result;
|
|
102
|
+
if (result === 'completed')
|
|
103
|
+
fraction = 1;
|
|
104
|
+
emit();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polotno-node",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Polotno workflow from NodeJS",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"type": "module",
|
|
@@ -35,25 +35,23 @@
|
|
|
35
35
|
"@polotno/video-export"
|
|
36
36
|
],
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"
|
|
38
|
+
"@polotno/design-fixtures": "0.0.0",
|
|
39
|
+
"@polotno/video-export": "0.4.0",
|
|
40
|
+
"@types/node": "^26.6.1",
|
|
40
41
|
"dotenv": "^17.4.2",
|
|
41
|
-
"fluent-ffmpeg": "^2.1.3",
|
|
42
42
|
"jspdf": "^4.2.1",
|
|
43
|
-
"konva": "^10.
|
|
44
|
-
"mediabunny": "^1.
|
|
43
|
+
"konva": "^10.5.0",
|
|
44
|
+
"mediabunny": "^1.56.3",
|
|
45
45
|
"pixelmatch": "^7.2.0",
|
|
46
46
|
"pngjs": "^7.0.0",
|
|
47
|
-
"
|
|
48
|
-
"react
|
|
47
|
+
"polotno": "4.13.0",
|
|
48
|
+
"react": "^19.3.0",
|
|
49
|
+
"react-dom": "^19.3.0",
|
|
49
50
|
"tmp": "^0.2.7",
|
|
50
51
|
"typescript": "~6.0.3",
|
|
51
|
-
"vite": "^8.
|
|
52
|
-
"vitest": "^
|
|
53
|
-
"web-audio-api": "^1.5.
|
|
54
|
-
"@polotno/video-export": "0.2.5",
|
|
55
|
-
"polotno": "4.12.0",
|
|
56
|
-
"@polotno/design-fixtures": "0.0.0"
|
|
52
|
+
"vite": "^8.3.0",
|
|
53
|
+
"vitest": "^5.0.1",
|
|
54
|
+
"web-audio-api": "^1.5.6"
|
|
57
55
|
},
|
|
58
56
|
"overrides": {
|
|
59
57
|
"@polotno/video-export": {
|
|
@@ -63,10 +61,10 @@
|
|
|
63
61
|
},
|
|
64
62
|
"author": "Anton Lavrenov",
|
|
65
63
|
"dependencies": {
|
|
66
|
-
"@
|
|
67
|
-
"
|
|
68
|
-
"puppeteer
|
|
69
|
-
"
|
|
64
|
+
"@polotno/schema": "0.6.2",
|
|
65
|
+
"@sparticuz/chromium": "^153.0.0",
|
|
66
|
+
"puppeteer": "25.11.0",
|
|
67
|
+
"puppeteer-core": "25.11.0"
|
|
70
68
|
},
|
|
71
69
|
"bugs": {
|
|
72
70
|
"url": "https://github.com/polotno-project/polotno-node/issues"
|