diff --git a/.idea/FristProject.iml b/.idea/FristProject.iml index 719bacc..8678458 100644 --- a/.idea/FristProject.iml +++ b/.idea/FristProject.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 55756c3..25bfb8c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/config.yaml b/config.yaml index 6b06a1a..b9bc873 100644 --- a/config.yaml +++ b/config.yaml @@ -28,11 +28,11 @@ ALLOWED_EXTENSIONS : {'zip'} RTSP_Check_Time : 600 #10分钟 #model -model_platform : acl #acl gpu cpu +model_platform : cpu #acl gpu cpu weight_path: /model/weights yolov5_path: D:/Project/FristProject/model/base_model/yolov5 #使用绝对路径,不同的部署环境需要修改! cap_sleep_time: 300 #5分钟 -buffer_len: 300 #分析后画面缓冲区帧数 -- 可以与验证帧率结合确定缓冲区大小 +buffer_len: 100 #分析后画面缓冲区帧数 -- 可以与验证帧率结合确定缓冲区大小 RESET_INTERVAL : 100000 #帧数重置上限 frame_rate : 20 #帧率参考值 -- 后续作用主要基于verify_rate进行帧率控制 -verify_rate : 5 #验证帧率--- 也就是视频输出的帧率 +verify_rate : 10 #验证帧率--- 也就是视频输出的帧率 diff --git a/core/ModelManager.py b/core/ModelManager.py index c9c9b0b..08fef10 100644 --- a/core/ModelManager.py +++ b/core/ModelManager.py @@ -175,32 +175,32 @@ class ModelManager: # 使用 模型 进行目标检测 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: - # result.append(0) + 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: + result.append(0) if not isverify: #没做处理,直接返回的,需要控制下帧率,太快读取没有意义。 time.sleep(1.0/self.frame_rate) #给个默认帧率,不超过30帧,---若经过模型计算,CPU下单模型也就12帧这样 @@ -247,7 +247,8 @@ class ModelManager: #获取视频通道的模型相关数据-list for model in myModels: #基于基类实例化模块类 - m = self._import_model("",model[5],model[8]) #动态加载模型处理文件py --需要验证模型文件是否能加载 + #m = self._import_model("",model[5],model[8]) #动态加载模型处理文件py --需要验证模型文件是否能加载 + m = None if m: myModle_list.append(m) #没有成功加载的模型原画输出 myModle_data.append(model) @@ -326,9 +327,10 @@ class ModelManager: # end_time = time.time() # 结束时间 # print(f"Processing time: {end_time - start_time} seconds") # 本地显示---测试使用 - # cv2.imshow('Frame', img) - # if cv2.waitKey(1) & 0xFF == ord('q'): - # break + if channel_id == 2: + cv2.imshow(str(channel_id), img) + if cv2.waitKey(1) & 0xFF == ord('q'): + break #结束线程 cap.release() #cv2.destroyAllWindows() diff --git a/model/plugins/ModelBase.py b/model/plugins/ModelBase.py index 4c7e5d8..6bded86 100644 --- a/model/plugins/ModelBase.py +++ b/model/plugins/ModelBase.py @@ -5,7 +5,8 @@ import numpy as np import cv2 import ast import platform -import acl +if myCongif.get_data("model_platform") == "acl": + import acl #-----acl相关------ SUCCESS = 0 # 成功状态值 diff --git a/run.py b/run.py index 4c49222..b4e9534 100644 --- a/run.py +++ b/run.py @@ -25,6 +25,6 @@ if __name__ == '__main__': raise NotImplementedError(f"Unsupported operating system: {system}") print(free/(1024*1024)) mMM.start_work() # 启动所有通道的处理 - mVManager.start_check_rtsp() #线程更新视频在线情况 + #mVManager.start_check_rtsp() #线程更新视频在线情况 web.run(debug=True,port=5001,host="0.0.0.0") diff --git a/web/API/viedo.py b/web/API/viedo.py index 2a293d3..aab604a 100644 --- a/web/API/viedo.py +++ b/web/API/viedo.py @@ -2,7 +2,7 @@ import cv2 import asyncio import time from . import api -from quart import jsonify, request +from quart import jsonify, request,websocket from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack,RTCConfiguration from core.ModelManager import mMM from core.DBManager import mDBM @@ -10,6 +10,7 @@ from myutils.ConfigManager import myCongif from fractions import Fraction import threading import logging +from quart.helpers import Response # 配置日志 logging.basicConfig(level=logging.INFO) @@ -53,48 +54,48 @@ pcs = {} ''' ---------------------传输-------------------------- ''' -class VideoTransformTrack(VideoStreamTrack): - kind = "video" - def __init__(self,cid,itype=1): #0-usb,1-RTSP,2-海康SDK --itype已没有作用 - super().__init__() - self.channel_id = cid - - self.frame_rate = myCongif.get_data("frame_rate") - self.time_base = Fraction(1, self.frame_rate) - self.frame_count = 0 - self.start_time = time.time() - - async def recv(self): - new_frame = None - time.sleep(1.0 / self.frame_rate) - new_frame = mMM.verify_list[self.channel_id][4] - # while True: - # new_frame = mMM.verify_list[self.channel_id][4] - # #new_frame = av.VideoFrame.from_ndarray(img, format="rgb24") - # if new_frame is not None: - # break - # else: - # time.sleep(1.0 / self.frame_rate) - # 设置时间戳和时间基数 -- 根据耗时实时计算帧率 - # elapsed_time = time.time() - self.start_time - # self.frame_count += 1 - # actual_frame_rate = self.frame_count / elapsed_time - # self.time_base = Fraction(1, int(actual_frame_rate)) - #设置时间戳和帧率 - self.frame_count += 1 - if self.frame_count > myCongif.get_data("RESET_INTERVAL"): - self.frame_count = 0 - new_frame.pts = self.frame_count - new_frame.time_base = self.time_base - print(f"{self.channel_id} -- Frame pts: {new_frame.pts}, time_base: {new_frame.time_base}") - - # 将检测结果图像转换为帧 - # new_frame.pts = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES)) - # new_frame.time_base = self.time_base - # end_time = time.time() # 结束时间 - # print(f"Processing time: {end_time - start_time} seconds") - #print("webRTC recv at:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) - return new_frame +# class VideoTransformTrack(VideoStreamTrack): +# kind = "video" +# def __init__(self,cid,itype=1): #0-usb,1-RTSP,2-海康SDK --itype已没有作用 +# super().__init__() +# self.channel_id = cid +# +# self.frame_rate = myCongif.get_data("frame_rate") +# self.time_base = Fraction(1, self.frame_rate) +# self.frame_count = 0 +# self.start_time = time.time() +# +# async def recv(self): +# new_frame = None +# time.sleep(1.0 / self.frame_rate) +# new_frame = mMM.verify_list[self.channel_id][4] +# # while True: +# # new_frame = mMM.verify_list[self.channel_id][4] +# # #new_frame = av.VideoFrame.from_ndarray(img, format="rgb24") +# # if new_frame is not None: +# # break +# # else: +# # time.sleep(1.0 / self.frame_rate) +# # 设置时间戳和时间基数 -- 根据耗时实时计算帧率 +# # elapsed_time = time.time() - self.start_time +# # self.frame_count += 1 +# # actual_frame_rate = self.frame_count / elapsed_time +# # self.time_base = Fraction(1, int(actual_frame_rate)) +# #设置时间戳和帧率 +# self.frame_count += 1 +# if self.frame_count > myCongif.get_data("RESET_INTERVAL"): +# self.frame_count = 0 +# new_frame.pts = self.frame_count +# new_frame.time_base = self.time_base +# #print(f"{self.channel_id} -- Frame pts: {new_frame.pts}, time_base: {new_frame.time_base}") +# +# # 将检测结果图像转换为帧 +# # new_frame.pts = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES)) +# # new_frame.time_base = self.time_base +# # end_time = time.time() # 结束时间 +# # print(f"Processing time: {end_time - start_time} seconds") +# #print("webRTC recv at:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) +# return new_frame async def get_stats(peer_connection): stats = await peer_connection.getStats() @@ -104,89 +105,137 @@ async def get_stats(peer_connection): print(f"Packets Sent: {report.packetsSent}") print(f"Bytes Sent: {report.bytesSent}") -@api.route('/offer', methods=['POST']) -async def offer(): - #接收客户端的连接请求 - params = await request.json - channel_id = params["cid"] #需要传参过来 - itype = params["mytype"] - element_id = params["element_id"] - reStatus = 0 - reMsg = "" - ret = False - if itype == 1: - #添加通道和页面控件ID的关系 - strsql = f"select * from channel where element_id = {element_id}" - data = mDBM.do_select(strsql,1) - if data: - reStatus = 0 - reMsg = '该控件已经关联某一视频通道,请确认数据是否正确!' - return jsonify({'status': reStatus, 'msg': reMsg}) - 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: - reStatus = 0 - reMsg = '播放画面失败,请联系技术支持!' - #提取客户端发来的SDP,生成服务器端的SDP - offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - # # 配置 STUN 服务器 - #ice_servers = [{"urls": []}]# 禁用 STUN 服务器 - # pc = RTCPeerConnection(configuration={"iceServers": ice_servers}) - config = RTCConfiguration(iceServers=[]) - pc = RTCPeerConnection(configuration=config) - # t = threading.Thread(target=get_stats,args=(pc,)) - # t.start() - - #pc = RTCPeerConnection() # 实例化一个rtcp对象 - pcs[channel_id] = pc # 集合中添加一个对象,若已存在则不添 - - @pc.on("datachannel") - def on_datachannel(channel): - @channel.on("message") - def on_message(message): - print(f'Message received: {message}') - # 处理接收到的数据 - if message == 'pause': - # 执行暂停操作 - pass - elif message == 'resume': - # 执行继续操作 - pass - - #监听RTC连接状态 - @pc.on("iconnectionstatechange") #当ice连接状态发生变化时 - async def iconnectionstatechange(): - if pc.iceConnectionState == "failed": - await pc.close() - pcs.pop(channel_id, None) #移除对象 - - # 添加视频轨道 - video_track = VideoTransformTrack(channel_id) - pc.addTrack(video_track) +# @api.route('/offer', methods=['POST']) +# async def offer(): +# #接收客户端的连接请求 +# params = await request.json +# channel_id = params["cid"] #需要传参过来 +# itype = params["mytype"] +# element_id = params["element_id"] +# reStatus = 0 +# reMsg = "" +# ret = False +# if itype == 1: +# #添加通道和页面控件ID的关系 +# strsql = f"select * from channel where element_id = {element_id}" +# data = mDBM.do_select(strsql,1) +# if data: +# reStatus = 0 +# reMsg = '该控件已经关联某一视频通道,请确认数据是否正确!' +# return jsonify({'status': reStatus, 'msg': reMsg}) +# 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: +# reStatus = 0 +# reMsg = '播放画面失败,请联系技术支持!' +# #提取客户端发来的SDP,生成服务器端的SDP +# offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) +# # # 配置 STUN 服务器 +# #ice_servers = [{"urls": []}]# 禁用 STUN 服务器 +# # pc = RTCPeerConnection(configuration={"iceServers": ice_servers}) +# config = RTCConfiguration(iceServers=[]) +# pc = RTCPeerConnection(configuration=config) +# # t = threading.Thread(target=get_stats,args=(pc,)) +# # t.start() +# +# #pc = RTCPeerConnection() # 实例化一个rtcp对象 +# pcs[channel_id] = pc # 集合中添加一个对象,若已存在则不添 +# +# @pc.on("datachannel") +# def on_datachannel(channel): +# @channel.on("message") +# def on_message(message): +# print(f'Message received: {message}') +# # 处理接收到的数据 +# if message == 'pause': +# # 执行暂停操作 +# pass +# elif message == 'resume': +# # 执行继续操作 +# pass +# +# #监听RTC连接状态 +# @pc.on("iconnectionstatechange") #当ice连接状态发生变化时 +# async def iconnectionstatechange(): +# if pc.iceConnectionState == "failed": +# await pc.close() +# pcs.pop(channel_id, None) #移除对象 +# +# # 添加视频轨道 +# video_track = VideoTransformTrack(channel_id) +# pc.addTrack(video_track) +# +# # @pc.on('track') --- stream.getTracks().forEach(track => pc.addTrack(track, stream)); 猜测是这里触发的添加了摄像头通道 +# # def on_track(track): +# # if track.kind == 'video': +# # local_video = VideoTransformTrack(track) +# # pc.addTrack(local_video) +# +# # 记录客户端 SDP +# await pc.setRemoteDescription(offer) +# # 生成本地 SDP +# answer = await pc.createAnswer() +# # 记录本地 SDP +# await pc.setLocalDescription(answer) +# +# print("返回sdp") +# return jsonify({ +# "sdp": pc.localDescription.sdp, +# "type": pc.localDescription.type, +# "status":reStatus, #itype =1 的时候才有意义 +# "msg":reMsg +# }) - # @pc.on('track') --- stream.getTracks().forEach(track => pc.addTrack(track, stream)); 猜测是这里触发的添加了摄像头通道 - # def on_track(track): - # if track.kind == 'video': - # local_video = VideoTransformTrack(track) - # pc.addTrack(local_video) +async def generate_frames(stream_id): + #video_capture = streams[stream_id] + start_time = time.time() + icount = 0 + frame_rate_time = 1.0 / myCongif.get_data("frame_rate") + while True: + try: + await asyncio.sleep(frame_rate_time) + frame = mMM.verify_list[int(stream_id)][4] + if frame is not None: + img = frame.to_ndarray(format="bgr24") + ret, buffer = cv2.imencode('.jpg', img) + if not ret: + continue + frame = buffer.tobytes() + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + end_time = time.time() # 结束时间 + icount += 1 + print(f"icount:{icount},Processing time: {end_time - start_time} seconds") + except asyncio.CancelledError: + print("Task was cancelled") + break # 处理任务取消 + except Exception as e: + print(f"Error: {e}") + break # 在出现异常时退出循环 + print("退出循环") - # 记录客户端 SDP - await pc.setRemoteDescription(offer) - # 生成本地 SDP - answer = await pc.createAnswer() - # 记录本地 SDP - await pc.setLocalDescription(answer) +@api.route('/video_feed/') +async def video_feed(stream_id): + if int(stream_id) != 2: + return "None" + print("执行一次:video_feed") + return Response(generate_frames(stream_id),mimetype='multipart/x-mixed-replace; boundary=frame') - print("返回sdp") - return jsonify({ - "sdp": pc.localDescription.sdp, - "type": pc.localDescription.type, - "status":reStatus, #itype =1 的时候才有意义 - "msg":reMsg - }) +@api.websocket('/ws/video_feed/') +async def ws_video_feed(channel_id): + while True: + frame = mMM.verify_list[int(channel_id)][4] + if frame is not None: + img = frame.to_ndarray(format="bgr24") + ret, buffer = cv2.imencode('.jpg', img) + if not ret: + continue + frame = buffer.tobytes() + await websocket.send(frame) + await asyncio.sleep(1.0 / myCongif.get_data("frame_rate")) # Adjust based on frame rate @api.route('/shutdown', methods=['POST']) async def shutdown():#这是全关 diff --git a/web/__init__.py b/web/__init__.py index c05247f..72d1d41 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -4,6 +4,7 @@ from pymemcache.client import base from .main import main from .API import api from functools import wraps +from myutils.ConfigManager import myCongif from quart_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -34,8 +35,10 @@ def create_app(): # app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' # app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'zfxxkj_2024_!@#' - - app.config['SESSION_TYPE'] = 'memcached' # session类型 + if myCongif.get_data("model_platform") == "acl": + app.config['SESSION_TYPE'] = 'memcached' # session类型 + elif myCongif.get_data("model_platform") =="cpu": + app.config['SESSION_TYPE'] = 'redis' # session类型 #app.config['SESSION_FILE_DIR'] = './sessions' # session保存路径 #app.config['SESSION_MEMCACHED'] = base.Client(('localhost', 11211)) app.config['SESSION_PERMANENT'] = True # 如果设置为True,则关闭浏览器session就失效。 @@ -45,12 +48,6 @@ def create_app(): app.session_interface = MemcachedSessionInterface(memcached_client) Session(app) - #ORM数据库管理 - # db = SQLAlchemy() - # migrate = Migrate() - # db.init_app(app) - # migrate.init_app(app, db) # 创建对象,使用它来将ORM模型映射到数据库 -- 效果是及时更新数据的修改到文档? - # 注册main app.register_blueprint(main) #注册API模块 diff --git a/web/main/static/resources/scripts/aiortc-client-new.js b/web/main/static/resources/scripts/aiortc-client-new.js index 84ec533..6efecca 100644 --- a/web/main/static/resources/scripts/aiortc-client-new.js +++ b/web/main/static/resources/scripts/aiortc-client-new.js @@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', async function() { // console.log(`Type: ${channel.type}`); // console.log(`Status: ${channel.status}`); // console.log(`Element ID: ${channel.element_id}`); - start(channel.element_id,channel.ID,0) + connectToStream(channel.element_id,channel.ID,channel.area_name,channel.channel_name) } }); } catch (error) { @@ -28,86 +28,37 @@ document.addEventListener('DOMContentLoaded', async function() { } }); -function negotiate(pc,channel_id,itype,element_id) { - pc.addTransceiver('video', { direction: 'recvonly' }); - return pc.createOffer().then((offer) => { - return pc.setLocalDescription(offer); - }).then(() => { - // wait for ICE gathering to complete - return new Promise((resolve) => { - if (pc.iceGatheringState === 'complete') { - resolve(); - } else { - const checkState = () => { - if (pc.iceGatheringState === 'complete') { - pc.removeEventListener('icegatheringstatechange', checkState); - resolve(); +function connectToStream(element_id,channel_id,area_name,channel_name) { +// const videoContainer = document.getElementById('video-container'); +// const imgElement = document.createElement('img'); +// imgElement.id = `${element_id}`; +// videoContainer.appendChild(imgElement); + 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); }; - pc.addEventListener('icegatheringstatechange', checkState); - } - }); - }).then(() => { - var offer = pc.localDescription; - return fetch('/api/offer', { - body: JSON.stringify({ - sdp: offer.sdp, - type: offer.type, - cid: channel_id, - mytype: itype, - element_id:element_id - }), - headers: { - 'Content-Type': 'application/json' - }, - method: 'POST' - }); - }).then((response) => { - return response.json(); - }).then((answer) => { - return pc.setRemoteDescription(answer); - }).catch((e) => { - alert(e); - }); -} -function start(element_id,channel_id,itype) {// iytpe =0,不用修改数据库,1,需要添加数据库记录 - console.log(`Element ID: ${element_id}`); - console.log(`Channel ID: ${channel_id}`); - pc = new RTCPeerConnection(); - pc_list[channel_id] = pc; //保留pc - pc.addEventListener('track', (evt) => { - if (evt.track.kind == 'video') { - //document.getElementById('video').srcObject = evt.streams[0]; - document.getElementById(element_id).srcObject = evt.streams[0]; - } - }); - negotiate(pc,channel_id,itype,element_id); -} + socket.onclose = function() { + setTimeout(connect, 1000); // 尝试在1秒后重新连接 + }; -function showView(){ - channel_list.forEach(channel => { - if(channel.element_id){ //""空为false,非空为true - console.log('status:',document.getElementById(channel.element_id).paused) - if (document.getElementById(channel.element_id).paused) { - document.getElementById(channel.element_id).play().catch(error => { - console.error('Error playing video:', error); - }); + socket.onerror = function() { + console.log(`WebSocket错误,尝试重新连接... Channel ID: ${channel_id}`); + socket.close(); + }; } - } - }); -} -async function closestream(channel_id){ - let response = await fetch('/close_stream', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ channel_id: channel_id }) - }); - let data = await response.json(); - console.log(data); - if(pc_list[channel_id]){ - pc_list[channel_id].close(); - delete pc_list[channel_id]; - } -} \ No newline at end of file + connect(); + } \ No newline at end of file diff --git a/web/main/templates/实时预览.html b/web/main/templates/实时预览.html index d8c6c4d..82af553 100644 --- a/web/main/templates/实时预览.html +++ b/web/main/templates/实时预览.html @@ -171,58 +171,86 @@

+
- + Video Stream
- +
- + Video Stream
+
- + + Video Stream
- + + Video Stream
- + + Video Stream
- + + Video Stream
- + + Video Stream
- + + Video Stream
diff --git a/zfbox.db b/zfbox.db index 851002e..b971ee5 100644 Binary files a/zfbox.db and b/zfbox.db differ