Refractor, unify some APIs to be more DRY

This commit is contained in:
Lukas Herman
2020-02-05 22:47:57 -08:00
parent 2640f6c1f4
commit aece2b94c6
19 changed files with 362 additions and 489 deletions

View File

@@ -7,7 +7,6 @@ import (
"github.com/pion/mediadevices/examples/internal/signal" "github.com/pion/mediadevices/examples/internal/signal"
_ "github.com/pion/mediadevices/pkg/codec/openh264" // This is required to register h264 video encoder _ "github.com/pion/mediadevices/pkg/codec/openh264" // This is required to register h264 video encoder
_ "github.com/pion/mediadevices/pkg/codec/opus" // This is required to register opus audio encoder _ "github.com/pion/mediadevices/pkg/codec/opus" // This is required to register opus audio encoder
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
) )
@@ -32,20 +31,18 @@ func main() {
fmt.Printf("Connection State has changed %s \n", connectionState.String()) fmt.Printf("Connection State has changed %s \n", connectionState.String())
}) })
mediaDevices := mediadevices.NewMediaDevices(peerConnection) md := mediadevices.NewMediaDevices(peerConnection)
s, err := mediaDevices.GetUserMedia(mediadevices.MediaStreamConstraints{ s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
Audio: mediadevices.AudioTrackConstraints{ Audio: func(c *mediadevices.MediaTrackConstraints) {
Enabled: true, c.Codec = webrtc.Opus
Codec: webrtc.Opus, c.Enabled = true
}, },
Video: mediadevices.VideoTrackConstraints{ Video: func(c *mediadevices.MediaTrackConstraints) {
Enabled: true, c.Codec = webrtc.H264
Property: video.Property{ c.Enabled = true
Width: 800, // Optional. This is just an ideal value. c.Width = 800
Height: 480, // Optional. This is just an ideal value. c.Height = 480
},
Codec: webrtc.H264,
}, },
}) })
if err != nil { if err != nil {

View File

@@ -5,8 +5,7 @@ import (
"math" "math"
"github.com/pion/mediadevices/pkg/driver" "github.com/pion/mediadevices/pkg/driver"
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/prop"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
) )
@@ -33,8 +32,17 @@ func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaSt
// TODO: It should return media stream based on constraints // TODO: It should return media stream based on constraints
trackers := make([]Tracker, 0) trackers := make([]Tracker, 0)
if constraints.Video.Enabled { var videoConstraints, audioConstraints MediaTrackConstraints
tracker, err := m.videoSelect(constraints.Video) if constraints.Video != nil {
constraints.Video(&videoConstraints)
}
if constraints.Audio != nil {
constraints.Audio(&audioConstraints)
}
if videoConstraints.Enabled {
tracker, err := m.selectVideo(videoConstraints)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -42,8 +50,8 @@ func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaSt
trackers = append(trackers, tracker) trackers = append(trackers, tracker)
} }
if constraints.Audio.Enabled { if audioConstraints.Enabled {
tracker, err := m.audioSelect(constraints.Audio) tracker, err := m.selectAudio(audioConstraints)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -59,102 +67,76 @@ func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaSt
return s, nil return s, nil
} }
// videoSelect implements SelectSettings algorithm for video type. func queryDriverProperties(filter driver.FilterFn) map[driver.Driver][]prop.Media {
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings var needToClose []driver.Driver
func (m *mediaDevices) videoSelect(constraints VideoTrackConstraints) (Tracker, error) { drivers := driver.GetManager().Query(filter)
drivers := driver.GetManager().VideoDrivers() m := make(map[driver.Driver][]prop.Media)
var bestDriver driver.VideoDriver
var bestProp video.AdvancedProperty
minFitnessDist := math.Inf(1)
for _, d := range drivers { for _, d := range drivers {
wasClosed := d.Status() == driver.StateClosed if d.Status() == driver.StateClosed {
if wasClosed {
err := d.Open() err := d.Open()
if err != nil { if err != nil {
// Skip this driver if we failed to open because we can't get the settings // Skip this driver if we failed to open because we can't get the properties
continue continue
} }
needToClose = append(needToClose, d)
} }
vd := d.(driver.VideoDriver) m[d] = d.Properties()
for _, prop := range vd.Properties() { }
fitnessDist := constraints.fitnessDistance(prop)
for _, d := range needToClose {
// Since it was closed, we should close it to avoid a leak
d.Close()
}
return m
}
// select implements SelectSettings algorithm.
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings
func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) {
var bestDriver driver.Driver
var bestProp prop.Media
minFitnessDist := math.Inf(1)
driverProperties := queryDriverProperties(filter)
for d, props := range driverProperties {
for _, p := range props {
fitnessDist := constraints.Media.FitnessDistance(p)
if fitnessDist < minFitnessDist { if fitnessDist < minFitnessDist {
minFitnessDist = fitnessDist minFitnessDist = fitnessDist
bestDriver = vd bestDriver = d
bestProp = prop bestProp = p
} }
} }
if wasClosed {
// Since it was closed, we should close it to avoid a leak
d.Close()
}
} }
if bestDriver == nil { if bestDriver == nil {
return nil, fmt.Errorf("failed to find the best setting") return nil, MediaTrackConstraints{}, fmt.Errorf("failed to find the best driver that fits the constraints")
} }
if bestDriver.Status() == driver.StateClosed { bestConstraint := MediaTrackConstraints{
err := bestDriver.Open() Media: bestProp,
if err != nil { Enabled: true,
return nil, fmt.Errorf("failed in opening the best video driver") Codec: constraints.Codec,
}
} }
return newVideoTrack(m.pc, bestDriver, bestProp, constraints.Codec) return bestDriver, bestConstraint, nil
} }
// audioSelect implements SelectSettings algorithm for audio type. func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings d, c, err := selectBestDriver(driver.FilterAudioRecorder(), constraints)
func (m *mediaDevices) audioSelect(constraints AudioTrackConstraints) (Tracker, error) { if err != nil {
drivers := driver.GetManager().AudioDrivers() return nil, err
var bestDriver driver.AudioDriver
var bestProp audio.AdvancedProperty
minFitnessDist := math.Inf(1)
for _, d := range drivers {
wasClosed := d.Status() == driver.StateClosed
if wasClosed {
err := d.Open()
if err != nil {
// Skip this driver if we failed to open because we can't get the settings
continue
}
}
ad := d.(driver.AudioDriver)
for _, prop := range ad.Properties() {
fitnessDist := constraints.fitnessDistance(prop)
if fitnessDist < minFitnessDist {
minFitnessDist = fitnessDist
bestDriver = ad
bestProp = prop
}
}
if wasClosed {
// Since it was closed, we should close it to avoid a leak
d.Close()
}
} }
if bestDriver == nil { return newAudioTrack(m.pc, d, c)
return nil, fmt.Errorf("failed to find the best setting") }
} func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker, error) {
d, c, err := selectBestDriver(driver.FilterVideoRecorder(), constraints)
if bestDriver.Status() == driver.StateClosed { if err != nil {
err := bestDriver.Open() return nil, err
if err != nil { }
return nil, fmt.Errorf("failed in opening the best audio driver")
} return newVideoTrack(m.pc, d, c)
}
return newAudioTrack(m.pc, bestDriver, bestProp, constraints.Codec)
} }

