Compare commits

..

12 Commits

Author SHA1 Message Date
Lukas Herman
25a1bf5a16 Convert underlying wave buffer to be uint8
This follows image package from the standard library.
By having a homogenous data type for storing the samples,
it makes easier to manipulate the raw data in a generic way.
2020-09-24 23:54:09 -07:00
Renovate Bot
a44240be5f Update module pion/webrtc/v2 to v2.2.26
Generated by Renovate Bot
2020-09-21 13:00:01 -07:00
Lukas Herman
70f7360b92 Enhance failed to find driver error message 2020-09-11 12:39:48 -04:00
Lukas Herman
30d49e1fd3 Add human friendly string implementation 2020-09-11 12:39:48 -04:00
Lukas Herman
0cd870fd4b Add generic FrameBuffer 2020-09-07 00:33:25 -04:00
Lukas Herman
13e6dcc437 Remove redundant comments
From pkg.go.dev or godoc, the removed comments are not necessary
as they won't get rendered or goes without saying.
2020-09-06 23:59:28 -04:00
Lukas Herman
366885e01c Hide DecoderFunc
Since DecoderFunc is not being used as a public API, there's no need
to increase the API surface area.
2020-09-06 23:59:28 -04:00
Lukas Herman
86e3a3f14c Update CI to use Go 1.15 and 1.14 2020-09-03 00:12:25 -04:00
Renovate Bot
b4c11d5a0c Update golang.org/x/sys commit hash to 196b9ba
Generated by Renovate Bot
2020-08-31 21:04:03 -04:00
Renovate Bot
18da7ff1c6 Update module pion/webrtc/v2 to v2.2.24
Generated by Renovate Bot
2020-08-23 18:25:51 -07:00
Lukas Herman
f7068296d3 Add V4L2_PIX_FMT_YUV420 support for Linux 2020-08-19 23:09:29 -07:00
Renovate Bot
6d07cc2a58 Update github.com/jfreymuth/pulse commit hash to a82ccdb
Generated by Renovate Bot
2020-08-18 11:41:34 +09:00
47 changed files with 2086 additions and 781 deletions

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ '1.14', '1.13' ]
go: [ '1.15', '1.14' ]
name: Linux Go ${{ matrix.go }}
steps:
- name: Checkout
@@ -47,7 +47,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
go: [ '1.14', '1.13' ]
go: [ '1.15', '1.14' ]
name: Darwin Go ${{ matrix.go }}
steps:
- name: Checkout

View File

@@ -0,0 +1,29 @@
## Instructions
### Download facedetection
```
go get github.com/pion/mediadevices/examples/facedetection
```
### Open example page
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/video) you should see two text-areas and a 'Start Session' button
### Run facedetection with your browsers SessionDescription as stdin
In the jsfiddle the top textarea is your browser, copy that and:
#### Linux
Run `echo $BROWSER_SDP | facedetection`
### Input facedetection's SessionDescription into your browser
Copy the text that `facedetection` just emitted and copy into second text area
### Hit 'Start Session' in jsfiddle, enjoy your video!
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
Congrats, you have used pion-WebRTC! Now start building something cool

View File

@@ -0,0 +1,118 @@
package main
import (
"image"
"image/color"
"image/draw"
"io/ioutil"
"log"
"github.com/disintegration/imaging"
pigo "github.com/esimov/pigo/core"
)
var (
cascade []byte
err error
classifier *pigo.Pigo
)
func imgToGrayscale(img image.Image) []uint8 {
bounds := img.Bounds()
flatten := bounds.Dy() * bounds.Dx()
grayImg := make([]uint8, flatten)
i := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
pix := img.At(x, y)
grayPix := color.GrayModel.Convert(pix).(color.Gray)
grayImg[i] = grayPix.Y
i++
}
}
return grayImg
}
// clusterDetection runs Pigo face detector core methods
// and returns a cluster with the detected faces coordinates.
func clusterDetection(img image.Image) []pigo.Detection {
grayscale := imgToGrayscale(img)
bounds := img.Bounds()
cParams := pigo.CascadeParams{
MinSize: 100,
MaxSize: 600,
ShiftFactor: 0.15,
ScaleFactor: 1.1,
ImageParams: pigo.ImageParams{
Pixels: grayscale,
Rows: bounds.Dy(),
Cols: bounds.Dx(),
Dim: bounds.Dx(),
},
}
if len(cascade) == 0 {
cascade, err = ioutil.ReadFile("facefinder")
if err != nil {
log.Fatalf("Error reading the cascade file: %s", err)
}
p := pigo.NewPigo()
// Unpack the binary file. This will return the number of cascade trees,
// the tree depth, the threshold and the prediction from tree's leaf nodes.
classifier, err = p.Unpack(cascade)
if err != nil {
log.Fatalf("Error unpacking the cascade file: %s", err)
}
}
// Run the classifier over the obtained leaf nodes and return the detection results.
// The result contains quadruplets representing the row, column, scale and detection score.
dets := classifier.RunCascade(cParams, 0.0)
// Calculate the intersection over union (IoU) of two clusters.
dets = classifier.ClusterDetections(dets, 0)
return dets
}
func drawCircle(img draw.Image, x0, y0, r int, c color.Color) {
x, y, dx, dy := r-1, 0, 1, 1
err := dx - (r * 2)
for x > y {
img.Set(x0+x, y0+y, c)
img.Set(x0+y, y0+x, c)
img.Set(x0-y, y0+x, c)
img.Set(x0-x, y0+y, c)
img.Set(x0-x, y0-y, c)
img.Set(x0-y, y0-x, c)
img.Set(x0+y, y0-x, c)
img.Set(x0+x, y0-y, c)
if err <= 0 {
y++
err += dy
dy += 2
}
if err > 0 {
x--
dx += 2
err += dx - (r * 2)
}
}
}
func markFaces(img image.Image) image.Image {
nrgba := imaging.Clone(img)
dets := clusterDetection(img)
for _, det := range dets {
if det.Q < 5.0 {
continue
}
drawCircle(nrgba, det.Col, det.Row, det.Scale/2, color.Black)
}
return nrgba
}

Binary file not shown.

View File

@@ -0,0 +1,119 @@
package main
import (
"fmt"
"image"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/examples/internal/signal"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v2"
)
func markFacesTransformer(r video.Reader) video.Reader {
return video.ReaderFunc(func() (img image.Image, err error) {
img, err = r.Read()
if err != nil {
return
}
img = markFaces(img)
return
})
}
func main() {
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Create a new RTCPeerConnection
mediaEngine := webrtc.MediaEngine{}
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
panic(err)
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(config)
if err != nil {
panic(err)
}
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
md := mediadevices.NewMediaDevices(peerConnection)
vp8Params, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vp8Params.BitRate = 100000 // 100kbps
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormatExact(frame.FormatI420) // most of the encoder accepts I420
c.Enabled = true
c.Width = prop.Int(640)
c.Height = prop.Int(480)
c.VideoTransform = markFacesTransformer
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
},
})
if err != nil {
panic(err)
}
for _, tracker := range s.GetTracks() {
t := tracker.Track()
tracker.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
t.ID(), t.Label(), err)
})
_, err = peerConnection.AddTransceiverFromTrack(t,
webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
},
)
if err != nil {
panic(err)
}
}
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)
if err != nil {
panic(err)
}
// Create an answer
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
panic(err)
}
// Output the answer in base64 so we can paste it in browser
fmt.Println(signal.Encode(answer))
select {}
}

View File

@@ -0,0 +1,29 @@
## Instructions
### Download rtp-send example
```
go get github.com/pion/mediadevices/examples/rtp-send
```
### Listen RTP
Install GStreamer and run:
```
gst-launch-1.0 udpsrc port=5000 caps=application/x-rtp,encode-name=VP8 \
! rtpvp8depay ! vp8dec ! videoconvert ! autovideosink
```
Or run VLC media plyer:
```
vlc ./vp8.sdp
```
### Run rtp-send
Run `rtp-send localhost:5000`
A video should start playing in your GStreamer or VLC window.
It's not WebRTC, but pure RTP.
Congrats, you have used pion-MediaDevices! Now start building something cool

120
examples/rtp-send/main.go Normal file
View File

@@ -0,0 +1,120 @@
package main
import (
"fmt"
"net"
"os"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/rtp"
"github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v2/pkg/media"
)
const (
mtu = 1000
)
func main() {
if len(os.Args) != 2 {
fmt.Printf("usage: %s host:port\n", os.Args[0])
return
}
md := mediadevices.NewMediaDevicesFromCodecs(
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
webrtc.NewRTPVP8Codec(100, 90000),
},
},
mediadevices.WithTrackGenerator(
func(_ uint8, _ uint32, id, _ string, codec *webrtc.RTPCodec) (
mediadevices.LocalTrack, error,
) {
return newTrack(codec, id, os.Args[1]), nil
},
),
)
vp8Params, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vp8Params.BitRate = 100000 // 100kbps
_, err = md.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Enabled = true
c.Width = prop.Int(640)
c.Height = prop.Int(480)
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
},
})
if err != nil {
panic(err)
}
select {}
}
type track struct {
codec *webrtc.RTPCodec
packetizer rtp.Packetizer
id string
conn net.Conn
}
func newTrack(codec *webrtc.RTPCodec, id, dest string) *track {
addr, err := net.ResolveUDPAddr("udp", dest)
if err != nil {
panic(err)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
panic(err)
}
return &track{
codec: codec,
packetizer: rtp.NewPacketizer(
mtu,
codec.PayloadType,
1,
codec.Payloader,
rtp.NewRandomSequencer(),
codec.ClockRate,
),
id: id,
conn: conn,
}
}
func (t *track) WriteSample(s media.Sample) error {
buf := make([]byte, mtu)
pkts := t.packetizer.Packetize(s.Data, s.Samples)
for _, p := range pkts {
n, err := p.MarshalTo(buf)
if err != nil {
panic(err)
}
_, _ = t.conn.Write(buf[:n])
}
return nil
}
func (t *track) Codec() *webrtc.RTPCodec {
return t.codec
}
func (t *track) ID() string {
return t.id
}
func (t *track) Kind() webrtc.RTPCodecType {
return t.codec.Type
}

View File

@@ -0,0 +1,9 @@
v=0
o=- 1234567890 1234567890 IN IP4 0.0.0.0
s=RTP-Send Example
i=Example
c=IN IP4 0.0.0.0
t=0 0
a=recvonly
m=video 5000 RTP/AVP 100
a=rtpmap:100 VP8/90000

View File

@@ -0,0 +1,29 @@
## Instructions
### Download screenshare
```
go get github.com/pion/mediadevices/examples/screenshare
```
### Open example page
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
### Run screenshare with your browsers SessionDescription as stdin
In the jsfiddle the top textarea is your browser, copy that and:
#### Linux
Run `echo $BROWSER_SDP | screenshare`
### Input screenshare's SessionDescription into your browser
Copy the text that `screenshare` just emitted and copy into second text area
### Hit 'Start Session' in jsfiddle, enjoy your video!
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
Congrats, you have used pion-WebRTC! Now start building something cool

View File

@@ -0,0 +1,101 @@
package main
import (
"fmt"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/examples/internal/signal"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
_ "github.com/pion/mediadevices/pkg/driver/screen" // This is required to register screen capture adapter
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/webrtc/v2"
)
func main() {
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Create a new RTCPeerConnection
mediaEngine := webrtc.MediaEngine{}
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
panic(err)
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(config)
if err != nil {
panic(err)
}
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
md := mediadevices.NewMediaDevices(peerConnection)
vp8Params, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vp8Params.BitRate = 100000 // 100kbps
s, err := md.GetDisplayMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.Enabled = true
c.VideoTransform = video.Scale(-1, 360, nil) // Resize to 360p
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
},
})
if err != nil {
panic(err)
}
for _, tracker := range s.GetTracks() {
t := tracker.Track()
tracker.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
t.ID(), t.Label(), err)
})
_, err = peerConnection.AddTransceiverFromTrack(t,
webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
},
)
if err != nil {
panic(err)
}
}
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)
if err != nil {
panic(err)
}
// Create an answer
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
panic(err)
}
// Output the answer in base64 so we can paste it in browser
fmt.Println(signal.Encode(answer))
select {}
}

View File

