759 lines
22 KiB
Go
759 lines
22 KiB
Go
//go:build cgo && !disable_library_libaom
|
|
|
|
package libaom
|
|
|
|
/*
|
|
#cgo pkg-config: aom
|
|
#include "libaom.h"
|
|
*/
|
|
import "C"
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"git.gammaspectra.live/S.O.N.G/Ignite/color"
|
|
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/filmgrain"
|
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/obuwriter"
|
|
"golang.org/x/exp/constraints"
|
|
"io"
|
|
"log"
|
|
"maps"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
)
|
|
|
|
type Encoder struct {
|
|
w *obuwriter.Writer
|
|
cleaned atomic.Bool
|
|
cfg C.aom_codec_enc_cfg_t
|
|
codec C.aom_codec_ctx_t
|
|
raw *C.aom_image_t
|
|
rawBuffer []byte
|
|
framesIn, framesOut int
|
|
frameStatistics frame.FrameStatistics
|
|
resourcePinner runtime.Pinner
|
|
logger utilities.Logger
|
|
free []func()
|
|
}
|
|
|
|
var libaomVersion = "libaom-av1 " + C.GoString(C.aom_codec_version_str()) + " ABI " + strconv.FormatUint(C.AOM_ENCODER_ABI_VERSION, 10)
|
|
|
|
func Version() string {
|
|
return libaomVersion
|
|
}
|
|
|
|
const (
|
|
UsageGoodQuality = C.AOM_USAGE_GOOD_QUALITY
|
|
UsageRealtime = C.AOM_USAGE_REALTIME
|
|
UsageAllIntra = C.AOM_USAGE_ALL_INTRA
|
|
)
|
|
|
|
func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[string]any, logger utilities.Logger) (*Encoder, error) {
|
|
e := &Encoder{
|
|
frameStatistics: make(frame.FrameStatistics),
|
|
logger: logger,
|
|
}
|
|
|
|
clonedSettings := make(map[string]any)
|
|
|
|
if modelPath := os.Getenv("VMAF_MODEL_PATH"); modelPath != "" {
|
|
clonedSettings["vmaf-model-path"] = modelPath
|
|
}
|
|
|
|
maps.Copy(clonedSettings, settings)
|
|
|
|
photonNoiseIso := getSettingUnsigned[uint](clonedSettings, "photon-noise-iso", 0)
|
|
photonNoiseTransferFunction := filmgrain.GetTransferFunction(getSettingString(clonedSettings, "photon-noise-transfer", "bt709"))
|
|
|
|
if photonNoiseIso > 0 && photonNoiseTransferFunction != nil {
|
|
//create table
|
|
noiseWidth := getSettingUnsigned[uint](clonedSettings, "photon-noise-width", uint(properties.Width))
|
|
noiseHeight := getSettingUnsigned[uint](clonedSettings, "photon-noise-height", uint(properties.Height))
|
|
table, err := filmgrain.CreatePhotonNoiseTable(int(noiseWidth), int(noiseHeight), float64(photonNoiseIso), photonNoiseTransferFunction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmpFile, err := os.CreateTemp(os.TempDir(), "photon-table*.tbl")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = tmpFile.Write(table)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tmpName := tmpFile.Name()
|
|
tmpFile.Close()
|
|
e.free = append(e.free, func() {
|
|
os.Remove(tmpName)
|
|
})
|
|
clonedSettings["film-grain-table"] = tmpName
|
|
|
|
}
|
|
|
|
var aomErr C.aom_codec_err_t
|
|
|
|
encoder := C.aom_codec_av1_cx()
|
|
if encoder == nil {
|
|
return nil, errors.New("unsupported codec")
|
|
}
|
|
|
|
e.cfg.g_usage = C.uint(getSettingUnsigned(clonedSettings, "usage", uint(UsageGoodQuality)))
|
|
|
|
if getSettingBool(clonedSettings, "good", false) {
|
|
e.cfg.g_usage = UsageGoodQuality
|
|
}
|
|
if getSettingBool(clonedSettings, "rt", false) {
|
|
e.cfg.g_usage = UsageRealtime
|
|
}
|
|
if getSettingBool(clonedSettings, "allintra", false) {
|
|
e.cfg.g_usage = UsageAllIntra
|
|
}
|
|
|
|
var imageFormat C.aom_img_fmt_t
|
|
var flags C.aom_codec_flags_t
|
|
|
|
if aomErr = C.aom_codec_enc_config_default(encoder, &e.cfg, e.cfg.g_usage); aomErr != 0 {
|
|
return nil, errors.New("failed to get default codec config")
|
|
}
|
|
|
|
var bitsPerSample, decH, decV C.int
|
|
|
|
switch true {
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 4 && properties.ColorSpace.ChromaSampling.B == 4:
|
|
imageFormat = C.AOM_IMG_FMT_I444
|
|
e.cfg.g_profile = 1
|
|
switch properties.ColorSpace.BitDepth {
|
|
case C.AOM_BITS_8:
|
|
bitsPerSample = 24
|
|
case C.AOM_BITS_10:
|
|
bitsPerSample = 30
|
|
case C.AOM_BITS_12:
|
|
bitsPerSample = 36
|
|
}
|
|
decH = 1
|
|
decV = 1
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 2 && properties.ColorSpace.ChromaSampling.B == 2:
|
|
imageFormat = C.AOM_IMG_FMT_I422
|
|
e.cfg.g_profile = 2
|
|
switch properties.ColorSpace.BitDepth {
|
|
case C.AOM_BITS_8:
|
|
bitsPerSample = 16
|
|
case C.AOM_BITS_10:
|
|
bitsPerSample = 20
|
|
case C.AOM_BITS_12:
|
|
bitsPerSample = 24
|
|
}
|
|
decH = 2
|
|
decV = 1
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 2 && properties.ColorSpace.ChromaSampling.B == 0:
|
|
imageFormat = C.AOM_IMG_FMT_I420
|
|
e.cfg.g_profile = 0
|
|
switch properties.ColorSpace.BitDepth {
|
|
case C.AOM_BITS_8:
|
|
bitsPerSample = 12
|
|
case C.AOM_BITS_10:
|
|
bitsPerSample = 15
|
|
case C.AOM_BITS_12:
|
|
bitsPerSample = 18
|
|
}
|
|
decH = 2
|
|
decV = 2
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 0 && properties.ColorSpace.ChromaSampling.B == 0:
|
|
//mono is defined as 4:2:0, but monochrome is set on config
|
|
imageFormat = C.AOM_IMG_FMT_I420
|
|
e.cfg.g_profile = 0
|
|
e.cfg.monochrome = 1
|
|
bitsPerSample = C.int(properties.ColorSpace.BitDepth)
|
|
decH = 0
|
|
decV = 2
|
|
default:
|
|
return nil, errors.New("unsupported input chroma subsampling")
|
|
|
|
}
|
|
|
|
e.cfg.g_input_bit_depth = C.uint(properties.ColorSpace.BitDepth)
|
|
e.cfg.g_bit_depth = C.aom_bit_depth_t(properties.ColorSpace.BitDepth)
|
|
|
|
if e.cfg.g_bit_depth >= C.AOM_BITS_12 { //only bitdepths up to 12 are supported, see aom_bit_depth_t
|
|
e.cfg.g_bit_depth = C.AOM_BITS_12
|
|
e.cfg.g_profile = 2
|
|
}
|
|
|
|
if e.cfg.g_input_bit_depth > C.AOM_BITS_8 {
|
|
imageFormat |= C.AOM_IMG_FMT_HIGHBITDEPTH
|
|
}
|
|
if e.cfg.g_bit_depth > C.AOM_BITS_8 {
|
|
flags |= C.AOM_CODEC_USE_HIGHBITDEPTH
|
|
}
|
|
|
|
frameSize, err := properties.ColorSpace.FrameSize(properties.Width, properties.Height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bytesPerSample := 1
|
|
if properties.ColorSpace.BitDepth > C.AOM_BITS_8 {
|
|
bytesPerSample = 2
|
|
}
|
|
|
|
e.raw = &C.aom_image_t{}
|
|
|
|
e.raw.bit_depth = C.uint(properties.ColorSpace.BitDepth)
|
|
//todo: color primaries
|
|
e.raw.cp = C.AOM_CICP_CP_UNSPECIFIED
|
|
//todo: transfer characteristics
|
|
e.raw.tc = C.AOM_CICP_TC_UNSPECIFIED
|
|
//todo: matrix coefficients
|
|
e.raw.mc = C.AOM_CICP_MC_UNSPECIFIED
|
|
e.raw.monochrome = C.int(e.cfg.monochrome)
|
|
e.raw.fmt = imageFormat
|
|
|
|
switch properties.ColorSpace.ChromaSamplePosition {
|
|
case color.ChromaSamplePositionLeft:
|
|
e.raw.csp = C.AOM_CSP_VERTICAL
|
|
case color.ChromaSamplePositionCenter:
|
|
e.raw.csp = C.AOM_CSP_UNKNOWN
|
|
case color.ChromaSamplePositionTopLeft:
|
|
e.raw.csp = C.AOM_CSP_COLOCATED
|
|
default:
|
|
e.raw.csp = C.AOM_CSP_UNKNOWN
|
|
}
|
|
|
|
if properties.FullColorRange {
|
|
e.raw._range = C.AOM_CR_FULL_RANGE
|
|
} else {
|
|
e.raw._range = C.AOM_CR_STUDIO_RANGE
|
|
}
|
|
|
|
e.cfg.g_w = C.uint(properties.Width)
|
|
e.cfg.g_h = C.uint(properties.Height)
|
|
|
|
e.raw.w = e.cfg.g_w
|
|
e.raw.d_w = e.cfg.g_w
|
|
e.raw.h = e.cfg.g_h
|
|
e.raw.d_h = e.cfg.g_h
|
|
e.raw.bps = bitsPerSample
|
|
e.raw.x_chroma_shift = C.uint(decH >> 1)
|
|
e.raw.y_chroma_shift = C.uint(decV >> 1)
|
|
|
|
c_w := (C.int(properties.Width) + decH - 1) / decH
|
|
c_w *= C.int(bytesPerSample)
|
|
|
|
e.rawBuffer = make([]byte, frameSize)
|
|
e.resourcePinner.Pin(e.raw)
|
|
e.resourcePinner.Pin(unsafe.SliceData(e.rawBuffer))
|
|
|
|
iY := properties.ColorSpace.ChromaSampling.PlaneLumaSamples(properties.Width, properties.Height)
|
|
iCb := properties.ColorSpace.ChromaSampling.PlaneCbSamples(properties.Width, properties.Height)
|
|
iCr := properties.ColorSpace.ChromaSampling.PlaneCrSamples(properties.Width, properties.Height)
|
|
|
|
iY *= bytesPerSample
|
|
iCb *= bytesPerSample
|
|
iCr *= bytesPerSample
|
|
|
|
e.raw.stride[C.AOM_PLANE_Y] = C.int(properties.Width * bytesPerSample)
|
|
e.raw.stride[C.AOM_PLANE_U] = c_w
|
|
e.raw.stride[C.AOM_PLANE_V] = e.raw.stride[C.AOM_PLANE_U]
|
|
|
|
e.raw.planes[C.AOM_PLANE_Y] = (*C.uchar)(unsafe.Pointer(unsafe.SliceData(e.rawBuffer[:iY])))
|
|
e.raw.planes[C.AOM_PLANE_U] = (*C.uchar)(unsafe.Pointer(unsafe.SliceData(e.rawBuffer[iY : iY+iCb])))
|
|
e.raw.planes[C.AOM_PLANE_V] = (*C.uchar)(unsafe.Pointer(unsafe.SliceData(e.rawBuffer[iY+iCb : iY+iCb+iCr])))
|
|
|
|
runtime.SetFinalizer(e, func(encoder *Encoder) {
|
|
encoder.Close()
|
|
})
|
|
|
|
/*!\brief Stream timebase units
|
|
*
|
|
* Indicates the smallest interval of time, in seconds, used by the stream.
|
|
* For fixed frame rate material, or variable frame rate material where
|
|
* frames are timed at a multiple of a given clock (ex: video capture),
|
|
* the \ref RECOMMENDED method is to set the timebase to the reciprocal
|
|
* of the frame rate (ex: 1001/30000 for 29.970 Hz NTSC). This allows the
|
|
* pts to correspond to the frame number, which can be handy. For
|
|
* re-encoding video from containers with absolute time timestamps, the
|
|
* \ref RECOMMENDED method is to set the timebase to that of the parent
|
|
* container or multimedia framework (ex: 1/1000 for ms, as in FLV).
|
|
*/
|
|
timeBase := properties.TimeBase()
|
|
|
|
e.cfg.g_timebase.num = C.int(timeBase.Numerator)
|
|
e.cfg.g_timebase.den = C.int(timeBase.Denominator)
|
|
|
|
// boolean settings
|
|
|
|
if getSettingBool(clonedSettings, "large-scale-tile", e.cfg.large_scale_tile != 0) {
|
|
e.cfg.large_scale_tile = 1
|
|
}
|
|
if getSettingBool(clonedSettings, "monochrome", e.cfg.monochrome != 0) {
|
|
e.cfg.monochrome = 1
|
|
}
|
|
if getSettingBool(clonedSettings, "enable-fwd-kf", e.cfg.fwd_kf_enabled != 0) {
|
|
e.cfg.fwd_kf_enabled = 1
|
|
}
|
|
|
|
if getSettingBool(clonedSettings, "kf-disabled", false) {
|
|
e.cfg.kf_mode = C.AOM_KF_DISABLED
|
|
}
|
|
|
|
// integer settings
|
|
|
|
type uintSettingPair struct {
|
|
p *C.uint
|
|
n string
|
|
}
|
|
|
|
for _, s := range []uintSettingPair{
|
|
{&e.cfg.g_threads, "threads"},
|
|
|
|
{&e.cfg.g_lag_in_frames, "lag-in-frames"},
|
|
|
|
{&e.cfg.g_forced_max_frame_width, "forced_max_frame_width"},
|
|
{&e.cfg.g_forced_max_frame_height, "forced_max_frame_height"},
|
|
|
|
{&e.cfg.rc_dropframe_thresh, "drop-frame"},
|
|
{&e.cfg.rc_resize_mode, "resize-mode"},
|
|
{&e.cfg.rc_resize_denominator, "resize-denominator"},
|
|
{&e.cfg.rc_resize_kf_denominator, "resize-kf-denominator"},
|
|
|
|
{(*C.uint)(&e.cfg.rc_superres_mode), "superres-mode"},
|
|
{&e.cfg.rc_superres_denominator, "superres-denominator"},
|
|
|
|
{&e.cfg.rc_superres_kf_denominator, "superres-kf-denominator"},
|
|
|
|
{&e.cfg.rc_superres_qthresh, "superres-qthresh"},
|
|
{&e.cfg.rc_superres_kf_qthresh, "superres-kf-qthresh"},
|
|
|
|
{&e.cfg.rc_target_bitrate, "target-bitrate"},
|
|
{&e.cfg.rc_min_quantizer, "min-q"},
|
|
{&e.cfg.rc_max_quantizer, "max-q"},
|
|
{&e.cfg.rc_undershoot_pct, "undershoot-pct"},
|
|
{&e.cfg.rc_overshoot_pct, "overshoot-pct"},
|
|
{&e.cfg.rc_buf_sz, "buf-sz"},
|
|
{&e.cfg.rc_buf_initial_sz, "buf-initial-sz"},
|
|
{&e.cfg.rc_buf_optimal_sz, "buf-optimal-sz"},
|
|
//{&e.cfg.rc_2pass_vbr_bias_pct, "bias-pct"},
|
|
//{&e.cfg.rc_2pass_vbr_minsection_pct, "minsection-pct"},
|
|
//{&e.cfg.rc_2pass_vbr_maxsection_pct, "maxsection-pct"},
|
|
|
|
{&e.cfg.kf_min_dist, "kf-min-dist"},
|
|
{&e.cfg.kf_max_dist, "kf-max-dist"},
|
|
{&e.cfg.sframe_dist, "sframe-dist"},
|
|
{&e.cfg.sframe_mode, "sframe-mode"},
|
|
|
|
{&e.cfg.use_fixed_qp_offsets, "use-fixed-qp-offsets"},
|
|
} {
|
|
//todo: unset setting from map
|
|
*s.p = C.uint(getSettingUnsigned(clonedSettings, s.n, uint(*s.p)))
|
|
}
|
|
|
|
// string/enum settings
|
|
|
|
endUsage := getSettingString(clonedSettings, "end-usage", "vbr")
|
|
|
|
switch endUsage {
|
|
case "vbr":
|
|
e.cfg.rc_end_usage = C.AOM_VBR
|
|
case "cbr":
|
|
e.cfg.rc_end_usage = C.AOM_CBR
|
|
case "cq":
|
|
e.cfg.rc_end_usage = C.AOM_CQ
|
|
case "q":
|
|
e.cfg.rc_end_usage = C.AOM_Q
|
|
default:
|
|
return nil, errors.New("unknown end-usage setting: " + endUsage)
|
|
}
|
|
|
|
//TODO: find all settings not set on AV1 encoder and place them on e.cfg
|
|
|
|
if aomErr = C.aom_codec_enc_init_ver(&e.codec, encoder, &e.cfg, flags, C.AOM_ENCODER_ABI_VERSION); aomErr != 0 {
|
|
return nil, fmt.Errorf("failed to initialize encoder: err %d %s", aomErr, C.GoString(e.codec.err_detail))
|
|
}
|
|
e.free = append(e.free, func() {
|
|
C.aom_codec_destroy(&e.codec)
|
|
})
|
|
|
|
if properties.FullColorRange {
|
|
if aomErr = C.aom_codec_control_uint(&e.codec, C.AV1E_SET_COLOR_RANGE, C.AOM_CR_FULL_RANGE); aomErr != 0 {
|
|
return nil, fmt.Errorf("failed to set color range")
|
|
}
|
|
} else {
|
|
if aomErr = C.aom_codec_control_uint(&e.codec, C.AV1E_SET_COLOR_RANGE, C.AOM_CR_STUDIO_RANGE); aomErr != 0 {
|
|
return nil, fmt.Errorf("failed to set color range")
|
|
}
|
|
}
|
|
|
|
//??? aomenc does this
|
|
if properties.ColorSpace.BitDepth == 12 {
|
|
if aomErr = C.aom_codec_control_uint(&e.codec, C.AV1E_SET_CHROMA_SUBSAMPLING_X, C.uint(decH>>1)); aomErr != 0 {
|
|
return nil, fmt.Errorf("failed to set chroma subsampling X")
|
|
}
|
|
if aomErr = C.aom_codec_control_uint(&e.codec, C.AV1E_SET_CHROMA_SUBSAMPLING_Y, C.uint(decV>>1)); aomErr != 0 {
|
|
return nil, fmt.Errorf("failed to set chroma subsampling Y")
|
|
}
|
|
}
|
|
|
|
//TODO: fill all
|
|
controlSettings := map[string]C.int{
|
|
"cpu-used": C.AOME_SET_CPUUSED,
|
|
"auto-alt-ref": C.AOME_SET_ENABLEAUTOALTREF,
|
|
"sharpness": C.AOME_SET_SHARPNESS,
|
|
"row-mt": C.AV1E_SET_ROW_MT,
|
|
//"fp-mt": C.AV1E_SET_FP_MT,
|
|
"tile-columns": C.AV1E_SET_TILE_COLUMNS,
|
|
"tile-rows": C.AV1E_SET_TILE_ROWS,
|
|
"cq-level": C.AOME_SET_CQ_LEVEL,
|
|
"max-reference-frames": C.AV1E_SET_MAX_REFERENCE_FRAMES,
|
|
}
|
|
|
|
for key, controlKey := range controlSettings {
|
|
if _, ok := clonedSettings[key]; ok {
|
|
val := getSettingUnsigned[uint](clonedSettings, key, 0)
|
|
if aomErr = C.aom_codec_control_uint(&e.codec, controlKey, C.uint(val)); aomErr != 0 {
|
|
return nil, fmt.Errorf("error setting parameter %s: %s", key, C.GoString(C.aom_codec_error_detail(&e.codec)))
|
|
}
|
|
}
|
|
}
|
|
|
|
if fgt := getSettingString(clonedSettings, "film-grain-table", ""); fgt != "" {
|
|
strVal := C.CString(fgt)
|
|
defer C.free(unsafe.Pointer(strVal))
|
|
if aomErr = C.aom_codec_control_charptr(&e.codec, C.AV1E_SET_FILM_GRAIN_TABLE, strVal); aomErr != 0 {
|
|
return nil, fmt.Errorf("error setting FILM_GRAIN_TABLE parameter: %s", C.GoString(C.aom_codec_error_detail(&e.codec)))
|
|
}
|
|
}
|
|
|
|
for k, v := range clonedSettings {
|
|
if err := func() error {
|
|
var strVal *C.char
|
|
if val, ok := v.(string); ok {
|
|
strVal = C.CString(val)
|
|
} else if val, ok := v.(int); ok {
|
|
strVal = C.CString(strconv.FormatInt(int64(val), 10))
|
|
} else if val, ok := v.(int64); ok {
|
|
strVal = C.CString(strconv.FormatInt(val, 10))
|
|
} else if val, ok := v.(uint); ok {
|
|
strVal = C.CString(strconv.FormatUint(uint64(val), 10))
|
|
} else if val, ok := v.(uint64); ok {
|
|
strVal = C.CString(strconv.FormatUint(val, 10))
|
|
} else if val, ok := v.(bool); ok {
|
|
if val {
|
|
strVal = C.CString("1")
|
|
} else {
|
|
strVal = C.CString("0")
|
|
}
|
|
}
|
|
|
|
if strVal != nil {
|
|
defer C.free(unsafe.Pointer(strVal))
|
|
} else {
|
|
return fmt.Errorf("could not get parameter %s", k)
|
|
}
|
|
strKey := C.CString(k)
|
|
defer C.free(unsafe.Pointer(strKey))
|
|
if ret := C.aom_codec_set_option(&e.codec, strKey, strVal); ret != 0 {
|
|
if ret == C.AOM_CODEC_INVALID_PARAM {
|
|
return fmt.Errorf("bad parameter value %s for %s: %s", C.GoString(strVal), k, C.GoString(C.aom_codec_error_detail(&e.codec)))
|
|
} else if ret == C.AOM_CODEC_ERROR {
|
|
return fmt.Errorf("error setting parameter %s: %s", k, C.GoString(C.aom_codec_error_detail(&e.codec)))
|
|
} else {
|
|
return fmt.Errorf("error setting parameter %s: %s", k, C.GoString(C.aom_codec_error_detail(&e.codec)))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if e.w, err = obuwriter.NewWriter(w, properties.Width, properties.Height, 0x31305641, timeBase); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
func (e *Encoder) EncodeStream(stream *frame.Stream) error {
|
|
for f := range stream.Channel() {
|
|
if err := e.Encode(f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return e.Flush()
|
|
}
|
|
|
|
func (e *Encoder) Encode(f frame.Frame) error {
|
|
/*luma := f.GetLuma()
|
|
cb := f.GetCb()
|
|
cr := f.GetCr()
|
|
copy(unsafe.Slice((*byte)(e.raw.planes[C.AOM_PLANE_Y]), len(luma)), luma)
|
|
copy(unsafe.Slice((*byte)(e.raw.planes[C.AOM_PLANE_U]), len(cb)), cb)
|
|
copy(unsafe.Slice((*byte)(e.raw.planes[C.AOM_PLANE_V]), len(cr)), cr)
|
|
*/
|
|
copy(e.rawBuffer, f.GetJoint())
|
|
|
|
if _, err := e.encodeFrame(f.PTS(), f.NextPTS(), e.raw); err != nil {
|
|
return err
|
|
}
|
|
runtime.KeepAlive(f)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) encodeFrame(pts, nextPts int64, raw *C.aom_image_t) (pkts int, err error) {
|
|
var aomErr C.aom_codec_err_t
|
|
|
|
if aomErr = C.aom_codec_encode(&e.codec, raw, C.long(pts), C.ulong(nextPts-pts), 0); aomErr != C.AOM_CODEC_OK {
|
|
if aomErr == C.AOM_CODEC_INCAPABLE {
|
|
return 0, errors.New("error encoding frame: AOM_CODEC_INCAPABLE")
|
|
} else if aomErr == C.AOM_CODEC_INVALID_PARAM {
|
|
return 0, errors.New("error encoding frame: AOM_CODEC_INVALID_PARAM")
|
|
} else if aomErr == C.AOM_CODEC_ERROR {
|
|
return 0, errors.New("error encoding frame: AOM_CODEC_ERROR")
|
|
} else {
|
|
return 0, errors.New("error encoding frame")
|
|
}
|
|
}
|
|
|
|
if raw != nil {
|
|
e.framesIn++
|
|
}
|
|
|
|
var quant64 C.int
|
|
if e.framesIn >= int(e.cfg.g_lag_in_frames) {
|
|
//TODO: maybe call this on new packets?
|
|
if aomErr = C.aom_codec_control_intptr(&e.codec, C.AOME_GET_LAST_QUANTIZER_64, &quant64); aomErr != C.AOM_CODEC_OK {
|
|
return 0, errors.New("error getting LAST_QUANTIZER_64")
|
|
}
|
|
}
|
|
|
|
var iter C.aom_codec_iter_t
|
|
|
|
for {
|
|
pkt := C.aom_codec_get_cx_data(&e.codec, &iter)
|
|
if pkt == nil {
|
|
break
|
|
}
|
|
pkts++
|
|
|
|
if pkt.kind == C.AOM_CODEC_CX_FRAME_PKT {
|
|
partitionId := int(C.aom_get_pkt_partition_id(pkt))
|
|
if partitionId > 0 {
|
|
return 0, errors.New("partition id not supported")
|
|
}
|
|
packetPts := uint64(C.aom_get_pkt_pts(pkt))
|
|
buf := unsafe.Slice((*byte)(C.aom_get_pkt_buf(pkt)), int(C.aom_get_pkt_sz(pkt)))
|
|
flags := C.aom_get_pkt_flags(pkt)
|
|
|
|
e.frameStatistics[frame.FrameStatisticsKeyNumber] = e.framesOut
|
|
e.frameStatistics[frame.FrameStatisticsKeyPts] = int64(packetPts)
|
|
e.frameStatistics[frame.FrameStatisticsKeyQuantizer] = int(quant64)
|
|
e.frameStatistics[frame.FrameStatisticsKeySize] = len(buf)
|
|
e.frameStatistics[frame.FrameStatisticsKeyIsKeyFrame] = false
|
|
e.frameStatistics[frame.FrameStatisticsKeyIsIntraFrame] = false
|
|
|
|
var flagStr []string
|
|
if flags&C.AOM_FRAME_IS_KEY > 0 {
|
|
e.frameStatistics[frame.FrameStatisticsKeyIsKeyFrame] = true
|
|
flagStr = append(flagStr, "K-Frame")
|
|
}
|
|
if flags&C.AOM_FRAME_IS_DROPPABLE > 0 {
|
|
flagStr = append(flagStr, "droppable")
|
|
}
|
|
if flags&C.AOM_FRAME_IS_INTRAONLY > 0 {
|
|
e.frameStatistics[frame.FrameStatisticsKeyIsIntraFrame] = true
|
|
flagStr = append(flagStr, "I-Frame")
|
|
}
|
|
if flags&C.AOM_FRAME_IS_SWITCH > 0 {
|
|
flagStr = append(flagStr, "S-Frame")
|
|
}
|
|
if flags&C.AOM_FRAME_IS_ERROR_RESILIENT > 0 {
|
|
flagStr = append(flagStr, "resilient")
|
|
}
|
|
if flags&C.AOM_FRAME_IS_DELAYED_RANDOM_ACCESS_POINT > 0 {
|
|
//Forward keyframe
|
|
e.frameStatistics[frame.FrameStatisticsKeyIsKeyFrame] = true
|
|
flagStr = append(flagStr, "delayedrandom")
|
|
}
|
|
|
|
e.logger.Printf("libaom: partition = %d, n = %d, pts = %d, quant64 = %d, bytes = %d, flags = %s", partitionId, e.framesOut, packetPts, quant64, len(buf), strings.Join(flagStr, ", "))
|
|
if err = e.w.WriteFrameBytes(packetPts, buf); err != nil {
|
|
return pkts, err
|
|
}
|
|
|
|
e.framesOut++
|
|
runtime.KeepAlive(buf)
|
|
} else {
|
|
|
|
log.Printf("kind %d", pkt.kind)
|
|
}
|
|
}
|
|
|
|
return pkts, nil
|
|
}
|
|
|
|
func (e *Encoder) Statistics() frame.Statistics {
|
|
if len(e.frameStatistics) > 0 {
|
|
return frame.Statistics{
|
|
frame.StatisticsKeyFramesIn: e.framesIn,
|
|
frame.StatisticsKeyFramesOut: e.framesOut,
|
|
frame.StatisticsKeyLastFrameOut: maps.Clone(e.frameStatistics),
|
|
}
|
|
} else {
|
|
return frame.Statistics{
|
|
frame.StatisticsKeyFramesIn: e.framesIn,
|
|
frame.StatisticsKeyFramesOut: e.framesOut,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Encoder) Flush() error {
|
|
|
|
var pkts int
|
|
var err error
|
|
for {
|
|
if pkts, err = e.encodeFrame(-1, 0, nil); err != nil {
|
|
return err
|
|
}
|
|
if pkts == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
_ = e.w.WriteLength(uint32(e.framesIn))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) Close() {
|
|
if e.cleaned.Swap(true) == false {
|
|
e.resourcePinner.Unpin()
|
|
if e.raw != nil {
|
|
e.raw = nil
|
|
}
|
|
if e.rawBuffer != nil {
|
|
e.rawBuffer = nil
|
|
}
|
|
C.aom_codec_destroy(&e.codec)
|
|
}
|
|
}
|
|
|
|
func (e *Encoder) Version() string {
|
|
return Version()
|
|
}
|
|
|
|
func getSettingBool(m map[string]any, name string, fallback bool) bool {
|
|
if v, ok := m[name]; ok {
|
|
if val, ok := v.(string); ok {
|
|
delete(m, name)
|
|
return val == "false" || val == "f" || val == "n"
|
|
}
|
|
if val, ok := v.(int); ok {
|
|
delete(m, name)
|
|
return val != 0
|
|
}
|
|
if val, ok := v.(int64); ok {
|
|
delete(m, name)
|
|
return val != 0
|
|
}
|
|
if val, ok := v.(uint); ok {
|
|
delete(m, name)
|
|
return val != 0
|
|
}
|
|
if val, ok := v.(uint64); ok {
|
|
delete(m, name)
|
|
return val != 0
|
|
}
|
|
|
|
return true
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func getSettingString(m map[string]any, name string, fallback string) string {
|
|
if v, ok := m[name]; ok {
|
|
if val, ok := v.(string); ok {
|
|
delete(m, name)
|
|
return val
|
|
}
|
|
if val, ok := v.(int); ok {
|
|
delete(m, name)
|
|
return strconv.FormatInt(int64(val), 10)
|
|
}
|
|
if val, ok := v.(int64); ok {
|
|
delete(m, name)
|
|
return strconv.FormatInt(val, 10)
|
|
}
|
|
if val, ok := v.(uint); ok {
|
|
delete(m, name)
|
|
return strconv.FormatUint(uint64(val), 10)
|
|
}
|
|
if val, ok := v.(uint64); ok {
|
|
delete(m, name)
|
|
return strconv.FormatUint(val, 10)
|
|
}
|
|
if val, ok := v.(bool); ok {
|
|
delete(m, name)
|
|
if val {
|
|
return "1"
|
|
} else {
|
|
return "0"
|
|
}
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func getSettingUnsigned[T constraints.Unsigned](m map[string]any, name string, fallback T) T {
|
|
if v, ok := m[name]; ok {
|
|
if val, ok := v.(string); ok {
|
|
if intVal, err := strconv.ParseUint(val, 10, 0); err != nil {
|
|
delete(m, name)
|
|
return T(intVal)
|
|
} else {
|
|
return fallback
|
|
}
|
|
}
|
|
if val, ok := v.(int); ok {
|
|
delete(m, name)
|
|
return T(val)
|
|
}
|
|
if val, ok := v.(int64); ok {
|
|
delete(m, name)
|
|
return T(val)
|
|
}
|
|
if val, ok := v.(uint); ok {
|
|
delete(m, name)
|
|
return T(val)
|
|
}
|
|
if val, ok := v.(uint64); ok {
|
|
delete(m, name)
|
|
return T(val)
|
|
}
|
|
if val, ok := v.(C.int); ok {
|
|
delete(m, name)
|
|
return T(val)
|
|
}
|
|
if val, ok := v.(C.uint); ok {
|
|
delete(m, name)
|
|
return T(val)
|
|
}
|
|
if val, ok := v.(bool); ok {
|
|
delete(m, name)
|
|
if val {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
return fallback
|
|
}
|