primocode 8.22.1 → 8.26.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/app/app.css +230 -0
- package/app/app.js +273 -0
- package/app/icone.svg +6 -0
- package/app/index.html +68 -0
- package/bin/primocode.js +52 -0
- package/lib/app.js +334 -0
- package/lib/catalogo.js +2 -0
- package/lib/config.js +53 -2
- package/lib/studio.js +125 -11
- package/lib/tools.js +35 -1
- package/package.json +3 -2
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-estatico-corte-a5bab.json +150 -0
- package/studio/arquivos/corte-founders-ai-teste-do-player-2de1d.json +65 -0
- package/studio/arquivos/corte-legado-teste.json +1 -0
- package/studio/arquivos/corte-legado2.json +1 -0
- package/studio/arquivos/corte-minimo-2a829.json +24 -0
- package/studio/arquivos/corte-ok-00a3b.json +18 -0
- package/studio/arquivos/corte-ok-1270e.json +22 -0
- package/studio/arquivos/corte-sweep-corte-cfb29.json +147 -0
- package/studio/arquivos/corte-visual-completo-78f50.json +35 -0
- package/studio/arquivos/grade-sweep-grade-0c2f1.json +119 -0
- package/studio/arquivos/prisma-estatico-prisma-d8740.json +182 -0
- package/studio/arquivos/prisma-sweep-prisma-4c58c.json +166 -0
- package/studio/arquivos/prisma-y-b0785.json +52 -0
- package/studio/arquivos/prosa-sweep-prosa-f29cf.json +80 -0
- package/studio/arquivos/tela-estatico-tela-c7daf.json +90 -0
- package/studio/arquivos/tela-sweep-tela-0d2bf.json +84 -0
- package/studio/arquivos/traco-estatico-traco-25872.json +113 -0
- package/studio/arquivos/traco-sweep-traco-0bcf4.json +102 -0
- package/studio/ferramentas/corte.md +159 -12
- package/studio/server.py +264 -8
- package/studio/web/analise.html +52 -0
- package/studio/web/js/analise.js +202 -0
- package/studio/web/js/exportar.js +88 -1
- package/studio/web/js/player.js +172 -46
- package/studio/web/js/render.js +99 -1
- package/studio/web/js/video.js +434 -0
- package/studio/web/render.css +27 -0
- package/studio/web/tool.html +1 -0
- package/studio/web/view.html +2 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
video.js — o vídeo vira ARQUIVO. Sem gravar a tela de ninguém.
|
|
3
|
+
|
|
4
|
+
── O QUE ESTAVA ERRADO ─────────────────────────────────────────────────
|
|
5
|
+
O botão de baixar usava getDisplayMedia: pedia permissão, o usuário
|
|
6
|
+
escolhia uma janela, e o navegador salvava o que estivesse aparecendo.
|
|
7
|
+
Isso é gravação de tela, não exportação. O dono foi direto ao ponto, em
|
|
8
|
+
quatro conversas diferentes:
|
|
9
|
+
|
|
10
|
+
"O mp4 não vai ser uma gravação da tela do usuário, tem nada a ver.
|
|
11
|
+
Tu clica no botão download ele já vai baixar o arquivo."
|
|
12
|
+
|
|
13
|
+
E não é preferência. Gravando a tela: o ponteiro do mouse entra no vídeo,
|
|
14
|
+
uma notificação entra no vídeo, a resolução é a da janela e não a do
|
|
15
|
+
projeto, e quem for VENDER a ferramenta não vai pedir para o cliente
|
|
16
|
+
compartilhar a tela para exportar um arquivo dele mesmo.
|
|
17
|
+
|
|
18
|
+
── COMO FUNCIONA ───────────────────────────────────────────────────────
|
|
19
|
+
O vídeo é DESENHADO. Um canvas do tamanho exato do projeto é fotografado
|
|
20
|
+
a partir da cena que está tocando, quadro a quadro, e vira faixa de vídeo
|
|
21
|
+
por captureStream. O áudio vem do grafo que o player já monta — trilha,
|
|
22
|
+
narração e efeitos, com o ducking que abaixa a música na voz. O
|
|
23
|
+
MediaRecorder junta os dois num arquivo. Zero permissão.
|
|
24
|
+
|
|
25
|
+
── POR QUE ELE DIRIGE O PLAYER EM VEZ DE RENDERIZAR SOZINHO ────────────
|
|
26
|
+
Porque o player já sabe tocar: ele encadeia as cenas, dispara a narração
|
|
27
|
+
no tempo certo, aplica ducking e toca os efeitos de transição. Refazer
|
|
28
|
+
isso aqui seria manter duas versões da mesma lógica — e elas divergiriam,
|
|
29
|
+
como já aconteceu neste código com o editor e o renderizador. Aqui o
|
|
30
|
+
exportador só pergunta "qual cena está na tela agora?" e fotografa.
|
|
31
|
+
|
|
32
|
+
── POR QUE EM TEMPO REAL ───────────────────────────────────────────────
|
|
33
|
+
O ideal seria renderizar sem pressa e carimbar cada quadro com o tempo
|
|
34
|
+
dele (MediaStreamTrackGenerator). Foi testado: o MediaRecorder não produz
|
|
35
|
+
um byte com faixa vinda de gerador — zero em mp4, webm/vp8 e webm/vp9. Só
|
|
36
|
+
o captureStream de canvas grava, e ele é preso ao relógio.
|
|
37
|
+
|
|
38
|
+
O custo aparece na taxa de quadros: rasterizar 1920×1080 leva ~76 ms.
|
|
39
|
+
Mas quase nunca é preciso rasterizar — numa cena de 4 segundos a entrada
|
|
40
|
+
dura menos de um, e o resto do tempo o quadro está PARADO. Este arquivo
|
|
41
|
+
pergunta ao navegador se ainda há animação rodando; enquanto há,
|
|
42
|
+
redesenha; quando todas terminam, para, e o captureStream segue emitindo
|
|
43
|
+
o último quadro na taxa cheia. Movimento suave onde há movimento, imagem
|
|
44
|
+
limpa onde não há.
|
|
45
|
+
========================================================================== */
|
|
46
|
+
|
|
47
|
+
var VideoExport = (function () {
|
|
48
|
+
"use strict";
|
|
49
|
+
|
|
50
|
+
/* Do mais compatível para o menos.
|
|
51
|
+
*
|
|
52
|
+
* H.264 com AAC é o MP4 que toca em tudo — WhatsApp, Instagram,
|
|
53
|
+
* PowerPoint, TV, editor de vídeo. AV1 dentro de MP4 é MP4 de verdade mas
|
|
54
|
+
* ainda não toca em todo lugar, então vem depois. O webm é o último
|
|
55
|
+
* recurso: abre em navegador e pouco mais.
|
|
56
|
+
*
|
|
57
|
+
* Testar só "video/mp4" não basta: o Chrome responde false para o nome
|
|
58
|
+
* solto e true para a forma com codec. Já custou MP4 a quem pediu MP4. */
|
|
59
|
+
var FORMATOS = [
|
|
60
|
+
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
61
|
+
"video/mp4;codecs=avc1.4d002a,mp4a.40.2",
|
|
62
|
+
"video/mp4;codecs=avc1,mp4a.40.2",
|
|
63
|
+
"video/mp4;codecs=avc1",
|
|
64
|
+
"video/mp4",
|
|
65
|
+
"video/webm;codecs=vp9,opus",
|
|
66
|
+
"video/webm;codecs=vp8,opus",
|
|
67
|
+
"video/webm",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function formato() {
|
|
71
|
+
if (!window.MediaRecorder) return null;
|
|
72
|
+
for (var i = 0; i < FORMATOS.length; i++) {
|
|
73
|
+
if (MediaRecorder.isTypeSupported(FORMATOS[i])) return FORMATOS[i];
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function disponivel() {
|
|
79
|
+
return !!(formato() && document.createElement("canvas").captureStream
|
|
80
|
+
&& typeof Exportar !== "undefined" && Exportar.retrato);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ehMp4(tipo) { return /mp4/.test(tipo || ""); }
|
|
84
|
+
function extensaoDe(tipo) { return ehMp4(tipo) ? ".mp4" : ".webm"; }
|
|
85
|
+
|
|
86
|
+
/* Ainda há algo se mexendo nesta cena?
|
|
87
|
+
*
|
|
88
|
+
* É a pergunta que decide se vale gastar dezenas de milissegundos
|
|
89
|
+
* redesenhando. Animação infinita (Ken Burns, cubo 3D girando, brilho de
|
|
90
|
+
* gradiente) nunca termina — e aí redesenhar é o certo, porque a imagem
|
|
91
|
+
* MUDA mesmo. */
|
|
92
|
+
function aindaAnima(no) {
|
|
93
|
+
if (!no || !no.getAnimations) return true; // sem a API: redesenha sempre
|
|
94
|
+
var todas;
|
|
95
|
+
try { todas = no.getAnimations({ subtree: true }); }
|
|
96
|
+
catch (e) { return true; }
|
|
97
|
+
for (var i = 0; i < todas.length; i++) {
|
|
98
|
+
if (todas[i].playState === "running") return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function duracaoDaCena(c) {
|
|
104
|
+
var d = c && c.duracao;
|
|
105
|
+
return (typeof d === "number" && isFinite(d) && d > 0) ? d : 4;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ------------------------------------------------------------ a fonte -- */
|
|
109
|
+
|
|
110
|
+
/* Uma "fonte" diz ao exportador o que fotografar e por quanto tempo.
|
|
111
|
+
*
|
|
112
|
+
* Quando o player está na página, ele é a fonte: sabe tocar, narrar e
|
|
113
|
+
* mixar. Fora dele (no editor, ou num teste), o exportador monta uma fonte
|
|
114
|
+
* própria que percorre as cenas por conta — mais simples, sem narração.
|
|
115
|
+
*/
|
|
116
|
+
function fonteDoDocumento(doc, palco) {
|
|
117
|
+
var cenas = doc.cenas || [];
|
|
118
|
+
var nos = cenas.map(function (c) {
|
|
119
|
+
var no = renderCena(c, doc, { animar: true });
|
|
120
|
+
no.style.position = "static";
|
|
121
|
+
no.style.transform = "none";
|
|
122
|
+
no.style.display = "none";
|
|
123
|
+
palco.appendChild(no);
|
|
124
|
+
return no;
|
|
125
|
+
});
|
|
126
|
+
var atual = -1;
|
|
127
|
+
var t0 = 0;
|
|
128
|
+
var total = cenas.reduce(function (t, c) { return t + duracaoDaCena(c); }, 0);
|
|
129
|
+
|
|
130
|
+
function qualCena(t) {
|
|
131
|
+
var acc = 0;
|
|
132
|
+
for (var i = 0; i < cenas.length; i++) {
|
|
133
|
+
acc += duracaoDaCena(cenas[i]);
|
|
134
|
+
if (t < acc) return i;
|
|
135
|
+
}
|
|
136
|
+
return cenas.length - 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
duracao: total,
|
|
141
|
+
comecar: function () { t0 = performance.now(); },
|
|
142
|
+
parar: function () {},
|
|
143
|
+
no: function () {
|
|
144
|
+
var t = (performance.now() - t0) / 1000;
|
|
145
|
+
var i = qualCena(t);
|
|
146
|
+
if (i !== atual) {
|
|
147
|
+
atual = i;
|
|
148
|
+
nos.forEach(function (n, k) { n.style.display = k === i ? "block" : "none"; });
|
|
149
|
+
// reinicia entrada e transição, como o player faz
|
|
150
|
+
nos[i].classList.remove("animar");
|
|
151
|
+
void nos[i].offsetWidth;
|
|
152
|
+
nos[i].classList.add("animar");
|
|
153
|
+
}
|
|
154
|
+
return nos[i];
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ------------------------------------------------------------ clipes -- */
|
|
160
|
+
|
|
161
|
+
/* Desenha os vídeos do usuário no canvas, ANTES do desenho da cena.
|
|
162
|
+
*
|
|
163
|
+
* O retrato de uma cena é feita por SVG com foreignObject, e foreignObject
|
|
164
|
+
* NÃO rasteriza <video>: o quadro do vídeo sairia em branco, por mais
|
|
165
|
+
* correto que o resto estivesse. Então o vídeo é desenhado direto no
|
|
166
|
+
* canvas — drawImage aceita um <video> e pega o quadro atual — e a cena
|
|
167
|
+
* entra por cima, onde o buraco do vídeo é transparente.
|
|
168
|
+
*
|
|
169
|
+
* A conta de posição vem do layout real: getBoundingClientRect do vídeo
|
|
170
|
+
* contra o da cena, convertido para as coordenadas do projeto. Assim vale
|
|
171
|
+
* para clipe posicionado à mão (x/y) e para clipe que o layout arrumou.
|
|
172
|
+
*
|
|
173
|
+
* "Onde o buraco é transparente" não era verdade e por isso a gravação
|
|
174
|
+
* sumia do arquivo: a cena carrega o fundo do tema, opaco, e ele cobria o
|
|
175
|
+
* vídeo inteiro. Um vídeo do usuário que não aparece no vídeo do usuário —
|
|
176
|
+
* e nada denunciava, porque na tela, no DOM, estava certo.
|
|
177
|
+
*
|
|
178
|
+
* Então numa cena com clipe a ordem é: a cor de chão do tema, o vídeo, e a
|
|
179
|
+
* cena SEM fundo por cima (é o que `semFundo` faz no clone). O texto, a
|
|
180
|
+
* legenda e a marca continuam por cima da imagem, que é onde devem estar.
|
|
181
|
+
*/
|
|
182
|
+
function desenharClipes(ctx2d, cenaEl, L, A) {
|
|
183
|
+
var vids = cenaEl.querySelectorAll("video");
|
|
184
|
+
if (!vids.length) return;
|
|
185
|
+
var caixaCena = cenaEl.getBoundingClientRect();
|
|
186
|
+
if (!caixaCena.width || !caixaCena.height) return;
|
|
187
|
+
var escX = L / caixaCena.width;
|
|
188
|
+
var escY = A / caixaCena.height;
|
|
189
|
+
|
|
190
|
+
Array.prototype.forEach.call(vids, function (v) {
|
|
191
|
+
// readyState < 2 é vídeo sem quadro nenhum decodificado ainda: pedir
|
|
192
|
+
// para desenhar levanta exceção em alguns navegadores e mancha o
|
|
193
|
+
// canvas em outros.
|
|
194
|
+
if (v.readyState < 2 || !v.videoWidth) return;
|
|
195
|
+
var r = v.getBoundingClientRect();
|
|
196
|
+
var dx = (r.left - caixaCena.left) * escX;
|
|
197
|
+
var dy = (r.top - caixaCena.top) * escY;
|
|
198
|
+
var dw = r.width * escX;
|
|
199
|
+
var dh = r.height * escY;
|
|
200
|
+
if (dw <= 0 || dh <= 0) return;
|
|
201
|
+
|
|
202
|
+
// "cobrir" recorta o vídeo para encher a caixa sem deformar — o mesmo
|
|
203
|
+
// que o object-fit faz na tela. Sem isto o export esticaria a imagem
|
|
204
|
+
// e ficaria diferente do que a pessoa aprovou no player.
|
|
205
|
+
var conter = v.style.objectFit === "contain";
|
|
206
|
+
var escala = conter
|
|
207
|
+
? Math.min(dw / v.videoWidth, dh / v.videoHeight)
|
|
208
|
+
: Math.max(dw / v.videoWidth, dh / v.videoHeight);
|
|
209
|
+
var lw = v.videoWidth * escala;
|
|
210
|
+
var lh = v.videoHeight * escala;
|
|
211
|
+
var lx = dx + (dw - lw) / 2;
|
|
212
|
+
var ly = dy + (dh - lh) / 2;
|
|
213
|
+
|
|
214
|
+
ctx2d.save();
|
|
215
|
+
ctx2d.beginPath();
|
|
216
|
+
ctx2d.rect(dx, dy, dw, dh);
|
|
217
|
+
ctx2d.clip();
|
|
218
|
+
// "cinza", "sepia", "contraste": na tela é filter do CSS, e drawImage
|
|
219
|
+
// ignora o CSS do elemento. O canvas tem o mesmo filtro, com a mesma
|
|
220
|
+
// gramática — então é só repassar, e o arquivo sai como o player.
|
|
221
|
+
var f = window.getComputedStyle(v).filter;
|
|
222
|
+
if (f && f !== "none") { try { ctx2d.filter = f; } catch (e) {} }
|
|
223
|
+
try { ctx2d.drawImage(v, lx, ly, lw, lh); } catch (e) { /* quadro ainda não veio */ }
|
|
224
|
+
ctx2d.restore();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* O chão da cena, para o clipe que não ocupa o quadro inteiro. Sem isto,
|
|
229
|
+
a volta do vídeo sairia preta em vez da cor do tema. */
|
|
230
|
+
function corDeChao(no) {
|
|
231
|
+
var c = window.getComputedStyle(no).backgroundColor;
|
|
232
|
+
return (c && c !== "rgba(0, 0, 0, 0)" && c !== "transparent") ? c : "#0b0e14";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Prepara o CLONE da cena que tem gravação — só o clone, a tela do usuário
|
|
236
|
+
não muda. Duas coisas precisam sair, e as duas escondiam o vídeo:
|
|
237
|
+
|
|
238
|
+
· o fundo da cena, opaco por cima da gravação. `data-fundo` sai junto
|
|
239
|
+
porque os padrões (aurora, raios, ondas) são pseudo-elementos: zerar
|
|
240
|
+
`background` deixaria a camada de cima intacta;
|
|
241
|
+
|
|
242
|
+
· o próprio <video>. foreignObject não decodifica vídeo, mas também não
|
|
243
|
+
deixa o espaço vazio: pinta a caixa do elemento no cinza padrão
|
|
244
|
+
(#333). O quadro saía com a gravação desenhada embaixo e um retângulo
|
|
245
|
+
cinza chapado exatamente em cima dela. */
|
|
246
|
+
function semFundo(clone) {
|
|
247
|
+
clone.removeAttribute("data-fundo");
|
|
248
|
+
clone.style.background = "transparent";
|
|
249
|
+
clone.style.backgroundImage = "none";
|
|
250
|
+
Array.prototype.forEach.call(clone.querySelectorAll("video"), function (v) {
|
|
251
|
+
v.style.visibility = "hidden";
|
|
252
|
+
v.style.background = "transparent";
|
|
253
|
+
// A caixa que segura o vídeo também pinta (é o cinza de espera, útil na
|
|
254
|
+
// tela enquanto o arquivo carrega). No quadro exportado ela é só mais
|
|
255
|
+
// uma tampa em cima da gravação — e foi a última a ser encontrada,
|
|
256
|
+
// porque some junto com o resto do fundo e parece o fundo.
|
|
257
|
+
for (var pai = v.parentNode; pai && pai !== clone; pai = pai.parentNode) {
|
|
258
|
+
pai.style.background = "transparent";
|
|
259
|
+
pai.style.backgroundImage = "none";
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Cena com vídeo nunca fica "parada": o clipe corre mesmo sem animação
|
|
265
|
+
CSS, e pular o redesenho congelaria a imagem do usuário. */
|
|
266
|
+
function temClipe(no) {
|
|
267
|
+
return !!(no && no.querySelector && no.querySelector("video"));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* --------------------------------------------------------------- gerar -- */
|
|
271
|
+
|
|
272
|
+
/* Monta o arquivo e devolve { blob, bytes, formato, mp4, segundos }.
|
|
273
|
+
*
|
|
274
|
+
* `op.fonte` — quem diz o que fotografar (o player, normalmente).
|
|
275
|
+
* `op.fluxoAudio` — MediaStream já mixado. Sem ele o vídeo sai mudo.
|
|
276
|
+
* `op.aoAndar(fracao)` — progresso, para a interface mostrar.
|
|
277
|
+
*/
|
|
278
|
+
function gerar(arq, op) {
|
|
279
|
+
op = op || {};
|
|
280
|
+
var doc = (arq && arq.doc) || {};
|
|
281
|
+
var cenas = doc.cenas || [];
|
|
282
|
+
if (!cenas.length) return Promise.reject(new Error("este arquivo não tem cenas"));
|
|
283
|
+
|
|
284
|
+
var tipo = formato();
|
|
285
|
+
if (!tipo) return Promise.reject(new Error("este navegador não sabe gerar vídeo"));
|
|
286
|
+
|
|
287
|
+
var L = doc.largura || 1920;
|
|
288
|
+
var A = doc.altura || 1080;
|
|
289
|
+
var FPS = op.fps || 30;
|
|
290
|
+
var andar = op.aoAndar || function () {};
|
|
291
|
+
|
|
292
|
+
var tela = document.createElement("canvas");
|
|
293
|
+
tela.width = L; tela.height = A;
|
|
294
|
+
var ctx2d = tela.getContext("2d", { alpha: false });
|
|
295
|
+
ctx2d.fillStyle = "#000";
|
|
296
|
+
ctx2d.fillRect(0, 0, L, A);
|
|
297
|
+
|
|
298
|
+
// Palco próprio só quando o exportador tem de montar as cenas sozinho.
|
|
299
|
+
// Fica DENTRO do documento (fora dele não há layout nem animação) mas
|
|
300
|
+
// fora da vista — e não com display:none, que também mataria o layout.
|
|
301
|
+
var palco = null;
|
|
302
|
+
var fonte = op.fonte;
|
|
303
|
+
if (!fonte) {
|
|
304
|
+
palco = document.createElement("div");
|
|
305
|
+
palco.setAttribute("aria-hidden", "true");
|
|
306
|
+
palco.style.cssText = "position:fixed;left:-99999px;top:0;pointer-events:none;"
|
|
307
|
+
+ "width:" + L + "px;height:" + A + "px;overflow:hidden";
|
|
308
|
+
document.body.appendChild(palco);
|
|
309
|
+
fonte = fonteDoDocumento(doc, palco);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
var fluxo = tela.captureStream(FPS);
|
|
313
|
+
if (op.fluxoAudio) {
|
|
314
|
+
op.fluxoAudio.getAudioTracks().forEach(function (f) { fluxo.addTrack(f); });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
var pedacos = [];
|
|
318
|
+
var rec = new MediaRecorder(fluxo, {
|
|
319
|
+
mimeType: tipo,
|
|
320
|
+
videoBitsPerSecond: op.bitrate || (L * A > 1000000 ? 8000000 : 4000000),
|
|
321
|
+
});
|
|
322
|
+
rec.ondataavailable = function (ev) { if (ev.data && ev.data.size) pedacos.push(ev.data); };
|
|
323
|
+
|
|
324
|
+
var totalMs = (op.duracao || fonte.duracao || 0) * 1000;
|
|
325
|
+
var cancelado = false;
|
|
326
|
+
var folhaCss = null;
|
|
327
|
+
|
|
328
|
+
function limpar() {
|
|
329
|
+
try { if (fonte.parar) fonte.parar(); } catch (e) {}
|
|
330
|
+
if (palco) { try { palco.remove(); } catch (e) {} }
|
|
331
|
+
fluxo.getVideoTracks().forEach(function (t) { try { t.stop(); } catch (e) {} });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
var promessa = Exportar.css().then(function (folha) {
|
|
335
|
+
folhaCss = folha;
|
|
336
|
+
// A fonte pode ter o que carregar antes do primeiro quadro — a gravação
|
|
337
|
+
// do usuário, que sem quadro decodificado desenharia em branco.
|
|
338
|
+
return fonte.preparar ? fonte.preparar() : null;
|
|
339
|
+
}).then(function () {
|
|
340
|
+
rec.start(1000);
|
|
341
|
+
if (fonte.comecar) fonte.comecar();
|
|
342
|
+
|
|
343
|
+
return new Promise(function (pronto) {
|
|
344
|
+
var t0 = performance.now();
|
|
345
|
+
var primeiro = true;
|
|
346
|
+
|
|
347
|
+
function volta() {
|
|
348
|
+
if (cancelado) return pronto();
|
|
349
|
+
var t = performance.now() - t0;
|
|
350
|
+
if (t >= totalMs) return pronto();
|
|
351
|
+
andar(Math.min(0.995, t / totalMs));
|
|
352
|
+
|
|
353
|
+
var no = fonte.no();
|
|
354
|
+
if (!no) return setTimeout(volta, 1000 / FPS);
|
|
355
|
+
|
|
356
|
+
// Quadro idêntico ao anterior não precisa ser refeito: o
|
|
357
|
+
// captureStream repete o último sozinho, na taxa cheia.
|
|
358
|
+
if (!primeiro && !aindaAnima(no) && !temClipe(no)) {
|
|
359
|
+
return setTimeout(volta, 1000 / FPS);
|
|
360
|
+
}
|
|
361
|
+
primeiro = false;
|
|
362
|
+
|
|
363
|
+
var comVideo = temClipe(no);
|
|
364
|
+
Exportar.retrato(no, L, A, folhaCss, false, comVideo ? semFundo : null)
|
|
365
|
+
.then(function (img) {
|
|
366
|
+
if (cancelado) return pronto();
|
|
367
|
+
if (comVideo) {
|
|
368
|
+
// chão do tema → gravação do usuário → cena sem fundo
|
|
369
|
+
ctx2d.fillStyle = corDeChao(no);
|
|
370
|
+
ctx2d.fillRect(0, 0, L, A);
|
|
371
|
+
desenharClipes(ctx2d, no, L, A);
|
|
372
|
+
}
|
|
373
|
+
ctx2d.drawImage(img, 0, 0, L, A);
|
|
374
|
+
volta();
|
|
375
|
+
}).catch(function () {
|
|
376
|
+
// Um quadro que não rasterizou não derruba o vídeo: segue, e o
|
|
377
|
+
// quadro anterior continua na tela.
|
|
378
|
+
setTimeout(volta, 1000 / FPS);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
volta();
|
|
382
|
+
});
|
|
383
|
+
}).then(function () {
|
|
384
|
+
return new Promise(function (ok) {
|
|
385
|
+
// Um instante a mais antes de parar: o codificador precisa fechar o
|
|
386
|
+
// último trecho, e parar em cima do fim corta o final do vídeo.
|
|
387
|
+
setTimeout(function () {
|
|
388
|
+
rec.onstop = function () { ok(); };
|
|
389
|
+
if (rec.state !== "inactive") rec.stop(); else ok();
|
|
390
|
+
}, 400);
|
|
391
|
+
});
|
|
392
|
+
}).then(function () {
|
|
393
|
+
limpar();
|
|
394
|
+
var blob = new Blob(pedacos, { type: tipo.split(";")[0] });
|
|
395
|
+
if (!blob.size) throw new Error("o navegador não produziu vídeo");
|
|
396
|
+
andar(1);
|
|
397
|
+
return {
|
|
398
|
+
ok: true, blob: blob, bytes: blob.size,
|
|
399
|
+
formato: extensaoDe(tipo), tipo: tipo, mp4: ehMp4(tipo),
|
|
400
|
+
segundos: totalMs / 1000,
|
|
401
|
+
};
|
|
402
|
+
}).catch(function (e) {
|
|
403
|
+
limpar();
|
|
404
|
+
throw e;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
promessa.cancelar = function () { cancelado = true; };
|
|
408
|
+
return promessa;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Gera e entrega o arquivo. É isto que o botão chama. */
|
|
412
|
+
function baixar(arq, op) {
|
|
413
|
+
op = op || {};
|
|
414
|
+
return gerar(arq, op).then(function (r) {
|
|
415
|
+
var url = URL.createObjectURL(r.blob);
|
|
416
|
+
var a = document.createElement("a");
|
|
417
|
+
a.href = url;
|
|
418
|
+
a.download = (op.nome || arq.titulo || "video")
|
|
419
|
+
.replace(/[^\w\-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() + r.formato;
|
|
420
|
+
document.body.appendChild(a);
|
|
421
|
+
a.click();
|
|
422
|
+
a.remove();
|
|
423
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 8000);
|
|
424
|
+
return r;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
disponivel: disponivel, formato: formato, gerar: gerar, baixar: baixar,
|
|
430
|
+
FORMATOS: FORMATOS, _aindaAnima: aindaAnima,
|
|
431
|
+
};
|
|
432
|
+
})();
|
|
433
|
+
|
|
434
|
+
if (typeof module !== "undefined" && module.exports) module.exports = VideoExport;
|
package/studio/web/render.css
CHANGED
|
@@ -214,6 +214,33 @@
|
|
|
214
214
|
}
|
|
215
215
|
.cena .fluxo > .el { flex: 0 0 auto; max-width: 100%; }
|
|
216
216
|
|
|
217
|
+
/* --------------------------------------------------- legenda de fala ----
|
|
218
|
+
Legenda de vídeo é lida em movimento e, muitas vezes, sem som — e por cima
|
|
219
|
+
de uma imagem que muda a cada quadro. Cor de tema não resolve: sobre um céu
|
|
220
|
+
claro, texto claro some; sobre uma sombra, texto escuro some.
|
|
221
|
+
|
|
222
|
+
Então ela não confia no fundo. Branco puro, peso alto, chapa escura atrás e
|
|
223
|
+
um contorno de sombra que segura a leitura mesmo quando a chapa cai em cima
|
|
224
|
+
de algo claro. É a mesma decisão de toda legenda de streaming. */
|
|
225
|
+
.cena .faixa-fala {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 10px;
|
|
230
|
+
text-align: center;
|
|
231
|
+
pointer-events: none;
|
|
232
|
+
}
|
|
233
|
+
.cena .el.txt.fala {
|
|
234
|
+
display: inline-block;
|
|
235
|
+
max-width: 86%;
|
|
236
|
+
color: #fff;
|
|
237
|
+
background: rgba(6, 8, 12, .72);
|
|
238
|
+
padding: .32em .7em;
|
|
239
|
+
border-radius: .28em;
|
|
240
|
+
text-shadow: 0 2px 10px rgba(0, 0, 0, .85), 0 0 2px rgba(0, 0, 0, .9);
|
|
241
|
+
text-wrap: balance;
|
|
242
|
+
}
|
|
243
|
+
|
|
217
244
|
.cena .fluxo[data-layout="capa"],
|
|
218
245
|
.cena .fluxo[data-layout="centro"],
|
|
219
246
|
.cena .fluxo[data-layout="citacao"] { align-items: center; text-align: center; }
|
package/studio/web/tool.html
CHANGED
package/studio/web/view.html
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
<script src="/js/sfx.js"></script>
|
|
21
21
|
<script src="/js/voz-neural.js"></script>
|
|
22
22
|
<script src="/js/narracao.js"></script>
|
|
23
|
+
<script src="/js/exportar.js"></script>
|
|
24
|
+
<script src="/js/video.js"></script>
|
|
23
25
|
<script src="/js/player.js"></script>
|
|
24
26
|
</body>
|
|
25
27
|
</html>
|