Compare commits
3 commits
4cee28b8bb
...
c54e961aee
Author | SHA1 | Date | |
---|---|---|---|
DataHoarder | c54e961aee | ||
DataHoarder | b08b662354 | ||
DataHoarder | 5b80960420 |
|
@ -55,10 +55,13 @@ RUN go mod download -x && go mod verify
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN go build -v -ldflags '-linkmode external -extldflags "-Wl,-z,stack-size=2097152 -fno-PIC -static -lstdc++"' -buildmode pie -tags 'osusergo netgo static_build' -o /usr/bin/ignite-encode-libaom git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-libaom
|
||||
RUN go build -v -ldflags '-linkmode external -extldflags "-Wl,-z,stack-size=2097152 -fno-PIC -static -lstdc++"' -buildmode pie -tags 'osusergo netgo static_build' \
|
||||
-o /usr/bin/ git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-libaom git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-server
|
||||
RUN CGO_ENABLED=0 go build -v -tags 'osusergo netgo static_build' \
|
||||
-o /usr/bin/encode-pool git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-pool
|
||||
|
||||
FROM alpine:3.18
|
||||
|
||||
COPY --from=builder /usr/bin/ignite-encode-libaom /usr/bin/ignite-encode-libaom
|
||||
COPY --from=builder /usr/bin/encode-* /usr/bin/
|
||||
|
||||
WORKDIR /
|
|
@ -15,6 +15,7 @@ import (
|
|||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -33,6 +34,44 @@ type ServerConfig struct {
|
|||
URL string `yaml:"url"`
|
||||
Key string `yaml:"key"`
|
||||
InsecureSSL bool `yaml:"insecure_ssl"`
|
||||
lastStatus atomic.Pointer[encode_utils.StatusData]
|
||||
nextStatus atomic.Int64
|
||||
}
|
||||
|
||||
func (s *ServerConfig) Status() *encode_utils.StatusData {
|
||||
if time.Unix(s.nextStatus.Load(), 0).Compare(time.Now()) < 0 {
|
||||
defer func() {
|
||||
s.nextStatus.Store(time.Now().Add(time.Second * 30).Unix())
|
||||
}()
|
||||
u, _ := url.Parse(s.URL + "/status?k=" + s.Key)
|
||||
response, err := s.Do(&http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
})
|
||||
if err != nil {
|
||||
return s.lastStatus.Load()
|
||||
}
|
||||
defer response.Body.Close()
|
||||
dataBuf, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return s.lastStatus.Load()
|
||||
}
|
||||
var status encode_utils.StatusData
|
||||
err = json.Unmarshal(dataBuf, &status)
|
||||
if err != nil {
|
||||
return s.lastStatus.Load()
|
||||
}
|
||||
s.lastStatus.Store(&status)
|
||||
}
|
||||
return s.lastStatus.Load()
|
||||
}
|
||||
|
||||
func (s *ServerConfig) Do(r *http.Request) (*http.Response, error) {
|
||||
if s.InsecureSSL {
|
||||
return InsecureDefaultClient.Do(r)
|
||||
} else {
|
||||
return DefaultClient.Do(r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerConfig) Pass(r *http.Request, replaceValues ...[2]string) (*http.Response, error) {
|
||||
|
@ -47,21 +86,12 @@ func (s *ServerConfig) Pass(r *http.Request, replaceValues ...[2]string) (*http.
|
|||
}
|
||||
urlPath.RawQuery = q.Encode()
|
||||
|
||||
if s.InsecureSSL {
|
||||
return InsecureDefaultClient.Do(&http.Request{
|
||||
Method: r.Method,
|
||||
URL: urlPath,
|
||||
Header: r.Header,
|
||||
Body: r.Body,
|
||||
})
|
||||
} else {
|
||||
return DefaultClient.Do(&http.Request{
|
||||
Method: r.Method,
|
||||
URL: urlPath,
|
||||
Header: r.Header,
|
||||
Body: r.Body,
|
||||
})
|
||||
}
|
||||
return s.Do(&http.Request{
|
||||
Method: r.Method,
|
||||
URL: urlPath,
|
||||
Header: r.Header,
|
||||
Body: r.Body,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ServerConfig) Redirect(w http.ResponseWriter, r *http.Request, replaceValues ...[2]string) {
|
||||
|
|
|
@ -52,7 +52,13 @@ func encodeFromReader(reader io.ReadCloser, job *Job, w http.ResponseWriter) {
|
|||
job.Status.Read.Store(0)
|
||||
job.Status.Processed.Store(0)
|
||||
|
||||
decoder, err := y4m.NewDecoder(reader, nil)
|
||||
settings := make(map[string]any)
|
||||
|
||||
if len(job.Config.Timecodes) > 0 {
|
||||
settings["timecodes"] = job.Config.Timecodes
|
||||
}
|
||||
|
||||
decoder, err := y4m.NewDecoder(reader, settings)
|
||||
if err != nil {
|
||||
w.Header().Set("x-encoder-error", "")
|
||||
w.Header().Set("x-decoder-error", err.Error())
|
||||
|
@ -61,7 +67,12 @@ func encodeFromReader(reader io.ReadCloser, job *Job, w http.ResponseWriter) {
|
|||
}
|
||||
|
||||
//check decoder frame properties match
|
||||
if decoder.Properties() != job.Config.Properties {
|
||||
decProps := decoder.Properties()
|
||||
if decProps.Width != job.Config.Properties.Width ||
|
||||
decProps.Height != job.Config.Properties.Height ||
|
||||
decProps.ColorSpace != job.Config.Properties.ColorSpace ||
|
||||
decProps.FullColorRange != job.Config.Properties.FullColorRange ||
|
||||
decProps.TimeBase() != job.Config.Properties.TimeBase() {
|
||||
w.Header().Set("x-encoder-error", "")
|
||||
w.Header().Set("x-decoder-error", "mismatched config properties")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
encode_utils "git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-utils"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/encoder/libaom"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/encoder/libx264"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/shirou/gopsutil/load"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
|
@ -35,11 +37,6 @@ func main() {
|
|||
authKeys = strings.Split(*authKeysStr, ",")
|
||||
}
|
||||
|
||||
type encoderData struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
writeHeaders := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("x-encoder-max-jobs", strconv.FormatUint(*maxJobs, 10))
|
||||
w.Header().Set("x-encoder-current-jobs", strconv.FormatUint(uint64(len(jobs)), 10))
|
||||
|
@ -51,18 +48,13 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
var statusData struct {
|
||||
Encoders []encoderData `json:"encoders"`
|
||||
Host *host.InfoStat `json:"host,omitempty"`
|
||||
CPU []cpu.InfoStat `json:"cpu"`
|
||||
Memory *mem.VirtualMemoryStat `json:"memory,omitempty"`
|
||||
}
|
||||
var statusData encode_utils.StatusData
|
||||
|
||||
statusData.Encoders = append(statusData.Encoders, encoderData{
|
||||
statusData.Encoders = append(statusData.Encoders, encode_utils.StatusEncoderData{
|
||||
Name: encode_utils.EncoderX264,
|
||||
Version: libx264.Version(),
|
||||
})
|
||||
statusData.Encoders = append(statusData.Encoders, encoderData{
|
||||
statusData.Encoders = append(statusData.Encoders, encode_utils.StatusEncoderData{
|
||||
Name: encode_utils.EncoderAOM,
|
||||
Version: libaom.Version(),
|
||||
})
|
||||
|
@ -80,6 +72,11 @@ func main() {
|
|||
statusData.Host = hostInfo
|
||||
}
|
||||
|
||||
loadInfo, err := load.Avg()
|
||||
if err == nil {
|
||||
statusData.Load = loadInfo
|
||||
}
|
||||
|
||||
memInfo, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
statusData.Memory = memInfo
|
||||
|
@ -128,6 +125,15 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
if cfg.TimecodesV1 != "" {
|
||||
cfg.Timecodes, err = utilities.ParseTimecodesV1(strings.NewReader(cfg.TimecodesV1))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if job := func() *Job {
|
||||
JobsMutex.Lock()
|
||||
defer JobsMutex.Unlock()
|
||||
|
@ -365,7 +371,7 @@ func main() {
|
|||
})
|
||||
|
||||
s := http.Server{
|
||||
ReadTimeout: time.Second * 10,
|
||||
ReadTimeout: 0,
|
||||
IdleTimeout: time.Second * 60,
|
||||
WriteTimeout: 0,
|
||||
Addr: *listenAddr,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package encode_utils
|
||||
|
||||
import "git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
||||
)
|
||||
|
||||
type JobConfig struct {
|
||||
Encoder struct {
|
||||
|
@ -9,6 +12,9 @@ type JobConfig struct {
|
|||
} `json:"encoder" yaml:"encoder"`
|
||||
|
||||
Properties frame.StreamProperties `json:"properties" yaml:"properties"`
|
||||
|
||||
TimecodesV1 string `json:"timecodes_v1" yaml:"timecodes_v1"`
|
||||
Timecodes utilities.Timecodes `json:"timecodes" yaml:"timecodes"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
22
cli/encode-utils/status.go
Normal file
22
cli/encode-utils/status.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package encode_utils
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/shirou/gopsutil/load"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
)
|
||||
|
||||
type StatusEncoderData struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
type StatusData struct {
|
||||
Encoders []StatusEncoderData `json:"encoders"`
|
||||
Host *host.InfoStat `json:"host,omitempty"`
|
||||
Load *load.AvgStat `json:"load,omitempty"`
|
||||
CPU []cpu.InfoStat `json:"cpu"`
|
||||
CPULoad float64 `json:"cpu_load"`
|
||||
Memory *mem.VirtualMemoryStat `json:"memory,omitempty"`
|
||||
}
|
34
cli/timecodes/timecodes.go
Normal file
34
cli/timecodes/timecodes.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
inputPath := flag.String("input", "", "Input timecodes file")
|
||||
inputFormat := flag.String("format", "v1", "Input format. Supported: v1")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
f, err := os.Open(*inputPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
switch *inputFormat {
|
||||
case "v1":
|
||||
tc, err := utilities.ParseTimecodesV1(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, e := range tc {
|
||||
_, _ = fmt.Printf("%d\n", e)
|
||||
}
|
||||
default:
|
||||
panic("unsupported format")
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
void set_threading(Dav1dSettings* s, int threads) {
|
||||
#if DAV1D_VERSION_AT_LEAST(6,0)
|
||||
|
||||
s->n_threads = threads;
|
||||
#else
|
||||
s->n_tile_threads = MIN(floor(sqrt(threads)), DAV1D_MAX_TILE_THREADS);
|
||||
s->n_frame_threads = MIN(ceil(threads / s->n_tile_threads), DAV1D_MAX_FRAME_THREADS);
|
||||
|
|
|
@ -15,8 +15,10 @@ import (
|
|||
"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/ivfreader"
|
||||
"golang.org/x/exp/constraints"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
@ -35,6 +37,8 @@ type Decoder struct {
|
|||
picture C.Dav1dPicture
|
||||
data C.Dav1dData
|
||||
|
||||
closer sync.Once
|
||||
|
||||
bufPool sync.Pool
|
||||
}
|
||||
|
||||
|
@ -57,12 +61,20 @@ func NewDecoder(r io.Reader, settings map[string]any) (d *Decoder, err error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
maxThreads := int(getSettingUnsigned[uint64](settings, "threads", 0))
|
||||
if maxThreads == 0 {
|
||||
maxThreads = runtime.NumCPU()
|
||||
}
|
||||
|
||||
//TODO settings: apply_grain, output_invisible_frames, decode_frame_type, inloop_filters
|
||||
|
||||
C.dav1d_default_settings(&d.settings)
|
||||
C.set_threading(&d.settings, C.int(runtime.NumCPU()))
|
||||
C.set_threading(&d.settings, C.int(maxThreads))
|
||||
if ret := C.dav1d_open(&d.ctx, &d.settings); ret != 0 {
|
||||
return nil, fmt.Errorf("error %d", ret)
|
||||
}
|
||||
if d.firstFrame, err = d.Decode(); err != nil {
|
||||
d.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -146,11 +158,14 @@ func (d *Decoder) flushPicture() C.int {
|
|||
}
|
||||
|
||||
func (d *Decoder) Close() {
|
||||
if d.data.sz > 0 {
|
||||
C.dav1d_data_unref(&d.data)
|
||||
}
|
||||
//TODO: close other context
|
||||
|
||||
d.closer.Do(func() {
|
||||
if d.data.sz > 0 {
|
||||
C.dav1d_data_unref(&d.data)
|
||||
}
|
||||
if d.ctx != nil {
|
||||
C.dav1d_close(&d.ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -196,12 +211,8 @@ func (d *Decoder) pictureToFrame() (frame.Frame, error) {
|
|||
}
|
||||
|
||||
properties.ColorSpace.BitDepth = byte(bitDepth)
|
||||
properties.FullColorRange = false
|
||||
|
||||
if d.picture.seq_hdr.color_range == 1 {
|
||||
//TODO check
|
||||
properties.FullColorRange = true
|
||||
}
|
||||
//TODO check
|
||||
properties.FullColorRange = d.picture.seq_hdr.color_range == 1
|
||||
|
||||
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionUnspecified
|
||||
if d.picture.seq_hdr.chr == C.DAV1D_CHR_UNKNOWN {
|
||||
|
@ -316,3 +327,65 @@ func (d *Decoder) Decode() (frame.Frame, error) {
|
|||
func (d *Decoder) Version() string {
|
||||
return Version()
|
||||
}
|
||||
|
||||
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 {
|
||||
return T(val)
|
||||
}
|
||||
if val, ok := v.(int64); ok {
|
||||
return T(val)
|
||||
}
|
||||
if val, ok := v.(uint); ok {
|
||||
return T(val)
|
||||
}
|
||||
if val, ok := v.(uint64); ok {
|
||||
return T(val)
|
||||
}
|
||||
if val, ok := v.(C.int); ok {
|
||||
return T(val)
|
||||
}
|
||||
if val, ok := v.(C.uint); ok {
|
||||
return T(val)
|
||||
}
|
||||
if val, ok := v.(bool); ok {
|
||||
if val {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getSettingBool(m map[string]any, name string, fallback bool) bool {
|
||||
if v, ok := m[name]; ok {
|
||||
if val, ok := v.(string); ok {
|
||||
return val == "false" || val == "f" || val == "n"
|
||||
}
|
||||
if val, ok := v.(int); ok {
|
||||
return val != 0
|
||||
}
|
||||
if val, ok := v.(int64); ok {
|
||||
return val != 0
|
||||
}
|
||||
if val, ok := v.(uint); ok {
|
||||
return val != 0
|
||||
}
|
||||
if val, ok := v.(uint64); ok {
|
||||
return val != 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"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"
|
||||
"github.com/ulikunitz/xz"
|
||||
"io"
|
||||
"runtime"
|
||||
|
@ -25,6 +26,7 @@ type Decoder struct {
|
|||
frameStartOffset int64
|
||||
|
||||
frameCounter int
|
||||
timecodes utilities.Timecodes
|
||||
|
||||
bufPool sync.Pool
|
||||
}
|
||||
|
@ -76,9 +78,42 @@ func NewDecoder(reader io.Reader, settings map[string]any) (*Decoder, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if t, ok := settings["timecodes"]; ok {
|
||||
if tc, ok := t.(utilities.Timecodes); ok {
|
||||
s.timecodes = tc
|
||||
}
|
||||
}
|
||||
|
||||
// Flagged VFR
|
||||
if s.properties.FrameRate.Numerator == 0 && s.properties.FrameRate.Denominator == 0 && len(s.timecodes) == 0 {
|
||||
return nil, errors.New("Y4M indicates VFR but timecodes is not set")
|
||||
}
|
||||
|
||||
if s.properties.PixelAspectRatio.Numerator == 0 && s.properties.PixelAspectRatio.Denominator == 0 {
|
||||
// Assume 1:1
|
||||
s.properties.PixelAspectRatio = utilities.NewRatio(1, 1)
|
||||
}
|
||||
|
||||
s.properties.VFR = s.IsVFR()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Decoder) IsVFR() bool {
|
||||
return len(s.timecodes) != 0
|
||||
}
|
||||
|
||||
// FramePTS Returns -1 if not found
|
||||
func (s *Decoder) FramePTS(n int) int64 {
|
||||
if s.IsVFR() {
|
||||
if n >= len(s.timecodes) {
|
||||
return -1
|
||||
}
|
||||
return s.timecodes[n]
|
||||
}
|
||||
return int64(n)
|
||||
}
|
||||
|
||||
func (s *Decoder) Properties() frame.StreamProperties {
|
||||
return s.properties
|
||||
}
|
||||
|
@ -175,14 +210,19 @@ func (s *Decoder) GetFrame() (parameters map[Parameter][]string, frameObject fra
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
pts := s.FramePTS(s.frameCounter)
|
||||
if pts == -1 {
|
||||
return nil, nil, fmt.Errorf("frame %d PTS could not be calculated", s.frameCounter)
|
||||
}
|
||||
|
||||
if s.properties.ColorSpace.BitDepth > 8 {
|
||||
//it's copied below
|
||||
defer s.bufPool.Put(buf)
|
||||
if frameObject, err = frame.NewUint16FrameFromBytes(s.properties.FrameProperties(), int64(s.frameCounter), buf); err != nil {
|
||||
if frameObject, err = frame.NewUint16FrameFromBytes(s.properties.FrameProperties(), pts, buf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
if f8, err := frame.NewUint8FrameFromBytes(s.properties.FrameProperties(), int64(s.frameCounter), buf); err != nil {
|
||||
if f8, err := frame.NewUint8FrameFromBytes(s.properties.FrameProperties(), pts, buf); err != nil {
|
||||
s.bufPool.Put(buf)
|
||||
return nil, nil, err
|
||||
} else {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/obuwriter"
|
||||
"golang.org/x/exp/constraints"
|
||||
"io"
|
||||
"maps"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
@ -45,6 +46,8 @@ const (
|
|||
func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[string]any) (*Encoder, error) {
|
||||
e := &Encoder{}
|
||||
|
||||
clonedSettings := maps.Clone(settings)
|
||||
|
||||
var aomErr C.aom_codec_err_t
|
||||
|
||||
encoder := C.aom_codec_av1_cx()
|
||||
|
@ -52,15 +55,15 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
return nil, errors.New("unsupported codec")
|
||||
}
|
||||
|
||||
e.cfg.g_usage = C.uint(getSettingUnsigned(settings, "usage", uint(UsageGoodQuality)))
|
||||
e.cfg.g_usage = C.uint(getSettingUnsigned(clonedSettings, "usage", uint(UsageGoodQuality)))
|
||||
|
||||
if getSettingBool(settings, "good", false) {
|
||||
if getSettingBool(clonedSettings, "good", false) {
|
||||
e.cfg.g_usage = UsageGoodQuality
|
||||
}
|
||||
if getSettingBool(settings, "rt", false) {
|
||||
if getSettingBool(clonedSettings, "rt", false) {
|
||||
e.cfg.g_usage = UsageRealtime
|
||||
}
|
||||
if getSettingBool(settings, "allintra", false) {
|
||||
if getSettingBool(clonedSettings, "allintra", false) {
|
||||
e.cfg.g_usage = UsageAllIntra
|
||||
}
|
||||
|
||||
|
@ -131,24 +134,24 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
* \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).
|
||||
*/
|
||||
reciprocalFrameRate := properties.FrameRate.Reciprocal()
|
||||
timeBase := properties.TimeBase()
|
||||
|
||||
e.cfg.g_timebase.num = C.int(reciprocalFrameRate.Numerator)
|
||||
e.cfg.g_timebase.den = C.int(reciprocalFrameRate.Denominator)
|
||||
e.cfg.g_timebase.num = C.int(timeBase.Numerator)
|
||||
e.cfg.g_timebase.den = C.int(timeBase.Denominator)
|
||||
|
||||
// boolean settings
|
||||
|
||||
if getSettingBool(settings, "large-scale-tile", e.cfg.large_scale_tile != 0) {
|
||||
if getSettingBool(clonedSettings, "large-scale-tile", e.cfg.large_scale_tile != 0) {
|
||||
e.cfg.large_scale_tile = 1
|
||||
}
|
||||
if getSettingBool(settings, "monochrome", e.cfg.monochrome != 0) {
|
||||
if getSettingBool(clonedSettings, "monochrome", e.cfg.monochrome != 0) {
|
||||
e.cfg.monochrome = 1
|
||||
}
|
||||
if getSettingBool(settings, "enable-fwd-kf", e.cfg.fwd_kf_enabled != 0) {
|
||||
if getSettingBool(clonedSettings, "enable-fwd-kf", e.cfg.fwd_kf_enabled != 0) {
|
||||
e.cfg.fwd_kf_enabled = 1
|
||||
}
|
||||
|
||||
if getSettingBool(settings, "kf-disabled", false) {
|
||||
if getSettingBool(clonedSettings, "kf-disabled", false) {
|
||||
e.cfg.kf_mode = C.AOM_KF_DISABLED
|
||||
}
|
||||
|
||||
|
@ -198,12 +201,12 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
{&e.cfg.sframe_mode, "sframe-mode"},
|
||||
} {
|
||||
//todo: unset setting from map
|
||||
*s.p = C.uint(getSettingUnsigned(settings, s.n, uint(*s.p)))
|
||||
*s.p = C.uint(getSettingUnsigned(clonedSettings, s.n, uint(*s.p)))
|
||||
}
|
||||
|
||||
// string/enum settings
|
||||
|
||||
endUsage := getSettingString(settings, "end-usage", "vbr")
|
||||
endUsage := getSettingString(clonedSettings, "end-usage", "vbr")
|
||||
|
||||
switch endUsage {
|
||||
case "vbr":
|
||||
|
@ -234,7 +237,7 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
}
|
||||
}
|
||||
|
||||
for k, v := range settings {
|
||||
for k, v := range clonedSettings {
|
||||
if err := func() error {
|
||||
var strVal *C.char
|
||||
if val, ok := v.(string); ok {
|
||||
|
@ -279,7 +282,7 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
}
|
||||
|
||||
var err error
|
||||
if e.w, err = obuwriter.NewWriter(w, properties.Width, properties.Height, 0x31305641, reciprocalFrameRate); err != nil {
|
||||
if e.w, err = obuwriter.NewWriter(w, properties.Width, properties.Height, 0x31305641, timeBase); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -102,11 +102,19 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
|||
|
||||
e.params.i_width = C.int(properties.Width)
|
||||
e.params.i_height = C.int(properties.Height)
|
||||
e.params.b_vfr_input = 0
|
||||
|
||||
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
|
||||
e.params.i_fps_num = C.uint32_t(properties.FrameRate.Numerator)
|
||||
e.params.i_fps_den = C.uint32_t(properties.FrameRate.Denominator)
|
||||
|
||||
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 {
|
||||
|
|
|
@ -11,7 +11,7 @@ type AllowedFrameTypes interface {
|
|||
|
||||
type Frame interface {
|
||||
Properties() Properties
|
||||
// PTS usually frame number
|
||||
// PTS usually frame number, but can differ on VFR
|
||||
PTS() int64
|
||||
|
||||
// Get16 get a pixel sample in 16-bit depth
|
||||
|
|
|
@ -32,6 +32,16 @@ type StreamProperties struct {
|
|||
ColorSpace color.Space `json:"colorspace" yaml:"colorspace"`
|
||||
FrameRate utilities.Ratio `json:"framerate" yaml:"framerate"`
|
||||
FullColorRange bool `json:"fullrange" yaml:"fullrange"`
|
||||
VFR bool `json:"vfr" yaml:"vfr"`
|
||||
}
|
||||
|
||||
func (p StreamProperties) TimeBase() utilities.Ratio {
|
||||
timeBase := p.FrameRate.Reciprocal()
|
||||
if p.VFR {
|
||||
timeBase = utilities.NewRatio(1, 1000)
|
||||
}
|
||||
|
||||
return timeBase
|
||||
}
|
||||
|
||||
func (p StreamProperties) FrameProperties() Properties {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module git.gammaspectra.live/S.O.N.G/Ignite
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/nethruster/go-fraction v0.0.0-20221224165113-1b5f693330ad
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
|
|
2
go.sum
2
go.sum
|
@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/nethruster/go-fraction v0.0.0-20221224165113-1b5f693330ad h1:HtuO+7iVoOXFaZouOdlIgT8gjN3lhk8Gfa2Eah34qSw=
|
||||
github.com/nethruster/go-fraction v0.0.0-20221224165113-1b5f693330ad/go.mod h1:pyvrvZatpiWIxRlZpeEU0DD6SKOxgLMqZV/AU4Yz6V8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
|
|
|
@ -3,6 +3,7 @@ package utilities
|
|||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Ratio struct {
|
||||
|
@ -36,3 +37,36 @@ func (r Ratio) String() string {
|
|||
func (r Ratio) Reciprocal() Ratio {
|
||||
return Ratio{Numerator: r.Denominator, Denominator: r.Numerator}
|
||||
}
|
||||
|
||||
const NTSCRatio = float64(1001) / 1000
|
||||
|
||||
const ratioEpsilon = 1.0e-9
|
||||
|
||||
func NewRatio(n, d int) Ratio {
|
||||
return Ratio{
|
||||
Numerator: n,
|
||||
Denominator: d,
|
||||
}
|
||||
}
|
||||
|
||||
// FPSToRatio Attempt common FPS conversions to ratios
|
||||
func FPSToRatio(fps float64) Ratio {
|
||||
// common NTSC
|
||||
ntscValue := fps * NTSCRatio
|
||||
|
||||
// Number is whole NTSC
|
||||
if math.Abs(ntscValue-math.Round(ntscValue)) < ratioEpsilon {
|
||||
return NewRatio(int(math.Round(ntscValue))*1000, 1001)
|
||||
}
|
||||
|
||||
// Number is whole other
|
||||
if math.Abs(fps-math.Round(fps)) < ratioEpsilon {
|
||||
return NewRatio(int(math.Round(fps)), 1)
|
||||
}
|
||||
|
||||
// Get a few decimals of precision
|
||||
return Ratio{
|
||||
Numerator: int(fps * 1000),
|
||||
Denominator: 1000,
|
||||
}
|
||||
}
|
||||
|
|
99
utilities/timecodes.go
Normal file
99
utilities/timecodes.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package utilities
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/nethruster/go-fraction"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Timecodes Entries in milliseconds
|
||||
type Timecodes []int64
|
||||
|
||||
const timecodesv1Header = "# timecode format v1"
|
||||
|
||||
func ParseTimecodesV1(reader io.Reader) (Timecodes, error) {
|
||||
var tc Timecodes
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
// Read header
|
||||
if !scanner.Scan() {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
switch strings.TrimSpace(scanner.Text()) {
|
||||
case timecodesv1Header:
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected line: %s", strings.TrimSpace(scanner.Text()))
|
||||
}
|
||||
// Read assume line
|
||||
|
||||
if !scanner.Scan() {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
var fps float64
|
||||
|
||||
var from, to int64
|
||||
|
||||
_, err := fmt.Sscanf(strings.ToLower(strings.TrimSpace(scanner.Text())), "assume %f", &fps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assumed := FPSToRatio(fps)
|
||||
assumedFraction, err := fraction.New(assumed.Denominator, assumed.Numerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if assumed.Denominator == 1000 {
|
||||
assumedFraction, err = fraction.FromFloat64(1 / fps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
running, err := fraction.New(0, 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var currentFrame int64
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.ReplaceAll(strings.TrimSpace(scanner.Text()), " ", "")
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := fmt.Sscanf(strings.ReplaceAll(strings.TrimSpace(scanner.Text()), " ", ""), "%d,%d,%f", &from, &to, &fps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ratio := FPSToRatio(fps)
|
||||
frac, err := fraction.New(ratio.Denominator, ratio.Numerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ratio.Denominator == 1000 {
|
||||
frac, err = fraction.FromFloat64(1 / fps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for ; currentFrame < from; currentFrame++ {
|
||||
tc = append(tc, (running.Numerator()*1000)/running.Denominator())
|
||||
running = running.Add(assumedFraction)
|
||||
}
|
||||
|
||||
for ; currentFrame <= to; currentFrame++ {
|
||||
tc = append(tc, (running.Numerator()*1000)/running.Denominator())
|
||||
running = running.Add(frac)
|
||||
}
|
||||
}
|
||||
|
||||
return tc, nil
|
||||
}
|
88
utilities/timecodes_test.go
Normal file
88
utilities/timecodes_test.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package utilities
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var tcTest = []byte(`# timecode format v1
|
||||
Assume 23.976023976024
|
||||
380,382,17.982017982018
|
||||
439,443,29.970029970030
|
||||
488,490,17.982017982018
|
||||
539,543,29.970029970030
|
||||
608,610,17.982017982018
|
||||
775,779,29.970029970030
|
||||
1284,1288,29.970029970030
|
||||
1481,1485,29.970029970030
|
||||
1726,1728,17.982017982018
|
||||
1985,1989,29.970029970030
|
||||
2034,2036,17.982017982018
|
||||
2153,2157,29.970029970030
|
||||
2226,2230,29.970029970030
|
||||
2419,2421,17.982017982018
|
||||
2486,2490,29.970029970030
|
||||
2891,2893,17.982017982018
|
||||
5818,5822,29.970029970030
|
||||
6407,6409,17.982017982018
|
||||
6458,6462,29.970029970030
|
||||
6715,6719,29.970029970030
|
||||
7276,7280,29.970029970030
|
||||
8873,8877,29.970029970030
|
||||
9066,9070,29.970029970030
|
||||
10083,10087,29.970029970030
|
||||
12096,12100,29.970029970030
|
||||
13077,13079,17.982017982018
|
||||
13392,13396,29.970029970030
|
||||
14213,14215,17.982017982018
|
||||
14228,14232,29.970029970030
|
||||
15941,15945,29.970029970030
|
||||
16894,16898,29.970029970030
|
||||
17079,17081,17.982017982018
|
||||
17214,17218,29.970029970030
|
||||
17255,17257,17.982017982018
|
||||
17462,17466,29.970029970030
|
||||
17691,17695,29.970029970030
|
||||
19804,19808,29.970029970030
|
||||
23665,23669,29.970029970030
|
||||
23890,23894,29.970029970030
|
||||
24411,24415,29.970029970030
|
||||
25572,25576,29.970029970030
|
||||
25637,25639,17.982017982018
|
||||
25744,25748,29.970029970030
|
||||
25809,25811,17.982017982018
|
||||
26100,26104,29.970029970030
|
||||
27533,27537,29.970029970030
|
||||
28610,28614,29.970029970030
|
||||
29099,29101,17.982017982018
|
||||
29258,29262,29.970029970030
|
||||
29619,29623,29.970029970030
|
||||
29776,29778,17.982017982018
|
||||
29995,29999,29.970029970030
|
||||
30164,30166,17.982017982018
|
||||
30191,30195,29.970029970030
|
||||
30752,30754,17.982017982018
|
||||
31995,31997,17.982017982018
|
||||
32546,32550,29.970029970030
|
||||
34243,34247,29.970029970030
|
||||
35580,35584,29.970029970030
|
||||
37581,37585,29.970029970030
|
||||
38778,38782,29.970029970030
|
||||
39735,39739,29.970029970030
|
||||
39956,39960,29.970029970030
|
||||
40121,40125,29.970029970030
|
||||
40322,40324,17.982017982018
|
||||
40349,40351,17.982017982018
|
||||
40372,40376,29.970029970030
|
||||
40677,47463,59.940059940059
|
||||
`)
|
||||
|
||||
func TestParseTimecodesV1(t *testing.T) {
|
||||
tc, err := ParseTimecodesV1(bytes.NewReader(tcTest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
runtime.KeepAlive(tc)
|
||||
}
|
Loading…
Reference in a new issue