@@ -1,17 +1,29 @@
## Instructions
### Download the example
### Download gstreamer-send
```
go get github.com/pion/mediadevices/examples/simple
```
### Run the sample
### Open example page
```
simple
```
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
### View yourself in the browser
### Run simple with your browsers SessionDescription as stdin
Open your browser and go to "http://localhost:1313"
In the jsfiddle the top textarea is your browser, copy that and:
#### Linux
Run `echo $BROWSER_SDP | simple`
### Input simple's SessionDescription into your browser
Copy the text that `simple` just emitted and copy into second text area
### Hit 'Start Session' in jsfiddle, enjoy your video!
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
Congrats, you have used pion-WebRTC! Now start building something cool

View File

@@ -2,68 +2,131 @@ package main
import (
"fmt"
"image/jpeg"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/examples/internal/signal"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v2"
// This is required to use opus audio encoder
"github.com/pion/mediadevices/pkg/codec/opus"
// If you don't like vpx, you can also use x264 by importing as below
// "github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video encoder
// or you can also use openh264 for alternative h264 implementation
// "github.com/pion/mediadevices/pkg/codec/openh264"
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
// Note: If you don't have a camera or microphone or your adapters are not supported,
// you can always swap your adapters with our dummy adapters below.
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
// _ "github.com/pion/mediadevices/pkg/driver/audiotest"
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
_ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter
)
const (
videoCodecName = webrtc.VP8
)
func main() {
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(constraint *mediadevices.MediaTrackConstraints) {
constraint.Width = prop.Int(600)
constraint.Height = prop.Int(400)
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
// Create a new RTCPeerConnection
mediaEngine := webrtc.MediaEngine{}
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
panic(err)
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(config)
if err != nil {
panic(err)
}
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
md := mediadevices.NewMediaDevices(peerConnection)
opusParams, err := opus.NewParams()
if err != nil {
panic(err)
}
opusParams.BitRate = 32000 // 32kbps
vp8Params, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vp8Params.BitRate = 100000 // 100kbps
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
Audio: func(c *mediadevices.MediaTrackConstraints) {
c.Enabled = true
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{&opusParams}
},
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Enabled = true
c.Width = prop.Int(640)
c.Height = prop.Int(480)
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
},
})
if err != nil {
panic(err)
}
t := s.GetVideoTracks()[0]
defer t.Stop()
videoTrack := t.(*mediadevices.VideoTrack)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
videoReader := videoTrack.NewReader()
mimeWriter := multipart.NewWriter(w)
contentType := fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary())
w.Header().Add("Content-Type", contentType)
partHeader := make(textproto.MIMEHeader)
partHeader.Add("Content-Type", "image/jpeg")
for {
frame, err := videoReader.Read()
if err != nil {
if err == io.EOF {
return
}
panic(err)
}
partWriter, err := mimeWriter.CreatePart(partHeader)
if err != nil {
panic(err)
}
err = jpeg.Encode(partWriter, frame, nil)
if err != nil {
panic(err)
}
for _, tracker := range s.GetTracks() {
t := tracker.Track()
tracker.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
t.ID(), t.Label(), err)
})
_, err = peerConnection.AddTransceiverFromTrack(t,
webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
},
)
if err != nil {
panic(err)
}
})
}
fmt.Println("listening on http://localhost:1313")
log.Println(http.ListenAndServe("localhost:1313", nil))
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)
if err != nil {
panic(err)
}
// Create an answer
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
panic(err)
}
// Output the answer in base64 so we can paste it in browser
fmt.Println(signal.Encode(answer))
select {}
}

8
go.mod
View File

@@ -4,10 +4,10 @@ go 1.13
require (
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4
github.com/pion/webrtc/v2 v2.2.23
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa
github.com/lherman-cs/opus v0.0.0-20200925064139-8edf1852fd1f
github.com/pion/webrtc/v2 v2.2.26
github.com/satori/go.uuid v1.2.0
golang.org/x/image v0.0.0-20200801110659-972c09e46d76
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a
)

24
go.sum
View File

@@ -15,15 +15,15 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214 h1:2xVJKIumEUWeV3vczQwn61SHjNZ94Bwk+4CTjmcePxk=
github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa h1:qUZIj5+D3UDgfshNe8Cz/9maOxe8ddt43qwQH9vEEC8=
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4 h1:2ydMA2KbxRkYmIw3R8Me8dn90bejxBR4MKYXJ5THK3I=
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
github.com/lherman-cs/opus v0.0.0-20200925064139-8edf1852fd1f h1:xZKyjUoki95rRDQl3mDf20j2WZ+jZaFVzPZO72Jdi4A=
github.com/lherman-cs/opus v0.0.0-20200925064139-8edf1852fd1f/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
@@ -33,8 +33,8 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pion/datachannel v1.4.19 h1:IcOmm5fdDzJVCMgFYDCMtFC+lrjG78KcMYXH+gOo6ys=
github.com/pion/datachannel v1.4.19/go.mod h1:JzKF/zzeWgkOYwQ+KFb8JzbrUt8s63um+Qunu8VqTyw=
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA=
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E=
@@ -54,8 +54,8 @@ github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA=
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/sctp v1.7.8 h1:tEWel2BKXLZitU+LxY3GDeQXoKeTafYasiu/X+XBKNM=
github.com/pion/sctp v1.7.8/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8=
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI=
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
@@ -73,8 +73,8 @@ github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pion/webrtc/v2 v2.2.23 h1:rZdOC95fwUCoQFVjHooPAayx/vhs3SLHFz8J/iRkAuk=
github.com/pion/webrtc/v2 v2.2.23/go.mod h1:1lN/3EcATkQxc7GJSQbISCGC2l64Xu2VSLpwEG3c/tM=
github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -119,8 +119,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a h1:i47hUS795cOydZI4AwJQCKXOr4BvxzvikwDoDtHhP2Y=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

View File

