Files
core/restream/app/process.go

419 lines
9.0 KiB
Go

package app
import (
"bytes"
"crypto/md5"
"encoding/json"
"strconv"
"strings"
"sync"
"github.com/datarhei/core/v16/ffmpeg/parse"
"github.com/datarhei/core/v16/process"
)
type ConfigIOCleanup struct {
Pattern string
MaxFiles uint
MaxFileAge uint
PurgeOnDelete bool
}
func (c *ConfigIOCleanup) HashString() string {
b := strings.Builder{}
b.WriteString(c.Pattern)
b.WriteString(strconv.FormatUint(uint64(c.MaxFiles), 10))
b.WriteString(strconv.FormatUint(uint64(c.MaxFileAge), 10))
b.WriteString(strconv.FormatBool(c.PurgeOnDelete))
return b.String()
}
type ConfigIO struct {
ID string
Address string
Options []string
Cleanup []ConfigIOCleanup
}
func (io *ConfigIO) Clone() ConfigIO {
clone := ConfigIO{
ID: io.ID,
Address: io.Address,
}
clone.Options = make([]string, len(io.Options))
copy(clone.Options, io.Options)
clone.Cleanup = make([]ConfigIOCleanup, len(io.Cleanup))
copy(clone.Cleanup, io.Cleanup)
return clone
}
func (io *ConfigIO) HashString() string {
b := strings.Builder{}
b.WriteString(io.ID)
b.WriteString(io.Address)
b.WriteString(strings.Join(io.Options, ","))
for _, x := range io.Cleanup {
b.WriteString(x.HashString())
}
return b.String()
}
type Config struct {
ID string
Reference string
Owner string
Domain string
FFVersion string
Input []ConfigIO
Output []ConfigIO
Options []string
Reconnect bool
ReconnectDelay uint64 // seconds
Autostart bool
StaleTimeout uint64 // seconds
Timeout uint64 // seconds
Scheduler string // crontab pattern or RFC3339 timestamp
LogPatterns []string // will be interpreted as regular expressions
LimitCPU float64 // percent
LimitMemory uint64 // bytes
LimitWaitFor uint64 // seconds
}
func (config *Config) Clone() *Config {
clone := &Config{
ID: config.ID,
Reference: config.Reference,
Owner: config.Owner,
Domain: config.Domain,
FFVersion: config.FFVersion,
Reconnect: config.Reconnect,
ReconnectDelay: config.ReconnectDelay,
Autostart: config.Autostart,
StaleTimeout: config.StaleTimeout,
Timeout: config.Timeout,
Scheduler: config.Scheduler,
LimitCPU: config.LimitCPU,
LimitMemory: config.LimitMemory,
LimitWaitFor: config.LimitWaitFor,
}
clone.Input = make([]ConfigIO, len(config.Input))
for i, io := range config.Input {
clone.Input[i] = io.Clone()
}
clone.Output = make([]ConfigIO, len(config.Output))
for i, io := range config.Output {
clone.Output[i] = io.Clone()
}
clone.Options = make([]string, len(config.Options))
copy(clone.Options, config.Options)
clone.LogPatterns = make([]string, len(config.LogPatterns))
copy(clone.LogPatterns, config.LogPatterns)
return clone
}
// CreateCommand created the FFmpeg command from this config.
func (config *Config) CreateCommand() []string {
var command []string
// Copy global options
command = append(command, config.Options...)
for _, input := range config.Input {
// Add the resolved input to the process command
command = append(command, input.Options...)
command = append(command, "-i", input.Address)
}
for _, output := range config.Output {
// Add the resolved output to the process command
command = append(command, output.Options...)
command = append(command, output.Address)
}
return command
}
func (config *Config) String() string {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err.Error()
}
return string(data)
}
func (config *Config) Hash() []byte {
b := bytes.Buffer{}
b.WriteString(config.ID)
b.WriteString(config.Reference)
b.WriteString(config.Owner)
b.WriteString(config.Domain)
b.WriteString(config.Scheduler)
b.WriteString(strings.Join(config.Options, ","))
b.WriteString(strings.Join(config.LogPatterns, ","))
b.WriteString(strconv.FormatBool(config.Reconnect))
b.WriteString(strconv.FormatBool(config.Autostart))
b.WriteString(strconv.FormatUint(config.ReconnectDelay, 10))
b.WriteString(strconv.FormatUint(config.StaleTimeout, 10))
b.WriteString(strconv.FormatUint(config.Timeout, 10))
b.WriteString(strconv.FormatUint(config.LimitMemory, 10))
b.WriteString(strconv.FormatUint(config.LimitWaitFor, 10))
b.WriteString(strconv.FormatFloat(config.LimitCPU, 'f', -1, 64))
for _, x := range config.Input {
b.WriteString(x.HashString())
}
for _, x := range config.Output {
b.WriteString(x.HashString())
}
sum := md5.Sum(b.Bytes())
return sum[:]
}
func (c *Config) Equal(a *Config) bool {
return bytes.Equal(c.Hash(), a.Hash())
}
func (c *Config) ProcessID() ProcessID {
return ProcessID{
ID: c.ID,
Domain: c.Domain,
}
}
type order struct {
order string
lock sync.RWMutex
}
func NewOrder(o string) order {
return order{
order: o,
}
}
func (o *order) Clone() order {
return order{
order: o.order,
}
}
func (o *order) String() string {
o.lock.RLock()
defer o.lock.RUnlock()
return o.order
}
func (o *order) Set(order string) {
o.lock.Lock()
defer o.lock.Unlock()
o.order = order
}
type Process struct {
ID string
Owner string
Domain string
Reference string
Config *Config
CreatedAt int64
UpdatedAt int64
Order order
}
func (process *Process) Clone() *Process {
clone := &Process{
ID: process.ID,
Owner: process.Owner,
Domain: process.Domain,
Reference: process.Reference,
Config: process.Config.Clone(),
CreatedAt: process.CreatedAt,
UpdatedAt: process.UpdatedAt,
Order: process.Order.Clone(),
}
return clone
}
func (process *Process) ProcessID() ProcessID {
return ProcessID{
ID: process.ID,
Domain: process.Domain,
}
}
type ProcessStates struct {
Finished uint64
Starting uint64
Running uint64
Finishing uint64
Failed uint64
Killed uint64
}
func (p *ProcessStates) Marshal(s process.States) {
p.Finished = s.Finished
p.Starting = s.Starting
p.Running = s.Running
p.Finishing = s.Finishing
p.Failed = s.Failed
p.Killed = s.Killed
}
type State struct {
Order string // Current order, e.g. "start", "stop"
State string // Current state, e.g. "running"
States ProcessStates // Cumulated process states
Time int64 // Unix timestamp of last status change
Duration float64 // Runtime in seconds since last status change
Reconnect float64 // Seconds until next reconnect, negative if not reconnecting
LastLog string // Last recorded line from the process
Progress Progress // Progress data of the process
Memory uint64 // Current memory consumption in bytes
CPU float64 // Current CPU consumption in percent
LimitMode string // How the process is limited (hard or soft)
Resources ProcessUsage // Current resource usage, include CPU and memory consumption
Command []string // ffmpeg command line parameters
}
type ProcessUsageCPU struct {
NCPU float64 // Number of logical CPUs
Current float64 // percent 0-100*ncpu
Average float64 // percent 0-100*ncpu
Max float64 // percent 0-100*ncpu
Limit float64 // percent 0-100*ncpu
IsThrottling bool
}
func (p *ProcessUsageCPU) UnmarshalParser(pp *parse.UsageCPU) {
p.NCPU = pp.NCPU
p.Average = pp.Average
p.Max = pp.Max
p.Limit = pp.Limit
}
func (p *ProcessUsageCPU) MarshalParser() parse.UsageCPU {
pp := parse.UsageCPU{
NCPU: p.NCPU,
Average: p.Average,
Max: p.Max,
Limit: p.Limit,
}
return pp
}
type ProcessUsageMemory struct {
Current uint64 // bytes
Average float64 // bytes
Max uint64 // bytes
Limit uint64 // bytes
}
func (p *ProcessUsageMemory) UnmarshalParser(pp *parse.UsageMemory) {
p.Average = pp.Average
p.Max = pp.Max
p.Limit = pp.Limit
}
func (p *ProcessUsageMemory) MarshalParser() parse.UsageMemory {
pp := parse.UsageMemory{
Average: p.Average,
Max: p.Max,
Limit: p.Limit,
}
return pp
}
type ProcessUsage struct {
CPU ProcessUsageCPU
Memory ProcessUsageMemory
}
func (p *ProcessUsage) UnmarshalParser(pp *parse.Usage) {
p.CPU.UnmarshalParser(&pp.CPU)
p.Memory.UnmarshalParser(&pp.Memory)
}
func (p *ProcessUsage) MarshalParser() parse.Usage {
pp := parse.Usage{
CPU: p.CPU.MarshalParser(),
Memory: p.Memory.MarshalParser(),
}
return pp
}
type ProcessID struct {
ID string
Domain string
}
func NewProcessID(id, domain string) ProcessID {
return ProcessID{
ID: id,
Domain: domain,
}
}
func ParseProcessID(pid string) ProcessID {
p := ProcessID{}
p.Parse(pid)
return p
}
func (p ProcessID) String() string {
return p.ID + "@" + p.Domain
}
func (p ProcessID) Equal(b ProcessID) bool {
if p.ID == b.ID && p.Domain == b.Domain {
return true
}
return false
}
func (p *ProcessID) Parse(pid string) {
i := strings.LastIndex(pid, "@")
if i == -1 {
p.ID = pid
p.Domain = ""
}
p.ID = pid[:i]
p.Domain = pid[i+1:]
}
func (p ProcessID) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *ProcessID) UnmarshalText(text []byte) error {
p.Parse(string(text))
return nil
}