|
| 1 | +package videosource |
| 2 | + |
| 3 | +import ( |
| 4 | + "math" |
| 5 | + "strings" |
| 6 | + "time" |
| 7 | + |
| 8 | + "github.com/pion/mediadevices" |
| 9 | + "github.com/pion/mediadevices/pkg/driver" |
| 10 | + "github.com/pion/mediadevices/pkg/driver/availability" |
| 11 | + "github.com/pion/mediadevices/pkg/driver/camera" |
| 12 | + "github.com/pion/mediadevices/pkg/io/video" |
| 13 | + "github.com/pion/mediadevices/pkg/prop" |
| 14 | + "github.com/pkg/errors" |
| 15 | + |
| 16 | + "go.viam.com/rdk/logging" |
| 17 | +) |
| 18 | + |
| 19 | +// Below is adapted from github.com/pion/mediadevices. |
| 20 | +// It is further adapted from gostream's query.go |
| 21 | +// However, this is the minimum code needed for webcam to work, placed in this directory. |
| 22 | +// This vastly improves the debugging and feature development experience, by not over-DRY-ing. |
| 23 | + |
| 24 | +// GetNamedVideoSource attempts to find a device (not a screen) by the given name. |
| 25 | +// If name is empty, it finds any device. |
| 26 | +func getReaderAndDriver( |
| 27 | + name string, |
| 28 | + constraints mediadevices.MediaStreamConstraints, |
| 29 | + logger logging.Logger, |
| 30 | +) (video.Reader, driver.Driver, error) { |
| 31 | + var ptr *string |
| 32 | + if name == "" { |
| 33 | + ptr = nil |
| 34 | + } else { |
| 35 | + ptr = &name |
| 36 | + } |
| 37 | + d, selectedMedia, err := getUserVideoDriver(constraints, ptr, logger) |
| 38 | + if err != nil { |
| 39 | + return nil, nil, err |
| 40 | + } |
| 41 | + reader, err := newReaderFromDriver(d, selectedMedia) |
| 42 | + if err != nil { |
| 43 | + return nil, nil, err |
| 44 | + } |
| 45 | + return reader, d, nil |
| 46 | +} |
| 47 | + |
| 48 | +func getUserVideoDriver( |
| 49 | + constraints mediadevices.MediaStreamConstraints, |
| 50 | + label *string, |
| 51 | + logger logging.Logger, |
| 52 | +) (driver.Driver, prop.Media, error) { |
| 53 | + var videoConstraints mediadevices.MediaTrackConstraints |
| 54 | + if constraints.Video != nil { |
| 55 | + constraints.Video(&videoConstraints) |
| 56 | + } |
| 57 | + return selectVideo(videoConstraints, label, logger) |
| 58 | +} |
| 59 | + |
| 60 | +func newReaderFromDriver( |
| 61 | + videoDriver driver.Driver, |
| 62 | + mediaProp prop.Media, |
| 63 | +) (video.Reader, error) { |
| 64 | + recorder, ok := videoDriver.(driver.VideoRecorder) |
| 65 | + if !ok { |
| 66 | + return nil, errors.New("driver not a driver.VideoRecorder") |
| 67 | + } |
| 68 | + |
| 69 | + if ok, err := driver.IsAvailable(videoDriver); !errors.Is(err, availability.ErrUnimplemented) && !ok { |
| 70 | + return nil, errors.Wrap(err, "video driver not available") |
| 71 | + } else if driverStatus := videoDriver.Status(); driverStatus != driver.StateClosed { |
| 72 | + return nil, errors.New("video driver in use") |
| 73 | + } else if err := videoDriver.Open(); err != nil { |
| 74 | + return nil, errors.Wrap(err, "cannot open video driver") |
| 75 | + } |
| 76 | + |
| 77 | + mediaProp.DiscardFramesOlderThan = time.Second |
| 78 | + reader, err := recorder.VideoRecord(mediaProp) |
| 79 | + if err != nil { |
| 80 | + return nil, err |
| 81 | + } |
| 82 | + return reader, nil |
| 83 | +} |
| 84 | + |
| 85 | +func labelFilter(target string, useSep bool) driver.FilterFn { |
| 86 | + return driver.FilterFn(func(d driver.Driver) bool { |
| 87 | + if !useSep { |
| 88 | + return d.Info().Label == target |
| 89 | + } |
| 90 | + labels := strings.Split(d.Info().Label, camera.LabelSeparator) |
| 91 | + for _, label := range labels { |
| 92 | + if label == target { |
| 93 | + return true |
| 94 | + } |
| 95 | + } |
| 96 | + return false |
| 97 | + }) |
| 98 | +} |
| 99 | + |
| 100 | +func selectVideo( |
| 101 | + constraints mediadevices.MediaTrackConstraints, |
| 102 | + label *string, |
| 103 | + logger logging.Logger, |
| 104 | +) (driver.Driver, prop.Media, error) { |
| 105 | + return selectBestDriver(getVideoFilterBase(), getVideoFilter(label), constraints, logger) |
| 106 | +} |
| 107 | + |
| 108 | +func getVideoFilterBase() driver.FilterFn { |
| 109 | + typeFilter := driver.FilterVideoRecorder() |
| 110 | + notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen)) |
| 111 | + return driver.FilterAnd(typeFilter, notScreenFilter) |
| 112 | +} |
| 113 | + |
| 114 | +func getVideoFilter(label *string) driver.FilterFn { |
| 115 | + filter := getVideoFilterBase() |
| 116 | + if label != nil { |
| 117 | + filter = driver.FilterAnd(filter, labelFilter(*label, true)) |
| 118 | + } |
| 119 | + return filter |
| 120 | +} |
| 121 | + |
| 122 | +// select implements SelectSettings algorithm. |
| 123 | +// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings |
| 124 | +func selectBestDriver( |
| 125 | + baseFilter driver.FilterFn, |
| 126 | + filter driver.FilterFn, |
| 127 | + constraints mediadevices.MediaTrackConstraints, |
| 128 | + logger logging.Logger, |
| 129 | +) (driver.Driver, prop.Media, error) { |
| 130 | + var bestDriver driver.Driver |
| 131 | + var bestProp prop.Media |
| 132 | + minFitnessDist := math.Inf(1) |
| 133 | + |
| 134 | + baseDrivers := driver.GetManager().Query(baseFilter) |
| 135 | + logger.Debugw("before specific filter, we found the following drivers", "count", len(baseDrivers)) |
| 136 | + for _, d := range baseDrivers { |
| 137 | + logger.Debugw(d.Info().Label, "priority", float32(d.Info().Priority), "type", d.Info().DeviceType) |
| 138 | + } |
| 139 | + |
| 140 | + driverProperties := queryDriverProperties(filter, logger) |
| 141 | + if len(driverProperties) == 0 { |
| 142 | + logger.Debugw("found no drivers matching filter") |
| 143 | + } else { |
| 144 | + logger.Debugw("found drivers matching specific filter", "count", len(driverProperties)) |
| 145 | + } |
| 146 | + for d, props := range driverProperties { |
| 147 | + priority := float64(d.Info().Priority) |
| 148 | + logger.Debugw( |
| 149 | + "considering driver", |
| 150 | + "label", d.Info().Label, |
| 151 | + "priority", priority) |
| 152 | + for _, p := range props { |
| 153 | + fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p) |
| 154 | + if !ok { |
| 155 | + logger.Debugw("driver does not satisfy any constraints", "label", d.Info().Label) |
| 156 | + continue |
| 157 | + } |
| 158 | + fitnessDistWithPriority := fitnessDist - priority |
| 159 | + logger.Debugw( |
| 160 | + "driver properties satisfy some constraints", |
| 161 | + "label", d.Info().Label, |
| 162 | + "props", p, |
| 163 | + "distance", fitnessDist, |
| 164 | + "distance_with_priority", fitnessDistWithPriority) |
| 165 | + if fitnessDistWithPriority < minFitnessDist { |
| 166 | + minFitnessDist = fitnessDistWithPriority |
| 167 | + bestDriver = d |
| 168 | + bestProp = p |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + if bestDriver == nil { |
| 174 | + return nil, prop.Media{}, errors.New("failed to find the best driver that fits the constraints") |
| 175 | + } |
| 176 | + |
| 177 | + logger.Debugw("winning driver", "label", bestDriver.Info().Label, "props", bestProp) |
| 178 | + selectedMedia := prop.Media{} |
| 179 | + selectedMedia.MergeConstraints(constraints.MediaConstraints) |
| 180 | + selectedMedia.Merge(bestProp) |
| 181 | + return bestDriver, selectedMedia, nil |
| 182 | +} |
| 183 | + |
| 184 | +func queryDriverProperties( |
| 185 | + filter driver.FilterFn, |
| 186 | + logger logging.Logger, |
| 187 | +) map[driver.Driver][]prop.Media { |
| 188 | + var needToClose []driver.Driver |
| 189 | + drivers := driver.GetManager().Query(filter) |
| 190 | + m := make(map[driver.Driver][]prop.Media) |
| 191 | + |
| 192 | + for _, d := range drivers { |
| 193 | + var status string |
| 194 | + isAvailable, err := driver.IsAvailable(d) |
| 195 | + if errors.Is(err, availability.ErrUnimplemented) { |
| 196 | + s := d.Status() |
| 197 | + status = string(s) |
| 198 | + isAvailable = s == driver.StateClosed |
| 199 | + } else if err != nil { |
| 200 | + status = err.Error() |
| 201 | + } |
| 202 | + |
| 203 | + if isAvailable { |
| 204 | + err := d.Open() |
| 205 | + if err != nil { |
| 206 | + logger.Debugw("error opening driver for querying", "error", err) |
| 207 | + // Skip this driver if we failed to open because we can't get the properties |
| 208 | + continue |
| 209 | + } |
| 210 | + needToClose = append(needToClose, d) |
| 211 | + m[d] = d.Properties() |
| 212 | + } else { |
| 213 | + logger.Debugw("driver not available", "name", d.Info().Name, "label", d.Info().Label, "status", status) |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + for _, d := range needToClose { |
| 218 | + // Since it was closed, we should close it to avoid a leak |
| 219 | + if err := d.Close(); err != nil { |
| 220 | + logger.Errorw("error closing driver", "error", err) |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + return m |
| 225 | +} |
0 commit comments