DataHoarder
8c6e604f26
All checks were successful
continuous-integration/drone/push Build is passing
348 lines
8.8 KiB
Go
348 lines
8.8 KiB
Go
//go:build cgo && !disable_codec_libx264
|
|
|
|
package libx264
|
|
|
|
/*
|
|
#cgo pkg-config: x264
|
|
#include "libx264.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"
|
|
"io"
|
|
"runtime"
|
|
"runtime/cgo"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
)
|
|
|
|
type Encoder struct {
|
|
w io.Writer
|
|
cleaned atomic.Bool
|
|
params C.x264_param_t
|
|
pictureIn *C.x264_picture_t
|
|
pictureOut C.x264_picture_t
|
|
h *C.x264_t
|
|
resourcePinner runtime.Pinner
|
|
logger utilities.Logger
|
|
}
|
|
|
|
var x264Version = fmt.Sprintf("x264 core:%d%s", C.X264_BUILD, C.GoString(C.Version()))
|
|
|
|
func Version() string {
|
|
return x264Version
|
|
}
|
|
|
|
func Build() int {
|
|
return int(C.X264_BUILD)
|
|
}
|
|
|
|
const (
|
|
LogLevelNone int = C.X264_LOG_NONE
|
|
LogLevelError int = C.X264_LOG_ERROR
|
|
LogLevelWarning int = C.X264_LOG_WARNING
|
|
LogLevelInfo int = C.X264_LOG_INFO
|
|
LogLevelDebug int = C.X264_LOG_DEBUG
|
|
)
|
|
|
|
func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[string]any, logger utilities.Logger) (*Encoder, error) {
|
|
e := &Encoder{
|
|
w: w,
|
|
logger: logger,
|
|
}
|
|
|
|
preset := C.CString(utilities.GetSettingString(settings, "preset", "medium"))
|
|
defer C.free(unsafe.Pointer(preset))
|
|
var tune *C.char
|
|
if strVal := utilities.GetSettingString(settings, "tune", ""); strVal != "" {
|
|
tune = C.CString(strVal)
|
|
defer C.free(unsafe.Pointer(tune))
|
|
}
|
|
|
|
if C.x264_param_default_preset(&e.params, preset, tune) < 0 {
|
|
return nil, errors.New("error setting preset or tune")
|
|
}
|
|
|
|
defaultProfile := "high"
|
|
|
|
switch true {
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 4 && properties.ColorSpace.ChromaSampling.B == 4:
|
|
e.params.i_csp = C.X264_CSP_I444
|
|
defaultProfile = "high444"
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 2 && properties.ColorSpace.ChromaSampling.B == 2:
|
|
e.params.i_csp = C.X264_CSP_I422
|
|
defaultProfile = "high422"
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 2 && properties.ColorSpace.ChromaSampling.B == 0:
|
|
e.params.i_csp = C.X264_CSP_I420
|
|
defaultProfile = "high"
|
|
case properties.ColorSpace.ChromaSampling.J == 4 && properties.ColorSpace.ChromaSampling.A == 0 && properties.ColorSpace.ChromaSampling.B == 0:
|
|
e.params.i_csp = C.X264_CSP_I400
|
|
defaultProfile = "high"
|
|
default:
|
|
return nil, errors.New("unsupported input chroma subsampling")
|
|
|
|
}
|
|
|
|
if properties.ColorSpace.BitDepth > 8 {
|
|
e.params.i_csp |= C.X264_CSP_HIGH_DEPTH
|
|
if defaultProfile == "high" {
|
|
defaultProfile = "high10"
|
|
}
|
|
}
|
|
|
|
profile := C.CString(utilities.GetSettingString(settings, "profile", defaultProfile))
|
|
defer C.free(unsafe.Pointer(profile))
|
|
|
|
e.params.i_bitdepth = C.int(properties.ColorSpace.BitDepth)
|
|
|
|
e.params.i_width = C.int(properties.Width)
|
|
e.params.i_height = C.int(properties.Height)
|
|
|
|
if properties.VFR {
|
|
e.params.b_vfr_input = 1
|
|
} else {
|
|
e.params.b_vfr_input = 0
|
|
}
|
|
e.params.b_repeat_headers = 1
|
|
e.params.b_annexb = 1
|
|
|
|
timeBase := properties.TimeBase()
|
|
|
|
e.params.i_timebase_num = C.uint32_t(timeBase.Numerator)
|
|
e.params.i_timebase_den = C.uint32_t(timeBase.Denominator)
|
|
if properties.FullColorRange {
|
|
e.params.vui.b_fullrange = 1
|
|
} else {
|
|
e.params.vui.b_fullrange = 0
|
|
}
|
|
|
|
switch properties.ColorSpace.ChromaSamplePosition {
|
|
case color.ChromaSamplePositionUnspecified:
|
|
e.params.vui.i_chroma_loc = 0 //?
|
|
case color.ChromaSamplePositionLeft:
|
|
e.params.vui.i_chroma_loc = 0
|
|
case color.ChromaSamplePositionCenter:
|
|
e.params.vui.i_chroma_loc = 1
|
|
case color.ChromaSamplePositionTopLeft:
|
|
e.params.vui.i_chroma_loc = 2
|
|
case color.ChromaSamplePositionTop:
|
|
e.params.vui.i_chroma_loc = 3
|
|
case color.ChromaSamplePositionBottomLeft:
|
|
e.params.vui.i_chroma_loc = 4
|
|
case color.ChromaSamplePositionBottom:
|
|
e.params.vui.i_chroma_loc = 5
|
|
}
|
|
|
|
for k, v := range settings {
|
|
if k == "profile" || k == "preset" || k == "tune" {
|
|
continue
|
|
}
|
|
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))
|
|
}
|
|
|
|
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.x264_param_parse(&e.params, strKey, strVal); ret != 0 {
|
|
if ret == C.X264_PARAM_BAD_NAME {
|
|
return fmt.Errorf("bad parameter name %s", k)
|
|
} else if ret == C.X264_PARAM_BAD_VALUE {
|
|
return fmt.Errorf("bad parameter value %s for %s", C.GoString(strVal), k)
|
|
} else {
|
|
return fmt.Errorf("error setting parameter %s", k)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if e.params.rc.b_stat_read == 0 && e.params.rc.b_stat_write == 1 && C.GoString(preset) != "placebo" {
|
|
//doing first pass
|
|
C.x264_param_apply_fastfirstpass(&e.params)
|
|
}
|
|
|
|
if C.x264_param_apply_profile(&e.params, profile) < 0 {
|
|
return nil, errors.New("error setting profile")
|
|
}
|
|
|
|
runtime.SetFinalizer(e, func(encoder *Encoder) {
|
|
encoder.Close()
|
|
})
|
|
|
|
e.pictureIn = &C.x264_picture_t{}
|
|
e.resourcePinner.Pin(e.pictureIn)
|
|
|
|
if C.x264_picture_alloc(e.pictureIn, e.params.i_csp, e.params.i_width, e.params.i_height) < 0 {
|
|
return nil, errors.New("error allocating input picture")
|
|
}
|
|
|
|
e.params.p_log_private = unsafe.Pointer(cgo.NewHandle(e))
|
|
C.SetLogCallback(&e.params)
|
|
|
|
if e.h = C.x264_encoder_open(&e.params); e.h == nil {
|
|
return nil, errors.New("error opening encoder")
|
|
}
|
|
|
|
if err := e.encoderHeaders(); err != nil {
|
|
return e, 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.pictureIn.img.plane[0]), len(luma)), luma)
|
|
copy(unsafe.Slice((*byte)(e.pictureIn.img.plane[1]), len(cb)), cb)
|
|
copy(unsafe.Slice((*byte)(e.pictureIn.img.plane[2]), len(cr)), cr)
|
|
|
|
e.pictureIn.i_pts = C.int64_t(f.PTS())
|
|
//TODO: read flags for statistic tracking
|
|
|
|
return e.encoderEncode(e.pictureIn)
|
|
}
|
|
|
|
func (e *Encoder) encoderHeaders() error {
|
|
if e.params.b_repeat_headers > 0 {
|
|
return nil
|
|
}
|
|
|
|
var nal *C.x264_nal_t
|
|
var iNal C.int
|
|
var frameSize C.int
|
|
|
|
if frameSize = C.x264_encoder_headers(e.h, &nal, &iNal); frameSize < 0 {
|
|
return errors.New("error encoding headers")
|
|
}
|
|
|
|
if frameSize == 0 || iNal == 0 {
|
|
return nil
|
|
}
|
|
|
|
if iNal != 1 {
|
|
//return errors.New("more than one NAL present")
|
|
}
|
|
|
|
// All NAL payloads are sequential to each other in memory
|
|
buf := unsafe.Slice((*byte)(nal.p_payload), int(frameSize))
|
|
if _, err := e.w.Write(buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) encoderEncode(picture *C.x264_picture_t) error {
|
|
var nal *C.x264_nal_t
|
|
var iNal C.int
|
|
var frameSize C.int
|
|
|
|
if frameSize = C.x264_encoder_encode(e.h, &nal, &iNal, picture, &e.pictureOut); frameSize < 0 {
|
|
return errors.New("error encoding frame")
|
|
}
|
|
|
|
if frameSize == 0 || iNal == 0 {
|
|
return nil
|
|
}
|
|
|
|
if iNal != 1 {
|
|
//return errors.New("more than one NAL present")
|
|
}
|
|
|
|
// All NAL payloads are sequential to each other in memory
|
|
buf := unsafe.Slice((*byte)(nal.p_payload), int(frameSize))
|
|
if _, err := e.w.Write(buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) Flush() error {
|
|
for C.x264_encoder_delayed_frames(e.h) > 0 {
|
|
if err := e.encoderEncode(nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) Log(logLevel int, message string) {
|
|
var prefix string
|
|
switch logLevel {
|
|
case C.X264_LOG_ERROR:
|
|
prefix = "error"
|
|
case C.X264_LOG_WARNING:
|
|
prefix = "warning"
|
|
case C.X264_LOG_INFO:
|
|
prefix = "info"
|
|
case C.X264_LOG_DEBUG:
|
|
prefix = "debug"
|
|
default:
|
|
prefix = "unknown"
|
|
}
|
|
e.logger.Printf("x264 [%s]: %s", prefix, message)
|
|
}
|
|
|
|
func (e *Encoder) Version() string {
|
|
return Version()
|
|
}
|
|
|
|
func (e *Encoder) Close() {
|
|
if e.cleaned.Swap(true) == false {
|
|
if e.h != nil {
|
|
C.x264_encoder_close(e.h)
|
|
e.h = nil
|
|
}
|
|
if e.pictureIn != nil {
|
|
C.x264_picture_clean(e.pictureIn)
|
|
e.pictureIn = nil
|
|
}
|
|
if e.params.p_log_private != nil {
|
|
(cgo.Handle)(e.params.p_log_private).Delete()
|
|
e.params.p_log_private = nil
|
|
}
|
|
e.resourcePinner.Unpin()
|
|
}
|
|
}
|
|
|
|
//export logCallback
|
|
func logCallback(logLevel C.int, message *C.char, size C.size_t, data unsafe.Pointer) {
|
|
(cgo.Handle)(data).Value().(*Encoder).Log(int(logLevel), C.GoStringN(message, C.int(size)))
|
|
}
|