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/player.js
DELETED
|
@@ -1,903 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
player.js — apresenta, reproduz com trilha, grava e imprime.
|
|
3
|
-
Serve tanto para /v/<id> quanto para o HTML autônomo exportado.
|
|
4
|
-
========================================================================== */
|
|
5
|
-
|
|
6
|
-
(function () {
|
|
7
|
-
"use strict";
|
|
8
|
-
|
|
9
|
-
var palco = document.getElementById("palco");
|
|
10
|
-
if (!palco) return;
|
|
11
|
-
|
|
12
|
-
var arq = null, doc = {}, cenas = [], atual = 0;
|
|
13
|
-
var tocando = false, quadro = null, t0 = 0, decorrido = 0;
|
|
14
|
-
var trilha = null, gravador = null, pedacos = [];
|
|
15
|
-
/* Resolve quando o arquivo está MONTADO no palco. É por ela que a exportação
|
|
16
|
-
escondida (studio_exportar) sabe quando pode começar — antes disto não há
|
|
17
|
-
cena para fotografar, e um export que começa cedo sai preto. */
|
|
18
|
-
var carregadoOk = null;
|
|
19
|
-
var carregado = new Promise(function (ok) { carregadoOk = ok; });
|
|
20
|
-
var ehVideo = false, transporte = null;
|
|
21
|
-
|
|
22
|
-
/* ------------------------------------------------------------- avisos -- */
|
|
23
|
-
|
|
24
|
-
function aviso(texto, ms) {
|
|
25
|
-
var a = document.querySelector(".aviso");
|
|
26
|
-
if (!a) { a = document.createElement("div"); a.className = "aviso"; document.body.appendChild(a); }
|
|
27
|
-
a.textContent = texto;
|
|
28
|
-
a.classList.add("on");
|
|
29
|
-
clearTimeout(a._t);
|
|
30
|
-
a._t = setTimeout(function () { a.classList.remove("on"); }, ms || 1800);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function mmss(s) {
|
|
34
|
-
s = Math.max(0, Math.floor(s || 0));
|
|
35
|
-
return String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* Quanto dura o maior trecho de vídeo do usuário nesta cena.
|
|
39
|
-
Cortar as pausas de uma gravação vira uma fila de cenas, cada uma com um
|
|
40
|
-
pedaço do MESMO arquivo. Se cada uma dessas cenas durasse os 4 segundos
|
|
41
|
-
do padrão, o corte não seria corte nenhum: o trecho de 1,2s ficaria
|
|
42
|
-
congelado 2,8s e o de 9s sairia pela metade. O trecho é que manda. */
|
|
43
|
-
function duracaoClipe(c) {
|
|
44
|
-
var maior = 0;
|
|
45
|
-
(c.elementos || []).forEach(function (e) {
|
|
46
|
-
if (!e || e.tipo !== "clipe") return;
|
|
47
|
-
var de = Number(e.de) || 0, ate = Number(e.ate);
|
|
48
|
-
if (isFinite(ate) && ate > de) maior = Math.max(maior, ate - de);
|
|
49
|
-
});
|
|
50
|
-
return maior;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function duracaoDe(i) {
|
|
54
|
-
/* "duracao": "3s" (texto) fazia acc virar concatenação e o relógio dava
|
|
55
|
-
vídeo de 00:00. Número são ou o padrão — nunca corrente de texto. */
|
|
56
|
-
var c = cenas[i] || {};
|
|
57
|
-
var d = Number(c.duracao);
|
|
58
|
-
/* O padrão de 4s vale para cena de texto. Onde há gravação, o padrão é o
|
|
59
|
-
trecho — e o teto sobe junto, porque 60s é guarda contra erro de
|
|
60
|
-
digitação e um pedaço de vídeo medido não é erro de digitação. */
|
|
61
|
-
var clipe = duracaoClipe(c);
|
|
62
|
-
if (!(isFinite(d) && d > 0)) d = clipe > 0 ? Math.min(600, clipe) : 4;
|
|
63
|
-
else d = Math.min(clipe > 0 ? 600 : 60, d);
|
|
64
|
-
/* A narração pré-renderizada tem duração conhecida (o servidor mediu o
|
|
65
|
-
WAV e gravou `narracaoDur` na cena). A cena dura o que for MAIOR:
|
|
66
|
-
o que o autor pediu ou a fala inteira + um respiro. É isto que torna
|
|
67
|
-
a linha do tempo determinística — o "cena espera a voz" do laço vira
|
|
68
|
-
só a rede de segurança para narração sintetizada na hora. */
|
|
69
|
-
var n = Number(c.narracaoDur);
|
|
70
|
-
if (isFinite(n) && n > 0 && c.narracaoAudio) d = Math.max(d, Math.min(90, n + 0.4));
|
|
71
|
-
return d;
|
|
72
|
-
}
|
|
73
|
-
function total() { return cenas.reduce(function (s, _, i) { return s + duracaoDe(i); }, 0); }
|
|
74
|
-
function inicioDe(i) { var s = 0; for (var k = 0; k < i; k++) s += duracaoDe(k); return s; }
|
|
75
|
-
|
|
76
|
-
/* ----------------------------------------------------------- montagem -- */
|
|
77
|
-
|
|
78
|
-
function montar(a) {
|
|
79
|
-
arq = a;
|
|
80
|
-
doc = a.doc || {};
|
|
81
|
-
document.title = a.titulo || "PrimoCode";
|
|
82
|
-
palco.innerHTML = "";
|
|
83
|
-
ehVideo = a.ferramenta === "corte";
|
|
84
|
-
|
|
85
|
-
if (a.ferramenta === "prosa") return montarProsa();
|
|
86
|
-
if (a.ferramenta === "grade") return montarGrade();
|
|
87
|
-
|
|
88
|
-
cenas = doc.cenas || [];
|
|
89
|
-
// O rodapé (nome do deck, "03 / 08") é coisa de APRESENTAÇÃO: o vídeo e
|
|
90
|
-
// a arte não têm. Ver o rodapé em render.js.
|
|
91
|
-
var rodape = a.ferramenta === "prisma" ? { titulo: a.titulo, total: cenas.length } : null;
|
|
92
|
-
cenas.forEach(function (c, i) {
|
|
93
|
-
var no = renderCena(c, doc, {
|
|
94
|
-
animar: true,
|
|
95
|
-
rodape: rodape ? { titulo: rodape.titulo, indice: i + 1, total: rodape.total } : null,
|
|
96
|
-
});
|
|
97
|
-
no.style.position = "absolute";
|
|
98
|
-
no.style.inset = "0";
|
|
99
|
-
no.style.display = i === 0 ? "block" : "none";
|
|
100
|
-
palco.appendChild(no);
|
|
101
|
-
});
|
|
102
|
-
palco.style.cssText = "position:absolute;left:50%;top:50%;width:" +
|
|
103
|
-
(doc.largura || 1920) + "px;height:" + (doc.altura || 1080) + "px";
|
|
104
|
-
|
|
105
|
-
ajustar();
|
|
106
|
-
ir(0);
|
|
107
|
-
if (carregadoOk) { carregadoOk(a); carregadoOk = null; }
|
|
108
|
-
|
|
109
|
-
if (location.hash === "#imprimir") return setTimeout(imprimirTudo, 350);
|
|
110
|
-
|
|
111
|
-
if (ehVideo) {
|
|
112
|
-
montarTransporte();
|
|
113
|
-
prepararTrilha();
|
|
114
|
-
tocar();
|
|
115
|
-
// /v/<id>#gravar: o CLI abre assim quando o usuário pede "baixa o
|
|
116
|
-
// vídeo". O navegador pede a permissão de tela (a "aprovação"), o
|
|
117
|
-
// usuário aceita, o vídeo roda do zero gravando e baixa no fim —
|
|
118
|
-
// nenhuma chamada de IA no caminho inteiro.
|
|
119
|
-
if (location.hash.indexOf("gravar") !== -1) {
|
|
120
|
-
setTimeout(function () {
|
|
121
|
-
aviso("aceite a permissão para gravar — o download sai no fim", 4000);
|
|
122
|
-
iniciarGravacao();
|
|
123
|
-
}, 700);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function montarProsa() {
|
|
129
|
-
palco.style.cssText = "position:static";
|
|
130
|
-
document.body.style.overflow = "auto";
|
|
131
|
-
document.body.style.background = "#17171b";
|
|
132
|
-
var w = document.createElement("div");
|
|
133
|
-
w.style.cssText = "display:grid;place-items:center;padding:40px 20px";
|
|
134
|
-
w.appendChild(renderProsa(doc));
|
|
135
|
-
palco.appendChild(w);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function montarGrade() {
|
|
139
|
-
palco.style.cssText = "position:static";
|
|
140
|
-
document.body.style.overflow = "auto";
|
|
141
|
-
document.body.style.background = "#17171b";
|
|
142
|
-
var g = document.createElement("div");
|
|
143
|
-
g.style.cssText = "padding:26px";
|
|
144
|
-
(doc.abas || []).forEach(function (aba, i) {
|
|
145
|
-
var t = document.createElement("div");
|
|
146
|
-
t.style.cssText = "color:#8d8d9a;font:600 11px/1 system-ui;letter-spacing:.12em;" +
|
|
147
|
-
"text-transform:uppercase;margin:22px 0 10px";
|
|
148
|
-
t.textContent = aba.nome || "Planilha " + (i + 1);
|
|
149
|
-
g.appendChild(t);
|
|
150
|
-
var caixa = document.createElement("div");
|
|
151
|
-
caixa.style.cssText = "overflow:auto;border-radius:12px;border:1px solid rgba(255,255,255,.09)";
|
|
152
|
-
caixa.appendChild(renderGrade(doc, { aba: i }));
|
|
153
|
-
g.appendChild(caixa);
|
|
154
|
-
});
|
|
155
|
-
palco.appendChild(g);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function ajustar() {
|
|
159
|
-
if (!cenas.length) return;
|
|
160
|
-
var alturaUI = ehVideo ? 74 : 0;
|
|
161
|
-
var larguraUI = painel ? 360 : 0;
|
|
162
|
-
var e = Math.min((window.innerWidth - larguraUI) / (doc.largura || 1920),
|
|
163
|
-
(window.innerHeight - alturaUI) / (doc.altura || 1080));
|
|
164
|
-
palco.style.left = ((window.innerWidth - larguraUI) / 2) + "px";
|
|
165
|
-
palco.style.transform = "translate(-50%,-50%) scale(" + e + ")";
|
|
166
|
-
palco.style.marginTop = ehVideo ? (-alturaUI / 2) + "px" : "0";
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/* ---------------------------------------------------------- navegação -- */
|
|
170
|
-
|
|
171
|
-
function ir(n, semRelogio) {
|
|
172
|
-
if (!cenas.length) return;
|
|
173
|
-
var anterior = atual;
|
|
174
|
-
atual = Math.max(0, Math.min(cenas.length - 1, n));
|
|
175
|
-
if (anterior !== atual) pararClipes(anterior); // o áudio da cena que saiu vai junto
|
|
176
|
-
Array.prototype.forEach.call(palco.children, function (no, i) {
|
|
177
|
-
no.style.display = i === atual ? "block" : "none";
|
|
178
|
-
if (i === atual) {
|
|
179
|
-
no.classList.remove("animar");
|
|
180
|
-
void no.offsetWidth; // reinicia entrada e transição
|
|
181
|
-
no.classList.add("animar");
|
|
182
|
-
contar(no);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
/* SFX na entrada da cena — só durante a reprodução (navegar com as setas
|
|
186
|
-
não faz barulho) e nunca na primeira montagem. `doc.sfx === false`
|
|
187
|
-
desliga tudo; `cena.sfx` escolhe o efeito ("whoosh" | "impacto" |
|
|
188
|
-
"click" | "nenhum") e ganha do automático por transição. */
|
|
189
|
-
if (sfx && tocando && ehVideo && atual !== anterior && doc.sfx !== false) {
|
|
190
|
-
var cenaNova = cenas[atual] || {};
|
|
191
|
-
var somDaCena = cenaNova.som != null ? cenaNova.som : cenaNova.sfx;
|
|
192
|
-
if (somDaCena !== "nenhum") {
|
|
193
|
-
if (somDaCena) sfx.tocar(somDaCena);
|
|
194
|
-
else sfx.transicao(cenaNova.transicao, atual);
|
|
195
|
-
}
|
|
196
|
-
/* SOM POR ELEMENTO: `som` num elemento toca quando ELE entra — no
|
|
197
|
-
mesmo instante da animação de entrada (o mesmo atraso da cascata
|
|
198
|
-
que o render.js dá ao --atraso). Um número que faz "ding" ao
|
|
199
|
-
aparecer, um ícone que faz "pop", a barra que sobe com o riser: é
|
|
200
|
-
o que separa motion de slide animado. Nome da biblioteca ou
|
|
201
|
-
{arquivo, volume} — o arquivo entra no grafo e vai para o MP4. */
|
|
202
|
-
agendarSonsDosElementos(cenaNova);
|
|
203
|
-
}
|
|
204
|
-
narrar();
|
|
205
|
-
pintarApresentador();
|
|
206
|
-
if (!semRelogio) decorrido = inicioDe(atual);
|
|
207
|
-
sincronizarClipes();
|
|
208
|
-
pintar();
|
|
209
|
-
if (location.hash.indexOf("#imprimir") !== 0) {
|
|
210
|
-
history.replaceState(null, "", "#/" + (atual + 1));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function proxima() { if (atual < cenas.length - 1) ir(atual + 1); else if (ehVideo) fim(); }
|
|
215
|
-
function anterior() { ir(atual - 1); }
|
|
216
|
-
|
|
217
|
-
/* ── clipes: a gravação do usuário andando junto com a cena ─────────────
|
|
218
|
-
Um `clipe` é um trecho do arquivo — `de` e `ate` em segundos. Quem manda
|
|
219
|
-
no tempo é o relógio do player, não o do <video>: senão cada cena com
|
|
220
|
-
gravação puxaria a linha do tempo para o lado dela e a trilha, a narração
|
|
221
|
-
e a exportação sairiam cada uma num compasso.
|
|
222
|
-
|
|
223
|
-
Mas corrigir `currentTime` a cada quadro deixa a imagem tremida e corta o
|
|
224
|
-
áudio. Então o <video> toca sozinho, que é o que ele faz bem, e só é
|
|
225
|
-
puxado de volta quando a diferença passa de um quinto de segundo — que é
|
|
226
|
-
mais que o ouvido perdoa numa fala. */
|
|
227
|
-
var DERIVA = 0.2;
|
|
228
|
-
|
|
229
|
-
function clipesDe(i) {
|
|
230
|
-
var no = palco.children[i];
|
|
231
|
-
return no ? no.querySelectorAll("video") : [];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function trechoDe(v) {
|
|
235
|
-
var de = parseFloat(v.dataset.de), ate = parseFloat(v.dataset.ate);
|
|
236
|
-
return { de: isFinite(de) ? de : 0, ate: isFinite(ate) ? ate : Infinity };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/* A voz do usuário tem de entrar no ARQUIVO, não só sair na caixa de som.
|
|
240
|
-
O MP4 leva as faixas de áudio que estão no grafo da trilha e mais nada —
|
|
241
|
-
um <video> tocando por fora sai pelo alto-falante e some do arquivo, que
|
|
242
|
-
é o pior dos mundos: parece certo na tela e chega mudo em quem recebe.
|
|
243
|
-
|
|
244
|
-
Entra pela `entradaVoz`, a mesma porta da narração: fica fora do volume
|
|
245
|
-
da trilha e fora do duck. Quem abaixa é a música; a fala, nunca.
|
|
246
|
-
`createMediaElementSource` só aceita ser chamado uma vez por elemento —
|
|
247
|
-
daí a marca no próprio nó. */
|
|
248
|
-
function ligarAoGrafo(v) {
|
|
249
|
-
if (v._ligado || !trilha || !trilha.ctx || !trilha.entradaVoz) return;
|
|
250
|
-
v._ligado = true;
|
|
251
|
-
if (v.muted) return; // clipe mudo não tem o que ligar
|
|
252
|
-
try {
|
|
253
|
-
trilha.ctx.createMediaElementSource(v).connect(trilha.entradaVoz);
|
|
254
|
-
if (trilha.duck) trilha.duck(true); // a música cede enquanto ele fala
|
|
255
|
-
} catch (_) { /* já conectado, ou sem Web Audio: toca direto mesmo */ }
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function tocarClipe(v) {
|
|
259
|
-
ligarAoGrafo(v);
|
|
260
|
-
if (trilha && trilha.ctx && trilha.ctx.state === "suspended") trilha.ctx.resume();
|
|
261
|
-
var p = v.play();
|
|
262
|
-
if (p && p.catch) p.catch(function () {}); // autoplay barrado: fica o quadro
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function pararClipes(i) {
|
|
266
|
-
Array.prototype.forEach.call(clipesDe(i), function (v) { try { v.pause(); } catch (e) {} });
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/* Chamado a cada quadro do laço, e depois de qualquer salto no tempo. */
|
|
270
|
-
function sincronizarClipes() {
|
|
271
|
-
var vs = clipesDe(atual);
|
|
272
|
-
if (!vs.length) return;
|
|
273
|
-
var dentro = Math.max(0, decorrido - inicioDe(atual));
|
|
274
|
-
Array.prototype.forEach.call(vs, function (v) {
|
|
275
|
-
if (v.readyState < 1) return; // ainda sem metadados: nada a buscar
|
|
276
|
-
var t = trechoDe(v);
|
|
277
|
-
var alvo = t.de + dentro;
|
|
278
|
-
/* Passou do fim do trecho: segura o último quadro em vez de invadir o
|
|
279
|
-
que vem depois no arquivo — invadir é justamente a pausa que se
|
|
280
|
-
queria cortar. */
|
|
281
|
-
if (alvo >= t.ate) { if (!v.paused) v.pause(); return; }
|
|
282
|
-
/* Uma busca por vez, e com respiro. Numa máquina lenta (ou na
|
|
283
|
-
exportação escondida, que rasteriza a cena a cada quadro no mesmo
|
|
284
|
-
fio), o <video> fica atrás do relógio por mais de DERIVA em TODO
|
|
285
|
-
quadro: puxá-lo de volta a cada rAF é uma busca em cima da outra, e
|
|
286
|
-
um vídeo que só busca nunca tem quadro pronto — readyState fica em 1,
|
|
287
|
-
o export não desenha o clipe e o arquivo sai com o chão da cena no
|
|
288
|
-
lugar da gravação. Medido quadro a quadro no diagnóstico. */
|
|
289
|
-
if (v.seeking) return;
|
|
290
|
-
var agora = performance.now();
|
|
291
|
-
if (Math.abs(v.currentTime - alvo) > DERIVA && (!v._ultimaBusca || agora - v._ultimaBusca > 600)) {
|
|
292
|
-
v._ultimaBusca = agora;
|
|
293
|
-
try { v.currentTime = alvo; } catch (e) {}
|
|
294
|
-
}
|
|
295
|
-
if (tocando && v.paused) tocarClipe(v);
|
|
296
|
-
else if (!tocando && !v.paused) v.pause();
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/* números que contam do zero até o valor ------------------------------- */
|
|
301
|
-
|
|
302
|
-
function contar(cena) {
|
|
303
|
-
Array.prototype.forEach.call(cena.querySelectorAll("b[data-contar]"), function (b) {
|
|
304
|
-
var alvo = parseFloat(b.getAttribute("data-contar"));
|
|
305
|
-
var molde = b.getAttribute("data-molde") || String(alvo);
|
|
306
|
-
var casas = (String(alvo).split(".")[1] || "").length;
|
|
307
|
-
var trecho = molde.match(/-?[\d.,]+/);
|
|
308
|
-
if (!trecho) return;
|
|
309
|
-
var inicio = performance.now(), dur = 1100;
|
|
310
|
-
(function passo(agora) {
|
|
311
|
-
var t = Math.min(1, (agora - inicio) / dur);
|
|
312
|
-
var suave = 1 - Math.pow(1 - t, 3);
|
|
313
|
-
var v = (alvo * suave).toFixed(casas);
|
|
314
|
-
b.textContent = molde.replace(trecho[0], String(v).replace(".", casas ? "," : ""));
|
|
315
|
-
if (t < 1) requestAnimationFrame(passo);
|
|
316
|
-
else b.textContent = molde;
|
|
317
|
-
})(inicio);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/* --------------------------------------------------------- reprodução -- */
|
|
322
|
-
|
|
323
|
-
function tocar() {
|
|
324
|
-
if (tocando || !cenas.length) return;
|
|
325
|
-
tocando = true;
|
|
326
|
-
t0 = performance.now() - decorrido * 1000;
|
|
327
|
-
if (trilha) { trilha.tocar(); }
|
|
328
|
-
sincronizarClipes();
|
|
329
|
-
laco();
|
|
330
|
-
atualizarBotao();
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function pausar() {
|
|
334
|
-
tocando = false;
|
|
335
|
-
cancelAnimationFrame(quadro);
|
|
336
|
-
pararClipes(atual);
|
|
337
|
-
if (trilha) trilha.parar();
|
|
338
|
-
if (typeof Narracao !== "undefined") Narracao.parar();
|
|
339
|
-
if (vozGravada) { vozGravada.pause(); vozGravada = null; }
|
|
340
|
-
atualizarBotao();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function fim() {
|
|
344
|
-
pausar();
|
|
345
|
-
decorrido = total();
|
|
346
|
-
pintar();
|
|
347
|
-
aviso("fim", 1200);
|
|
348
|
-
if (gravador && gravador.state === "recording") setTimeout(pararGravacao, 600);
|
|
349
|
-
if (gravadorAudio && gravadorAudio.state === "recording") setTimeout(pararGravacao, 600);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/* A narração da cena ainda está no ar? */
|
|
353
|
-
function narrando() {
|
|
354
|
-
if (vozGravada && !vozGravada.ended && !vozGravada.paused) return true;
|
|
355
|
-
if (typeof Narracao !== "undefined" && Narracao.falando && Narracao.falando()) return true;
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function laco() {
|
|
360
|
-
if (!tocando) return;
|
|
361
|
-
decorrido = (performance.now() - t0) / 1000;
|
|
362
|
-
var alvo = cenas.length - 1;
|
|
363
|
-
var acc = 0;
|
|
364
|
-
for (var i = 0; i < cenas.length; i++) {
|
|
365
|
-
acc += duracaoDe(i);
|
|
366
|
-
if (decorrido < acc) { alvo = i; break; }
|
|
367
|
-
}
|
|
368
|
-
/* A CENA ESPERA A VOZ. O relógio andava só pela `duracao`, e uma narração
|
|
369
|
-
mais longa que a cena vazava por cima da seguinte — era a "narração
|
|
370
|
-
fora de sincronia". Se a fala da cena atual ainda está no ar, o relógio
|
|
371
|
-
congela na beirada da cena: quem dita o corte é a voz. */
|
|
372
|
-
var fimDaAtual = inicioDe(atual) + duracaoDe(atual);
|
|
373
|
-
if ((alvo !== atual || decorrido >= total()) && narrando()) {
|
|
374
|
-
t0 = performance.now() - (fimDaAtual - 0.05) * 1000;
|
|
375
|
-
decorrido = fimDaAtual - 0.05;
|
|
376
|
-
pintar();
|
|
377
|
-
quadro = requestAnimationFrame(laco);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
if (decorrido >= total()) return fim();
|
|
381
|
-
if (alvo !== atual) ir(alvo, true);
|
|
382
|
-
sincronizarClipes();
|
|
383
|
-
pintar();
|
|
384
|
-
quadro = requestAnimationFrame(laco);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function buscar(segundos) {
|
|
388
|
-
decorrido = Math.max(0, Math.min(total() - 0.01, segundos));
|
|
389
|
-
var acc = 0, alvo = 0;
|
|
390
|
-
for (var i = 0; i < cenas.length; i++) {
|
|
391
|
-
acc += duracaoDe(i);
|
|
392
|
-
if (decorrido < acc) { alvo = i; break; }
|
|
393
|
-
alvo = i;
|
|
394
|
-
}
|
|
395
|
-
ir(alvo, true);
|
|
396
|
-
t0 = performance.now() - decorrido * 1000;
|
|
397
|
-
if (trilha && trilha.elemento) trilha.elemento.currentTime = (doc.trilha.inicio || 0) + decorrido;
|
|
398
|
-
sincronizarClipes();
|
|
399
|
-
pintar();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/* Antes de exportar, os clipes precisam ter chegado — `drawImage` de um
|
|
403
|
-
<video> sem quadro decodificado desenha nada, e o resultado seria a
|
|
404
|
-
gravação do usuário faltando do vídeo dele. Espera com prazo: um arquivo
|
|
405
|
-
que não carrega não pode travar a exportação da peça inteira. */
|
|
406
|
-
function clipesProntos(prazo) {
|
|
407
|
-
var todos = [];
|
|
408
|
-
Array.prototype.forEach.call(palco.querySelectorAll("video"), function (v) {
|
|
409
|
-
if (v.readyState >= 2) return;
|
|
410
|
-
todos.push(new Promise(function (ok) {
|
|
411
|
-
var pronto = function () { v.removeEventListener("loadeddata", pronto); ok(); };
|
|
412
|
-
v.addEventListener("loadeddata", pronto);
|
|
413
|
-
v.addEventListener("error", pronto);
|
|
414
|
-
try { v.load(); } catch (e) {}
|
|
415
|
-
}));
|
|
416
|
-
});
|
|
417
|
-
if (!todos.length) return Promise.resolve();
|
|
418
|
-
return Promise.race([
|
|
419
|
-
Promise.all(todos),
|
|
420
|
-
new Promise(function (ok) { setTimeout(ok, prazo || 15000); }),
|
|
421
|
-
]);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/* ---------------------------------------------------------- narração -- */
|
|
425
|
-
|
|
426
|
-
var vozGravada = null;
|
|
427
|
-
|
|
428
|
-
function narrar() {
|
|
429
|
-
if (typeof Narracao !== "undefined") Narracao.parar();
|
|
430
|
-
if (vozGravada) { vozGravada.pause(); vozGravada = null; }
|
|
431
|
-
if (trilha && trilha.duck) trilha.duck(false);
|
|
432
|
-
if (mudo || doc.narracao === false) return;
|
|
433
|
-
|
|
434
|
-
var cena = cenas[atual] || {};
|
|
435
|
-
|
|
436
|
-
// voz gravada pelo usuário ganha da sintetizada, sempre
|
|
437
|
-
if (cena.narracaoAudio) {
|
|
438
|
-
vozGravada = new Audio(cena.narracaoAudio);
|
|
439
|
-
vozGravada.volume = (doc.voz && doc.voz.volume != null) ? doc.voz.volume : 1;
|
|
440
|
-
/* Pelo grafo da trilha, quando ele existe: a voz entra na gravação MP4
|
|
441
|
-
sem "compartilhar áudio da aba", e a música cede espaço (duck)
|
|
442
|
-
enquanto a fala dura. Sem grafo, toca direto como antes. */
|
|
443
|
-
if (trilha && trilha.ctx && trilha.entradaVoz) {
|
|
444
|
-
try {
|
|
445
|
-
var fonteVoz = trilha.ctx.createMediaElementSource(vozGravada);
|
|
446
|
-
fonteVoz.connect(trilha.entradaVoz);
|
|
447
|
-
} catch (_) { /* elemento já conectado ou grafo indisponível */ }
|
|
448
|
-
if (trilha.duck) {
|
|
449
|
-
trilha.duck(true);
|
|
450
|
-
vozGravada.onended = function () { if (trilha && trilha.duck) trilha.duck(false); };
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
var p = vozGravada.play();
|
|
454
|
-
if (p && p.catch) p.catch(function () {});
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (typeof Narracao === "undefined" || !Narracao.disponivel()) return;
|
|
459
|
-
if (cena.narracao) {
|
|
460
|
-
if (trilha && trilha.duck) {
|
|
461
|
-
trilha.duck(true);
|
|
462
|
-
Narracao.falar(cena.narracao, doc.voz || {}).then(function () {
|
|
463
|
-
if (trilha && trilha.duck) trilha.duck(false);
|
|
464
|
-
});
|
|
465
|
-
} else {
|
|
466
|
-
Narracao.falar(cena.narracao, doc.voz || {});
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/* ----------------------------------------------------- apresentador --- */
|
|
472
|
-
|
|
473
|
-
var painel = null, cronometro = 0, tiquetaque = null;
|
|
474
|
-
|
|
475
|
-
function alternarApresentador() {
|
|
476
|
-
if (painel) {
|
|
477
|
-
painel.remove(); painel = null;
|
|
478
|
-
clearInterval(tiquetaque); tiquetaque = null;
|
|
479
|
-
ajustar();
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
painel = document.createElement("div");
|
|
483
|
-
painel.className = "apresentador";
|
|
484
|
-
painel.innerHTML =
|
|
485
|
-
'<div><h4>Tempo</h4><div class="relogio" id="ap-relogio">00:00</div></div>' +
|
|
486
|
-
'<div><h4>Notas</h4><div class="corpo" id="ap-notas"></div></div>' +
|
|
487
|
-
'<div class="prox" id="ap-prox"></div>' +
|
|
488
|
-
'<div class="dica">→ avança · ← volta<br>S fecha · T zera o tempo<br>F tela cheia · P imprime</div>';
|
|
489
|
-
document.body.appendChild(painel);
|
|
490
|
-
tiquetaque = setInterval(function () {
|
|
491
|
-
cronometro++;
|
|
492
|
-
var r = document.getElementById("ap-relogio");
|
|
493
|
-
if (r) r.textContent = mmss(cronometro);
|
|
494
|
-
}, 1000);
|
|
495
|
-
pintarApresentador();
|
|
496
|
-
ajustar();
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function pintarApresentador() {
|
|
500
|
-
if (!painel) return;
|
|
501
|
-
var notas = (cenas[atual] || {}).notas;
|
|
502
|
-
document.getElementById("ap-notas").innerHTML = notas
|
|
503
|
-
? String(notas).replace(/&/g, "&").replace(/</g, "<").replace(/\n/g, "<br>")
|
|
504
|
-
: '<span style="color:#5e6a77">— sem notas —</span>';
|
|
505
|
-
var prox = cenas[atual + 1];
|
|
506
|
-
var t = prox ? (prox.elementos || []).filter(function (e) {
|
|
507
|
-
return e.tipo === "texto" || e.tipo === "citacao";
|
|
508
|
-
})[0] : null;
|
|
509
|
-
document.getElementById("ap-prox").textContent = prox
|
|
510
|
-
? "Próxima: " + ((t && t.texto) || "cena " + (atual + 2)).split("\n")[0]
|
|
511
|
-
: "Última cena";
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/* ------------------------------------------------------------ trilha -- */
|
|
515
|
-
|
|
516
|
-
var sfx = null;
|
|
517
|
-
|
|
518
|
-
/* Os sons dos ELEMENTOS da cena que acabou de entrar. O atraso é o mesmo
|
|
519
|
-
que o render.js dá à animação de entrada (cascata: i × 0,09 s + 0,1 s, ou
|
|
520
|
-
o `anim.atraso` declarado), então o som cai no instante em que o
|
|
521
|
-
elemento aparece. Trocar de cena antes cancela o que ainda não tocou —
|
|
522
|
-
senão o "ding" da cena 3 tocava em cima da cena 4. */
|
|
523
|
-
var sonsAgendados = [];
|
|
524
|
-
function agendarSonsDosElementos(cena) {
|
|
525
|
-
sonsAgendados.forEach(clearTimeout);
|
|
526
|
-
sonsAgendados = [];
|
|
527
|
-
if (!sfx || !cena || !Array.isArray(cena.elementos)) return;
|
|
528
|
-
cena.elementos.forEach(function (e, i) {
|
|
529
|
-
if (!e || !e.som) return;
|
|
530
|
-
var atraso = (e.anim && typeof e.anim === "object" && e.anim.atraso != null)
|
|
531
|
-
? +e.anim.atraso
|
|
532
|
-
: (e.chaves && e.chaves.length > 1 ? 0 : i * 0.09 + 0.1);
|
|
533
|
-
if (typeof e.som === "object" && e.som.em != null) atraso = +e.som.em; // segundos na cena, explícito
|
|
534
|
-
sonsAgendados.push(setTimeout(function () { sfx.tocar(e.som); }, Math.max(0, atraso) * 1000));
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function prepararTrilha() {
|
|
539
|
-
if (typeof criarTrilha !== "function") return;
|
|
540
|
-
var cfg = doc.trilha;
|
|
541
|
-
/* Sem trilha o grafo ainda nasce (tipo "nenhuma"): é por ele que a
|
|
542
|
-
narração e os SFX entram na gravação MP4. */
|
|
543
|
-
if (!cfg || (!cfg.src && cfg.tipo !== "gerada")) cfg = { tipo: "nenhuma" };
|
|
544
|
-
trilha = criarTrilha(cfg, function (msg) { aviso(msg, 3000); });
|
|
545
|
-
if (trilha && trilha.ctx && trilha.no && typeof criarSfx === "function" && doc.sfx !== false) {
|
|
546
|
-
sfx = criarSfx(trilha.ctx, trilha.no);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/* --------------------------------------------------------- transporte -- */
|
|
551
|
-
|
|
552
|
-
function montarTransporte() {
|
|
553
|
-
transporte = document.createElement("div");
|
|
554
|
-
transporte.className = "transporte";
|
|
555
|
-
transporte.innerHTML =
|
|
556
|
-
'<button class="tp-btn" id="tp-tocar" title="Tocar / pausar (K)"></button>' +
|
|
557
|
-
'<div class="tp-tempo" id="tp-agora">00:00</div>' +
|
|
558
|
-
'<div class="tp-barra" id="tp-barra"><div class="tp-cheio" id="tp-cheio"></div>' +
|
|
559
|
-
'<div class="tp-marcas" id="tp-marcas"></div></div>' +
|
|
560
|
-
'<div class="tp-tempo" id="tp-total">00:00</div>' +
|
|
561
|
-
'<button class="tp-btn pequeno" id="tp-som" title="Mudo (M)"></button>' +
|
|
562
|
-
'<input class="tp-vol" id="tp-vol" type="range" min="0" max="100" value="70">' +
|
|
563
|
-
'<button class="tp-btn texto primo" id="tp-gravar" title="Baixar vídeo ou áudio">Baixar ▾</button>';
|
|
564
|
-
document.body.appendChild(transporte);
|
|
565
|
-
|
|
566
|
-
document.getElementById("tp-tocar").onclick = function () { tocando ? pausar() : tocar(); };
|
|
567
|
-
document.getElementById("tp-som").onclick = alternarMudo;
|
|
568
|
-
document.getElementById("tp-gravar").onclick = menuBaixar;
|
|
569
|
-
document.getElementById("tp-vol").oninput = function () {
|
|
570
|
-
if (trilha) trilha.volume(this.value / 100);
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
var barra = document.getElementById("tp-barra");
|
|
574
|
-
barra.onclick = function (ev) {
|
|
575
|
-
var r = barra.getBoundingClientRect();
|
|
576
|
-
buscar(((ev.clientX - r.left) / r.width) * total());
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
var marcas = document.getElementById("tp-marcas");
|
|
580
|
-
cenas.forEach(function (_, i) {
|
|
581
|
-
if (!i) return;
|
|
582
|
-
var m = document.createElement("i");
|
|
583
|
-
m.style.left = (inicioDe(i) / total() * 100) + "%";
|
|
584
|
-
marcas.appendChild(m);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
document.getElementById("tp-total").textContent = mmss(total());
|
|
588
|
-
atualizarBotao();
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function atualizarBotao() {
|
|
592
|
-
var b = document.getElementById("tp-tocar");
|
|
593
|
-
if (b) b.innerHTML = svgIcone(tocando ? "pausa" : "play");
|
|
594
|
-
var s = document.getElementById("tp-som");
|
|
595
|
-
if (s) s.innerHTML = svgIcone("olho");
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
var mudo = false;
|
|
599
|
-
function alternarMudo() {
|
|
600
|
-
mudo = !mudo;
|
|
601
|
-
if (trilha) trilha.volume(mudo ? 0 : (document.getElementById("tp-vol").value / 100));
|
|
602
|
-
aviso(mudo ? "som desligado" : "som ligado", 900);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function pintar() {
|
|
606
|
-
var c = document.getElementById("tp-cheio");
|
|
607
|
-
if (!c) return;
|
|
608
|
-
c.style.width = (decorrido / total() * 100) + "%";
|
|
609
|
-
document.getElementById("tp-agora").textContent = mmss(decorrido);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/* ---------------------------------------------------------- gravação --- */
|
|
613
|
-
|
|
614
|
-
function alternarGravacao() {
|
|
615
|
-
if (gravador && gravador.state === "recording") return pararGravacao();
|
|
616
|
-
iniciarGravacao();
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/* Menu do botão Baixar: vídeo completo ou só o áudio.
|
|
620
|
-
|
|
621
|
-
O áudio sai DIRETO do grafo (trilha + narração + SFX, já mixados com
|
|
622
|
-
ducking) — sem pedir permissão de tela nenhuma, porque não há tela:
|
|
623
|
-
é o MediaRecorder pendurado no fluxo da Web Audio. */
|
|
624
|
-
function menuBaixar() {
|
|
625
|
-
if ((gravador && gravador.state === "recording") ||
|
|
626
|
-
(gravadorAudio && gravadorAudio.state === "recording")) {
|
|
627
|
-
return pararGravacao();
|
|
628
|
-
}
|
|
629
|
-
var velho = document.getElementById("menu-baixar");
|
|
630
|
-
if (velho) { velho.remove(); return; }
|
|
631
|
-
var m = document.createElement("div");
|
|
632
|
-
m.id = "menu-baixar";
|
|
633
|
-
m.className = "menu-baixar";
|
|
634
|
-
m.innerHTML =
|
|
635
|
-
'<button data-acao="mp4">🎬 Vídeo completo <span>' + (gravaMp4() ? "MP4" : "WebM") + " · imagem + som</span></button>" +
|
|
636
|
-
'<button data-acao="audio">🎧 Só o áudio <span>narração + trilha + efeitos</span></button>';
|
|
637
|
-
document.body.appendChild(m);
|
|
638
|
-
m.onclick = function (ev) {
|
|
639
|
-
var b = ev.target.closest("button");
|
|
640
|
-
if (!b) return;
|
|
641
|
-
m.remove();
|
|
642
|
-
if (b.dataset.acao === "mp4") iniciarGravacao();
|
|
643
|
-
else iniciarGravacaoAudio();
|
|
644
|
-
};
|
|
645
|
-
setTimeout(function () {
|
|
646
|
-
document.addEventListener("click", function fecha(e) {
|
|
647
|
-
if (!m.contains(e.target)) { m.remove(); document.removeEventListener("click", fecha); }
|
|
648
|
-
});
|
|
649
|
-
}, 0);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/* ------------------------------------------------ gravação só de áudio -- */
|
|
653
|
-
|
|
654
|
-
var gravadorAudio = null, pedacosAudio = [];
|
|
655
|
-
|
|
656
|
-
var FORMATOS_AUDIO = [
|
|
657
|
-
"audio/mp4", // AAC → .m4a: toca em tudo, importa em tudo
|
|
658
|
-
"audio/webm;codecs=opus",
|
|
659
|
-
"audio/webm",
|
|
660
|
-
];
|
|
661
|
-
|
|
662
|
-
function iniciarGravacaoAudio() {
|
|
663
|
-
if (!trilha || !trilha.fluxo) return aviso("áudio indisponível neste navegador", 2500);
|
|
664
|
-
var tipo = "";
|
|
665
|
-
for (var i = 0; i < FORMATOS_AUDIO.length; i++) {
|
|
666
|
-
if (MediaRecorder.isTypeSupported(FORMATOS_AUDIO[i])) { tipo = FORMATOS_AUDIO[i]; break; }
|
|
667
|
-
}
|
|
668
|
-
pedacosAudio = [];
|
|
669
|
-
gravadorAudio = new MediaRecorder(trilha.fluxo, tipo ? { mimeType: tipo, audioBitsPerSecond: 192000 } : undefined);
|
|
670
|
-
gravadorAudio.ondataavailable = function (ev) { if (ev.data.size) pedacosAudio.push(ev.data); };
|
|
671
|
-
gravadorAudio.onstop = baixarAudio;
|
|
672
|
-
gravadorAudio.start();
|
|
673
|
-
var b = document.getElementById("tp-gravar");
|
|
674
|
-
if (b) { b.classList.add("gravando"); b.textContent = "Parar"; }
|
|
675
|
-
buscar(0);
|
|
676
|
-
tocar();
|
|
677
|
-
aviso("gravando o áudio — para sozinho no fim", 2600);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function baixarAudio() {
|
|
681
|
-
var mime = (gravadorAudio && gravadorAudio.mimeType) || "audio/webm";
|
|
682
|
-
var ext = /mp4/.test(mime) ? ".m4a" : ".weba";
|
|
683
|
-
var blob = new Blob(pedacosAudio, { type: mime.split(";")[0] });
|
|
684
|
-
var url = URL.createObjectURL(blob);
|
|
685
|
-
var a = document.createElement("a");
|
|
686
|
-
a.href = url;
|
|
687
|
-
a.download = (arq.titulo || "audio").replace(/[^\w\-]+/g, "-").toLowerCase() + ext;
|
|
688
|
-
a.click();
|
|
689
|
-
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
690
|
-
aviso("áudio salvo em " + ext, 2600);
|
|
691
|
-
gravadorAudio = null;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/* O melhor formato que este navegador sabe gravar.
|
|
695
|
-
*
|
|
696
|
-
* A ordem importa: H.264 com AAC é o MP4 que toca em qualquer lugar —
|
|
697
|
-
* WhatsApp, Instagram, PowerPoint, TV. AV1 em MP4 é MP4 de verdade mas ainda
|
|
698
|
-
* não toca em tudo, então vem depois. O webm é o último recurso: abre em
|
|
699
|
-
* navegador e pouco mais.
|
|
700
|
-
*/
|
|
701
|
-
var FORMATOS = [
|
|
702
|
-
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
|
|
703
|
-
"video/mp4;codecs=avc1,mp4a.40.2",
|
|
704
|
-
"video/mp4;codecs=avc1",
|
|
705
|
-
"video/mp4",
|
|
706
|
-
"video/webm;codecs=vp9,opus",
|
|
707
|
-
"video/webm",
|
|
708
|
-
];
|
|
709
|
-
|
|
710
|
-
function escolherFormato() {
|
|
711
|
-
for (var i = 0; i < FORMATOS.length; i++) {
|
|
712
|
-
if (MediaRecorder.isTypeSupported(FORMATOS[i])) return FORMATOS[i];
|
|
713
|
-
}
|
|
714
|
-
return "video/webm";
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function gravaMp4() { return /mp4/.test(escolherFormato()); }
|
|
718
|
-
|
|
719
|
-
/* Baixar o vídeo = DESENHAR o vídeo, não gravar a tela.
|
|
720
|
-
*
|
|
721
|
-
* Aqui havia getDisplayMedia: pedia permissão, o usuário escolhia uma
|
|
722
|
-
* janela, e o navegador salvava o que estivesse aparecendo. O ponteiro do
|
|
723
|
-
* mouse entrava no vídeo, notificação entrava no vídeo, e a resolução era
|
|
724
|
-
* a da janela em vez da do projeto. Quem vai VENDER a ferramenta não pede
|
|
725
|
-
* ao cliente para compartilhar a tela para exportar um arquivo dele.
|
|
726
|
-
*
|
|
727
|
-
* Agora o player TOCA e o video.js fotografa a cena que está no palco,
|
|
728
|
-
* quadro a quadro, para um canvas do tamanho exato do projeto. O áudio é
|
|
729
|
-
* o mesmo grafo de sempre — trilha, narração e efeitos, já mixados com o
|
|
730
|
-
* ducking — então o arquivo sai idêntico ao que se ouve aqui.
|
|
731
|
-
*/
|
|
732
|
-
/* Monta o arquivo com o PLAYER como fonte, sem baixar. É o que o botão usa
|
|
733
|
-
por baixo e o que a exportação escondida (studio_exportar) chama por
|
|
734
|
-
fora — um caminho só, para o arquivo sair igual nos dois.
|
|
735
|
-
|
|
736
|
-
O player é a fonte: ele sabe encadear cenas, narrar no tempo certo e
|
|
737
|
-
tocar os efeitos. O exportador só pergunta o que está na tela. */
|
|
738
|
-
function gerarArquivo(op) {
|
|
739
|
-
op = op || {};
|
|
740
|
-
if (typeof VideoExport === "undefined" || !VideoExport.disponivel()) {
|
|
741
|
-
return Promise.reject(new Error("este navegador não sabe gerar vídeo"));
|
|
742
|
-
}
|
|
743
|
-
var fonte = {
|
|
744
|
-
duracao: total(),
|
|
745
|
-
// A gravação do usuário precisa estar decodificada antes do primeiro
|
|
746
|
-
// quadro; sem isso ela sairia do arquivo final, e só nele.
|
|
747
|
-
preparar: function () { return clipesProntos(); },
|
|
748
|
-
comecar: function () { buscar(0); tocar(); },
|
|
749
|
-
parar: function () { pausar(); },
|
|
750
|
-
no: function () { return palco.children[atual] || null; },
|
|
751
|
-
};
|
|
752
|
-
// O AudioContext nasce suspenso sem gesto; a exportação escondida chega
|
|
753
|
-
// com um gesto emprestado, e este resume é o que o gasta.
|
|
754
|
-
if (trilha && trilha.ctx && trilha.ctx.state === "suspended") { try { trilha.ctx.resume(); } catch (e) {} }
|
|
755
|
-
return VideoExport.gerar(arq, {
|
|
756
|
-
fonte: fonte,
|
|
757
|
-
fps: op.fps,
|
|
758
|
-
fluxoAudio: trilha && trilha.fluxo,
|
|
759
|
-
aoAndar: op.aoAndar,
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function iniciarGravacao() {
|
|
764
|
-
var b = document.getElementById("tp-gravar");
|
|
765
|
-
if (!b || b.dataset.exportando) return;
|
|
766
|
-
if (typeof VideoExport === "undefined" || !VideoExport.disponivel()) {
|
|
767
|
-
return aviso("este navegador não sabe gerar vídeo", 3200);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
b.dataset.exportando = "1";
|
|
771
|
-
b.classList.add("gravando");
|
|
772
|
-
aviso("montando o vídeo — não feche esta aba", 3000);
|
|
773
|
-
|
|
774
|
-
gerarArquivo({
|
|
775
|
-
aoAndar: function (f) { b.textContent = Math.round(f * 100) + "%"; },
|
|
776
|
-
}).then(function (r) {
|
|
777
|
-
VideoExport.salvar(r, arq.titulo);
|
|
778
|
-
aviso(r.mp4 ? "vídeo salvo em .mp4" : "salvo em .webm (este navegador não faz mp4)", 3400);
|
|
779
|
-
}).catch(function (e) {
|
|
780
|
-
aviso("não consegui gerar: " + e.message, 4200);
|
|
781
|
-
}).then(function () {
|
|
782
|
-
delete b.dataset.exportando;
|
|
783
|
-
b.classList.remove("gravando");
|
|
784
|
-
b.textContent = "Baixar ▾";
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
function pararGravacao() {
|
|
790
|
-
if (gravador && gravador.state === "recording") gravador.stop();
|
|
791
|
-
if (gravadorAudio && gravadorAudio.state === "recording") gravadorAudio.stop();
|
|
792
|
-
var b = document.getElementById("tp-gravar");
|
|
793
|
-
if (b) { b.classList.remove("gravando"); b.textContent = "Baixar ▾"; }
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function baixarGravacao() {
|
|
797
|
-
var mime = (gravador && gravador.mimeType) || "video/webm";
|
|
798
|
-
var ext = /mp4/.test(mime) ? ".mp4" : ".webm";
|
|
799
|
-
var blob = new Blob(pedacos, { type: mime.split(";")[0] });
|
|
800
|
-
var url = URL.createObjectURL(blob);
|
|
801
|
-
var a = document.createElement("a");
|
|
802
|
-
a.href = url;
|
|
803
|
-
a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() + ext;
|
|
804
|
-
a.click();
|
|
805
|
-
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
806
|
-
aviso("vídeo salvo em " + ext, 2600);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/* -------------------------------------------------------------- print -- */
|
|
810
|
-
|
|
811
|
-
function imprimirTudo() {
|
|
812
|
-
palco.style.cssText = "position:static;transform:none;width:auto;height:auto";
|
|
813
|
-
Array.prototype.forEach.call(palco.children, function (n) {
|
|
814
|
-
n.style.display = "block";
|
|
815
|
-
n.style.position = "relative";
|
|
816
|
-
n.style.inset = "auto";
|
|
817
|
-
n.classList.remove("animar");
|
|
818
|
-
});
|
|
819
|
-
var p = document.getElementById("progresso");
|
|
820
|
-
if (p) p.remove();
|
|
821
|
-
if (transporte) transporte.remove();
|
|
822
|
-
window.print();
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/* ------------------------------------------------------------ eventos -- */
|
|
826
|
-
|
|
827
|
-
window.addEventListener("resize", ajustar);
|
|
828
|
-
|
|
829
|
-
document.addEventListener("keydown", function (ev) {
|
|
830
|
-
if (ev.metaKey || ev.ctrlKey || /input|textarea/i.test(ev.target.tagName)) return;
|
|
831
|
-
switch (ev.key) {
|
|
832
|
-
case "ArrowRight": case " ": case "PageDown":
|
|
833
|
-
ev.preventDefault(); if (ehVideo) pausar(); proxima(); break;
|
|
834
|
-
case "ArrowLeft": case "PageUp":
|
|
835
|
-
ev.preventDefault(); if (ehVideo) pausar(); anterior(); break;
|
|
836
|
-
case "Home": pausar(); ir(0); break;
|
|
837
|
-
case "End": pausar(); ir(cenas.length - 1); break;
|
|
838
|
-
case "k": case "K": tocando ? pausar() : tocar(); break;
|
|
839
|
-
case "m": case "M": alternarMudo(); break;
|
|
840
|
-
case "s": case "S": alternarApresentador(); break;
|
|
841
|
-
case "t": case "T": cronometro = 0; break;
|
|
842
|
-
case "n": case "N":
|
|
843
|
-
if (typeof Narracao !== "undefined" && Narracao.disponivel()) {
|
|
844
|
-
Narracao.falando() ? Narracao.parar() : narrar();
|
|
845
|
-
}
|
|
846
|
-
break;
|
|
847
|
-
case "f": case "F":
|
|
848
|
-
if (document.fullscreenElement) document.exitFullscreen();
|
|
849
|
-
else document.documentElement.requestFullscreen();
|
|
850
|
-
break;
|
|
851
|
-
case "p": case "P": window.print(); break;
|
|
852
|
-
case "Escape": pausar(); pararGravacao(); break;
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
var tx = null;
|
|
857
|
-
document.addEventListener("touchstart", function (e) { tx = e.touches[0].clientX; }, { passive: true });
|
|
858
|
-
document.addEventListener("touchend", function (e) {
|
|
859
|
-
if (tx == null) return;
|
|
860
|
-
var d = e.changedTouches[0].clientX - tx;
|
|
861
|
-
if (Math.abs(d) > 50) { pausar(); d < 0 ? proxima() : anterior(); }
|
|
862
|
-
tx = null;
|
|
863
|
-
}, { passive: true });
|
|
864
|
-
|
|
865
|
-
document.addEventListener("click", function (e) {
|
|
866
|
-
if (!cenas.length || e.target.closest("a,button,input,.transporte")) return;
|
|
867
|
-
if (ehVideo) { tocando ? pausar() : tocar(); return; }
|
|
868
|
-
e.clientX < window.innerWidth * 0.3 ? anterior() : proxima();
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
window.addEventListener("message", function (e) {
|
|
872
|
-
var m = e.data || {};
|
|
873
|
-
if (m.tipo === "arquivo") montar(m.arquivo);
|
|
874
|
-
if (m.tipo === "ir") { pausar(); ir(m.n); }
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
/* --------------------------------------------------------------- carga */
|
|
878
|
-
|
|
879
|
-
var embutido = document.getElementById("dados");
|
|
880
|
-
if (embutido) {
|
|
881
|
-
montar(JSON.parse(embutido.textContent));
|
|
882
|
-
} else {
|
|
883
|
-
var id = location.pathname.split("/").pop();
|
|
884
|
-
// lido antes de montar: ir() reescreve o hash e não deve pausar a reprodução
|
|
885
|
-
var inicio = parseInt((location.hash.match(/^#\/(\d+)/) || [])[1], 10);
|
|
886
|
-
fetch("/api/arquivos/" + id)
|
|
887
|
-
.then(function (r) { return r.json(); })
|
|
888
|
-
.then(function (a) {
|
|
889
|
-
if (a.erro) { palco.innerHTML = '<div style="color:#888;padding:40px">Arquivo não encontrado.</div>'; return; }
|
|
890
|
-
montar(a);
|
|
891
|
-
if (!isNaN(inicio) && inicio > 1) { pausar(); ir(inicio - 1); }
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
window.Player = {
|
|
896
|
-
ir: function (n) { ir(n - 1); }, proxima: proxima, anterior: anterior,
|
|
897
|
-
tocar: tocar, parar: pausar, buscar: buscar, gravar: alternarGravacao,
|
|
898
|
-
get cena() { return atual + 1; }, get total() { return cenas.length; },
|
|
899
|
-
// Para a exportação escondida: espera o arquivo montado, troca o que
|
|
900
|
-
// precisar (um clipe convertido) e gera pelo mesmo caminho do botão.
|
|
901
|
-
carregado: carregado, remontar: montar, exportar: gerarArquivo,
|
|
902
|
-
};
|
|
903
|
-
})();
|