@@ -3,40 +3,110 @@ package mediadevices
import (
"fmt"
"math"
"strings"
"github.com/pion/mediadevices/pkg/driver"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v2"
)
var errNotFound = fmt.Errorf("failed to find the best driver that fits the constraints")
// MediaDevices is an interface that's defined on https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
type MediaDevices interface {
GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error)
GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error)
EnumerateDevices() []MediaDeviceInfo
}
// NewMediaDevices creates MediaDevices interface that provides access to connected media input devices
// like cameras and microphones, as well as screen sharing.
// In essence, it lets you obtain access to any hardware source of media data.
func NewMediaDevices(pc *webrtc.PeerConnection, opts ...MediaDevicesOption) MediaDevices {
codecs := make(map[webrtc.RTPCodecType][]*webrtc.RTPCodec)
for _, kind := range []webrtc.RTPCodecType{
webrtc.RTPCodecTypeAudio,
webrtc.RTPCodecTypeVideo,
} {
codecs[kind] = pc.GetRegisteredRTPCodecs(kind)
}
return NewMediaDevicesFromCodecs(codecs, opts...)
}
// NewMediaDevicesFromCodecs creates MediaDevices interface from lists of the available codecs
// that provides access to connected media input devices like cameras and microphones,
// as well as screen sharing.
// In essence, it lets you obtain access to any hardware source of media data.
func NewMediaDevicesFromCodecs(codecs map[webrtc.RTPCodecType][]*webrtc.RTPCodec, opts ...MediaDevicesOption) MediaDevices {
mdo := MediaDevicesOptions{
codecs: codecs,
trackGenerator: defaultTrackGenerator,
}
for _, o := range opts {
o(&mdo)
}
return &mediaDevices{
MediaDevicesOptions: mdo,
}
}
// TrackGenerator is a function to create new track.
type TrackGenerator func(payloadType uint8, ssrc uint32, id, label string, codec *webrtc.RTPCodec) (LocalTrack, error)
var defaultTrackGenerator = TrackGenerator(func(pt uint8, ssrc uint32, id, label string, codec *webrtc.RTPCodec) (LocalTrack, error) {
return webrtc.NewTrack(pt, ssrc, id, label, codec)
})
type mediaDevices struct {
MediaDevicesOptions
}
// MediaDevicesOptions stores parameters used by MediaDevices.
type MediaDevicesOptions struct {
codecs map[webrtc.RTPCodecType][]*webrtc.RTPCodec
trackGenerator TrackGenerator
}
// MediaDevicesOption is a type of MediaDevices functional option.
type MediaDevicesOption func(*MediaDevicesOptions)
// WithTrackGenerator specifies a TrackGenerator to use customized track.
func WithTrackGenerator(gen TrackGenerator) MediaDevicesOption {
return func(o *MediaDevicesOptions) {
o.trackGenerator = gen
}
}
// GetDisplayMedia prompts the user to select and grant permission to capture the contents
// of a display or portion thereof (such as a window) as a MediaStream.
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
tracks := make([]Track, 0)
func (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
trackers := make([]Tracker, 0)
cleanTracks := func() {
for _, t := range tracks {
cleanTrackers := func() {
for _, t := range trackers {
t.Stop()
}
}
var videoConstraints MediaTrackConstraints
if constraints.Video != nil {
var p MediaTrackConstraints
constraints.Video(&p)
track, err := selectScreen(p)
constraints.Video(&videoConstraints)
}
if videoConstraints.Enabled {
tracker, err := m.selectScreen(videoConstraints)
if err != nil {
cleanTracks()
cleanTrackers()
return nil, err
}
tracks = append(tracks, track)
trackers = append(trackers, tracker)
}
s, err := NewMediaStream(tracks...)
s, err := NewMediaStream(trackers...)
if err != nil {
cleanTracks()
cleanTrackers()
return nil, err
}
@@ -46,42 +116,48 @@ func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
// GetUserMedia prompts the user for permission to use a media input which produces a MediaStream
// with tracks containing the requested types of media.
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
tracks := make([]Track, 0)
func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
// TODO: It should return media stream based on constraints
trackers := make([]Tracker, 0)
cleanTracks := func() {
for _, t := range tracks {
cleanTrackers := func() {
for _, t := range trackers {
t.Stop()
}
}
var videoConstraints, audioConstraints MediaTrackConstraints
if constraints.Video != nil {
var p MediaTrackConstraints
constraints.Video(&p)
track, err := selectVideo(p)
if err != nil {
cleanTracks()
return nil, err
}
tracks = append(tracks, track)
constraints.Video(&videoConstraints)
}
if constraints.Audio != nil {
var p MediaTrackConstraints
constraints.Audio(&p)
track, err := selectAudio(p)
constraints.Audio(&audioConstraints)
}
if videoConstraints.Enabled {
tracker, err := m.selectVideo(videoConstraints)
if err != nil {
cleanTracks()
cleanTrackers()
return nil, err
}
tracks = append(tracks, track)
trackers = append(trackers, tracker)
}
s, err := NewMediaStream(tracks...)
if audioConstraints.Enabled {
tracker, err := m.selectAudio(audioConstraints)
if err != nil {
cleanTrackers()
return nil, err
}
trackers = append(trackers, tracker)
}
s, err := NewMediaStream(trackers...)
if err != nil {
cleanTracks()
cleanTrackers()
return nil, err
}
@@ -139,7 +215,23 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
}
if bestDriver == nil {
return nil, MediaTrackConstraints{}, errNotFound
var foundProperties []string
for _, props := range driverProperties {
for _, p := range props {
foundProperties = append(foundProperties, fmt.Sprint(&p))
}
}
err := fmt.Errorf(`%w:
============ Found Properties ============
%s
=============== Constraints ==============
%s
`, errNotFound, strings.Join(foundProperties, "\n\n"), &constraints)
return nil, MediaTrackConstraints{}, err
}
constraints.selectedMedia = prop.Media{}
@@ -148,7 +240,7 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
return bestDriver, constraints, nil
}
func selectAudio(constraints MediaTrackConstraints) (Track, error) {
func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
typeFilter := driver.FilterAudioRecorder()
d, c, err := selectBestDriver(typeFilter, constraints)
@@ -156,10 +248,9 @@ func selectAudio(constraints MediaTrackConstraints) (Track, error) {
return nil, err
}
return newAudioTrack(d, c)
return newTrack(&m.MediaDevicesOptions, d, c)
}
func selectVideo(constraints MediaTrackConstraints) (Track, error) {
func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker, error) {
typeFilter := driver.FilterVideoRecorder()
notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen))
filter := driver.FilterAnd(typeFilter, notScreenFilter)
@@ -169,10 +260,10 @@ func selectVideo(constraints MediaTrackConstraints) (Track, error) {
return nil, err
}
return newVideoTrack(d, c)
return newTrack(&m.MediaDevicesOptions, d, c)
}
func selectScreen(constraints MediaTrackConstraints) (Track, error) {
func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker, error) {
typeFilter := driver.FilterVideoRecorder()
screenFilter := driver.FilterDeviceType(driver.Screen)
filter := driver.FilterAnd(typeFilter, screenFilter)
@@ -182,10 +273,10 @@ func selectScreen(constraints MediaTrackConstraints) (Track, error) {
return nil, err
}
return newVideoTrack(d, c)
return newTrack(&m.MediaDevicesOptions, d, c)
}
func EnumerateDevices() []MediaDeviceInfo {
func (m *mediaDevices) EnumerateDevices() []MediaDeviceInfo {
drivers := driver.GetManager().Query(
driver.FilterFn(func(driver.Driver) bool { return true }))
info := make([]MediaDeviceInfo, 0, len(drivers))

View File

@@ -19,25 +19,18 @@ import (
)
func TestGetUserMedia(t *testing.T) {
brokenVideoParams := mockParams{
videoParams := mockParams{
BaseParams: codec.BaseParams{
BitRate: 100000,
},
name: "MockVideo",
}
videoParams := brokenVideoParams
videoParams.BitRate = 100000
audioParams := mockParams{
BaseParams: codec.BaseParams{
BitRate: 32000,
},
name: "MockAudio",
}
constraints := MediaStreamConstraints{
Video: func(p *prop.Media) {
p.Width = 640
p.Height = 480
},
Audio: func(p *prop.Media) {},
}
md := NewMediaDevicesFromCodecs(
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
webrtc.RTPCodecTypeVideo: {
@@ -54,10 +47,7 @@ func TestGetUserMedia(t *testing.T) {
return newMockTrack(codec, id), nil
},
),
WithVideoEncoders(&brokenVideoParams),
WithAudioEncoders(&audioParams),
)
<<<<<<< HEAD
constraints := MediaStreamConstraints{
Video: func(c *MediaTrackConstraints) {
c.Enabled = true
@@ -87,35 +77,13 @@ func TestGetUserMedia(t *testing.T) {
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{&params}
},
}
=======
>>>>>>> ccd7985... Redesign GetUserMedia API
// GetUserMedia with broken parameters
ms, err := md.GetUserMedia(constraints)
ms, err := md.GetUserMedia(constraintsWrong)
if err == nil {
t.Fatal("Expected error, but got nil")
}
md = NewMediaDevicesFromCodecs(
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
&webrtc.RTPCodec{Type: webrtc.RTPCodecTypeVideo, Name: "MockVideo", PayloadType: 1},
},
webrtc.RTPCodecTypeAudio: []*webrtc.RTPCodec{
&webrtc.RTPCodec{Type: webrtc.RTPCodecTypeAudio, Name: "MockAudio", PayloadType: 2},
},
},
WithTrackGenerator(
func(_ uint8, _ uint32, id, _ string, codec *webrtc.RTPCodec) (
LocalTrack, error,
) {
return newMockTrack(codec, id), nil
},
),
WithVideoEncoders(&videoParams),
WithAudioEncoders(&audioParams),
)
// GetUserMedia with correct parameters
ms, err = md.GetUserMedia(constraints)
if err != nil {

View File

@@ -9,82 +9,82 @@ import (
// MediaStream is an interface that represents a collection of existing tracks.
type MediaStream interface {
// GetAudioTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getaudiotracks
GetAudioTracks() []Track
GetAudioTracks() []Tracker
// GetVideoTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getvideotracks
GetVideoTracks() []Track
GetVideoTracks() []Tracker
// GetTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks
GetTracks() []Track
GetTracks() []Tracker
// AddTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-addtrack
AddTrack(t Track)
AddTrack(t Tracker)
// RemoveTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack
RemoveTrack(t Track)
RemoveTrack(t Tracker)
}
type mediaStream struct {
tracks map[string]Track
l sync.RWMutex
trackers map[string]Tracker
l sync.RWMutex
}
const rtpCodecTypeDefault webrtc.RTPCodecType = 0
// NewMediaStream creates a MediaStream interface that's defined in
// https://w3c.github.io/mediacapture-main/#dom-mediastream
func NewMediaStream(tracks ...Track) (MediaStream, error) {
m := mediaStream{tracks: make(map[string]Track)}
func NewMediaStream(trackers ...Tracker) (MediaStream, error) {
m := mediaStream{trackers: make(map[string]Tracker)}
for _, track := range tracks {
id := track.ID()
if _, ok := m.tracks[id]; !ok {
m.tracks[id] = track
for _, tracker := range trackers {
id := tracker.LocalTrack().ID()
if _, ok := m.trackers[id]; !ok {
m.trackers[id] = tracker
}
}
return &m, nil
}
func (m *mediaStream) GetAudioTracks() []Track {
return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindAudio })
func (m *mediaStream) GetAudioTracks() []Tracker {
return m.queryTracks(webrtc.RTPCodecTypeAudio)
}
func (m *mediaStream) GetVideoTracks() []Track {
return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindVideo })
func (m *mediaStream) GetVideoTracks() []Tracker {
return m.queryTracks(webrtc.RTPCodecTypeVideo)
}
func (m *mediaStream) GetTracks() []Track {
return m.queryTracks(func(t Track) bool { return true })
func (m *mediaStream) GetTracks() []Tracker {
return m.queryTracks(rtpCodecTypeDefault)
}
// 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(filter func(track Track) bool) []Track {
func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Tracker {
m.l.RLock()
defer m.l.RUnlock()
result := make([]Track, 0)
for _, track := range m.tracks {
if filter(track) {
result = append(result, track)
result := make([]Tracker, 0)
for _, tracker := range m.trackers {
if tracker.LocalTrack().Kind() == t || t == rtpCodecTypeDefault {
result = append(result, tracker)
}
}
return result
}
func (m *mediaStream) AddTrack(t Track) {
func (m *mediaStream) AddTrack(t Tracker) {
m.l.Lock()
defer m.l.Unlock()
id := t.ID()
if _, ok := m.tracks[id]; ok {
id := t.LocalTrack().ID()
if _, ok := m.trackers[id]; ok {
return
}
m.tracks[id] = t
m.trackers[id] = t
}
func (m *mediaStream) RemoveTrack(t Track) {
func (m *mediaStream) RemoveTrack(t Tracker) {
m.l.Lock()
defer m.l.Unlock()
delete(m.tracks, t.ID())
delete(m.trackers, t.LocalTrack().ID())
}

View File

@@ -1,6 +1,9 @@
package mediadevices
import (
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
)
@@ -12,6 +15,26 @@ type MediaStreamConstraints struct {
// MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
type MediaTrackConstraints struct {
prop.MediaConstraints
Enabled bool
// VideoEncoderBuilders are codec builders that are used for encoding the video
// and later being used for sending the appropriate RTP payload type.
//
// If one encoder builder fails to build the codec, the next builder will be used,
// repeating until a codec builds. If no builders build successfully, an error is returned.
VideoEncoderBuilders []codec.VideoEncoderBuilder
// AudioEncoderBuilders are codec builders that are used for encoding the audio
// and later being used for sending the appropriate RTP payload type.
//
// If one encoder builder fails to build the codec, the next builder will be used,
// repeating until a codec builds. If no builders build successfully, an error is returned.
AudioEncoderBuilders []codec.AudioEncoderBuilder
// VideoTransform will be used to transform the video that's coming from the driver.
// So, basically it'll look like following: driver -> VideoTransform -> codec
VideoTransform video.TransformFunc
// AudioTransform will be used to transform the audio that's coming from the driver.
// So, basically it'll look like following: driver -> AudioTransform -> code
AudioTransform audio.TransformFunc
selectedMedia prop.Media
}

View File

@@ -80,13 +80,13 @@ func (e *encoder) Read(p []byte) (int, error) {
switch b := buff.(type) {
case *wave.Int16Interleaved:
n, err := e.engine.Encode(b.Data, p)
n, err := e.engine.EncodeBytes(b.Data, p, false)
if err != nil {
return n, err
}
return n, nil
case *wave.Float32Interleaved:
n, err := e.engine.EncodeFloat32(b.Data, p)
n, err := e.engine.EncodeBytes(b.Data, p, true)
if err != nil {
return n, err
}

View File

@@ -57,10 +57,11 @@ func init() {
func newCamera(path string) *camera {
formats := map[webcam.PixelFormat]frame.Format{
webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV): frame.FormatYUYV,
webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY): frame.FormatUYVY,
webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV21,
webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG,
webcam.PixelFormat(C.V4L2_PIX_FMT_YUV420): frame.FormatI420,
webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV): frame.FormatYUYV,
webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY): frame.FormatUYVY,
webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV21,
webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG,
}
reversedFormats := make(map[frame.Format]webcam.PixelFormat)

View File

@@ -3,8 +3,6 @@ package frame
type Format string
const (
// YUV Formats
// FormatI420 https://www.fourcc.org/pixel-format/yuv-i420/
FormatI420 Format = "I420"
// FormatI444 is a YUV format without sub-sampling
@@ -16,18 +14,11 @@ const (
// FormatUYVY https://www.fourcc.org/pixel-format/yuv-uyvy/
FormatUYVY = "UYVY"
// RGB Formats
// FormatRGBA https://www.fourcc.org/pixel-format/rgb-rgba/
FormatRGBA Format = "RGBA"
// Compressed Formats
// FormatMJPEG https://www.fourcc.org/mjpg/
FormatMJPEG = "MJPEG"
)
// YUV aliases
// FormatYUYV is an alias of FormatYUY2
const FormatYUYV = FormatYUY2

View File

@@ -5,7 +5,7 @@ import (
)
func NewDecoder(f Format) (Decoder, error) {
var decoder DecoderFunc
var decoder decoderFunc
switch f {
case FormatI420:

View File

@@ -7,8 +7,8 @@ type Decoder interface {
}
// DecoderFunc is a proxy type for Decoder
type DecoderFunc func(frame []byte, width, height int) (image.Image, error)
type decoderFunc func(frame []byte, width, height int) (image.Image, error)
func (f DecoderFunc) Decode(frame []byte, width, height int) (image.Image, error) {
func (f decoderFunc) Decode(frame []byte, width, height int) (image.Image, error) {
return f(frame, width, height)
}

View File

@@ -66,8 +66,8 @@ func NewBuffer(nSamples int) TransformFunc {
case *wave.Int16Interleaved:
ibCopy := *ib
ibCopy.Size.Len = nSamples
n := nSamples * ib.Size.Channels
ibCopy.Data = make([]int16, n)
n := nSamples * ib.Size.Channels * 2
ibCopy.Data = make([]uint8, n)
copy(ibCopy.Data, ib.Data)
ib.Data = ib.Data[n:]
ib.Size.Len -= nSamples
@@ -76,8 +76,8 @@ func NewBuffer(nSamples int) TransformFunc {
case *wave.Float32Interleaved:
ibCopy := *ib
ibCopy.Size.Len = nSamples
n := nSamples * ib.Size.Channels
ibCopy.Data = make([]float32, n)
n := nSamples * ib.Size.Channels * 4
ibCopy.Data = make([]uint8, n)
copy(ibCopy.Data, ib.Data)
ib.Data = ib.Data[n:]
ib.Size.Len -= nSamples

View File

@@ -9,41 +9,83 @@ import (
)
func TestBuffer(t *testing.T) {
input1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234})
input1.SetInt16(0, 0, 1)
input1.SetInt16(0, 1, 2)
input2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
input2.SetInt16(0, 0, 3)
input2.SetInt16(0, 1, 4)
input2.SetInt16(1, 0, 5)
input2.SetInt16(1, 1, 6)
input2.SetInt16(2, 0, 7)
input2.SetInt16(2, 1, 8)
input3 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 2, Channels: 2, SamplingRate: 1234})
input3.SetInt16(0, 0, 9)
input3.SetInt16(0, 1, 10)
input3.SetInt16(1, 0, 11)
input3.SetInt16(1, 1, 12)
input4 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 7, Channels: 2, SamplingRate: 1234})
input4.SetInt16(0, 0, 13)
input4.SetInt16(0, 1, 14)
input4.SetInt16(1, 0, 15)
input4.SetInt16(1, 1, 16)
input4.SetInt16(2, 0, 17)
input4.SetInt16(2, 1, 18)
input4.SetInt16(3, 0, 19)
input4.SetInt16(3, 1, 20)
input4.SetInt16(4, 0, 21)
input4.SetInt16(4, 1, 22)
input4.SetInt16(5, 0, 23)
input4.SetInt16(5, 1, 24)
input4.SetInt16(6, 0, 25)
input4.SetInt16(6, 1, 26)
expected1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected1.SetInt16(0, 0, 1)
expected1.SetInt16(0, 1, 2)
expected1.SetInt16(1, 0, 3)
expected1.SetInt16(1, 1, 4)
expected1.SetInt16(2, 0, 5)
expected1.SetInt16(2, 1, 6)
expected2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected2.SetInt16(0, 0, 7)
expected2.SetInt16(0, 1, 8)
expected2.SetInt16(1, 0, 9)
expected2.SetInt16(1, 1, 10)
expected2.SetInt16(2, 0, 11)
expected2.SetInt16(2, 1, 12)
expected3 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected3.SetInt16(0, 0, 13)
expected3.SetInt16(0, 1, 14)
expected3.SetInt16(1, 0, 15)
expected3.SetInt16(1, 1, 16)
expected3.SetInt16(2, 0, 17)
expected3.SetInt16(2, 1, 18)
expected4 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
expected4.SetInt16(0, 0, 19)
expected4.SetInt16(0, 1, 20)
expected4.SetInt16(1, 0, 21)
expected4.SetInt16(1, 1, 22)
expected4.SetInt16(2, 0, 23)
expected4.SetInt16(2, 1, 24)
input := []wave.Audio{
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234},
Data: []int16{1, 2},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{3, 4, 5, 6, 7, 8},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 2, Channels: 2, SamplingRate: 1234},
Data: []int16{9, 10, 11, 12},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 7, Channels: 2, SamplingRate: 1234},
Data: []int16{13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26},
},
input1,
input2,
input3,
input4,
}
expected := []wave.Audio{
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{1, 2, 3, 4, 5, 6},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{7, 8, 9, 10, 11, 12},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{13, 14, 15, 16, 17, 18},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{19, 20, 21, 22, 23, 24},
},
expected1,
expected2,
expected3,
expected4,
}
trans := NewBuffer(3)

View File

@@ -10,25 +10,33 @@ import (
)
func TestMixer(t *testing.T) {
input1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234})
input1.SetInt16(0, 0, 1)
input1.SetInt16(0, 1, 3)
input2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234})
input2.SetInt16(0, 0, 2)
input2.SetInt16(0, 1, 4)
input2.SetInt16(1, 0, 3)
input2.SetInt16(1, 1, 5)
input2.SetInt16(2, 0, 4)
input2.SetInt16(2, 1, 6)
expected1 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 1, Channels: 1, SamplingRate: 1234})
expected1.SetInt16(0, 0, 2)
expected2 := wave.NewInt16Interleaved(wave.ChunkInfo{Len: 3, Channels: 1, SamplingRate: 1234})
expected2.SetInt16(0, 0, 3)
expected2.SetInt16(1, 0, 4)
expected2.SetInt16(2, 0, 5)
input := []wave.Audio{
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 1, Channels: 2, SamplingRate: 1234},
Data: []int16{1, 3},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 2, SamplingRate: 1234},
Data: []int16{2, 4, 3, 5, 4, 6},
},
input1,
input2,
}
expected := []wave.Audio{
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 1, Channels: 1, SamplingRate: 1234},
Data: []int16{2},
},
&wave.Int16Interleaved{
Size: wave.ChunkInfo{Len: 3, Channels: 1, SamplingRate: 1234},
Data: []int16{3, 4, 5},
},
expected1,
expected2,
}
trans := NewChannelMixer(1, &mixer.MonoMixer{})

