87 lines
4.0 KiB
Python
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
|