|
|
|
|
@@ -11,6 +11,7 @@ interface DroneVideoPlayerProps {
|
|
|
|
|
device: any;
|
|
|
|
|
height?: number | string;
|
|
|
|
|
showSourceSelect?: boolean;
|
|
|
|
|
dualVideo?: boolean;
|
|
|
|
|
defaultSource?: 'station' | 'drone';
|
|
|
|
|
videoSource?: 'station' | 'drone';
|
|
|
|
|
onSourceChange?: (source: 'station' | 'drone') => void;
|
|
|
|
|
@@ -20,6 +21,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
device,
|
|
|
|
|
height = 160,
|
|
|
|
|
showSourceSelect = true,
|
|
|
|
|
dualVideo = false,
|
|
|
|
|
defaultSource = 'drone',
|
|
|
|
|
videoSource: propsVideoSource,
|
|
|
|
|
onSourceChange
|
|
|
|
|
@@ -35,8 +37,10 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
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 [droneStreamData, setDroneStreamData] = useState<any>(null);
|
|
|
|
|
const [stationStreamData, setStationStreamData] = useState<any>(null);
|
|
|
|
|
const [loadingDrone, setLoadingDrone] = useState(false);
|
|
|
|
|
const [loadingStation, setLoadingStation] = useState(false);
|
|
|
|
|
|
|
|
|
|
const switchingLock = useRef(false);
|
|
|
|
|
|
|
|
|
|
@@ -45,11 +49,11 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
setSelectedCamera(cameraPosition);
|
|
|
|
|
if (!device?.gateway_sn || switchingLock.current) return;
|
|
|
|
|
switchingLock.current = true;
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setLoadingStation(true);
|
|
|
|
|
try {
|
|
|
|
|
const cameraIndex = device?.gateway_camera_list[0]?.camera_index || ""; // 目前先默认用第一个摄像头
|
|
|
|
|
if (!cameraIndex) {
|
|
|
|
|
setStreamData(null);
|
|
|
|
|
setStationStreamData(null);
|
|
|
|
|
utils.message.warning(`未找到${cameraPosition === 'indoor' ? '室内' : '室外'}摄像头配置`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -62,12 +66,12 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
videoExpire: 7200,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.code == 200) setStreamData(res.data);
|
|
|
|
|
if (res.code == 200) setStationStreamData(res.data);
|
|
|
|
|
else utils.message.error(res.msg || '切换机场摄像头失败');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('切换机场摄像头失败', err);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
setLoadingStation(false);
|
|
|
|
|
switchingLock.current = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
@@ -78,7 +82,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
if (!device?.device_sn || switchingLock.current) return;
|
|
|
|
|
|
|
|
|
|
switchingLock.current = true;
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setLoadingDrone(true);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const cameraList = device.drone_camera_list || [];
|
|
|
|
|
@@ -98,12 +102,12 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
videoExpire: 7200,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.code === 200) setStreamData(res.data);
|
|
|
|
|
if (res.code === 200) setDroneStreamData(res.data);
|
|
|
|
|
else utils.message.error(res.msg || '切换无人机镜头失败');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('切换无人机镜头失败', err);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
setLoadingDrone(false);
|
|
|
|
|
switchingLock.current = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
@@ -116,14 +120,22 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
// 自动加载
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (device) {
|
|
|
|
|
setStreamData(null);
|
|
|
|
|
if (videoSource === 'station') {
|
|
|
|
|
if (dualVideo) {
|
|
|
|
|
setDroneStreamData(null);
|
|
|
|
|
setStationStreamData(null);
|
|
|
|
|
loadStationCamera(selectedCamera);
|
|
|
|
|
} else {
|
|
|
|
|
loadDroneLens(selectedLens);
|
|
|
|
|
} else {
|
|
|
|
|
if (videoSource === 'station') {
|
|
|
|
|
setStationStreamData(null);
|
|
|
|
|
loadStationCamera(selectedCamera);
|
|
|
|
|
} else {
|
|
|
|
|
setDroneStreamData(null);
|
|
|
|
|
loadDroneLens(selectedLens);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [device, videoSource, selectedDroneCameraIdx]);
|
|
|
|
|
}, [device, videoSource, selectedDroneCameraIdx, dualVideo]);
|
|
|
|
|
|
|
|
|
|
// 生成摄像头下拉选项
|
|
|
|
|
const droneCameraOptions = device?.drone_camera_list?.map((item: any, idx: number) => ({
|
|
|
|
|
@@ -131,30 +143,18 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
const renderVideo = (type: 'drone' | 'station', streamData: any, loading: boolean) => {
|
|
|
|
|
const isDrone = type === 'drone';
|
|
|
|
|
return (
|
|
|
|
|
<div style={{
|
|
|
|
|
width: '100%',
|
|
|
|
|
height,
|
|
|
|
|
flex: 1,
|
|
|
|
|
height: height,
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
position: 'relative',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
background: '#000'
|
|
|
|
|
background: '#000',
|
|
|
|
|
border: dualVideo ? '1px solid #243157' : 'none',
|
|
|
|
|
minWidth: dualVideo ? '300px' : 'auto' // 确保在双视频模式下有最小宽度
|
|
|
|
|
}}>
|
|
|
|
|
{/* 视频控制按钮 */}
|
|
|
|
|
<div style={{
|
|
|
|
|
@@ -166,26 +166,25 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
zIndex: 10,
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
}}>
|
|
|
|
|
{videoSource === 'station' ? (
|
|
|
|
|
{!isDrone ? (
|
|
|
|
|
<>
|
|
|
|
|
<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
|
|
|
|
|
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, 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
|
|
|
|
|
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
|
|
|
|
|
}} onClick={() => loadStationCamera('outdoor')}>室外</Button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* 动态摄像头下拉框 */}
|
|
|
|
|
{droneCameraOptions.length > 0 && (
|
|
|
|
|
{droneCameraOptions.length > 1 && (
|
|
|
|
|
<Select
|
|
|
|
|
disabled={loading}
|
|
|
|
|
size="small"
|
|
|
|
|
|
|
|
|
|
style={{ width: 80, fontSize: 10 }}
|
|
|
|
|
value={selectedDroneCameraIdx}
|
|
|
|
|
onChange={switchDroneCamera}
|
|
|
|
|
options={droneCameraOptions}
|
|
|
|
|
@@ -195,17 +194,17 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
|
|
|
|
|
<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
|
|
|
|
|
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, 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
|
|
|
|
|
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, 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
|
|
|
|
|
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
|
|
|
|
|
}} onClick={() => loadDroneLens('ir')}>红外</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
@@ -231,8 +230,22 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 6,
|
|
|
|
|
left: 6,
|
|
|
|
|
background: 'rgba(0,0,0,0.5)',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
padding: '2px 6px',
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
zIndex: 5
|
|
|
|
|
}}>
|
|
|
|
|
{isDrone ? '无人机视频' : '机场视频'}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 底部信息叠加 */}
|
|
|
|
|
{videoSource === 'drone' && device && (
|
|
|
|
|
{isDrone && device && (
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
bottom: 0,
|
|
|
|
|
@@ -242,14 +255,52 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
|
|
|
|
|
color: '#fff',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
padding: '8px 12px',
|
|
|
|
|
fontSize: 12
|
|
|
|
|
padding: '4px 8px',
|
|
|
|
|
fontSize: 10
|
|
|
|
|
}}>
|
|
|
|
|
<span>高度 {device.height?.toFixed(2) || '-'}m</span>
|
|
|
|
|
<span>风速 {device.wind_speed?.toFixed(2) || '-'}m/s</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
|
|
|
{!dualVideo && 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%',
|
|
|
|
|
flex: 1,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
gap: dualVideo ? 8 : 0
|
|
|
|
|
}}>
|
|
|
|
|
{dualVideo ? (
|
|
|
|
|
<>
|
|
|
|
|
{renderVideo('drone', droneStreamData, loadingDrone)}
|
|
|
|
|
{renderVideo('station', stationStreamData, loadingStation)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
videoSource === 'station'
|
|
|
|
|
? renderVideo('station', stationStreamData, loadingStation)
|
|
|
|
|
: renderVideo('drone', droneStreamData, loadingDrone)
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|