Moraviq Voice para desenvolvedoresAPI v2

SDK JavaScript

@moraviq/voice-web: módulo ES sem dependência, com tipos TypeScript. Só áudio, com o WebRTC do navegador.

npm install @moraviq/voice-web     # piloto: registro do GitHub Packages, escopo @moraviq

Configurar

import { VoiceClient } from '@moraviq/voice-web';

const voz = new VoiceClient({
  baseUrl: 'https://api.voice.moraviq.com.br',     // sandbox: o endereço do sandbox
  // O seu backend devolve o token de aparelho da pessoa logada.
  getToken: async () => {
    const r = await fetch('/api/voz/token', { method: 'POST', cache: 'no-store' });
    if (!r.ok) throw new Error('token');
    return (await r.json()).token;
  },
});

Portaria: entrar na chamada que o seu backend criou

// no clique de Ligar (o microfone é pedido aqui)
const { call_id } = await (await fetch('/api/portaria/chamar', { method: 'POST', body })).json();
const chamada = await voz.attachOutgoing(call_id, { audioElement: document.querySelector('audio') });

chamada.addEventListener('state', () => mostrar(chamada.status, chamada.answeredBy));
chamada.addEventListener('ended', (e) => fim(e.detail.status, e.detail.reason));
chamada.setMuted(true);
await chamada.hangup();
await chamada.setDisposition('allowed');   // informação para o seu sistema; não abre portão

Entre em até 10 s depois de criada, senão a chamada termina com origin_gone.

Morador: receber e atender

morador.jsno repositório
// Morador na web com o SDK JS (v2): registra o aparelho, fica no WSS e atende.
// ?usuario=res_77 (ext_id do morador no sistema do parceiro)
// ?auto=1 atende sozinho (robô dos testes); ?relay=1 força TURN.
// Escreve window.__voiceResult no fim, para o teste ponta a ponta.
import { VoiceClient } from '/sdk/web/moraviq-voice.js';

const q = new URLSearchParams(location.search);
const usuario = q.get('usuario') || 'res_77';
const auto = q.get('auto') === '1';
const relay = q.get('relay') === '1';
const $ = (id) => document.getElementById(id);
window.__voiceReady = false;
window.__voiceResult = null;

// installation_id: um por navegador. Fica no localStorage (não é segredo).
let inst = null;
try { inst = localStorage.getItem('voz.installation_id'); } catch (_) { /* aba privada */ }
if (!inst) {
  inst = `web-${crypto.randomUUID()}`;
  try { localStorage.setItem('voz.installation_id', inst); } catch (_) { /* segue sem guardar */ }
}

const config = await (await fetch('/api/config')).json();
const client = new VoiceClient({
  baseUrl: config.baseUrl,
  getToken: async () => {
    const r = await fetch('/api/voz/token', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, cache: 'no-store',
      body: JSON.stringify({ papel: 'morador', usuario, installation_id: inst }),
    });
    if (!r.ok) throw new Error('token');
    return (await r.json()).token;
  },
});

client.addEventListener('connectionStateChanged', (e) => { $('conexao').textContent = `conexão: ${e.detail.state}`; });

let atual = null;
let ultimas = null;
let medidor = null;

function fim(status, motivo) {
  clearInterval(medidor);
  $('tocando').hidden = true;
  $('falando').hidden = true;
  if (!window.__voiceResult) window.__voiceResult = { status, endReason: motivo, ...(ultimas || {}) };
}

async function atender() {
  const c = atual;
  try {
    await c.accept({ audioElement: $('audio'), forceRelay: relay });
  } catch (e) {
    fim(`erro_${e.code || 'atender'}`);
    return;
  }
  $('tocando').hidden = true;
  $('falando').hidden = false;
  // As métricas somem quando a conexão fecha: guarda a última a cada segundo.
  medidor = setInterval(async () => {
    if (c.pc) ultimas = await c.stats();
    $('estado-chamada').textContent = `${c.status}${ultimas ? `, ${ultimas.selectedCandidateType || '?'}` : ''}`;
  }, 1000);
}

client.addEventListener('incomingCall', (e) => {
  atual = e.detail.incoming;
  const posto = e.detail.call && e.detail.call.caller && e.detail.call.caller.station_label;
  $('quem').textContent = posto || 'Portaria';
  $('tocando').hidden = false;
  atual.addEventListener('ended', async (ev) => fim(ev.detail.status, ev.detail.reason));
  if (auto) atender();
});

$('atender').onclick = atender;
$('recusar').onclick = () => atual && atual.reject();
$('desligar').onclick = () => atual && atual.hangup();

await client.registerDevice({ installationId: inst, platform: 'web', pushType: 'none' });
await client.connect();
window.__voiceReady = true;

Eventos do cliente

Eventodetail
incomingCall{callId, call, incoming}; atenda com incoming.accept()
callStateChanged{callId, status, call, handle}
callEnded{callId, status, endReason, call, handle}
takenElsewhere{callId}
connectionStateChanged{state}: idle, connecting, open, reconnecting, closed
error{code}: mic_denied, token_failed, not_allowed, taken_elsewhere, attendant_busy, ...

Cada chamada (OutgoingCall, IncomingCall) emite state, ended, media, ice, ice-restart, track, autoplay-blocked (mostre um botão que chame audio.play()) e error. Métricas: await chamada.stats() (tipo de candidato, RTT, jitter, perda).

Outros métodos

Cabeçalhos do seu site