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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -13,6 +15,31 @@ type Space struct {
|
||||||
BitDepth byte
|
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 {
|
func (c Space) String() string {
|
||||||
if c.ChromaSampling.J == 4 && c.ChromaSampling.A == 2 && c.ChromaSampling.B == 0 && c.BitDepth == 8 {
|
if c.ChromaSampling.J == 4 && c.ChromaSampling.A == 2 && c.ChromaSampling.B == 0 && c.BitDepth == 8 {
|
||||||
switch c.ChromaSamplePosition {
|
switch c.ChromaSamplePosition {
|
||||||
|
|
|
@ -3,7 +3,7 @@ package encoder
|
||||||
import "git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
import "git.gammaspectra.live/S.O.N.G/Ignite/frame"
|
||||||
|
|
||||||
type Encoder interface {
|
type Encoder interface {
|
||||||
Encode(pts int64, f frame.Frame) error
|
Encode(f frame.Frame) error
|
||||||
EncodeStream(stream *frame.Stream) error
|
EncodeStream(stream *frame.Stream) error
|
||||||
Flush() error
|
Flush() error
|
||||||
Close()
|
Close()
|
||||||
|
|
|
@ -23,15 +23,15 @@ func NewStream(properties StreamProperties) (*Stream, chan<- Frame) {
|
||||||
|
|
||||||
type StreamProperties struct {
|
type StreamProperties struct {
|
||||||
// Width could be not populated until the first frame is read. Frame can contain different settings.
|
// 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 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 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 could be not populated until the first frame is read. Frame can contain different settings.
|
||||||
ColorSpace color.Space
|
ColorSpace color.Space `json:"colorspace" yaml:"colorspace"`
|
||||||
FrameRate utilities.Ratio
|
FrameRate utilities.Ratio `json:"framerate" yaml:"framerate"`
|
||||||
FullColorRange bool
|
FullColorRange bool `json:"fullrange" yaml:"fullrange"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p StreamProperties) FrameProperties() Properties {
|
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
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
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
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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
|
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.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 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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
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/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
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 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
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 h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package utilities
|
package utilities
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
type Ratio struct {
|
type Ratio struct {
|
||||||
Numerator int
|
Numerator int
|
||||||
|
@ -11,6 +14,20 @@ func (r Ratio) Float64() float64 {
|
||||||
return float64(r.Numerator) / float64(r.Denominator)
|
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 {
|
func (r Ratio) String() string {
|
||||||
return fmt.Sprintf("%d:%d", r.Numerator, r.Denominator)
|
return fmt.Sprintf("%d:%d", r.Numerator, r.Denominator)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue