Files
mediadevices/track.go
Lukas Herman c9b90fb233 Redesign codec
Resolves https://github.com/pion/mediadevices/issues/114

* Remove codec registrar
* Completely redesign how codec is being discovered, tuned, and built
* Update examples
* Update unit tests
2020-03-21 07:45:10 -04:00

288 lines
5.3 KiB
Go

package mediadevices
import (
"fmt"
"io"
"math/rand"
"sync"
"github.com/pion/mediadevices/pkg/driver"
mio "github.com/pion/mediadevices/pkg/io"
"github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v2/pkg/media"
)
// Tracker is an interface that represent MediaStreamTrack
// Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack
type Tracker interface {
Track() *webrtc.Track
LocalTrack() LocalTrack
Stop()
// OnEnded registers a handler to receive an error from the media stream track.
// If the error is already occured before registering, the handler will be
// immediately called.
OnEnded(func(error))
}
type LocalTrack interface {
WriteSample(s media.Sample) error
Codec() *webrtc.RTPCodec
ID() string
Kind() webrtc.RTPCodecType
}
type track struct {
t LocalTrack
s *sampler
onErrorHandler func(error)
err error
mu sync.Mutex
endOnce sync.Once
}
func newTrack(selectedCodec *webrtc.RTPCodec, trackGenerator TrackGenerator, d driver.Driver) (*track, error) {
if selectedCodec == nil {
panic("codec is required")
}
t, err := trackGenerator(
selectedCodec.PayloadType,
rand.Uint32(),
d.ID(),
selectedCodec.Type.String(),
selectedCodec,
)
if err != nil {
return nil, err
}
return &track{
t: t,
s: newSampler(t),
}, nil
}
func (t *track) OnEnded(handler func(error)) {
t.mu.Lock()
t.onErrorHandler = handler
err := t.err
t.mu.Unlock()
if err != nil && handler != nil {
// Already errored.
t.endOnce.Do(func() {
handler(err)
})
}
}
func (t *track) onError(err error) {
t.mu.Lock()
t.err = err
handler := t.onErrorHandler
t.mu.Unlock()
if handler != nil {
t.endOnce.Do(func() {
handler(err)
})
}
}
func (t *track) Track() *webrtc.Track {
return t.t.(*webrtc.Track)
}
func (t *track) LocalTrack() LocalTrack {
return t.t
}
type videoTrack struct {
*track
d driver.Driver
constraints MediaTrackConstraints
encoder io.ReadCloser
}
var _ Tracker = &videoTrack{}
func newVideoTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrackConstraints) (*videoTrack, error) {
err := d.Open()
if err != nil {
return nil, err
}
vr := d.(driver.VideoRecorder)
r, err := vr.VideoRecord(constraints.Media)
if err != nil {
return nil, err
}
if constraints.VideoTransform != nil {
r = constraints.VideoTransform(r)
}
var vt *videoTrack
rtpCodecs := opts.codecs[webrtc.RTPCodecTypeVideo]
for _, codecBuilder := range constraints.VideoEncoderBuilders {
var matchedRTPCodec *webrtc.RTPCodec
for _, rtpCodec := range rtpCodecs {
if rtpCodec.Name == codecBuilder.Name() {
matchedRTPCodec = rtpCodec
break
}
}
if matchedRTPCodec == nil {
continue
}
t, err := newTrack(matchedRTPCodec, opts.trackGenerator, d)
if err != nil {
continue
}
encoder, err := codecBuilder.BuildVideoEncoder(r, constraints.Media)
if err != nil {
continue
}
vt = &videoTrack{
track: t,
d: d,
constraints: constraints,
encoder: encoder,
}
break
}
if vt == nil {
d.Close()
return nil, fmt.Errorf("failed to find a matching video codec")
}
go vt.start()
return vt, nil
}
func (vt *videoTrack) start() {
var n int
var err error
buff := make([]byte, 1024)
for {
n, err = vt.encoder.Read(buff)
if err != nil {
if e, ok := err.(*mio.InsufficientBufferError); ok {
buff = make([]byte, 2*e.RequiredSize)
continue
}
vt.track.onError(err)
return
}
if err := vt.s.sample(buff[:n]); err != nil {
vt.track.onError(err)
return
}
}
}
func (vt *videoTrack) Stop() {
vt.d.Close()
vt.encoder.Close()
}
type audioTrack struct {
*track
d driver.Driver
constraints MediaTrackConstraints
encoder io.ReadCloser
}
var _ Tracker = &audioTrack{}
func newAudioTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrackConstraints) (*audioTrack, error) {
err := d.Open()
if err != nil {
return nil, err
}
ar := d.(driver.AudioRecorder)
r, err := ar.AudioRecord(constraints.Media)
if err != nil {
return nil, err
}
if constraints.AudioTransform != nil {
r = constraints.AudioTransform(r)
}
var at *audioTrack
rtpCodecs := opts.codecs[webrtc.RTPCodecTypeAudio]
for _, codecBuilder := range constraints.AudioEncoderBuilders {
var matchedRTPCodec *webrtc.RTPCodec
for _, rtpCodec := range rtpCodecs {
if rtpCodec.Name == codecBuilder.Name() {
matchedRTPCodec = rtpCodec
break
}
}
if matchedRTPCodec == nil {
continue
}
t, err := newTrack(matchedRTPCodec, opts.trackGenerator, d)
if err != nil {
continue
}
encoder, err := codecBuilder.BuildAudioEncoder(r, constraints.Media)
if err != nil {
continue
}
at = &audioTrack{
track: t,
d: d,
constraints: constraints,
encoder: encoder,
}
}
if at == nil {
d.Close()
return nil, fmt.Errorf("failed to find a matching audio codec")
}
go at.start()
return at, nil
}
func (t *audioTrack) start() {
buff := make([]byte, 1024)
sampleSize := uint32(float64(t.constraints.SampleRate) * t.constraints.Latency.Seconds())
for {
n, err := t.encoder.Read(buff)
if err != nil {
t.track.onError(err)
return
}
if err := t.t.WriteSample(media.Sample{
Data: buff[:n],
Samples: sampleSize,
}); err != nil {
t.track.onError(err)
return
}
}
}
func (t *audioTrack) Stop() {
t.d.Close()
t.encoder.Close()
}