View File

@@ -1,75 +1,19 @@
package mediadevices package mediadevices
import ( import (
"fmt" "github.com/pion/mediadevices/pkg/prop"
"math"
"strconv"
"github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video"
) )
type MediaStreamConstraints struct { type MediaStreamConstraints struct {
Audio AudioTrackConstraints Audio MediaOption
Video VideoTrackConstraints Video MediaOption
} }
type comparisons map[string]string // MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
type MediaTrackConstraints struct {
func (c comparisons) Add(actual, ideal interface{}) { prop.Media
c[fmt.Sprint(actual)] = fmt.Sprint(ideal)
}
// fitnessDistance is an implementation for https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
func (c comparisons) fitnessDistance() float64 {
var dist float64
for actual, ideal := range c {
if actual == ideal {
continue
}
actual, err1 := strconv.ParseFloat(actual, 64)
ideal, 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(actual-ideal) / math.Max(math.Abs(actual), math.Abs(ideal))
// If both of the values are not numeric, the only comparison value is either 1 (matched) or 0 (not matched)
case err1 != nil && err2 != nil:
dist++
// Comparing a numeric value with a non-numeric value is a an internal error, so panic.
default:
panic("fitnessDistance can't mix comparisons.")
}
}
return dist
}
type VideoTrackConstraints struct {
video.Property
Enabled bool Enabled bool
Codec string Codec string
} }
func (c *VideoTrackConstraints) fitnessDistance(prop video.AdvancedProperty) float64 { type MediaOption func(*MediaTrackConstraints)
cmps := comparisons{}
cmps.Add(prop.Width, c.Width)
cmps.Add(prop.Height, c.Height)
return cmps.fitnessDistance()
}
type AudioTrackConstraints struct {
audio.Property
Enabled bool
Codec string
}
func (c *AudioTrackConstraints) fitnessDistance(prop audio.AdvancedProperty) float64 {
cmps := comparisons{}
cmps.Add(prop.SampleRate, c.SampleRate)
cmps.Add(prop.Latency, c.Latency)
return cmps.fitnessDistance()
}

View File

@@ -5,7 +5,8 @@ import (
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
) )
type VideoEncoderBuilder func(r video.Reader, prop video.AdvancedProperty) (io.ReadCloser, error) type VideoEncoderBuilder func(r video.Reader, p prop.Video) (io.ReadCloser, error)
type AudioEncoderBuilder func(r audio.Reader, inProp, outProp audio.AdvancedProperty) (io.ReadCloser, error) type AudioEncoderBuilder func(r audio.Reader, p prop.Audio) (io.ReadCloser, error)

View File

