mirror of
https://github.com/pion/mediadevices.git
synced 2025-10-11 03:20:15 +08:00
Refractor, unify some APIs to be more DRY
This commit is contained in:
@@ -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 {
|
||||||
|
144
mediadevices.go
144
mediadevices.go
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
|
||||||
}
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
package driver
|
|
||||||
|
|
||||||
type DeviceType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Camera DeviceType = "camera"
|
|
||||||
Microphone = "microphone"
|
|
||||||
)
|
|
@@ -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
|
|
||||||
}
|
|
||||||
|
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@@ -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 = µphone{}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
GetManager().Register(µphone{})
|
GetManager().Register(µphone{})
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
@@ -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
26
pkg/driver/state_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
|
||||||
}
|
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -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
74
pkg/prop/prop.go
Normal 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
|
||||||
|
}
|
70
track.go
70
track.go
@@ -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()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user