cloud-game/pkg/encoder/vpx/libvpx.go
sergystepanov 9c42f1cc59 Retune VP8/VP9 codecs
Add new params
cpuUsed: 0
tileColumns: 0
tune: screen

In VP9 it will use multithreaded encoding now.

Keyframe interval was not set properly wasting bitrate. By default. It
is changed to 120 frames instead of 5.
2026-06-11 21:44:33 +03:00

283 lines
7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package vpx
/*
#cgo pkg-config: vpx
#cgo st LDFLAGS: -l:libvpx.a
#include "vpx/vpx_encoder.h"
#include "vpx/vpx_image.h"
#include "vpx/vp8cx.h"
#include <stdlib.h>
#include <string.h>
#define VP8_FOURCC 0x30385056
#define VP9_FOURCC 0x30395056
#ifndef VP9E_TUNE_SCREEN
#define VP9E_TUNE_SCREEN 1
#endif
typedef struct VpxInterface {
const char *const name;
const uint32_t fourcc;
vpx_codec_iface_t *(*const codec_interface)();
} VpxInterface;
typedef struct FrameBuffer {
void *ptr;
int size;
} FrameBuffer;
vpx_codec_err_t call_vpx_codec_enc_config_default(const VpxInterface *encoder, vpx_codec_enc_cfg_t *cfg) {
return vpx_codec_enc_config_default(encoder->codec_interface(), cfg, 0);
}
vpx_codec_err_t call_vpx_codec_enc_init(vpx_codec_ctx_t *codec, const VpxInterface *encoder, vpx_codec_enc_cfg_t *cfg) {
return vpx_codec_enc_init(codec, encoder->codec_interface(), cfg, 0);
}
FrameBuffer get_frame_buffer(vpx_codec_ctx_t *codec, vpx_codec_iter_t *iter) {
// iter has set to NULL when after add new image
FrameBuffer fb = {NULL, 0};
const vpx_codec_cx_pkt_t *pkt = vpx_codec_get_cx_data(codec, iter);
if (pkt != NULL && pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
fb.ptr = pkt->data.frame.buf;
fb.size = pkt->data.frame.sz;
}
return fb;
}
vpx_codec_err_t set_vpx_cpu_used(vpx_codec_ctx_t *ctx, int value) {
return vpx_codec_control_(ctx, VP8E_SET_CPUUSED, value);
}
vpx_codec_err_t set_vp9_tile_columns(vpx_codec_ctx_t *ctx, int value) {
return vpx_codec_control_(ctx, VP9E_SET_TILE_COLUMNS, value);
}
vpx_codec_err_t set_vp9_tune_content(vpx_codec_ctx_t *ctx, int value) {
return vpx_codec_control_(ctx, VP9E_SET_TUNE_CONTENT, value);
}
vpx_codec_err_t set_vp9_row_mt(vpx_codec_ctx_t *ctx, int value) {
return vpx_codec_control_(ctx, VP9E_SET_ROW_MT, value);
}
vpx_codec_err_t set_vp8_auto_alt_ref(vpx_codec_ctx_t *ctx, int value) {
return vpx_codec_control_(ctx, VP8E_SET_ENABLEAUTOALTREF, value);
}
vpx_codec_err_t set_vp8_noise_sensitivity(vpx_codec_ctx_t *ctx, int value) {
return vpx_codec_control_(ctx, VP8E_SET_NOISE_SENSITIVITY, value);
}
const VpxInterface vpx_encoders[] = {
{ "vp8", VP8_FOURCC, &vpx_codec_vp8_cx },
{ "vp9", VP9_FOURCC, &vpx_codec_vp9_cx },
};
int vpx_img_plane_width(const vpx_image_t *img, int plane) {
if (plane > 0 && img->x_chroma_shift > 0)
return (img->d_w + 1) >> img->x_chroma_shift;
else
return img->d_w;
}
int vpx_img_plane_height(const vpx_image_t *img, int plane) {
if (plane > 0 && img->y_chroma_shift > 0)
return (img->d_h + 1) >> img->y_chroma_shift;
else
return img->d_h;
}
void vpx_img_read(vpx_image_t *dst, void *src) {
for (int plane = 0; plane < 3; ++plane) {
unsigned char *buf = dst->planes[plane];
const int stride = dst->stride[plane];
const int w = vpx_img_plane_width(dst, plane);
const int h = vpx_img_plane_height(dst, plane);
for (int y = 0; y < h; ++y) {
memcpy(buf, src, w);
buf += stride;
src += w;
}
}
}
*/
import "C"
import (
"fmt"
"runtime"
"unsafe"
)
type Vpx struct {
frameCount C.int
image C.vpx_image_t
codecCtx C.vpx_codec_ctx_t
kfi C.int
flipped bool
v int
}
func (vpx *Vpx) SetFlip(b bool) { vpx.flipped = b }
func autoThreads(configured, cpus int) int {
if configured != 0 {
return configured
}
reserve := cpus / 4
if reserve < 1 {
reserve = 1
}
if t := cpus - reserve; t > 0 {
return t
}
return 1
}
type Options struct {
// Target bandwidth to use for this stream, in kilobits per second.
Bitrate uint
// Force keyframe interval.
KeyframeInterval uint
// Speed/quality tradeoff (0=auto, 18 manual). Defaults to 0.
// Applies to both VP8 and VP9.
CpuUsed int
// Log2 of tile columns for VP9 frame-level parallelism (06). VP9 only.
TileColumns int
// Content tuning: "screen" for game/screen content. VP9 only.
Tune string
}
func NewEncoder(w, h int, th int, version int, opts *Options) (*Vpx, error) {
idx := 0
if version == 9 {
idx = 1
}
encoder := &C.vpx_encoders[idx]
if encoder == nil {
return nil, fmt.Errorf("couldn't get the encoder")
}
if opts == nil {
opts = &Options{
Bitrate: 1200,
KeyframeInterval: 120,
TileColumns: 2,
Tune: "screen",
}
}
vpx := Vpx{
frameCount: C.int(0),
kfi: C.int(opts.KeyframeInterval),
v: version,
}
if C.vpx_img_alloc(&vpx.image, C.VPX_IMG_FMT_I420, C.uint(w), C.uint(h), 1) == nil {
return nil, fmt.Errorf("vpx_img_alloc failed")
}
var cfg C.vpx_codec_enc_cfg_t
if C.call_vpx_codec_enc_config_default(encoder, &cfg) != 0 {
return nil, fmt.Errorf("failed to get default codec config")
}
cfg.g_w = C.uint(w)
cfg.g_h = C.uint(h)
cfg.g_threads = C.uint(autoThreads(th, runtime.NumCPU()))
cfg.g_lag_in_frames = 0
cfg.rc_target_bitrate = C.uint(opts.Bitrate)
cfg.g_error_resilient = 0
if C.call_vpx_codec_enc_init(&vpx.codecCtx, encoder, &cfg) != 0 {
return nil, fmt.Errorf("failed to initialize encoder")
}
// Speed/quality tradeoff: higher = faster, lower = better quality.
if opts.CpuUsed == 0 {
thr := autoThreads(th, runtime.NumCPU())
switch {
case thr <= 2:
opts.CpuUsed = 8
case thr <= 4:
opts.CpuUsed = 7
default:
opts.CpuUsed = 6
}
}
C.set_vpx_cpu_used(&vpx.codecCtx, C.int(opts.CpuUsed))
// VP9-specific tuning.
if version == 9 {
if opts.TileColumns == 0 {
thr := autoThreads(th, runtime.NumCPU())
if thr >= 4 {
opts.TileColumns = 2
} else if thr >= 2 {
opts.TileColumns = 1
}
}
if opts.TileColumns > 0 {
C.set_vp9_tile_columns(&vpx.codecCtx, C.int(opts.TileColumns))
}
if opts.Tune == "screen" {
C.set_vp9_tune_content(&vpx.codecCtx, C.VP9E_TUNE_SCREEN)
}
C.set_vp9_row_mt(&vpx.codecCtx, 1)
}
// VP8 real-time tuning: disable features that add latency.
if version == 8 {
// No alt-ref frames for zero-latency.
C.set_vp8_auto_alt_ref(&vpx.codecCtx, 0)
// Reduce noise sensitivity — game content is low-noise.
C.set_vp8_noise_sensitivity(&vpx.codecCtx, 0)
}
return &vpx, nil
}
// Encode encodes yuv image with the VPX8 encoder.
// see: https://chromium.googlesource.com/webm/libvpx/+/master/examples/simple_encoder.c
func (vpx *Vpx) Encode(yuv []byte) []byte {
C.vpx_img_read(&vpx.image, unsafe.Pointer(&yuv[0]))
if vpx.flipped {
C.vpx_img_flip(&vpx.image)
}
var flags C.int
if vpx.kfi > 0 && vpx.frameCount%vpx.kfi == 0 {
flags |= C.VPX_EFLAG_FORCE_KF
}
if C.vpx_codec_encode(&vpx.codecCtx, &vpx.image, C.vpx_codec_pts_t(vpx.frameCount), 1, C.vpx_enc_frame_flags_t(flags), C.VPX_DL_REALTIME) != 0 {
fmt.Println("Failed to encode frame")
}
vpx.frameCount++
var iter C.vpx_codec_iter_t
fb := C.get_frame_buffer(&vpx.codecCtx, &iter)
if fb.ptr == nil {
return []byte{}
}
return C.GoBytes(fb.ptr, fb.size)
}
func (vpx *Vpx) Info() string {
return fmt.Sprintf("vpx (%v): %v", vpx.v, C.GoString(C.vpx_codec_version_str()))
}
func (vpx *Vpx) IntraRefresh() {
// !to implement
}
func (vpx *Vpx) Shutdown() error {
//if &vpx.image != nil {
C.vpx_img_free(&vpx.image)
//}
C.vpx_codec_destroy(&vpx.codecCtx)
vpx.flipped = false
return nil
}