Compare commits

..

9 Commits

Author SHA1 Message Date
Atsushi Watanabe
bb7611783d codec/vpx: support temporal layer 2020-05-31 03:14:12 +09:00
Atsushi Watanabe
00eca231a7 Select by DeviceID using StringConstraint 2020-05-24 11:15:29 -04:00
Atsushi Watanabe
27d966611e prop: add documents 2020-05-24 10:26:16 +09:00
Atsushi Watanabe
ecff5e63a5 prop: support ranged/exact/oneof constraints 2020-05-24 10:26:16 +09:00
Atsushi Watanabe
305b7086e3 Fix bitrate measurement stability
- improve accuracy of bitrate calculation
- reduce test input timing error
2020-05-23 15:17:59 -04:00
Renovate Bot
6471064956 Update module pion/webrtc/v2 to v2.2.14
Generated by Renovate Bot
2020-05-20 15:26:18 +09:00
Lukas Herman
c6e685964f Fix wrong required size 2020-05-15 00:48:54 -04:00
Renovate Bot
65b744f639 Update module pion/webrtc/v2 to v2.2.9
Generated by Renovate Bot
2020-05-11 16:20:41 +09:00
Renovate Bot
a2b74babc4 Update github.com/jfreymuth/pulse commit hash to 1534c4a
Generated by Renovate Bot
2020-05-11 16:19:14 +09:00
32 changed files with 1416 additions and 727 deletions

View File

