Implemented encode server
This commit is contained in:
parent
f605079083
commit
241ee4164e
113
cli/encode-server/encode.go
Normal file
113
cli/encode-server/encode.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/decoder/y4m"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
"github.com/ulikunitz/xz"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func handleDecompress(contentEncoding string, reader io.ReadCloser) (io.ReadCloser, error) {
|
||||
var err error
|
||||
switch contentEncoding {
|
||||
case "gzip":
|
||||
reader, err = gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "bzip2":
|
||||
reader = io.NopCloser(bzip2.NewReader(reader))
|
||||
case "deflate":
|
||||
reader = flate.NewReader(reader)
|
||||
case "xz":
|
||||
r, err := xz.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = io.NopCloser(r)
|
||||
case "":
|
||||
return reader, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported encoding")
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func encodeFromReader(reader io.ReadCloser, job *Job, w http.ResponseWriter) {
|
||||
defer reader.Close()
|
||||
defer func() {
|
||||
log.Printf("[job %s] finished, took %s", job.Id, time.Now().Sub(time.Unix(int64(job.Status.Start.Load()), 0)))
|
||||
}()
|
||||
|
||||
job.Status.Start.Store(uint64(time.Now().Unix()))
|
||||
job.Status.Read.Store(0)
|
||||
job.Status.Processed.Store(0)
|
||||
|
||||
decoder, err := y4m.NewDecoder(reader, nil)
|
||||
if err != nil {
|
||||
w.Header().Set("x-encoder-error", "")
|
||||
w.Header().Set("x-decoder-error", err.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
//check decoder frame properties match
|
||||
if decoder.Properties() != job.Config.Properties {
|
||||
w.Header().Set("x-encoder-error", "")
|
||||
w.Header().Set("x-decoder-error", "mismatched config properties")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Add("Trailer", "x-encoder-error, x-decoder-error")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
job.Logger = log.New(os.Stderr, fmt.Sprintf("[job %s] ", job.Id), log.LstdFlags)
|
||||
err = job.Init(w)
|
||||
if err != nil {
|
||||
w.Header().Set("x-encoder-error", err.Error())
|
||||
w.Header().Set("x-decoder-error", "")
|
||||
return
|
||||
}
|
||||
defer job.Close()
|
||||
|
||||
var f frame.Frame
|
||||
for {
|
||||
f, err = decoder.Decode()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
//we are done
|
||||
break
|
||||
}
|
||||
w.Header().Set("x-encoder-error", "")
|
||||
w.Header().Set("x-decoder-error", err.Error())
|
||||
return
|
||||
}
|
||||
job.Status.Read.Add(1)
|
||||
|
||||
err = job.Encoder.Encode(f)
|
||||
if err != nil {
|
||||
w.Header().Set("x-encoder-error", err.Error())
|
||||
w.Header().Set("x-decoder-error", "")
|
||||
return
|
||||
}
|
||||
job.Status.Processed.Add(1)
|
||||
//log.Printf("[job %s] %d", job.Id, job.Status.Read.Load())
|
||||
}
|
||||
|
||||
err = job.Encoder.Flush()
|
||||
if err != nil {
|
||||
w.Header().Set("x-encoder-error", err.Error())
|
||||
w.Header().Set("x-decoder-error", "")
|
||||
return
|
||||
}
|
||||
}
|
127
cli/encode-server/job.go
Normal file
127
cli/encode-server/job.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
encode_utils "git.gammaspectra.live/S.O.N.G/Ignite/cli/encode-utils"
|
||||
"git.gammaspectra.live/S.O.N.G/Ignite/encoder"
|
||||
"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"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var JobsMutex sync.Mutex
|
||||
|
||||
var jobs []*Job
|
||||
|
||||
type JobId string
|
||||
|
||||
type Job struct {
|
||||
Id JobId `json:"id" yaml:"id"`
|
||||
Config encode_utils.JobConfig `json:"config" yaml:"config"`
|
||||
Logger utilities.Logger `json:"-" yaml:"-"`
|
||||
Encoder encoder.Encoder `json:"-" yaml:"-"`
|
||||
sync.Mutex `json:"-" yaml:"-"`
|
||||
Status struct {
|
||||
Start atomic.Uint64
|
||||
Read atomic.Uint64
|
||||
Processed atomic.Uint64
|
||||
} `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func (j *Job) Init(w io.Writer) error {
|
||||
if j.Encoder != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
|
||||
// Do not modify original settings
|
||||
settings := maps.Clone(j.Config.Encoder.Settings)
|
||||
|
||||
switch j.Config.Encoder.Name {
|
||||
case encode_utils.EncoderX264:
|
||||
x264enc, err := libx264.NewEncoder(w, j.Config.Properties, settings, j.Logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j.Encoder = x264enc
|
||||
case encode_utils.EncoderAOM:
|
||||
aomenc, err := libaom.NewEncoder(w, j.Config.Properties, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j.Encoder = aomenc
|
||||
default:
|
||||
return errors.New("encoder not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *Job) Close() error {
|
||||
if j.Encoder != nil {
|
||||
j.Encoder.Close()
|
||||
j.Encoder = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateJob Creates a Job. JobsMutex must be held
|
||||
func CreateJob(cfg encode_utils.JobConfig) (*Job, error) {
|
||||
job := &Job{
|
||||
Config: cfg,
|
||||
Logger: utilities.DefaultLogger(),
|
||||
}
|
||||
var idBuf [32]byte
|
||||
_, err := io.ReadFull(rand.Reader, idBuf[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
job.Id = JobId(base64.RawURLEncoding.EncodeToString(idBuf[:]))
|
||||
|
||||
if slices.ContainsFunc(jobs, func(j *Job) bool {
|
||||
return j.Id == job.Id
|
||||
}) {
|
||||
return nil, errors.New("job already exists")
|
||||
}
|
||||
|
||||
jobs = append(jobs, job)
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// GetJob Removes a Job.
|
||||
func GetJob(jobId JobId) *Job {
|
||||
JobsMutex.Lock()
|
||||
defer JobsMutex.Unlock()
|
||||
if i := slices.IndexFunc(jobs, func(j *Job) bool {
|
||||
return j.Id == jobId
|
||||
}); i != -1 {
|
||||
return jobs[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveJob Removes a Job
|
||||
func RemoveJob(jobId JobId) error {
|
||||
JobsMutex.Lock()
|
||||
defer JobsMutex.Unlock()
|
||||
if i := slices.IndexFunc(jobs, func(j *Job) bool {
|
||||
return j.Id == jobId
|
||||
}); i == -1 {
|
||||
return errors.New("job does not exist")
|
||||
} else {
|
||||
err := jobs[i].Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobs = slices.Delete(jobs, i, i+1)
|
||||
return nil
|
||||
}
|
||||
}
|
381
cli/encode-server/main.go
Normal file
381
cli/encode-server/main.go
Normal file
|
@ -0,0 +1,381 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
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"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
maxJobs := flag.Uint64("max-jobs", 8, "Maximum number of active jobs")
|
||||
authKeysStr := flag.String("authKeys", "", "Allowed auth keys, comma separated. Leave empty to disable")
|
||||
listenAddr := flag.String("listen", "0.0.0.0:8383", "Listen address for the server")
|
||||
flag.Parse()
|
||||
|
||||
serveMux := http.NewServeMux()
|
||||
|
||||
var authKeys []string
|
||||
if strings.TrimSpace(*authKeysStr) != "" {
|
||||
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))
|
||||
}
|
||||
|
||||
serveMux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(authKeys) > 0 && !slices.Contains(authKeys, r.URL.Query().Get("k")) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
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"`
|
||||
}
|
||||
|
||||
statusData.Encoders = append(statusData.Encoders, encoderData{
|
||||
Name: encode_utils.EncoderX264,
|
||||
Version: libx264.Version(),
|
||||
})
|
||||
statusData.Encoders = append(statusData.Encoders, encoderData{
|
||||
Name: encode_utils.EncoderAOM,
|
||||
Version: libaom.Version(),
|
||||
})
|
||||
|
||||
cpuInfo, err := cpu.Info()
|
||||
if err != nil {
|
||||
//fallback
|
||||
statusData.CPU = make([]cpu.InfoStat, runtime.NumCPU())
|
||||
} else {
|
||||
statusData.CPU = cpuInfo
|
||||
}
|
||||
|
||||
hostInfo, err := host.Info()
|
||||
if err == nil {
|
||||
statusData.Host = hostInfo
|
||||
}
|
||||
|
||||
memInfo, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
statusData.Memory = memInfo
|
||||
}
|
||||
|
||||
data, err := json.Marshal(statusData)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
JobsMutex.Lock()
|
||||
defer JobsMutex.Unlock()
|
||||
writeHeaders(w, r)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", strconv.FormatUint(uint64(len(data)), 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(data)
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/create", func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(authKeys) > 0 && !slices.Contains(authKeys, r.URL.Query().Get("k")) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var cfg encode_utils.JobConfig
|
||||
|
||||
err = yaml.Unmarshal(buf, &cfg)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if job := func() *Job {
|
||||
JobsMutex.Lock()
|
||||
defer JobsMutex.Unlock()
|
||||
if uint64(len(jobs)) >= *maxJobs {
|
||||
writeHeaders(w, r)
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return nil
|
||||
}
|
||||
|
||||
job, err := CreateJob(cfg)
|
||||
if err != nil {
|
||||
writeHeaders(w, r)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return nil
|
||||
}
|
||||
|
||||
writeHeaders(w, r)
|
||||
return job
|
||||
}(); job == nil {
|
||||
return
|
||||
} else {
|
||||
job.Lock()
|
||||
defer job.Unlock()
|
||||
data, err := json.Marshal(job)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", strconv.FormatUint(uint64(len(data)), 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(data)
|
||||
log.Printf("[job %s] created", job.Id)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/start", func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(authKeys) > 0 && !slices.Contains(authKeys, r.URL.Query().Get("k")) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
jobId := query.Get("jobId")
|
||||
job := GetJob(JobId(jobId))
|
||||
if job == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !job.TryLock() {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
return
|
||||
}
|
||||
defer job.Unlock()
|
||||
|
||||
if r.Header.Get("Content-Type") != "video/x-yuv4mpeg2" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reader, err := handleDecompress(r.Header.Get("Content-Encoding"), r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
log.Printf("[job %s] started POST", job.Id)
|
||||
encodeFromReader(reader, job, w)
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/startURL", func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(authKeys) > 0 && !slices.Contains(authKeys, r.URL.Query().Get("k")) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
jobId := query.Get("jobId")
|
||||
job := GetJob(JobId(jobId))
|
||||
if job == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !job.TryLock() {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
return
|
||||
}
|
||||
defer job.Unlock()
|
||||
|
||||
urlVal, err := url.Parse(query.Get("url"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(&http.Request{
|
||||
Method: "GET",
|
||||
URL: urlVal,
|
||||
Header: http.Header{
|
||||
"Accept-Encoding": {"gzip, deflate, bzip2, xz"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
defer io.ReadAll(response.Body)
|
||||
defer response.Body.Close()
|
||||
|
||||
reader := response.Body
|
||||
if response.Header.Get("Content-Encoding") != "" {
|
||||
reader, err = handleDecompress(response.Header.Get("Content-Encoding"), reader)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle filenames
|
||||
switch strings.ToLower(path.Ext(urlVal.Path)) {
|
||||
case "gz":
|
||||
reader, err = handleDecompress("gz", reader)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
case "bz2", "bzip2":
|
||||
reader, err = handleDecompress("bz2", reader)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
case "xz":
|
||||
reader, err = handleDecompress("xz", reader)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[job %s] started URL", job.Id)
|
||||
encodeFromReader(reader, job, w)
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/job", func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(authKeys) > 0 && !slices.Contains(authKeys, r.URL.Query().Get("k")) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
jobId := query.Get("jobId")
|
||||
job := GetJob(JobId(jobId))
|
||||
if job == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var jobData struct {
|
||||
Start uint64 `json:"start"`
|
||||
Read uint64 `json:"in"`
|
||||
Processed uint64 `json:"out"`
|
||||
}
|
||||
jobData.Start = job.Status.Start.Load()
|
||||
jobData.Read = job.Status.Read.Load()
|
||||
jobData.Processed = job.Status.Processed.Load()
|
||||
|
||||
data, err := json.Marshal(jobData)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", strconv.FormatUint(uint64(len(data)), 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(data)
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/remove", func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(authKeys) > 0 && !slices.Contains(authKeys, r.URL.Query().Get("k")) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
jobId := query.Get("jobId")
|
||||
job := GetJob(JobId(jobId))
|
||||
if job == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !job.TryLock() {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
return
|
||||
}
|
||||
defer job.Unlock()
|
||||
|
||||
err := job.Close()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = RemoveJob(job.Id)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[job %s] removed", job.Id)
|
||||
})
|
||||
|
||||
s := http.Server{
|
||||
ReadTimeout: time.Second * 10,
|
||||
IdleTimeout: time.Second * 60,
|
||||
WriteTimeout: 0,
|
||||
Addr: *listenAddr,
|
||||
Handler: serveMux,
|
||||
}
|
||||
s.SetKeepAlivesEnabled(true)
|
||||
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
}
|
17
cli/encode-utils/config.go
Normal file
17
cli/encode-utils/config.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package encode_utils
|
||||
|
||||
import "git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
|
||||
type JobConfig struct {
|
||||
Encoder struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Settings map[string]any `json:"settings" yaml:"settings"`
|
||||
} `json:"encoder" yaml:"encoder"`
|
||||
|
||||
Properties frame.StreamProperties `json:"properties" yaml:"properties"`
|
||||
}
|
||||
|
||||
const (
|
||||
EncoderX264 = "libx264"
|
||||
EncoderAOM = "libaom"
|
||||
)
|
|
@ -3,6 +3,8 @@ package color
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
@ -13,6 +15,31 @@ type Space struct {
|
|||
BitDepth byte
|
||||
}
|
||||
|
||||
func (c *Space) UnmarshalJSON(buf []byte) error {
|
||||
if len(buf) < 2 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
s, err := NewColorFormatFromString(string(buf[1 : len(buf)-1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Space) UnmarshalYAML(node *yaml.Node) error {
|
||||
s, err := NewColorFormatFromString(node.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Space) MarshalJSON() ([]byte, error) {
|
||||
return []byte("\"" + c.String() + "\""), nil
|
||||
}
|
||||
|
||||
func (c Space) String() string {
|
||||
if c.ChromaSampling.J == 4 && c.ChromaSampling.A == 2 && c.ChromaSampling.B == 0 && c.BitDepth == 8 {
|
||||
switch c.ChromaSamplePosition {
|
||||
|
|
|
@ -3,7 +3,7 @@ package encoder
|
|||
import "git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||
|
||||
type Encoder interface {
|
||||
Encode(pts int64, f frame.Frame) error
|
||||
Encode(f frame.Frame) error
|
||||
EncodeStream(stream *frame.Stream) error
|
||||
Flush() error
|
||||
Close()
|
||||
|
|
|
@ -23,15 +23,15 @@ func NewStream(properties StreamProperties) (*Stream, chan<- Frame) {
|
|||
|
||||
type StreamProperties struct {
|
||||
// Width could be not populated until the first frame is read. Frame can contain different settings.
|
||||
Width int
|
||||
Width int `json:"width" yaml:"width"`
|
||||
// Height could be not populated until the first frame is read. Frame can contain different settings.
|
||||
Height int
|
||||
Height int `json:"height" yaml:"height"`
|
||||
// PixelAspectRatio could be not populated until the first frame is read. Frame can contain different settings.
|
||||
PixelAspectRatio utilities.Ratio
|
||||
PixelAspectRatio utilities.Ratio `json:"par" yaml:"par"`
|
||||
// ColorSpace could be not populated until the first frame is read. Frame can contain different settings.
|
||||
ColorSpace color.Space
|
||||
FrameRate utilities.Ratio
|
||||
FullColorRange bool
|
||||
ColorSpace color.Space `json:"colorspace" yaml:"colorspace"`
|
||||
FrameRate utilities.Ratio `json:"framerate" yaml:"framerate"`
|
||||
FullColorRange bool `json:"fullrange" yaml:"fullrange"`
|
||||
}
|
||||
|
||||
func (p StreamProperties) FrameProperties() Properties {
|
||||
|
|
8
go.mod
8
go.mod
|
@ -3,13 +3,19 @@ module git.gammaspectra.live/S.O.N.G/Ignite
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
)
|
||||
|
|
15
go.sum
15
go.sum
|
@ -1,8 +1,12 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
@ -10,10 +14,21 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package utilities
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Ratio struct {
|
||||
Numerator int
|
||||
|
@ -11,6 +14,20 @@ func (r Ratio) Float64() float64 {
|
|||
return float64(r.Numerator) / float64(r.Denominator)
|
||||
}
|
||||
|
||||
func (r *Ratio) UnmarshalJSON(buf []byte) error {
|
||||
_, err := fmt.Sscanf(string(buf), "\"%d:%d\"", &r.Numerator, &r.Denominator)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Ratio) UnmarshalYAML(node *yaml.Node) error {
|
||||
_, err := fmt.Sscanf(node.Value, "%d:%d", &r.Numerator, &r.Denominator)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r Ratio) MarshalJSON() ([]byte, error) {
|
||||
return []byte("\"" + r.String() + "\""), nil
|
||||
}
|
||||
|
||||
func (r Ratio) String() string {
|
||||
return fmt.Sprintf("%d:%d", r.Numerator, r.Denominator)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue