Skip to main content
Uma chamada em grupo é uma chamada endereçada à própria chamada (<call-id>@call), e não a um peer, com um roster que o servidor mantém e empurra. Desde o @zapo-js/voip 1.4.0 o zapo consegue fazer uma, atender uma, acompanhar quem está nela e sair dela. Tudo da página de chamadas 1:1 continua valendo — mute, levantar a mão, reações, vídeo, screen share — com uma diferença que atravessa tudo: todo anúncio remoto nomeia o device de onde veio, porque existe mais de um peer.
Uma chamada em grupo precisa de um host de mídia capaz de carregá-la. O host de browser ainda não consegue: mídia em grupo exige legs de relay em UDP cru, que um browser não tem como abrir. Um host de mídia remota que não consegue carregar a chamada é reportado uma vez em voip_call_error.

Fazendo uma

O startCall recebe ou o formato 1:1 ou o formato de grupo, nunca os dois — são uma union discriminada, então o TypeScript recusa a mistura.
string
obrigatório
O grupo (…@g.us) a que esta chamada pertence. A presença dele é o que torna a oferta uma oferta de grupo; peerJid não é lido.
readonly CallOfferParticipant[]
obrigatório
Quem chamar, nós mesmos de fora. Cada um é { jid, devices? }: jid é a conta a chamar (um LID), e devices são os JIDs de device dela, no máximo cinco. Omita devices e eles são resolvidos, caindo para o device primário da conta.
isVideo e audioFile funcionam como numa chamada 1:1. A chamada conecta quando o roster do servidor lista outra pessoa conectada — não quando a oferta recebe ack.

Atendendo uma

Uma chamada em grupo de entrada chega em voip_call_incoming como qualquer outra, e o acceptCall a atende.
Um callee de grupo faz preaccept, accept e decline para a própria chamada, então uma chamada que este lado recusa o lista como convidado em vez de ainda tocando para todos os outros.

Diferenciando grupo de 1:1

Use call.isGroupCall. Os campos por trás dele são mais sutis do que parecem:

Quem está na chamada

O call.groupComposition guarda o roster que este client incorporou: um WaCallGroupParticipant por conta, mais o transaction-id da última atualização aplicada.
Toda mudança chega em voip_call_participants_changed — das atualizações de roster do servidor, ou de um participante desligando:
O diff é um WaGroupCompositionDiff, e as listas dele não se sobrepõem:
Um <device pid> que muda não é uma mudança de quem está na chamada, e nem um roster que chega velho — uma atualização cujo transaction-id está atrás do último aplicado é descartada em vez de incorporada.
O WA_GROUP_REMOVAL_CAUSE distingue por que alguém saiu: absent (deixado de fora de um roster, que é sempre a chamada inteira), departed (terminated / invalid), departed_unexpected (rejected, timedout, cancel_offer) e nacked. O WA_CALL_PARTICIPANT_STATE carrega os states crus do roster — connected, outgoing, receipt, rejected, terminated, timedout, creating, invisible, visible, cancel_offer, invited, invalid.

Sair versus encerrar

Nenhum membro consegue encerrar uma chamada em grupo para todos, inclusive quem criou. Os dois métodos enviam um terminate à própria chamada, o que tira este lado dela; a chamada acaba para todos quando o último membro sai, e o servidor então envia group_call_ended. De qualquer forma a chamada local terminou e o voip_call_ended dispara.
  • O leaveCall lança numa chamada 1:1.
  • Uma chamada que perde o caminho de mídia sai, em vez de encerrá-la para os outros.
  • Deste lado, o terminate de um participante tira só aquele participante de uma chamada estabelecida. Só o group_call_ended, ou o servidor tirando este lado, encerra.
  • Quando o último outro membro sai, o servidor encerra a chamada numa fração de segundo — não existe um estado em que este lado espera sozinho.

Permanecendo na chamada

Todo membro conectado, este lado incluído, envia à chamada um heartbeat a cada 10 segundos enquanto estiver nela (o heartbeat_interval_s do <voip_settings> do servidor sobrepõe o padrão). Os membros também observam a mídia uns dos outros. Quando nenhuma mídia de um membro chega a este lado por cerca de 20 segundos enquanto a dos outros chega, este lado o reporta ao servidor como invisível (peer_state) — os clients oficiais fazem o mesmo depois de cerca de 30. O servidor então tira aquele membro da chamada.
Quando esse membro é este lado, a chamada segue sem ele: o voip_call_ended dispara com EndCallReason.Invisible, nada é enviado, e a chamada pode ser reentrada com joinCall.

Tocando, re-tocando e entrando

