You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RTSPtoWeb/web/static/js/camera-manager.js

318 lines
11 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 摄像头管理器 - 优先通过API获取摄像头列表
* 支持降级到流列表显示
*/
class CameraManager {
constructor() {
this.cameras = [];
this.streams = [];
this.loadingIndicator = document.getElementById('loading-indicator');
this.cameraContainer = document.getElementById('camera-list-container');
this.streamContainer = document.getElementById('stream-list-container');
this.init();
}
async init() {
try {
// 优先尝试获取摄像头列表
await this.loadCameras();
} catch (error) {
console.warn('摄像头API不可用降级到流列表:', error);
// 降级到流列表
await this.loadStreams();
}
}
async loadCameras() {
try {
const response = await fetch('/cameras');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.cameras && data.cameras.length > 0) {
this.cameras = data.cameras;
this.renderCameras();
this.updateTitle(`摄像头 (${this.cameras.length})`);
} else {
throw new Error('摄像头列表为空');
}
} catch (error) {
console.error('加载摄像头失败:', error);
throw error;
}
}
async loadStreams() {
try {
const response = await fetch('/streams');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.status === 1 && data.payload) {
this.streams = Object.entries(data.payload).map(([key, value]) => ({
id: key,
...value
}));
this.showStreamFallback();
this.updateTitle(`视频流 (${this.streams.length})`);
} else {
this.showEmptyState();
}
} catch (error) {
console.error('加载流列表失败:', error);
this.showEmptyState();
}
}
renderCameras() {
this.hideLoading();
this.cameraContainer.style.display = 'flex';
this.cameraContainer.innerHTML = '';
this.cameras.forEach(camera => {
const cameraCard = this.createCameraCard(camera);
this.cameraContainer.appendChild(cameraCard);
});
}
createCameraCard(camera) {
const col = document.createElement('div');
col.className = 'col-12 col-sm-6 col-md-3';
col.id = `camera-${camera.id || camera.camera_id}`;
const statusClass = camera.status === 'online' ? 'badge-success' : 'badge-secondary';
const statusText = camera.status === 'online' ? '在线' : '离线';
const enabledBadge = camera.enabled ?
'<span class="badge badge-success ml-1">已启用</span>' :
'<span class="badge badge-warning ml-1">已禁用</span>';
// 构建播放按钮
const playButtons = this.createPlayButtons(camera);
col.innerHTML = `
<div class="card card-outline ${camera.status === 'online' ? 'card-success' : 'card-secondary'}">
<div class="card-header">
<h3 class="card-title one-line-header">${camera.name || camera.camera_name || '未命名摄像头'}</h3>
<div class="card-tools">
<span class="badge ${statusClass}">${statusText}</span>
${enabledBadge}
</div>
</div>
<div class="card-body p-0">
<div class="camera-preview">
<img class="d-block w-100 stream-img fix-height"
src="/../static/img/noimage.svg"
alt="${camera.name || camera.camera_name}">
<div class="camera-info p-2">
<small class="text-muted">
<strong>IP:</strong> ${camera.ip}<br>
<strong>类型:</strong> ${camera.device_type || '网络摄像头'}<br>
${camera.unit_code ? `<strong>单位:</strong> ${camera.unit_code}<br>` : ''}
</small>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="btn-group stream">
${playButtons}
<a class="btn btn-secondary btn-flat btn-xs" href="/pages/cameras" title="管理摄像头">
<i class="fas fa-cog"></i> 管理
</a>
${camera.enabled ?
`<a class="btn btn-info btn-flat btn-xs" href="/pages/player/all/${camera.id || camera.camera_id}/0" title="预览">
<i class="fas fa-eye"></i> 预览
</a>` :
'<span class="btn btn-secondary btn-flat btn-xs disabled">已禁用</span>'
}
</div>
</div>
</div>
</div>
</div>
`;
return col;
}
createPlayButtons(camera) {
if (!camera.enabled || camera.status !== 'online') {
return '<span class="btn btn-secondary btn-flat btn-xs disabled">离线</span>';
}
const cameraId = camera.id || camera.camera_id;
return `
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/mse/${cameraId}/0">
<i class="fas fa-play"></i> MSE
</a>
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/hls/${cameraId}/0">
<i class="fas fa-play"></i> HLS
</a>
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/webrtc/${cameraId}/0">
<i class="fas fa-play"></i> WebRTC
</a>
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/all/${cameraId}/0">
<i class="fas fa-play"></i> ALL
</a>
`;
}
showStreamFallback() {
this.hideLoading();
this.streamContainer.style.display = 'flex';
console.info('使用流列表降级方案');
}
showEmptyState() {
this.hideLoading();
this.cameraContainer.style.display = 'flex';
this.cameraContainer.innerHTML = `
<div class="col-12">
<div class="alert alert-info text-center">
<i class="fas fa-info-circle"></i>
暂无可用的摄像头或视频流
<br><br>
<a href="/pages/cameras" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> 添加摄像头
</a>
<a href="/pages/stream/add" class="btn btn-success btn-sm ml-2">
<i class="fas fa-plus"></i> 添加流
</a>
</div>
</div>
`;
}
hideLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = 'none';
}
}
updateTitle(title) {
const titleElement = document.querySelector('h5.mt-4.mb-2');
if (titleElement) {
titleElement.textContent = title;
}
}
// 刷新摄像头列表
async refresh() {
this.showLoading();
await this.init();
}
showLoading() {
this.cameraContainer.style.display = 'none';
this.streamContainer.style.display = 'none';
this.loadingIndicator.style.display = 'flex';
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 确保在其他脚本加载后再初始化
setTimeout(() => {
window.cameraManager = new CameraManager();
}, 100);
});
// 为Java项目集成提供的全局API
window.RTSPtoWebAPI = {
// 获取摄像头列表
async getCameras() {
const response = await fetch('/cameras');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 获取流列表
async getStreams() {
const response = await fetch('/streams');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 获取特定摄像头信息
async getCamera(cameraId) {
const response = await fetch(`/camera/${cameraId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 添加摄像头
async addCamera(cameraData) {
const response = await fetch('/camera/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cameraData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 更新摄像头
async updateCamera(cameraId, cameraData) {
const response = await fetch(`/camera/${cameraId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cameraData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 删除摄像头
async deleteCamera(cameraId) {
const response = await fetch(`/camera/${cameraId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 刷新摄像头状态
async refreshCameras() {
const response = await fetch('/cameras/refresh', {
method: 'POST'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
},
// 获取播放URL
getPlayUrl(cameraId, channel = 0, type = 'hls') {
const baseUrl = window.location.origin;
switch (type.toLowerCase()) {
case 'hls':
return `${baseUrl}/stream/${cameraId}/channel/${channel}/hls/live/index.m3u8`;
case 'webrtc':
return `${baseUrl}/pages/player/webrtc/${cameraId}/${channel}`;
case 'mse':
return `${baseUrl}/pages/player/mse/${cameraId}/${channel}`;
default:
return `${baseUrl}/pages/player/all/${cameraId}/${channel}`;
}
}
};