@@ -18,6 +18,7 @@ import (
"github.com/pion/mediadevices/pkg/codec" "github.com/pion/mediadevices/pkg/codec"
mio "github.com/pion/mediadevices/pkg/io" mio "github.com/pion/mediadevices/pkg/io"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
) )
@@ -34,12 +35,12 @@ func init() {
codec.Register(webrtc.H264, codec.VideoEncoderBuilder(NewEncoder)) codec.Register(webrtc.H264, codec.VideoEncoderBuilder(NewEncoder))
} }
func NewEncoder(r video.Reader, prop video.AdvancedProperty) (io.ReadCloser, error) { func NewEncoder(r video.Reader, p prop.Video) (io.ReadCloser, error) {
cEncoder, err := C.enc_new(C.EncoderOptions{ cEncoder, err := C.enc_new(C.EncoderOptions{
width: C.int(prop.Width), width: C.int(p.Width),
height: C.int(prop.Height), height: C.int(p.Height),
target_bitrate: C.int(prop.BitRate), target_bitrate: C.int(p.BitRate),
max_fps: C.float(prop.FrameRate), max_fps: C.float(p.FrameRate),
}) })
if err != nil { if err != nil {
// TODO: better error message // TODO: better error message

View File

@@ -7,9 +7,9 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/faiface/beep"
"github.com/pion/mediadevices/pkg/codec" "github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
"gopkg.in/hraban/opus.v2" "gopkg.in/hraban/opus.v2"
) )
@@ -29,22 +29,18 @@ func init() {
codec.Register(webrtc.Opus, codec.AudioEncoderBuilder(NewEncoder)) codec.Register(webrtc.Opus, codec.AudioEncoderBuilder(NewEncoder))
} }
func NewEncoder(r audio.Reader, inProp, outProp audio.AdvancedProperty) (io.ReadCloser, error) { func NewEncoder(r audio.Reader, p prop.Audio) (io.ReadCloser, error) {
if inProp.SampleRate == 0 { if p.SampleRate == 0 {
return nil, fmt.Errorf("opus: inProp.SampleRate is required") return nil, fmt.Errorf("opus: inProp.SampleRate is required")
} }
if outProp.SampleRate == 0 { if p.Latency == 0 {
outProp.SampleRate = 48000 p.Latency = 20
}
if inProp.Latency == 0 {
inProp.Latency = 20
} }
// Select the nearest supported latency // Select the nearest supported latency
var targetLatency float64 var targetLatency float64
latencyInMS := float64(inProp.Latency.Milliseconds()) latencyInMS := float64(p.Latency.Milliseconds())
nearestDist := math.Inf(+1) nearestDist := math.Inf(+1)
for _, latency := range latencies { for _, latency := range latencies {
dist := math.Abs(latency - latencyInMS) dist := math.Abs(latency - latencyInMS)
@@ -59,20 +55,14 @@ func NewEncoder(r audio.Reader, inProp, outProp audio.AdvancedProperty) (io.Read
// Since audio.Reader only supports stereo mode, channels is always 2 // Since audio.Reader only supports stereo mode, channels is always 2
channels := 2 channels := 2
engine, err := opus.NewEncoder(outProp.SampleRate, channels, opus.AppVoIP) engine, err := opus.NewEncoder(p.SampleRate, channels, opus.AppVoIP)
if err != nil { if err != nil {
return nil, err return nil, err
} }
inBuffSize := targetLatency * float64(outProp.SampleRate) / 1000 inBuffSize := targetLatency * float64(p.SampleRate) / 1000
inBuff := make([][2]float32, int(inBuffSize)) inBuff := make([][2]float32, int(inBuffSize))
streamer := audio.ToBeep(r) e := encoder{engine, inBuff, r}
newSampleRate := beep.SampleRate(outProp.SampleRate)
oldSampleRate := beep.SampleRate(inProp.SampleRate)
streamer = beep.Resample(3, oldSampleRate, newSampleRate, streamer)
reader := audio.FromBeep(streamer)
e := encoder{engine, inBuff, reader}
return &e, nil return &e, nil
} }
@@ -91,16 +81,13 @@ func (e *encoder) Read(p []byte) (n int, err error) {
// While the buffer is not full, keep reading so that we meet the latency requirement // While the buffer is not full, keep reading so that we meet the latency requirement
for curN < len(e.inBuff) { for curN < len(e.inBuff) {
n, err := e.reader.Read(e.inBuff[curN:]) n, err = e.reader.Read(e.inBuff[curN:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
curN += n curN += n
} }
if err != nil {
return 0, err
}
n, err = e.engine.EncodeFloat32(flatten(e.inBuff), p) n, err = e.engine.EncodeFloat32(flatten(e.inBuff), p)
if err != nil { if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
) )
var ( var (
@@ -22,20 +23,20 @@ func Register(name string, builder interface{}) {
} }
} }
func BuildVideoEncoder(name string, r video.Reader, prop video.AdvancedProperty) (io.ReadCloser, error) { func BuildVideoEncoder(name string, r video.Reader, p prop.Video) (io.ReadCloser, error) {
b, ok := videoEncoders[name] b, ok := videoEncoders[name]
if !ok { if !ok {
return nil, fmt.Errorf("codec: can't find %s video encoder", name) return nil, fmt.Errorf("codec: can't find %s video encoder", name)
} }
return b(r, prop) return b(r, p)
} }
func BuildAudioEncoder(name string, r audio.Reader, inProp, outProp audio.AdvancedProperty) (io.ReadCloser, error) { func BuildAudioEncoder(name string, r audio.Reader, p prop.Audio) (io.ReadCloser, error) {
b, ok := audioEncoders[name] b, ok := audioEncoders[name]
if !ok { if !ok {
return nil, fmt.Errorf("codec: can't find %s audio encoder", name) return nil, fmt.Errorf("codec: can't find %s audio encoder", name)
} }
return b(r, inProp, outProp) return b(r, p)
} }

View File

@@ -10,6 +10,7 @@ import (
"github.com/blackjack/webcam" "github.com/blackjack/webcam"
"github.com/pion/mediadevices/pkg/frame" "github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
) )
// Camera implementation using v4l2 // Camera implementation using v4l2
@@ -19,11 +20,8 @@ type camera struct {
cam *webcam.Webcam cam *webcam.Webcam
formats map[webcam.PixelFormat]frame.Format formats map[webcam.PixelFormat]frame.Format
reversedFormats map[frame.Format]webcam.PixelFormat reversedFormats map[frame.Format]webcam.PixelFormat
properties []video.AdvancedProperty
} }
var _ VideoAdapter = &camera{}
func init() { func init() {
// TODO: Probably try to get more cameras // TODO: Probably try to get more cameras
// Get default camera // Get default camera
@@ -57,41 +55,27 @@ func (c *camera) Open() error {
return err return err
} }
properties := make([]video.AdvancedProperty, 0)
for format := range cam.GetSupportedFormats() {
for _, frameSize := range cam.GetSupportedFrameSizes(format) {
properties = append(properties, video.AdvancedProperty{
Property: video.Property{
Width: int(frameSize.MaxWidth),
Height: int(frameSize.MaxHeight),
},
FrameFormat: c.formats[format],
})
}
}
c.cam = cam c.cam = cam
c.properties = properties
return nil return nil
} }
func (c *camera) Close() error { func (c *camera) Close() error {
c.properties = nil
if c.cam == nil { if c.cam == nil {
return nil return nil
} }
return c.cam.StopStreaming() c.cam.StopStreaming()
return nil
} }
func (c *camera) Start(prop video.AdvancedProperty) (video.Reader, error) { func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
decoder, err := frame.NewDecoder(prop.FrameFormat) decoder, err := frame.NewDecoder(p.FrameFormat)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pf := c.reversedFormats[prop.FrameFormat] pf := c.reversedFormats[p.FrameFormat]
_, _, _, err = c.cam.SetImageFormat(pf, uint32(prop.Width), uint32(prop.Height)) _, _, _, err = c.cam.SetImageFormat(pf, uint32(p.Width), uint32(p.Height))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -124,23 +108,25 @@ func (c *camera) Start(prop video.AdvancedProperty) (video.Reader, error) {
continue continue
} }
return decoder.Decode(b, prop.Width, prop.Height) return decoder.Decode(b, p.Width, p.Height)
} }
}) })
return r, nil return r, nil
} }
func (c *camera) Stop() error { func (c *camera) Properties() []prop.Media {
return c.cam.StopStreaming() properties := make([]prop.Media, 0)
} for format := range c.cam.GetSupportedFormats() {
for _, frameSize := range c.cam.GetSupportedFrameSizes(format) {
func (c *camera) Info() Info { properties = append(properties, prop.Media{
return Info{ Video: prop.Video{
DeviceType: Camera, Width: int(frameSize.MaxWidth),
Height: int(frameSize.MaxHeight),
FrameFormat: c.formats[format],
},
})
}
} }
} return properties
func (c *camera) Properties() []video.AdvancedProperty {
return c.properties
} }

