完成视频浏览

This commit is contained in:
zqc
2026-04-02 13:17:30 +08:00
parent 68d6849120
commit aa4f167840
2 changed files with 139 additions and 17 deletions

View File

@@ -150,6 +150,63 @@
overflow: hidden; 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 { #video-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -196,9 +253,28 @@
</div> </div>
</div> </div>
<div class="video-area"> <div class="video-area">
<!-- 视频控制栏 -->
<div class="video-controls" id="video-controls" style="display: none;">
<label>码流类型:</label>
<select id="stream-type">
<option value="0">主码流(高清)</option>
<option value="1">子码流(流畅)</option>
</select>
</div>
<div id="video-container"> <div id="video-container">
<div class="placeholder">👈 从左侧选择一个直播源</div> <div class="placeholder">👈 从左侧选择一个直播源</div>
</div> </div>
<!-- 视频信息栏 -->
<div class="video-info" id="video-info" style="display: none;">
<div class="info-row">
<span class="info-label">摄像头ID</span>
<span class="info-value" id="info-camera-id">-</span>
</div>
<div class="info-row">
<span class="info-label">播放地址:</span>
<span class="info-value" id="info-url">-</span>
</div>
</div>
</div> </div>
</div> </div>
@@ -209,6 +285,7 @@
// 全局状态 // 全局状态
let currentVideoNode = null; // 当前播放的节点对象 let currentVideoNode = null; // 当前播放的节点对象
let hls = null; // HLS 实例 let hls = null; // HLS 实例
let currentStreamType = 0; // 当前码流类型
// 缓存已加载的子节点数据: { parentId: [childrenNodes] } // 缓存已加载的子节点数据: { parentId: [childrenNodes] }
const childrenCache = new Map(); const childrenCache = new Map();
@@ -243,13 +320,12 @@
} }
} }
// 获取节点详情(这里主要用来获取视频流地址,但也可以直接用子节点数据,不过为符合 API 设计,单独调用 stream 接口) // 获取视频流地址
async function fetchStreamUrl(nodeId) { async function fetchStreamUrl(nodeId, streamType = 0) {
try { 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}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); return await res.json();
return data.url;
} catch (err) { } catch (err) {
console.error(`获取节点 ${nodeId} 的视频地址失败:`, err); console.error(`获取节点 ${nodeId} 的视频地址失败:`, err);
return null; return null;
@@ -257,20 +333,26 @@
} }
// 播放视频 // 播放视频
async function playVideo(node) { async function playVideo(node, streamType = 0) {
if (!node || !node.is_leaf) return; if (!node || !node.is_leaf) return;
// 显示加载占位 // 显示加载占位
const container = document.getElementById('video-container'); const container = document.getElementById('video-container');
container.innerHTML = '<div class="placeholder">📡 正在加载直播流...</div>'; container.innerHTML = '<div class="placeholder">📡 正在加载直播流...</div>';
// 隐藏信息和控制栏
document.getElementById('video-controls').style.display = 'none';
document.getElementById('video-info').style.display = 'none';
// 获取流地址 // 获取流地址
const streamUrl = await fetchStreamUrl(node.id); const streamData = await fetchStreamUrl(node.id, streamType);
if (!streamUrl) { if (!streamData || !streamData.url) {
container.innerHTML = '<div class="placeholder">❌ 无法获取视频地址,请稍后重试</div>'; container.innerHTML = '<div class="placeholder">❌ 无法获取视频地址,请稍后重试</div>';
return; return;
} }
const streamUrl = streamData.url;
// 清理旧播放器 // 清理旧播放器
if (hls) { if (hls) {
hls.destroy(); 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; currentVideoNode = node;
} }
@@ -388,7 +478,7 @@
// 高亮当前选中的节点 // 高亮当前选中的节点
clearActiveHighlight(); clearActiveHighlight();
contentDiv.classList.add('active'); contentDiv.classList.add('active');
await playVideo(node); await playVideo(node, currentStreamType);
return; return;
} }
@@ -442,7 +532,14 @@
// 初始化视频区域(可选:尝试自动播放需用户交互) // 初始化视频区域(可选:尝试自动播放需用户交互)
function initVideoArea() { function initVideoArea() {
// 预留 // 监听码流类型切换
document.getElementById('stream-type').addEventListener('change', async (e) => {
currentStreamType = parseInt(e.target.value);
// 如果当前有播放的视频,重新加载
if (currentVideoNode) {
await playVideo(currentVideoNode, currentStreamType);
}
});
} }
// 启动 // 启动

View File

@@ -5,7 +5,7 @@ import json
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
import sys import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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 配置 ========== # ========== 海康威视 API 配置 ==========
ROOT_PARENT_INDEX_CODE = "4fa15af07b6b400f94af1e35d8235c30" ROOT_PARENT_INDEX_CODE = "4fa15af07b6b400f94af1e35d8235c30"
@@ -68,9 +68,24 @@ def get_children(parent_id):
print(f"调用海康威视 API 失败: {e}") print(f"调用海康威视 API 失败: {e}")
return [] return []
def get_stream_url(node_id): def get_stream_url(node_id, stream_type=0):
"""获取叶子节点的视频流地址(暂不实现,后续可扩展)""" """获取摄像头的视频流地址
return None
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 处理器 ========== # ========== HTTP 处理器 ==========
class APIHandler(SimpleHTTPRequestHandler): class APIHandler(SimpleHTTPRequestHandler):
@@ -134,16 +149,26 @@ class APIHandler(SimpleHTTPRequestHandler):
return return
elif path.startswith('/api/stream/'): elif path.startswith('/api/stream/'):
# GET /api/stream/21020000 # GET /api/stream/21020000?stream_type=0
node_id = path.split('/')[-1] node_id = path.split('/')[-1]
if not node_id: if not node_id:
self.send_error_json("Invalid node id", 400) self.send_error_json("Invalid node id", 400)
return 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: if url is None:
self.send_error_json("Stream not found or node is not a leaf", 404) self.send_error_json("Stream not found or node is not a leaf", 404)
return return
self.send_json_response({"url": url}) # 返回完整信息
self.send_json_response({
"cameraIndexCode": node_id,
"url": url,
"stream_type": stream_type
})
return return
# 静态文件服务(与原逻辑一致) # 静态文件服务(与原逻辑一致)