Uma chamada em grupo que este lado recusou, deixou tocar até o fim, saiu, ou de que foi tirado como invisível pode ser entrada enquanto ela segue, sob o mesmo call id.
O joinCall envia um accept à própria chamada, como o join do client oficial faz, e a chamada então passa por voip_call_state como uma aceita: o servidor responde com o relay e pede a este lado para chavear a chamada. Ele lança quando nenhuma chamada dessas é conhecida aqui, quando este lado já está nela, ou no limite de maxConcurrentCalls. Ele só conhece chamadas cuja oferta chegou a este lado, e esquece uma quando o servidor envia group_call_ended, quando ela é entrada, ou quando um novo toque dela chega.
Um membro que nunca recebeu a oferta não consegue entrar daqui. Sem a oferta, nada além de um group_update diria que a chamada existe.
Dois encerramentos conduzidos pelo servidor a esperar enquanto toca:

Recursos em chamada

Mute, levantar a mão, reações, vídeo e screen share todos funcionam. Os nossos anúncios vão para a própria chamada, e os remotos nomeiam quem os enviou.
Os campos de estado de 1:1 ficam undefined numa chamada em grupo. stateData.peerAudioMuted, peerVideoState e peerScreenShare descrevem o peer, e uma chamada em grupo não tem um. Leia o call.participantMedia no lugar — um Map indexado por JID de device de WaCallParticipantMedia: { deviceJid, senderId?, audioMuted?, video?, screenShare?, receiving? }. Ele é preenchido em chamadas 1:1 também, então código escrito contra ele serve para os dois casos.

Mute

Um pedido, não uma ação: ele vai para o device daquele participante, o alvo decide, e um voip_call_peer_mute vem depois se ele obedecer. Lança em um callId desconhecido, numa chamada 1:1, ou num envio que falha. Um pedido direcionado a este lado chega em voip_call_mute_requested e nunca é aplicado — obedeça com setMute(callId, true) se quiser:
O WhatsApp Web se recusa a pedir que um celular mute, então uma conta mobile raramente recebe um.

Vídeo

Toda câmera que está ligada chega a este lado sem subscription — um stream por remetente, atribuído por participantJid.
O setVideoEnabled lança numa chamada 1:1, que liga o vídeo pelo requestVideoUpgrade. Numa chamada em grupo que é de áudio, o requestVideoUpgrade pede ao servidor que transforme a chamada inteira numa chamada de vídeo. Todo membro então vê voip_call_video_upgraded, e nada é respondido: quem pediu fica com a câmera ligada quando o servidor confirmar, e toda outra câmera fica desligada até o dono chamar setVideoEnabled.
void
Escolhe de quem a chamada pede vídeo ao relay, e em qual qualidade — um array de CallVideoSubscription ({ participantJid, quality? }, com a qualidade sendo um de WA_VIDEO_QUALITY: Default, Low, Medium, High, Hd). null volta ao padrão, todo participante com a câmera ligada.Ainda não é enviado ao relay: o vídeo chega sem isso, e um host de mídia que não consegue carregar ignora. Lança em um callId desconhecido.

Screen share

O setScreenShare funciona e é enviado à chamada. Um início ou fim de share carrega screen_share_id, contado por chamada a partir de 1, como os clients oficiais enviam.
Os clients desktop do WhatsApp se recusam a mostrar um share numa chamada em grupo. Os mobile aceitam, o que ainda não foi confirmado contra uma chamada ao vivo.

Mídia dos participantes

O voip_call_participant_media reporta quando a mídia de um participante começa ou para de chegar a este lado — julgado localmente a partir dos pacotes recebidos, não de algo que o peer anunciou, e disparado por transição:
Com a opção participantAudio do plugin, o voip_call_participant_audio adicionalmente carrega o áudio decodificado de cada participante separado, antes da mixagem que o voip_call_inbound_audio carrega:
Ele é desligado por padrão porque cada frame é copiado para isso, e é só com mídia local.

Reações

Recebidas por remetente sem configuração nenhuma; o participantJid é definido quando o host de mídia consegue atribuí-las. Limpar uma reação não é reportado.

Chaves e relays

Uma oferta de grupo não carrega chave. A chamada é chaveada pelo enc_rekey que um participante envia quando este lado entra, respondido com um receipt e retido quando chega antes da própria oferta. Quando o servidor faz deste lado o rekey master, ele cunha a chave e a envia a todo device conectado. O device de cada participante é decriptado com a chave dele. O plano de mídia carrega uma chave por device remoto e declara os streams deste lado por layer. O groupSenderId(callId, deviceJid) calcula o id opaco de um remetente — o SSRC de áudio principal dele em hex, estável durante a chamada — que é como o host de mídia nomeia remetentes. Quando o servidor move a chamada para outro relay no meio dela, a mídia vai junto, e é rediscada quando o servidor re-credencia o relay em que ela está: por UDP cru na porta anunciada quando o novo relay não carrega credencial de ICE, do jeito comum quando carrega.

Sem suporte

  • Convidar ou remover alguém no meio da chamada.
  • Chamadas em grupo no host de mídia de browser (veja o aviso no topo).

Veja também

Chamadas (VoIP)

A superfície 1:1 em que toda chamada em grupo se apoia, e a referência completa de eventos e enums.

Mídia em outro host

Rodar a mídia da chamada em outro lugar, e as features que um host precisa declarar para carregar uma chamada em grupo.
Última modificação em 14 de outubro de 2026