mirror of
https://github.com/pion/mediadevices.git
synced 2025-10-04 08:16:33 +08:00

Some cameras support a range of resolutions with step wise. The fix is to not only capture the highest resolutions but uses the step wise to determine if we can support the hardcoded standard resolutions, https://commons.wikimedia.org/wiki/File:Vector_Video_Standards2.svg. In the future, we should use a custom data structure to capture more resolutions that are outside of the listed standard resolutions.
284 lines
6.6 KiB
Go
284 lines
6.6 KiB
Go
package camera
|
|
|
|
// #include <linux/videodev2.h>
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"image"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/blackjack/webcam"
|
|
"github.com/pion/mediadevices/pkg/driver"
|
|
"github.com/pion/mediadevices/pkg/frame"
|
|
"github.com/pion/mediadevices/pkg/io/video"
|
|
"github.com/pion/mediadevices/pkg/prop"
|
|
)
|
|
|
|
const (
|
|
maxEmptyFrameCount = 5
|
|
)
|
|
|
|
var (
|
|
errReadTimeout = errors.New("read timeout")
|
|
errEmptyFrame = errors.New("empty frame")
|
|
// Reference: https://commons.wikimedia.org/wiki/File:Vector_Video_Standards2.svg
|
|
supportedResolutions = [][2]int{
|
|
{320, 240},
|
|
{640, 480},
|
|
{768, 576},
|
|
{800, 600},
|
|
{1024, 768},
|
|
{1280, 854},
|
|
{1280, 960},
|
|
{1280, 1024},
|
|
{1400, 1050},
|
|
{1600, 1200},
|
|
{2048, 1536},
|
|
{320, 200},
|
|
{800, 480},
|
|
{854, 480},
|
|
{1024, 600},
|
|
{1152, 768},
|
|
{1280, 720},
|
|
{1280, 768},
|
|
{1366, 768},
|
|
{1280, 800},
|
|
{1440, 900},
|
|
{1440, 960},
|
|
{1680, 1050},
|
|
{1920, 1080},
|
|
{2048, 1080},
|
|
{1920, 1200},
|
|
{2560, 1600},
|
|
}
|
|
)
|
|
|
|
// Camera implementation using v4l2
|
|
// Reference: https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/videodev.html#videodev
|
|
type camera struct {
|
|
path string
|
|
cam *webcam.Webcam
|
|
formats map[webcam.PixelFormat]frame.Format
|
|
reversedFormats map[frame.Format]webcam.PixelFormat
|
|
started bool
|
|
mutex sync.Mutex
|
|
cancel func()
|
|
}
|
|
|
|
func init() {
|
|
discovered := make(map[string]struct{})
|
|
|
|
discover := func(pattern string) {
|
|
devices, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
// No v4l device.
|
|
return
|
|
}
|
|
for _, device := range devices {
|
|
label := filepath.Base(device)
|
|
reallink, err := os.Readlink(device)
|
|
if err != nil {
|
|
reallink = label
|
|
} else {
|
|
reallink = filepath.Base(reallink)
|
|
}
|
|
|
|
if _, ok := discovered[reallink]; ok {
|
|
continue
|
|
}
|
|
|
|
discovered[reallink] = struct{}{}
|
|
cam := newCamera(device)
|
|
driver.GetManager().Register(cam, driver.Info{
|
|
Label: label,
|
|
DeviceType: driver.Camera,
|
|
})
|
|
}
|
|
}
|
|
|
|
discover("/dev/v4l/by-path/*")
|
|
discover("/dev/video*")
|
|
}
|
|
|
|
func newCamera(path string) *camera {
|
|
formats := map[webcam.PixelFormat]frame.Format{
|
|
webcam.PixelFormat(C.V4L2_PIX_FMT_YUV420): frame.FormatI420,
|
|
webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV): frame.FormatYUYV,
|
|
webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY): frame.FormatUYVY,
|
|
webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV21,
|
|
webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG,
|
|
}
|
|
|
|
reversedFormats := make(map[frame.Format]webcam.PixelFormat)
|
|
for k, v := range formats {
|
|
reversedFormats[v] = k
|
|
}
|
|
|
|
c := &camera{
|
|
path: path,
|
|
formats: formats,
|
|
reversedFormats: reversedFormats,
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (c *camera) Open() error {
|
|
cam, err := webcam.Open(c.path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Late frames should be discarded. Buffering should be handled in higher level.
|
|
cam.SetBufferCount(1)
|
|
c.cam = cam
|
|
return nil
|
|
}
|
|
|
|
func (c *camera) Close() error {
|
|
if c.cam == nil {
|
|
return nil
|
|
}
|
|
|
|
if c.cancel != nil {
|
|
// Let the reader knows that the caller has closed the camera
|
|
c.cancel()
|
|
// Wait until the reader unref the buffer
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
// Note: StopStreaming frees frame buffers even if they are still used in Go code.
|
|
// There is currently no convenient way to do this safely.
|
|
// So, consumer of this stream must close camera after unusing all images.
|
|
c.cam.StopStreaming()
|
|
c.cancel = nil
|
|
}
|
|
c.cam.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
|
decoder, err := frame.NewDecoder(p.FrameFormat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pf := c.reversedFormats[p.FrameFormat]
|
|
_, _, _, err = c.cam.SetImageFormat(pf, uint32(p.Width), uint32(p.Height))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.cam.StartStreaming(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cam := c.cam
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
c.cancel = cancel
|
|
var buf []byte
|
|
r := video.ReaderFunc(func() (img image.Image, err error) {
|
|
// Lock to avoid accessing the buffer after StopStreaming()
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
// Wait until a frame is ready
|
|
for i := 0; i < maxEmptyFrameCount; i++ {
|
|
if ctx.Err() != nil {
|
|
// Return EOF if the camera is already closed.
|
|
return nil, io.EOF
|
|
}
|
|
|
|
err := cam.WaitForFrame(5) // 5 seconds
|
|
switch err.(type) {
|
|
case nil:
|
|
case *webcam.Timeout:
|
|
return nil, errReadTimeout
|
|
default:
|
|
// Camera has been stopped.
|
|
return nil, err
|
|
}
|
|
|
|
b, err := cam.ReadFrame()
|
|
if err != nil {
|
|
// Camera has been stopped.
|
|
return nil, err
|
|
}
|
|
|
|
// Frame is empty.
|
|
// Retry reading and return errEmptyFrame if it exceeds maxEmptyFrameCount.
|
|
if len(b) == 0 {
|
|
continue
|
|
}
|
|
|
|
if len(b) > len(buf) {
|
|
// Grow the intermediate buffer
|
|
buf = make([]byte, len(b))
|
|
}
|
|
|
|
// move the memory from mmap to Go. This will guarantee that any data that's going out
|
|
// from this reader will be Go safe. Otherwise, it's possible that outside of this reader
|
|
// that this memory is still being used even after we close it.
|
|
n := copy(buf, b)
|
|
return decoder.Decode(buf[:n], p.Width, p.Height)
|
|
}
|
|
return nil, errEmptyFrame
|
|
})
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (c *camera) Properties() []prop.Media {
|
|
properties := make([]prop.Media, 0)
|
|
for format := range c.cam.GetSupportedFormats() {
|
|
for _, frameSize := range c.cam.GetSupportedFrameSizes(format) {
|
|
supportedFormat, ok := c.formats[format]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if frameSize.StepWidth == 0 || frameSize.StepHeight == 0 {
|
|
properties = append(properties, prop.Media{
|
|
Video: prop.Video{
|
|
Width: int(frameSize.MaxWidth),
|
|
Height: int(frameSize.MaxHeight),
|
|
FrameFormat: supportedFormat,
|
|
},
|
|
})
|
|
} else {
|
|
// FIXME: we should probably use a custom data structure to capture all of the supported resolutions
|
|
for _, supportedResolution := range supportedResolutions {
|
|
minWidth, minHeight := int(frameSize.MinWidth), int(frameSize.MinHeight)
|
|
maxWidth, maxHeight := int(frameSize.MaxWidth), int(frameSize.MaxHeight)
|
|
stepWidth, stepHeight := int(frameSize.StepWidth), int(frameSize.StepHeight)
|
|
width, height := supportedResolution[0], supportedResolution[1]
|
|
|
|
if width < minWidth || width > maxWidth ||
|
|
height < minHeight || height > maxHeight {
|
|
continue
|
|
}
|
|
|
|
if (width-minWidth)%stepWidth != 0 ||
|
|
(height-minHeight)%stepHeight != 0 {
|
|
continue
|
|
}
|
|
|
|
properties = append(properties, prop.Media{
|
|
Video: prop.Video{
|
|
Width: width,
|
|
Height: height,
|
|
FrameFormat: supportedFormat,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return properties
|
|
}
|