mirror of
https://github.com/pion/mediadevices.git
synced 2026-04-22 15:57:27 +08:00
Initialize SVT-AV1 codec
This commit is contained in:
@@ -27,6 +27,7 @@ jobs:
|
||||
sudo apt-get update -qq \
|
||||
&& sudo apt-get install --no-install-recommends -y \
|
||||
libopus-dev \
|
||||
libsvtav1enc-dev \
|
||||
libva-dev \
|
||||
libvpx-dev \
|
||||
libx11-dev \
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#include <EbSvtAv1Enc.h>
|
||||
#include <EbSvtAv1ErrorCodes.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define ERR_INIT_ENC_HANDLER 1
|
||||
#define ERR_SET_ENC_PARAM 2
|
||||
#define ERR_ENC_INIT 3
|
||||
|
||||
typedef struct Encoder {
|
||||
EbSvtAv1EncConfiguration *param;
|
||||
EbComponentType *handle;
|
||||
} Encoder;
|
||||
|
||||
typedef struct Buffer {
|
||||
unsigned char *data;
|
||||
int len;
|
||||
} Buffer;
|
||||
|
||||
int enc_new(Encoder **e) {
|
||||
EbErrorType sret;
|
||||
*e = malloc(sizeof(Encoder));
|
||||
(*e)->param = malloc(sizeof(EbSvtAv1EncConfiguration));
|
||||
|
||||
sret = svt_av1_enc_init_handle(&(*e)->handle, *e, (*e)->param);
|
||||
if (sret != EB_ErrorNone) {
|
||||
free((*e)->param);
|
||||
free(*e);
|
||||
return ERR_INIT_ENC_HANDLER;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int enc_init(Encoder *e) {
|
||||
EbErrorType sret;
|
||||
|
||||
sret = svt_av1_enc_set_parameter(e->handle, e->param);
|
||||
if (sret != EB_ErrorNone) {
|
||||
return ERR_SET_ENC_PARAM;
|
||||
}
|
||||
|
||||
sret = svt_av1_enc_init(e->handle);
|
||||
if (sret != EB_ErrorNone) {
|
||||
return ERR_ENC_INIT;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int enc_apply_param(Encoder *e) {
|
||||
EbErrorType sret = svt_av1_enc_set_parameter(e->handle, e->param);
|
||||
if (sret != EB_ErrorNone) {
|
||||
return ERR_SET_ENC_PARAM;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsigned char dummy[] = {0, 1, 2, 3};
|
||||
|
||||
int enc_encode(Encoder *e, Buffer *out, uint8_t *y, uint8_t *cb, uint8_t *cr) {
|
||||
// TODO: implement
|
||||
out->data = dummy;
|
||||
out->len = 4;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int enc_close(Encoder *e) {
|
||||
free(e->param);
|
||||
free(e);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package svtav1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownErrorCode = errors.New("unknown error code")
|
||||
ErrInitEncHandler = errors.New("failed to initialize encoder handler")
|
||||
ErrSetEncParam = errors.New("failed to set encoder parameters")
|
||||
ErrEncInit = errors.New("failed to initialize encoder")
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package svtav1
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
// Params stores libx264 specific encoding parameters.
|
||||
type Params struct {
|
||||
codec.BaseParams
|
||||
|
||||
// Preset configuration number of SVT-AV1
|
||||
// 1-3: extremely high efficiency but heavy
|
||||
// 4-6: a balance of efficiency and reasonable compute time
|
||||
// 7-13: real-time encoding
|
||||
Preset int
|
||||
}
|
||||
|
||||
// NewParams returns default x264 codec specific parameters.
|
||||
func NewParams() (Params, error) {
|
||||
return Params{
|
||||
BaseParams: codec.BaseParams{
|
||||
KeyFrameInterval: 60,
|
||||
},
|
||||
Preset: 9,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RTPCodec represents the codec metadata
|
||||
func (p *Params) RTPCodec() *codec.RTPCodec {
|
||||
return codec.NewRTPAV1Codec(90000)
|
||||
}
|
||||
|
||||
// BuildVideoEncoder builds x264 encoder with given params
|
||||
func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
|
||||
return newEncoder(r, property, *p)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Package svtav1 implements AV1 encoder.
|
||||
// This package requires libSvtAv1Enc headers and libraries to be built.
|
||||
package svtav1
|
||||
|
||||
// #cgo pkg-config: SvtAv1Enc
|
||||
// #include "bridge.h"
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
type encoder struct {
|
||||
engine *C.Encoder
|
||||
r video.Reader
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
||||
var enc *C.Encoder
|
||||
|
||||
if params.KeyFrameInterval == 0 {
|
||||
params.KeyFrameInterval = 60
|
||||
}
|
||||
if p.FrameRate == 0 {
|
||||
p.FrameRate = 30
|
||||
}
|
||||
|
||||
if err := errFromC(C.enc_new(&enc)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc.param.source_width = C.uint32_t(p.Width)
|
||||
enc.param.source_height = C.uint32_t(p.Height)
|
||||
enc.param.encoder_bit_depth = 8
|
||||
enc.param.encoder_color_format = C.EB_YUV420
|
||||
enc.param.profile = C.MAIN_PROFILE
|
||||
enc.param.level = 0 // auto
|
||||
enc.param.hierarchical_levels = 0 // auto
|
||||
enc.param.enc_mode = C.int8_t(params.Preset)
|
||||
enc.param.tier = 0 // main
|
||||
enc.param.rate_control_mode = C.SVT_AV1_RC_MODE_CBR
|
||||
enc.param.pred_structure = 1 // LOW_DELAY
|
||||
enc.param.qp = 50 // default
|
||||
enc.param.target_bit_rate = C.uint32_t(params.BitRate)
|
||||
enc.param.intra_period_length = -2 // auto
|
||||
enc.param.frame_rate_numerator = C.uint32_t(p.FrameRate * 1000)
|
||||
enc.param.frame_rate_denominator = 1000
|
||||
enc.param.enable_tpl_la = 0 // LOW_DELAY requires no TPL
|
||||
enc.param.max_qp_allowed = 63
|
||||
enc.param.min_qp_allowed = 0
|
||||
enc.param.intra_refresh_type = C.SVT_AV1_KF_REFRESH
|
||||
|
||||
if err := errFromC(C.enc_init(enc)); err != nil {
|
||||
_ = C.enc_close(enc)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := encoder{
|
||||
engine: enc,
|
||||
r: video.ToI420(r),
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func errFromC(ret C.int) error {
|
||||
switch ret {
|
||||
case 0:
|
||||
return nil
|
||||
case C.ERR_INIT_ENC_HANDLER:
|
||||
return ErrInitEncHandler
|
||||
case C.ERR_SET_ENC_PARAM:
|
||||
return ErrSetEncParam
|
||||
case C.ERR_ENC_INIT:
|
||||
return ErrEncInit
|
||||
default:
|
||||
return ErrUnknownErrorCode
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) Read() ([]byte, func(), error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
yuvImg := img.(*image.YCbCr)
|
||||
|
||||
var buf C.Buffer
|
||||
ret := C.enc_encode(
|
||||
e.engine,
|
||||
&buf,
|
||||
(*C.uchar)(&yuvImg.Y[0]),
|
||||
(*C.uchar)(&yuvImg.Cb[0]),
|
||||
(*C.uchar)(&yuvImg.Cr[0]),
|
||||
)
|
||||
if err := errFromC(ret); err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
encoded := C.GoBytes(unsafe.Pointer(buf.data), buf.len)
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
// TODO: Implement bit rate control
|
||||
//var _ codec.BitRateController = (*encoder)(nil)
|
||||
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.engine.param.force_key_frames = 1
|
||||
|
||||
if err := errFromC(C.enc_apply_param(e.engine)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) SetBitRate(bitrate int) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.engine.param.target_bit_rate = C.uint32_t(bitrate)
|
||||
|
||||
if err := errFromC(C.enc_apply_param(e.engine)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := errFromC(C.enc_close(e.engine)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.closed = true
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package svtav1
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func getTestVideoEncoder() (codec.ReadCloser, error) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.BitRate = 200000
|
||||
enc, err := p.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
return image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
), nil, nil
|
||||
}), prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.BitRate = 200000
|
||||
codectest.VideoEncoderSimpleReadTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.BitRate = 200000
|
||||
codectest.VideoEncoderCloseTwiceTest(t, &p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.BitRate = 200000
|
||||
codectest.VideoEncoderReadAfterCloseTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoErrorOnForceKeyFrame(t *testing.T) {
|
||||
enc, err := getTestVideoEncoder()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kfc, ok := enc.Controller().(codec.KeyFrameController)
|
||||
if !ok {
|
||||
t.Fatal("Failed to get KeyFrameController")
|
||||
}
|
||||
if err := kfc.ForceKeyFrame(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, rel, err := enc.Read() // try to read the encoded frame
|
||||
rel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoErrorOnSetBitRate(t *testing.T) {
|
||||
enc, err := getTestVideoEncoder()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
brc, ok := enc.Controller().(codec.BitRateController)
|
||||
if !ok {
|
||||
t.Fatal("Failed to get BitRateController")
|
||||
}
|
||||
if err := brc.SetBitRate(1000); err != nil { // 1000 bit/second is ridiculously low, but this is a testcase.
|
||||
t.Error(err)
|
||||
}
|
||||
_, rel, err := enc.Read() // try to read the encoded frame
|
||||
rel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user