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 @@
+ +
👈 从左侧选择一个直播源
+ +
@@ -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 # 静态文件服务(与原逻辑一致)