View File

@@ -1,8 +0,0 @@
package driver
type DeviceType string
const (
Camera DeviceType = "camera"
Microphone = "microphone"
)

View File

@@ -3,46 +3,21 @@ package driver
import ( import (
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
) )
type OpenCloser interface { type VideoRecorder interface {
Open() error VideoRecord(p prop.Media) (r video.Reader, err error)
Close() error
} }
type Infoer interface { type AudioRecorder interface {
Info() Info AudioRecord(p prop.Media) (r audio.Reader, err error)
}
type Info struct {
DeviceType DeviceType
}
type VideoCapable interface {
Start(prop video.AdvancedProperty) (video.Reader, error)
Stop() error
Properties() []video.AdvancedProperty
}
type AudioCapable interface {
Start(prop audio.AdvancedProperty) (audio.Reader, error)
Stop() error
Properties() []audio.AdvancedProperty
} }
type Adapter interface { type Adapter interface {
OpenCloser Open() error
Infoer Close() error
} Properties() []prop.Media
type VideoAdapter interface {
Adapter
VideoCapable
}
type AudioAdapter interface {
Adapter
AudioCapable
} }
type Driver interface { type Driver interface {
@@ -50,13 +25,3 @@ type Driver interface {
ID() string ID() string
Status() State Status() State
} }
type VideoDriver interface {
Driver
VideoCapable
}
type AudioDriver interface {
Driver
AudioCapable
}

View File

@@ -1,11 +1,25 @@
package driver package driver
import "fmt"
// FilterFn is being used to decide if a driver should be included in the // FilterFn is being used to decide if a driver should be included in the
// query result. // query result.
type FilterFn func(Driver) bool type FilterFn func(Driver) bool
// FilterVideoRecorder return a filter function to get a list of registered VideoRecorders
func FilterVideoRecorder() FilterFn {
return func(d Driver) bool {
_, ok := d.(VideoRecorder)
return ok
}
}
// FilterAudioRecorder return a filter function to get a list of registered AudioRecorders
func FilterAudioRecorder() FilterFn {
return func(d Driver) bool {
_, ok := d.(AudioRecorder)
return ok
}
}
// Manager is a singleton to manage multiple drivers and their states // Manager is a singleton to manage multiple drivers and their states
type Manager struct { type Manager struct {
drivers map[string]Driver drivers map[string]Driver
@@ -23,10 +37,6 @@ func GetManager() *Manager {
// Register registers adapter to be discoverable by Query // Register registers adapter to be discoverable by Query
func (m *Manager) Register(a Adapter) error { func (m *Manager) Register(a Adapter) error {
d := wrapAdapter(a) d := wrapAdapter(a)
if d == nil {
return fmt.Errorf("adapter has to be either VideoAdapter/AudioAdapter")
}
m.drivers[d.ID()] = d m.drivers[d.ID()] = d
return nil return nil
} }
@@ -42,19 +52,3 @@ func (m *Manager) Query(f FilterFn) []Driver {
return results return results
} }
// VideoDrivers gets a list of registered VideoDriver
func (m *Manager) VideoDrivers() []Driver {
return m.Query(func(d Driver) bool {
_, ok := d.(VideoDriver)
return ok
})
}
// AudioDrivers gets a list of registered AudioDriver
func (m *Manager) AudioDrivers() []Driver {
return m.Query(func(d Driver) bool {
_, ok := d.(AudioDriver)
return ok
})
}

View File

@@ -6,16 +6,14 @@ import (
"github.com/jfreymuth/pulse" "github.com/jfreymuth/pulse"
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/prop"
) )
type microphone struct { type microphone struct {
c *pulse.Client c *pulse.Client
s *pulse.RecordStream
samplesChan chan<- []float32 samplesChan chan<- []float32
} }
var _ AudioAdapter = &microphone{}
func init() { func init() {
GetManager().Register(&microphone{}) GetManager().Register(&microphone{})
} }
@@ -31,52 +29,30 @@ func (m *microphone) Open() error {
} }
func (m *microphone) Close() error { func (m *microphone) Close() error {
m.c.Close() if m.samplesChan != nil {
if m.s != nil { close(m.samplesChan)
m.s.Close() m.samplesChan = nil
} }
m.c.Close()
return nil return nil
} }
func (m *microphone) Start(prop audio.AdvancedProperty) (audio.Reader, error) { func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) {
var options []pulse.RecordOption var options []pulse.RecordOption
if prop.ChannelCount == 1 { if p.ChannelCount == 1 {
options = append(options, pulse.RecordMono) options = append(options, pulse.RecordMono)
} else { } else {
options = append(options, pulse.RecordStereo) options = append(options, pulse.RecordStereo)
} }
latency := prop.Latency.Seconds() latency := p.Latency.Seconds()
options = append(options, pulse.RecordSampleRate(prop.SampleRate), pulse.RecordLatency(latency)) options = append(options, pulse.RecordSampleRate(p.SampleRate), pulse.RecordLatency(latency))
samplesChan := make(chan []float32, 1) samplesChan := make(chan []float32, 1)
var buff []float32 var buff []float32
var bi int var bi int
var more bool var more bool
reader := audio.ReaderFunc(func(samples [][2]float32) (n int, err error) {
for i := range samples {
// if we don't have anything left in buff, we'll wait until we receive
// more samples
if bi == len(buff) {
buff, more = <-samplesChan
if !more {
return i, io.EOF
}
bi = 0
}
samples[i][0] = buff[bi]
if prop.ChannelCount == 2 {
samples[i][1] = buff[bi+1]
bi++
}
bi++
}
return len(samples), nil
})
handler := func(b []float32) { handler := func(b []float32) {
samplesChan <- b samplesChan <- b
} }
@@ -86,28 +62,39 @@ func (m *microphone) Start(prop audio.AdvancedProperty) (audio.Reader, error) {
return nil, err return nil, err
} }
reader := audio.ReaderFunc(func(samples [][2]float32) (n int, err error) {
for i := range samples {
// if we don't have anything left in buff, we'll wait until we receive
// more samples
if bi == len(buff) {
buff, more = <-samplesChan
if !more {
stream.Close()
return i, io.EOF
}
bi = 0
}
samples[i][0] = buff[bi]
if p.ChannelCount == 2 {
samples[i][1] = buff[bi+1]
bi++
}
bi++
}
return len(samples), nil
})
stream.Start() stream.Start()
m.s = stream
m.samplesChan = samplesChan m.samplesChan = samplesChan
return reader, nil return reader, nil
} }
func (m *microphone) Stop() error { func (m *microphone) Properties() []prop.Media {
close(m.samplesChan)
m.s.Stop()
return nil
}
func (m *microphone) Info() Info {
return Info{
DeviceType: Microphone,
}
}
func (m *microphone) Properties() []audio.AdvancedProperty {
// TODO: Get actual properties // TODO: Get actual properties
monoProp := audio.AdvancedProperty{ monoProp := prop.Media{
Property: audio.Property{ Audio: prop.Audio{
SampleRate: 48000, SampleRate: 48000,
Latency: time.Millisecond * 20, Latency: time.Millisecond * 20,
ChannelCount: 1, ChannelCount: 1,
@@ -117,5 +104,5 @@ func (m *microphone) Properties() []audio.AdvancedProperty {
stereoProp := monoProp stereoProp := monoProp
stereoProp.ChannelCount = 2 stereoProp.ChannelCount = 2
return []audio.AdvancedProperty{monoProp, stereoProp} return []prop.Media{monoProp, stereoProp}
} }

View File

@@ -3,22 +3,19 @@ package driver
import "fmt" import "fmt"
// State represents driver's state // State represents driver's state
type State uint type State string
const ( const (
// StateClosed means that the driver has not been opened. In this state, // StateClosed means that the driver has not been opened. In this state,
// all information related to the hardware are still unknown. For example, // all information related to the hardware are still unknown. For example,
// if it's a video driver, the pixel format information is still unknown. // if it's a video driver, the pixel format information is still unknown.
StateClosed State = iota StateClosed State = "closed"
// StateOpened means that the driver is already opened and information about // StateOpened means that the driver is already opened and information about
// the hardware are already known and may be extracted from the driver. // the hardware are already known and may be extracted from the driver.
StateOpened StateOpened = "opened"
// StateStarted means that the driver has been sending data. The caller // StateRunning means that the driver has been sending data. The caller
// who started the driver may start reading data from the hardware. // who started the driver may start reading data from the hardware.
StateStarted StateRunning = "running"
// StateStopped means that the driver is no longer sending data. In this state,
// information about the hardware is still available.
StateStopped
) )
// Update updates current state, s, to next. If f fails to execute, // Update updates current state, s, to next. If f fails to execute,
@@ -28,8 +25,7 @@ func (s *State) Update(next State, f func() error) error {
m := map[State]checkFunc{ m := map[State]checkFunc{
StateOpened: s.toOpened, StateOpened: s.toOpened,
StateClosed: s.toClosed, StateClosed: s.toClosed,
StateStarted: s.toStarted, StateRunning: s.toRunning,
StateStopped: s.toStopped,
} }
err := m[next]() err := m[next]()
@@ -55,21 +51,13 @@ func (s *State) toClosed() error {
return nil return nil
} }
func (s *State) toStarted() error { func (s *State) toRunning() error {
if *s == StateClosed { if *s == StateClosed {
return fmt.Errorf("invalid state: driver hasn't been opened") return fmt.Errorf("invalid state: driver is closed")
} }
if *s == StateStarted { if *s == StateRunning {
return fmt.Errorf("invalid state: driver has been started") return fmt.Errorf("invalid state: driver is already running")
}
return nil
}
func (s *State) toStopped() error {
if *s != StateStarted {
return fmt.Errorf("invalid state: driver hasn't been started")
} }
return nil return nil

26
pkg/driver/state_test.go Normal file
View File

@@ -0,0 +1,26 @@
package driver
import "testing"
var noop = func() error { return nil }
func TestUpdate1(t *testing.T) {
s := StateClosed
s.Update(StateOpened, noop)
if s != StateOpened {
t.Fatalf("expected %s, got %s", StateOpened, s)
}
s.Update(StateClosed, noop)
if s != StateClosed {
t.Fatalf("expected %s, got %s", StateClosed, s)
}
s.Update(StateOpened, noop)
if s != StateOpened {
t.Fatalf("expected %s, got %s", StateOpened, s)
}
}

View File

@@ -3,32 +3,39 @@ package driver
import ( import (
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
) )
func wrapAdapter(a Adapter) Driver { func wrapAdapter(a Adapter) Driver {
var d Driver
id := uuid.NewV4().String() id := uuid.NewV4().String()
wrapper := adapterWrapper{Adapter: a, id: id} d := &adapterWrapper{Adapter: a, id: id, state: StateClosed}
switch v := a.(type) { switch v := a.(type) {
case VideoCapable: case VideoRecorder:
d = &videoAdapterWrapper{ // Only expose Driver and VideoRecorder interfaces
adapterWrapper: &wrapper, d.VideoRecorder = v
VideoCapable: v, r := &struct {
} Driver
case AudioCapable: VideoRecorder
d = &audioAdapterWrapper{ }{d, d}
adapterWrapper: &wrapper, return r
AudioCapable: v, case AudioRecorder:
} // Only expose Driver and AudioRecorder interfaces
d.AudioRecorder = v
return &struct {
Driver
AudioRecorder
}{d, d}
default:
panic("adapter has to be either VideoRecorder/AudioRecorder")
} }
return d
} }
type adapterWrapper struct { type adapterWrapper struct {
Adapter Adapter
VideoRecorder
AudioRecorder
id string id string
state State state State
} }
@@ -49,53 +56,18 @@ func (w *adapterWrapper) Close() error {
return w.state.Update(StateClosed, w.Adapter.Close) return w.state.Update(StateClosed, w.Adapter.Close)
} }
// TODO: Add state validation func (w *adapterWrapper) VideoRecord(p prop.Media) (r video.Reader, err error) {
type videoAdapterWrapper struct { w.state.Update(StateRunning, func() error {
*adapterWrapper r, err = w.VideoRecorder.VideoRecord(p)
VideoCapable
}
func (w *videoAdapterWrapper) Start(prop video.AdvancedProperty) (r video.Reader, err error) {
w.state.Update(StateStarted, func() error {
r, err = w.VideoCapable.Start(prop)
return err return err
}) })
return return
} }
func (w *videoAdapterWrapper) Stop() error { func (w *adapterWrapper) AudioRecord(p prop.Media) (r audio.Reader, err error) {
return w.state.Update(StateStopped, w.VideoCapable.Stop) w.state.Update(StateRunning, func() error {
} r, err = w.AudioRecorder.AudioRecord(p)
func (w *videoAdapterWrapper) Properties() []video.AdvancedProperty {
if w.state == StateClosed {
return nil
}
return w.VideoCapable.Properties()
}
type audioAdapterWrapper struct {
*adapterWrapper
AudioCapable
}
func (w *audioAdapterWrapper) Start(prop audio.AdvancedProperty) (r audio.Reader, err error) {
w.state.Update(StateStarted, func() error {
r, err = w.AudioCapable.Start(prop)
return err return err
}) })
return return
} }
func (w *audioAdapterWrapper) Stop() error {
return w.state.Update(StateStopped, w.AudioCapable.Stop)
}
func (w *audioAdapterWrapper) Properties() []audio.AdvancedProperty {
if w.state == StateClosed {
return nil
}
return w.AudioCapable.Properties()
}

View File

@@ -1,16 +0,0 @@
package audio
import "time"
// Property represents an audio's basic properties
type Property struct {
ChannelCount int
Latency time.Duration
SampleRate int
SampleSize int
}
// AdvancedProperty represents an audio's advanced properties.
type AdvancedProperty struct {
Property
}

View File

@@ -1,16 +0,0 @@
package video
import "github.com/pion/mediadevices/pkg/frame"
// Property represents a video's basic properties
type Property struct {
Width, Height int
FrameRate float32
}
// AdvancedProperty represents a video's advanced properties.
type AdvancedProperty struct {
Property
FrameFormat frame.Format
BitRate int
}

74
pkg/prop/prop.go Normal file
View File

@@ -0,0 +1,74 @@
package prop
import (
"fmt"
"math"
"strconv"
"time"
"github.com/pion/mediadevices/pkg/frame"
)
type Media struct {
Video
Audio
}
func (p *Media) FitnessDistance(o Media) float64 {
cmps := comparisons{}
cmps.add(p.Width, o.Width)
cmps.add(p.Height, o.Height)
cmps.add(p.SampleRate, o.SampleRate)
cmps.add(p.Latency, o.Latency)
return cmps.fitnessDistance()
}
type comparisons map[string]string
func (c comparisons) add(actual, ideal interface{}) {
c[fmt.Sprint(actual)] = fmt.Sprint(ideal)
}
// fitnessDistance is an implementation for https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
func (c comparisons) fitnessDistance() float64 {
var dist float64
for actual, ideal := range c {
if actual == ideal {
continue
}
actual, err1 := strconv.ParseFloat(actual, 64)
ideal, 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(actual-ideal) / math.Max(math.Abs(actual), math.Abs(ideal))
// If both of the values are not numeric, the only comparison value is either 1 (matched) or 0 (not matched)
case err1 != nil && err2 != nil:
dist++
// Comparing a numeric value with a non-numeric value is a an internal error, so panic.
default:
panic("fitnessDistance can't mix comparisons.")
}
}
return dist
}
// Video represents a video's properties
type Video struct {
Width, Height int
FrameRate float32
FrameFormat frame.Format
BitRate int
}
// Audio represents an audio's properties
type Audio struct {
ChannelCount int
Latency time.Duration
SampleRate int
SampleSize int
}

View File

@@ -8,8 +8,6 @@ import (
"github.com/pion/mediadevices/pkg/codec" "github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/driver" "github.com/pion/mediadevices/pkg/driver"
mio "github.com/pion/mediadevices/pkg/io" mio "github.com/pion/mediadevices/pkg/io"
"github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/webrtc/v2" "github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v2/pkg/media" "github.com/pion/webrtc/v2/pkg/media"
) )
@@ -30,9 +28,9 @@ type track struct {
func newTrack(pc *webrtc.PeerConnection, d driver.Driver, codecName string) (*track, error) { func newTrack(pc *webrtc.PeerConnection, d driver.Driver, codecName string) (*track, error) {
var kind webrtc.RTPCodecType var kind webrtc.RTPCodecType
switch d.(type) { switch d.(type) {
case driver.VideoDriver: case driver.VideoRecorder:
kind = webrtc.RTPCodecTypeVideo kind = webrtc.RTPCodecTypeVideo
case driver.AudioDriver: case driver.AudioRecorder:
kind = webrtc.RTPCodecTypeAudio kind = webrtc.RTPCodecTypeAudio
} }
@@ -66,36 +64,43 @@ func (t *track) Track() *webrtc.Track {
type videoTrack struct { type videoTrack struct {
*track *track
d driver.VideoDriver d driver.Driver
property video.AdvancedProperty constraints MediaTrackConstraints
encoder io.ReadCloser encoder io.ReadCloser
} }
var _ Tracker = &videoTrack{} var _ Tracker = &videoTrack{}
func newVideoTrack(pc *webrtc.PeerConnection, d driver.VideoDriver, prop video.AdvancedProperty, codecName string) (*videoTrack, error) { func newVideoTrack(pc *webrtc.PeerConnection, d driver.Driver, constraints MediaTrackConstraints) (*videoTrack, error) {
codecName := constraints.Codec
t, err := newTrack(pc, d, codecName) t, err := newTrack(pc, d, codecName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r, err := d.Start(prop) err = d.Open()
if err != nil {
return nil, err
}
vr := d.(driver.VideoRecorder)
r, err := vr.VideoRecord(constraints.Media)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: Remove hardcoded bitrate // TODO: Remove hardcoded bitrate
prop.BitRate = 100000 constraints.BitRate = 100000
encoder, err := codec.BuildVideoEncoder(codecName, r, prop) encoder, err := codec.BuildVideoEncoder(codecName, r, constraints.Video)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vt := videoTrack{ vt := videoTrack{
track: t, track: t,
d: d, d: d,
property: prop, constraints: constraints,
encoder: encoder, encoder: encoder,
} }
go vt.start() go vt.start()
@@ -123,44 +128,47 @@ func (vt *videoTrack) start() {
} }
func (vt *videoTrack) Stop() { func (vt *videoTrack) Stop() {
vt.d.Stop() vt.d.Close()
vt.encoder.Close() vt.encoder.Close()
} }
type audioTrack struct { type audioTrack struct {
*track *track
d driver.AudioDriver d driver.Driver
property audio.AdvancedProperty constraints MediaTrackConstraints
encoder io.ReadCloser encoder io.ReadCloser
} }
var _ Tracker = &audioTrack{} var _ Tracker = &audioTrack{}
func newAudioTrack(pc *webrtc.PeerConnection, d driver.AudioDriver, prop audio.AdvancedProperty, codecName string) (*audioTrack, error) { func newAudioTrack(pc *webrtc.PeerConnection, d driver.Driver, constraints MediaTrackConstraints) (*audioTrack, error) {
codecName := constraints.Codec
t, err := newTrack(pc, d, codecName) t, err := newTrack(pc, d, codecName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
reader, err := d.Start(prop) err = d.Open()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: Not sure how to decide inProp and outProp ar := d.(driver.AudioRecorder)
inProp := prop reader, err := ar.AudioRecord(constraints.Media)
outProp := prop if err != nil {
return nil, err
}
encoder, err := codec.BuildAudioEncoder(codecName, reader, inProp, outProp) encoder, err := codec.BuildAudioEncoder(codecName, reader, constraints.Audio)
if err != nil { if err != nil {
return nil, err return nil, err
} }
at := audioTrack{ at := audioTrack{
track: t, track: t,
d: d, d: d,
property: prop, constraints: constraints,
encoder: encoder, encoder: encoder,
} }
go at.start() go at.start()
return &at, nil return &at, nil
@@ -168,7 +176,7 @@ func newAudioTrack(pc *webrtc.PeerConnection, d driver.AudioDriver, prop audio.A
func (t *audioTrack) start() { func (t *audioTrack) start() {
buff := make([]byte, 1024) buff := make([]byte, 1024)
sampleSize := uint32(float64(t.property.SampleRate) * t.property.Latency.Seconds()) sampleSize := uint32(float64(t.constraints.SampleRate) * t.constraints.Latency.Seconds())
for { for {
n, err := t.encoder.Read(buff) n, err := t.encoder.Read(buff)
if err != nil { if err != nil {
@@ -184,6 +192,6 @@ func (t *audioTrack) start() {
} }
func (t *audioTrack) Stop() { func (t *audioTrack) Stop() {
t.d.Stop() t.d.Close()
t.encoder.Close() t.encoder.Close()
} }