@@ -4,6 +4,7 @@ package videosource
44import (
55 "context"
66 "fmt"
7+ "image"
78 "path/filepath"
89 "strings"
910 "sync"
4344// as equally acceptable. See https://github.com/pion/mediadevices/blob/c10fb000dbbb28597e068468f3175dc68a281bfd/pkg/prop/int.go#L104
4445// Setting it to 1 could theoretically allow 1x1 resolutions. 2 is small enough and even,
4546// allowing all real camera resolutions while ensuring proper distance calculations.
46- const minResolutionDimension = 2
47+ const (
48+ minResolutionDimension = 2
49+ defaultFrameRate = float32 (30.0 )
50+ )
4751
4852func init () {
4953 resource .RegisterComponent (
@@ -54,6 +58,15 @@ func init() {
5458 })
5559}
5660
61+ // WebcamBuffer is a buffer for webcam frames.
62+ // WARNING: This struct is NOT thread safe. It must be protected by the mutex in the webcam struct.
63+ type WebcamBuffer struct {
64+ frame image.Image // Holds the frames and their release functions in the buffer
65+ release func ()
66+ err error
67+ worker * goutils.StoppableWorkers // A separate worker for the webcam buffer that allows stronger concurrency control.
68+ }
69+
5770// WebcamConfig is the native config attribute struct for webcams.
5871type WebcamConfig struct {
5972 CameraParameters * transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"`
@@ -199,6 +212,8 @@ type webcam struct {
199212 disconnected bool
200213 logger logging.Logger
201214 workers * goutils.StoppableWorkers
215+
216+ buffer * WebcamBuffer
202217}
203218
204219// NewWebcam returns the webcam discovered based on the given config as the Camera interface type.
@@ -213,6 +228,8 @@ func NewWebcam(
213228 logger : logger .WithFields ("camera_name" , conf .ResourceName ().ShortName ()),
214229 workers : goutils .NewBackgroundStoppableWorkers (),
215230 }
231+ cam .buffer = NewWebcamBuffer (cam .workers .Context ())
232+
216233 if err := cam .Reconfigure (ctx , deps , conf ); err != nil {
217234 return nil , err
218235 }
@@ -230,12 +247,12 @@ func (c *webcam) Reconfigure(
230247 if err != nil {
231248 return err
232249 }
233-
250+ c . buffer . worker . Stop () // Calling this before locking shuts down the goroutines, and allows stopBuffer() to handle rest of the shutdown.
234251 c .mu .Lock ()
235252 defer c .mu .Unlock ()
253+ c .buffer .stopBuffer ()
236254
237255 c .cameraModel = camera .NewPinholeModelWithBrownConradyDistortion (newConf .CameraParameters , newConf .DistortionParameters )
238-
239256 driverReinitNotNeeded := c .conf .Format == newConf .Format &&
240257 c .conf .Path == newConf .Path &&
241258 c .conf .Width == newConf .Width &&
@@ -256,6 +273,13 @@ func (c *webcam) Reconfigure(
256273
257274 // only set once we're good
258275 c .conf = * newConf
276+
277+ if c .conf .FrameRate == 0.0 {
278+ c .conf .FrameRate = defaultFrameRate
279+ }
280+ c .buffer = NewWebcamBuffer (c .workers .Context ())
281+ c .startBuffer ()
282+
259283 return nil
260284}
261285
@@ -371,15 +395,11 @@ func (c *webcam) Images(ctx context.Context) ([]camera.NamedImage, resource.Resp
371395 return nil , resource.ResponseMetadata {}, err
372396 }
373397
374- img , release , err := c .reader . Read ()
398+ img , err := c .getLatestFrame ()
375399 if err != nil {
376400 return nil , resource.ResponseMetadata {}, errors .Wrap (err , "monitoredWebcam: call to get Images failed" )
377401 }
378- defer func () {
379- if release != nil {
380- release ()
381- }
382- }()
402+
383403 namedImg , err := camera .NamedImageFromImage (img , c .Name ().Name , utils .MimeTypeJPEG )
384404 if err != nil {
385405 return nil , resource.ResponseMetadata {}, err
@@ -408,11 +428,10 @@ func (c *webcam) Image(ctx context.Context, mimeType string, extra map[string]in
408428 if c .reader == nil {
409429 return nil , camera.ImageMetadata {}, errors .New ("underlying reader is nil" )
410430 }
411- img , release , err := c .reader . Read ()
431+ img , err := c .getLatestFrame ()
412432 if err != nil {
413433 return nil , camera.ImageMetadata {}, err
414434 }
415- defer release ()
416435
417436 if mimeType == "" {
418437 mimeType = utils .MimeTypeJPEG
@@ -476,15 +495,84 @@ func (c *webcam) Geometries(ctx context.Context, extra map[string]interface{}) (
476495 return make ([]spatialmath.Geometry , 0 ), nil
477496}
478497
498+ // NewWebcamBuffer creates a new WebcamBuffer struct.
499+ func NewWebcamBuffer (ctx context.Context ) * WebcamBuffer {
500+ return & WebcamBuffer {
501+ worker : goutils .NewStoppableWorkers (ctx ),
502+ }
503+ }
504+
505+ // Must lock the mutex before calling this function.
506+ func (c * webcam ) getLatestFrame () (image.Image , error ) {
507+ if c .buffer .frame == nil {
508+ if c .buffer .err != nil {
509+ return nil , c .buffer .err
510+ }
511+ return nil , errors .New ("no frames available to read" )
512+ }
513+
514+ return c .buffer .frame , nil
515+ }
516+
517+ func (c * webcam ) startBuffer () {
518+ if c .buffer .frame != nil {
519+ return // webcam buffer already started
520+ }
521+
522+ interFrameDuration := time .Duration (float32 (time .Second ) / c .conf .FrameRate )
523+ ticker := time .NewTicker (interFrameDuration )
524+ c .buffer .worker .Add (func (closedCtx context.Context ) {
525+ defer ticker .Stop ()
526+ for {
527+ select {
528+ case <- closedCtx .Done ():
529+ return
530+ case <- ticker .C :
531+ // We must unlock the mutex even if the release() or read() functions panic.
532+ func () {
533+ c .mu .Lock ()
534+ defer c .mu .Unlock ()
535+ if c .buffer .release != nil {
536+ c .buffer .release ()
537+ c .buffer .release = nil
538+ c .buffer .frame = nil
539+ }
540+ img , release , err := c .reader .Read ()
541+ c .buffer .err = err
542+ if err != nil {
543+ c .logger .Errorf ("error reading frame: %v" , err )
544+ return // next iteration of for loop
545+ }
546+ c .buffer .frame = img
547+ c .buffer .release = release
548+ }()
549+ }
550+ }
551+ })
552+ }
553+
554+ // Must lock the mutex before using this function.
555+ func (buffer * WebcamBuffer ) stopBuffer () {
556+ if buffer == nil {
557+ return
558+ }
559+
560+ // Release the remaining frame.
561+ if buffer .release != nil {
562+ buffer .release ()
563+ buffer .release = nil
564+ }
565+ }
566+
479567func (c * webcam ) Close (ctx context.Context ) error {
568+ c .workers .Stop ()
569+ c .buffer .worker .Stop ()
480570 c .mu .Lock ()
571+ defer c .mu .Unlock ()
481572 if c .closed {
482- c .mu .Unlock ()
483573 return errors .New ("webcam already closed" )
484574 }
485575 c .closed = true
486- c .mu .Unlock ()
487- c .workers .Stop ()
488576
489577 return c .driver .Close ()
490578}
0 commit comments