Files

87 lines
4.0 KiB
Python

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