feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Sinalização WebRTC local; vídeo passa diretamente entre celular e navegador."""
|
||||
from collections import deque
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
|
||||
class Sinalizacao:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.id = None
|
||||
self.visto = 0
|
||||
self.seq = 0
|
||||
self.mensagens = deque(maxlen=256)
|
||||
|
||||
def encerrar(self):
|
||||
with self.lock:
|
||||
self.id = None
|
||||
self.mensagens.clear()
|
||||
|
||||
def atender(self, h, metodo, habilitado):
|
||||
url = urlsplit(h.path)
|
||||
if url.path not in ('/rtc/session', '/rtc/send', '/rtc/poll'):
|
||||
return False
|
||||
codigo, resposta = 200, {}
|
||||
try:
|
||||
dados = {}
|
||||
if metodo == 'POST':
|
||||
tamanho = int(h.headers.get('Content-Length', '0'))
|
||||
if not 0 < tamanho <= 65536:
|
||||
raise ValueError('Sinalização excede limite de 64 KB')
|
||||
dados = json.loads(h.rfile.read(tamanho))
|
||||
if not isinstance(dados, dict):
|
||||
raise ValueError('Esperava objeto JSON')
|
||||
with self.lock:
|
||||
if not habilitado or time.monotonic() - self.visto > 10:
|
||||
self.id = None
|
||||
self.mensagens.clear()
|
||||
if url.path == '/rtc/session' and metodo == 'POST':
|
||||
if dados.get('acao') == 'abrir':
|
||||
if not habilitado:
|
||||
codigo, resposta = 409, {'erro': 'Transmissão desligada'}
|
||||
else:
|
||||
self.id = uuid.uuid4().hex
|
||||
self.visto = time.monotonic()
|
||||
self.seq = 0
|
||||
self.mensagens.clear()
|
||||
resposta = {'id': self.id}
|
||||
elif dados.get('acao') == 'fechar' and dados.get('id') == self.id:
|
||||
self.id = None
|
||||
self.mensagens.clear()
|
||||
resposta = {'ok': True}
|
||||
else:
|
||||
raise ValueError('Ação ou sessão inválida')
|
||||
elif url.path == '/rtc/send' and metodo == 'POST':
|
||||
if not self.id or dados.get('id') != self.id:
|
||||
codigo, resposta = 409, {'erro': 'Sessão encerrada ou substituída'}
|
||||
else:
|
||||
lado = dados.get('de')
|
||||
msg = dados.get('mensagem')
|
||||
if lado not in ('app', 'pc') or not isinstance(msg, dict) or msg.get('tipo') not in ('offer', 'answer', 'candidate', 'erro', 'stats'):
|
||||
raise ValueError('Mensagem de sinalização inválida')
|
||||
if msg['tipo'] in ('offer', 'answer') and not isinstance(msg.get('sdp'), str):
|
||||
raise ValueError('SDP inválido')
|
||||
self.seq += 1
|
||||
self.mensagens.append({'seq': self.seq, 'para': 'app' if lado == 'pc' else 'pc', 'mensagem': msg})
|
||||
resposta = {'ok': True}
|
||||
elif url.path == '/rtc/poll' and metodo == 'GET':
|
||||
qs = parse_qs(url.query)
|
||||
lado = qs.get('lado', [''])[0]
|
||||
if lado not in ('app', 'pc'):
|
||||
raise ValueError('Lado inválido')
|
||||
desde = max(0, int(qs.get('desde', ['0'])[0]))
|
||||
if qs.get('id', [''])[0] != (self.id or ''):
|
||||
desde = 0
|
||||
if lado == 'pc' and self.id:
|
||||
self.visto = time.monotonic()
|
||||
resposta = {'id': self.id, 'mensagens': [dict(seq=m['seq'], mensagem=m['mensagem']) for m in self.mensagens if m['para'] == lado and m['seq'] > desde]}
|
||||
else:
|
||||
codigo, resposta = 405, {'erro': 'Método não permitido'}
|
||||
except (ValueError, TypeError) as e:
|
||||
codigo, resposta = 400, {'erro': str(e)}
|
||||
h.enviar(json.dumps(resposta), codigo=codigo)
|
||||
return True
|
||||
Reference in New Issue
Block a user