Este projeto implementa o backend de um NVR (Network Video Recorder) IP, focado em:
- Autenticação e autorização de usuários (JWT).
- Cadastro, gestão e controle de câmeras IP.
- Integração profunda com o MediaMTX (servidor de media/RTSP/HLS/WebRTC/Playback).
- Registro e listagem de gravações de vídeo.
- Geração de URLs temporárias de playback com controle de acesso.
A API é escrita em Python 3.11, usando FastAPI, SQLModel/SQLAlchemy e PostgreSQL, com orquestração via Docker Compose.
-
API Backend (FastAPI)
Containerapi, exposto emhttp://localhost:8000
Responsável por:- Autenticação JWT e RBAC (admin / viewer).
- CRUD de usuários.
- CRUD de câmeras.
- Integração e orquestração com o MediaMTX.
- Endpoints para listagem e playback de gravações.
- Exposição de arquivos gravados via
/api/v1/videos(static).
-
Banco de Dados (PostgreSQL)
Containerdb(tcc-postgres), exposto emlocalhost:5433.
Usado para armazenar:- Usuários e roles.
- Câmeras cadastradas.
- Registros de gravações (metadados).
- Configurações de sistema (tabela
system_setting).
-
MediaMTX
Containermediamtx(tcc-mediamtx), principal servidor de mídia:- RTSP (porta
8554) – ingestão de câmeras. - HLS (porta
8888) – streaming HTTP para players HLS. - WebRTC/WHEP (porta
8889) – baixa latência via navegador. - Playback API (porta
9996) – leitura de arquivos gravados. - API de controle v3 (porta
9997) – criação/remoção/atualização de paths, listagem, kick de sessões, etc. - Tudo configurado via
mediamtx.yml, com autenticação interna (authInternalUsers).
- RTSP (porta
-
Volumes de Gravação
- Pasta
./recordingsmontada tanto no container da API quanto no do MediaMTX. - MediaMTX escreve arquivos de gravação (fmp4) nessa pasta.
- A API expõe essa mesma pasta via
/api/v1/videos.
- Pasta
- Autenticação via JSON Web Token (JWT), implementada em
app/security/security.py:- Funções principais:
criar_hash_senha: hash de senha com SHA-256.verificar_senha: validação de senha.gerar_token: criação do JWT (camposub= email do usuário).pegar_usuario_atual: dependency FastAPI que valida o token Bearer e retorna o usuário autenticado.
- Funções principais:
- Modelo de token de resposta:
TokenResponse. - Papéis de usuário (RBAC):
- Definidos em
UserRoleEnum(admin,viewer). - Persistidos em
User_Role. - Seed automático em
seed_user_roles.
- Definidos em
- Admin (user_role_id = 1):
- Pode criar, listar e apagar usuários.
- Pode cadastrar, atualizar e deletar câmeras.
- Pode executar todas as operações de listagem e playback.
- Viewer (user_role_id = 2):
- Pode logar, acessar
/perfil, listar câmeras (GET /cameras) e consumir playback. - Não pode criar/editar/deletar usuários.
- Não pode criar/editar/deletar câmeras.
- Pode logar, acessar
As checagens estão implementadas principalmente em:
Principais entidades (em app/domain):
-
id,email,password_hash,full_nameuser_role_id(FK paraUser_Role)is_active,created_at,updated_at
-
id,role_name(adminouviewer),description
-
id,name,rtsp_url,is_recordingcreated_by_user_id(FK paraUser)path_id(ex:live/cam1) – path principal no MediaMTX.path_id_low(ex:live/cam1_low) – path de baixa resolução.created_at,updated_at
-
id,camera_id(FK)nome_arquivo,url_acessoduracao_segundosdata_criacao,data_inicio_segmento,data_fim_segmento
-
- Configurações de sistema (key/value).
A API é inicializada em app/main.py e registra os routers:
usersController– prefixo/api/v1cameraController– prefixo/api/v1recordController– prefixo/api/v1, tagrecordsvideoController– static/api/v1/videosplaybackController– prefixo/api/v1
Controller: usersController.py
-
POST
/api/v1/login- Body:
LoginData{ email, password } - Retorna
TokenResponse:access_token,token_type,user_id,role. - Implementa fluxo de autenticação via
authenticate_user.
- Body:
-
POST
/api/v1/usuarios(Admin)- Cria novo usuário com role
viewer. - Body:
NovoUsuario{ email, password, full_name }. - Verifica se o
usuario_atual.user_role_id == 1.
- Cria novo usuário com role
-
GET
/api/v1/usuarios(Admin)- Lista todos os usuários, retorna
UserDatacomroletextual (admin/viewer).
- Lista todos os usuários, retorna
-
GET
/api/v1/perfil- Retorna os dados do usuário autenticado (
UserData).
- Retorna os dados do usuário autenticado (
-
GET
/api/v1/area-restrita- Endpoint simples para testar proteção via token.
-
DELETE
/api/v1/usuarios/{user_id}(Admin)- Admin pode deletar outros usuários (não pode deletar a si mesmo).
Controller: cameraController.py
Serviço: camera_services.py
Repositório: camera_repository.py
-
POST
/api/v1/camera(Admin)- Body:
CamCreate{ name, rtsp_url, is_recording } - Passos:
- Verifica permissão (apenas admin).
- Gera
path_id = "live/{nome_normalizado}". - Chama
media_mtx_service.create_and_verify_camera_pathpara configurar o path no MediaMTX. - Cria entrada na tabela
camera. - Retorna
CamDatacom URLs HLS/WebRTC baseadas emsettings:visualisation_url_hlsvisualisation_url_hls_lowvisualisation_url_webrtc
- Body:
-
GET
/api/v1/cameras- Lista todas as câmeras (admin e viewer podem consumir).
- Retorna lista de
CamData.
-
GET
/api/v1/camera/{camera_id}- Retorna detalhes de uma câmera específica.
-
GET
/api/v1/camera/user/{user_id}- Lista câmeras criadas por um determinado usuário.
-
PUT
/api/v1/camera/{camera_id}(Admin)- Atualiza nome/URL/flag de gravação.
- Se
rtsp_urlouis_recordingmudarem, chama
media_mtx_service.create_camera_pathpara reconfigurar o path no MediaMTX.
-
DELETE
/api/v1/camera/{camera_id}(Admin)- Fluxo:
- Checa permissão de admin.
- Busca a câmera.
- Chama
media_mtx_service.delete_camera_pathpara remover a configuração no MediaMTX. - Remove a entrada da câmera do banco.
- Fluxo:
Controller: recordController.py
DTOs: RecordCreate, RecordData
- POST
/api/v1/record- Usado principalmente como endpoint interno/webhook para registrar metadados de gravações.
- Cria um
Recordcom:camera_idnome_arquivourl_acesso(montado como/recordings/{nome_arquivo})duracao_segundosdata_inicio_segmento,data_fim_segmento
- Requer usuário autenticado.
Embora exista o DTO MediaMTXWebhookPayload e um serviço inicial WebhookService (pensado para processar webhooks do MediaMTX), o fluxo principal de gravação hoje é baseado no próprio MediaMTX gravando em disco e a API consultando via Playback API.
Controller: playbackController.py
Segurança: funções de playback em security.py
- GET
/api/v1/camera/{camera_id}/recordings
Fluxo:
- Busca a câmera (
Camera) e obtémpath_id. - Monta chamada para MediaMTX Playback API:
- URL:
settings.media_mtx_playback_url + "/list"
(por padrão:http://mediamtx:9996/list) - Autenticação:
MEDIAMTX_API_USER/MEDIAMTX_API_PASS. - Query params:
path=camera.path_idstart(opcional)end(opcional)
- URL:
- Faz
GETviahttpx.AsyncClient. - Retorna JSON vindo do MediaMTX ou
[]em alguns casos de erro 404.
- GET
/api/v1/camera/{camera_id}/playback-url
Fluxo:
- Usuário autenticado solicita playback para uma câmera específica.
- API gera um token de playback temporário:
- Função:
create_temp_playback_token - Payload inclui:
sub=user_idpath=camera.path_idstartduration
- Expira em
PLAYBACK_TOKEN_EXPIRE_SECONDS(padrão 600s).
- Função:
- API retorna JSON:
{"playbackUrl": "/api/v1/playback/video?token=<TOKEN>"}
- GET
/api/v1/playback/video?token=...
Fluxo:
- Decodifica o token com
decode_temp_playback_token. - Extrai
path,start,duration. - Chama MediaMTX Playback API
/get:- URL base:
settings.media_mtx_playback_url + "/get" - Autenticação via
MEDIAMTX_API_USER / MEDIAMTX_API_PASS. - Query params:
path,start,duration,format=mp4.
- URL base:
- Abre um stream HTTP (
stream=Trueviahttpx) e retransmite para o cliente comovideo/mp4usandoStreamingResponse. - Fecha conexões no final usando
BackgroundTask.
Este fluxo garante:
- Controle de acesso (apenas usuários com token válido).
- Tokens com vida curta, mais seguros.
- Consumo simples via front-end (basta usar a URL retornada).
Toda a lógica de integração com MediaMTX está concentrada em
app/service/mediaMtx_services.py, através da classe MediaMtxService.
self.command_client–httpx.AsyncClientcom timeout de 10s.self.polling_client–httpx.AsyncClientcom timeout de 30s.- Ambos autenticam via
BasicAuth(MEDIAMTX_API_USER, MEDIAMTX_API_PASS). - Base URL:
settings.media_mtx_control_api_url(ex:http://mediamtx:9997).
Método principal:
MediaMtxService.create_and_verify_camera_path
Objetivo:
Criar ou assumir um path no MediaMTX (v3 API) e garantir que ele está pronto para receber/entregar stream.
Passos principais:
-
Normalização e encoding do path
- O
path_name(ex:live/cam1) é URL-encoded para evitar problemas com/e caracteres especiais. - Endpoints montados:
/v3/config/paths/add/{encoded_path}/v3/config/paths/patch/{encoded_path}/v3/config/paths/delete/{encoded_path}/v3/paths/get/{encoded_path}
- O
-
Montagem do payload
- Se
rtsp_urlcomeça com"publisher":source = "publisher"(MediaMTX espera publisher externo conectando no path).
- Senão:
source = rtsp_url(MediaMTX faz pull do RTSP remoto).
- Opções de gravação (
record) serecord=True:recordPath = "/recordings/%path/%Y-%m-%d_%H-%M-%S-%f"recordFormat = "fmp4"recordSegmentDuration = "10s"recordDeleteAfter = "24h"
- Se
-
Tentativa de PATCH (Upsert)
- Primeiro tenta
PATCH /v3/config/paths/patch/{path}:- Se
200 OK: path atualizado/assumido => parte para verificação de prontidão. - Se
404: path não existe, irá tentarADD. - Outros códigos: segue para a estratégia de
ADD.
- Se
- Primeiro tenta
-
Loop de ADD com Retry e Kick
- Antes de criar, faz um blind delete de configs antigas:
POST /v3/config/paths/delete/{path}(ignorando erros).
- Tenta até 20 vezes:
POST /v3/config/paths/add/{path}.- Se sucesso, prossegue.
- Se
400 Bad Request(conflito):- Interpreta como possível conflito de path/sessão.
- Chama
/v3/paths/listpara inspecionar path em runtime. - Se encontrar item com
name == path_name, aplica kick:
- Antes de criar, faz um blind delete de configs antigas:
Função auxiliar:
_get_kick_endpoint
- Recebe
session_typeesession_id. - Mapeia para endpoints padrão do MediaMTX v3:
rtspSession→/v3/rtspsessions/kick/{id}rtmpSession→/v3/rtmpsessions/kick/{id}hlsSession→/v3/hlssessions/kick/{id}webrtcSession→/v3/webrtcsessions/kick/{id}- Fallback:
/v3/sessions/kick/{id}.
Fluxo de kick dentro do retry:
- Chama
/v3/paths/list. - Para o path conflitado:
- Se
sourcetiverid:- Monta endpoint via
_get_kick_endpoint(type, id). - Tenta desconectar o publisher.
- Monta endpoint via
- Para cada
reader:- Mesmo processo, chutando leitores ativos.
- Se
- Reforça a deleção da conf antiga:
POST /v3/config/paths/delete/{path}.
- Aguarda:
0.5sse encontrou publisher.1.0sse não encontrou.
- Tenta
ADDnovamente.
Após sucesso no PATCH ou no ADD, o serviço faz polling:
- Até 10 tentativas.
- Exponencial backoff (0.5s → até 5s).
- Chama
/v3/paths/get/{path}. - Se
200 OK, considera o path pronto (True).
Se não ficar pronto a tempo, lança TimeoutError.
Método:
MediaMtxService.create_camera_path
- Versão simplificada usada ao atualizar uma câmera (PUT).
- Mesma ideia de
PATCH+ fallback paraADDcom retry e kick.
Método:
MediaMtxService.delete_camera_path
POST /v3/config/paths/delete/{path_name}- Se
404, apenas loga que o path já não existe. - Qualquer outro erro gera exceção.
Pasta scripts:
-
Criar/atualizar admin e usuários de teste:
seed_admin.py: criaadmin@sistema.com(senhaadmin123) se não existir.create_test_user.py: cria/atualizatester@test.com(senhatest).list_users.py: lista usuários do banco.
-
Auxílio para MediaMTX:
simulate_camera.sh/simulate_camera.ps1: simulam câmeras com FFMPEG dentro de container Docker, publicando emrtsp://host.docker.internal:8554/live/<nome>.- Vários scripts de debug/testes (
debug_*,verify_*,stress_test_api.py, etc.) foram usados para:- Reproduzir cenários de conflito de path.
- Validar lógica de kick.
- Verificar endpoints da API v3 do MediaMTX.
Esses scripts são especialmente úteis para relatar no TCC o processo de teste e validação da integração com MediaMTX.
Gerenciadas em app/resources/settings/config.py via pydantic-settings.
Campos principais:
DATABASE_URLMEDIA_MTX_HOST(ex:http://mediamtx)CONTROL_API_PORT(ex:9997)HLS_PORT(ex:8888)WEBRTC_PORT(ex:8889)MEDIAMTX_API_USER,MEDIAMTX_API_PASSSECRET_KEY,ALGORITHM,ACCESS_TOKEN_EXPIRE_MINUTES
Campos adicionais (fixos ou default):
MEDIA_MTX_PLAYBACK_PORT = 9996PLAYBACK_TOKEN_SECRET_KEYPLAYBACK_TOKEN_ALGORITHMPLAYBACK_TOKEN_EXPIRE_SECONDS = 600
DATABASE_URL="postgresql://tcc_usr:tcc_pwd@db:5432/tcc_db"
MEDIA_MTX_HOST="http://127.0.0.1"
CONTROL_API_PORT="9997"
HLS_PORT="8888"
WEBRTC_PORT="8889"
MEDIAMTX_API_USER="api-backend"
MEDIAMTX_API_PASS="UMA_SENHA_FORTE_E_SECRETA_AQUI"
SECRET_KEY="dev_secret_key_change_me_in_prod"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=60Arquivo: docker-compose.yml
docker-compose up --buildServiços disponíveis:
- API:
http://localhost:8000 - Docs Swagger:
http://localhost:8000/docs - MediaMTX HLS:
http://localhost:8888 - MediaMTX API v3:
http://localhost:9997 - MediaMTX Playback:
http://localhost:9996 - PostgreSQL:
localhost:5433(para acesso local / ferramentas externas)
Após subir o ambiente pela primeira vez, crie o admin (caso não tenha seed automático):
docker exec -it tcc-postgres psql -U tcc_usr -d tcc_db \
-c "INSERT INTO public.\"user\" (email, password_hash, full_name, user_role_id, is_active, created_at, updated_at) VALUES ('admin@sistema.com', '9bd2e6bb09a1aa991525f397da02abaaf67733b4b760ce96f287a91f5383e461', 'Administrador', 1, true, NOW(), NOW());"O hash corresponde à senha
admin123gerada porcriar_hash_senha(SHA-256).
Alternativamente, use o script seed_admin.py com as variáveis de ambiente corretas.
curl -X POST "http://localhost:8000/api/v1/login" \
-H "Content-Type: application/json" \
-d "{\"email\": \"admin@sistema.com\", \"password\": \"admin123\"}"Guarde o campo access_token da resposta.
Exemplo com uma câmera remota pública (instável):
curl -X POST "http://localhost:8000/api/v1/camera" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <SEU_TOKEN>" \
-d "{
\"name\": \"Câmera Teste\",
\"rtsp_url\": \"rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream\",
\"is_recording\": false
}"A resposta trará, por exemplo:
visualisation_url_hls:http://localhost:8888/live/camera_teste/index.m3u8visualisation_url_webrtc:http://localhost:8889/live/camera_teste
- Abra um player HLS (ex.: https://hls-js.netlify.app/demo/).
- Cole a URL
visualisation_url_hls. - Clique em Play.
- Use a URL retornada em
visualisation_url_webrtc(ex.:http://localhost:8889/live/camera_teste). - Em produção, recomenda-se HTTPS e configuração adequada de STUN/TURN.
./scripts/simulate_camera.sh cam1- Gera um sinal de teste por FFMPEG.
- Publica via RTSP em:
rtsp://host.docker.internal:8554/live/cam1. - O próprio script tenta auto-registrar a câmera na API usando o admin padrão.
.\scripts\simulate_camera.ps1 -CameraName cam1- Mesmo conceito, mas adaptado para Windows.
- Usa a mesma credencial admin.
app/
├── main.py # Inicialização da API e routers
├── controller/
│ ├── usersController.py # Login, usuários, perfil
│ ├── cameraController.py # Câmeras, listing, playback-url
│ ├── recordController.py # Registro de gravações
│ ├── playbackController.py # Endpoint de streaming playback
│ └── videoController.py # Exposição da pasta recordings
├── domain/
│ ├── user.py
│ ├── user_role.py
│ ├── camera.py
│ ├── record.py
│ └── system_settings.py
├── dtos/
│ ├── login.py
│ ├── camera.py
│ ├── record.py
│ └── webhook.py
├── repository/
│ ├── user_repository.py
│ ├── login_repository.py
│ └── camera_repository.py
├── resources/
│ ├── database/
│ │ └── connection.py # engine, create_db_and_tables, seed_user_roles, get_session
│ └── settings/
│ └── config.py # Settings (env vars) + URLs MediaMTX
├── security/
│ ├── security.py # JWT, hash, token de playback
│ └── TokenContext.py # TokenResponse
└── service/
├── user_services.py
├── camera_services.py
├── mediaMtx_services.py # Integração robusta com MediaMTX
└── webhook_services.py # Base para processamento de webhooks- O token JWT pode ter expirado (padrão: 8 horas para login, 10 minutos para playback).
- Verifique se foi copiado sem quebras de linha.
- Faça login novamente.
- Confirme
admin@sistema.com/admin123ou usuários de teste criados via scripts.
- Apenas admin pode:
- Criar/editar/deletar usuários.
- Criar/editar/deletar câmeras.
- Faça login com conta admin.
- Podem estar relacionados a:
- MediaMTX não estar rodando ou inacessível pela API.
- Conflitos de path resolvidos por meio da lógica de kick e retry.
- Verifique os logs do container
mediamtxe da API.
Ao descrever este sistema em um trabalho acadêmico, alguns tópicos técnicos importantes:
-
Arquitetura em Microsserviços/Containers
- Separação clara entre API, DB e servidor de mídia.
- Orquestração com Docker Compose.
-
Persistência e Modelo de Dados
- Uso de SQLModel (integração Pydantic + SQLAlchemy).
- Modelagem de
User,Camera,RecordeUser_Role.
-
Segurança
- Uso de JWT para autenticação stateless.
- RBAC com roles
admineviewer. - Tokens temporários de playback.
-
Integração com MediaMTX
- Uso das APIs v3 (
/v3/config/paths,/v3/paths,/v3/*sessions/kick). - Estratégia de PATCH / ADD + Retry + Kick para garantir consistência em cenários de conflito.
- Mecanismo de playback via HTTP e controle de sessões.
- Uso das APIs v3 (
-
Gravação e Playback
- Gravação no nível do MediaMTX com fmp4 segmentado.
- API de Playback da MediaMTX (
/liste/get). - Tokenização de acesso e re-streaming do vídeo via FastAPI.
-
Ferramentas de Simulação e Testes
- Uso de FFMPEG para simulação de câmeras.
- Scripts de stress test e debug para validar a robustez do sistema.
Recomendado apenas para ambiente de desenvolvimento.
- Criar venv e instalar dependências:
python -m venv venv
source venv/bin/activate # ou venv\Scripts\activate no Windows
pip install -r requirements.txt- Configurar
.envconforme seção anterior. - Subir PostgreSQL e MediaMTX manualmente (ou usar o
docker-composeapenas para eles). - Rodar a API:
uvicorn app.main:app --reloadEste README busca documentar o máximo possível do fluxo interno da aplicação, suas responsabilidades e a integração detalhada com o MediaMTX, servindo como base para descrição técnica no TCC.