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