🔌 WebSocket Streaming¶
Documentação completa do WebSocket para streaming de dados em tempo real
O SADIONLINE oferece uma API WebSocket robusta para streaming de frames de vídeo em tempo real, permitindo análise de engajamento ao vivo durante sessões de aprendizagem.
📋 Índice¶
- Visão Geral
- Conexão WebSocket
- Autenticação
- Parâmetros de Conexão
- Formato de Mensagens
- Rate Limiting
- Códigos de Erro
- Reconexão WebSocket
- Exemplos Práticos
- Melhores Práticas
🎯 Visão Geral¶
O WebSocket do SADIONLINE permite:
- Streaming em tempo real de frames de vídeo para análise de engajamento
- Processamento assíncrono com confirmações de recebimento
- Rate limiting inteligente para controle de fluxo
- Suporte multi-idioma com localização automática
- Gestão automática de sessões com timeouts configuráveis
🔌 Conexão WebSocket¶
URL Base¶
Pausar e retomar uma sessão¶
Pausa é uma ação explícita e persistida; ela é diferente de uma desconexão de rede, que continua usando a janela curta de reconexão. Para pausar, um cliente autorizado chama POST /tenant/{organization_id}/v1/sessions/{session_id}/pause/ com {"reason": "user_requested"} opcional. A chamada é idempotente.
O socket ativo recebe o close code 4008 com o motivo session_paused. Não faça reconexão automática nesse caso. Quando o usuário escolher retomar, abra o WebSocket normal informando o mesmo session_id; o servidor preserva a sessão e a numeração dos frames. O tempo pausado não entra na duração, indicadores ou cobrança. Indicadores de sessões abertas/pausadas são provisórios; tornam-se finais após o encerramento.
URL Completa com Parâmetros¶
Nota: - A autenticação deve ser enviada via header HTTP Authorization, igual às requisições HTTP REST. - O idioma (locale) deve ser enviado via header Accept-Language, igual às requisições HTTP REST. - O parâmetro organization_id só é aceito em ambiente local (desenvolvimento).
Exemplo de Conexão (Organização Enterprise)¶
// Recomendado: Autenticação e locale via headers (formato idêntico às requisições HTTP REST)
const ws = new WebSocket(
'wss://ws.sadionline.com.br/v1/stream/?student_id=550e8400-e29b-41d4-a716-446655440000&activity_id=550e8400-e29b-41d4-a716-446655440001',
[],
{
headers: {
'Authorization': 'APIKey sk_abc123def456ghi789jkl012mno345pqr678',
'Accept-Language': 'pt-br'
}
}
);
// Alternativa (compatibilidade): Autenticação via query string (menos seguro)
const ws = new WebSocket('wss://ws.sadionline.com.br/v1/stream/?student_id=550e8400-e29b-41d4-a716-446655440000&activity_id=550e8400-e29b-41d4-a716-446655440001&api_key=sk_abc123def456');
🔐 Autenticação¶
Método de Autenticação¶
A autenticação é feita via header HTTP Authorization, no mesmo formato das requisições HTTP REST.
Via Header HTTP (Recomendado - Mais Seguro)¶
Para Organizações Enterprise (API Key):
const ws = new WebSocket(url, [], {
headers: {
'Authorization': 'APIKey sk_sua_chave_api_aqui',
'Accept-Language': 'pt-br'
}
});
O formato é idêntico às requisições HTTP REST: Authorization: APIKey sk_...
Via Query String (Compatibilidade - Menos Seguro)¶
⚠️ Aviso: Autenticação via query string é menos segura pois os tokens aparecem em logs e URLs. Use headers sempre que possível.
Formato da Chave API: - Prefixo: sk_ - Exemplo: sk_abc123def456ghi789jkl012mno345pqr678
Validação de Tenant¶
O sistema valida automaticamente se a chave API tem acesso à organização correspondente.
📝 Parâmetros de Conexão¶
Parâmetros Obrigatórios (Organização Enterprise)¶
| Parâmetro | Tipo | Obrigatório | Descrição | Exemplo |
|---|---|---|---|---|
student_id | UUID | Sim | Identificador único do estudante | 550e8400-e29b-41d4-a716-446655440000 |
activity_id | UUID | Sim | Identificador único da atividade | 550e8400-e29b-41d4-a716-446655440001 |
Autenticação (via Header HTTP - Recomendado): - Authorization: APIKey sk_... - Formato idêntico às requisições HTTP REST
Autenticação (via Query String - Compatibilidade): - api_key - Chave de autenticação da API
Headers HTTP¶
| Header | Tipo | Obrigatório | Descrição | Valores Aceitos | Padrão |
|---|---|---|---|---|---|
Authorization | string | Sim | Chave API para autenticação | APIKey sk_... | - |
Accept-Language | string | Não | Idioma para localização de mensagens | pt-br, en-us, es-es | en-us |
Nota: O header Accept-Language segue o mesmo padrão das requisições HTTP REST da API.
Parâmetros Opcionais (Apenas Ambiente Local)¶
| Parâmetro | Tipo | Descrição | Exemplo |
|---|---|---|---|
organization_id | UUID | Identificador da organização (apenas em ambiente local) | 550e8400-e29b-41d4-a716-446655440002 |
Validação de Parâmetros¶
- Todos os UUIDs devem estar no formato padrão:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - A chave API deve começar com
sk_ - O locale deve ser um dos idiomas suportados
- O estudante e atividade devem existir na organização correspondente
- Organizações Enterprise:
student_ideactivity_idsão obrigatórios na query string - Organizações Pessoais: Não passam
student_id(sistema usa o único estudante da organização), apenasactivity_id
💬 Formato de Mensagens¶
Mensagem de Boas-vindas¶
Após conexão bem-sucedida, o servidor envia:
{
"success": true,
"message": "Conexão WebSocket estabelecida com sucesso",
"data": {
"session_id": "550e8400-e29b-41d4-a716-446655440003",
"session_context_status": "required"
},
"response_type": "connection_established"
}
Contexto da sessão antes dos frames¶
Novas sessões devem enviar uma mensagem de contexto limitada antes de enviar o primeiro frame. O handshake informa required, saved ou legacy_unavailable. Uma reconexão com saved reutiliza o snapshot persistido; required e legacy_unavailable exigem uma nova resposta concluída ou um skip explícito.
{
"type": "session_context",
"schema_version": 1,
"completion_status": "completed",
"additional_screens": {
"status": "present",
"locations": [
"viewer_left",
"viewer_right",
"viewer_above",
"viewer_below",
"viewer_center"
]
},
"auxiliary_materials": {
"status": "none",
"items": [],
"locations": []
},
"session_mode": "exercises",
"camera_position": "center"
}
additional_screens.locations é um array semelhante a um conjunto, não uma única localização. Quando o status é present, ele pode conter qualquer combinação das zonas relativas ao observador, incluindo as cinco zonas mostradas no exemplo. A ordem dos valores não tem significado e valores duplicados são rejeitados. O contrato v1 registra zonas de atenção ocupadas; ele não atribui identidades, quantidades ou tipos de dispositivo a telas individuais. Uma versão futura do schema será necessária se esses detalhes forem exigidos.
As zonas suportadas para telas e materiais são viewer_left, viewer_right, viewer_center, viewer_above e viewer_below. Elas se referem à prévia renderizada da câmera na perspectiva do observador, e não à esquerda/direita anatômica. Quando um status de presença não é present, seu array locations (e, para materiais, items) deve estar vazio.
Para ignorar o questionário, envie somente:
O servidor confirma um envio válido com response_type igual a session_context_saved. Frames enviados antes dessa confirmação são rejeitados com errors.reason=session_context_required e não são persistidos. Uma atualização conflitante do contexto após o primeiro frame aceito é rejeitada com session_context_immutable_after_first_frame; novas tentativas idênticas são idempotentes. viewer_left e viewer_right são relativos à prévia renderizada na perspectiva do observador, e não à esquerda/direita anatômica.
Envio de Frame¶
Formato da mensagem do cliente:
O WebSocket recebe mensagens JSON com os dados do frame:
Especificações dos Campos:
| Campo | Tipo | Obrigatório | Descrição | Validação |
|---|---|---|---|---|
img_base64 | string | Sim | Imagem em base64 com prefixo data URI | Deve começar com data:image/ |
is_active_tab | boolean | Não | Se o estudante está na aba ativa | Padrão: true |
Exemplo de Envio:
// Capturar frame da câmera
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// ... capturar frame da câmera ...
const imageData = canvas.toDataURL('image/jpeg', 0.8);
// Enviar via WebSocket
const message = {
img_base64: imageData,
is_active_tab: document.hasFocus() // Verifica se a aba está ativa
};
ws.send(JSON.stringify(message));
Especificações:
- Formato: JSON com imagem base64
- Tamanho máximo: 10 MB por frame
- Resolução recomendada: 640x480 ou 1280x720
- Qualidade: JPEG 70-80% para balancear qualidade e tamanho
- is_active_tab: Indica se o estudante está visualizando a aba do curso/atividade
Resposta de Confirmação¶
Sucesso:
{
"success": true,
"message": "Frame enfileirado com sucesso",
"data": {
"session_id": "550e8400-e29b-41d4-a716-446655440003"
},
"response_type": "frame_processed"
}
⚡ Rate Limiting¶
Limites de Taxa¶
| Categoria | Limite | Janela de Tempo | Descrição |
|---|---|---|---|
| Frames por Conexão | 60 frames | 60 segundos | Máximo 1 frame por segundo |
| Sessões Ativas | 1 sessão | Por estudante | Apenas 1 sessão ativa por estudante |
| Tamanho do Frame | 10 MB | Por mensagem | Tamanho máximo da imagem |
Resposta de Rate Limit¶
Quando o limite é excedido:
{
"success": false,
"message": "Limite de taxa excedido. Aguarde 15.50 segundos antes de enviar outra mensagem.",
"meta": {
"retry_after": 15.5
},
"response_type": "rate_limited"
}
Estratégia de Retry¶
- Backoff Exponencial: Aguarde progressivamente mais tempo entre tentativas
- Respeitar retry_after: Use o valor fornecido na resposta
- Máximo de Tentativas: Limite a 10 tentativas por frame
❌ Códigos de Erro¶
Códigos de Fechamento WebSocket¶
| Código | Descrição | Causa |
|---|---|---|
4000 | Parâmetros Inválidos | Parâmetros obrigatórios ausentes ou inválidos |
4001 | Erro de Sessão | Falha ao criar sessão (estudante/atividade não encontrados) |
4002 | Sessão Finalizada Externamente | Sessão foi finalizada via API enquanto WebSocket estava ativo |
4003 | Autenticação Falhada | API key inválida ou sem permissões |
4004 | Limite de Billing | Limites de uso da organização excedidos |
4005 | Frames Inválidos | Muitos frames inválidos consecutivos |
4006 | Device Revogado | Sessão do dispositivo revogada (JWT) |
4007 | Supersede | Outra conexão assumiu esta sessão (reconnect) |
Mensagens de Erro¶
Erro de Validação:
{
"success": false,
"message": "Falha na validação",
"errors": {
"frame_data": ["Campo obrigatório"],
"timestamp": ["Formato de data inválido"]
},
"response_type": "validation_error"
}
Erro de Processamento:
{
"success": false,
"message": "Erro ao processar mensagem",
"errors": "Formato de imagem não suportado",
"response_type": "error"
}
JSON Inválido:
{
"success": false,
"message": "Formato JSON inválido",
"errors": {
"json_error": "Expecting ',' delimiter: line 1 column 45 (char 44)"
},
"response_type": "invalid_json"
}
🔄 Reconexão WebSocket¶
O servidor suporta reconexão à mesma sessão após queda de rede. O grace é ilimitado por padrão (DJANGO_WS_RECONNECT_GRACE_SECONDS=0), preservando o mesmo UUID e a sequência de frames. Um valor maior que zero, como 60, ativa o modo finito legado com deadline e auto-finalização.
Guia completo para integradores
Consulte Guia de Reconexão WebSocket para o fluxo detalhado e o código de exemplo.
Parâmetro session_id¶
Para reconectar à mesma sessão, inclua session_id na URL de conexão:
Fluxo de reconnect¶
- Conexão cai (1000/1006) → servidor marca pending reconnect
- Cliente reconecta com mesmo
session_id(sem prazo no default; deadline no modo finito) - Servidor restaura
frame_counterdo banco - Conexão antiga recebe 4007 (supersede) se ainda estiver ativa
- Nova conexão aceita; próximo frame continua a numeração
Códigos relevantes¶
| Código | Ação do cliente |
|---|---|
4007 | Parar esta conexão; não reconectar nem chamar POST /end/ |
4002 | Sessão encerrada nesta conexão; reconectar com session_id para reativar, ou nova sessão |
4001 + reconnect_cooldown_active | Aguardar 5s e tentar novamente |
O que NÃO fazer¶
- Não chamar
POST /sessions/{id}/end/nooncloseapós disconnect transitório - Não auto-reconnect na aba que recebeu 4007
- Não manter múltiplas abas enviando frames para a mesma sessão
💻 Exemplos Práticos¶
JavaScript (Browser)¶
class SADIONLINEWebSocket {
constructor(studentId, activityId, apiKey, locale = 'pt-br') {
this.ws = null;
this.sessionId = null;
this.frameCounter = 0;
const params = new URLSearchParams({
student_id: studentId,
activity_id: activityId
});
const url = `wss://ws.sadionline.com.br/v1/stream/?${params.toString()}`;
this.connect(url, apiKey, locale);
}
connect(url, apiKey, locale) {
this.ws = new WebSocket(url, [], {
headers: {
'Authorization': `APIKey ${apiKey}`,
'Accept-Language': locale
}
});
this.ws.onopen = () => {
console.log('Conectado ao WebSocket');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.ws.onclose = (event) => {
console.log(`WebSocket fechado: ${event.code} - ${event.reason}`);
};
this.ws.onerror = (error) => {
console.error('Erro no WebSocket:', error);
};
}
handleMessage(data) {
if (data.response_type === 'connection_established') {
this.sessionId = data.data.session_id;
console.log(`Sessão criada: ${this.sessionId}`);
} else if (data.response_type === 'frame_processed') {
console.log('Frame processado com sucesso');
} else if (data.response_type === 'rate_limited') {
console.warn(`Rate limit: aguarde ${data.meta.retry_after}s`);
}
}
sendFrame(imageData, isActiveTab = true) {
if (this.ws.readyState !== WebSocket.OPEN) {
console.error('WebSocket não está conectado');
return;
}
const message = {
img_base64: imageData, // data:image/jpeg;base64,{base64_data}
is_active_tab: isActiveTab
};
this.ws.send(JSON.stringify(message));
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// Uso - Organização Enterprise (API Key)
// Authorization: APIKey sk_... (formato idêntico às requisições HTTP REST)
// Accept-Language: pt-br (formato idêntico às requisições HTTP REST)
const stream = new SADIONLINEWebSocket(
'550e8400-e29b-41d4-a716-446655440000', // student_id
'550e8400-e29b-41d4-a716-446655440001', // activity_id
'sk_abc123def456ghi789jkl012mno345pqr678', // API Key
'pt-br' // locale (enviado via header Accept-Language)
);
// Enviar frame de uma câmera
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
const video = document.createElement('video');
video.srcObject = stream;
video.play();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
setInterval(() => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
const imageData = canvas.toDataURL('image/jpeg', 0.8);
const isActiveTab = document.hasFocus();
stream.sendFrame(imageData, isActiveTab);
}, 1000); // 1 frame por segundo
});
Python (asyncio + websockets)¶
import asyncio
import base64
import json
import time
import websockets
from datetime import datetime
import cv2
class SADIONLINEWebSocket:
def __init__(self, student_id, activity_id, api_key, locale='pt-br'):
self.student_id = student_id
self.activity_id = activity_id
self.api_key = api_key
self.locale = locale
self.session_id = None
self.frame_counter = 0
self.ws = None
async def connect(self):
url = f"wss://ws.sadionline.com.br/v1/stream/?student_id={self.student_id}&activity_id={self.activity_id}"
headers = {
'Authorization': f'APIKey {self.api_key}',
'Accept-Language': self.locale
}
try:
self.ws = await websockets.connect(
url,
extra_headers=headers,
ping_interval=20,
ping_timeout=10,
close_timeout=10,
max_size=10 * 1024 * 1024 # 10MB
)
# Aguardar mensagem de boas-vindas
welcome = await asyncio.wait_for(self.ws.recv(), timeout=5.0)
data = json.loads(welcome)
if data.get('response_type') == 'connection_established':
self.session_id = data['data']['session_id']
print(f"Conectado! Sessão: {self.session_id}")
return True
else:
print(f"Erro na conexão: {data}")
return False
except Exception as e:
print(f"Erro ao conectar: {e}")
return False
async def send_frame(self, image_data, is_active_tab=True):
if not self.ws:
raise Exception("WebSocket não conectado")
# Converter imagem para base64
_, buffer = cv2.imencode('.jpg', image_data, [cv2.IMWRITE_JPEG_QUALITY, 80])
image_base64 = base64.b64encode(buffer).decode('utf-8')
frame_data = f"data:image/jpeg;base64,{image_base64}"
message = {
"img_base64": frame_data,
"is_active_tab": is_active_tab
}
self.frame_counter += 1
# Enviar JSON
try:
await self.ws.send(json.dumps(message))
print(f"Frame {self.frame_counter} enviado com sucesso")
return True
except Exception as e:
print(f"Erro enviando frame: {e}")
return False
async def close(self):
if self.ws:
await self.ws.close()
# Exemplo de uso
async def stream_video():
# Inicializar WebSocket
# Authorization: APIKey sk_... (formato idêntico às requisições HTTP REST)
# Accept-Language: pt-br (formato idêntico às requisições HTTP REST)
ws_client = SADIONLINEWebSocket(
student_id="550e8400-e29b-41d4-a716-446655440000",
activity_id="550e8400-e29b-41d4-a716-446655440001",
api_key="sk_abc123def456ghi789jkl012mno345pqr678",
locale="pt-br"
)
if not await ws_client.connect():
return
# Capturar vídeo da webcam
cap = cv2.VideoCapture(0)
try:
while True:
ret, frame = cap.read()
if not ret:
break
# Redimensionar frame se necessário
frame = cv2.resize(frame, (640, 480))
# Enviar frame (assumindo que a aba está ativa)
await ws_client.send_frame(frame, is_active_tab=True)
# Aguardar 1 segundo (1 FPS)
await asyncio.sleep(1.0)
except KeyboardInterrupt:
print("Interrompido pelo usuário")
finally:
cap.release()
await ws_client.close()
# Executar
if __name__ == "__main__":
asyncio.run(stream_video())
Node.js¶
const WebSocket = require('ws');
const fs = require('fs');
class SADIONLINEWebSocket {
constructor(studentId, activityId, apiKey, locale = 'pt-br') {
this.studentId = studentId;
this.activityId = activityId;
this.apiKey = apiKey;
this.locale = locale;
this.sessionId = null;
this.frameCounter = 0;
this.ws = null;
this.connect();
}
connect() {
const url = `wss://ws.sadionline.com.br/v1/stream/?student_id=${this.studentId}&activity_id=${this.activityId}`;
this.ws = new WebSocket(url, {
headers: {
'Authorization': `APIKey ${this.apiKey}`,
'Accept-Language': this.locale
},
perMessageDeflate: false,
maxPayload: 10 * 1024 * 1024 // 10MB
});
this.ws.on('open', () => {
console.log('Conectado ao WebSocket');
});
this.ws.on('message', (data) => {
const message = JSON.parse(data.toString());
this.handleMessage(message);
});
this.ws.on('close', (code, reason) => {
console.log(`WebSocket fechado: ${code} - ${reason}`);
});
this.ws.on('error', (error) => {
console.error('Erro no WebSocket:', error);
});
}
handleMessage(data) {
if (data.response_type === 'connection_established') {
this.sessionId = data.data.session_id;
console.log(`Sessão criada: ${this.sessionId}`);
} else if (data.response_type === 'frame_processed') {
console.log('Frame processado com sucesso');
} else if (data.response_type === 'rate_limited') {
console.warn(`Rate limit: aguarde ${data.meta.retry_after}s`);
}
}
async sendFrame(imagePath, isActiveTab = true) {
if (this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket não está conectado');
}
// Ler imagem e converter para base64
const imageBuffer = fs.readFileSync(imagePath);
const imageBase64 = imageBuffer.toString('base64');
const frameData = `data:image/jpeg;base64,${imageBase64}`;
const message = {
img_base64: frameData,
is_active_tab: isActiveTab
};
return new Promise((resolve, reject) => {
this.ws.send(JSON.stringify(message), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// Uso
// Authorization: APIKey sk_... (formato idêntico às requisições HTTP REST)
// Accept-Language: pt-br (formato idêntico às requisições HTTP REST)
const stream = new SADIONLINEWebSocket(
'550e8400-e29b-41d4-a716-446655440000', // student_id
'550e8400-e29b-41d4-a716-446655440001', // activity_id
'sk_abc123def456ghi789jkl012mno345pqr678', // api_key
'pt-br' // locale (enviado via header Accept-Language)
);
// Enviar frames de imagens
setTimeout(async () => {
try {
await stream.sendFrame('./frame1.jpg', true); // true = aba ativa
console.log('Frame enviado');
} catch (error) {
console.error('Erro:', error);
}
}, 2000);
🏆 Melhores Práticas¶
1. Gestão de Conexão¶
- Reconexão com session_id: Após disconnect transitório, reconecte com o mesmo
session_id; o prazo é ilimitado por padrão - Tratamento de 4007: Pare a conexão substituída; não reconecte nem chame
/end/ - Tratamento de 4002: Pare esta conexão; reconecte com
session_idpara reativar a sessão, ou crie nova sessão semsession_id - Timeout Handling: Configure timeouts apropriados para evitar conexões órfãs
- Gestão de Sessão: Certifique-se de que apenas 1 conexão ativa por sessão
2. Otimização de Performance¶
- Compressão de Imagem: Use qualidade JPEG entre 70-80% para balancear qualidade e tamanho
- Resolução Adequada: Redimensione frames para 640x480 ou 1280x720 máximo
- Buffer Management: Implemente buffer para frames em caso de rate limiting
- Detecção de Aba Ativa: Use
document.hasFocus()para detectar se o estudante está visualizando a aba
3. Tratamento de Erros¶
- Retry Logic: Implemente backoff exponencial para rate limiting
- Error Logging: Registre todos os erros para debugging
- Graceful Degradation: Continue funcionando mesmo com alguns frames perdidos
4. Segurança¶
- API Key Segura: Nunca exponha chaves API no frontend
- Validação de Dados: Valide todos os dados antes de enviar
- Rate Limiting: Respeite os limites de taxa do servidor
5. Finalização de Sessões¶
- Finalização via API: Sessões podem ser finalizadas via
POST /sessions/{id}/end/— somente quando o usuário encerra explicitamente - Não finalizar no onclose: Disconnect transitório não deve disparar
POST /end/; o servidor mantém a sessão emRECONNECTING - Fechamento Automático: WebSocket fecha com código
4002quando sessão é finalizada externamente - Verificação Periódica: Sistema verifica se sessão ainda está ativa antes de processar cada frame
- Billing Automático: Sistema de cobrança é acionado automaticamente ao finalizar sessão
🔧 Troubleshooting¶
Problemas Comuns¶
Conexão Recusada (4003) - Verifique se a API key está correta - Confirme se a organização tem acesso ativo - Verifique se a chave não expirou
Rate Limiting Excessivo - Reduza a frequência de envio (máximo 1 FPS) - Implemente backoff exponencial - Verifique se não há múltiplas conexões simultâneas
Frames Muito Grandes - Reduza a qualidade JPEG (70-80%) - Redimensione a resolução da imagem - Verifique o limite de 10MB por mensagem
Timeout de Conexão - Verifique conectividade de rede - Confirme se os parâmetros estão corretos - Verifique se não há outra sessão ativa para o mesmo estudante
Sessão Finalizada Externamente (4002) - Sessão foi finalizada via API ou o grace finito configurado expirou - Conexões WebSocket ativas recebem o evento de encerramento e são fechadas imediatamente - Verifique se o integrador não está chamando POST /end/ no onclose - Ao receber 4002: pare esta conexão; reconecte com o mesmo session_id para continuar, ou descarte o session_id e crie nova sessão
Loop de 4007 (supersede) - Duas abas/conexões disputando a mesma sessão - Apenas uma conexão deve permanecer ativa; a que recebe 4007 deve parar
Reconnect falha com reconnect_cooldown_active - Aguarde 5 segundos entre reconnects bem-sucedidos
Construído com ❤️ pelo Instituto Anexo