Criando Chamadas WebRTC com Asterisk em Docker

Quer permitir chamadas de voz diretamente pelo navegador usando WebRTC e integrá-las ao Asterisk? Neste post, vou te mostrar passo a passo como rodar o Asterisk dentro de um container Docker, configurar os transportes necessários e conectar um frontend WebRTC usando JavaScript (com JsSIP).

A ideia aqui é montar um ambiente completo, mas simples o suficiente para você testar localmente ou em um servidor de homologação. Vamos começar configurando o Asterisk no Docker.

🐳Subindo o Asterisk com Docker (com WebRTC)

Para ter controle total sobre os módulos e dependências do Asterisk — incluindo suporte ao WebRTC e ao codec Opus — usaremos uma imagem personalizada baseada no Debian Bookworm.

📦Dockerfile

Crie um arquivo Dockerfile com o conteúdo abaixo:

Dockerfile
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
    wget curl build-essential \
    libedit-dev uuid-dev libjansson-dev libxml2-dev \
    unixodbc unixodbc-dev odbc-postgresql libpq-dev \
    && apt-get clean

WORKDIR /usr/src
RUN wget http://downloads.asterisk.org/pub/telephony/asterisk/asterisk-20-current.tar.gz \
    && tar xvf asterisk-20-current.tar.gz \
    && cd asterisk-20.* \
    && contrib/scripts/install_prereq install \
    && ./configure --with-pjproject-bundled --with-jansson-bundled \
    && make menuselect.makeopts \
    && menuselect/menuselect --enable codec_opus --enable res_odbc --enable cdr_adaptive_odbc --enable res_config_odbc menuselect.makeopts \
    && make -j$(nproc) && make install && make samples && make config && ldconfig

RUN mkdir -p /opt/asterisk-config

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"]

🔁Script de inicialização

Crie um arquivo entrypoint.sh ao lado do seu Dockerfile:

Bash
#!/bin/bash

CONFIG_SRC="/opt/asterisk-config"
CONFIG_DEST="/etc/asterisk"

cp -r "$CONFIG_SRC/"* "$CONFIG_DEST/"

exec asterisk -vvv -f

🛠️ Build e execução do container

Agora, para construir e rodar o container:

Bash
docker build -t asterisk-webrtc .

🌐 Criando uma rede Docker dedicada

Antes de usarmos o Docker Compose, é uma boa prática criar uma rede personalizada para isolar e facilitar a comunicação entre os serviços da stack (como o Asterisk e, futuramente, um possível backend ou frontend).

Execute o seguinte comando para criar uma rede Docker chamada asterisk-webrtc-network:

Bash
docker network create asterisk-webrtc-network

📦 Usando Docker Compose

Agora, vamos substituir o comando manual de docker run por um docker-compose.yml, que facilita o gerenciamento da stack como um todo.

Crie um arquivo docker-compose.yml com o seguinte conteúdo:

YAML
name: asterisk-webrtc

services:
  asterisk:
    image: asterisk-webrtc # Sua image local
    container_name: asterisk
    ports:
      - "8089:8089"   # WebSocket TLS (WSS)
      - "8088:8088"   # WebSocket (WS) ou HTTP API
    volumes:
      - ./asterisk:/opt/asterisk-config
      - ./etc/odbc.ini:/etc/odbc.ini
      - ./etc/odbcinst.ini:/etc/odbcinst.ini
    networks:
      - voip-asterisk

networks:
  voip-asterisk:
    external: true

💡 Importante: Certifique-se de que o diretório ./asterisk contenha os arquivos de configuração como pjsip.conf, http.conf, etc., e que os arquivos odbc.ini e odbcinst.ini estejam na pasta ./etc.

⚙️ 2. Configurações do Asterisk para WebRTC

Agora que o container está rodando, precisamos configurar o Asterisk para suportar chamadas WebRTC, armazenamento de CDR (Call Detail Records), comunicação via WebSocket, entre outros pontos essenciais.

