● Zion Boggan
repos/Sightline/backend/rtsp_worker.py
zionboggan.com ↗
86 lines · python
History for this file →
1
"""Continuous RTSP ingestion worker. Connects to an RTSP/HTTP camera stream, runs the ALPR
2
engine on sampled frames, and ingests recognized plates as real reads - continuously, with
3
auto-reconnect. One worker per stream.
4
 
5
    python rtsp_worker.py <rtsp_url> [camera_external_id] [state]
6
 
7
Stops on SIGTERM/SIGINT. Dedup in the pipeline collapses the same plate seen repeatedly.
8
"""
9
from __future__ import annotations
10
 
11
import signal
12
import sys
13
import time
14
from datetime import datetime, timezone
15
 
16
from sqlalchemy import select
17
 
18
from app.db.session import SessionLocal
19
from app.models import Camera, Location, Source
20
from app.services.ingest import ingest_live_reads
21
from app.services.video_alpr import open_stream, process_capture
22
 
23
WINDOW_SECONDS = 12
24
SAMPLE_FPS = 2.0
25
_running = True
26
 
27
 
28
def _stop(*_):
29
    global _running
30
    _running = False
31
 
32
 
33
def main():
34
    if len(sys.argv) < 2:
35
        print("usage: python rtsp_worker.py <rtsp_url> [camera_external_id] [state]")
36
        sys.exit(2)
37
    url = sys.argv[1]
38
    cam_ext = sys.argv[2] if len(sys.argv) > 2 else None
39
    state = sys.argv[3] if len(sys.argv) > 3 else None
40
    signal.signal(signal.SIGTERM, _stop)
41
    signal.signal(signal.SIGINT, _stop)
42
 
43
    db = SessionLocal()
44
    cam = db.scalar(select(Camera).where(Camera.external_camera_id == cam_ext)) if cam_ext else None
45
    loc = db.get(Location, cam.location_id) if cam and cam.location_id else None
46
    src_id = cam.source_id if cam else (db.scalar(select(Source.id).limit(1)))
47
    print(f"RTSP worker: {url} -> camera {cam_ext or '(none)'}", flush=True)
48
 
49
    total = 0
50
    while _running:
51
        try:
52
            cap = open_stream(url)
53
        except Exception as e:
54
            print(f"open failed ({e}); retry in 5s", flush=True)
55
            time.sleep(5)
56
            continue
57
        try:
58
            res = process_capture(cap, sample_fps=SAMPLE_FPS, max_frames=int(WINDOW_SECONDS * SAMPLE_FPS),
59
                                  max_seconds=WINDOW_SECONDS, min_conf=0.45)
60
        except Exception as e:
61
            print(f"capture error: {e}", flush=True)
62
            res = {"plates": []}
63
        finally:
64
            cap.release()
65
 
66
        if res["plates"]:
67
            now = datetime.now(timezone.utc)
68
            reads = [{"plate": p["plate_norm"], "state": state or (cam.state if cam else None),
69
                      "timestamp": now.isoformat(), "camera": cam_ext or "rtsp-stream",
70
                      "latitude": loc.lat if loc else None, "longitude": loc.lon if loc else None,
71
                      "confidence": p["ocr_confidence"]} for p in res["plates"]]
72
            try:
73
                s = ingest_live_reads(db, src_id, reads, label=f"rtsp-{int(now.timestamp())}")
74
                total += s.get("inserted", 0)
75
                print(f"+{s.get('inserted',0)} reads ({[p['plate_norm'] for p in res['plates']]}) total={total}", flush=True)
76
            except Exception as e:
77
                print(f"ingest error: {e}", flush=True)
78
                db.rollback()
79
        if not _running:
80
            break
81
    db.close()
82
    print(f"RTSP worker stopped after {total} reads", flush=True)
83
 
84
 
85
if __name__ == "__main__":
86
    main()