214
pkg/io/video/framebuffer.go Normal file
View File

@@ -0,0 +1,214 @@
package video
import (
"image"
)
// FrameBuffer is a buffer that can store any image format.
type FrameBuffer struct {
buffer []uint8
tmp image.Image
}
// NewFrameBuffer creates a new FrameBuffer instance and initialize internal buffer
// with initialSize
func NewFrameBuffer(initialSize int) *FrameBuffer {
return &FrameBuffer{
buffer: make([]uint8, initialSize),
}
}
func (buff *FrameBuffer) storeInOrder(srcs ...[]uint8) {
var neededSize int
for _, src := range srcs {
neededSize += len(src)
}
if len(buff.buffer) < neededSize {
if cap(buff.buffer) >= neededSize {
buff.buffer = buff.buffer[:neededSize]
} else {
buff.buffer = make([]uint8, neededSize)
}
}
var currentLen int
for _, src := range srcs {
copy(buff.buffer[currentLen:], src)
currentLen += len(src)
}
}
// Load loads the current owned image
func (buff *FrameBuffer) Load() image.Image {
return buff.tmp
}
// StoreCopy makes a copy of src and store its copy. StoreCopy will reuse as much memory as it can
// from the previous copies. For example, if StoreCopy is given an image that has the same resolution
// and format from the previous call, StoreCopy will not allocate extra memory and only copy the content
// from src to the previous buffer.
func (buff *FrameBuffer) StoreCopy(src image.Image) {
switch src := src.(type) {
case *image.Alpha:
clone, ok := buff.tmp.(*image.Alpha)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.Alpha16:
clone, ok := buff.tmp.(*image.Alpha16)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.CMYK:
clone, ok := buff.tmp.(*image.CMYK)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.Gray:
clone, ok := buff.tmp.(*image.Gray)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.Gray16:
clone, ok := buff.tmp.(*image.Gray16)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.NRGBA:
clone, ok := buff.tmp.(*image.NRGBA)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.NRGBA64:
clone, ok := buff.tmp.(*image.NRGBA64)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.RGBA:
clone, ok := buff.tmp.(*image.RGBA)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.RGBA64:
clone, ok := buff.tmp.(*image.RGBA64)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
buff.storeInOrder(src.Pix)
clone.Pix = buff.buffer[:len(src.Pix)]
buff.tmp = clone
case *image.NYCbCrA:
clone, ok := buff.tmp.(*image.NYCbCrA)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
var currentLen int
buff.storeInOrder(src.Y, src.Cb, src.Cr, src.A)
clone.Y = buff.buffer[currentLen : currentLen+len(src.Y) : currentLen+len(src.Y)]
currentLen += len(src.Y)
clone.Cb = buff.buffer[currentLen : currentLen+len(src.Cb) : currentLen+len(src.Cb)]
currentLen += len(src.Cb)
clone.Cr = buff.buffer[currentLen : currentLen+len(src.Cr) : currentLen+len(src.Cr)]
currentLen += len(src.Cr)
clone.A = buff.buffer[currentLen : currentLen+len(src.A) : currentLen+len(src.A)]
buff.tmp = clone
case *image.YCbCr:
clone, ok := buff.tmp.(*image.YCbCr)
if ok {
*clone = *src
} else {
copied := *src
clone = &copied
}
var currentLen int
buff.storeInOrder(src.Y, src.Cb, src.Cr)
clone.Y = buff.buffer[currentLen : currentLen+len(src.Y) : currentLen+len(src.Y)]
currentLen += len(src.Y)
clone.Cb = buff.buffer[currentLen : currentLen+len(src.Cb) : currentLen+len(src.Cb)]
currentLen += len(src.Cb)
clone.Cr = buff.buffer[currentLen : currentLen+len(src.Cr) : currentLen+len(src.Cr)]
buff.tmp = clone
default:
var converted image.RGBA
imageToRGBA(&converted, src)
buff.StoreCopy(&converted)
}
}

View File

@@ -0,0 +1,195 @@
package video
import (
"image"
"math/rand"
"reflect"
"testing"
)
func randomize(arr []uint8) {
for i := range arr {
arr[i] = uint8(rand.Uint32())
}
}
func BenchmarkFrameBufferCopyOptimized(b *testing.B) {
frameBuffer := NewFrameBuffer(0)
resolution := image.Rect(0, 0, 1920, 1080)
src := image.NewYCbCr(resolution, image.YCbCrSubsampleRatio420)
for i := 0; i < b.N; i++ {
frameBuffer.StoreCopy(src)
}
}
func BenchmarkFrameBufferCopyNaive(b *testing.B) {
resolution := image.Rect(0, 0, 1920, 1080)
src := image.NewYCbCr(resolution, image.YCbCrSubsampleRatio420)
var dst image.Image
for i := 0; i < b.N; i++ {
clone := *src
clone.Cb = make([]uint8, len(src.Cb))
clone.Cr = make([]uint8, len(src.Cr))
clone.Y = make([]uint8, len(src.Y))
copy(clone.Cb, src.Cb)
copy(clone.Cr, src.Cr)
copy(clone.Y, src.Y)
dst = &clone
_ = dst
}
}
func TestFrameBufferStoreCopyAndLoad(t *testing.T) {
resolution := image.Rect(0, 0, 16, 8)
rgbaLike := image.NewRGBA64(resolution)
randomize(rgbaLike.Pix)
testCases := map[string]struct {
New func() image.Image
Update func(image.Image)
}{
"Alpha": {
New: func() image.Image {
return (*image.Alpha)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.Alpha)
randomize(img.Pix)
},
},
"Alpha16": {
New: func() image.Image {
return (*image.Alpha16)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.Alpha16)
randomize(img.Pix)
},
},
"CMYK": {
New: func() image.Image {
return (*image.CMYK)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.CMYK)
randomize(img.Pix)
},
},
"Gray": {
New: func() image.Image {
return (*image.Gray)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.Gray)
randomize(img.Pix)
},
},
"Gray16": {
New: func() image.Image {
return (*image.Gray16)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.Gray16)
randomize(img.Pix)
},
},
"NRGBA": {
New: func() image.Image {
return (*image.NRGBA)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.NRGBA)
randomize(img.Pix)
},
},
"NRGBA64": {
New: func() image.Image {
return (*image.NRGBA64)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.NRGBA64)
randomize(img.Pix)
},
},
"RGBA": {
New: func() image.Image {
return (*image.RGBA)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.RGBA)
randomize(img.Pix)
},
},
"RGBA64": {
New: func() image.Image {
return (*image.RGBA64)(rgbaLike)
},
Update: func(src image.Image) {
img := src.(*image.RGBA64)
randomize(img.Pix)
},
},
"NYCbCrA": {
New: func() image.Image {
img := image.NewNYCbCrA(resolution, image.YCbCrSubsampleRatio420)
randomize(img.Y)
randomize(img.Cb)
randomize(img.Cr)
randomize(img.A)
img.CStride = 10
img.YStride = 5
return img
},
Update: func(src image.Image) {
img := src.(*image.NYCbCrA)
randomize(img.Y)
randomize(img.Cb)
randomize(img.Cr)
randomize(img.A)
img.CStride = 3
img.YStride = 2
},
},
"YCbCr": {
New: func() image.Image {
img := image.NewYCbCr(resolution, image.YCbCrSubsampleRatio420)
randomize(img.Y)
randomize(img.Cb)
randomize(img.Cr)
img.CStride = 10
img.YStride = 5
return img
},
Update: func(src image.Image) {
img := src.(*image.YCbCr)
randomize(img.Y)
randomize(img.Cb)
randomize(img.Cr)
img.CStride = 3
img.YStride = 2
},
},
}
frameBuffer := NewFrameBuffer(0)
for name, testCase := range testCases {
// Since the test also wants to make sure that Copier can convert from 1 type to another,
// t.Run is not ideal since it'll run the tests separately
t.Log("Testing", name)
src := testCase.New()
frameBuffer.StoreCopy(src)
if !reflect.DeepEqual(frameBuffer.Load(), src) {
t.Fatal("Expected the copied image to be identical with the source")
}
testCase.Update(src)
frameBuffer.StoreCopy(src)
if !reflect.DeepEqual(frameBuffer.Load(), src) {
t.Fatal("Expected the copied image to be identical with the source after an update in source")
}
}
}

View File

