mirror of
https://github.com/gen2brain/cam2ip.git
synced 2026-07-26 23:05:06 +00:00
Add device enumeration
This commit is contained in:
@@ -40,6 +40,8 @@ This command will install `cam2ip` in `GOBIN`, you can point `GOBIN` to e.g. `/u
|
||||
Usage: cam2ip [<flags>]
|
||||
--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 [<flags>]
|
||||
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")
|
||||
```
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 [<flags>]\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"
|
||||
|
||||
1
go.mod
1
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 (
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user