feat: Enhance camera streaming functionality with stop streaming feature and update UI for better user experience

This commit is contained in:
Alireza Vaezi
2025-07-31 22:17:08 -04:00
parent 1f47e89a4d
commit 97f22d239d
7 changed files with 756 additions and 37 deletions

View File

@@ -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>

View File

@@ -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}
/>
)}

View File

@@ -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),
})
}