mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-28 21:32:13 +08:00
Compare commits
5 Commits
renovate/g
...
refractor
Author | SHA1 | Date | |
---|---|---|---|
![]() |
031b9a95c6 | ||
![]() |
668ef32cd5 | ||
![]() |
7e739a814b | ||
![]() |
22282bc1d7 | ||
![]() |
d7ee554323 |
@@ -6,11 +6,10 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/mediadevices"
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/examples/internal/signal"
|
"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/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/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/io/video"
|
||||||
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
"github.com/pion/webrtc/v2"
|
"github.com/pion/webrtc/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,22 +55,22 @@ func main() {
|
|||||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
md := mediadevices.NewMediaDevices(peerConnection)
|
|
||||||
|
|
||||||
vp8Params, err := vpx.NewVP8Params()
|
vp8Params, err := vpx.NewVP8Params()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
vp8Params.BitRate = 100000 // 100kbps
|
vp8Params.BitRate = 100000 // 100kbps
|
||||||
|
|
||||||
|
md := mediadevices.NewMediaDevices(
|
||||||
|
peerConnection,
|
||||||
|
mediadevices.WithVideoEncoders(&vp8Params),
|
||||||
|
mediadevices.WithVideoTransformers(markFacesTransformer),
|
||||||
|
)
|
||||||
|
|
||||||
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
|
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
Video: func(p *prop.Media) {
|
||||||
c.FrameFormat = frame.FormatI420 // most of the encoder accepts I420
|
p.Width = 640
|
||||||
c.Enabled = true
|
p.Height = 480
|
||||||
c.Width = 640
|
|
||||||
c.Height = 480
|
|
||||||
c.VideoTransform = markFacesTransformer
|
|
||||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -6,10 +6,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pion/mediadevices"
|
"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/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/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/rtp"
|
||||||
"github.com/pion/webrtc/v2"
|
"github.com/pion/webrtc/v2"
|
||||||
"github.com/pion/webrtc/v2/pkg/media"
|
"github.com/pion/webrtc/v2/pkg/media"
|
||||||
@@ -25,6 +24,12 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vp8Params, err := vpx.NewVP8Params()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
vp8Params.BitRate = 100000 // 100kbps
|
||||||
|
|
||||||
md := mediadevices.NewMediaDevicesFromCodecs(
|
md := mediadevices.NewMediaDevicesFromCodecs(
|
||||||
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
|
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
|
||||||
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
|
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
|
||||||
@@ -38,21 +43,13 @@ func main() {
|
|||||||
return newTrack(codec, id, os.Args[1]), nil
|
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{
|
_, err = md.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
Video: func(p *prop.Media) {
|
||||||
c.FrameFormat = frame.FormatYUY2
|
p.Width = 640
|
||||||
c.Enabled = true
|
p.Height = 480
|
||||||
c.Width = 640
|
|
||||||
c.Height = 480
|
|
||||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -5,10 +5,13 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/mediadevices"
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/examples/internal/signal"
|
"github.com/pion/mediadevices/examples/internal/signal"
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec/openh264"
|
||||||
"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
|
// This is required to use VP8/VP9 video encoder
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
// _ "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/webrtc/v2"
|
"github.com/pion/webrtc/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,12 +28,16 @@ func main() {
|
|||||||
offer := webrtc.SessionDescription{}
|
offer := webrtc.SessionDescription{}
|
||||||
signal.Decode(signal.MustReadStdin(), &offer)
|
signal.Decode(signal.MustReadStdin(), &offer)
|
||||||
|
|
||||||
// Create a new RTCPeerConnection
|
openh264Encoder, err := openh264.NewParams()
|
||||||
mediaEngine := webrtc.MediaEngine{}
|
if err != nil {
|
||||||
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
|
openh264Encoder.BitRate = 100000 // 100kbps
|
||||||
|
|
||||||
|
// Create a new RTCPeerConnection
|
||||||
|
mediaEngine := extwebrtc.MediaEngine{}
|
||||||
|
mediaEngine.AddEncoderBuilders(&openh264Encoder)
|
||||||
|
api := extwebrtc.NewAPI(extwebrtc.WithMediaEngine(mediaEngine))
|
||||||
peerConnection, err := api.NewPeerConnection(config)
|
peerConnection, err := api.NewPeerConnection(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -42,32 +49,15 @@ func main() {
|
|||||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
md := mediadevices.NewMediaDevices(peerConnection)
|
s, err := mediadevices.GetDisplayMedia(mediadevices.MediaStreamConstraints{
|
||||||
|
Video: func(p *prop.Media) {},
|
||||||
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tracker := range s.GetTracks() {
|
for _, track := range s.GetTracks() {
|
||||||
t := tracker.Track()
|
_, err = peerConnection.ExtAddTransceiverFromTrack(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{
|
webrtc.RtpTransceiverInit{
|
||||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||||
},
|
},
|
||||||
|
@@ -2,130 +2,64 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
|
||||||
"github.com/pion/mediadevices"
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/examples/internal/signal"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
|
||||||
"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,
|
// 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.
|
// you can always swap your adapters with our dummy adapters below.
|
||||||
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||||
// _ "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/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() {
|
func main() {
|
||||||
config := webrtc.Configuration{
|
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||||
ICEServers: []webrtc.ICEServer{
|
Video: func(p *prop.Media) {},
|
||||||
{
|
|
||||||
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 = frame.FormatYUY2
|
|
||||||
c.Enabled = true
|
|
||||||
c.Width = 640
|
|
||||||
c.Height = 480
|
|
||||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tracker := range s.GetTracks() {
|
t := s.GetVideoTracks()[0]
|
||||||
t := tracker.Track()
|
defer t.Stop()
|
||||||
tracker.OnEnded(func(err error) {
|
videoTrack := t.(*mediadevices.VideoTrack)
|
||||||
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
|
|
||||||
t.ID(), t.Label(), err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
_, err = peerConnection.AddTransceiverFromTrack(t,
|
|
||||||
webrtc.RtpTransceiverInit{
|
|
||||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the remote SessionDescription
|
log.Println(http.ListenAndServe(":1313", nil))
|
||||||
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 {}
|
|
||||||
}
|
}
|
||||||
|
158
mediadevices.go
158
mediadevices.go
@@ -6,106 +6,37 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"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")
|
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
|
// 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.
|
// 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
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
|
||||||
func (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||||
trackers := make([]Tracker, 0)
|
tracks := make([]Track, 0)
|
||||||
|
|
||||||
cleanTrackers := func() {
|
cleanTracks := func() {
|
||||||
for _, t := range trackers {
|
for _, t := range tracks {
|
||||||
t.Stop()
|
t.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoConstraints MediaTrackConstraints
|
|
||||||
if constraints.Video != nil {
|
if constraints.Video != nil {
|
||||||
constraints.Video(&videoConstraints)
|
var p prop.Media
|
||||||
}
|
constraints.Video(&p)
|
||||||
|
track, err := selectScreen(p)
|
||||||
if videoConstraints.Enabled {
|
|
||||||
tracker, err := m.selectScreen(videoConstraints)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanTrackers()
|
cleanTracks()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
trackers = append(trackers, tracker)
|
tracks = append(tracks, track)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := NewMediaStream(trackers...)
|
s, err := NewMediaStream(tracks...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanTrackers()
|
cleanTracks()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,48 +46,42 @@ func (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (Medi
|
|||||||
// GetUserMedia prompts the user for permission to use a media input which produces a MediaStream
|
// GetUserMedia prompts the user for permission to use a media input which produces a MediaStream
|
||||||
// with tracks containing the requested types of media.
|
// with tracks containing the requested types of media.
|
||||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||||
// TODO: It should return media stream based on constraints
|
tracks := make([]Track, 0)
|
||||||
trackers := make([]Tracker, 0)
|
|
||||||
|
|
||||||
cleanTrackers := func() {
|
cleanTracks := func() {
|
||||||
for _, t := range trackers {
|
for _, t := range tracks {
|
||||||
t.Stop()
|
t.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoConstraints, audioConstraints MediaTrackConstraints
|
|
||||||
if constraints.Video != nil {
|
if constraints.Video != nil {
|
||||||
constraints.Video(&videoConstraints)
|
var p prop.Media
|
||||||
|
constraints.Video(&p)
|
||||||
|
track, err := selectVideo(p)
|
||||||
|
if err != nil {
|
||||||
|
cleanTracks()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks = append(tracks, track)
|
||||||
}
|
}
|
||||||
|
|
||||||
if constraints.Audio != nil {
|
if constraints.Audio != nil {
|
||||||
constraints.Audio(&audioConstraints)
|
var p prop.Media
|
||||||
}
|
constraints.Audio(&p)
|
||||||
|
track, err := selectAudio(p)
|
||||||
if videoConstraints.Enabled {
|
|
||||||
tracker, err := m.selectVideo(videoConstraints)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanTrackers()
|
cleanTracks()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
trackers = append(trackers, tracker)
|
tracks = append(tracks, track)
|
||||||
}
|
}
|
||||||
|
|
||||||
if audioConstraints.Enabled {
|
s, err := NewMediaStream(tracks...)
|
||||||
tracker, err := m.selectAudio(audioConstraints)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanTrackers()
|
cleanTracks()
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
trackers = append(trackers, tracker)
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := NewMediaStream(trackers...)
|
|
||||||
if err != nil {
|
|
||||||
cleanTrackers()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +116,7 @@ func queryDriverProperties(filter driver.FilterFn) map[driver.Driver][]prop.Medi
|
|||||||
|
|
||||||
// select implements SelectSettings algorithm.
|
// select implements SelectSettings algorithm.
|
||||||
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings
|
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings
|
||||||
func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) {
|
func selectBestDriver(filter driver.FilterFn, constraints prop.Media) (driver.Driver, prop.Media, error) {
|
||||||
var bestDriver driver.Driver
|
var bestDriver driver.Driver
|
||||||
var bestProp prop.Media
|
var bestProp prop.Media
|
||||||
minFitnessDist := math.Inf(1)
|
minFitnessDist := math.Inf(1)
|
||||||
@@ -200,7 +125,7 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
|
|||||||
for d, props := range driverProperties {
|
for d, props := range driverProperties {
|
||||||
priority := float64(d.Info().Priority)
|
priority := float64(d.Info().Priority)
|
||||||
for _, p := range props {
|
for _, p := range props {
|
||||||
fitnessDist := constraints.Media.FitnessDistance(p) - priority
|
fitnessDist := constraints.FitnessDistance(p) - priority
|
||||||
if fitnessDist < minFitnessDist {
|
if fitnessDist < minFitnessDist {
|
||||||
minFitnessDist = fitnessDist
|
minFitnessDist = fitnessDist
|
||||||
bestDriver = d
|
bestDriver = d
|
||||||
@@ -210,14 +135,14 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bestDriver == nil {
|
if bestDriver == nil {
|
||||||
return nil, MediaTrackConstraints{}, errNotFound
|
return nil, prop.Media{}, errNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
constraints.Merge(bestProp)
|
constraints.Merge(bestProp)
|
||||||
return bestDriver, constraints, nil
|
return bestDriver, constraints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
|
func selectAudio(constraints prop.Media) (Track, error) {
|
||||||
typeFilter := driver.FilterAudioRecorder()
|
typeFilter := driver.FilterAudioRecorder()
|
||||||
filter := typeFilter
|
filter := typeFilter
|
||||||
if constraints.DeviceID != "" {
|
if constraints.DeviceID != "" {
|
||||||
@@ -230,9 +155,10 @@ func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTrack(&m.MediaDevicesOptions, d, c)
|
return newAudioTrack(d, c)
|
||||||
}
|
}
|
||||||
func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker, error) {
|
|
||||||
|
func selectVideo(constraints prop.Media) (Track, error) {
|
||||||
typeFilter := driver.FilterVideoRecorder()
|
typeFilter := driver.FilterVideoRecorder()
|
||||||
notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen))
|
notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen))
|
||||||
filter := driver.FilterAnd(typeFilter, notScreenFilter)
|
filter := driver.FilterAnd(typeFilter, notScreenFilter)
|
||||||
@@ -246,10 +172,10 @@ func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTrack(&m.MediaDevicesOptions, d, c)
|
return newVideoTrack(d, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker, error) {
|
func selectScreen(constraints prop.Media) (Track, error) {
|
||||||
typeFilter := driver.FilterVideoRecorder()
|
typeFilter := driver.FilterVideoRecorder()
|
||||||
screenFilter := driver.FilterDeviceType(driver.Screen)
|
screenFilter := driver.FilterDeviceType(driver.Screen)
|
||||||
filter := driver.FilterAnd(typeFilter, screenFilter)
|
filter := driver.FilterAnd(typeFilter, screenFilter)
|
||||||
@@ -263,10 +189,10 @@ func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTrack(&m.MediaDevicesOptions, d, c)
|
return newVideoTrack(d, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaDevices) EnumerateDevices() []MediaDeviceInfo {
|
func EnumerateDevices() []MediaDeviceInfo {
|
||||||
drivers := driver.GetManager().Query(
|
drivers := driver.GetManager().Query(
|
||||||
driver.FilterFn(func(driver.Driver) bool { return true }))
|
driver.FilterFn(func(driver.Driver) bool { return true }))
|
||||||
info := make([]MediaDeviceInfo, 0, len(drivers))
|
info := make([]MediaDeviceInfo, 0, len(drivers))
|
||||||
|
@@ -18,18 +18,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetUserMedia(t *testing.T) {
|
func TestGetUserMedia(t *testing.T) {
|
||||||
videoParams := mockParams{
|
brokenVideoParams := mockParams{
|
||||||
BaseParams: codec.BaseParams{
|
|
||||||
BitRate: 100000,
|
|
||||||
},
|
|
||||||
name: "MockVideo",
|
name: "MockVideo",
|
||||||
}
|
}
|
||||||
|
videoParams := brokenVideoParams
|
||||||
|
videoParams.BitRate = 100000
|
||||||
audioParams := mockParams{
|
audioParams := mockParams{
|
||||||
BaseParams: codec.BaseParams{
|
BaseParams: codec.BaseParams{
|
||||||
BitRate: 32000,
|
BitRate: 32000,
|
||||||
},
|
},
|
||||||
name: "MockAudio",
|
name: "MockAudio",
|
||||||
}
|
}
|
||||||
|
constraints := MediaStreamConstraints{
|
||||||
|
Video: func(p *prop.Media) {
|
||||||
|
p.Width = 640
|
||||||
|
p.Height = 480
|
||||||
|
},
|
||||||
|
Audio: func(p *prop.Media) {},
|
||||||
|
}
|
||||||
|
|
||||||
md := NewMediaDevicesFromCodecs(
|
md := NewMediaDevicesFromCodecs(
|
||||||
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
|
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
|
||||||
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
|
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
|
||||||
@@ -46,43 +53,36 @@ func TestGetUserMedia(t *testing.T) {
|
|||||||
return newMockTrack(codec, id), nil
|
return newMockTrack(codec, id), nil
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
WithVideoEncoders(&brokenVideoParams),
|
||||||
|
WithAudioEncoders(&audioParams),
|
||||||
)
|
)
|
||||||
constraints := MediaStreamConstraints{
|
|
||||||
Video: func(c *MediaTrackConstraints) {
|
|
||||||
c.Enabled = true
|
|
||||||
c.Width = 640
|
|
||||||
c.Height = 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 = 640
|
|
||||||
c.Height = 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
|
// GetUserMedia with broken parameters
|
||||||
ms, err := md.GetUserMedia(constraintsWrong)
|
ms, err := md.GetUserMedia(constraints)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Expected error, but got 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
|
// GetUserMedia with correct parameters
|
||||||
ms, err = md.GetUserMedia(constraints)
|
ms, err = md.GetUserMedia(constraints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -9,19 +9,19 @@ import (
|
|||||||
// MediaStream is an interface that represents a collection of existing tracks.
|
// MediaStream is an interface that represents a collection of existing tracks.
|
||||||
type MediaStream interface {
|
type MediaStream interface {
|
||||||
// GetAudioTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getaudiotracks
|
// GetAudioTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getaudiotracks
|
||||||
GetAudioTracks() []Tracker
|
GetAudioTracks() []Track
|
||||||
// GetVideoTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getvideotracks
|
// GetVideoTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getvideotracks
|
||||||
GetVideoTracks() []Tracker
|
GetVideoTracks() []Track
|
||||||
// GetTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks
|
// GetTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks
|
||||||
GetTracks() []Tracker
|
GetTracks() []Track
|
||||||
// AddTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-addtrack
|
// AddTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-addtrack
|
||||||
AddTrack(t Tracker)
|
AddTrack(t Track)
|
||||||
// RemoveTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack
|
// RemoveTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack
|
||||||
RemoveTrack(t Tracker)
|
RemoveTrack(t Track)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaStream struct {
|
type mediaStream struct {
|
||||||
trackers map[string]Tracker
|
tracks map[string]Track
|
||||||
l sync.RWMutex
|
l sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,62 +29,62 @@ const rtpCodecTypeDefault webrtc.RTPCodecType = 0
|
|||||||
|
|
||||||
// NewMediaStream creates a MediaStream interface that's defined in
|
// NewMediaStream creates a MediaStream interface that's defined in
|
||||||
// https://w3c.github.io/mediacapture-main/#dom-mediastream
|
// https://w3c.github.io/mediacapture-main/#dom-mediastream
|
||||||
func NewMediaStream(trackers ...Tracker) (MediaStream, error) {
|
func NewMediaStream(tracks ...Track) (MediaStream, error) {
|
||||||
m := mediaStream{trackers: make(map[string]Tracker)}
|
m := mediaStream{tracks: make(map[string]Track)}
|
||||||
|
|
||||||
for _, tracker := range trackers {
|
for _, track := range tracks {
|
||||||
id := tracker.LocalTrack().ID()
|
id := track.ID()
|
||||||
if _, ok := m.trackers[id]; !ok {
|
if _, ok := m.tracks[id]; !ok {
|
||||||
m.trackers[id] = tracker
|
m.tracks[id] = track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaStream) GetAudioTracks() []Tracker {
|
func (m *mediaStream) GetAudioTracks() []Track {
|
||||||
return m.queryTracks(webrtc.RTPCodecTypeAudio)
|
return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindAudio })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaStream) GetVideoTracks() []Tracker {
|
func (m *mediaStream) GetVideoTracks() []Track {
|
||||||
return m.queryTracks(webrtc.RTPCodecTypeVideo)
|
return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindVideo })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaStream) GetTracks() []Tracker {
|
func (m *mediaStream) GetTracks() []Track {
|
||||||
return m.queryTracks(rtpCodecTypeDefault)
|
return m.queryTracks(func(t Track) bool { return true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryTracks returns all tracks that are the same kind as t.
|
// 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.
|
// If t is 0, which is the default, queryTracks will return all the tracks.
|
||||||
func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Tracker {
|
func (m *mediaStream) queryTracks(filter func(track Track) bool) []Track {
|
||||||
m.l.RLock()
|
m.l.RLock()
|
||||||
defer m.l.RUnlock()
|
defer m.l.RUnlock()
|
||||||
|
|
||||||
result := make([]Tracker, 0)
|
result := make([]Track, 0)
|
||||||
for _, tracker := range m.trackers {
|
for _, track := range m.tracks {
|
||||||
if tracker.LocalTrack().Kind() == t || t == rtpCodecTypeDefault {
|
if filter(track) {
|
||||||
result = append(result, tracker)
|
result = append(result, track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaStream) AddTrack(t Tracker) {
|
func (m *mediaStream) AddTrack(t Track) {
|
||||||
m.l.Lock()
|
m.l.Lock()
|
||||||
defer m.l.Unlock()
|
defer m.l.Unlock()
|
||||||
|
|
||||||
id := t.LocalTrack().ID()
|
id := t.ID()
|
||||||
if _, ok := m.trackers[id]; ok {
|
if _, ok := m.tracks[id]; ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.trackers[id] = t
|
m.tracks[id] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediaStream) RemoveTrack(t Tracker) {
|
func (m *mediaStream) RemoveTrack(t Track) {
|
||||||
m.l.Lock()
|
m.l.Lock()
|
||||||
defer m.l.Unlock()
|
defer m.l.Unlock()
|
||||||
|
|
||||||
delete(m.trackers, t.LocalTrack().ID())
|
delete(m.tracks, t.ID())
|
||||||
}
|
}
|
||||||
|
@@ -1,39 +1,13 @@
|
|||||||
package mediadevices
|
package mediadevices
|
||||||
|
|
||||||
import (
|
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"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaStreamConstraints struct {
|
type MediaStreamConstraints struct {
|
||||||
Audio MediaOption
|
Audio MediaTrackConstraints
|
||||||
Video MediaOption
|
Video MediaTrackConstraints
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
|
// MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
|
||||||
type MediaTrackConstraints struct {
|
type MediaTrackConstraints func(*prop.Media)
|
||||||
prop.Media
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type MediaOption func(*MediaTrackConstraints)
|
|
||||||
|
77
pkg/codec/adapter.go
Normal file
77
pkg/codec/adapter.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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,33 +3,23 @@ package codec
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/rtp"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/webrtc/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AudioEncoderBuilder is the interface that wraps basic operations that are
|
type RTPReader interface {
|
||||||
// necessary to build the audio encoder.
|
ReadRTP() (*rtp.Packet, error)
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VideoEncoderBuilder is the interface that wraps basic operations that are
|
type RTPReadCloser interface {
|
||||||
// necessary to build the video encoder.
|
RTPReader
|
||||||
//
|
Close()
|
||||||
// This interface is for codec implementors to provide codec specific params,
|
}
|
||||||
// but still giving generality for the users.
|
|
||||||
type VideoEncoderBuilder interface {
|
type EncoderBuilder interface {
|
||||||
// Name represents the codec name
|
Codec() *webrtc.RTPCodec
|
||||||
Name() string
|
BuildEncoder(mediadevices.Track) (RTPReadCloser, error)
|
||||||
// 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
|
// ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame
|
||||||
|
@@ -15,10 +15,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
mio "github.com/pion/mediadevices/pkg/io"
|
mio "github.com/pion/mediadevices/pkg/io"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type encoder struct {
|
type encoder struct {
|
||||||
@@ -30,17 +30,19 @@ type encoder struct {
|
|||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
func newEncoder(track *mediadevices.VideoTrack, params Params) (codec.ReadCloser, error) {
|
||||||
if params.BitRate == 0 {
|
if params.BitRate == 0 {
|
||||||
params.BitRate = 100000
|
params.BitRate = 100000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constraints := track.GetConstraints()
|
||||||
|
|
||||||
var rv C.int
|
var rv C.int
|
||||||
cEncoder := C.enc_new(C.EncoderOptions{
|
cEncoder := C.enc_new(C.EncoderOptions{
|
||||||
width: C.int(p.Width),
|
width: C.int(constraints.Width),
|
||||||
height: C.int(p.Height),
|
height: C.int(constraints.Height),
|
||||||
target_bitrate: C.int(params.BitRate),
|
target_bitrate: C.int(params.BitRate),
|
||||||
max_fps: C.float(p.FrameRate),
|
max_fps: C.float(constraints.FrameRate),
|
||||||
}, &rv)
|
}, &rv)
|
||||||
if err := errResult(rv); err != nil {
|
if err := errResult(rv); err != nil {
|
||||||
return nil, fmt.Errorf("failed in creating encoder: %v", err)
|
return nil, fmt.Errorf("failed in creating encoder: %v", err)
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
package openh264
|
package openh264
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
|
||||||
"github.com/pion/webrtc/v2"
|
"github.com/pion/webrtc/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,11 +23,25 @@ func NewParams() (Params, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Name represents the codec name
|
// Name represents the codec name
|
||||||
func (p *Params) Name() string {
|
func (p *Params) Codec() *webrtc.RTPCodec {
|
||||||
return webrtc.H264
|
return webrtc.NewRTPH264Codec(webrtc.DefaultPayloadTypeH264, 90000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildVideoEncoder builds openh264 encoder with given params
|
// BuildVideoEncoder builds openh264 encoder with given params
|
||||||
func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
|
func (p *Params) BuildEncoder(track mediadevices.Track) (codec.RTPReadCloser, error) {
|
||||||
return newEncoder(r, property, *p)
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
35
pkg/codec/sampler.go
Normal file
35
pkg/codec/sampler.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
117
pkg/ext/webrtc/webrtc.go
Normal file
117
pkg/ext/webrtc/webrtc.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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
|
||||||
|
}
|
35
sampler.go
35
sampler.go
@@ -1,35 +0,0 @@
|
|||||||
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})
|
|
||||||
})
|
|
||||||
}
|
|
341
track.go
341
track.go
@@ -2,21 +2,29 @@ package mediadevices
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"image"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
mio "github.com/pion/mediadevices/pkg/io"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
"github.com/pion/webrtc/v2"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/webrtc/v2/pkg/media"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
|
"github.com/pion/mediadevices/pkg/wave"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tracker is an interface that represent MediaStreamTrack
|
type TrackKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TrackKindVideo TrackKind = "video"
|
||||||
|
TrackKindAudio TrackKind = "audio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track is an interface that represent MediaStreamTrack
|
||||||
// Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack
|
// Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack
|
||||||
type Tracker interface {
|
type Track interface {
|
||||||
Track() *webrtc.Track
|
ID() string
|
||||||
LocalTrack() LocalTrack
|
GetConstraints() prop.Media
|
||||||
|
Kind() TrackKind
|
||||||
Stop()
|
Stop()
|
||||||
// OnEnded registers a handler to receive an error from the media stream track.
|
// 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
|
// If the error is already occured before registering, the handler will be
|
||||||
@@ -24,18 +32,147 @@ type Tracker interface {
|
|||||||
OnEnded(func(error))
|
OnEnded(func(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalTrack interface {
|
type VideoTrack struct {
|
||||||
WriteSample(s media.Sample) error
|
baseTrack
|
||||||
Codec() *webrtc.RTPCodec
|
src video.Reader
|
||||||
ID() string
|
transformed video.Reader
|
||||||
Kind() webrtc.RTPCodecType
|
mux sync.Mutex
|
||||||
|
frameCount int
|
||||||
|
lastFrame image.Image
|
||||||
|
lastErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
type track struct {
|
func newVideoTrack(d driver.Driver, constraints prop.Media) (*VideoTrack, error) {
|
||||||
localTrack LocalTrack
|
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
|
d driver.Driver
|
||||||
sample samplerFunc
|
constraints prop.Media
|
||||||
encoder codec.ReadCloser
|
|
||||||
|
|
||||||
onErrorHandler func(error)
|
onErrorHandler func(error)
|
||||||
err error
|
err error
|
||||||
@@ -43,83 +180,17 @@ type track struct {
|
|||||||
endOnce sync.Once
|
endOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrackConstraints) (*track, error) {
|
func (t *baseTrack) ID() string {
|
||||||
var encoderBuilders []encoderBuilder
|
return t.d.ID()
|
||||||
var rtpCodecs []*webrtc.RTPCodec
|
}
|
||||||
var buildSampler func(t LocalTrack) samplerFunc
|
|
||||||
var err error
|
|
||||||
|
|
||||||
err = d.Open()
|
func (t *baseTrack) GetConstraints() prop.Media {
|
||||||
if err != nil {
|
return t.constraints
|
||||||
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.Latency)
|
|
||||||
}
|
|
||||||
encoderBuilders, err = newAudioEncoderBuilders(r, constraints)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("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, fmt.Errorf("newTrack: failed to find a matching codec")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnEnded sets an error handler. When a track has been created and started, if an
|
// 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.
|
// error occurs, handler will get called with the error given to the parameter.
|
||||||
func (t *track) OnEnded(handler func(error)) {
|
func (t *baseTrack) OnEnded(handler func(error)) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
t.onErrorHandler = handler
|
t.onErrorHandler = handler
|
||||||
err := t.err
|
err := t.err
|
||||||
@@ -134,7 +205,7 @@ func (t *track) OnEnded(handler func(error)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// onError is a callback when an error occurs
|
// onError is a callback when an error occurs
|
||||||
func (t *track) onError(err error) {
|
func (t *baseTrack) onError(err error) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
t.err = err
|
t.err = err
|
||||||
handler := t.onErrorHandler
|
handler := t.onErrorHandler
|
||||||
@@ -147,92 +218,6 @@ func (t *track) onError(err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// start starts the data flow from the driver all the way to the localTrack
|
func (t *baseTrack) Stop() {
|
||||||
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.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.Media)
|
|
||||||
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.Media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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.Media)
|
|
||||||
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.Media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return encoderBuilders, nil
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user