258 lines
7.7 KiB
TypeScript
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;
|