视频记录
This commit is contained in:
Binary file not shown.
35
check_encoders.py
Normal file
35
check_encoders.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import cv2
|
||||
|
||||
print("OpenCV版本:", cv2.__version__)
|
||||
print("可用的视频编码器:")
|
||||
|
||||
# 打印常见编码器是否可用
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"MP4V")
|
||||
print("MP4V (MPEG-4 Part 2)", "可用")
|
||||
except Exception as e:
|
||||
print("MP4V (MPEG-4 Part 2)", "不可用:", e)
|
||||
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"avc1")
|
||||
print("avc1 (H.264)", "可用")
|
||||
except Exception as e:
|
||||
print("avc1 (H.264)", "不可用:", e)
|
||||
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"DIVX")
|
||||
print("DIVX (DivX)", "可用")
|
||||
except Exception as e:
|
||||
print("DIVX (DivX)", "不可用:", e)
|
||||
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"XVID")
|
||||
print("XVID (Xvid)", "可用")
|
||||
except Exception as e:
|
||||
print("XVID (Xvid)", "不可用:", e)
|
||||
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"MJPG")
|
||||
print("MJPG (Motion JPEG)", "可用")
|
||||
except Exception as e:
|
||||
print("MJPG (Motion JPEG)", "不可用:", e)
|
||||
@@ -1,11 +1,17 @@
|
||||
# mjpeg_streamer.pynano
|
||||
import threading
|
||||
import time
|
||||
from flask import Flask, Response, render_template, request, redirect, session, url_for
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, Response, render_template, request, redirect, session, url_for, send_from_directory
|
||||
import cv2
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import stat
|
||||
|
||||
# ====== 新增:登录配置 ======
|
||||
AUTO_LOGIN = None # 👈 设置为 True 可跳过登录
|
||||
AUTO_LOGIN = True # 👈 设置为 True 可跳过登录
|
||||
VALID_USER = {"username": "admin", "password": "admin"}
|
||||
|
||||
class MJPEGServer:
|
||||
@@ -16,11 +22,35 @@ class MJPEGServer:
|
||||
self.app = Flask(__name__)
|
||||
self.app.secret_key = 'your-secret-key-change-in-prod' # 用于 session
|
||||
|
||||
# H264编码器参数
|
||||
self.h264_encoder = None
|
||||
self.h264_fourcc = None
|
||||
self.h264_width = None
|
||||
self.h264_height = None
|
||||
|
||||
# 视频录制配置
|
||||
self.recording_enabled = True
|
||||
self.segment_duration = 60 # 视频分段时长(秒)
|
||||
self.storage_path = "/video_records" # 视频存储路径
|
||||
self.max_retention_days = 30 # 最大保留天数
|
||||
self.video_writer = None
|
||||
self.current_segment_start = None
|
||||
self.recording_thread = None
|
||||
self.recording_stop_event = threading.Event()
|
||||
|
||||
# 确保存储目录存在
|
||||
os.makedirs(self.storage_path, exist_ok=True)
|
||||
os.chmod(self.storage_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||||
|
||||
# 路由
|
||||
self.app.add_url_rule('/', 'index', self.index)
|
||||
self.app.add_url_rule('/login', 'login', self.login, methods=['GET', 'POST'])
|
||||
self.app.add_url_rule('/logout', 'logout', self.logout)
|
||||
self.app.add_url_rule('/video_feed', 'video_feed', self.video_feed)
|
||||
self.app.add_url_rule('/h264_feed', 'h264_feed', self.h264_feed)
|
||||
self.app.add_url_rule('/api/videos', 'get_videos', self.get_videos)
|
||||
self.app.add_url_rule('/api/video/<path:video_path>', 'serve_video', self.serve_video)
|
||||
self.app.add_url_rule('/api/disk_usage', 'get_disk_usage', self.get_disk_usage)
|
||||
|
||||
# 静态文件自动托管(Layui)
|
||||
self.app.static_folder = 'static'
|
||||
@@ -66,13 +96,312 @@ class MJPEGServer:
|
||||
if not success or frame is None:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
ret, buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
|
||||
ret, buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 95])
|
||||
if not ret:
|
||||
continue
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
||||
|
||||
def h264_feed(self):
|
||||
"""提供H264视频流"""
|
||||
if not self.check_auth():
|
||||
return '', 403
|
||||
return Response(self._gen_h264_ffmpeg(),
|
||||
mimetype='video/H264')
|
||||
|
||||
def _gen_h264_ffmpeg(self):
|
||||
"""使用ffmpeg生成H264视频流"""
|
||||
# 获取第一帧以确定视频参数
|
||||
success, frame = self.frame_buffer.get_frame()
|
||||
if not success or frame is None:
|
||||
raise ValueError("无法获取视频帧")
|
||||
|
||||
height, width = frame.shape[:2]
|
||||
fps = 25.0
|
||||
|
||||
# 构造ffmpeg命令
|
||||
ffmpeg_cmd = [
|
||||
'ffmpeg',
|
||||
'-f', 'rawvideo',
|
||||
'-vcodec', 'rawvideo',
|
||||
'-pix_fmt', 'bgr24',
|
||||
'-s', f'{width}x{height}',
|
||||
'-r', str(fps),
|
||||
'-i', '-',
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'ultrafast',
|
||||
'-tune', 'zerolatency',
|
||||
'-b:v', '2000k',
|
||||
'-f', 'h264',
|
||||
'-'
|
||||
]
|
||||
|
||||
# 启动ffmpeg进程
|
||||
ffmpeg_process = subprocess.Popen(
|
||||
ffmpeg_cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 获取新帧
|
||||
success, frame = self.frame_buffer.get_frame()
|
||||
if not success or frame is None:
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# 确保帧尺寸一致
|
||||
if frame.shape[1] != width or frame.shape[0] != height:
|
||||
frame = cv2.resize(frame, (width, height))
|
||||
|
||||
# 写入ffmpeg标准输入
|
||||
ffmpeg_process.stdin.write(frame.tobytes())
|
||||
ffmpeg_process.stdin.flush()
|
||||
|
||||
# 读取编码后的H264数据
|
||||
if ffmpeg_process.stdout.poll() is None:
|
||||
h264_data = ffmpeg_process.stdout.read(4096)
|
||||
if h264_data:
|
||||
yield h264_data
|
||||
finally:
|
||||
# 清理资源
|
||||
if ffmpeg_process.stdin:
|
||||
ffmpeg_process.stdin.close()
|
||||
if ffmpeg_process.stdout:
|
||||
ffmpeg_process.stdout.close()
|
||||
if ffmpeg_process.stderr:
|
||||
ffmpeg_process.stderr.close()
|
||||
ffmpeg_process.wait()
|
||||
|
||||
def _start_recording_thread(self):
|
||||
"""启动视频录制线程"""
|
||||
if not self.recording_thread or not self.recording_thread.is_alive():
|
||||
self.recording_stop_event.clear()
|
||||
self.recording_thread = threading.Thread(target=self._record_video, daemon=True)
|
||||
self.recording_thread.start()
|
||||
print(f"[RECORDING] 视频录制线程已启动,分段时长: {self.segment_duration}秒")
|
||||
|
||||
def _record_video(self):
|
||||
"""视频录制主循环"""
|
||||
while not self.recording_stop_event.is_set():
|
||||
try:
|
||||
success, frame = self.frame_buffer.get_frame()
|
||||
if not success or frame is None:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 检查是否需要创建新的视频分段
|
||||
now = datetime.now()
|
||||
if not self.video_writer or (now - self.current_segment_start).total_seconds() >= self.segment_duration:
|
||||
self._create_new_segment(frame)
|
||||
|
||||
# 写入视频帧
|
||||
if self.video_writer:
|
||||
if self.video_writer == "ffmpeg":
|
||||
# 使用FFmpeg写入帧
|
||||
if hasattr(self, 'ffmpeg_process') and self.ffmpeg_process.stdin:
|
||||
try:
|
||||
self.ffmpeg_process.stdin.write(frame.tobytes())
|
||||
self.ffmpeg_process.stdin.flush()
|
||||
except BrokenPipeError:
|
||||
print(f"[ERROR] FFmpeg进程已断开,重新创建分段")
|
||||
self._create_new_segment(frame)
|
||||
else:
|
||||
# 使用OpenCV写入帧
|
||||
self.video_writer.write(frame)
|
||||
|
||||
# 定期清理旧视频
|
||||
if now.second % 60 == 0: # 每分钟检查一次
|
||||
self._clean_old_videos()
|
||||
|
||||
# 移除sleep,让录制线程尽可能快地处理帧
|
||||
|
||||
except Exception as e:
|
||||
print(f"[RECORDING ERROR] {e}")
|
||||
time.sleep(1)
|
||||
|
||||
def _create_new_segment(self, frame):
|
||||
"""创建新的视频分段文件"""
|
||||
# 关闭当前视频写入器
|
||||
if self.video_writer:
|
||||
if self.video_writer == "ffmpeg":
|
||||
# 关闭FFmpeg进程
|
||||
if hasattr(self, 'ffmpeg_process'):
|
||||
try:
|
||||
if self.ffmpeg_process.stdin:
|
||||
self.ffmpeg_process.stdin.close()
|
||||
if self.ffmpeg_process.stdout:
|
||||
self.ffmpeg_process.stdout.close()
|
||||
if self.ffmpeg_process.stderr:
|
||||
self.ffmpeg_process.stderr.close()
|
||||
self.ffmpeg_process.wait(timeout=5)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 关闭FFmpeg进程失败: {e}")
|
||||
self.ffmpeg_process.terminate()
|
||||
self.ffmpeg_process.wait(timeout=2)
|
||||
else:
|
||||
# 关闭OpenCV VideoWriter
|
||||
self.video_writer.release()
|
||||
self.video_writer = None
|
||||
|
||||
# 创建日期目录
|
||||
now = datetime.now()
|
||||
self.current_segment_start = now
|
||||
date_dir = os.path.join(self.storage_path, now.strftime("%Y-%m-%d"))
|
||||
os.makedirs(date_dir, exist_ok=True)
|
||||
os.chmod(date_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||||
|
||||
# 生成视频文件名
|
||||
timestamp = now.strftime("%H-%M-%S")
|
||||
video_path = os.path.join(date_dir, f"video_{timestamp}.mp4")
|
||||
|
||||
# 创建新的视频写入器
|
||||
height, width = frame.shape[:2]
|
||||
fps = 15.0
|
||||
|
||||
# 使用FFmpeg通过subprocess创建视频录制进程
|
||||
try:
|
||||
# 构建FFmpeg命令
|
||||
self.ffmpeg_cmd = [
|
||||
'ffmpeg',
|
||||
'-y', # 覆盖现有文件
|
||||
'-f', 'rawvideo', # 输入格式为原始视频
|
||||
'-vcodec', 'rawvideo',
|
||||
'-pix_fmt', 'bgr24', # OpenCV默认的BGR格式
|
||||
'-s', f'{width}x{height}', # 视频分辨率
|
||||
'-r', str(fps), # 帧率
|
||||
'-i', '-', # 从标准输入读取
|
||||
'-c:v', 'libx264', # 使用H.264编码器
|
||||
'-preset', 'ultrafast', # 编码速度优先
|
||||
'-tune', 'zerolatency', # 低延迟
|
||||
'-b:v', '2M', # 比特率
|
||||
'-f', 'mp4', # 输出格式为MP4
|
||||
video_path
|
||||
]
|
||||
|
||||
# 启动FFmpeg进程
|
||||
self.ffmpeg_process = subprocess.Popen(
|
||||
self.ffmpeg_cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
self.video_writer = "ffmpeg" # 标记为使用FFmpeg
|
||||
print(f"[RECORDING] 使用FFmpeg开始录制新分段: {video_path}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] FFmpeg录制失败: {e}")
|
||||
# 降级使用OpenCV VideoWriter
|
||||
fourcc = cv2.VideoWriter_fourcc(*"MJPG")
|
||||
self.video_writer = cv2.VideoWriter(video_path + '.avi', fourcc, fps, (width, height))
|
||||
print(f"[RECORDING] 降级使用OpenCV开始录制新分段: {video_path}.avi")
|
||||
|
||||
def _clean_old_videos(self):
|
||||
"""清理超过保留期限的旧视频"""
|
||||
try:
|
||||
cutoff_date = datetime.now() - timedelta(days=self.max_retention_days)
|
||||
cutoff_str = cutoff_date.strftime("%Y-%m-%d")
|
||||
|
||||
# 遍历所有日期目录
|
||||
for date_dir in os.listdir(self.storage_path):
|
||||
if date_dir < cutoff_str:
|
||||
dir_path = os.path.join(self.storage_path, date_dir)
|
||||
if os.path.isdir(dir_path):
|
||||
shutil.rmtree(dir_path)
|
||||
print(f"[CLEANUP] 删除过期视频目录: {dir_path}")
|
||||
except Exception as e:
|
||||
print(f"[CLEANUP ERROR] {e}")
|
||||
|
||||
def get_videos(self):
|
||||
"""API: 获取视频文件列表"""
|
||||
if not self.check_auth():
|
||||
return {'error': 'Unauthorized'}, 403
|
||||
|
||||
videos = []
|
||||
try:
|
||||
# 获取所有日期目录
|
||||
date_dirs = sorted([d for d in os.listdir(self.storage_path) if os.path.isdir(os.path.join(self.storage_path, d))], reverse=True)
|
||||
|
||||
for date_dir in date_dirs:
|
||||
date_path = os.path.join(self.storage_path, date_dir)
|
||||
video_files = sorted([f for f in os.listdir(date_path) if f.endswith('.avi') or f.endswith('.mp4')])
|
||||
|
||||
for video_file in video_files:
|
||||
video_path = os.path.join(date_dir, video_file)
|
||||
full_path = os.path.join(self.storage_path, video_path)
|
||||
size = os.path.getsize(full_path) / (1024 * 1024) # MB
|
||||
mtime = os.path.getmtime(full_path)
|
||||
# 根据文件扩展名获取时间
|
||||
if video_file.endswith('.avi'):
|
||||
time_str = video_file.split('_')[1].replace('.avi', '')
|
||||
else:
|
||||
time_str = video_file.split('_')[1].replace('.mp4', '')
|
||||
videos.append({
|
||||
'path': video_path,
|
||||
'date': date_dir,
|
||||
'time': time_str,
|
||||
'size': round(size, 2),
|
||||
'mtime': mtime
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[API ERROR] 获取视频列表失败: {e}")
|
||||
return {'error': 'Failed to get videos'}, 500
|
||||
|
||||
return {'videos': videos}
|
||||
|
||||
def serve_video(self, video_path):
|
||||
"""API: 提供视频文件下载/播放"""
|
||||
if not self.check_auth():
|
||||
return {'error': 'Unauthorized'}, 403
|
||||
|
||||
try:
|
||||
# 确保路径安全,防止目录遍历
|
||||
full_path = os.path.abspath(os.path.join(self.storage_path, video_path))
|
||||
if not full_path.startswith(os.path.abspath(self.storage_path)):
|
||||
return {'error': 'Invalid path'}, 400
|
||||
|
||||
directory = os.path.dirname(full_path)
|
||||
filename = os.path.basename(full_path)
|
||||
# 根据文件扩展名设置正确的MIME类型
|
||||
if filename.endswith('.avi'):
|
||||
mimetype = 'video/x-msvideo'
|
||||
elif filename.endswith('.mp4'):
|
||||
mimetype = 'video/mp4'
|
||||
else:
|
||||
mimetype = 'video/mp4' # 默认使用mp4的MIME类型
|
||||
return send_from_directory(directory, filename, mimetype=mimetype)
|
||||
except Exception as e:
|
||||
print(f"[API ERROR] 提供视频文件失败: {e}")
|
||||
return {'error': 'Failed to serve video'}, 500
|
||||
|
||||
def get_disk_usage(self):
|
||||
"""API: 获取磁盘使用情况"""
|
||||
if not self.check_auth():
|
||||
return {'error': 'Unauthorized'}, 403
|
||||
|
||||
try:
|
||||
statvfs = os.statvfs(self.storage_path)
|
||||
total = statvfs.f_frsize * statvfs.f_blocks / (1024 * 1024 * 1024) # GB
|
||||
used = statvfs.f_frsize * (statvfs.f_blocks - statvfs.f_bfree) / (1024 * 1024 * 1024) # GB
|
||||
free = statvfs.f_frsize * statvfs.f_bfree / (1024 * 1024 * 1024) # GB
|
||||
usage = (used / total) * 100 if total > 0 else 0
|
||||
|
||||
return {
|
||||
'total': round(total, 2),
|
||||
'used': round(used, 2),
|
||||
'free': round(free, 2),
|
||||
'usage_percent': round(usage, 1)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[API ERROR] 获取磁盘使用情况失败: {e}")
|
||||
return {'error': 'Failed to get disk usage'}, 500
|
||||
|
||||
def start(self):
|
||||
# 启动视频录制
|
||||
self._start_recording_thread()
|
||||
|
||||
# 启动Web服务器
|
||||
thread = threading.Thread(
|
||||
target=self.app.run,
|
||||
kwargs={'host': self.host, 'port': self.port, 'debug': False, 'use_reloader': False},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,15 +4,22 @@
|
||||
<meta charset="utf-8">
|
||||
<title>环视系统</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link href="https://vjs.zencdn.net/8.23.3/video-js.css" rel="stylesheet" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }
|
||||
#app { display: flex; height: 100vh; }
|
||||
#sidebar { width: 200px; background: #393D49; color: white; padding: 20px 0; }
|
||||
#main { flex: 1; position: relative; background: black; }
|
||||
#video { width: 100%; height: 100%; object-fit: contain; }
|
||||
#video-player { width: 100%; height: 100%; display: none; }
|
||||
.menu-item { padding: 12px 20px; cursor: pointer; }
|
||||
.menu-item:hover { background: #4B515D; }
|
||||
.active { background: #009688 !important; }
|
||||
.disk-info { padding: 10px; background: #4B515D; margin: 10px; border-radius: 5px; font-size: 12px; }
|
||||
.disk-bar { width: 100%; height: 6px; background: #5a5e6a; border-radius: 3px; margin: 5px 0; overflow: hidden; }
|
||||
.disk-fill { height: 100%; background: #009688; transition: width 0.3s; }
|
||||
.control-panel { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; color: white; }
|
||||
.playback-panel { position: absolute; top: 10px; left: 220px; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; color: white; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -24,26 +31,192 @@
|
||||
<div class="menu-item" data-view="left">左视</div>
|
||||
<div class="menu-item" data-view="right">右视</div>
|
||||
<hr style="border-color:#5a5e6a;margin:15px 10px">
|
||||
<div class="menu-item" id="live-view">实时监控</div>
|
||||
<div class="menu-item" id="playback-view">视频回放</div>
|
||||
<hr style="border-color:#5a5e6a;margin:15px 10px">
|
||||
<div class="disk-info">
|
||||
<div>硬盘使用率</div>
|
||||
<div class="disk-bar"><div class="disk-fill" id="disk-fill" style="width: 0%"></div></div>
|
||||
<div id="disk-text">0%</div>
|
||||
</div>
|
||||
<hr style="border-color:#5a5e6a;margin:15px 10px">
|
||||
<div class="menu-item" onclick="logout()">退出登录</div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<img id="video" src="/video_feed" />
|
||||
<video id="video-player" class="video-js vjs-default-skin" controls preload="auto" data-setup="{}">
|
||||
<p class="vjs-no-js">
|
||||
要查看此视频,请启用JavaScript,并考虑升级到支持HTML5视频的浏览器
|
||||
<a href="https://videojs.com.cn/html5-video-support/" target="_blank">支持HTML5视频</a>
|
||||
</p>
|
||||
</video>
|
||||
<div class="control-panel">
|
||||
<div>当前模式:<span id="mode-text">实时监控</span></div>
|
||||
</div>
|
||||
<div class="playback-panel" id="playback-panel">
|
||||
<div style="margin-bottom: 10px">
|
||||
<label>选择日期:</label>
|
||||
<input type="date" id="date-picker" style="padding: 5px; border-radius: 3px; border: 1px solid #ccc;">
|
||||
<button onclick="loadVideos()" style="padding: 5px 10px; background: #009688; color: white; border: none; border-radius: 3px; cursor: pointer;">加载</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>选择视频:</label>
|
||||
<select id="video-select" style="padding: 5px; border-radius: 3px; border: 1px solid #ccc; width: 200px;">
|
||||
<option value="">请选择视频</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://vjs.zencdn.net/8.23.3/video.min.js"></script>
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script>
|
||||
// 切换视图(可选:后续通过 WebSocket 或 REST API 控制后端 current_view)
|
||||
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
document.querySelector('.active').classList.remove('active');
|
||||
this.classList.add('active');
|
||||
const view = this.getAttribute('data-view');
|
||||
// TODO: 发送 AJAX 请求通知后端切换 current_view
|
||||
// fetch('/api/set_view?view=' + view);
|
||||
});
|
||||
// 初始化日期选择器为当前日期
|
||||
document.getElementById('date-picker').value = new Date().toISOString().split('T')[0];
|
||||
|
||||
// 实时监控和回放模式切换
|
||||
document.getElementById('live-view').addEventListener('click', function() {
|
||||
setMode('live');
|
||||
});
|
||||
|
||||
document.getElementById('playback-view').addEventListener('click', function() {
|
||||
setMode('playback');
|
||||
});
|
||||
|
||||
function setMode(mode) {
|
||||
const liveViewBtn = document.getElementById('live-view');
|
||||
const playbackViewBtn = document.getElementById('playback-view');
|
||||
const videoImg = document.getElementById('video');
|
||||
const videoPlayer = document.getElementById('video-player');
|
||||
const playbackPanel = document.getElementById('playback-panel');
|
||||
const modeText = document.getElementById('mode-text');
|
||||
|
||||
if (mode === 'live') {
|
||||
liveViewBtn.classList.add('active');
|
||||
playbackViewBtn.classList.remove('active');
|
||||
videoImg.style.display = 'block';
|
||||
videoPlayer.style.display = 'none';
|
||||
playbackPanel.style.display = 'none';
|
||||
modeText.textContent = '实时监控';
|
||||
} else if (mode === 'playback') {
|
||||
liveViewBtn.classList.remove('active');
|
||||
playbackViewBtn.classList.add('active');
|
||||
videoImg.style.display = 'none';
|
||||
videoPlayer.style.display = 'block';
|
||||
playbackPanel.style.display = 'block';
|
||||
modeText.textContent = '视频回放';
|
||||
loadVideos(); // 加载当天视频
|
||||
}
|
||||
}
|
||||
|
||||
// 加载指定日期的视频列表
|
||||
function loadVideos() {
|
||||
const datePicker = document.getElementById('date-picker');
|
||||
const videoSelect = document.getElementById('video-select');
|
||||
const selectedDate = datePicker.value;
|
||||
|
||||
if (!selectedDate) {
|
||||
alert('请选择日期');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空视频选择器
|
||||
videoSelect.innerHTML = '<option value="">请选择视频</option>';
|
||||
|
||||
// 请求视频列表
|
||||
fetch('/api/videos')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
alert('获取视频列表失败:' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 筛选指定日期的视频
|
||||
const filteredVideos = data.videos.filter(video => video.date === selectedDate);
|
||||
|
||||
if (filteredVideos.length === 0) {
|
||||
videoSelect.innerHTML += '<option value="">当天没有视频</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加视频选项
|
||||
filteredVideos.forEach(video => {
|
||||
const option = document.createElement('option');
|
||||
option.value = video.path;
|
||||
option.textContent = video.time + ' (' + video.size + 'MB)';
|
||||
videoSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 监听选择变化,播放视频
|
||||
videoSelect.addEventListener('change', function() {
|
||||
const selectedPath = this.value;
|
||||
if (selectedPath) {
|
||||
playVideo(selectedPath);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取视频列表出错:', error);
|
||||
alert('获取视频列表失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 播放选定的视频
|
||||
function playVideo(videoPath) {
|
||||
const videoElement = document.getElementById('video-player');
|
||||
const player = videojs(videoElement);
|
||||
|
||||
// 根据文件扩展名设置视频类型
|
||||
const videoType = videoPath.endsWith('.avi') ? 'video/x-msvideo' : 'video/mp4';
|
||||
|
||||
// 设置视频源
|
||||
player.src({
|
||||
src: '/api/video/' + videoPath,
|
||||
type: videoType
|
||||
});
|
||||
|
||||
// 播放视频
|
||||
player.play();
|
||||
}
|
||||
|
||||
// 更新磁盘使用率信息
|
||||
function updateDiskUsage() {
|
||||
fetch('/api/disk_usage')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('获取磁盘使用率失败:', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const diskFill = document.getElementById('disk-fill');
|
||||
const diskText = document.getElementById('disk-text');
|
||||
const usagePercent = data.usage_percent;
|
||||
|
||||
diskFill.style.width = usagePercent + '%';
|
||||
diskText.textContent = usagePercent + '% (' + data.used + 'GB / ' + data.total + 'GB)';
|
||||
|
||||
// 根据使用率设置不同颜色
|
||||
if (usagePercent > 80) {
|
||||
diskFill.style.background = '#FF5722'; // 红色
|
||||
} else if (usagePercent > 60) {
|
||||
diskFill.style.background = '#FFC107'; // 黄色
|
||||
} else {
|
||||
diskFill.style.background = '#009688'; // 绿色
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取磁盘使用率出错:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化并定期更新磁盘使用率
|
||||
updateDiskUsage();
|
||||
setInterval(updateDiskUsage, 60000); // 每分钟更新一次
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
if (confirm('确定退出?')) {
|
||||
window.location.href = '/logout';
|
||||
|
||||
Reference in New Issue
Block a user