diff --git a/components/camera/videosource/observer_darwin.go b/components/camera/videosource/observer_darwin.go new file mode 100644 index 00000000000..6ce5c3646b8 --- /dev/null +++ b/components/camera/videosource/observer_darwin.go @@ -0,0 +1,18 @@ +//go:build darwin + +package videosource + +import ( + mediadevicescamera "github.com/pion/mediadevices/pkg/driver/camera" + + "go.viam.com/rdk/logging" +) + +// startCameraObserver starts the Darwin camera device observer for hot-plug support. +// This should be called after SetupObserver has been called from the main thread. +// startCameraObserver is idempotent and can safely be called multiple times. +func startCameraObserver(logger logging.Logger) { + if err := mediadevicescamera.StartObserver(); err != nil { + logger.Errorw("failed to start darwin mediadevices camera observer; webcams will not handle hot unplug/replug", "error", err) + } +} diff --git a/components/camera/videosource/observer_stub.go b/components/camera/videosource/observer_stub.go new file mode 100644 index 00000000000..0eafc58113c --- /dev/null +++ b/components/camera/videosource/observer_stub.go @@ -0,0 +1,9 @@ +//go:build !darwin + +package videosource + +import "go.viam.com/rdk/logging" + +// startCameraObserver is a no-op on non-Darwin platforms. +// Camera hot-plug detection via AVFoundation observers is only supported on macOS. +func startCameraObserver(_ logging.Logger) {} diff --git a/components/camera/videosource/webcam.go b/components/camera/videosource/webcam.go index 744606ac7e4..ed50a4a1526 100644 --- a/components/camera/videosource/webcam.go +++ b/components/camera/videosource/webcam.go @@ -132,6 +132,13 @@ func NewWebcam( conf resource.Config, logger logging.Logger, ) (camera.Camera, error) { + // Start camera observer for hot-plug support (darwin only, no-op on other platforms). + // SetupObserver and DestroyObserver are called in RDK's entrypoint main.go to satisfy + // AVFoundation's threading requirements. startCameraObserver is idempotent and safely + // starts the observer for this component. + // See web/cmd/server/observer_darwin.go for details on threading. + startCameraObserver(logger) + c := &webcam{ Named: conf.ResourceName().AsNamed(), logger: logger.WithFields("camera_name", conf.ResourceName().ShortName()), diff --git a/go.mod b/go.mod index d84fab010bd..4de8dcce7c2 100644 --- a/go.mod +++ b/go.mod @@ -65,10 +65,10 @@ require ( github.com/muesli/kmeans v0.3.1 github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/pion/interceptor v0.1.41 + github.com/pion/interceptor v0.1.42 github.com/pion/logging v0.2.4 - github.com/pion/mediadevices v0.8.0 - github.com/pion/rtp v1.8.25 + github.com/pion/mediadevices v0.9.0 + github.com/pion/rtp v1.8.26 github.com/pion/stun v0.6.1 github.com/prometheus/procfs v0.15.1 github.com/pterm/pterm v0.12.82 @@ -266,22 +266,22 @@ require ( github.com/pierrec/lz4 v2.0.5+incompatible // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.0.7 // indirect - github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/dtls/v3 v3.0.8 // indirect + github.com/pion/ice/v4 v4.0.13 // indirect github.com/pion/mdns v0.0.12 // indirect - github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/mdns/v2 v2.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.16 // indirect - github.com/pion/sctp v1.8.40 // indirect + github.com/pion/sctp v1.8.41 // indirect github.com/pion/sdp/v3 v3.0.16 // indirect github.com/pion/srtp/v2 v2.0.20 // indirect - github.com/pion/srtp/v3 v3.0.8 // indirect - github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/srtp/v3 v3.0.9 // indirect + github.com/pion/stun/v3 v3.0.2 // indirect github.com/pion/transport/v2 v2.2.10 // indirect - github.com/pion/transport/v3 v3.0.8 // indirect + github.com/pion/transport/v3 v3.1.1 // indirect github.com/pion/turn/v2 v2.1.6 // indirect - github.com/pion/turn/v4 v4.1.1 // indirect - github.com/pion/webrtc/v4 v4.1.6 // indirect + github.com/pion/turn/v4 v4.1.3 // indirect + github.com/pion/webrtc/v4 v4.1.8 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/profile v1.7.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect diff --git a/go.sum b/go.sum index 5a3612799fe..25f7e5eb063 100644 --- a/go.sum +++ b/go.sum @@ -805,54 +805,54 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= -github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= -github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= -github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw= -github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY= +github.com/pion/dtls/v3 v3.0.8 h1:ZrPUrvPVDaTJDM8Vu1veatzXebLlsIWeT7Vaate/zwM= +github.com/pion/dtls/v3 v3.0.8/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os= +github.com/pion/ice/v4 v4.0.13 h1:1cdmd80gmLdnVTM2bXzw2CBebvXvkGNEaWi/CuDK9WQ= +github.com/pion/ice/v4 v4.0.13/go.mod h1:Xo5f5DBbEjQac+6pR7i83AGuwoGxnxwXkOOvHFVnfnM= +github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ= +github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= -github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= -github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= -github.com/pion/mediadevices v0.8.0 h1:EcfJWv97nOEg0TRshilSwJIoRuJ/eyLNZaDFj14RDGg= -github.com/pion/mediadevices v0.8.0/go.mod h1:nJiX2bpJJDqKwaIB22HV1K3Nk4nAk2petUD240yKfVA= +github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= +github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= +github.com/pion/mediadevices v0.9.0 h1:3ozqxVjN0PTbb1IfTbZ4af4R/fQ5cSlbYL3euCZag7w= +github.com/pion/mediadevices v0.9.0/go.mod h1:0dGJQq8VCPo7AXWmhqRITIFyw66uylwDecq7oN+G3gM= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.25 h1:b8+y44GNbwOJTYWuVan7SglX/hMlicVCAtL50ztyZHw= -github.com/pion/rtp v1.8.25/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= -github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8= -github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo= +github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= +github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= +github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs= +github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY= github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= -github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM= -github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg= +github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY= +github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU= +github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= -github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= -github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= -github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw= -github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU= +github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA= +github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A= +github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk= +github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/web/cmd/server/main.go b/web/cmd/server/main.go index 9885a85cc3a..7ccff0daa64 100644 --- a/web/cmd/server/main.go +++ b/web/cmd/server/main.go @@ -17,5 +17,10 @@ import ( var logger = logging.NewDebugLogger("entrypoint") func main() { + // Set up camera observer for hot-plug support (darwin only, no-op on other platforms). + // See server/observer_darwin.go for details on why this must be called from main(). + cleanup := setupCameraObserver(logger) + defer cleanup() + utils.ContextualMain(server.RunServer, logger) } diff --git a/web/cmd/server/observer_darwin.go b/web/cmd/server/observer_darwin.go new file mode 100644 index 00000000000..da954a32190 --- /dev/null +++ b/web/cmd/server/observer_darwin.go @@ -0,0 +1,33 @@ +//go:build darwin + +package main + +import ( + mediadevicescamera "github.com/pion/mediadevices/pkg/driver/camera" + + "go.viam.com/rdk/logging" +) + +// setupCameraObserver initializes the Darwin camera device observer for hot-plug support. +// +// On Darwin/macOS, SetupObserver must be called from the main.go because AVFoundation requires +// that camera device notification events and Key-Value Observation (KVO) updates occur on the main thread. +// The mediadevices library uses runtime.LockOSThread() to pin the background goroutine to whatever thread +// calls SetupObserver, so calling it in main() ensures that we run on the correct thread. +// +// This is why SetupObserver is called here rather than in the webcam component constructor: +// component constructors can be invoked from arbitrary goroutines, which would violate +// AVFoundation's threading requirements. StartObserver (called in webcam.go) can safely run from +// any thread after SetupObserver has been called. +// +// See: https://github.com/pion/mediadevices/pull/670 +func setupCameraObserver(logger logging.Logger) func() { + if err := mediadevicescamera.SetupObserver(); err != nil { + logger.Errorw("failed to set up darwin mediadevices camera observer; webcams will not handle hot unplug/replug", "error", err) + } + return func() { + if err := mediadevicescamera.DestroyObserver(); err != nil { + logger.Errorw("failed to destroy darwin mediadevices camera observer", "error", err) + } + } +} diff --git a/web/cmd/server/observer_stub.go b/web/cmd/server/observer_stub.go new file mode 100644 index 00000000000..b52a6576215 --- /dev/null +++ b/web/cmd/server/observer_stub.go @@ -0,0 +1,13 @@ +//go:build !darwin + +package main + +import "go.viam.com/rdk/logging" + +// setupCameraObserver is a no-op on non-Darwin platforms. +// Camera hot-plug detection via AVFoundation observers is only supported on macOS. +// On Linux, poll-based device enumeration is used instead (checking /dev/video* files). +// On Windows, Media Foundation APIs handle device enumeration differently. +func setupCameraObserver(_ logging.Logger) func() { + return func() {} +}