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. Exemplos Práticos
  9. 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/

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"
  },
  "response_type": "connection_established"
}

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

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"
}

💻 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 Automática: Implemente lógica de reconexão em caso de desconexão
  • Timeout Handling: Configure timeouts apropriados para evitar conexões órfãs
  • Gestão de Sessão: Certifique-se de que apenas 1 sessão ativa por estudante

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 externamente via POST /sessions/{id}/end/
  • Fechamento Automático: WebSocket fecha automaticamente 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 enquanto WebSocket estava ativo - Comportamento normal - WebSocket fecha automaticamente - Verifique se não há finalização acidental de sessões - Implemente reconexão se necessário para novas sessões


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