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 . .
|
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
|
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 /
|
WORKDIR /
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,6 +34,44 @@ type ServerConfig struct {
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
InsecureSSL bool `yaml:"insecure_ssl"`
|
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) {
|
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()
|
urlPath.RawQuery = q.Encode()
|
||||||
|
|
||||||
if s.InsecureSSL {
|
return s.Do(&http.Request{
|
||||||
return InsecureDefaultClient.Do(&http.Request{
|
Method: r.Method,
|
||||||
Method: r.Method,
|
URL: urlPath,
|
||||||
URL: urlPath,
|
Header: r.Header,
|
||||||
Header: r.Header,
|
Body: r.Body,
|
||||||
Body: r.Body,
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return DefaultClient.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) {
|
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.Read.Store(0)
|
||||||
job.Status.Processed.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 {
|
if err != nil {
|
||||||
w.Header().Set("x-encoder-error", "")
|
w.Header().Set("x-encoder-error", "")
|
||||||
w.Header().Set("x-decoder-error", err.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
|
//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-encoder-error", "")
|
||||||
w.Header().Set("x-decoder-error", "mismatched config properties")
|
w.Header().Set("x-decoder-error", "mismatched config properties")
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
|
@ -6,8 +6,10 @@ import (
|
||||||
encode_utils "git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-utils"
|
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/libaom"
|
||||||
"git.gammaspectra.live/S.O.N.G/Ignite/encoder/libx264"
|
"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/cpu"
|
||||||
"github.com/shirou/gopsutil/host"
|
"github.com/shirou/gopsutil/host"
|
||||||
|
"github.com/shirou/gopsutil/load"
|
||||||
"github.com/shirou/gopsutil/mem"
|
"github.com/shirou/gopsutil/mem"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
|
@ -35,11 +37,6 @@ func main() {
|
||||||
authKeys = strings.Split(*authKeysStr, ",")
|
authKeys = strings.Split(*authKeysStr, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
type encoderData struct {
|
|
||||||
Name string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
|
|
||||||
writeHeaders := func(w http.ResponseWriter, r *http.Request) {
|
writeHeaders := func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("x-encoder-max-jobs", strconv.FormatUint(*maxJobs, 10))
|
w.Header().Set("x-encoder-max-jobs", strconv.FormatUint(*maxJobs, 10))
|
||||||
w.Header().Set("x-encoder-current-jobs", strconv.FormatUint(uint64(len(jobs)), 10))
|
w.Header().Set("x-encoder-current-jobs", strconv.FormatUint(uint64(len(jobs)), 10))
|
||||||
|
@ -51,18 +48,13 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusData struct {
|
var statusData encode_utils.StatusData
|
||||||
Encoders []encoderData `json:"encoders"`
|
|
||||||
Host *host.InfoStat `json:"host,omitempty"`
|
|
||||||
CPU []cpu.InfoStat `json:"cpu"`
|
|
||||||
Memory *mem.VirtualMemoryStat `json:"memory,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
statusData.Encoders = append(statusData.Encoders, encoderData{
|
statusData.Encoders = append(statusData.Encoders, encode_utils.StatusEncoderData{
|
||||||
Name: encode_utils.EncoderX264,
|
Name: encode_utils.EncoderX264,
|
||||||
Version: libx264.Version(),
|
Version: libx264.Version(),
|
||||||
})
|
})
|
||||||
statusData.Encoders = append(statusData.Encoders, encoderData{
|
statusData.Encoders = append(statusData.Encoders, encode_utils.StatusEncoderData{
|
||||||
Name: encode_utils.EncoderAOM,
|
Name: encode_utils.EncoderAOM,
|
||||||
Version: libaom.Version(),
|
Version: libaom.Version(),
|
||||||
})
|
})
|
||||||
|
@ -80,6 +72,11 @@ func main() {
|
||||||
statusData.Host = hostInfo
|
statusData.Host = hostInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadInfo, err := load.Avg()
|
||||||
|
if err == nil {
|
||||||
|
statusData.Load = loadInfo
|
||||||
|
}
|
||||||
|
|
||||||
memInfo, err := mem.VirtualMemory()
|
memInfo, err := mem.VirtualMemory()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
statusData.Memory = memInfo
|
statusData.Memory = memInfo
|
||||||
|
@ -128,6 +125,15 @@ func main() {
|
||||||
return
|
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 {
|
if job := func() *Job {
|
||||||
JobsMutex.Lock()
|
JobsMutex.Lock()
|
||||||
defer JobsMutex.Unlock()
|
defer JobsMutex.Unlock()
|
||||||
|
@ -365,7 +371,7 @@ func main() {
|
||||||
})
|
})
|
||||||
|
|
||||||
s := http.Server{
|
s := http.Server{
|
||||||
ReadTimeout: time.Second * 10,
|
ReadTimeout: 0,
|
||||||
IdleTimeout: time.Second * 60,
|
IdleTimeout: time.Second * 60,
|
||||||
WriteTimeout: 0,
|
WriteTimeout: 0,
|
||||||
Addr: *listenAddr,
|
Addr: *listenAddr,
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package encode_utils
|
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 {
|
type JobConfig struct {
|
||||||
Encoder struct {
|
Encoder struct {
|
||||||
|
@ -9,6 +12,9 @@ type JobConfig struct {
|
||||||
} `json:"encoder" yaml:"encoder"`
|
} `json:"encoder" yaml:"encoder"`
|
||||||
|
|
||||||
Properties frame.StreamProperties `json:"properties" yaml:"properties"`
|
Properties frame.StreamProperties `json:"properties" yaml:"properties"`
|
||||||
|
|
||||||
|
TimecodesV1 string `json:"timecodes_v1" yaml:"timecodes_v1"`
|
||||||
|
Timecodes utilities.Timecodes `json:"timecodes" yaml:"timecodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
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) {
|
void set_threading(Dav1dSettings* s, int threads) {
|
||||||
#if DAV1D_VERSION_AT_LEAST(6,0)
|
#if DAV1D_VERSION_AT_LEAST(6,0)
|
||||||
|
s->n_threads = threads;
|
||||||
#else
|
#else
|
||||||
s->n_tile_threads = MIN(floor(sqrt(threads)), DAV1D_MAX_TILE_THREADS);
|
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);
|
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/frame"
|
||||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
||||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/ivfreader"
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/ivfreader"
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
"io"
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
@ -35,6 +37,8 @@ type Decoder struct {
|
||||||
picture C.Dav1dPicture
|
picture C.Dav1dPicture
|
||||||
data C.Dav1dData
|
data C.Dav1dData
|
||||||
|
|
||||||
|
closer sync.Once
|
||||||
|
|
||||||
bufPool sync.Pool
|
bufPool sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,12 +61,20 @@ func NewDecoder(r io.Reader, settings map[string]any) (d *Decoder, err error) {
|
||||||
return nil, err
|
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.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 {
|
if ret := C.dav1d_open(&d.ctx, &d.settings); ret != 0 {
|
||||||
return nil, fmt.Errorf("error %d", ret)
|
return nil, fmt.Errorf("error %d", ret)
|
||||||
}
|
}
|
||||||
if d.firstFrame, err = d.Decode(); err != nil {
|
if d.firstFrame, err = d.Decode(); err != nil {
|
||||||
|
d.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,11 +158,14 @@ func (d *Decoder) flushPicture() C.int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) Close() {
|
func (d *Decoder) Close() {
|
||||||
if d.data.sz > 0 {
|
d.closer.Do(func() {
|
||||||
C.dav1d_data_unref(&d.data)
|
if d.data.sz > 0 {
|
||||||
}
|
C.dav1d_data_unref(&d.data)
|
||||||
//TODO: close other context
|
}
|
||||||
|
if d.ctx != nil {
|
||||||
|
C.dav1d_close(&d.ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -196,12 +211,8 @@ func (d *Decoder) pictureToFrame() (frame.Frame, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
properties.ColorSpace.BitDepth = byte(bitDepth)
|
properties.ColorSpace.BitDepth = byte(bitDepth)
|
||||||
properties.FullColorRange = false
|
//TODO check
|
||||||
|
properties.FullColorRange = d.picture.seq_hdr.color_range == 1
|
||||||
if d.picture.seq_hdr.color_range == 1 {
|
|
||||||
//TODO check
|
|
||||||
properties.FullColorRange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionUnspecified
|
properties.ColorSpace.ChromaSamplePosition = color.ChromaSamplePositionUnspecified
|
||||||
if d.picture.seq_hdr.chr == C.DAV1D_CHR_UNKNOWN {
|
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 {
|
func (d *Decoder) Version() string {
|
||||||
return Version()
|
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"
|
"fmt"
|
||||||
"git.gammaspectra.live/S.O.N.G/Ignite/color"
|
"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/frame"
|
||||||
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities"
|
||||||
"github.com/ulikunitz/xz"
|
"github.com/ulikunitz/xz"
|
||||||
"io"
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -25,6 +26,7 @@ type Decoder struct {
|
||||||
frameStartOffset int64
|
frameStartOffset int64
|
||||||
|
|
||||||
frameCounter int
|
frameCounter int
|
||||||
|
timecodes utilities.Timecodes
|
||||||
|
|
||||||
bufPool sync.Pool
|
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
|
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 {
|
func (s *Decoder) Properties() frame.StreamProperties {
|
||||||
return s.properties
|
return s.properties
|
||||||
}
|
}
|
||||||
|
@ -175,14 +210,19 @@ func (s *Decoder) GetFrame() (parameters map[Parameter][]string, frameObject fra
|
||||||
return nil, nil, err
|
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 {
|
if s.properties.ColorSpace.BitDepth > 8 {
|
||||||
//it's copied below
|
//it's copied below
|
||||||
defer s.bufPool.Put(buf)
|
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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
s.bufPool.Put(buf)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/obuwriter"
|
"git.gammaspectra.live/S.O.N.G/Ignite/utilities/obuwriter"
|
||||||
"golang.org/x/exp/constraints"
|
"golang.org/x/exp/constraints"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -45,6 +46,8 @@ const (
|
||||||
func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[string]any) (*Encoder, error) {
|
func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[string]any) (*Encoder, error) {
|
||||||
e := &Encoder{}
|
e := &Encoder{}
|
||||||
|
|
||||||
|
clonedSettings := maps.Clone(settings)
|
||||||
|
|
||||||
var aomErr C.aom_codec_err_t
|
var aomErr C.aom_codec_err_t
|
||||||
|
|
||||||
encoder := C.aom_codec_av1_cx()
|
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")
|
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
|
e.cfg.g_usage = UsageGoodQuality
|
||||||
}
|
}
|
||||||
if getSettingBool(settings, "rt", false) {
|
if getSettingBool(clonedSettings, "rt", false) {
|
||||||
e.cfg.g_usage = UsageRealtime
|
e.cfg.g_usage = UsageRealtime
|
||||||
}
|
}
|
||||||
if getSettingBool(settings, "allintra", false) {
|
if getSettingBool(clonedSettings, "allintra", false) {
|
||||||
e.cfg.g_usage = UsageAllIntra
|
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
|
* \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).
|
* 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.num = C.int(timeBase.Numerator)
|
||||||
e.cfg.g_timebase.den = C.int(reciprocalFrameRate.Denominator)
|
e.cfg.g_timebase.den = C.int(timeBase.Denominator)
|
||||||
|
|
||||||
// boolean settings
|
// 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
|
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
|
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
|
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
|
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"},
|
{&e.cfg.sframe_mode, "sframe-mode"},
|
||||||
} {
|
} {
|
||||||
//todo: unset setting from map
|
//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
|
// string/enum settings
|
||||||
|
|
||||||
endUsage := getSettingString(settings, "end-usage", "vbr")
|
endUsage := getSettingString(clonedSettings, "end-usage", "vbr")
|
||||||
|
|
||||||
switch endUsage {
|
switch endUsage {
|
||||||
case "vbr":
|
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 {
|
if err := func() error {
|
||||||
var strVal *C.char
|
var strVal *C.char
|
||||||
if val, ok := v.(string); ok {
|
if val, ok := v.(string); ok {
|
||||||
|
@ -279,7 +282,7 @@ func NewEncoder(w io.Writer, properties frame.StreamProperties, settings map[str
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
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
|
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_width = C.int(properties.Width)
|
||||||
e.params.i_height = C.int(properties.Height)
|
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_repeat_headers = 1
|
||||||
e.params.b_annexb = 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 {
|
if properties.FullColorRange {
|
||||||
e.params.vui.b_fullrange = 1
|
e.params.vui.b_fullrange = 1
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,7 +11,7 @@ type AllowedFrameTypes interface {
|
||||||
|
|
||||||
type Frame interface {
|
type Frame interface {
|
||||||
Properties() Properties
|
Properties() Properties
|
||||||
// PTS usually frame number
|
// PTS usually frame number, but can differ on VFR
|
||||||
PTS() int64
|
PTS() int64
|
||||||
|
|
||||||
// Get16 get a pixel sample in 16-bit depth
|
// Get16 get a pixel sample in 16-bit depth
|
||||||
|
|
|
@ -32,6 +32,16 @@ type StreamProperties struct {
|
||||||
ColorSpace color.Space `json:"colorspace" yaml:"colorspace"`
|
ColorSpace color.Space `json:"colorspace" yaml:"colorspace"`
|
||||||
FrameRate utilities.Ratio `json:"framerate" yaml:"framerate"`
|
FrameRate utilities.Ratio `json:"framerate" yaml:"framerate"`
|
||||||
FullColorRange bool `json:"fullrange" yaml:"fullrange"`
|
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 {
|
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
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/nethruster/go-fraction v0.0.0-20221224165113-1b5f693330ad
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/ulikunitz/xz v0.5.11
|
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/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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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=
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
|
|
|
@ -3,6 +3,7 @@ package utilities
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ratio struct {
|
type Ratio struct {
|
||||||
|
@ -36,3 +37,36 @@ func (r Ratio) String() string {
|
||||||
func (r Ratio) Reciprocal() Ratio {
|
func (r Ratio) Reciprocal() Ratio {
|
||||||
return Ratio{Numerator: r.Denominator, Denominator: r.Numerator}
|
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