diff --git a/README.md b/README.md index 0f4fbe9..b3f0a09 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ This command will install `cam2ip` in `GOBIN`, you can point `GOBIN` to e.g. `/u Usage: cam2ip [] --index Camera index [CAM2IP_INDEX] (default "0") + --device + Camera name to use, matched as substring, overrides index [CAM2IP_DEVICE] (default "") --delay Delay between frames, in milliseconds [CAM2IP_DELAY] (default "10") --width @@ -62,6 +64,8 @@ Usage: cam2ip [] Bind address [CAM2IP_BIND_ADDR] (default ":56000") --htpasswd-file Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE] (default "") + --list-devices + List available cameras and exit (default "false") --version Print version and exit (default "false") ``` diff --git a/camera/camera.go b/camera/camera.go index ba55448..4a7cbd5 100644 --- a/camera/camera.go +++ b/camera/camera.go @@ -16,6 +16,12 @@ type Options struct { TimeFormat string } +// DeviceInfo describes a capture device. +type DeviceInfo struct { + Index int + Name string +} + var ( yuy2FourCC = fourcc("YUY2") yuyvFourCC = fourcc("YUYV") diff --git a/camera/camera_android.go b/camera/camera_android.go index 4d00665..2bfea1c 100644 --- a/camera/camera_android.go +++ b/camera/camera_android.go @@ -296,3 +296,8 @@ func (c *Camera) Close() (err error) { return } + +// Devices returns the available capture devices. +func Devices() ([]DeviceInfo, error) { + return nil, fmt.Errorf("camera: device enumeration not implemented on android") +} diff --git a/camera/camera_darwin.go b/camera/camera_darwin.go index bab7f70..0979128 100644 --- a/camera/camera_darwin.go +++ b/camera/camera_darwin.go @@ -100,6 +100,52 @@ func New(opts Options) (c *Camera, err error) { return c, nil } +// Devices returns the available capture devices. +func Devices() ([]DeviceInfo, error) { + if err := loadFrameworks(); err != nil { + return nil, err + } + + pool := objc.ID(objc.GetClass("NSAutoreleasePool")).Send(selAlloc).Send(selInit) + defer pool.Send(selDrain) + + avCaptureDevice := objc.ID(objc.GetClass("AVCaptureDevice")) + list := avCaptureDevice.Send(selDevicesWithMediaType, avMediaTypeVideo) + count := int(objc.Send[uint64](list, selCount)) + + devices := make([]DeviceInfo, 0, count) + for i := 0; i < count; i++ { + device := list.Send(selObjectAtIndex, uint64(i)) + devices = append(devices, DeviceInfo{Index: i, Name: goString(device.Send(selLocalizedName))}) + } + + return devices, nil +} + +// goString converts an NSString to a Go string. +func goString(s objc.ID) string { + if s == 0 { + return "" + } + + p := objc.Send[*byte](s, selUTF8String) + if p == nil { + return "" + } + + var b []byte + for i := 0; ; i++ { + ch := *(*byte)(unsafe.Add(unsafe.Pointer(p), i)) + if ch == 0 { + break + } + + b = append(b, ch) + } + + return string(b) +} + // Read reads next frame from camera and returns image. func (c *Camera) Read() (img image.Image, err error) { c.mu.Lock() @@ -366,6 +412,8 @@ var ( selSetSampleBufferQueue = objc.RegisterName("setSampleBufferDelegate:queue:") selStartRunning = objc.RegisterName("startRunning") selStopRunning = objc.RegisterName("stopRunning") + selLocalizedName = objc.RegisterName("localizedName") + selUTF8String = objc.RegisterName("UTF8String") ) var ( diff --git a/camera/camera_linux.go b/camera/camera_linux.go index 298475e..2261cff 100644 --- a/camera/camera_linux.go +++ b/camera/camera_linux.go @@ -214,3 +214,20 @@ func (c *Camera) Close() (err error) { return } + +// Devices returns the available capture devices. +func Devices() ([]DeviceInfo, error) { + infos := v4l.FindDevices() + devices := make([]DeviceInfo, 0, len(infos)) + + for i, d := range infos { + name := d.DeviceName + if name == "" { + name = d.Path + } + + devices = append(devices, DeviceInfo{Index: i, Name: name}) + } + + return devices, nil +} diff --git a/camera/camera_windows_msmf.go b/camera/camera_windows_msmf.go index 1e9e3c4..5f5121d 100644 --- a/camera/camera_windows_msmf.go +++ b/camera/camera_windows_msmf.go @@ -25,6 +25,7 @@ var ( iidIMFMediaSource = guid{0x279a808d, 0xaec7, 0x40c8, [8]byte{0x9c, 0x6b, 0xa6, 0xb4, 0x92, 0xc7, 0x8a, 0x66}} mfDevsourceAttributeSourceType = guid{0xc60ac5fe, 0x252a, 0x478f, [8]byte{0xa0, 0xef, 0xbc, 0x8f, 0xa5, 0xf7, 0xca, 0xd3}} mfDevsourceAttributeSourceTypeVidcap = guid{0x8ac3587a, 0x4ae7, 0x42d8, [8]byte{0x99, 0xe0, 0x0a, 0x60, 0x13, 0xee, 0xf9, 0x0f}} + mfDevsourceAttributeFriendlyName = guid{0x60d0e559, 0x52f8, 0x4fa2, [8]byte{0xbb, 0xce, 0xac, 0xdb, 0x34, 0xa8, 0xec, 0x01}} mfSourceReaderEnableVideoProcessing = guid{0xfb394f3d, 0xccf1, 0x42ee, [8]byte{0xbb, 0xb3, 0xf9, 0xb8, 0x45, 0xd5, 0x68, 0x1d}} mfMTMajorType = guid{0x48eba18e, 0xf8c9, 0x4687, [8]byte{0xbf, 0x11, 0x0a, 0x74, 0xc9, 0xf9, 0x6a, 0x8f}} mfMTSubtype = guid{0xf7e34c9a, 0x42e8, 0x4714, [8]byte{0xb7, 0x4b, 0xcb, 0x29, 0xd7, 0x2c, 0x35, 0xe5}} @@ -448,6 +449,70 @@ func nv12ToYCbCr420(data []byte, stride, width, height int, dst *image.YCbCr) { } } +// Devices returns the available capture devices. +func Devices() ([]DeviceInfo, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + for _, p := range []*syscall.LazyProc{procCoInitializeEx, procMFStartup, procMFCreateAttributes, procMFEnumDeviceSources} { + if err := p.Find(); err != nil { + return nil, fmt.Errorf("camera: %s: %w", p.Name, err) + } + } + + if r, _, _ := procCoInitializeEx.Call(0, coinitMultithreaded); failed(r) && uint32(r) != rpcEChangedMode { + return nil, fmt.Errorf("camera: CoInitializeEx: %#x", r) + } + + if r, _, _ := procMFStartup.Call(mfVersion, mfstartupLite); failed(r) { + return nil, fmt.Errorf("camera: MFStartup: %#x", r) + } + defer procMFShutdown.Call() + defer procCoUninitialize.Call() + + var attr unsafe.Pointer + if r, _, _ := procMFCreateAttributes.Call(uintptr(unsafe.Pointer(&attr)), 1); failed(r) { + return nil, fmt.Errorf("camera: MFCreateAttributes: %#x", r) + } + attrSetGUID(attr, &mfDevsourceAttributeSourceType, &mfDevsourceAttributeSourceTypeVidcap) + + var list *unsafe.Pointer + var count uint32 + r, _, _ := procMFEnumDeviceSources.Call(uintptr(attr), uintptr(unsafe.Pointer(&list)), uintptr(unsafe.Pointer(&count))) + release(attr) + if failed(r) { + return nil, fmt.Errorf("camera: MFEnumDeviceSources: %#x", r) + } + defer procCoTaskMemFree.Call(uintptr(unsafe.Pointer(list))) + + activates := unsafe.Slice(list, count) + devices := make([]DeviceInfo, 0, count) + + for i, a := range activates { + devices = append(devices, DeviceInfo{Index: i, Name: friendlyName(a)}) + release(a) + } + + return devices, nil +} + +// friendlyName reads MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME from an activate object. +func friendlyName(activate unsafe.Pointer) string { + var str *uint16 + var length uint32 + + if r := comCall(activate, 13, uintptr(unsafe.Pointer(&mfDevsourceAttributeFriendlyName)), uintptr(unsafe.Pointer(&str)), uintptr(unsafe.Pointer(&length))); failed(r) { + return "" + } + defer procCoTaskMemFree.Call(uintptr(unsafe.Pointer(str))) + + if str == nil { + return "" + } + + return syscall.UTF16ToString(unsafe.Slice(str, length)) +} + // comCall invokes the method at the given vtable index on a COM object. func comCall(obj unsafe.Pointer, index int, args ...uintptr) uintptr { vtbl := *(*unsafe.Pointer)(obj) diff --git a/camera/camera_windows_vfw.go b/camera/camera_windows_vfw.go index a0d6daf..41106b3 100644 --- a/camera/camera_windows_vfw.go +++ b/camera/camera_windows_vfw.go @@ -279,8 +279,9 @@ var ( registerClassExW = user32.NewProc("RegisterClassExW") unregisterClassW = user32.NewProc("UnregisterClassW") - getModuleHandleW = kernel32.NewProc("GetModuleHandleW") - capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW") + getModuleHandleW = kernel32.NewProc("GetModuleHandleW") + capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW") + capGetDriverDescriptionW = avicap32.NewProc("capGetDriverDescriptionW") ) const ( @@ -481,3 +482,28 @@ func capCreateCaptureWindow(lpszWindowName string, dwStyle, x, y, width, height return syscall.Handle(ret), nil } + +// Devices returns the available capture devices. +func Devices() ([]DeviceInfo, error) { + var devices []DeviceInfo + + for i := 0; i < 10; i++ { + var name [128]uint16 + + ret, _, _ := capGetDriverDescriptionW.Call(uintptr(i), + uintptr(unsafe.Pointer(&name[0])), unsafe.Sizeof(name), + 0, 0) + if ret == 0 { + continue + } + + n := syscall.UTF16ToString(name[:]) + if n == "" { + continue + } + + devices = append(devices, DeviceInfo{Index: i, Name: n}) + } + + return devices, nil +} diff --git a/cmd/cam2ip/main.go b/cmd/cam2ip/main.go index d46ef26..de2ce5d 100644 --- a/cmd/cam2ip/main.go +++ b/cmd/cam2ip/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "runtime/debug" + "strings" "go.senan.xyz/flagconf" @@ -44,6 +45,7 @@ func main() { srv := server.NewServer() flag.IntVar(&srv.Index, "index", 0, "Camera index [CAM2IP_INDEX]") + flag.StringVar(&srv.Device, "device", "", "Camera name to use, matched as substring, overrides index [CAM2IP_DEVICE]") flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds [CAM2IP_DELAY]") flag.Float64Var(&srv.Width, "width", 640, "Frame width [CAM2IP_WIDTH]") flag.Float64Var(&srv.Height, "height", 480, "Frame height [CAM2IP_HEIGHT]") @@ -59,12 +61,15 @@ func main() { var showVersion bool flag.BoolVar(&showVersion, "version", false, "Print version and exit") + var listDevices bool + flag.BoolVar(&listDevices, "list-devices", false, "List available cameras and exit") + flag.Usage = func() { color := useColor(os.Stderr) stderr("%s %s []\n", colorize(color, colorBold, "Usage:"), name) - order := []string{"index", "delay", "width", "height", "quality", "rotate", "flip", "no-webgl", - "timestamp", "time-format", "bind-addr", "htpasswd-file", "version"} + order := []string{"index", "device", "delay", "width", "height", "quality", "rotate", "flip", "no-webgl", + "timestamp", "time-format", "bind-addr", "htpasswd-file", "list-devices", "version"} for _, name := range order { f := flag.Lookup(name) @@ -82,6 +87,30 @@ func main() { os.Exit(0) } + if listDevices { + devices, err := camera.Devices() + if err != nil { + stderr("%s\n", err.Error()) + os.Exit(1) + } + + for _, d := range devices { + fmt.Printf("%d: %s\n", d.Index, d.Name) + } + + os.Exit(0) + } + + if srv.Device != "" { + index, err := deviceIndex(srv.Device) + if err != nil { + stderr("%s\n", err.Error()) + os.Exit(1) + } + + srv.Index = index + } + srv.Name = name srv.Version = version @@ -123,6 +152,23 @@ func stderr(format string, a ...any) { _, _ = fmt.Fprintf(os.Stderr, format, a...) } +// deviceIndex returns the index of the first camera whose name contains the query. +func deviceIndex(name string) (int, error) { + devices, err := camera.Devices() + if err != nil { + return 0, err + } + + want := strings.ToLower(name) + for _, d := range devices { + if strings.Contains(strings.ToLower(d.Name), want) { + return d.Index, nil + } + } + + return 0, fmt.Errorf("camera: no device matching %q", name) +} + const ( colorBold = "\033[1m" colorCyan = "\033[36m" diff --git a/go.mod b/go.mod index 4ec5988..2eb21d5 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d go.senan.xyz/flagconf v0.1.11 - gocv.io/x/gocv v0.43.0 ) require ( diff --git a/go.sum b/go.sum index cad766d..a079c8b 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= go.senan.xyz/flagconf v0.1.11 h1:ApA9DpoSrfVQlLt8spKJC3fOT7oTEZ2/MmBGMnb9mt4= go.senan.xyz/flagconf v0.1.11/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ= -gocv.io/x/gocv v0.43.0 h1:PFNpRUcV8fgBRDbVHHN+4BDZjjPnVveo5N/+e15BTuA= -gocv.io/x/gocv v0.43.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= diff --git a/server/server.go b/server/server.go index a78d314..4017986 100644 --- a/server/server.go +++ b/server/server.go @@ -17,8 +17,9 @@ type Server struct { Name string Version string - Index int - Delay int + Index int + Device string + Delay int Width float64 Height float64