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 }