From aa4f167840700b3623fe0043d6ac688fcd8374a1 Mon Sep 17 00:00:00 2001
From: zqc <835569504@qq.com>
Date: Thu, 2 Apr 2026 13:17:30 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=A7=86=E9=A2=91=E6=B5=8F?=
=?UTF-8?q?=E8=A7=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
live_catalog/index.html | 117 ++++++++++++++++++++++++---
live_catalog/live_catalog_backend.py | 39 +++++++--
2 files changed, 139 insertions(+), 17 deletions(-)
diff --git a/live_catalog/index.html b/live_catalog/index.html
index d7c2b53..64d06ed 100644
--- a/live_catalog/index.html
+++ b/live_catalog/index.html
@@ -150,6 +150,63 @@
overflow: hidden;
}
+ /* 视频控制栏 */
+ .video-controls {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ padding: 12px 16px;
+ background: linear-gradient(to bottom, rgba(0,0,0,0.7), transparent);
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ z-index: 10;
+ }
+
+ .video-controls label {
+ color: #fff;
+ font-size: 13px;
+ }
+
+ .video-controls select {
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px solid #444;
+ background: #333;
+ color: #fff;
+ font-size: 13px;
+ cursor: pointer;
+ }
+
+ /* 视频信息栏 */
+ .video-info {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 12px 16px;
+ background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
+ color: #ccc;
+ font-size: 12px;
+ z-index: 10;
+ font-family: monospace;
+ word-break: break-all;
+ }
+
+ .video-info .info-row {
+ margin: 4px 0;
+ }
+
+ .video-info .info-label {
+ color: #888;
+ margin-right: 8px;
+ }
+
+ .video-info .info-value {
+ color: #0f0;
+ }
+
#video-container {
width: 100%;
height: 100%;
@@ -196,9 +253,28 @@
+
+
+
+
+
+
+
+
+ 摄像头ID:
+ -
+
+
+ 播放地址:
+ -
+
+
@@ -209,6 +285,7 @@
// 全局状态
let currentVideoNode = null; // 当前播放的节点对象
let hls = null; // HLS 实例
+ let currentStreamType = 0; // 当前码流类型
// 缓存已加载的子节点数据: { parentId: [childrenNodes] }
const childrenCache = new Map();
@@ -243,13 +320,12 @@
}
}
- // 获取节点详情(这里主要用来获取视频流地址,但也可以直接用子节点数据,不过为符合 API 设计,单独调用 stream 接口)
- async function fetchStreamUrl(nodeId) {
+ // 获取视频流地址
+ async function fetchStreamUrl(nodeId, streamType = 0) {
try {
- const res = await fetch(`${API_BASE}/stream/${nodeId}`);
+ const res = await fetch(`${API_BASE}/stream/${nodeId}?stream_type=${streamType}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
- const data = await res.json();
- return data.url;
+ return await res.json();
} catch (err) {
console.error(`获取节点 ${nodeId} 的视频地址失败:`, err);
return null;
@@ -257,20 +333,26 @@
}
// 播放视频
- async function playVideo(node) {
+ async function playVideo(node, streamType = 0) {
if (!node || !node.is_leaf) return;
// 显示加载占位
const container = document.getElementById('video-container');
container.innerHTML = '📡 正在加载直播流...
';
+
+ // 隐藏信息和控制栏
+ document.getElementById('video-controls').style.display = 'none';
+ document.getElementById('video-info').style.display = 'none';
// 获取流地址
- const streamUrl = await fetchStreamUrl(node.id);
- if (!streamUrl) {
+ const streamData = await fetchStreamUrl(node.id, streamType);
+ if (!streamData || !streamData.url) {
container.innerHTML = '❌ 无法获取视频地址,请稍后重试
';
return;
}
+ const streamUrl = streamData.url;
+
// 清理旧播放器
if (hls) {
hls.destroy();
@@ -313,6 +395,14 @@
});
}
+ // 更新并显示视频信息
+ document.getElementById('info-camera-id').textContent = streamData.cameraIndexCode;
+ document.getElementById('info-url').textContent = streamUrl;
+ document.getElementById('video-info').style.display = 'block';
+
+ // 显示控制栏
+ document.getElementById('video-controls').style.display = 'flex';
+
currentVideoNode = node;
}
@@ -388,7 +478,7 @@
// 高亮当前选中的节点
clearActiveHighlight();
contentDiv.classList.add('active');
- await playVideo(node);
+ await playVideo(node, currentStreamType);
return;
}
@@ -442,7 +532,14 @@
// 初始化视频区域(可选:尝试自动播放需用户交互)
function initVideoArea() {
- // 预留
+ // 监听码流类型切换
+ document.getElementById('stream-type').addEventListener('change', async (e) => {
+ currentStreamType = parseInt(e.target.value);
+ // 如果当前有播放的视频,重新加载
+ if (currentVideoNode) {
+ await playVideo(currentVideoNode, currentStreamType);
+ }
+ });
}
// 启动
diff --git a/live_catalog/live_catalog_backend.py b/live_catalog/live_catalog_backend.py
index 0399b1e..9d7acf2 100644
--- a/live_catalog/live_catalog_backend.py
+++ b/live_catalog/live_catalog_backend.py
@@ -5,7 +5,7 @@ import json
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from utils.hikvision_cam_utils import get_organization_list, get_final_list
+from utils.hikvision_cam_utils import get_organization_list, get_final_list, get_camera_preview_url
# ========== 海康威视 API 配置 ==========
ROOT_PARENT_INDEX_CODE = "4fa15af07b6b400f94af1e35d8235c30"
@@ -68,9 +68,24 @@ def get_children(parent_id):
print(f"调用海康威视 API 失败: {e}")
return []
-def get_stream_url(node_id):
- """获取叶子节点的视频流地址(暂不实现,后续可扩展)"""
- return None
+def get_stream_url(node_id, stream_type=0):
+ """获取摄像头的视频流地址
+
+ Args:
+ node_id: 摄像头的 cameraIndexCode
+ stream_type: 码流类型,0=主码流,1=子码流
+ """
+ try:
+ result = get_camera_preview_url(node_id, stream_type)
+ if result.get("code") != "0":
+ print(f"获取视频流地址失败: {result.get('msg')}")
+ return None
+
+ url = result.get("data", {}).get("url")
+ return url
+ except Exception as e:
+ print(f"调用 get_camera_preview_url 失败: {e}")
+ return None
# ========== HTTP 处理器 ==========
class APIHandler(SimpleHTTPRequestHandler):
@@ -134,16 +149,26 @@ class APIHandler(SimpleHTTPRequestHandler):
return
elif path.startswith('/api/stream/'):
- # GET /api/stream/21020000
+ # GET /api/stream/21020000?stream_type=0
node_id = path.split('/')[-1]
if not node_id:
self.send_error_json("Invalid node id", 400)
return
- url = get_stream_url(node_id)
+
+ # 解析 stream_type 参数
+ params = urllib.parse.parse_qs(query)
+ stream_type = int(params.get('stream_type', ['0'])[0])
+
+ url = get_stream_url(node_id, stream_type)
if url is None:
self.send_error_json("Stream not found or node is not a leaf", 404)
return
- self.send_json_response({"url": url})
+ # 返回完整信息
+ self.send_json_response({
+ "cameraIndexCode": node_id,
+ "url": url,
+ "stream_type": stream_type
+ })
return
# 静态文件服务(与原逻辑一致)