Browse Source

web初步版本一,基于一通道一线程上进行修改

detached
张龙 9 months ago
parent
commit
1f61f60a93
  1. 2
      .idea/FristProject.iml
  2. 2
      .idea/misc.xml
  3. BIN
      DB_table.xlsx
  4. 10
      config.yaml
  5. 11
      core/ChannelManager.py
  6. 15
      core/DBManager.py
  7. 279
      core/ModelManager.py
  8. 102
      core/WarnManager.py
  9. BIN
      model/plugins/Peo_ACL/yolov5s_bs1.om
  10. 4
      model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py
  11. 24
      run.py
  12. 170
      web/API/channel.py
  13. 29
      web/API/user.py
  14. 116
      web/API/viedo.py
  15. 2
      web/__init__.py
  16. 2
      web/main/__init__.py
  17. 24
      web/main/routes.py
  18. BIN
      web/main/static/favicon.ico
  19. BIN
      web/main/static/images/登录/u12.png
  20. 6
      web/main/static/images/登录/u4.svg
  21. BIN
      web/main/static/images/登录/zf.png
  22. 26
      web/main/static/images/登录/zf.svg
  23. 15
      web/main/static/resources/css/headers.css
  24. 33
      web/main/static/resources/css/sign-in.css
  25. BIN
      web/main/static/resources/images/zf.png
  26. 26
      web/main/static/resources/images/zf.svg
  27. 337
      web/main/static/resources/scripts/aiortc-client-new.js
  28. 47
      web/main/static/resources/scripts/base.js
  29. 608
      web/main/static/resources/scripts/channel_manager.js
  30. 4
      web/main/static/resources/scripts/jquery-1.7.1.min.js
  31. 14
      web/main/static/resources/scripts/jquery-3.2.1.min.js
  32. 4
      web/main/static/resources/scripts/jquery-3.2.1.slim.min.js
  33. 5
      web/main/static/resources/scripts/popper.min.js
  34. 46
      web/main/templates/base.html
  35. 291
      web/main/templates/channel_manager.html
  36. 29
      web/main/templates/footer.html
  37. 35
      web/main/templates/header.html
  38. 205
      web/main/templates/index_webrtc.html
  39. 122
      web/main/templates/login.html
  40. 156
      web/main/templates/schedule.html
  41. 146
      web/main/templates/view_main.html
  42. 170
      web/main/templates/登录.html
  43. BIN
      zfbox.db

