@bravophone/webphone 0.1.1 → 0.2.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 +49 -3
- package/dist/bravophone.mjs +359 -287
- package/dist/bravophone.umd.js +43 -7
- package/host/_locales/pt_BR/messages.json +1430 -0
- package/host/allowed-origins.json +8 -0
- package/host/css/bravophoneContactDialer.css +385 -0
- package/host/css/dark-theme.css +116 -0
- package/host/favicon.ico +0 -0
- package/host/fonts/Audiowide-Regular.ttf +0 -0
- package/host/fonts/Seguiemj.ttf +0 -0
- package/host/images/answer-gray.png +0 -0
- package/host/images/answer.png +0 -0
- package/host/images/auto-answer-off.png +0 -0
- package/host/images/auto-answer-on.png +0 -0
- package/host/images/avatar.png +0 -0
- package/host/images/bravophone-logo.svg +39 -0
- package/host/images/bravotech_Bravotech_logo.png +0 -0
- package/host/images/circle-green.png +0 -0
- package/host/images/circle-orange.png +0 -0
- package/host/images/circle-red.png +0 -0
- package/host/images/dialer-button-down.png +0 -0
- package/host/images/dialer-button.png +0 -0
- package/host/images/gateways/activecampaign.png +0 -0
- package/host/images/gateways/agendor.png +0 -0
- package/host/images/gateways/appfacilita.png +0 -0
- package/host/images/gateways/call-in.png +0 -0
- package/host/images/gateways/click-to-call.png +0 -0
- package/host/images/gateways/clickup.png +0 -0
- package/host/images/gateways/exactsales.png +0 -0
- package/host/images/gateways/go-high-lvl.png +0 -0
- package/host/images/gateways/groner.png +0 -0
- package/host/images/gateways/hubspot.png +0 -0
- package/host/images/gateways/kommo.png +0 -0
- package/host/images/gateways/moskit.png +0 -0
- package/host/images/gateways/nectar.png +0 -0
- package/host/images/gateways/pipedrive.png +0 -0
- package/host/images/gateways/pipefy.png +0 -0
- package/host/images/gateways/piperun.png +0 -0
- package/host/images/gateways/ploomes.png +0 -0
- package/host/images/gateways/rdstation-crm.png +0 -0
- package/host/images/gateways/salesforce.png +0 -0
- package/host/images/gateways/sharpspring.png +0 -0
- package/host/images/gateways/sunhub.png +0 -0
- package/host/images/gateways/vendfly.png +0 -0
- package/host/images/gateways/zendesk.png +0 -0
- package/host/images/gateways/zoho.png +0 -0
- package/host/images/hangup.png +0 -0
- package/host/images/hold.png +0 -0
- package/host/images/icons/logo-128-christmas-celebration.png +0 -0
- package/host/images/icons/logo-128.png +0 -0
- package/host/images/icons/logo-16-christmas-celebration.png +0 -0
- package/host/images/icons/logo-16.png +0 -0
- package/host/images/icons/logo-24-christmas-celebration.png +0 -0
- package/host/images/icons/logo-24.png +0 -0
- package/host/images/icons/logo-32-christmas-celebration.png +0 -0
- package/host/images/icons/logo-32.png +0 -0
- package/host/images/icons/logo-48-christmas-celebration.png +0 -0
- package/host/images/icons/logo-48.png +0 -0
- package/host/images/loading.gif +0 -0
- package/host/images/logo-bravotech-white.png +0 -0
- package/host/images/logo-bravotech.png +0 -0
- package/host/images/logo-inline-christmas-celebration.png +0 -0
- package/host/images/logo-inline.png +0 -0
- package/host/images/mic-off.png +0 -0
- package/host/images/mic-on.png +0 -0
- package/host/images/play.png +0 -0
- package/host/images/transfer.png +0 -0
- package/host/index.html +34 -0
- package/host/js/bravophone-noise-suppressor.js +418 -0
- package/host/js/bravophone-route-selector.js +1099 -0
- package/host/js/libwebphone.js +20 -0
- package/host/js/noise/LICENSE-web-noise-suppressor.txt +21 -0
- package/host/js/noise/rnnoise-worklet.js +13 -0
- package/host/js/noise/rnnoise.wasm +0 -0
- package/host/js/noise/rnnoise_simd.wasm +0 -0
- package/host/mock-webphone.js +195 -0
- package/host/mock.html +54 -0
- package/host/popup.js +23 -0
- package/host/shim/chrome-shim.js +287 -0
- package/host/shim/guest-bridge.js +353 -0
- package/host/shim/messages.js +13 -0
- package/host/styles/theme-fixes.css +92 -0
- package/package.json +4 -2
- package/types/index.d.ts +21 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/host/index.html
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="referrer" content="strict-origin">
|
|
7
|
+
<title>Webphone BRAVOPHONE</title>
|
|
8
|
+
<link rel="icon" href="./favicon.ico">
|
|
9
|
+
<link rel="stylesheet" href="./css/dark-theme.css">
|
|
10
|
+
<!-- Correções de tema que o dark-theme.css da extensão não cobre.
|
|
11
|
+
Fica em styles/ (e não em css/) porque o sync apaga css/ inteiro. -->
|
|
12
|
+
<link rel="stylesheet" href="./styles/theme-fixes.css">
|
|
13
|
+
<style>
|
|
14
|
+
html, body { margin: 0; height: 100%; background: #1e1e2d; overflow: hidden; }
|
|
15
|
+
#app { height: 100%; }
|
|
16
|
+
</style>
|
|
17
|
+
|
|
18
|
+
<!-- ORDEM IMPORTA: o shim precisa existir antes de qualquer código que
|
|
19
|
+
toque em chrome.* durante a avaliação do bundle. -->
|
|
20
|
+
<!-- Mensagens de _locales/: alimentam chrome.i18n.getMessage, de onde vem
|
|
21
|
+
praticamente todo label da interface. Precisa vir antes do popup.js. -->
|
|
22
|
+
<script src="./shim/messages.js"></script>
|
|
23
|
+
<script src="./shim/chrome-shim.js"></script>
|
|
24
|
+
<script src="./js/libwebphone.js"></script>
|
|
25
|
+
<script src="./js/bravophone-route-selector.js"></script>
|
|
26
|
+
<script defer src="./popup.js"></script>
|
|
27
|
+
<!-- guest-bridge roda depois do app montar (defer preserva a ordem). -->
|
|
28
|
+
<script defer src="./shim/guest-bridge.js"></script>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<noscript><strong>O webphone precisa de JavaScript habilitado.</strong></noscript>
|
|
32
|
+
<div id="app"></div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* BravoPhone > Supressor de ruido de fundo (AudioWorklet + RNNoise/WASM)
|
|
3
|
+
*
|
|
4
|
+
* Onde entra no caminho do audio
|
|
5
|
+
* ------------------------------
|
|
6
|
+
* O microfone nunca passava por Web Audio: o libwebphone pega o stream do
|
|
7
|
+
* getUserMedia e entrega direto pro JsSIP. O funil e o _createCallStream(),
|
|
8
|
+
* em js/libwebphone.js, que clona as tracks e devolve o MediaStream usado como
|
|
9
|
+
* `mediaStream` no call()/answer(). E la que este modulo se enfia:
|
|
10
|
+
*
|
|
11
|
+
* getUserMedia -> [source -> rnnoise worklet -> destination] -> JsSIP
|
|
12
|
+
*
|
|
13
|
+
* Contrato com quem chama
|
|
14
|
+
* -----------------------
|
|
15
|
+
* process() e SINCRONO (o _createCallStream tambem e) e NUNCA lanca. Se o
|
|
16
|
+
* supressor nao estiver pronto, estiver desligado, ou qualquer coisa falhar,
|
|
17
|
+
* ele devolve o stream original intacto. Ruido no audio e um incomodo; chamada
|
|
18
|
+
* que nao completa e um defeito. Na duvida, passa reto.
|
|
19
|
+
*
|
|
20
|
+
* Por isso o carregamento e feito uma vez, no boot do popup (init), e nao na
|
|
21
|
+
* hora da ligacao: compilar o WASM e registrar o worklet sao operacoes async
|
|
22
|
+
* que nao cabem dentro do _createCallStream.
|
|
23
|
+
*
|
|
24
|
+
* 48 kHz nao e negociavel
|
|
25
|
+
* -----------------------
|
|
26
|
+
* O RNNoise processa quadros de 480 amostras = 10 ms a 48 kHz, e o worklet nao
|
|
27
|
+
* reamostra. O AudioContext daqui e criado com sampleRate: 48000, separado do
|
|
28
|
+
* AudioContext de saida do libwebphone (que e forcado a 44100 no Chrome). Se o
|
|
29
|
+
* navegador nao entregar 48 kHz, o supressor se desliga em vez de processar em
|
|
30
|
+
* taxa errada — o resultado seria voz acelerada/grave, muito pior que o ruido.
|
|
31
|
+
*
|
|
32
|
+
* Terceiros: js/noise/rnnoise-worklet.js e rnnoise*.wasm vem de
|
|
33
|
+
* @sapphi-red/web-noise-suppressor 0.3.5 (MIT), que empacota xiph/rnnoise.
|
|
34
|
+
* Licenca em js/noise/LICENSE-web-noise-suppressor.txt.
|
|
35
|
+
*/
|
|
36
|
+
(function () {
|
|
37
|
+
"use strict";
|
|
38
|
+
|
|
39
|
+
var LOG = "BravoPhone > NoiseSuppressor >";
|
|
40
|
+
|
|
41
|
+
/* Id sob o qual o processador se registra dentro do worklet. Vem do pacote
|
|
42
|
+
original; mudar aqui sem mudar o arquivo do worklet quebra o addModule. */
|
|
43
|
+
var WORKLET_ID = "@sapphi-red/web-noise-suppressor/rnnoise";
|
|
44
|
+
var WORKLET_PATH = "js/noise/rnnoise-worklet.js";
|
|
45
|
+
var WASM_PATH = "js/noise/rnnoise.wasm";
|
|
46
|
+
var WASM_SIMD_PATH = "js/noise/rnnoise_simd.wasm";
|
|
47
|
+
|
|
48
|
+
var STORAGE_KEY = "bravophoneNoiseSuppression";
|
|
49
|
+
var REQUIRED_SAMPLE_RATE = 48000;
|
|
50
|
+
|
|
51
|
+
var enabled = true; // ligado por padrao; storage sobrescreve no init
|
|
52
|
+
var ready = false;
|
|
53
|
+
var initPromise = null;
|
|
54
|
+
var ctx = null;
|
|
55
|
+
var wasmBinary = null;
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* O no do worklet e criado UMA vez, no boot, e reaproveitado em todas as
|
|
59
|
+
* chamadas. Motivo medido: o RNNoise instancia o wasm de forma assincrona
|
|
60
|
+
* dentro do worklet, entao criar o no na hora da ligacao custava ~29 ms de
|
|
61
|
+
* silencio no comeco da chamada. Criando no boot, esse custo acontece
|
|
62
|
+
* enquanto ninguem esta falando.
|
|
63
|
+
*
|
|
64
|
+
* O destino, ao contrario, precisa ser novo a cada chamada: o libwebphone
|
|
65
|
+
* chama track.stop() na track do stream que devolvemos, e uma track de
|
|
66
|
+
* MediaStreamAudioDestinationNode parada nao volta.
|
|
67
|
+
*/
|
|
68
|
+
var node = null;
|
|
69
|
+
|
|
70
|
+
/* Um grafo por vez. Cada process() derruba o anterior, entao uma ligacao que
|
|
71
|
+
troca de microfone no meio nao deixa worklet orfao girando. */
|
|
72
|
+
var active = null;
|
|
73
|
+
|
|
74
|
+
var stats = { processed: 0, bypassed: 0, autoBypass: 0, lastError: null };
|
|
75
|
+
|
|
76
|
+
/* ─── SIMD ────────────────────────────────────────────────────────────────
|
|
77
|
+
O build SIMD e bem mais rapido, mas so roda onde o navegador suporta. O
|
|
78
|
+
teste e o modulo minimo do wasm-feature-detect: se validar, tem SIMD. */
|
|
79
|
+
function hasSimd() {
|
|
80
|
+
try {
|
|
81
|
+
return WebAssembly.validate(new Uint8Array([
|
|
82
|
+
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
|
|
83
|
+
3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11
|
|
84
|
+
]));
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* A CSP da pagina precisa permitir WebAssembly.
|
|
92
|
+
*
|
|
93
|
+
* Este modulo so faz FETCH do .wasm; quem COMPILA e o worklet. E worklet
|
|
94
|
+
* herda a CSP do documento — mas falha calado: sob CSP restritiva ele nao
|
|
95
|
+
* lanca, nao dispara onprocessorerror, nao loga nada, so devolve zeros. O
|
|
96
|
+
* resultado e uma chamada com RTP saindo normalmente e ninguem ouvindo nada
|
|
97
|
+
* do outro lado. Foi exatamente isso que aconteceu em producao.
|
|
98
|
+
*
|
|
99
|
+
* A compilacao na thread principal e barrada pela mesma regra, entao esta
|
|
100
|
+
* sonda de tres bytes responde pelo worklet antes de prometer supressao.
|
|
101
|
+
*/
|
|
102
|
+
function wasmPermitido() {
|
|
103
|
+
try {
|
|
104
|
+
new WebAssembly.Module(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
|
|
105
|
+
return true;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function url(path) {
|
|
112
|
+
return (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.getURL)
|
|
113
|
+
? chrome.runtime.getURL(path)
|
|
114
|
+
: path;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readEnabledFromStorage() {
|
|
118
|
+
return new Promise(function (resolve) {
|
|
119
|
+
try {
|
|
120
|
+
if (typeof chrome === "undefined" || !chrome.storage || !chrome.storage.local) return resolve(true);
|
|
121
|
+
chrome.storage.local.get(STORAGE_KEY, function (got) {
|
|
122
|
+
void (chrome.runtime && chrome.runtime.lastError);
|
|
123
|
+
var v = got && got[STORAGE_KEY];
|
|
124
|
+
resolve(v === undefined || v === null ? true : !!v);
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
resolve(true);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ─── Carga unica ─────────────────────────────────────────────────────── */
|
|
133
|
+
function init() {
|
|
134
|
+
if (initPromise) return initPromise;
|
|
135
|
+
|
|
136
|
+
initPromise = readEnabledFromStorage()
|
|
137
|
+
.then(function (fromStorage) {
|
|
138
|
+
enabled = fromStorage;
|
|
139
|
+
|
|
140
|
+
if (!wasmPermitido()) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"WebAssembly bloqueado pela CSP desta pagina. Falta " +
|
|
143
|
+
"\"content_security_policy\": {\"extension_pages\": \"script-src 'self' 'wasm-unsafe-eval'; " +
|
|
144
|
+
"object-src 'self'\"} no manifest.json (e recarregar a extensao)."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ctx = new (window.AudioContext || window.webkitAudioContext)({
|
|
149
|
+
sampleRate: REQUIRED_SAMPLE_RATE,
|
|
150
|
+
latencyHint: "interactive"
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (ctx.sampleRate !== REQUIRED_SAMPLE_RATE) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"navegador entregou AudioContext a " + ctx.sampleRate +
|
|
156
|
+
" Hz; o RNNoise exige " + REQUIRED_SAMPLE_RATE
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var simd = hasSimd();
|
|
161
|
+
return Promise.all([
|
|
162
|
+
ctx.audioWorklet.addModule(url(WORKLET_PATH)),
|
|
163
|
+
fetch(url(simd ? WASM_SIMD_PATH : WASM_PATH)).then(function (r) {
|
|
164
|
+
if (!r.ok) throw new Error("wasm HTTP " + r.status);
|
|
165
|
+
return r.arrayBuffer();
|
|
166
|
+
})
|
|
167
|
+
]).then(function (out) {
|
|
168
|
+
wasmBinary = out[1];
|
|
169
|
+
|
|
170
|
+
node = new AudioWorkletNode(ctx, WORKLET_ID, {
|
|
171
|
+
processorOptions: { maxChannels: 1, wasmBinary: wasmBinary.slice(0) }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/* O wasm so termina de instanciar quando a thread de audio roda, e
|
|
175
|
+
ela nao roda com o contexto suspenso. Tenta agora (costuma falhar
|
|
176
|
+
sem gesto) e, se falhar, no primeiro clique/tecla — bem antes de
|
|
177
|
+
existir uma ligacao, que exige clique de qualquer forma. */
|
|
178
|
+
resumeOnGesture();
|
|
179
|
+
|
|
180
|
+
ready = true;
|
|
181
|
+
console.info(LOG, "pronto", {
|
|
182
|
+
sampleRate: ctx.sampleRate,
|
|
183
|
+
simd: simd,
|
|
184
|
+
wasmBytes: wasmBinary.byteLength,
|
|
185
|
+
habilitado: enabled,
|
|
186
|
+
estadoDoContexto: ctx.state
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
})
|
|
190
|
+
.catch(function (err) {
|
|
191
|
+
ready = false;
|
|
192
|
+
stats.lastError = err && err.message ? err.message : String(err);
|
|
193
|
+
/* Falhar aqui e aceitavel: process() passa o stream reto e a ligacao
|
|
194
|
+
acontece sem supressao. Erro no console pra nao virar silencio. */
|
|
195
|
+
console.error(LOG, "indisponivel — chamadas seguem sem supressao:", stats.lastError);
|
|
196
|
+
if (ctx) { try { ctx.close(); } catch (e) {} ctx = null; }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return initPromise;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ─── Contexto ────────────────────────────────────────────────────────── */
|
|
203
|
+
function resumeOnGesture() {
|
|
204
|
+
if (!ctx) return;
|
|
205
|
+
if (ctx.state !== "suspended") return;
|
|
206
|
+
ctx.resume().catch(function () {});
|
|
207
|
+
var once = function () {
|
|
208
|
+
if (ctx) ctx.resume().catch(function () {});
|
|
209
|
+
document.removeEventListener("pointerdown", once, true);
|
|
210
|
+
document.removeEventListener("keydown", once, true);
|
|
211
|
+
};
|
|
212
|
+
document.addEventListener("pointerdown", once, true);
|
|
213
|
+
document.addEventListener("keydown", once, true);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ─── Vigia de silencio ───────────────────────────────────────────────────
|
|
217
|
+
* Cinto de seguranca para o modo de falha que ninguem ve: worklet vivo,
|
|
218
|
+
* track viva, RTP saindo — e zero audio dentro. Se o microfone tem energia
|
|
219
|
+
* e a saida do worklet nao tem, religa o microfone cru direto no destino.
|
|
220
|
+
* Perder a supressao e um incomodo; perder a voz e uma chamada perdida.
|
|
221
|
+
*/
|
|
222
|
+
var LIMIAR_ENTRADA = 0.01; // acima disso alguem esta claramente falando
|
|
223
|
+
var AMOSTRAS_COM_VOZ = 5; // quantos momentos de fala observar antes de julgar
|
|
224
|
+
|
|
225
|
+
function bypassEmChamada(motivo, medidas) {
|
|
226
|
+
if (!active || active.bypassed) return;
|
|
227
|
+
console.error(LOG, "sem audio na saida (" + motivo + ") — voltando pro microfone cru", medidas || {});
|
|
228
|
+
try { active.source.disconnect(node); } catch (e) {}
|
|
229
|
+
try { node.disconnect(active.destination); } catch (e) {}
|
|
230
|
+
try { active.source.connect(active.destination); } catch (e) {}
|
|
231
|
+
active.bypassed = true;
|
|
232
|
+
stats.autoBypass++;
|
|
233
|
+
if (active.vigia) { clearInterval(active.vigia); active.vigia = null; }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function armarVigia(source, destination) {
|
|
237
|
+
var entrada = ctx.createAnalyser(); entrada.fftSize = 1024;
|
|
238
|
+
var saida = ctx.createAnalyser(); saida.fftSize = 1024;
|
|
239
|
+
source.connect(entrada);
|
|
240
|
+
node.connect(saida);
|
|
241
|
+
|
|
242
|
+
/* Um AnalyserNode que nao chega a nenhum destino nao e renderizado: ele
|
|
243
|
+
simplesmente devolve zeros, e o vigia ficaria cego achando que esta
|
|
244
|
+
tudo em silencio. O dreno de ganho zero coloca os dois no caminho de
|
|
245
|
+
renderizacao sem produzir som nenhum. */
|
|
246
|
+
var dreno = ctx.createGain();
|
|
247
|
+
dreno.gain.value = 0;
|
|
248
|
+
entrada.connect(dreno);
|
|
249
|
+
saida.connect(dreno);
|
|
250
|
+
dreno.connect(ctx.destination);
|
|
251
|
+
|
|
252
|
+
var buf = new Float32Array(1024);
|
|
253
|
+
var pico = function (a) {
|
|
254
|
+
a.getFloatTimeDomainData(buf);
|
|
255
|
+
var p = 0;
|
|
256
|
+
for (var i = 0; i < buf.length; i++) { var v = Math.abs(buf[i]); if (v > p) p = v; }
|
|
257
|
+
return p;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (active) active.dreno = dreno;
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* O criterio NAO pode ser "saida baixa": o RNNoise derrubando um ventilador
|
|
264
|
+
* alto produz exatamente isso — entrada alta, saida quase zero — e eu
|
|
265
|
+
* desligaria a supressao justo quando ela esta funcionando.
|
|
266
|
+
*
|
|
267
|
+
* O que separa os dois casos e que um worklet que nunca rodou devolve zero
|
|
268
|
+
* ABSOLUTO, para sempre. Um worklet vivo, mesmo suprimindo com forca, uma
|
|
269
|
+
* hora entrega alguma amostra diferente de zero. Entao a pergunta e "esse
|
|
270
|
+
* worklet ja produziu audio alguma vez?", nao "esta baixo agora?".
|
|
271
|
+
*
|
|
272
|
+
* Assim que ele produz qualquer coisa, o vigia se aposenta: alem de nao
|
|
273
|
+
* haver mais o que vigiar, para de custar CPU pelo resto da chamada.
|
|
274
|
+
*/
|
|
275
|
+
var comVoz = 0;
|
|
276
|
+
var id = setInterval(function () {
|
|
277
|
+
if (!active || active.bypassed) return;
|
|
278
|
+
var pin = pico(entrada), pout = pico(saida);
|
|
279
|
+
stats.ultimoPicoEntrada = pin;
|
|
280
|
+
stats.ultimoPicoSaida = pout;
|
|
281
|
+
|
|
282
|
+
if (pout > 0) { // worklet vivo: nada a fazer
|
|
283
|
+
stats.workletProduziuAudio = true;
|
|
284
|
+
clearInterval(id);
|
|
285
|
+
if (active) active.vigia = null;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (pin <= LIMIAR_ENTRADA) return; // ninguem falando: nao julga
|
|
290
|
+
comVoz++;
|
|
291
|
+
stats.amostrasComVoz = comVoz;
|
|
292
|
+
if (comVoz >= AMOSTRAS_COM_VOZ) {
|
|
293
|
+
bypassEmChamada("worklet nunca produziu audio", {
|
|
294
|
+
picoEntrada: pin, amostrasComVoz: comVoz, estadoDoContexto: ctx.state
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}, 400);
|
|
298
|
+
return id;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ─── Grafo ───────────────────────────────────────────────────────────── */
|
|
302
|
+
function teardown() {
|
|
303
|
+
if (!active) return;
|
|
304
|
+
var a = active;
|
|
305
|
+
active = null;
|
|
306
|
+
if (a.vigia) { try { clearInterval(a.vigia); } catch (e) {} }
|
|
307
|
+
if (a.dreno) { try { a.dreno.disconnect(); } catch (e) {} }
|
|
308
|
+
try { a.source.disconnect(); } catch (e) {}
|
|
309
|
+
/* O no e reaproveitado entre chamadas: so desconecta, nunca destroy() —
|
|
310
|
+
destroy() libera o wasm e o proximo process() sairia mudo. Desconectado
|
|
311
|
+
da saida, ele sai do grafo de renderizacao e nao consome CPU. */
|
|
312
|
+
try { if (node) node.disconnect(); } catch (e) {}
|
|
313
|
+
/* As tracks de entrada eram clones feitos pelo _createCallStream. Quem
|
|
314
|
+
para o stream la fora para o stream de SAIDA, entao estas ficariam
|
|
315
|
+
vivas segurando o microfone se nao parassem aqui. */
|
|
316
|
+
try {
|
|
317
|
+
a.inputStream.getTracks().forEach(function (t) { try { t.stop(); } catch (e) {} });
|
|
318
|
+
} catch (e) {}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/*
|
|
322
|
+
* Recebe o MediaStream que iria pro JsSIP e devolve o processado.
|
|
323
|
+
* Sincrono e a prova de falha: qualquer problema devolve a entrada.
|
|
324
|
+
*/
|
|
325
|
+
function process(stream) {
|
|
326
|
+
try {
|
|
327
|
+
if (!enabled || !ready || !ctx || !node || !stream) { stats.bypassed++; return stream; }
|
|
328
|
+
if (stream.getAudioTracks().length === 0) { stats.bypassed++; return stream; }
|
|
329
|
+
|
|
330
|
+
teardown();
|
|
331
|
+
|
|
332
|
+
/* O contexto pode ter suspendido junto com o popup; a ligacao parte de um
|
|
333
|
+
clique, entao o resume aqui costuma ser aceito. Se nao for, o worklet
|
|
334
|
+
so volta a rodar quando o contexto acordar — sem quebrar nada. */
|
|
335
|
+
if (ctx.state === "suspended") ctx.resume().catch(function () {});
|
|
336
|
+
|
|
337
|
+
var source = ctx.createMediaStreamSource(stream);
|
|
338
|
+
var destination = ctx.createMediaStreamDestination();
|
|
339
|
+
|
|
340
|
+
source.connect(node);
|
|
341
|
+
node.connect(destination);
|
|
342
|
+
|
|
343
|
+
var out = new MediaStream();
|
|
344
|
+
destination.stream.getAudioTracks().forEach(function (t) { out.addTrack(t); });
|
|
345
|
+
/* Video esta desligado neste produto, mas se um dia entrar ele passa
|
|
346
|
+
reto em vez de sumir. */
|
|
347
|
+
stream.getVideoTracks().forEach(function (t) { out.addTrack(t); });
|
|
348
|
+
|
|
349
|
+
active = { source: source, destination: destination, inputStream: stream, bypassed: false, vigia: null };
|
|
350
|
+
/* Um erro dentro do processador tambem derruba o audio; ai o bypass e
|
|
351
|
+
imediato, sem esperar o vigia. */
|
|
352
|
+
node.onprocessorerror = function () { bypassEmChamada("onprocessorerror"); };
|
|
353
|
+
active.vigia = armarVigia(source, destination);
|
|
354
|
+
|
|
355
|
+
stats.processed++;
|
|
356
|
+
console.info(LOG, "supressao ativa na chamada", {
|
|
357
|
+
sampleRate: ctx.sampleRate, estadoDoContexto: ctx.state
|
|
358
|
+
});
|
|
359
|
+
return out;
|
|
360
|
+
} catch (err) {
|
|
361
|
+
stats.lastError = err && err.message ? err.message : String(err);
|
|
362
|
+
console.error(LOG, "falhou — seguindo sem supressao:", stats.lastError);
|
|
363
|
+
try { teardown(); } catch (e) {}
|
|
364
|
+
stats.bypassed++;
|
|
365
|
+
return stream;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
var api = {
|
|
370
|
+
/** Pre-aquece (AudioContext 48k + worklet + wasm). Idempotente. */
|
|
371
|
+
init: init,
|
|
372
|
+
/** true quando worklet e wasm ja estao carregados. */
|
|
373
|
+
isReady: function () { return ready; },
|
|
374
|
+
/** true quando o usuario nao desligou a supressao. */
|
|
375
|
+
isEnabled: function () { return enabled; },
|
|
376
|
+
/** Liga/desliga e persiste. Passa a valer na proxima chamada. */
|
|
377
|
+
setEnabled: function (v) {
|
|
378
|
+
enabled = !!v;
|
|
379
|
+
if (!enabled) teardown();
|
|
380
|
+
try {
|
|
381
|
+
var patch = {};
|
|
382
|
+
patch[STORAGE_KEY] = enabled;
|
|
383
|
+
chrome.storage.local.set(patch, function () { void (chrome.runtime && chrome.runtime.lastError); });
|
|
384
|
+
} catch (e) {}
|
|
385
|
+
console.info(LOG, enabled ? "ligado" : "desligado");
|
|
386
|
+
return enabled;
|
|
387
|
+
},
|
|
388
|
+
/** Chamado pelo _createCallStream. Sincrono, nunca lanca. */
|
|
389
|
+
process: process,
|
|
390
|
+
/** Derruba o grafo ativo (fim de chamada). */
|
|
391
|
+
stop: teardown,
|
|
392
|
+
/** Diagnostico rapido pelo console do popup. */
|
|
393
|
+
getStats: function () {
|
|
394
|
+
return {
|
|
395
|
+
pronto: ready,
|
|
396
|
+
habilitado: enabled,
|
|
397
|
+
sampleRate: ctx ? ctx.sampleRate : null,
|
|
398
|
+
estadoDoContexto: ctx ? ctx.state : null,
|
|
399
|
+
wasmPermitido: wasmPermitido(),
|
|
400
|
+
noPreAquecido: !!node,
|
|
401
|
+
grafoAtivo: !!active,
|
|
402
|
+
emBypassNestaChamada: !!(active && active.bypassed),
|
|
403
|
+
simd: hasSimd(),
|
|
404
|
+
processadas: stats.processed,
|
|
405
|
+
semSupressao: stats.bypassed,
|
|
406
|
+
bypassAutomatico: stats.autoBypass,
|
|
407
|
+
ultimoPicoEntrada: stats.ultimoPicoEntrada,
|
|
408
|
+
ultimoPicoSaida: stats.ultimoPicoSaida,
|
|
409
|
+
amostrasComVoz: stats.amostrasComVoz,
|
|
410
|
+
workletProduziuAudio: !!stats.workletProduziuAudio,
|
|
411
|
+
ultimoErro: stats.lastError
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
window.BravoPhoneNoiseSuppressor = api;
|
|
417
|
+
init();
|
|
418
|
+
})();
|