|
|
/**
|
|
|
* 摄像头管理器 - 优先通过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}`;
|
|
|
}
|
|
|
}
|
|
|
}; |