param( [Parameter(Position = 0)] [string]$Action = "start", [string]$MediaConfig = "configs/sample_cam1.json", [string]$VideoFile = "", [string]$SafesightdConfig = "safesightd.json" ) $ErrorActionPreference = "Stop" $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $controlDir = (Resolve-Path (Join-Path $scriptDir "..")).Path $edgeDir = (Resolve-Path (Join-Path $controlDir "..\safesight-edge")).Path # ── paths ────────────────────────────────────────────── $mediamtx = "C:\Software\mediamtx\mediamtx.exe" $mediamtxDir = "C:\Software\mediamtx" $ffmpeg = "ffmpeg" $minio = "C:\Users\Tellme\minio\minio.exe" $minioData = "C:\Users\Tellme\minio\myminio" $mockAlarm = Join-Path $scriptDir "../safesight-edge/scripts/mock_alarm_server.py" $safesightdExe = Join-Path $controlDir "safesightd.exe" $safesightdConfig = Join-Path $controlDir $SafesightdConfig $mediaServerConfig = Join-Path $edgeDir $MediaConfig $mediaServerExe = Join-Path $edgeDir "build\media-server.exe" $rtspUrl = "rtsp://10.0.0.49:8554/cam" $defaultVideo = "C:\Users\Tellme\Pictures\人脸库\reg_008_unk_011_多人_正面_黑色鞋_白色鞋_1.mp4" function Write-Banner { param([string]$Text) Write-Host ("`n" + ("=" * 54)) -ForegroundColor Cyan Write-Host " $Text" -ForegroundColor Cyan Write-Host ("=" * 54) -ForegroundColor Cyan } function Start-Detached { param([string]$Name, [string]$Exe, [string[]]$Args, [switch]$Visible, [string]$WorkDir) $exePath = $Exe if (-not (Test-Path $Exe)) { $cmd = Get-Command $Exe -ErrorAction SilentlyContinue if ($cmd) { $exePath = $cmd.Source } } if (-not $exePath -or -not (Test-Path $exePath)) { Write-Host " [SKIP] $Name : 未找到 $Exe" -ForegroundColor Yellow return $null } Write-Host " [START] $Name ($exePath)" -ForegroundColor Green $style = if ($Visible) { "Normal" } else { "Minimized" } $procArgs = @{FilePath=$exePath; WindowStyle=$style; PassThru=$true} if ($WorkDir) { $procArgs.WorkingDirectory = $WorkDir } if ($Args -and $Args.Count -gt 0) { $procArgs.ArgumentList = $Args } $proc = Start-Process @procArgs Start-Sleep -Milliseconds 500 return $proc } function Stop-ByName { param([string]$Name) $procs = Get-Process -Name $Name -ErrorAction SilentlyContinue if ($procs) { $procs | Stop-Process -Force Write-Host " [STOP] $Name" -ForegroundColor DarkYellow } } # ════════════════════════════════════════════════════════ # START # ════════════════════════════════════════════════════════ function Invoke-Start { # 1. RTSP server Write-Banner "1/5 RTSP Server (mediamtx)" Start-Detached "mediamtx" $mediamtx -WorkDir $mediamtxDir Write-Host " [WAIT] 等待 RTSP 服务就绪..." -ForegroundColor DarkGray $ready = $false for ($i = 0; $i -lt 10; $i++) { Start-Sleep 1 $tcp = netstat -an 2>$null | Select-String ':8554.*LISTENING' if ($tcp) { $ready = $true; break } } if ($ready) { Write-Host " [OK] RTSP 服务就绪" -ForegroundColor Green } else { Write-Host " [WARN] RTSP 服务可能未就绪" -ForegroundColor Yellow } # 2. ffmpeg push Write-Banner "2/5 RTSP 推流 (ffmpeg)" $videoToUse = if ($VideoFile -and (Test-Path $VideoFile)) { $VideoFile } elseif (Test-Path $defaultVideo) { $defaultVideo } else { $null } if ($videoToUse) { $ffArgs = @("-re", "-stream_loop", "-1", "-i", $videoToUse, "-c", "copy", "-rtsp_transport", "tcp", "-f", "rtsp", $rtspUrl) Write-Host " 视频文件: $videoToUse" } else { # Try to find a webcam $webcam = (& $ffmpeg -list_devices true -f dshow -i dummy 2>&1 | Select-String '\[dshow.*"(.+)"' | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1) if ($webcam) { Write-Host " 摄像头: $webcam" $ffArgs = @("-f", "dshow", "-rtbufsize", "100M", "-video_size", "1280x720", "-framerate", "30", "-vcodec", "mjpeg", "-i", "video=$webcam", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-f", "rtsp", $rtspUrl) } else { Write-Host " [SKIP] 未指定视频文件,也未检测到摄像头" -ForegroundColor Yellow $ffArgs = $null } } if ($ffArgs) { $ffPath = (Get-Command $ffmpeg -ErrorAction SilentlyContinue).Source if (-not $ffPath) { $ffPath = $ffmpeg } $bat = Join-Path $edgeDir "scripts\ffmpeg-push.bat" "@echo off`r`nchcp 65001 >nul`r`n`"$ffPath`" $($ffArgs -join ' ')" | Out-File -FilePath $bat -Encoding UTF8 Start-Process "cmd" -ArgumentList "/k", $bat Write-Host " [START] ffmpeg (bat窗口)" -ForegroundColor Green } # 3. Mock alarm server (if uv available) Write-Banner "3/4 Mock Alarm Server" if (Get-Command uv -ErrorAction SilentlyContinue) { $alarmBat = Join-Path $edgeDir "scripts\mock-alarm.bat" "@echo off`r`ncd /d `"$(Join-Path $edgeDir 'scripts')`"`r`nuv run --with flask mock_alarm_server.py" | Out-File -FilePath $alarmBat -Encoding ASCII Start-Process "cmd" -ArgumentList "/k", $alarmBat Write-Host " [START] mock_alarm_server (端口 8080)" -ForegroundColor Green } else { Write-Host " [SKIP] uv 未安装" -ForegroundColor Yellow } # 5. Management server (safesightd) Write-Banner "4/4 Management Server (safesightd)" if (Test-Path $safesightdExe) { Start-Process -FilePath $safesightdExe ` -ArgumentList $safesightdConfig ` -WorkingDirectory $controlDir ` -WindowStyle Minimized Write-Host " [START] safesightd" -ForegroundColor Green Write-Host " URL: http://localhost:18080" -ForegroundColor White } elseif (Get-Command go -ErrorAction SilentlyContinue) { Write-Host " [START] go run (safesightd 未编译)" -ForegroundColor Green Start-Process -FilePath "go" ` -ArgumentList "run", ".\cmd\safesightd\", $safesightdConfig ` -WorkingDirectory $controlDir ` -WindowStyle Minimized } else { Write-Host " [SKIP] 既无 exe 也无 go" -ForegroundColor Yellow } # 6. MinIO (optional) Write-Banner "MinIO (object storage)" if (Test-Path $minio) { if (-not (Test-Path $minioData)) { New-Item -ItemType Directory -Path $minioData -Force | Out-Null } $minioBat = Join-Path $env:TEMP "safesight-minio.bat" "@echo off`r`n`"$minio`" server `"$minioData`" --address :9000 --console-address :9001" | Out-File -FilePath $minioBat -Encoding ASCII Start-Process "cmd" -ArgumentList "/k", $minioBat Write-Host " Console: http://localhost:9001 (admin/password)" -ForegroundColor White } # Summary Write-Banner "全部启动完成" Write-Host " RTSP 输入: $rtspUrl" Write-Host " 默认视频: $defaultVideo" Write-Host " 管理端: http://localhost:18080" Write-Host " 视频监控: http://localhost:18080/ui/monitor" Write-Host " MinIO: http://localhost:9001" Write-Host "" Write-Host " 停止所有: powershell .\scripts\dev-env.ps1 stop" -ForegroundColor DarkGray } # ════════════════════════════════════════════════════════ # STOP # ════════════════════════════════════════════════════════ function Invoke-Stop { Write-Banner "停止所有模拟环境进程" Stop-ByName "mediamtx" Stop-ByName "ffmpeg" Stop-ByName "minio" Stop-ByName "safesightd" # Kill python mock_alarm if running Get-Process -Name "python" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match "mock_alarm" } | Stop-Process -Force Write-Host "" Write-Host " 已全部停止" -ForegroundColor Green } # ════════════════════════════════════════════════════════ # STATUS # ════════════════════════════════════════════════════════ function Invoke-Status { Write-Banner "模拟环境状态" @( @{Name="mediamtx"; Port=8554; URL="rtsp://localhost:8554"}, @{Name="safesightd"; Port=18080; URL="http://localhost:18080/health"}, @{Name="minio"; Port=9000; URL="http://localhost:9000"} ) | ForEach-Object { $proc = Get-Process -Name $_.Name -ErrorAction SilentlyContinue $status = if ($proc) { "● 运行中" } else { "○ 未运行" } $color = if ($proc) { "Green" } else { "DarkGray" } Write-Host " $status $($_.Name) $($_.URL)" -ForegroundColor $color } } # ════════════════════════════════════════════════════════ switch ($Action.ToLowerInvariant()) { "start" { Invoke-Start } "stop" { Invoke-Stop } "status" { Invoke-Status } default { Write-Host "用法: powershell .\scripts\dev-env.ps1 [start|stop|status]" Write-Host "" Write-Host "可选参数 (仅 start):" Write-Host " -MediaConfig media-server 配置 (默认: configs/sample_cam1.json)" Write-Host " -VideoFile 推流视频文件路径" } }