<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.
Fazendo uma
OstartCall 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 emvoip_call_incoming como qualquer outra, e o acceptCall a atende.
Diferenciando grupo de 1:1
Usecall.isGroupCall. Os campos por trás dele são mais sutis do que parecem:
Quem está na chamada
Ocall.groupComposition guarda o roster que este client incorporou: um WaCallGroupParticipant por conta, mais o transaction-id da última atualização aplicada.
voip_call_participants_changed — das atualizações de roster do servidor, ou de um participante desligando:
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.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
group_call_ended. De qualquer forma a chamada local terminou e o voip_call_ended dispara.
- O
leaveCalllanç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 (oheartbeat_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.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.
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.Mute
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 porparticipantJid.
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
OsetScreenShare 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.
Mídia dos participantes
Ovoip_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:
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:
Reações
Recebidas por remetente sem configuração nenhuma; oparticipantJid é 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 peloenc_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.
