@shotstack/shotstack-canvas 1.0.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 +13 -0
- package/dist/chunk-5BH5YLM5.js +140 -0
- package/dist/chunk-FPPKRKBX.js +66 -0
- package/dist/chunk-KBAXJEJG.js +178 -0
- package/dist/entry.node.cjs +6998 -0
- package/dist/entry.node.d.cts +1345 -0
- package/dist/entry.node.d.ts +1345 -0
- package/dist/entry.node.js +6594 -0
- package/dist/entry.web.d.ts +1288 -0
- package/dist/entry.web.js +38227 -0
- package/dist/hb-HSWG3Q47.js +550 -0
- package/dist/hbjs-VGYWXH44.js +499 -0
- package/dist/mediarecorder-fallback-5JYZBGT3.js +133 -0
- package/dist/mediarecorder-fallback-TSLY4MAU.js +11 -0
- package/dist/web-encoder-7CLF7KX4.js +171 -0
- package/dist/web-encoder-MXBT3N36.js +9 -0
- package/package.json +78 -0
- package/scripts/postinstall.js +58 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/core/video/web-encoder.ts
|
|
2
|
+
var WebCodecsEncoder = class {
|
|
3
|
+
encoder = null;
|
|
4
|
+
muxer = null;
|
|
5
|
+
config = null;
|
|
6
|
+
frameCount = 0;
|
|
7
|
+
totalFrames = 0;
|
|
8
|
+
startTime = 0;
|
|
9
|
+
fps = 30;
|
|
10
|
+
keyframeInterval = 150;
|
|
11
|
+
encoderError = null;
|
|
12
|
+
onProgress;
|
|
13
|
+
async configure(config) {
|
|
14
|
+
if (typeof VideoEncoder === "undefined") {
|
|
15
|
+
throw new Error("WebCodecs API not supported in this browser.");
|
|
16
|
+
}
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.fps = config.fps;
|
|
19
|
+
this.totalFrames = Math.max(2, Math.round(config.duration * config.fps) + 1);
|
|
20
|
+
this.frameCount = 0;
|
|
21
|
+
this.startTime = Date.now();
|
|
22
|
+
this.keyframeInterval = Math.round(config.fps * 10);
|
|
23
|
+
this.encoderError = null;
|
|
24
|
+
const { Muxer, ArrayBufferTarget } = await import("mp4-muxer");
|
|
25
|
+
this.muxer = new Muxer({
|
|
26
|
+
target: new ArrayBufferTarget(),
|
|
27
|
+
video: {
|
|
28
|
+
codec: "avc",
|
|
29
|
+
width: config.width,
|
|
30
|
+
height: config.height
|
|
31
|
+
},
|
|
32
|
+
fastStart: "in-memory"
|
|
33
|
+
});
|
|
34
|
+
const candidates = getH264CodecCandidates(config.profile || "high");
|
|
35
|
+
let encoderConfig = null;
|
|
36
|
+
for (const codec of candidates) {
|
|
37
|
+
const candidate = {
|
|
38
|
+
codec,
|
|
39
|
+
width: config.width,
|
|
40
|
+
height: config.height,
|
|
41
|
+
bitrate: config.bitrate ?? 8e6,
|
|
42
|
+
framerate: config.fps,
|
|
43
|
+
hardwareAcceleration: config.hardwareAcceleration ?? "prefer-hardware",
|
|
44
|
+
latencyMode: "quality"
|
|
45
|
+
};
|
|
46
|
+
const support = await VideoEncoder.isConfigSupported(candidate);
|
|
47
|
+
if (support.supported) {
|
|
48
|
+
encoderConfig = candidate;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!encoderConfig) {
|
|
53
|
+
throw new Error("H.264 encoding not supported. Tried codecs: " + candidates.join(", "));
|
|
54
|
+
}
|
|
55
|
+
this.encoder = new VideoEncoder({
|
|
56
|
+
output: (chunk, metadata) => {
|
|
57
|
+
this.muxer.addVideoChunk(chunk, metadata);
|
|
58
|
+
},
|
|
59
|
+
error: (e) => {
|
|
60
|
+
this.encoderError = e;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this.encoder.configure(encoderConfig);
|
|
64
|
+
}
|
|
65
|
+
async encodeFrame(frameData, frameIndex) {
|
|
66
|
+
if (this.encoderError) {
|
|
67
|
+
throw this.encoderError;
|
|
68
|
+
}
|
|
69
|
+
if (!this.encoder || !this.config) {
|
|
70
|
+
throw new Error("Encoder not configured. Call configure() first.");
|
|
71
|
+
}
|
|
72
|
+
const { width, height } = this.config;
|
|
73
|
+
const timestamp = Math.round(frameIndex * 1e6 / this.fps);
|
|
74
|
+
const pixelData = frameData instanceof ArrayBuffer ? new Uint8ClampedArray(frameData) : frameData;
|
|
75
|
+
const imageData = new ImageData(pixelData, width, height);
|
|
76
|
+
const videoFrame = new VideoFrame(imageData, { timestamp });
|
|
77
|
+
const isKeyFrame = frameIndex % this.keyframeInterval === 0;
|
|
78
|
+
this.encoder.encode(videoFrame, { keyFrame: isKeyFrame });
|
|
79
|
+
videoFrame.close();
|
|
80
|
+
this.frameCount++;
|
|
81
|
+
this.reportProgress();
|
|
82
|
+
}
|
|
83
|
+
async encodeCanvas(canvas, frameIndex) {
|
|
84
|
+
if (this.encoderError) {
|
|
85
|
+
throw this.encoderError;
|
|
86
|
+
}
|
|
87
|
+
if (!this.encoder || !this.config) {
|
|
88
|
+
throw new Error("Encoder not configured. Call configure() first.");
|
|
89
|
+
}
|
|
90
|
+
const timestamp = Math.round(frameIndex * 1e6 / this.fps);
|
|
91
|
+
const videoFrame = new VideoFrame(canvas, { timestamp });
|
|
92
|
+
const isKeyFrame = frameIndex % this.keyframeInterval === 0;
|
|
93
|
+
this.encoder.encode(videoFrame, { keyFrame: isKeyFrame });
|
|
94
|
+
videoFrame.close();
|
|
95
|
+
this.frameCount++;
|
|
96
|
+
this.reportProgress();
|
|
97
|
+
}
|
|
98
|
+
async encodeCanvasRepeat(canvas, startFrameIndex, repeatCount) {
|
|
99
|
+
if (this.encoderError) {
|
|
100
|
+
throw this.encoderError;
|
|
101
|
+
}
|
|
102
|
+
if (!this.encoder || !this.config) {
|
|
103
|
+
throw new Error("Encoder not configured. Call configure() first.");
|
|
104
|
+
}
|
|
105
|
+
for (let i = 0; i < repeatCount; i++) {
|
|
106
|
+
const actualFrameIndex = startFrameIndex + i;
|
|
107
|
+
const timestamp = Math.round(actualFrameIndex * 1e6 / this.fps);
|
|
108
|
+
const videoFrame = new VideoFrame(canvas, { timestamp });
|
|
109
|
+
const isKeyFrame = actualFrameIndex % this.keyframeInterval === 0;
|
|
110
|
+
this.encoder.encode(videoFrame, { keyFrame: isKeyFrame });
|
|
111
|
+
videoFrame.close();
|
|
112
|
+
this.frameCount++;
|
|
113
|
+
}
|
|
114
|
+
this.reportProgress();
|
|
115
|
+
}
|
|
116
|
+
async flush() {
|
|
117
|
+
if (this.encoderError) {
|
|
118
|
+
throw this.encoderError;
|
|
119
|
+
}
|
|
120
|
+
if (!this.encoder || !this.muxer) {
|
|
121
|
+
throw new Error("Encoder not configured.");
|
|
122
|
+
}
|
|
123
|
+
await this.encoder.flush();
|
|
124
|
+
this.muxer.finalize();
|
|
125
|
+
const buffer = this.muxer.target.buffer;
|
|
126
|
+
return new Blob([buffer], { type: "video/mp4" });
|
|
127
|
+
}
|
|
128
|
+
close() {
|
|
129
|
+
if (this.encoder && this.encoder.state !== "closed") {
|
|
130
|
+
this.encoder.close();
|
|
131
|
+
}
|
|
132
|
+
this.encoder = null;
|
|
133
|
+
this.muxer = null;
|
|
134
|
+
}
|
|
135
|
+
reportProgress() {
|
|
136
|
+
if (!this.onProgress) return;
|
|
137
|
+
const elapsedMs = Date.now() - this.startTime;
|
|
138
|
+
if (elapsedMs === 0) return;
|
|
139
|
+
const framesPerSecond = this.frameCount / (elapsedMs / 1e3);
|
|
140
|
+
const remainingFrames = this.totalFrames - this.frameCount;
|
|
141
|
+
const estimatedRemainingMs = remainingFrames / framesPerSecond * 1e3;
|
|
142
|
+
this.onProgress({
|
|
143
|
+
framesEncoded: this.frameCount,
|
|
144
|
+
totalFrames: this.totalFrames,
|
|
145
|
+
percentage: this.frameCount / this.totalFrames * 100,
|
|
146
|
+
elapsedMs,
|
|
147
|
+
estimatedRemainingMs: Math.round(estimatedRemainingMs),
|
|
148
|
+
currentFps: Math.round(framesPerSecond * 10) / 10
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function getH264CodecCandidates(profile) {
|
|
153
|
+
switch (profile) {
|
|
154
|
+
case "baseline":
|
|
155
|
+
return ["avc1.42E028", "avc1.42001F", "avc1.42001E"];
|
|
156
|
+
case "main":
|
|
157
|
+
return ["avc1.4D0028", "avc1.4D001F", "avc1.4D001E"];
|
|
158
|
+
case "high":
|
|
159
|
+
default:
|
|
160
|
+
return ["avc1.640028", "avc1.640029", "avc1.64001F", "avc1.64001E"];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function createWebCodecsEncoder(config) {
|
|
164
|
+
const encoder = new WebCodecsEncoder();
|
|
165
|
+
await encoder.configure(config);
|
|
166
|
+
return encoder;
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
WebCodecsEncoder,
|
|
170
|
+
createWebCodecsEncoder
|
|
171
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shotstack/shotstack-canvas",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/entry.node.cjs",
|
|
7
|
+
"module": "./dist/entry.node.js",
|
|
8
|
+
"browser": "./dist/entry.web.js",
|
|
9
|
+
"types": "./dist/entry.node.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"node": {
|
|
13
|
+
"import": "./dist/entry.node.js",
|
|
14
|
+
"require": "./dist/entry.node.cjs"
|
|
15
|
+
},
|
|
16
|
+
"browser": "./dist/entry.web.js",
|
|
17
|
+
"default": "./dist/entry.web.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**",
|
|
22
|
+
"scripts/postinstall.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"postinstall": "node scripts/postinstall.js",
|
|
30
|
+
"vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
|
|
31
|
+
"example:node": "node examples/node-example.mjs",
|
|
32
|
+
"example:video": "node examples/node-video.mjs",
|
|
33
|
+
"example:web": "vite dev examples/web-example",
|
|
34
|
+
"test:caption-web": "vite dev examples/caption-tests",
|
|
35
|
+
"prepublishOnly": "node scripts/publish-guard.cjs && pnpm build",
|
|
36
|
+
"test": "node --test tests/build-verify.mjs"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
48
|
+
"@resvg/resvg-wasm": "^2.6.2",
|
|
49
|
+
"@shotstack/schemas": "1.8.7",
|
|
50
|
+
"bidi-js": "^1.0.3",
|
|
51
|
+
"canvas": "npm:@napi-rs/canvas@^0.1.54",
|
|
52
|
+
"ffmpeg-static": "^5.2.0",
|
|
53
|
+
"fontkit": "^2.0.4",
|
|
54
|
+
"harfbuzzjs": "0.4.12",
|
|
55
|
+
"lru-cache": "^11.2.5",
|
|
56
|
+
"mp4-muxer": "^5.1.3",
|
|
57
|
+
"zod": "^4.2.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
61
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
62
|
+
"@semantic-release/git": "^10.0.1",
|
|
63
|
+
"@semantic-release/github": "^12.0.6",
|
|
64
|
+
"@semantic-release/npm": "^13.1.5",
|
|
65
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
66
|
+
"@types/node": "^20.14.10",
|
|
67
|
+
"semantic-release": "^25.0.3",
|
|
68
|
+
"tsup": "^8.2.3",
|
|
69
|
+
"typescript": "^5.5.3",
|
|
70
|
+
"vite": "^5.3.3",
|
|
71
|
+
"vite-plugin-top-level-await": "1.6.0",
|
|
72
|
+
"vite-plugin-wasm": "3.5.0"
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/shotstack/shotstack-canvas.git"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script to verify native canvas bindings are available
|
|
5
|
+
* This helps catch issues early and provides helpful guidance
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { platform as _platform, arch as _arch } from 'os';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
import { readdirSync } from 'fs';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
const platform = _platform();
|
|
16
|
+
const arch = _arch();
|
|
17
|
+
|
|
18
|
+
// Map platform/arch to package names
|
|
19
|
+
const platformMap = {
|
|
20
|
+
'darwin-arm64': '@napi-rs/canvas-darwin-arm64',
|
|
21
|
+
'darwin-x64': '@napi-rs/canvas-darwin-x64',
|
|
22
|
+
'linux-arm64': '@napi-rs/canvas-linux-arm64-gnu',
|
|
23
|
+
'linux-x64': '@napi-rs/canvas-linux-x64-gnu',
|
|
24
|
+
'win32-x64': '@napi-rs/canvas-win32-x64-msvc',
|
|
25
|
+
'linux-arm': '@napi-rs/canvas-linux-arm-gnueabihf',
|
|
26
|
+
'android-arm64': '@napi-rs/canvas-android-arm64',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const platformKey = `${platform}-${arch}`;
|
|
30
|
+
const requiredPackage = platformMap[platformKey];
|
|
31
|
+
|
|
32
|
+
if (!requiredPackage) {
|
|
33
|
+
console.warn(`\n⚠️ Warning: Unsupported platform ${platformKey} for @napi-rs/canvas`);
|
|
34
|
+
console.warn(' Canvas rendering may not work on this platform.\n');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if the native binding package is installed
|
|
39
|
+
try {
|
|
40
|
+
const packagePath = require.resolve(`${requiredPackage}/package.json`);
|
|
41
|
+
const packageDir = dirname(packagePath);
|
|
42
|
+
|
|
43
|
+
// Verify the .node file exists
|
|
44
|
+
const nodeFiles = readdirSync(packageDir).filter(f => f.endsWith('.node'));
|
|
45
|
+
|
|
46
|
+
if (nodeFiles.length > 0) {
|
|
47
|
+
console.log(`✅ @shotstack/shotstack-canvas: Native canvas binding found for ${platformKey}`);
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error('No .node file found');
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(`\n⚠️ Warning: Native canvas binding not found for ${platformKey}`);
|
|
53
|
+
console.warn(` Expected package: ${requiredPackage}`);
|
|
54
|
+
console.warn('\n If you see "Cannot find native binding" errors, try:');
|
|
55
|
+
console.warn(' 1. Delete node_modules and package-lock.json');
|
|
56
|
+
console.warn(' 2. Run: npm install');
|
|
57
|
+
console.warn(` 3. Or manually install: npm install ${requiredPackage}\n`);
|
|
58
|
+
}
|