Files
mediadevices/pkg/prop/prop.go
2025-09-14 21:55:37 -04:00

266 lines
6.6 KiB
Go

package prop
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/pion/mediadevices/pkg/frame"
)
// MediaConstraints represents set of media property constraints.
// Each field constrains property by min/ideal/max range, exact match, or oneof match.
type MediaConstraints struct {
DeviceID StringConstraint
VideoConstraints
AudioConstraints
}
func (m *MediaConstraints) String() string {
return prettifyStruct(m)
}
// Media stores single set of media propaties.
type Media struct {
DeviceID string
Video
Audio
}
func (m *Media) String() string {
return prettifyStruct(m)
}
func prettifyStruct(i any) string {
var rows []string
var addRows func(int, reflect.Value)
addRows = func(level int, obj reflect.Value) {
typeOf := obj.Type()
for i := 0; i < obj.NumField(); i++ {
field := typeOf.Field(i)
value := obj.Field(i)
padding := strings.Repeat(" ", level)
switch value.Kind() {
case reflect.Struct:
rows = append(rows, fmt.Sprintf("%s%v:", padding, field.Name))
addRows(level+1, value)
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
if value.IsNil() {
rows = append(rows, fmt.Sprintf("%s%v: any", padding, field.Name))
} else {
rows = append(rows, fmt.Sprintf("%s%v: %v", padding, field.Name, value))
}
default:
rows = append(rows, fmt.Sprintf("%s%v: %v", padding, field.Name, value))
}
}
}
addRows(0, reflect.ValueOf(i).Elem())
return strings.Join(rows, "\n")
}
// setterFn is a callback function to set value from fieldB to fieldA
type setterFn func(fieldA, fieldB reflect.Value)
// merge merges all the field values from o to p, except zero values. It's guaranteed that setterFn will be called
// when fieldA and fieldB are not struct.
func (p *Media) merge(o any, set setterFn) {
rp := reflect.ValueOf(p).Elem()
ro := reflect.ValueOf(o)
// merge b fields to a recursively
var merge func(a, b reflect.Value)
merge = func(a, b reflect.Value) {
numFields := a.NumField()
for i := 0; i < numFields; i++ {
fieldA := a.Field(i)
fieldB := b.Field(i)
// if b is a struct, a is also a struct. Then,
// we recursively merge them
if fieldB.Kind() == reflect.Struct {
merge(fieldA, fieldB)
continue
}
// TODO: Replace this with fieldB.IsZero() when we move to go1.13
// If non-boolean or non-discrete values are zeroes we skip them
if fieldB.Interface() == reflect.Zero(fieldB.Type()).Interface() &&
fieldB.Kind() != reflect.Bool {
continue
}
set(fieldA, fieldB)
}
}
merge(rp, ro)
}
func (p *Media) Merge(o Media) {
p.merge(o, func(fieldA, fieldB reflect.Value) {
fieldA.Set(fieldB)
})
}
func (p *Media) MergeConstraints(o MediaConstraints) {
p.merge(o, func(fieldA, fieldB reflect.Value) {
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))
}
case StringConstraint:
if v, ok := c.Value(); ok {
fieldA.Set(reflect.ValueOf(v))
}
case BoolConstraint:
fieldA.Set(reflect.ValueOf(c.Value()))
default:
panic("unsupported property type")
}
})
}
// 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.DeviceID, o.DeviceID)
cmps.add(p.Width, o.Width)
cmps.add(p.Height, o.Height)
cmps.add(p.FrameFormat, o.FrameFormat)
// skip framerate if not available in media properties
if o.FrameRate > 0.0 {
cmps.add(p.FrameRate, o.FrameRate)
}
cmps.add(p.SampleRate, o.SampleRate)
cmps.add(p.Latency, o.Latency)
cmps.add(p.ChannelCount, o.ChannelCount)
cmps.add(p.IsBigEndian, o.IsBigEndian)
cmps.add(p.IsFloat, o.IsFloat)
cmps.add(p.IsInterleaved, o.IsInterleaved)
return cmps.fitnessDistance()
}
type comparisons []struct {
desired, actual any
}
func (c *comparisons) add(desired, actual any) {
if desired != nil {
*c = append(*c,
struct{ desired, actual any }{
desired, actual,
},
)
}
}
// fitnessDistance is an implementation for https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
func (c *comparisons) fitnessDistance() (float64, bool) {
var dist float64
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")
}
case StringConstraint:
if actual, typeOK := field.actual.(string); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
case BoolConstraint:
if actual, typeOK := field.actual.(bool); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
default:
panic("unsupported constraint type")
}
dist += d
if !ok {
return 0, false
}
}
return dist, true
}
// VideoConstraints represents a video's constraints
type VideoConstraints struct {
Width, Height IntConstraint
FrameRate FloatConstraint
FrameFormat FrameFormatConstraint
DiscardFramesOlderThan time.Duration
}
// Video represents a video's constraints
type Video struct {
Width, Height int
FrameRate float32
FrameFormat frame.Format
DiscardFramesOlderThan time.Duration
}
// AudioConstraints represents an audio's constraints
type AudioConstraints struct {
ChannelCount IntConstraint
Latency DurationConstraint
SampleRate IntConstraint
SampleSize IntConstraint
IsBigEndian BoolConstraint
IsFloat BoolConstraint
IsInterleaved BoolConstraint
}
// Audio represents an audio's constraints
type Audio struct {
ChannelCount int
Latency time.Duration
SampleRate int
SampleSize int
IsBigEndian bool
IsFloat bool
IsInterleaved bool
}