mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-26 20:41:46 +08:00
Compare commits
5 Commits
21d2f4618c
...
refractor
Author | SHA1 | Date | |
---|---|---|---|
![]() |
031b9a95c6 | ||
![]() |
668ef32cd5 | ||
![]() |
7e739a814b | ||
![]() |
22282bc1d7 | ||
![]() |
d7ee554323 |
@@ -6,11 +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"
|
||||
)
|
||||
|
||||
@@ -56,22 +55,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(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = frame.FormatI420 // most of the encoder accepts I420
|
||||
c.Enabled = true
|
||||
c.Width = 640
|
||||
c.Height = 480
|
||||
c.VideoTransform = markFacesTransformer
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
||||
Video: func(p *prop.Media) {
|
||||
p.Width = 640
|
||||
p.Height = 480
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@@ -6,10 +6,9 @@ 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"
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
@@ -25,6 +24,12 @@ 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{
|
||||
@@ -38,21 +43,13 @@ 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(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = frame.FormatYUY2
|
||||
c.Enabled = true
|
||||
c.Width = 640
|
||||
c.Height = 480
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
||||
Video: func(p *prop.Media) {
|
||||
p.Width = 640
|
||||
p.Height = 480
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@@ -5,10 +5,13 @@ 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/screen" // This is required to register screen capture adapter
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"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/webrtc/v2"
|
||||
)
|
||||
|
||||
@@ -25,12 +28,16 @@ func main() {
|
||||
offer := webrtc.SessionDescription{}
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
|
||||
openh264Encoder, err := openh264.NewParams()
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -42,32 +49,15 @@ 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
|
||||
|
||||
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}
|
||||
},
|
||||
s, err := mediadevices.GetDisplayMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(p *prop.Media) {},
|
||||
})
|
||||
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,
|
||||
for _, track := range s.GetTracks() {
|
||||
_, err = peerConnection.ExtAddTransceiverFromTrack(track,
|
||||
webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
|
@@ -2,130 +2,64 @@ 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/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
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
|
||||
// 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/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
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Wait for the offer to be pasted
|
||||
offer := webrtc.SessionDescription{}
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set the handler for ICE connection state
|
||||
// This will notify you when the peer has connected/disconnected
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||
})
|
||||
|
||||
md := mediadevices.NewMediaDevices(peerConnection)
|
||||
|
||||
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}
|
||||
},
|
||||
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(p *prop.Media) {},
|
||||
})
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 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 {}
|
||||
log.Println(http.ListenAndServe(":1313", nil))
|
||||
}
|
||||
|
158
mediadevices.go
158
mediadevices.go
@@ -6,106 +6,37 @@ 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 (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
trackers := make([]Tracker, 0)
|
||||
func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
tracks := make([]Track, 0)
|
||||
|
||||
cleanTrackers := func() {
|
||||
for _, t := range trackers {
|
||||
cleanTracks := func() {
|
||||
for _, t := range tracks {
|
||||
t.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
var videoConstraints MediaTrackConstraints
|
||||
if constraints.Video != nil {
|
||||
constraints.Video(&videoConstraints)
|
||||
}
|
||||
|
||||
if videoConstraints.Enabled {
|
||||
tracker, err := m.selectScreen(videoConstraints)
|
||||
var p prop.Media
|
||||
constraints.Video(&p)
|
||||
track, err := selectScreen(p)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
cleanTracks()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackers = append(trackers, tracker)
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
|
||||
s, err := NewMediaStream(trackers...)
|
||||
s, err := NewMediaStream(tracks...)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
cleanTracks()
|
||||
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
|
||||
// with tracks containing the requested types of media.
|
||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
// TODO: It should return media stream based on constraints
|
||||
trackers := make([]Tracker, 0)
|
||||
func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
tracks := make([]Track, 0)
|
||||
|
||||
cleanTrackers := func() {
|
||||
for _, t := range trackers {
|
||||
cleanTracks := func() {
|
||||
for _, t := range tracks {
|
||||
t.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
var videoConstraints, audioConstraints MediaTrackConstraints
|
||||
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 {
|
||||
constraints.Audio(&audioConstraints)
|
||||
}
|
||||
|
||||
if videoConstraints.Enabled {
|
||||
tracker, err := m.selectVideo(videoConstraints)
|
||||
var p prop.Media
|
||||
constraints.Audio(&p)
|
||||
track, err := selectAudio(p)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
cleanTracks()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackers = append(trackers, tracker)
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
|
||||
if audioConstraints.Enabled {
|
||||
tracker, err := m.selectAudio(audioConstraints)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackers = append(trackers, tracker)
|
||||
}
|
||||
|
||||
s, err := NewMediaStream(trackers...)
|
||||
s, err := NewMediaStream(tracks...)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
cleanTracks()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -191,7 +116,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 MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) {
|
||||
func selectBestDriver(filter driver.FilterFn, constraints prop.Media) (driver.Driver, prop.Media, error) {
|
||||
var bestDriver driver.Driver
|
||||
var bestProp prop.Media
|
||||
minFitnessDist := math.Inf(1)
|
||||
@@ -200,7 +125,7 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
|
||||
for d, props := range driverProperties {
|
||||
priority := float64(d.Info().Priority)
|
||||
for _, p := range props {
|
||||
fitnessDist := constraints.Media.FitnessDistance(p) - priority
|
||||
fitnessDist := constraints.FitnessDistance(p) - priority
|
||||
if fitnessDist < minFitnessDist {
|
||||
minFitnessDist = fitnessDist
|
||||
bestDriver = d
|
||||
@@ -210,14 +135,14 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
|
||||
}
|
||||
|
||||
if bestDriver == nil {
|
||||
return nil, MediaTrackConstraints{}, errNotFound
|
||||
return nil, prop.Media{}, errNotFound
|
||||
}
|
||||
|
||||
constraints.Merge(bestProp)
|
||||
return bestDriver, constraints, nil
|
||||
}
|
||||
|
||||
func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
|
||||
func selectAudio(constraints prop.Media) (Track, error) {
|
||||
typeFilter := driver.FilterAudioRecorder()
|
||||
filter := typeFilter
|
||||
if constraints.DeviceID != "" {
|
||||
@@ -230,9 +155,10 @@ func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker,
|
||||
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()
|
||||
notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen))
|
||||
filter := driver.FilterAnd(typeFilter, notScreenFilter)
|
||||
@@ -246,10 +172,10 @@ func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker,
|
||||
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()
|
||||
screenFilter := driver.FilterDeviceType(driver.Screen)
|
||||
filter := driver.FilterAnd(typeFilter, screenFilter)
|
||||
@@ -263,10 +189,10 @@ func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker,
|
||||
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(
|
||||
driver.FilterFn(func(driver.Driver) bool { return true }))
|
||||
info := make([]MediaDeviceInfo, 0, len(drivers))
|
||||
|
@@ -18,18 +18,25 @@ import (
|
||||
)
|
||||
|
||||
func TestGetUserMedia(t *testing.T) {
|
||||
videoParams := mockParams{
|
||||
BaseParams: codec.BaseParams{
|
||||
BitRate: 100000,
|
||||
},
|
||||
brokenVideoParams := mockParams{
|
||||
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{
|
||||
@@ -46,43 +53,36 @@ 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 = 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
|
||||
ms, err := md.GetUserMedia(constraintsWrong)
|
||||
ms, err := md.GetUserMedia(constraints)
|
||||
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() []Tracker
|
||||
GetAudioTracks() []Track
|
||||
// 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() []Tracker
|
||||
GetTracks() []Track
|
||||
// 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(t Tracker)
|
||||
RemoveTrack(t Track)
|
||||
}
|
||||
|
||||
type mediaStream struct {
|
||||
trackers map[string]Tracker
|
||||
l sync.RWMutex
|
||||
tracks map[string]Track
|
||||
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(trackers ...Tracker) (MediaStream, error) {
|
||||
m := mediaStream{trackers: make(map[string]Tracker)}
|
||||
func NewMediaStream(tracks ...Track) (MediaStream, error) {
|
||||
m := mediaStream{tracks: make(map[string]Track)}
|
||||
|
||||
for _, tracker := range trackers {
|
||||
id := tracker.LocalTrack().ID()
|
||||
if _, ok := m.trackers[id]; !ok {
|
||||
m.trackers[id] = tracker
|
||||
for _, track := range tracks {
|
||||
id := track.ID()
|
||||
if _, ok := m.tracks[id]; !ok {
|
||||
m.tracks[id] = track
|
||||
}
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (m *mediaStream) GetAudioTracks() []Tracker {
|
||||
return m.queryTracks(webrtc.RTPCodecTypeAudio)
|
||||
func (m *mediaStream) GetAudioTracks() []Track {
|
||||
return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindAudio })
|
||||
}
|
||||
|
||||
func (m *mediaStream) GetVideoTracks() []Tracker {
|
||||
return m.queryTracks(webrtc.RTPCodecTypeVideo)
|
||||
func (m *mediaStream) GetVideoTracks() []Track {
|
||||
return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindVideo })
|
||||
}
|
||||
|
||||
func (m *mediaStream) GetTracks() []Tracker {
|
||||
return m.queryTracks(rtpCodecTypeDefault)
|
||||
func (m *mediaStream) GetTracks() []Track {
|
||||
return m.queryTracks(func(t Track) bool { return true })
|
||||
}
|
||||
|
||||
// queryTracks returns all tracks that are the same kind as t.
|
||||
// If t is 0, which is the default, queryTracks will return all the tracks.
|
||||
func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Tracker {
|
||||
func (m *mediaStream) queryTracks(filter func(track Track) bool) []Track {
|
||||
m.l.RLock()
|
||||
defer m.l.RUnlock()
|
||||
|
||||
result := make([]Tracker, 0)
|
||||
for _, tracker := range m.trackers {
|
||||
if tracker.LocalTrack().Kind() == t || t == rtpCodecTypeDefault {
|
||||
result = append(result, tracker)
|
||||
result := make([]Track, 0)
|
||||
for _, track := range m.tracks {
|
||||
if filter(track) {
|
||||
result = append(result, track)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *mediaStream) AddTrack(t Tracker) {
|
||||
func (m *mediaStream) AddTrack(t Track) {
|
||||
m.l.Lock()
|
||||
defer m.l.Unlock()
|
||||
|
||||
id := t.LocalTrack().ID()
|
||||
if _, ok := m.trackers[id]; ok {
|
||||
id := t.ID()
|
||||
if _, ok := m.tracks[id]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
m.trackers[id] = t
|
||||
m.tracks[id] = t
|
||||
}
|
||||
|
||||
func (m *mediaStream) RemoveTrack(t Tracker) {
|
||||
func (m *mediaStream) RemoveTrack(t Track) {
|
||||
m.l.Lock()
|
||||
defer m.l.Unlock()
|
||||
|
||||
delete(m.trackers, t.LocalTrack().ID())
|
||||
delete(m.tracks, t.ID())
|
||||
}
|
||||
|
@@ -1,39 +1,13 @@
|
||||
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 MediaOption
|
||||
Video MediaOption
|
||||
Audio MediaTrackConstraints
|
||||
Video MediaTrackConstraints
|
||||
}
|
||||
|
||||
// MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
|
||||
type MediaTrackConstraints struct {
|
||||
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)
|
||||
type MediaTrackConstraints func(*prop.Media)
|
||||
|
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 (
|
||||
"io"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
// 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 RTPReader interface {
|
||||
ReadRTP() (*rtp.Packet, 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)
|
||||
type RTPReadCloser interface {
|
||||
RTPReader
|
||||
Close()
|
||||
}
|
||||
|
||||
type EncoderBuilder interface {
|
||||
Codec() *webrtc.RTPCodec
|
||||
BuildEncoder(mediadevices.Track) (RTPReadCloser, 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,17 +30,19 @@ type encoder struct {
|
||||
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 {
|
||||
params.BitRate = 100000
|
||||
}
|
||||
|
||||
constraints := track.GetConstraints()
|
||||
|
||||
var rv C.int
|
||||
cEncoder := C.enc_new(C.EncoderOptions{
|
||||
width: C.int(p.Width),
|
||||
height: C.int(p.Height),
|
||||
width: C.int(constraints.Width),
|
||||
height: C.int(constraints.Height),
|
||||
target_bitrate: C.int(params.BitRate),
|
||||
max_fps: C.float(p.FrameRate),
|
||||
max_fps: C.float(constraints.FrameRate),
|
||||
}, &rv)
|
||||
if err := errResult(rv); err != nil {
|
||||
return nil, fmt.Errorf("failed in creating encoder: %v", err)
|
||||
|
@@ -1,9 +1,10 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -22,11 +23,25 @@ func NewParams() (Params, error) {
|
||||
}
|
||||
|
||||
// Name represents the codec name
|
||||
func (p *Params) Name() string {
|
||||
return webrtc.H264
|
||||
func (p *Params) Codec() *webrtc.RTPCodec {
|
||||
return webrtc.NewRTPH264Codec(webrtc.DefaultPayloadTypeH264, 90000)
|
||||
}
|
||||
|
||||
// BuildVideoEncoder builds openh264 encoder with given params
|
||||
func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
|
||||
return newEncoder(r, property, *p)
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
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})
|
||||
})
|
||||
}
|
343
track.go
343
track.go
@@ -2,21 +2,29 @@ package mediadevices
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
mio "github.com/pion/mediadevices/pkg/io"
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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
|
||||
type Tracker interface {
|
||||
Track() *webrtc.Track
|
||||
LocalTrack() LocalTrack
|
||||
type Track interface {
|
||||
ID() string
|
||||
GetConstraints() prop.Media
|
||||
Kind() TrackKind
|
||||
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
|
||||
@@ -24,18 +32,147 @@ type Tracker interface {
|
||||
OnEnded(func(error))
|
||||
}
|
||||
|
||||
type LocalTrack interface {
|
||||
WriteSample(s media.Sample) error
|
||||
Codec() *webrtc.RTPCodec
|
||||
ID() string
|
||||
Kind() webrtc.RTPCodecType
|
||||
type VideoTrack struct {
|
||||
baseTrack
|
||||
src video.Reader
|
||||
transformed video.Reader
|
||||
mux sync.Mutex
|
||||
frameCount int
|
||||
lastFrame image.Image
|
||||
lastErr error
|
||||
}
|
||||
|
||||
type track struct {
|
||||
localTrack LocalTrack
|
||||
d driver.Driver
|
||||
sample samplerFunc
|
||||
encoder codec.ReadCloser
|
||||
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
|
||||
|
||||
onErrorHandler func(error)
|
||||
err error
|
||||
@@ -43,83 +180,17 @@ type track struct {
|
||||
endOnce sync.Once
|
||||
}
|
||||
|
||||
func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrackConstraints) (*track, error) {
|
||||
var encoderBuilders []encoderBuilder
|
||||
var rtpCodecs []*webrtc.RTPCodec
|
||||
var buildSampler func(t LocalTrack) samplerFunc
|
||||
var err error
|
||||
func (t *baseTrack) ID() string {
|
||||
return t.d.ID()
|
||||
}
|
||||
|
||||
err = d.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch r := d.(type) {
|
||||
case driver.VideoRecorder:
|
||||
rtpCodecs = opts.codecs[webrtc.RTPCodecTypeVideo]
|
||||
buildSampler = newVideoSampler
|
||||
encoderBuilders, err = newVideoEncoderBuilders(r, constraints)
|
||||
case driver.AudioRecorder:
|
||||
rtpCodecs = opts.codecs[webrtc.RTPCodecTypeAudio]
|
||||
buildSampler = func(t LocalTrack) samplerFunc {
|
||||
return newAudioSampler(t, constraints.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")
|
||||
func (t *baseTrack) GetConstraints() prop.Media {
|
||||
return t.constraints
|
||||
}
|
||||
|
||||
// OnEnded sets an error handler. When a track has been created and started, if an
|
||||
// error occurs, handler will get called with the error given to the parameter.
|
||||
func (t *track) OnEnded(handler func(error)) {
|
||||
func (t *baseTrack) OnEnded(handler func(error)) {
|
||||
t.mu.Lock()
|
||||
t.onErrorHandler = handler
|
||||
err := t.err
|
||||
@@ -134,7 +205,7 @@ func (t *track) OnEnded(handler func(error)) {
|
||||
}
|
||||
|
||||
// onError is a callback when an error occurs
|
||||
func (t *track) onError(err error) {
|
||||
func (t *baseTrack) onError(err error) {
|
||||
t.mu.Lock()
|
||||
t.err = err
|
||||
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 *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() {
|
||||
func (t *baseTrack) 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.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