Files
web-Iot/src/components/devices/DroneVideoPlayer.tsx
2026-05-25 14:39:05 +08:00

258 lines
7.7 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { Button, Select, Space, Typography } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import DroneLivePlayer from '../VolcRtcPlayer.jsx';
import { changeCameragateway, changeCameraPlane } from '../../api/device.ts';
import utils from '../../lib/utils';
const { Text } = Typography;
interface DroneVideoPlayerProps {
device: any;
height?: number | string;
showSourceSelect?: boolean;
defaultSource?: 'station' | 'drone';
videoSource?: 'station' | 'drone';
onSourceChange?: (source: 'station' | 'drone') => void;
}
const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
device,
height = 160,
showSourceSelect = true,
defaultSource = 'drone',
videoSource: propsVideoSource,
onSourceChange
}) => {
const [internalVideoSource, setInternalVideoSource] = useState<'station' | 'drone'>(defaultSource);
const videoSource = propsVideoSource !== undefined ? propsVideoSource : internalVideoSource;
const setVideoSource = (source: 'station' | 'drone') => {
if (onSourceChange) onSourceChange(source);
else setInternalVideoSource(source);
};
const [selectedCamera, setSelectedCamera] = useState<'indoor' | 'outdoor'>('indoor');
const [selectedLens, setSelectedLens] = useState<'wide' | 'zoom' | 'ir'>('wide');
// 当前选中的摄像头 index
const [selectedDroneCameraIdx, setSelectedDroneCameraIdx] = useState<number>(0);
const [streamData, setStreamData] = useState<any>(null);
const [loading, setLoading] = useState(false);
const switchingLock = useRef(false);
// 加载机场摄像头
const loadStationCamera = async (cameraPosition: 'indoor' | 'outdoor') => {
setSelectedCamera(cameraPosition);
if (!device?.gateway_sn || switchingLock.current) return;
switchingLock.current = true;
setLoading(true);
try {
const cameraIndex = device?.gateway_camera_list[0]?.camera_index || ""; // 目前先默认用第一个摄像头
if (!cameraIndex) {
setStreamData(null);
utils.message.warning(`未找到${cameraPosition === 'indoor' ? '室内' : '室外'}摄像头配置`);
return;
}
const res = await changeCameragateway({
sn: device.gateway_sn,
cameraIndex,
cameraPosition,
qualityType: "adaptive",
videoExpire: 7200,
});
if (res.code == 200) setStreamData(res.data);
else utils.message.error(res.msg || '切换机场摄像头失败');
} catch (err) {
console.error('切换机场摄像头失败', err);
} finally {
setLoading(false);
switchingLock.current = false;
}
};
// 加载无人机镜头(使用当前选中的摄像头索引)
const loadDroneLens = async (lensType: 'wide' | 'zoom' | 'ir') => {
setSelectedLens(lensType);
if (!device?.device_sn || switchingLock.current) return;
switchingLock.current = true;
setLoading(true);
try {
const cameraList = device.drone_camera_list || [];
const targetCamera = cameraList[selectedDroneCameraIdx];
const cameraIndex = targetCamera?.camera_index || "";
if (!cameraIndex) {
utils.message.warning('未找到无人机相机配置');
return;
}
const res = await changeCameraPlane({
sn: device.device_sn,
lensType,
cameraIndex,
qualityType: "adaptive",
videoExpire: 7200,
});
if (res.code === 200) setStreamData(res.data);
else utils.message.error(res.msg || '切换无人机镜头失败');
} catch (err) {
console.error('切换无人机镜头失败', err);
} finally {
setLoading(false);
switchingLock.current = false;
}
};
// 切换无人机摄像头
const switchDroneCamera = (idx: number) => {
setSelectedDroneCameraIdx(idx);
};
// 自动加载
useEffect(() => {
if (device) {
setStreamData(null);
if (videoSource === 'station') {
loadStationCamera(selectedCamera);
} else {
loadDroneLens(selectedLens);
}
}
}, [device, videoSource, selectedDroneCameraIdx]);
// 生成摄像头下拉选项
const droneCameraOptions = device?.drone_camera_list?.map((item: any, idx: number) => ({
label: item.camera_name || `摄像头 ${idx + 1}`,
value: idx,
})) || [];
return (
<div style={{ width: '100%' }}>
{showSourceSelect && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<Select
size="small"
style={{ width: 120 }}
value={videoSource}
onChange={(val) => setVideoSource(val)}
options={[
{ label: '机场视频', value: 'station' },
{ label: '无人机视频', value: 'drone' },
]}
/>
</div>
)}
<div style={{
width: '100%',
height,
borderRadius: 8,
position: 'relative',
overflow: 'hidden',
background: '#000'
}}>
{/* 视频控制按钮 */}
<div style={{
position: 'absolute',
top: '6px',
right: '6px',
display: 'flex',
gap: '4px',
zIndex: 10,
flexWrap: 'wrap',
}}>
{videoSource === 'station' ? (
<>
<Button disabled={loading} size="small" type="text" style={{
background: selectedCamera === 'indoor' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
}} onClick={() => loadStationCamera('indoor')}>室内</Button>
<Button disabled={loading} size="small" type="text" style={{
background: selectedCamera === 'outdoor' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
}} onClick={() => loadStationCamera('outdoor')}>室外</Button>
</>
) : (
<>
{/* 动态摄像头下拉框 */}
{droneCameraOptions.length > 0 && (
<Select
disabled={loading}
size="small"
value={selectedDroneCameraIdx}
onChange={switchDroneCamera}
options={droneCameraOptions}
status={loading ? 'warning' : undefined}
/>
)}
<Button disabled={loading} size="small" type="text" style={{
background: selectedLens === 'wide' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
}} onClick={() => loadDroneLens('wide')}>广角</Button>
<Button disabled={loading} size="small" type="text" style={{
background: selectedLens === 'zoom' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
}} onClick={() => loadDroneLens('zoom')}>变焦</Button>
<Button disabled={loading} size="small" type="text" style={{
background: selectedLens === 'ir' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
}} onClick={() => loadDroneLens('ir')}>红外</Button>
</>
)}
</div>
{/* 视频内容 */}
{streamData ? (
<DroneLivePlayer streamData={streamData} />
) : (
<div style={{
color: '#ccc',
display: 'flex',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 8
}}>
{loading ? <ReloadOutlined spin style={{ color: '#fff' }} /> : null}
<span style={{ color: '#fff', fontSize: 12 }}>
{loading ? '视频加载中...' : '暂无视频信号'}
</span>
</div>
)}
{/* 底部信息叠加 */}
{videoSource === 'drone' && device && (
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
background: 'rgba(0,0,0,0.6)',
color: '#fff',
display: 'flex',
justifyContent: 'space-between',
padding: '8px 12px',
fontSize: 12
}}>
<span>高度 {device.height?.toFixed(2) || '-'}m</span>
<span>风速 {device.wind_speed?.toFixed(2) || '-'}m/s</span>
</div>
)}
</div>
</div>
);
};
export default DroneVideoPlayer;