Vamos começar com os arquivos de configuração um a um. Todos devem ser colocados no diretório ./asterisk, que é montado dentro do container em /opt/asterisk-config.

📄 cdr_adaptive_odbc.conf

Este arquivo configura o Asterisk para gravar os registros de chamadas (CDR) diretamente em um banco de dados via ODBC. Neste caso, usamos uma conexão chamada asterisk que deve ser definida em res_odbc.conf.

INI
[asterisk]
connection=asterisk
table=cdr
  • connection=asterisk: nome da conexão ODBC (deve bater com res_odbc.conf)
  • table=cdr: nome da tabela onde os registros de chamadas serão gravados

💡 Essa configuração é opcional para chamadas WebRTC, mas recomendada em ambientes de produção para registrar todas as ligações.

📄 extconfig.conf

Este arquivo define que algumas configurações do Asterisk devem ser carregadas dinamicamente de um banco de dados via ODBC. Isso é útil para gerenciar endpoints SIP (usuários) de forma centralizada.

INI
[settings]
ps_endpoints => odbc,asterisk
ps_auths => odbc,asterisk
ps_aors => odbc,asterisk
  • ps_endpoints, ps_auths, ps_aors: são partes do módulo PJSIP, que define usuários, autenticações e associações.
  • odbc,asterisk: significa que os dados virão de uma fonte ODBC chamada asterisk (configurada em res_odbc.conf).

✅ Essencial para ambientes onde os dados de usuários são armazenados no banco, como sistemas multiusuário ou integrações com backends web.

📄 extensions.conf

Este é o dialplan do Asterisk — a “lógica de chamadas”. No exemplo abaixo, estamos criando um contexto chamado webrtc, que atende qualquer número (_.) e tenta discar para o endpoint SIP correspondente.

  • _.: coringa que corresponde a qualquer número discado.
  • Dial(PJSIP/${EXTEN},60): tenta estabelecer a chamada com o endpoint SIP identificado por ${EXTEN}, com timeout de 60 segundos.
  • Hangup(): encerra a chamada caso o dial falhe ou termine.

⚠️ Certifique-se de que os usuários/dispositivos WebRTC estejam registrados com nomes que combinem com os valores que você vai discar (ex: ramais como 1001, alice, etc.).

📄 http.conf

Este arquivo ativa o servidor HTTP interno do Asterisk e também o suporte a WebSocket (WS/WSS), necessário para que navegadores possam se conectar via WebRTC.

