diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go index 8493868..8521d8b 100644 --- a/pkg/codec/codec.go +++ b/pkg/codec/codec.go @@ -179,6 +179,12 @@ type BitRateController interface { 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 type BaseParams struct { // Target bitrate in bps. diff --git a/pkg/codec/vpx/vpx.go b/pkg/codec/vpx/vpx.go index 691d4ad..534c879 100644 --- a/pkg/codec/vpx/vpx.go +++ b/pkg/codec/vpx/vpx.go @@ -54,6 +54,7 @@ import ( "fmt" "image" "io" + "math" "sync" "time" "unsafe" @@ -81,6 +82,12 @@ type encoder struct { closed bool } +const ( + kRateControlThreshold = 0.15 + kMinQuantizer = 20 + kMaxQuantizer = 63 +) + // VP8Params is codec specific paramaters type VP8Params struct { 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) } + 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() // 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, @@ -322,6 +333,24 @@ func (e *encoder) SetBitRate(bitrate int) error { 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 { return e } diff --git a/pkg/codec/vpx/vpx_test.go b/pkg/codec/vpx/vpx_test.go index cf5007c..eb1b6bb 100644 --- a/pkg/codec/vpx/vpx_test.go +++ b/pkg/codec/vpx/vpx_test.go @@ -4,6 +4,8 @@ import ( "context" "image" "io" + "math" + "math/rand" "sync/atomic" "testing" "time" @@ -13,6 +15,7 @@ import ( "github.com/pion/mediadevices/pkg/frame" "github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/prop" + "github.com/stretchr/testify/assert" ) 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))) + }) +}