package camera import ( "fmt" "sync" "time" ) // firstFrameTimeout 是 Open() 等待攝影機產出第一張 frame 的上限。 // // 為何 25s(不是更短):首次開攝影機時 macOS 的 TCC 攝影機授權對話框是「同步阻擋」的, // 逾時從進入 WaitForFirstFrame 就起算、會把使用者在彈窗前猶豫/反應的時間也算進去。 // 若太短(如 8s),首次點「開始推論」極可能在使用者還沒按下「允許」前就逾時失敗、 // 要按第二次才成功——首次體驗變成非預期失敗。25s 足夠涵蓋 TCC 授權彈窗的使用者反應時間。 // // 為何拉長不會拖慢正常情境:授權彈窗期間 ffmpeg 進程仍存活、不會早退;且一拿到第一張 // frame 就立即返回,已授權情境仍是秒開,不會真的等滿 25s。只有「真的開不了」(權限被拒 // 後 ffmpeg exit / 裝置忙碌)時才會等到逾時——ffmpeg 早退會由 done 分支提前回錯誤, // 真正等滿 25s 的僅剩「進程活著但持續不出 frame」的少數情況。 const firstFrameTimeout = 25 * time.Second type CameraInfo struct { ID string `json:"id"` Name string `json:"name"` Index int `json:"index"` Width int `json:"width"` Height int `json:"height"` } type Manager struct { ffmpegCam *FFmpegCamera isOpen bool mu sync.Mutex } func NewManager() *Manager { return &Manager{} } func (m *Manager) ListCameras() []CameraInfo { // Try to detect real cameras via ffmpeg (auto-detects OS) devices := ListFFmpegDevices() if len(devices) > 0 { return devices } if !DetectFFmpeg() { fmt.Println("[WARN] ffmpeg not found — install with: brew install ffmpeg (macOS) or winget install ffmpeg (Windows)") } else { fmt.Println("[WARN] No video devices detected by ffmpeg") } return []CameraInfo{} } func (m *Manager) Open(index, width, height int) error { m.mu.Lock() defer m.mu.Unlock() // Try real camera via ffmpeg if !DetectFFmpeg() { return fmt.Errorf("ffmpeg not found — install with: brew install ffmpeg (macOS) or winget install ffmpeg (Windows)") } cam, err := NewFFmpegCamera(index, width, height, 30) if err != nil { return fmt.Errorf("failed to open camera (index=%d): %w", index, err) } // cmd.Start() 成功 ≠ 攝影機真的開起來。等第一張 frame 才算成功;否則清掉 ffmpeg // 進程並回錯誤(含 ffmpeg stderr 尾端),讓 handler 回非 200、前端看到真實失敗, // 而不是「200 假成功、畫面永遠空白」。 if err := cam.WaitForFirstFrame(firstFrameTimeout); err != nil { if tail := cam.StderrTail(); tail != "" { fmt.Printf("[ERROR] Camera open failed (index=%d): %v\n[ffmpeg stderr]\n%s\n", index, err, tail) } else { fmt.Printf("[ERROR] Camera open failed (index=%d): %v\n", index, err) } _ = cam.Close() return fmt.Errorf("failed to open camera (index=%d): %w", index, err) } m.ffmpegCam = cam m.isOpen = true fmt.Printf("[INFO] Opened real camera (index=%d) via ffmpeg (first frame received)\n", index) return nil } func (m *Manager) Close() error { m.mu.Lock() defer m.mu.Unlock() if m.ffmpegCam != nil { _ = m.ffmpegCam.Close() m.ffmpegCam = nil } m.isOpen = false return nil } func (m *Manager) ReadFrame() ([]byte, error) { m.mu.Lock() defer m.mu.Unlock() if !m.isOpen { return nil, fmt.Errorf("camera not open") } if m.ffmpegCam != nil { return m.ffmpegCam.ReadFrame() } return nil, fmt.Errorf("no camera available") } func (m *Manager) IsOpen() bool { m.mu.Lock() defer m.mu.Unlock() return m.isOpen }