commit 6db0ec3a77c4ac93561e939c99aaa3e617165c7f Author: garionion Date: Thu Nov 5 18:34:54 2020 +0100 initial commit diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..68a5403 --- /dev/null +++ b/config.example.yml @@ -0,0 +1,8 @@ +outputs: + - "1" + - "2" + - "3" + - "4" + +address: ":3000" +default_duration: "15m" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..06dcf4f --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module ffmpeg-playout + +go 1.15 + +require ( + github.com/andybalholm/brotli v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.4.9 + github.com/gofiber/fiber/v2 v2.1.3 + github.com/google/uuid v1.1.2 + github.com/ilyakaznacheev/cleanenv v1.2.5 + github.com/klauspost/compress v1.11.2 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..be9ed56 --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gofiber/fiber/v2 v2.1.3 h1:d2fkRf6fkLa1uXgzXqN5iqAydjytoocS83hm1o00Ocg= +github.com/gofiber/fiber/v2 v2.1.3/go.mod h1:MMiSv1HrDkN8Pv7NeVDYK+T/lwXOEKAvPBbLvJPCEfA= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ilyakaznacheev/cleanenv v1.2.5 h1:/SlcF9GaIvefWqFJzsccGG/NJdoaAwb7Mm7ImzhO3DM= +github.com/ilyakaznacheev/cleanenv v1.2.5/go.mod h1:/i3yhzwZ3s7hacNERGFwvlhwXMDcaqwIzmayEhbRplk= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= +github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ= +github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.16.0 h1:9zAqOYLl8Tuy3E5R6ckzGDJ1g8+pw15oQp2iL9Jl6gQ= +github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +olympos.io/encoding/edn v0.0.0-20200308123125-93e3b8dd0e24 h1:sreVOrDp0/ezb0CHKVek/l7YwpxPJqv+jT3izfSphA4= +olympos.io/encoding/edn v0.0.0-20200308123125-93e3b8dd0e24/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7a6cab4 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "errors" + "ffmpeg-playout/playout" + "ffmpeg-playout/store" + "fmt" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/ilyakaznacheev/cleanenv" + "log" + "time" +) + +type Config struct { + Outputs []string `yaml:"outputs"` + Address string `yaml:"address" env:"ADDRESS" env-default:":3000"` + DefaultDuration string `yaml:"default_duration" env:"DEFAULT_DURATION" env-default:"15"` +} + +type Job struct { + StartAt string `json:"startAt,omitempty"` + StopAt string `json:"stopAt,omitempty"` + Source string `json:"source"` +} + +func schedulePlayout(s *store.Store) fiber.Handler { + // TODO return custom error + return func(c *fiber.Ctx) error { + var p playout.Job + job := new(Job) + var err error + if err := c.BodyParser(job); err != nil { + log.Println("got defective request: ", err) + c.SendStatus(400) + return err + } + + if job.Source == "" { + c.SendStatus(400) + return errors.New("Got Empty Source. I can't play »Nothing«") + } + p.Source = job.Source + + if p.StartAt, err = time.Parse(time.RFC3339, job.StartAt); err != nil { + log.Println("got strange date: ", job.StartAt, "; error:", err) + c.SendStatus(400) + return err + } + + if job.StopAt != "" { + if p.StopAt, err = time.Parse(time.RFC3339, job.StopAt); err != nil { + log.Println("got strange date: ", job.StopAt, "; error:", err) + c.SendStatus(400) + return err + } + } else { + p.StopAt = p.StartAt.Add(s.DefaultDuration) + } + + s.Lock() + uid, err := s.AddPlayout(&p) + s.Unlock() + if err != nil { + c.SendStatus(500) + return fmt.Errorf("can not schedule playout: %s", err) + } + c.SendString(uid.String()) + + go p.Playout() + + // staus 409 when no schedule possible + + return nil + } +} + +var cfg Config + +func main() { + // TODO configure config file path via cmd parameter + if err := cleanenv.ReadConfig("config.yml", &cfg); err != nil { + log.Fatal("No configfile: ", err) + } + + s, err := store.NewStore(cfg.Outputs, cfg.DefaultDuration) + if err != nil { + log.Fatal("Failed to init Store: ", err.Error()) + } + + app := fiber.New() + app.Use(cors.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + app.Post("/schedulePlayout", schedulePlayout(s)) + + log.Fatal(app.Listen(cfg.Address)) +} diff --git a/playout/playout.go b/playout/playout.go new file mode 100644 index 0000000..e3a3aba --- /dev/null +++ b/playout/playout.go @@ -0,0 +1,35 @@ +package playout + +import ( + "fmt" + "log" + "time" +) + +type PlayoutType int + +const ( + rtmpPlayout PlayoutType = iota + icecastPlayout +) + +type Job struct { + PlayoutType PlayoutType + StartAt time.Time + StopAt time.Time + Source string + Output string + ControlChannel chan string +} + +func (p *Job) Playout() { + // TODO delete playout Job from store after finishing/aborting playout + log.Println(time.Until(p.StartAt)) + select { + case ctrlMsg := <-p.ControlChannel: + fmt.Println(ctrlMsg) + return + case <-time.After(time.Until(p.StartAt)): + log.Println("Start Playout") + } +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..03ac321 --- /dev/null +++ b/store/store.go @@ -0,0 +1,77 @@ +package store + +import ( + "errors" + "ffmpeg-playout/playout" + "fmt" + "github.com/google/uuid" + "log" + "sync" + "time" +) + +type Store struct { + Playouts map[uuid.UUID]*playout.Job + DefaultDuration time.Duration + Outputs []string + sync.RWMutex +} + +func NewStore(o []string, defaultDuration string) (*Store, error) { + playouts := make(map[uuid.UUID]*playout.Job) + + var d time.Duration + var err error + if d, err = time.ParseDuration(defaultDuration); err != nil { + log.Fatal("Failed to set Default Duration: ", err) + } + + store := &Store{Playouts: playouts, DefaultDuration: d, Outputs: o} + + return store, nil +} + +func (s *Store) AddPlayout(p *playout.Job) (uuid.UUID, error) { + outputs := s.Outputs + for _, value := range s.Playouts { + if len(outputs) != 0 && + (value.StartAt.After(p.StartAt) && value.StartAt.Before(p.StopAt) || + value.StopAt.After(p.StartAt) && value.StopAt.Before(p.StopAt) || + p.StartAt.After(value.StartAt) && p.StartAt.Before(value.StopAt) || + p.StopAt.After(value.StartAt) && p.StopAt.Before(value.StopAt)) { + inSlice, index := stringInSlice(value.Output, outputs) + if inSlice { + outputs = remove(outputs, index) + } + } + } + var output string + if len(outputs) != 0 { + output = outputs[0] + } else { + return uuid.Nil, errors.New("no output available") + } + uid, err := uuid.NewUUID() + if err != nil { + return uuid.Nil, fmt.Errorf("couldn't generate uuid: %s", err.Error()) + } + p.Output = output + s.Playouts[uid] = p + return uid, nil +} + +//https://stackoverflow.com/a/15323988/10997297 +func stringInSlice(a string, list []string) (bool, int) { + for index, b := range list { + if b == a { + return true, index + } + } + return false, 0 +} + +func remove(s []string, i int) []string { + s[i] = s[len(s)-1] + // We do not need to put s[i] at the end, as it will be discarded anyway + return s[:len(s)-1] +}