@remotion/renderer 3.0.17 → 3.0.18

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.
@@ -30,6 +30,9 @@ const combineVideos = async ({ files, filelistDir, output, onProgress, numberOfF
30
30
  remotion_1.Internals.isAudioCodec(codec) ? null : 'copy',
31
31
  '-c:a',
32
32
  (0, get_audio_codec_name_1.getAudioCodecName)(codec),
33
+ // Set max bitrate up to 1024kbps, will choose lower if that's too much
34
+ '-b:a',
35
+ '512K',
33
36
  codec === 'h264' ? '-movflags' : null,
34
37
  codec === 'h264' ? 'faststart' : null,
35
38
  '-shortest',
@@ -1,4 +1,7 @@
1
1
  export declare const createFfmpegComplexFilter: (filters: number) => Promise<{
2
- complexFilterFlag: [string, string] | null;
2
+ complexFilterFlag: [
3
+ string,
4
+ string
5
+ ] | null;
3
6
  cleanup: () => void;
4
7
  }>;
@@ -1,5 +1,4 @@
1
1
  /// <reference types="node" />
2
- /// <reference types="node" />
3
2
  import { FfmpegExecutable } from 'remotion';
4
3
  import { Readable } from 'stream';
5
4
  import { LastFrameOptions } from './last-frame-from-video-cache';
package/dist/get-port.js CHANGED
@@ -5,16 +5,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getDesiredPort = void 0;
7
7
  const net_1 = __importDefault(require("net"));
8
- const getAvailablePort = (portToTry) => new Promise((resolve, reject) => {
9
- const server = net_1.default.createServer();
10
- server.unref();
11
- server.on('error', reject);
12
- server.listen({ port: portToTry }, () => {
13
- const { port } = server.address();
14
- server.close(() => {
15
- resolve(port);
16
- });
8
+ const p_limit_1 = require("./p-limit");
9
+ const getAvailablePort = (portToTry) => new Promise((resolve) => {
10
+ let status = 'unavailable';
11
+ const host = '127.0.0.1';
12
+ const socket = new net_1.default.Socket();
13
+ socket.on('connect', () => {
14
+ status = 'unavailable';
15
+ socket.destroy();
17
16
  });
17
+ socket.setTimeout(1000);
18
+ socket.on('timeout', () => {
19
+ status = 'unavailable';
20
+ socket.destroy();
21
+ resolve(status);
22
+ });
23
+ socket.on('error', () => {
24
+ status = 'available';
25
+ });
26
+ socket.on('close', () => resolve(status));
27
+ socket.connect(portToTry, host);
18
28
  });
19
29
  const portCheckSequence = function* (ports) {
20
30
  if (ports) {
@@ -22,30 +32,18 @@ const portCheckSequence = function* (ports) {
22
32
  }
23
33
  yield 0; // Fall back to 0 if anything else failed
24
34
  };
25
- const isPortAvailable = async (port) => {
26
- try {
27
- await getAvailablePort(port);
28
- return true;
29
- }
30
- catch (error) {
31
- if (!['EADDRINUSE', 'EACCES'].includes(error.code)) {
32
- throw error;
33
- }
34
- return false;
35
- }
36
- };
37
35
  const getPort = async (from, to) => {
38
36
  const ports = makeRange(from, to);
39
37
  for (const port of portCheckSequence(ports)) {
40
- if (await isPortAvailable(port)) {
38
+ if ((await getAvailablePort(port)) === 'available') {
41
39
  return port;
42
40
  }
43
41
  }
44
42
  throw new Error('No available ports found');
45
43
  };
46
- const getDesiredPort = async (desiredPort, from, to) => {
44
+ const getDesiredPortUnlimited = async (desiredPort, from, to) => {
47
45
  if (typeof desiredPort !== 'undefined' &&
48
- (await isPortAvailable(desiredPort))) {
46
+ (await getAvailablePort(desiredPort)) === 'available') {
49
47
  return desiredPort;
50
48
  }
51
49
  const actualPort = await getPort(from, to);
@@ -55,6 +53,10 @@ const getDesiredPort = async (desiredPort, from, to) => {
55
53
  }
56
54
  return actualPort;
57
55
  };
56
+ const limit = (0, p_limit_1.pLimit)(1);
57
+ const getDesiredPort = (desiredPort, from, to) => {
58
+ return limit(() => getDesiredPortUnlimited(desiredPort, from, to));
59
+ };
58
60
  exports.getDesiredPort = getDesiredPort;
59
61
  const makeRange = (from, to) => {
60
62
  if (!Number.isInteger(from) || !Number.isInteger(to)) {
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { FfmpegExecutable } from 'remotion';
3
2
  export declare type LastFrameOptions = {
4
3
  ffmpegExecutable: FfmpegExecutable;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import puppeteer from 'puppeteer-core';
3
2
  import { ImageFormat } from 'remotion';
4
3
  export declare const provideScreenshot: ({ page, imageFormat, options, quality, }: {
@@ -1,3 +1,2 @@
1
- /// <reference types="node" />
2
1
  import { Page, ScreenshotOptions } from 'puppeteer-core';
3
2
  export declare const screenshot: (page: Page, options?: ScreenshotOptions) => Promise<Buffer | string | void>;
@@ -0,0 +1,2 @@
1
+ import { RenderMediaOptions } from './render-media';
2
+ export declare const renderGif: ({ parallelism, proResProfile, crf, composition, imageFormat, ffmpegExecutable, inputProps, pixelFormat, codec, envVariables, quality, frameRange, puppeteerInstance, outputLocation, onProgress, overwrite, onDownload, loop, skipNFrames, dumpBrowserLogs, onBrowserLog, onStart, timeoutInMilliseconds, chromiumOptions, scale, browserExecutable, port, cancelSignal, ...options }: RenderMediaOptions) => Promise<void>;
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderGif = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const remotion_1 = require("remotion");
11
+ const can_use_parallel_encoding_1 = require("./can-use-parallel-encoding");
12
+ const ensure_frames_in_order_1 = require("./ensure-frames-in-order");
13
+ const ensure_output_directory_1 = require("./ensure-output-directory");
14
+ const get_duration_from_frame_range_1 = require("./get-duration-from-frame-range");
15
+ const get_extension_from_codec_1 = require("./get-extension-from-codec");
16
+ const get_extension_of_filename_1 = require("./get-extension-of-filename");
17
+ const get_frame_to_render_1 = require("./get-frame-to-render");
18
+ const legacy_webpack_config_1 = require("./legacy-webpack-config");
19
+ const make_cancel_signal_1 = require("./make-cancel-signal");
20
+ const prespawn_ffmpeg_1 = require("./prespawn-ffmpeg");
21
+ const render_frames_1 = require("./render-frames");
22
+ const stitch_frames_to_gif_1 = require("./stitch-frames-to-gif");
23
+ const tmp_dir_1 = require("./tmp-dir");
24
+ const validate_even_dimensions_with_codec_1 = require("./validate-even-dimensions-with-codec");
25
+ const validate_output_filename_1 = require("./validate-output-filename");
26
+ const validate_scale_1 = require("./validate-scale");
27
+ const renderGif = ({ parallelism, proResProfile, crf, composition, imageFormat, ffmpegExecutable, inputProps, pixelFormat, codec, envVariables, quality, frameRange, puppeteerInstance, outputLocation, onProgress, overwrite, onDownload, loop, skipNFrames, dumpBrowserLogs, onBrowserLog, onStart, timeoutInMilliseconds, chromiumOptions, scale, browserExecutable, port, cancelSignal, ...options }) => {
28
+ remotion_1.Internals.validateQuality(quality);
29
+ if (typeof crf !== 'undefined' && crf !== null) {
30
+ remotion_1.Internals.validateSelectedCrfAndCodecCombination(crf, codec);
31
+ }
32
+ (0, validate_output_filename_1.validateOutputFilename)(codec, (0, get_extension_of_filename_1.getExtensionOfFilename)(outputLocation));
33
+ (0, validate_scale_1.validateScale)(scale);
34
+ const serveUrl = (0, legacy_webpack_config_1.getServeUrlWithFallback)(options);
35
+ let stitchStage = 'encoding';
36
+ let stitcherFfmpeg;
37
+ let preStitcher = null;
38
+ let encodedFrames = 0;
39
+ let renderedFrames = 0;
40
+ let renderedDoneIn = null;
41
+ let encodedDoneIn = null;
42
+ let cancelled = false;
43
+ const renderStart = Date.now();
44
+ const tmpdir = (0, tmp_dir_1.tmpDir)('pre-encode');
45
+ const parallelEncoding = (0, can_use_parallel_encoding_1.canUseParallelEncoding)(codec);
46
+ const actualImageFormat = imageFormat !== null && imageFormat !== void 0 ? imageFormat : 'jpeg';
47
+ const preEncodedFileLocation = parallelEncoding
48
+ ? path_1.default.join(tmpdir, 'pre-encode.' + (0, get_extension_from_codec_1.getFileExtensionFromCodec)(codec, 'chunk'))
49
+ : null;
50
+ const outputDir = parallelEncoding
51
+ ? null
52
+ : fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'react-motion-render'));
53
+ (0, validate_even_dimensions_with_codec_1.validateEvenDimensionsWithCodec)({
54
+ codec,
55
+ height: composition.height,
56
+ scale: scale !== null && scale !== void 0 ? scale : 1,
57
+ width: composition.width,
58
+ });
59
+ const callUpdate = () => {
60
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
61
+ encodedDoneIn,
62
+ encodedFrames,
63
+ renderedDoneIn,
64
+ renderedFrames,
65
+ stitchStage,
66
+ });
67
+ };
68
+ const realFrameRange = (0, get_frame_to_render_1.getRealFrameRange)(composition.durationInFrames, frameRange !== null && frameRange !== void 0 ? frameRange : null);
69
+ const cancelRenderFrames = (0, make_cancel_signal_1.makeCancelSignal)();
70
+ const cancelPrestitcher = (0, make_cancel_signal_1.makeCancelSignal)();
71
+ const cancelStitcher = (0, make_cancel_signal_1.makeCancelSignal)();
72
+ cancelSignal === null || cancelSignal === void 0 ? void 0 : cancelSignal(() => {
73
+ cancelRenderFrames.cancel();
74
+ });
75
+ const { waitForRightTimeOfFrameToBeInserted, setFrameToStitch, waitForFinish } = (0, ensure_frames_in_order_1.ensureFramesInOrder)(realFrameRange);
76
+ const actualGifFps = Math.floor(composition.fps / (skipNFrames + 1));
77
+ const createPrestitcherIfNecessary = async () => {
78
+ if (preEncodedFileLocation) {
79
+ preStitcher = await (0, prespawn_ffmpeg_1.prespawnFfmpeg)({
80
+ width: composition.width * (scale !== null && scale !== void 0 ? scale : 1),
81
+ height: composition.height * (scale !== null && scale !== void 0 ? scale : 1),
82
+ fps: actualGifFps,
83
+ outputLocation: preEncodedFileLocation,
84
+ pixelFormat,
85
+ codec,
86
+ proResProfile,
87
+ crf,
88
+ onProgress: (frame) => {
89
+ encodedFrames = frame;
90
+ callUpdate();
91
+ },
92
+ verbose: remotion_1.Internals.Logging.isEqualOrBelowLogLevel(remotion_1.Internals.Logging.getLogLevel(), 'verbose'),
93
+ ffmpegExecutable,
94
+ imageFormat: actualImageFormat,
95
+ signal: cancelPrestitcher.cancelSignal,
96
+ });
97
+ stitcherFfmpeg = preStitcher.task;
98
+ }
99
+ };
100
+ const waitForPrestitcherIfNecessary = async () => {
101
+ var _a;
102
+ if (stitcherFfmpeg) {
103
+ await waitForFinish();
104
+ (_a = stitcherFfmpeg === null || stitcherFfmpeg === void 0 ? void 0 : stitcherFfmpeg.stdin) === null || _a === void 0 ? void 0 : _a.end();
105
+ try {
106
+ await stitcherFfmpeg;
107
+ }
108
+ catch (err) {
109
+ throw new Error(preStitcher === null || preStitcher === void 0 ? void 0 : preStitcher.getLogs());
110
+ }
111
+ }
112
+ };
113
+ const happyPath = createPrestitcherIfNecessary()
114
+ .then(() => {
115
+ const renderFramesProc = (0, render_frames_1.renderFrames)({
116
+ config: composition,
117
+ onFrameUpdate: (frame) => {
118
+ stitchStage = 'gif';
119
+ renderedFrames = frame;
120
+ callUpdate();
121
+ },
122
+ parallelism,
123
+ outputDir,
124
+ skipNFrames,
125
+ onStart: (data) => {
126
+ renderedFrames = 0;
127
+ callUpdate();
128
+ onStart === null || onStart === void 0 ? void 0 : onStart(data);
129
+ },
130
+ inputProps,
131
+ envVariables,
132
+ imageFormat: actualImageFormat,
133
+ quality,
134
+ frameRange: frameRange !== null && frameRange !== void 0 ? frameRange : null,
135
+ puppeteerInstance,
136
+ onFrameBuffer: parallelEncoding
137
+ ? async (buffer, frame) => {
138
+ var _a;
139
+ await waitForRightTimeOfFrameToBeInserted(frame);
140
+ if (cancelled) {
141
+ return;
142
+ }
143
+ (_a = stitcherFfmpeg === null || stitcherFfmpeg === void 0 ? void 0 : stitcherFfmpeg.stdin) === null || _a === void 0 ? void 0 : _a.write(buffer);
144
+ setFrameToStitch(frame + 1);
145
+ }
146
+ : undefined,
147
+ serveUrl,
148
+ dumpBrowserLogs,
149
+ onBrowserLog,
150
+ onDownload,
151
+ timeoutInMilliseconds,
152
+ chromiumOptions,
153
+ scale,
154
+ ffmpegExecutable,
155
+ browserExecutable,
156
+ port,
157
+ cancelSignal: cancelRenderFrames.cancelSignal,
158
+ });
159
+ return renderFramesProc;
160
+ })
161
+ .then((renderFramesReturn) => {
162
+ return Promise.all([renderFramesReturn, waitForPrestitcherIfNecessary()]);
163
+ })
164
+ .then(([{ assetsInfo }]) => {
165
+ renderedDoneIn = Date.now() - renderStart;
166
+ callUpdate();
167
+ (0, ensure_output_directory_1.ensureOutputDirectory)(outputLocation);
168
+ const stitchStart = Date.now();
169
+ return Promise.all([
170
+ (0, stitch_frames_to_gif_1.stitchFramesToGif)({
171
+ width: composition.width * (scale !== null && scale !== void 0 ? scale : 1),
172
+ height: composition.height * (scale !== null && scale !== void 0 ? scale : 1),
173
+ fps: actualGifFps,
174
+ outputLocation,
175
+ internalOptions: {
176
+ preEncodedFileLocation,
177
+ imageFormat: actualImageFormat,
178
+ },
179
+ force: overwrite !== null && overwrite !== void 0 ? overwrite : remotion_1.Internals.DEFAULT_OVERWRITE,
180
+ pixelFormat,
181
+ codec,
182
+ proResProfile,
183
+ crf,
184
+ loop,
185
+ assetsInfo,
186
+ ffmpegExecutable,
187
+ onProgress: (frame) => {
188
+ stitchStage = 'gif';
189
+ encodedFrames = frame;
190
+ callUpdate();
191
+ },
192
+ verbose: remotion_1.Internals.Logging.isEqualOrBelowLogLevel(remotion_1.Internals.Logging.getLogLevel(), 'verbose'),
193
+ dir: outputDir !== null && outputDir !== void 0 ? outputDir : undefined,
194
+ cancelSignal: cancelStitcher.cancelSignal,
195
+ }),
196
+ stitchStart,
197
+ ]);
198
+ })
199
+ .then(([, stitchStart]) => {
200
+ encodedFrames = (0, get_duration_from_frame_range_1.getDurationFromFrameRange)(frameRange !== null && frameRange !== void 0 ? frameRange : null, composition.durationInFrames, skipNFrames);
201
+ encodedDoneIn = Date.now() - stitchStart;
202
+ callUpdate();
203
+ })
204
+ .catch((err) => {
205
+ /**
206
+ * When an error is thrown in renderFrames(...) (e.g., when delayRender() is used incorrectly), fs.unlinkSync(...) throws an error that the file is locked because ffmpeg is still running, and renderMedia returns it.
207
+ * Therefore we first kill the FFMPEG process before deleting the file
208
+ */
209
+ cancelled = true;
210
+ cancelRenderFrames.cancel();
211
+ cancelStitcher.cancel();
212
+ cancelPrestitcher.cancel();
213
+ if (stitcherFfmpeg !== undefined && stitcherFfmpeg.exitCode === null) {
214
+ const promise = new Promise((resolve) => {
215
+ setTimeout(() => {
216
+ resolve();
217
+ }, 2000);
218
+ stitcherFfmpeg.on('close', resolve);
219
+ });
220
+ stitcherFfmpeg.kill();
221
+ return promise.then(() => {
222
+ throw err;
223
+ });
224
+ }
225
+ throw err;
226
+ })
227
+ .finally(() => {
228
+ if (preEncodedFileLocation !== null &&
229
+ fs_1.default.existsSync(preEncodedFileLocation)) {
230
+ fs_1.default.unlinkSync(preEncodedFileLocation);
231
+ }
232
+ });
233
+ return Promise.race([
234
+ happyPath,
235
+ new Promise((_resolve, reject) => {
236
+ cancelSignal === null || cancelSignal === void 0 ? void 0 : cancelSignal(() => {
237
+ reject(new Error('renderMedia() got cancelled'));
238
+ });
239
+ }),
240
+ ]);
241
+ };
242
+ exports.renderGif = renderGif;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import puppeteer from 'puppeteer-core';
3
2
  import { ImageFormat } from 'remotion';
4
3
  export declare const screenshotDOMElement: ({ page, imageFormat, quality, opts, }: {
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { Page, ScreenshotOptions } from 'puppeteer-core';
3
2
  import { StillImageFormat } from 'remotion';
4
3
  export declare const _screenshotTask: (page: Page, format: StillImageFormat, options: ScreenshotOptions) => Promise<Buffer | string>;
@@ -0,0 +1 @@
1
+ export declare const slasher: (value: string) => string;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /* ! The MIT License (MIT) Copyright (c) 2014 Scott Corgan */
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.slasher = void 0;
8
+ // This is adopted from https://github.com/scottcorgan/glob-slash/
9
+ const path_1 = __importDefault(require("path"));
10
+ const normalize = (value) => path_1.default.posix.normalize(path_1.default.posix.join('/', value));
11
+ const slasher = (value) => value.charAt(0) === '!' ? `!${normalize(value.substr(1))}` : normalize(value);
12
+ exports.slasher = slasher;
@@ -0,0 +1,4 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ export declare const serveHandler: (request: IncomingMessage, response: ServerResponse, config: {
3
+ public: string;
4
+ }) => Promise<void>;
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.serveHandler = void 0;
7
+ // Native
8
+ const fs_1 = require("fs");
9
+ const path_1 = __importDefault(require("path"));
10
+ const url_1 = __importDefault(require("url"));
11
+ // Packages
12
+ const mime_types_1 = __importDefault(require("mime-types"));
13
+ const is_path_inside_1 = require("./is-path-inside");
14
+ const range_parser_1 = require("./range-parser");
15
+ const getHeaders = (absolutePath, stats) => {
16
+ const related = {};
17
+ const { base } = path_1.default.parse(absolutePath);
18
+ let defaultHeaders = {};
19
+ if (stats) {
20
+ defaultHeaders = {
21
+ 'Content-Length': String(stats.size),
22
+ 'Accept-Ranges': 'bytes',
23
+ };
24
+ defaultHeaders['Last-Modified'] = stats.mtime.toUTCString();
25
+ const contentType = mime_types_1.default.contentType(base);
26
+ if (contentType) {
27
+ defaultHeaders['Content-Type'] = contentType;
28
+ }
29
+ }
30
+ const headers = Object.assign(defaultHeaders, related);
31
+ for (const key in headers) {
32
+ if (headers[key] === null) {
33
+ delete headers[key];
34
+ }
35
+ }
36
+ return headers;
37
+ };
38
+ const getPossiblePaths = (relativePath, extension) => [
39
+ path_1.default.join(relativePath, `index${extension}`),
40
+ relativePath.endsWith('/')
41
+ ? relativePath.replace(/\/$/g, extension)
42
+ : relativePath + extension,
43
+ ].filter((item) => path_1.default.basename(item) !== extension);
44
+ const findRelated = async (current, relativePath) => {
45
+ const possible = getPossiblePaths(relativePath, '.html');
46
+ let stats = null;
47
+ for (let index = 0; index < possible.length; index++) {
48
+ const related = possible[index];
49
+ const absolutePath = path_1.default.join(current, related);
50
+ try {
51
+ stats = await fs_1.promises.lstat(absolutePath);
52
+ }
53
+ catch (err) {
54
+ if (err.code !== 'ENOENT' &&
55
+ err.code !== 'ENOTDIR') {
56
+ throw err;
57
+ }
58
+ }
59
+ if (stats) {
60
+ return {
61
+ stats,
62
+ absolutePath,
63
+ };
64
+ }
65
+ }
66
+ return null;
67
+ };
68
+ const sendError = (absolutePath, response, spec) => {
69
+ const { message, statusCode } = spec;
70
+ response.statusCode = statusCode;
71
+ const headers = getHeaders(absolutePath, null);
72
+ response.writeHead(statusCode, headers);
73
+ response.setHeader('content-type', 'application/json');
74
+ response.end(JSON.stringify({ statusCode, message }));
75
+ };
76
+ const internalError = (absolutePath, response) => {
77
+ return sendError(absolutePath, response, {
78
+ statusCode: 500,
79
+ code: 'internal_server_error',
80
+ message: 'A server error has occurred',
81
+ });
82
+ };
83
+ const serveHandler = async (request, response, config) => {
84
+ const cwd = process.cwd();
85
+ const current = path_1.default.resolve(cwd, config.public);
86
+ let relativePath = null;
87
+ try {
88
+ relativePath = decodeURIComponent(url_1.default.parse(request.url).pathname);
89
+ }
90
+ catch (err) {
91
+ return sendError('/', response, {
92
+ statusCode: 400,
93
+ code: 'bad_request',
94
+ message: 'Bad Request',
95
+ });
96
+ }
97
+ let absolutePath = path_1.default.join(current, relativePath);
98
+ // Prevent path traversal vulnerabilities. We could do this
99
+ // by ourselves, but using the package covers all the edge cases.
100
+ if (!(0, is_path_inside_1.isPathInside)(absolutePath, current)) {
101
+ return sendError(absolutePath, response, {
102
+ statusCode: 400,
103
+ code: 'bad_request',
104
+ message: 'Bad Request',
105
+ });
106
+ }
107
+ let stats = null;
108
+ // It's extremely important that we're doing multiple stat calls. This one
109
+ // right here could technically be removed, but then the program
110
+ // would be slower. Because for directories, we always want to see if a related file
111
+ // exists and then (after that), fetch the directory itself if no
112
+ // related file was found. However (for files, of which most have extensions), we should
113
+ // always stat right away.
114
+ //
115
+ // When simulating a file system without directory indexes, calculating whether a
116
+ // directory exists requires loading all the file paths and then checking if
117
+ // one of them includes the path of the directory. As that's a very
118
+ // performance-expensive thing to do, we need to ensure it's not happening if not really necessary.
119
+ if (path_1.default.extname(relativePath) !== '') {
120
+ try {
121
+ stats = await fs_1.promises.lstat(absolutePath);
122
+ }
123
+ catch (err) {
124
+ if (err.code !== 'ENOENT' &&
125
+ err.code !== 'ENOTDIR') {
126
+ return internalError(absolutePath, response);
127
+ }
128
+ }
129
+ }
130
+ if (!stats) {
131
+ try {
132
+ const related = await findRelated(current, relativePath);
133
+ if (related) {
134
+ ({ stats, absolutePath } = related);
135
+ }
136
+ }
137
+ catch (err) {
138
+ if (err.code !== 'ENOENT' &&
139
+ err.code !== 'ENOTDIR') {
140
+ return internalError(absolutePath, response);
141
+ }
142
+ }
143
+ try {
144
+ stats = await fs_1.promises.lstat(absolutePath);
145
+ }
146
+ catch (err) {
147
+ if (err.code !== 'ENOENT' &&
148
+ err.code !== 'ENOTDIR') {
149
+ return internalError(absolutePath, response);
150
+ }
151
+ }
152
+ }
153
+ if (stats === null || stats === void 0 ? void 0 : stats.isDirectory()) {
154
+ const directory = null;
155
+ const singleFile = null;
156
+ if (directory) {
157
+ const contentType = 'text/html; charset=utf-8';
158
+ response.statusCode = 200;
159
+ response.setHeader('Content-Type', contentType);
160
+ response.end('Is a directory');
161
+ return;
162
+ }
163
+ if (!singleFile) {
164
+ // The directory listing is disabled, so we want to
165
+ // render a 404 error.
166
+ stats = null;
167
+ }
168
+ }
169
+ const isSymLink = stats === null || stats === void 0 ? void 0 : stats.isSymbolicLink();
170
+ // There are two scenarios in which we want to reply with
171
+ // a 404 error: Either the path does not exist, or it is a
172
+ // symlink while the `symlinks` option is disabled (which it is by default).
173
+ if (!stats || isSymLink) {
174
+ // allow for custom 404 handling
175
+ return sendError(absolutePath, response, {
176
+ statusCode: 404,
177
+ code: 'not_found',
178
+ message: 'The requested path could not be found',
179
+ });
180
+ }
181
+ let streamOpts = null;
182
+ if (request.headers.range && stats.size) {
183
+ const range = (0, range_parser_1.rangeParser)(stats.size, request.headers.range);
184
+ if (typeof range === 'object' && range.type === 'bytes') {
185
+ const { start, end } = range.ranges[0];
186
+ streamOpts = {
187
+ start,
188
+ end,
189
+ };
190
+ response.statusCode = 206;
191
+ }
192
+ else {
193
+ response.statusCode = 416;
194
+ response.setHeader('Content-Range', `bytes */${stats.size}`);
195
+ }
196
+ }
197
+ let stream = null;
198
+ try {
199
+ stream = (0, fs_1.createReadStream)(absolutePath, streamOpts !== null && streamOpts !== void 0 ? streamOpts : {});
200
+ }
201
+ catch (err) {
202
+ return internalError(absolutePath, response);
203
+ }
204
+ const headers = getHeaders(absolutePath, stats);
205
+ if (streamOpts !== null) {
206
+ headers['Content-Range'] = `bytes ${streamOpts.start}-${streamOpts.end}/${stats.size}`;
207
+ headers['Content-Length'] = String(streamOpts.end - streamOpts.start + 1);
208
+ }
209
+ response.writeHead(response.statusCode || 200, headers);
210
+ stream.pipe(response);
211
+ };
212
+ exports.serveHandler = serveHandler;
@@ -0,0 +1 @@
1
+ export declare const isPathInside: (thePath: string, potentialParent: string) => boolean;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isPathInside = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const isPathInside = function (thePath, potentialParent) {
9
+ // For inside-directory checking, we want to allow trailing slashes, so normalize.
10
+ thePath = stripTrailingSep(thePath);
11
+ potentialParent = stripTrailingSep(potentialParent);
12
+ // Node treats only Windows as case-insensitive in its path module; we follow those conventions.
13
+ if (process.platform === 'win32') {
14
+ thePath = thePath.toLowerCase();
15
+ potentialParent = potentialParent.toLowerCase();
16
+ }
17
+ return (thePath.lastIndexOf(potentialParent, 0) === 0 &&
18
+ (thePath[potentialParent.length] === path_1.default.sep ||
19
+ thePath[potentialParent.length] === undefined));
20
+ };
21
+ exports.isPathInside = isPathInside;
22
+ function stripTrailingSep(thePath) {
23
+ if (thePath[thePath.length - 1] === path_1.default.sep) {
24
+ return thePath.slice(0, -1);
25
+ }
26
+ return thePath;
27
+ }
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * range-parser
3
+ * Copyright(c) 2012-2014 TJ Holowaychuk
4
+ * Copyright(c) 2015-2016 Douglas Christopher Wilson
5
+ * MIT Licensed
6
+ */
7
+ export declare const rangeParser: (size: number, str: string) => -1 | {
8
+ type: string;
9
+ ranges: {
10
+ start: number;
11
+ end: number;
12
+ }[];
13
+ } | -2;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /*!
3
+ * range-parser
4
+ * Copyright(c) 2012-2014 TJ Holowaychuk
5
+ * Copyright(c) 2015-2016 Douglas Christopher Wilson
6
+ * MIT Licensed
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.rangeParser = void 0;
10
+ const rangeParser = (size, str) => {
11
+ if (typeof str !== 'string') {
12
+ throw new TypeError('argument str must be a string');
13
+ }
14
+ const index = str.indexOf('=');
15
+ if (index === -1) {
16
+ return -2;
17
+ }
18
+ // split the range string
19
+ const arr = str.slice(index + 1).split(',');
20
+ const ranges = [];
21
+ // add ranges type
22
+ const type = str.slice(0, index);
23
+ // parse all ranges
24
+ for (let i = 0; i < arr.length; i++) {
25
+ const range = arr[i].split('-');
26
+ let start = parseInt(range[0], 10);
27
+ let end = parseInt(range[1], 10);
28
+ // -nnn
29
+ if (isNaN(start)) {
30
+ start = size - end;
31
+ end = size - 1;
32
+ // nnn-
33
+ }
34
+ else if (isNaN(end)) {
35
+ end = size - 1;
36
+ }
37
+ // limit last-byte-pos to current length
38
+ if (end > size - 1) {
39
+ end = size - 1;
40
+ }
41
+ // invalid or unsatisifiable
42
+ if (isNaN(start) || isNaN(end) || start > end || start < 0) {
43
+ continue;
44
+ }
45
+ // add range
46
+ ranges.push({
47
+ start,
48
+ end,
49
+ });
50
+ }
51
+ if (ranges.length < 1) {
52
+ // unsatisifiable
53
+ return -1;
54
+ }
55
+ return { ranges, type };
56
+ };
57
+ exports.rangeParser = rangeParser;
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.serveStatic = void 0;
7
7
  const http_1 = __importDefault(require("http"));
8
8
  const remotion_1 = require("remotion");
9
- const serve_handler_1 = __importDefault(require("serve-handler"));
10
9
  const get_port_1 = require("./get-port");
11
10
  const offthread_video_server_1 = require("./offthread-video-server");
11
+ const serve_handler_1 = require("./serve-handler");
12
12
  const serveStatic = async (path, options) => {
13
13
  var _a, _b;
14
14
  const port = await (0, get_port_1.getDesiredPort)((_b = (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : remotion_1.Internals.getServerPort()) !== null && _b !== void 0 ? _b : undefined, 3000, 3100);
@@ -31,10 +31,8 @@ const serveStatic = async (path, options) => {
31
31
  response.end('Server only supports /proxy');
32
32
  return;
33
33
  }
34
- (0, serve_handler_1.default)(request, response, {
34
+ (0, serve_handler_1.serveHandler)(request, response, {
35
35
  public: path,
36
- directoryListing: false,
37
- cleanUrls: false,
38
36
  }).catch(() => {
39
37
  response.statusCode = 500;
40
38
  response.end('Error serving file');
@@ -0,0 +1,8 @@
1
+ import { StitcherOptions } from './stitch-frames-to-video';
2
+ declare type ReturnType = {
3
+ task: Promise<unknown>;
4
+ getLogs: () => string;
5
+ };
6
+ export declare const spawnFfmpeg: (options: StitcherOptions) => Promise<ReturnType>;
7
+ export declare const stitchFramesToGif: (options: StitcherOptions) => Promise<void>;
8
+ export {};
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.stitchFramesToGif = exports.spawnFfmpeg = void 0;
7
+ const execa_1 = __importDefault(require("execa"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const remotion_1 = require("remotion");
11
+ const get_codec_name_1 = require("./get-codec-name");
12
+ const get_prores_profile_name_1 = require("./get-prores-profile-name");
13
+ const parse_ffmpeg_progress_1 = require("./parse-ffmpeg-progress");
14
+ const validate_even_dimensions_with_codec_1 = require("./validate-even-dimensions-with-codec");
15
+ const validate_ffmpeg_1 = require("./validate-ffmpeg");
16
+ const packageJsonPath = path_1.default.join(__dirname, '..', 'package.json');
17
+ const packageJson = fs_1.default.existsSync(packageJsonPath)
18
+ ? JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'))
19
+ : null;
20
+ const spawnFfmpeg = async (options) => {
21
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
22
+ remotion_1.Internals.validateDimension(options.height, 'height', 'passed to `stitchFramesToVideo()`');
23
+ remotion_1.Internals.validateDimension(options.width, 'width', 'passed to `stitchFramesToVideo()`');
24
+ remotion_1.Internals.validateFps(options.fps, 'passed to `stitchFramesToVideo()`');
25
+ const codec = (_a = options.codec) !== null && _a !== void 0 ? _a : remotion_1.Internals.DEFAULT_CODEC;
26
+ (0, validate_even_dimensions_with_codec_1.validateEvenDimensionsWithCodec)({
27
+ width: options.width,
28
+ height: options.height,
29
+ codec,
30
+ scale: 1,
31
+ });
32
+ const crf = (_b = options.crf) !== null && _b !== void 0 ? _b : remotion_1.Internals.getDefaultCrfForCodec(codec);
33
+ const pixelFormat = (_c = options.pixelFormat) !== null && _c !== void 0 ? _c : remotion_1.Internals.DEFAULT_PIXEL_FORMAT;
34
+ await (0, validate_ffmpeg_1.validateFfmpeg)((_d = options.ffmpegExecutable) !== null && _d !== void 0 ? _d : null);
35
+ const encoderName = (0, get_codec_name_1.getCodecName)(codec);
36
+ const proResProfileName = (0, get_prores_profile_name_1.getProResProfileName)(codec, options.proResProfile);
37
+ const supportsCrf = encoderName && codec !== 'prores';
38
+ if (options.verbose) {
39
+ console.log('[verbose] ffmpeg', (_e = options.ffmpegExecutable) !== null && _e !== void 0 ? _e : 'ffmpeg in PATH');
40
+ console.log('[verbose] encoder', encoderName);
41
+ console.log('[verbose] pixelFormat', pixelFormat);
42
+ if (supportsCrf) {
43
+ console.log('[verbose] crf', crf);
44
+ }
45
+ console.log('[verbose] codec', codec);
46
+ console.log('[verbose] proResProfileName', proResProfileName);
47
+ }
48
+ remotion_1.Internals.validateSelectedCrfAndCodecCombination(crf, codec);
49
+ remotion_1.Internals.validateSelectedPixelFormatAndCodecCombination(pixelFormat, codec);
50
+ const expectedFrames = options.assetsInfo.assets.length;
51
+ const updateProgress = (preStitchProgress, muxProgress) => {
52
+ var _a;
53
+ const totalFrameProgress = 0.5 * preStitchProgress * expectedFrames + muxProgress * 0.5;
54
+ (_a = options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, Math.round(totalFrameProgress));
55
+ };
56
+ const ffmpegArgs = [
57
+ ['-r', String(options.fps)],
58
+ ...(((_f = options.internalOptions) === null || _f === void 0 ? void 0 : _f.preEncodedFileLocation)
59
+ ? [['-i', (_g = options.internalOptions) === null || _g === void 0 ? void 0 : _g.preEncodedFileLocation]]
60
+ : [
61
+ ['-f', 'image2'],
62
+ ['-s', `${options.width}x${options.height}`],
63
+ ['-start_number', String(options.assetsInfo.firstFrameIndex)],
64
+ ['-i', options.assetsInfo.imageSequenceName],
65
+ ]),
66
+ // -c:v is the same as -vcodec as -codec:video
67
+ // and specified the video codec.
68
+ options.loop === null
69
+ ? null
70
+ : ['-loop', typeof options.loop === 'number' ? options.loop : '-1'],
71
+ // Ignore metadata that may come from remote media
72
+ ['-map_metadata', '-1'],
73
+ [
74
+ '-metadata',
75
+ `comment=` +
76
+ [`Made with Remotion`, packageJson ? packageJson.version : null].join(' '),
77
+ ],
78
+ options.force ? '-y' : null,
79
+ options.outputLocation,
80
+ ];
81
+ if (options.verbose) {
82
+ console.log('Generated FFMPEG command:');
83
+ console.log(ffmpegArgs);
84
+ }
85
+ const ffmpegString = ffmpegArgs.flat(2).filter(Boolean);
86
+ const task = (0, execa_1.default)((_h = options.ffmpegExecutable) !== null && _h !== void 0 ? _h : 'ffmpeg', ffmpegString, {
87
+ cwd: options.dir,
88
+ });
89
+ (_j = options.cancelSignal) === null || _j === void 0 ? void 0 : _j.call(options, () => {
90
+ task.kill();
91
+ });
92
+ let ffmpegOutput = '';
93
+ let isFinished = false;
94
+ (_k = task.stderr) === null || _k === void 0 ? void 0 : _k.on('data', (data) => {
95
+ var _a;
96
+ const str = data.toString();
97
+ ffmpegOutput += str;
98
+ if (options.onProgress) {
99
+ const parsed = (0, parse_ffmpeg_progress_1.parseFfmpegProgress)(str);
100
+ // FFMPEG bug: In some cases, FFMPEG does hang after it is finished with it's job
101
+ // Example repo: https://github.com/JonnyBurger/ffmpeg-repro (access can be given upon request)
102
+ if (parsed !== undefined) {
103
+ // If two times in a row the finishing frame is logged, we quit the render
104
+ if (parsed === expectedFrames) {
105
+ if (isFinished) {
106
+ (_a = task.stdin) === null || _a === void 0 ? void 0 : _a.write('q');
107
+ }
108
+ else {
109
+ isFinished = true;
110
+ }
111
+ }
112
+ updateProgress(1, parsed);
113
+ }
114
+ }
115
+ });
116
+ return { task, getLogs: () => ffmpegOutput };
117
+ };
118
+ exports.spawnFfmpeg = spawnFfmpeg;
119
+ const stitchFramesToGif = async (options) => {
120
+ const { task, getLogs } = await (0, exports.spawnFfmpeg)(options);
121
+ try {
122
+ await task;
123
+ }
124
+ catch (err) {
125
+ throw new Error(getLogs());
126
+ }
127
+ };
128
+ exports.stitchFramesToGif = stitchFramesToGif;
@@ -128,6 +128,9 @@ const spawnFfmpeg = async (options) => {
128
128
  audio,
129
129
  '-c:a',
130
130
  audioCodecName,
131
+ // Set bitrate up to 320k, for aac it might effectively be lower
132
+ '-b:a',
133
+ '320k',
131
134
  options.force ? '-y' : null,
132
135
  options.outputLocation,
133
136
  ].filter(remotion_1.Internals.truthy));
@@ -168,6 +171,8 @@ const spawnFfmpeg = async (options) => {
168
171
  ]),
169
172
  codec === 'h264' ? ['-movflags', 'faststart'] : null,
170
173
  audioCodecName ? ['-c:a', audioCodecName] : null,
174
+ // Set max bitrate up to 1024kbps, will choose lower if that's too much
175
+ audioCodecName ? ['-b:a', '512K'] : null,
171
176
  // Ignore metadata that may come from remote media
172
177
  ['-map_metadata', '-1'],
173
178
  [
@@ -0,0 +1,2 @@
1
+ import { Codec } from 'remotion';
2
+ export declare const validateFpsForGif: (codec: Codec, fps: number) => void;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateFpsForGif = void 0;
4
+ const validateFpsForGif = (codec, fps) => {
5
+ if (codec === 'gif' && (fps > 50 || fps < 1)) {
6
+ throw new Error(`To render a GIF, the FPS must be less than or equal to 50 and greater than 0 but got ${fps} instead`);
7
+ }
8
+ };
9
+ exports.validateFpsForGif = validateFpsForGif;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/renderer",
3
- "version": "3.0.17",
3
+ "version": "3.0.18",
4
4
  "description": "Renderer for Remotion",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "execa": "5.1.1",
24
+ "mime-types": "2.1.35",
24
25
  "puppeteer-core": "13.5.1",
25
- "remotion": "3.0.17",
26
- "serve-handler": "6.1.3",
26
+ "remotion": "3.0.18",
27
27
  "source-map": "^0.8.0-beta.0"
28
28
  },
29
29
  "peerDependencies": {
@@ -35,10 +35,10 @@
35
35
  "@testing-library/dom": "^8.7.2",
36
36
  "@testing-library/react": "13.1.1",
37
37
  "@types/jest": "^27.4.0",
38
+ "@types/mime-types": "2.1.1",
38
39
  "@types/node": "^16.7.5",
39
40
  "@types/react": "18.0.1",
40
41
  "@types/react-dom": "18.0.0",
41
- "@types/serve-handler": "^6.1.0",
42
42
  "eslint": "8.13.0",
43
43
  "jest": "^27.2.4",
44
44
  "prettier": "^2.0.5",
@@ -59,5 +59,5 @@
59
59
  "publishConfig": {
60
60
  "access": "public"
61
61
  },
62
- "gitHead": "c973653a9ced0d8793ce0cb03ce5f837b149d865"
62
+ "gitHead": "64eadd48b303d0f627ed5883f9200345af002ee2"
63
63
  }