peasy-video 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peasy Tools
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # peasy-video-js
2
+
3
+ [![npm](https://img.shields.io/npm/v/peasy-video-js)](https://www.npmjs.com/package/peasy-video-js)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)](https://www.typescriptlang.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Video processing library for Node.js -- trim, resize, rotate, extract audio, generate thumbnails, convert to GIF, concatenate clips, adjust speed, and reverse video. FFmpeg-powered, TypeScript-first with full type safety. Handles MP4, WebM, MKV, AVI, MOV, and any container format supported by FFmpeg.
8
+
9
+ Built from [PeasyVideo](https://peasyvideo.com), a free online video toolkit with 15 browser-based tools for trimming, resizing, format conversion, thumbnail extraction, and GIF creation.
10
+
11
+ > **Try the interactive tools at [peasyvideo.com](https://peasyvideo.com)** -- video trimming, resizing, audio extraction, GIF conversion, and thumbnail generation
12
+
13
+ <p align="center">
14
+ <img src="demo.gif" alt="peasy-video-js demo — video info, thumbnail extraction, and format operations in Node.js" width="800">
15
+ </p>
16
+
17
+ ## Table of Contents
18
+
19
+ - [Prerequisites](#prerequisites)
20
+ - [Install](#install)
21
+ - [Quick Start](#quick-start)
22
+ - [What You Can Do](#what-you-can-do)
23
+ - [Video Info & Metadata](#video-info--metadata)
24
+ - [Trimming & Concatenation](#trimming--concatenation)
25
+ - [Resize & Transform](#resize--transform)
26
+ - [Audio Extraction](#audio-extraction)
27
+ - [Thumbnails](#thumbnails)
28
+ - [GIF Conversion](#gif-conversion)
29
+ - [Speed & Reverse](#speed--reverse)
30
+ - [TypeScript Types](#typescript-types)
31
+ - [API Reference](#api-reference)
32
+ - [Also Available for Python](#also-available-for-python)
33
+ - [Peasy Developer Tools](#peasy-developer-tools)
34
+ - [License](#license)
35
+
36
+ ## Prerequisites
37
+
38
+ peasy-video-js uses FFmpeg under the hood. Install it before using this library:
39
+
40
+ | Platform | Command |
41
+ |----------|---------|
42
+ | **macOS** | `brew install ffmpeg` |
43
+ | **Ubuntu/Debian** | `sudo apt install ffmpeg` |
44
+ | **Fedora/RHEL** | `sudo dnf install ffmpeg-free` |
45
+ | **Windows** | `choco install ffmpeg` |
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ npm install peasy-video-js
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```typescript
56
+ import { info, trim, resize, thumbnail, videoToGif } from "peasy-video-js";
57
+
58
+ // Get video metadata
59
+ const meta = await info("movie.mp4");
60
+ console.log(meta.width, meta.height, meta.duration); // 1920 1080 7200
61
+
62
+ // Trim to a 30-second clip
63
+ const clip = await trim("movie.mp4", { start: 60, duration: 30 });
64
+
65
+ // Resize to 720p
66
+ const resized = await resize("movie.mp4", { width: 1280, height: 720 });
67
+
68
+ // Extract a thumbnail at 5 seconds
69
+ const thumb = await thumbnail("movie.mp4", { time: 5 });
70
+
71
+ // Convert a clip to GIF
72
+ const gif = await videoToGif("clip.mp4", { fps: 15, width: 480 });
73
+ ```
74
+
75
+ ## What You Can Do
76
+
77
+ ### Video Info & Metadata
78
+
79
+ Extract comprehensive metadata from video files without decoding frames. FFprobe reads container headers and stream information to report resolution, frame rate, codec, bitrate, duration, and whether an audio track is present.
80
+
81
+ ```typescript
82
+ import { info } from "peasy-video-js";
83
+
84
+ // Extract video metadata using FFprobe
85
+ const meta = await info("presentation.mp4");
86
+ console.log(meta.width); // 1920
87
+ console.log(meta.height); // 1080
88
+ console.log(meta.fps); // 30
89
+ console.log(meta.duration); // 3600.5 (seconds)
90
+ console.log(meta.codec); // "h264"
91
+ console.log(meta.hasAudio); // true
92
+ console.log(meta.bitrate); // 5000000
93
+ ```
94
+
95
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
96
+
97
+ ### Trimming & Concatenation
98
+
99
+ Video trimming extracts a segment using keyframe-accurate seeking. Concatenation joins multiple clips sequentially using the FFmpeg concat demuxer, maintaining codec compatibility across segments.
100
+
101
+ ```typescript
102
+ import { trim, concatenate } from "peasy-video-js";
103
+
104
+ // Extract a 30-second clip starting at 1 minute
105
+ const clip = await trim("lecture.mp4", { start: 60, duration: 30 });
106
+
107
+ // Trim from start to end time
108
+ const intro = await trim("movie.mp4", { start: 0, end: 15 });
109
+
110
+ // Join multiple clips into one video
111
+ const combined = await concatenate([
112
+ "chapter1.mp4",
113
+ "chapter2.mp4",
114
+ "chapter3.mp4",
115
+ ]);
116
+ ```
117
+
118
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
119
+
120
+ ### Resize & Transform
121
+
122
+ Video resizing scales frames to target dimensions while maintaining aspect ratio. Rotation applies transpose filters for 90/180/270 degree rotations, handling both the video stream and any embedded rotation metadata.
123
+
124
+ | Resolution | Dimensions | Common Name |
125
+ |-----------|------------|-------------|
126
+ | 4K UHD | 3840 x 2160 | Ultra HD |
127
+ | 1080p | 1920 x 1080 | Full HD |
128
+ | 720p | 1280 x 720 | HD |
129
+ | 480p | 854 x 480 | SD |
130
+
131
+ ```typescript
132
+ import { resize, rotate } from "peasy-video-js";
133
+
134
+ // Resize to 720p (maintains aspect ratio)
135
+ const hd = await resize("4k-video.mp4", { width: 1280, height: 720 });
136
+
137
+ // Resize by width only (auto-calculates height)
138
+ const small = await resize("video.mp4", { width: 640 });
139
+
140
+ // Rotate 90 degrees clockwise
141
+ const rotated = await rotate("portrait.mp4", { degrees: 90 });
142
+
143
+ // Rotate 180 degrees (flip upside down)
144
+ const flipped = await rotate("upside-down.mp4", { degrees: 180 });
145
+ ```
146
+
147
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
148
+
149
+ ### Audio Extraction
150
+
151
+ Extract the audio track from a video file as a standalone audio file, or strip the audio track entirely to produce a silent video. Audio extraction preserves the original codec when possible, avoiding re-encoding for faster processing.
152
+
153
+ ```typescript
154
+ import { extractAudio, stripAudio } from "peasy-video-js";
155
+
156
+ // Extract audio as MP3
157
+ const audio = await extractAudio("interview.mp4", "mp3");
158
+
159
+ // Extract audio as WAV (lossless)
160
+ const wav = await extractAudio("concert.mp4", "wav");
161
+
162
+ // Remove audio track from video (silent output)
163
+ const silent = await stripAudio("presentation.mp4");
164
+ ```
165
+
166
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
167
+
168
+ ### Thumbnails
169
+
170
+ Extract individual frames as images for preview thumbnails, video galleries, or content analysis. Single-frame extraction uses precise seeking, while multi-frame extraction distributes captures evenly across the video duration.
171
+
172
+ ```typescript
173
+ import { thumbnail, thumbnails } from "peasy-video-js";
174
+
175
+ // Extract a frame at 5 seconds as PNG
176
+ const frame = await thumbnail("video.mp4", { time: 5 });
177
+
178
+ // Extract a frame with custom dimensions
179
+ const small = await thumbnail("video.mp4", {
180
+ time: 10,
181
+ width: 320,
182
+ height: 180,
183
+ });
184
+
185
+ // Generate 10 evenly-spaced thumbnails
186
+ const frames = await thumbnails("video.mp4", 10, { width: 320 });
187
+ // Returns array of paths: ["/tmp/peasy-video-xxx-0.png", ...]
188
+ ```
189
+
190
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
191
+
192
+ ### GIF Conversion
193
+
194
+ Convert video clips to animated GIFs with palette optimization for smaller file sizes and better color reproduction. Convert GIFs back to MP4 for efficient playback and embedding.
195
+
196
+ ```typescript
197
+ import { videoToGif, gifToVideo } from "peasy-video-js";
198
+
199
+ // Convert video to GIF with custom settings
200
+ const gif = await videoToGif("clip.mp4", {
201
+ fps: 15, // frames per second
202
+ width: 480, // pixel width
203
+ start: 2, // start at 2 seconds
204
+ duration: 5, // 5-second GIF
205
+ });
206
+
207
+ // Convert GIF back to MP4 (much smaller file size)
208
+ const mp4 = await gifToVideo("animation.gif");
209
+ ```
210
+
211
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
212
+
213
+ ### Speed & Reverse
214
+
215
+ Adjust playback speed using FFmpeg's `setpts` (video) and `atempo` (audio) filters. Speed factors below 1.0 create slow motion, above 1.0 create fast motion. Reverse plays the video backwards, re-encoding all frames.
216
+
217
+ ```typescript
218
+ import { speed, reverseVideo } from "peasy-video-js";
219
+
220
+ // Double the playback speed
221
+ const fast = await speed("lecture.mp4", { factor: 2.0 });
222
+
223
+ // Slow motion at half speed
224
+ const slow = await speed("action.mp4", { factor: 0.5 });
225
+
226
+ // Reverse the entire video
227
+ const reversed = await reverseVideo("clip.mp4");
228
+ ```
229
+
230
+ Learn more: [Peasy Video Tools](https://peasyvideo.com) · [Glossary](https://peasytools.com/glossary/)
231
+
232
+ ## TypeScript Types
233
+
234
+ ```typescript
235
+ import type {
236
+ VideoFormat,
237
+ VideoInfo,
238
+ TrimOptions,
239
+ ResizeOptions,
240
+ RotateOptions,
241
+ ThumbnailOptions,
242
+ GifOptions,
243
+ SpeedOptions,
244
+ ThumbnailResult,
245
+ } from "peasy-video-js";
246
+
247
+ // VideoFormat — "mp4" | "webm" | "mkv" | "avi" | "mov" | "gif"
248
+ const format: VideoFormat = "mp4";
249
+
250
+ // VideoInfo — metadata from info()
251
+ const meta: VideoInfo = {
252
+ duration: 120.5,
253
+ width: 1920,
254
+ height: 1080,
255
+ fps: 30,
256
+ codec: "h264",
257
+ format: "mov,mp4,m4a,3gp,3g2,mj2",
258
+ bitrate: 5_000_000,
259
+ size: 75_000_000,
260
+ hasAudio: true,
261
+ };
262
+ ```
263
+
264
+ ## API Reference
265
+
266
+ | Function | Description |
267
+ |----------|-------------|
268
+ | `info(input)` | Get video metadata (resolution, fps, codec, duration, bitrate) |
269
+ | `trim(input, options)` | Extract a segment by start/end/duration |
270
+ | `resize(input, options)` | Scale video to target dimensions |
271
+ | `rotate(input, options)` | Rotate video 90/180/270 degrees |
272
+ | `concatenate(inputs)` | Join multiple video files sequentially |
273
+ | `extractAudio(input, format?)` | Extract audio track as standalone file |
274
+ | `stripAudio(input)` | Remove audio track from video |
275
+ | `thumbnail(input, options?)` | Extract a single frame as PNG |
276
+ | `thumbnails(input, count, options?)` | Extract multiple evenly-spaced frames |
277
+ | `videoToGif(input, options?)` | Convert video clip to animated GIF |
278
+ | `gifToVideo(input)` | Convert GIF to MP4 |
279
+ | `reverseVideo(input)` | Play video in reverse |
280
+ | `speed(input, options)` | Adjust playback speed (0.5x to 4.0x) |
281
+
282
+ ## Also Available for Python
283
+
284
+ ```bash
285
+ pip install peasy-video
286
+ ```
287
+
288
+ The Python package provides the same 13 video operations with CLI and moviepy engine. See [peasy-video on PyPI](https://pypi.org/project/peasy-video/).
289
+
290
+ ## Peasy Developer Tools
291
+
292
+ | Package | PyPI | npm | Description |
293
+ |---------|------|-----|-------------|
294
+ | peasy-pdf | [PyPI](https://pypi.org/project/peasy-pdf/) | [npm](https://www.npmjs.com/package/peasy-pdf-js) | PDF merge, split, compress, rotate, watermark |
295
+ | peasy-image | [PyPI](https://pypi.org/project/peasy-image/) | [npm](https://www.npmjs.com/package/peasy-image-js) | Image resize, crop, compress, convert, watermark |
296
+ | peasytext | [PyPI](https://pypi.org/project/peasytext/) | [npm](https://www.npmjs.com/package/peasytext-js) | Text analysis, case conversion, slugs, word count |
297
+ | peasy-css | [PyPI](https://pypi.org/project/peasy-css/) | [npm](https://www.npmjs.com/package/peasy-css-js) | CSS gradients, shadows, flexbox, grid generators |
298
+ | peasy-compress | [PyPI](https://pypi.org/project/peasy-compress/) | [npm](https://www.npmjs.com/package/peasy-compress-js) | ZIP, gzip, brotli, deflate compression |
299
+ | peasy-document | [PyPI](https://pypi.org/project/peasy-document/) | [npm](https://www.npmjs.com/package/peasy-document-js) | Markdown, HTML, CSV, JSON, YAML conversion |
300
+ | peasy-audio | [PyPI](https://pypi.org/project/peasy-audio/) | [npm](https://www.npmjs.com/package/peasy-audio-js) | Audio convert, trim, merge, normalize, effects |
301
+ | **peasy-video** | [PyPI](https://pypi.org/project/peasy-video/) | **[npm](https://www.npmjs.com/package/peasy-video-js)** | **Video trim, resize, thumbnails, GIF conversion** |
302
+
303
+ Part of the [Peasy](https://peasytools.com) developer tools ecosystem.
304
+
305
+ ## License
306
+
307
+ MIT
@@ -0,0 +1,264 @@
1
+ /**
2
+ * peasy-video-js — Type definitions for video processing.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ /** Supported video container formats. */
7
+ type VideoFormat = "mp4" | "webm" | "mkv" | "avi" | "mov" | "gif";
8
+ /** Video file metadata. */
9
+ interface VideoInfo {
10
+ duration: number;
11
+ width: number;
12
+ height: number;
13
+ fps: number;
14
+ codec: string;
15
+ format: string;
16
+ bitrate: number;
17
+ size: number;
18
+ hasAudio: boolean;
19
+ }
20
+ /** Options for video trimming. */
21
+ interface TrimOptions {
22
+ start?: number;
23
+ end?: number;
24
+ duration?: number;
25
+ }
26
+ /** Options for video resizing. */
27
+ interface ResizeOptions {
28
+ width?: number;
29
+ height?: number;
30
+ fit?: "contain" | "cover" | "fill";
31
+ }
32
+ /** Options for video rotation. */
33
+ interface RotateOptions {
34
+ degrees: 90 | 180 | 270;
35
+ }
36
+ /** Options for thumbnail extraction. */
37
+ interface ThumbnailOptions {
38
+ time?: number;
39
+ width?: number;
40
+ height?: number;
41
+ }
42
+ /** Options for GIF conversion. */
43
+ interface GifOptions {
44
+ fps?: number;
45
+ width?: number;
46
+ start?: number;
47
+ duration?: number;
48
+ }
49
+ /** Options for speed adjustment. */
50
+ interface SpeedOptions {
51
+ factor: number;
52
+ }
53
+ /** Thumbnail result. */
54
+ interface ThumbnailResult {
55
+ path: string;
56
+ time: number;
57
+ width: number;
58
+ height: number;
59
+ }
60
+
61
+ /**
62
+ * peasy-video-js — Video processing engine powered by FFmpeg.
63
+ *
64
+ * 13 functions: info, trim, resize, rotate, concatenate, extractAudio,
65
+ * stripAudio, thumbnail, thumbnails, videoToGif, gifToVideo, reverseVideo, speed.
66
+ * All output functions return the path to the generated file.
67
+ *
68
+ * @packageDocumentation
69
+ */
70
+
71
+ /**
72
+ * Get metadata for a video file using ffprobe.
73
+ *
74
+ * @param input - Path to the video file
75
+ * @returns Video metadata including duration, dimensions, fps, codec, and more
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const metadata = await info("sample.mp4");
80
+ * console.log(metadata.duration); // 120.5
81
+ * console.log(metadata.width); // 1920
82
+ * console.log(metadata.height); // 1080
83
+ * ```
84
+ */
85
+ declare function info(input: string): Promise<VideoInfo>;
86
+ /**
87
+ * Trim a video to a specific time range.
88
+ *
89
+ * @param input - Path to the input video
90
+ * @param options - Trim options: start, end, and/or duration in seconds
91
+ * @returns Path to the trimmed output file
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Trim from 10s to 30s
96
+ * const trimmed = await trim("input.mp4", { start: 10, end: 30 });
97
+ *
98
+ * // Trim first 5 seconds
99
+ * const first5 = await trim("input.mp4", { duration: 5 });
100
+ * ```
101
+ */
102
+ declare function trim(input: string, options: TrimOptions): Promise<string>;
103
+ /**
104
+ * Resize a video to the specified dimensions.
105
+ *
106
+ * @param input - Path to the input video
107
+ * @param options - Resize options: width, height, and fit mode
108
+ * @returns Path to the resized output file
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * // Resize to 1280x720
113
+ * const resized = await resize("input.mp4", { width: 1280, height: 720 });
114
+ *
115
+ * // Scale width to 640, maintain aspect ratio
116
+ * const scaled = await resize("input.mp4", { width: 640 });
117
+ * ```
118
+ */
119
+ declare function resize(input: string, options: ResizeOptions): Promise<string>;
120
+ /**
121
+ * Rotate a video by 90, 180, or 270 degrees.
122
+ *
123
+ * @param input - Path to the input video
124
+ * @param options - Rotation options: degrees (90, 180, or 270)
125
+ * @returns Path to the rotated output file
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const rotated = await rotate("input.mp4", { degrees: 90 });
130
+ * ```
131
+ */
132
+ declare function rotate(input: string, options: RotateOptions): Promise<string>;
133
+ /**
134
+ * Concatenate multiple video files into one.
135
+ *
136
+ * @param inputs - Array of paths to video files (in order)
137
+ * @returns Path to the concatenated output file
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const joined = await concatenate(["part1.mp4", "part2.mp4", "part3.mp4"]);
142
+ * ```
143
+ */
144
+ declare function concatenate(inputs: string[]): Promise<string>;
145
+ /**
146
+ * Extract the audio track from a video file.
147
+ *
148
+ * @param input - Path to the input video
149
+ * @param format - Audio output format (default: "mp3")
150
+ * @returns Path to the extracted audio file
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const audio = await extractAudio("video.mp4"); // MP3
155
+ * const wav = await extractAudio("video.mp4", "wav"); // WAV
156
+ * ```
157
+ */
158
+ declare function extractAudio(input: string, format?: string): Promise<string>;
159
+ /**
160
+ * Remove the audio track from a video, producing a silent video.
161
+ *
162
+ * @param input - Path to the input video
163
+ * @returns Path to the silent output video
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const silent = await stripAudio("video-with-music.mp4");
168
+ * ```
169
+ */
170
+ declare function stripAudio(input: string): Promise<string>;
171
+ /**
172
+ * Extract a single thumbnail frame from a video.
173
+ *
174
+ * @param input - Path to the input video
175
+ * @param options - Thumbnail options: time (seconds), width, height
176
+ * @returns Path to the generated thumbnail image (PNG)
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * // Thumbnail at 5 seconds
181
+ * const thumb = await thumbnail("video.mp4", { time: 5 });
182
+ *
183
+ * // Thumbnail at start with custom size
184
+ * const small = await thumbnail("video.mp4", { width: 320, height: 180 });
185
+ * ```
186
+ */
187
+ declare function thumbnail(input: string, options?: ThumbnailOptions): Promise<string>;
188
+ /**
189
+ * Extract multiple evenly-spaced thumbnails from a video.
190
+ *
191
+ * @param input - Path to the input video
192
+ * @param count - Number of thumbnails to extract
193
+ * @param options - Optional width and height for the thumbnails
194
+ * @returns Array of paths to the generated thumbnail images (PNG)
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * // Extract 5 thumbnails spread across the video
199
+ * const thumbs = await thumbnails("video.mp4", 5);
200
+ * ```
201
+ */
202
+ declare function thumbnails(input: string, count: number, options?: Pick<ThumbnailOptions, "width" | "height">): Promise<string[]>;
203
+ /**
204
+ * Convert a video to an animated GIF.
205
+ *
206
+ * Uses a two-pass palette generation for high-quality GIF output.
207
+ *
208
+ * @param input - Path to the input video
209
+ * @param options - GIF options: fps, width, start time, duration
210
+ * @returns Path to the generated GIF file
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // Convert full video to GIF at 10fps
215
+ * const gif = await videoToGif("input.mp4", { fps: 10, width: 480 });
216
+ *
217
+ * // Convert a 3-second segment starting at 5s
218
+ * const clip = await videoToGif("input.mp4", { start: 5, duration: 3 });
219
+ * ```
220
+ */
221
+ declare function videoToGif(input: string, options?: GifOptions): Promise<string>;
222
+ /**
223
+ * Convert a GIF to a video (MP4).
224
+ *
225
+ * @param input - Path to the input GIF file
226
+ * @returns Path to the generated MP4 video file
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * const video = await gifToVideo("animation.gif");
231
+ * ```
232
+ */
233
+ declare function gifToVideo(input: string): Promise<string>;
234
+ /**
235
+ * Reverse a video so it plays backwards.
236
+ *
237
+ * @param input - Path to the input video
238
+ * @returns Path to the reversed output video
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const reversed = await reverseVideo("input.mp4");
243
+ * ```
244
+ */
245
+ declare function reverseVideo(input: string): Promise<string>;
246
+ /**
247
+ * Change the playback speed of a video.
248
+ *
249
+ * @param input - Path to the input video
250
+ * @param options - Speed options: factor (>1 = faster, <1 = slower)
251
+ * @returns Path to the speed-adjusted output video
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * // 2x speed
256
+ * const fast = await speed("input.mp4", { factor: 2 });
257
+ *
258
+ * // Half speed (slow motion)
259
+ * const slow = await speed("input.mp4", { factor: 0.5 });
260
+ * ```
261
+ */
262
+ declare function speed(input: string, options: SpeedOptions): Promise<string>;
263
+
264
+ export { type GifOptions, type ResizeOptions, type RotateOptions, type SpeedOptions, type ThumbnailOptions, type ThumbnailResult, type TrimOptions, type VideoFormat, type VideoInfo, concatenate, extractAudio, gifToVideo, info, resize, reverseVideo, rotate, speed, stripAudio, thumbnail, thumbnails, trim, videoToGif };
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ // src/engine.ts
2
+ import ffmpeg from "fluent-ffmpeg";
3
+ import { tmpdir } from "os";
4
+ import { join, extname } from "path";
5
+ import { randomUUID } from "crypto";
6
+ import { writeFileSync } from "fs";
7
+ function tmpOutput(ext) {
8
+ return join(tmpdir(), `peasy-video-${randomUUID()}.${ext}`);
9
+ }
10
+ function parseFps(rate) {
11
+ if (!rate) return 0;
12
+ const parts = rate.split("/");
13
+ if (parts.length === 2) {
14
+ const num = Number(parts[0]);
15
+ const den = Number(parts[1]);
16
+ return den > 0 ? num / den : 0;
17
+ }
18
+ return Number(rate) || 0;
19
+ }
20
+ function run(command, outputPath) {
21
+ return new Promise((resolve, reject) => {
22
+ command.output(outputPath).on("end", () => resolve(outputPath)).on("error", (err) => reject(err)).run();
23
+ });
24
+ }
25
+ function getExt(filePath) {
26
+ const ext = extname(filePath).slice(1).toLowerCase();
27
+ return ext || "mp4";
28
+ }
29
+ function info(input) {
30
+ return new Promise((resolve, reject) => {
31
+ ffmpeg.ffprobe(input, (err, metadata) => {
32
+ if (err) return reject(err);
33
+ const video = metadata.streams.find((s) => s.codec_type === "video");
34
+ const audio = metadata.streams.find((s) => s.codec_type === "audio");
35
+ resolve({
36
+ duration: metadata.format.duration ?? 0,
37
+ width: video?.width ?? 0,
38
+ height: video?.height ?? 0,
39
+ fps: parseFps(video?.r_frame_rate),
40
+ codec: video?.codec_name ?? "unknown",
41
+ format: metadata.format.format_name ?? "unknown",
42
+ bitrate: metadata.format.bit_rate ? Number(metadata.format.bit_rate) : 0,
43
+ size: metadata.format.size ? Number(metadata.format.size) : 0,
44
+ hasAudio: !!audio
45
+ });
46
+ });
47
+ });
48
+ }
49
+ function trim(input, options) {
50
+ const output = tmpOutput(getExt(input));
51
+ const cmd = ffmpeg(input);
52
+ if (options.start !== void 0) {
53
+ cmd.setStartTime(options.start);
54
+ }
55
+ if (options.end !== void 0 && options.start !== void 0) {
56
+ cmd.setDuration(options.end - options.start);
57
+ } else if (options.duration !== void 0) {
58
+ cmd.setDuration(options.duration);
59
+ }
60
+ cmd.outputOptions("-c", "copy");
61
+ return run(cmd, output);
62
+ }
63
+ function resize(input, options) {
64
+ const output = tmpOutput(getExt(input));
65
+ const cmd = ffmpeg(input);
66
+ let scaleFilter;
67
+ if (options.width && options.height) {
68
+ if (options.fit === "cover") {
69
+ scaleFilter = `scale=${options.width}:${options.height}:force_original_aspect_ratio=increase,crop=${options.width}:${options.height}`;
70
+ } else if (options.fit === "contain") {
71
+ scaleFilter = `scale=${options.width}:${options.height}:force_original_aspect_ratio=decrease,pad=${options.width}:${options.height}:(ow-iw)/2:(oh-ih)/2`;
72
+ } else {
73
+ scaleFilter = `scale=${options.width}:${options.height}`;
74
+ }
75
+ } else if (options.width) {
76
+ scaleFilter = `scale=${options.width}:-2`;
77
+ } else if (options.height) {
78
+ scaleFilter = `scale=-2:${options.height}`;
79
+ } else {
80
+ return run(cmd, output);
81
+ }
82
+ cmd.videoFilter(scaleFilter);
83
+ return run(cmd, output);
84
+ }
85
+ function rotate(input, options) {
86
+ const output = tmpOutput(getExt(input));
87
+ const cmd = ffmpeg(input);
88
+ switch (options.degrees) {
89
+ case 90:
90
+ cmd.videoFilter("transpose=1");
91
+ break;
92
+ case 180:
93
+ cmd.videoFilter("transpose=1,transpose=1");
94
+ break;
95
+ case 270:
96
+ cmd.videoFilter("transpose=2");
97
+ break;
98
+ }
99
+ return run(cmd, output);
100
+ }
101
+ function concatenate(inputs) {
102
+ if (inputs.length < 2) {
103
+ return Promise.reject(new Error("At least 2 inputs are required for concatenation"));
104
+ }
105
+ const output = tmpOutput(getExt(inputs[0]));
106
+ const listPath = tmpOutput("txt");
107
+ const listContent = inputs.map((f) => `file '${f}'`).join("\n");
108
+ writeFileSync(listPath, listContent, "utf-8");
109
+ const cmd = ffmpeg().input(listPath).inputOptions("-f", "concat", "-safe", "0").outputOptions("-c", "copy");
110
+ return run(cmd, output);
111
+ }
112
+ function extractAudio(input, format = "mp3") {
113
+ const output = tmpOutput(format);
114
+ const cmd = ffmpeg(input).noVideo();
115
+ return run(cmd, output);
116
+ }
117
+ function stripAudio(input) {
118
+ const output = tmpOutput(getExt(input));
119
+ const cmd = ffmpeg(input).noAudio().outputOptions("-c:v", "copy");
120
+ return run(cmd, output);
121
+ }
122
+ function thumbnail(input, options = {}) {
123
+ const output = tmpOutput("png");
124
+ const cmd = ffmpeg(input);
125
+ if (options.time !== void 0) {
126
+ cmd.seekInput(options.time);
127
+ }
128
+ cmd.frames(1);
129
+ if (options.width || options.height) {
130
+ const w = options.width ?? -2;
131
+ const h = options.height ?? -2;
132
+ cmd.videoFilter(`scale=${w}:${h}`);
133
+ }
134
+ return run(cmd, output);
135
+ }
136
+ async function thumbnails(input, count, options = {}) {
137
+ const meta = await info(input);
138
+ const duration = meta.duration;
139
+ const results = [];
140
+ for (let i = 0; i < count; i++) {
141
+ const time = count > 1 ? i / (count - 1) * duration : 0;
142
+ const clampedTime = Math.min(time, Math.max(0, duration - 0.01));
143
+ const thumbPath = await thumbnail(input, {
144
+ time: clampedTime,
145
+ ...options
146
+ });
147
+ results.push(thumbPath);
148
+ }
149
+ return results;
150
+ }
151
+ function videoToGif(input, options = {}) {
152
+ const output = tmpOutput("gif");
153
+ const cmd = ffmpeg(input);
154
+ if (options.start !== void 0) {
155
+ cmd.setStartTime(options.start);
156
+ }
157
+ if (options.duration !== void 0) {
158
+ cmd.setDuration(options.duration);
159
+ }
160
+ const filters = [];
161
+ if (options.fps) {
162
+ filters.push(`fps=${options.fps}`);
163
+ }
164
+ if (options.width) {
165
+ filters.push(`scale=${options.width}:-1:flags=lanczos`);
166
+ }
167
+ if (filters.length > 0) {
168
+ cmd.videoFilter(filters.join(","));
169
+ }
170
+ return run(cmd, output);
171
+ }
172
+ function gifToVideo(input) {
173
+ const output = tmpOutput("mp4");
174
+ const cmd = ffmpeg(input).outputOptions("-movflags", "+faststart").outputOptions("-pix_fmt", "yuv420p");
175
+ return run(cmd, output);
176
+ }
177
+ function reverseVideo(input) {
178
+ const output = tmpOutput(getExt(input));
179
+ const cmd = ffmpeg(input).videoFilter("reverse").audioFilter("areverse");
180
+ return run(cmd, output);
181
+ }
182
+ function speed(input, options) {
183
+ if (options.factor <= 0) {
184
+ return Promise.reject(new Error("Speed factor must be positive"));
185
+ }
186
+ const output = tmpOutput(getExt(input));
187
+ const cmd = ffmpeg(input);
188
+ const ptsFactor = 1 / options.factor;
189
+ cmd.videoFilter(`setpts=${ptsFactor}*PTS`);
190
+ const atempoFilters = [];
191
+ let remaining = options.factor;
192
+ while (remaining > 100) {
193
+ atempoFilters.push("atempo=100.0");
194
+ remaining /= 100;
195
+ }
196
+ while (remaining < 0.5) {
197
+ atempoFilters.push("atempo=0.5");
198
+ remaining /= 0.5;
199
+ }
200
+ atempoFilters.push(`atempo=${remaining}`);
201
+ cmd.audioFilter(atempoFilters.join(","));
202
+ return run(cmd, output);
203
+ }
204
+ export {
205
+ concatenate,
206
+ extractAudio,
207
+ gifToVideo,
208
+ info,
209
+ resize,
210
+ reverseVideo,
211
+ rotate,
212
+ speed,
213
+ stripAudio,
214
+ thumbnail,
215
+ thumbnails,
216
+ trim,
217
+ videoToGif
218
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "peasy-video",
3
+ "version": "0.1.1",
4
+ "description": "Video processing library for Node.js \u2014 trim, resize, rotate, thumbnails, GIF conversion. FFmpeg-powered, TypeScript-first.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup src/index.ts --format esm --dts",
19
+ "test": "vitest run",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "video",
24
+ "mp4",
25
+ "ffmpeg",
26
+ "trim",
27
+ "resize",
28
+ "thumbnail",
29
+ "gif",
30
+ "peasy"
31
+ ],
32
+ "author": "Peasy Tools",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "url": "https://github.com/peasytools/peasy-video-js.git"
36
+ },
37
+ "homepage": "https://peasyvideo.com",
38
+ "dependencies": {
39
+ "fluent-ffmpeg": "^2.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/fluent-ffmpeg": "^2.1",
43
+ "tsup": "^8.0",
44
+ "typescript": "^5.7",
45
+ "vitest": "^3.0"
46
+ }
47
+ }