mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-26 20:41:46 +08:00
303 lines
8.3 KiB
Go
303 lines
8.3 KiB
Go
package mediadevices
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/pion/mediadevices/pkg/driver"
|
|
"github.com/pion/mediadevices/pkg/prop"
|
|
"github.com/pion/webrtc/v2"
|
|
)
|
|
|
|
var errNotFound = fmt.Errorf("failed to find the best driver that fits the constraints")
|
|
|
|
// MediaDevices is an interface that's defined on https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
|
|
type MediaDevices interface {
|
|
GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error)
|
|
GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error)
|
|
EnumerateDevices() []MediaDeviceInfo
|
|
}
|
|
|
|
// NewMediaDevices creates MediaDevices interface that provides access to connected media input devices
|
|
// like cameras and microphones, as well as screen sharing.
|
|
// In essence, it lets you obtain access to any hardware source of media data.
|
|
func NewMediaDevices(pc *webrtc.PeerConnection, opts ...MediaDevicesOption) MediaDevices {
|
|
codecs := make(map[webrtc.RTPCodecType][]*webrtc.RTPCodec)
|
|
for _, kind := range []webrtc.RTPCodecType{
|
|
webrtc.RTPCodecTypeAudio,
|
|
webrtc.RTPCodecTypeVideo,
|
|
} {
|
|
codecs[kind] = pc.GetRegisteredRTPCodecs(kind)
|
|
}
|
|
return NewMediaDevicesFromCodecs(codecs, opts...)
|
|
}
|
|
|
|
// NewMediaDevicesFromCodecs creates MediaDevices interface from lists of the available codecs
|
|
// that provides access to connected media input devices like cameras and microphones,
|
|
// as well as screen sharing.
|
|
// In essence, it lets you obtain access to any hardware source of media data.
|
|
func NewMediaDevicesFromCodecs(codecs map[webrtc.RTPCodecType][]*webrtc.RTPCodec, opts ...MediaDevicesOption) MediaDevices {
|
|
mdo := MediaDevicesOptions{
|
|
codecs: codecs,
|
|
trackGenerator: defaultTrackGenerator,
|
|
}
|
|
for _, o := range opts {
|
|
o(&mdo)
|
|
}
|
|
return &mediaDevices{
|
|
MediaDevicesOptions: mdo,
|
|
}
|
|
}
|
|
|
|
// TrackGenerator is a function to create new track.
|
|
type TrackGenerator func(payloadType uint8, ssrc uint32, id, label string, codec *webrtc.RTPCodec) (LocalTrack, error)
|
|
|
|
var defaultTrackGenerator = TrackGenerator(func(pt uint8, ssrc uint32, id, label string, codec *webrtc.RTPCodec) (LocalTrack, error) {
|
|
return webrtc.NewTrack(pt, ssrc, id, label, codec)
|
|
})
|
|
|
|
type mediaDevices struct {
|
|
MediaDevicesOptions
|
|
}
|
|
|
|
// MediaDevicesOptions stores parameters used by MediaDevices.
|
|
type MediaDevicesOptions struct {
|
|
codecs map[webrtc.RTPCodecType][]*webrtc.RTPCodec
|
|
trackGenerator TrackGenerator
|
|
}
|
|
|
|
// MediaDevicesOption is a type of MediaDevices functional option.
|
|
type MediaDevicesOption func(*MediaDevicesOptions)
|
|
|
|
// WithTrackGenerator specifies a TrackGenerator to use customized track.
|
|
func WithTrackGenerator(gen TrackGenerator) MediaDevicesOption {
|
|
return func(o *MediaDevicesOptions) {
|
|
o.trackGenerator = gen
|
|
}
|
|
}
|
|
|
|
// GetDisplayMedia prompts the user to select and grant permission to capture the contents
|
|
// of a display or portion thereof (such as a window) as a MediaStream.
|
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
|
|
func (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
|
trackers := make([]Tracker, 0)
|
|
|
|
cleanTrackers := func() {
|
|
for _, t := range trackers {
|
|
t.Stop()
|
|
}
|
|
}
|
|
|
|
var videoConstraints MediaTrackConstraints
|
|
if constraints.Video != nil {
|
|
constraints.Video(&videoConstraints)
|
|
}
|
|
|
|
if videoConstraints.Enabled {
|
|
tracker, err := m.selectScreen(videoConstraints)
|
|
if err != nil {
|
|
cleanTrackers()
|
|
return nil, err
|
|
}
|
|
|
|
trackers = append(trackers, tracker)
|
|
}
|
|
|
|
s, err := NewMediaStream(trackers...)
|
|
if err != nil {
|
|
cleanTrackers()
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// 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)
|
|
|
|
cleanTrackers := func() {
|
|
for _, t := range trackers {
|
|
t.Stop()
|
|
}
|
|
}
|
|
|
|
var videoConstraints, audioConstraints MediaTrackConstraints
|
|
if constraints.Video != nil {
|
|
constraints.Video(&videoConstraints)
|
|
}
|
|
|
|
if constraints.Audio != nil {
|
|
constraints.Audio(&audioConstraints)
|
|
}
|
|
|
|
if videoConstraints.Enabled {
|
|
tracker, err := m.selectVideo(videoConstraints)
|
|
if err != nil {
|
|
cleanTrackers()
|
|
return nil, err
|
|
}
|
|
|
|
trackers = append(trackers, tracker)
|
|
}
|
|
|
|
if audioConstraints.Enabled {
|
|
tracker, err := m.selectAudio(audioConstraints)
|
|
if err != nil {
|
|
cleanTrackers()
|
|
return nil, err
|
|
}
|
|
|
|
trackers = append(trackers, tracker)
|
|
}
|
|
|
|
s, err := NewMediaStream(trackers...)
|
|
if err != nil {
|
|
cleanTrackers()
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func queryDriverProperties(filter driver.FilterFn) map[driver.Driver][]prop.Media {
|
|
var needToClose []driver.Driver
|
|
drivers := driver.GetManager().Query(filter)
|
|
m := make(map[driver.Driver][]prop.Media)
|
|
|
|
for _, d := range drivers {
|
|
if d.Status() == driver.StateClosed {
|
|
err := d.Open()
|
|
if err != nil {
|
|
// Skip this driver if we failed to open because we can't get the properties
|
|
continue
|
|
}
|
|
needToClose = append(needToClose, d)
|
|
}
|
|
|
|
m[d] = d.Properties()
|
|
}
|
|
|
|
for _, d := range needToClose {
|
|
// Since it was closed, we should close it to avoid a leak
|
|
d.Close()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// select implements SelectSettings algorithm.
|
|
// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings
|
|
func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) {
|
|
var bestDriver driver.Driver
|
|
var bestProp prop.Media
|
|
minFitnessDist := math.Inf(1)
|
|
|
|
driverProperties := queryDriverProperties(filter)
|
|
for d, props := range driverProperties {
|
|
priority := float64(d.Info().Priority)
|
|
for _, p := range props {
|
|
fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fitnessDist -= priority
|
|
if fitnessDist < minFitnessDist {
|
|
minFitnessDist = fitnessDist
|
|
bestDriver = d
|
|
bestProp = p
|
|
}
|
|
}
|
|
}
|
|
|
|
if bestDriver == nil {
|
|
var foundProperties []string
|
|
for _, props := range driverProperties {
|
|
for _, p := range props {
|
|
foundProperties = append(foundProperties, fmt.Sprint(&p))
|
|
}
|
|
}
|
|
|
|
err := fmt.Errorf(`%w:
|
|
============ Found Properties ============
|
|
|
|
%s
|
|
|
|
=============== Constraints ==============
|
|
|
|
%s
|
|
`, errNotFound, strings.Join(foundProperties, "\n\n"), &constraints)
|
|
return nil, MediaTrackConstraints{}, err
|
|
}
|
|
|
|
constraints.selectedMedia = prop.Media{}
|
|
constraints.selectedMedia.MergeConstraints(constraints.MediaConstraints)
|
|
constraints.selectedMedia.Merge(bestProp)
|
|
return bestDriver, constraints, nil
|
|
}
|
|
|
|
func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
|
|
typeFilter := driver.FilterAudioRecorder()
|
|
|
|
d, c, err := selectBestDriver(typeFilter, constraints)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newTrack(&m.MediaDevicesOptions, d, c)
|
|
}
|
|
func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker, error) {
|
|
typeFilter := driver.FilterVideoRecorder()
|
|
notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen))
|
|
filter := driver.FilterAnd(typeFilter, notScreenFilter)
|
|
|
|
d, c, err := selectBestDriver(filter, constraints)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newTrack(&m.MediaDevicesOptions, d, c)
|
|
}
|
|
|
|
func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker, error) {
|
|
typeFilter := driver.FilterVideoRecorder()
|
|
screenFilter := driver.FilterDeviceType(driver.Screen)
|
|
filter := driver.FilterAnd(typeFilter, screenFilter)
|
|
|
|
d, c, err := selectBestDriver(filter, constraints)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newTrack(&m.MediaDevicesOptions, d, c)
|
|
}
|
|
|
|
func (m *mediaDevices) EnumerateDevices() []MediaDeviceInfo {
|
|
drivers := driver.GetManager().Query(
|
|
driver.FilterFn(func(driver.Driver) bool { return true }))
|
|
info := make([]MediaDeviceInfo, 0, len(drivers))
|
|
for _, d := range drivers {
|
|
var kind MediaDeviceType
|
|
switch {
|
|
case driver.FilterVideoRecorder()(d):
|
|
kind = VideoInput
|
|
case driver.FilterAudioRecorder()(d):
|
|
kind = AudioInput
|
|
default:
|
|
continue
|
|
}
|
|
driverInfo := d.Info()
|
|
info = append(info, MediaDeviceInfo{
|
|
DeviceID: d.ID(),
|
|
Kind: kind,
|
|
Label: driverInfo.Label,
|
|
DeviceType: driverInfo.DeviceType,
|
|
})
|
|
}
|
|
return info
|
|
}
|