ffmpeg-framecraft 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/LICENSE +15 -0
- package/README.md +467 -0
- package/index.js +29 -0
- package/package.json +43 -0
- package/src/engine.js +365 -0
- package/src/executor.js +158 -0
- package/src/filters.js +173 -0
- package/src/presets/presets.js +105 -0
- package/src/presets/shorts.js +28 -0
- package/src/presets/transitions.js +49 -0
- package/src/utils.js +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, the ffmpeg-framecraft contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# ffmpeg-framecraft
|
|
2
|
+
|
|
3
|
+
A Node.js FFmpeg wrapper for video processing: crop to 9:16, slice by time, add transitions between clips, subtitles, background music, and audio extraction. Suited for vertical/short-form output (e.g. YouTube Shorts, TikTok, Reels) and general pipelines.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of contents
|
|
8
|
+
|
|
9
|
+
- [Prerequisites](#prerequisites)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Quick start](#quick-start)
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Crop to 9:16](#1-crop-to-916)
|
|
14
|
+
- [Slice by timestamp](#2-slice-by-timestamp)
|
|
15
|
+
- [Multiple slices with transitions](#3-multiple-slices-with-transitions)
|
|
16
|
+
- [Subtitles](#4-subtitles)
|
|
17
|
+
- [Background music](#5-background-music)
|
|
18
|
+
- [Extract thumbnail](#6-extract-thumbnail)
|
|
19
|
+
- [Extract audio only](#7-extract-audio-only)
|
|
20
|
+
- [Pipelines (compose)](#pipelines-compose)
|
|
21
|
+
- [Platform presets](#platform-presets)
|
|
22
|
+
- [Transition presets](#transition-presets)
|
|
23
|
+
- [Progress reporting](#progress-reporting)
|
|
24
|
+
- [API reference](#api-reference)
|
|
25
|
+
- [Advanced: filter builders](#advanced-filter-builders)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
**FFmpeg** and **ffprobe** must be installed:
|
|
33
|
+
|
|
34
|
+
| Platform | Command |
|
|
35
|
+
|-----------|--------|
|
|
36
|
+
| macOS | `brew install ffmpeg` |
|
|
37
|
+
| Ubuntu/Debian | `sudo apt install ffmpeg` |
|
|
38
|
+
| Windows | [FFmpeg download](https://ffmpeg.org/download.html) |
|
|
39
|
+
|
|
40
|
+
If the binaries are not in `PATH`, set:
|
|
41
|
+
|
|
42
|
+
- `FFMPEG_PATH` — path to `ffmpeg`
|
|
43
|
+
- `FFPROBE_PATH` — path to `ffprobe`
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd shared/video_tool
|
|
51
|
+
npm install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
From another package in the repo:
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
const { FramecraftEngine } = require('../shared/video_tool');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or when installed as a dependency:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const { FramecraftEngine } = require('ffmpeg-framecraft');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const { FramecraftEngine } = require('ffmpeg-framecraft');
|
|
72
|
+
|
|
73
|
+
const engine = new FramecraftEngine();
|
|
74
|
+
|
|
75
|
+
// Crop a landscape video to vertical 9:16
|
|
76
|
+
await engine.cropTo916('input.mp4', 'shorts.mp4');
|
|
77
|
+
|
|
78
|
+
// Extract one segment
|
|
79
|
+
await engine.slice('input.mp4', 'clip.mp4', { start: 10, end: 30 });
|
|
80
|
+
|
|
81
|
+
// Combine several segments with a fade between them
|
|
82
|
+
await engine.slicesWithTransitions('input.mp4', 'highlight.mp4', {
|
|
83
|
+
slices: [
|
|
84
|
+
{ start: 0, end: 10 },
|
|
85
|
+
{ start: 45, end: 55 },
|
|
86
|
+
{ start: 120, end: 130 },
|
|
87
|
+
],
|
|
88
|
+
transition: 'fade',
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Features
|
|
95
|
+
|
|
96
|
+
### 1. Crop to 9:16
|
|
97
|
+
|
|
98
|
+
Converts horizontal video to vertical (9:16). Uses a centered crop then scales to **720×1280** (baseline-friendly). Suited for Shorts/Reels/TikTok and similar formats.
|
|
99
|
+
|
|
100
|
+
| Parameter | Type | Description |
|
|
101
|
+
|---------------|--------|-------------|
|
|
102
|
+
| `inputPath` | string | Source video path |
|
|
103
|
+
| `outputPath` | string | Output path (e.g. `.mp4`) |
|
|
104
|
+
| `opts.onProgress` | function | `(progress) => {}`; `progress.percent` is 0–100 |
|
|
105
|
+
|
|
106
|
+
**Example**
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
await engine.cropTo916('landscape.mp4', 'vertical.mp4');
|
|
110
|
+
|
|
111
|
+
await engine.cropTo916('landscape.mp4', 'vertical.mp4', {
|
|
112
|
+
onProgress: (p) => console.log(`${p.percent?.toFixed(1) ?? '-'}%`),
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 2. Slice by timestamp
|
|
119
|
+
|
|
120
|
+
Extracts a single segment from the video. Start/end can be **seconds** or **time strings** (`"MM:SS"`, `"HH:MM:SS"`).
|
|
121
|
+
|
|
122
|
+
| Parameter | Type | Description |
|
|
123
|
+
|-------------|--------|-------------|
|
|
124
|
+
| `inputPath` | string | Source video |
|
|
125
|
+
| `outputPath`| string | Output path |
|
|
126
|
+
| `range` | object | `{ start, end }` in seconds or time string |
|
|
127
|
+
| `opts.onProgress` | function | Optional progress callback |
|
|
128
|
+
|
|
129
|
+
**Example**
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
// Seconds
|
|
133
|
+
await engine.slice('input.mp4', 'clip.mp4', { start: 10, end: 30 });
|
|
134
|
+
|
|
135
|
+
// Time strings
|
|
136
|
+
await engine.slice('input.mp4', 'clip.mp4', { start: '0:10', end: '1:30' });
|
|
137
|
+
await engine.slice('input.mp4', 'clip.mp4', { start: '1:00:00', end: '1:05:00' });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 3. Multiple slices with transitions
|
|
143
|
+
|
|
144
|
+
Trims several time ranges from **one** video and concatenates them with a transition (xfade + acrossfade). Progress is based on **output** duration.
|
|
145
|
+
|
|
146
|
+
| Parameter | Type | Description |
|
|
147
|
+
|---------------|--------|-------------|
|
|
148
|
+
| `inputPath` | string | Source video |
|
|
149
|
+
| `outputPath` | string | Output path |
|
|
150
|
+
| `options.slices` | array | `[{ start, end }, ...]`; times in seconds or `"MM:SS"` |
|
|
151
|
+
| `options.transition` | string \| object | Preset name (e.g. `'fade'`) or `{ type, duration }` (seconds) |
|
|
152
|
+
| `options.preset` | string \| object | Platform preset name or object; sets default transition |
|
|
153
|
+
| `opts.onProgress` | function | Optional progress callback |
|
|
154
|
+
|
|
155
|
+
**Per-slice transition:** Any slice (except the first) can override the transition:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
{ start: 10, end: 20, transition: 'wipeleft' } // only this boundary uses wipeleft
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Example — same transition for all boundaries**
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
await engine.slicesWithTransitions('input.mp4', 'highlight.mp4', {
|
|
165
|
+
slices: [
|
|
166
|
+
{ start: 0, end: 8 },
|
|
167
|
+
{ start: 20, end: 28 },
|
|
168
|
+
{ start: 60, end: 68 },
|
|
169
|
+
],
|
|
170
|
+
transition: 'fade',
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Example — custom duration**
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
|
|
178
|
+
slices: [{ start: 0, end: 5 }, { start: 10, end: 15 }],
|
|
179
|
+
transition: { type: 'dissolve', duration: 1.5 },
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Example — platform preset (transition from preset)**
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
|
|
187
|
+
slices: [{ start: 0, end: 10 }, { start: 30, end: 40 }],
|
|
188
|
+
preset: 'youtubeShort', // uses preset's transition + duration
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Example — per-slice transitions**
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
|
|
196
|
+
slices: [
|
|
197
|
+
{ start: 0, end: 9 },
|
|
198
|
+
{ start: 38, end: 62, transition: 'fadewhite', duration: 2 },
|
|
199
|
+
{ start: 172, end: 176.4, transition: 'dissolve', duration: 1 },
|
|
200
|
+
],
|
|
201
|
+
transition: 'fade', // default for boundaries that don't specify one
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Slices are clamped to the file duration; invalid or zero-length ranges throw.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### 4. Subtitles
|
|
210
|
+
|
|
211
|
+
Burns an **SRT** file into the video. Optional ASS-style options for font/size/colour (useful for AI-generated captions).
|
|
212
|
+
|
|
213
|
+
| Parameter | Type | Description |
|
|
214
|
+
|---------------|--------|-------------|
|
|
215
|
+
| `inputPath` | string | Source video |
|
|
216
|
+
| `outputPath` | string | Output path |
|
|
217
|
+
| `srtPath` | string | Path to `.srt` file |
|
|
218
|
+
| `opts` | object | `onProgress` + optional style: `fontName`, `fontSize`, `primaryColour`, `outlineColour`, `backColour`, `outline`, `shadow` |
|
|
219
|
+
|
|
220
|
+
**Example**
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
await engine.addSubtitles('input.mp4', 'with_subs.mp4', 'captions.srt');
|
|
224
|
+
|
|
225
|
+
await engine.addSubtitles('input.mp4', 'with_subs.mp4', 'captions.srt', {
|
|
226
|
+
fontSize: 28,
|
|
227
|
+
primaryColour: '&Hffffff',
|
|
228
|
+
outline: 2,
|
|
229
|
+
onProgress: (p) => console.log(p.percent?.toFixed(1) + '%'),
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### 5. Background music
|
|
236
|
+
|
|
237
|
+
Mixes the video’s audio with a second audio file (e.g. music). If the video has no audio, only the music track is used.
|
|
238
|
+
|
|
239
|
+
| Parameter | Type | Description |
|
|
240
|
+
|---------------|--------|-------------|
|
|
241
|
+
| `inputPath` | string | Source video |
|
|
242
|
+
| `outputPath` | string | Output path |
|
|
243
|
+
| `musicPath` | string | Path to music/audio (e.g. `.mp3`, `.m4a`) |
|
|
244
|
+
| `opts.onProgress` | function | Optional progress callback |
|
|
245
|
+
|
|
246
|
+
**Example**
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
await engine.addBackgroundMusic('talk.mp4', 'with_music.mp4', 'background.mp3');
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### 6. Extract thumbnail
|
|
255
|
+
|
|
256
|
+
Exports a single frame as an image. Format is taken from the file extension (`.jpg`, `.png`).
|
|
257
|
+
|
|
258
|
+
| Parameter | Type | Description |
|
|
259
|
+
|---------------|--------|-------------|
|
|
260
|
+
| `inputPath` | string | Source video |
|
|
261
|
+
| `outputPath` | string | Output image path (e.g. `frame.jpg`, `poster.png`) |
|
|
262
|
+
| `time` | number \| string | Time in seconds or `"MM:SS"` / `"HH:MM:SS"` (default: 0) |
|
|
263
|
+
|
|
264
|
+
**Example**
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
await engine.extractThumbnail('input.mp4', 'frame.jpg', 5);
|
|
268
|
+
await engine.extractThumbnail('input.mp4', 'poster.png', '0:01:30');
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### 7. Extract audio only
|
|
274
|
+
|
|
275
|
+
Re-encodes audio to **AAC** (`.m4a`) or **MP3** (`.mp3`) based on the output extension. No video stream.
|
|
276
|
+
|
|
277
|
+
| Parameter | Type | Description |
|
|
278
|
+
|---------------|--------|-------------|
|
|
279
|
+
| `inputPath` | string | Source video |
|
|
280
|
+
| `outputPath` | string | Output path (`.m4a` or `.mp3`) |
|
|
281
|
+
| `opts.bitrate`| number | kbps (default: 256 for AAC, 320 for MP3) |
|
|
282
|
+
| `opts.sampleRate` | number | Hz (default: 48000) |
|
|
283
|
+
| `opts.onProgress` | function | Optional progress callback |
|
|
284
|
+
|
|
285
|
+
**Example**
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
await engine.extractAudioOnly('video.mp4', 'audio.m4a');
|
|
289
|
+
await engine.extractAudioOnly('video.mp4', 'audio.mp3');
|
|
290
|
+
await engine.extractAudioOnly('video.mp4', 'audio.mp3', { bitrate: 256 });
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Pipelines (compose)
|
|
296
|
+
|
|
297
|
+
Run several operations in sequence with a single call. Intermediate files are written to the same directory as the final output and **deleted** after the pipeline finishes.
|
|
298
|
+
|
|
299
|
+
**Preset:** one named operation.
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
await engine.compose('input.mp4', 'output.mp4', 'shorts');
|
|
303
|
+
// Same as: cropTo916(input, output)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Pipeline:** array of steps. Each step has an `op` and step-specific options.
|
|
307
|
+
|
|
308
|
+
| Step `op` | Required options | Description |
|
|
309
|
+
|-----------|------------------|-------------|
|
|
310
|
+
| `crop916` | — | Crop to 9:16 |
|
|
311
|
+
| `slice` | `start`, `end` | Extract segment |
|
|
312
|
+
| `subtitles` | `srtPath` | Burn SRT |
|
|
313
|
+
| `music` | `musicPath` | Add background music |
|
|
314
|
+
| `slicesWithTransitions` | `slices` | Multiple slices + transitions; optional `transition`, `preset` |
|
|
315
|
+
| `audioOnly` | `outputPath` | Extract audio to given path (no video output file from compose) |
|
|
316
|
+
|
|
317
|
+
**Example — crop then slice**
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
await engine.compose('input.mp4', 'output.mp4', [
|
|
321
|
+
{ op: 'crop916' },
|
|
322
|
+
{ op: 'slice', start: 0, end: 60 },
|
|
323
|
+
]);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Example — slice then extract audio only**
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
await engine.compose('input.mp4', 'output.mp4', [
|
|
330
|
+
{ op: 'slice', start: 10, end: 20 },
|
|
331
|
+
{ op: 'audioOnly', outputPath: path.join(__dirname, 'data', 'clip_audio.mp3') },
|
|
332
|
+
], { onProgress: (p) => console.log(p.percent?.toFixed(1) + '%') });
|
|
333
|
+
// Only clip_audio.mp3 is kept; intermediate slice file is removed.
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Example — full pipeline**
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
await engine.compose('input.mp4', 'output.mp4', [
|
|
340
|
+
{ op: 'slicesWithTransitions', slices: [{ start: 0, end: 10 }, { start: 30, end: 40 }], preset: 'youtubeShort' },
|
|
341
|
+
{ op: 'subtitles', srtPath: 'subs.srt' },
|
|
342
|
+
{ op: 'music', musicPath: 'bgm.mp3' },
|
|
343
|
+
], { onProgress: (p) => console.log(p.percent?.toFixed(1) + '%') });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Platform presets
|
|
349
|
+
|
|
350
|
+
Presets define aspect ratio, resolution, transition, and codec hints. Use **preset name** (string) or the **object** from the package.
|
|
351
|
+
|
|
352
|
+
| Preset | Name | Notes |
|
|
353
|
+
|----------------------|------------------|--------|
|
|
354
|
+
| YouTube Shorts | `youtubeShort` | 9:16, 1080×1920, fade, subtitles/watermark flags |
|
|
355
|
+
| TikTok | `tiktok` | 9:16, 1080×1920, fade |
|
|
356
|
+
| Instagram Reels | `instagramReels` | 9:16, 1080×1920, dissolve |
|
|
357
|
+
| Shorts (720p) | `shorts` | 9:16, 720×1280, baseline profile |
|
|
358
|
+
|
|
359
|
+
**Example**
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
const { youtubeShortPreset, getPreset } = require('ffmpeg-framecraft');
|
|
363
|
+
|
|
364
|
+
console.log(youtubeShortPreset.transition); // 'fade'
|
|
365
|
+
console.log(getPreset('tiktok').aspectRatio); // '9:16'
|
|
366
|
+
|
|
367
|
+
await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
|
|
368
|
+
slices: [...],
|
|
369
|
+
preset: 'youtubeShort',
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Transition presets
|
|
376
|
+
|
|
377
|
+
Used by `slicesWithTransitions`. You can pass a **preset name** or **`{ type, duration }`** (duration in seconds).
|
|
378
|
+
|
|
379
|
+
**Available names:**
|
|
380
|
+
`fade`, `fadeLong`, `wipeleft`, `wiperight`, `wipeup`, `wipedown`, `slideleft`, `slideright`, `slideup`, `slidedown`, `circleopen`, `circleclose`, `rectcrop`, `distance`, `fadeblack`, `fadewhite`, `radial`, `dissolve`, `pixelize`, `zoomin`, `zoomout`.
|
|
381
|
+
|
|
382
|
+
**Example**
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
const { TRANSITION_PRESETS, getTransition } = require('ffmpeg-framecraft');
|
|
386
|
+
|
|
387
|
+
// Preset name
|
|
388
|
+
transition: 'dissolve'
|
|
389
|
+
|
|
390
|
+
// Custom type + duration
|
|
391
|
+
transition: { type: 'fade', duration: 1 }
|
|
392
|
+
|
|
393
|
+
// Resolve preset to object
|
|
394
|
+
getTransition('fadeLong'); // { type: 'fade', duration: 1 }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Transition duration is automatically clamped so it does not exceed the length of either segment at that boundary.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Progress reporting
|
|
402
|
+
|
|
403
|
+
All operations that take `opts.onProgress` report:
|
|
404
|
+
|
|
405
|
+
- **`percent`** — 0–100, based on **output** duration when known (slice, slicesWithTransitions, cropTo916, addBackgroundMusic). Otherwise from FFmpeg input duration.
|
|
406
|
+
- **`timemark`** — current output time (e.g. `"0:00:12.50"`).
|
|
407
|
+
- **`frames`**, **`currentFps`**, **`currentKbps`** when available.
|
|
408
|
+
|
|
409
|
+
On completion, the executor emits a final `onProgress({ percent: 100 })` so the UI can show 100% before “Compose completed”.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## API reference
|
|
414
|
+
|
|
415
|
+
| Method | Description |
|
|
416
|
+
|--------|-------------|
|
|
417
|
+
| `cropTo916(inputPath, outputPath, opts?)` | Crop to 720×1280 vertical (9:16). |
|
|
418
|
+
| `slice(inputPath, outputPath, { start, end }, opts?)` | Extract one segment; start/end in seconds or time string. |
|
|
419
|
+
| `addSubtitles(inputPath, outputPath, srtPath, opts?)` | Burn SRT; optional ASS style options. |
|
|
420
|
+
| `extractThumbnail(inputPath, outputPath, time?)` | Single frame to image (.jpg / .png). |
|
|
421
|
+
| `addBackgroundMusic(inputPath, outputPath, musicPath, opts?)` | Mix video audio with music. |
|
|
422
|
+
| `extractAudioOnly(inputPath, outputPath, opts?)` | Audio only → .m4a (AAC) or .mp3. |
|
|
423
|
+
| `slicesWithTransitions(inputPath, outputPath, { slices, transition?, preset? }, opts?)` | Multiple slices joined with xfade/acrossfade. |
|
|
424
|
+
| `compose(inputPath, outputPath, pipeline?, opts?)` | Run preset `'shorts'` or pipeline array. |
|
|
425
|
+
|
|
426
|
+
**Common `opts`:** `onProgress: (progress) => {}`.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Advanced: filter builders
|
|
431
|
+
|
|
432
|
+
For tests or custom FFmpeg graphs you can use the low-level filter helpers.
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
const {
|
|
436
|
+
cropTo916Filter,
|
|
437
|
+
subtitleFilter,
|
|
438
|
+
amixFilter,
|
|
439
|
+
buildSlicesWithTransitionsFilter,
|
|
440
|
+
SHORTS_WIDTH,
|
|
441
|
+
SHORTS_HEIGHT,
|
|
442
|
+
} = require('ffmpeg-framecraft');
|
|
443
|
+
|
|
444
|
+
// Build -vf style filter strings (no execution)
|
|
445
|
+
cropTo916Filter(1920, 1080);
|
|
446
|
+
// => "crop=607:1080:656:0,scale=720:1280"
|
|
447
|
+
|
|
448
|
+
subtitleFilter('/path/to/subs.srt', { fontSize: 24 });
|
|
449
|
+
// => "subtitles='...':force_style='FontSize=24'"
|
|
450
|
+
|
|
451
|
+
amixFilter(true);
|
|
452
|
+
// => "[0:a][1:a]amix=inputs=2:duration=shortest[aout]"
|
|
453
|
+
|
|
454
|
+
// Slices + transitions: pass normalized slices and hasAudio
|
|
455
|
+
const slices = [
|
|
456
|
+
{ startSeconds: 0, endSeconds: 10, transition: null },
|
|
457
|
+
{ startSeconds: 20, endSeconds: 30, transition: { type: 'fade', duration: 0.5 } },
|
|
458
|
+
];
|
|
459
|
+
const { filterComplex, mapVideo, mapAudio } = buildSlicesWithTransitionsFilter(slices, true);
|
|
460
|
+
// Use filterComplex with -filter_complex and mapVideo/mapAudio with -map
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## License
|
|
466
|
+
|
|
467
|
+
ISC
|
package/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { FramecraftEngine } = require('./src/engine');
|
|
2
|
+
const { cropTo916Filter, subtitleFilter, amixFilter, buildSlicesWithTransitionsFilter, SHORTS_WIDTH, SHORTS_HEIGHT } = require('./src/filters');
|
|
3
|
+
const { PRESETS: TRANSITION_PRESETS, getTransition } = require('./src/presets/transitions');
|
|
4
|
+
const {
|
|
5
|
+
youtubeShortPreset,
|
|
6
|
+
tiktokPreset,
|
|
7
|
+
instagramReelsPreset,
|
|
8
|
+
shortsPresetConfig,
|
|
9
|
+
PLATFORM_PRESETS,
|
|
10
|
+
getPreset,
|
|
11
|
+
} = require('./src/presets/presets');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
FramecraftEngine,
|
|
15
|
+
cropTo916Filter,
|
|
16
|
+
subtitleFilter,
|
|
17
|
+
amixFilter,
|
|
18
|
+
buildSlicesWithTransitionsFilter,
|
|
19
|
+
SHORTS_WIDTH,
|
|
20
|
+
SHORTS_HEIGHT,
|
|
21
|
+
TRANSITION_PRESETS,
|
|
22
|
+
getTransition,
|
|
23
|
+
youtubeShortPreset,
|
|
24
|
+
tiktokPreset,
|
|
25
|
+
instagramReelsPreset,
|
|
26
|
+
shortsPresetConfig,
|
|
27
|
+
PLATFORM_PRESETS,
|
|
28
|
+
getPreset,
|
|
29
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ffmpeg-framecraft",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FFmpeg-based video processing: crop, slice, transitions, subtitles, audio extraction",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test test/"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md",
|
|
12
|
+
"index.js",
|
|
13
|
+
"src/"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ffmpeg",
|
|
17
|
+
"video",
|
|
18
|
+
"crop",
|
|
19
|
+
"slice",
|
|
20
|
+
"transitions",
|
|
21
|
+
"subtitles",
|
|
22
|
+
"9:16",
|
|
23
|
+
"shorts",
|
|
24
|
+
"reels",
|
|
25
|
+
"tiktok"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/dxmari/ffmpeg-framecraft",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/dxmari/ffmpeg-framecraft/issues"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/dxmari/ffmpeg-framecraft.git"
|
|
37
|
+
},
|
|
38
|
+
"author": "Mariselvam",
|
|
39
|
+
"license": "ISC",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"fluent-ffmpeg": "^2.1.3"
|
|
42
|
+
}
|
|
43
|
+
}
|