Pular para conteúdo

🔌 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

  1. Visão Geral
  2. Conexão WebSocket
  3. Autenticação
  4. Parâmetros de Conexão
  5. Formato de Mensagens
  6. Rate Limiting
  7. Códigos de Erro
  8. Reconexão WebSocket
  9. Exemplos Práticos
  10. 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

wss://ws.sadionline.com.br/v1/stream/

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

wss://ws.sadionline.com.br/v1/stream/?student_id={STUDENT_ID}&activity_id={ACTIVITY_ID}

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)

?api_key=sk_sua_chave_api_aqui

⚠️ 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_id e activity_id são obrigatórios na query string
  • Organizações Pessoais: Não passam student_id (sistema usa o único estudante da organização), apenas activity_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:

{
  "type": "session_context",
  "schema_version": 1,
  "completion_status": "skipped"
}

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:

{
  "img_base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...",
  "is_active_tab": true
}

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

  1. Backoff Exponencial: Aguarde progressivamente mais tempo entre tentativas
  2. Respeitar retry_after: Use o valor fornecido na resposta
  3. 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:

wss://ws.sadionline.com.br/v1/stream/?student_id={id}&activity_id={id}&session_id={session_id}

Fluxo de reconnect

  1. Conexão cai (1000/1006) → servidor marca pending reconnect
  2. Cliente reconecta com mesmo session_id (sem prazo no default; deadline no modo finito)
  3. Servidor restaura frame_counter do banco
  4. Conexão antiga recebe 4007 (supersede) se ainda estiver ativa
  5. 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/ no onclose apó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_id para reativar a sessão, ou crie nova sessão sem session_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 em RECONNECTING
  • Fechamento Automático: WebSocket fecha com código 4002 quando 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


WebSocket Streaming Completo 🚀
Construído com ❤️ pelo Instituto Anexo