//go:build !opencv && !android // Package camera. package camera import ( "fmt" "image" "io" "slices" "github.com/korandiz/v4l" im "github.com/gen2brain/cam2ip/image" ) // supportedFormats lists the formats the camera can decode, in order of preference. var supportedFormats = []uint32{ mjpgFourCC, jpegFourCC, yuyvFourCC, uyvyFourCC, yvyuFourCC, vyuyFourCC, nv12FourCC, yu12FourCC, yv12FourCC, rgb24FourCC, bgr24FourCC, greyFourCC, } // Camera represents camera. type Camera struct { opts Options camera *v4l.Device config v4l.DeviceConfig ycbcr *image.YCbCr rgba *image.RGBA gray *image.Gray } // New returns new Camera for given camera index. func New(opts Options) (c *Camera, err error) { c = &Camera{} c.opts = opts devices := v4l.FindDevices() if len(devices) < opts.Index+1 { err = fmt.Errorf("camera: no camera at index %d", opts.Index) return } c.camera, err = v4l.Open(devices[opts.Index].Path) if err != nil { err = fmt.Errorf("camera: %w", err) return } if c.camera == nil { err = fmt.Errorf("camera: can not open camera %d", opts.Index) return } configs, e := c.camera.ListConfigs() if e != nil { err = fmt.Errorf("camera: can not list configs: %w", e) return } formats := make([]uint32, 0) for _, config := range configs { formats = append(formats, config.Format) } c.config, err = c.camera.GetConfig() if err != nil { err = fmt.Errorf("camera: can not get config: %w", err) return } format, ok := selectFormat(formats) if !ok { err = fmt.Errorf("camera: no supported pixel format") return } c.config.Format = format c.config.Width = int(opts.Width) c.config.Height = int(opts.Height) err = c.camera.SetConfig(c.config) if err != nil { err = fmt.Errorf("camera: format %d: can not set config: %w", c.config.Format, err) return } rect := image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)) switch c.config.Format { case yuy2FourCC, yuyvFourCC, uyvyFourCC, yvyuFourCC, vyuyFourCC: c.ycbcr = image.NewYCbCr(rect, image.YCbCrSubsampleRatio422) case nv12FourCC, yu12FourCC, yv12FourCC: c.ycbcr = image.NewYCbCr(rect, image.YCbCrSubsampleRatio420) case rgb24FourCC, bgr24FourCC: c.rgba = image.NewRGBA(rect) case greyFourCC: c.gray = image.NewGray(rect) } err = c.camera.TurnOn() if err != nil { err = fmt.Errorf("camera: format %d: can not turn on: %w", c.config.Format, err) return } return } // selectFormat returns the first supported format the device offers. func selectFormat(formats []uint32) (uint32, bool) { for _, f := range supportedFormats { if slices.Contains(formats, f) { return f, true } } return 0, false } // Read reads next frame from camera and returns image. func (c *Camera) Read() (img image.Image, err error) { buffer, err := c.camera.Capture() if err != nil { err = fmt.Errorf("camera: format %d: can not grab frame: %w", c.config.Format, err) return } if c.config.Format == mjpgFourCC || c.config.Format == jpegFourCC { img, err = im.NewDecoder(buffer).Decode() if err != nil { err = fmt.Errorf("camera: format %d: can not decode frame: %w", c.config.Format, err) return } } else { data, e := io.ReadAll(buffer) if e != nil { err = fmt.Errorf("camera: format %d: can not read buffer: %w", c.config.Format, e) return } img, err = c.convert(data) if err != nil { err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.config.Format, err) return } } if c.opts.Rotate != 0 { img = im.Rotate(img, c.opts.Rotate) } if c.opts.Flip != "" { img = im.Flip(img, c.opts.Flip) } if c.opts.Timestamp { img = im.Timestamp(img, c.opts.TimeFormat) } return } // convert converts a raw frame in the negotiated format to an image. func (c *Camera) convert(data []byte) (image.Image, error) { switch c.config.Format { case yuy2FourCC, yuyvFourCC: return c.ycbcr, packedYUV422ToYCbCr(data, c.ycbcr, 0, 2, 1, 3) case uyvyFourCC: return c.ycbcr, packedYUV422ToYCbCr(data, c.ycbcr, 1, 3, 0, 2) case yvyuFourCC: return c.ycbcr, packedYUV422ToYCbCr(data, c.ycbcr, 0, 2, 3, 1) case vyuyFourCC: return c.ycbcr, packedYUV422ToYCbCr(data, c.ycbcr, 1, 3, 2, 0) case yu12FourCC: return c.ycbcr, planar420ToYCbCr(data, c.ycbcr, false) case yv12FourCC: return c.ycbcr, planar420ToYCbCr(data, c.ycbcr, true) case nv12FourCC: return c.ycbcr, nv12ToYCbCr(data, c.ycbcr) case rgb24FourCC: return c.rgba, rgb24ToRgba(data, c.rgba, false) case bgr24FourCC: return c.rgba, rgb24ToRgba(data, c.rgba, true) case greyFourCC: return c.gray, greyToGray(data, c.gray) } return nil, fmt.Errorf("unsupported format %d", c.config.Format) } // Close closes camera. func (c *Camera) Close() (err error) { if c.camera == nil { err = fmt.Errorf("camera: close: camera is not opened") return } c.camera.TurnOff() c.camera.Close() c.camera = nil return }