vibeeditor-render 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/cli/vibeeditor-render.js +19 -3
- package/core/audio.js +140 -18
- package/core/cache.js +21 -1
- package/core/config.js +5 -0
- package/core/ffmpeg.js +149 -0
- package/core/jobs.js +169 -24
- package/core/normalizar.js +454 -0
- package/core/nucleo.js +8 -3
- package/core/servidor.js +22 -1
- package/core/transcricao.js +197 -0
- package/core/voz.js +227 -0
- package/host-dist/15fb600d42056579.woff2 +0 -0
- package/host-dist/1895916a733c4d0e.woff2 +0 -0
- package/host-dist/1e96c5356ee905bc.woff2 +0 -0
- package/host-dist/2a8028ec716097bf.woff2 +0 -0
- package/host-dist/3061f5b6eb250216.woff2 +0 -0
- package/host-dist/376dd8dc38524313.woff2 +0 -0
- package/host-dist/41be664a74a50674.woff2 +0 -0
- package/host-dist/428.bundle.js +1 -1
- package/host-dist/46019c506ae1d42c.woff2 +0 -0
- package/host-dist/50129cb1dcd58da5.woff2 +0 -0
- package/host-dist/5ab57485954531d6.woff2 +0 -0
- package/host-dist/606cdd767ee835e0.woff2 +0 -0
- package/host-dist/66df0abfcfe8da3e.woff2 +0 -0
- package/host-dist/6903de9124a5cab6.woff2 +0 -0
- package/host-dist/693cf6c39ab15de9.woff2 +0 -0
- package/host-dist/72884976680cf128.woff2 +0 -0
- package/host-dist/7777133e901cd5ed.woff2 +0 -0
- package/host-dist/78db4f537e4b09d0.woff2 +0 -0
- package/host-dist/7a78f1ce0329757f.woff2 +0 -0
- package/host-dist/7c3e92e6007b76fd.woff2 +0 -0
- package/host-dist/81df42c325a360a6.woff2 +0 -0
- package/host-dist/8633e74025a037d3.woff2 +0 -0
- package/host-dist/872.bundle.js +1 -1
- package/host-dist/8cf68dfb15aca3e4.woff2 +0 -0
- package/host-dist/8fb1a96451cec840.woff2 +0 -0
- package/host-dist/916d3686010a8de2.woff2 +0 -0
- package/host-dist/92d1bbe48d4eec3e.woff2 +0 -0
- package/host-dist/93e25f31623ab732.woff2 +0 -0
- package/host-dist/95045d685be8648f.woff2 +0 -0
- package/host-dist/9545a3d1fceacf5c.woff2 +0 -0
- package/host-dist/96be080130d6f32c.woff2 +0 -0
- package/host-dist/9a881e2ac07d406b.woff2 +0 -0
- package/host-dist/ad20d13d2a924905.woff2 +0 -0
- package/host-dist/af2e87f5d61b5665.woff2 +0 -0
- package/host-dist/b19b14aaaeb386b9.woff2 +0 -0
- package/host-dist/b447814dc676cec1.woff2 +0 -0
- package/host-dist/b7b43acee81d05a8.woff2 +0 -0
- package/host-dist/bd427f25ac24d036.woff2 +0 -0
- package/host-dist/bundle.js +1509 -12
- package/host-dist/bundle.js.map +1 -1
- package/host-dist/c04551857776278f.woff2 +0 -0
- package/host-dist/c050c0dd427d93a6.woff2 +0 -0
- package/host-dist/cb2a73f0b29f1031.woff2 +0 -0
- package/host-dist/d36a2a2bb416f59e.woff2 +0 -0
- package/host-dist/d4df281bd2c98884.woff2 +0 -0
- package/host-dist/d869208648ca5469.woff2 +0 -0
- package/host-dist/db9d659a1acad1e2.woff2 +0 -0
- package/host-dist/ebeb24f1e7287d3a.woff2 +0 -0
- package/host-dist/ef6d3f52c547a6e3.woff2 +0 -0
- package/host-dist/f4646f71883e1c80.woff2 +0 -0
- package/package.json +13 -2
package/core/jobs.js
CHANGED
|
@@ -8,12 +8,50 @@ const {LIMITES, PRESETS} = require("./config");
|
|
|
8
8
|
const {sanitizarNome, shaValido} = require("./seguranca");
|
|
9
9
|
|
|
10
10
|
const inteiro = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
11
|
+
const numero = (v, min, max) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
|
|
12
|
+
const NOME_ARQUIVO = /^[\w.-]{1,64}$/;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* pedido de áudio tratado (opcional): antes do render, o app trata a fala de arquivos[de] com o ffmpeg (cadeia do
|
|
16
|
+
* montar.py) e põe o WAV no job como arquivos[para]. {de, para, trechos [[ini, fim]], ganhos_db [], lufs_alvo,
|
|
17
|
+
* pico_dbtp, passa_alta_hz, reducao_ruido_db, voz_gerada, pico_final_dbtp (opcional: limitador no MP4 final)}
|
|
18
|
+
*/
|
|
19
|
+
function validarAudio(a, arquivos, erro) {
|
|
20
|
+
if (a == null) return null;
|
|
21
|
+
if (typeof a !== "object" || Array.isArray(a)) throw erro("audio precisa ser um objeto");
|
|
22
|
+
if (typeof a.de !== "string" || !Object.prototype.hasOwnProperty.call(arquivos, a.de)) throw erro("audio.de precisa ser um dos arquivos do job");
|
|
23
|
+
if (typeof a.para !== "string" || !NOME_ARQUIVO.test(a.para) || Object.prototype.hasOwnProperty.call(arquivos, a.para)) throw erro("audio.para precisa ser um nome novo (letras, números, . _ -)");
|
|
24
|
+
const t = a.trechos;
|
|
25
|
+
if (!Array.isArray(t) || !t.length || t.length > 2000) throw erro("audio.trechos precisa de 1 a 2000 trechos");
|
|
26
|
+
let ant = 0;
|
|
27
|
+
for (const x of t) {
|
|
28
|
+
if (!Array.isArray(x) || x.length !== 2 || !numero(x[0], 0, 86400) || !numero(x[1], 0, 86400) || x[1] < x[0] || x[0] < ant) throw erro("audio.trechos: [[ini, fim]] em ordem, em segundos");
|
|
29
|
+
ant = x[1];
|
|
30
|
+
}
|
|
31
|
+
const g = a.ganhos_db == null ? t.map(() => 0) : a.ganhos_db;
|
|
32
|
+
if (!Array.isArray(g) || g.length !== t.length || !g.every((v) => numero(v, -24, 24))) throw erro("audio.ganhos_db: um ganho (−24 a 24 dB) por trecho");
|
|
33
|
+
if (!numero(a.lufs_alvo, -40, -5) || !numero(a.pico_dbtp, -12, 0) || !numero(a.passa_alta_hz ?? 70, 0, 500) || !numero(a.reducao_ruido_db ?? 0, 0, 97)) {
|
|
34
|
+
throw erro("audio: lufs_alvo (−40 a −5), pico_dbtp (−12 a 0), passa_alta_hz (0 a 500), reducao_ruido_db (0 a 97)");
|
|
35
|
+
}
|
|
36
|
+
if (a.pico_final_dbtp != null && !numero(a.pico_final_dbtp, -12, 0)) throw erro("audio.pico_final_dbtp: −12 a 0");
|
|
37
|
+
return {de: a.de, para: a.para, trechos: t.map(([x, y]) => [x, y]), ganhos_db: [...g], lufs_alvo: a.lufs_alvo, pico_dbtp: a.pico_dbtp,
|
|
38
|
+
passa_alta_hz: a.passa_alta_hz ?? 70, reducao_ruido_db: a.reducao_ruido_db ?? 0, voz_gerada: Boolean(a.voz_gerada),
|
|
39
|
+
pico_final_dbtp: a.pico_final_dbtp ?? null};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** arquivos do job que são o vídeo original da Edição Simples (plat/src/motor/job-edicao.js, NOME_VIDEO) */
|
|
43
|
+
const NOMES_NORMALIZAVEIS = ["original"];
|
|
44
|
+
|
|
45
|
+
/** quantas vezes um job tenta renderizar quando a falha é passageira (do compositor, não do código do vídeo) */
|
|
46
|
+
const TENTATIVAS = 2;
|
|
47
|
+
/** erros do compositor do Remotion que não dependem do código nem dos arquivos: vale tentar de novo */
|
|
48
|
+
const falhaPassageira = (e) => /No frame found at position|Compositor exited|Compositor quit|Request closed/i.test(String((e && e.message) || e));
|
|
11
49
|
|
|
12
50
|
/** valida o corpo do POST /v1/jobs; devolve a especificação limpa ou lança {status, message} */
|
|
13
51
|
function validarJob(d, cache) {
|
|
14
52
|
const erro = (m, status = 400) => Object.assign(new Error(m), {status});
|
|
15
53
|
if (!d || typeof d !== "object") throw erro("corpo inválido");
|
|
16
|
-
if (!PRESETS
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(PRESETS, d.preset)) throw erro(`preset precisa ser um de: ${Object.keys(PRESETS).join(", ")}`);
|
|
17
55
|
const c = d.composicao || {};
|
|
18
56
|
if (!inteiro(c.largura, 16, LIMITES.ladoMax) || !inteiro(c.altura, 16, LIMITES.ladoMax)) throw erro("largura/altura inválidas");
|
|
19
57
|
if (!inteiro(c.fps, 1, 120)) throw erro("fps inválido");
|
|
@@ -30,19 +68,33 @@ function validarJob(d, cache) {
|
|
|
30
68
|
if (!cache.existe(sha)) faltam.push(nome);
|
|
31
69
|
}
|
|
32
70
|
if (faltam.length) throw erro(`arquivos ainda não enviados: ${faltam.join(", ")}`, 409);
|
|
33
|
-
|
|
71
|
+
const audio = validarAudio(d.audio, arquivos, erro);
|
|
72
|
+
return {nome: sanitizarNome(d.nome), preset: d.preset, composicao: {largura: c.largura, altura: c.altura, fps: c.fps, duracaoEmQuadros: c.duracaoEmQuadros}, codigo: d.codigo, props, arquivos: {...arquivos}, audio};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* cache de quadros do <OffthreadVideo>: o padrão do Remotion é metade da memória LIVRE na hora do render; com a
|
|
77
|
+
* máquina ocupada isso fica em centenas de MB, os quadros saem do cache logo depois de extraídos e o render da
|
|
78
|
+
* Edição (vários trechos do mesmo original) falha com "No frame found at position". 15% da RAM, entre 1 e 4 GB.
|
|
79
|
+
*/
|
|
80
|
+
function cacheDeQuadros() {
|
|
81
|
+
return Math.round(Math.min(4 * 2 ** 30, Math.max(2 ** 30, os.totalmem() * 0.15)));
|
|
34
82
|
}
|
|
35
83
|
|
|
36
84
|
/** quantas abas do Chromium renderizam em paralelo: metade dos núcleos, limitado pela RAM (~1 GB por aba) e a 8 */
|
|
37
85
|
function concorrencia() {
|
|
86
|
+
const fixa = Number(process.env.VIBE_RENDER_CONCORRENCIA);
|
|
87
|
+
if (Number.isInteger(fixa) && fixa >= 1 && fixa <= 16) return fixa;
|
|
38
88
|
const nucleos = os.cpus().length || 2;
|
|
39
89
|
const porRam = Math.floor(os.totalmem() / 2 ** 30 - 3);
|
|
40
90
|
return Math.max(1, Math.min(Math.floor(nucleos / 2) || 1, porRam, 8));
|
|
41
91
|
}
|
|
42
92
|
|
|
43
|
-
/** escala para o lado curto do preset (
|
|
93
|
+
/** escala para o lado curto do preset (só 2K/4K aumentam) e mantém dimensões pares (yuv420p exige) */
|
|
44
94
|
function escalaDo(preset, largura, altura) {
|
|
45
|
-
|
|
95
|
+
const p = PRESETS[preset];
|
|
96
|
+
let s = p.ladoCurto / Math.min(largura, altura);
|
|
97
|
+
if (!p.aumenta) s = Math.min(1, s);
|
|
46
98
|
for (let i = 0; i < 50; i++) {
|
|
47
99
|
const w = Math.round(largura * s), h = Math.round(altura * s);
|
|
48
100
|
if (w % 2 === 0 && h % 2 === 0) return s;
|
|
@@ -61,18 +113,25 @@ function nomeSaida(dir, nome) {
|
|
|
61
113
|
}
|
|
62
114
|
|
|
63
115
|
/**
|
|
64
|
-
* opc: {cache, hostDir, navegador, binarios, dirSaida, dirTmp, urlAtivo(jobId, sha, token), licenseKey, renderer
|
|
65
|
-
*
|
|
116
|
+
* opc: {cache, hostDir, navegador, binarios, dirSaida, dirTmp, dirDados, urlAtivo(jobId, sha, token), licenseKey, renderer?, audio?,
|
|
117
|
+
* normalizar?, env?}
|
|
118
|
+
* renderer, audio ({garantirFfmpeg, tratarFala, limitarPico}) e normalizar ({garantirProxy, FPS}) são injetáveis nos
|
|
119
|
+
* testes; por padrão, @remotion/renderer, core/ffmpeg.js + core/audio.js e core/normalizar.js.
|
|
66
120
|
*/
|
|
67
121
|
function criarJobs(opc) {
|
|
68
122
|
const R = opc.renderer || require("@remotion/renderer");
|
|
123
|
+
const A = opc.audio || {...require("./ffmpeg"), ...require("./audio")};
|
|
124
|
+
const N = opc.normalizar || require("./normalizar");
|
|
69
125
|
const jobs = new Map();
|
|
70
126
|
const ouvintes = new Map(); // id → Set(fn)
|
|
71
127
|
const fila = [];
|
|
72
128
|
let rodando = null;
|
|
73
129
|
|
|
74
130
|
const visao = (j) => ({id: j.id, nome: j.nome, preset: j.preset, status: j.status, progresso: j.progresso, etapa: j.etapa, erro: j.erro,
|
|
75
|
-
arquivo: j.saida && j.status === "pronto" ? {nome: path.basename(j.saida), bytes: j.bytes} : null, tempoMs: j.tempoMs
|
|
131
|
+
arquivo: j.saida && j.status === "pronto" ? {nome: path.basename(j.saida), bytes: j.bytes} : null, tempoMs: j.tempoMs,
|
|
132
|
+
tentativas: j.tentativas || 1,
|
|
133
|
+
...(j.proxy ? {proxy: j.proxy} : {}),
|
|
134
|
+
...(j.medicaoAudio ? {audio: {normalizacao: j.medicaoAudio.normalizacao, loudnorm_medido: j.medicaoAudio.loudnorm_medido, pico: j.pico || null}} : {})});
|
|
76
135
|
function mudar(j, patch) {
|
|
77
136
|
Object.assign(j, patch);
|
|
78
137
|
for (const fn of ouvintes.get(j.id) || []) fn(visao(j));
|
|
@@ -88,6 +147,50 @@ function criarJobs(opc) {
|
|
|
88
147
|
return visao(j);
|
|
89
148
|
}
|
|
90
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Troca o vídeo original (NOMES_NORMALIZAVEIS) pelo proxy de 30 fps / GOP curto (core/normalizar.js), feito uma
|
|
152
|
+
* vez por sha256 e reaproveitado nos próximos jobs. Só com composição a 30 fps (o proxy é 30 fps constantes).
|
|
153
|
+
* O áudio da fala já foi tratado a partir do original; aqui só muda o que o Chromium lê. Se a normalização
|
|
154
|
+
* falhar (sem ser cancelamento), o job segue com o original, como antes.
|
|
155
|
+
*/
|
|
156
|
+
async function normalizarOriginais(j, ffmpeg, cancelRender) {
|
|
157
|
+
if ((opc.env || process.env).VIBE_RENDER_NORMALIZAR === "0" || j.composicao.fps !== N.FPS) return ffmpeg;
|
|
158
|
+
const nomes = NOMES_NORMALIZAVEIS.filter((n) => Object.prototype.hasOwnProperty.call(j.arquivos, n));
|
|
159
|
+
for (const nome of nomes) {
|
|
160
|
+
const sha = j.arquivos[nome];
|
|
161
|
+
const ab = new AbortController();
|
|
162
|
+
j.cancelar = () => ab.abort();
|
|
163
|
+
const t0 = Date.now();
|
|
164
|
+
try {
|
|
165
|
+
if (!ffmpeg) {
|
|
166
|
+
mudar(j, {status: "preparando", etapa: "Preparando o ffmpeg"});
|
|
167
|
+
ffmpeg = await A.garantirFfmpeg({dirDados: opc.dirDados, onProgresso: (p) => mudar(j, {etapa: `Baixando o ffmpeg (uma vez só) ${Math.round(p * 100)}%`})});
|
|
168
|
+
}
|
|
169
|
+
if (j.status === "cancelado") throw new Error("cancelado");
|
|
170
|
+
const r = await N.garantirProxy(sha, {cache: opc.cache, ffmpeg: ffmpeg.caminho, dirTmp: opc.dirTmp, sinal: ab.signal, env: opc.env || process.env,
|
|
171
|
+
aoSondar: () => mudar(j, {status: "preparando", etapa: "Conferindo o vídeo original"}),
|
|
172
|
+
onProgresso: (p) => {
|
|
173
|
+
const etapa = `Preparando o vídeo original (uma vez só) ${Math.round(p * 100)}%`;
|
|
174
|
+
if (etapa !== j.etapa) mudar(j, {etapa});
|
|
175
|
+
}});
|
|
176
|
+
j.proxy = {...(j.proxy || {}), [nome]: {usado: r.proxy, novo: r.novo, motivos: r.motivos, plano: r.plano || null,
|
|
177
|
+
segundos: r.novo ? Math.round((Date.now() - t0) / 100) / 10 : 0}};
|
|
178
|
+
if (r.proxy) {
|
|
179
|
+
j.originais = {...(j.originais || {}), [nome]: sha};
|
|
180
|
+
j.arquivos[nome] = r.sha; // o servidor só entrega ao Chromium os sha256 que estão em j.arquivos
|
|
181
|
+
opc.cache.tocar(r.sha);
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (j.status === "cancelado" || ab.signal.aborted) throw new Error("cancelado");
|
|
185
|
+
j.proxy = {...(j.proxy || {}), [nome]: {usado: false, erro: String((e && e.message) || e).slice(0, 500)}};
|
|
186
|
+
} finally {
|
|
187
|
+
j.cancelar = cancelRender;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (nomes.length) mudar(j, {etapa: "Abrindo o navegador de render"});
|
|
191
|
+
return ffmpeg;
|
|
192
|
+
}
|
|
193
|
+
|
|
91
194
|
async function proximo() {
|
|
92
195
|
if (rodando || !fila.length) return;
|
|
93
196
|
const j = fila.shift();
|
|
@@ -101,26 +204,68 @@ function criarJobs(opc) {
|
|
|
101
204
|
try {
|
|
102
205
|
mudar(j, {status: "preparando", etapa: "Abrindo o navegador de render"});
|
|
103
206
|
for (const sha of Object.values(j.arquivos)) opc.cache.tocar(sha);
|
|
207
|
+
let ffmpeg = null;
|
|
208
|
+
if (j.audio) {
|
|
209
|
+
mudar(j, {status: "preparando", etapa: "Preparando o ffmpeg"});
|
|
210
|
+
ffmpeg = await A.garantirFfmpeg({dirDados: opc.dirDados, onProgresso: (p) => mudar(j, {etapa: `Baixando o ffmpeg (uma vez só) ${Math.round(p * 100)}%`})});
|
|
211
|
+
if (j.status === "cancelado") throw new Error("cancelado");
|
|
212
|
+
mudar(j, {etapa: "Tratando o áudio da fala"});
|
|
213
|
+
const wav = path.join(opc.dirTmp, `${j.id}-fala.wav`);
|
|
214
|
+
fs.mkdirSync(opc.dirTmp, {recursive: true});
|
|
215
|
+
const ab = new AbortController();
|
|
216
|
+
j.cancelar = () => ab.abort();
|
|
217
|
+
j.medicaoAudio = await A.tratarFala(opc.cache.caminho(j.arquivos[j.audio.de]), wav, j.audio, {ffmpeg: ffmpeg.caminho, versao: ffmpeg.versao, sinal: ab.signal, dirTmp: opc.dirTmp});
|
|
218
|
+
j.arquivos[j.audio.para] = await opc.cache.adotar(wav);
|
|
219
|
+
j.cancelar = cancel;
|
|
220
|
+
if (j.status === "cancelado") throw new Error("cancelado");
|
|
221
|
+
mudar(j, {etapa: "Abrindo o navegador de render"});
|
|
222
|
+
}
|
|
223
|
+
ffmpeg = await normalizarOriginais(j, ffmpeg, cancel);
|
|
104
224
|
const ativos = Object.fromEntries(Object.entries(j.arquivos).map(([nome, sha]) => [nome, opc.urlAtivo(j.id, sha, j.token)]));
|
|
105
225
|
const inputProps = {codigo: j.codigo, props: j.props, ativos, composicao: j.composicao};
|
|
106
226
|
const comum = {browserExecutable: opc.navegador || null, binariesDirectory: opc.binarios || null, chromeMode: "headless-shell", logLevel: "warn"};
|
|
107
|
-
browser = await R.openBrowser("chrome", {browserExecutable: comum.browserExecutable, chromeMode: "headless-shell", logLevel: "warn"});
|
|
108
|
-
const composition = await R.selectComposition({...comum, serveUrl: opc.hostDir, id: "VibeHost", inputProps, puppeteerInstance: browser});
|
|
109
227
|
const p = PRESETS[j.preset];
|
|
110
228
|
fs.mkdirSync(opc.dirTmp, {recursive: true});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
229
|
+
for (let tentativa = 1; ; tentativa++) {
|
|
230
|
+
try {
|
|
231
|
+
browser = await R.openBrowser("chrome", {browserExecutable: comum.browserExecutable, chromeMode: "headless-shell", logLevel: "warn"});
|
|
232
|
+
const composition = await R.selectComposition({...comum, serveUrl: opc.hostDir, id: "VibeHost", inputProps, puppeteerInstance: browser});
|
|
233
|
+
mudar(j, {status: "renderizando", etapa: "Renderizando quadros"});
|
|
234
|
+
// na repetição vai com 1 aba: vídeo com GOP longo / fps variável (ex.: WhatsApp, 1 quadro-chave a cada 10 s)
|
|
235
|
+
// com várias abas pedindo pontos distantes do mesmo original faz o compositor "não achar" o quadro
|
|
236
|
+
const abas = tentativa > 1 ? 1 : concorrencia();
|
|
237
|
+
await R.renderMedia({
|
|
238
|
+
...comum, composition, serveUrl: opc.hostDir, inputProps, puppeteerInstance: browser,
|
|
239
|
+
codec: "h264", outputLocation: tmp, overwrite: true,
|
|
240
|
+
scale: escalaDo(j.preset, composition.width, composition.height),
|
|
241
|
+
crf: p.crf, x264Preset: p.x264Preset, imageFormat: "jpeg", jpegQuality: p.jpegQuality,
|
|
242
|
+
concurrency: abas, cancelSignal, timeoutInMilliseconds: 120000,
|
|
243
|
+
offthreadVideoCacheSizeInBytes: cacheDeQuadros(),
|
|
244
|
+
// o padrão (2) deixa o original 4K HEVC (iPhone) decodificando em 2 threads enquanto as abas esperam; mais
|
|
245
|
+
// threads só com memória livre (cada uma segura quadros 4K; com a máquina apertada o compositor cai)
|
|
246
|
+
offthreadVideoThreads: os.freemem() > 6 * 2 ** 30 ? Math.min(4, Math.max(2, abas)) : 2,
|
|
247
|
+
licenseKey: opc.licenseKey || null,
|
|
248
|
+
onProgress: ({progress, stitchStage}) => {
|
|
249
|
+
const etapa = stitchStage === "muxing" ? "Juntando áudio e vídeo" : abas === 1 && tentativa > 1 ? "Renderizando quadros (1 aba)" : "Renderizando quadros";
|
|
250
|
+
if (Math.abs(progress - j.progresso) >= 0.01 || etapa !== j.etapa) mudar(j, {progresso: progress, etapa});
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
} catch (e) {
|
|
255
|
+
if (j.status === "cancelado" || tentativa >= TENTATIVAS || !falhaPassageira(e)) throw e;
|
|
256
|
+
// o compositor do Remotion às vezes "não acha" um quadro que existe no vídeo-base (<OffthreadVideo>), num
|
|
257
|
+
// ponto diferente a cada render; acontece também com o bundle do editor local, então não é do host.
|
|
258
|
+
// Recomeça do zero com navegador novo e 1 aba (docs/M1-REFERENCIAS.md, docs/M2-MOTOR-ETAPA1.md)
|
|
259
|
+
if (browser) await browser.close({silent: true}).catch(() => {});
|
|
260
|
+
browser = null;
|
|
261
|
+
j.tentativas = tentativa + 1;
|
|
262
|
+
mudar(j, {status: "preparando", progresso: 0, etapa: "Renderizando de novo, com 1 aba (vídeo difícil de buscar)"});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (j.audio && j.audio.pico_final_dbtp != null) {
|
|
266
|
+
mudar(j, {etapa: "Conferindo o pico do áudio"});
|
|
267
|
+
j.pico = await A.limitarPico(tmp, j.audio.pico_final_dbtp, {ffmpeg: ffmpeg.caminho});
|
|
268
|
+
}
|
|
124
269
|
const dirSaida = typeof opc.dirSaida === "function" ? opc.dirSaida() : opc.dirSaida;
|
|
125
270
|
fs.mkdirSync(dirSaida, {recursive: true});
|
|
126
271
|
const saida = nomeSaida(dirSaida, j.nome);
|
|
@@ -159,7 +304,7 @@ function criarJobs(opc) {
|
|
|
159
304
|
}
|
|
160
305
|
const ativos = () => [...jobs.values()].filter((j) => ["fila", "preparando", "renderizando"].includes(j.status)).map(visao);
|
|
161
306
|
|
|
162
|
-
return {criar, obter, interno, cancelar, ouvir, ativos, emUso: () => new Set([...jobs.values()].filter((j) => j.status !== "pronto").flatMap((j) => Object.values(j.arquivos || {})))};
|
|
307
|
+
return {criar, obter, interno, cancelar, ouvir, ativos, emUso: () => new Set([...jobs.values()].filter((j) => j.status !== "pronto").flatMap((j) => [...Object.values(j.arquivos || {}), ...Object.values(j.originais || {})]))};
|
|
163
308
|
}
|
|
164
309
|
|
|
165
|
-
module.exports = {criarJobs, validarJob, escalaDo, concorrencia, nomeSaida};
|
|
310
|
+
module.exports = {criarJobs, validarJob, escalaDo, concorrencia, nomeSaida, falhaPassageira, TENTATIVAS, NOMES_NORMALIZAVEIS};
|