@@ -6,8 +6,10 @@ import (
"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"
@@ -55,22 +57,22 @@ func main() {
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
md := mediadevices.NewMediaDevices(
peerConnection,
mediadevices.WithVideoEncoders(&vp8Params),
mediadevices.WithVideoTransformers(markFacesTransformer),
)
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(p *prop.Media) {
p.Width = 640
p.Height = 480
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 {

View File

@@ -6,8 +6,10 @@ import (
"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"
@@ -24,12 +26,6 @@ func main() {
return
}
vp8Params, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vp8Params.BitRate = 100000 // 100kbps
md := mediadevices.NewMediaDevicesFromCodecs(
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
@@ -43,13 +39,21 @@ func main() {
return newTrack(codec, id, os.Args[1]), nil
},
),
mediadevices.WithVideoEncoders(&vp8Params),
)
vp8Params, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vp8Params.BitRate = 100000 // 100kbps
_, err = md.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(p *prop.Media) {
p.Width = 640
p.Height = 480
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 {

View File

@@ -5,13 +5,10 @@ import (
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/examples/internal/signal"
"github.com/pion/mediadevices/pkg/codec/openh264"
// 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/driver/videotest" // This is required to register screen capture adapter
extwebrtc "github.com/pion/mediadevices/pkg/ext/webrtc"
"github.com/pion/mediadevices/pkg/prop"
"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"
)
@@ -28,16 +25,12 @@ func main() {
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
openh264Encoder, err := openh264.NewParams()
if err != nil {
// Create a new RTCPeerConnection
mediaEngine := webrtc.MediaEngine{}
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
panic(err)
}
openh264Encoder.BitRate = 100000 // 100kbps
// Create a new RTCPeerConnection
mediaEngine := extwebrtc.MediaEngine{}
mediaEngine.AddEncoderBuilders(&openh264Encoder)
api := extwebrtc.NewAPI(extwebrtc.WithMediaEngine(mediaEngine))
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(config)
if err != nil {
panic(err)
@@ -49,15 +42,32 @@ func main() {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
s, err := mediadevices.GetDisplayMedia(mediadevices.MediaStreamConstraints{
Video: func(p *prop.Media) {},
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 _, track := range s.GetTracks() {
_, err = peerConnection.ExtAddTransceiverFromTrack(track,
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,
},

View File

@@ -2,64 +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(p *prop.Media) {},
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)
}
})
}
log.Println(http.ListenAndServe(":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 {}
}

4
go.mod
View File

@@ -5,9 +5,9 @@ go 1.13
require (
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
github.com/faiface/beep v1.0.2
github.com/jfreymuth/pulse v0.0.0-20200424182717-3b0820ad352f
github.com/jfreymuth/pulse v0.0.0-20200506145638-1534c4af9659
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4
github.com/pion/webrtc/v2 v2.2.8
github.com/pion/webrtc/v2 v2.2.14
github.com/satori/go.uuid v1.2.0
golang.org/x/image v0.0.0-20200430140353-33d19683fad8
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3

30
go.sum
View File

@@ -28,8 +28,8 @@ github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTg
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/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
github.com/jfreymuth/pulse v0.0.0-20200424182717-3b0820ad352f h1:XyMNiJ5vCUTlgl4R/pfw11rzt1sbdzNLbZCk/bb3LfU=
github.com/jfreymuth/pulse v0.0.0-20200424182717-3b0820ad352f/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/jfreymuth/pulse v0.0.0-20200506145638-1534c4af9659 h1:DRA4BuRlhEILiud720WFWqqdADPzp1jTjQvyCr/PP80=
github.com/jfreymuth/pulse v0.0.0-20200506145638-1534c4af9659/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -50,12 +50,12 @@ 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.16 h1:dvuDC0IBMUDQvwO+gRu0Dv+W5j7rrgNpCmtheb6iYnc=
github.com/pion/datachannel v1.4.16/go.mod h1:gRGhxZv7X2/30Qxes4WEXtimKBXcwj/3WsDtBlHnvJY=
github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5hE=
github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM=
github.com/pion/dtls/v2 v2.0.0 h1:Fk+MBhLZ/U1bImzAhmzwbO/pP2rKhtTw8iA934H3ybE=
github.com/pion/dtls/v2 v2.0.0/go.mod h1:VkY5VL2wtsQQOG60xQ4lkV5pdn0wwBBTzCfRJqXhp3A=
github.com/pion/ice v0.7.14 h1:lin/tzVc562t0Qk62/JlfOMX/RWuUSq/YyXakH2HTTQ=
github.com/pion/ice v0.7.14/go.mod h1:/Lz6jAUhsvXed7kNJImXtvVSgjtcdGKoZAZIYb9WEm0=
github.com/pion/ice v0.7.15 h1:s1In+gnuyVq7WKWGVQL+1p+OcrMsbfL+VfSe2isH8Ag=
github.com/pion/ice v0.7.15/go.mod h1:Z6zybEQgky5mZkKcLfmvc266JukK2srz3VZBBD1iXBw=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
@@ -64,16 +64,14 @@ github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak=
github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
github.com/pion/rtp v1.3.2 h1:Yfzf1mU4Zmg7XWHitzYe2i+l+c68iO+wshzIUW44p1c=
github.com/pion/rtp v1.3.2/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY=
github.com/pion/rtp v1.4.0 h1:EkeHEXKuJhZoRUxtL2Ie80vVg9gBH+poT9UoL8M14nw=
github.com/pion/rtp v1.4.0/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
github.com/pion/rtp v1.5.4 h1:PuNg6xqV3brIUihatcKZj1YDUs+M45L0ZbrZWYtkDxY=
github.com/pion/rtp v1.5.4/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg=
github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4=
github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8=
github.com/pion/sdp/v2 v2.3.7 h1:WUZHI3pfiYCaE8UGUYcabk863LCK+Bq3AklV5O0oInQ=
github.com/pion/sdp/v2 v2.3.7/go.mod h1:+ZZf35r1+zbaWYiZLfPutWfx58DAWcGb2QsS3D/s9M8=
github.com/pion/srtp v1.3.1 h1:WNDLN41ST0P6cXRpzx97JJW//vChAEo1+Etdqo+UMnM=
github.com/pion/srtp v1.3.1/go.mod h1:nxEytDDGTN+eNKJ1l5gzOCWQFuksgijorsSlgEjc40Y=
github.com/pion/srtp v1.3.3 h1:8bjs9YaSNvSrbH0OfKxzPX+PTrCyAC2LoT9Qesugi+U=
github.com/pion/srtp v1.3.3/go.mod h1:jNe0jmIOqksuurR9S/7yoKDalfPeluUFrNPCBqI4FOI=
github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw=
github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
@@ -83,8 +81,8 @@ github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA=
github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs=
github.com/pion/webrtc/v2 v2.2.8 h1:vCSPnXmERhJTNfkPztkEQb8YKI1jrtGSK9e7/aZ4jOc=
github.com/pion/webrtc/v2 v2.2.8/go.mod h1:Zl5bY5AGfc9gW0U20VSGHUKbiDcfuRDEmsb7cte8cwk=
github.com/pion/webrtc/v2 v2.2.14 h1:bRjnXTqMDJ3VERPF45z439Sv6QfDfjdYvdQk1QcIx8M=
github.com/pion/webrtc/v2 v2.2.14/go.mod h1:G+8lShCMbHhjpMF1ZJBkyuvrxXrvW4bxs3nOt+mJ2UI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -118,8 +116,8 @@ golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@@ -11,12 +11,15 @@ import (
func MeasureBitRate(r io.Reader, dur time.Duration) (float64, error) {
var n, totalBytes int
var err error
buf := make([]byte, 1024)
start := time.Now()
now := start
end := now.Add(dur)
for now.Before(end) {
for {
n, err = r.Read(buf)
now = time.Now()
if err != nil {
if e, ok := err.(*mio.InsufficientBufferError); ok {
buf = make([]byte, 2*e.RequiredSize)
@@ -24,6 +27,7 @@ func MeasureBitRate(r io.Reader, dur time.Duration) (float64, error) {
}
if err == io.EOF {
dur = now.Sub(start)
totalBytes += n
break
}
@@ -31,11 +35,12 @@ func MeasureBitRate(r io.Reader, dur time.Duration) (float64, error) {
return 0, err
}
totalBytes += n
now = time.Now()
if now.After(end) {
break
}
totalBytes += n // count bytes if the data arrived within the period
}
elapsed := time.Now().Sub(start).Seconds()
avg := float64(totalBytes*8) / elapsed
avg := float64(totalBytes*8) / dur.Seconds()
return avg, nil
}

View File

@@ -9,18 +9,25 @@ import (
func TestMeasureBitRateStatic(t *testing.T) {
r, w := io.Pipe()
dur := time.Second * 5
dataSize := 1000
var precision float64 = 8 // 1 byte
const (
dataSize = 1000
dur = 5 * time.Second
packetInterval = time.Second
precision = 8.0 // 1 byte
)
var wg sync.WaitGroup
wg.Add(1)
done := make(chan struct{})
go func() {
data := make([]byte, dataSize)
ticker := time.NewTicker(packetInterval)
// Wait half interval
time.Sleep(packetInterval / 2)
// Make sure that this goroutine is synchronized with main goroutine
wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
@@ -48,30 +55,34 @@ func TestMeasureBitRateStatic(t *testing.T) {
func TestMeasureBitRateDynamic(t *testing.T) {
r, w := io.Pipe()
dur := time.Second * 5
dataSize := 1000
var precision float64 = 8 // 1 byte
const (
dataSize = 1000
dur = 5 * time.Second
packetInterval = time.Millisecond * 250
precision = 8.0 // 1 byte
)
var wg sync.WaitGroup
wg.Add(1)
done := make(chan struct{})
go func() {
data := make([]byte, dataSize)
wg.Done()
ticker := time.NewTicker(time.Millisecond * 500)
var count int
ticker := time.NewTicker(packetInterval)
// Wait half interval
time.Sleep(packetInterval / 2)
wg.Done()
var count int
for {
select {
case <-ticker.C:
w.Write(data)
count++
// Wait until 4 slow ticks, which is also equal to 2 seconds
if count == 4 {
ticker.Stop()
// Speed up the tick by 2 times for the rest
ticker = time.NewTicker(time.Millisecond * 250)
// 4 x 500ms ticks and 250ms ticks
if count%2 == 1 || count >= 8 {
w.Write(data)
}
count++
case <-done:
w.Close()
return

View File

@@ -6,37 +6,106 @@ import (
"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 prop.Media
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 +115,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 prop.Media
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 prop.Media
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
}
@@ -116,7 +191,7 @@ func queryDriverProperties(filter driver.FilterFn) map[driver.Driver][]prop.Medi
// select implements SelectSettings algorithm.
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings
func selectBestDriver(filter driver.FilterFn, constraints prop.Media) (driver.Driver, prop.Media, error) {
func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) {
var bestDriver driver.Driver
var bestProp prop.Media
minFitnessDist := math.Inf(1)
@@ -125,7 +200,11 @@ func selectBestDriver(filter driver.FilterFn, constraints prop.Media) (driver.Dr
for d, props := range driverProperties {
priority := float64(d.Info().Priority)
for _, p := range props {
fitnessDist := constraints.FitnessDistance(p) - priority
fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p)
if !ok {
continue
}
fitnessDist -= priority
if fitnessDist < minFitnessDist {
minFitnessDist = fitnessDist
bestDriver = d
@@ -135,64 +214,51 @@ func selectBestDriver(filter driver.FilterFn, constraints prop.Media) (driver.Dr
}
if bestDriver == nil {
return nil, prop.Media{}, errNotFound
return nil, MediaTrackConstraints{}, errNotFound
}
constraints.Merge(bestProp)
constraints.selectedMedia = bestProp
constraints.selectedMedia.Merge(constraints.MediaConstraints)
return bestDriver, constraints, nil
}
func selectAudio(constraints prop.Media) (Track, error) {
func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
typeFilter := driver.FilterAudioRecorder()
filter := typeFilter
if constraints.DeviceID != "" {
idFilter := driver.FilterID(constraints.DeviceID)
filter = driver.FilterAnd(typeFilter, idFilter)
}
d, c, err := selectBestDriver(filter, constraints)
d, c, err := selectBestDriver(typeFilter, constraints)
if err != nil {
return nil, err
}
return newAudioTrack(d, c)
return newTrack(&m.MediaDevicesOptions, d, c)
}
func selectVideo(constraints prop.Media) (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)
if constraints.DeviceID != "" {
idFilter := driver.FilterID(constraints.DeviceID)
filter = driver.FilterAnd(typeFilter, notScreenFilter, idFilter)
}
d, c, err := selectBestDriver(filter, constraints)
if err != nil {
return nil, err
}
return newVideoTrack(d, c)
return newTrack(&m.MediaDevicesOptions, d, c)
}
func selectScreen(constraints prop.Media) (Track, error) {
func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker, error) {
typeFilter := driver.FilterVideoRecorder()
screenFilter := driver.FilterDeviceType(driver.Screen)
filter := driver.FilterAnd(typeFilter, screenFilter)
if constraints.DeviceID != "" {
idFilter := driver.FilterID(constraints.DeviceID)
filter = driver.FilterAnd(typeFilter, screenFilter, idFilter)
}
d, c, err := selectBestDriver(filter, constraints)
if err != nil {
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

@@ -18,25 +18,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: []*webrtc.RTPCodec{
@@ -53,36 +46,43 @@ func TestGetUserMedia(t *testing.T) {
return newMockTrack(codec, id), nil
},
),
WithVideoEncoders(&brokenVideoParams),
WithAudioEncoders(&audioParams),
)
constraints := MediaStreamConstraints{
Video: func(c *MediaTrackConstraints) {
c.Enabled = true
c.Width = prop.Int(640)
c.Height = prop.Int(480)
params := videoParams
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&params}
},
Audio: func(c *MediaTrackConstraints) {
c.Enabled = true
params := audioParams
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{&params}
},
}
constraintsWrong := MediaStreamConstraints{
Video: func(c *MediaTrackConstraints) {
c.Enabled = true
c.Width = prop.Int(640)
c.Height = prop.Int(480)
params := videoParams
params.BitRate = 0
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&params}
},
Audio: func(c *MediaTrackConstraints) {
c.Enabled = true
params := audioParams
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{&params}
},
}
// 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,13 +1,41 @@
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"
)
type MediaStreamConstraints struct {
Audio MediaTrackConstraints
Video MediaTrackConstraints
Audio MediaOption
Video MediaOption
}
// MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
type MediaTrackConstraints func(*prop.Media)
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
}
type MediaOption func(*MediaTrackConstraints)

View File

@@ -1,77 +0,0 @@
package codec
import (
"math/rand"
mio "github.com/pion/mediadevices/pkg/io"
"github.com/pion/rtp"
"github.com/pion/webrtc/v2"
)
const (
defaultMTU = 1200
)
type rtpReadCloserImpl struct {
packetize func(payload []byte) []*rtp.Packet
encoder ReadCloser
buff []byte
unreadRTPPackets []*rtp.Packet
}
func NewRTPReadCloser(codec *webrtc.RTPCodec, reader ReadCloser, sample SamplerFunc) (RTPReadCloser, error) {
packetizer := rtp.NewPacketizer(
defaultMTU,
codec.PayloadType,
rand.Uint32(),
codec.Payloader,
rtp.NewRandomSequencer(),
codec.ClockRate,
)
return &rtpReadCloserImpl{
packetize: func(payload []byte) []*rtp.Packet {
return packetizer.Packetize(payload, sample())
},
}, nil
}
func (rc *rtpReadCloserImpl) ReadRTP() (packet *rtp.Packet, err error) {
var n int
packet = rc.readRTPPacket()
if packet != nil {
return
}
for {
n, err = rc.encoder.Read(rc.buff)
if err == nil {
break
}
e, ok := err.(*mio.InsufficientBufferError)
if !ok {
return nil, err
}
rc.buff = make([]byte, 2*e.RequiredSize)
}
rc.unreadRTPPackets = rc.packetize(rc.buff[:n])
return rc.readRTPPacket(), nil
}
// readRTPPacket reads unreadRTPPackets and mark the rtp packet as "read",
// which essentially removes it from the list. If the return value is nil,
// it means that there's no unread rtp packets.
func (rc *rtpReadCloserImpl) readRTPPacket() (packet *rtp.Packet) {
if len(rc.unreadRTPPackets) == 0 {
return
}
packet, rc.unreadRTPPackets = rc.unreadRTPPackets[0], rc.unreadRTPPackets[1:]
return
}
func (rc *rtpReadCloserImpl) Close() {
rc.encoder.Close()
}

View File

@@ -3,23 +3,33 @@ package codec
import (
"io"
"github.com/pion/mediadevices"
"github.com/pion/rtp"
"github.com/pion/webrtc/v2"
"github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
)
type RTPReader interface {
ReadRTP() (*rtp.Packet, error)
// AudioEncoderBuilder is the interface that wraps basic operations that are
// necessary to build the audio encoder.
//
// This interface is for codec implementors to provide codec specific params,
// but still giving generality for the users.
type AudioEncoderBuilder interface {
// Name represents the codec name
Name() string
// BuildAudioEncoder builds audio encoder by given media params and audio input
BuildAudioEncoder(r audio.Reader, p prop.Media) (ReadCloser, error)
}
type RTPReadCloser interface {
RTPReader
Close()
}
type EncoderBuilder interface {
Codec() *webrtc.RTPCodec
BuildEncoder(mediadevices.Track) (RTPReadCloser, error)
// VideoEncoderBuilder is the interface that wraps basic operations that are
// necessary to build the video encoder.
//
// This interface is for codec implementors to provide codec specific params,
// but still giving generality for the users.
type VideoEncoderBuilder interface {
// Name represents the codec name
Name() string
// BuildVideoEncoder builds video encoder by given media params and video input
BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error)
}
// ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame

View File

@@ -15,10 +15,10 @@ import (
"sync"
"unsafe"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec"
mio "github.com/pion/mediadevices/pkg/io"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
)
type encoder struct {
@@ -30,19 +30,17 @@ type encoder struct {
closed bool
}
func newEncoder(track *mediadevices.VideoTrack, params Params) (codec.ReadCloser, error) {
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
if params.BitRate == 0 {
params.BitRate = 100000
}
constraints := track.GetConstraints()
var rv C.int
cEncoder := C.enc_new(C.EncoderOptions{
width: C.int(constraints.Width),
height: C.int(constraints.Height),
width: C.int(p.Width),
height: C.int(p.Height),
target_bitrate: C.int(params.BitRate),
max_fps: C.float(constraints.FrameRate),
max_fps: C.float(p.FrameRate),
}, &rv)
if err := errResult(rv); err != nil {
return nil, fmt.Errorf("failed in creating encoder: %v", err)

View File

@@ -1,10 +1,9 @@
package openh264
import (
"fmt"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v2"
)
@@ -23,25 +22,11 @@ func NewParams() (Params, error) {
}
// Name represents the codec name
func (p *Params) Codec() *webrtc.RTPCodec {
return webrtc.NewRTPH264Codec(webrtc.DefaultPayloadTypeH264, 90000)
func (p *Params) Name() string {
return webrtc.H264
}
// BuildVideoEncoder builds openh264 encoder with given params
func (p *Params) BuildEncoder(track mediadevices.Track) (codec.RTPReadCloser, error) {
videoTrack, ok := track.(*mediadevices.VideoTrack)
if !ok {
return nil, fmt.Errorf("track is not a video track")
}
encoder, err := newEncoder(videoTrack, *p)
if err != nil {
return nil, err
}
return codec.NewRTPReadCloser(
p.Codec(),
encoder,
codec.NewVideoSampler(p.Codec().ClockRate),
)
func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
return newEncoder(r, property, *p)
}

View File

@@ -1,35 +0,0 @@
package codec
import (
"math"
"time"
)
// SamplerFunc returns the number of samples. Each invocation may return different
// different amount of samples due to how it's calculated/measured.
type SamplerFunc func() uint32
// 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(clockRate uint32) SamplerFunc {
clockRateFloat := float64(clockRate)
lastTimestamp := time.Now()
return SamplerFunc(func() uint32 {
now := time.Now()
duration := now.Sub(lastTimestamp).Seconds()
samples := uint32(math.Round(clockRateFloat * duration))
lastTimestamp = now
return 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(clockRate uint32, latency time.Duration) SamplerFunc {
samples := uint32(math.Round(float64(clockRate) * latency.Seconds()))
return SamplerFunc(func() uint32 {
return samples
})
}

View File

@@ -17,6 +17,13 @@ type Params struct {
RateControlMinQuantizer uint
RateControlMaxQuantizer uint
ErrorResilient ErrorResilientMode
TemporalLayers []TemporalLayer
}
// TemporalLayer represents temporal layer config.
type TemporalLayer struct {
TargetBitrate uint // in kbps
RateDecimator uint
}
// RateControlMode represents rate control mode.

View File

@@ -180,6 +180,12 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c
cfg.rc_resize_allowed = 0
cfg.g_pass = C.VPX_RC_ONE_PASS
cfg.ts_number_layers = C.uint(len(params.TemporalLayers))
for i := range params.TemporalLayers {
cfg.ts_target_bitrate[i] = C.uint(params.TemporalLayers[i].TargetBitrate)
cfg.ts_rate_decimator[i] = C.uint(params.TemporalLayers[i].RateDecimator)
}
raw := &C.vpx_image_t{}
if C.vpx_img_alloc(raw, C.VPX_IMG_FMT_I420, cfg.g_w, cfg.g_h, 1) == nil {
return nil, errors.New("vpx_img_alloc failed")

View File

@@ -71,7 +71,11 @@ func (w *adapterWrapper) Properties() []prop.Media {
return nil
}
return w.Adapter.Properties()
p := w.Adapter.Properties()
for i := range p {
p[i].DeviceID = w.id
}
return p
}
func (w *adapterWrapper) VideoRecord(p prop.Media) (r video.Reader, err error) {

View File

@@ -1,117 +0,0 @@
package webrtc
import (
"fmt"
"math/rand"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/webrtc/v2"
)
type Track interface {
mediadevices.Track
}
type LocalTrack interface {
codec.RTPReadCloser
}
type EncoderBuilder interface {
Codec() *webrtc.RTPCodec
BuildEncoder(Track) (LocalTrack, error)
}
type MediaEngine struct {
webrtc.MediaEngine
encoderBuilders []EncoderBuilder
}
func (engine *MediaEngine) AddEncoderBuilders(builders ...EncoderBuilder) {
engine.encoderBuilders = append(engine.encoderBuilders, builders...)
for _, builder := range builders {
engine.RegisterCodec(builder.Codec())
}
}
type API struct {
webrtc.API
mediaEngine MediaEngine
}
func NewAPI(options ...func(*API)) *API {
var api API
for _, option := range options {
option(&api)
}
return &api
}
func WithMediaEngine(m MediaEngine) func(*API) {
return func(a *API) {
a.mediaEngine = m
}
}
func (api *API) NewPeerConnection(configuration webrtc.Configuration) (*PeerConnection, error) {
pc, err := api.API.NewPeerConnection(configuration)
return &PeerConnection{
PeerConnection: pc,
api: api,
}, err
}
type PeerConnection struct {
webrtc.PeerConnection
api *API
}
func buildEncoder(encoderBuilders []EncoderBuilder, track Track) LocalTrack {
for _, encoderBuilder := range encoderBuilders {
encoder, err := encoderBuilder.BuildEncoder(track)
if err == nil {
return encoder
}
}
return nil
}
func (pc *PeerConnection) ExtAddTransceiverFromTrack(track Track, init ...webrtc.RtpTransceiverInit) (*webrtc.RTPTransceiver, error) {
encoder := buildEncoder(pc.api.mediaEngine.encoderBuilders, track)
if builder == nil {
return nil, fmt.Errorf("failed to find a compatible encoder")
}
trackImpl, err := pc.NewTrack(rtpCodec.PayloadType, rand.Uint32(), track.ID(), rtpCodec.Type.String())
if err != nil {
return nil, err
}
localTrack, err := builder.BuildEncoder(track)
if err != nil {
return nil, err
}
trans, err := pc.AddTransceiverFromTrack(trackImpl, init...)
if err != nil {
return nil, err
}
go func() {
for {
rtpPackets, err := localTrack.ReadRTP()
if err != nil {
return
}
for _, rtpPacket := range rtpPackets {
err = trackImpl.WriteRTP(rtpPacket)
if err != nil {
return
}
}
}
}()
return trans, nil
}

View File

@@ -4,7 +4,7 @@ package io
// InsufficientBufferError.
func Copy(dst, src []byte) (n int, err error) {
if len(dst) < len(src) {
return 0, &InsufficientBufferError{len(dst)}
return 0, &InsufficientBufferError{len(src)}
}
return copy(dst, src), nil

45
pkg/io/io_test.go Normal file
View File

@@ -0,0 +1,45 @@
package io
import (
"log"
"testing"
)
func TestCopy(t *testing.T) {
var dst []byte
src := make([]byte, 4)
n, err := Copy(dst, src)
if err == nil {
t.Fatal("expected err to be non-nill")
}
if n != 0 {
t.Fatalf("expected n to be 0, but got %d", n)
}
e, ok := err.(*InsufficientBufferError)
if !ok {
t.Fatalf("expected error to be InsufficientBufferError")
}
if e.RequiredSize != len(src) {
t.Fatalf("expected required size to be %d, but got %d", len(src), e.RequiredSize)
}
dst = make([]byte, 2*e.RequiredSize)
n, err = Copy(dst, src)
if err != nil {
t.Fatalf("expected to not get an error after expanding the buffer")
}
if n != len(src) {
t.Fatalf("expected n to be %d, but got %d", len(src), n)
}
for i := 0; i < len(src); i++ {
if src[i] != dst[i] {
log.Fatalf("expected value at %d to be %d, but got %d", i, src[i], dst[i])
}
}
}

98
pkg/prop/duration.go Normal file
View File

@@ -0,0 +1,98 @@
package prop
import (
"math"
"time"
)
// DurationConstraint is an interface to represent time.Duration constraint.
type DurationConstraint interface {
Compare(time.Duration) (float64, bool)
Value() (time.Duration, bool)
}
// Duration specifies ideal duration value.
// Any value may be selected, but closest value takes priority.
type Duration time.Duration
// Compare implements DurationConstraint.
func (d Duration) Compare(a time.Duration) (float64, bool) {
return math.Abs(float64(a-time.Duration(d))) / math.Max(math.Abs(float64(a)), math.Abs(float64(d))), true
}
// Value implements DurationConstraint.
func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true }
// DurationExact specifies exact duration value.
type DurationExact time.Duration
// Compare implements DurationConstraint.
func (d DurationExact) Compare(a time.Duration) (float64, bool) {
if time.Duration(d) == a {
return 0.0, true
}
return 1.0, false
}
// Value implements DurationConstraint.
func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true }
// DurationOneOf specifies list of expected duration values.
type DurationOneOf []time.Duration
// Compare implements DurationConstraint.
func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
for _, ii := range d {
if ii == a {
return 0.0, true
}
}
return 1.0, false
}
// Value implements DurationConstraint.
func (DurationOneOf) Value() (time.Duration, bool) { return 0, false }
// DurationRanged specifies range of expected duration value.
// If Ideal is non-zero, closest value to Ideal takes priority.
type DurationRanged struct {
Min time.Duration
Max time.Duration
Ideal time.Duration
}
// Compare implements DurationConstraint.
func (d DurationRanged) Compare(a time.Duration) (float64, bool) {
if d.Min != 0 && d.Min > a {
// Out of range
return 1.0, false
}
if d.Max != 0 && d.Max < a {
// Out of range
return 1.0, false
}
if d.Ideal == 0 {
// If the value is in the range and Ideal is not specified,
// any value is evenly acceptable.
return 0.0, true
}
switch {
case a == d.Ideal:
return 0.0, true
case a < d.Ideal:
if d.Min == 0 {
// If Min is not specified, smaller values than Ideal are even.
return 0.0, true
}
return float64(d.Ideal-a) / float64(d.Ideal-d.Min), true
default:
if d.Max == 0 {
// If Max is not specified, larger values than Ideal are even.
return 0.0, true
}
return float64(a-d.Ideal) / float64(d.Max-d.Ideal), true
}
}
// Value implements DurationConstraint.
func (DurationRanged) Value() (time.Duration, bool) { return 0, false }

97
pkg/prop/float.go Normal file
View File

@@ -0,0 +1,97 @@
package prop
import (
"math"
)
// FloatConstraint is an interface to represent float value constraint.
type FloatConstraint interface {
Compare(float32) (float64, bool)
Value() (float32, bool)
}
// Float specifies ideal float value.
// Any value may be selected, but closest value takes priority.
type Float float32
// Compare implements FloatConstraint.
func (f Float) Compare(a float32) (float64, bool) {
return math.Abs(float64(a-float32(f))) / math.Max(math.Abs(float64(a)), math.Abs(float64(f))), true
}
// Value implements FloatConstraint.
func (f Float) Value() (float32, bool) { return float32(f), true }
// FloatExact specifies exact float value.
type FloatExact float32
// Compare implements FloatConstraint.
func (f FloatExact) Compare(a float32) (float64, bool) {
if float32(f) == a {
return 0.0, true
}
return 1.0, false
}
// Value implements FloatConstraint.
func (f FloatExact) Value() (float32, bool) { return float32(f), true }
// FloatOneOf specifies list of expected float values.
type FloatOneOf []float32
// Compare implements FloatConstraint.
func (f FloatOneOf) Compare(a float32) (float64, bool) {
for _, ff := range f {
if ff == a {
return 0.0, true
}
}
return 1.0, false
}
// Value implements FloatConstraint.
func (FloatOneOf) Value() (float32, bool) { return 0, false }
// FloatRanged specifies range of expected float value.
// If Ideal is non-zero, closest value to Ideal takes priority.
type FloatRanged struct {
Min float32
Max float32
Ideal float32
}
// Compare implements FloatConstraint.
func (f FloatRanged) Compare(a float32) (float64, bool) {
if f.Min != 0 && f.Min > a {
// Out of range
return 1.0, false
}
if f.Max != 0 && f.Max < a {
// Out of range
return 1.0, false
}
if f.Ideal == 0 {
// If the value is in the range and Ideal is not specified,
// any value is evenly acceptable.
return 0.0, true
}
switch {
case a == f.Ideal:
return 0.0, true
case a < f.Ideal:
if f.Min == 0 {
// If Min is not specified, smaller values than Ideal are even.
return 0.0, true
}
return float64(f.Ideal-a) / float64(f.Ideal-f.Min), true
default:
if f.Max == 0 {
// If Max is not specified, larger values than Ideal are even.
return 0.0, true
}
return float64(a-f.Ideal) / float64(f.Max-f.Ideal), true
}
}
// Value implements FloatConstraint.
func (FloatRanged) Value() (float32, bool) { return 0, false }

56
pkg/prop/format.go Normal file
View File

@@ -0,0 +1,56 @@
package prop
import (
"github.com/pion/mediadevices/pkg/frame"
)
// FrameFormatConstraint is an interface to represent frame format constraint.
type FrameFormatConstraint interface {
Compare(frame.Format) (float64, bool)
Value() (frame.Format, bool)
}
// FrameFormat specifies expected frame format.
// Any value may be selected, but matched value takes priority.
type FrameFormat frame.Format
// Compare implements FrameFormatConstraint.
func (f FrameFormat) Compare(a frame.Format) (float64, bool) {
if frame.Format(f) == a {
return 0.0, true
}
return 1.0, true
}
// Value implements FrameFormatConstraint.
func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true }
// FrameFormatExact specifies exact frame format.
type FrameFormatExact frame.Format
// Compare implements FrameFormatConstraint.
func (f FrameFormatExact) Compare(a frame.Format) (float64, bool) {
if frame.Format(f) == a {
return 0.0, true
}
return 1.0, false
}
// Value implements FrameFormatConstraint.
func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true }
// FrameFormatOneOf specifies list of expected frame format.
type FrameFormatOneOf []frame.Format
// Compare implements FrameFormatConstraint.
func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) {
for _, ff := range f {
if ff == a {
return 0.0, true
}
}
return 1.0, false
}
// Value implements FrameFormatConstraint.
func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false }

97
pkg/prop/int.go Normal file
View File

@@ -0,0 +1,97 @@
package prop
import (
"math"
)
// IntConstraint is an interface to represent integer value constraint.
type IntConstraint interface {
Compare(int) (float64, bool)
Value() (int, bool)
}
// Int specifies ideal int value.
// Any value may be selected, but closest value takes priority.
type Int int
// Compare implements IntConstraint.
func (i Int) Compare(a int) (float64, bool) {
return math.Abs(float64(a-int(i))) / math.Max(math.Abs(float64(a)), math.Abs(float64(i))), true
}
// Value implements IntConstraint.
func (i Int) Value() (int, bool) { return int(i), true }
// IntExact specifies exact int value.
type IntExact int
// Compare implements IntConstraint.
func (i IntExact) Compare(a int) (float64, bool) {
if int(i) == a {
return 0.0, true
}
return 1.0, false
}
// Value implements IntConstraint.
func (i IntExact) Value() (int, bool) { return int(i), true }
// IntOneOf specifies list of expected float values.
type IntOneOf []int
// Compare implements IntConstraint.
func (i IntOneOf) Compare(a int) (float64, bool) {
for _, ii := range i {
if ii == a {
return 0.0, true
}
}
return 1.0, false
}
// Value implements IntConstraint.
func (IntOneOf) Value() (int, bool) { return 0, false }
// IntRanged specifies range of expected int value.
// If Ideal is non-zero, closest value to Ideal takes priority.
type IntRanged struct {
Min int
Max int
Ideal int
}
// Compare implements IntConstraint.
func (i IntRanged) Compare(a int) (float64, bool) {
if i.Min != 0 && i.Min > a {
// Out of range
return 1.0, false
}
if i.Max != 0 && i.Max < a {
// Out of range
return 1.0, false
}
if i.Ideal == 0 {
// If the value is in the range and Ideal is not specified,
// any value is evenly acceptable.
return 0.0, true
}
switch {
case a == i.Ideal:
return 0.0, true
case a < i.Ideal:
if i.Min == 0 {
// If Min is not specified, smaller values than Ideal are even.
return 0.0, true
}
return float64(i.Ideal-a) / float64(i.Ideal-i.Min), true
default:
if i.Max == 0 {
// If Max is not specified, larger values than Ideal are even.
return 0.0, true
}
return float64(a-i.Ideal) / float64(i.Max-i.Ideal), true
}
}
// Value implements IntConstraint.
func (IntRanged) Value() (int, bool) { return 0, false }

View File

@@ -1,15 +1,21 @@
package prop
import (
"fmt"
"math"
"reflect"
"strconv"
"time"
"github.com/pion/mediadevices/pkg/frame"
)
// MediaConstraints represents set of media propaty constraints.
// Each field constrains property by min/ideal/max range, exact match, or oneof match.
type MediaConstraints struct {
DeviceID StringConstraint
VideoConstraints
AudioConstraints
}
// Media stores single set of media propaties.
type Media struct {
DeviceID string
Video
@@ -17,7 +23,7 @@ type Media struct {
}
// Merge merges all the field values from o to p, except zero values.
func (p *Media) Merge(o Media) {
func (p *Media) Merge(o MediaConstraints) {
rp := reflect.ValueOf(p).Elem()
ro := reflect.ValueOf(o)
@@ -29,9 +35,9 @@ func (p *Media) Merge(o Media) {
fieldA := a.Field(i)
fieldB := b.Field(i)
// if a is a struct, b is also a struct. Then,
// if b is a struct, a is also a struct. Then,
// we recursively merge them
if fieldA.Kind() == reflect.Struct {
if fieldB.Kind() == reflect.Struct {
merge(fieldA, fieldB)
continue
}
@@ -43,67 +49,135 @@ func (p *Media) Merge(o Media) {
continue
}
fieldA.Set(fieldB)
switch c := fieldB.Interface().(type) {
case IntConstraint:
if v, ok := c.Value(); ok {
fieldA.Set(reflect.ValueOf(v))
}
case FloatConstraint:
if v, ok := c.Value(); ok {
fieldA.Set(reflect.ValueOf(v))
}
case DurationConstraint:
if v, ok := c.Value(); ok {
fieldA.Set(reflect.ValueOf(v))
}
case FrameFormatConstraint:
if v, ok := c.Value(); ok {
fieldA.Set(reflect.ValueOf(v))
}
case StringConstraint:
if v, ok := c.Value(); ok {
fieldA.Set(reflect.ValueOf(v))
}
default:
panic("unsupported property type")
}
}
}
merge(rp, ro)
}
func (p *Media) FitnessDistance(o Media) float64 {
// FitnessDistance calculates fitness of media property and media constraints.
// If no media satisfies given constraints, second return value will be false.
func (p *MediaConstraints) FitnessDistance(o Media) (float64, bool) {
cmps := comparisons{}
cmps.add(p.DeviceID, o.DeviceID)
cmps.add(p.Width, o.Width)
cmps.add(p.Height, o.Height)
cmps.add(p.FrameFormat, o.FrameFormat)
cmps.add(p.SampleRate, o.SampleRate)
cmps.add(p.Latency, o.Latency)
return cmps.fitnessDistance()
}
type comparisons map[string]string
type comparisons []struct {
desired, actual interface{}
}
func (c comparisons) add(actual, ideal interface{}) {
c[fmt.Sprint(actual)] = fmt.Sprint(ideal)
func (c *comparisons) add(desired, actual interface{}) {
if desired != nil {
*c = append(*c,
struct{ desired, actual interface{} }{
desired, actual,
},
)
}
}
// fitnessDistance is an implementation for https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
func (c comparisons) fitnessDistance() float64 {
func (c *comparisons) fitnessDistance() (float64, bool) {
var dist float64
for actual, ideal := range c {
if actual == ideal {
continue
}
actualF, err1 := strconv.ParseFloat(actual, 64)
idealF, err2 := strconv.ParseFloat(ideal, 64)
switch {
// If both of the values are numeric, we need to normalize the values to get the distance
case err1 == nil && err2 == nil:
dist += math.Abs(actualF-idealF) / math.Max(math.Abs(actualF), math.Abs(idealF))
// If both of the values are not numeric, the only comparison value is either 0 (matched) or 1 (not matched)
case err1 != nil && err2 != nil:
if actual != ideal {
dist++
for _, field := range *c {
var d float64
var ok bool
switch c := field.desired.(type) {
case IntConstraint:
if actual, typeOK := field.actual.(int); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
case FloatConstraint:
if actual, typeOK := field.actual.(float32); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
case DurationConstraint:
if actual, typeOK := field.actual.(time.Duration); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
case FrameFormatConstraint:
if actual, typeOK := field.actual.(frame.Format); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
case StringConstraint:
if actual, typeOK := field.actual.(string); typeOK {
d, ok = c.Compare(actual)
} else {
panic("wrong type of actual value")
}
// Comparing a numeric value with a non-numeric value is a an internal error, so panic.
default:
panic("fitnessDistance can't mix comparisons.")
panic("unsupported constraint type")
}
dist += d
if !ok {
return 0, false
}
}
return dist
return dist, true
}
// Video represents a video's properties
// VideoConstraints represents a video's constraints
type VideoConstraints struct {
Width, Height IntConstraint
FrameRate FloatConstraint
FrameFormat FrameFormatConstraint
}
// Video represents a video's constraints
type Video struct {
Width, Height int
FrameRate float32
FrameFormat frame.Format
}
// Audio represents an audio's properties
// AudioConstraints represents an audio's constraints
type AudioConstraints struct {
ChannelCount IntConstraint
Latency DurationConstraint
SampleRate IntConstraint
SampleSize IntConstraint
}
// Audio represents an audio's constraints
type Audio struct {
ChannelCount int
Latency time.Duration

View File

@@ -2,8 +2,156 @@ package prop
import (
"testing"
"time"
"github.com/pion/mediadevices/pkg/frame"
)
func TestCompareMatch(t *testing.T) {
testDataSet := map[string]struct {
a MediaConstraints
b Media
match bool
}{
"DeviceIDExactUnmatch": {
MediaConstraints{
DeviceID: StringExact("abc"),
},
Media{
DeviceID: "cde",
},
false,
},
"DeviceIDExactMatch": {
MediaConstraints{
DeviceID: StringExact("abc"),
},
Media{
DeviceID: "abc",
},
true,
},
"IntIdealUnmatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
Width: Int(30),
}},
Media{Video: Video{
Width: 50,
}},
true,
},
"IntIdealMatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
Width: Int(30),
}},
Media{Video: Video{
Width: 30,
}},
true,
},
"IntExactUnmatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
Width: IntExact(30),
}},
Media{Video: Video{
Width: 50,
}},
false,
},
"IntExactMatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
Width: IntExact(30),
}},
Media{Video: Video{
Width: 30,
}},
true,
},
"IntRangeUnmatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
Width: IntRanged{Min: 30, Max: 40},
}},
Media{Video: Video{
Width: 50,
}},
false,
},
"IntRangeMatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
Width: IntRanged{Min: 30, Max: 40},
}},
Media{Video: Video{
Width: 35,
}},
true,
},
"FrameFormatOneOfUnmatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
FrameFormat: FrameFormatOneOf{frame.FormatYUYV, frame.FormatUYVY},
}},
Media{Video: Video{
FrameFormat: frame.FormatYUYV,
}},
true,
},
"FrameFormatOneOfMatch": {
MediaConstraints{VideoConstraints: VideoConstraints{
FrameFormat: FrameFormatOneOf{frame.FormatYUYV, frame.FormatUYVY},
}},
Media{Video: Video{
FrameFormat: frame.FormatMJPEG,
}},
false,
},
"DurationExactUnmatch": {
MediaConstraints{AudioConstraints: AudioConstraints{
Latency: DurationExact(time.Second),
}},
Media{Audio: Audio{
Latency: time.Second + time.Millisecond,
}},
false,
},
"DurationExactMatch": {
MediaConstraints{AudioConstraints: AudioConstraints{
Latency: DurationExact(time.Second),
}},
Media{Audio: Audio{
Latency: time.Second,
}},
true,
},
"DurationRangedUnmatch": {
MediaConstraints{AudioConstraints: AudioConstraints{
Latency: DurationRanged{Max: time.Second},
}},
Media{Audio: Audio{
Latency: time.Second + time.Millisecond,
}},
false,
},
"DurationRangedMatch": {
MediaConstraints{AudioConstraints: AudioConstraints{
Latency: DurationRanged{Max: time.Second},
}},
Media{Audio: Audio{
Latency: time.Millisecond,
}},
true,
},
}
for name, testData := range testDataSet {
testData := testData
t.Run(name, func(t *testing.T) {
_, match := testData.a.FitnessDistance(testData.b)
if match != testData.match {
t.Errorf("matching flag differs, expected: %v, got: %v", testData.match, match)
}
})
}
}
func TestMergeWithZero(t *testing.T) {
a := Media{
Video: Video{
@@ -11,9 +159,9 @@ func TestMergeWithZero(t *testing.T) {
},
}
b := Media{
Video: Video{
Height: 100,
b := MediaConstraints{
VideoConstraints: VideoConstraints{
Height: Int(100),
},
}
@@ -35,9 +183,9 @@ func TestMergeWithSameField(t *testing.T) {
},
}
b := Media{
Video: Video{
Width: 100,
b := MediaConstraints{
VideoConstraints: VideoConstraints{
Width: Int(100),
},
}
@@ -61,9 +209,9 @@ func TestMergeNested(t *testing.T) {
},
}
b := Media{
Video: Video{
Width: 100,
b := MediaConstraints{
VideoConstraints: VideoConstraints{
Width: Int(100),
},
}

52
pkg/prop/string.go Normal file
View File

@@ -0,0 +1,52 @@
package prop
// StringConstraint is an interface to represent string constraint.
type StringConstraint interface {
Compare(string) (float64, bool)
Value() (string, bool)
}
// String specifies expected string.
// Any value may be selected, but matched value takes priority.
type String string
// Compare implements StringConstraint.
func (f String) Compare(a string) (float64, bool) {
if string(f) == a {
return 0.0, true
}
return 1.0, true
}
// Value implements StringConstraint.
func (f String) Value() (string, bool) { return string(f), true }
// StringExact specifies exact string.
type StringExact string
// Compare implements StringConstraint.
func (f StringExact) Compare(a string) (float64, bool) {
if string(f) == a {
return 0.0, true
}
return 1.0, false
}
// Value implements StringConstraint.
func (f StringExact) Value() (string, bool) { return string(f), true }
// StringOneOf specifies list of expected string.
type StringOneOf []string
// Compare implements StringConstraint.
func (f StringOneOf) Compare(a string) (float64, bool) {
for _, ff := range f {
if ff == a {
return 0.0, true
}
}
return 1.0, false
}
// Value implements StringConstraint.
func (StringOneOf) Value() (string, bool) { return "", false }

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})
})
}

347
track.go
View File

@@ -1,30 +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/prop"
"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"
)
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
GetConstraints() prop.Media
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
@@ -32,147 +24,18 @@ type Track interface {
OnEnded(func(error))
}
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 prop.Media) (*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)
if err != nil {
d.Close()
return nil, err
}
return &VideoTrack{
baseTrack: baseTrack{d: d, constraints: constraints},
src: r,
transformed: r,
}, nil
}
func (track *VideoTrack) Kind() TrackKind {
return TrackKindVideo
}
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++
}
curFrameCount = track.frameCount
return
})
}
// TODO: implement copy in place
func copyFrame(dst, src image.Image) image.Image { return src }
func (track *VideoTrack) Transform(fns ...video.TransformFunc) {
track.mux.Lock()
defer track.mux.Unlock()
track.transformed = video.Merge(fns...)(track.src)
}
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 prop.Media) (*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)
if err != nil {
d.Close()
return nil, err
}
return &AudioTrack{
baseTrack: baseTrack{d: d, constraints: constraints},
src: r,
transformed: r,
}, nil
}
func (track *AudioTrack) Kind() TrackKind {
return TrackKindAudio
}
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++
}
currChunkCount = track.chunkCount
return
})
}
// TODO: implement copy in place
func copyChunks(dst, src wave.Audio) wave.Audio { return src }
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 prop.Media
type track struct {
localTrack LocalTrack
d driver.Driver
sample samplerFunc
encoder codec.ReadCloser
onErrorHandler func(error)
err error
@@ -180,17 +43,83 @@ type baseTrack struct {
endOnce sync.Once
}
func (t *baseTrack) ID() string {
return t.d.ID()
}
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) GetConstraints() prop.Media {
return t.constraints
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
@@ -205,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
@@ -218,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
}