mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-26 20:41:46 +08:00
Compare commits
3 Commits
427feefb33
...
1158ae5212
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1158ae5212 | ||
![]() |
6047a32ea0 | ||
![]() |
60bf158757 |
4
go.mod
4
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/pion/mediadevices
|
module github.com/pion/mediadevices
|
||||||
|
|
||||||
go 1.21
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blackjack/webcam v0.6.1
|
github.com/blackjack/webcam v0.6.1
|
||||||
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/pion/rtp v1.8.19
|
github.com/pion/rtp v1.8.19
|
||||||
github.com/pion/webrtc/v4 v4.1.2
|
github.com/pion/webrtc/v4 v4.1.2
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/image v0.23.0
|
golang.org/x/image v0.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
4
go.sum
4
go.sum
@@ -62,8 +62,8 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
|||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
48
pkg/codec/bitrate_tracker.go
Normal file
48
pkg/codec/bitrate_tracker.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BitrateTracker struct {
|
||||||
|
windowSize time.Duration
|
||||||
|
buffer []int
|
||||||
|
times []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBitrateTracker(windowSize time.Duration) *BitrateTracker {
|
||||||
|
return &BitrateTracker{
|
||||||
|
windowSize: windowSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BitrateTracker) AddFrame(sizeBytes int, timestamp time.Time) {
|
||||||
|
bt.buffer = append(bt.buffer, sizeBytes)
|
||||||
|
bt.times = append(bt.times, timestamp)
|
||||||
|
|
||||||
|
// Remove old entries outside the window
|
||||||
|
cutoff := timestamp.Add(-bt.windowSize)
|
||||||
|
i := 0
|
||||||
|
for ; i < len(bt.times); i++ {
|
||||||
|
if bt.times[i].After(cutoff) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bt.buffer = bt.buffer[i:]
|
||||||
|
bt.times = bt.times[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BitrateTracker) GetBitrate() float64 {
|
||||||
|
if len(bt.times) < 2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
totalBytes := 0
|
||||||
|
for _, b := range bt.buffer {
|
||||||
|
totalBytes += b
|
||||||
|
}
|
||||||
|
duration := bt.times[len(bt.times)-1].Sub(bt.times[0]).Seconds()
|
||||||
|
if duration <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(totalBytes*8) / duration // bits per second
|
||||||
|
}
|
19
pkg/codec/bitrate_tracker_test.go
Normal file
19
pkg/codec/bitrate_tracker_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBitrateTracker(t *testing.T) {
|
||||||
|
packetSize := 1000
|
||||||
|
bt := NewBitrateTracker(time.Second)
|
||||||
|
bt.AddFrame(packetSize, time.Now())
|
||||||
|
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*100))
|
||||||
|
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*999))
|
||||||
|
eps := float64(packetSize*8) / 10
|
||||||
|
if got, want := bt.GetBitrate(), float64(packetSize*8)*3; math.Abs(got-want) > eps {
|
||||||
|
t.Fatalf("GetBitrate() = %v, want %v (|diff| <= %v)", got, want, eps)
|
||||||
|
}
|
||||||
|
}
|
@@ -179,6 +179,12 @@ type BitRateController interface {
|
|||||||
SetBitRate(int) error
|
SetBitRate(int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QPController interface {
|
||||||
|
EncoderController
|
||||||
|
// DynamicQPControl adjusts the QP of the encoder based on the current and target bitrate
|
||||||
|
DynamicQPControl(currentBitrate int, targetBitrate int) error
|
||||||
|
}
|
||||||
|
|
||||||
// BaseParams represents an codec's encoding properties
|
// BaseParams represents an codec's encoding properties
|
||||||
type BaseParams struct {
|
type BaseParams struct {
|
||||||
// Target bitrate in bps.
|
// Target bitrate in bps.
|
||||||
|
@@ -54,6 +54,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -81,6 +82,12 @@ type encoder struct {
|
|||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
kRateControlThreshold = 0.15
|
||||||
|
kMinQuantizer = 20
|
||||||
|
kMaxQuantizer = 63
|
||||||
|
)
|
||||||
|
|
||||||
// VP8Params is codec specific paramaters
|
// VP8Params is codec specific paramaters
|
||||||
type VP8Params struct {
|
type VP8Params struct {
|
||||||
Params
|
Params
|
||||||
@@ -254,6 +261,10 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ec := C.vpx_codec_enc_config_set(e.codec, e.cfg); ec != 0 {
|
||||||
|
return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", ec)
|
||||||
|
}
|
||||||
|
|
||||||
duration := t.Sub(e.tLastFrame).Microseconds()
|
duration := t.Sub(e.tLastFrame).Microseconds()
|
||||||
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
||||||
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
||||||
@@ -322,6 +333,24 @@ func (e *encoder) SetBitRate(bitrate int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *encoder) DynamicQPControl(currentBitrate int, targetBitrate int) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
bitrateDiff := math.Abs(float64(currentBitrate - targetBitrate))
|
||||||
|
if bitrateDiff <= float64(currentBitrate)*kRateControlThreshold {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currentMax := e.cfg.rc_max_quantizer
|
||||||
|
|
||||||
|
if targetBitrate < currentBitrate {
|
||||||
|
e.cfg.rc_max_quantizer = min(currentMax+1, kMaxQuantizer)
|
||||||
|
} else {
|
||||||
|
e.cfg.rc_max_quantizer = max(currentMax-1, kMinQuantizer)
|
||||||
|
}
|
||||||
|
e.cfg.rc_min_quantizer = e.cfg.rc_max_quantizer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoder) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,6 +15,7 @@ import (
|
|||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncoder(t *testing.T) {
|
func TestEncoder(t *testing.T) {
|
||||||
@@ -360,3 +363,65 @@ func TestEncoderFrameMonotonic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVP8DynamicQPControl(t *testing.T) {
|
||||||
|
t.Run("VP8", func(t *testing.T) {
|
||||||
|
p, err := NewVP8Params()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p.LagInFrames = 0 // Disable frame lag buffering for real-time encoding
|
||||||
|
p.RateControlEndUsage = RateControlCBR
|
||||||
|
totalFrames := 100
|
||||||
|
frameRate := 10
|
||||||
|
initialWidth, initialHeight := 800, 600
|
||||||
|
var cnt uint32
|
||||||
|
|
||||||
|
r, err := p.BuildVideoEncoder(
|
||||||
|
video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
i := atomic.AddUint32(&cnt, 1)
|
||||||
|
if i == uint32(totalFrames+1) {
|
||||||
|
return nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
img := image.NewYCbCr(image.Rect(0, 0, initialWidth, initialHeight), image.YCbCrSubsampleRatio420)
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
for i := range img.Y {
|
||||||
|
img.Y[i] = uint8(r.Intn(256))
|
||||||
|
}
|
||||||
|
for i := range img.Cb {
|
||||||
|
img.Cb[i] = uint8(r.Intn(256))
|
||||||
|
}
|
||||||
|
for i := range img.Cr {
|
||||||
|
img.Cr[i] = uint8(r.Intn(256))
|
||||||
|
}
|
||||||
|
return img, func() {}, nil
|
||||||
|
}),
|
||||||
|
prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: initialWidth,
|
||||||
|
Height: initialHeight,
|
||||||
|
FrameRate: float32(frameRate),
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
initialBitrate := 100
|
||||||
|
currentBitrate := initialBitrate
|
||||||
|
targetBitrate := 300
|
||||||
|
for i := 0; i < totalFrames; i++ {
|
||||||
|
r.Controller().(codec.KeyFrameController).ForceKeyFrame()
|
||||||
|
r.Controller().(codec.QPController).DynamicQPControl(currentBitrate, targetBitrate)
|
||||||
|
data, rel, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rel()
|
||||||
|
encodedSize := len(data)
|
||||||
|
currentBitrate = encodedSize * 8 / 1000 / frameRate
|
||||||
|
}
|
||||||
|
assert.Less(t, math.Abs(float64(targetBitrate-currentBitrate)), math.Abs(float64(initialBitrate-currentBitrate)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user