feat: Enhance camera streaming functionality with stop streaming feature and update UI for better user experience
This commit is contained in:
@@ -457,31 +457,18 @@ export function CameraConfigModal({ cameraName, isOpen, onClose, onSuccess, onEr
|
||||
<p className="text-xs text-gray-500 mt-1">Start recording when MQTT machine state changes to ON</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.auto_start_recording_enabled ?? false}
|
||||
onChange={(e) => updateSetting('auto_start_recording_enabled', e.target.checked)}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Enhanced Auto Recording</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1">Advanced auto-recording with retry logic</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Max Retries: {config.auto_recording_max_retries ?? 3}
|
||||
Max Retries: {config.auto_recording_max_retries}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
value={config.auto_recording_max_retries ?? 3}
|
||||
step="1"
|
||||
value={config.auto_recording_max_retries}
|
||||
onChange={(e) => updateSetting('auto_recording_max_retries', parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
disabled={!config.auto_start_recording_enabled}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>1</span>
|
||||
@@ -491,16 +478,16 @@ export function CameraConfigModal({ cameraName, isOpen, onClose, onSuccess, onEr
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Retry Delay: {config.auto_recording_retry_delay_seconds ?? 5}s
|
||||
Retry Delay (seconds): {config.auto_recording_retry_delay_seconds}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="30"
|
||||
value={config.auto_recording_retry_delay_seconds ?? 5}
|
||||
step="1"
|
||||
value={config.auto_recording_retry_delay_seconds}
|
||||
onChange={(e) => updateSetting('auto_recording_retry_delay_seconds', parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
disabled={!config.auto_start_recording_enabled}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>1s</span>
|
||||
@@ -526,8 +513,6 @@ export function CameraConfigModal({ cameraName, isOpen, onClose, onSuccess, onEr
|
||||
<li>Noise reduction settings require camera restart to take effect</li>
|
||||
<li>Use "Apply & Restart" to apply settings that require restart</li>
|
||||
<li>HDR mode may impact performance when enabled</li>
|
||||
<li>Auto-recording monitors MQTT machine state changes for automatic recording</li>
|
||||
<li>Enhanced auto-recording provides retry logic for failed recording attempts</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,13 +168,15 @@ const CamerasStatus = memo(({
|
||||
onConfigureCamera,
|
||||
onStartRecording,
|
||||
onStopRecording,
|
||||
onPreviewCamera
|
||||
onPreviewCamera,
|
||||
onStopStreaming
|
||||
}: {
|
||||
systemStatus: SystemStatus,
|
||||
onConfigureCamera: (cameraName: string) => void,
|
||||
onStartRecording: (cameraName: string) => Promise<void>,
|
||||
onStopRecording: (cameraName: string) => Promise<void>,
|
||||
onPreviewCamera: (cameraName: string) => void
|
||||
onPreviewCamera: (cameraName: string) => void,
|
||||
onStopStreaming: (cameraName: string) => Promise<void>
|
||||
}) => {
|
||||
const { isAdmin } = useAuth()
|
||||
|
||||
@@ -325,10 +327,14 @@ const CamerasStatus = memo(({
|
||||
Stop Recording
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview and Streaming Controls */}
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => onPreviewCamera(cameraName)}
|
||||
disabled={!isConnected}
|
||||
className={`px-3 py-2 text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 ${isConnected
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 ${isConnected
|
||||
? 'text-blue-600 bg-blue-50 border border-blue-200 hover:bg-blue-100 focus:ring-blue-500'
|
||||
: 'text-gray-400 bg-gray-50 border border-gray-200 cursor-not-allowed'
|
||||
}`}
|
||||
@@ -339,10 +345,27 @@ const CamerasStatus = memo(({
|
||||
</svg>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Admin Configuration Button */}
|
||||
{isAdmin() && (
|
||||
<button
|
||||
onClick={() => onStopStreaming(cameraName)}
|
||||
disabled={!isConnected}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 ${isConnected
|
||||
? 'text-orange-600 bg-orange-50 border border-orange-200 hover:bg-orange-100 focus:ring-orange-500'
|
||||
: 'text-gray-400 bg-gray-50 border border-gray-200 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Stop Streaming
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Configuration Button */}
|
||||
{isAdmin() && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<button
|
||||
onClick={() => onConfigureCamera(cameraName)}
|
||||
className="w-full px-3 py-2 text-sm font-medium text-indigo-600 bg-indigo-50 border border-indigo-200 rounded-md hover:bg-indigo-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
@@ -353,8 +376,8 @@ const CamerasStatus = memo(({
|
||||
</svg>
|
||||
Configure Camera
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -617,8 +640,7 @@ export function VisionSystem() {
|
||||
const result = await visionApi.stopRecording(cameraName)
|
||||
|
||||
if (result.success) {
|
||||
const duration = result.duration_seconds ? ` (${result.duration_seconds}s)` : ''
|
||||
setNotification({ type: 'success', message: `Recording stopped${duration}` })
|
||||
setNotification({ type: 'success', message: `Recording stopped: ${result.filename}` })
|
||||
// Refresh data to update recording status
|
||||
fetchData(false)
|
||||
} else {
|
||||
@@ -635,6 +657,23 @@ export function VisionSystem() {
|
||||
setPreviewModalOpen(true)
|
||||
}
|
||||
|
||||
const handleStopStreaming = async (cameraName: string) => {
|
||||
try {
|
||||
const result = await visionApi.stopStream(cameraName)
|
||||
|
||||
if (result.success) {
|
||||
setNotification({ type: 'success', message: `Streaming stopped for ${cameraName}` })
|
||||
// Refresh data to update camera status
|
||||
fetchData(false)
|
||||
} else {
|
||||
setNotification({ type: 'error', message: `Failed to stop streaming: ${result.message}` })
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
setNotification({ type: 'error', message: `Error stopping stream: ${errorMessage}` })
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string, isRecording: boolean = false) => {
|
||||
// If camera is recording, always show red regardless of status
|
||||
if (isRecording) {
|
||||
@@ -797,6 +836,7 @@ export function VisionSystem() {
|
||||
onStartRecording={handleStartRecording}
|
||||
onStopRecording={handleStopRecording}
|
||||
onPreviewCamera={handlePreviewCamera}
|
||||
onStopStreaming={handleStopStreaming}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -391,9 +391,11 @@ class VisionApiClient {
|
||||
try {
|
||||
const config = await this.request(`/cameras/${cameraName}/config`) as any
|
||||
|
||||
// Ensure auto-recording fields have default values if missing
|
||||
// Map API field names to UI expected field names and ensure auto-recording fields have default values if missing
|
||||
return {
|
||||
...config,
|
||||
// Map auto_start_recording_enabled from API to auto_record_on_machine_start for UI
|
||||
auto_record_on_machine_start: config.auto_start_recording_enabled ?? false,
|
||||
auto_start_recording_enabled: config.auto_start_recording_enabled ?? false,
|
||||
auto_recording_max_retries: config.auto_recording_max_retries ?? 3,
|
||||
auto_recording_retry_delay_seconds: config.auto_recording_retry_delay_seconds ?? 5
|
||||
@@ -418,12 +420,14 @@ class VisionApiClient {
|
||||
|
||||
const rawConfig = await response.json()
|
||||
|
||||
// Add missing auto-recording fields with defaults
|
||||
// Add missing auto-recording fields with defaults and map field names
|
||||
return {
|
||||
...rawConfig,
|
||||
auto_start_recording_enabled: false,
|
||||
auto_recording_max_retries: 3,
|
||||
auto_recording_retry_delay_seconds: 5
|
||||
// Map auto_start_recording_enabled from API to auto_record_on_machine_start for UI
|
||||
auto_record_on_machine_start: rawConfig.auto_start_recording_enabled ?? false,
|
||||
auto_start_recording_enabled: rawConfig.auto_start_recording_enabled ?? false,
|
||||
auto_recording_max_retries: rawConfig.auto_recording_max_retries ?? 3,
|
||||
auto_recording_retry_delay_seconds: rawConfig.auto_recording_retry_delay_seconds ?? 5
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
throw new Error(`Failed to load camera configuration: ${error.message}`)
|
||||
@@ -435,9 +439,19 @@ class VisionApiClient {
|
||||
}
|
||||
|
||||
async updateCameraConfig(cameraName: string, config: CameraConfigUpdate): Promise<CameraConfigUpdateResponse> {
|
||||
// Map UI field names to API field names
|
||||
const apiConfig = { ...config }
|
||||
|
||||
// If auto_record_on_machine_start is present, map it to auto_start_recording_enabled for the API
|
||||
if ('auto_record_on_machine_start' in config) {
|
||||
apiConfig.auto_start_recording_enabled = config.auto_record_on_machine_start
|
||||
// Remove the UI field name to avoid confusion
|
||||
delete apiConfig.auto_record_on_machine_start
|
||||
}
|
||||
|
||||
return this.request(`/cameras/${cameraName}/config`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(config),
|
||||
body: JSON.stringify(apiConfig),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user