@@ -1,5 +1,7 @@
package prop
import "fmt"
// BoolConstraint is an interface to represent bool value constraint.
type BoolConstraint interface {
Compare(bool) (float64, bool)
@@ -20,6 +22,11 @@ func (b BoolExact) Compare(o bool) (float64, bool) {
// Value implements BoolConstraint.
func (b BoolExact) Value() bool { return bool(b) }
// String implements Stringify
func (b BoolExact) String() string {
return fmt.Sprintf("%t (exact)", b)
}
// Bool specifies ideal bool value.
type Bool BoolExact

View File

@@ -1,7 +1,9 @@
package prop
import (
"fmt"
"math"
"strings"
"time"
)
@@ -23,6 +25,11 @@ func (d Duration) Compare(a time.Duration) (float64, bool) {
// Value implements DurationConstraint.
func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true }
// String implements Stringify
func (d Duration) String() string {
return fmt.Sprintf("%v (ideal)", time.Duration(d))
}
// DurationExact specifies exact duration value.
type DurationExact time.Duration
@@ -37,6 +44,11 @@ func (d DurationExact) Compare(a time.Duration) (float64, bool) {
// Value implements DurationConstraint.
func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true }
// String implements Stringify
func (d DurationExact) String() string {
return fmt.Sprintf("%v (exact)", time.Duration(d))
}
// DurationOneOf specifies list of expected duration values.
type DurationOneOf []time.Duration
@@ -53,6 +65,16 @@ func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
// Value implements DurationConstraint.
func (DurationOneOf) Value() (time.Duration, bool) { return 0, false }
// String implements Stringify
func (d DurationOneOf) String() string {
var opts []string
for _, v := range d {
opts = append(opts, fmt.Sprint(v))
}
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
}
// DurationRanged specifies range of expected duration value.
// If Ideal is non-zero, closest value to Ideal takes priority.
type DurationRanged struct {
@@ -96,3 +118,8 @@ func (d DurationRanged) Compare(a time.Duration) (float64, bool) {
// Value implements DurationConstraint.
func (DurationRanged) Value() (time.Duration, bool) { return 0, false }
// String implements Stringify
func (d DurationRanged) String() string {
return fmt.Sprintf("%s - %s (range), %s (ideal)", d.Min, d.Max, d.Ideal)
}

View File

@@ -1,7 +1,9 @@
package prop
import (
"fmt"
"math"
"strings"
)
// FloatConstraint is an interface to represent float value constraint.
@@ -22,6 +24,11 @@ func (f Float) Compare(a float32) (float64, bool) {
// Value implements FloatConstraint.
func (f Float) Value() (float32, bool) { return float32(f), true }
// String implements Stringify
func (f Float) String() string {
return fmt.Sprintf("%.2f (ideal)", f)
}
// FloatExact specifies exact float value.
type FloatExact float32
@@ -36,6 +43,11 @@ func (f FloatExact) Compare(a float32) (float64, bool) {
// Value implements FloatConstraint.
func (f FloatExact) Value() (float32, bool) { return float32(f), true }
// String implements Stringify
func (f FloatExact) String() string {
return fmt.Sprintf("%.2f (exact)", f)
}
// FloatOneOf specifies list of expected float values.
type FloatOneOf []float32
@@ -52,6 +64,16 @@ func (f FloatOneOf) Compare(a float32) (float64, bool) {
// Value implements FloatConstraint.
func (FloatOneOf) Value() (float32, bool) { return 0, false }
// String implements Stringify
func (f FloatOneOf) String() string {
var opts []string
for _, v := range f {
opts = append(opts, fmt.Sprintf("%.2f", v))
}
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
}
// FloatRanged specifies range of expected float value.
// If Ideal is non-zero, closest value to Ideal takes priority.
type FloatRanged struct {
@@ -95,3 +117,8 @@ func (f FloatRanged) Compare(a float32) (float64, bool) {
// Value implements FloatConstraint.
func (FloatRanged) Value() (float32, bool) { return 0, false }
// String implements Stringify
func (f FloatRanged) String() string {
return fmt.Sprintf("%.2f - %.2f (range), %.2f (ideal)", f.Min, f.Max, f.Ideal)
}

View File

@@ -1,7 +1,9 @@
package prop
import (
"fmt"
"github.com/pion/mediadevices/pkg/frame"
"strings"
)
// FrameFormatConstraint is an interface to represent frame format constraint.
@@ -25,6 +27,11 @@ func (f FrameFormat) Compare(a frame.Format) (float64, bool) {
// Value implements FrameFormatConstraint.
func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true }
// String implements Stringify
func (f FrameFormat) String() string {
return fmt.Sprintf("%s (ideal)", frame.Format(f))
}
// FrameFormatExact specifies exact frame format.
type FrameFormatExact frame.Format
@@ -39,6 +46,11 @@ func (f FrameFormatExact) Compare(a frame.Format) (float64, bool) {
// Value implements FrameFormatConstraint.
func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true }
// String implements Stringify
func (f FrameFormatExact) String() string {
return fmt.Sprintf("%s (exact)", frame.Format(f))
}
// FrameFormatOneOf specifies list of expected frame format.
type FrameFormatOneOf []frame.Format
@@ -54,3 +66,13 @@ func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) {
// Value implements FrameFormatConstraint.
func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false }
// String implements Stringify
func (f FrameFormatOneOf) String() string {
var opts []string
for _, v := range f {
opts = append(opts, fmt.Sprint(v))
}
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
}

View File

@@ -1,7 +1,9 @@
package prop
import (
"fmt"
"math"
"strings"
)
// IntConstraint is an interface to represent integer value constraint.
@@ -22,6 +24,11 @@ func (i Int) Compare(a int) (float64, bool) {
// Value implements IntConstraint.
func (i Int) Value() (int, bool) { return int(i), true }
// String implements Stringify
func (i Int) String() string {
return fmt.Sprintf("%d (ideal)", i)
}
// IntExact specifies exact int value.
type IntExact int
@@ -33,6 +40,11 @@ func (i IntExact) Compare(a int) (float64, bool) {
return 1.0, false
}
// String implements Stringify
func (i IntExact) String() string {
return fmt.Sprintf("%d (exact)", i)
}
// Value implements IntConstraint.
func (i IntExact) Value() (int, bool) { return int(i), true }
@@ -52,6 +64,16 @@ func (i IntOneOf) Compare(a int) (float64, bool) {
// Value implements IntConstraint.
func (IntOneOf) Value() (int, bool) { return 0, false }
// String implements Stringify
func (i IntOneOf) String() string {
var opts []string
for _, v := range i {
opts = append(opts, fmt.Sprint(v))
}
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
}
// IntRanged specifies range of expected int value.
// If Ideal is non-zero, closest value to Ideal takes priority.
type IntRanged struct {
@@ -95,3 +117,8 @@ func (i IntRanged) Compare(a int) (float64, bool) {
// Value implements IntConstraint.
func (IntRanged) Value() (int, bool) { return 0, false }
// String implements Stringify
func (i IntRanged) String() string {
return fmt.Sprintf("%d - %d (range), %d (ideal)", i.Min, i.Max, i.Ideal)
}

View File

@@ -1,7 +1,9 @@
package prop
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/pion/mediadevices/pkg/frame"
@@ -15,6 +17,10 @@ type MediaConstraints struct {
AudioConstraints
}
func (m *MediaConstraints) String() string {
return prettifyStruct(m)
}
// Media stores single set of media propaties.
type Media struct {
DeviceID string
@@ -22,6 +28,33 @@ type Media struct {
Audio
}
func (m *Media) String() string {
return prettifyStruct(m)
}
func prettifyStruct(i interface{}) string {
var rows []string
var addRows func(int, reflect.Value)
addRows = func(level int, obj reflect.Value) {
typeOf := obj.Type()
for i := 0; i < obj.NumField(); i++ {
field := typeOf.Field(i)
value := obj.Field(i)
padding := strings.Repeat(" ", level)
if value.Kind() == reflect.Struct {
rows = append(rows, fmt.Sprintf("%s%v:", padding, field.Name))
addRows(level+1, value)
} else {
rows = append(rows, fmt.Sprintf("%s%v: %v", padding, field.Name, value))
}
}
}
addRows(0, reflect.ValueOf(i).Elem())
return strings.Join(rows, "\n")
}
// setterFn is a callback function to set value from fieldB to fieldA
type setterFn func(fieldA, fieldB reflect.Value)

View File

@@ -309,3 +309,60 @@ func TestMergeConstraintsNested(t *testing.T) {
t.Error("expected a.Width to be 100, but got 0")
}
}
func TestString(t *testing.T) {
t.Run("IdealValues", func(t *testing.T) {
t.Log("\n", &MediaConstraints{
DeviceID: String("one"),
VideoConstraints: VideoConstraints{
Width: Int(1920),
FrameRate: Float(30.0),
FrameFormat: FrameFormat(frame.FormatI420),
},
AudioConstraints: AudioConstraints{
Latency: Duration(time.Millisecond * 20),
},
})
})
t.Run("ExactValues", func(t *testing.T) {
t.Log("\n", &MediaConstraints{
DeviceID: StringExact("one"),
VideoConstraints: VideoConstraints{
Width: IntExact(1920),
FrameRate: FloatExact(30.0),
FrameFormat: FrameFormatExact(frame.FormatI420),
},
AudioConstraints: AudioConstraints{
Latency: DurationExact(time.Millisecond * 20),
IsBigEndian: BoolExact(true),
},
})
})
t.Run("OneOfValues", func(t *testing.T) {
t.Log("\n", &MediaConstraints{
DeviceID: StringOneOf{"one", "two"},
VideoConstraints: VideoConstraints{
Width: IntOneOf{1920, 1080},
FrameRate: FloatOneOf{30.0, 60.1234},
FrameFormat: FrameFormatOneOf{frame.FormatI420, frame.FormatI444},
},
AudioConstraints: AudioConstraints{
Latency: DurationOneOf{time.Millisecond * 20, time.Millisecond * 40},
},
})
})
t.Run("RangedValues", func(t *testing.T) {
t.Log("\n", &MediaConstraints{
VideoConstraints: VideoConstraints{
Width: &IntRanged{Min: 1080, Max: 1920, Ideal: 1500},
FrameRate: &FloatRanged{Min: 30.123, Max: 60.12321312, Ideal: 45.12312312},
},
AudioConstraints: AudioConstraints{
Latency: &DurationRanged{Min: time.Millisecond * 20, Max: time.Millisecond * 40, Ideal: time.Millisecond * 30},
},
})
})
}

View File

@@ -1,5 +1,10 @@
package prop
import (
"fmt"
"strings"
)
// StringConstraint is an interface to represent string constraint.
type StringConstraint interface {
Compare(string) (float64, bool)
@@ -21,6 +26,11 @@ func (f String) Compare(a string) (float64, bool) {
// Value implements StringConstraint.
func (f String) Value() (string, bool) { return string(f), true }
// String implements Stringify
func (f String) String() string {
return fmt.Sprintf("%s (ideal)", string(f))
}
// StringExact specifies exact string.
type StringExact string
@@ -35,6 +45,11 @@ func (f StringExact) Compare(a string) (float64, bool) {
// Value implements StringConstraint.
func (f StringExact) Value() (string, bool) { return string(f), true }
// String implements Stringify
func (f StringExact) String() string {
return fmt.Sprintf("%s (exact)", string(f))
}
// StringOneOf specifies list of expected string.
type StringOneOf []string
@@ -50,3 +65,8 @@ func (f StringOneOf) Compare(a string) (float64, bool) {
// Value implements StringConstraint.
func (StringOneOf) Value() (string, bool) { return "", false }
// String implements Stringify
func (f StringOneOf) String() string {
return fmt.Sprintf("%s (one of values)", strings.Join([]string(f), ","))
}

View File

@@ -134,18 +134,15 @@ func TestDecodeInt16Interleaved(t *testing.T) {
decoder, _ := newInt16InterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := &Int16Interleaved{
Data: []int16{
int16(binary.BigEndian.Uint16([]byte{0x01, 0x02})),
int16(binary.BigEndian.Uint16([]byte{0x03, 0x04})),
int16(binary.BigEndian.Uint16([]byte{0x05, 0x06})),
int16(binary.BigEndian.Uint16([]byte{0x07, 0x08})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewInt16Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x01, 0x02})))
expected.SetInt16(0, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x03, 0x04})))
expected.SetInt16(1, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x05, 0x06})))
expected.SetInt16(1, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x07, 0x08})))
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -157,18 +154,15 @@ func TestDecodeInt16Interleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := &Int16Interleaved{
Data: []int16{
int16(binary.LittleEndian.Uint16([]byte{0x01, 0x02})),
int16(binary.LittleEndian.Uint16([]byte{0x03, 0x04})),
int16(binary.LittleEndian.Uint16([]byte{0x05, 0x06})),
int16(binary.LittleEndian.Uint16([]byte{0x07, 0x08})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewInt16Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x02, 0x01})))
expected.SetInt16(0, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x04, 0x03})))
expected.SetInt16(1, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x06, 0x05})))
expected.SetInt16(1, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x08, 0x07})))
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -190,16 +184,15 @@ func TestDecodeInt16NonInterleaved(t *testing.T) {
decoder, _ := newInt16NonInterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := &Int16NonInterleaved{
Data: [][]int16{
{int16(binary.BigEndian.Uint16([]byte{0x01, 0x02})), int16(binary.BigEndian.Uint16([]byte{0x03, 0x04}))},
{int16(binary.BigEndian.Uint16([]byte{0x05, 0x06})), int16(binary.BigEndian.Uint16([]byte{0x07, 0x08}))},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewInt16NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x01, 0x02})))
expected.SetInt16(0, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x05, 0x06})))
expected.SetInt16(1, 0, Int16Sample(binary.BigEndian.Uint16([]byte{0x03, 0x04})))
expected.SetInt16(1, 1, Int16Sample(binary.BigEndian.Uint16([]byte{0x07, 0x08})))
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -211,16 +204,15 @@ func TestDecodeInt16NonInterleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := &Int16NonInterleaved{
Data: [][]int16{
{int16(binary.LittleEndian.Uint16([]byte{0x01, 0x02})), int16(binary.LittleEndian.Uint16([]byte{0x03, 0x04}))},
{int16(binary.LittleEndian.Uint16([]byte{0x05, 0x06})), int16(binary.LittleEndian.Uint16([]byte{0x07, 0x08}))},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewInt16NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetInt16(0, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x02, 0x01})))
expected.SetInt16(0, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x06, 0x05})))
expected.SetInt16(1, 0, Int16Sample(binary.LittleEndian.Uint16([]byte{0x04, 0x03})))
expected.SetInt16(1, 1, Int16Sample(binary.LittleEndian.Uint16([]byte{0x08, 0x07})))
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -242,18 +234,15 @@ func TestDecodeFloat32Interleaved(t *testing.T) {
decoder, _ := newFloat32InterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := &Float32Interleaved{
Data: []float32{
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewFloat32Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10}))))
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -265,18 +254,15 @@ func TestDecodeFloat32Interleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := &Float32Interleaved{
Data: []float32{
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewFloat32Interleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x04, 0x03, 0x02, 0x01}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x08, 0x07, 0x06, 0x05}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0c, 0x0b, 0x0a, 0x09}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x10, 0x0f, 0x0e, 0x0d}))))
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -298,22 +284,15 @@ func TestDecodeFloat32NonInterleaved(t *testing.T) {
decoder, _ := newFloat32NonInterleavedDecoder()
t.Run("BigEndian", func(t *testing.T) {
expected := &Float32NonInterleaved{
Data: [][]float32{
{
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
},
{
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewFloat32NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.BigEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10}))))
actual, err := decoder.Decode(binary.BigEndian, raw, 2)
if err != nil {
t.Fatal(err)
@@ -325,22 +304,15 @@ func TestDecodeFloat32NonInterleaved(t *testing.T) {
})
t.Run("LittleEndian", func(t *testing.T) {
expected := &Float32NonInterleaved{
Data: [][]float32{
{
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x05, 0x06, 0x07, 0x08})),
},
{
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x09, 0x0a, 0x0b, 0x0c})),
math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0d, 0x0e, 0x0f, 0x10})),
},
},
Size: ChunkInfo{
Len: 2,
Channels: 2,
},
}
expected := NewFloat32NonInterleaved(ChunkInfo{
Len: 2,
Channels: 2,
})
expected.SetFloat32(0, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x04, 0x03, 0x02, 0x01}))))
expected.SetFloat32(0, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x0c, 0x0b, 0x0a, 0x09}))))
expected.SetFloat32(1, 0, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x08, 0x07, 0x06, 0x05}))))
expected.SetFloat32(1, 1, Float32Sample(math.Float32frombits(binary.LittleEndian.Uint32([]byte{0x10, 0x0f, 0x0e, 0x0d}))))
actual, err := decoder.Decode(binary.LittleEndian, raw, 2)
if err != nil {
t.Fatal(err)

View File

@@ -1,5 +1,7 @@
package wave
import "math"
// Float32Sample is a 32-bits float audio sample.
type Float32Sample float32
@@ -9,7 +11,7 @@ func (s Float32Sample) Int() int64 {
// Float32Interleaved multi-channel interlaced Audio.
type Float32Interleaved struct {
Data []float32
Data []uint8
Size ChunkInfo
}
@@ -23,22 +25,36 @@ func (a *Float32Interleaved) SampleFormat() SampleFormat {
}
func (a *Float32Interleaved) At(i, ch int) Sample {
return Float32Sample(a.Data[i*a.Size.Channels+ch])
loc := 4 * (a.Size.Channels*i + ch)
var v uint32
v |= uint32(a.Data[loc]) << 24
v |= uint32(a.Data[loc+1]) << 16
v |= uint32(a.Data[loc+2]) << 8
v |= uint32(a.Data[loc+3])
return Float32Sample(math.Float32frombits(v))
}
func (a *Float32Interleaved) Set(i, ch int, s Sample) {
a.Data[i*a.Size.Channels+ch] = float32(Float32SampleFormat.Convert(s).(Float32Sample))
a.SetFloat32(i, ch, Float32SampleFormat.Convert(s).(Float32Sample))
}
func (a *Float32Interleaved) SetFloat32(i, ch int, s Float32Sample) {
a.Data[i*a.Size.Channels+ch] = float32(s)
loc := 4 * (a.Size.Channels*i + ch)
v := math.Float32bits(float32(s))
a.Data[loc] = uint8(v >> 24)
a.Data[loc+1] = uint8(v >> 16)
a.Data[loc+2] = uint8(v >> 8)
a.Data[loc+3] = uint8(v)
}
// SubAudio returns part of the original audio sharing the buffer.
func (a *Float32Interleaved) SubAudio(offsetSamples, nSamples int) *Float32Interleaved {
ret := *a
offset := offsetSamples * a.Size.Channels
n := nSamples * a.Size.Channels
offset := 4 * offsetSamples * a.Size.Channels
n := 4 * nSamples * a.Size.Channels
ret.Data = ret.Data[offset : offset+n]
ret.Size.Len = nSamples
return &ret
@@ -46,14 +62,14 @@ func (a *Float32Interleaved) SubAudio(offsetSamples, nSamples int) *Float32Inter
func NewFloat32Interleaved(size ChunkInfo) *Float32Interleaved {
return &Float32Interleaved{
Data: make([]float32, size.Channels*size.Len),
Data: make([]uint8, size.Channels*size.Len*4),
Size: size,
}
}
// Float32NonInterleaved multi-channel interlaced Audio.
type Float32NonInterleaved struct {
Data [][]float32
Data [][]uint8
Size ChunkInfo
}
@@ -67,31 +83,48 @@ func (a *Float32NonInterleaved) SampleFormat() SampleFormat {
}
func (a *Float32NonInterleaved) At(i, ch int) Sample {
return Float32Sample(a.Data[ch][i])
loc := i * 4
var v uint32
v |= uint32(a.Data[ch][loc]) << 24
v |= uint32(a.Data[ch][loc+1]) << 16
v |= uint32(a.Data[ch][loc+2]) << 8
v |= uint32(a.Data[ch][loc+3])
return Float32Sample(math.Float32frombits(v))
}
func (a *Float32NonInterleaved) Set(i, ch int, s Sample) {
a.Data[ch][i] = float32(Float32SampleFormat.Convert(s).(Float32Sample))
a.SetFloat32(i, ch, Float32SampleFormat.Convert(s).(Float32Sample))
}
func (a *Float32NonInterleaved) SetFloat32(i, ch int, s Float32Sample) {
a.Data[ch][i] = float32(s)
loc := i * 4
v := math.Float32bits(float32(s))
a.Data[ch][loc] = uint8(v >> 24)
a.Data[ch][loc+1] = uint8(v >> 16)
a.Data[ch][loc+2] = uint8(v >> 8)
a.Data[ch][loc+3] = uint8(v)
}
// SubAudio returns part of the original audio sharing the buffer.
func (a *Float32NonInterleaved) SubAudio(offsetSamples, nSamples int) *Float32NonInterleaved {
ret := *a
ret.Size.Len = nSamples
offsetSamples *= 4
nSamples *= 4
for i := range a.Data {
ret.Data[i] = ret.Data[i][offsetSamples : offsetSamples+nSamples]
}
ret.Size.Len = nSamples
return &ret
}
func NewFloat32NonInterleaved(size ChunkInfo) *Float32NonInterleaved {
d := make([][]float32, size.Channels)
d := make([][]uint8, size.Channels)
for i := 0; i < size.Channels; i++ {
d[i] = make([]float32, size.Len)
d[i] = make([]uint8, size.Len*4)
}
return &Float32NonInterleaved{
Data: d,

View File

@@ -1,39 +1,51 @@
package wave
import (
"math"
"reflect"
"testing"
)
func float32ToUint8(vs ...float32) []uint8 {
var b []uint8
for _, v := range vs {
s := math.Float32bits(v)
b = append(b, uint8(s>>24), uint8(s>>16), uint8(s>>8), uint8(s))
}
return b
}
func TestFloat32(t *testing.T) {
expected := [][]float32{
{0.0, 1.0, 2.0, 3.0},
{4.0, 5.0, 6.0, 7.0},
}
cases := map[string]struct {
in Audio
expected [][]float32
}{
"Interleaved": {
in: &Float32Interleaved{
Data: []float32{
0.1, -0.5, 0.2, -0.6, 0.3, -0.7, 0.4, -0.8, 0.5, -0.9, 0.6, -1.0, 0.7, -1.1, 0.8, -1.2,
},
Size: ChunkInfo{8, 2, 48000},
},
expected: [][]float32{
{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
Data: float32ToUint8(
0.0, 4.0, 1.0, 5.0,
2.0, 6.0, 3.0, 7.0,
),
Size: ChunkInfo{4, 2, 48000},
},
expected: expected,
},
"NonInterleaved": {
in: &Float32NonInterleaved{
Data: [][]float32{
{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
Data: [][]uint8{
float32ToUint8(expected[0]...),
float32ToUint8(expected[1]...),
},
Size: ChunkInfo{8, 2, 48000},
},
expected: [][]float32{
{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
Size: ChunkInfo{4, 2, 48000},
},
expected: expected,
},
}
for name, c := range cases {
@@ -55,38 +67,43 @@ func TestFloat32(t *testing.T) {
func TestFloat32SubAudio(t *testing.T) {
t.Run("Interleaved", func(t *testing.T) {
in := &Float32Interleaved{
Data: []float32{
0.1, -0.5, 0.2, -0.6, 0.3, -0.7, 0.4, -0.8, 0.5, -0.9, 0.6, -1.0, 0.7, -1.1, 0.8, -1.2,
},
Size: ChunkInfo{8, 2, 48000},
// Data: []uint8{
// 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
// 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
// },
Data: float32ToUint8(
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
),
Size: ChunkInfo{4, 2, 48000},
}
expected := &Float32Interleaved{
Data: []float32{
0.3, -0.7, 0.4, -0.8, 0.5, -0.9,
},
Size: ChunkInfo{3, 2, 48000},
Data: float32ToUint8(
5.0, 6.0, 7.0, 8.0,
),
Size: ChunkInfo{2, 2, 48000},
}
out := in.SubAudio(2, 3)
out := in.SubAudio(2, 2)
if !reflect.DeepEqual(expected, out) {
t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
}
})
t.Run("NonInterleaved", func(t *testing.T) {
in := &Float32NonInterleaved{
Data: [][]float32{
{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8},
{-0.5, -0.6, -0.7, -0.8, -0.9, -1.0, -1.1, -1.2},
Data: [][]uint8{
float32ToUint8(1.0, 2.0, 3.0, 4.0),
float32ToUint8(5.0, 6.0, 7.0, 8.0),
},
Size: ChunkInfo{8, 2, 48000},
Size: ChunkInfo{4, 2, 48000},
}
expected := &Float32NonInterleaved{
Data: [][]float32{
{0.3, 0.4, 0.5},
{-0.7, -0.8, -0.9},
Data: [][]uint8{
float32ToUint8(3.0, 4.0),
float32ToUint8(7.0, 8.0),
},
Size: ChunkInfo{3, 2, 48000},
Size: ChunkInfo{2, 2, 48000},
}
out := in.SubAudio(2, 3)
out := in.SubAudio(2, 2)
if !reflect.DeepEqual(expected, out) {
t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
}

View File

@@ -9,7 +9,7 @@ func (s Int16Sample) Int() int64 {
// Int16Interleaved multi-channel interlaced Audio.
type Int16Interleaved struct {
Data []int16
Data []uint8
Size ChunkInfo
}
@@ -23,22 +23,29 @@ func (a *Int16Interleaved) SampleFormat() SampleFormat {
}
func (a *Int16Interleaved) At(i, ch int) Sample {
return Int16Sample(a.Data[i*a.Size.Channels+ch])
loc := 2 * (i*a.Size.Channels + ch)
var s Int16Sample
s |= Int16Sample(a.Data[loc]) << 8
s |= Int16Sample(a.Data[loc+1])
return s
}
func (a *Int16Interleaved) Set(i, ch int, s Sample) {
a.Data[i*a.Size.Channels+ch] = int16(Int16SampleFormat.Convert(s).(Int16Sample))
a.SetInt16(i, ch, Int16SampleFormat.Convert(s).(Int16Sample))
}
func (a *Int16Interleaved) SetInt16(i, ch int, s Int16Sample) {
a.Data[i*a.Size.Channels+ch] = int16(s)
loc := 2 * (i*a.Size.Channels + ch)
a.Data[loc] = uint8(s >> 8)
a.Data[loc+1] = uint8(s)
}
// SubAudio returns part of the original audio sharing the buffer.
func (a *Int16Interleaved) SubAudio(offsetSamples, nSamples int) *Int16Interleaved {
ret := *a
offset := offsetSamples * a.Size.Channels
n := nSamples * a.Size.Channels
offset := 2 * offsetSamples * a.Size.Channels
n := 2 * nSamples * a.Size.Channels
ret.Data = ret.Data[offset : offset+n]
ret.Size.Len = nSamples
return &ret
@@ -46,14 +53,14 @@ func (a *Int16Interleaved) SubAudio(offsetSamples, nSamples int) *Int16Interleav
func NewInt16Interleaved(size ChunkInfo) *Int16Interleaved {
return &Int16Interleaved{
Data: make([]int16, size.Channels*size.Len),
Data: make([]uint8, size.Channels*size.Len*2),
Size: size,
}
}
// Int16NonInterleaved multi-channel interlaced Audio.
type Int16NonInterleaved struct {
Data [][]int16
Data [][]uint8
Size ChunkInfo
}
@@ -67,31 +74,41 @@ func (a *Int16NonInterleaved) SampleFormat() SampleFormat {
}
func (a *Int16NonInterleaved) At(i, ch int) Sample {
return Int16Sample(a.Data[ch][i])
loc := i * 2
var s Int16Sample
s |= Int16Sample(a.Data[ch][loc]) << 8
s |= Int16Sample(a.Data[ch][loc+1])
return s
}
func (a *Int16NonInterleaved) Set(i, ch int, s Sample) {
a.Data[ch][i] = int16(Int16SampleFormat.Convert(s).(Int16Sample))
a.SetInt16(i, ch, Int16SampleFormat.Convert(s).(Int16Sample))
}
func (a *Int16NonInterleaved) SetInt16(i, ch int, s Int16Sample) {
a.Data[ch][i] = int16(s)
loc := i * 2
a.Data[ch][loc] = uint8(s >> 8)
a.Data[ch][loc+1] = uint8(s)
}
// SubAudio returns part of the original audio sharing the buffer.
func (a *Int16NonInterleaved) SubAudio(offsetSamples, nSamples int) *Int16NonInterleaved {
ret := *a
ret.Size.Len = nSamples
nSamples *= 2
offsetSamples *= 2
for i := range a.Data {
ret.Data[i] = ret.Data[i][offsetSamples : offsetSamples+nSamples]
}
ret.Size.Len = nSamples
return &ret
}
func NewInt16NonInterleaved(size ChunkInfo) *Int16NonInterleaved {
d := make([][]int16, size.Channels)
d := make([][]uint8, size.Channels)
for i := 0; i < size.Channels; i++ {
d[i] = make([]int16, size.Len)
d[i] = make([]uint8, size.Len*2)
}
return &Int16NonInterleaved{
Data: d,

View File

@@ -12,27 +12,28 @@ func TestInt16(t *testing.T) {
}{
"Interleaved": {
in: &Int16Interleaved{
Data: []int16{
1, -5, 2, -6, 3, -7, 4, -8, 5, -9, 6, -10, 7, -11, 8, -12,
Data: []uint8{
0, 1, 1, 2, 2, 3, 3, 4,
4, 5, 5, 6, 6, 7, 7, 8,
},
Size: ChunkInfo{8, 2, 48000},
Size: ChunkInfo{4, 2, 48000},
},
expected: [][]int16{
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
{(0 << 8) | 1, (2 << 8) | 3, (4 << 8) | 5, (6 << 8) | 7},
{(1 << 8) | 2, (3 << 8) | 4, (5 << 8) | 6, (7 << 8) | 8},
},
},
"NonInterleaved": {
in: &Int16NonInterleaved{
Data: [][]int16{
Data: [][]uint8{
{0, 1, 2, 3, 4, 5, 6, 7},
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
},
Size: ChunkInfo{8, 2, 48000},
Size: ChunkInfo{4, 2, 48000},
},
expected: [][]int16{
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
{(0 << 8) | 1, (2 << 8) | 3, (4 << 8) | 5, (6 << 8) | 7},
{(1 << 8) | 2, (3 << 8) | 4, (5 << 8) | 6, (7 << 8) | 8},
},
},
}
@@ -55,38 +56,39 @@ func TestInt16(t *testing.T) {
func TestInt32SubAudio(t *testing.T) {
t.Run("Interleaved", func(t *testing.T) {
in := &Int16Interleaved{
Data: []int16{
1, -5, 2, -6, 3, -7, 4, -8, 5, -9, 6, -10, 7, -11, 8, -12,
Data: []uint8{
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
},
Size: ChunkInfo{8, 2, 48000},
Size: ChunkInfo{4, 2, 48000},
}
expected := &Int16Interleaved{
Data: []int16{
3, -7, 4, -8, 5, -9,
Data: []uint8{
9, 10, 11, 12, 13, 14, 15, 16,
},
Size: ChunkInfo{3, 2, 48000},
Size: ChunkInfo{2, 2, 48000},
}
out := in.SubAudio(2, 3)
out := in.SubAudio(2, 2)
if !reflect.DeepEqual(expected, out) {
t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
}
})
t.Run("NonInterleaved", func(t *testing.T) {
in := &Int16NonInterleaved{
Data: [][]int16{
{1, 2, 3, 4, 5, 6, 7, 8},
{-5, -6, -7, -8, -9, -10, -11, -12},
Data: [][]uint8{
{1, 2, 5, 6, 9, 10, 13, 14},
{3, 4, 7, 8, 11, 12, 15, 16},
},
Size: ChunkInfo{8, 2, 48000},
Size: ChunkInfo{4, 2, 48000},
}
expected := &Int16NonInterleaved{
Data: [][]int16{
{3, 4, 5},
{-7, -8, -9},
Data: [][]uint8{
{9, 10, 13, 14},
{11, 12, 15, 16},
},
Size: ChunkInfo{3, 2, 48000},
Size: ChunkInfo{2, 2, 48000},
}
out := in.SubAudio(2, 3)
out := in.SubAudio(2, 2)
if !reflect.DeepEqual(expected, out) {
t.Errorf("SubAudio differs, expected: %v, got: %v", expected, out)
}

View File

@@ -19,10 +19,10 @@ func TestMonoMixer(t *testing.T) {
Len: 3,
Channels: 3,
},
Data: []int16{
0, 2, 4,
1, -2, 1,
3, 3, 6,
Data: []uint8{
0x00, 0x01, 0x00, 0x02, 0x00, 0x04,
0x00, 0x01, 0x00, 0x02, 0x00, 0x01,
0x00, 0x03, 0x00, 0x03, 0x00, 0x06,
},
},
dst: &wave.Int16Interleaved{
@@ -30,14 +30,14 @@ func TestMonoMixer(t *testing.T) {
Len: 3,
Channels: 1,
},
Data: make([]int16, 3),
Data: make([]uint8, 3*2),
},
expected: &wave.Int16Interleaved{
Size: wave.ChunkInfo{
Len: 3,
Channels: 1,
},
Data: []int16{2, 0, 4},
Data: []uint8{0x00, 0x02, 0x00, 0x01, 0x00, 0x04},
},
},
"MonoToStereo": {
@@ -46,21 +46,21 @@ func TestMonoMixer(t *testing.T) {
Len: 3,
Channels: 1,
},
Data: []int16{0, 2, 4},
Data: []uint8{0x00, 0x00, 0x00, 0x02, 0x00, 0x04},
},
dst: &wave.Int16Interleaved{
Size: wave.ChunkInfo{
Len: 3,
Channels: 2,
},
Data: make([]int16, 6),
Data: make([]uint8, 6*2),
},
expected: &wave.Int16Interleaved{
Size: wave.ChunkInfo{
Len: 3,
Channels: 2,
},
Data: []int16{0, 0, 2, 2, 4, 4},
Data: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x04, 0x00, 0x04},
},
},
}

101
rtp.go
View File

@@ -1,101 +0,0 @@
package mediadevices
import (
"fmt"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v2"
)
type RTPTracker struct {
videoEncoders []codec.VideoEncoderBuilder
audioEncoders []codec.AudioEncoderBuilder
}
type RTPTrackerOption func(*RTPTracker)
func WithVideoEncoders(codecs ...codec.VideoEncoderBuilder) func(*RTPTracker) {
return func(tracker *RTPTracker) {
tracker.videoEncoders = codecs
}
}
func WithAudioEncoders(codecs ...codec.AudioEncoderBuilder) func(*RTPTracker) {
return func(tracker *RTPTracker) {
tracker.audioEncoders = codecs
}
}
func NewRTPTracker(opts ...RTPTrackerOption) *RTPTracker {
var tracker RTPTracker
for _, opt := range opts {
opt(&tracker)
}
return &tracker
}
func (tracker *RTPTracker) Track(track Track) *RTPTrack {
rtpTrack := RTPTrack{
Track: track,
}
return &rtpTrack
}
type RTPTrack struct {
Track
tracker *RTPTracker
currentEncoder codec.ReadCloser
currentParams RTPParameters
lastProp prop.Media
}
func (track *RTPTrack) SetParameters(params RTPParameters) error {
var err error
switch t := track.Track.(type) {
case *VideoTrack:
err = track.setParametersVideo(t, &params)
case *AudioTrack:
err = track.setParametersAudio(t, &params)
default:
err = fmt.Errorf("unsupported track type")
}
if err == nil {
track.currentParams = params
}
return err
}
func (track *RTPTrack) setParametersVideo(videoTrack *VideoTrack, params *RTPParameters) error {
if params.SelectedCodec.Type != webrtc.RTPCodecTypeVideo {
return fmt.Errorf("invalid selected RTP codec type. Expected video but got audio")
}
video.DetectChanges(interval time.Duration, onChange func(prop.Media))
return nil
}
func (track *RTPTrack) setParametersAudio(audioTrack *AudioTrack, params *RTPParameters) error {
return nil
}
func (track *RTPTrack) ReadRTP() (*rtp.Packet, error) {
if track.currentEncoder == nil {
return nil, fmt.Errorf("Encoder has not been specified. Please call SetParameters to specify.")
}
return nil, nil
}
func (track *RTPTrack) WriteRTCP(packet rtcp.Packet) error {
return nil
}

35
sampler.go Normal file
View File

@@ -0,0 +1,35 @@
package mediadevices
import (
"math"
"time"
"github.com/pion/webrtc/v2/pkg/media"
)
type samplerFunc func(b []byte) error
// newVideoSampler creates a video sampler that uses the actual video frame rate and
// the codec's clock rate to come up with a duration for each sample.
func newVideoSampler(t LocalTrack) samplerFunc {
clockRate := float64(t.Codec().ClockRate)
lastTimestamp := time.Now()
return samplerFunc(func(b []byte) error {
now := time.Now()
duration := now.Sub(lastTimestamp).Seconds()
samples := uint32(math.Round(clockRate * duration))
lastTimestamp = now
return t.WriteSample(media.Sample{Data: b, Samples: samples})
})
}
// newAudioSampler creates a audio sampler that uses a fixed latency and
// the codec's clock rate to come up with a duration for each sample.
func newAudioSampler(t LocalTrack, latency time.Duration) samplerFunc {
samples := uint32(math.Round(float64(t.Codec().ClockRate) * latency.Seconds()))
return samplerFunc(func(b []byte) error {
return t.WriteSample(media.Sample{Data: b, Samples: samples})
})
}

369
track.go
View File

@@ -1,29 +1,22 @@
package mediadevices
import (
"fmt"
"image"
"errors"
"math/rand"
"sync"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/driver"
"github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/wave"
mio "github.com/pion/mediadevices/pkg/io"
"github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v2/pkg/media"
)
// TrackKind represents content type of a track
type TrackKind string
const (
TrackKindVideo TrackKind = "video"
TrackKindAudio TrackKind = "audio"
)
// Track is an interface that represent MediaStreamTrack
// Tracker is an interface that represent MediaStreamTrack
// Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack
type Track interface {
ID() string
Kind() TrackKind
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
@@ -31,170 +24,18 @@ type Track interface {
OnEnded(func(error))
}
// VideoTrack is a specialized track for video
type VideoTrack struct {
baseTrack
src video.Reader
transformed video.Reader
mux sync.Mutex
frameCount int
lastFrame image.Image
lastErr error
type LocalTrack interface {
WriteSample(s media.Sample) error
Codec() *webrtc.RTPCodec
ID() string
Kind() webrtc.RTPCodecType
}
func newVideoTrack(d driver.Driver, constraints MediaTrackConstraints) (*VideoTrack, error) {
err := d.Open()
if err != nil {
return nil, err
}
recorder, ok := d.(driver.VideoRecorder)
if !ok {
d.Close()
return nil, fmt.Errorf("driver is not an video recorder")
}
r, err := recorder.VideoRecord(constraints.selectedMedia)
if err != nil {
d.Close()
return nil, err
}
return &VideoTrack{
baseTrack: newBaseTrack(d, constraints),
src: r,
transformed: r,
}, nil
}
// Kind returns track's kind
func (track *VideoTrack) Kind() TrackKind {
return TrackKindVideo
}
// NewReader returns a reader to read frames from the source. You may create multiple
// readers and read from them in different goroutines.
//
// In the case of multiple readers, reading from the source will only get triggered
// when the reader has the latest frame from the source
func (track *VideoTrack) NewReader() video.Reader {
var curFrameCount int
return video.ReaderFunc(func() (img image.Image, err error) {
track.mux.Lock()
defer track.mux.Unlock()
if curFrameCount != track.frameCount {
img = copyFrame(img, track.lastFrame)
err = track.lastErr
} else {
img, err = track.transformed.Read()
track.lastFrame = img
track.lastErr = err
track.frameCount++
if err != nil {
track.onErrorHandler(err)
}
}
curFrameCount = track.frameCount
return
})
}
// TODO: implement copy in place
func copyFrame(dst, src image.Image) image.Image { return src }
// Transform transforms the underlying source. The transformation will reflect to
// all readers
func (track *VideoTrack) Transform(fns ...video.TransformFunc) {
track.mux.Lock()
defer track.mux.Unlock()
track.transformed = video.Merge(fns...)(track.src)
}
// AudioTrack is a specialized track for audio
type AudioTrack struct {
baseTrack
src audio.Reader
transformed audio.Reader
mux sync.Mutex
chunkCount int
lastChunks wave.Audio
lastErr error
}
func newAudioTrack(d driver.Driver, constraints MediaTrackConstraints) (*AudioTrack, error) {
err := d.Open()
if err != nil {
return nil, err
}
recorder, ok := d.(driver.AudioRecorder)
if !ok {
d.Close()
return nil, fmt.Errorf("driver is not an audio recorder")
}
r, err := recorder.AudioRecord(constraints.selectedMedia)
if err != nil {
d.Close()
return nil, err
}
return &AudioTrack{
baseTrack: newBaseTrack(d, constraints),
src: r,
transformed: r,
}, nil
}
func (track *AudioTrack) Kind() TrackKind {
return TrackKindAudio
}
// NewReader returns a reader to read audio chunks from the source. You may create multiple
// readers and read from them in different goroutines.
//
// In the case of multiple readers, reading from the source will only get triggered
// when the reader has the latest chunk from the source
func (track *AudioTrack) NewReader() audio.Reader {
var currChunkCount int
return audio.ReaderFunc(func() (chunks wave.Audio, err error) {
track.mux.Lock()
defer track.mux.Unlock()
if currChunkCount != track.chunkCount {
chunks = copyChunks(chunks, track.lastChunks)
err = track.lastErr
} else {
chunks, err = track.transformed.Read()
track.lastChunks = chunks
track.lastErr = err
track.chunkCount++
if err != nil {
track.onErrorHandler(err)
}
}
currChunkCount = track.chunkCount
return
})
}
// TODO: implement copy in place
func copyChunks(dst, src wave.Audio) wave.Audio { return src }
// Transform transforms the underlying source. The transformation will reflect to
// all readers
func (track *AudioTrack) Transform(fns ...audio.TransformFunc) {
track.mux.Lock()
defer track.mux.Unlock()
track.transformed = audio.Merge(fns...)(track.src)
}
type baseTrack struct {
d driver.Driver
constraints MediaTrackConstraints
type track struct {
localTrack LocalTrack
d driver.Driver
sample samplerFunc
encoder codec.ReadCloser
onErrorHandler func(error)
err error
@@ -202,17 +43,83 @@ type baseTrack struct {
endOnce sync.Once
}
func newBaseTrack(d driver.Driver, constraints MediaTrackConstraints) baseTrack {
return baseTrack{d: d, constraints: constraints}
}
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 err error
func (t *baseTrack) ID() string {
return t.d.ID()
err = d.Open()
if err != nil {
return nil, err
}
switch r := d.(type) {
case driver.VideoRecorder:
rtpCodecs = opts.codecs[webrtc.RTPCodecTypeVideo]
buildSampler = newVideoSampler
encoderBuilders, err = newVideoEncoderBuilders(r, constraints)
case driver.AudioRecorder:
rtpCodecs = opts.codecs[webrtc.RTPCodecTypeAudio]
buildSampler = func(t LocalTrack) samplerFunc {
return newAudioSampler(t, constraints.selectedMedia.Latency)
}
encoderBuilders, err = newAudioEncoderBuilders(r, constraints)
default:
err = errors.New("newTrack: invalid driver type")
}
if err != nil {
d.Close()
return nil, err
}
for _, builder := range encoderBuilders {
var matchedRTPCodec *webrtc.RTPCodec
for _, rtpCodec := range rtpCodecs {
if rtpCodec.Name == builder.name {
matchedRTPCodec = rtpCodec
break
}
}
if matchedRTPCodec == nil {
continue
}
localTrack, err := opts.trackGenerator(
matchedRTPCodec.PayloadType,
rand.Uint32(),
d.ID(),
matchedRTPCodec.Type.String(),
matchedRTPCodec,
)
if err != nil {
continue
}
encoder, err := builder.build()
if err != nil {
continue
}
t := track{
localTrack: localTrack,
sample: buildSampler(localTrack),
d: d,
encoder: encoder,
}
go t.start()
return &t, nil
}
d.Close()
return nil, errors.New("newTrack: failed to find a matching codec")
}
// 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 *baseTrack) OnEnded(handler func(error)) {
func (t *track) OnEnded(handler func(error)) {
t.mu.Lock()
t.onErrorHandler = handler
err := t.err
@@ -227,7 +134,7 @@ func (t *baseTrack) OnEnded(handler func(error)) {
}
// onError is a callback when an error occurs
func (t *baseTrack) onError(err error) {
func (t *track) onError(err error) {
t.mu.Lock()
t.err = err
handler := t.onErrorHandler
@@ -240,6 +147,92 @@ func (t *baseTrack) onError(err error) {
}
}
func (t *baseTrack) Stop() {
t.d.Close()
// start starts the data flow from the driver all the way to the localTrack
func (t *track) start() {
var n int
var err error
buff := make([]byte, 1024)
for {
n, err = t.encoder.Read(buff)
if err != nil {
if e, ok := err.(*mio.InsufficientBufferError); ok {
buff = make([]byte, 2*e.RequiredSize)
continue
}
t.onError(err)
return
}
if err := t.sample(buff[:n]); err != nil {
t.onError(err)
return
}
}
}
// Stop stops the underlying driver and encoder
func (t *track) Stop() {
t.d.Close()
t.encoder.Close()
}
func (t *track) Track() *webrtc.Track {
return t.localTrack.(*webrtc.Track)
}
func (t *track) LocalTrack() LocalTrack {
return t.localTrack
}
// encoderBuilder is a generic encoder builder that acts as a delegator for codec.VideoEncoderBuilder and
// codec.AudioEncoderBuilder. The idea of having a delegator is to reduce redundant codes that are being
// duplicated for managing video and audio.
type encoderBuilder struct {
name string
build func() (codec.ReadCloser, error)
}
// newVideoEncoderBuilders transforms video given by VideoRecorder with the video transformer that is passed through
// constraints and create a list of generic encoder builders
func newVideoEncoderBuilders(vr driver.VideoRecorder, constraints MediaTrackConstraints) ([]encoderBuilder, error) {
r, err := vr.VideoRecord(constraints.selectedMedia)
if err != nil {
return nil, err
}
if constraints.VideoTransform != nil {
r = constraints.VideoTransform(r)
}
encoderBuilders := make([]encoderBuilder, len(constraints.VideoEncoderBuilders))
for i, b := range constraints.VideoEncoderBuilders {
encoderBuilders[i].name = b.Name()
encoderBuilders[i].build = func() (codec.ReadCloser, error) {
return b.BuildVideoEncoder(r, constraints.selectedMedia)
}
}
return encoderBuilders, nil
}
// newAudioEncoderBuilders transforms audio given by AudioRecorder with the audio transformer that is passed through
// constraints and create a list of generic encoder builders
func newAudioEncoderBuilders(ar driver.AudioRecorder, constraints MediaTrackConstraints) ([]encoderBuilder, error) {
r, err := ar.AudioRecord(constraints.selectedMedia)
if err != nil {
return nil, err
}
if constraints.AudioTransform != nil {
r = constraints.AudioTransform(r)
}
encoderBuilders := make([]encoderBuilder, len(constraints.AudioEncoderBuilders))
for i, b := range constraints.AudioEncoderBuilders {
encoderBuilders[i].name = b.Name()
encoderBuilders[i].build = func() (codec.ReadCloser, error) {
return b.BuildAudioEncoder(r, constraints.selectedMedia)
}
}
return encoderBuilders, nil
}

View File

@@ -10,7 +10,7 @@ func TestOnEnded(t *testing.T) {
errExpected := errors.New("an error")
t.Run("ErrorAfterRegister", func(t *testing.T) {
tr := &baseTrack{}
tr := &track{}
called := make(chan error, 1)
tr.OnEnded(func(error) {
@@ -35,7 +35,7 @@ func TestOnEnded(t *testing.T) {
})
t.Run("ErrorBeforeRegister", func(t *testing.T) {
tr := &baseTrack{}
tr := &track{}
tr.onError(errExpected)

View File

@@ -1,45 +0,0 @@
package mediadevices
import (
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v2"
)
// == WebRTC v3 design ==
// Reader is an interface to handle incoming RTP stream.
type Reader interface {
ReadRTP() (*rtp.Packet, error)
WriteRTCP(rtcp.Packet) error
}
// TrackBase represents common MediaStreamTrack functionality of LocalTrack and RemoteTrack.
type TrackBase interface {
ID() string
}
type LocalRTPTrack interface {
TrackBase
Reader
// SetParameters sets information about how the data is to be encoded.
// This will be called by PeerConnection according to the result of
// SDP based negotiation.
// It will be called via RTPSender.Parameters() by PeerConnection to
// tell the negotiated media codec information.
//
// This is pion's extension to process data without having encoder/decoder
// in webrtc package.
SetParameters(RTPParameters) error
}
// RTPParameters represents RTCRtpParameters which contains information about
// how the RTC data is to be encoded/decoded.
//
// ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSendParameters
type RTPParameters struct {
SSRC uint32
SelectedCodec *webrtc.RTPCodec
Codecs []*webrtc.RTPCodec
}