mirror of
https://github.com/pion/mediadevices.git
synced 2025-12-24 13:18:11 +08:00
Compare commits
9 Commits
refractor
...
vpx-suppor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb7611783d | ||
|
|
00eca231a7 | ||
|
|
27d966611e | ||
|
|
ecff5e63a5 | ||
|
|
305b7086e3 | ||
|
|
6471064956 | ||
|
|
c6e685964f | ||
|
|
65b744f639 | ||
|
|
a2b74babc4 |
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
4
go.mod
@@ -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
30
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
180
mediadevices.go
180
mediadevices.go
@@ -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))
|
||||
|
||||
@@ -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{¶ms}
|
||||
},
|
||||
Audio: func(c *MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
params := audioParams
|
||||
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms}
|
||||
},
|
||||
}
|
||||
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{¶ms}
|
||||
},
|
||||
Audio: func(c *MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
params := audioParams
|
||||
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms}
|
||||
},
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
45
pkg/io/io_test.go
Normal 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
98
pkg/prop/duration.go
Normal 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
97
pkg/prop/float.go
Normal 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
56
pkg/prop/format.go
Normal 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
97
pkg/prop/int.go
Normal 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 }
|
||||
144
pkg/prop/prop.go
144
pkg/prop/prop.go
@@ -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
|
||||
|
||||
@@ -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
52
pkg/prop/string.go
Normal 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
35
sampler.go
Normal 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
347
track.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user