initial commit
This commit is contained in:
commit
6db0ec3a77
6 changed files with 279 additions and 0 deletions
8
config.example.yml
Normal file
8
config.example.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
outputs:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
- "4"
|
||||
|
||||
address: ":3000"
|
||||
default_duration: "15m"
|
14
go.mod
Normal file
14
go.mod
Normal file
|
@ -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
|
||||
)
|
45
go.sum
Normal file
45
go.sum
Normal file
|
@ -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=
|
100
main.go
Normal file
100
main.go
Normal file
|
@ -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))
|
||||
}
|
35
playout/playout.go
Normal file
35
playout/playout.go
Normal file
|
@ -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")
|
||||
}
|
||||
}
|
77
store/store.go
Normal file
77
store/store.go
Normal file
|
@ -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]
|
||||
}
|
Loading…
Add table
Reference in a new issue