2
.idea/FristProject.iml

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Remote Python 3.9.2 (sftp://root@192.168.3.48:22/usr/local/miniconda3/bin/python)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="acl392" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/misc.xml

@ -3,7 +3,7 @@
<component name="Black">
<option name="sdkName" value="PyTorch" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Remote Python 3.9.2 (sftp://root@192.168.3.48:22/usr/local/miniconda3/bin/python)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="acl392" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>

BIN
DB_table.xlsx

Binary file not shown.

10
config.yaml

@ -27,15 +27,19 @@ ALLOWED_EXTENSIONS : {'zip'}
#RTSP
RTSP_Check_Time : 600 #10分钟 -- 2024-7-8 取消使用
#max_channel_num
max_channel_num : 4 #最大视频通道数量
#model
model_platform : acl #acl gpu cpu
model_platform : cpu #acl gpu cpu
device_id : 0 #单设备配置
weight_path: /model/weights
yolov5_path: D:/Project/FristProject/model/base_model/yolov5 #使用绝对路径,不同的部署环境需要修改!
cap_sleep_time: 120 #5分钟
cap_sleep_time: 120 #单位秒 -- 5分钟
buffer_len: 100 #分析后画面缓冲区帧数 -- 可以与验证帧率结合确定缓冲区大小
RESET_INTERVAL : 100000 #帧数重置上限
frame_rate : 20 #帧率参考值 -- 后续作用主要基于verify_rate进行帧率控制
verify_rate : 10 #验证帧率--- 也就是视频输出的帧率
verify_rate : 8 #验证帧率--- 也就是视频输出的帧率
warn_video_path: /mnt/zfbox/model/warn/
warn_interval: 120 #报警间隔--单位秒
video_error_count: 3 #单位秒 ---根据验证帧率,判断3秒内都是空帧的话,视频源链接有问题。

11
core/ChannelManager.py

@ -7,6 +7,7 @@ import queue
class ChannelData:
def __init__(self, str_url, int_type, bool_run, deque_length,icount_max):
self.cap = None
self.str_url = str_url #视频源地址
self.int_type = int_type #视频源类型,0-usb,1-rtsp,2-hksdk
self.bool_run = bool_run #线程运行标识
@ -80,7 +81,9 @@ class ChannelManager:
if channel_id in self.channels: #若已经有数据,先删除后再增加
self.channels[channel_id].clear() # 手动清理资源
del self.channels[channel_id]
self.channels[channel_id] = ChannelData(str_url, int_type, bool_run, deque_length,icount_max)
ch_data = ChannelData(str_url, int_type, bool_run, deque_length, icount_max)
self.channels[channel_id] = ch_data
return ch_data
#删除节点
def delete_channel(self, channel_id):
@ -94,15 +97,17 @@ class ChannelManager:
with self.lock:
return self.channels.get(channel_id)
#停止工作线程
#停止工作线程---要把视频采集线程停止掉
def stop_channel(self,channel_id):
with self.lock:
if channel_id == 0:
for clannel_id,clannel_data in self.channels.items():
clannel_data.clear()
clannel_data.cap.running = False
clannel_data.clear() #clear 里面已经停止了通道的工作线程
del self.channels
else:
if channel_id in self.channels:
self.channels[channel_id].cap.running = False
self.channels[channel_id].clear() # 手动清理资源
del self.channels[channel_id]

15
core/DBManager.py

@ -104,8 +104,13 @@ class DBManager():
return bok
#---------------------特定数据库操作函数---------------------
#根据通道ID或者模型ID删除通道和模型间的关联数据 1-通道ID,2-模型ID
def delC2M(self,ID,itype):
'''
#根据通道ID或者模型ID删除通道和模型间的关联数据 1-通道ID,2-模型ID ,注意会有删除没有数据的情况
:param ID:
:param itype:
:return:
'''
#channel2model
if itype ==1:
strsql = f"select ID from channel2model where channel_id={ID};"
@ -113,16 +118,12 @@ class DBManager():
strsql = f"delete from channel2model where channel_id={ID};"
ret = self.do_sql(strsql)
if ret == False:
return False
elif itype ==2:
strsql = f"select ID from channel2model where model_id={ID};"
datas = self.do_select(strsql)
strsql = f"delete from channel2model where model_id={ID};"
ret = self.do_sql(strsql)
if ret == False:
return False
else:
return False
#schedule
@ -130,13 +131,11 @@ class DBManager():
c2m_id = data[0]
strsql = f"delete from schedule where channel2model_id={c2m_id};"
ret = self.do_sql(strsql)
if ret == False:
return False
return True
#删除通道,需要关联删除布防时间,通道和算法的关联表
def delchannel(self,ID):
ret = self.delC2M(ID)
ret = self.delC2M(ID,1)
if ret == False:
return False
#channel

279
core/ModelManager.py

@ -8,6 +8,7 @@ import threading
import importlib.util
import datetime
import math
import copy
import queue
from collections import deque
from core.DBManager import mDBM,DBManager
@ -16,6 +17,8 @@ from myutils.ConfigManager import myCongif
from model.plugins.ModelBase import ModelBase
from core.ChannelManager import ChannelManager
from core.ACLModelManager import ACLModeManger
from core.WarnManager import WarnManager,WarnData
from PIL import Image
@ -25,7 +28,21 @@ class VideoCaptureWithFPS:
self.source = source
self.width = None
self.height = None
self.cap = cv2.VideoCapture(self.source)
# GStreamer
#rtsp_stream = f"rtspsrc location={self.source} ! decodebin ! videoconvert ! appsink"
pipeline = (
f"rtspsrc location={self.source} latency=0 ! "
"rtph264depay ! "
"h264parse ! "
"avdec_h264 ! " # 使用 avdec_h264 代替其他解码器
"videoscale ! "
"video/x-raw,width=640,height=480,framerate=10/1 ! " # 降低分辨率和帧率
"videoconvert ! "
"appsink"
)
self.cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)
# opencv
# self.cap = cv2.VideoCapture(self.source)
if self.cap.isOpened(): #若没有打开成功,在读取画面的时候,已有判断和处理 -- 这里也要检查下内存的释放情况
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
@ -52,12 +69,15 @@ class VideoCaptureWithFPS:
if self.cap.isOpened():
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(self.width,self.height)
# self.fps = fps # 线程保持最大帧率的刷新画面---过高的帧率会影响CPU性能,但过地的帧率会造成帧积压
self.fps = math.ceil(
self.cap.get(cv2.CAP_PROP_FPS) / float(myCongif.get_data("verify_rate"))) # 向上取整。
icount = 0
else:
time.sleep(1)
sleep_time = myCongif.get_data("cap_sleep_time")
print(f"{self.source}视频流,将于{sleep_time}秒后重连!")
time.sleep(sleep_time)
continue
#resized_frame = cv2.resize(frame, (int(self.width / 2), int(self.height / 2)))
with self.read_lock:
@ -88,7 +108,6 @@ class VideoCaptureWithFPS:
self.thread.join()
self.cap.release()
class ModelManager:
def __init__(self):
self.verify_list = ChannelManager() #模型的主要数据 -- 2024-7-5修改为类管理通道数据
@ -109,6 +128,9 @@ class ModelManager:
# acl初始化 -- 一个线程一个 -- 需要验证
if self.model_platform == "acl":
ACLModeManger.init_acl(self.device_id) #acl -- 全程序初始化
self.model_dic = {} # model_id model
# 报警处理线程-全进程独立一个线程处理
self.warnM = None
def __del__(self):
self.logger.debug("释放资源")
@ -151,6 +173,7 @@ class ModelManager:
else:
self.logger.error("{}文件不存在".format(model_path))
return None
self.logger.debug(f"{model_path} 加载成功!!!!")
return md
def getschedule(self,c2m_id,myDBM):
@ -199,43 +222,33 @@ class ModelManager:
def set_last_img(self,):
pass
def verify(self,frame,myModle_list,myModle_data,channel_id,schedule_list,result_list,isdraw=1):
'''验证执行主函数,实现遍历通道关联的模型,调用对应模型执行验证,模型文件遍历执行'''
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
#img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 转换为内存连续存储的数组 --该函数可以待定下是不是所有模型都可以做
# img = frame.to_ndarray(format="bgr24")
#img = frame
# 使用 模型 进行目标检测
i_warn_count = 0 #报警标签
#isverify = False
for i in range(len(myModle_list)): # 遍历通道关联的算法进行检测,若不控制模型数量,有可能需要考虑多线程执行。
model = myModle_list[i]
data = myModle_data[i]
schedule = schedule_list[i]
result = result_list[i]
#验证检测计划,是否在布防时间内
now = datetime.datetime.now() # 获取当前日期和时间
weekday = now.weekday() # 获取星期几,星期一是0,星期天是6
hour = now.hour
result.pop(0) # 保障结果数组定长 --先把最早的结果推出数组
if schedule[weekday][hour] == 1: #不在计划则不进行验证,直接返回图片
# 调用模型,进行检测,model是动态加载的,具体的判断标准由模型内执行 ---- *********
#isverify = True
detections, bwarn, warntext = model.verify(img, data,isdraw) #****************重要
# 对识别结果要部要进行处理
if bwarn: # 整个识别有产生报警
#根据模型设定的时间和占比判断是否
# 绘制报警文本
cv2.putText(img, 'Intruder detected!', (50, (i_warn_count + 1) * 50),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
i_warn_count += 1
result.append(1) #要验证数组修改,是地址修改吗?
else: #没有产生报警也需要记录,统一计算占比
result.append(0)
else:
def verify(self,frame,model,model_data,channel_id,schedule,result,isdraw=1):
'''验证执行主函数,实现遍历通道关联的模型,调用对应模型执行验证'''
#img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = frame
#验证检测计划,是否在布防时间内
now = datetime.datetime.now() # 获取当前日期和时间
weekday = now.weekday() # 获取星期几,星期一是0,星期天是6
hour = now.hour
result.pop(0) # 保障结果数组定长 --先把最早的结果推出数组
detections = None
bwarn = False
warntext = ""
if model and schedule[weekday][hour] == 1: #不在计划则不进行验证,直接返回图片
# 调用模型,进行检测,model是动态加载的,具体的判断标准由模型内执行 ---- *********
#isverify = True
detections, bwarn, warntext = model.verify(img, model_data,isdraw) #****************重要
# 对识别结果要部要进行处理
if bwarn:
# 绘制报警文本
cv2.putText(img, warntext, (50, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
result.append(1) #要验证数组修改,是地址修改吗?
else: #没有产生报警也需要记录,统一计算占比
warntext = ""
result.append(0)
# if not isverify: #没做处理,直接返回的,需要控制下帧率,太快读取没有意义。 --2024-7-5 取消休眠,帧率控制在dowork_thread完成
# time.sleep(1.0/self.frame_rate) #给个默认帧率,不超过30帧,---若经过模型计算,CPU下单模型也就12帧这样
else:
result.append(0)
# 将检测结果图像转换为帧--暂时用不到AVFrame--2024-7-5
# new_frame_rgb_avframe = av.VideoFrame.from_ndarray(img, format="rgb24") # AVFrame
@ -243,15 +256,14 @@ class ModelManager:
# if isinstance(img, np.ndarray): -- 留个纪念
#处理完的图片后返回-bgr模式
img_bgr_ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
#img_bgr_ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# 将检查结果转换为WebP格式图片 --在线程里面完成应该可以减少网页端处理时间
ret,frame_bgr_webp=cv2.imencode('.jpg', img_bgr_ndarray)
ret,frame_bgr_webp=cv2.imencode('.jpg', img)
if not ret:
buffer_bgr_webp = None
else:
buffer_bgr_webp = frame_bgr_webp.tobytes()
return buffer_bgr_webp,img_bgr_ndarray
return buffer_bgr_webp,img,warntext
def dowork_thread(self,channel_id):
'''一个通道一个线程,关联的模型在一个线程检测,局部变量都是一个通道独有'''
@ -267,38 +279,24 @@ class ModelManager:
f"t2.model_name,t1.conf_threshold "
f"from channel2model t1 left join model t2 on t1.model_id = t2.ID where t1.channel_id ={channel_id};")
#print(strsql)
myModels = myDBM.do_select(strsql)
#加载模型 --- 是不是要做个限制,一个视频通道关联算法模块的上限 --- 关联多了一个线程执行耗时较多,造成帧率太低,或者再多线程并发 #?
myModle_list = [] #存放模型对象List 一个模型一个
myModle_data = [] #存放检测参数 一个模型一个
schedule_list = [] #布防策略 -一个模型一个
result_list = [] #检测结果记录 -一个模型一个
warn_last_time =[] #最新的报警时间记录 -一个模型一个
proportion_list = []#占比设定 -一个模型一个
warn_save_count = []#没个模型触发报警后,保存录像的最新帧序号 -一个模型一个
#获取视频通道的模型相关数据-list
for model in myModels:
#基于基类实例化模块类
m = self._import_model("",model[5],model[8]) #动态加载模型处理文件py --需要验证模型文件是否能加载
#m = None
if m:
myModle_list.append(m) #没有成功加载的模型原画输出
myModle_data.append(model)
#model[6] -- c2m_id --布防计划 0-周一,6-周日
schedule_list.append(self.getschedule(model[6],myDBM))
result = [0 for _ in range(model[3] * myCongif.get_data("verify_rate"))] #初始化时间*验证帧率数量的结果list
result_list.append(result)
warn_last_time.append(time.time())
proportion_list.append(model[4]) #判断是否报警的占比
warn_save_count.append(0) #保存录像的最新帧初始化为0
model = myDBM.do_select(strsql,1) #2024-7-12调整规则,一个通道只关联一个模型,表结构暂时不动
if len(model) ==0:
print(f"{channel_id}视频通道没有关联模型,结束线程!")
return
#基于基类实例化模块类
m = self._import_model("",model[5],model[8]) #动态加载模型处理文件py
schedule = self.getschedule(model[6], myDBM)
result = [0 for _ in range(model[3] * myCongif.get_data("verify_rate"))] # 初始化时间*验证帧率数量的结果list
#model[6] -- c2m_id --布防计划 0-周一,6-周日
warn_last_time = time.time()
proportion = model[4] #判断是否报警的占比
warn_save_count = 0 #保存录像的最新帧初始化为0
#开始拉取画面循环检测
cap = None
#iread_count =0 #失败读取的次数
cap = channel_data.cap
last_frame_time = time.time() #初始化个读帧时间
cap_sleep_time = myCongif.get_data("cap_sleep_time")
#可以释放数据库资源
del myDBM
warn_interval = myCongif.get_data("warn_interval")
@ -310,53 +308,40 @@ class ModelManager:
time.sleep(self.frame_interval - elapsed_time) #若小于间隔时间则休眠
last_frame_time = time.time()
#*********取画面*************
if not cap: #第一次需要打开视频流
try:
cap = self._open_view(channel_data.str_url,channel_data.int_type) #创建子线程读画面
except:
self.logger.error("打开视频参数错误,终止线程!")
return
ret,frame = cap.read() #除了第一帧,其它应该都是有画面的
if not ret:
# if iread_count > 30: #2024-7-8 重连接机制放VideoCaptureWithFPS
# self.logger.warning(f"通道-{channel_id}:view disconnected. Reconnecting...")
# cap.release()
# cap = None
# time.sleep(cap_sleep_time)
# else:
# iread_count += 1
continue #没读到画面继续
#执行图片推理 -- 如何没有模型或不在工作时间,返回的是原画,要不要控制下帧率? -- 在verify中做了sleep
buffer_bgr_webp,img_bgr_ndarray = self.verify(frame,myModle_list,myModle_data,channel_id,schedule_list,result_list)
#执行图片推理
buffer_bgr_webp,img_bgr_ndarray,warn_text = self.verify(frame,m,model,channel_id,schedule,result)
#分析图片放入内存中
channel_data.add_deque(img_bgr_ndarray) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据
channel_data.increment_counter() #帧序列加一
#channel_data.increment_counter() #帧序列加一
# 一直更新最新帧,提供网页端显示
channel_data.update_last_frame(buffer_bgr_webp)
#print(f"{channel_id}--Frame updated at:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
#验证result_list -是否触发报警要求 --遍历每个模型执行的result
for i in range(len(result_list)):
result = result_list[i]
proportion = proportion_list[i]
if warn_text:
#验证result -是否触发报警要求 --遍历每个模型执行的result
count_one = float(sum(result)) #1,0 把1累加的和就是1的数量
ratio_of_ones = count_one / len(result)
#self.logger.debug(result)
if ratio_of_ones >= proportion: #触发报警
# 基于时间间隔判断
current_time = time.time()
elapsed_time = current_time - warn_last_time[i]
elapsed_time = current_time - warn_last_time
if elapsed_time < warn_interval:
continue
warn_last_time[i] = current_time
model_name = myModle_data[i][7]
w_s_count = warn_save_count[i]
buffer_count = channel_data.get_counter()
self.save_warn(model_name,w_s_count,buffer_count,channel_data.copy_deque(),
cap.width,cap.height,channel_id,None,self.FPS,self.fourcc)
self.send_warn()
#更新帧序列号
warn_save_count[i] = buffer_count
warn_last_time = current_time
# 处理报警
warn_data = WarnData()
warn_data.model_name = model[7]
warn_data.warn_text = warn_text
warn_data.img_buffer = channel_data.copy_deque() # 深度复制缓冲区
warn_data.width = cap.width
warn_data.height = cap.height
warn_data.channel_id = channel_id
self.warnM.add_warn_data(warn_data)
# #更新帧序列号 #加了报警间隔 --buffer的长度小于间隔
# warn_save_count = buffer_count
#结果记录要清空
for i in range(len(result)):
result[i] = 0
@ -369,72 +354,14 @@ class ModelManager:
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
#结束线程
cap.release() #视频采集线程结束
if context:#ACL线程中反初始化内容 -- 若线程异常退出,这些资源就不能正常释放了
#先释放每个模型资源
for model in myModle_list:
del model
del model
#再释放context
ACLModeManger.th_del_acl(context)
#cv2.destroyAllWindows()
def save_warn(self,model_name,w_s_count,buffer_count,buffer,width,height,channnel_id,myDBM,FPS,fourcc):
'''
保存报警信息 --- 涉及到I/O操作可以通过线程取执行 -- 避免主线程阻塞 --还未验证-2024-7-6
:param model_name: 模型名称如人员入侵
:param w_s_count: 报警已存储的最新帧序列
:param buffer_count: 当前视频缓冲区的最新帧序列
:param buffer: 视频缓存区
:param width: 视频画面的width
:param height: 视频画面的height
:param channnel_id: 视频通道ID
:return: ret 数据库操作记录
'''
return
def save_warn_th(model_name,w_s_count,buffer_count,buffer,width,height,channnel_id,myDBM,FPS,fourcc):
now = datetime.datetime.now() # 获取当前日期和时间
current_time_str = now.strftime("%Y-%m-%d_%H-%M-%S")
filename = f"{channnel_id}_{current_time_str}"
save_path = myCongif.get_data("warn_video_path")
#保存视频
video_writer = cv2.VideoWriter(f"{save_path}{filename}.mp4", fourcc, FPS, (width, height))
if not video_writer.isOpened():
print(f"Failed to open video writer for model/warn/{filename}.mp4")
return False
ilen = len(buffer)
istart = 0;
iend = ilen
if buffer_count < w_s_count or (buffer_count-w_s_count) > ilen: #buffer_count重置过
#buffer区,都保存为视频
istart = 0
else:#只取差异的缓冲区大小
istart = ilen - (buffer_count-w_s_count)
for i in range(istart,iend):
video_writer.write(buffer[i])
video_writer.release()
#保存图片
ret = cv2.imwrite(f"model/warn/{filename}.png",buffer[-1])
#buffer使用完后删除
del buffer
if not ret:
print("保存图片失败")
return False
#保存数据库
myDBM = DBManager()
myDBM.connect()
strsql = (f"INSERT INTO warn (model_name ,video_path ,img_path ,creat_time,channel_id ) "
f"Values ('{model_name}','model/warn/{filename}.mp4','model/warn/{filename}.png',"
f"'{current_time_str}','{channnel_id}');")
ret = myDBM.do_sql(strsql)
del myDBM #释放数据库连接资源
return ret
th_chn = threading.Thread(target=save_warn_th,
args=(model_name,w_s_count,buffer_count,buffer,width,height,channnel_id,None,FPS,fourcc,)) # 一个视频通道一个线程,线程句柄暂时部保留
th_chn.start()
def send_warn(self):
'''发送报警信息'''
pass
@ -454,18 +381,34 @@ class ModelManager:
strsql = f"select id,ulr,type from channel where is_work = 1 and id = {channel_id};" #单通道启动检测线程
datas = mDBM.do_select(strsql)
for data in datas:
# img_buffer = deque(maxlen=myCongif.get_data("buffer_len")) #创建个定长的视频buffer
# img = None
# icout = 0 #跟img_buffer对应,记录进入缓冲区的帧序列号
# run_data = [data[1],data[2],True,img_buffer,img,icout]
# self.verify_list[data[0]] = run_data #需要验证重复情况#? channel_id, str_url, int_type, bool_run, deque_length
self.verify_list.add_channel(data[0],data[1],data[2],True,myCongif.get_data("buffer_len"),myCongif.get_data("RESET_INTERVAL"))
# channel_id, str_url, int_type, bool_run, deque_length
c_data = self.verify_list.add_channel(data[0],data[1],data[2],True,
myCongif.get_data("buffer_len"),myCongif.get_data("RESET_INTERVAL"))
# 启动该通道的视频捕获线程 --把视频捕获线程,放主线程创建
c_data.cap = self._open_view(c_data.str_url, c_data.int_type) # 创建子线程读画面-把cap给模型就行--
th_chn = threading.Thread(target=self.dowork_thread, args=(data[0],)) #一个视频通道一个线程,线程句柄暂时部保留
th_chn.start()
# 启动告警线程
if self.warnM is None:
self.warnM = WarnManager()
self.warnM.start_warnmanager_th()
def stop_work(self,channel_id=0):
'''停止工作线程,0-停止所有,非0停止对应通道ID的线程'''
self.verify_list.stop_channel(channel_id)
if channel_id == 0:
#停止告警线程
self.warnM.brun = False
def restartC2M(self,channel_id):
'''
修改通道管理的算法模型后需要对该通道算法执行部分重新加载执行
:param channel_id:
:return:
'''
pass
#print(f"Current working directory (ModelManager.py): {os.getcwd()}")

102
core/WarnManager.py

@ -0,0 +1,102 @@
import threading
import queue
import datetime
import cv2
from core.DBManager import DBManager
from myutils.ConfigManager import myCongif
class WarnData:
def __init__(self):
self.width = None #视频画面的width
self.height = None #视频画面的height
self.channel_id = None
self.model_name = None #模型名称,如人员入侵
self.img_buffer = None #视频缓冲区 赋值时要拷贝一个备份
self.warn_text = None
self.channel_name = None
class WarnManager:
def __init__(self):
self.warn_q = queue.Queue() #线程安全
self.brun = True
# 保存视频相关内容
self.FPS = myCongif.get_data("verify_rate") # 视频帧率--是否能实现动态帧率
self.fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 使用 mp4 编码
def __del__(self):
pass
def add_warn_data(self,warn_data):
self.warn_q.put(warn_data)
def th_warnmanager(self):
myDBM = DBManager()
myDBM.connect()
while self.brun:
warn_data = self.warn_q.get()
self.save_warn(warn_data.model_name,warn_data.img_buffer,warn_data.width,warn_data.height,
warn_data.channel_id,self.FPS,self.fourcc,myDBM)
self.send_warn()
del warn_data.img_buffer
del warn_data
def start_warnmanager_th(self):
th_warn = threading.Thread(target=self.th_warnmanager) # 一个视频通道一个线程,线程句柄暂时部保留
th_warn.start()
def stop_warnmanager_th(self):
self.brun = False
del self.warn_q
def send_warn(self):
'''发送报警信息'''
pass
def save_warn(self,model_name,buffer,width,height,channnel_id,FPS,fourcc,myDBM):
'''
保存报警信息 --- 涉及到I/O操作可以通过线程取执行 -- 避免主线程阻塞 --还未验证-2024-7-6
:param model_name: 模型名称如人员入侵
:param w_s_count: 报警已存储的最新帧序列
:param buffer_count: 当前视频缓冲区的最新帧序列
:param buffer: 视频缓存区
:param width: 视频画面的width
:param height: 视频画面的height
:param channnel_id: 视频通道ID
:return: ret 数据库操作记录
'''
now = datetime.datetime.now() # 获取当前日期和时间
current_time_str = now.strftime("%Y-%m-%d_%H-%M-%S")
filename = f"{channnel_id}_{current_time_str}"
save_path = myCongif.get_data("warn_video_path")
# 保存视频
video_writer = cv2.VideoWriter(f"{save_path}{filename}.mp4", fourcc, FPS, (width, height))
if not video_writer.isOpened():
print(f"Failed to open video writer for model/warn/{filename}.mp4")
return False
ilen = len(buffer)
istart = 0;
iend = ilen
for i in range(len(buffer)):
video_writer.write(buffer[i])
video_writer.release()
# 保存图片
ret = cv2.imwrite(f"model/warn/{filename}.png", buffer[-1])
# buffer使用完后删除
del buffer
if not ret:
print("保存图片失败")
return False
# 保存数据库
strsql = (f"INSERT INTO warn (model_name ,video_path ,img_path ,creat_time,channel_id ) "
f"Values ('{model_name}','model/warn/{filename}.mp4','model/warn/{filename}.png',"
f"'{current_time_str}','{channnel_id}');")
ret = myDBM.do_sql(strsql)
del myDBM # 释放数据库连接资源
return ret

BIN
model/plugins/Peo_ACL/yolov5s_bs1.om

Binary file not shown.

4
model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py

@ -51,7 +51,7 @@ class Model(ModelBase):
# 模型推理, 得到模型输出
outputs = None
#outputs = self.execute([img,])#创建input,执行模型,返回结果 --失败返回None
outputs = self.execute([img,])#创建input,执行模型,返回结果 --失败返回None
filtered_pred_all = None
bwarn = False
@ -85,7 +85,7 @@ class Model(ModelBase):
continue #没产生报警-继续
#产生报警 -- 有一个符合即可
bwarn = True
warn_text = "Intruder detected!"
warn_text = "People Intruder detected!"
img_dw = draw_bbox(filtered_pred_all, image, (0, 255, 0), 2, labels_dict) # 画出检测框、类别、概率
#cv2.imwrite('img_res.png', img_dw)
return filtered_pred_all, bwarn, warn_text

24
run.py

@ -1,13 +1,29 @@
from core.ViewManager import mVManager
from web import create_app
from core.ModelManager import mMM
import os
import platform
import shutil
import asyncio
from hypercorn.asyncio import serve
from hypercorn.config import Config
from core.DBManager import mDBM
print(f"Current working directory (run.py): {os.getcwd()}")
web = create_app()
async def run_quart_app():
config = Config()
config.bind = ["0.0.0.0:5001"]
await serve(web, config)
def test():
area_id = 1
c_name = "55"
strsql = f"select ID from channel where area_id={area_id} and channel_name={c_name};"
data = mDBM.do_select(strsql, 1)
if data:
print("有值")
else:
print("无值")
if __name__ == '__main__':
system = platform.system()
@ -22,5 +38,7 @@ if __name__ == '__main__':
print(free/(1024*1024))
mMM.start_work() # 启动所有通道的处理
#mVManager.start_check_rtsp() #线程更新视频在线情况
web.run(debug=True,port=5001,host="0.0.0.0")
asyncio.run(run_quart_app())

170
web/API/channel.py

@ -4,6 +4,9 @@ from . import api
from web.common.utils import login_required
from core.DBManager import mDBM
from myutils.ReManager import mReM
from core.ModelManager import mMM
import cv2
import base64
@api.route('/channel/tree',methods=['GET'])
@login_required
@ -15,11 +18,15 @@ async def channel_tree(): #获取通道树
@api.route('/channel/list',methods=['GET'])
async def channel_list(): #获取通道列表 --分页查询,支持区域和通道名称关键字查询
strsql = ("select t2.area_name ,t1.ID ,t1.channel_name ,t1.ulr ,t1.'type' ,t1.status ,t1.element_id"
" from channel t1 left join area t2 on t1.area_id = t2.id where t1.is_work=1;")
strsql = ("select t2.area_name,t1.ID,t1.channel_name,t1.ulr,t1.'type',t1.status,t1.element_id,t4.model_name "
"from channel t1 left join area t2 on t1.area_id = t2.id "
"left JOIN channel2model t3 on t1.ID = t3.channel_id "
"left JOIN model t4 on t3.model_id = t4.ID "
"where t1.is_work=1 order by area_name desc;")
data = mDBM.do_select(strsql)
channel_list = [{"area_name": channel[0], "ID": channel[1], "channel_name": channel[2], "ulr": channel[3],
"type": channel[4], "status": channel[5], "element_id": channel[6]} for channel in data]
"type": channel[4], "status": channel[5],
"element_id": channel[6],"model_name":channel[7]} for channel in data]
return jsonify(channel_list)
@api.route('/channel/info',methods=['GET'])
@ -29,34 +36,85 @@ async def channel_info(): #获取通道信息 ---- list已获取详情
@api.route('/channel/add',methods=['POST'])
@login_required
async def channel_add(): #新增通道
area_id = (await request.form)['area_id']
channel_name = (await request.form)['channel_name']
url = (await request.form)['url']
if mReM.is_valid_rtsp_url(url) is not True:
async def channel_add(): #新增通道 -- 2024-8-1修改为与修改通道用一个接口
json_data = await request.get_json()
area = json_data.get('area')
cName = json_data.get('cName')
Rtsp = json_data.get('Rtsp')
cid = int(json_data.get('cid'))
if mReM.is_valid_rtsp_url(Rtsp) is not True:
reStatus = 0
reMsg = 'rtsp地址不合法'
return jsonify({'status': reStatus, 'msg': reMsg})
strsql = f"select area_name from area where id = {area_id};"
strsql = f"select id from area where area_name = '{area}';"
ret = mDBM.do_select(strsql,1)
if ret:
strsql = (f"INSERT INTO channel (area_id,channel_name,ulr,'type',status) values "
f"({area_id},'{channel_name}','{url}',1,0);")
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '添加通道成功'
else:
strsql = f"select ID from channel where area_id={ret[0]} and channel_name={cName};"
data = mDBM.do_select(strsql, 1)
if data: #有值--代表重复了
reStatus = 0
reMsg = '添加通道失败,请联系技术支持!'
reMsg = "同一区域内的通道名称不能相同!"
else:
if cid == -1:
strsql = (f"INSERT INTO channel (area_id,channel_name,ulr,'type',status) values "
f"({ret[0]},'{cName}','{Rtsp}',1,0);")
else:
strsql = (f"UPDATE channel SET area_id={ret[0]},channel_name='{cName}'"
f",ulr='{Rtsp}' where ID={cid};")
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
if cid == -1:
reMsg = '添加通道成功'
# 对新增的视频通道进行视频采集和
strsql = f"select ID from channel where area_id={ret[0]} and channel_name={cName};"
data = mDBM.do_select(strsql, 1)
if data:
cid = data[0]
mMM.start_work(cid)
else:
print("这里不应该没有值!!!!")
else:
reMsg = '修改通道成功'
#需要先停再启动---不区分有没有修改URL了,就当有修改使用
mMM.stop_work(cid)
mMM.start_work(cid)
else:
reStatus = 0
if cid == -1:
reMsg = '添加通道失败,请联系技术支持!'
else:
reMsg = '修改通道信息失败,请联系技术支持!'
else:
reStatus = 0
reMsg = '错误的通道ID,请修改'
reMsg = '错误的区域ID,请修改'
return jsonify({'status':reStatus,'msg':reMsg})
@api.route('/channel/img',methods=['POST'])
@login_required
async def channel_img():
json_data = await request.get_json()
cid = int(json_data.get('cid'))
channel_data = mMM.verify_list.get_channel(cid)
if channel_data:
# 执行视频传输
ret,frame = channel_data.cap.read()
if ret:
# 将帧转换为JPEG格式
_, buffer = cv2.imencode('.jpg', frame)
# 将图像数据编码为Base64
img_base64 = base64.b64encode(buffer).decode('utf-8')
# 返回JSON响应
return jsonify({"image": img_base64})
else:
return jsonify({"error": "Failed to capture frame"}), 404
else:
return jsonify({"error": "Channel not found"}), 500
@api.route('/channel/change',methods=['POST'])
@login_required
async def channel_change(): #修改通道信息
async def channel_change(): #修改通道信息 -- 已弃用
area_id = (await request.form)['area_id']
channel_id = (await request.form)['channel_id']
channel_name = (await request.form)['channel_name']
@ -84,9 +142,11 @@ async def channel_change(): #修改通道信息
@api.route('/channel/del',methods=['POST'])
@login_required
async def channel_del(): #删除通道
channel_id = (await request.form)['channel_id']
json_data = await request.get_json()
cid = int(json_data.get('cid'))
mMM.stop_work(cid)
#删除该通道和算法的关联信息:布防时间,算法模型数据----使用外键级联删除会方便很多,只要一个删除就可以
ret = mDBM.delchannel(channel_id)
ret = mDBM.delchannel(cid)
if ret == True:
reStatus = 1
reMsg = '删除通道信息成'
@ -97,7 +157,7 @@ async def channel_del(): #删除通道
@api.route('/channel/check',methods=['GET'])
@login_required
async def channel_check(): #检查通道视频的在线情况--可以获取全部 ID-0,全部,1*具体通道ID--10分钟会更新一次在线状态
async def channel_check(): #检查通道视频的在线情况--可以获取全部 ID-0,全部,1*具体通道ID--10分钟会更新一次在线状态-- 要验证
ID = request.args.get('ID')
if ID:
ID = int(ID)
@ -118,6 +178,40 @@ async def channel_check(): #检查通道视频的在线情况--可以获取全
reMsg = "参数错误"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/C2M',methods=['POST'])
@login_required
async def channel_to_model():
'''获取通道关联模型的相关数据'''
json_data = await request.get_json()
cid = int(json_data.get('cid'))
#获取算法数据
strsql = "select ID,model_name from model;"
model_datas = mDBM.do_select(strsql)
m_datas = []
if model_datas:
m_datas = [{'ID':row[0],'model_name':row[1]} for row in model_datas]
#获取c2m数据
strsql = f"select ID,model_id,check_area,polygon,conf_thres,iou_thres from channel2model where channel_id = {cid};"
c2m_data = mDBM.do_select(strsql,1)
#schedule数据
schedule = []
if c2m_data:
c2m_data_json = [{'model_id':c2m_data[1],'check_area':c2m_data[2],'polygon':c2m_data[3],
'conf_thres':c2m_data[4],'iou_thres':c2m_data[5]}]
c2m_id = c2m_data[0]
strsql = f"select day,hour,status from schedule where channel2model_id ={c2m_id} order by hour asc,day asc;"
schedule_datas = mDBM.do_select(strsql)
if schedule_datas:
schedule = [{'day': row[0], 'hour': row[1], 'status': row[2]} for row in schedule_datas]
else:
schedule = []
else:
schedule = []
c2m_data_json = []
#返回数据
redata = {"m_datas":m_datas,"c2m_data":c2m_data_json,"schedule":schedule}
return jsonify(redata)
@api.route('/channel/area/list',methods=['GET'])
@login_required
async def channel_area_list(): #获取区域列表
@ -175,29 +269,29 @@ async def channel_area_add(): #添加区域
reMsg = '新增区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/linklist',methods=['GET'])
@api.route('/channel/model/list',methods=['GET'])
@login_required
async def channel_model_linklist(): #获取算法列表 --关联算法时展示
ID = request.args.get('ID') #通道ID
strsql = (f"select t1.ID,t2.model_name from channel2model t1 left join model t2 "
f"on t1.model_id=t2.ID where channel_id={ID};")
async def channel_model_list(): #获取算法列表
strsql = "select ID,model_name,version from model;"
datas = mDBM.do_select(strsql)
reMsg = {}
if datas:
reMsg = [{'ID': data[0], 'model_name': data[1]} for data in datas]
reMsg = [{'ID': data[0], 'model_name': data[1],'version':data[2]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/model/list',methods=['GET'])
@api.route('/channel/model/linklist',methods=['GET'])
@login_required
async def channel_model_list(): #获取算法列表
strsql = "select ID,model_name,version from model;"
async def channel_model_linklist(): #获取算法列表 --关联算法时展示 --没调用。。
ID = request.args.get('ID') #通道ID
strsql = (f"select t1.ID,t2.model_name from channel2model t1 left join model t2 "
f"on t1.model_id=t2.ID where channel_id={ID};")
datas = mDBM.do_select(strsql)
reMsg = {}
if datas:
reMsg = [{'ID': data[0], 'model_name': data[1],'version':data[2]} for data in datas]
reMsg = [{'ID': data[0], 'model_name': data[1]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/model/linkmodel',methods=['POST'])
@api.route('/channel/model/linkmodel',methods=['POST']) #--没调用。。
@login_required
async def channel_model_linkmodel(): #获取算法列表 --关联算法时展示 #?关联算法时需要初始化布防计划,同样删除的需要删除
channel_id = (await request.form)['channel_id']
@ -234,7 +328,7 @@ async def channel_model_getarea(): #获取算法区域的备注信息 --- 同时
@api.route('/channel/model/changearea',methods=['POST'])
@login_required
async def channel_model_changearea(): #修改算法区域信息
async def channel_model_changearea(): #修改算法检测区域信息
ID = (await request.form)['ID']
check_area = (await request.form)['check_area']
check_x = (await request.form)['polygon']
@ -243,6 +337,8 @@ async def channel_model_changearea(): #修改算法区域信息
if ret == True:
reStatus = 1
reMsg = '修改算法检测区域成功'
#需要重启视频通道的执行程序 --需要cid
#?
else:
reStatus = 0
reMsg = '新修改算法检测区域失败,请联系技术支持!'
@ -250,7 +346,7 @@ async def channel_model_changearea(): #修改算法区域信息
@api.route('/channel/model/changethreshold',methods=['POST'])
@login_required
async def channel_model_changthreshold(): #修改算法区域信息
async def channel_model_changthreshold(): #修改算法阈值
ID = (await request.form)['ID']
conf_threshold = (await request.form)['conf_threshold']
@ -258,10 +354,10 @@ async def channel_model_changthreshold(): #修改算法区域信息
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '修改算法检测区域成功'
reMsg = '修改算法的阈值成功'
else:
reStatus = 0
reMsg = '新修改算法检测区域失败,请联系技术支持!'
reMsg = '新修改算法的阈值失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/getschedule',methods=['GET'])

29
web/API/user.py

@ -1,5 +1,5 @@
import os
from quart import Quart, render_template, request, session, redirect, url_for,jsonify,send_file
from quart import Quart, render_template, request, session, redirect, url_for,jsonify,send_file,flash
from quart_sqlalchemy import SQLAlchemy
from quart_session import Session
from web.common.utils import generate_captcha,login_required
@ -19,13 +19,23 @@ async def user_get_code(): #获取验证码
@api.route('/user/login',methods=['POST'])
async def user_login(): #用户登录
username = (await request.form)['username']
password = (await request.form)['password']
captcha = (await request.form)['captcha']
try:
form = await request.form
username = form['username']
password = form['password']
captcha = form['captcha']
except Exception as e:
await flash('请求数据格式错误', 'error')
return redirect(url_for('main.login'))
#return jsonify({'error': '请求数据格式错误'}), 400
if captcha != session.get('captcha'):
#验证码验证过后,需要失效
print(session.get('captcha'))
return '验证码错误', 400
# 验证码验证过后,需要失效
session.pop('captcha', None)
await flash('验证码错误', 'error')
return redirect(url_for('main.login'))
#return jsonify({'error': '验证码错误'}), 400
#return 'captcha error!', 400
#比对用户名和密码
strsql = f"select password from user where username = '{username}'"
db_password = mDBM.do_select(strsql,1)
@ -33,8 +43,9 @@ async def user_login(): #用户登录
if db_password[0] == password: #后续需要对密码进行MD5加默
print("登录成功")
session['user'] = username
return redirect(url_for('main.get_html', html='实时预览.html'))
return '用户名或密码错误', 400
return redirect(url_for('main.get_html', html='view_main.html'))
await flash('用户名或密码错误', 'error')
return redirect(url_for('main.login'))
@api.route('/user/userinfo',methods=['GET'])
@login_required

116
web/API/viedo.py

@ -5,6 +5,7 @@ from core.ModelManager import mMM
from core.DBManager import mDBM
from myutils.ConfigManager import myCongif
import logging
import time
# 配置日志
logging.basicConfig(level=logging.INFO)
@ -143,18 +144,44 @@ async def get_stats(peer_connection):
@api.websocket('/ws/video_feed/<int:channel_id>')
async def ws_video_feed(channel_id):
print(f"New connection for channel: {channel_id}")
channel_data = mMM.verify_list.get_channel(channel_id)
frame_rate = myCongif.get_data("frame_rate")
while channel_data.bool_run: #这里的多线程并发,还需要验证检查
frame = channel_data.get_last_frame()
if frame is not None:
#img = frame.to_ndarray(format="bgr24")
# ret, buffer = cv2.imencode('.jpg', frame)
# if not ret:
# continue
# frame = buffer.tobytes()
await websocket.send(frame)
await asyncio.sleep(1.0 / frame_rate) # Adjust based on frame rate
if channel_data:
verify_rate = int(myCongif.get_data("verify_rate"))
frame_interval = 1.0 / verify_rate #用于帧率控制
error_max_count = verify_rate * int(myCongif.get_data("video_error_count")) #视频帧捕获失败触发提示的上限
sleep_time = int(myCongif.get_data("cap_sleep_time"))
last_frame_time = time.time() # 初始化个读帧时间
icount = 0
while channel_data.bool_run: #这里的多线程并发,还需要验证检查
# 帧率控制帧率
current_time = time.time()
elapsed_time = current_time - last_frame_time
if elapsed_time < frame_interval:
await asyncio.sleep(frame_interval - elapsed_time) # 若小于间隔时间则休眠
last_frame_time = time.time()
#执行视频传输
frame = channel_data.get_last_frame()
if frame is not None:
#img = frame.to_ndarray(format="bgr24")
# ret, buffer = cv2.imencode('.jpg', frame)
# if not ret:
# continue
# frame = buffer.tobytes()
icount = 0
await websocket.send(frame)
else:
icount += 1
if icount > error_max_count:
icount = 0
error_message = b"video_error"
await websocket.send(error_message)
await asyncio.sleep(sleep_time*1000) #等待视频重连时间
else:
print("没有通道数据!")
error_message = b"client_error"
await websocket.send(error_message)
await asyncio.sleep(0.1) #等0.1秒前端处理时间
@api.route('/shutdown', methods=['POST'])
async def shutdown():#这是全关 --需要修改
@ -163,24 +190,59 @@ async def shutdown():#这是全关 --需要修改
pcs.clear()
return 'Server shutting down...'
@api.route('/viewlist', methods=['GET'])
async def viewlist():#视频列表
count = request.args.get('count')
channel_list = []
element_list = []
name_list = []
if count:
strsql = (f"select t1.ID,t1.element_id,t1.channel_name,t2.area_name from channel t1 left join "
f"area t2 on t1.area_id =t2.id where element_id between 0 and {count};")
datas = mDBM.do_select(strsql)
for data in datas:
channel_list.append(data[0])
element_list.append(data[1])
name_list.append(f"{data[3]}--{data[2]}")
return jsonify({'clist': channel_list, 'elist': element_list,'nlist':name_list})
@api.route('/start_stream', methods=['POST'])
async def start_stream(): #开启视频通道,把视频通道编号和元素编号进行关联
json_data = await request.get_json()
channel_id = json_data.get('channel_id')
element_id = json_data.get('element_id')
reStatus = 0
reMsg = ""
strsql = f"select element_id from channel where ID = {channel_id};"
data = mDBM.do_select(strsql,1)
if data:
if data[0] == '':
strsql = f"update channel set element_id = '{element_id}' where ID={channel_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '播放视频配置成功!'
else:
reMsg = '播放视频配置失败,请联系技术支持!'
else:
index = int(data[0]) +1
reMsg = f"该视频通道已在:Video Stream {index}播放,请先关闭"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/close_stream', methods=['POST'])
async def close_stream(): # 需要修改
channel_id = (await request.form)['channel_id']
async def close_stream(): #关闭视频通道
json_data = await request.get_json()
element_id = json_data.get('element_id')
reStatus = 0
reMsg =""
if channel_id in pcs:
await pcs[channel_id].close()
pcs.pop(channel_id, None)
print(f'Stream {channel_id} closed.')
#数据库中该通道的关联关系要清除
strsql = f"update channel set element_id = NULL where ID={channel_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '关闭画面成功!'
else:
reStatus = 0
reMsg = '删除通道与组件关联关系失败,请联系技术支持!'
#?关闭视频播放--业务逻辑-待确认后端是否需要执行
#数据库中该通道的关联关系要清除
strsql = f"update channel set element_id = '' where element_id={element_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '关闭画面成功!'
else:
reMsg = "通道编号不在系统内,请检查!"
reMsg = '删除通道与组件关联关系失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})

2
web/__init__.py

@ -1,5 +1,6 @@
from quart import Quart,session,redirect, url_for
from quart_session import Session
from quart_cors import cors
from pymemcache.client import base
from .main import main
from .API import api
@ -31,6 +32,7 @@ class MemcachedSessionInterface: #只是能用,不明所以
def create_app():
app = Quart(__name__)
#app = cors(app, allow_credentials=True) #allow_origin:指定允许跨域访问的来源
#相关配置--设置各种配置选项,这些选项会在整个应用程序中被访问和使用。
# app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

2
web/main/__init__.py

@ -1,5 +1,5 @@
from quart import Blueprint
main = Blueprint('main', __name__,template_folder='templates')
main = Blueprint('main', __name__,static_folder='static/resources',template_folder='templates')
from . import routes

24
web/main/routes.py

@ -14,7 +14,6 @@ from werkzeug.utils import secure_filename
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
print("decorated_function3")
if 'user' not in session:
return redirect(url_for('main.index',error='未登录,请重新登录'))
return await f(*args, **kwargs)
@ -22,12 +21,27 @@ def login_required(f):
@main.route('/')
async def index():
print("index")
#error = request.args.get('error')
return await render_template('实时预览.html')
#return await render_template('登录.html',error=error)
#return await render_template('实时预览.html')
return await render_template('login.html')
#return await render_template('index_webrtc.html')
@main.route('/login', methods=['GET', 'POST'])
async def login():
if request.method == 'POST':
form = await request.form
username = form.get('username')
password = form.get('password')
# Add your login logic here
if username == 'admin' and password == 'password':
return redirect(url_for('main.dashboard')) # Assuming you have a dashboard route
else:
return "Invalid credentials", 401
return await render_template('login.html')
@main.route('/dashboard')
async def dashboard():
return "Welcome to the dashboard!"
# @main.route('/', methods=['GET', 'POST'])
# async def upload_file():
# if request.method == 'POST':

BIN
web/main/static/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 70 KiB

BIN
web/main/static/images/登录/u12.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

6
web/main/static/images/登录/u4.svg

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="35px" height="40px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1165 -28 )">
<path d="M 34.767578125 28.9192708333333 C 34.9225260416667 30.2300347222222 35 31.6232638888889 35 33.0989583333333 C 35 34.9913194444444 34.4303385416667 36.6145833333333 33.291015625 37.96875 C 32.1516927083333 39.3229166666667 30.7799479166667 40 29.17578125 40 L 5.82421875 40 C 4.22005208333333 40 2.84830729166667 39.3229166666667 1.708984375 37.96875 C 0.569661458333333 36.6145833333333 0 34.9913194444444 0 33.0989583333333 C 0 31.6232638888889 0.0774739583333335 30.2300347222222 0.232421875 28.9192708333333 C 0.387369791666667 27.6085069444444 0.674479166666667 26.2890625 1.09375 24.9609375 C 1.51302083333333 23.6328125 2.04622395833333 22.4956597222222 2.693359375 21.5494791666667 C 3.34049479166667 20.6032986111111 4.197265625 19.8307291666667 5.263671875 19.2317708333333 C 6.330078125 18.6328125 7.55598958333333 18.3333333333333 8.94140625 18.3333333333333 C 11.3294270833333 20.5555555555556 14.1822916666667 21.6666666666667 17.5 21.6666666666667 C 20.8177083333333 21.6666666666667 23.6705729166667 20.5555555555556 26.05859375 18.3333333333333 C 27.4440104166667 18.3333333333333 28.669921875 18.6328125 29.736328125 19.2317708333333 C 30.802734375 19.8307291666667 31.6595052083333 20.6032986111111 32.306640625 21.5494791666667 C 32.9537760416667 22.4956597222222 33.4869791666667 23.6328125 33.90625 24.9609375 C 34.3255208333333 26.2890625 34.6126302083333 27.6085069444444 34.767578125 28.9192708333333 Z M 24.923828125 2.9296875 C 26.974609375 4.8828125 28 7.23958333333333 28 10 C 28 12.7604166666667 26.974609375 15.1171875 24.923828125 17.0703125 C 22.873046875 19.0234375 20.3984375 20 17.5 20 C 14.6015625 20 12.126953125 19.0234375 10.076171875 17.0703125 C 8.025390625 15.1171875 7 12.7604166666667 7 10 C 7 7.23958333333333 8.025390625 4.8828125 10.076171875 2.9296875 C 12.126953125 0.9765625 14.6015625 0 17.5 0 C 20.3984375 0 22.873046875 0.9765625 24.923828125 2.9296875 Z " fill-rule="nonzero" fill="#000000" stroke="none" transform="matrix(1 0 0 1 1165 28 )" />
</g>
</svg>

BIN
web/main/static/images/登录/zf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

26
web/main/static/images/登录/zf.svg

@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="410.000000pt" height="410.000000pt" viewBox="0 0 410.000000 410.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,410.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1815 3864 c-254 -30 -508 -114 -724 -241 -103 -60 -231 -153 -296
-215 -22 -21 -43 -38 -47 -38 -13 0 -187 -201 -240 -278 -160 -230 -258 -471
-309 -762 -18 -102 -18 -500 -1 -600 35 -194 84 -354 156 -506 254 -534 768
-909 1376 -1004 142 -23 426 -27 557 -9 598 81 1142 456 1394 959 152 304 207
565 196 920 -23 713 -407 1305 -1042 1605 -264 125 -502 176 -815 173 -96 0
-188 -3 -205 -4z m1320 -1023 c-11 -9 -236 -182 -356 -273 -42 -32 -103 -90
-135 -130 -32 -40 -90 -107 -129 -148 -38 -42 -137 -150 -220 -240 -82 -91
-186 -203 -231 -249 l-82 -85 -121 -4 -121 -4 -78 -89 c-78 -90 -142 -145
-352 -298 -169 -124 -451 -313 -457 -307 -7 6 27 55 91 131 71 83 378 419 500
545 311 324 728 767 742 791 12 19 253 137 397 193 131 51 325 115 452 150
102 28 113 30 100 17z m-1065 -89 c44 -22 62 -56 56 -106 -6 -47 -40 -85 -87
-94 -19 -4 -201 -7 -406 -7 l-372 0 -30 29 c-57 55 -50 128 18 173 34 23 35
23 410 23 338 0 380 -2 411 -18z m402 -1202 l330 0 34 -34 c29 -29 34 -41 34
-81 0 -42 -5 -52 -36 -81 l-35 -34 -385 0 -384 0 -38 34 c-32 30 -37 40 -37
80 0 38 5 51 31 77 34 34 82 50 128 43 16 -2 177 -4 358 -4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

15
web/main/static/resources/css/headers.css

@ -0,0 +1,15 @@
.form-control-dark {
border-color: var(--bs-gray);
}
.form-control-dark:focus {
border-color: #fff;
box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .25);
}
.text-small {
font-size: 85%;
}
.dropdown-toggle {
outline: 0;
}

33
web/main/static/resources/css/sign-in.css

@ -0,0 +1,33 @@
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
max-width: 400px;
padding: 15px;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: 5px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

BIN
web/main/static/resources/images/zf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

26
web/main/static/resources/images/zf.svg

@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="410.000000pt" height="410.000000pt" viewBox="0 0 410.000000 410.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,410.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1815 3864 c-254 -30 -508 -114 -724 -241 -103 -60 -231 -153 -296
-215 -22 -21 -43 -38 -47 -38 -13 0 -187 -201 -240 -278 -160 -230 -258 -471
-309 -762 -18 -102 -18 -500 -1 -600 35 -194 84 -354 156 -506 254 -534 768
-909 1376 -1004 142 -23 426 -27 557 -9 598 81 1142 456 1394 959 152 304 207
565 196 920 -23 713 -407 1305 -1042 1605 -264 125 -502 176 -815 173 -96 0
-188 -3 -205 -4z m1320 -1023 c-11 -9 -236 -182 -356 -273 -42 -32 -103 -90
-135 -130 -32 -40 -90 -107 -129 -148 -38 -42 -137 -150 -220 -240 -82 -91
-186 -203 -231 -249 l-82 -85 -121 -4 -121 -4 -78 -89 c-78 -90 -142 -145
-352 -298 -169 -124 -451 -313 -457 -307 -7 6 27 55 91 131 71 83 378 419 500
545 311 324 728 767 742 791 12 19 253 137 397 193 131 51 325 115 452 150
102 28 113 30 100 17z m-1065 -89 c44 -22 62 -56 56 -106 -6 -47 -40 -85 -87
-94 -19 -4 -201 -7 -406 -7 l-372 0 -30 29 c-57 55 -50 128 18 173 34 23 35
23 410 23 338 0 380 -2 411 -18z m402 -1202 l330 0 34 -34 c29 -29 34 -41 34
-81 0 -42 -5 -52 -36 -81 l-35 -34 -385 0 -384 0 -38 34 c-32 30 -37 40 -37
80 0 38 5 51 31 77 34 34 82 50 128 43 16 -2 177 -4 358 -4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

337
web/main/static/resources/scripts/aiortc-client-new.js

@ -1,9 +1,12 @@
var pc_list = {};
let video_list = {}; //element_id -- socket
let run_list = {}; //element_id -- runtag(替换berror)
var channel_list = null;
const fourViewButton = document.getElementById('fourView');
const nineViewButton = document.getElementById('nineView');
document.addEventListener('DOMContentLoaded', async function() {
console.log('DOM fully loaded and parsed');
// 发送请求获取额外数据
// 发送请求获取额外数据 --- 这个接口功能有点大了---暂时只是更新通道树2024-7-29
try {
let response = await fetch('/api/channel/list');
if (!response.ok) {
@ -11,50 +14,310 @@ document.addEventListener('DOMContentLoaded', async function() {
}
channel_list = await response.json();
// 遍历输出每个元素的信息
let area_name = ""
let html = '<ul class="list-group">';
channel_list.forEach(channel => {
if(channel.element_id){ //""空为false,非空为true
console.log(`Area Name: ${channel.area_name}`);
console.log(`ID: ${channel.ID}`);
console.log(`Channel Name: ${channel.channel_name}`);
console.log(`URL: ${channel.url}`);
console.log(`Type: ${channel.type}`);
console.log(`Status: ${channel.status}`);
console.log(`Element ID: ${channel.element_id}`);
connectToStream(channel.element_id,channel.ID,channel.area_name,channel.channel_name)
// console.log(`Area Name: ${channel.area_name}`);
// console.log(`ID: ${channel.ID}`);
// console.log(`Channel Name: ${channel.channel_name}`);
// console.log(`URL: ${channel.url}`);
// console.log(`Type: ${channel.type}`);
// console.log(`Status: ${channel.status}`);
// console.log(`Element ID: ${channel.element_id}`);
if(area_name !== `${channel.area_name}`){
if(area_name !== ""){
html += '</ul>';
html += '</li>';
}
area_name = `${channel.area_name}`;
html += `<li class="list-group-item"><strong>${area_name}</strong>`;
html += '<ul class="list-group">';
}
//html += `<li class="list-group-item">${channel.channel_name}</li>`;
html += `<li class="list-group-item" draggable="true" ondragstart="drag(event)"
data-node-id="${channel.ID}" data-node-name="${area_name}--${channel.channel_name}">
<svg class="bi" width="16" height="16"><use xlink:href="#view"/></svg>
${channel.channel_name}
</li>`;
});
if(area_name !== ""){
html += '</ul>';
html += '</li>';
}
html += '</ul>';
const treeView = document.getElementById('treeView');
treeView.innerHTML = html
generateVideoNodes(4);
} catch (error) {
console.error('Failed to fetch data:', error);
}
});
function connectToStream(element_id,channel_id,area_name,channel_name) {
const imgElement = document.getElementById(element_id);
imgElement.alt = `Stream ${area_name}--${channel_name}`;
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
console.log(streamUrl);
function connect() {
const socket = new WebSocket(streamUrl);
socket.onmessage = function(event) {
//const blob = new Blob([event.data], { type: 'image/jpeg' });
//imgElement.src = URL.createObjectURL(blob);
// 释放旧的对象URL
if (imgElement.src) {
URL.revokeObjectURL(imgElement.src);
}
imgElement.src = URL.createObjectURL(event.data);
};
//视频窗口
document.getElementById('fourView').addEventListener('click', function() {
if (fourViewButton.classList.contains('btn-primary')) {
return; // 如果按钮已经是选中状态,直接返回
}
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('nine');
videoGrid.classList.add('four');
generateVideoNodes(4);
//更新按钮点击状态
fourViewButton.classList.remove('btn-secondary');
fourViewButton.classList.add('btn-primary');
nineViewButton.classList.remove('btn-primary');
nineViewButton.classList.add('btn-secondary');
});
document.getElementById('nineView').addEventListener('click', function() {
if (nineViewButton.classList.contains('btn-primary')) {
return; // 如果按钮已经是选中状态,直接返回
}
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('four');
videoGrid.classList.add('nine');
generateVideoNodes(9);
//更新按钮点击状态
fourViewButton.classList.remove('btn-primary');
fourViewButton.classList.add('btn-secondary');
nineViewButton.classList.remove('btn-secondary');
nineViewButton.classList.add('btn-primary');
});
function generateVideoNodes(count) { //在这里显示视频-初始化
//结束在播放的socket
for(let key in video_list){
delete run_list[key];
video_list[key].close();
delete video_list[key];
}
//切换窗口布局
const videoGrid = document.getElementById('videoGrid');
let html = '';
for (let i = 0; i < count; i++) {
let frameWidth = count === 4 ? 'calc(50% - 10px)' : 'calc(33.33% - 10px)';
html += `
<div class="video-frame" data-frame-id="${i}" style="width: ${frameWidth};"
ondrop="drop(event)" ondragover="allowDrop(event)">
<div class="video-header">
<div class="video-title">Video Stream ${i+1}</div>
<div class="video-buttons">
<button onclick="toggleFullScreen(${i})">🔲</button>
<button onclick="closeVideo(${i})"></button>
</div>
</div>
<div class="video-area"><img id="video-${i}" alt="Video Stream" /></div>
</div>`;
}
videoGrid.innerHTML = html;
//获取视频接口
const url = `/api/viewlist?count=${count}`;
fetch(url)
.then(response => response.json())
.then(data => {
console.log('Success:', data);
clist = data.clist;
elist = data.elist;
nlist = data.nlist;
for(let i=0;i<clist.length;i++){
if(parseInt(elist[i]) < count){
connectToStream(elist[i],clist[i],nlist[i])
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function toggleFullScreen(id) {
console.log('toggleFullScreen');
const videoFrame = document.querySelector(`[data-frame-id="${id}"]`);
if (!document.fullscreenElement) {
videoFrame.requestFullscreen().catch(err => {
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
});
} else {
document.exitFullscreen();
};
}
function closeVideo(id) {
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
if (titleElement.textContent === `Video Stream ${Number(id)+1}`) {
showModal('当前视频窗口未播放视频。');
return;
};
console.log('closeVideo');
//发送视频链接接口
const url = '/api/close_stream';
const data = {"element_id":id};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
console.log('Success:', data);
const istatus = data.status;
if(istatus === 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
else{
const videoFrame = document.querySelector(`[data-frame-id="${id}"] .video-area img`);
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
run_list[id] = false;
video_list[id].close();
videoFrame.src = ''; // 清空画面
videoFrame.style.display = 'none'; // 停止播放时隐藏 img 元素
titleElement.textContent = `Video Stream ${id+1}`;
removeErrorMessage(videoFrame)
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
}
function allowDrop(event) {
event.preventDefault();
}
socket.onclose = function() {
setTimeout(connect, 1000); // 尝试在1秒后重新连接
};
function drag(event) {
event.dataTransfer.setData("text", event.target.dataset.nodeId);
event.dataTransfer.setData("name", event.target.dataset.nodeName);
}
socket.onerror = function() {
console.log(`WebSocket错误,尝试重新连接... Channel ID: ${channel_id}`);
socket.close();
};
function drop(event) {
event.preventDefault();
const nodeId = event.dataTransfer.getData("text");
const nodeName = event.dataTransfer.getData("name");
const frameId = event.currentTarget.dataset.frameId;
//需要判断下当前窗口是否已经在播放视频
const imgElement = document.getElementById(`video-${frameId}`);
const titleElement = document.querySelector(`[data-frame-id="${frameId}"] .video-title`);
if (titleElement.textContent !== `Video Stream ${Number(frameId)+1}`) {
showModal('请先关闭当前窗口视频,然后再播放新的视频。');
return;
};
//发送视频链接接口
const url = '/api/start_stream';
const data = {"channel_id":nodeId,"element_id":frameId};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
else{
//获取视频流
connectToStream(frameId,nodeId,nodeName);
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
//console.log('retrun 只是把fetch结束,这里的代码还是会执行');
}
function connectToStream(element_id,channel_id,channel_name) {
console.log("开始连接视频",channel_id);
// 设置视频区域的标题
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`);
titleElement.textContent = channel_name;
//获取视频
const imgElement = document.getElementById(`video-${element_id}`);
imgElement.alt = `Stream ${channel_name}`;
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
let berror = false;
function connect() {
const socket = new WebSocket(streamUrl);
video_list[element_id] = socket;
run_list[element_id] = true;
// 处理连接打开事件
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = function(event) {
// 释放旧的对象URL---需要吗?
// if (imgElement.src) {
// URL.revokeObjectURL(imgElement.src);
// }
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const decoder = new TextDecoder("utf-8");
const decodedData = decoder.decode(arrayBuffer);
if (decodedData === "video_error") { //video_error
displayErrorMessage(imgElement, "该视频源未获取到画面,请检查,默认5分钟后重新链接视频源。");
} else if(decodedData === "client_error"){ //client_error
run_list[element_id] = false;
displayErrorMessage(imgElement, "该通道节点数据存在问题,请重启或联系技术支持!");
socket.close(); // 停止连接
}
else {
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
const imageUrl = URL.createObjectURL(blob);
imgElement.src = imageUrl;
imgElement.style.display = 'block';
// imgElement.src = URL.createObjectURL(result);
// imgElement.style.display = 'block';
}
};
reader.readAsArrayBuffer(event.data);
};
socket.onclose = function() {
if(run_list[element_id]){
console.log(`尝试重新连接... Channel ID: ${channel_id}`);
setTimeout(connect, 1000*10); // 尝试在10秒后重新连接
}
};
socket.onerror = function() {
console.log(`WebSocket错误,Channel ID: ${channel_id}`);
socket.close();
};
};
connect();
}
function displayErrorMessage(imgElement, message) {
removeErrorMessage(imgElement)
imgElement.style.display = 'none'; // 隐藏图片
const errorElement = document.createElement('div');
errorElement.textContent = message;
errorElement.classList.add('error-message');
imgElement.parentNode.appendChild(errorElement);
}
function removeErrorMessage(imgElement) {
const errorElement = imgElement.parentNode.querySelector('.error-message');
if (errorElement) {
imgElement.parentNode.removeChild(errorElement);
}
}
connect();
}

47
web/main/static/resources/scripts/base.js

@ -0,0 +1,47 @@
document.addEventListener('DOMContentLoaded', function() {
const links = document.querySelectorAll('.nav-pills .nav-link');
// Get the current page path
const currentPath = window.location.pathname;
links.forEach(link => {
// Compare the link's href with the current path
if (link.href.endsWith(currentPath)) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
});
function showModal(message) {
// 设置 Modal 中的消息
document.getElementById('modalMessage').innerText = message;
// 显示 Modal
const responseModal = new bootstrap.Modal(document.getElementById('responseModal'));
responseModal.show();
}
//动态填充select控件
function set_select_data(select_ele_id,datas){
const select_Ele = document.getElementById(select_ele_id);
//清空老数据
select_Ele.innerHTML = '';
//添加列表
datas.forEach(option => {
const optionElement = document.createElement('option');
optionElement.textContent = option;
select_Ele.appendChild(optionElement);
});
}
//设定选项选中状态
function set_select_selct(select_ele_id,option_str){
const select_Ele = document.getElementById(select_ele_id);
for(let i=0;i< select_Ele.options.length;i++){
if(select_Ele.options[i].value === option_str){
select_Ele.options[i].selected = true;
break;
}
}
}

608
web/main/static/resources/scripts/channel_manager.js

@ -0,0 +1,608 @@
const apiEndpoint = '/api/channel/list';
const rowsPerPage = 10;
//算法配置窗口部分控件
const searchEndpoint = '/api/channel/select';
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const backgroundCanvas = document.getElementById('backgroundCanvas');
const backgroundCtx = backgroundCanvas.getContext('2d');
const img = new Image();
const tbody = document.getElementById('schedule-body');//布防计划
let currentPage = 1;
let channelData = [];
let channelData_bak = [];
let areaData = ["请选择"];
let currentEditingRow = null;
let cid_schedule = "-1";
let m_polygon = "";
let check_area = 0;
let draw_status = false; //是否是绘制状态,处于绘制状态才能开始绘制
let b_img = false; //有没有加载图片成功,如果没有初始化的时候就不绘制线条了。
let points = [];
document.addEventListener('DOMContentLoaded', function () {
fetchChannelData(); //初始化页面元素数据
document.getElementById('searchButton').addEventListener('click', function () {
performSearch();
});
//新增通道
document.getElementById('saveButton').addEventListener('click', function () {
addChannel(1);
});
//修改通道
document.getElementById('saveButton_cc').addEventListener('click', function () {
addChannel(2);
});
//算法配置
document.getElementById('cancelButton_mx').addEventListener('click', function () {
close_mx_model();
});
//保存算法配置
document.getElementById('saveButton_mx').addEventListener('click', function () {
save_mx_model();
});
//开始绘制区域按钮
document.getElementById('but_hzqy').addEventListener('click', function () {
startDraw();
});
});
//添加和修改通道 1--新增,2--修改
function addChannel(itype) {
let area;
let cName;
let Rtsp;
let cid;
if(itype ==1){
const saveButton = document.getElementById('saveButton');
const CNameInput = document.getElementById('CNameInput');
const RTSPInput = document.getElementById('RTSPInput');
area = document.getElementById('areaSelect_M').value;
cName = CNameInput.value.trim();
Rtsp = RTSPInput.value.trim();
cid = -1
}
else if(itype ==2){
const saveButton = document.getElementById('saveButton_cc');
const CNameInput = document.getElementById('CNameInput_cc');
const RTSPInput = document.getElementById('RTSPInput_cc');
area = document.getElementById('areaSelect_CC').value;
cName = CNameInput.value.trim();
Rtsp = RTSPInput.value.trim();
cid = currentEditingRow.cells[0].innerText;
}
if(area === "请选择"){
alert('请选择所属区域');
}
else{
if (cName && Rtsp) {
saveButton.disabled = true;
//发送视频链接接口
const url = '/api/channel/add';
const data = {"area":area,"cName":cName,"Rtsp":Rtsp,"cid":cid};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
alert(data.msg); // 使用 Modal 显示消息
// 启用保存按钮
saveButton.disabled = false;
return;
}
else{
// 启用保存按钮
saveButton.disabled = false;
//刷新列表
fetchChannelData();
if(itype ==1){
//添加通道成功
$('#channelModal').modal('hide');
alert("添加通道成功!"); // 使用 Modal 显示消息
}
else if(itype==2){
//修改通道成功
currentEditingRow = null;
$('#ChangeC').modal('hide');
alert("修改通道成功!"); // 使用 Modal 显示消息
}
}
})
.catch((error) => {
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
// 启用保存按钮
saveButton.disabled = false;
return;
});
} else {
alert('通道名称和RTSP地址不能为空');
}
}
}
async function fetchChannelData() { //刷新通道数据
try {
const response = await fetch(apiEndpoint);
channelData = await response.json();
channelData_bak = channelData;
renderTable(); //读取通道list接口,刷新表格
renderPagination(); //刷新分页元素
renderAreaOptions(); //所属区域下来框
} catch (error) {
console.error('Error fetching channel data:', error);
}
}
//关键字查询数据
async function performSearch() {
try {
const area = document.getElementById('areaSelect').value;
const channelName = document.getElementById('channelNameInput').value;
if(area === "请选择" && channelName===""){
channelData = channelData_bak;
}
else if(area === "请选择"){
channelData = [];
channelData_bak.forEach((channel) => {
if(channelName === channel.channel_name){
channelData.push(channel);
}
});
}
else if(channelName === ""){
channelData = [];
channelData_bak.forEach((channel) => {
if(area === channel.area_name){
channelData.push(channel);
}
});
}
else{
channelData = [];
channelData_bak.forEach((channel) => {
if(area === channel.area_name && channelName === channel.channel_name){
channelData.push(channel);
}
});
}
// 渲染表格和分页控件
currentPage = 1; // 重置当前页为第一页
renderTable();
renderPagination();
} catch (error) {
console.error('Error performing search:', error);
}
}
//刷新表单页面数据
function renderTable() {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const end = start + rowsPerPage;
const pageData = channelData.slice(start, end);
const surplus_count = rowsPerPage - pageData.length;
let area_name = "";
pageData.forEach((channel) => {
if(area_name!==channel.area_name){ //这里要求区域名称一样的要在一起
area_name = channel.area_name;
areaData.push(area_name);
}
const row = document.createElement('tr');
row.innerHTML = `
<td>${channel.ID}</td>
<td>${channel.area_name}</td>
<td>${channel.channel_name}</td>
<td>${channel.ulr}</td>
<td>${channel.model_name}</td>
<td>
<button class="btn btn-primary btn-sm modify-btn">修改</button>
<button class="btn btn-secondary btn-sm algorithm-btn">算法</button>
<button class="btn btn-danger btn-sm delete-btn">删除</button>
</td>
`;
tableBody.appendChild(row);
row.querySelector('.modify-btn').addEventListener('click', () => modifyChannel(row));
row.querySelector('.algorithm-btn').addEventListener('click', () => configureAlgorithm(row));
row.querySelector('.delete-btn').addEventListener('click', () => deleteChannel(row));
});
}
//修改通道信息
function modifyChannel(row) {
// const cid = row.cells[0].innerText;
const areaName = row.cells[1].innerText;
const channelName = row.cells[2].innerText;
const url = row.cells[3].innerText;
const area = document.getElementById('areaSelect_CC');
const CName = document.getElementById('CNameInput_cc');
const RTSP = document.getElementById('RTSPInput_cc');
for(let i=0;i< area.options.length;i++){
if(area.options[i].value === areaName){
area.options[i].selected = true;
break;
}
}
CName.value = channelName;
RTSP.value = url;
currentEditingRow = row;
$('#ChangeC').modal('show');
}
//算法配置 -- 点击算法按钮
function configureAlgorithm(row) {
//获取当前行信息
currentEditingRow = row;
const cid = row.cells[0].innerText;
//清除数据,若需要的话
ctx.clearRect(0, 0, canvas.width, canvas.height); //清除左侧绘画和画线信息
tbody.innerHTML = ''; //清空布防控件数据
points = []; //清空绘制检测区域
draw_status = false;
b_img = false;
document.getElementById('but_hzqy').textContent = "绘制区域";
//开始初始化算法管理模块
show_channel_img(cid); //显示一帧图片 -- 获取不到图片就是黑画面
show_channel_model_schedule(cid); //显示结构化数据
//显示窗口
$('#MX_M').modal('show');
}
//获取一帧图片
function show_channel_img(cid){
const data = {"cid":cid};
fetch('/api/channel/img', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.image) {
b_img = true;
img.src = 'data:image/jpeg;base64,' + data.image;
} else {
console.error('Error:', data.error);
}
})
.catch(error => console.error('Error:', error));
}
//图片加载事项
img.onload = () => { //清除、画图和画线应该分开
// 设置画布宽高
backgroundCanvas.width = canvas.width = img.width;
backgroundCanvas.height = canvas.height = img.height;
// 将图片绘制到背景画布上
backgroundCtx.drawImage(img, 0, 0, img.width, img.height);
// 将背景画布的内容复制到前台画布上
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); //绘制画面
};
//开始和重新绘制
function startDraw(){
if(!document.getElementById('zdjc').checked){
alert("请先选择指定区域!");
return;
}
let but = document.getElementById('but_hzqy');
if(!draw_status){//开始绘制
if(points.length >0){
if (confirm('开始绘制将清除未提交保存的绘制数据,是否继续?')) {
draw_status = true;
points = [];
//按钮文字调整为结束绘制
but.textContent = '结 束 绘 制';
// 清除前台画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 将背景画布的内容复制到前台画布上
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height);
}
}
else{
draw_status = true;
but.textContent = '结束绘制';
}
}
else{//结束绘制
draw_status = false;
but.textContent = '绘制区域';
}
}
//单选按钮点击事件处理
function handleRadioClick(event) {
const selectedRadio = event.target;
console.log('Selected Radio:', selectedRadio.id);
// 根据选中的单选按钮执行相应操作
if (selectedRadio.id === 'qjjc') {
console.log("points.length",points.length);
// 处理全画面生效的逻辑
if(points.length>0){
if (!confirm('已经绘制了检测区域,确认要切换到全画面生效吗?')) {
document.getElementById('zdjc').checked = true;
}
}
console.log('全画面生效');
} else if (selectedRadio.id === 'zdjc') {
// 处理指定区域的逻辑
console.log('指定区域');
}
}
// 鼠标点击事件处理--动态绘图
canvas.addEventListener('click', (event) => {
if(draw_status){
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// 获取鼠标相对于canvas的位置
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
points.push({ x, y });
//绘制线条
drawLines();
}
});
//初始化读取该视频通道与算法配置的相关信息 --- 这里用GET会更加贴切一些
function show_channel_model_schedule(cid){
//发送视频链接接口
const url = '/api/channel/C2M';
const data = {"cid":cid};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const m_datas = data.m_datas;
const c2m_data = data.c2m_data;
const schedule = data.schedule;
//console.log("m_datas--",m_datas);
//console.log("c2m_data--",c2m_data);
//console.log("schedule--",schedule);
//配置算法下拉清单
select_datas = ["请选择"];
m_datas.forEach(option => {
select_datas.push(option.model_name);
});
set_select_data("model_select",select_datas);
select_str = currentEditingRow.cells[4].innerText;
//检测区域
if(c2m_data.length >0){
model_id = c2m_data[0].model_id;
model_name = currentEditingRow.cells[4].innerText;
set_select_selct("model_select",model_name);
check_area = c2m_data[0].check_area
if( check_area == 0){ //全画面生效
document.getElementById('qjjc').checked = true;
m_polygon = "";
}
else{//指定区域
document.getElementById('zdjc').checked = true;
m_polygon = c2m_data[0].polygon;
if(m_polygon !== ""){ //指定区域了,一般是会有数据的。
const coords = parseCoordStr(m_polygon);
points = coords;
drawLines();
}
}
console.log("m_polygon",m_polygon);
//阈值
document.getElementById('zxyz').value = c2m_data[0].conf_thres
document.getElementById('iouyz').value = c2m_data[0].iou_thres
}
const days = ['一', '二', '三', '四', '五', '六','日'];
const num_days=['0','1','2','3','4','5','6']
days.forEach((day, dayIndex) => {
const row = document.createElement('tr');
const dayCell = document.createElement('th');
dayCell.textContent = day;
row.appendChild(dayCell);
num_day = num_days[dayIndex]
for (let hour = 0; hour < 24; hour++) {
const cell = document.createElement('td');
if(schedule.length >0){
const status = schedule.find(item => item.day === num_day && item.hour === hour);
if (status && status.status === 1) {
cell.classList.add('blocked');
} else {
cell.classList.add('allowed');
}
}
else{
cell.classList.add('blocked');
}
row.appendChild(cell);
cell.addEventListener('click', () => {
if (cell.classList.contains('blocked')) {
cell.classList.remove('blocked');
cell.classList.add('allowed');
// Update status in the database
//updateStatus(day, hour, 0);
} else {
cell.classList.remove('allowed');
cell.classList.add('blocked');
// Update status in the database
//updateStatus(day, hour, 1);
}
});
}
tbody.appendChild(row);
});
})
.catch((error) => {
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
}
// 将字符串转换为数组
function parseCoordStr(str) {
return str.match(/\(([^)]+)\)/g).map(pair => {
const [x, y] = pair.replace(/[()]/g, '').split(',').map(Number);
return { x, y };
});
}
// 绘制区域,各点连接
function drawLines() {
if(b_img){
// 清除前台画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 将背景画布的内容复制到前台画布上
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height);
// 绘制点和线
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
if (points.length > 0) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
// 连接最后一个点到起点
ctx.lineTo(points[0].x, points[0].y);
ctx.stroke();
}
points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
});
}
}
//关闭算法配置窗口
function close_mx_model(){
if (confirm('确定退出窗口吗?未保存的修改将丢失!')) {
$('#MX_M').modal('hide');
}
}
//保存算法配置窗口数据
function save_mx_model(){
//?
currentEditingRow =null;
}
//删除通道
function deleteChannel(row) {
if (confirm('确定删除此区域吗?')) {
cid = row.cells[0].innerText;
//发送视频链接接口
const url = '/api/channel/del';
const data = {"cid":cid};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
alert(data.msg); // 使用 Modal 显示消息
// 启用保存按钮
saveButton.disabled = false;
return;
}
else{
// 启用保存按钮
saveButton.disabled = false;
//刷新列表
row.remove();
alert("删除通道成功!");
}
})
.catch((error) => {
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
// 启用保存按钮
saveButton.disabled = false;
return;
});
}
}
//刷新分页标签
function renderPagination() {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
const totalPages = Math.ceil(channelData.length / rowsPerPage);
for (let i = 1; i <= totalPages; i++) {
const pageItem = document.createElement('li');
pageItem.className = 'page-item' + (i === currentPage ? ' active' : '');
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`;
pageItem.addEventListener('click', (event) => {
event.preventDefault();
currentPage = i;
renderTable();
renderPagination();
});
pagination.appendChild(pageItem);
}
}
//刷新区域下拉控件
function renderAreaOptions() {
const areaSelect = document.getElementById('areaSelect');
const areaSelect_M = document.getElementById('areaSelect_M')
const areaSelect_CC = document.getElementById('areaSelect_CC')
areaData.forEach(option => {
const optionElement = document.createElement('option');
optionElement.textContent = option;
areaSelect.appendChild(optionElement);
const optionElement_m = document.createElement('option');
optionElement_m.textContent = option;
areaSelect_M.appendChild(optionElement_m);
const optionElement_cc = document.createElement('option');
optionElement_cc.textContent = option;
areaSelect_CC.appendChild(optionElement_cc);
});
}

4
web/main/static/resources/scripts/jquery-1.7.1.min.js

File diff suppressed because one or more lines are too long

14
web/main/static/resources/scripts/jquery-3.2.1.min.js

File diff suppressed because one or more lines are too long

4
web/main/static/resources/scripts/jquery-3.2.1.slim.min.js

File diff suppressed because one or more lines are too long

5
web/main/static/resources/scripts/popper.min.js

File diff suppressed because one or more lines are too long

46
web/main/templates/base.html

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Website{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet">
<style>
.bi {
vertical-align: -.125em;
fill: currentColor;
}
{% block style %}{% endblock %}
</style>
</head>
<body>
{% include 'header.html' %}
<!-- Modal Structure -->
<div class="modal fade" id="responseModal" tabindex="-1" aria-labelledby="responseModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="responseModalLabel">提示信息</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="modalMessage">
<!-- The message from the server will be inserted here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<main class="p-0 mb-0">
{% block content %}{% endblock %}
</main>
{% include 'footer.html' %}
<script src="{{ url_for('main.static', filename='scripts/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/base.js') }}"></script>
{% block script %}{% endblock %}
</body>
</html>

291
web/main/templates/channel_manager.html

@ -0,0 +1,291 @@
{% extends 'base.html' %}
{% block title %}ZFBOX{% endblock %}
{% block style %}
.pagination {
justify-content: flex-end; /* 右对齐 */
}
.page-item .page-link {
padding: 0.25rem 0.5rem; /* 缩小按钮 */
font-size: 0.875rem; /* 调整字体大小 */
}
.btn-group-sm > .btn, .btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
}
.btn-group-sm .btn {
margin-right: 5px; /* 按钮之间的间距 */
}
.form-group-right h5 {
text-align: right;
margin-bottom: 0;
}
.table-container {
min-height: 400px; /* 设置最小高度,可以根据需要调整 */
}
.video-area {
width: 100%;
position: relative;
background-color: #000;
border: 1px solid #ddd;
}
.video-area::before {
content: "";
display: block;
padding-top: 75%; /* 4:3 ratio */
}
.video-area canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.schedule-table {
width: 100%;
table-layout: fixed;
}
.schedule-table th, .schedule-table td {
text-align: center;
vertical-align: middle;
cursor: pointer;
border: 1px solid #dee2e6;
padding: 8px;
}
.schedule-table td.allowed {
background-color: white;
}
.schedule-table td.blocked {
background-color: blue;
}
/* 缩小表格行高 */
.table-sm th,
.table-sm td {
padding: 0.2rem; /* 调整这里的值来改变行高 */
}
canvas {
border: 1px solid black;
}
{% endblock %}
{% block content %}
<div class="container d-flex flex-column" >
<div class="row mb-3 d-flex justify-content-center align-items-center ">
<div class="col"><h5 class="form-group-right">所属区域:</h5></div>
<div class="col-3 mr-2">
<select id="areaSelect" class="form-select mr-2" aria-label="Default select example">
<!-- 数据动态填充 -->
</select></div>
<div class="col"><h5 class="form-group-right">通道名称:</h5></div>
<div class="col-5 mr-2">
<input id="channelNameInput" type="text" class="form-control mr-2" placeholder="Channel_name" aria-label="Channel_name"></div>
<div class="col-1"><button id="searchButton" type="button" class="btn btn-primary">查 询</button></div>
</div>
<div class="mb-3">
<!-- <button id="manageAreaButton" type="button" class="btn btn-primary mr-2">区域管理</button>-->
<button id="addChannelButton" type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#channelModal">
新增通道
</button>
</div>
<!-- 新增通道模态框 -->
<div class="modal fade" id="channelModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="add_channel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="channelModalLabel">新增通道</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="areaSelect_M">所属区域:</label>
<select id="areaSelect_M" class="form-select" aria-label="D">
<!-- 数据动态填充 -->
</select>
</div>
<div class="form-group">
<label for="CNameInput">通道名称:</label>
<input type="text" class="form-control" id="CNameInput">
</div>
<div class="form-group">
<label for="RTSPInput">RTSP地址:</label>
<input type="text" class="form-control" id="RTSPInput">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveButton">保存</button>
</div>
</div>
</div>
</div>
<!-- 修改通道模态框 -->
<div class="modal fade" id="ChangeC" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="change_c" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ChangeChannel">修改通道</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="areaSelect_CC">所属区域:</label>
<select id="areaSelect_CC" class="form-select" aria-label="D">
<!-- 数据动态填充 -->
</select>
</div>
<div class="form-group">
<label for="CNameInput_cc">通道名称:</label>
<input type="text" class="form-control" id="CNameInput_cc">
</div>
<div class="form-group">
<label for="RTSPInput_cc">RTSP地址:</label>
<input type="text" class="form-control" id="RTSPInput_cc">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveButton_cc">保存</button>
</div>
</div>
</div>
</div>
<!-- 算法管理模态框 -->
<div class="modal fade" id="MX_M" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="m_mx" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="MX_Title">配置算法</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-6">
<div class="video-area">
<!-- <img id="video-mx" alt="Video Stream" />-->
<canvas id="backgroundCanvas" style="display: none;"></canvas>
<canvas id="myCanvas"></canvas>
</div>
</div>
<div class="col-6 ms-auto">
<!-- 配置算法 -->
<div class="row align-items-center mb-2">
<div class="col-3"><label for="model_select">配置算法:</label></div>
<div class="col-9">
<select id="model_select" class="form-select" aria-label="D">
<!-- 数据动态填充 -->
</select>
</div>
</div>
<!-- 检测区域 -->
<div class="row align-items-center mb-2">
<div class="col-3"><label for="model_select">检测区域:</label></div>
<div class="col-9">
<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="jcqy"
id="qjjc" checked onclick="handleRadioClick(event)">
<label class="form-check-label" for="qjjc">全画面生效</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="jcqy"
id="zdjc" onclick="handleRadioClick(event)">
<label class="form-check-label" for="zdjc">指定区域</label>
</div>
</div>
</div>
<div class="row">
<button type="button" class="btn btn-primary" id="but_hzqy" style="--bs-btn-padding-y:.20rem; --bs-btn-font-size: .70rem;">
绘制区域</button>
</div>
</div>
</div>
<!-- 检测阈值 -->
<div class="row align-items-center mb-2">
<div class="col-3">
<label>置信阈值:</label>
</div>
<div class="col-3">
<input type="text" class="form-control" id="zxyz">
</div>
<div class="col-3">
<label>IOU阈值:</label>
</div>
<div class="col-3">
<input type="text" class="form-control" id="iouyz">
</div>
</div>
<!-- 布放计划 -->
<div class="row form-group">
<label for="11">布放计划:</label>
<div id="11">
<table class="schedule-table table table-sm table-bordered" style="font-size: 7px;">
<thead>
<tr>
<th></th><th>00</th><th>01</th><th>02</th><th>03</th><th>04</th>
<th>05</th><th>06</th><th>07</th><th>08</th><th>09</th><th>10</th>
<th>11</th><th>12</th><th>13</th><th>14</th><th>15</th><th>16</th>
<th>17</th><th>18</th><th>19</th><th>20</th><th>21</th><th>22</th>
<th>23</th>
</tr>
</thead>
<tbody id="schedule-body">
<!-- 表格数据将由JavaScript动态填充 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="cancelButton_mx">取消</button>
<button type="button" class="btn btn-primary" id="saveButton_mx">保存</button>
</div>
</div>
</div>
</div>
<div class="table-container">
<table class="table">
<thead class="table-light">
<tr>
<th scope="col">ID</th>
<th scope="col">所属区域</th>
<th scope="col">通道名称</th>
<th scope="col">RTSP地址</th>
<th scope="col">配置算法</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="table-body" class="table-group-divider">
<!-- 表格数据动态填充 -->
</tbody>
</table>
<nav>
<ul id="pagination" class="pagination">
<!-- 分页控件将动态生成 -->
</ul>
</nav>
</div>
</div>
{% endblock %}
{% block script %}
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/popper.min.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/channel_manager.js') }}"></script>
{% endblock %}

29
web/main/templates/footer.html

@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="facebook" viewBox="0 0 16 16">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"/>
</symbol>
<symbol id="instagram" viewBox="0 0 16 16">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334z"/>
</symbol>
<symbol id="twitter" viewBox="0 0 16 16">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"/>
</symbol>
<symbol id="view" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z"/>
</symbol>
</svg>
<footer class="py-3 my-4 border-top">
<div class="container justify-content-between align-items-center d-flex flex-wrap">
<div class="col-md-4 d-flex align-items-center">
<img src="{{ url_for('main.static', filename='images/zf.svg') }}" alt="zfai" width="30" height="24">
<span class="mb-3 mb-md-0 text-muted">&copy; ZFKJ All Rights Reserved</span>
</div>
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
<li class="ms-3"><a class="text-muted" ><svg class="bi" width="24" height="24"><use xlink:href="#twitter"/></svg></a></li>
<li class="ms-3"><a class="text-muted" ><svg class="bi" width="24" height="24"><use xlink:href="#instagram"/></svg></a></li>
<li class="ms-3"><a class="text-muted" ><svg class="bi" width="24" height="24"><use xlink:href="#facebook"/></svg></a></li>
</ul>
</div>
</footer>

After

Width:  |  Height:  |  Size: 3.8 KiB

35
web/main/templates/header.html

@ -0,0 +1,35 @@
<header class="p-3 mb-3 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<img src="images/登录/zf.svg" alt="" width="40" height="40">
<!-- <svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"/></svg>-->
<span class="fs-4">ZF_BOX</span>
</a>
<ul class="nav nav-pills">
<li class="nav-item"><a href="/view_main.html" class="nav-link active" aria-current="page">实时预览</a></li>
<li class="nav-item"><a href="/channel_manager.html" class="nav-link">通道管理</a></li>
<li class="nav-item"><a href="/schedule.html" class="nav-link">算法管理</a></li>
<li class="nav-item"><a href="#" class="nav-link">系统管理</a></li>
<li class="nav-item"><a href="#" class="nav-link">用户管理</a></li>
</ul>
<div class="dropdown text-end">
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-person-bounding-box" viewBox="0 0 16 16">
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5z"/>
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm8-9a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
</svg>
<!-- <img src="https://github.com/mdo.png" alt="mdo" width="32" height="32" class="rounded-circle">-->
</a>
<ul class="dropdown-menu text-small">
<li><a class="dropdown-item" href="#">修改密码</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">退 出</a></li>
</ul>
</div>
</div>
</div>
</header>

205
web/main/templates/index_webrtc.html

@ -1,45 +1,176 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>WebRTC Video Stream</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZFBOX</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<link href="../static/resources/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
height: 100vh;
}
header {
background-color: #007bff;
color: white;
padding: 10px 0;
}
.navbar-nav .nav-link {
color: white;
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
.tree-view {
border-right: 1px solid #ddd;
padding: 10px;
overflow-y: auto;
}
.video-content {
padding: 10px;
overflow-y: auto;
flex: 1;
}
footer {
background-color: #f8f9fa;
text-align: center;
padding: 10px 0;
}
.video-frame {
background-color: #f8f9fa;
border: 1px solid #ddd;
margin-bottom: 10px;
}
.video-frame img {
width: 100%;
height: auto;
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.video-grid.eight {
grid-template-columns: repeat(4, 1fr);
}
.toggle-buttons {
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>WebRTC Video Stream</h1>
<video id="video" autoplay playsinline></video>
<header>
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">智凡BOX</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#">实时预览</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">通道管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">算法管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">系统管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">用户管理</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="#">张三 退出</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<main>
<div class="tree-view col-md-3">
<ul class="list-group">
<li class="list-group-item">一区
<ul class="list-group">
<li class="list-group-item">北门通道一</li>
<li class="list-group-item">南门通道二</li>
<li class="list-group-item">通道三</li>
</ul>
</li>
<li class="list-group-item">二区域
<ul class="list-group">
<li class="list-group-item">通道一</li>
</ul>
</li>
</ul>
</div>
<div class="video-content col-md-9">
<div class="toggle-buttons">
<button id="fourView" class="btn btn-primary">四画面</button>
<button id="eightView" class="btn btn-secondary">八画面</button>
</div>
<div id="videoGrid" class="video-grid">
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
</div>
</div>
</main>
<footer>
&copy; 2024 ZFKJ All Rights Reserved
</footer>
<script src="{{ url_for('main.static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script>
const pc = new RTCPeerConnection();
const video = document.getElementById('video');
pc.ontrack = (event) => {
video.srcObject = event.streams[0];
};
const ws = new WebSocket('ws://' + window.location.host + '/api/ws');
ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
await pc.setRemoteDescription(new RTCSessionDescription(data));
if (data.type === 'offer') {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({
'sdp': pc.localDescription.sdp,
'type': pc.localDescription.type
}));
}
};
ws.onopen = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({
'sdp': pc.localDescription.sdp,
'type': pc.localDescription.type
}));
};
document.getElementById('fourView').addEventListener('click', function() {
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('eight');
videoGrid.classList.add('four');
videoGrid.innerHTML = `
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
`;
});
document.getElementById('eightView').addEventListener('click', function() {
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('four');
videoGrid.classList.add('eight');
videoGrid.innerHTML = `
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
`;
});
</script>
</body>
</html>

122
web/main/templates/login.html

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZFBOX</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<!-- <link href="../static/resources/css/bootstrap.min.css" rel="stylesheet"> -->
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.captcha-group {
display: flex;
align-items: center;
gap: 10px;
}
</style>
<!-- Custom styles for this template -->
<link href="{{ url_for('main.static', filename='css/sign-in.css') }}" rel="stylesheet">
<!-- <link href="../static/resources/css/sign-in.css" rel="stylesheet"> -->
</head>
<body class="text-center">
<main class="form-signin w-100 m-auto">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alert alert-danger" role="alert">
{% for category, message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form id="loginForm" method="post" action="/api/user/login">
<img class="mb-4" src="images/登录/zf.svg" alt="" width="72" height="57">
<h1 class="h3 mb-3 fw-normal">ZF-AI</h1>
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" placeholder="UserName" required>
<label for="username">UserName</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
<label for="password">Password</label>
</div>
<div class="captcha-group">
<div class="form-floating captcha">
<input type="text" class="form-control" id="captcha" name="captcha" placeholder="Captcha" required>
<label for="captcha">Captcha</label>
</div>
<img id="captchaImage" src="" alt="Captcha" class="captcha">
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">登 录</button>
<p class="mt-5 mb-3 text-muted">&copy; 2024–2025 ZFKJ All Rights Reserved</p>
</form>
</main>
<script>
const form = document.getElementById('loginForm');
const captchaImage = document.getElementById('captchaImage');
captchaImage.src = '/api/user/code?' + new Date().getTime();
form.addEventListener('submit', function(event) {
});
function showError(errorText) {
alert(errorText);
}
</script>
</body>
</html>

156
web/main/templates/schedule.html

@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>布防时间计划</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<style>
.schedule-table {
width: 100%;
table-layout: fixed;
}
.schedule-table th, .schedule-table td {
text-align: center;
vertical-align: middle;
cursor: pointer;
border: 1px solid #dee2e6;
padding: 8px;
}
.schedule-table td.allowed {
background-color: white;
}
.schedule-table td.blocked {
background-color: blue;
}
</style>
</head>
<body>
<div class="container mt-5">
<table class="schedule-table table table-bordered">
<thead>
<tr>
<th>小时</th>
<th>00</th>
<th>01</th>
<th>02</th>
<th>03</th>
<th>04</th>
<th>05</th>
<th>06</th>
<th>07</th>
<th>08</th>
<th>09</th>
<th>10</th>
<th>11</th>
<th>12</th>
<th>13</th>
<th>14</th>
<th>15</th>
<th>16</th>
<th>17</th>
<th>18</th>
<th>19</th>
<th>20</th>
<th>21</th>
<th>22</th>
<th>23</th>
</tr>
</thead>
<tbody>
<!-- Repeat above row for each day of the week (Monday to Saturday) -->
<tr>
<th>星期一</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期二</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期三</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期四</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期五</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期六</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期日</th>
<!-- 24 cells for each hour -->
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
</tbody>
</table>
<div class="mt-3">
<div class="d-inline-block mr-3">
<div class="d-inline-block align-middle" style="width: 20px; height: 20px; background-color: white; border: 1px solid black;"></div>
<span class="align-middle">允许</span>
</div>
<div class="d-inline-block">
<div class="d-inline-block align-middle" style="width: 20px; height: 20px; background-color: blue;"></div>
<span class="align-middle">阻止</span>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const cells = document.querySelectorAll('.schedule-table tbody td');
cells.forEach(cell => {
cell.addEventListener('click', () => {
if (cell.classList.contains('blocked')) {
cell.classList.remove('blocked');
cell.classList.add('allowed');
} else {
cell.classList.remove('allowed');
cell.classList.add('blocked');
}
});
});
});
</script>
</body>
</html>

146
web/main/templates/view_main.html

@ -0,0 +1,146 @@
{% extends 'base.html' %}
{% block title %}ZFBOX{% endblock %}
{% block style %}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.blue-svg {
color: blue; /* 这将影响所有使用currentColor的fill属性 */
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
.tree-view {
border-right: 1px solid #ddd;
padding: 10px;
overflow-y: auto;
}
.video-frame {
position: relative;
width: calc(50% - 10px); /* 默认4画面布局,每行2个视频框架 */
margin: 5px;
float: left;
background-color: #ccc; /* 默认灰色填充 */
border: 1px solid #000; /* 边框 */
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2c3e50;
color: #fff;
padding: 5px;
}
.video-area {
width: 100%;
padding-bottom: 75%; /* 4:3 比例 */
background-color: #000; /* 默认灰色填充 */
position: relative;
border: 1px solid #ddd; /* 视频区域边框 */
}
.video-area img {
display: none; /* 初始隐藏 */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.video-buttons {
display: flex;
gap: 10px;
}
.video-buttons button {
background: none;
border: none;
color: white;
cursor: pointer;
}
.video-buttons button:hover {
color: #f39c12;
}
.dropdown-menu {
min-width: 100px;
}
.video-frame img {
width: 100%;
height: auto;
}
#videoGrid.four .video-frame {
width: calc(50% - 10px); /* 每行4个视频框架 */
}
#videoGrid.eight .video-frame {
width: calc(12.5% - 10px); /* 每行8个视频框架 */
}
#videoGrid.nine .video-frame {
width: calc(33.33% - 10px); /* 每行3个视频框架,9画面布局 */
}
.toggle-buttons {
margin-bottom: 5px;
}
.btn-small {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: red;
background-color: rgba(255, 255, 255, 0.8); /* 半透明背景 */
padding: 10px;
border-radius: 5px;
}
{% endblock %}
{% block content %}
<div class="container d-flex flex-wrap" >
<div id="treeView" class="tree-view col-md-3 ">
<!-- 动态树视图 -->
</div>
<div class="video-content col-md-9">
<div id="videoGrid" class="row four">
<!-- 动态视频节点 -->
</div>
<div class="toggle-buttons">
<button id="fourView" class="btn btn-primary btn-small">四画面</button>
<button id="nineView" class="btn btn-secondary btn-small">九画面</button>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script src="{{ url_for('main.static', filename='scripts/aiortc-client-new.js') }}"></script>
{% endblock %}

170
web/main/templates/登录.html

@ -1,170 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>登录</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<link href="resources/css/axure_rp_page.css" type="text/css" rel="stylesheet"/>
<link href="data/styles.css" type="text/css" rel="stylesheet"/>
<link href="files/登录/styles.css" type="text/css" rel="stylesheet"/>
<script src="resources/scripts/jquery-3.2.1.min.js"></script>
<script src="resources/scripts/axure/axQuery.js"></script>
<script src="resources/scripts/axure/globals.js"></script>
<script src="resources/scripts/axutils.js"></script>
<script src="resources/scripts/axure/annotation.js"></script>
<script src="resources/scripts/axure/axQuery.std.js"></script>
<script src="resources/scripts/axure/doc.js"></script>
<script src="resources/scripts/messagecenter.js"></script>
<script src="resources/scripts/axure/events.js"></script>
<script src="resources/scripts/axure/recording.js"></script>
<script src="resources/scripts/axure/action.js"></script>
<script src="resources/scripts/axure/expr.js"></script>
<script src="resources/scripts/axure/geometry.js"></script>
<script src="resources/scripts/axure/flyout.js"></script>
<script src="resources/scripts/axure/model.js"></script>
<script src="resources/scripts/axure/repeater.js"></script>
<script src="resources/scripts/axure/sto.js"></script>
<script src="resources/scripts/axure/utils.temp.js"></script>
<script src="resources/scripts/axure/variables.js"></script>
<script src="resources/scripts/axure/drag.js"></script>
<script src="resources/scripts/axure/move.js"></script>
<script src="resources/scripts/axure/visibility.js"></script>
<script src="resources/scripts/axure/style.js"></script>
<script src="resources/scripts/axure/adaptive.js"></script>
<script src="resources/scripts/axure/tree.js"></script>
<script src="resources/scripts/axure/init.temp.js"></script>
<script src="resources/scripts/axure/legacy.js"></script>
<script src="resources/scripts/axure/viewer.js"></script>
<script src="resources/scripts/axure/math.js"></script>
<script src="resources/scripts/axure/jquery.nicescroll.min.js"></script>
<script src="data/document.js"></script>
<script src="files/登录/data.js"></script>
<script type="text/javascript">
$axure.utils.getTransparentGifPath = function() { return 'resources/images/transparent.gif'; };
$axure.utils.getOtherPath = function() { return 'resources/Other.html'; };
$axure.utils.getReloadPath = function() { return 'resources/reload.html'; };
</script>
</head>
<body>
{% if error %}
<div style="color: red;">{{ error }}</div>
{% endif %}
<div id="base" class="">
<!-- Unnamed (Rectangle) -->
<div id="u0" class="ax_default flow_shape">
<div id="u0_div" class=""></div>
<div id="u0_text" class="text " style="display:none; visibility: hidden">
<p></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u1" class="ax_default flow_shape">
<div id="u1_div" class=""></div>
<div id="u1_text" class="text " style="display:none; visibility: hidden">
<p></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u2" class="ax_default heading_1">
<div id="u2_div" class=""></div>
<div id="u2_text" class="text ">
<p><span>智凡BOX</span></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u3" class="ax_default heading_3">
<div id="u3_div" class=""></div>
<div id="u3_text" class="text ">
<p><span>@2024 ZFKJ All Rights Reserved </span></p>
</div>
</div>
<!-- Unnamed (Shape) -->
<div id="u4" class="ax_default icon">
<img id="u4_img" class="img " src="images/登录/u4.svg"/>
<div id="u4_text" class="text " style="display:none; visibility: hidden">
<p></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u5" class="ax_default heading_2">
<div id="u5_div" class=""></div>
<div id="u5_text" class="text ">
<p><span>用户名:</span></p>
</div>
</div>
<!-- Unnamed (Text Field) -->
<div id="u6" class="ax_default text_field">
<div id="u6_div" class=""></div>
<input id="u6_input" type="text" value="" class="u6_input"/>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u7" class="ax_default heading_2">
<div id="u7_div" class=""></div>
<div id="u7_text" class="text ">
<p><span>密码:</span></p>
</div>
</div>
<!-- Unnamed (Text Field) -->
<div id="u8" class="ax_default text_field">
<div id="u8_div" class=""></div>
<input id="u8_input" type="text" value="" class="u8_input"/>
</div>
<!-- Unnamed (Text Field) -->
<div id="u9" class="ax_default text_field">
<div id="u9_div" class=""></div>
<input id="u9_input" type="text" value="" class="u9_input"/>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u10" class="ax_default heading_2">
<div id="u10_div" class=""></div>
<div id="u10_text" class="text ">
<p><span>验证码:</span></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u11" class="ax_default primary_button">
<div id="u11_div" class=""></div>
<div id="u11_text" class="text ">
<p><span>登录</span></p>
</div>
</div>
<!-- Unnamed (Image) -->
<div id="u12" class="ax_default image">
<img id="u12_img" class="img " src="images/登录/u12.png"/>
<div id="u12_text" class="text " style="display:none; visibility: hidden">
<p></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u13" class="ax_default button">
<div id="u13_div" class=""></div>
<div id="u13_text" class="text ">
<p><span>忘记密码</span></p>
</div>
</div>
<!-- Unnamed (Rectangle) -->
<div id="u14" class="ax_default label">
<div id="u14_div" class=""></div>
<div id="u14_text" class="text ">
<p><span>注:通过手机验证码修改密码。</span></p>
</div>
</div>
</div>
<script src="resources/scripts/axure/ios.js"></script>
</body>
</html>

BIN
zfbox.db

Binary file not shown.
Loading…
Cancel
Save