diff --git a/examples/go.sum b/examples/go.sum index b14340f..171498a 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,5 +1,7 @@ github.com/blackjack/webcam v0.0.0-20220329180758-ba064708e165 h1:QsIbRyO2tn5eSJZ/skuDqSTo0GWI5H4G1AT7Mm2H0Nw= github.com/blackjack/webcam v0.0.0-20220329180758-ba064708e165/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4= +github.com/blackjack/webcam v0.0.0-20230411204030-32744c21431f h1:qBxp6Oz8y0AfeqjrYcHaYdfWQf+vUXAwgZ+GWnTtd/E= +github.com/blackjack/webcam v0.0.0-20230411204030-32744c21431f/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -111,8 +113,11 @@ golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -130,6 +135,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -152,6 +158,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -165,10 +173,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/driver/availability/error.go b/pkg/driver/availability/error.go new file mode 100644 index 0000000..23683de --- /dev/null +++ b/pkg/driver/availability/error.go @@ -0,0 +1,25 @@ +package availability + +import ( + "errors" +) + +type Error interface { + Error() string +} + +var ErrUnimplemented Error = errors.New("not implemented") +var ErrBusy Error = errors.New("device or resource busy") +var ErrNoDevice Error = errors.New("no such device") + +type errorString struct { + s string +} + +func NewError(text string) Error { + return &errorString{text} +} + +func (e *errorString) Error() string { + return e.s +} diff --git a/pkg/driver/camera/camera_linux.go b/pkg/driver/camera/camera_linux.go index 09ebf74..e6ceebd 100644 --- a/pkg/driver/camera/camera_linux.go +++ b/pkg/driver/camera/camera_linux.go @@ -6,12 +6,14 @@ import "C" import ( "context" "errors" + "github.com/pion/mediadevices/pkg/driver/availability" "image" "io" "os" "path/filepath" "strconv" "sync" + "syscall" "time" "github.com/blackjack/webcam" @@ -350,3 +352,36 @@ func (c *camera) Properties() []prop.Media { } return properties } + +func (c *camera) IsAvailable() (bool, availability.Error) { + var err error + + // close the opened file descriptor as quickly as possible and in all cases, including panics + func() { + var cam *webcam.Webcam + if cam, err = webcam.Open(c.path); err == nil { + defer cam.Close() + var index int32 + // "Drivers must implement all the input ioctls when the device has one or more inputs..." + // Source: https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/video.html?highlight=vidioc_enuminput + if index, err = cam.GetInput(); err == nil { + err = cam.SelectInput(uint32(index)) + } + } + }() + + var errno syscall.Errno + errors.As(err, &errno) + + // See https://man7.org/linux/man-pages/man3/errno.3.html + switch { + case err == nil: + return true, nil + case errno == syscall.EBUSY: + return false, availability.ErrBusy + case errno == syscall.ENODEV || errno == syscall.ENOENT: + return false, availability.ErrNoDevice + default: + return false, availability.NewError(errno.Error()) + } +} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 9f9c33c..e6357b3 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -1,6 +1,7 @@ package driver import ( + "github.com/pion/mediadevices/pkg/driver/availability" "github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/prop" @@ -45,3 +46,14 @@ type Driver interface { Info() Info Status() State } + +type AvailabilityAdapter interface { + IsAvailable() (bool, availability.Error) +} + +func IsAvailable(d Driver) (bool, availability.Error) { + if aa, ok := d.(AvailabilityAdapter); ok { + return aa.IsAvailable() + } + return false, availability.ErrUnimplemented +} diff --git a/pkg/driver/wrapper.go b/pkg/driver/wrapper.go index 4defc76..8878b07 100644 --- a/pkg/driver/wrapper.go +++ b/pkg/driver/wrapper.go @@ -2,6 +2,7 @@ package driver import ( "github.com/google/uuid" + "github.com/pion/mediadevices/pkg/driver/availability" "github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/prop" @@ -21,6 +22,10 @@ func wrapAdapter(a Adapter, info Info) Driver { state: StateClosed, } + if aa, ok := a.(AvailabilityAdapter); ok { + d.isAvailable = aa.IsAvailable + } + switch v := a.(type) { case VideoRecorder: // Only expose Driver and VideoRecorder interfaces @@ -28,7 +33,8 @@ func wrapAdapter(a Adapter, info Info) Driver { r := &struct { Driver VideoRecorder - }{d, d} + AvailabilityAdapter + }{d, d, d} return r case AudioRecorder: // Only expose Driver and AudioRecorder interfaces @@ -36,7 +42,8 @@ func wrapAdapter(a Adapter, info Info) Driver { return &struct { Driver AudioRecorder - }{d, d} + AvailabilityAdapter + }{d, d, d} default: panic("adapter has to be either VideoRecorder/AudioRecorder") } @@ -46,9 +53,10 @@ type adapterWrapper struct { Adapter VideoRecorder AudioRecorder - id string - info Info - state State + id string + info Info + state State + isAvailable func() (bool, availability.Error) } func (w *adapterWrapper) ID() string { @@ -104,3 +112,10 @@ func (w *adapterWrapper) AudioRecord(p prop.Media) (r audio.Reader, err error) { } return } + +func (w *adapterWrapper) IsAvailable() (bool, availability.Error) { + if w.isAvailable == nil { + return false, availability.ErrUnimplemented + } + return w.isAvailable() +} diff --git a/pkg/driver/wrapper_test.go b/pkg/driver/wrapper_test.go index aceeec9..81baebd 100644 --- a/pkg/driver/wrapper_test.go +++ b/pkg/driver/wrapper_test.go @@ -2,6 +2,7 @@ package driver import ( "fmt" + "github.com/pion/mediadevices/pkg/driver/availability" "testing" "github.com/pion/mediadevices/pkg/io/audio" @@ -39,6 +40,10 @@ func (a *audioAdapterBrokenMock) AudioRecord(p prop.Media) (r audio.Reader, err return nil, recordErr } +type availabilityAdapterMock struct{ videoAdapterMock } + +func (a *availabilityAdapterMock) IsAvailable() (bool, availability.Error) { return true, nil } + func TestVideoWrapperState(t *testing.T) { var a videoAdapterMock d := wrapAdapter(&a, Info{}) @@ -136,3 +141,38 @@ func TestAudioWrapperWithBrokenRecorderState(t *testing.T) { t.Errorf("expected the status to be %v, but got %v", StateClosed, d.Status()) } } + +func TestWrapperAvailabilityAdapter(t *testing.T) { + var aa availabilityAdapterMock + d := wrapAdapter(&aa, Info{}) + + ok, err := IsAvailable(d) + if err != nil { + t.Errorf("expected nil, but got %v", err) + } + if !ok { + t.Errorf("expected true, but got %v", ok) + } + + var v videoAdapterMock + d = wrapAdapter(&v, Info{}) + + ok, err = IsAvailable(d) + if err == nil { + t.Errorf("expected err, but got %v", err) + } + if ok { + t.Errorf("expected false, but got %v", ok) + } + + var a audioAdapterMock + d = wrapAdapter(&a, Info{}) + + ok, err = IsAvailable(d) + if err == nil { + t.Errorf("expected err, but got %v", err) + } + if ok { + t.Errorf("expected false, but got %v", ok) + } +}