[VPX] vpx dynamic encoding (#647)

* Add vp8 decoder and dynamic vp8 decoding

* Add QPController

* change parameters into const

* move decoder into another PR

* use explicit parameter name
This commit is contained in:
Leo (Lei) Kang
2025-09-04 14:33:07 -07:00
committed by GitHub
parent 60bf158757
commit 6047a32ea0
3 changed files with 100 additions and 0 deletions

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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)))
})
}