Repository: pion/mediadevices (go)

Issue:
Support ranged media constraints
Pixel format is scored by complete match at now.
For example, I want to select one of `YUY2` and `UYVY` but not want to use `JPEG`.

https://github.com/pion/mediadevices/blob/949e850239b347a0750f005975caec7b8b77b5bb/pkg/prop/prop.go#L99-L112

Pull request:
Support ranged/exact/oneof media constraints
### Reference issue
closes #151

Patch:
diff --git a/examples/facedetection/main.go b/examples/facedetection/main.go
index 1fb9d3b..7288229 100644
--- a/examples/facedetection/main.go
+++ b/examples/facedetection/main.go
@@ -11,6 +11,7 @@ import (
 	_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
 	"github.com/pion/mediadevices/pkg/frame"
 	"github.com/pion/mediadevices/pkg/io/video"
+	"github.com/pion/mediadevices/pkg/prop"
 	"github.com/pion/webrtc/v2"
 )
 
@@ -66,10 +67,10 @@ func main() {
 
 	s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
 		Video: func(c *mediadevices.MediaTrackConstraints) {
-			c.FrameFormat = frame.FormatI420 // most of the encoder accepts I420
+			c.FrameFormat = prop.FrameFormatExact(frame.FormatI420) // most of the encoder accepts I420
 			c.Enabled = true
-			c.Width = 640
-			c.Height = 480
+			c.Width = prop.Int(640)
+			c.Height = prop.Int(480)
 			c.VideoTransform = markFacesTransformer
 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
 		},
diff --git a/examples/rtp-send/main.go b/examples/rtp-send/main.go
index 588550c..b8a38a4 100644
--- a/examples/rtp-send/main.go
+++ b/examples/rtp-send/main.go
@@ -10,6 +10,7 @@ import (
 	"github.com/pion/mediadevices/pkg/codec/vpx"       // This is required to use VP8/VP9 video encoder
 	_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
 	"github.com/pion/mediadevices/pkg/frame"
+	"github.com/pion/mediadevices/pkg/prop"
 	"github.com/pion/rtp"
 	"github.com/pion/webrtc/v2"
 	"github.com/pion/webrtc/v2/pkg/media"
@@ -48,10 +49,10 @@ func main() {
 
 	_, err = md.GetUserMedia(mediadevices.MediaStreamConstraints{
 		Video: func(c *mediadevices.MediaTrackConstraints) {
-			c.FrameFormat = frame.FormatYUY2
+			c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
 			c.Enabled = true
-			c.Width = 640
-			c.Height = 480
+			c.Width = prop.Int(640)
+			c.Height = prop.Int(480)
 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
 		},
 	})
diff --git a/examples/simple/main.go b/examples/simple/main.go
index a3c877f..e8372f0 100644
--- a/examples/simple/main.go
+++ b/examples/simple/main.go
@@ -7,6 +7,7 @@ import (
 	"github.com/pion/mediadevices/examples/internal/signal"
 	"github.com/pion/mediadevices/pkg/codec"
 	"github.com/pion/mediadevices/pkg/frame"
+	"github.com/pion/mediadevices/pkg/prop"
 	"github.com/pion/webrtc/v2"
 
 	// This is required to use opus audio encoder
@@ -80,10 +81,10 @@ func main() {
 			c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{&opusParams}
 		},
 		Video: func(c *mediadevices.MediaTrackConstraints) {
-			c.FrameFormat = frame.FormatYUY2
+			c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
 			c.Enabled = true
-			c.Width = 640
-			c.Height = 480
+			c.Width = prop.Int(640)
+			c.Height = prop.Int(480)
 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
 		},
 	})
diff --git a/internal/codec/measurement.go b/internal/codec/measurement.go
index c14e45b..3010aa7 100644
--- a/internal/codec/measurement.go
+++ b/internal/codec/measurement.go
@@ -11,12 +11,15 @@ import (
 func MeasureBitRate(r io.Reader, dur time.Duration) (float64, error) {
 	var n, totalBytes int
 	var err error
+
 	buf := make([]byte, 1024)
 	start := time.Now()
 	now := start
 	end := now.Add(dur)
-	for now.Before(end) {
+	for {
 		n, err = r.Read(buf)
+		now = time.Now()
+
 		if err != nil {
 			if e, ok := err.(*mio.InsufficientBufferError); ok {
 				buf = make([]byte, 2*e.RequiredSize)
@@ -24,6 +27,7 @@ func MeasureBitRate(r io.Reader, dur time.Duration) (float64, error) {
 			}
 
 			if err == io.EOF {
+				dur = now.Sub(start)
 				totalBytes += n
 				break
 			}
@@ -31,11 +35,12 @@ func MeasureBitRate(r io.Reader, dur time.Duration) (float64, error) {
 			return 0, err
 		}
 
-		totalBytes += n
-		now = time.Now()
+		if now.After(end) {
+			break
+		}
+		totalBytes += n // count bytes if the data arrived within the period
 	}
 
-	elapsed := time.Now().Sub(start).Seconds()
-	avg := float64(totalBytes*8) / elapsed
+	avg := float64(totalBytes*8) / dur.Seconds()
 	return avg, nil
 }
diff --git a/mediadevices.go b/mediadevices.go
index 1fd9644..1462df5 100644
--- a/mediadevices.go
+++ b/mediadevices.go
@@ -200,7 +200,11 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
 	for d, props := range driverProperties {
 		priority := float64(d.Info().Priority)
 		for _, p := range props {
-			fitnessDist := constraints.Media.FitnessDistance(p) - priority
+			fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p)
+			if !ok {
+				continue
+			}
+			fitnessDist -= priority
 			if fitnessDist < minFitnessDist {
 				minFitnessDist = fitnessDist
 				bestDriver = d
@@ -213,7 +217,8 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
 		return nil, MediaTrackConstraints{}, errNotFound
 	}
 
-	constraints.Merge(bestProp)
+	constraints.selectedMedia = bestProp
+	constraints.selectedMedia.Merge(constraints.MediaConstraints)
 	return bestDriver, constraints, nil
 }
 
diff --git a/mediastreamconstraints.go b/mediastreamconstraints.go
index c01c911..8e3e429 100644
--- a/mediastreamconstraints.go
+++ b/mediastreamconstraints.go
@@ -14,7 +14,7 @@ type MediaStreamConstraints struct {
 
 // MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
 type MediaTrackConstraints struct {
-	prop.Media
+	prop.MediaConstraints
 	Enabled bool
 	// VideoEncoderBuilders are codec builders that are used for encoding the video
 	// and later being used for sending the appropriate RTP payload type.
@@ -34,6 +34,8 @@ type MediaTrackConstraints struct {
 	// AudioTransform will be used to transform the audio that's coming from the driver.
 	// So, basically it'll look like following: driver -> AudioTransform -> code
 	AudioTransform audio.TransformFunc
+
+	selectedMedia prop.Media
 }
 
 type MediaOption func(*MediaTrackConstraints)
diff --git a/pkg/prop/duration.go b/pkg/prop/duration.go
new file mode 100644
index 0000000..8de7cc4
--- /dev/null
+++ b/pkg/prop/duration.go
@@ -0,0 +1,98 @@
+package prop
+
+import (
+	"math"
+	"time"
+)
+
+// DurationConstraint is an interface to represent time.Duration constraint.
+type DurationConstraint interface {
+	Compare(time.Duration) (float64, bool)
+	Value() (time.Duration, bool)
+}
+
+// Duration specifies ideal duration value.
+// Any value may be selected, but closest value takes priority.
+type Duration time.Duration
+
+// Compare implements DurationConstraint.
+func (d Duration) Compare(a time.Duration) (float64, bool) {
+	return math.Abs(float64(a-time.Duration(d))) / math.Max(math.Abs(float64(a)), math.Abs(float64(d))), true
+}
+
+// Value implements DurationConstraint.
+func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true }
+
+// DurationExact specifies exact duration value.
+type DurationExact time.Duration
+
+// Compare implements DurationConstraint.
+func (d DurationExact) Compare(a time.Duration) (float64, bool) {
+	if time.Duration(d) == a {
+		return 0.0, true
+	}
+	return 1.0, false
+}
+
+// Value implements DurationConstraint.
+func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true }
+
+// DurationOneOf specifies list of expected duration values.
+type DurationOneOf []time.Duration
+
+// Compare implements DurationConstraint.
+func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
+	for _, ii := range d {
+		if ii == a {
+			return 0.0, true
+		}
+	}
+	return 1.0, false
+}
+
+// Value implements DurationConstraint.
+func (DurationOneOf) Value() (time.Duration, bool) { return 0, false }
+
+// DurationRanged specifies range of expected duration value.
+// If Ideal is non-zero, closest value to Ideal takes priority.
+type DurationRanged struct {
+	Min   time.Duration
+	Max   time.Duration
+	Ideal time.Duration
+}
+
+// Compare implements DurationConstraint.
+func (d DurationRanged) Compare(a time.Duration) (float64, bool) {
+	if d.Min != 0 && d.Min > a {
+		// Out of range
+		return 1.0, false
+	}
+	if d.Max != 0 && d.Max < a {
+		// Out of range
+		return 1.0, false
+	}
+	if d.Ideal == 0 {
+		// If the value is in the range and Ideal is not specified,
+		// any value is evenly acceptable.
+		return 0.0, true
+	}
+	switch {
+	case a == d.Ideal:
+		return 0.0, true
+	case a < d.Ideal:
+		if d.Min == 0 {
+			// If Min is not specified, smaller values than Ideal are even.
+			return 0.0, true
+		}
+		return float64(d.Ideal-a) / float64(d.Ideal-d.Min), true
+	default:
+		if d.Max == 0 {
+			// If Max is not specified, larger values than Ideal are even.
+			return 0.0, true
+		}
+		return float64(a-d.Ideal) / float64(d.Max-d.Ideal), true
+	}
+}
+
+// Value implements DurationConstraint.
+func (DurationRanged) Value() (time.Duration, bool) { return 0, false }
diff --git a/pkg/prop/float.go b/pkg/prop/float.go
new file mode 100644
index 0000000..3238c08
--- /dev/null
+++ b/pkg/prop/float.go
@@ -0,0 +1,97 @@
+package prop
+
+import (
+	"math"
+)
+
+// FloatConstraint is an interface to represent float value constraint.
+type FloatConstraint interface {
+	Compare(float32) (float64, bool)
+	Value() (float32, bool)
+}
+
+// Float specifies ideal float value.
+// Any value may be selected, but closest value takes priority.
+type Float float32
+
+// Compare implements FloatConstraint.
+func (f Float) Compare(a float32) (float64, bool) {
+	return math.Abs(float64(a-float32(f))) / math.Max(math.Abs(float64(a)), math.Abs(float64(f))), true
+}
+
+// Value implements FloatConstraint.
+func (f Float) Value() (float32, bool) { return float32(f), true }
+
+// FloatExact specifies exact float value.
+type FloatExact float32
+
+// Compare implements FloatConstraint.
+func (f FloatExact) Compare(a float32) (float64, bool) {
+	if float32(f) == a {
+		return 0.0, true
+	}
+	return 1.0, false
+}
+
+// Value implements FloatConstraint.
+func (f FloatExact) Value() (float32, bool) { return float32(f), true }
+
+// FloatOneOf specifies list of expected float values.
+type FloatOneOf []float32
+
+// Compare implements FloatConstraint.
+func (f FloatOneOf) Compare(a float32) (float64, bool) {
+	for _, ff := range f {
+		if ff == a {
+			return 0.0, true
+		}
+	}
+	return 1.0, false
+}
+
+// Value implements FloatConstraint.
+func (FloatOneOf) Value() (float32, bool) { return 0, false }
+
+// FloatRanged specifies range of expected float value.
+// If Ideal is non-zero, closest value to Ideal takes priority.
+type FloatRanged struct {
+	Min   float32
+	Max   float32
+	Ideal float32
+}
+
+// Compare implements FloatConstraint.
+func (f FloatRanged) Compare(a float32) (float64, bool) {
+	if f.Min != 0 && f.Min > a {
+		// Out of range
+		return 1.0, false
+	}
+	if f.Max != 0 && f.Max < a {
+		// Out of range
+		return 1.0, false
+	}
+	if f.Ideal == 0 {
+		// If the value is in the range and Ideal is not specified,
+		// any value is evenly acceptable.
+		return 0.0, true
+	}
+	switch {
+	case a == f.Ideal:
+		return 0.0, true
+	case a < f.Ideal:
+		if f.Min == 0 {
+			// If Min is not specified, smaller values than Ideal are even.
+			return 0.0, true
+		}
+		return float64(f.Ideal-a) / float64(f.Ideal-f.Min), true
+	default:
+		if f.Max == 0 {
+			// If Max is not specified, larger values than Ideal are even.
+			return 0.0, true
+		}
+		return float64(a-f.Ideal) / float64(f.Max-f.Ideal), true
+	}
+}
+
+// Value implements FloatConstraint.
+func (FloatRanged) Value() (float32, bool) { return 0, false }
diff --git a/pkg/prop/format.go b/pkg/prop/format.go
new file mode 100644
index 0000000..126476d
--- /dev/null
+++ b/pkg/prop/format.go
@@ -0,0 +1,56 @@
+package prop
+
+import (
+	"github.com/pion/mediadevices/pkg/frame"
+)
+
+// FrameFormatConstraint is an interface to represent frame format constraint.
+type FrameFormatConstraint interface {
+	Compare(frame.Format) (float64, bool)
+	Value() (frame.Format, bool)
+}
+
+// FrameFormat specifies expected frame format.
+// Any value may be selected, but matched value takes priority.
+type FrameFormat frame.Format
+
+// Compare implements FrameFormatConstraint.
+func (f FrameFormat) Compare(a frame.Format) (float64, bool) {
+	if frame.Format(f) == a {
+		return 0.0, true
+	}
+	return 1.0, true
+}
+
+// Value implements FrameFormatConstraint.
+func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true }
+
+// FrameFormatExact specifies exact frame format.
+type FrameFormatExact frame.Format
+
+// Compare implements FrameFormatConstraint.
+func (f FrameFormatExact) Compare(a frame.Format) (float64, bool) {
+	if frame.Format(f) == a {
+		return 0.0, true
+	}
+	return 1.0, false
+}
+
+// Value implements FrameFormatConstraint.
+func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true }
+
+// FrameFormatOneOf specifies list of expected frame format.
+type FrameFormatOneOf []frame.Format
+
+// Compare implements FrameFormatConstraint.
+func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) {
+	for _, ff := range f {
+		if ff == a {
+			return 0.0, true
+		}
+	}
+	return 1.0, false
+}
+
+// Value implements FrameFormatConstraint.
+func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false }
diff --git a/pkg/prop/int.go b/pkg/prop/int.go
new file mode 100644
index 0000000..a41a12d
--- /dev/null
+++ b/pkg/prop/int.go
@@ -0,0 +1,97 @@
+package prop
+
+import (
+	"math"
+)
+
+// IntConstraint is an interface to represent integer value constraint.
+type IntConstraint interface {
+	Compare(int) (float64, bool)
+	Value() (int, bool)
+}
+
+// Int specifies ideal int value.
+// Any value may be selected, but closest value takes priority.
+type Int int
+
+// Compare implements IntConstraint.
+func (i Int) Compare(a int) (float64, bool) {
+	return math.Abs(float64(a-int(i))) / math.Max(math.Abs(float64(a)), math.Abs(float64(i))), true
+}
+
+// Value implements IntConstraint.
+func (i Int) Value() (int, bool) { return int(i), true }
+
+// IntExact specifies exact int value.
+type IntExact int
+
+// Compare implements IntConstraint.
+func (i IntExact) Compare(a int) (float64, bool) {
+	if int(i) == a {
+		return 0.0, true
+	}
+	return 1.0, false
+}
+
+// Value implements IntConstraint.
+func (i IntExact) Value() (int, bool) { return int(i), true }
+
+// IntOneOf specifies list of expected float values.
+type IntOneOf []int
+
+// Compare implements IntConstraint.
+func (i IntOneOf) Compare(a int) (float64, bool) {
+	for _, ii := range i {
+		if ii == a {
+			return 0.0, true
+		}
+	}
+	return 1.0, false
+}
+
+// Value implements IntConstraint.
+func (IntOneOf) Value() (int, bool) { return 0, false }
+
+// IntRanged specifies range of expected int value.
+// If Ideal is non-zero, closest value to Ideal takes priority.
+type IntRanged struct {
+	Min   int
+	Max   int
+	Ideal int
+}
+
+// Compare implements IntConstraint.
+func (i IntRanged) Compare(a int) (float64, bool) {
+	if i.Min != 0 && i.Min > a {
+		// Out of range
+		return 1.0, false
+	}
+	if i.Max != 0 && i.Max < a {
+		// Out of range
+		return 1.0, false
+	}
+	if i.Ideal == 0 {
+		// If the value is in the range and Ideal is not specified,
+		// any value is evenly acceptable.
+		return 0.0, true
+	}
+	switch {
+	case a == i.Ideal:
+		return 0.0, true
+	case a < i.Ideal:
+		if i.Min == 0 {
+			// If Min is not specified, smaller values than Ideal are even.
+			return 0.0, true
+		}
+		return float64(i.Ideal-a) / float64(i.Ideal-i.Min), true
+	default:
+		if i.Max == 0 {
+			// If Max is not specified, larger values than Ideal are even.
+			return 0.0, true
+		}
+		return float64(a-i.Ideal) / float64(i.Max-i.Ideal), true
+	}
+}
+
+// Value implements IntConstraint.
+func (IntRanged) Value() (int, bool) { return 0, false }
diff --git a/pkg/prop/prop.go b/pkg/prop/prop.go
index 80bf1b5..26bf4cb 100644
--- a/pkg/prop/prop.go
+++ b/pkg/prop/prop.go
@@ -1,15 +1,21 @@
 package prop
 
 import (
-	"fmt"
-	"math"
 	"reflect"
-	"strconv"
 	"time"
 
 	"github.com/pion/mediadevices/pkg/frame"
 )
 
+// MediaConstraints represents set of media propaty constraints.
+// Each field constrains property by min/ideal/max range, exact match, or oneof match.
+type MediaConstraints struct {
+	DeviceID string
+	VideoConstraints
+	AudioConstraints
+}
+
+// Media stores single set of media propaties.
 type Media struct {
 	DeviceID string
 	Video
@@ -17,7 +23,7 @@ type Media struct {
 }
 
 // Merge merges all the field values from o to p, except zero values.
-func (p *Media) Merge(o Media) {
+func (p *Media) Merge(o MediaConstraints) {
 	rp := reflect.ValueOf(p).Elem()
 	ro := reflect.ValueOf(o)
 
@@ -29,9 +35,9 @@ func (p *Media) Merge(o Media) {
 			fieldA := a.Field(i)
 			fieldB := b.Field(i)
 
-			// if a is a struct, b is also a struct. Then,
+			// if b is a struct, a is also a struct. Then,
 			// we recursively merge them
-			if fieldA.Kind() == reflect.Struct {
+			if fieldB.Kind() == reflect.Struct {
 				merge(fieldA, fieldB)
 				continue
 			}
@@ -43,67 +49,124 @@ func (p *Media) Merge(o Media) {
 				continue
 			}
 
-			fieldA.Set(fieldB)
+			switch c := fieldB.Interface().(type) {
+			case IntConstraint:
+				if v, ok := c.Value(); ok {
+					fieldA.Set(reflect.ValueOf(v))
+				}
+			case FloatConstraint:
+				if v, ok := c.Value(); ok {
+					fieldA.Set(reflect.ValueOf(v))
+				}
+			case DurationConstraint:
+				if v, ok := c.Value(); ok {
+					fieldA.Set(reflect.ValueOf(v))
+				}
+			case FrameFormatConstraint:
+				if v, ok := c.Value(); ok {
+					fieldA.Set(reflect.ValueOf(v))
+				}
+			default:
+				panic("unsupported property type")
+			}
 		}
 	}
 
 	merge(rp, ro)
 }
 
-func (p *Media) FitnessDistance(o Media) float64 {
+// FitnessDistance calculates fitness of media property and media constraints.
+// If no media satisfies given constraints, second return value will be false.
+func (p *MediaConstraints) FitnessDistance(o Media) (float64, bool) {
 	cmps := comparisons{}
 	cmps.add(p.Width, o.Width)
 	cmps.add(p.Height, o.Height)
 	cmps.add(p.FrameFormat, o.FrameFormat)
 	cmps.add(p.SampleRate, o.SampleRate)
 	cmps.add(p.Latency, o.Latency)
+
 	return cmps.fitnessDistance()
 }
 
-type comparisons map[string]string
+type comparisons []struct {
+	desired, actual interface{}
+}
 
-func (c comparisons) add(actual, ideal interface{}) {
-	c[fmt.Sprint(actual)] = fmt.Sprint(ideal)
+func (c *comparisons) add(desired, actual interface{}) {
+	if desired != nil {
+		*c = append(*c,
+			struct{ desired, actual interface{} }{
+				desired, actual,
+			},
+		)
+	}
 }
 
 // fitnessDistance is an implementation for https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
-func (c comparisons) fitnessDistance() float64 {
+func (c *comparisons) fitnessDistance() (float64, bool) {
 	var dist float64
-
-	for actual, ideal := range c {
-		if actual == ideal {
-			continue
-		}
-
-		actualF, err1 := strconv.ParseFloat(actual, 64)
-		idealF, err2 := strconv.ParseFloat(ideal, 64)
-
-		switch {
-		// If both of the values are numeric, we need to normalize the values to get the distance
-		case err1 == nil && err2 == nil:
-			dist += math.Abs(actualF-idealF) / math.Max(math.Abs(actualF), math.Abs(idealF))
-		// If both of the values are not numeric, the only comparison value is either 0 (matched) or 1 (not matched)
-		case err1 != nil && err2 != nil:
-			if actual != ideal {
-				dist++
+	for _, field := range *c {
+		var d float64
+		var ok bool
+		switch c := field.desired.(type) {
+		case IntConstraint:
+			if actual, typeOK := field.actual.(int); typeOK {
+				d, ok = c.Compare(actual)
+			} else {
+				panic("wrong type of actual value")
+			}
+		case FloatConstraint:
+			if actual, typeOK := field.actual.(float32); typeOK {
+				d, ok = c.Compare(actual)
+			} else {
+				panic("wrong type of actual value")
+			}
+		case DurationConstraint:
+			if actual, typeOK := field.actual.(time.Duration); typeOK {
+				d, ok = c.Compare(actual)
+			} else {
+				panic("wrong type of actual value")
+			}
+		case FrameFormatConstraint:
+			if actual, typeOK := field.actual.(frame.Format); typeOK {
+				d, ok = c.Compare(actual)
+			} else {
+				panic("wrong type of actual value")
 			}
-		// Comparing a numeric value with a non-numeric value is a an internal error, so panic.
 		default:
-			panic("fitnessDistance can't mix comparisons.")
+			panic("unsupported constraint type")
+		}
+		dist += d
+		if !ok {
+			return 0, false
 		}
 	}
+	return dist, true
+}
 
-	return dist
+// VideoConstraints represents a video's constraints
+type VideoConstraints struct {
+	Width, Height IntConstraint
+	FrameRate     FloatConstraint
+	FrameFormat   FrameFormatConstraint
 }
 
-// Video represents a video's properties
+// Video represents a video's constraints
 type Video struct {
 	Width, Height int
 	FrameRate     float32
 	FrameFormat   frame.Format
 }
 
-// Audio represents an audio's properties
+// AudioConstraints represents an audio's constraints
+type AudioConstraints struct {
+	ChannelCount IntConstraint
+	Latency      DurationConstraint
+	SampleRate   IntConstraint
+	SampleSize   IntConstraint
+}
+
+// Audio represents an audio's constraints
 type Audio struct {
 	ChannelCount int
 	Latency      time.Duration
diff --git a/track.go b/track.go
index 620260a..f4e5ca8 100644
--- a/track.go
+++ b/track.go
@@ -1,7 +1,7 @@
 package mediadevices
 
 import (
-	"fmt"
+	"errors"
 	"math/rand"
 	"sync"
 
@@ -62,11 +62,11 @@ func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrack
 	case driver.AudioRecorder:
 		rtpCodecs = opts.codecs[webrtc.RTPCodecTypeAudio]
 		buildSampler = func(t LocalTrack) samplerFunc {
-			return newAudioSampler(t, constraints.Latency)
+			return newAudioSampler(t, constraints.selectedMedia.Latency)
 		}
 		encoderBuilders, err = newAudioEncoderBuilders(r, constraints)
 	default:
-		err = fmt.Errorf("newTrack: invalid driver type")
+		err = errors.New("newTrack: invalid driver type")
 	}
 
 	if err != nil {
@@ -114,7 +114,7 @@ func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrack
 	}
 
 	d.Close()
-	return nil, fmt.Errorf("newTrack: failed to find a matching codec")
+	return nil, errors.New("newTrack: failed to find a matching codec")
 }
 
 // OnEnded sets an error handler. When a track has been created and started, if an
@@ -196,7 +196,7 @@ type encoderBuilder struct {
 // newVideoEncoderBuilders transforms video given by VideoRecorder with the video transformer that is passed through
 // constraints and create a list of generic encoder builders
 func newVideoEncoderBuilders(vr driver.VideoRecorder, constraints MediaTrackConstraints) ([]encoderBuilder, error) {
-	r, err := vr.VideoRecord(constraints.Media)
+	r, err := vr.VideoRecord(constraints.selectedMedia)
 	if err != nil {
 		return nil, err
 	}
@@ -209,7 +209,7 @@ func newVideoEncoderBuilders(vr driver.VideoRecorder, constraints MediaTrackCons
 	for i, b := range constraints.VideoEncoderBuilders {
 		encoderBuilders[i].name = b.Name()
 		encoderBuilders[i].build = func() (codec.ReadCloser, error) {
-			return b.BuildVideoEncoder(r, constraints.Media)
+			return b.BuildVideoEncoder(r, constraints.selectedMedia)
 		}
 	}
 	return encoderBuilders, nil
@@ -218,7 +218,7 @@ func newVideoEncoderBuilders(vr driver.VideoRecorder, constraints MediaTrackCons
 // newAudioEncoderBuilders transforms audio given by AudioRecorder with the audio transformer that is passed through
 // constraints and create a list of generic encoder builders
 func newAudioEncoderBuilders(ar driver.AudioRecorder, constraints MediaTrackConstraints) ([]encoderBuilder, error) {
-	r, err := ar.AudioRecord(constraints.Media)
+	r, err := ar.AudioRecord(constraints.selectedMedia)
 	if err != nil {
 		return nil, err
 	}
@@ -231,7 +231,7 @@ func newAudioEncoderBuilders(ar driver.AudioRecorder, constraints MediaTrackCons
 	for i, b := range constraints.AudioEncoderBuilders {
 		encoderBuilders[i].name = b.Name()
 		encoderBuilders[i].build = func() (codec.ReadCloser, error) {
-			return b.BuildAudioEncoder(r, constraints.Media)
+			return b.BuildAudioEncoder(r, constraints.selectedMedia)
 		}
 	}
 	return encoderBuilders, nil


Test patch:
diff --git a/internal/codec/measurement_test.go b/internal/codec/measurement_test.go
index 0b2d711..8796089 100644
--- a/internal/codec/measurement_test.go
+++ b/internal/codec/measurement_test.go
@@ -9,18 +9,25 @@ import (
 
 func TestMeasureBitRateStatic(t *testing.T) {
 	r, w := io.Pipe()
-	dur := time.Second * 5
-	dataSize := 1000
-	var precision float64 = 8 // 1 byte
+	const (
+		dataSize       = 1000
+		dur            = 5 * time.Second
+		packetInterval = time.Second
+		precision      = 8.0 // 1 byte
+	)
 	var wg sync.WaitGroup
 	wg.Add(1)
 	done := make(chan struct{})
 
 	go func() {
 		data := make([]byte, dataSize)
+		ticker := time.NewTicker(packetInterval)
+
+		// Wait half interval
+		time.Sleep(packetInterval / 2)
+
 		// Make sure that this goroutine is synchronized with main goroutine
 		wg.Done()
-		ticker := time.NewTicker(time.Second)
 
 		for {
 			select {
@@ -48,30 +55,34 @@ func TestMeasureBitRateStatic(t *testing.T) {
 
 func TestMeasureBitRateDynamic(t *testing.T) {
 	r, w := io.Pipe()
-	dur := time.Second * 5
-	dataSize := 1000
-	var precision float64 = 8 // 1 byte
+	const (
+		dataSize       = 1000
+		dur            = 5 * time.Second
+		packetInterval = time.Millisecond * 250
+		precision      = 8.0 // 1 byte
+	)
 	var wg sync.WaitGroup
 	wg.Add(1)
 	done := make(chan struct{})
 
 	go func() {
 		data := make([]byte, dataSize)
+		ticker := time.NewTicker(packetInterval)
+
+		// Wait half interval
+		time.Sleep(packetInterval / 2)
+
 		wg.Done()
-		ticker := time.NewTicker(time.Millisecond * 500)
-		var count int
 
+		var count int
 		for {
 			select {
 			case <-ticker.C:
-				w.Write(data)
-				count++
-				// Wait until 4 slow ticks, which is also equal to 2 seconds
-				if count == 4 {
-					ticker.Stop()
-					// Speed up the tick by 2 times for the rest
-					ticker = time.NewTicker(time.Millisecond * 250)
+				// 4 x 500ms ticks and 250ms ticks
+				if count%2 == 1 || count >= 8 {
+					w.Write(data)
 				}
+				count++
 			case <-done:
 				w.Close()
 				return
diff --git a/mediadevices_test.go b/mediadevices_test.go
index 825b0c8..e771bcb 100644
--- a/mediadevices_test.go
+++ b/mediadevices_test.go
@@ -50,8 +50,8 @@ func TestGetUserMedia(t *testing.T) {
 	constraints := MediaStreamConstraints{
 		Video: func(c *MediaTrackConstraints) {
 			c.Enabled = true
-			c.Width = 640
-			c.Height = 480
+			c.Width = prop.Int(640)
+			c.Height = prop.Int(480)
 			params := videoParams
 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&params}
 		},
@@ -64,8 +64,8 @@ func TestGetUserMedia(t *testing.T) {
 	constraintsWrong := MediaStreamConstraints{
 		Video: func(c *MediaTrackConstraints) {
 			c.Enabled = true
-			c.Width = 640
-			c.Height = 480
+			c.Width = prop.Int(640)
+			c.Height = prop.Int(480)
 			params := videoParams
 			params.BitRate = 0
 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&params}
diff --git a/pkg/prop/prop_test.go b/pkg/prop/prop_test.go
index 6264704..eda2a7a 100644
--- a/pkg/prop/prop_test.go
+++ b/pkg/prop/prop_test.go
@@ -2,8 +2,138 @@ package prop
 
 import (
 	"testing"
+	"time"
+
+	"github.com/pion/mediadevices/pkg/frame"
 )
 
+func TestCompareMatch(t *testing.T) {
+	testDataSet := map[string]struct {
+		a     MediaConstraints
+		b     Media
+		match bool
+	}{
+		"IntIdealUnmatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				Width: Int(30),
+			}},
+			Media{Video: Video{
+				Width: 50,
+			}},
+			true,
+		},
+		"IntIdealMatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				Width: Int(30),
+			}},
+			Media{Video: Video{
+				Width: 30,
+			}},
+			true,
+		},
+		"IntExactUnmatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				Width: IntExact(30),
+			}},
+			Media{Video: Video{
+				Width: 50,
+			}},
+			false,
+		},
+		"IntExactMatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				Width: IntExact(30),
+			}},
+			Media{Video: Video{
+				Width: 30,
+			}},
+			true,
+		},
+		"IntRangeUnmatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				Width: IntRanged{Min: 30, Max: 40},
+			}},
+			Media{Video: Video{
+				Width: 50,
+			}},
+			false,
+		},
+		"IntRangeMatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				Width: IntRanged{Min: 30, Max: 40},
+			}},
+			Media{Video: Video{
+				Width: 35,
+			}},
+			true,
+		},
+		"FrameFormatOneOfUnmatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				FrameFormat: FrameFormatOneOf{frame.FormatYUYV, frame.FormatUYVY},
+			}},
+			Media{Video: Video{
+				FrameFormat: frame.FormatYUYV,
+			}},
+			true,
+		},
+		"FrameFormatOneOfMatch": {
+			MediaConstraints{VideoConstraints: VideoConstraints{
+				FrameFormat: FrameFormatOneOf{frame.FormatYUYV, frame.FormatUYVY},
+			}},
+			Media{Video: Video{
+				FrameFormat: frame.FormatMJPEG,
+			}},
+			false,
+		},
+		"DurationExactUnmatch": {
+			MediaConstraints{AudioConstraints: AudioConstraints{
+				Latency: DurationExact(time.Second),
+			}},
+			Media{Audio: Audio{
+				Latency: time.Second + time.Millisecond,
+			}},
+			false,
+		},
+		"DurationExactMatch": {
+			MediaConstraints{AudioConstraints: AudioConstraints{
+				Latency: DurationExact(time.Second),
+			}},
+			Media{Audio: Audio{
+				Latency: time.Second,
+			}},
+			true,
+		},
+		"DurationRangedUnmatch": {
+			MediaConstraints{AudioConstraints: AudioConstraints{
+				Latency: DurationRanged{Max: time.Second},
+			}},
+			Media{Audio: Audio{
+				Latency: time.Second + time.Millisecond,
+			}},
+			false,
+		},
+		"DurationRangedMatch": {
+			MediaConstraints{AudioConstraints: AudioConstraints{
+				Latency: DurationRanged{Max: time.Second},
+			}},
+			Media{Audio: Audio{
+				Latency: time.Millisecond,
+			}},
+			true,
+		},
+	}
+
+	for name, testData := range testDataSet {
+		testData := testData
+		t.Run(name, func(t *testing.T) {
+			_, match := testData.a.FitnessDistance(testData.b)
+			if match != testData.match {
+				t.Errorf("matching flag differs, expected: %v, got: %v", testData.match, match)
+			}
+		})
+	}
+}
+
 func TestMergeWithZero(t *testing.T) {
 	a := Media{
 		Video: Video{
@@ -11,9 +141,9 @@ func TestMergeWithZero(t *testing.T) {
 		},
 	}
 
-	b := Media{
-		Video: Video{
-			Height: 100,
+	b := MediaConstraints{
+		VideoConstraints: VideoConstraints{
+			Height: Int(100),
 		},
 	}
 
@@ -35,9 +165,9 @@ func TestMergeWithSameField(t *testing.T) {
 		},
 	}
 
-	b := Media{
-		Video: Video{
-			Width: 100,
+	b := MediaConstraints{
+		VideoConstraints: VideoConstraints{
+			Width: Int(100),
 		},
 	}
 
@@ -61,9 +191,9 @@ func TestMergeNested(t *testing.T) {
 		},
 	}
 
-	b := Media{
-		Video: Video{
-			Width: 100,
+	b := MediaConstraints{
+		VideoConstraints: VideoConstraints{
+			Width: Int(100),
 		},
 	}