añadido gitea y medio argos
This commit is contained in:
15
argos/configmaps/configmap-mediamtx.yaml
Normal file
15
argos/configmaps/configmap-mediamtx.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: mediamtx-config
|
||||
namespace: argos-core
|
||||
data:
|
||||
mediamtx.yml: |
|
||||
logLevel: info
|
||||
rtsp: yes
|
||||
rtmp: no
|
||||
hls: no
|
||||
webrtc: yes
|
||||
paths:
|
||||
all:
|
||||
sourceOnDemand: yes
|
||||
9
argos/configmaps/configmap-mosquitto.yaml
Normal file
9
argos/configmaps/configmap-mosquitto.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: mosquitto-conf
|
||||
namespace: argos-core
|
||||
data:
|
||||
mosquitto.conf: |
|
||||
listener 1883 0.0.0.0
|
||||
allow_anonymous true
|
||||
156
argos/configmaps/configmap-orchestrator.yaml
Normal file
156
argos/configmaps/configmap-orchestrator.yaml
Normal file
@@ -0,0 +1,156 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argos-orchestrator-config
|
||||
namespace: argos-core
|
||||
data:
|
||||
settings.yaml: |
|
||||
mqtt:
|
||||
host: mqtt.argos.interna
|
||||
port: 1883
|
||||
topic: frigate/events
|
||||
client_id: argos-core
|
||||
minio:
|
||||
bucket: argos
|
||||
region: us-east-1
|
||||
edges:
|
||||
- name: rpi01
|
||||
base_url: http://10.20.0.10:5000 # URL del Frigate del edge (VPN). 5000 por defecto.
|
||||
api_token: "" # si usas auth, ponlo aquí
|
||||
cameras:
|
||||
- name: cam1
|
||||
rtsp_main: rtsp://10.20.0.10:8554/cam1_main
|
||||
path_mtx: cam1 # para abrir live en MediaMTX: /?path=cam1
|
||||
app.py: |
|
||||
import os, json, time, datetime, sqlite3, subprocess, yaml, requests
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
import paho.mqtt.client as mqtt
|
||||
from minio import Minio
|
||||
|
||||
CFG_PATH="/app/settings.yaml"
|
||||
DB_PATH="/data/argos.db"
|
||||
TMP="/tmp"
|
||||
with open(CFG_PATH,"r") as f:
|
||||
CFG=yaml.safe_load(f)
|
||||
|
||||
# Map quick-lookup: camera -> (edge, cfg)
|
||||
CAM_MAP={}
|
||||
for e in CFG.get("edges", []):
|
||||
for c in e.get("cameras", []):
|
||||
CAM_MAP[c["name"]]=(e, c)
|
||||
|
||||
# MinIO
|
||||
mc=Minio(os.getenv("MINIO_ENDPOINT","s3.argos.interna"),
|
||||
access_key=os.getenv("MINIO_ACCESS_KEY"),
|
||||
secret_key=os.getenv("MINIO_SECRET_KEY"),
|
||||
secure=os.getenv("MINIO_SECURE","false").lower()=="true")
|
||||
BUCKET=CFG["minio"]["bucket"]
|
||||
if not mc.bucket_exists(BUCKET): mc.make_bucket(BUCKET)
|
||||
|
||||
# DB
|
||||
Path("/data").mkdir(parents=True, exist_ok=True)
|
||||
con=sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||
cur=con.cursor()
|
||||
cur.execute("""CREATE TABLE IF NOT EXISTS events(
|
||||
id TEXT PRIMARY KEY, ts INTEGER, edge TEXT, camera TEXT, label TEXT,
|
||||
s3url TEXT, thumb_s3 TEXT
|
||||
)""")
|
||||
con.commit()
|
||||
|
||||
def upload_file(local_path, key, content_type):
|
||||
mc.fput_object(BUCKET, key, local_path, content_type=content_type)
|
||||
return f"s3://{BUCKET}/{key}"
|
||||
|
||||
def fetch_frigate_clip(edge_cfg, ev_id):
|
||||
""" Descarga el clip nativo de Frigate si existe """
|
||||
base=edge_cfg["base_url"]
|
||||
url=urljoin(base, f"/api/events/{ev_id}/clip")
|
||||
headers={}
|
||||
if edge_cfg.get("api_token"):
|
||||
headers["Authorization"]=f"Bearer {edge_cfg['api_token']}"
|
||||
r=requests.get(url, headers=headers, stream=True, timeout=30)
|
||||
if r.status_code!=200: return None
|
||||
tmp=f"{TMP}/{ev_id}.mp4"
|
||||
with open(tmp,"wb") as f:
|
||||
for chunk in r.iter_content(1<<20):
|
||||
if chunk: f.write(chunk)
|
||||
return tmp
|
||||
|
||||
def fetch_frigate_thumb(edge_cfg, ev_id):
|
||||
base=edge_cfg["base_url"]
|
||||
url=urljoin(base, f"/api/events/{ev_id}/thumbnail.jpg")
|
||||
headers={}
|
||||
if edge_cfg.get("api_token"):
|
||||
headers["Authorization"]=f"Bearer {edge_cfg['api_token']}"
|
||||
r=requests.get(url, headers=headers, timeout=10)
|
||||
if r.status_code!=200: return None
|
||||
tmp=f"{TMP}/{ev_id}.jpg"
|
||||
with open(tmp,"wb") as f: f.write(r.content)
|
||||
return tmp
|
||||
|
||||
def record_rtsp(rtsp_url, seconds=30):
|
||||
tmp=f"{TMP}/rtsp_{int(time.time())}.mp4"
|
||||
cmd=["ffmpeg","-nostdin","-y","-rtsp_transport","tcp","-i",rtsp_url,"-t",str(seconds),"-c","copy",tmp]
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
return tmp
|
||||
except Exception as e:
|
||||
print("FFmpeg fallback failed:", e)
|
||||
return None
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
try:
|
||||
payload=json.loads(msg.payload.decode("utf-8"))
|
||||
except Exception as e:
|
||||
print("Bad JSON", e); return
|
||||
|
||||
ev_type=payload.get("type")
|
||||
after=payload.get("after") or {}
|
||||
ev_id=after.get("id") or payload.get("id")
|
||||
cam=after.get("camera") or payload.get("camera")
|
||||
label=after.get("label") or payload.get("label","")
|
||||
|
||||
if ev_type!="new" or not cam or not ev_id: return
|
||||
if cam not in CAM_MAP:
|
||||
print("Unknown camera:", cam); return
|
||||
|
||||
edge_cfg, cam_cfg = CAM_MAP[cam]
|
||||
ts=int(time.time())
|
||||
print(f"[ARGOS] {ev_id} {cam} → try Frigate clip")
|
||||
path_local = fetch_frigate_clip(edge_cfg, ev_id)
|
||||
if not path_local:
|
||||
print(f"[ARGOS] {ev_id} no native clip, fallback RTSP")
|
||||
path_local = record_rtsp(cam_cfg["rtsp_main"], seconds=30)
|
||||
if not path_local:
|
||||
print(f"[ARGOS] {ev_id} failed recording"); return
|
||||
|
||||
# upload clip
|
||||
date=datetime.datetime.utcfromtimestamp(ts)
|
||||
key=f"{cam}/{date.year:04d}/{date.month:02d}/{date.day:02d}/{ts}_{ev_id}.mp4"
|
||||
s3url=upload_file(path_local, key, "video/mp4")
|
||||
try: os.remove(path_local)
|
||||
except: pass
|
||||
|
||||
# thumbnail
|
||||
thumb_local = fetch_frigate_thumb(edge_cfg, ev_id)
|
||||
thumb_s3=None
|
||||
if thumb_local:
|
||||
tkey=f"{cam}/thumbs/{ts}_{ev_id}.jpg"
|
||||
thumb_s3=upload_file(thumb_local, tkey, "image/jpeg")
|
||||
try: os.remove(thumb_local)
|
||||
except: pass
|
||||
|
||||
cur.execute("INSERT OR REPLACE INTO events(id, ts, edge, camera, label, s3url, thumb_s3) VALUES (?,?,?,?,?,?,?)",
|
||||
(ev_id, ts, edge_cfg["name"], cam, label, s3url, thumb_s3))
|
||||
con.commit()
|
||||
print(f"[ARGOS] stored {s3url}")
|
||||
|
||||
def main():
|
||||
m=mqtt.Client(client_id=CFG["mqtt"]["client_id"], clean_session=True)
|
||||
m.connect(CFG["mqtt"]["host"], CFG["mqtt"]["port"], keepalive=60)
|
||||
m.subscribe(CFG["mqtt"]["topic"])
|
||||
m.on_message=on_message
|
||||
m.loop_forever()
|
||||
|
||||
if __name__=="__main__": main()
|
||||
87
argos/configmaps/configmap-panel.yaml
Normal file
87
argos/configmaps/configmap-panel.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argos-panel-config
|
||||
namespace: argos-core
|
||||
data:
|
||||
app.py: |
|
||||
import os, sqlite3, time
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from minio import Minio
|
||||
from urllib.parse import urlparse
|
||||
|
||||
DB="/data/argos.db"
|
||||
mc=Minio(os.getenv("MINIO_ENDPOINT","s3.argos.interna"),
|
||||
access_key=os.getenv("MINIO_ACCESS_KEY"),
|
||||
secret_key=os.getenv("MINIO_SECRET_KEY"),
|
||||
secure=os.getenv("MINIO_SECURE","false").lower()=="true")
|
||||
app=FastAPI()
|
||||
|
||||
def rows(limit=100, camera=None, since=None):
|
||||
q="SELECT id, ts, edge, camera, label, s3url, thumb_s3 FROM events"
|
||||
cond=[]; args=[]
|
||||
if camera: cond.append("camera=?"); args.append(camera)
|
||||
if since: cond.append("ts>=?"); args.append(int(since))
|
||||
if cond: q+=" WHERE "+ " AND ".join(cond)
|
||||
q+=" ORDER BY ts DESC LIMIT ?"; args.append(limit)
|
||||
con=sqlite3.connect(DB); cur=con.cursor()
|
||||
cur.execute(q, tuple(args)); r=cur.fetchall(); con.close()
|
||||
return r
|
||||
|
||||
@app.get("/api/events")
|
||||
def api_events(limit:int=100, camera:str=None, since:int=None):
|
||||
return [dict(id=i, ts=t, edge=e, camera=c, label=l or "", s3url=s, thumb=th or "")
|
||||
for (i,t,e,c,l,s,th) in rows(limit,camera,since)]
|
||||
|
||||
@app.get("/api/url/{event_id}")
|
||||
def presign(event_id: str, expires: int = 600):
|
||||
con=sqlite3.connect(DB); cur=con.cursor()
|
||||
cur.execute("SELECT s3url FROM events WHERE id=?", (event_id,))
|
||||
row=cur.fetchone(); con.close()
|
||||
if not row: raise HTTPException(404, "Not found")
|
||||
s3url=row[0]; p=urlparse(s3url); b=p.netloc; k=p.path.lstrip("/")
|
||||
return {"url": mc.presigned_get_object(b, k, expires=expires)}
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index():
|
||||
return """
|
||||
<!doctype html><meta charset="utf-8"><title>ARGOS Panel</title>
|
||||
<style>body{font-family:system-ui;margin:1.5rem} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
|
||||
.card{border:1px solid #ddd;border-radius:10px;padding:10px} img{width:100%;height:160px;object-fit:cover;border-radius:8px}
|
||||
button{padding:.4rem .6rem;margin-right:.3rem}</style>
|
||||
<h1>ARGOS – Alarmas</h1>
|
||||
<div class="grid" id="grid"></div>
|
||||
<div style="margin-top:1rem"><video id="player" width="960" controls></video></div>
|
||||
<script>
|
||||
const fmt=t=>new Date(t*1000).toLocaleString();
|
||||
async function load(){
|
||||
const r=await fetch('/api/events?limit=100'); const data=await r.json();
|
||||
const g=document.getElementById('grid'); g.innerHTML='';
|
||||
for(const ev of data){
|
||||
const d=document.createElement('div'); d.className='card';
|
||||
const img = ev.thumb ? `<img src="${ev.thumb.replace('s3://','/api/url/THUMB?key=')}" alt="thumb">` : '';
|
||||
d.innerHTML = `${img}<div><b>${ev.camera}</b> — ${fmt(ev.ts)}<br>${ev.label||''}</div>
|
||||
<div style="margin-top:.4rem">
|
||||
<button data-id="${ev.id}" data-action="clip">Ver clip</button>
|
||||
<button data-path="${ev.camera}" data-action="live">En directo</button>
|
||||
</div>`;
|
||||
g.appendChild(d);
|
||||
}
|
||||
g.onclick=async (e)=>{
|
||||
if(e.target.tagName!=='BUTTON') return;
|
||||
const v=document.getElementById('player');
|
||||
if(e.target.dataset.action==='clip'){
|
||||
const id=e.target.dataset.id;
|
||||
const j=await (await fetch('/api/url/'+id)).json();
|
||||
v.src=j.url; v.play();
|
||||
}else if(e.target.dataset.action==='live'){
|
||||
const path=e.target.dataset.path;
|
||||
// usa MediaMTX web player
|
||||
window.open('http://mediamtx.argos.interna/?path='+encodeURIComponent(path),'_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
load(); setInterval(load,10000);
|
||||
</script>
|
||||
"""
|
||||
Reference in New Issue
Block a user