-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender.ts
More file actions
148 lines (136 loc) · 5.68 KB
/
Copy pathrender.ts
File metadata and controls
148 lines (136 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import 'server-only'
import { statSync } from 'node:fs'
import { join } from 'node:path'
import { bundleDir, rendersDir } from './paths'
import { renderConcurrency, renderTimeoutMs } from './config'
import { FILM_COMP } from '@/remotion/constants'
import type { DemoVideoProps } from './types'
/**
* Rendering an MP4, on this machine.
*
* The app this came from rendered on Remotion Lambda: an AWS account, a
* deployed function, a bucket, four environment variables, and a per-render
* cost that existed to be metered. All of that was there to make export a
* thing worth charging for. A tool you host yourself can just write the file.
*
* Two pieces of setup make that fast:
*
* The bundle. Remotion needs the composition built as a static site before
* it can render. That is a webpack build, so it happens once at image-build
* time and is reused by every render afterwards. Built on demand in
* development, where the source is right there.
*
* The browser. Remotion drives a headless Chrome to rasterise each frame and
* muxes with its own bundled compositor, so there is no system ffmpeg in the
* path and nothing to install by hand.
*/
export interface RenderHandle {
file: string
bytes: number
}
let bundlePromise: Promise<string> | null = null
/**
* Where the built composition lives.
*
* `FLOWY_BUNDLE_DIR` is set in the container, where the bundle was produced
* during the image build and the source is not present at runtime. Without it
* — a local `npm run dev` — the bundle is built once into data/ and reused,
* which costs about twenty seconds on the first render of a session and
* nothing after that.
*/
async function ensureBundle(): Promise<string> {
const prebuilt = process.env.FLOWY_BUNDLE_DIR
if (prebuilt) {
try {
if (statSync(join(prebuilt, 'index.html')).isFile()) return prebuilt
} catch {
throw new Error(
`FLOWY_BUNDLE_DIR is set to ${prebuilt} but there is no index.html there. ` +
`The image was built without running \`npx remotion bundle\`.`,
)
}
}
if (!bundlePromise) {
bundlePromise = (async () => {
const { bundle } = await import('@remotion/bundler')
return bundle({
// Deliberately a path string rather than `require.resolve`. Resolving
// the entry statically makes the bundler follow it, which pulls the
// whole composition tree — components, hooks and all — into the server
// build, and the build then fails on the first `useEffect` it finds in
// what it now believes is a server module.
entryPoint: join(process.cwd(), 'src', 'remotion', 'index.ts'),
outDir: bundleDir(),
})
})().catch((err) => {
// A failed bundle must not be cached, or every later render in this
// process replays the same failure without retrying.
bundlePromise = null
throw err
})
}
return bundlePromise
}
export interface RenderOptions {
props: DemoVideoProps
outputFile: string
onProgress?: (progress: number) => void
signal?: AbortSignal
}
export async function renderFilm(opts: RenderOptions): Promise<RenderHandle> {
const { renderMedia, selectComposition } = await import('@remotion/renderer')
const serveUrl = await ensureBundle()
const outputLocation = join(rendersDir(), opts.outputFile)
const composition = await selectComposition({
serveUrl,
id: FILM_COMP.id,
// Passed here as well as to renderMedia because the composition's
// calculateMetadata derives its length from the script — select it with
// different props and you render somebody else's runtime.
inputProps: opts.props as unknown as Record<string, unknown>,
})
const concurrency = renderConcurrency()
await renderMedia({
composition,
serveUrl,
codec: 'h264',
outputLocation,
inputProps: opts.props as unknown as Record<string, unknown>,
...(concurrency ? { concurrency } : {}),
/**
* A ceiling on how long any single `delayRender` handle may stay open, and
* generous on purpose.
*
* This is not a performance budget, though it is easy to use it as one and
* I did: at 120 seconds every containerised render of a film longer than
* about thirty seconds died partway through, always naming a font. Docker
* on macOS runs this workload roughly two and a half times slower than the
* host, so those renders simply outlived the timeout — and because a
* timeout reports the *oldest* open handle rather than the thing that is
* actually stuck, the report pointed at typography that had been on screen
* for a minute. The native renders looked healthy only because they
* finished underneath it.
*
* Ten minutes bounds a genuinely wedged render without punishing a slow
* machine for being slow. Raise it with RENDER_TIMEOUT_MS.
*/
timeoutInMilliseconds: renderTimeoutMs(),
/**
* `offthreadVideoCacheSizeInBytes` is deliberately not set.
*
* Capping it looks like the responsible thing to do and is how this render
* was broken for an afternoon: pinned to 512 MB, the film stalled at a
* different frame every run — 340, 387, then 570 — and reported the oldest
* open handle, which is a font, so every failure blamed typography that
* had loaded correctly. Left alone, Remotion sizes the cache against
* actual free memory and all 948 frames render straight through.
*/
onProgress: ({ progress }) => opts.onProgress?.(progress),
cancelSignal: opts.signal
? (cancel) => {
opts.signal?.addEventListener('abort', () => cancel(), { once: true })
}
: undefined,
})
return { file: opts.outputFile, bytes: statSync(outputLocation).size }
}