mirror of
https://github.com/pion/mediadevices.git
synced 2025-10-03 15:56:28 +08:00
Compare commits
10 Commits
generic-re
...
v0.1.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
943906e125 | ||
![]() |
f3e3dc9589 | ||
![]() |
a3d374f528 | ||
![]() |
cba0042f5d | ||
![]() |
1732e2751d | ||
![]() |
5b1527d455 | ||
![]() |
00f0a44ab1 | ||
![]() |
a44240be5f | ||
![]() |
70f7360b92 | ||
![]() |
30d49e1fd3 |
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -30,15 +30,15 @@ jobs:
|
|||||||
libvpx-dev \
|
libvpx-dev \
|
||||||
libx264-dev
|
libx264-dev
|
||||||
- name: go vet
|
- name: go vet
|
||||||
run: go vet ./...
|
run: go vet -tags nolibopusfile ./...
|
||||||
- name: go build
|
- name: go build
|
||||||
run: go build ./...
|
run: go build -tags nolibopusfile ./...
|
||||||
- name: go build without CGO
|
- name: go build without CGO
|
||||||
run: go build . pkg/...
|
run: go build . pkg/...
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
- name: go test
|
- name: go test
|
||||||
run: go test ./... -v -race
|
run: go test -tags nolibopusfile ./... -v -race
|
||||||
- name: go test without CGO
|
- name: go test without CGO
|
||||||
run: go test . pkg/... -v
|
run: go test . pkg/... -v
|
||||||
env:
|
env:
|
||||||
|
6
go.mod
6
go.mod
@@ -5,9 +5,9 @@ go 1.13
|
|||||||
require (
|
require (
|
||||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
|
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
|
||||||
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa
|
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa
|
||||||
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4
|
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39
|
||||||
github.com/pion/webrtc/v2 v2.2.24
|
github.com/pion/webrtc/v2 v2.2.26
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
golang.org/x/image v0.0.0-20200801110659-972c09e46d76
|
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
||||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a
|
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a
|
||||||
)
|
)
|
||||||
|
20
go.sum
20
go.sum
@@ -22,8 +22,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4 h1:2ydMA2KbxRkYmIw3R8Me8dn90bejxBR4MKYXJ5THK3I=
|
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39 h1:WEYmSwg/uoPVmfmpXWPYplb1UUx/Jr4TXGNrPaI8Cj4=
|
||||||
github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
|
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
|
||||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
|
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
|
||||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
||||||
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
|
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
|
||||||
@@ -33,8 +33,8 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
|||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/pion/datachannel v1.4.20 h1:+uYUrxbhGuEt+9En81Necda5ul8M2h7mMsvGWkYZ/yI=
|
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
|
||||||
github.com/pion/datachannel v1.4.20/go.mod h1:hsjWYdTW5fMmtM4hVIxUNYqViRPv2A6ixzkQFd82wSc=
|
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
||||||
github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA=
|
github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA=
|
||||||
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
|
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
|
||||||
github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E=
|
github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E=
|
||||||
@@ -54,8 +54,8 @@ github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA=
|
|||||||
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
||||||
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
|
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
|
||||||
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
|
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
|
||||||
github.com/pion/sctp v1.7.9 h1:n+A37cTMU08xL3Oodkz39XjtPReQliKyk01q96mGB5M=
|
github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8=
|
||||||
github.com/pion/sctp v1.7.9/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
||||||
github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI=
|
github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI=
|
||||||
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
||||||
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
||||||
@@ -73,8 +73,8 @@ github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk=
|
|||||||
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
||||||
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
|
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
|
||||||
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
||||||
github.com/pion/webrtc/v2 v2.2.24 h1:l7q/iO96tMTElxuE2XGdNhCzklGcd9aVZ00XufASp0g=
|
github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc=
|
||||||
github.com/pion/webrtc/v2 v2.2.24/go.mod h1:U/m+nvG1t8gInf8PwiDyJEDcd7qfl+jmGQXqTX2zGvo=
|
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -98,8 +98,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnk
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw=
|
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
|
||||||
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
@@ -3,6 +3,7 @@ package mediadevices
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
@@ -214,7 +215,23 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bestDriver == nil {
|
if bestDriver == nil {
|
||||||
return nil, MediaTrackConstraints{}, errNotFound
|
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 = prop.Media{}
|
||||||
|
162
pkg/io/broadcast.go
Normal file
162
pkg/io/broadcast.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package io
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maskReading = 1 << 63
|
||||||
|
defaultBroadcasterRingSize = 32
|
||||||
|
// TODO: If the data source has fps greater than 30, they'll see some
|
||||||
|
// fps fluctuation. But, 30 fps should be enough for general cases.
|
||||||
|
defaultBroadcasterRingPollDuration = time.Millisecond * 33
|
||||||
|
)
|
||||||
|
|
||||||
|
var errEmptySource = fmt.Errorf("Source can't be nil")
|
||||||
|
|
||||||
|
type broadcasterData struct {
|
||||||
|
data interface{}
|
||||||
|
count uint32
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type broadcasterRing struct {
|
||||||
|
// reading (1 bit) + reserved (31 bits) + data count (32 bits)
|
||||||
|
// IMPORTANT: state has to be the first element in struct, otherwise LoadUint64 will panic in 32 bits systems
|
||||||
|
// due to unallignment
|
||||||
|
state uint64
|
||||||
|
buffer []atomic.Value
|
||||||
|
pollDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBroadcasterRing(size uint, pollDuration time.Duration) *broadcasterRing {
|
||||||
|
return &broadcasterRing{buffer: make([]atomic.Value, size), pollDuration: pollDuration}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ring *broadcasterRing) index(count uint32) int {
|
||||||
|
return int(count) % len(ring.buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ring *broadcasterRing) acquire(count uint32) func(*broadcasterData) {
|
||||||
|
// Reader has reached the latest data, should read from the source.
|
||||||
|
// Only allow 1 reader to read from the source. When there are more than 1 readers,
|
||||||
|
// the other readers will need to share the same data that the first reader gets from
|
||||||
|
// the source.
|
||||||
|
state := uint64(count)
|
||||||
|
if atomic.CompareAndSwapUint64(&ring.state, state, state|maskReading) {
|
||||||
|
return func(data *broadcasterData) {
|
||||||
|
i := ring.index(count)
|
||||||
|
ring.buffer[i].Store(data)
|
||||||
|
atomic.StoreUint64(&ring.state, uint64(count+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ring *broadcasterRing) get(count uint32) *broadcasterData {
|
||||||
|
for {
|
||||||
|
reading := uint64(count) | maskReading
|
||||||
|
// TODO: since it's lockless, it spends a lot of resources in the scheduling.
|
||||||
|
for atomic.LoadUint64(&ring.state) == reading {
|
||||||
|
// Yield current goroutine to let other goroutines to run instead
|
||||||
|
time.Sleep(ring.pollDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := ring.index(count)
|
||||||
|
data := ring.buffer[i].Load().(*broadcasterData)
|
||||||
|
if data.count == count {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ring *broadcasterRing) lastCount() uint32 {
|
||||||
|
// ring.state always keeps track the next count, so we need to subtract it by 1 to get the
|
||||||
|
// last count
|
||||||
|
return uint32(atomic.LoadUint64(&ring.state)) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcaster is a generic pull-based broadcaster. Broadcaster is unique in a sense that
|
||||||
|
// readers can come and go at anytime, and readers don't need to close or notify broadcaster.
|
||||||
|
type Broadcaster struct {
|
||||||
|
source atomic.Value
|
||||||
|
buffer *broadcasterRing
|
||||||
|
}
|
||||||
|
|
||||||
|
// BroadcasterConfig is a config to control broadcaster behaviour
|
||||||
|
type BroadcasterConfig struct {
|
||||||
|
// BufferSize configures the underlying ring buffer size that's being used
|
||||||
|
// to avoid data lost for late readers. The default value is 32.
|
||||||
|
BufferSize uint
|
||||||
|
// PollDuration configures the sleep duration in waiting for new data to come.
|
||||||
|
// The default value is 33 ms.
|
||||||
|
PollDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBroadcaster creates a new broadcaster. Source is expected to drop frames
|
||||||
|
// when any of the readers is slower than the source.
|
||||||
|
func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
||||||
|
pollDuration := defaultBroadcasterRingPollDuration
|
||||||
|
var bufferSize uint = defaultBroadcasterRingSize
|
||||||
|
if config != nil {
|
||||||
|
if config.PollDuration != 0 {
|
||||||
|
pollDuration = config.PollDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BufferSize != 0 {
|
||||||
|
bufferSize = config.BufferSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var broadcaster Broadcaster
|
||||||
|
broadcaster.buffer = newBroadcasterRing(bufferSize, pollDuration)
|
||||||
|
broadcaster.ReplaceSource(source)
|
||||||
|
|
||||||
|
return &broadcaster
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader creates a new reader. Each reader will retrieve the same data from the source.
|
||||||
|
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
|
||||||
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
|
// in the ring buffer.
|
||||||
|
func (broadcaster *Broadcaster) NewReader(copyFn func(interface{}) interface{}) Reader {
|
||||||
|
currentCount := broadcaster.buffer.lastCount()
|
||||||
|
|
||||||
|
return ReaderFunc(func() (data interface{}, err error) {
|
||||||
|
currentCount++
|
||||||
|
if push := broadcaster.buffer.acquire(currentCount); push != nil {
|
||||||
|
data, err = broadcaster.source.Load().(Reader).Read()
|
||||||
|
push(&broadcasterData{
|
||||||
|
data: data,
|
||||||
|
err: err,
|
||||||
|
count: currentCount,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ringData := broadcaster.buffer.get(currentCount)
|
||||||
|
data, err, currentCount = ringData.data, ringData.err, ringData.count
|
||||||
|
}
|
||||||
|
|
||||||
|
data = copyFn(data)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
||||||
|
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
||||||
|
if source == nil {
|
||||||
|
return errEmptySource
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcaster.source.Store(source)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceSource retrieves the underlying source. This operation is thread safe.
|
||||||
|
func (broadcaster *Broadcaster) Source() Reader {
|
||||||
|
return broadcaster.source.Load().(Reader)
|
||||||
|
}
|
@@ -1,12 +1,12 @@
|
|||||||
package io
|
package io
|
||||||
|
|
||||||
// Reader is a generic reader. When generic is ready, interface{} will be replaced
|
// Reader is a generic data reader. In the future, interface{} should be replaced by a generic type
|
||||||
// with a generic type and will provide type safety.
|
// to provide strong type.
|
||||||
type Reader interface {
|
type Reader interface {
|
||||||
Read() (interface{}, error)
|
Read() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReaderFunc is a proxy type to make easier for users to implement Reader
|
// ReaderFunc is a proxy type for Reader
|
||||||
type ReaderFunc func() (interface{}, error)
|
type ReaderFunc func() (interface{}, error)
|
||||||
|
|
||||||
func (f ReaderFunc) Read() (interface{}, error) {
|
func (f ReaderFunc) Read() (interface{}, error) {
|
||||||
|
76
pkg/io/video/broadcast.go
Normal file
76
pkg/io/video/broadcast.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices/pkg/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errEmptySource = fmt.Errorf("Source can't be nil")
|
||||||
|
|
||||||
|
// Broadcaster is a specialized video broadcaster.
|
||||||
|
type Broadcaster struct {
|
||||||
|
ioBroadcaster *io.Broadcaster
|
||||||
|
}
|
||||||
|
|
||||||
|
type BroadcasterConfig struct {
|
||||||
|
Core *io.BroadcasterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBroadcaster creates a new broadcaster. Source is expected to drop frames
|
||||||
|
// when any of the readers is slower than the source.
|
||||||
|
func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
||||||
|
var coreConfig *io.BroadcasterConfig
|
||||||
|
|
||||||
|
if config != nil {
|
||||||
|
coreConfig = config.Core
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, error) {
|
||||||
|
return source.Read()
|
||||||
|
}), coreConfig)
|
||||||
|
|
||||||
|
return &Broadcaster{broadcaster}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader creates a new reader. Each reader will retrieve the same data from the source.
|
||||||
|
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
|
||||||
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
|
// in the ring buffer.
|
||||||
|
func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
||||||
|
copyFn := func(src interface{}) interface{} { return src }
|
||||||
|
|
||||||
|
if copyFrame {
|
||||||
|
buffer := NewFrameBuffer(0)
|
||||||
|
copyFn = func(src interface{}) interface{} {
|
||||||
|
realSrc, _ := src.(image.Image)
|
||||||
|
buffer.StoreCopy(realSrc)
|
||||||
|
return buffer.Load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := broadcaster.ioBroadcaster.NewReader(copyFn)
|
||||||
|
return ReaderFunc(func() (image.Image, error) {
|
||||||
|
data, err := reader.Read()
|
||||||
|
img, _ := data.(image.Image)
|
||||||
|
return img, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
||||||
|
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
||||||
|
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, error) {
|
||||||
|
return source.Read()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source retrieves the underlying source. This operation is thread safe.
|
||||||
|
func (broadcaster *Broadcaster) Source() Reader {
|
||||||
|
source := broadcaster.ioBroadcaster.Source()
|
||||||
|
return ReaderFunc(func() (image.Image, error) {
|
||||||
|
data, err := source.Read()
|
||||||
|
img, _ := data.(image.Image)
|
||||||
|
return img, err
|
||||||
|
})
|
||||||
|
}
|
187
pkg/io/video/broadcast_test.go
Normal file
187
pkg/io/video/broadcast_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkBroadcast(b *testing.B) {
|
||||||
|
var src Reader
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 1920, 1080))
|
||||||
|
interval := time.NewTicker(time.Millisecond * 33) // 30 fps
|
||||||
|
defer interval.Stop()
|
||||||
|
src = ReaderFunc(func() (image.Image, error) {
|
||||||
|
<-interval.C
|
||||||
|
return img, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
for n := 1; n <= 4096; n *= 16 {
|
||||||
|
n := n
|
||||||
|
|
||||||
|
b.Run(fmt.Sprintf("Readers-%d", n), func(b *testing.B) {
|
||||||
|
b.SetParallelism(n)
|
||||||
|
broadcaster := NewBroadcaster(src, nil)
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
reader := broadcaster.NewReader(false)
|
||||||
|
for pb.Next() {
|
||||||
|
reader.Read()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcast(t *testing.T) {
|
||||||
|
// https://github.com/pion/mediadevices/issues/198
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
|
||||||
|
}
|
||||||
|
frames := make([]image.Image, 5*30) // 5 seconds worth of frames
|
||||||
|
resolution := image.Rect(0, 0, 1920, 1080)
|
||||||
|
for i := range frames {
|
||||||
|
rgba := image.NewRGBA(resolution)
|
||||||
|
rgba.Pix[0] = uint8(i >> 24)
|
||||||
|
rgba.Pix[1] = uint8(i >> 16)
|
||||||
|
rgba.Pix[2] = uint8(i >> 8)
|
||||||
|
rgba.Pix[3] = uint8(i)
|
||||||
|
frames[i] = rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
routinePauseConds := []struct {
|
||||||
|
src bool
|
||||||
|
dst bool
|
||||||
|
expectedFPS float64
|
||||||
|
expectedDrop float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
src: false,
|
||||||
|
dst: false,
|
||||||
|
expectedFPS: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: true,
|
||||||
|
dst: false,
|
||||||
|
expectedFPS: 20,
|
||||||
|
expectedDrop: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: false,
|
||||||
|
dst: true,
|
||||||
|
expectedFPS: 20,
|
||||||
|
expectedDrop: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pauseCond := range routinePauseConds {
|
||||||
|
pauseCond := pauseCond
|
||||||
|
t.Run(fmt.Sprintf("SrcPause-%v/DstPause-%v", pauseCond.src, pauseCond.dst), func(t *testing.T) {
|
||||||
|
for n := 1; n <= 256; n *= 16 {
|
||||||
|
n := n
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("Readers-%d", n), func(t *testing.T) {
|
||||||
|
var src Reader
|
||||||
|
interval := time.NewTicker(time.Millisecond * 33) // 30 fps
|
||||||
|
defer interval.Stop()
|
||||||
|
frameCount := 0
|
||||||
|
frameSent := 0
|
||||||
|
lastSend := time.Now()
|
||||||
|
src = ReaderFunc(func() (image.Image, error) {
|
||||||
|
if pauseCond.src && frameSent == 30 {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
<-interval.C
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if interval := now.Sub(lastSend); interval > time.Millisecond*33*3/2 {
|
||||||
|
// Source reader should drop frames to catch up the latest frame.
|
||||||
|
drop := int(interval/(time.Millisecond*33)) - 1
|
||||||
|
frameCount += drop
|
||||||
|
t.Logf("Skipped %d frames", drop)
|
||||||
|
}
|
||||||
|
lastSend = now
|
||||||
|
frame := frames[frameCount]
|
||||||
|
frameCount++
|
||||||
|
frameSent++
|
||||||
|
return frame, nil
|
||||||
|
})
|
||||||
|
broadcaster := NewBroadcaster(src, nil)
|
||||||
|
var done uint32
|
||||||
|
duration := time.Second * 3
|
||||||
|
fpsChan := make(chan []float64)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
go func() {
|
||||||
|
reader := broadcaster.NewReader(false)
|
||||||
|
count := 0
|
||||||
|
lastFrameCount := -1
|
||||||
|
droppedFrames := 0
|
||||||
|
wg.Done()
|
||||||
|
wg.Wait()
|
||||||
|
for atomic.LoadUint32(&done) == 0 {
|
||||||
|
if pauseCond.dst && count == 30 {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
frame, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
rgba := frame.(*image.RGBA)
|
||||||
|
var frameCount int
|
||||||
|
frameCount |= int(rgba.Pix[0]) << 24
|
||||||
|
frameCount |= int(rgba.Pix[1]) << 16
|
||||||
|
frameCount |= int(rgba.Pix[2]) << 8
|
||||||
|
frameCount |= int(rgba.Pix[3])
|
||||||
|
|
||||||
|
droppedFrames += (frameCount - lastFrameCount - 1)
|
||||||
|
lastFrameCount = frameCount
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
fps := float64(count) / duration.Seconds()
|
||||||
|
if fps < pauseCond.expectedFPS-2 || fps > pauseCond.expectedFPS+2 {
|
||||||
|
t.Fatal("Unexpected average FPS")
|
||||||
|
}
|
||||||
|
|
||||||
|
droppedFramesPerSecond := float64(droppedFrames) / duration.Seconds()
|
||||||
|
if droppedFramesPerSecond < pauseCond.expectedDrop-2 || droppedFramesPerSecond > pauseCond.expectedDrop+2 {
|
||||||
|
t.Fatal("Unexpected drop count")
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsChan <- []float64{fps, droppedFramesPerSecond, float64(lastFrameCount)}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(duration)
|
||||||
|
atomic.StoreUint32(&done, 1)
|
||||||
|
|
||||||
|
var fpsAvg float64
|
||||||
|
var droppedFramesPerSecondAvg float64
|
||||||
|
var lastFrameCountAvg float64
|
||||||
|
var count int
|
||||||
|
for metric := range fpsChan {
|
||||||
|
fps, droppedFramesPerSecond, lastFrameCount := metric[0], metric[1], metric[2]
|
||||||
|
fpsAvg += fps
|
||||||
|
droppedFramesPerSecondAvg += droppedFramesPerSecond
|
||||||
|
lastFrameCountAvg += lastFrameCount
|
||||||
|
count++
|
||||||
|
if count == n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Average FPS :", fpsAvg/float64(n))
|
||||||
|
t.Log("Average dropped frames per second:", droppedFramesPerSecondAvg/float64(n))
|
||||||
|
t.Log("Last frame count (src) :", frameCount)
|
||||||
|
t.Log("Average last frame count (dst) :", lastFrameCountAvg/float64(n))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,7 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
// BoolConstraint is an interface to represent bool value constraint.
|
// BoolConstraint is an interface to represent bool value constraint.
|
||||||
type BoolConstraint interface {
|
type BoolConstraint interface {
|
||||||
Compare(bool) (float64, bool)
|
Compare(bool) (float64, bool)
|
||||||
@@ -20,6 +22,11 @@ func (b BoolExact) Compare(o bool) (float64, bool) {
|
|||||||
// Value implements BoolConstraint.
|
// Value implements BoolConstraint.
|
||||||
func (b BoolExact) Value() bool { return bool(b) }
|
func (b BoolExact) Value() bool { return bool(b) }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (b BoolExact) String() string {
|
||||||
|
return fmt.Sprintf("%t (exact)", b)
|
||||||
|
}
|
||||||
|
|
||||||
// Bool specifies ideal bool value.
|
// Bool specifies ideal bool value.
|
||||||
type Bool BoolExact
|
type Bool BoolExact
|
||||||
|
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +25,11 @@ func (d Duration) Compare(a time.Duration) (float64, bool) {
|
|||||||
// Value implements DurationConstraint.
|
// Value implements DurationConstraint.
|
||||||
func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true }
|
func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (d Duration) String() string {
|
||||||
|
return fmt.Sprintf("%v (ideal)", time.Duration(d))
|
||||||
|
}
|
||||||
|
|
||||||
// DurationExact specifies exact duration value.
|
// DurationExact specifies exact duration value.
|
||||||
type DurationExact time.Duration
|
type DurationExact time.Duration
|
||||||
|
|
||||||
@@ -37,6 +44,11 @@ func (d DurationExact) Compare(a time.Duration) (float64, bool) {
|
|||||||
// Value implements DurationConstraint.
|
// Value implements DurationConstraint.
|
||||||
func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true }
|
func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (d DurationExact) String() string {
|
||||||
|
return fmt.Sprintf("%v (exact)", time.Duration(d))
|
||||||
|
}
|
||||||
|
|
||||||
// DurationOneOf specifies list of expected duration values.
|
// DurationOneOf specifies list of expected duration values.
|
||||||
type DurationOneOf []time.Duration
|
type DurationOneOf []time.Duration
|
||||||
|
|
||||||
@@ -53,6 +65,16 @@ func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
|
|||||||
// Value implements DurationConstraint.
|
// Value implements DurationConstraint.
|
||||||
func (DurationOneOf) Value() (time.Duration, bool) { return 0, false }
|
func (DurationOneOf) Value() (time.Duration, bool) { return 0, false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (d DurationOneOf) String() string {
|
||||||
|
var opts []string
|
||||||
|
for _, v := range d {
|
||||||
|
opts = append(opts, fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
|
||||||
|
}
|
||||||
|
|
||||||
// DurationRanged specifies range of expected duration value.
|
// DurationRanged specifies range of expected duration value.
|
||||||
// If Ideal is non-zero, closest value to Ideal takes priority.
|
// If Ideal is non-zero, closest value to Ideal takes priority.
|
||||||
type DurationRanged struct {
|
type DurationRanged struct {
|
||||||
@@ -96,3 +118,8 @@ func (d DurationRanged) Compare(a time.Duration) (float64, bool) {
|
|||||||
|
|
||||||
// Value implements DurationConstraint.
|
// Value implements DurationConstraint.
|
||||||
func (DurationRanged) Value() (time.Duration, bool) { return 0, false }
|
func (DurationRanged) Value() (time.Duration, bool) { return 0, false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (d DurationRanged) String() string {
|
||||||
|
return fmt.Sprintf("%s - %s (range), %s (ideal)", d.Min, d.Max, d.Ideal)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FloatConstraint is an interface to represent float value constraint.
|
// FloatConstraint is an interface to represent float value constraint.
|
||||||
@@ -22,6 +24,11 @@ func (f Float) Compare(a float32) (float64, bool) {
|
|||||||
// Value implements FloatConstraint.
|
// Value implements FloatConstraint.
|
||||||
func (f Float) Value() (float32, bool) { return float32(f), true }
|
func (f Float) Value() (float32, bool) { return float32(f), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f Float) String() string {
|
||||||
|
return fmt.Sprintf("%.2f (ideal)", f)
|
||||||
|
}
|
||||||
|
|
||||||
// FloatExact specifies exact float value.
|
// FloatExact specifies exact float value.
|
||||||
type FloatExact float32
|
type FloatExact float32
|
||||||
|
|
||||||
@@ -36,6 +43,11 @@ func (f FloatExact) Compare(a float32) (float64, bool) {
|
|||||||
// Value implements FloatConstraint.
|
// Value implements FloatConstraint.
|
||||||
func (f FloatExact) Value() (float32, bool) { return float32(f), true }
|
func (f FloatExact) Value() (float32, bool) { return float32(f), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f FloatExact) String() string {
|
||||||
|
return fmt.Sprintf("%.2f (exact)", f)
|
||||||
|
}
|
||||||
|
|
||||||
// FloatOneOf specifies list of expected float values.
|
// FloatOneOf specifies list of expected float values.
|
||||||
type FloatOneOf []float32
|
type FloatOneOf []float32
|
||||||
|
|
||||||
@@ -52,6 +64,16 @@ func (f FloatOneOf) Compare(a float32) (float64, bool) {
|
|||||||
// Value implements FloatConstraint.
|
// Value implements FloatConstraint.
|
||||||
func (FloatOneOf) Value() (float32, bool) { return 0, false }
|
func (FloatOneOf) Value() (float32, bool) { return 0, false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f FloatOneOf) String() string {
|
||||||
|
var opts []string
|
||||||
|
for _, v := range f {
|
||||||
|
opts = append(opts, fmt.Sprintf("%.2f", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
|
||||||
|
}
|
||||||
|
|
||||||
// FloatRanged specifies range of expected float value.
|
// FloatRanged specifies range of expected float value.
|
||||||
// If Ideal is non-zero, closest value to Ideal takes priority.
|
// If Ideal is non-zero, closest value to Ideal takes priority.
|
||||||
type FloatRanged struct {
|
type FloatRanged struct {
|
||||||
@@ -95,3 +117,8 @@ func (f FloatRanged) Compare(a float32) (float64, bool) {
|
|||||||
|
|
||||||
// Value implements FloatConstraint.
|
// Value implements FloatConstraint.
|
||||||
func (FloatRanged) Value() (float32, bool) { return 0, false }
|
func (FloatRanged) Value() (float32, bool) { return 0, false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f FloatRanged) String() string {
|
||||||
|
return fmt.Sprintf("%.2f - %.2f (range), %.2f (ideal)", f.Min, f.Max, f.Ideal)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FrameFormatConstraint is an interface to represent frame format constraint.
|
// FrameFormatConstraint is an interface to represent frame format constraint.
|
||||||
@@ -25,6 +27,11 @@ func (f FrameFormat) Compare(a frame.Format) (float64, bool) {
|
|||||||
// Value implements FrameFormatConstraint.
|
// Value implements FrameFormatConstraint.
|
||||||
func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true }
|
func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f FrameFormat) String() string {
|
||||||
|
return fmt.Sprintf("%s (ideal)", frame.Format(f))
|
||||||
|
}
|
||||||
|
|
||||||
// FrameFormatExact specifies exact frame format.
|
// FrameFormatExact specifies exact frame format.
|
||||||
type FrameFormatExact frame.Format
|
type FrameFormatExact frame.Format
|
||||||
|
|
||||||
@@ -39,6 +46,11 @@ func (f FrameFormatExact) Compare(a frame.Format) (float64, bool) {
|
|||||||
// Value implements FrameFormatConstraint.
|
// Value implements FrameFormatConstraint.
|
||||||
func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true }
|
func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f FrameFormatExact) String() string {
|
||||||
|
return fmt.Sprintf("%s (exact)", frame.Format(f))
|
||||||
|
}
|
||||||
|
|
||||||
// FrameFormatOneOf specifies list of expected frame format.
|
// FrameFormatOneOf specifies list of expected frame format.
|
||||||
type FrameFormatOneOf []frame.Format
|
type FrameFormatOneOf []frame.Format
|
||||||
|
|
||||||
@@ -54,3 +66,13 @@ func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) {
|
|||||||
|
|
||||||
// Value implements FrameFormatConstraint.
|
// Value implements FrameFormatConstraint.
|
||||||
func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false }
|
func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f FrameFormatOneOf) String() string {
|
||||||
|
var opts []string
|
||||||
|
for _, v := range f {
|
||||||
|
opts = append(opts, fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IntConstraint is an interface to represent integer value constraint.
|
// IntConstraint is an interface to represent integer value constraint.
|
||||||
@@ -22,6 +24,11 @@ func (i Int) Compare(a int) (float64, bool) {
|
|||||||
// Value implements IntConstraint.
|
// Value implements IntConstraint.
|
||||||
func (i Int) Value() (int, bool) { return int(i), true }
|
func (i Int) Value() (int, bool) { return int(i), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (i Int) String() string {
|
||||||
|
return fmt.Sprintf("%d (ideal)", i)
|
||||||
|
}
|
||||||
|
|
||||||
// IntExact specifies exact int value.
|
// IntExact specifies exact int value.
|
||||||
type IntExact int
|
type IntExact int
|
||||||
|
|
||||||
@@ -33,6 +40,11 @@ func (i IntExact) Compare(a int) (float64, bool) {
|
|||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (i IntExact) String() string {
|
||||||
|
return fmt.Sprintf("%d (exact)", i)
|
||||||
|
}
|
||||||
|
|
||||||
// Value implements IntConstraint.
|
// Value implements IntConstraint.
|
||||||
func (i IntExact) Value() (int, bool) { return int(i), true }
|
func (i IntExact) Value() (int, bool) { return int(i), true }
|
||||||
|
|
||||||
@@ -52,6 +64,16 @@ func (i IntOneOf) Compare(a int) (float64, bool) {
|
|||||||
// Value implements IntConstraint.
|
// Value implements IntConstraint.
|
||||||
func (IntOneOf) Value() (int, bool) { return 0, false }
|
func (IntOneOf) Value() (int, bool) { return 0, false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (i IntOneOf) String() string {
|
||||||
|
var opts []string
|
||||||
|
for _, v := range i {
|
||||||
|
opts = append(opts, fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (one of values)", strings.Join(opts, ","))
|
||||||
|
}
|
||||||
|
|
||||||
// IntRanged specifies range of expected int value.
|
// IntRanged specifies range of expected int value.
|
||||||
// If Ideal is non-zero, closest value to Ideal takes priority.
|
// If Ideal is non-zero, closest value to Ideal takes priority.
|
||||||
type IntRanged struct {
|
type IntRanged struct {
|
||||||
@@ -95,3 +117,8 @@ func (i IntRanged) Compare(a int) (float64, bool) {
|
|||||||
|
|
||||||
// Value implements IntConstraint.
|
// Value implements IntConstraint.
|
||||||
func (IntRanged) Value() (int, bool) { return 0, false }
|
func (IntRanged) Value() (int, bool) { return 0, false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (i IntRanged) String() string {
|
||||||
|
return fmt.Sprintf("%d - %d (range), %d (ideal)", i.Min, i.Max, i.Ideal)
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
@@ -15,6 +17,10 @@ type MediaConstraints struct {
|
|||||||
AudioConstraints
|
AudioConstraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MediaConstraints) String() string {
|
||||||
|
return prettifyStruct(m)
|
||||||
|
}
|
||||||
|
|
||||||
// Media stores single set of media propaties.
|
// Media stores single set of media propaties.
|
||||||
type Media struct {
|
type Media struct {
|
||||||
DeviceID string
|
DeviceID string
|
||||||
@@ -22,6 +28,33 @@ type Media struct {
|
|||||||
Audio
|
Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Media) String() string {
|
||||||
|
return prettifyStruct(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettifyStruct(i interface{}) string {
|
||||||
|
var rows []string
|
||||||
|
var addRows func(int, reflect.Value)
|
||||||
|
addRows = func(level int, obj reflect.Value) {
|
||||||
|
typeOf := obj.Type()
|
||||||
|
for i := 0; i < obj.NumField(); i++ {
|
||||||
|
field := typeOf.Field(i)
|
||||||
|
value := obj.Field(i)
|
||||||
|
|
||||||
|
padding := strings.Repeat(" ", level)
|
||||||
|
if value.Kind() == reflect.Struct {
|
||||||
|
rows = append(rows, fmt.Sprintf("%s%v:", padding, field.Name))
|
||||||
|
addRows(level+1, value)
|
||||||
|
} else {
|
||||||
|
rows = append(rows, fmt.Sprintf("%s%v: %v", padding, field.Name, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows(0, reflect.ValueOf(i).Elem())
|
||||||
|
return strings.Join(rows, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
// setterFn is a callback function to set value from fieldB to fieldA
|
// setterFn is a callback function to set value from fieldB to fieldA
|
||||||
type setterFn func(fieldA, fieldB reflect.Value)
|
type setterFn func(fieldA, fieldB reflect.Value)
|
||||||
|
|
||||||
|
@@ -309,3 +309,60 @@ func TestMergeConstraintsNested(t *testing.T) {
|
|||||||
t.Error("expected a.Width to be 100, but got 0")
|
t.Error("expected a.Width to be 100, but got 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
t.Run("IdealValues", func(t *testing.T) {
|
||||||
|
t.Log("\n", &MediaConstraints{
|
||||||
|
DeviceID: String("one"),
|
||||||
|
VideoConstraints: VideoConstraints{
|
||||||
|
Width: Int(1920),
|
||||||
|
FrameRate: Float(30.0),
|
||||||
|
FrameFormat: FrameFormat(frame.FormatI420),
|
||||||
|
},
|
||||||
|
AudioConstraints: AudioConstraints{
|
||||||
|
Latency: Duration(time.Millisecond * 20),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ExactValues", func(t *testing.T) {
|
||||||
|
t.Log("\n", &MediaConstraints{
|
||||||
|
DeviceID: StringExact("one"),
|
||||||
|
VideoConstraints: VideoConstraints{
|
||||||
|
Width: IntExact(1920),
|
||||||
|
FrameRate: FloatExact(30.0),
|
||||||
|
FrameFormat: FrameFormatExact(frame.FormatI420),
|
||||||
|
},
|
||||||
|
AudioConstraints: AudioConstraints{
|
||||||
|
Latency: DurationExact(time.Millisecond * 20),
|
||||||
|
IsBigEndian: BoolExact(true),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OneOfValues", func(t *testing.T) {
|
||||||
|
t.Log("\n", &MediaConstraints{
|
||||||
|
DeviceID: StringOneOf{"one", "two"},
|
||||||
|
VideoConstraints: VideoConstraints{
|
||||||
|
Width: IntOneOf{1920, 1080},
|
||||||
|
FrameRate: FloatOneOf{30.0, 60.1234},
|
||||||
|
FrameFormat: FrameFormatOneOf{frame.FormatI420, frame.FormatI444},
|
||||||
|
},
|
||||||
|
AudioConstraints: AudioConstraints{
|
||||||
|
Latency: DurationOneOf{time.Millisecond * 20, time.Millisecond * 40},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RangedValues", func(t *testing.T) {
|
||||||
|
t.Log("\n", &MediaConstraints{
|
||||||
|
VideoConstraints: VideoConstraints{
|
||||||
|
Width: &IntRanged{Min: 1080, Max: 1920, Ideal: 1500},
|
||||||
|
FrameRate: &FloatRanged{Min: 30.123, Max: 60.12321312, Ideal: 45.12312312},
|
||||||
|
},
|
||||||
|
AudioConstraints: AudioConstraints{
|
||||||
|
Latency: &DurationRanged{Min: time.Millisecond * 20, Max: time.Millisecond * 40, Ideal: time.Millisecond * 30},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
package prop
|
package prop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// StringConstraint is an interface to represent string constraint.
|
// StringConstraint is an interface to represent string constraint.
|
||||||
type StringConstraint interface {
|
type StringConstraint interface {
|
||||||
Compare(string) (float64, bool)
|
Compare(string) (float64, bool)
|
||||||
@@ -21,6 +26,11 @@ func (f String) Compare(a string) (float64, bool) {
|
|||||||
// Value implements StringConstraint.
|
// Value implements StringConstraint.
|
||||||
func (f String) Value() (string, bool) { return string(f), true }
|
func (f String) Value() (string, bool) { return string(f), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f String) String() string {
|
||||||
|
return fmt.Sprintf("%s (ideal)", string(f))
|
||||||
|
}
|
||||||
|
|
||||||
// StringExact specifies exact string.
|
// StringExact specifies exact string.
|
||||||
type StringExact string
|
type StringExact string
|
||||||
|
|
||||||
@@ -35,6 +45,11 @@ func (f StringExact) Compare(a string) (float64, bool) {
|
|||||||
// Value implements StringConstraint.
|
// Value implements StringConstraint.
|
||||||
func (f StringExact) Value() (string, bool) { return string(f), true }
|
func (f StringExact) Value() (string, bool) { return string(f), true }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f StringExact) String() string {
|
||||||
|
return fmt.Sprintf("%s (exact)", string(f))
|
||||||
|
}
|
||||||
|
|
||||||
// StringOneOf specifies list of expected string.
|
// StringOneOf specifies list of expected string.
|
||||||
type StringOneOf []string
|
type StringOneOf []string
|
||||||
|
|
||||||
@@ -50,3 +65,8 @@ func (f StringOneOf) Compare(a string) (float64, bool) {
|
|||||||
|
|
||||||
// Value implements StringConstraint.
|
// Value implements StringConstraint.
|
||||||
func (StringOneOf) Value() (string, bool) { return "", false }
|
func (StringOneOf) Value() (string, bool) { return "", false }
|
||||||
|
|
||||||
|
// String implements Stringify
|
||||||
|
func (f StringOneOf) String() string {
|
||||||
|
return fmt.Sprintf("%s (one of values)", strings.Join([]string(f), ","))
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user