(3/3) Fix concurrency webcam windows (#684)

* Import wmcodecdsp and try to use their nv12 const

* Remove ifndef

* Enhance getCameraName function to retrieve friendly name from IPropertyBag, with fallback to display name. Update LDFLAGS in camera_windows.go to include oleaut32 library.

* Don't mistake label for friendly name

* Initialize COM for thread safety in Open and VideoRecord methods of camera_windows.go

* Refactor camera handling in camera_windows.go and camera_windows.cpp

- Introduced mutex for thread safety in camera struct to protect shared resources.
- Updated Open method to initialize done channel and manage camera state.
- Enhanced Close method to ensure proper cleanup and prevent double closing.
- Improved resolution listing logic in camera_windows.cpp by ensuring media types are freed correctly.
- Changed memory deletion from single to array deletion for camera properties.

* Fix issues post-rebase

* Refactor Close method

* Transfer golang buffer management to c to avoid gc

* Fix race condition in imageCallback by fixing unlock code location
This commit is contained in:
sean yu
2026-04-21 09:25:31 -04:00
committed by GitHub
parent 63c09cb1f4
commit 3d4fceab3d
2 changed files with 87 additions and 23 deletions
+1 -2
View File
@@ -253,7 +253,6 @@ int listResolution(camera* cam, const char** errstr)
ICaptureGraphBuilder2* captureGraph = nullptr;
IAMStreamConfig* config = nullptr;
IPin* src = nullptr;
LPOLESTR name;
if (!selectCamera(cam, &moniker, errstr))
{
@@ -623,7 +622,7 @@ void freeCamera(camera* cam)
if (cam->props)
{
delete cam->props;
delete[] cam->props;
cam->props = nullptr;
}
}
+82 -17
View File
@@ -26,9 +26,15 @@ var (
type camera struct {
name string
// mu protects the fields under as per the mutex hat convention.
mu sync.Mutex
cam *C.camera
closed bool
ch chan []byte
buf []byte
done chan struct{}
cbuf unsafe.Pointer // C.malloc'd buffer for DirectShow writes
bufLen int // byte length of cbuf
bufGo []byte
}
@@ -83,7 +89,20 @@ func DestroyObserver() error {
}
func (c *camera) Open() error {
// COM is per-thread on Windows. Go goroutines can run on any OS thread,
// so ensure COM is initialized on the current one before making COM calls.
C.CoInitializeEx(nil, C.COINIT_MULTITHREADED)
c.mu.Lock()
defer c.mu.Unlock()
if c.cam != nil {
return fmt.Errorf("camera already open")
}
c.ch = make(chan []byte)
c.done = make(chan struct{})
c.closed = false
c.cam = &C.camera{
name: C.CString(c.name),
}
@@ -91,6 +110,7 @@ func (c *camera) Open() error {
var errStr *C.char
if C.listResolution(c.cam, &errStr) != 0 {
C.free(unsafe.Pointer(c.cam.name))
c.cam = nil
return fmt.Errorf("failed to open device: %s", C.GoString(errStr))
}
@@ -101,36 +121,74 @@ func (c *camera) Open() error {
func imageCallback(cam uintptr) {
callbacksMu.RLock()
cb, ok := callbacks[uintptr(unsafe.Pointer(cam))]
callbacksMu.RUnlock()
if !ok {
callbacksMu.RUnlock()
return
}
copy(cb.bufGo, unsafe.Slice((*byte)(cb.cbuf), cb.bufLen))
callbacksMu.RUnlock()
copy(cb.bufGo, cb.buf)
cb.ch <- cb.bufGo
select {
case cb.ch <- cb.bufGo:
case <-cb.done:
}
}
func (c *camera) Close() error {
callbacksMu.Lock()
key := uintptr(unsafe.Pointer(c.cam))
if _, ok := callbacks[key]; ok {
delete(callbacks, key)
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
callbacksMu.Unlock()
close(c.ch)
if c.cam != nil {
C.free(unsafe.Pointer(c.cam.name))
C.freeCamera(c.cam)
c.closed = true
cam := c.cam
c.cam = nil
cbuf := c.cbuf
c.cbuf = nil
done := c.done
ch := c.ch
c.mu.Unlock()
// Remove from callbacks map so no new imageCallback calls find this cam
callbacksMu.Lock()
delete(callbacks, uintptr(unsafe.Pointer(cam)))
callbacksMu.Unlock()
if done != nil {
close(done)
}
if cam != nil {
C.free(unsafe.Pointer(cam.name))
C.freeCamera(cam)
}
C.free(cbuf)
if ch != nil {
close(ch)
}
return nil
}
func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
C.CoInitializeEx(nil, C.COINIT_MULTITHREADED)
c.mu.Lock()
defer c.mu.Unlock()
if c.cam == nil {
return nil, fmt.Errorf("camera not open")
}
nPix := p.Width * p.Height
c.buf = make([]byte, nPix*2)
c.bufGo = make([]byte, nPix*2)
bufSize := nPix * 2
c.cbuf = C.malloc(C.size_t(bufSize))
if c.cbuf == nil {
return nil, fmt.Errorf("failed to allocate frame buffer")
}
c.bufLen = bufSize
c.bufGo = make([]byte, bufSize)
c.cam.width = C.int(p.Width)
c.cam.height = C.int(p.Height)
@@ -141,10 +199,12 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
c.cam.fcc = fourccYUY2
}
c.cam.buf = C.size_t(uintptr(unsafe.Pointer(&c.buf[0])))
c.cam.buf = C.size_t(uintptr(c.cbuf))
var errStr *C.char
if C.openCamera(c.cam, &errStr) != 0 {
C.free(c.cbuf)
c.cbuf = nil
return nil, fmt.Errorf("failed to open device: %s", C.GoString(errStr))
}
@@ -184,6 +244,11 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
}
func (c *camera) Properties() []prop.Media {
c.mu.Lock()
defer c.mu.Unlock()
if c.cam == nil {
return nil
}
properties := []prop.Media{}
for i := 0; i < int(c.cam.numProps); i++ {
p := C.getProp(c.cam, C.int(i))