🔌 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
- 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¶
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"
},
"response_type": "connection_established"
}
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 |
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
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 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
Construído com ❤️ pelo Instituto Anexo