video_monitor/app/views/NvrView.py
2026-09-04 18:16:14 +08:00

365 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
NvrView 模块 — 24/7 录像 + FFmpeg 截图
"""
import os
import re
import time
import subprocess
from datetime import timedelta
from app.views.ViewsBase import *
from app.utils.Utils import buildPageLabels
from app.utils.LanguageUtils import LANG_VIEWS_T
from django.http import HttpResponse
from django.shortcuts import render
def record_index(request):
params = f_parseGetParams(request)
stream_id = int(params.get("stream_id", 0) or 0)
stream_name = ""
record_enable = 0
is_recording = False
if stream_id:
from app.models import StreamModel
stream = StreamModel.objects.filter(id=stream_id).first()
if stream:
stream_name = stream.nickname or stream.name or ("#" + str(stream_id))
record_enable = int(stream.record_enable or 0)
try:
from app.recording.manager import get_recording_manager
is_recording = get_recording_manager().is_recording(stream_id)
except Exception:
pass
else:
stream_id = 0
if not stream_id:
from django.shortcuts import redirect
return redirect("/stream/index")
return render(request, "app/record/index.html", {
"stream_id": stream_id,
"stream_name": stream_name,
"record_enable": record_enable,
"is_recording": is_recording,
})
def _sync_stream_recording(stream):
"""根据 record_enable 与转发状态同步录像进程(关闭录像时停止进程,保留历史文件)"""
try:
from app.recording.manager import get_recording_manager
mgr = get_recording_manager()
if int(stream.record_enable or 0) == 1 and int(stream.forward_state or 0) == 1:
mgr.start_stream(stream)
else:
mgr.stop_stream(stream.id)
except Exception as e:
g_logger.warning("sync recording stream=%s err: %s" % (stream.id, str(e)))
def _get_stream(params):
from app.models import StreamModel
sid = int(params.get("stream_id") or params.get("id") or 0)
app = (params.get("app") or "").strip()
name = (params.get("name") or "").strip()
if sid:
return StreamModel.objects.filter(id=sid).first()
if app and name:
return StreamModel.objects.filter(app=app, name=name).first()
return None
def api_openVideoIsRecording(request):
if request.method != 'GET':
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "msg_method_not_supported")})
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return f_responseJson({"code": 0, "msg": __check_msg})
params = f_parseGetParams(request)
stream = _get_stream(params)
if not stream:
return f_responseJson({"code": 0, "msg": "stream not found"})
from app.recording.manager import get_recording_manager
mgr = get_recording_manager()
return f_responseJson({
"code": 1000, "msg": "ok",
"is_recording": mgr.is_recording(stream.id),
"record_enable": stream.record_enable,
})
def api_openStartRecordVideo(request):
if request.method != 'POST':
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "msg_method_not_supported")})
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return f_responseJson({"code": 0, "msg": __check_msg})
params = f_parsePostParams(request)
stream = _get_stream(params)
if not stream:
return f_responseJson({"code": 0, "msg": "stream not found"})
stream.record_enable = 1
stream.save(update_fields=["record_enable"])
# 同进程(嵌入式)立即同步启动;分进程则由领导者进程的同步循环异步接管
_sync_stream_recording(stream)
return f_responseJson({"code": 1000, "msg": "ok"})
def api_openStopRecordVideo(request):
if request.method != 'POST':
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "msg_method_not_supported")})
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return f_responseJson({"code": 0, "msg": __check_msg})
params = f_parsePostParams(request)
stream = _get_stream(params)
if not stream:
return f_responseJson({"code": 0, "msg": "stream not found"})
stream.record_enable = 0
stream.save(update_fields=["record_enable"])
_sync_stream_recording(stream)
return f_responseJson({"code": 1000, "msg": "ok"})
def _scan_disk_recordings(stream_id=None):
"""从磁盘扫描录像分段FFmpeg segment 输出)"""
items = []
try:
base = g_config.storageRecordDir
except Exception:
return items
if not base or not os.path.isdir(base):
return items
for root, _dirs, files in os.walk(base):
for fn in files:
if not fn.lower().endswith((".mp4", ".ts", ".mkv")):
continue
fp = os.path.join(root, fn)
try:
st = os.stat(fp)
except Exception:
continue
sid = 0
parts = fn.split("_", 1)
if parts and parts[0].isdigit():
sid = int(parts[0])
if stream_id and sid != int(stream_id):
continue
from datetime import datetime
start = datetime.fromtimestamp(st.st_mtime)
items.append({
"id": 0,
"stream_id": sid,
"stream_name": "",
"file_path": fp,
"start_time": str(start),
"end_time": str(start),
"duration": 0,
"file_size": st.st_size,
"file_exists": True,
})
items.sort(key=lambda x: x["start_time"], reverse=True)
return items
def api_openRecordIndex(request):
"""分页列出录像分段"""
if request.method != 'GET':
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "msg_method_not_supported")})
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return f_responseJson({"code": 0, "msg": __check_msg})
from app.models import RecordingModel, StreamModel
params = f_parseGetParams(request)
page = int(params.get('p', 1) or 1)
ps = int(params.get('ps', 20) or 20)
sid = params.get('stream_id')
stream_filter = int(sid) if sid else None
qs = RecordingModel.objects.all().order_by('-start_time')
if stream_filter:
qs = qs.filter(stream_id=stream_filter)
db_rows = list(qs.select_related('stream')[:500])
data = []
seen_paths = set()
for r in db_rows:
seen_paths.add(r.file_path)
data.append({
"id": r.id,
"stream_id": r.stream_id,
"stream_name": (r.stream.nickname if r.stream else ""),
"file_path": r.file_path,
"start_time": str(r.start_time),
"end_time": str(r.end_time),
"duration": r.duration,
"file_size": r.file_size,
"file_exists": os.path.isfile(r.file_path) if r.file_path else False,
})
for item in _scan_disk_recordings(stream_filter):
if item["file_path"] in seen_paths:
continue
if item["stream_id"]:
try:
s = StreamModel.objects.filter(id=item["stream_id"]).first()
if s:
item["stream_name"] = s.nickname or s.name
except Exception:
pass
data.append(item)
data.sort(key=lambda x: x.get("start_time", ""), reverse=True)
count = len(data)
skip = (page - 1) * ps
page_rows = data[skip:skip + ps]
page_num = int(count / ps) + (1 if count % ps else 0)
page_data = {
"page": page, "page_size": ps, "page_num": max(1, page_num), "count": count,
"pageLabels": buildPageLabels(page=page, page_num=max(1, page_num), lang=f_parseRequestLang(request)),
}
return f_responseJson({"code": 1000, "msg": "ok", "data": page_rows, "pageData": page_data})
def api_openRecordFile(request):
if request.method != 'GET':
return HttpResponse(b"method not allowed", status=405)
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return HttpResponse(__check_msg.encode("utf-8"), status=403)
params = f_parseGetParams(request)
rid = int(params.get("id", 0) or 0)
fpath = (params.get("path") or "").strip()
fp = None
if rid:
from app.models import RecordingModel
try:
rec = RecordingModel.objects.get(id=rid)
fp = rec.file_path
except Exception:
return HttpResponse(b"not found", status=404)
elif fpath and os.path.isfile(fpath):
try:
base = g_config.storageRecordDir
if base and os.path.commonpath([os.path.abspath(fpath), os.path.abspath(base)]) == os.path.abspath(base):
fp = fpath
except Exception:
pass
if not fp or not os.path.isfile(fp):
return HttpResponse(b"file missing", status=404)
with open(fp, "rb") as f:
data = f.read()
resp = HttpResponse(data, content_type="video/mp4")
resp["Content-Disposition"] = 'inline; filename="%s"' % os.path.basename(fp)
return resp
def api_openRecordDel(request):
if request.method != 'POST':
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "msg_method_not_supported")})
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return f_responseJson({"code": 0, "msg": __check_msg})
params = f_parsePostParams(request)
rid = int(params.get("id", 0) or 0)
fpath = (params.get("path") or "").strip()
if not rid and not fpath:
return f_responseJson({"code": 0, "msg": "missing id or path"})
from app.models import RecordingModel
try:
if rid:
rec = RecordingModel.objects.get(id=rid)
fp = rec.file_path
rec.delete()
else:
fp = fpath
base = g_config.storageRecordDir
if not base or not os.path.isfile(fp):
return f_responseJson({"code": 0, "msg": "file missing"})
if os.path.commonpath([os.path.abspath(fp), os.path.abspath(base)]) != os.path.abspath(base):
return f_responseJson({"code": 0, "msg": "invalid path"})
RecordingModel.objects.filter(file_path=fp).delete()
if fp and os.path.isfile(fp):
os.remove(fp)
return f_responseJson({"code": 1000, "msg": LANG_VIEWS_T(request, "msg_success")})
except Exception as e:
return f_responseJson({"code": 0, "msg": str(e)})
def api_openSnapShot(request):
if request.method != 'POST':
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "msg_method_not_supported")})
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return f_responseJson({"code": 0, "msg": __check_msg})
return f_responseJson({"code": 0, "msg": LANG_VIEWS_T(request, "nvr_snapshot_removed")})
def _snap_http_response(data, status=200):
resp = HttpResponse(data, content_type="image/jpeg", status=status)
resp["Cache-Control"] = "no-store"
return resp
def _capture_snap_file(rtsp_url, snap_path, ffmpeg_cmd):
if os.path.exists(snap_path):
try:
os.remove(snap_path)
except Exception:
pass
command = [
ffmpeg_cmd, "-nostdin", "-loglevel", "quiet", "-rtsp_transport", "tcp",
"-stimeout", "5000000", "-i", rtsp_url, "-frames:v", "1", "-y", snap_path,
]
subprocess.run(command, shell=False, timeout=12, capture_output=True, check=False)
if os.path.exists(snap_path):
with open(snap_path, "rb") as f:
data = f.read()
if len(data) > 100:
return data
return None
def api_openSnap(request):
try:
if request.method != "GET":
return HttpResponse(b"method not allowed", status=405, content_type="text/plain")
__check_ret, __check_msg = f_checkRequestSafe(request)
if not __check_ret:
return HttpResponse(b"forbidden", status=403, content_type="text/plain")
params = f_parseGetParams(request)
app = params.get("app", "").strip()
name = params.get("name", "").strip()
force = str(params.get("force", "0")).strip() == "1"
if not app or not name:
return HttpResponse(b"missing app or name", status=400, content_type="text/plain")
if not re.fullmatch(r"[A-Za-z0-9_.-]{1,50}", app) or not re.fullmatch(r"[A-Za-z0-9_.-]{1,50}", name):
return HttpResponse(b"invalid app or name", status=400, content_type="text/plain")
from app.models import StreamModel
stream = StreamModel.objects.filter(app=app, name=name).only("app", "name").first()
if not stream:
return HttpResponse(b"stream not found", status=404, content_type="text/plain")
app, name = stream.app, stream.name
snap_dir = os.path.join(g_config.storageDir, "snapshots")
snap_path = os.path.join(snap_dir, "%s_%s.jpg" % (app, name))
if not force and os.path.exists(snap_path):
mtime = os.path.getmtime(snap_path)
if (time.time() - mtime) < 30:
with open(snap_path, "rb") as f:
data = f.read()
if len(data) > 100:
return _snap_http_response(data)
rtsp_url = g_zlm.get_rtspUrl(app=app, name=name, request_ip="127.0.0.1")
if not rtsp_url:
return HttpResponse(b"no stream url available", status=404, content_type="text/plain")
os.makedirs(snap_dir, exist_ok=True)
ffmpeg_cmd = g_config.ffmpeg
data = _capture_snap_file(rtsp_url, snap_path, ffmpeg_cmd)
if not data:
# RTSP 首帧可能较慢,短暂等待后重试一次
time.sleep(0.6)
data = _capture_snap_file(rtsp_url, snap_path, ffmpeg_cmd)
if data:
return _snap_http_response(data)
return HttpResponse(b"snap failed", status=404, content_type="text/plain")
except subprocess.TimeoutExpired:
return HttpResponse(b"snap timeout", status=404, content_type="text/plain")
except Exception as e:
return HttpResponse(("snap exception: " + str(e)).encode("utf-8"), status=500, content_type="text/plain")