primocode 8.42.1 → 9.0.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 +41 -37
- package/bin/primocode.js +599 -327
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/studio/web/js/sfx.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
sfx.js — efeitos sonoros do Corte, sintetizados na hora.
|
|
3
|
-
|
|
4
|
-
Nenhum arquivo, nenhuma licença: whoosh é ruído filtrado com varredura,
|
|
5
|
-
impacto é um seno grave despencando, click é um blip curto. Tudo entra no
|
|
6
|
-
mesmo grafo da trilha (via `destino`), então a gravação MP4 captura os
|
|
7
|
-
efeitos junto — sem depender de "compartilhar áudio da aba".
|
|
8
|
-
|
|
9
|
-
Uso:
|
|
10
|
-
var sfx = criarSfx(ctx, destino); // destino: nó do grafo (compressor)
|
|
11
|
-
sfx.whoosh(); // transição de cena
|
|
12
|
-
sfx.impacto(); // ênfase (flash, número grande)
|
|
13
|
-
sfx.click(); // entrada de elemento, feedback discreto
|
|
14
|
-
|
|
15
|
-
O volume de cada efeito já vem calibrado para sentar SOB a narração.
|
|
16
|
-
========================================================================== */
|
|
17
|
-
|
|
18
|
-
function criarSfx(ctx, destino) {
|
|
19
|
-
"use strict";
|
|
20
|
-
if (!ctx || !destino) return null;
|
|
21
|
-
|
|
22
|
-
var saida = ctx.createGain();
|
|
23
|
-
saida.gain.value = 1;
|
|
24
|
-
saida.connect(destino);
|
|
25
|
-
|
|
26
|
-
var ruido = null;
|
|
27
|
-
function bufferRuido() {
|
|
28
|
-
if (ruido) return ruido;
|
|
29
|
-
ruido = ctx.createBuffer(1, ctx.sampleRate * 1.2, ctx.sampleRate);
|
|
30
|
-
var d = ruido.getChannelData(0);
|
|
31
|
-
for (var i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
|
32
|
-
return ruido;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/* Varredura de ruído — o "passar de página" do vídeo. `variacao` muda o
|
|
36
|
-
miolo da varredura para os whooshes não saírem todos idênticos. */
|
|
37
|
-
function whoosh(variacao) {
|
|
38
|
-
var agora = ctx.currentTime;
|
|
39
|
-
var v = (variacao == null ? Math.random() : variacao) % 1;
|
|
40
|
-
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
41
|
-
s.buffer = bufferRuido();
|
|
42
|
-
f.type = "bandpass";
|
|
43
|
-
f.Q.value = 1.1;
|
|
44
|
-
f.frequency.setValueAtTime(300 + v * 300, agora);
|
|
45
|
-
f.frequency.exponentialRampToValueAtTime(2200 + v * 1400, agora + 0.16);
|
|
46
|
-
f.frequency.exponentialRampToValueAtTime(500, agora + 0.42);
|
|
47
|
-
g.gain.setValueAtTime(0.0001, agora);
|
|
48
|
-
g.gain.exponentialRampToValueAtTime(0.11, agora + 0.10);
|
|
49
|
-
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.45);
|
|
50
|
-
s.connect(f); f.connect(g); g.connect(saida);
|
|
51
|
-
s.start(agora); s.stop(agora + 0.5);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/* Seno grave despencando + um sopro de ruído no ataque. */
|
|
55
|
-
function impacto() {
|
|
56
|
-
var agora = ctx.currentTime;
|
|
57
|
-
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
58
|
-
o.type = "sine";
|
|
59
|
-
o.frequency.setValueAtTime(120, agora);
|
|
60
|
-
o.frequency.exponentialRampToValueAtTime(36, agora + 0.5);
|
|
61
|
-
g.gain.setValueAtTime(0.30, agora);
|
|
62
|
-
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.9);
|
|
63
|
-
o.connect(g); g.connect(saida);
|
|
64
|
-
o.start(agora); o.stop(agora + 1.0);
|
|
65
|
-
|
|
66
|
-
var s = ctx.createBufferSource(), gr = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
67
|
-
s.buffer = bufferRuido();
|
|
68
|
-
f.type = "lowpass";
|
|
69
|
-
f.frequency.value = 900;
|
|
70
|
-
gr.gain.setValueAtTime(0.09, agora);
|
|
71
|
-
gr.gain.exponentialRampToValueAtTime(0.0001, agora + 0.22);
|
|
72
|
-
s.connect(f); f.connect(gr); gr.connect(saida);
|
|
73
|
-
s.start(agora); s.stop(agora + 0.3);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* Blip de interface: curto, agudo, discreto. */
|
|
77
|
-
function click() {
|
|
78
|
-
var agora = ctx.currentTime;
|
|
79
|
-
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
80
|
-
o.type = "sine";
|
|
81
|
-
o.frequency.setValueAtTime(2100, agora);
|
|
82
|
-
g.gain.setValueAtTime(0.06, agora);
|
|
83
|
-
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.05);
|
|
84
|
-
o.connect(g); g.connect(saida);
|
|
85
|
-
o.start(agora); o.stop(agora + 0.07);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/* ── A biblioteca cresceu porque três sons não fazem motion ───────────
|
|
89
|
-
Pedido do dono: "efeitos sonoros… bem motion mesmo". Whoosh, impacto e
|
|
90
|
-
click cobrem a transição; o que entra num elemento — um número que
|
|
91
|
-
aparece, um ícone que pousa, uma barra que sobe — pedia mais. Todos
|
|
92
|
-
sintetizados, sem arquivo e sem licença, calibrados para ficar SOB a
|
|
93
|
-
narração. */
|
|
94
|
-
|
|
95
|
-
/* Bolha estourando: um seno que sobe rápido e some. Entrada de item. */
|
|
96
|
-
function pop() {
|
|
97
|
-
var agora = ctx.currentTime;
|
|
98
|
-
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
99
|
-
o.type = "sine";
|
|
100
|
-
o.frequency.setValueAtTime(380, agora);
|
|
101
|
-
o.frequency.exponentialRampToValueAtTime(900, agora + 0.06);
|
|
102
|
-
g.gain.setValueAtTime(0.0001, agora);
|
|
103
|
-
g.gain.exponentialRampToValueAtTime(0.16, agora + 0.012);
|
|
104
|
-
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.14);
|
|
105
|
-
o.connect(g); g.connect(saida);
|
|
106
|
-
o.start(agora); o.stop(agora + 0.16);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* Sino de notificação: dois parciais (fundamental + quinta acima), cauda
|
|
110
|
-
longa. Confirmação, número final, "pronto". */
|
|
111
|
-
function ding() {
|
|
112
|
-
var agora = ctx.currentTime;
|
|
113
|
-
[[1320, 0.11], [1980, 0.05], [2640, 0.025]].forEach(function (p, i) {
|
|
114
|
-
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
115
|
-
o.type = "sine";
|
|
116
|
-
o.frequency.value = p[0];
|
|
117
|
-
g.gain.setValueAtTime(0.0001, agora);
|
|
118
|
-
g.gain.exponentialRampToValueAtTime(p[1], agora + 0.008);
|
|
119
|
-
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.9 - i * 0.15);
|
|
120
|
-
o.connect(g); g.connect(saida);
|
|
121
|
-
o.start(agora); o.stop(agora + 1.0);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* Subida (riser): ruído filtrado que sobe por um segundo e meio e para
|
|
126
|
-
seco — a tensão antes do número grande ou da assinatura. */
|
|
127
|
-
function subida() {
|
|
128
|
-
var agora = ctx.currentTime;
|
|
129
|
-
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
130
|
-
s.buffer = bufferRuido();
|
|
131
|
-
s.loop = true;
|
|
132
|
-
f.type = "bandpass"; f.Q.value = 2.2;
|
|
133
|
-
f.frequency.setValueAtTime(180, agora);
|
|
134
|
-
f.frequency.exponentialRampToValueAtTime(3800, agora + 1.5);
|
|
135
|
-
g.gain.setValueAtTime(0.0001, agora);
|
|
136
|
-
g.gain.exponentialRampToValueAtTime(0.13, agora + 1.4);
|
|
137
|
-
g.gain.setValueAtTime(0.0001, agora + 1.52);
|
|
138
|
-
s.connect(f); f.connect(g); g.connect(saida);
|
|
139
|
-
s.start(agora); s.stop(agora + 1.6);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* Tique de relógio/teclado: madeira seca. Passo de lista, contagem. */
|
|
143
|
-
function tique() {
|
|
144
|
-
var agora = ctx.currentTime;
|
|
145
|
-
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
146
|
-
s.buffer = bufferRuido();
|
|
147
|
-
f.type = "highpass"; f.frequency.value = 2600;
|
|
148
|
-
g.gain.setValueAtTime(0.14, agora);
|
|
149
|
-
g.gain.exponentialRampToValueAtTime(0.0001, agora + 0.04);
|
|
150
|
-
s.connect(f); f.connect(g); g.connect(saida);
|
|
151
|
-
s.start(agora); s.stop(agora + 0.05);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/* Brilho: arpejo rápido e agudo, o "sparkle" de um destaque ou de uma
|
|
155
|
-
estrela acendendo. */
|
|
156
|
-
function brilho() {
|
|
157
|
-
var agora = ctx.currentTime;
|
|
158
|
-
[1568, 2093, 2637, 3136].forEach(function (hz, i) {
|
|
159
|
-
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
160
|
-
o.type = "triangle";
|
|
161
|
-
o.frequency.value = hz;
|
|
162
|
-
var t = agora + i * 0.045;
|
|
163
|
-
g.gain.setValueAtTime(0.0001, t);
|
|
164
|
-
g.gain.exponentialRampToValueAtTime(0.06, t + 0.01);
|
|
165
|
-
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);
|
|
166
|
-
o.connect(g); g.connect(saida);
|
|
167
|
-
o.start(t); o.stop(t + 0.4);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/* Um ARQUIVO de som (o efeito baixado da internet pelo agente, ou o do
|
|
172
|
-
usuário), tocado dentro do MESMO grafo — é o que faz ele entrar no MP4
|
|
173
|
-
exportado junto da trilha. Decodificado uma vez por URL. */
|
|
174
|
-
var cacheArquivos = {};
|
|
175
|
-
function arquivo(url, volume) {
|
|
176
|
-
if (!url) return;
|
|
177
|
-
var pronto = cacheArquivos[url] || (cacheArquivos[url] = fetch(url)
|
|
178
|
-
.then(function (r) { return r.arrayBuffer(); })
|
|
179
|
-
.then(function (b) { return ctx.decodeAudioData(b); })
|
|
180
|
-
.catch(function () { return null; }));
|
|
181
|
-
pronto.then(function (buffer) {
|
|
182
|
-
if (!buffer) return;
|
|
183
|
-
var s = ctx.createBufferSource(), g = ctx.createGain();
|
|
184
|
-
s.buffer = buffer;
|
|
185
|
-
g.gain.value = volume == null ? 0.7 : Math.max(0, Math.min(1, volume));
|
|
186
|
-
s.connect(g); g.connect(saida);
|
|
187
|
-
s.start(ctx.currentTime);
|
|
188
|
-
});
|
|
189
|
-
return pronto;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/* O efeito certo para a ENTRADA de uma cena, pela transição dela.
|
|
193
|
-
flash e zoom pedem peso; corte seco não pede nada. */
|
|
194
|
-
function transicao(tipo, indice) {
|
|
195
|
-
if (tipo === "corte") return;
|
|
196
|
-
if (tipo === "flash" || tipo === "zoom") return impacto();
|
|
197
|
-
whoosh(((indice || 0) * 0.37) % 1);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/* Toca pelo NOME ou por {arquivo, volume}. É o que o player chama para o
|
|
201
|
-
`som` de um elemento ou de uma cena. Nome desconhecido não toca nada —
|
|
202
|
-
e não quebra. */
|
|
203
|
-
function tocar(som) {
|
|
204
|
-
if (!som) return;
|
|
205
|
-
if (typeof som === "string") { if (typeof api[som] === "function" && som !== "tocar") api[som](); return; }
|
|
206
|
-
if (som.arquivo) return arquivo(som.arquivo, som.volume);
|
|
207
|
-
if (som.nome && typeof api[som.nome] === "function") api[som.nome]();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
var api = {
|
|
211
|
-
whoosh: whoosh,
|
|
212
|
-
impacto: impacto,
|
|
213
|
-
click: click,
|
|
214
|
-
pop: pop,
|
|
215
|
-
ding: ding,
|
|
216
|
-
subida: subida,
|
|
217
|
-
tique: tique,
|
|
218
|
-
brilho: brilho,
|
|
219
|
-
arquivo: arquivo,
|
|
220
|
-
tocar: tocar,
|
|
221
|
-
transicao: transicao,
|
|
222
|
-
volume: function (v) { saida.gain.value = v; },
|
|
223
|
-
};
|
|
224
|
-
return api;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/* Os nomes que o documento pode usar em `som`/`sfx` — lidos pela spec e pelo
|
|
228
|
-
servidor, para recusar nome inventado com a lista certa. */
|
|
229
|
-
var SONS = ["whoosh", "impacto", "click", "pop", "ding", "subida", "tique", "brilho"];
|
|
230
|
-
|
|
231
|
-
if (typeof module !== "undefined" && module.exports) {
|
|
232
|
-
module.exports = { criarSfx: criarSfx, SONS: SONS };
|
|
233
|
-
}
|
package/studio/web/js/video.js
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
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
|
-
// Clipe de SÓ ÁUDIO: entra pelo som e não pelo quadro. Ele tem 1px na
|
|
196
|
-
// tela, então sem esta linha o export desenharia uma tira de um pixel
|
|
197
|
-
// esticada por cima da cena montada — visível, e sem causa aparente.
|
|
198
|
-
if (v.dataset && v.dataset.som === "1") return;
|
|
199
|
-
var r = v.getBoundingClientRect();
|
|
200
|
-
var dx = (r.left - caixaCena.left) * escX;
|
|
201
|
-
var dy = (r.top - caixaCena.top) * escY;
|
|
202
|
-
var dw = r.width * escX;
|
|
203
|
-
var dh = r.height * escY;
|
|
204
|
-
if (dw <= 0 || dh <= 0) return;
|
|
205
|
-
|
|
206
|
-
// "cobrir" recorta o vídeo para encher a caixa sem deformar — o mesmo
|
|
207
|
-
// que o object-fit faz na tela. Sem isto o export esticaria a imagem
|
|
208
|
-
// e ficaria diferente do que a pessoa aprovou no player.
|
|
209
|
-
var conter = v.style.objectFit === "contain";
|
|
210
|
-
var escala = conter
|
|
211
|
-
? Math.min(dw / v.videoWidth, dh / v.videoHeight)
|
|
212
|
-
: Math.max(dw / v.videoWidth, dh / v.videoHeight);
|
|
213
|
-
var lw = v.videoWidth * escala;
|
|
214
|
-
var lh = v.videoHeight * escala;
|
|
215
|
-
var lx = dx + (dw - lw) / 2;
|
|
216
|
-
var ly = dy + (dh - lh) / 2;
|
|
217
|
-
|
|
218
|
-
ctx2d.save();
|
|
219
|
-
ctx2d.beginPath();
|
|
220
|
-
ctx2d.rect(dx, dy, dw, dh);
|
|
221
|
-
ctx2d.clip();
|
|
222
|
-
// "cinza", "sepia", "contraste": na tela é filter do CSS, e drawImage
|
|
223
|
-
// ignora o CSS do elemento. O canvas tem o mesmo filtro, com a mesma
|
|
224
|
-
// gramática — então é só repassar, e o arquivo sai como o player.
|
|
225
|
-
var f = window.getComputedStyle(v).filter;
|
|
226
|
-
if (f && f !== "none") { try { ctx2d.filter = f; } catch (e) {} }
|
|
227
|
-
try { ctx2d.drawImage(v, lx, ly, lw, lh); } catch (e) { /* quadro ainda não veio */ }
|
|
228
|
-
ctx2d.restore();
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/* O chão da cena, para o clipe que não ocupa o quadro inteiro. Sem isto,
|
|
233
|
-
a volta do vídeo sairia preta em vez da cor do tema. */
|
|
234
|
-
function corDeChao(no) {
|
|
235
|
-
var c = window.getComputedStyle(no).backgroundColor;
|
|
236
|
-
return (c && c !== "rgba(0, 0, 0, 0)" && c !== "transparent") ? c : "#0b0e14";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/* Prepara o CLONE da cena que tem gravação — só o clone, a tela do usuário
|
|
240
|
-
não muda. Duas coisas precisam sair, e as duas escondiam o vídeo:
|
|
241
|
-
|
|
242
|
-
· o fundo da cena, opaco por cima da gravação. `data-fundo` sai junto
|
|
243
|
-
porque os padrões (aurora, raios, ondas) são pseudo-elementos: zerar
|
|
244
|
-
`background` deixaria a camada de cima intacta;
|
|
245
|
-
|
|
246
|
-
· o próprio <video>. foreignObject não decodifica vídeo, mas também não
|
|
247
|
-
deixa o espaço vazio: pinta a caixa do elemento no cinza padrão
|
|
248
|
-
(#333). O quadro saía com a gravação desenhada embaixo e um retângulo
|
|
249
|
-
cinza chapado exatamente em cima dela. */
|
|
250
|
-
function semFundo(clone) {
|
|
251
|
-
clone.removeAttribute("data-fundo");
|
|
252
|
-
clone.style.background = "transparent";
|
|
253
|
-
clone.style.backgroundImage = "none";
|
|
254
|
-
Array.prototype.forEach.call(clone.querySelectorAll("video"), function (v) {
|
|
255
|
-
v.style.visibility = "hidden";
|
|
256
|
-
v.style.background = "transparent";
|
|
257
|
-
// A caixa que segura o vídeo também pinta (é o cinza de espera, útil na
|
|
258
|
-
// tela enquanto o arquivo carrega). No quadro exportado ela é só mais
|
|
259
|
-
// uma tampa em cima da gravação — e foi a última a ser encontrada,
|
|
260
|
-
// porque some junto com o resto do fundo e parece o fundo.
|
|
261
|
-
for (var pai = v.parentNode; pai && pai !== clone; pai = pai.parentNode) {
|
|
262
|
-
pai.style.background = "transparent";
|
|
263
|
-
pai.style.backgroundImage = "none";
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/* Cena com vídeo nunca fica "parada": o clipe corre mesmo sem animação
|
|
269
|
-
CSS, e pular o redesenho congelaria a imagem do usuário. */
|
|
270
|
-
function temClipe(no) {
|
|
271
|
-
return !!(no && no.querySelector && no.querySelector("video"));
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/* --------------------------------------------------------------- gerar -- */
|
|
275
|
-
|
|
276
|
-
/* Monta o arquivo e devolve { blob, bytes, formato, mp4, segundos }.
|
|
277
|
-
*
|
|
278
|
-
* `op.fonte` — quem diz o que fotografar (o player, normalmente).
|
|
279
|
-
* `op.fluxoAudio` — MediaStream já mixado. Sem ele o vídeo sai mudo.
|
|
280
|
-
* `op.aoAndar(fracao)` — progresso, para a interface mostrar.
|
|
281
|
-
*/
|
|
282
|
-
function gerar(arq, op) {
|
|
283
|
-
op = op || {};
|
|
284
|
-
var doc = (arq && arq.doc) || {};
|
|
285
|
-
var cenas = doc.cenas || [];
|
|
286
|
-
if (!cenas.length) return Promise.reject(new Error("este arquivo não tem cenas"));
|
|
287
|
-
|
|
288
|
-
var tipo = formato();
|
|
289
|
-
if (!tipo) return Promise.reject(new Error("este navegador não sabe gerar vídeo"));
|
|
290
|
-
|
|
291
|
-
var L = doc.largura || 1920;
|
|
292
|
-
var A = doc.altura || 1080;
|
|
293
|
-
var FPS = op.fps || 30;
|
|
294
|
-
var andar = op.aoAndar || function () {};
|
|
295
|
-
|
|
296
|
-
var tela = document.createElement("canvas");
|
|
297
|
-
tela.width = L; tela.height = A;
|
|
298
|
-
var ctx2d = tela.getContext("2d", { alpha: false });
|
|
299
|
-
ctx2d.fillStyle = "#000";
|
|
300
|
-
ctx2d.fillRect(0, 0, L, A);
|
|
301
|
-
|
|
302
|
-
// Palco próprio só quando o exportador tem de montar as cenas sozinho.
|
|
303
|
-
// Fica DENTRO do documento (fora dele não há layout nem animação) mas
|
|
304
|
-
// fora da vista — e não com display:none, que também mataria o layout.
|
|
305
|
-
var palco = null;
|
|
306
|
-
var fonte = op.fonte;
|
|
307
|
-
if (!fonte) {
|
|
308
|
-
palco = document.createElement("div");
|
|
309
|
-
palco.setAttribute("aria-hidden", "true");
|
|
310
|
-
palco.style.cssText = "position:fixed;left:-99999px;top:0;pointer-events:none;"
|
|
311
|
-
+ "width:" + L + "px;height:" + A + "px;overflow:hidden";
|
|
312
|
-
document.body.appendChild(palco);
|
|
313
|
-
fonte = fonteDoDocumento(doc, palco);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
var fluxo = tela.captureStream(FPS);
|
|
317
|
-
if (op.fluxoAudio) {
|
|
318
|
-
op.fluxoAudio.getAudioTracks().forEach(function (f) { fluxo.addTrack(f); });
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
var pedacos = [];
|
|
322
|
-
var rec = new MediaRecorder(fluxo, {
|
|
323
|
-
mimeType: tipo,
|
|
324
|
-
videoBitsPerSecond: op.bitrate || (L * A > 1000000 ? 8000000 : 4000000),
|
|
325
|
-
});
|
|
326
|
-
rec.ondataavailable = function (ev) { if (ev.data && ev.data.size) pedacos.push(ev.data); };
|
|
327
|
-
|
|
328
|
-
var totalMs = (op.duracao || fonte.duracao || 0) * 1000;
|
|
329
|
-
var cancelado = false;
|
|
330
|
-
var folhaCss = null;
|
|
331
|
-
|
|
332
|
-
function limpar() {
|
|
333
|
-
try { if (fonte.parar) fonte.parar(); } catch (e) {}
|
|
334
|
-
if (palco) { try { palco.remove(); } catch (e) {} }
|
|
335
|
-
fluxo.getVideoTracks().forEach(function (t) { try { t.stop(); } catch (e) {} });
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
var promessa = Exportar.css().then(function (folha) {
|
|
339
|
-
folhaCss = folha;
|
|
340
|
-
// A fonte pode ter o que carregar antes do primeiro quadro — a gravação
|
|
341
|
-
// do usuário, que sem quadro decodificado desenharia em branco.
|
|
342
|
-
return fonte.preparar ? fonte.preparar() : null;
|
|
343
|
-
}).then(function () {
|
|
344
|
-
rec.start(1000);
|
|
345
|
-
if (fonte.comecar) fonte.comecar();
|
|
346
|
-
|
|
347
|
-
return new Promise(function (pronto) {
|
|
348
|
-
var t0 = performance.now();
|
|
349
|
-
var primeiro = true;
|
|
350
|
-
/* A camada de cima de uma cena com clipe (título, legenda, marca),
|
|
351
|
-
já rasterizada. Enquanto nada nela anima, só o quadro do vídeo
|
|
352
|
-
muda — e rasterizar o SVG inteiro trinta vezes por segundo era o
|
|
353
|
-
que engasgava a máquina: o decodificador do <video> ficava sem
|
|
354
|
-
CPU, o clipe atrasava, o player o puxava de volta e a busca não
|
|
355
|
-
terminava nunca (medido: seeking=true e readyState=1 do 0,7s ao
|
|
356
|
-
fim, arquivo com o chão da cena no lugar da gravação). Com a
|
|
357
|
-
camada guardada, cada quadro é dois drawImage. */
|
|
358
|
-
var camada = null;
|
|
359
|
-
|
|
360
|
-
function pintarComClipe(no, img) {
|
|
361
|
-
// chão do tema → gravação do usuário → cena sem fundo
|
|
362
|
-
ctx2d.fillStyle = corDeChao(no);
|
|
363
|
-
ctx2d.fillRect(0, 0, L, A);
|
|
364
|
-
desenharClipes(ctx2d, no, L, A);
|
|
365
|
-
ctx2d.drawImage(img, 0, 0, L, A);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function volta() {
|
|
369
|
-
if (cancelado) return pronto();
|
|
370
|
-
var t = performance.now() - t0;
|
|
371
|
-
if (t >= totalMs) return pronto();
|
|
372
|
-
andar(Math.min(0.995, t / totalMs));
|
|
373
|
-
|
|
374
|
-
var no = fonte.no();
|
|
375
|
-
if (!no) return setTimeout(volta, 1000 / FPS);
|
|
376
|
-
|
|
377
|
-
// Quadro idêntico ao anterior não precisa ser refeito: o
|
|
378
|
-
// captureStream repete o último sozinho, na taxa cheia.
|
|
379
|
-
if (!primeiro && !aindaAnima(no) && !temClipe(no)) {
|
|
380
|
-
return setTimeout(volta, 1000 / FPS);
|
|
381
|
-
}
|
|
382
|
-
primeiro = false;
|
|
383
|
-
|
|
384
|
-
var comVideo = temClipe(no);
|
|
385
|
-
if (comVideo && camada && camada.no === no && !aindaAnima(no)) {
|
|
386
|
-
pintarComClipe(no, camada.img);
|
|
387
|
-
return setTimeout(volta, 1000 / FPS);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
Exportar.retrato(no, L, A, folhaCss, false, comVideo ? semFundo : null)
|
|
391
|
-
.then(function (img) {
|
|
392
|
-
if (cancelado) return pronto();
|
|
393
|
-
if (comVideo) {
|
|
394
|
-
camada = aindaAnima(no) ? null : { no: no, img: img };
|
|
395
|
-
pintarComClipe(no, img);
|
|
396
|
-
} else {
|
|
397
|
-
ctx2d.drawImage(img, 0, 0, L, A);
|
|
398
|
-
}
|
|
399
|
-
volta();
|
|
400
|
-
}).catch(function () {
|
|
401
|
-
// Um quadro que não rasterizou não derruba o vídeo: segue, e o
|
|
402
|
-
// quadro anterior continua na tela.
|
|
403
|
-
setTimeout(volta, 1000 / FPS);
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
volta();
|
|
407
|
-
});
|
|
408
|
-
}).then(function () {
|
|
409
|
-
return new Promise(function (ok) {
|
|
410
|
-
// Um instante a mais antes de parar: o codificador precisa fechar o
|
|
411
|
-
// último trecho, e parar em cima do fim corta o final do vídeo.
|
|
412
|
-
setTimeout(function () {
|
|
413
|
-
rec.onstop = function () { ok(); };
|
|
414
|
-
if (rec.state !== "inactive") rec.stop(); else ok();
|
|
415
|
-
}, 400);
|
|
416
|
-
});
|
|
417
|
-
}).then(function () {
|
|
418
|
-
limpar();
|
|
419
|
-
var blob = new Blob(pedacos, { type: tipo.split(";")[0] });
|
|
420
|
-
if (!blob.size) throw new Error("o navegador não produziu vídeo");
|
|
421
|
-
andar(1);
|
|
422
|
-
return {
|
|
423
|
-
ok: true, blob: blob, bytes: blob.size,
|
|
424
|
-
formato: extensaoDe(tipo), tipo: tipo, mp4: ehMp4(tipo),
|
|
425
|
-
segundos: totalMs / 1000,
|
|
426
|
-
};
|
|
427
|
-
}).catch(function (e) {
|
|
428
|
-
limpar();
|
|
429
|
-
throw e;
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
promessa.cancelar = function () { cancelado = true; };
|
|
433
|
-
return promessa;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/* Entrega ao usuário o arquivo que `gerar` montou. Separado de `baixar`
|
|
437
|
-
porque a exportação ESCONDIDA (studio_exportar) gera pelo mesmo caminho e
|
|
438
|
-
não baixa: manda o blob de volta ao estúdio. Um só `gerar` para os dois. */
|
|
439
|
-
function salvar(r, nome) {
|
|
440
|
-
var url = URL.createObjectURL(r.blob);
|
|
441
|
-
var a = document.createElement("a");
|
|
442
|
-
a.href = url;
|
|
443
|
-
a.download = (nome || "video")
|
|
444
|
-
.replace(/[^\w\-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() + r.formato;
|
|
445
|
-
document.body.appendChild(a);
|
|
446
|
-
a.click();
|
|
447
|
-
a.remove();
|
|
448
|
-
setTimeout(function () { URL.revokeObjectURL(url); }, 8000);
|
|
449
|
-
return r;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/* Gera e entrega o arquivo. É isto que o botão chama. */
|
|
453
|
-
function baixar(arq, op) {
|
|
454
|
-
op = op || {};
|
|
455
|
-
return gerar(arq, op).then(function (r) { return salvar(r, op.nome || arq.titulo); });
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
disponivel: disponivel, formato: formato, gerar: gerar, baixar: baixar, salvar: salvar,
|
|
460
|
-
FORMATOS: FORMATOS, _aindaAnima: aindaAnima,
|
|
461
|
-
};
|
|
462
|
-
})();
|
|
463
|
-
|
|
464
|
-
if (typeof module !== "undefined" && module.exports) module.exports = VideoExport;
|