package camera import ( "fmt" "os/exec" "regexp" "runtime" "strconv" "strings" ) // DetectFFmpeg checks if ffmpeg is available on the system. func DetectFFmpeg() bool { _, err := exec.LookPath("ffmpeg") return err == nil } // ListFFmpegDevices detects available video devices using ffmpeg. // Automatically selects the correct capture framework for the current OS: // - macOS: AVFoundation // - Windows: DirectShow (dshow) func ListFFmpegDevices() []CameraInfo { if !DetectFFmpeg() { return nil } switch runtime.GOOS { case "windows": return listDShowDevices() default: return listAVFoundationDevices() } } // --- macOS (AVFoundation) --- func listAVFoundationDevices() []CameraInfo { cmd := exec.Command("ffmpeg", "-f", "avfoundation", "-list_devices", "true", "-i", "") output, _ := cmd.CombinedOutput() return parseAVFoundationOutput(string(output)) } // parseAVFoundationOutput parses ffmpeg AVFoundation device listing. // Example: // // [AVFoundation indev @ 0x...] AVFoundation video devices: // [AVFoundation indev @ 0x...] [0] FaceTime HD Camera // [AVFoundation indev @ 0x...] [1] Capture screen 0 // [AVFoundation indev @ 0x...] AVFoundation audio devices: func parseAVFoundationOutput(output string) []CameraInfo { var cameras []CameraInfo lines := strings.Split(output, "\n") deviceRe := regexp.MustCompile(`\[AVFoundation[^\]]*\]\s*\[(\d+)\]\s*(.+)`) inVideoSection := false for _, line := range lines { if strings.Contains(line, "AVFoundation video devices") { inVideoSection = true continue } if strings.Contains(line, "AVFoundation audio devices") { break } if !inVideoSection { continue } matches := deviceRe.FindStringSubmatch(line) if len(matches) == 3 { index, err := strconv.Atoi(matches[1]) if err != nil { continue } name := strings.TrimSpace(matches[2]) // Skip screen capture devices if strings.Contains(strings.ToLower(name), "capture screen") { continue } cameras = append(cameras, CameraInfo{ ID: fmt.Sprintf("cam-%d", index), Name: name, Index: index, Width: 640, Height: 480, }) } } return cameras } // --- Windows (DirectShow) --- func listDShowDevices() []CameraInfo { cmd := exec.Command("ffmpeg", "-f", "dshow", "-list_devices", "true", "-i", "dummy") output, _ := cmd.CombinedOutput() return parseDShowOutput(string(output)) } // parseDShowOutput parses ffmpeg DirectShow device listing. // Example: // // [dshow @ 0x...] "Integrated Camera" (video) // [dshow @ 0x...] Alternative name "@device_pnp_..." // [dshow @ 0x...] "Microphone" (audio) func parseDShowOutput(output string) []CameraInfo { var cameras []CameraInfo lines := strings.Split(output, "\n") // Match: [dshow @ 0x...] "Device Name" (video) deviceRe := regexp.MustCompile(`\[dshow[^\]]*\]\s*"([^"]+)"\s*\(video\)`) index := 0 for _, line := range lines { matches := deviceRe.FindStringSubmatch(line) if len(matches) == 2 { name := strings.TrimSpace(matches[1]) cameras = append(cameras, CameraInfo{ ID: fmt.Sprintf("cam-%d", index), Name: name, Index: index, Width: 640, Height: 480, }) index++ } } return cameras }