video_manager.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>RTSP视频流管理系统</title>
  7. <!--
  8. 自定义API基础URL(可选):
  9. <script>window.API_BASE_URL = 'http://your-custom-domain:port';</script>
  10. 如果不设置,将自动使用当前页面的域名和端口
  11. -->
  12. <style>
  13. * {
  14. margin: 0;
  15. padding: 0;
  16. box-sizing: border-box;
  17. }
  18. body {
  19. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  20. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  21. min-height: 100vh;
  22. padding: 20px;
  23. }
  24. .container {
  25. max-width: 1400px;
  26. margin: 0 auto;
  27. }
  28. .header {
  29. background: white;
  30. padding: 30px;
  31. border-radius: 10px;
  32. box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  33. margin-bottom: 30px;
  34. }
  35. h1 {
  36. color: #333;
  37. margin-bottom: 10px;
  38. }
  39. .subtitle {
  40. color: #666;
  41. font-size: 14px;
  42. }
  43. .stats {
  44. display: grid;
  45. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  46. gap: 20px;
  47. margin-bottom: 30px;
  48. }
  49. .stat-card {
  50. background: white;
  51. padding: 20px;
  52. border-radius: 10px;
  53. box-shadow: 0 5px 15px rgba(0,0,0,0.1);
  54. }
  55. .stat-card h3 {
  56. color: #666;
  57. font-size: 14px;
  58. margin-bottom: 10px;
  59. }
  60. .stat-card .value {
  61. color: #333;
  62. font-size: 32px;
  63. font-weight: bold;
  64. }
  65. .content {
  66. background: white;
  67. padding: 30px;
  68. border-radius: 10px;
  69. box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  70. }
  71. .tabs {
  72. display: flex;
  73. gap: 10px;
  74. margin-bottom: 20px;
  75. border-bottom: 2px solid #eee;
  76. }
  77. .tab {
  78. padding: 10px 20px;
  79. background: none;
  80. border: none;
  81. cursor: pointer;
  82. font-size: 16px;
  83. color: #666;
  84. transition: all 0.3s;
  85. }
  86. .tab.active {
  87. color: #667eea;
  88. border-bottom: 2px solid #667eea;
  89. margin-bottom: -2px;
  90. }
  91. .tab-content {
  92. display: none;
  93. }
  94. .tab-content.active {
  95. display: block;
  96. }
  97. .timestamp-list {
  98. display: grid;
  99. gap: 15px;
  100. }
  101. .timestamp-item {
  102. background: #f8f9fa;
  103. padding: 20px;
  104. border-radius: 8px;
  105. border-left: 4px solid #667eea;
  106. }
  107. .timestamp-header {
  108. display: flex;
  109. justify-content: space-between;
  110. align-items: center;
  111. margin-bottom: 10px;
  112. }
  113. .timestamp-name {
  114. font-size: 18px;
  115. font-weight: bold;
  116. color: #333;
  117. }
  118. .timestamp-info {
  119. color: #666;
  120. font-size: 14px;
  121. }
  122. .btn {
  123. padding: 8px 16px;
  124. border: none;
  125. border-radius: 5px;
  126. cursor: pointer;
  127. font-size: 14px;
  128. transition: all 0.3s;
  129. }
  130. .btn-primary {
  131. background: #667eea;
  132. color: white;
  133. }
  134. .btn-primary:hover {
  135. background: #5568d3;
  136. }
  137. .btn-danger {
  138. background: #dc3545;
  139. color: white;
  140. }
  141. .btn-danger:hover {
  142. background: #c82333;
  143. }
  144. .btn-success {
  145. background: #28a745;
  146. color: white;
  147. }
  148. .btn-success:hover {
  149. background: #218838;
  150. }
  151. .video-grid {
  152. display: grid;
  153. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  154. gap: 20px;
  155. margin-top: 15px;
  156. }
  157. .video-card {
  158. background: white;
  159. border: 1px solid #ddd;
  160. border-radius: 8px;
  161. padding: 15px;
  162. transition: all 0.3s;
  163. }
  164. .video-card:hover {
  165. box-shadow: 0 5px 15px rgba(0,0,0,0.1);
  166. }
  167. .video-name {
  168. font-weight: bold;
  169. color: #333;
  170. margin-bottom: 8px;
  171. }
  172. .video-info {
  173. font-size: 12px;
  174. color: #666;
  175. margin-bottom: 5px;
  176. }
  177. .loading {
  178. text-align: center;
  179. padding: 40px;
  180. color: #666;
  181. }
  182. .error {
  183. background: #f8d7da;
  184. color: #721c24;
  185. padding: 15px;
  186. border-radius: 5px;
  187. margin-bottom: 20px;
  188. }
  189. .success {
  190. background: #d4edda;
  191. color: #155724;
  192. padding: 15px;
  193. border-radius: 5px;
  194. margin-bottom: 20px;
  195. }
  196. .refresh-btn {
  197. float: right;
  198. margin-bottom: 20px;
  199. }
  200. /* 视频播放器样式 */
  201. .video-modal {
  202. display: none;
  203. position: fixed;
  204. top: 0;
  205. left: 0;
  206. width: 100%;
  207. height: 100%;
  208. background: rgba(0, 0, 0, 0.9);
  209. z-index: 1000;
  210. justify-content: center;
  211. align-items: center;
  212. }
  213. .video-modal.active {
  214. display: flex;
  215. }
  216. .video-modal-content {
  217. background: white;
  218. border-radius: 10px;
  219. padding: 20px;
  220. max-width: 90%;
  221. max-height: 90%;
  222. position: relative;
  223. }
  224. .video-modal-header {
  225. display: flex;
  226. justify-content: space-between;
  227. align-items: center;
  228. margin-bottom: 15px;
  229. }
  230. .video-modal-title {
  231. font-size: 18px;
  232. font-weight: bold;
  233. color: #333;
  234. }
  235. .close-btn {
  236. background: #dc3545;
  237. color: white;
  238. border: none;
  239. border-radius: 50%;
  240. width: 35px;
  241. height: 35px;
  242. cursor: pointer;
  243. font-size: 20px;
  244. line-height: 1;
  245. transition: all 0.3s;
  246. }
  247. .close-btn:hover {
  248. background: #c82333;
  249. transform: rotate(90deg);
  250. }
  251. .video-player {
  252. width: 100%;
  253. max-width: 1200px;
  254. max-height: 70vh;
  255. background: #000;
  256. border-radius: 5px;
  257. }
  258. .video-info-panel {
  259. margin-top: 15px;
  260. padding: 15px;
  261. background: #f8f9fa;
  262. border-radius: 5px;
  263. }
  264. .video-info-item {
  265. display: flex;
  266. justify-content: space-between;
  267. margin-bottom: 8px;
  268. font-size: 14px;
  269. }
  270. .video-info-label {
  271. color: #666;
  272. font-weight: bold;
  273. }
  274. .video-info-value {
  275. color: #333;
  276. }
  277. .btn-play {
  278. background: #28a745;
  279. color: white;
  280. margin-right: 5px;
  281. }
  282. .btn-play:hover {
  283. background: #218838;
  284. }
  285. </style>
  286. </head>
  287. <body>
  288. <div class="container">
  289. <div class="header">
  290. <h1>🎥 RTSP视频流管理系统</h1>
  291. <p class="subtitle">查看和管理您的视频流录制文件</p>
  292. </div>
  293. <div class="stats" id="stats">
  294. <div class="stat-card">
  295. <h3>时间戳目录</h3>
  296. <div class="value" id="timestamp-count">-</div>
  297. </div>
  298. <div class="stat-card">
  299. <h3>视频总数</h3>
  300. <div class="value" id="video-count">-</div>
  301. </div>
  302. <div class="stat-card">
  303. <h3>总大小</h3>
  304. <div class="value" id="total-size">-</div>
  305. </div>
  306. </div>
  307. <div class="content">
  308. <div id="message"></div>
  309. <button class="btn btn-success refresh-btn" onclick="loadData()">🔄 刷新</button>
  310. <div class="tabs">
  311. <button class="tab active" onclick="switchTab('timestamps')">时间戳目录</button>
  312. <button class="tab" onclick="switchTab('videos')">所有视频</button>
  313. <button class="tab" onclick="switchTab('reports')">轮询报告</button>
  314. </div>
  315. <div id="timestamps-tab" class="tab-content active">
  316. <div class="loading">加载中...</div>
  317. </div>
  318. <div id="videos-tab" class="tab-content">
  319. <div class="loading">加载中...</div>
  320. </div>
  321. <div id="reports-tab" class="tab-content">
  322. <div class="loading">加载中...</div>
  323. </div>
  324. </div>
  325. </div>
  326. <!-- 视频播放器模态框 -->
  327. <div id="video-modal" class="video-modal">
  328. <div class="video-modal-content">
  329. <div class="video-modal-header">
  330. <div class="video-modal-title" id="video-title">视频播放</div>
  331. <button class="close-btn" onclick="closeVideoPlayer()">×</button>
  332. </div>
  333. <video id="video-player" class="video-player" controls>
  334. 您的浏览器不支持视频播放。
  335. </video>
  336. <div class="video-info-panel" id="video-details">
  337. <!-- 视频详细信息将在这里显示 -->
  338. </div>
  339. </div>
  340. </div>
  341. <script>
  342. // 动态获取API基础URL,支持不同环境部署
  343. function getApiBase() {
  344. // 优先使用环境变量或配置
  345. if (window.API_BASE_URL) {
  346. return window.API_BASE_URL;
  347. }
  348. // 默认使用当前页面的主机和端口
  349. return `${window.location.protocol}//${window.location.host}`;
  350. }
  351. const API_BASE = getApiBase();
  352. console.log('当前 API 基础URL:', API_BASE);
  353. function switchTab(tab) {
  354. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  355. document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
  356. event.target.classList.add('active');
  357. document.getElementById(tab + '-tab').classList.add('active');
  358. }
  359. function formatBytes(bytes) {
  360. if (bytes === 0) return '0 B';
  361. const k = 1024;
  362. const sizes = ['B', 'KB', 'MB', 'GB'];
  363. const i = Math.floor(Math.log(bytes) / Math.log(k));
  364. return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
  365. }
  366. function showMessage(message, type = 'success') {
  367. const messageDiv = document.getElementById('message');
  368. messageDiv.innerHTML = `<div class="${type}">${message}</div>`;
  369. setTimeout(() => {
  370. messageDiv.innerHTML = '';
  371. }, 3000);
  372. }
  373. function playVideo(videoPath, filename, fileSize, createdTime, timestampDir, streamIndex) {
  374. const modal = document.getElementById('video-modal');
  375. const player = document.getElementById('video-player');
  376. const title = document.getElementById('video-title');
  377. const details = document.getElementById('video-details');
  378. // 设置视频标题
  379. title.textContent = `▶ ${filename}`;
  380. // 设置视频源 - 使用相对路径
  381. const relativePath = `${timestampDir}/${filename}`;
  382. const videoUrl = `${API_BASE}/videos/${encodeURIComponent(relativePath)}`;
  383. console.log('设置视频源:', videoUrl);
  384. player.src = videoUrl;
  385. player.load();
  386. // 显示视频详细信息
  387. details.innerHTML = `
  388. <div class="video-info-item">
  389. <span class="video-info-label">📁 文件名:</span>
  390. <span class="video-info-value">${filename}</span>
  391. </div>
  392. <div class="video-info-item">
  393. <span class="video-info-label">📊 文件大小:</span>
  394. <span class="video-info-value">${formatBytes(fileSize)}</span>
  395. </div>
  396. <div class="video-info-item">
  397. <span class="video-info-label">🕒 创建时间:</span>
  398. <span class="video-info-value">${createdTime}</span>
  399. </div>
  400. <div class="video-info-item">
  401. <span class="video-info-label">📂 时间戳目录:</span>
  402. <span class="video-info-value">${timestampDir}</span>
  403. </div>
  404. <div class="video-info-item">
  405. <span class="video-info-label">📍 流索引:</span>
  406. <span class="video-info-value">${streamIndex}</span>
  407. </div>
  408. <div class="video-info-item">
  409. <span class="video-info-label">📍 完整路径:</span>
  410. <span class="video-info-value">${videoPath}</span>
  411. </div>
  412. `;
  413. // 显示模态框
  414. modal.classList.add('active');
  415. // 尝试自动播放
  416. player.play().catch(err => {
  417. console.log('自动播放失败,需要用户交互:', err);
  418. });
  419. }
  420. function closeVideoPlayer() {
  421. const modal = document.getElementById('video-modal');
  422. const player = document.getElementById('video-player');
  423. // 暂停播放
  424. player.pause();
  425. player.src = '';
  426. // 隐藏模态框
  427. modal.classList.remove('active');
  428. }
  429. // 点击模态框背景关闭
  430. document.getElementById('video-modal').addEventListener('click', function(e) {
  431. if (e.target === this) {
  432. closeVideoPlayer();
  433. }
  434. });
  435. // ESC键关闭
  436. document.addEventListener('keydown', function(e) {
  437. if (e.key === 'Escape') {
  438. closeVideoPlayer();
  439. }
  440. });
  441. async function loadStats() {
  442. try {
  443. const [timestampsRes, videosRes] = await Promise.all([
  444. fetch(`${API_BASE}/api/timestamps`),
  445. fetch(`${API_BASE}/api/videos`)
  446. ]);
  447. const timestamps = await timestampsRes.json();
  448. const videos = await videosRes.json();
  449. document.getElementById('timestamp-count').textContent = timestamps.count;
  450. document.getElementById('video-count').textContent = videos.count;
  451. const totalSize = videos.videos.reduce((sum, video) => sum + video.file_size, 0);
  452. document.getElementById('total-size').textContent = formatBytes(totalSize);
  453. } catch (error) {
  454. console.error('加载统计数据失败:', error);
  455. }
  456. }
  457. async function loadTimestamps() {
  458. const container = document.getElementById('timestamps-tab');
  459. container.innerHTML = '<div class="loading">加载中...</div>';
  460. try {
  461. const response = await fetch(`${API_BASE}/api/timestamps`);
  462. const data = await response.json();
  463. if (data.count === 0) {
  464. container.innerHTML = '<div class="loading">暂无时间戳目录</div>';
  465. return;
  466. }
  467. let html = '<div class="timestamp-list">';
  468. for (const ts of data.timestamps) {
  469. html += `
  470. <div class="timestamp-item">
  471. <div class="timestamp-header">
  472. <div>
  473. <div class="timestamp-name">📁 ${ts.timestamp}</div>
  474. <div class="timestamp-info">
  475. ${ts.video_count} 个视频 | ${formatBytes(ts.total_size)}
  476. </div>
  477. </div>
  478. <div>
  479. <button class="btn btn-primary" onclick="viewTimestamp('${ts.timestamp}')">查看</button>
  480. <button class="btn btn-danger" onclick="deleteTimestamp('${ts.timestamp}')">删除</button>
  481. </div>
  482. </div>
  483. <div id="videos-${ts.timestamp}"></div>
  484. </div>
  485. `;
  486. }
  487. html += '</div>';
  488. container.innerHTML = html;
  489. } catch (error) {
  490. container.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
  491. }
  492. }
  493. async function viewTimestamp(timestamp) {
  494. const container = document.getElementById(`videos-${timestamp}`);
  495. if (container.innerHTML) {
  496. container.innerHTML = '';
  497. return;
  498. }
  499. container.innerHTML = '<div class="loading">加载中...</div>';
  500. try {
  501. const response = await fetch(`${API_BASE}/api/videos/${timestamp}`);
  502. const data = await response.json();
  503. let html = '<div class="video-grid">';
  504. for (const video of data.videos) {
  505. html += `
  506. <div class="video-card">
  507. <div class="video-name">🎬 ${video.filename}</div>
  508. <div class="video-info">📊 大小: ${formatBytes(video.file_size)}</div>
  509. <div class="video-info">🕒 创建时间: ${video.created_time}</div>
  510. <div class="video-info">📍 流索引: ${video.stream_index}</div>
  511. <div style="margin-top: 10px; display: flex; gap: 5px;">
  512. <button class="btn btn-play" style="flex: 1;"
  513. onclick="playVideo('${video.full_path}', '${video.filename}', ${video.file_size}, '${video.created_time}', '${video.timestamp_dir}', '${video.stream_index}')">▶ 播放</button>
  514. <button class="btn btn-danger" style="flex: 1;"
  515. onclick="deleteVideo('${video.full_path}')">删除</button>
  516. </div>
  517. </div>
  518. `;
  519. }
  520. html += '</div>';
  521. container.innerHTML = html;
  522. } catch (error) {
  523. container.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
  524. }
  525. }
  526. async function loadAllVideos() {
  527. const container = document.getElementById('videos-tab');
  528. container.innerHTML = '<div class="loading">加载中...</div>';
  529. try {
  530. const response = await fetch(`${API_BASE}/api/videos`);
  531. const data = await response.json();
  532. if (data.count === 0) {
  533. container.innerHTML = '<div class="loading">暂无视频文件</div>';
  534. return;
  535. }
  536. let html = '<div class="video-grid">';
  537. for (const video of data.videos) {
  538. html += `
  539. <div class="video-card">
  540. <div class="video-name">🎬 ${video.filename}</div>
  541. <div class="video-info">📁 目录: ${video.timestamp_dir}</div>
  542. <div class="video-info">📊 大小: ${formatBytes(video.file_size)}</div>
  543. <div class="video-info">🕒 创建时间: ${video.created_time}</div>
  544. <div class="video-info">📍 流索引: ${video.stream_index}</div>
  545. <div style="margin-top: 10px; display: flex; gap: 5px;">
  546. <button class="btn btn-play" style="flex: 1;"
  547. onclick="playVideo('${video.full_path}', '${video.filename}', ${video.file_size}, '${video.created_time}', '${video.timestamp_dir}', '${video.stream_index}')">▶ 播放</button>
  548. <button class="btn btn-danger" style="flex: 1;"
  549. onclick="deleteVideo('${video.full_path}')">删除</button>
  550. </div>
  551. </div>
  552. `;
  553. }
  554. html += '</div>';
  555. container.innerHTML = html;
  556. } catch (error) {
  557. container.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
  558. }
  559. }
  560. async function deleteVideo(path) {
  561. if (!confirm(`确定要删除视频文件吗?\n${path}`)) {
  562. return;
  563. }
  564. try {
  565. const response = await fetch(`${API_BASE}/api/video?path=${encodeURIComponent(path)}`, {
  566. method: 'DELETE'
  567. });
  568. const data = await response.json();
  569. if (data.success) {
  570. showMessage('视频删除成功', 'success');
  571. loadData();
  572. } else {
  573. showMessage('视频删除失败: ' + data.message, 'error');
  574. }
  575. } catch (error) {
  576. showMessage('删除失败: ' + error.message, 'error');
  577. }
  578. }
  579. async function deleteTimestamp(timestamp) {
  580. if (!confirm(`确定要删除整个时间戳目录吗?\n${timestamp}\n\n这将删除该目录下的所有文件!`)) {
  581. return;
  582. }
  583. try {
  584. const response = await fetch(`${API_BASE}/api/timestamp/${timestamp}`, {
  585. method: 'DELETE'
  586. });
  587. const data = await response.json();
  588. if (data.success) {
  589. showMessage('目录删除成功', 'success');
  590. loadData();
  591. } else {
  592. showMessage('目录删除失败: ' + data.message, 'error');
  593. }
  594. } catch (error) {
  595. showMessage('删除失败: ' + error.message, 'error');
  596. }
  597. }
  598. async function loadReports() {
  599. const container = document.getElementById('reports-tab');
  600. container.innerHTML = '<div class="loading">加载中...</div>';
  601. try {
  602. const response = await fetch(`${API_BASE}/api/reports`);
  603. const data = await response.json();
  604. if (data.count === 0) {
  605. container.innerHTML = '<div class="loading">暂无轮询报告</div>';
  606. return;
  607. }
  608. let html = '<div class="timestamp-list">';
  609. for (const report of data.reports) {
  610. const summary = report.summary || {};
  611. const successRate = (summary.success_rate * 100 || 0).toFixed(1);
  612. html += `
  613. <div class="timestamp-item">
  614. <div class="timestamp-header">
  615. <div>
  616. <div class="timestamp-name">📊 ${report.timestamp}</div>
  617. <div class="timestamp-info">
  618. 生成时间: ${report.generated_at || 'N/A'}
  619. </div>
  620. <div class="timestamp-info">
  621. 成功率: ${successRate}% |
  622. 成功: ${summary.successful_streams || 0} |
  623. 失败: ${summary.failed_streams || 0} |
  624. 总帧数: ${summary.total_frames || 0}
  625. </div>
  626. </div>
  627. <div>
  628. <button class="btn btn-primary" onclick="viewReport('${report.timestamp}')"查看详情</button>
  629. </div>
  630. </div>
  631. <div id="report-${report.timestamp}"></div>
  632. </div>
  633. `;
  634. }
  635. html += '</div>';
  636. container.innerHTML = html;
  637. } catch (error) {
  638. container.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
  639. }
  640. }
  641. async function viewReport(timestamp) {
  642. const container = document.getElementById(`report-${timestamp}`);
  643. if (container.innerHTML) {
  644. container.innerHTML = '';
  645. return;
  646. }
  647. container.innerHTML = '<div class="loading">加载中...</div>';
  648. try {
  649. const response = await fetch(`${API_BASE}/api/report/${timestamp}`);
  650. const report = await response.json();
  651. let html = '<div style="margin-top: 15px; padding: 15px; background: white; border-radius: 8px;">';
  652. // 显示流详细信息
  653. if (report.streams && report.streams.length > 0) {
  654. html += '<h3 style="margin-bottom: 15px;">流详细信息</h3>';
  655. html += '<div class="video-grid">';
  656. for (const stream of report.streams) {
  657. const statusEmoji = stream.status === '完成' ? '✅' : '❌';
  658. const statusClass = stream.status === '完成' ? 'success' : 'error';
  659. html += `
  660. <div class="video-card">
  661. <div class="video-name">${statusEmoji} 流 ${stream.stream_index}</div>
  662. <div class="video-info">📹 RTSP: ${stream.rtsp_url}</div>
  663. <div class="video-info">🎬 状态: <span style="color: ${stream.status === '完成' ? 'green' : 'red'}; font-weight: bold;">${stream.status}</span></div>
  664. <div class="video-info">💾 输出: ${stream.output_file || 'N/A'}</div>
  665. <div class="video-info">⏱️ 开始: ${stream.start_time || 'N/A'}</div>
  666. <div class="video-info">⏹️ 结束: ${stream.end_time || 'N/A'}</div>
  667. <div class="video-info">🕒 持续: ${stream.actual_duration_seconds || 0}秒 / ${stream.duration_seconds || 0}秒</div>
  668. <div class="video-info">🎬 帧数: ${stream.frames_received || 0}</div>
  669. <div class="video-info">📊 大小: ${formatBytes(stream.bytes_received || 0)}</div>
  670. ${stream.error_message ? `<div class="video-info" style="color: red;">❌ 错误: ${stream.error_message}</div>` : ''}
  671. </div>
  672. `;
  673. }
  674. html += '</div>';
  675. } else {
  676. html += '<p>暂无流信息</p>';
  677. }
  678. html += '</div>';
  679. container.innerHTML = html;
  680. } catch (error) {
  681. container.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
  682. }
  683. }
  684. function loadData() {
  685. loadStats();
  686. loadTimestamps();
  687. loadAllVideos();
  688. loadReports();
  689. }
  690. // 页面加载时初始化
  691. window.onload = loadData;
  692. </script>
  693. </body>
  694. </html>