INI
[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8088

tlsenable=yes
tlsbindaddr=0.0.0.0:8089
tlscertfile=/etc/asterisk/keys/asterisk-local-cert.pem
tlsprivatekey=/etc/asterisk/keys/asterisk-local-key.pem
  • enabled=yes: ativa o servidor HTTP.
  • bindaddr=0.0.0.0 / bindport=8088: escuta requisições HTTP normais.
  • tlsenable=yes: habilita o suporte a HTTPS/WSS.
  • tlsbindaddr=0.0.0.0:8089: escuta conexões seguras.
  • tlscertfile e tlsprivatekey: certificados TLS para WebSocket seguro (WSS).

🔒 Importante: para funcionar em navegadores modernos, é obrigatório usar WSS (WebSocket seguro) com certificado válido — inclusive em ambientes de teste. Você pode gerar um autoassinado ou usar Let’s Encrypt.

📄 pjsip.conf

Este é um dos arquivos mais importantes — ele define os transportes, endpoints (usuários), autenticação e associação dos dispositivos SIP. Começamos pelos transportes:

INI
[transport-wss]
type=transport
protocol=wss
bind=0.0.0.0

[transport-ws]
type=transport
protocol=ws
bind=0.0.0.0
  • transport-wss: transporte seguro para WebRTC via WSS (WebSocket Secure). Usado pela maioria dos navegadores.
  • transport-ws: transporte não seguro para ambientes de desenvolvimento (WebSocket simples).
  • bind=0.0.0.0: escuta em todas as interfaces de rede do container.

⚠️ Para WebRTC em produção, use apenas o WSS. O transporte WS sem TLS é bloqueado por navegadores em conexões HTTPS.

📄 res_odbc.conf

Este arquivo configura a conexão ODBC que o Asterisk usará para acessar dados como CDRs e registros do PJSIP (quando integrados ao banco de dados).

INI
[asterisk]
enabled=yes
dsn=Asterisk-DB
username=postgres
password=postgres
pre-connect=yes
  • enabled=yes: ativa essa conexão.
  • dsn=Asterisk-DB: nome do DSN definido no odbc.ini.
  • username e password: credenciais de acesso ao banco de dados (exemplo: PostgreSQL).
  • pre-connect=yes: mantém a conexão ativa durante o tempo de execução.

🧠 Essa seção deve estar alinhada com a configuração no cdr_adaptive_odbc.conf e no extconfig.conf, que fazem referência à conexão asterisk.

📄 rtp.conf

Este arquivo configura o intervalo de portas que o Asterisk usará para o tráfego RTP (Real-time Transport Protocol), que é fundamental para o transporte de áudio nas chamadas.

INI
[general]
rtpstart=10000
rtpend=20000
  • rtpstart=10000: porta inicial do intervalo de portas RTP.
  • rtpend=20000: porta final do intervalo de portas RTP.

🔐 Dica: Certifique-se de que o firewall do seu servidor permita o tráfego nas portas que você definir para o RTP. Para WebRTC, é importante garantir que o tráfego UDP seja liberado para esse intervalo.

📄 sorcery.conf

Este arquivo configura o Asterisk para usar sorcery, que é uma estrutura para carregar configurações dinâmicas para os módulos do Asterisk a partir de fontes externas, como um banco de dados via ODBC.

INI
[res_pjsip]
endpoint=realtime,ps_endpoints
auth=realtime,ps_auths
aor=realtime,ps_aors
  • endpoint=realtime,ps_endpoints: define que os endpoints (usuários) serão carregados em tempo real de uma tabela no banco de dados (configurado em extconfig.conf).
  • auth=realtime,ps_auths: define que as informações de autenticação também virão de uma tabela externa.
  • aor=realtime,ps_aors: associações de objetos de recursos (AOR) serão obtidas dinamicamente.

💡 O uso de “realtime” significa que o Asterisk está configurado para buscar esses dados de um banco de dados em vez de um arquivo estático, proporcionando maior flexibilidade na gestão dos usuários e configurações.

⚙️ Configurações ODBC para PostgreSQL

Na raiz do seu projeto Docker, a pasta ./etc deve conter os arquivos:

  • odbc.ini: define a conexão com o banco (DSN).
  • odbcinst.ini: registra o driver PostgreSQL ODBC no sistema.

Esses arquivos serão montados no container e utilizados pelo Asterisk.

📄 odbc.ini

Este arquivo define o DSN (Data Source Name) usado pelo Asterisk para se conectar ao banco PostgreSQL:

INI
[Asterisk-DB]
Description=PostgreSQL Asterisk DB
Driver=PostgreSQL
Servername=postgres # Deve estar conectado a sua network com esse host
Database=asterisk
Username=postgres
Password=postgres
Port=5432
  • [Asterisk-DB]: nome do DSN (referenciado em res_odbc.conf).
  • Driver=PostgreSQL: nome do driver (deve bater com o odbcinst.ini).
  • Servername=postgres: nome do host ou container do PostgreSQL (se estiver no docker-compose, o nome do serviço).
  • Database=asterisk: nome do banco de dados.
  • Username e Password: credenciais do banco.
  • Port=5432: porta padrão do PostgreSQL.

📄 odbcinst.ini

Este arquivo registra o driver ODBC do PostgreSQL no sistema:

INI
[PostgreSQL]
Description=ODBC for PostgreSQL
Driver=/usr/lib/x86_64-linux-gnu/odbc/psqlodbcw.so

O caminho do driver (Driver) pode variar entre distribuições. No Debian (como usamos no Dockerfile), estes caminhos são comuns. Caso dê erro, use find /usr -name "*psql*.so" no container para confirmar.

Com isso, você conecta o Asterisk a um banco PostgreSQL com suporte a Realtime e CDRs via ODBC.

📞 Integração WebRTC com Asterisk usando JsSIP

Este código Vue + TypeScript define um serviço de chamadas VoIP usando a biblioteca JsSIP, que implementa o protocolo SIP sobre WebSockets.

🧩 Estrutura do Serviço SIP

O arquivo define a classe SIPService, que gerencia:

  • Registro SIP no Asterisk (via WebSocket seguro wss).
  • Recebimento e realização de chamadas.
  • Emissão de eventos para uso no Vue (como incomingCall, onCall, endedCall, etc).
  • Controle de mídia (mute, unmute, encerramento).

🔐 Registro SIP

TypeScript
this.user = "4000";
this.password = "4000";
  • O número de ramal (4000) é usado para registrar o cliente no Asterisk.
  • A conexão é feita via wss://localhost:8089/ws — porta 8089 corresponde ao WebSocket TLS configurado no http.conf.

⚙️ Configuração do Asterisk

O funcionamento depende de:

  • http.conf com tlsenable=yes, bindport=8089.
  • pjsip.conf com os transportes ws e wss.
  • extensions.conf para roteamento de chamadas (Dial(PJSIP/${EXTEN})).
  • sorcery.conf e extconfig.conf configurados para usar ODBC (realtime).

📡 Realizando uma chamada

TypeScript
sipService.makeCall("4001");
  • O método verifica se o usuário está registrado e usa o getUserMedia() para capturar áudio.
  • A chamada é feita para outro ramal SIP (4001), por exemplo.

📥 Atendendo uma chamada

TypeScript
sipService.answerCall();
  • Aceita uma chamada recebida.
  • Usa também o getUserMedia() para abrir o microfone.

🎙️ Eventos disparados

A classe emite eventos que podem ser usados nos componentes Vue:

  • "incomingCall": chamada recebida.
  • "outgoingCall": chamada sendo realizada.
  • "onCall": chamada ativa.
  • "endedCall": chamada encerrada.
  • "canceledCall" e "rejectedCall": cancelamentos/falhas.

🧪 Dicas para testar

  1. Use o console para instanciar e chamar métodos do serviço.
  2. Tenha dois navegadores diferentes (ou abas privadas) com ramais distintos registrados para testes de chamada.

✅ Próximos Passos

Com o Asterisk rodando com suporte completo a WebRTC e JsSIP integrado no frontend, você já pode:

  • Realizar chamadas entre navegadores
  • Conectar dispositivos SIP tradicionais com navegadores
  • Gravar CDRs em tempo real em banco de dados

Se quiser dar o próximo passo, considere:

  • Adicionar autenticação com backend
  • Usar STUN/TURN para suporte em NAT
  • Publicar o serviço com domínio e HTTPS via reverse proxy (ex: Traefik ou Nginx)

Melhorando a Testabilidade do Código com Factory e Adapter

A testabilidade do código é um fator essencial para garantir a qualidade e a manutenção do software. Quando lidamos com classes que não são interfaces ou que possuem dependências complexas, testar esses componentes pode se tornar um desafio. Neste post, vamos explorar como podemos melhorar a testabilidade do código usando dois padrões de projeto: Adapter e Factory.

O Problema

Suponha que temos uma classe Process, que representa um processo do sistema operacional. No entanto, essa classe não implementa uma interface e possui métodos que fazem chamadas diretas ao sistema, o que dificulta a substituição dessa dependência nos testes.

Para resolver esse problema, utilizaremos o padrão Adapter para encapsular a classe Process em uma interface testável e o padrão Factory para criar instâncias dessa interface de forma flexível.

Implementação do Adapter

A primeira etapa é criar uma interface para representar a funcionalidade do Process de forma genérica e testável:

C#
public interface IProcessAdapter
{
    void BeginErrorReadLine();
    void BeginOutputReadLine();
    void Close();
    void SetErrorDataReceived(Action<string?> action);
    void SetOutputDataReceived(Action<string?> action);
    void SetStartInfoArguments(string arguments);
    void SetStartInfoFileName(string fileName);
    void SetStartInfoRedirectStandardError(bool redirectStandardError);
    void SetStartInfoRedirectStandardOutput(bool redirectStandardOutput);
    void SetStartInfoUseShellExecute(bool useShellExecute);
    void SetStartInfoVerb(string verb);
    bool Start();
    Task WaitForExitAsync(CancellationToken cancellationToken = default);
}

Agora, criamos uma classe ProcessAdapter que implementa essa interface e encapsula a funcionalidade da classe Process:

C#
public sealed class ProcessAdapter : IProcessAdapter
{
    private readonly Process _process;
    private readonly ProcessStartInfo _processStartInfo;

    public ProcessAdapter()
    {
        _process = new Process();
        _processStartInfo = new ProcessStartInfo();
        _process.StartInfo = _processStartInfo;
    }

    public void BeginErrorReadLine() => _process.BeginErrorReadLine();
    public void BeginOutputReadLine() => _process.BeginOutputReadLine();
    public void Close() => _process.Close();
    public void SetErrorDataReceived(Action<string?> action) =>
        _process.ErrorDataReceived += (sender, args) => action(args.Data);
    public void SetOutputDataReceived(Action<string?> action) =>
        _process.OutputDataReceived += (sender, args) => action(args.Data);
    public void SetStartInfoArguments(string arguments) =>
        _processStartInfo.Arguments = arguments;
    public void SetStartInfoFileName(string fileName) =>
        _processStartInfo.FileName = fileName;
    public void SetStartInfoRedirectStandardError(bool redirectStandardError) =>
        _processStartInfo.RedirectStandardError = redirectStandardError;
    public void SetStartInfoRedirectStandardOutput(bool redirectStandardOutput) =>
        _processStartInfo.RedirectStandardOutput = redirectStandardOutput;
    public void SetStartInfoUseShellExecute(bool useShellExecute) =>
        _processStartInfo.UseShellExecute = useShellExecute;
    public void SetStartInfoVerb(string verb) =>
        _processStartInfo.Verb = verb;
    public bool Start() => _process.Start();
    public Task WaitForExitAsync(CancellationToken cancellationToken = default) =>
        _process.WaitForExitAsync(cancellationToken);
}

Implementação do Factory

Agora, para facilitar a criação e a substituição do ProcessAdapter nos testes, usamos um Factory para encapsular a instância:

C#
public interface IProcessAdapterFactory
{
    IProcessAdapter Create();
}

public sealed class ProcessAdapterFactory : IProcessAdapterFactory
{
    public IProcessAdapter Create() => new ProcessAdapter();
}

Benefícios da Abordagem

Com essa implementação, conseguimos:

  1. Facilitar a injeção de dependência: Podemos injetar IProcessAdapterFactory nos serviços, permitindo a criação flexível de processos.
  2. Melhorar a testabilidade: Nos testes, podemos substituir ProcessAdapter por um mock da interface IProcessAdapter, sem depender de processos reais do sistema operacional.
  3. Encapsular a complexidade: O ProcessAdapter encapsula detalhes internos da classe Process, proporcionando uma interface mais controlada e previsível.

Conclusão

Os padrões Adapter e Factory são ferramentas poderosas para melhorar a testabilidade e a flexibilidade do código. Adaptando classes que não seguem princípios SOLID para interfaces testáveis e utilizando fábricas para criar instâncias controladas, conseguimos construir um código mais modular, reutilizável e fácil de testar.

Se você já teve dificuldades em testar código acoplado a classes do sistema, experimente essa abordagem e veja a diferença!