Repository: pion/mediadevices
Pull request #229 (2020-10-10 07:11:23)

Use MediaDeviceInfo instead of webrtc.RTPCodecType
Changes:
  * Add unit tests for mediastream
  * Remove webrtc.RTPCodecType dependency in mediastream
  * Add Kind to Tracker interface


codecov[bot]: # [Codecov](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=h1) Report
> Merging [#229](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=desc) into [master](https://codecov.io/gh/pion/mediadevices/commit/abdd96e6b2103a3106b8e3118ee3296ea7eca790?el=desc) will **increase** coverage by `0.16%`.
> The diff coverage is `80.00%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/mediadevices/pull/229/graphs/tree.svg?width=650&height=150&src=pr&token=tEFGHRPDQ7)](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #229      +/-   ##
==========================================
+ Coverage   60.44%   60.61%   +0.16%     
==========================================
  Files          48       48              
  Lines        1924     1927       +3     
==========================================
+ Hits         1163     1168       +5     
+ Misses        684      682       -2     
  Partials       77       77              
```


| [Impacted Files](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [mediastream.go](https://codecov.io/gh/pion/mediadevices/pull/229/diff?src=pr&el=tree#diff-bWVkaWFzdHJlYW0uZ28=) | `61.90% <70.00%> (+9.73%)` | :arrow_up: |
| [track.go](https://codecov.io/gh/pion/mediadevices/pull/229/diff?src=pr&el=tree#diff-dHJhY2suZ28=) | `74.76% <100.00%> (+0.25%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=footer). Last update [abdd96e...99a47e5](https://codecov.io/gh/pion/mediadevices/pull/229?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/mediadeviceinfo.go b/mediadeviceinfo.go
index 21c119e..ce6c4fa 100644
--- a/mediadeviceinfo.go
+++ b/mediadeviceinfo.go
@@ -7,7 +7,7 @@ type MediaDeviceType int
 
 // MediaDeviceType definitions.
 const (
-	VideoInput MediaDeviceType = iota
+	VideoInput MediaDeviceType = iota + 1
 	AudioInput
 	AudioOutput
 )
diff --git a/mediastream.go b/mediastream.go
index d852090..6b99239 100644
--- a/mediastream.go
+++ b/mediastream.go
@@ -2,8 +2,6 @@ package mediadevices
 
 import (
 	"sync"
-
-	"github.com/pion/webrtc/v2"
 )
 
 // MediaStream is an interface that represents a collection of existing tracks.
@@ -21,21 +19,20 @@ type MediaStream interface {
 }
 
 type mediaStream struct {
-	trackers map[string]Tracker
+	trackers map[Tracker]struct{}
 	l        sync.RWMutex
 }
 
-const rtpCodecTypeDefault webrtc.RTPCodecType = 0
+const trackTypeDefault MediaDeviceType = 0
 
 // NewMediaStream creates a MediaStream interface that's defined in
 // https://w3c.github.io/mediacapture-main/#dom-mediastream
 func NewMediaStream(trackers ...Tracker) (MediaStream, error) {
-	m := mediaStream{trackers: make(map[string]Tracker)}
+	m := mediaStream{trackers: make(map[Tracker]struct{})}
 
 	for _, tracker := range trackers {
-		id := tracker.LocalTrack().ID()
-		if _, ok := m.trackers[id]; !ok {
-			m.trackers[id] = tracker
+		if _, ok := m.trackers[tracker]; !ok {
+			m.trackers[tracker] = struct{}{}
 		}
 	}
 
@@ -43,26 +40,26 @@ func NewMediaStream(trackers ...Tracker) (MediaStream, error) {
 }
 
 func (m *mediaStream) GetAudioTracks() []Tracker {
-	return m.queryTracks(webrtc.RTPCodecTypeAudio)
+	return m.queryTracks(AudioInput)
 }
 
 func (m *mediaStream) GetVideoTracks() []Tracker {
-	return m.queryTracks(webrtc.RTPCodecTypeVideo)
+	return m.queryTracks(VideoInput)
 }
 
 func (m *mediaStream) GetTracks() []Tracker {
-	return m.queryTracks(rtpCodecTypeDefault)
+	return m.queryTracks(trackTypeDefault)
 }
 
 // queryTracks returns all tracks that are the same kind as t.
 // If t is 0, which is the default, queryTracks will return all the tracks.
-func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Tracker {
+func (m *mediaStream) queryTracks(t MediaDeviceType) []Tracker {
 	m.l.RLock()
 	defer m.l.RUnlock()
 
 	result := make([]Tracker, 0)
-	for _, tracker := range m.trackers {
-		if tracker.LocalTrack().Kind() == t || t == rtpCodecTypeDefault {
+	for tracker := range m.trackers {
+		if tracker.Kind() == t || t == trackTypeDefault {
 			result = append(result, tracker)
 		}
 	}
@@ -74,17 +71,16 @@ func (m *mediaStream) AddTrack(t Tracker) {
 	m.l.Lock()
 	defer m.l.Unlock()
 
-	id := t.LocalTrack().ID()
-	if _, ok := m.trackers[id]; ok {
+	if _, ok := m.trackers[t]; ok {
 		return
 	}
 
-	m.trackers[id] = t
+	m.trackers[t] = struct{}{}
 }
 
 func (m *mediaStream) RemoveTrack(t Tracker) {
 	m.l.Lock()
 	defer m.l.Unlock()
 
-	delete(m.trackers, t.LocalTrack().ID())
+	delete(m.trackers, t)
 }
diff --git a/pkg/io/audio/detect.go b/pkg/io/audio/detect.go
new file mode 100644
index 0000000..454e5d6
--- /dev/null
+++ b/pkg/io/audio/detect.go
@@ -0,0 +1,46 @@
+package audio
+
+import (
+	"time"
+
+	"github.com/pion/mediadevices/pkg/prop"
+	"github.com/pion/mediadevices/pkg/wave"
+)
+
+// DetectChanges will detect chunk and audio property changes. For audio property detection,
+// since it's time related, interval will be used to determine the sample rate.
+func DetectChanges(interval time.Duration, onChange func(prop.Media)) TransformFunc {
+	return func(r Reader) Reader {
+		var currentProp prop.Media
+		var chunkCount uint
+		return ReaderFunc(func() (wave.Audio, error) {
+			var dirty bool
+
+			chunk, err := r.Read()
+			if err != nil {
+				return nil, err
+			}
+
+			info := chunk.ChunkInfo()
+			if currentProp.ChannelCount != info.Channels {
+				currentProp.ChannelCount = info.Channels
+				dirty = true
+			}
+
+			if currentProp.SampleRate != info.SamplingRate {
+				currentProp.SampleRate = info.SamplingRate
+				dirty = true
+			}
+
+			// TODO: Also detect sample format changes?
+			// TODO: Add audio detect changes. As of now, there's no useful property to track.
+
+			if dirty {
+				onChange(currentProp)
+			}
+
+			chunkCount++
+			return chunk, nil
+		})
+	}
+}
diff --git a/track.go b/track.go
index 74cba6f..8ca69ce 100644
--- a/track.go
+++ b/track.go
@@ -18,6 +18,7 @@ type Tracker interface {
 	Track() *webrtc.Track
 	LocalTrack() LocalTrack
 	Stop()
+	Kind() MediaDeviceType
 	// 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.
@@ -41,12 +42,14 @@ type track struct {
 	err            error
 	mu             sync.Mutex
 	endOnce        sync.Once
+	kind           MediaDeviceType
 }
 
 func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrackConstraints) (*track, error) {
 	var encoderBuilders []encoderBuilder
 	var rtpCodecs []*webrtc.RTPCodec
 	var buildSampler func(t LocalTrack) samplerFunc
+	var kind MediaDeviceType
 	var err error
 
 	err = d.Open()
@@ -56,10 +59,12 @@ func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrack
 
 	switch r := d.(type) {
 	case driver.VideoRecorder:
+		kind = VideoInput
 		rtpCodecs = opts.codecs[webrtc.RTPCodecTypeVideo]
 		buildSampler = newVideoSampler
 		encoderBuilders, err = newVideoEncoderBuilders(r, constraints)
 	case driver.AudioRecorder:
+		kind = AudioInput
 		rtpCodecs = opts.codecs[webrtc.RTPCodecTypeAudio]
 		buildSampler = func(t LocalTrack) samplerFunc {
 			return newAudioSampler(t, constraints.selectedMedia.Latency)
@@ -108,6 +113,7 @@ func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrack
 			sample:     buildSampler(localTrack),
 			d:          d,
 			encoder:    encoder,
+			kind:       kind,
 		}
 		go t.start()
 		return &t, nil
@@ -117,6 +123,11 @@ func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrack
 	return nil, errors.New("newTrack: failed to find a matching codec")
 }
 
+// Kind returns track's kind
+func (t *track) Kind() MediaDeviceType {
+	return t.kind
+}
+
 // OnEnded sets an error handler. When a track has been created and started, if an
 // error occurs, handler will get called with the error given to the parameter.
 func (t *track) OnEnded(handler func(error)) {


Test patch:
diff --git a/mediastream_test.go b/mediastream_test.go
new file mode 100644
index 0000000..c86b28b
--- /dev/null
+++ b/mediastream_test.go
@@ -0,0 +1,83 @@
+package mediadevices
+
+import (
+	"testing"
+
+	"github.com/pion/webrtc/v2"
+)
+
+type mockMediaStreamTrack struct {
+	kind MediaDeviceType
+}
+
+func (track *mockMediaStreamTrack) Track() *webrtc.Track {
+	return nil
+}
+
+func (track *mockMediaStreamTrack) LocalTrack() LocalTrack {
+	return nil
+}
+
+func (track *mockMediaStreamTrack) Stop() {
+}
+
+func (track *mockMediaStreamTrack) Kind() MediaDeviceType {
+	return track.kind
+}
+
+func (track *mockMediaStreamTrack) OnEnded(handler func(error)) {
+}
+
+func TestMediaStreamFilters(t *testing.T) {
+	audioTracks := []Tracker{
+		&mockMediaStreamTrack{AudioInput},
+		&mockMediaStreamTrack{AudioInput},
+		&mockMediaStreamTrack{AudioInput},
+		&mockMediaStreamTrack{AudioInput},
+		&mockMediaStreamTrack{AudioInput},
+	}
+
+	videoTracks := []Tracker{
+		&mockMediaStreamTrack{VideoInput},
+		&mockMediaStreamTrack{VideoInput},
+		&mockMediaStreamTrack{VideoInput},
+	}
+
+	tracks := append(audioTracks, videoTracks...)
+	stream, err := NewMediaStream(tracks...)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expect := func(t *testing.T, actual, expected []Tracker) {
+		if len(actual) != len(expected) {
+			t.Fatalf("%s: Expected to get %d trackers, but got %d trackers", t.Name(), len(expected), len(actual))
+		}
+
+		for _, a := range actual {
+			found := false
+			for _, e := range expected {
+				if e == a {
+					found = true
+					break
+				}
+			}
+
+			if !found {
+				t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
+			}
+		}
+	}
+
+	t.Run("GetAudioTracks", func(t *testing.T) {
+		expect(t, stream.GetAudioTracks(), audioTracks)
+	})
+
+	t.Run("GetVideoTracks", func(t *testing.T) {
+		expect(t, stream.GetVideoTracks(), videoTracks)
+	})
+
+	t.Run("GetTracks", func(t *testing.T) {
+		expect(t, stream.GetTracks(), tracks)
+	})
+}
diff --git a/pkg/io/audio/detect_test.go b/pkg/io/audio/detect_test.go
new file mode 100644
index 0000000..450e402
--- /dev/null
+++ b/pkg/io/audio/detect_test.go
@@ -0,0 +1,77 @@
+package audio
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/pion/mediadevices/pkg/prop"
+	"github.com/pion/mediadevices/pkg/wave"
+)
+
+func TestDetectChanges(t *testing.T) {
+	buildSource := func(p prop.Media) (Reader, func(prop.Media)) {
+		return ReaderFunc(func() (wave.Audio, error) {
+				return wave.NewFloat32Interleaved(wave.ChunkInfo{
+					Len:          0,
+					Channels:     p.ChannelCount,
+					SamplingRate: p.SampleRate,
+				}), nil
+			}), func(newProp prop.Media) {
+				p = newProp
+			}
+	}
+
+	t.Run("OnChangeCalledBeforeFirstFrame", func(t *testing.T) {
+		var detectBeforeFirstChunk bool
+		var expected prop.Media
+		var actual prop.Media
+		expected.ChannelCount = 2
+		expected.SampleRate = 48000
+		src, _ := buildSource(expected)
+		src = DetectChanges(time.Second, func(p prop.Media) {
+			actual = p
+			detectBeforeFirstChunk = true
+		})(src)
+
+		_, err := src.Read()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if !detectBeforeFirstChunk {
+			t.Fatal("on change callback should have called before first chunk")
+		}
+
+		if !reflect.DeepEqual(actual, expected) {
+			t.Fatalf("Received an unexpected prop\nExpected:\n%v\nActual:\n%v\n", expected, actual)
+		}
+	})
+
+	t.Run("DetectChangesOnEveryUpdate", func(t *testing.T) {
+		var expected prop.Media
+		var actual prop.Media
+		expected.ChannelCount = 2
+		expected.SampleRate = 48000
+		src, update := buildSource(expected)
+		src = DetectChanges(time.Second, func(p prop.Media) {
+			actual = p
+		})(src)
+
+		for channelCount := 1; channelCount < 8; channelCount++ {
+			for sampleRate := 12000; sampleRate <= 48000; sampleRate += 4000 {
+				expected.ChannelCount = channelCount
+				expected.SampleRate = sampleRate
+				update(expected)
+				_, err := src.Read()
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				if !reflect.DeepEqual(actual, expected) {
+					t.Fatalf("Received an unexpected prop\nExpected:\n%v\nActual:\n%v\n", expected, actual)
+				}
+			}
+		}
+	})
+}