@remotion/renderer 4.0.0-preload.13 → 4.0.0-spawn.13
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/dist/assets/download-and-map-assets-to-file.d.ts +6 -0
- package/dist/assets/download-and-map-assets-to-file.js +46 -19
- package/dist/assets/ffmpeg-volume-expression.d.ts +2 -1
- package/dist/assets/ffmpeg-volume-expression.js +15 -12
- package/dist/combine-videos.js +2 -0
- package/dist/cycle-browser-tabs.d.ts +2 -1
- package/dist/cycle-browser-tabs.js +2 -2
- package/dist/extract-frame-from-video.d.ts +18 -0
- package/dist/extract-frame-from-video.js +132 -0
- package/dist/frame-to-ffmpeg-timestamp.d.ts +1 -0
- package/dist/frame-to-ffmpeg-timestamp.js +8 -0
- package/dist/get-compositions.d.ts +4 -2
- package/dist/get-compositions.js +22 -5
- package/dist/index.d.ts +7 -8
- package/dist/index.js +0 -4
- package/dist/merge-audio-track.js +2 -2
- package/dist/offthread-video-server.d.ts +13 -0
- package/dist/offthread-video-server.js +65 -0
- package/dist/open-browser.d.ts +5 -5
- package/dist/prepare-server.d.ts +12 -2
- package/dist/prepare-server.js +22 -5
- package/dist/prespawn-ffmpeg.d.ts +1 -0
- package/dist/prespawn-ffmpeg.js +11 -10
- package/dist/puppeteer-screenshot.js +5 -1
- package/dist/render-frames.d.ts +7 -2
- package/dist/render-frames.js +90 -33
- package/dist/render-media.d.ts +8 -2
- package/dist/render-media.js +52 -3
- package/dist/render-still.d.ts +7 -2
- package/dist/render-still.js +33 -9
- package/dist/serve-static.d.ts +9 -3
- package/dist/serve-static.js +16 -0
- package/dist/set-props-and-env.d.ts +3 -1
- package/dist/set-props-and-env.js +25 -3
- package/dist/stitch-frames-to-video.js +1 -0
- package/dist/stringify-ffmpeg-filter.js +3 -0
- package/dist/tmp-dir.js +5 -1
- package/package.json +4 -5
|
@@ -2,6 +2,7 @@ import { TAsset } from 'remotion';
|
|
|
2
2
|
export declare type RenderMediaOnDownload = (src: string) => ((progress: {
|
|
3
3
|
percent: number;
|
|
4
4
|
}) => void) | undefined | void;
|
|
5
|
+
export declare const waitForAssetToBeDownloaded: (src: string, to: string) => Promise<void>;
|
|
5
6
|
export declare const markAllAssetsAsDownloaded: () => void;
|
|
6
7
|
export declare const getSanitizedFilenameForAssetUrl: ({ src, downloadDir, }: {
|
|
7
8
|
src: string;
|
|
@@ -12,3 +13,8 @@ export declare const downloadAndMapAssetsToFileUrl: ({ asset, downloadDir, onDow
|
|
|
12
13
|
downloadDir: string;
|
|
13
14
|
onDownload: RenderMediaOnDownload;
|
|
14
15
|
}) => Promise<TAsset>;
|
|
16
|
+
export declare const startDownloadForSrc: ({ src, downloadDir, onDownload, }: {
|
|
17
|
+
src: string;
|
|
18
|
+
downloadDir: string;
|
|
19
|
+
onDownload: RenderMediaOnDownload;
|
|
20
|
+
}) => Promise<string>;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.downloadAndMapAssetsToFileUrl = exports.getSanitizedFilenameForAssetUrl = exports.markAllAssetsAsDownloaded = void 0;
|
|
6
|
+
exports.startDownloadForSrc = exports.downloadAndMapAssetsToFileUrl = exports.getSanitizedFilenameForAssetUrl = exports.markAllAssetsAsDownloaded = exports.waitForAssetToBeDownloaded = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const remotion_1 = require("remotion");
|
|
@@ -13,21 +13,38 @@ const sanitize_filepath_1 = require("./sanitize-filepath");
|
|
|
13
13
|
const isDownloadingMap = {};
|
|
14
14
|
const hasBeenDownloadedMap = {};
|
|
15
15
|
const listeners = {};
|
|
16
|
-
const waitForAssetToBeDownloaded = (src) => {
|
|
16
|
+
const waitForAssetToBeDownloaded = (src, to) => {
|
|
17
|
+
var _a;
|
|
18
|
+
if ((_a = hasBeenDownloadedMap[src]) === null || _a === void 0 ? void 0 : _a[to]) {
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
17
21
|
if (!listeners[src]) {
|
|
18
|
-
listeners[src] =
|
|
22
|
+
listeners[src] = {};
|
|
23
|
+
}
|
|
24
|
+
if (!listeners[src][to]) {
|
|
25
|
+
listeners[src][to] = [];
|
|
19
26
|
}
|
|
20
27
|
return new Promise((resolve) => {
|
|
21
|
-
listeners[src].push(() => resolve());
|
|
28
|
+
listeners[src][to].push(() => resolve());
|
|
22
29
|
});
|
|
23
30
|
};
|
|
24
|
-
|
|
31
|
+
exports.waitForAssetToBeDownloaded = waitForAssetToBeDownloaded;
|
|
32
|
+
const notifyAssetIsDownloaded = (src, to) => {
|
|
25
33
|
if (!listeners[src]) {
|
|
26
|
-
listeners[src] =
|
|
34
|
+
listeners[src] = {};
|
|
35
|
+
}
|
|
36
|
+
if (!listeners[src][to]) {
|
|
37
|
+
listeners[src][to] = [];
|
|
27
38
|
}
|
|
28
|
-
listeners[src].forEach((fn) => fn());
|
|
29
|
-
isDownloadingMap[src]
|
|
30
|
-
|
|
39
|
+
listeners[src][to].forEach((fn) => fn());
|
|
40
|
+
if (!isDownloadingMap[src]) {
|
|
41
|
+
isDownloadingMap[src] = {};
|
|
42
|
+
}
|
|
43
|
+
isDownloadingMap[src][to] = false;
|
|
44
|
+
if (!hasBeenDownloadedMap[src]) {
|
|
45
|
+
hasBeenDownloadedMap[src] = {};
|
|
46
|
+
}
|
|
47
|
+
hasBeenDownloadedMap[src][to] = true;
|
|
31
48
|
};
|
|
32
49
|
const validateMimeType = (mimeType, src) => {
|
|
33
50
|
if (!mimeType.includes('/')) {
|
|
@@ -69,13 +86,17 @@ function validateBufferEncoding(potentialEncoding, dataUrl) {
|
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
const downloadAsset = async (src, to, onDownload) => {
|
|
72
|
-
|
|
89
|
+
var _a, _b;
|
|
90
|
+
if ((_a = hasBeenDownloadedMap[src]) === null || _a === void 0 ? void 0 : _a[to]) {
|
|
73
91
|
return;
|
|
74
92
|
}
|
|
75
|
-
if (isDownloadingMap[src]) {
|
|
76
|
-
return waitForAssetToBeDownloaded(src);
|
|
93
|
+
if ((_b = isDownloadingMap[src]) === null || _b === void 0 ? void 0 : _b[to]) {
|
|
94
|
+
return (0, exports.waitForAssetToBeDownloaded)(src, to);
|
|
95
|
+
}
|
|
96
|
+
if (!isDownloadingMap[src]) {
|
|
97
|
+
isDownloadingMap[src] = {};
|
|
77
98
|
}
|
|
78
|
-
isDownloadingMap[src] = true;
|
|
99
|
+
isDownloadingMap[src][to] = true;
|
|
79
100
|
const onProgress = onDownload(src);
|
|
80
101
|
(0, ensure_output_directory_1.ensureOutputDirectory)(to);
|
|
81
102
|
if (src.startsWith('data:')) {
|
|
@@ -94,7 +115,7 @@ const downloadAsset = async (src, to, onDownload) => {
|
|
|
94
115
|
validateBufferEncoding(encoding, src);
|
|
95
116
|
const buff = Buffer.from(assetData, encoding);
|
|
96
117
|
await fs_1.default.promises.writeFile(to, buff);
|
|
97
|
-
notifyAssetIsDownloaded(src);
|
|
118
|
+
notifyAssetIsDownloaded(src, to);
|
|
98
119
|
return;
|
|
99
120
|
}
|
|
100
121
|
await (0, download_file_1.downloadFile)(src, to, ({ progress }) => {
|
|
@@ -102,7 +123,7 @@ const downloadAsset = async (src, to, onDownload) => {
|
|
|
102
123
|
percent: progress,
|
|
103
124
|
});
|
|
104
125
|
});
|
|
105
|
-
notifyAssetIsDownloaded(src);
|
|
126
|
+
notifyAssetIsDownloaded(src, to);
|
|
106
127
|
};
|
|
107
128
|
const markAllAssetsAsDownloaded = () => {
|
|
108
129
|
Object.keys(hasBeenDownloadedMap).forEach((key) => {
|
|
@@ -127,16 +148,22 @@ const getSanitizedFilenameForAssetUrl = ({ src, downloadDir, }) => {
|
|
|
127
148
|
};
|
|
128
149
|
exports.getSanitizedFilenameForAssetUrl = getSanitizedFilenameForAssetUrl;
|
|
129
150
|
const downloadAndMapAssetsToFileUrl = async ({ asset, downloadDir, onDownload, }) => {
|
|
130
|
-
const newSrc = (0, exports.
|
|
151
|
+
const newSrc = await (0, exports.startDownloadForSrc)({
|
|
131
152
|
src: asset.src,
|
|
132
153
|
downloadDir,
|
|
154
|
+
onDownload,
|
|
133
155
|
});
|
|
134
|
-
if (!remotion_1.Internals.AssetCompression.isAssetCompressed(newSrc)) {
|
|
135
|
-
await downloadAsset(asset.src, newSrc, onDownload);
|
|
136
|
-
}
|
|
137
156
|
return {
|
|
138
157
|
...asset,
|
|
139
158
|
src: newSrc,
|
|
140
159
|
};
|
|
141
160
|
};
|
|
142
161
|
exports.downloadAndMapAssetsToFileUrl = downloadAndMapAssetsToFileUrl;
|
|
162
|
+
const startDownloadForSrc = async ({ src, downloadDir, onDownload, }) => {
|
|
163
|
+
const newSrc = (0, exports.getSanitizedFilenameForAssetUrl)({ downloadDir, src });
|
|
164
|
+
if (!remotion_1.Internals.AssetCompression.isAssetCompressed(newSrc)) {
|
|
165
|
+
await downloadAsset(src, newSrc, onDownload);
|
|
166
|
+
}
|
|
167
|
+
return newSrc;
|
|
168
|
+
};
|
|
169
|
+
exports.startDownloadForSrc = startDownloadForSrc;
|
|
@@ -4,9 +4,10 @@ declare type FfmpegVolumeExpression = {
|
|
|
4
4
|
eval: FfmpegEval;
|
|
5
5
|
value: string;
|
|
6
6
|
};
|
|
7
|
-
export declare const ffmpegVolumeExpression: ({ volume, startInVideo, fps, }: {
|
|
7
|
+
export declare const ffmpegVolumeExpression: ({ volume, startInVideo, fps, trimLeft, }: {
|
|
8
8
|
volume: AssetVolume;
|
|
9
9
|
startInVideo: number;
|
|
10
|
+
trimLeft: number;
|
|
10
11
|
fps: number;
|
|
11
12
|
}) => FfmpegVolumeExpression;
|
|
12
13
|
export {};
|
|
@@ -12,12 +12,12 @@ const FFMPEG_TIME_VARIABLE = 't';
|
|
|
12
12
|
const ffmpegIfOrElse = (condition, then, elseDo) => {
|
|
13
13
|
return `if(${condition},${then},${elseDo})`;
|
|
14
14
|
};
|
|
15
|
-
const ffmpegIsOneOfFrames = (frames, fps) => {
|
|
15
|
+
const ffmpegIsOneOfFrames = ({ frames, trimLeft, fps, }) => {
|
|
16
16
|
const consecutiveArrays = [];
|
|
17
17
|
for (let i = 0; i < frames.length; i++) {
|
|
18
18
|
const previousFrame = frames[i - 1];
|
|
19
19
|
const frame = frames[i];
|
|
20
|
-
if (
|
|
20
|
+
if (previousFrame === undefined || frame !== previousFrame + 1) {
|
|
21
21
|
consecutiveArrays.push([]);
|
|
22
22
|
}
|
|
23
23
|
consecutiveArrays[consecutiveArrays.length - 1].push(frame);
|
|
@@ -28,25 +28,22 @@ const ffmpegIsOneOfFrames = (frames, fps) => {
|
|
|
28
28
|
const lastFrame = f[f.length - 1];
|
|
29
29
|
const before = (firstFrame - 0.5) / fps;
|
|
30
30
|
const after = (lastFrame + 0.5) / fps;
|
|
31
|
-
return `between(${FFMPEG_TIME_VARIABLE},${before.toFixed(4)},${after.toFixed(4)})`;
|
|
31
|
+
return `between(${FFMPEG_TIME_VARIABLE},${(before + trimLeft).toFixed(4)},${(after + trimLeft).toFixed(4)})`;
|
|
32
32
|
})
|
|
33
33
|
.join('+');
|
|
34
34
|
};
|
|
35
|
-
const ffmpegBuildVolumeExpression = (arr, fps) => {
|
|
35
|
+
const ffmpegBuildVolumeExpression = (arr, delay, fps) => {
|
|
36
36
|
if (arr.length === 0) {
|
|
37
37
|
throw new Error('Volume array expression should never have length 0');
|
|
38
38
|
}
|
|
39
39
|
if (arr.length === 1) {
|
|
40
|
-
|
|
41
|
-
// where the audio actually plays.
|
|
42
|
-
// If this is the case, we just return volume 0 to clip it.
|
|
43
|
-
return ffmpegIfOrElse(ffmpegIsOneOfFrames(arr[0][1], fps), String(arr[0][0]), String(0));
|
|
40
|
+
return String(arr[0][0]);
|
|
44
41
|
}
|
|
45
42
|
const [first, ...rest] = arr;
|
|
46
43
|
const [volume, frames] = first;
|
|
47
|
-
return ffmpegIfOrElse(ffmpegIsOneOfFrames(frames, fps), String(volume), ffmpegBuildVolumeExpression(rest, fps));
|
|
44
|
+
return ffmpegIfOrElse(ffmpegIsOneOfFrames({ frames, trimLeft: delay, fps }), String(volume), ffmpegBuildVolumeExpression(rest, delay, fps));
|
|
48
45
|
};
|
|
49
|
-
const ffmpegVolumeExpression = ({ volume, startInVideo, fps, }) => {
|
|
46
|
+
const ffmpegVolumeExpression = ({ volume, startInVideo, fps, trimLeft, }) => {
|
|
50
47
|
// If it's a static volume, we return it and tell
|
|
51
48
|
// FFMPEG it only has to evaluate it once
|
|
52
49
|
if (typeof volume === 'number') {
|
|
@@ -60,14 +57,20 @@ const ffmpegVolumeExpression = ({ volume, startInVideo, fps, }) => {
|
|
|
60
57
|
volume: volume[0],
|
|
61
58
|
startInVideo,
|
|
62
59
|
fps,
|
|
60
|
+
trimLeft,
|
|
63
61
|
});
|
|
64
62
|
}
|
|
63
|
+
// A 1 sec video with frames 0-29 would mean that
|
|
64
|
+
// frame 29 corresponds to timestamp 0.966666...
|
|
65
|
+
// but the audio is actually 1 sec long. For that reason we pad the last
|
|
66
|
+
// timestamp.
|
|
67
|
+
const paddedVolume = [...volume, volume[volume.length - 1]];
|
|
65
68
|
// Otherwise, we construct an FFMPEG expression. First step:
|
|
66
69
|
// Make a map of all possible volumes
|
|
67
70
|
// {possibleVolume1} => [frame1, frame2]
|
|
68
71
|
// {possibleVolume2} => [frame3, frame4]
|
|
69
72
|
const volumeMap = {};
|
|
70
|
-
|
|
73
|
+
paddedVolume.forEach((baseVolume, frame) => {
|
|
71
74
|
// Adjust volume based on how many other tracks have not yet finished
|
|
72
75
|
const actualVolume = (0, round_volume_to_avoid_stack_overflow_1.roundVolumeToAvoidStackOverflow)(Math.min(1, baseVolume));
|
|
73
76
|
if (!volumeMap[actualVolume]) {
|
|
@@ -81,7 +84,7 @@ const ffmpegVolumeExpression = ({ volume, startInVideo, fps, }) => {
|
|
|
81
84
|
.map((key) => [Number(key), volumeMap[key]])
|
|
82
85
|
.sort((a, b) => a[1].length - b[1].length);
|
|
83
86
|
// Construct and tell FFMPEG it has to evaluate expression on each frame
|
|
84
|
-
const expression = ffmpegBuildVolumeExpression(volumeArray, fps);
|
|
87
|
+
const expression = ffmpegBuildVolumeExpression(volumeArray, trimLeft, fps);
|
|
85
88
|
return {
|
|
86
89
|
eval: 'frame',
|
|
87
90
|
value: `'${expression}'`,
|
package/dist/combine-videos.js
CHANGED
|
@@ -28,6 +28,8 @@ const combineVideos = async ({ files, filelistDir, output, onProgress, numberOfF
|
|
|
28
28
|
remotion_1.Internals.isAudioCodec(codec) ? null : 'copy',
|
|
29
29
|
'-c:a',
|
|
30
30
|
(0, get_audio_codec_name_1.getAudioCodecName)(codec),
|
|
31
|
+
codec === 'h264' ? '-movflags' : null,
|
|
32
|
+
codec === 'h264' ? 'faststart' : null,
|
|
31
33
|
'-shortest',
|
|
32
34
|
'-y',
|
|
33
35
|
output,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { openBrowser } from './open-browser';
|
|
2
2
|
declare type Await<T> = T extends PromiseLike<infer U> ? U : T;
|
|
3
|
-
|
|
3
|
+
declare type Browser = ReturnType<typeof openBrowser>;
|
|
4
|
+
export declare const cycleBrowserTabs: (puppeteerInstance: Browser | Await<Browser>, concurrency: number) => {
|
|
4
5
|
stopCycling: () => void;
|
|
5
6
|
};
|
|
6
7
|
export {};
|
|
@@ -11,8 +11,8 @@ const cycleBrowserTabs = (puppeteerInstance, concurrency) => {
|
|
|
11
11
|
let i = 0;
|
|
12
12
|
const set = () => {
|
|
13
13
|
interval = setTimeout(() => {
|
|
14
|
-
puppeteerInstance
|
|
15
|
-
.pages()
|
|
14
|
+
Promise.resolve(puppeteerInstance)
|
|
15
|
+
.then((instance) => instance.pages())
|
|
16
16
|
.then((pages) => {
|
|
17
17
|
var _a, _b;
|
|
18
18
|
const currentPage = pages[i % pages.length];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { FfmpegExecutable } from 'remotion';
|
|
4
|
+
import { Readable } from 'stream';
|
|
5
|
+
export declare function streamToString(stream: Readable): Promise<string>;
|
|
6
|
+
export declare const getLastFrameOfVideo: ({ ffmpegExecutable, offset, src, }: {
|
|
7
|
+
ffmpegExecutable: FfmpegExecutable;
|
|
8
|
+
offset: number;
|
|
9
|
+
src: string;
|
|
10
|
+
}) => Promise<Buffer>;
|
|
11
|
+
declare type Options = {
|
|
12
|
+
time: number;
|
|
13
|
+
src: string;
|
|
14
|
+
ffmpegExecutable: FfmpegExecutable;
|
|
15
|
+
};
|
|
16
|
+
export declare const extractFrameFromVideoFn: ({ time, src, ffmpegExecutable, }: Options) => Promise<Buffer>;
|
|
17
|
+
export declare const extractFrameFromVideo: (options: Options) => Promise<Buffer>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
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.extractFrameFromVideo = exports.extractFrameFromVideoFn = exports.getLastFrameOfVideo = exports.streamToString = void 0;
|
|
7
|
+
const execa_1 = __importDefault(require("execa"));
|
|
8
|
+
const frame_to_ffmpeg_timestamp_1 = require("./frame-to-ffmpeg-timestamp");
|
|
9
|
+
const p_limit_1 = require("./p-limit");
|
|
10
|
+
function streamToString(stream) {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
14
|
+
stream.on('error', (err) => reject(err));
|
|
15
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
exports.streamToString = streamToString;
|
|
19
|
+
const getLastFrameOfVideo = async ({ ffmpegExecutable, offset, src, }) => {
|
|
20
|
+
if (offset > 100) {
|
|
21
|
+
throw new Error('could not get last frame of ' +
|
|
22
|
+
src +
|
|
23
|
+
'. Tried to seek 100ms before the end of the video and no frame was found. The video container has a duration that is longer than it contains video.');
|
|
24
|
+
}
|
|
25
|
+
const actualOffset = `-${offset + 10}ms`;
|
|
26
|
+
const { stdout, stderr } = (0, execa_1.default)(ffmpegExecutable !== null && ffmpegExecutable !== void 0 ? ffmpegExecutable : 'ffmpeg', [
|
|
27
|
+
'-sseof',
|
|
28
|
+
actualOffset,
|
|
29
|
+
'-i',
|
|
30
|
+
src,
|
|
31
|
+
'-frames:v',
|
|
32
|
+
'1',
|
|
33
|
+
'-f',
|
|
34
|
+
'image2pipe',
|
|
35
|
+
'-',
|
|
36
|
+
]);
|
|
37
|
+
if (!stderr) {
|
|
38
|
+
throw new Error('unexpectedly did not get stderr');
|
|
39
|
+
}
|
|
40
|
+
if (!stdout) {
|
|
41
|
+
throw new Error('unexpectedly did not get stdout');
|
|
42
|
+
}
|
|
43
|
+
const stderrChunks = [];
|
|
44
|
+
const stdoutChunks = [];
|
|
45
|
+
const stdErrString = new Promise((resolve, reject) => {
|
|
46
|
+
stderr.on('data', (d) => stderrChunks.push(d));
|
|
47
|
+
stderr.on('error', (err) => reject(err));
|
|
48
|
+
stderr.on('end', () => resolve(Buffer.concat(stderrChunks).toString('utf-8')));
|
|
49
|
+
});
|
|
50
|
+
const stdoutChunk = new Promise((resolve, reject) => {
|
|
51
|
+
stdout.on('data', (d) => {
|
|
52
|
+
stdoutChunks.push(d);
|
|
53
|
+
});
|
|
54
|
+
stdout.on('error', (err) => {
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
stdout.on('end', () => {
|
|
58
|
+
resolve(Buffer.concat(stdoutChunks));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
const [stdErr, stdoutBuffer] = await Promise.all([stdErrString, stdoutChunk]);
|
|
62
|
+
const isEmpty = stdErr.includes('Output file is empty');
|
|
63
|
+
if (isEmpty) {
|
|
64
|
+
return (0, exports.getLastFrameOfVideo)({ ffmpegExecutable, offset: offset + 10, src });
|
|
65
|
+
}
|
|
66
|
+
return stdoutBuffer;
|
|
67
|
+
};
|
|
68
|
+
exports.getLastFrameOfVideo = getLastFrameOfVideo;
|
|
69
|
+
const limit = (0, p_limit_1.pLimit)(5);
|
|
70
|
+
const extractFrameFromVideoFn = async ({ time, src, ffmpegExecutable, }) => {
|
|
71
|
+
const ffmpegTimestamp = (0, frame_to_ffmpeg_timestamp_1.frameToFfmpegTimestamp)(time);
|
|
72
|
+
const { stdout, stderr } = (0, execa_1.default)(ffmpegExecutable !== null && ffmpegExecutable !== void 0 ? ffmpegExecutable : 'ffmpeg', [
|
|
73
|
+
'-ss',
|
|
74
|
+
ffmpegTimestamp,
|
|
75
|
+
'-i',
|
|
76
|
+
src,
|
|
77
|
+
'-frames:v',
|
|
78
|
+
'1',
|
|
79
|
+
'-f',
|
|
80
|
+
'image2pipe',
|
|
81
|
+
'-',
|
|
82
|
+
], {
|
|
83
|
+
buffer: false,
|
|
84
|
+
});
|
|
85
|
+
if (!stderr) {
|
|
86
|
+
throw new Error('unexpectedly did not get stderr');
|
|
87
|
+
}
|
|
88
|
+
if (!stdout) {
|
|
89
|
+
throw new Error('unexpectedly did not get stdout');
|
|
90
|
+
}
|
|
91
|
+
const stdoutChunks = [];
|
|
92
|
+
const stderrChunks = [];
|
|
93
|
+
const stderrStringProm = new Promise((resolve, reject) => {
|
|
94
|
+
stderr.on('data', (d) => {
|
|
95
|
+
stderrChunks.push(d);
|
|
96
|
+
});
|
|
97
|
+
stderr.on('error', (err) => {
|
|
98
|
+
reject(err);
|
|
99
|
+
});
|
|
100
|
+
stderr.on('end', () => {
|
|
101
|
+
resolve(Buffer.concat(stderrChunks).toString('utf8'));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
const stdoutBuffer = new Promise((resolve, reject) => {
|
|
105
|
+
stdout.on('data', (d) => {
|
|
106
|
+
stdoutChunks.push(d);
|
|
107
|
+
});
|
|
108
|
+
stdout.on('error', (err) => {
|
|
109
|
+
reject(err);
|
|
110
|
+
});
|
|
111
|
+
stdout.on('end', () => {
|
|
112
|
+
resolve(Buffer.concat(stdoutChunks));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const [stderrStr, stdOut] = await Promise.all([
|
|
116
|
+
stderrStringProm,
|
|
117
|
+
stdoutBuffer,
|
|
118
|
+
]);
|
|
119
|
+
if (stderrStr.includes('Output file is empty')) {
|
|
120
|
+
return (0, exports.getLastFrameOfVideo)({
|
|
121
|
+
ffmpegExecutable,
|
|
122
|
+
offset: 0,
|
|
123
|
+
src,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return stdOut;
|
|
127
|
+
};
|
|
128
|
+
exports.extractFrameFromVideoFn = extractFrameFromVideoFn;
|
|
129
|
+
const extractFrameFromVideo = (options) => {
|
|
130
|
+
return limit(exports.extractFrameFromVideoFn, options);
|
|
131
|
+
};
|
|
132
|
+
exports.extractFrameFromVideo = extractFrameFromVideo;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const frameToFfmpegTimestamp: (time: number) => string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.frameToFfmpegTimestamp = void 0;
|
|
4
|
+
const frameToFfmpegTimestamp = (time) => {
|
|
5
|
+
// We ceil because FFMPEG seeks to the closest frame BEFORE the position
|
|
6
|
+
return (Math.ceil(time * 1000000) / 1000).toFixed(3) + 'ms';
|
|
7
|
+
};
|
|
8
|
+
exports.frameToFfmpegTimestamp = frameToFfmpegTimestamp;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Browser } from 'puppeteer-core';
|
|
2
|
-
import { BrowserExecutable, TCompMetadata } from 'remotion';
|
|
2
|
+
import { BrowserExecutable, FfmpegExecutable, TCompMetadata } from 'remotion';
|
|
3
3
|
import { BrowserLog } from './browser-log';
|
|
4
4
|
import { ChromiumOptions } from './open-browser';
|
|
5
5
|
declare type GetCompositionsConfig = {
|
|
@@ -10,6 +10,8 @@ declare type GetCompositionsConfig = {
|
|
|
10
10
|
browserExecutable?: BrowserExecutable;
|
|
11
11
|
timeoutInMilliseconds?: number;
|
|
12
12
|
chromiumOptions?: ChromiumOptions;
|
|
13
|
+
ffmpegExecutable?: FfmpegExecutable;
|
|
14
|
+
port?: number | null;
|
|
13
15
|
};
|
|
14
|
-
export declare const getCompositions: (serveUrlOrWebpackUrl: string, config?: GetCompositionsConfig
|
|
16
|
+
export declare const getCompositions: (serveUrlOrWebpackUrl: string, config?: GetCompositionsConfig) => Promise<TCompMetadata[]>;
|
|
15
17
|
export {};
|
package/dist/get-compositions.js
CHANGED
|
@@ -3,11 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getCompositions = void 0;
|
|
4
4
|
const handle_javascript_exception_1 = require("./error-handling/handle-javascript-exception");
|
|
5
5
|
const get_browser_instance_1 = require("./get-browser-instance");
|
|
6
|
+
const make_assets_download_dir_1 = require("./make-assets-download-dir");
|
|
6
7
|
const prepare_server_1 = require("./prepare-server");
|
|
7
8
|
const puppeteer_evaluate_1 = require("./puppeteer-evaluate");
|
|
8
9
|
const set_props_and_env_1 = require("./set-props-and-env");
|
|
9
10
|
const validate_puppeteer_timeout_1 = require("./validate-puppeteer-timeout");
|
|
10
|
-
const innerGetCompositions = async (serveUrl, page, config) => {
|
|
11
|
+
const innerGetCompositions = async (serveUrl, page, config, proxyPort) => {
|
|
11
12
|
if (config === null || config === void 0 ? void 0 : config.onBrowserLog) {
|
|
12
13
|
page.on('console', (log) => {
|
|
13
14
|
var _a;
|
|
@@ -26,6 +27,8 @@ const innerGetCompositions = async (serveUrl, page, config) => {
|
|
|
26
27
|
serveUrl,
|
|
27
28
|
initialFrame: 0,
|
|
28
29
|
timeoutInMilliseconds: config === null || config === void 0 ? void 0 : config.timeoutInMilliseconds,
|
|
30
|
+
proxyPort,
|
|
31
|
+
retriesRemaining: 2,
|
|
29
32
|
});
|
|
30
33
|
await (0, puppeteer_evaluate_1.puppeteerEvaluateWithCatch)({
|
|
31
34
|
page,
|
|
@@ -50,26 +53,40 @@ const innerGetCompositions = async (serveUrl, page, config) => {
|
|
|
50
53
|
};
|
|
51
54
|
const getCompositions = async (serveUrlOrWebpackUrl, config) => {
|
|
52
55
|
var _a, _b;
|
|
53
|
-
const
|
|
56
|
+
const downloadDir = (0, make_assets_download_dir_1.makeAssetsDownloadTmpDir)();
|
|
54
57
|
const { page, cleanup } = await (0, get_browser_instance_1.getPageAndCleanupFn)({
|
|
55
58
|
passedInInstance: config === null || config === void 0 ? void 0 : config.puppeteerInstance,
|
|
56
59
|
browserExecutable: (_a = config === null || config === void 0 ? void 0 : config.browserExecutable) !== null && _a !== void 0 ? _a : null,
|
|
57
60
|
chromiumOptions: (_b = config === null || config === void 0 ? void 0 : config.chromiumOptions) !== null && _b !== void 0 ? _b : {},
|
|
58
61
|
});
|
|
59
62
|
return new Promise((resolve, reject) => {
|
|
63
|
+
var _a, _b;
|
|
64
|
+
const onError = (err) => reject(err);
|
|
60
65
|
const cleanupPageError = (0, handle_javascript_exception_1.handleJavascriptException)({
|
|
61
66
|
page,
|
|
62
67
|
frame: null,
|
|
63
|
-
onError
|
|
68
|
+
onError,
|
|
64
69
|
});
|
|
65
|
-
|
|
70
|
+
let close = null;
|
|
71
|
+
(0, prepare_server_1.prepareServer)({
|
|
72
|
+
webpackConfigOrServeUrl: serveUrlOrWebpackUrl,
|
|
73
|
+
downloadDir,
|
|
74
|
+
onDownload: () => undefined,
|
|
75
|
+
onError,
|
|
76
|
+
ffmpegExecutable: (_a = config === null || config === void 0 ? void 0 : config.ffmpegExecutable) !== null && _a !== void 0 ? _a : null,
|
|
77
|
+
port: (_b = config === null || config === void 0 ? void 0 : config.port) !== null && _b !== void 0 ? _b : null,
|
|
78
|
+
})
|
|
79
|
+
.then(({ serveUrl, closeServer, offthreadPort }) => {
|
|
80
|
+
close = closeServer;
|
|
81
|
+
return innerGetCompositions(serveUrl, page, config !== null && config !== void 0 ? config : {}, offthreadPort);
|
|
82
|
+
})
|
|
66
83
|
.then((comp) => resolve(comp))
|
|
67
84
|
.catch((err) => {
|
|
68
85
|
reject(err);
|
|
69
86
|
})
|
|
70
87
|
.finally(() => {
|
|
71
88
|
cleanup();
|
|
72
|
-
|
|
89
|
+
close === null || close === void 0 ? void 0 : close();
|
|
73
90
|
cleanupPageError();
|
|
74
91
|
});
|
|
75
92
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -29,9 +29,13 @@ export declare const RenderInternals: {
|
|
|
29
29
|
getFfmpegBuildInfo: (options: {
|
|
30
30
|
ffmpegExecutable: string | null;
|
|
31
31
|
}) => Promise<string>;
|
|
32
|
-
serveStatic: (path: string, options
|
|
33
|
-
port
|
|
34
|
-
|
|
32
|
+
serveStatic: (path: string | null, options: {
|
|
33
|
+
port: number | null;
|
|
34
|
+
ffmpegExecutable: import("remotion").FfmpegExecutable;
|
|
35
|
+
downloadDir: string;
|
|
36
|
+
onDownload: import("./assets/download-and-map-assets-to-file").RenderMediaOnDownload;
|
|
37
|
+
onError: (err: Error) => void;
|
|
38
|
+
}) => Promise<{
|
|
35
39
|
port: number;
|
|
36
40
|
close: () => Promise<void>;
|
|
37
41
|
}>;
|
|
@@ -47,13 +51,8 @@ export declare const RenderInternals: {
|
|
|
47
51
|
getLogs: () => string;
|
|
48
52
|
}>;
|
|
49
53
|
getFileExtensionFromCodec: (codec: "h264" | "h265" | "vp8" | "vp9" | "mp3" | "aac" | "wav" | "prores" | "h264-mkv", type: "chunk" | "final") => "mp3" | "aac" | "wav" | "mp4" | "mkv" | "mov" | "webm";
|
|
50
|
-
makeAssetsDownloadTmpDir: () => string;
|
|
51
54
|
tmpDir: (str: string) => string;
|
|
52
55
|
deleteDirectory: (directory: string) => Promise<void>;
|
|
53
|
-
prepareServer: (webpackConfigOrServeUrl: string) => Promise<{
|
|
54
|
-
serveUrl: string;
|
|
55
|
-
closeServer: () => Promise<void>;
|
|
56
|
-
}>;
|
|
57
56
|
isServeUrl: (potentialUrl: string) => boolean;
|
|
58
57
|
ensureOutputDirectory: (outputLocation: string) => void;
|
|
59
58
|
getRealFrameRange: (durationInFrames: number, frameRange: import("remotion").FrameRange | null) => [number, number];
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,9 @@ const get_extension_of_filename_1 = require("./get-extension-of-filename");
|
|
|
14
14
|
const get_frame_to_render_1 = require("./get-frame-to-render");
|
|
15
15
|
const get_local_browser_executable_1 = require("./get-local-browser-executable");
|
|
16
16
|
const is_serve_url_1 = require("./is-serve-url");
|
|
17
|
-
const make_assets_download_dir_1 = require("./make-assets-download-dir");
|
|
18
17
|
const normalize_serve_url_1 = require("./normalize-serve-url");
|
|
19
18
|
const open_browser_1 = require("./open-browser");
|
|
20
19
|
const parse_browser_error_stack_1 = require("./parse-browser-error-stack");
|
|
21
|
-
const prepare_server_1 = require("./prepare-server");
|
|
22
20
|
const serve_static_1 = require("./serve-static");
|
|
23
21
|
const stitch_frames_to_video_1 = require("./stitch-frames-to-video");
|
|
24
22
|
const tmp_dir_1 = require("./tmp-dir");
|
|
@@ -55,10 +53,8 @@ exports.RenderInternals = {
|
|
|
55
53
|
normalizeServeUrl: normalize_serve_url_1.normalizeServeUrl,
|
|
56
54
|
spawnFfmpeg: stitch_frames_to_video_1.spawnFfmpeg,
|
|
57
55
|
getFileExtensionFromCodec: get_extension_from_codec_1.getFileExtensionFromCodec,
|
|
58
|
-
makeAssetsDownloadTmpDir: make_assets_download_dir_1.makeAssetsDownloadTmpDir,
|
|
59
56
|
tmpDir: tmp_dir_1.tmpDir,
|
|
60
57
|
deleteDirectory: delete_directory_1.deleteDirectory,
|
|
61
|
-
prepareServer: prepare_server_1.prepareServer,
|
|
62
58
|
isServeUrl: is_serve_url_1.isServeUrl,
|
|
63
59
|
ensureOutputDirectory: ensure_output_directory_1.ensureOutputDirectory,
|
|
64
60
|
getRealFrameRange: get_frame_to_render_1.getRealFrameRange,
|
|
@@ -30,8 +30,8 @@ const mergeAudioTrackUnlimited = async ({ ffmpegExecutable, outName, files, numb
|
|
|
30
30
|
});
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
// FFMPEG
|
|
34
|
-
if (files.length
|
|
33
|
+
// In FFMPEG, the total number of left and right tracks that can be merged at one time is limited to 64
|
|
34
|
+
if (files.length >= 32) {
|
|
35
35
|
const chunked = (0, chunk_1.chunk)(files, 10);
|
|
36
36
|
const tempPath = (0, tmp_dir_1.tmpDir)('remotion-large-audio-mixing');
|
|
37
37
|
const chunkNames = await Promise.all(chunked.map(async (chunkFiles, i) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RequestListener } from 'http';
|
|
2
|
+
import { FfmpegExecutable } from 'remotion';
|
|
3
|
+
import { RenderMediaOnDownload } from './assets/download-and-map-assets-to-file';
|
|
4
|
+
export declare const extractUrlAndSourceFromUrl: (url: string) => {
|
|
5
|
+
src: string;
|
|
6
|
+
time: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const startOffthreadVideoServer: ({ ffmpegExecutable, downloadDir, onDownload, onError, }: {
|
|
9
|
+
ffmpegExecutable: FfmpegExecutable;
|
|
10
|
+
downloadDir: string;
|
|
11
|
+
onDownload: RenderMediaOnDownload;
|
|
12
|
+
onError: (err: Error) => void;
|
|
13
|
+
}) => RequestListener;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startOffthreadVideoServer = exports.extractUrlAndSourceFromUrl = void 0;
|
|
4
|
+
const url_1 = require("url");
|
|
5
|
+
const download_and_map_assets_to_file_1 = require("./assets/download-and-map-assets-to-file");
|
|
6
|
+
const extract_frame_from_video_1 = require("./extract-frame-from-video");
|
|
7
|
+
const extractUrlAndSourceFromUrl = (url) => {
|
|
8
|
+
const parsed = new URL(url, 'http://localhost');
|
|
9
|
+
const query = parsed.search;
|
|
10
|
+
if (!query.trim()) {
|
|
11
|
+
throw new Error('Expected query from ' + url);
|
|
12
|
+
}
|
|
13
|
+
const params = new url_1.URLSearchParams(query);
|
|
14
|
+
const src = params.get('src');
|
|
15
|
+
if (!src) {
|
|
16
|
+
throw new Error('Did not pass `src` parameter');
|
|
17
|
+
}
|
|
18
|
+
const time = params.get('time');
|
|
19
|
+
if (!time) {
|
|
20
|
+
throw new Error('Did not get `time` parameter');
|
|
21
|
+
}
|
|
22
|
+
return { src, time: parseFloat(time) };
|
|
23
|
+
};
|
|
24
|
+
exports.extractUrlAndSourceFromUrl = extractUrlAndSourceFromUrl;
|
|
25
|
+
const startOffthreadVideoServer = ({ ffmpegExecutable, downloadDir, onDownload, onError, }) => {
|
|
26
|
+
return (req, res) => {
|
|
27
|
+
if (!req.url) {
|
|
28
|
+
throw new Error('Request came in without URL');
|
|
29
|
+
}
|
|
30
|
+
if (!req.url.startsWith('/proxy')) {
|
|
31
|
+
res.writeHead(404);
|
|
32
|
+
res.end();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
36
|
+
res.setHeader('content-type', 'image/jpg');
|
|
37
|
+
const { src, time } = (0, exports.extractUrlAndSourceFromUrl)(req.url);
|
|
38
|
+
const to = (0, download_and_map_assets_to_file_1.getSanitizedFilenameForAssetUrl)({ downloadDir, src });
|
|
39
|
+
(0, download_and_map_assets_to_file_1.startDownloadForSrc)({ src, downloadDir, onDownload }).catch((err) => {
|
|
40
|
+
onError(new Error(`Error while downloading asset: ${err.stack}`));
|
|
41
|
+
});
|
|
42
|
+
(0, download_and_map_assets_to_file_1.waitForAssetToBeDownloaded)(src, to)
|
|
43
|
+
.then(() => {
|
|
44
|
+
return (0, extract_frame_from_video_1.extractFrameFromVideo)({
|
|
45
|
+
time,
|
|
46
|
+
src: to,
|
|
47
|
+
ffmpegExecutable,
|
|
48
|
+
});
|
|
49
|
+
})
|
|
50
|
+
.then((readable) => {
|
|
51
|
+
if (!readable) {
|
|
52
|
+
throw new Error('no readable from ffmpeg');
|
|
53
|
+
}
|
|
54
|
+
res.writeHead(200);
|
|
55
|
+
res.write(readable);
|
|
56
|
+
res.end();
|
|
57
|
+
})
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
res.writeHead(500);
|
|
60
|
+
res.end();
|
|
61
|
+
console.log('Error occurred', err);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
exports.startOffthreadVideoServer = startOffthreadVideoServer;
|