From 8b36733e366d1375dde944ad524f3b73813caaee Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 03:24:33 -0800 Subject: [PATCH 001/149] Initial commit --- .gitignore | 22 ++++++++++++ go.mod | 3 ++ main.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++ manager.go | 25 ++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 main.go create mode 100644 manager.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5543a277 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +go-vod + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..3b63aa0f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/pulsejet/go-vod + +go 1.16 diff --git a/main.go b/main.go new file mode 100644 index 00000000..7869b255 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "log" + "net/http" + "strings" + "sync" +) + +type Handler struct { + managers map[string]*Manager + mutex sync.RWMutex + close chan string +} + +func NewHandler() *Handler { + h := &Handler{managers: make(map[string]*Manager), close: make(chan string)} + go h.watchClose() + return h +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + url := r.URL.Path + parts := make([]string, 0) + + for _, part := range strings.Split(url, "/") { + if part != "" { + parts = append(parts, part) + } + } + + if len(parts) != 3 { + log.Println("Invalid URL", url, len(parts)) + w.WriteHeader(http.StatusBadRequest) + return + } + + path := parts[0] + streamid := parts[1] + chunk := parts[2] + + if streamid == "" || chunk == "" || path == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + manager := h.getManager(streamid) + if manager == nil { + manager = h.createManager(path, streamid) + } + manager.ServeHTTP(w, r, chunk) +} + +func (h *Handler) getManager(streamid string) *Manager { + h.mutex.RLock() + defer h.mutex.RUnlock() + return h.managers[streamid] +} + +func (h *Handler) createManager(path string, streamid string) *Manager { + h.mutex.Lock() + defer h.mutex.Unlock() + manager := NewManager(path, streamid, h.close) + h.managers[streamid] = manager + return manager +} + +func (h *Handler) removeManager(streamid string) { + h.mutex.Lock() + defer h.mutex.Unlock() + delete(h.managers, streamid) +} + +func (h *Handler) watchClose() { + for { + id := <-h.close + if id == "" { + return + } + + log.Println("Closing stream", id) + h.removeManager(id) + } +} + +func (h *Handler) Close() { + h.close <- "" +} + +func main() { + log.Println("Starting VOD server") + + h := NewHandler() + + http.Handle("/", h) + http.ListenAndServe(":47788", nil) + + log.Println("Exiting VOD server") + h.Close() +} diff --git a/manager.go b/manager.go new file mode 100644 index 00000000..7ea4bf84 --- /dev/null +++ b/manager.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "net/http" +) + +type Manager struct { + path string + id string + close chan string +} + +func NewManager(path string, id string, close chan string) *Manager { + m := &Manager{path: path, id: id, close: close} + return m +} + +func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) { + fmt.Println("Manager.ServeHTTP", m.id, chunk) + w.Write([]byte("Hello, world!")) + w.Write([]byte(chunk)) + w.Write([]byte(m.id)) + +} From d75dd806729a52f0f08a843e9a1a4e34c91ea08f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 04:09:35 -0800 Subject: [PATCH 002/149] Add stream --- main.go | 12 ++++++----- manager.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++----- stream.go | 32 ++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 stream.go diff --git a/main.go b/main.go index 7869b255..e7f80cbb 100644 --- a/main.go +++ b/main.go @@ -29,15 +29,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - if len(parts) != 3 { - log.Println("Invalid URL", url, len(parts)) + if len(parts) < 3 { + log.Println("Invalid URL", url) w.WriteHeader(http.StatusBadRequest) return } - path := parts[0] - streamid := parts[1] - chunk := parts[2] + streamid := parts[0] + path := "/" + strings.Join(parts[1:len(parts)-2], "/") + chunk := parts[len(parts)-1] + + log.Println("Serving", path, streamid, chunk) if streamid == "" || chunk == "" || path == "" { w.WriteHeader(http.StatusBadRequest) diff --git a/manager.go b/manager.go index 7ea4bf84..37e6dd86 100644 --- a/manager.go +++ b/manager.go @@ -2,24 +2,75 @@ package main import ( "fmt" + "math" "net/http" + "sort" ) type Manager struct { path string id string close chan string + + duration float64 + numChunks int + chunkSize float64 + + streams map[string]*Stream } func NewManager(path string, id string, close chan string) *Manager { m := &Manager{path: path, id: id, close: close} + m.streams = make(map[string]*Stream) + m.chunkSize = 4 + + m.duration = 300 + + m.numChunks = int(math.Ceil(m.duration / m.chunkSize)) + + m.streams["360p.m3u8"] = &Stream{m: m, quality: "360p", height: 360, width: 640, bitrate: 945000} + m.streams["480p.m3u8"] = &Stream{m: m, quality: "480p", height: 480, width: 640, bitrate: 1365000} + m.streams["720p.m3u8"] = &Stream{m: m, quality: "720p", height: 720, width: 1280, bitrate: 3045000} + m.streams["1080p.m3u8"] = &Stream{m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 6045000} + m.streams["1440p.m3u8"] = &Stream{m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9045000} + m.streams["2160p.m3u8"] = &Stream{m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14045000} return m } -func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) { - fmt.Println("Manager.ServeHTTP", m.id, chunk) - w.Write([]byte("Hello, world!")) - w.Write([]byte(chunk)) - w.Write([]byte(m.id)) +func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { + if chunk == "index.m3u8" { + return m.ServeIndex(w, r) + } + if stream, ok := m.streams[chunk]; ok { + return stream.ServeList(w, r) + } + + w.WriteHeader(http.StatusNotFound) + return nil +} + +func (m *Manager) ServeIndex(w http.ResponseWriter, r *http.Request) error { + WriteM3U8ContentType(w) + w.Write([]byte("#EXTM3U\n")) + + // get sorted streams by bitrate + streams := make([]*Stream, 0) + for _, stream := range m.streams { + streams = append(streams, stream) + } + sort.Slice(streams, func(i, j int) bool { + return streams[i].bitrate < streams[j].bitrate + }) + + // Write all streams + for _, stream := range streams { + s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,NAME=%s\n%s.m3u8\n", stream.bitrate, stream.width, stream.height, stream.quality, stream.quality) + w.Write([]byte(s)) + } + return nil +} + +func WriteM3U8ContentType(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/x-mpegURL") } diff --git a/stream.go b/stream.go new file mode 100644 index 00000000..88d0299d --- /dev/null +++ b/stream.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "net/http" +) + +type Stream struct { + m *Manager + quality string + height int + width int + bitrate int +} + +func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { + WriteM3U8ContentType(w) + w.Write([]byte("#EXTM3U\n")) + w.Write([]byte("#EXT-X-VERSION:4\n")) + w.Write([]byte("#EXT-X-MEDIA-SEQUENCE:0\n")) + w.Write([]byte("#EXT-X-PLAYLIST-TYPE:VOD\n")) + w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%.3f\n", s.m.chunkSize))) + + for i := 0; i < s.m.numChunks; i++ { + w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", s.m.chunkSize))) + w.Write([]byte(fmt.Sprintf("%s-%06d.ts\n", s.quality, i))) + } + + w.Write([]byte("#EXT-X-ENDLIST\n")) + + return nil +} From 34e4b9f3d573a8342471bda51ac3eb299a115baa Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 04:27:29 -0800 Subject: [PATCH 003/149] ffprobe --- config.go | 7 ++++ main.go | 29 ++++++++++--- manager.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++------- stream.go | 5 ++- 4 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 config.go diff --git a/config.go b/config.go new file mode 100644 index 00000000..277417b7 --- /dev/null +++ b/config.go @@ -0,0 +1,7 @@ +package main + +type Config struct { + ffmpeg string + ffprobe string + chunkSize float64 +} diff --git a/main.go b/main.go index e7f80cbb..e82356e9 100644 --- a/main.go +++ b/main.go @@ -8,13 +8,18 @@ import ( ) type Handler struct { + c *Config managers map[string]*Manager mutex sync.RWMutex close chan string } -func NewHandler() *Handler { - h := &Handler{managers: make(map[string]*Manager), close: make(chan string)} +func NewHandler(c *Config) *Handler { + h := &Handler{ + c: c, + managers: make(map[string]*Manager), + close: make(chan string), + } go h.watchClose() return h } @@ -36,7 +41,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } streamid := parts[0] - path := "/" + strings.Join(parts[1:len(parts)-2], "/") + path := "/" + strings.Join(parts[1:len(parts)-1], "/") chunk := parts[len(parts)-1] log.Println("Serving", path, streamid, chunk) @@ -50,6 +55,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if manager == nil { manager = h.createManager(path, streamid) } + + if manager == nil { + w.WriteHeader(http.StatusInternalServerError) + return + } manager.ServeHTTP(w, r, chunk) } @@ -62,7 +72,12 @@ func (h *Handler) getManager(streamid string) *Manager { func (h *Handler) createManager(path string, streamid string) *Manager { h.mutex.Lock() defer h.mutex.Unlock() - manager := NewManager(path, streamid, h.close) + manager, err := NewManager(h.c, path, streamid, h.close) + if err != nil { + log.Println("Error creating manager", err) + return nil + } + h.managers[streamid] = manager return manager } @@ -92,7 +107,11 @@ func (h *Handler) Close() { func main() { log.Println("Starting VOD server") - h := NewHandler() + h := NewHandler(&Config{ + ffmpeg: "ffmpeg", + ffprobe: "ffprobe", + chunkSize: 4.0, + }) http.Handle("/", h) http.ListenAndServe(":47788", nil) diff --git a/manager.go b/manager.go index 37e6dd86..a93d5ec9 100644 --- a/manager.go +++ b/manager.go @@ -1,40 +1,65 @@ package main import ( + "bytes" + "context" + "encoding/json" "fmt" + "log" "math" "net/http" + "os/exec" "sort" + "time" ) type Manager struct { + c *Config + path string id string close chan string - duration float64 + probe *ProbeVideoData numChunks int - chunkSize float64 streams map[string]*Stream } -func NewManager(path string, id string, close chan string) *Manager { - m := &Manager{path: path, id: id, close: close} +type ProbeVideoData struct { + Width int + Height int + Duration time.Duration +} + +func NewManager(c *Config, path string, id string, close chan string) (*Manager, error) { + m := &Manager{c: c, path: path, id: id, close: close} m.streams = make(map[string]*Stream) - m.chunkSize = 4 - m.duration = 300 + if err := m.ffprobe(); err != nil { + return nil, err + } - m.numChunks = int(math.Ceil(m.duration / m.chunkSize)) + log.Println("Video duration:", m.probe) - m.streams["360p.m3u8"] = &Stream{m: m, quality: "360p", height: 360, width: 640, bitrate: 945000} - m.streams["480p.m3u8"] = &Stream{m: m, quality: "480p", height: 480, width: 640, bitrate: 1365000} - m.streams["720p.m3u8"] = &Stream{m: m, quality: "720p", height: 720, width: 1280, bitrate: 3045000} - m.streams["1080p.m3u8"] = &Stream{m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 6045000} - m.streams["1440p.m3u8"] = &Stream{m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9045000} - m.streams["2160p.m3u8"] = &Stream{m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14045000} - return m + m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / c.chunkSize)) + + // Possible streams + m.streams["360p.m3u8"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 945000} + m.streams["480p.m3u8"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1365000} + m.streams["720p.m3u8"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 3045000} + m.streams["1080p.m3u8"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 6045000} + m.streams["1440p.m3u8"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9045000} + m.streams["2160p.m3u8"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14045000} + + // Only keep streams that are smaller than the video + for k, stream := range m.streams { + if stream.height > m.probe.Height || stream.width > m.probe.Width { + delete(m.streams, k) + } + } + + return m, nil } func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { @@ -74,3 +99,68 @@ func (m *Manager) ServeIndex(w http.ResponseWriter, r *http.Request) error { func WriteM3U8ContentType(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/x-mpegURL") } + +func (m *Manager) ffprobe() error { + args := []string{ + // Hide debug information + "-v", "error", + + // video + "-show_entries", "format=duration", + "-show_entries", "stream=duration,width,height", + "-select_streams", "v", // Video stream only, we're not interested in audio + + "-of", "json", + m.path, + } + + ctx, _ := context.WithDeadline(context.TODO(), time.Now().Add(5*time.Second)) + cmd := exec.CommandContext(ctx, m.c.ffprobe, args...) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Println(stderr.String()) + return err + } + + out := struct { + Streams []struct { + Width int `json:"width"` + Height int `json:"height"` + Duration string `json:"duration"` + } `json:"streams"` + Format struct { + Duration string `json:"duration"` + } `json:"format"` + }{} + + if err := json.Unmarshal(stdout.Bytes(), &out); err != nil { + return err + } + + var duration time.Duration + if out.Streams[0].Duration != "" { + duration, err = time.ParseDuration(out.Streams[0].Duration + "s") + if err != nil { + return err + } + } + if out.Format.Duration != "" { + duration, err = time.ParseDuration(out.Format.Duration + "s") + if err != nil { + return err + } + } + + m.probe = &ProbeVideoData{ + Width: out.Streams[0].Width, + Height: out.Streams[0].Height, + Duration: duration, + } + + return nil +} diff --git a/stream.go b/stream.go index 88d0299d..fda3d4d2 100644 --- a/stream.go +++ b/stream.go @@ -6,6 +6,7 @@ import ( ) type Stream struct { + c *Config m *Manager quality string height int @@ -19,10 +20,10 @@ func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { w.Write([]byte("#EXT-X-VERSION:4\n")) w.Write([]byte("#EXT-X-MEDIA-SEQUENCE:0\n")) w.Write([]byte("#EXT-X-PLAYLIST-TYPE:VOD\n")) - w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%.3f\n", s.m.chunkSize))) + w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%.3f\n", s.c.chunkSize))) for i := 0; i < s.m.numChunks; i++ { - w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", s.m.chunkSize))) + w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", s.c.chunkSize))) w.Write([]byte(fmt.Sprintf("%s-%06d.ts\n", s.quality, i))) } From 81607447bdc6821e60218d6a78f5cb8094402eff Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 04:45:10 -0800 Subject: [PATCH 004/149] Update --- manager.go | 54 ++++++++++++++++++++++++++++++++++++++++++++---------- stream.go | 20 ++++++++++++++++++-- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/manager.go b/manager.go index a93d5ec9..8ea83df3 100644 --- a/manager.go +++ b/manager.go @@ -10,6 +10,8 @@ import ( "net/http" "os/exec" "sort" + "strconv" + "strings" "time" ) @@ -40,17 +42,15 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, return nil, err } - log.Println("Video duration:", m.probe) - m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / c.chunkSize)) // Possible streams - m.streams["360p.m3u8"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 945000} - m.streams["480p.m3u8"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1365000} - m.streams["720p.m3u8"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 3045000} - m.streams["1080p.m3u8"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 6045000} - m.streams["1440p.m3u8"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9045000} - m.streams["2160p.m3u8"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14045000} + m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 945000} + m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1365000} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 3045000} + m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 6045000} + m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9045000} + m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14045000} // Only keep streams that are smaller than the video for k, stream := range m.streams { @@ -59,16 +59,50 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, } } + log.Println("New manager", m.id, + "with streams:", len(m.streams), + "duration:", m.probe.Duration, + "resolution:", m.probe.Width, "x", m.probe.Height, + ) + return m, nil } func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { + // Master list if chunk == "index.m3u8" { return m.ServeIndex(w, r) } - if stream, ok := m.streams[chunk]; ok { - return stream.ServeList(w, r) + // Stream list + m3u8Sfx := ".m3u8" + if strings.HasSuffix(chunk, m3u8Sfx) { + quality := strings.TrimSuffix(chunk, m3u8Sfx) + if stream, ok := m.streams[quality]; ok { + return stream.ServeList(w, r) + } + } + + // Stream chunk + tsSfx := ".ts" + if strings.HasSuffix(chunk, tsSfx) { + parts := strings.Split(chunk, "-") + if len(parts) != 2 { + w.WriteHeader(http.StatusBadRequest) + return nil + } + + quality := parts[0] + chunkIdStr := strings.TrimSuffix(parts[1], tsSfx) + chunkId, err := strconv.Atoi(chunkIdStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return nil + } + + if stream, ok := m.streams[quality]; ok { + return stream.ServeChunk(w, r, chunkId) + } } w.WriteHeader(http.StatusNotFound) diff --git a/stream.go b/stream.go index fda3d4d2..755d8087 100644 --- a/stream.go +++ b/stream.go @@ -22,12 +22,28 @@ func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { w.Write([]byte("#EXT-X-PLAYLIST-TYPE:VOD\n")) w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%.3f\n", s.c.chunkSize))) - for i := 0; i < s.m.numChunks; i++ { - w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", s.c.chunkSize))) + duration := s.m.probe.Duration.Seconds() + i := 0 + for duration > 0 { + size := s.c.chunkSize + if duration < size { + size = duration + } + + w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", size))) w.Write([]byte(fmt.Sprintf("%s-%06d.ts\n", s.quality, i))) + + duration -= s.c.chunkSize + i++ } w.Write([]byte("#EXT-X-ENDLIST\n")) return nil } + +// Bulk +func (s *Stream) ServeChunk(w http.ResponseWriter, r *http.Request, chunkId int) error { + w.Write([]byte("chunk")) + return nil +} From 339b7f1e9e2912061cc3cc280b0e7f1d9c9adc8d Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 06:54:32 -0800 Subject: [PATCH 005/149] First stream --- main.go | 7 +- manager.go | 8 +- stream.go | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 310 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index e82356e9..d2c4c11d 100644 --- a/main.go +++ b/main.go @@ -70,14 +70,15 @@ func (h *Handler) getManager(streamid string) *Manager { } func (h *Handler) createManager(path string, streamid string) *Manager { - h.mutex.Lock() - defer h.mutex.Unlock() manager, err := NewManager(h.c, path, streamid, h.close) if err != nil { log.Println("Error creating manager", err) return nil } + h.mutex.Lock() + defer h.mutex.Unlock() + h.managers[streamid] = manager return manager } @@ -110,7 +111,7 @@ func main() { h := NewHandler(&Config{ ffmpeg: "ffmpeg", ffprobe: "ffprobe", - chunkSize: 4.0, + chunkSize: 2.0, }) http.Handle("/", h) diff --git a/manager.go b/manager.go index 8ea83df3..c0ee8600 100644 --- a/manager.go +++ b/manager.go @@ -71,7 +71,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { // Master list if chunk == "index.m3u8" { - return m.ServeIndex(w, r) + return m.ServeIndex(w) } // Stream list @@ -79,7 +79,7 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string if strings.HasSuffix(chunk, m3u8Sfx) { quality := strings.TrimSuffix(chunk, m3u8Sfx) if stream, ok := m.streams[quality]; ok { - return stream.ServeList(w, r) + return stream.ServeList(w) } } @@ -101,7 +101,7 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string } if stream, ok := m.streams[quality]; ok { - return stream.ServeChunk(w, r, chunkId) + return stream.ServeChunk(w, chunkId) } } @@ -109,7 +109,7 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string return nil } -func (m *Manager) ServeIndex(w http.ResponseWriter, r *http.Request) error { +func (m *Manager) ServeIndex(w http.ResponseWriter) error { WriteM3U8ContentType(w) w.Write([]byte("#EXTM3U\n")) diff --git a/stream.go b/stream.go index 755d8087..ec6b9a27 100644 --- a/stream.go +++ b/stream.go @@ -1,10 +1,33 @@ package main import ( + "bufio" "fmt" + "io" + "log" "net/http" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "time" ) +type Chunk struct { + id int + done bool + notifs []chan bool +} + +func NewChunk(id int) *Chunk { + return &Chunk{ + id: id, + done: false, + notifs: make([]chan bool, 0), + } +} + type Stream struct { c *Config m *Manager @@ -12,9 +35,14 @@ type Stream struct { height int width int bitrate int + + mutex sync.Mutex + chunks map[int]*Chunk + + coder *exec.Cmd } -func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { +func (s *Stream) ServeList(w http.ResponseWriter) error { WriteM3U8ContentType(w) w.Write([]byte("#EXTM3U\n")) w.Write([]byte("#EXT-X-VERSION:4\n")) @@ -42,8 +70,278 @@ func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { return nil } -// Bulk -func (s *Stream) ServeChunk(w http.ResponseWriter, r *http.Request, chunkId int) error { - w.Write([]byte("chunk")) +func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + // Already have this chunk + if chunk, ok := s.chunks[id]; ok { + // Chunk is finished, just return it + if chunk.done { + s.returnChunk(w, chunk) + return nil + } + + // Still waiting on transcoder + s.waitForChunk(w, chunk) + return nil + } + + // Will have this soon enough + foundBehind := false + for i := id - 1; i > id-4 && i >= 0; i++ { + if _, ok := s.chunks[i]; ok { + foundBehind = true + } + } + if foundBehind { + // Make sure the chunk exists + chunk := s.createChunk(id) + + // Wait for it + s.waitForChunk(w, chunk) + return nil + } + + // Let's start over + s.restartAtChunk(w, id) return nil } + +func (s *Stream) createChunk(id int) *Chunk { + if c, ok := s.chunks[id]; ok { + return c + } else { + s.chunks[id] = NewChunk(id) + return s.chunks[id] + } +} + +func (s *Stream) returnChunk(w http.ResponseWriter, chunk *Chunk) { + // Read file and write to response + filename := s.getTsPath(chunk.id) + f, err := os.Open(filename) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer f.Close() + log.Printf("Served chunk %d", chunk.id) + w.Header().Set("Content-Type", "video/MP2T") + io.Copy(w, f) +} + +func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { + if chunk.done { + s.returnChunk(w, chunk) + return + } + + // Add our channel + notif := make(chan bool) + chunk.notifs = append(chunk.notifs, notif) + t := time.NewTimer(5 * time.Second) + + s.mutex.Unlock() + + select { + case <-notif: + t.Stop() + case <-t.C: + } + + s.mutex.Lock() + + // remove channel + for i, c := range chunk.notifs { + if c == notif { + chunk.notifs = append(chunk.notifs[:i], chunk.notifs[i+1:]...) + break + } + } + + // check for success + if chunk.done { + s.returnChunk(w, chunk) + } +} + +func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { + // Clear everything + s.chunks = make(map[int]*Chunk) + + chunk := s.createChunk(id) // create first chunk + + // Start the transcoder + s.transcode(id) + + s.waitForChunk(w, chunk) // this is also a request +} + +func (s *Stream) transcode(startId int) { + startAt := float64(startId) * s.c.chunkSize + + args := []string{ + "-loglevel", "warning", + } + + if startAt > 0 { + args = append(args, []string{ + "-ss", fmt.Sprintf("%.6f", startAt), + }...) + } + + // Input specs + args = append(args, []string{ + "-autorotate", "0", // consistent behavior + "-i", s.m.path, // Input file + "-copyts", // So the "-to" refers to the original TS + }...) + + // QSV / encoder selection + VAAPI := os.Getenv("VAAPI") == "1" + CV := "libx264" + VF := "" + if VAAPI { + CV = "h264_vaapi" + VF = "scale_vaapi=w=SCALE_WIDTH:h=SCALE_HEIGHT:force_original_aspect_ratio=decrease" + extra := strings.Split("-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi", " ") + args = append(args, extra...) + } + + // Scaling for output + var scale string + if VAAPI { + scale = strings.Replace(VF, "SCALE_WIDTH", fmt.Sprintf("%d", s.width), 1) + scale = strings.Replace(scale, "SCALE_HEIGHT", fmt.Sprintf("%d", s.height), 1) + } else if s.width >= s.height { + scale = fmt.Sprintf("scale=-2:%d", s.height) + } else { + scale = fmt.Sprintf("scale=%d:-2", s.width) + } + + // Output specs + args = append(args, []string{ + "-vf", scale, + "-c:v", CV, + "-profile:v", "high", + "-b:v", fmt.Sprintf("%dk", s.bitrate/1000), + }...) + + // Extra args only for x264 + if !VAAPI { + args = append(args, []string{ + "-preset", "faster", + "-level:v", "4.0", + }...) + } + + // Audio + args = append(args, []string{ + "-c:a", "aac", + "-b:a", "192k", + }...) + + // Segmenting specs + args = append(args, []string{ + "-avoid_negative_ts", "disabled", + "-max_muxing_queue_size", "2048", + "-f", "hls", + "-max_delay", "5000000", + "-hls_time", fmt.Sprintf("%.6f", s.c.chunkSize), + "-g", fmt.Sprintf("%.6f", s.c.chunkSize), + "-hls_segment_type", "mpegts", + "-start_number", fmt.Sprintf("%d", startId), + "-hls_segment_filename", s.getTsPath(-1), + "-", + }...) + + s.coder = exec.Command(s.c.ffmpeg, args...) + // log.Println("Starting FFmpeg process with args", strings.Join(s.coder.Args[:], " ")) + + cmdStdOut, err := s.coder.StdoutPipe() + if err != nil { + fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + } + + cmdStdErr, err := s.coder.StderrPipe() + if err != nil { + fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + } + + err = s.coder.Start() + if err != nil { + log.Printf("FATAL: ffmpeg command failed with %s\n", err) + } + + go s.monitorTranscodeOutput(cmdStdOut, startAt) + go s.monitorStderr(cmdStdErr) +} + +func (s *Stream) getTsPath(id int) string { + if id == -1 { + return fmt.Sprintf("/tmp/go-vod/%s/%s-%%06d.ts", s.m.id, s.quality) + } + return fmt.Sprintf("/tmp/go-vod/%s/%s-%06d.ts", s.m.id, s.quality, id) +} + +// Separate goroutine +func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64) { + defer cmdStdOut.Close() + stdoutReader := bufio.NewReader(cmdStdOut) + + for { + line, err := stdoutReader.ReadBytes('\n') + if err == io.EOF { + if len(line) == 0 { + break + } + } else { + if err != nil { + log.Fatal(err) + } + line = line[:(len(line) - 1)] + } + + l := string(line) + + if strings.Contains(l, ".ts") { + // 1080p-000003.ts + idx := strings.Split(strings.Split(l, "-")[1], ".")[0] + id, err := strconv.Atoi(idx) + if err != nil { + log.Println("Error parsing chunk id") + } + + s.mutex.Lock() + chunk := s.createChunk(id) + chunk.done = true + for _, n := range chunk.notifs { + n <- true + } + s.mutex.Unlock() + } + + // log.Println("ffmpeg:", l) + } +} + +func (s *Stream) monitorStderr(cmdStdErr io.ReadCloser) { + stderrReader := bufio.NewReader(cmdStdErr) + + for { + line, err := stderrReader.ReadBytes('\n') + if err == io.EOF { + if len(line) == 0 { + break + } + } else { + if err != nil { + log.Fatal(err) + } + line = line[:(len(line) - 1)] + } + log.Println("ffmpeg-error:", string(line)) + } +} From 31bab910d2aea47b77f959ffb765b8dd88644da4 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 07:34:07 -0800 Subject: [PATCH 006/149] Ugh --- stream.go | 57 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/stream.go b/stream.go index ec6b9a27..edac4603 100644 --- a/stream.go +++ b/stream.go @@ -89,7 +89,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { // Will have this soon enough foundBehind := false - for i := id - 1; i > id-4 && i >= 0; i++ { + for i := id - 1; i > id-4 && i >= 0; i-- { if _, ok := s.chunks[i]; ok { foundBehind = true } @@ -168,6 +168,12 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { } func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { + // Stop current transcoder + if s.coder != nil { + s.coder.Process.Kill() + s.coder.Wait() + } + // Clear everything s.chunks = make(map[int]*Chunk) @@ -288,10 +294,18 @@ func (s *Stream) getTsPath(id int) string { // Separate goroutine func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64) { + s.mutex.Lock() + coder := s.coder + s.mutex.Unlock() + defer cmdStdOut.Close() stdoutReader := bufio.NewReader(cmdStdOut) for { + if s.coder != coder { + break + } + line, err := stdoutReader.ReadBytes('\n') if err == io.EOF { if len(line) == 0 { @@ -307,23 +321,36 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 l := string(line) if strings.Contains(l, ".ts") { - // 1080p-000003.ts - idx := strings.Split(strings.Split(l, "-")[1], ".")[0] - id, err := strconv.Atoi(idx) - if err != nil { - log.Println("Error parsing chunk id") - } + go func() { + // 1080p-000003.ts + idx := strings.Split(strings.Split(l, "-")[1], ".")[0] + id, err := strconv.Atoi(idx) + if err != nil { + log.Println("Error parsing chunk id") + } - s.mutex.Lock() - chunk := s.createChunk(id) - chunk.done = true - for _, n := range chunk.notifs { - n <- true - } - s.mutex.Unlock() + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.coder != coder { + return + } + + chunk := s.createChunk(id) + chunk.done = true + for _, n := range chunk.notifs { + n <- true + } + }() } - // log.Println("ffmpeg:", l) + log.Println("ffmpeg:", l) + } + + // Join the process + err := s.coder.Wait() + if err != nil { + log.Println("FFmpeg process wait failed with", err) } } From c2fd1659ce444e3723bcd6f82860fc04ab5f1d56 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 07:46:35 -0800 Subject: [PATCH 007/149] Add goal --- stream.go | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/stream.go b/stream.go index edac4603..23a72f5b 100644 --- a/stream.go +++ b/stream.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" ) @@ -36,6 +37,8 @@ type Stream struct { width int bitrate int + goal int + mutex sync.Mutex chunks map[int]*Chunk @@ -74,6 +77,8 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { s.mutex.Lock() defer s.mutex.Unlock() + s.checkGoal(id) + // Already have this chunk if chunk, ok := s.chunks[id]; ok { // Chunk is finished, just return it @@ -172,6 +177,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { if s.coder != nil { s.coder.Process.Kill() s.coder.Wait() + s.coder = nil } // Clear everything @@ -180,6 +186,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { chunk := s.createChunk(id) // create first chunk // Start the transcoder + s.checkGoal(id) s.transcode(id) s.waitForChunk(w, chunk) // this is also a request @@ -285,6 +292,18 @@ func (s *Stream) transcode(startId int) { go s.monitorStderr(cmdStdErr) } +func (s *Stream) checkGoal(id int) { + goal := id + 5 + if goal > s.goal { + s.goal = goal + + // resume encoding + if s.coder != nil { + s.coder.Process.Signal(syscall.SIGCONT) + } + } +} + func (s *Stream) getTsPath(id int) string { if id == -1 { return fmt.Sprintf("/tmp/go-vod/%s/%s-%%06d.ts", s.m.id, s.quality) @@ -321,26 +340,34 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 l := string(line) if strings.Contains(l, ".ts") { - go func() { - // 1080p-000003.ts - idx := strings.Split(strings.Split(l, "-")[1], ".")[0] - id, err := strconv.Atoi(idx) - if err != nil { - log.Println("Error parsing chunk id") - } + // 1080p-000003.ts + idx := strings.Split(strings.Split(l, "-")[1], ".")[0] + id, err := strconv.Atoi(idx) + if err != nil { + log.Println("Error parsing chunk id") + } + go func() { s.mutex.Lock() defer s.mutex.Unlock() + // The coder has changed; do nothing if s.coder != coder { return } + // Notify everyone chunk := s.createChunk(id) chunk.done = true for _, n := range chunk.notifs { n <- true } + + // Check goal satisfied + if id >= s.goal { + log.Println("Goal satisfied, pausing encoding") + s.coder.Process.Signal(syscall.SIGSTOP) + } }() } From 91c6a43fc109fa926ded195c31588641d5dd7845 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 07:59:31 -0800 Subject: [PATCH 008/149] Fix goal --- stream.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/stream.go b/stream.go index 23a72f5b..a3965382 100644 --- a/stream.go +++ b/stream.go @@ -176,7 +176,6 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { // Stop current transcoder if s.coder != nil { s.coder.Process.Kill() - s.coder.Wait() s.coder = nil } @@ -186,7 +185,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { chunk := s.createChunk(id) // create first chunk // Start the transcoder - s.checkGoal(id) + s.goal = id + 5 s.transcode(id) s.waitForChunk(w, chunk) // this is also a request @@ -299,6 +298,7 @@ func (s *Stream) checkGoal(id int) { // resume encoding if s.coder != nil { + log.Println("Resuming encoding") s.coder.Process.Signal(syscall.SIGCONT) } } @@ -375,10 +375,7 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 } // Join the process - err := s.coder.Wait() - if err != nil { - log.Println("FFmpeg process wait failed with", err) - } + coder.Wait() } func (s *Stream) monitorStderr(cmdStdErr io.ReadCloser) { From 75d2768fe645e30d6b95b9f0d0510a97c876c65b Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 09:39:09 -0800 Subject: [PATCH 009/149] Tuning --- config.go | 2 +- main.go | 2 +- manager.go | 14 +++++++------- stream.go | 24 +++++++++++++----------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/config.go b/config.go index 277417b7..5b976860 100644 --- a/config.go +++ b/config.go @@ -3,5 +3,5 @@ package main type Config struct { ffmpeg string ffprobe string - chunkSize float64 + chunkSize int } diff --git a/main.go b/main.go index d2c4c11d..be5f60af 100644 --- a/main.go +++ b/main.go @@ -111,7 +111,7 @@ func main() { h := NewHandler(&Config{ ffmpeg: "ffmpeg", ffprobe: "ffprobe", - chunkSize: 2.0, + chunkSize: 3, }) http.Handle("/", h) diff --git a/manager.go b/manager.go index c0ee8600..fefb26de 100644 --- a/manager.go +++ b/manager.go @@ -42,15 +42,15 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, return nil, err } - m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / c.chunkSize)) + m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.chunkSize))) // Possible streams - m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 945000} - m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1365000} - m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 3045000} - m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 6045000} - m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9045000} - m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14045000} + m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 800000} + m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1500000} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 3000000} + m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 5000000} + m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9000000} + m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14000000} // Only keep streams that are smaller than the video for k, stream := range m.streams { diff --git a/stream.go b/stream.go index a3965382..43486e71 100644 --- a/stream.go +++ b/stream.go @@ -51,12 +51,12 @@ func (s *Stream) ServeList(w http.ResponseWriter) error { w.Write([]byte("#EXT-X-VERSION:4\n")) w.Write([]byte("#EXT-X-MEDIA-SEQUENCE:0\n")) w.Write([]byte("#EXT-X-PLAYLIST-TYPE:VOD\n")) - w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%.3f\n", s.c.chunkSize))) + w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", s.c.chunkSize))) duration := s.m.probe.Duration.Seconds() i := 0 for duration > 0 { - size := s.c.chunkSize + size := float64(s.c.chunkSize) if duration < size { size = duration } @@ -64,7 +64,7 @@ func (s *Stream) ServeList(w http.ResponseWriter) error { w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", size))) w.Write([]byte(fmt.Sprintf("%s-%06d.ts\n", s.quality, i))) - duration -= s.c.chunkSize + duration -= float64(s.c.chunkSize) i++ } @@ -192,7 +192,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { } func (s *Stream) transcode(startId int) { - startAt := float64(startId) * s.c.chunkSize + startAt := float64(startId * s.c.chunkSize) args := []string{ "-loglevel", "warning", @@ -238,13 +238,14 @@ func (s *Stream) transcode(startId int) { "-vf", scale, "-c:v", CV, "-profile:v", "high", - "-b:v", fmt.Sprintf("%dk", s.bitrate/1000), + "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), + "-bufsize", fmt.Sprintf("%dK", s.bitrate/3000), }...) // Extra args only for x264 if !VAAPI { args = append(args, []string{ - "-preset", "faster", + "-preset", "fast", "-level:v", "4.0", }...) } @@ -258,11 +259,9 @@ func (s *Stream) transcode(startId int) { // Segmenting specs args = append(args, []string{ "-avoid_negative_ts", "disabled", - "-max_muxing_queue_size", "2048", "-f", "hls", - "-max_delay", "5000000", - "-hls_time", fmt.Sprintf("%.6f", s.c.chunkSize), - "-g", fmt.Sprintf("%.6f", s.c.chunkSize), + "-hls_time", fmt.Sprintf("%d", s.c.chunkSize), + "-g", fmt.Sprintf("%d", s.c.chunkSize), "-hls_segment_type", "mpegts", "-start_number", fmt.Sprintf("%d", startId), "-hls_segment_filename", s.getTsPath(-1), @@ -270,7 +269,7 @@ func (s *Stream) transcode(startId int) { }...) s.coder = exec.Command(s.c.ffmpeg, args...) - // log.Println("Starting FFmpeg process with args", strings.Join(s.coder.Args[:], " ")) + log.Println("Starting FFmpeg process with args", strings.Join(s.coder.Args[:], " ")) cmdStdOut, err := s.coder.StdoutPipe() if err != nil { @@ -358,6 +357,9 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 // Notify everyone chunk := s.createChunk(id) + if chunk.done { + return + } chunk.done = true for _, n := range chunk.notifs { n <- true From 6591b3c39cd7fea2fc0b1784d501c99b3ca75e22 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 17:56:38 -0800 Subject: [PATCH 010/149] Add max stream --- manager.go | 13 +++++++++++-- stream.go | 14 ++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/manager.go b/manager.go index fefb26de..1cceaf9d 100644 --- a/manager.go +++ b/manager.go @@ -53,12 +53,21 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14000000} // Only keep streams that are smaller than the video + var highest int for k, stream := range m.streams { - if stream.height > m.probe.Height || stream.width > m.probe.Width { + if stream.height >= m.probe.Height || stream.width >= m.probe.Width { delete(m.streams, k) + } else if stream.bitrate > highest { + highest = stream.bitrate } } + // Original stream + m.streams["max"] = &Stream{ + c: c, m: m, quality: "max", height: m.probe.Height, width: m.probe.Width, + bitrate: int(float64(highest) * 1.5), + } + log.Println("New manager", m.id, "with streams:", len(m.streams), "duration:", m.probe.Duration, @@ -124,7 +133,7 @@ func (m *Manager) ServeIndex(w http.ResponseWriter) error { // Write all streams for _, stream := range streams { - s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,NAME=%s\n%s.m3u8\n", stream.bitrate, stream.width, stream.height, stream.quality, stream.quality) + s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,NAME=\"%s\"\n%s.m3u8\n", stream.bitrate, stream.width, stream.height, stream.quality, stream.quality) w.Write([]byte(s)) } return nil diff --git a/stream.go b/stream.go index 43486e71..fd3f90c3 100644 --- a/stream.go +++ b/stream.go @@ -233,19 +233,25 @@ func (s *Stream) transcode(startId int) { scale = fmt.Sprintf("scale=%d:-2", s.width) } + // not original (max) + if s.quality != "max" { + args = append(args, []string{ + "-vf", scale, + "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), + "-bufsize", fmt.Sprintf("%dK", s.bitrate/3000), + }...) + } + // Output specs args = append(args, []string{ - "-vf", scale, "-c:v", CV, "-profile:v", "high", - "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), - "-bufsize", fmt.Sprintf("%dK", s.bitrate/3000), }...) // Extra args only for x264 if !VAAPI { args = append(args, []string{ - "-preset", "fast", + "-preset", "faster", "-level:v", "4.0", }...) } From 1141c358b8184cc2b451f62834168ea92c448693 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 18:01:33 -0800 Subject: [PATCH 011/149] refactor to config --- config.go | 12 ++++++++++-- main.go | 8 +++++--- stream.go | 4 ++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/config.go b/config.go index 5b976860..d261747b 100644 --- a/config.go +++ b/config.go @@ -1,7 +1,15 @@ package main type Config struct { - ffmpeg string - ffprobe string + // FFmpeg binary + ffmpeg string + // FFprobe binary + ffprobe string + + // Size of each chunk in seconds chunkSize int + // How many *chunks* to look behind before restarting transcoding + lookBehind int + // How many chunks to buffer ahead of player position + goalBuffer int } diff --git a/main.go b/main.go index be5f60af..f7731ef2 100644 --- a/main.go +++ b/main.go @@ -109,9 +109,11 @@ func main() { log.Println("Starting VOD server") h := NewHandler(&Config{ - ffmpeg: "ffmpeg", - ffprobe: "ffprobe", - chunkSize: 3, + ffmpeg: "ffmpeg", + ffprobe: "ffprobe", + chunkSize: 3, + lookBehind: 5, + goalBuffer: 5, }) http.Handle("/", h) diff --git a/stream.go b/stream.go index fd3f90c3..ad3c30ca 100644 --- a/stream.go +++ b/stream.go @@ -94,7 +94,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { // Will have this soon enough foundBehind := false - for i := id - 1; i > id-4 && i >= 0; i-- { + for i := id - 1; i > id-s.c.lookBehind && i >= 0; i-- { if _, ok := s.chunks[i]; ok { foundBehind = true } @@ -297,7 +297,7 @@ func (s *Stream) transcode(startId int) { } func (s *Stream) checkGoal(id int) { - goal := id + 5 + goal := id + s.c.goalBuffer if goal > s.goal { s.goal = goal From a094c37fb5011020c2145884c259375dde0eb384 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 18:20:47 -0800 Subject: [PATCH 012/149] log improv --- config.go | 6 ++++-- main.go | 13 +++++++------ manager.go | 6 +----- stream.go | 24 ++++++++++++++---------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/config.go b/config.go index d261747b..010284e2 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,8 @@ type Config struct { chunkSize int // How many *chunks* to look behind before restarting transcoding lookBehind int - // How many chunks to buffer ahead of player position - goalBuffer int + // Number of chunks in goal to restart encoding + goalBufferMin int + // Number of chunks in goal to stop encoding + goalBufferMax int } diff --git a/main.go b/main.go index f7731ef2..f0d79fa3 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := "/" + strings.Join(parts[1:len(parts)-1], "/") chunk := parts[len(parts)-1] - log.Println("Serving", path, streamid, chunk) + // log.Println("Serving", path, streamid, chunk) if streamid == "" || chunk == "" || path == "" { w.WriteHeader(http.StatusBadRequest) @@ -109,11 +109,12 @@ func main() { log.Println("Starting VOD server") h := NewHandler(&Config{ - ffmpeg: "ffmpeg", - ffprobe: "ffprobe", - chunkSize: 3, - lookBehind: 5, - goalBuffer: 5, + ffmpeg: "ffmpeg", + ffprobe: "ffprobe", + chunkSize: 3, + lookBehind: 5, + goalBufferMin: 3, + goalBufferMax: 8, }) http.Handle("/", h) diff --git a/manager.go b/manager.go index 1cceaf9d..33f43e2b 100644 --- a/manager.go +++ b/manager.go @@ -68,11 +68,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, bitrate: int(float64(highest) * 1.5), } - log.Println("New manager", m.id, - "with streams:", len(m.streams), - "duration:", m.probe.Duration, - "resolution:", m.probe.Width, "x", m.probe.Height, - ) + log.Printf("%s: new manager for %s", m.id, m.path) return m, nil } diff --git a/stream.go b/stream.go index ad3c30ca..3d2e754e 100644 --- a/stream.go +++ b/stream.go @@ -123,6 +123,10 @@ func (s *Stream) createChunk(id int) *Chunk { } func (s *Stream) returnChunk(w http.ResponseWriter, chunk *Chunk) { + // This function is called with lock, but we don't need it + s.mutex.Unlock() + defer s.mutex.Lock() + // Read file and write to response filename := s.getTsPath(chunk.id) f, err := os.Open(filename) @@ -132,7 +136,6 @@ func (s *Stream) returnChunk(w http.ResponseWriter, chunk *Chunk) { return } defer f.Close() - log.Printf("Served chunk %d", chunk.id) w.Header().Set("Content-Type", "video/MP2T") io.Copy(w, f) } @@ -185,7 +188,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { chunk := s.createChunk(id) // create first chunk // Start the transcoder - s.goal = id + 5 + s.goal = id + s.c.goalBufferMax s.transcode(id) s.waitForChunk(w, chunk) // this is also a request @@ -275,7 +278,7 @@ func (s *Stream) transcode(startId int) { }...) s.coder = exec.Command(s.c.ffmpeg, args...) - log.Println("Starting FFmpeg process with args", strings.Join(s.coder.Args[:], " ")) + log.Printf("%s-%s: %s", strings.Join(s.coder.Args[:], " ")) cmdStdOut, err := s.coder.StdoutPipe() if err != nil { @@ -297,13 +300,13 @@ func (s *Stream) transcode(startId int) { } func (s *Stream) checkGoal(id int) { - goal := id + s.c.goalBuffer + goal := id + s.c.goalBufferMin if goal > s.goal { - s.goal = goal + s.goal = id + s.c.goalBufferMax // resume encoding if s.coder != nil { - log.Println("Resuming encoding") + log.Printf("%s-%s: resuming transcoding", s.m.id, s.quality) s.coder.Process.Signal(syscall.SIGCONT) } } @@ -345,6 +348,9 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 l := string(line) if strings.Contains(l, ".ts") { + // Debug + log.Printf("%s-%s: recv %s", s.m.id, s.quality, l) + // 1080p-000003.ts idx := strings.Split(strings.Split(l, "-")[1], ".")[0] id, err := strconv.Atoi(idx) @@ -352,7 +358,7 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 log.Println("Error parsing chunk id") } - go func() { + func() { s.mutex.Lock() defer s.mutex.Unlock() @@ -373,13 +379,11 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 // Check goal satisfied if id >= s.goal { - log.Println("Goal satisfied, pausing encoding") + log.Printf("%s-%s: goal satisfied: %d", s.m.id, s.quality, s.goal) s.coder.Process.Signal(syscall.SIGSTOP) } }() } - - log.Println("ffmpeg:", l) } // Join the process From d466a1b7b8e1805270edae71f697d43dab02b5f3 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 18:49:55 -0800 Subject: [PATCH 013/149] Pruning --- manager.go | 5 ++++ stream.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/manager.go b/manager.go index 33f43e2b..9330dfa9 100644 --- a/manager.go +++ b/manager.go @@ -68,6 +68,11 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, bitrate: int(float64(highest) * 1.5), } + // Start all streams + for _, stream := range m.streams { + go stream.Run() + } + log.Printf("%s: new manager for %s", m.id, m.path) return m, nil diff --git a/stream.go b/stream.go index 3d2e754e..ce9b2e26 100644 --- a/stream.go +++ b/stream.go @@ -43,6 +43,56 @@ type Stream struct { chunks map[int]*Chunk coder *exec.Cmd + + inactive int +} + +func (s *Stream) Run() { + // run every 5s + t := time.NewTicker(5 * time.Second) + defer t.Stop() + + for { + <-t.C + s.mutex.Lock() + // Prune chunks + for id := range s.chunks { + if id < s.goal-s.c.goalBufferMax { + s.pruneChunk(id) + } + } + s.inactive++ + if s.inactive >= 4 { + t.Stop() + s.Stop() + s.mutex.Unlock() + return + } + s.mutex.Unlock() + } +} + +func (s *Stream) Stop() { + log.Printf("%s-%s: stopping stream", s.m.id, s.quality) + + for _, chunk := range s.chunks { + // Delete files + s.pruneChunk(chunk.id) + } + + s.chunks = make(map[int]*Chunk) + s.goal = 0 + + if s.coder != nil { + s.coder.Process.Kill() + s.coder = nil + } +} + +func (s *Stream) StopWithLock() { + s.mutex.Lock() + defer s.mutex.Unlock() + s.Stop() } func (s *Stream) ServeList(w http.ResponseWriter) error { @@ -77,6 +127,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { s.mutex.Lock() defer s.mutex.Unlock() + s.inactive = 0 s.checkGoal(id) // Already have this chunk @@ -122,6 +173,14 @@ func (s *Stream) createChunk(id int) *Chunk { } } +func (s *Stream) pruneChunk(id int) { + delete(s.chunks, id) + + // Remove file + filename := s.getTsPath(id) + os.Remove(filename) +} + func (s *Stream) returnChunk(w http.ResponseWriter, chunk *Chunk) { // This function is called with lock, but we don't need it s.mutex.Unlock() @@ -177,13 +236,7 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { // Stop current transcoder - if s.coder != nil { - s.coder.Process.Kill() - s.coder = nil - } - - // Clear everything - s.chunks = make(map[int]*Chunk) + s.Stop() chunk := s.createChunk(id) // create first chunk @@ -278,7 +331,7 @@ func (s *Stream) transcode(startId int) { }...) s.coder = exec.Command(s.c.ffmpeg, args...) - log.Printf("%s-%s: %s", strings.Join(s.coder.Args[:], " ")) + log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(s.coder.Args[:], " ")) cmdStdOut, err := s.coder.StdoutPipe() if err != nil { From 62646cfc81a7992c72b8905e55cbc7167ea8575f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 19:23:28 -0800 Subject: [PATCH 014/149] Add multi-file support --- main.go | 16 +++++++++++++--- manager.go | 40 +++++++++++++++++++++++++++++++++++++--- stream.go | 48 +++++++++++++++++++++++++++++++----------------- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/main.go b/main.go index f0d79fa3..f1d7312c 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - manager := h.getManager(streamid) + manager := h.getManager(path, streamid) if manager == nil { manager = h.createManager(path, streamid) } @@ -63,10 +63,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { manager.ServeHTTP(w, r, chunk) } -func (h *Handler) getManager(streamid string) *Manager { +func (h *Handler) getManager(path string, streamid string) *Manager { h.mutex.RLock() defer h.mutex.RUnlock() - return h.managers[streamid] + + m := h.managers[streamid] + if m == nil || m.path != path { + return nil + } + return m } func (h *Handler) createManager(path string, streamid string) *Manager { @@ -79,6 +84,11 @@ func (h *Handler) createManager(path string, streamid string) *Manager { h.mutex.Lock() defer h.mutex.Unlock() + old := h.managers[streamid] + if old != nil { + old.Destroy() + } + h.managers[streamid] = manager return manager } diff --git a/manager.go b/manager.go index 9330dfa9..9180a91a 100644 --- a/manager.go +++ b/manager.go @@ -18,9 +18,10 @@ import ( type Manager struct { c *Config - path string - id string - close chan string + path string + id string + close chan string + inactive int probe *ProbeVideoData numChunks int @@ -75,9 +76,42 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, log.Printf("%s: new manager for %s", m.id, m.path) + // Check for inactivity + go func() { + t := time.NewTicker(5 * time.Second) + defer t.Stop() + for { + <-t.C + m.inactive++ + + // Check if any stream is active + for _, stream := range m.streams { + if stream.coder != nil { + m.inactive = 0 + break + } + } + + // Nothing done for 5 minutes + if m.inactive >= 60 { + log.Printf("%s: inactive, closing", m.id) + m.Destroy() + m.close <- m.id + return + } + } + }() + return m, nil } +// Destroys streams. DOES NOT emit on the close channel. +func (m *Manager) Destroy() { + for _, stream := range m.streams { + stream.Stop() + } +} + func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { // Master list if chunk == "index.m3u8" { diff --git a/stream.go b/stream.go index ce9b2e26..68f40224 100644 --- a/stream.go +++ b/stream.go @@ -45,6 +45,7 @@ type Stream struct { coder *exec.Cmd inactive int + stop chan bool } func (s *Stream) Run() { @@ -52,27 +53,42 @@ func (s *Stream) Run() { t := time.NewTicker(5 * time.Second) defer t.Stop() + s.stop = make(chan bool) + for { - <-t.C - s.mutex.Lock() - // Prune chunks - for id := range s.chunks { - if id < s.goal-s.c.goalBufferMax { - s.pruneChunk(id) + select { + case <-t.C: + s.mutex.Lock() + // Prune chunks + for id := range s.chunks { + if id < s.goal-s.c.goalBufferMax { + s.pruneChunk(id) + } } - } - s.inactive++ - if s.inactive >= 4 { + + s.inactive++ + + // Nothing done for 2 minutes + if s.inactive >= 24 { + t.Stop() + s.clear() + s.mutex.Unlock() + return + } + s.mutex.Unlock() + + case <-s.stop: + log.Printf("%s-%s: received stop signal", s.m.id, s.quality) t.Stop() - s.Stop() + s.mutex.Lock() + s.clear() s.mutex.Unlock() return } - s.mutex.Unlock() } } -func (s *Stream) Stop() { +func (s *Stream) clear() { log.Printf("%s-%s: stopping stream", s.m.id, s.quality) for _, chunk := range s.chunks { @@ -89,10 +105,8 @@ func (s *Stream) Stop() { } } -func (s *Stream) StopWithLock() { - s.mutex.Lock() - defer s.mutex.Unlock() - s.Stop() +func (s *Stream) Stop() { + s.stop <- true } func (s *Stream) ServeList(w http.ResponseWriter) error { @@ -236,7 +250,7 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { // Stop current transcoder - s.Stop() + s.clear() chunk := s.createChunk(id) // create first chunk From 0e87287035ad2912cd3e3cf574921673c5017a85 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 19:40:53 -0800 Subject: [PATCH 015/149] Tempdir cleanup --- config.go | 2 ++ main.go | 7 +++++++ manager.go | 15 +++++++++++++++ stream.go | 4 ++-- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 010284e2..f64ad77d 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,8 @@ type Config struct { ffmpeg string // FFprobe binary ffprobe string + // Temp files directory + tempdir string // Size of each chunk in seconds chunkSize int diff --git a/main.go b/main.go index f1d7312c..150bc3e0 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "log" "net/http" + "os" "strings" "sync" ) @@ -20,6 +21,11 @@ func NewHandler(c *Config) *Handler { managers: make(map[string]*Manager), close: make(chan string), } + + // Recreate tempdir + os.RemoveAll(c.tempdir) + os.MkdirAll(c.tempdir, 0755) + go h.watchClose() return h } @@ -121,6 +127,7 @@ func main() { h := NewHandler(&Config{ ffmpeg: "ffmpeg", ffprobe: "ffprobe", + tempdir: "/tmp/go-vod", chunkSize: 3, lookBehind: 5, goalBufferMin: 3, diff --git a/manager.go b/manager.go index 9180a91a..85b41940 100644 --- a/manager.go +++ b/manager.go @@ -5,9 +5,11 @@ import ( "context" "encoding/json" "fmt" + "hash/fnv" "log" "math" "net/http" + "os" "os/exec" "sort" "strconv" @@ -19,6 +21,7 @@ type Manager struct { c *Config path string + tempDir string id string close chan string inactive int @@ -39,6 +42,15 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m := &Manager{c: c, path: path, id: id, close: close} m.streams = make(map[string]*Stream) + h := fnv.New32a() + h.Write([]byte(path)) + ph := fmt.Sprint(h.Sum32()) + m.tempDir = fmt.Sprintf("%s/%s-%s", m.c.tempdir, id, ph) + + // Delete temp dir if exists + os.RemoveAll(m.tempDir) + os.MkdirAll(m.tempDir, 0755) + if err := m.ffprobe(); err != nil { return nil, err } @@ -110,6 +122,9 @@ func (m *Manager) Destroy() { for _, stream := range m.streams { stream.Stop() } + + // Delete temp dir + os.RemoveAll(m.tempDir) } func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { diff --git a/stream.go b/stream.go index 68f40224..13592744 100644 --- a/stream.go +++ b/stream.go @@ -381,9 +381,9 @@ func (s *Stream) checkGoal(id int) { func (s *Stream) getTsPath(id int) string { if id == -1 { - return fmt.Sprintf("/tmp/go-vod/%s/%s-%%06d.ts", s.m.id, s.quality) + return fmt.Sprintf("%s/%s-%%06d.ts", s.m.tempDir, s.quality) } - return fmt.Sprintf("/tmp/go-vod/%s/%s-%06d.ts", s.m.id, s.quality, id) + return fmt.Sprintf("%s/%s-%06d.ts", s.m.tempDir, s.quality, id) } // Separate goroutine From fb9a83de56fc0945441b60c7abb7ddf04de681d6 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 20:14:38 -0800 Subject: [PATCH 016/149] Minor fixes --- main.go | 2 -- manager.go | 13 +++++++++++-- stream.go | 10 +++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 150bc3e0..11683be8 100644 --- a/main.go +++ b/main.go @@ -111,8 +111,6 @@ func (h *Handler) watchClose() { if id == "" { return } - - log.Println("Closing stream", id) h.removeManager(id) } } diff --git a/manager.go b/manager.go index 85b41940..bc89894e 100644 --- a/manager.go +++ b/manager.go @@ -94,6 +94,12 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, defer t.Stop() for { <-t.C + + if m.inactive == -1 { + t.Stop() + return + } + m.inactive++ // Check if any stream is active @@ -105,8 +111,8 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, } // Nothing done for 5 minutes - if m.inactive >= 60 { - log.Printf("%s: inactive, closing", m.id) + if m.inactive >= 4 { + t.Stop() m.Destroy() m.close <- m.id return @@ -119,6 +125,9 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Destroys streams. DOES NOT emit on the close channel. func (m *Manager) Destroy() { + log.Printf("%s: destroying manager", m.id) + m.inactive = -1 + for _, stream := range m.streams { stream.Stop() } diff --git a/stream.go b/stream.go index 13592744..a315aead 100644 --- a/stream.go +++ b/stream.go @@ -69,16 +69,13 @@ func (s *Stream) Run() { s.inactive++ // Nothing done for 2 minutes - if s.inactive >= 24 { + if s.inactive >= 3 && s.coder != nil { t.Stop() s.clear() - s.mutex.Unlock() - return } s.mutex.Unlock() case <-s.stop: - log.Printf("%s-%s: received stop signal", s.m.id, s.quality) t.Stop() s.mutex.Lock() s.clear() @@ -106,7 +103,10 @@ func (s *Stream) clear() { } func (s *Stream) Stop() { - s.stop <- true + select { + case s.stop <- true: + default: + } } func (s *Stream) ServeList(w http.ResponseWriter) error { From 7fbcabe2e2b6c3d1e22280d4ea2242b8be3a92fd Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 20:20:45 -0800 Subject: [PATCH 017/149] Prep for release --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 16 +++++ 2 files changed, 218 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..4a5d134a --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# go-vod + +Extremely minimal on-demand video transcoding server in go. + +## Usage + +You need go and ffmpeg/ffprobe installed + +```bash +CGO_ENABLED=0 go build -ldflags="-s -w" +``` + +The server exposes all files as HLS streams, at the URL +``` +http://localhost:47788/player-id/path/to/file/index.m3u8 +``` From 66b2dca145dc56b5493a19b7502637dbb344473c Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 20:22:07 -0800 Subject: [PATCH 018/149] Add cmd --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4a5d134a..71e9e870 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ You need go and ffmpeg/ffprobe installed ```bash CGO_ENABLED=0 go build -ldflags="-s -w" +./go-vod ``` The server exposes all files as HLS streams, at the URL From f9f43ebb4227eb4c7c3a8bd496ad2f34a2756d30 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 20:26:21 -0800 Subject: [PATCH 019/149] Add workflows --- .circleci/config.yml | 48 +++++++++++++++++++++++++++++++++++++ .github/workflows/amd64.yml | 30 +++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .github/workflows/amd64.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..94774e2e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,48 @@ +version: 2.1 + +jobs: + build: + machine: + image: ubuntu-2004:current + resource_class: arm.medium + steps: + - checkout + + - run: | + docker run -it --rm -v "$PWD":/work -w /work golang:1.19-bullseye bash -c 'CGO_ENABLED=0 go build -ldflags="-s -w"' + sudo mv go-vod go-vod-aarch64 + + - persist_to_workspace: + root: . + paths: + - go-vod-aarch64 + + publish-github-release: + docker: + - image: cimg/go:1.17 + steps: + - attach_workspace: + at: ./artifacts + - run: + name: "Publish Release on GitHub" + command: | + go get github.com/tcnksm/ghr + ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace ${CIRCLE_TAG} ./artifacts/ + +workflows: + aarch64: + jobs: + - build: + filters: + branches: + ignore: /.*/ + tags: + only: /^.*/ + - publish-github-release: + requires: + - build + filters: + branches: + ignore: /.*/ + tags: + only: /^.*/ diff --git a/.github/workflows/amd64.yml b/.github/workflows/amd64.yml new file mode 100644 index 00000000..2312e4ad --- /dev/null +++ b/.github/workflows/amd64.yml @@ -0,0 +1,30 @@ +name: amd64 + +on: + push: + tags: + - '*' + +jobs: + build-amd64: + name: amd64 + runs-on: ubuntu-latest + + container: + image: golang:1.19-bullseye + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Build + run: CGO_ENABLED=0 go build -ldflags="-s -w" + + - name: Upload to releases + uses: svenstaro/upload-release-action@v2 + id: attach_to_release + with: + file: go-vod + asset_name: go-vod-amd64 + tag: ${{ github.ref }} + overwrite: true \ No newline at end of file From 00b9e98f177674f0c493c86caf50d062eec318da Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:03:55 -0800 Subject: [PATCH 020/149] Add test arg --- main.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.go b/main.go index 11683be8..0fa56521 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "net/http" "os" @@ -120,6 +121,11 @@ func (h *Handler) Close() { } func main() { + if len(os.Args) >= 2 && os.Args[1] == "test" { + fmt.Println("test successful") + return + } + log.Println("Starting VOD server") h := NewHandler(&Config{ From a9cf287618688af4107ab0d4d316d43343fde52d Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:20:23 -0800 Subject: [PATCH 021/149] Adjust timers --- config.go | 5 +++++ main.go | 16 +++++++++------- manager.go | 2 +- stream.go | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/config.go b/config.go index f64ad77d..ba73034c 100644 --- a/config.go +++ b/config.go @@ -16,4 +16,9 @@ type Config struct { goalBufferMin int // Number of chunks in goal to stop encoding goalBufferMax int + + // Number of seconds to wait before shutting down encoding + streamIdleTime int + // Number of seconds to wait before shutting down a client + managerIdleTime int } diff --git a/main.go b/main.go index 0fa56521..347e0d83 100644 --- a/main.go +++ b/main.go @@ -129,13 +129,15 @@ func main() { log.Println("Starting VOD server") h := NewHandler(&Config{ - ffmpeg: "ffmpeg", - ffprobe: "ffprobe", - tempdir: "/tmp/go-vod", - chunkSize: 3, - lookBehind: 5, - goalBufferMin: 3, - goalBufferMax: 8, + ffmpeg: "ffmpeg", + ffprobe: "ffprobe", + tempdir: "/tmp/go-vod", + chunkSize: 3, + lookBehind: 5, + goalBufferMin: 3, + goalBufferMax: 8, + streamIdleTime: 120, + managerIdleTime: 240, }) http.Handle("/", h) diff --git a/manager.go b/manager.go index bc89894e..919eaa35 100644 --- a/manager.go +++ b/manager.go @@ -111,7 +111,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, } // Nothing done for 5 minutes - if m.inactive >= 4 { + if m.inactive >= m.c.managerIdleTime/5 { t.Stop() m.Destroy() m.close <- m.id diff --git a/stream.go b/stream.go index a315aead..e0a895b5 100644 --- a/stream.go +++ b/stream.go @@ -69,7 +69,7 @@ func (s *Stream) Run() { s.inactive++ // Nothing done for 2 minutes - if s.inactive >= 3 && s.coder != nil { + if s.inactive >= s.c.streamIdleTime/5 && s.coder != nil { t.Stop() s.clear() } From 689aa6eae4efbdd79510ce749f82c1a2f46a196d Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:23:20 -0800 Subject: [PATCH 022/149] Add link to memories --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71e9e870..acf9e60c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # go-vod -Extremely minimal on-demand video transcoding server in go. +Extremely minimal on-demand video transcoding server in go. Used by the FOSS photos app, [Memories](https://github.com/pulsejet/memories). ## Usage From 2cdc5d51311b1670f0abb3e4a2795ab9a79f8cd4 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:29:38 -0800 Subject: [PATCH 023/149] Adjustment for VAAPI --- stream.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/stream.go b/stream.go index e0a895b5..b77e4f2a 100644 --- a/stream.go +++ b/stream.go @@ -274,6 +274,15 @@ func (s *Stream) transcode(startId int) { }...) } + // QSV / encoder selection + VAAPI := os.Getenv("VAAPI") == "1" + CV := "libx264" + if VAAPI { + CV = "h264_vaapi" + extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" + args = append(args, strings.Split(extra, " ")...) + } + // Input specs args = append(args, []string{ "-autorotate", "0", // consistent behavior @@ -281,22 +290,10 @@ func (s *Stream) transcode(startId int) { "-copyts", // So the "-to" refers to the original TS }...) - // QSV / encoder selection - VAAPI := os.Getenv("VAAPI") == "1" - CV := "libx264" - VF := "" - if VAAPI { - CV = "h264_vaapi" - VF = "scale_vaapi=w=SCALE_WIDTH:h=SCALE_HEIGHT:force_original_aspect_ratio=decrease" - extra := strings.Split("-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi", " ") - args = append(args, extra...) - } - // Scaling for output var scale string if VAAPI { - scale = strings.Replace(VF, "SCALE_WIDTH", fmt.Sprintf("%d", s.width), 1) - scale = strings.Replace(scale, "SCALE_HEIGHT", fmt.Sprintf("%d", s.height), 1) + scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else if s.width >= s.height { scale = fmt.Sprintf("scale=-2:%d", s.height) } else { From 88d34fdf497af1092779c2f84230f8ca66116343 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:30:54 -0800 Subject: [PATCH 024/149] Add link to go-transcode --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index acf9e60c..76313bb7 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,6 @@ The server exposes all files as HLS streams, at the URL ``` http://localhost:47788/player-id/path/to/file/index.m3u8 ``` + +## Thanks +Inspired from [go-transcode](https://github.com/m1k1o/go-transcode) From e993d738ee8dcd04ef594a2c390913c9d01b3102 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:31:47 -0800 Subject: [PATCH 025/149] Add link to go-transcode --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76313bb7..cfaa8399 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ http://localhost:47788/player-id/path/to/file/index.m3u8 ``` ## Thanks -Inspired from [go-transcode](https://github.com/m1k1o/go-transcode) +Partially inspired from [go-transcode](https://github.com/m1k1o/go-transcode). The projects use different approaches for segmenting the transcodes. From 7764a704714de1c40fc0cf24758c14ea301a0766 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 10 Nov 2022 21:49:46 -0800 Subject: [PATCH 026/149] Increase idle time --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 347e0d83..46ab91c0 100644 --- a/main.go +++ b/main.go @@ -136,8 +136,8 @@ func main() { lookBehind: 5, goalBufferMin: 3, goalBufferMax: 8, - streamIdleTime: 120, - managerIdleTime: 240, + streamIdleTime: 300, + managerIdleTime: 600, }) http.Handle("/", h) From ae1c4ae6823197c211b66b908912b324270e2247 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 11 Nov 2022 18:18:16 -0800 Subject: [PATCH 027/149] Return timeout in waitForChunk --- stream.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stream.go b/stream.go index b77e4f2a..0a24656c 100644 --- a/stream.go +++ b/stream.go @@ -246,6 +246,9 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { if chunk.done { s.returnChunk(w, chunk) } + + // Return timeout error + w.WriteHeader(http.StatusRequestTimeout) } func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { From 5b5a5afe788ef78ade5b36bd84ff9ad8726a7c72 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 02:09:09 -0800 Subject: [PATCH 028/149] Adjust bitrates --- manager.go | 8 ++++---- stream.go | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/manager.go b/manager.go index 919eaa35..3f729970 100644 --- a/manager.go +++ b/manager.go @@ -58,10 +58,10 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.chunkSize))) // Possible streams - m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 800000} - m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1500000} - m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 3000000} - m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 5000000} + m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 750000} + m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1000000} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2500000} + m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 4500000} m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9000000} m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14000000} diff --git a/stream.go b/stream.go index 0a24656c..1a904b04 100644 --- a/stream.go +++ b/stream.go @@ -307,8 +307,7 @@ func (s *Stream) transcode(startId int) { if s.quality != "max" { args = append(args, []string{ "-vf", scale, - "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), - "-bufsize", fmt.Sprintf("%dK", s.bitrate/3000), + "-b:v", fmt.Sprintf("%dk", s.bitrate/1000), }...) } @@ -327,9 +326,15 @@ func (s *Stream) transcode(startId int) { } // Audio + ab := "192k" + if s.bitrate < 1000000 { + ab = "64k" + } else if s.bitrate < 3000000 { + ab = "128k" + } args = append(args, []string{ "-c:a", "aac", - "-b:a", "192k", + "-b:a", ab, }...) // Segmenting specs From 22e6715b2afbbef369f9731dda07cf56421448ae Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 02:39:56 -0800 Subject: [PATCH 029/149] More tuning --- manager.go | 10 +++++----- stream.go | 10 +++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/manager.go b/manager.go index 3f729970..3100c32b 100644 --- a/manager.go +++ b/manager.go @@ -58,12 +58,12 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.chunkSize))) // Possible streams - m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 750000} + m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 500000} m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1000000} - m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2500000} - m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 4500000} - m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 9000000} - m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 14000000} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2000000} + m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 3500000} + m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 6000000} + m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 10000000} // Only keep streams that are smaller than the video var highest int diff --git a/stream.go b/stream.go index 1a904b04..03c5d51b 100644 --- a/stream.go +++ b/stream.go @@ -317,8 +317,12 @@ func (s *Stream) transcode(startId int) { "-profile:v", "high", }...) - // Extra args only for x264 - if !VAAPI { + // Device specific output args + if VAAPI { + args = append(args, []string{ + "-low_power", "1", + }...) + } else { args = append(args, []string{ "-preset", "faster", "-level:v", "4.0", @@ -342,7 +346,7 @@ func (s *Stream) transcode(startId int) { "-avoid_negative_ts", "disabled", "-f", "hls", "-hls_time", fmt.Sprintf("%d", s.c.chunkSize), - "-g", fmt.Sprintf("%d", s.c.chunkSize), + "-g", "64", "-keyint_min", "64", "-hls_segment_type", "mpegts", "-start_number", fmt.Sprintf("%d", startId), "-hls_segment_filename", s.getTsPath(-1), From 2c4575171f6d524e4b684d691dbcbd3d37897e64 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 02:41:42 -0800 Subject: [PATCH 030/149] stream: double header write --- stream.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stream.go b/stream.go index 03c5d51b..79de6f8d 100644 --- a/stream.go +++ b/stream.go @@ -245,6 +245,7 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { // check for success if chunk.done { s.returnChunk(w, chunk) + return } // Return timeout error From 3408e48db9f928481c95269b0bb166459a2fbc91 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 03:05:30 -0800 Subject: [PATCH 031/149] More tuning --- stream.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/stream.go b/stream.go index 79de6f8d..d6e0b14b 100644 --- a/stream.go +++ b/stream.go @@ -39,8 +39,9 @@ type Stream struct { goal int - mutex sync.Mutex - chunks map[int]*Chunk + mutex sync.Mutex + chunks map[int]*Chunk + seenChunks map[int]bool // only for stdout reader coder *exec.Cmd @@ -94,6 +95,7 @@ func (s *Stream) clear() { } s.chunks = make(map[int]*Chunk) + s.seenChunks = make(map[int]bool) s.goal = 0 if s.coder != nil { @@ -347,7 +349,7 @@ func (s *Stream) transcode(startId int) { "-avoid_negative_ts", "disabled", "-f", "hls", "-hls_time", fmt.Sprintf("%d", s.c.chunkSize), - "-g", "64", "-keyint_min", "64", + "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.chunkSize), "-hls_segment_type", "mpegts", "-start_number", fmt.Sprintf("%d", startId), "-hls_segment_filename", s.getTsPath(-1), @@ -425,9 +427,6 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 l := string(line) if strings.Contains(l, ".ts") { - // Debug - log.Printf("%s-%s: recv %s", s.m.id, s.quality, l) - // 1080p-000003.ts idx := strings.Split(strings.Split(l, "-")[1], ".")[0] id, err := strconv.Atoi(idx) @@ -435,6 +434,14 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 log.Println("Error parsing chunk id") } + if s.seenChunks[id] { + continue + } + s.seenChunks[id] = true + + // Debug + log.Printf("%s-%s: recv %s", s.m.id, s.quality, l) + func() { s.mutex.Lock() defer s.mutex.Unlock() From bf2f137d89d9eefe653a6b55260211e2e40b2edd Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 04:35:05 -0800 Subject: [PATCH 032/149] Tune with framerate --- manager.go | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/manager.go b/manager.go index 3100c32b..0dc0b7bd 100644 --- a/manager.go +++ b/manager.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "hash/fnv" "log" @@ -33,9 +34,10 @@ type Manager struct { } type ProbeVideoData struct { - Width int - Height int - Duration time.Duration + Width int + Height int + Duration time.Duration + FrameRate int } func NewManager(c *Config, path string, id string, close chan string) (*Manager, error) { @@ -59,8 +61,8 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Possible streams m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 500000} - m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1000000} - m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2000000} + m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1200000} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2500000} m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 3500000} m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 6000000} m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 10000000} @@ -68,6 +70,9 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Only keep streams that are smaller than the video var highest int for k, stream := range m.streams { + // scale bitrate by frame rate with reference 30 + stream.bitrate = int(float64(stream.bitrate) * float64(m.probe.FrameRate) / 30.0) + if stream.height >= m.probe.Height || stream.width >= m.probe.Width { delete(m.streams, k) } else if stream.bitrate > highest { @@ -209,7 +214,7 @@ func (m *Manager) ffprobe() error { // video "-show_entries", "format=duration", - "-show_entries", "stream=duration,width,height", + "-show_entries", "stream=duration,width,height,avg_frame_rate", "-select_streams", "v", // Video stream only, we're not interested in audio "-of", "json", @@ -231,9 +236,10 @@ func (m *Manager) ffprobe() error { out := struct { Streams []struct { - Width int `json:"width"` - Height int `json:"height"` - Duration string `json:"duration"` + Width int `json:"width"` + Height int `json:"height"` + Duration string `json:"duration"` + FrameRate string `json:"avg_frame_rate"` } `json:"streams"` Format struct { Duration string `json:"duration"` @@ -244,6 +250,10 @@ func (m *Manager) ffprobe() error { return err } + if len(out.Streams) == 0 { + return errors.New("no video streams found") + } + var duration time.Duration if out.Streams[0].Duration != "" { duration, err = time.ParseDuration(out.Streams[0].Duration + "s") @@ -258,11 +268,27 @@ func (m *Manager) ffprobe() error { } } - m.probe = &ProbeVideoData{ - Width: out.Streams[0].Width, - Height: out.Streams[0].Height, - Duration: duration, + // FrameRate is a fraction string + frac := strings.Split(out.Streams[0].FrameRate, "/") + if len(frac) != 2 { + frac = []string{"30", "1"} } + num, e1 := strconv.Atoi(frac[0]) + den, e2 := strconv.Atoi(frac[1]) + if e1 != nil || e2 != nil { + num = 30 + den = 1 + } + frameRate := float64(num) / float64(den) + + m.probe = &ProbeVideoData{ + Width: out.Streams[0].Width, + Height: out.Streams[0].Height, + Duration: duration, + FrameRate: int(frameRate), + } + + fmt.Println(m.probe) return nil } From a42fcd69789e199f5e3d043451e4e1f02560249d Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 08:27:14 -0800 Subject: [PATCH 033/149] Remove stray print statement --- manager.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/manager.go b/manager.go index 0dc0b7bd..4bd4ed09 100644 --- a/manager.go +++ b/manager.go @@ -288,7 +288,5 @@ func (m *Manager) ffprobe() error { FrameRate: int(frameRate), } - fmt.Println(m.probe) - return nil } From af6c0eb190238928f7ba67e6bc0911cfdf5244c5 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 09:50:16 -0800 Subject: [PATCH 034/149] Use copy if we can --- manager.go | 5 ++++- stream.go | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/manager.go b/manager.go index 4bd4ed09..0e256647 100644 --- a/manager.go +++ b/manager.go @@ -38,6 +38,7 @@ type ProbeVideoData struct { Height int Duration time.Duration FrameRate int + CodecName string } func NewManager(c *Config, path string, id string, close chan string) (*Manager, error) { @@ -214,7 +215,7 @@ func (m *Manager) ffprobe() error { // video "-show_entries", "format=duration", - "-show_entries", "stream=duration,width,height,avg_frame_rate", + "-show_entries", "stream=duration,width,height,avg_frame_rate,codec_name", "-select_streams", "v", // Video stream only, we're not interested in audio "-of", "json", @@ -240,6 +241,7 @@ func (m *Manager) ffprobe() error { Height int `json:"height"` Duration string `json:"duration"` FrameRate string `json:"avg_frame_rate"` + CodecName string `json:"codec_name"` } `json:"streams"` Format struct { Duration string `json:"duration"` @@ -286,6 +288,7 @@ func (m *Manager) ffprobe() error { Height: out.Streams[0].Height, Duration: duration, FrameRate: int(frameRate), + CodecName: out.Streams[0].CodecName, } return nil diff --git a/stream.go b/stream.go index d6e0b14b..5144a455 100644 --- a/stream.go +++ b/stream.go @@ -280,10 +280,13 @@ func (s *Stream) transcode(startId int) { }...) } - // QSV / encoder selection - VAAPI := os.Getenv("VAAPI") == "1" + // encoder selection CV := "libx264" - if VAAPI { + + // no need to transcode h264 streams for max quality + if s.quality == "max" && s.m.probe.CodecName == "h264" { + CV = "copy" + } else if os.Getenv("VAAPI") == "1" { CV = "h264_vaapi" extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" args = append(args, strings.Split(extra, " ")...) @@ -298,7 +301,7 @@ func (s *Stream) transcode(startId int) { // Scaling for output var scale string - if VAAPI { + if CV == "h264_vaapi" { scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else if s.width >= s.height { scale = fmt.Sprintf("scale=-2:%d", s.height) @@ -306,26 +309,26 @@ func (s *Stream) transcode(startId int) { scale = fmt.Sprintf("scale=%d:-2", s.width) } - // not original (max) + // do not scale or set bitrate for full quality if s.quality != "max" { args = append(args, []string{ "-vf", scale, "-b:v", fmt.Sprintf("%dk", s.bitrate/1000), + "-profile:v", "high", }...) } // Output specs args = append(args, []string{ "-c:v", CV, - "-profile:v", "high", }...) // Device specific output args - if VAAPI { + if CV == "h264_vaapi" { args = append(args, []string{ "-low_power", "1", }...) - } else { + } else if CV == "libx264" { args = append(args, []string{ "-preset", "faster", "-level:v", "4.0", From 8fc89b853cd0b1d6e99a35cb20986288aaac57d3 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 12 Nov 2022 10:51:31 -0800 Subject: [PATCH 035/149] More tuning --- manager.go | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/manager.go b/manager.go index 0e256647..5fbb69e6 100644 --- a/manager.go +++ b/manager.go @@ -39,6 +39,7 @@ type ProbeVideoData struct { Duration time.Duration FrameRate int CodecName string + BitRate int } func NewManager(c *Config, path string, id string, close chan string) (*Manager, error) { @@ -58,33 +59,43 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, return nil, err } + // heuristic + if m.probe.CodecName != "h264" { + m.probe.BitRate *= 2 + } + m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.chunkSize))) // Possible streams m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 500000} m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1200000} - m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2500000} - m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 3500000} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2200000} + m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 3600000} m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 6000000} m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 10000000} // Only keep streams that are smaller than the video - var highest int for k, stream := range m.streams { // scale bitrate by frame rate with reference 30 stream.bitrate = int(float64(stream.bitrate) * float64(m.probe.FrameRate) / 30.0) - if stream.height >= m.probe.Height || stream.width >= m.probe.Width { + if stream.height > m.probe.Height || stream.width > m.probe.Width { delete(m.streams, k) - } else if stream.bitrate > highest { - highest = stream.bitrate + } + + if stream.height == m.probe.Height || stream.width == m.probe.Width { + if stream.bitrate > m.probe.BitRate || float64(stream.bitrate) > float64(m.probe.BitRate)*0.8 { + // no point in "upscaling" + // should have at least 20% difference + delete(m.streams, k) + } } } // Original stream m.streams["max"] = &Stream{ c: c, m: m, quality: "max", height: m.probe.Height, width: m.probe.Width, - bitrate: int(float64(highest) * 1.5), + bitrate: m.probe.BitRate, } // Start all streams @@ -198,7 +209,7 @@ func (m *Manager) ServeIndex(w http.ResponseWriter) error { // Write all streams for _, stream := range streams { - s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,NAME=\"%s\"\n%s.m3u8\n", stream.bitrate, stream.width, stream.height, stream.quality, stream.quality) + s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,FRAME-RATE=%d\n%s.m3u8\n", stream.bitrate, stream.width, stream.height, m.probe.FrameRate, stream.quality) w.Write([]byte(s)) } return nil @@ -215,7 +226,7 @@ func (m *Manager) ffprobe() error { // video "-show_entries", "format=duration", - "-show_entries", "stream=duration,width,height,avg_frame_rate,codec_name", + "-show_entries", "stream=duration,width,height,avg_frame_rate,codec_name,bit_rate", "-select_streams", "v", // Video stream only, we're not interested in audio "-of", "json", @@ -242,6 +253,7 @@ func (m *Manager) ffprobe() error { Duration string `json:"duration"` FrameRate string `json:"avg_frame_rate"` CodecName string `json:"codec_name"` + BitRate string `json:"bit_rate"` } `json:"streams"` Format struct { Duration string `json:"duration"` @@ -283,12 +295,19 @@ func (m *Manager) ffprobe() error { } frameRate := float64(num) / float64(den) + // BitRate is a string + bitRate, err := strconv.Atoi(out.Streams[0].BitRate) + if err != nil { + bitRate = 5000000 + } + m.probe = &ProbeVideoData{ Width: out.Streams[0].Width, Height: out.Streams[0].Height, Duration: duration, FrameRate: int(frameRate), CodecName: out.Streams[0].CodecName, + BitRate: bitRate, } return nil From 10a4e469baa9d388bcc01b3e494463a2e53c93f9 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 13 Nov 2022 02:41:12 -0800 Subject: [PATCH 036/149] Revert to maxrate for bitrate https://github.com/pulsejet/memories/issues/190 --- stream.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stream.go b/stream.go index 5144a455..21d86185 100644 --- a/stream.go +++ b/stream.go @@ -313,7 +313,8 @@ func (s *Stream) transcode(startId int) { if s.quality != "max" { args = append(args, []string{ "-vf", scale, - "-b:v", fmt.Sprintf("%dk", s.bitrate/1000), + "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), + "-bufsize", fmt.Sprintf("%dK", s.bitrate/1000), "-profile:v", "high", }...) } From e20888f24440b076505ac114b56620cdfc563957 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 13 Nov 2022 19:34:12 -0800 Subject: [PATCH 037/149] revert copy encoding --- stream.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/stream.go b/stream.go index 21d86185..040fa531 100644 --- a/stream.go +++ b/stream.go @@ -284,9 +284,7 @@ func (s *Stream) transcode(startId int) { CV := "libx264" // no need to transcode h264 streams for max quality - if s.quality == "max" && s.m.probe.CodecName == "h264" { - CV = "copy" - } else if os.Getenv("VAAPI") == "1" { + if os.Getenv("VAAPI") == "1" { CV = "h264_vaapi" extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" args = append(args, strings.Split(extra, " ")...) @@ -315,13 +313,13 @@ func (s *Stream) transcode(startId int) { "-vf", scale, "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), "-bufsize", fmt.Sprintf("%dK", s.bitrate/1000), - "-profile:v", "high", }...) } // Output specs args = append(args, []string{ "-c:v", CV, + "-profile:v", "high", }...) // Device specific output args From e40f53c18b5e878dbc793543ddab3d46a29555fe Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 13 Nov 2022 22:08:14 -0800 Subject: [PATCH 038/149] Fix keyframe alignment --- stream.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stream.go b/stream.go index 040fa531..5ca108e1 100644 --- a/stream.go +++ b/stream.go @@ -268,6 +268,11 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { } func (s *Stream) transcode(startId int) { + if startId > 0 { + // Start one frame before + // This ensures that the keyframes are aligned + startId-- + } startAt := float64(startId * s.c.chunkSize) args := []string{ From 7f26d300e3982b243eed47ca09b9ec69142f7985 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 14 Nov 2022 00:57:03 -0800 Subject: [PATCH 039/149] Adjust crf --- stream.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/stream.go b/stream.go index 5ca108e1..1851267a 100644 --- a/stream.go +++ b/stream.go @@ -313,11 +313,16 @@ func (s *Stream) transcode(startId int) { } // do not scale or set bitrate for full quality - if s.quality != "max" { + if s.quality == "max" { + args = append(args, []string{ + "-crf", "22", + }...) + } else { args = append(args, []string{ "-vf", scale, - "-maxrate", fmt.Sprintf("%dk", s.bitrate/1000), - "-bufsize", fmt.Sprintf("%dK", s.bitrate/1000), + "-crf", "24", + "-maxrate", fmt.Sprintf("%d", s.bitrate), + "-bufsize", fmt.Sprintf("%d", s.bitrate*2), }...) } From 3bfdc856039a6dc812e4518d27fa95c6d3f36dcf Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 14 Nov 2022 01:25:42 -0800 Subject: [PATCH 040/149] Add global_quality --- stream.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/stream.go b/stream.go index 1851267a..f2f4c6bb 100644 --- a/stream.go +++ b/stream.go @@ -313,14 +313,9 @@ func (s *Stream) transcode(startId int) { } // do not scale or set bitrate for full quality - if s.quality == "max" { - args = append(args, []string{ - "-crf", "22", - }...) - } else { + if s.quality != "max" { args = append(args, []string{ "-vf", scale, - "-crf", "24", "-maxrate", fmt.Sprintf("%d", s.bitrate), "-bufsize", fmt.Sprintf("%d", s.bitrate*2), }...) @@ -336,11 +331,13 @@ func (s *Stream) transcode(startId int) { if CV == "h264_vaapi" { args = append(args, []string{ "-low_power", "1", + "-global_quality", "25", }...) } else if CV == "libx264" { args = append(args, []string{ "-preset", "faster", "-level:v", "4.0", + "-crf", "24", }...) } From 1469f9eb9d615a8af300e2465f83996ce3e21929 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 15 Nov 2022 02:09:33 -0800 Subject: [PATCH 041/149] Read paths from env variables --- main.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 46ab91c0..b3159046 100644 --- a/main.go +++ b/main.go @@ -128,10 +128,27 @@ func main() { log.Println("Starting VOD server") + // get executable paths + ffmpeg := os.Getenv("FFMPEG") + if ffmpeg == "" { + ffmpeg = "ffmpeg" + } + + ffprobe := os.Getenv("FFPROBE") + if ffprobe == "" { + ffprobe = "ffprobe" + } + + // get tempdir + tempdir := os.Getenv("GOVOD_TEMPDIR") + if tempdir == "" { + tempdir = "/tmp/go-vod" + } + h := NewHandler(&Config{ - ffmpeg: "ffmpeg", - ffprobe: "ffprobe", - tempdir: "/tmp/go-vod", + ffmpeg: ffmpeg, + ffprobe: ffprobe, + tempdir: tempdir, chunkSize: 3, lookBehind: 5, goalBufferMin: 3, From 513e9410ca58855991f3e45e7675bbd48b100401 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 16 Nov 2022 07:10:21 -0800 Subject: [PATCH 042/149] Force only one audio track --- stream.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stream.go b/stream.go index f2f4c6bb..87fbeb83 100644 --- a/stream.go +++ b/stream.go @@ -350,6 +350,7 @@ func (s *Stream) transcode(startId int) { } args = append(args, []string{ "-c:a", "aac", + "-ac", "1", "-b:a", ab, }...) From 88685d0d69b4e6aeab26f4b47ee4bec315547724 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 21 Nov 2022 02:22:38 -0800 Subject: [PATCH 043/149] Reduce idle times --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index b3159046..799fabbd 100644 --- a/main.go +++ b/main.go @@ -153,8 +153,8 @@ func main() { lookBehind: 5, goalBufferMin: 3, goalBufferMax: 8, - streamIdleTime: 300, - managerIdleTime: 600, + streamIdleTime: 60, + managerIdleTime: 60, }) http.Handle("/", h) From 630178e6cb843185a6d18fd4736a6c51220c9d88 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 21 Nov 2022 02:23:35 -0800 Subject: [PATCH 044/149] Reduce buffer goal --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 799fabbd..de463bac 100644 --- a/main.go +++ b/main.go @@ -151,8 +151,8 @@ func main() { tempdir: tempdir, chunkSize: 3, lookBehind: 5, - goalBufferMin: 3, - goalBufferMax: 8, + goalBufferMin: 1, + goalBufferMax: 4, streamIdleTime: 60, managerIdleTime: 60, }) From afdda3038b3bde7c5efd16cb432aa93b9dabda84 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 21 Nov 2022 16:22:02 -0800 Subject: [PATCH 045/149] Fix 10bit decoding --- stream.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stream.go b/stream.go index 87fbeb83..7c78f379 100644 --- a/stream.go +++ b/stream.go @@ -305,11 +305,11 @@ func (s *Stream) transcode(startId int) { // Scaling for output var scale string if CV == "h264_vaapi" { - scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) + scale = fmt.Sprintf("format=nv12|vaapi,hwupload,scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else if s.width >= s.height { - scale = fmt.Sprintf("scale=-2:%d", s.height) + scale = fmt.Sprintf("format=nv12,scale=-2:%d", s.height) } else { - scale = fmt.Sprintf("scale=%d:-2", s.width) + scale = fmt.Sprintf("format=nv12,scale=%d:-2", s.width) } // do not scale or set bitrate for full quality From 8f26483140a9fcdd288cfeb7b7a1d464be17da25 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 21 Nov 2022 16:22:16 -0800 Subject: [PATCH 046/149] Add map 0 to ffmpeg --- stream.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stream.go b/stream.go index 7c78f379..e65255ac 100644 --- a/stream.go +++ b/stream.go @@ -323,6 +323,7 @@ func (s *Stream) transcode(startId int) { // Output specs args = append(args, []string{ + "-map", "0", "-c:v", CV, "-profile:v", "high", }...) From d0f1433f64e0244f61e4be0d624e8f7ba1196f76 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 21 Nov 2022 16:22:28 -0800 Subject: [PATCH 047/149] Increase timeout to 10s --- stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream.go b/stream.go index e65255ac..377f5498 100644 --- a/stream.go +++ b/stream.go @@ -224,7 +224,7 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { // Add our channel notif := make(chan bool) chunk.notifs = append(chunk.notifs, notif) - t := time.NewTimer(5 * time.Second) + t := time.NewTimer(10 * time.Second) s.mutex.Unlock() From c90483f61a75f8fec0c7994e1007ebdc6dfa9361 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 22 Nov 2022 01:57:29 -0800 Subject: [PATCH 048/149] Remove -map 0 --- stream.go | 1 - 1 file changed, 1 deletion(-) diff --git a/stream.go b/stream.go index 377f5498..08c5c163 100644 --- a/stream.go +++ b/stream.go @@ -323,7 +323,6 @@ func (s *Stream) transcode(startId int) { // Output specs args = append(args, []string{ - "-map", "0", "-c:v", CV, "-profile:v", "high", }...) From 56767bc56d246dd0816713aa1af0ac4dd8f0080a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 22 Nov 2022 09:58:41 -0800 Subject: [PATCH 049/149] Add format to max stream --- stream.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/stream.go b/stream.go index 08c5c163..06b1633e 100644 --- a/stream.go +++ b/stream.go @@ -304,18 +304,29 @@ func (s *Stream) transcode(startId int) { // Scaling for output var scale string + var format string if CV == "h264_vaapi" { - scale = fmt.Sprintf("format=nv12|vaapi,hwupload,scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) - } else if s.width >= s.height { - scale = fmt.Sprintf("format=nv12,scale=-2:%d", s.height) + // VAAPI + format = "format=nv12|vaapi,hwupload" + scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else { - scale = fmt.Sprintf("format=nv12,scale=%d:-2", s.width) + // x264 + format = "format=nv12" + if s.width >= s.height { + scale = fmt.Sprintf("scale=-2:%d", s.height) + } else { + scale = fmt.Sprintf("scale=%d:-2", s.width) + } } // do not scale or set bitrate for full quality - if s.quality != "max" { + if s.quality == "max" { args = append(args, []string{ - "-vf", scale, + "-vf", format, + }...) + } else { + args = append(args, []string{ + "-vf", fmt.Sprintf("%s,%s", format, scale), "-maxrate", fmt.Sprintf("%d", s.bitrate), "-bufsize", fmt.Sprintf("%d", s.bitrate*2), }...) From 9972fc23c6c0dc3a8aab28dc246065ae8d9e330b Mon Sep 17 00:00:00 2001 From: MB-Finski <64466176+MB-Finski@users.noreply.github.com> Date: Fri, 25 Nov 2022 13:59:33 +0200 Subject: [PATCH 050/149] Add support for NVIDIA GPU accelerated transcoding Add a transcoding profile for ffmpeg suitable for using with NVENC. --- stream.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/stream.go b/stream.go index 06b1633e..01492e59 100644 --- a/stream.go +++ b/stream.go @@ -288,12 +288,16 @@ func (s *Stream) transcode(startId int) { // encoder selection CV := "libx264" - // no need to transcode h264 streams for max quality + // Check whether hwaccel should be used if os.Getenv("VAAPI") == "1" { CV = "h264_vaapi" extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" args = append(args, strings.Split(extra, " ")...) - } + } else if os.Getenv("NVENC") == "1" { + CV = "h264_nvenc" + extra := "-hwaccel cuda -hwaccel_output_format cuda" + args = append(args, strings.Split(extra, " ")...) + } // Input specs args = append(args, []string{ @@ -309,7 +313,15 @@ func (s *Stream) transcode(startId int) { // VAAPI format = "format=nv12|vaapi,hwupload" scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) - } else { + } else if CV == "h264_nvenc" { + // NVENC + format = "format=nv12|cuda,hwupload" + if s.width >= s.height { + scale = fmt.Sprintf("scale_cuda=-2:%d", s.height) + } else { + scale = fmt.Sprintf("scale_cuda=%d:-2", s.width) + } + } else { // x264 format = "format=nv12" if s.width >= s.height { From 0dd14fabe2b19fd093cf08b9c7a4ffd6a73120a8 Mon Sep 17 00:00:00 2001 From: MB-Finski <64466176+MB-Finski@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:33:26 +0200 Subject: [PATCH 051/149] Optimizations for NVENC transcode profile Permormance and quality related optimizations for NVENC. Also contains a bugfix where transcoding with "max"-profile would fail due to a bug in the NVENC itself. --- stream.go | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/stream.go b/stream.go index 01492e59..2c85ee32 100644 --- a/stream.go +++ b/stream.go @@ -315,12 +315,7 @@ func (s *Stream) transcode(startId int) { scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else if CV == "h264_nvenc" { // NVENC - format = "format=nv12|cuda,hwupload" - if s.width >= s.height { - scale = fmt.Sprintf("scale_cuda=-2:%d", s.height) - } else { - scale = fmt.Sprintf("scale_cuda=%d:-2", s.width) - } + scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) } else { // x264 format = "format=nv12" @@ -333,15 +328,31 @@ func (s *Stream) transcode(startId int) { // do not scale or set bitrate for full quality if s.quality == "max" { - args = append(args, []string{ - "-vf", format, - }...) + if CV == "h264_nvenc" { + // Due to a bug(?) in NVENC, passthrough=0 must be set + args = append(args, []string{ + "-vf", "scale_cuda=passthrough=0", + }...) + } else { + args = append(args, []string{ + "-vf", format, + }...) + } } else { - args = append(args, []string{ - "-vf", fmt.Sprintf("%s,%s", format, scale), - "-maxrate", fmt.Sprintf("%d", s.bitrate), - "-bufsize", fmt.Sprintf("%d", s.bitrate*2), - }...) + if CV == "h264_nvenc" { + args = append(args, []string{ + "-vf", scale, + }...) + } else { + args = append(args, []string{ + "-vf", fmt.Sprintf("%s,%s", format, scale), + }...) + } + // Common arguments + args = append(args, []string{ + "-maxrate", fmt.Sprintf("%d", s.bitrate), + "-bufsize", fmt.Sprintf("%d", s.bitrate*2), + }...) } // Output specs @@ -356,6 +367,15 @@ func (s *Stream) transcode(startId int) { "-low_power", "1", "-global_quality", "25", }...) + } else if CV == "h264_nvenc" { + args = append(args, []string{ + "-preset", "p6", + "-tune", "ll", + "-temporal-aq", "1", + "-rc", "vbr", + "-rc-lookahead","30", + "-cq", "24", + }...) } else if CV == "libx264" { args = append(args, []string{ "-preset", "faster", From 2c92130c4070d6a1898ea65929dd9072985479d5 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 29 Nov 2022 13:00:36 -0800 Subject: [PATCH 052/149] Support serving full video --- manager.go | 9 ++++++ stream.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/manager.go b/manager.go index 5fbb69e6..f1ca9dbe 100644 --- a/manager.go +++ b/manager.go @@ -190,6 +190,15 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string } } + // Stream full video + mp4Sfx := ".mp4" + if strings.HasSuffix(chunk, mp4Sfx) { + quality := strings.TrimSuffix(chunk, mp4Sfx) + if stream, ok := m.streams[quality]; ok { + return stream.ServeFullVideo(w) + } + } + w.WriteHeader(http.StatusNotFound) return nil } diff --git a/stream.go b/stream.go index 06b1633e..6904d7c7 100644 --- a/stream.go +++ b/stream.go @@ -100,6 +100,7 @@ func (s *Stream) clear() { if s.coder != nil { s.coder.Process.Kill() + s.coder.Wait() s.coder = nil } } @@ -180,6 +181,75 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { return nil } +func (s *Stream) ServeFullVideo(w http.ResponseWriter) error { + args := s.transcodeArgs(0) + + // Output mp4 + args = append(args, []string{ + "-movflags", "frag_keyframe+empty_moov+faststart", "-f", "mp4", "pipe:1", + }...) + + coder := exec.Command(s.c.ffmpeg, args...) + log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(coder.Args[:], " ")) + + cmdStdOut, err := coder.StdoutPipe() + if err != nil { + fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + } + + cmdStdErr, err := coder.StderrPipe() + if err != nil { + fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + } + + err = coder.Start() + if err != nil { + log.Printf("FATAL: ffmpeg command failed with %s\n", err) + } + go s.monitorStderr(cmdStdErr) + + // Write to response + defer cmdStdOut.Close() + stdoutReader := bufio.NewReader(cmdStdOut) + + // Write mp4 header + w.Header().Set("Content-Type", "video/mp4") + w.WriteHeader(http.StatusOK) + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Server does not support Flusher!", + http.StatusInternalServerError) + return nil + } + + // Write data, flusing every 1MB + buf := make([]byte, 1024*1024) + for { + n, err := stdoutReader.Read(buf) + if err != nil { + if err == io.EOF { + break + } + log.Printf("FATAL: ffmpeg command failed with %s\n", err) + break + } + + _, err = w.Write(buf[:n]) + if err != nil { + log.Printf("%s-%s: client closed connection", s.m.id, s.quality) + log.Println(err) + break + } + flusher.Flush() + } + + // Terminate ffmpeg process + coder.Process.Kill() + coder.Wait() + + return nil +} + func (s *Stream) createChunk(id int) *Chunk { if c, ok := s.chunks[id]; ok { return c @@ -267,14 +337,8 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { s.waitForChunk(w, chunk) // this is also a request } -func (s *Stream) transcode(startId int) { - if startId > 0 { - // Start one frame before - // This ensures that the keyframes are aligned - startId-- - } - startAt := float64(startId * s.c.chunkSize) - +// Get arguments to ffmpeg +func (s *Stream) transcodeArgs(startAt float64) []string { args := []string{ "-loglevel", "warning", } @@ -365,6 +429,19 @@ func (s *Stream) transcode(startId int) { "-b:a", ab, }...) + return args +} + +func (s *Stream) transcode(startId int) { + if startId > 0 { + // Start one frame before + // This ensures that the keyframes are aligned + startId-- + } + startAt := float64(startId * s.c.chunkSize) + + args := s.transcodeArgs(startAt) + // Segmenting specs args = append(args, []string{ "-avoid_negative_ts", "disabled", From a7b7fa5360b9bfe83ea2f7b3dfc91d5422ff35a1 Mon Sep 17 00:00:00 2001 From: MB-Finski <64466176+MB-Finski@users.noreply.github.com> Date: Tue, 29 Nov 2022 23:03:35 +0200 Subject: [PATCH 053/149] Add "format" back into NVENC transcode profile --- stream.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/stream.go b/stream.go index 2c85ee32..ac1b9323 100644 --- a/stream.go +++ b/stream.go @@ -315,6 +315,7 @@ func (s *Stream) transcode(startId int) { scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else if CV == "h264_nvenc" { // NVENC + format = "format=nv12|cuda,hwupload" scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) } else { // x264 @@ -331,7 +332,7 @@ func (s *Stream) transcode(startId int) { if CV == "h264_nvenc" { // Due to a bug(?) in NVENC, passthrough=0 must be set args = append(args, []string{ - "-vf", "scale_cuda=passthrough=0", + "-vf", fmt.Sprintf("%s,%s", format, "scale_cuda=passthrough=0"), }...) } else { args = append(args, []string{ @@ -339,17 +340,8 @@ func (s *Stream) transcode(startId int) { }...) } } else { - if CV == "h264_nvenc" { - args = append(args, []string{ - "-vf", scale, - }...) - } else { - args = append(args, []string{ - "-vf", fmt.Sprintf("%s,%s", format, scale), - }...) - } - // Common arguments - args = append(args, []string{ + args = append(args, []string{ + "-vf", fmt.Sprintf("%s,%s", format, scale), "-maxrate", fmt.Sprintf("%d", s.bitrate), "-bufsize", fmt.Sprintf("%d", s.bitrate*2), }...) From 1729a04527425942940770281c5308653e5b02ca Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 29 Nov 2022 13:12:35 -0800 Subject: [PATCH 054/149] Copy full video if h264 --- stream.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stream.go b/stream.go index 6904d7c7..100b5bd1 100644 --- a/stream.go +++ b/stream.go @@ -184,6 +184,11 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { func (s *Stream) ServeFullVideo(w http.ResponseWriter) error { args := s.transcodeArgs(0) + if s.m.probe.CodecName == "h264" && s.quality == "max" { + // no need to transcode, just copy + args = []string{"-loglevel", "warning", "-i", s.m.path, "-c", "copy"} + } + // Output mp4 args = append(args, []string{ "-movflags", "frag_keyframe+empty_moov+faststart", "-f", "mp4", "pipe:1", From a203bc8f267700b5c5356212bc72258610e056e7 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 29 Nov 2022 13:55:00 -0800 Subject: [PATCH 055/149] Whitespace fixes --- stream.go | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/stream.go b/stream.go index d52c3d40..367b3967 100644 --- a/stream.go +++ b/stream.go @@ -363,10 +363,10 @@ func (s *Stream) transcodeArgs(startAt float64) []string { extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" args = append(args, strings.Split(extra, " ")...) } else if os.Getenv("NVENC") == "1" { - CV = "h264_nvenc" - extra := "-hwaccel cuda -hwaccel_output_format cuda" - args = append(args, strings.Split(extra, " ")...) - } + CV = "h264_nvenc" + extra := "-hwaccel cuda -hwaccel_output_format cuda" + args = append(args, strings.Split(extra, " ")...) + } // Input specs args = append(args, []string{ @@ -383,10 +383,10 @@ func (s *Stream) transcodeArgs(startAt float64) []string { format = "format=nv12|vaapi,hwupload" scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) } else if CV == "h264_nvenc" { - // NVENC + // NVENC format = "format=nv12|cuda,hwupload" scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) - } else { + } else { // x264 format = "format=nv12" if s.width >= s.height { @@ -401,19 +401,19 @@ func (s *Stream) transcodeArgs(startAt float64) []string { if CV == "h264_nvenc" { // Due to a bug(?) in NVENC, passthrough=0 must be set args = append(args, []string{ - "-vf", fmt.Sprintf("%s,%s", format, "scale_cuda=passthrough=0"), - }...) - } else { - args = append(args, []string{ - "-vf", format, - }...) - } + "-vf", fmt.Sprintf("%s,%s", format, "scale_cuda=passthrough=0"), + }...) + } else { + args = append(args, []string{ + "-vf", format, + }...) + } } else { args = append(args, []string{ "-vf", fmt.Sprintf("%s,%s", format, scale), - "-maxrate", fmt.Sprintf("%d", s.bitrate), - "-bufsize", fmt.Sprintf("%d", s.bitrate*2), - }...) + "-maxrate", fmt.Sprintf("%d", s.bitrate), + "-bufsize", fmt.Sprintf("%d", s.bitrate*2), + }...) } // Output specs @@ -429,14 +429,14 @@ func (s *Stream) transcodeArgs(startAt float64) []string { "-global_quality", "25", }...) } else if CV == "h264_nvenc" { - args = append(args, []string{ - "-preset", "p6", - "-tune", "ll", - "-temporal-aq", "1", - "-rc", "vbr", - "-rc-lookahead","30", - "-cq", "24", - }...) + args = append(args, []string{ + "-preset", "p6", + "-tune", "ll", + "-temporal-aq", "1", + "-rc", "vbr", + "-rc-lookahead", "30", + "-cq", "24", + }...) } else if CV == "libx264" { args = append(args, []string{ "-preset", "faster", From c57fb1ca56d800808fb8e2b7f7bddd3961892dd5 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 29 Nov 2022 14:16:27 -0800 Subject: [PATCH 056/149] Change mp4 to mov --- manager.go | 6 +++--- stream.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/manager.go b/manager.go index f1ca9dbe..390158ec 100644 --- a/manager.go +++ b/manager.go @@ -191,9 +191,9 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string } // Stream full video - mp4Sfx := ".mp4" - if strings.HasSuffix(chunk, mp4Sfx) { - quality := strings.TrimSuffix(chunk, mp4Sfx) + movSfx := ".mov" + if strings.HasSuffix(chunk, movSfx) { + quality := strings.TrimSuffix(chunk, movSfx) if stream, ok := m.streams[quality]; ok { return stream.ServeFullVideo(w) } diff --git a/stream.go b/stream.go index 367b3967..6540fd5c 100644 --- a/stream.go +++ b/stream.go @@ -189,9 +189,9 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter) error { args = []string{"-loglevel", "warning", "-i", s.m.path, "-c", "copy"} } - // Output mp4 + // Output mov args = append(args, []string{ - "-movflags", "frag_keyframe+empty_moov+faststart", "-f", "mp4", "pipe:1", + "-movflags", "frag_keyframe+empty_moov+faststart", "-f", "mov", "pipe:1", }...) coder := exec.Command(s.c.ffmpeg, args...) @@ -217,8 +217,8 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter) error { defer cmdStdOut.Close() stdoutReader := bufio.NewReader(cmdStdOut) - // Write mp4 header - w.Header().Set("Content-Type", "video/mp4") + // Write mov headers + w.Header().Set("Content-Type", "video/quicktime") w.WriteHeader(http.StatusOK) flusher, ok := w.(http.Flusher) if !ok { From 758812c69958d524dcadab8f5e89547c4edc267e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 1 Dec 2022 13:05:19 -0800 Subject: [PATCH 057/149] Make sure max is at end --- manager.go | 11 +++++++++-- stream.go | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/manager.go b/manager.go index 390158ec..2259eb62 100644 --- a/manager.go +++ b/manager.go @@ -76,6 +76,8 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Only keep streams that are smaller than the video for k, stream := range m.streams { + stream.order = 0 + // scale bitrate by frame rate with reference 30 stream.bitrate = int(float64(stream.bitrate) * float64(m.probe.FrameRate) / 30.0) @@ -94,8 +96,12 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Original stream m.streams["max"] = &Stream{ - c: c, m: m, quality: "max", height: m.probe.Height, width: m.probe.Width, + c: c, m: m, + quality: "max", + height: m.probe.Height, + width: m.probe.Width, bitrate: m.probe.BitRate, + order: 1, } // Start all streams @@ -213,7 +219,8 @@ func (m *Manager) ServeIndex(w http.ResponseWriter) error { streams = append(streams, stream) } sort.Slice(streams, func(i, j int) bool { - return streams[i].bitrate < streams[j].bitrate + return streams[i].order < streams[j].order || + (streams[i].order == streams[j].order && streams[i].bitrate < streams[j].bitrate) }) // Write all streams diff --git a/stream.go b/stream.go index 6540fd5c..179be5f3 100644 --- a/stream.go +++ b/stream.go @@ -33,6 +33,7 @@ type Stream struct { c *Config m *Manager quality string + order int height int width int bitrate int From 34b96aa1aaa5b19b1582c2d21ad45d2aae0495c3 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 2 Dec 2022 22:06:03 -0800 Subject: [PATCH 058/149] Preserve query strings --- manager.go | 21 +++++++++------------ stream.go | 6 ++++-- util.go | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 util.go diff --git a/manager.go b/manager.go index 2259eb62..83fc8bb8 100644 --- a/manager.go +++ b/manager.go @@ -98,10 +98,10 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m.streams["max"] = &Stream{ c: c, m: m, quality: "max", - height: m.probe.Height, - width: m.probe.Width, + height: m.probe.Height, + width: m.probe.Width, bitrate: m.probe.BitRate, - order: 1, + order: 1, } // Start all streams @@ -162,7 +162,7 @@ func (m *Manager) Destroy() { func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { // Master list if chunk == "index.m3u8" { - return m.ServeIndex(w) + return m.ServeIndex(w, r) } // Stream list @@ -170,7 +170,7 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string if strings.HasSuffix(chunk, m3u8Sfx) { quality := strings.TrimSuffix(chunk, m3u8Sfx) if stream, ok := m.streams[quality]; ok { - return stream.ServeList(w) + return stream.ServeList(w, r) } } @@ -209,7 +209,7 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string return nil } -func (m *Manager) ServeIndex(w http.ResponseWriter) error { +func (m *Manager) ServeIndex(w http.ResponseWriter, r *http.Request) error { WriteM3U8ContentType(w) w.Write([]byte("#EXTM3U\n")) @@ -220,21 +220,18 @@ func (m *Manager) ServeIndex(w http.ResponseWriter) error { } sort.Slice(streams, func(i, j int) bool { return streams[i].order < streams[j].order || - (streams[i].order == streams[j].order && streams[i].bitrate < streams[j].bitrate) + (streams[i].order == streams[j].order && streams[i].bitrate < streams[j].bitrate) }) // Write all streams + query := GetQueryString(r) for _, stream := range streams { - s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,FRAME-RATE=%d\n%s.m3u8\n", stream.bitrate, stream.width, stream.height, m.probe.FrameRate, stream.quality) + s := fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,FRAME-RATE=%d\n%s.m3u8%s\n", stream.bitrate, stream.width, stream.height, m.probe.FrameRate, stream.quality, query) w.Write([]byte(s)) } return nil } -func WriteM3U8ContentType(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/x-mpegURL") -} - func (m *Manager) ffprobe() error { args := []string{ // Hide debug information diff --git a/stream.go b/stream.go index 179be5f3..70b396ef 100644 --- a/stream.go +++ b/stream.go @@ -113,7 +113,7 @@ func (s *Stream) Stop() { } } -func (s *Stream) ServeList(w http.ResponseWriter) error { +func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { WriteM3U8ContentType(w) w.Write([]byte("#EXTM3U\n")) w.Write([]byte("#EXT-X-VERSION:4\n")) @@ -121,6 +121,8 @@ func (s *Stream) ServeList(w http.ResponseWriter) error { w.Write([]byte("#EXT-X-PLAYLIST-TYPE:VOD\n")) w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", s.c.chunkSize))) + query := GetQueryString(r) + duration := s.m.probe.Duration.Seconds() i := 0 for duration > 0 { @@ -130,7 +132,7 @@ func (s *Stream) ServeList(w http.ResponseWriter) error { } w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", size))) - w.Write([]byte(fmt.Sprintf("%s-%06d.ts\n", s.quality, i))) + w.Write([]byte(fmt.Sprintf("%s-%06d.ts%s\n", s.quality, i, query))) duration -= float64(s.c.chunkSize) i++ diff --git a/util.go b/util.go new file mode 100644 index 00000000..cf2e1551 --- /dev/null +++ b/util.go @@ -0,0 +1,15 @@ +package main + +import "net/http" + +func GetQueryString(r *http.Request) string { + query := r.URL.Query().Encode() + if query != "" { + query = "?" + query + } + return query +} + +func WriteM3U8ContentType(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/x-mpegURL") +} From ab49efa89b90040c52d66691b26f43fc957f4459 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 3 Dec 2022 07:24:24 -0800 Subject: [PATCH 059/149] Don't run ffmpeg for h264 full video --- manager.go | 2 +- stream.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/manager.go b/manager.go index 83fc8bb8..d4f30110 100644 --- a/manager.go +++ b/manager.go @@ -201,7 +201,7 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string if strings.HasSuffix(chunk, movSfx) { quality := strings.TrimSuffix(chunk, movSfx) if stream, ok := m.streams[quality]; ok { - return stream.ServeFullVideo(w) + return stream.ServeFullVideo(w, r) } } diff --git a/stream.go b/stream.go index 70b396ef..13a83195 100644 --- a/stream.go +++ b/stream.go @@ -184,12 +184,16 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { return nil } -func (s *Stream) ServeFullVideo(w http.ResponseWriter) error { +func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { args := s.transcodeArgs(0) if s.m.probe.CodecName == "h264" && s.quality == "max" { // no need to transcode, just copy - args = []string{"-loglevel", "warning", "-i", s.m.path, "-c", "copy"} + // args = []string{"-loglevel", "warning", "-i", s.m.path, "-c", "copy"} + + // try to just send the original file + http.ServeFile(w, r, s.m.path) + return nil } // Output mov From d003a271bf8874bff0aced9f5e9e0e85a1cf8df0 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 31 Jan 2023 19:45:09 -0800 Subject: [PATCH 060/149] Allow setting bind port --- main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index de463bac..4d592535 100644 --- a/main.go +++ b/main.go @@ -145,6 +145,12 @@ func main() { tempdir = "/tmp/go-vod" } + // get port + bind := os.Getenv("GOVOD_BIND") + if bind == "" { + bind = ":47788" + } + h := NewHandler(&Config{ ffmpeg: ffmpeg, ffprobe: ffprobe, @@ -158,7 +164,7 @@ func main() { }) http.Handle("/", h) - http.ListenAndServe(":47788", nil) + http.ListenAndServe(bind, nil) log.Println("Exiting VOD server") h.Close() From 3c2a4051333ff37b9887cbc82f7db6f11afa4e34 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 24 Feb 2023 00:26:10 -0800 Subject: [PATCH 061/149] Return conflict when coder changed --- stream.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stream.go b/stream.go index 13a83195..dfd40cce 100644 --- a/stream.go +++ b/stream.go @@ -307,6 +307,7 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { notif := make(chan bool) chunk.notifs = append(chunk.notifs, notif) t := time.NewTimer(10 * time.Second) + coder := s.coder s.mutex.Unlock() @@ -332,6 +333,12 @@ func (s *Stream) waitForChunk(w http.ResponseWriter, chunk *Chunk) { return } + // Check if coder was changed + if coder != s.coder { + w.WriteHeader(http.StatusConflict) + return + } + // Return timeout error w.WriteHeader(http.StatusRequestTimeout) } From 563d14248ce56a729896cf4166b031f8edc52889 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 24 Feb 2023 00:29:50 -0800 Subject: [PATCH 062/149] go 1.20 --- .circleci/config.yml | 2 +- .github/workflows/amd64.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 94774e2e..b3cee831 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ jobs: - checkout - run: | - docker run -it --rm -v "$PWD":/work -w /work golang:1.19-bullseye bash -c 'CGO_ENABLED=0 go build -ldflags="-s -w"' + docker run -it --rm -v "$PWD":/work -w /work golang:1.20-bullseye bash -c 'CGO_ENABLED=0 go build -ldflags="-s -w"' sudo mv go-vod go-vod-aarch64 - persist_to_workspace: diff --git a/.github/workflows/amd64.yml b/.github/workflows/amd64.yml index 2312e4ad..ec255ab4 100644 --- a/.github/workflows/amd64.yml +++ b/.github/workflows/amd64.yml @@ -3,7 +3,7 @@ name: amd64 on: push: tags: - - '*' + - "*" jobs: build-amd64: @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest container: - image: golang:1.19-bullseye + image: golang:1.20-bullseye steps: - name: Checkout @@ -27,4 +27,4 @@ jobs: file: go-vod asset_name: go-vod-amd64 tag: ${{ github.ref }} - overwrite: true \ No newline at end of file + overwrite: true From 35b4b3a8b2c21ed032114e990a03db460f5b5e15 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 24 Feb 2023 00:35:24 -0800 Subject: [PATCH 063/149] buildvcs=false --- .circleci/config.yml | 2 +- .github/workflows/amd64.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b3cee831..c86359bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ jobs: - checkout - run: | - docker run -it --rm -v "$PWD":/work -w /work golang:1.20-bullseye bash -c 'CGO_ENABLED=0 go build -ldflags="-s -w"' + docker run -it --rm -v "$PWD":/work -w /work golang:1.20-bullseye bash -c 'CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w"' sudo mv go-vod go-vod-aarch64 - persist_to_workspace: diff --git a/.github/workflows/amd64.yml b/.github/workflows/amd64.yml index ec255ab4..98dfe89a 100644 --- a/.github/workflows/amd64.yml +++ b/.github/workflows/amd64.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v3 - name: Build - run: CGO_ENABLED=0 go build -ldflags="-s -w" + run: CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" - name: Upload to releases uses: svenstaro/upload-release-action@v2 From f41680455c4c1d576403378aee05356e6fe5aaf6 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 9 Mar 2023 11:57:15 -0800 Subject: [PATCH 064/149] Switch to config file --- config.go | 25 +++++++++++------ main.go | 81 +++++++++++++++++++++++++++++------------------------- manager.go | 8 +++--- stream.go | 32 ++++++++++----------- 4 files changed, 79 insertions(+), 67 deletions(-) diff --git a/config.go b/config.go index ba73034c..688cfc2e 100644 --- a/config.go +++ b/config.go @@ -1,24 +1,31 @@ package main type Config struct { + // Bind address + Bind string `json:"bind"` + // FFmpeg binary - ffmpeg string + FFmpeg string `json:"ffmpeg"` // FFprobe binary - ffprobe string + FFprobe string `json:"ffprobe"` // Temp files directory - tempdir string + TempDir string `json:"tempdir"` // Size of each chunk in seconds - chunkSize int + ChunkSize int `json:"chunkSize"` // How many *chunks* to look behind before restarting transcoding - lookBehind int + LookBehind int `json:"lookBehind"` // Number of chunks in goal to restart encoding - goalBufferMin int + GoalBufferMin int `json:"goalBufferMin"` // Number of chunks in goal to stop encoding - goalBufferMax int + GoalBufferMax int `json:"goalBufferMax"` // Number of seconds to wait before shutting down encoding - streamIdleTime int + StreamIdleTime int `json:"streamIdleTime"` // Number of seconds to wait before shutting down a client - managerIdleTime int + ManagerIdleTime int `json:"managerIdleTime"` + + // Hardware acceleration configuration + VAAPI bool `json:"vaapi"` + NVENC bool `json:"nvenc"` } diff --git a/main.go b/main.go index 4d592535..896db9ad 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "encoding/json" "fmt" + "io/ioutil" "log" "net/http" "os" @@ -24,8 +26,8 @@ func NewHandler(c *Config) *Handler { } // Recreate tempdir - os.RemoveAll(c.tempdir) - os.MkdirAll(c.tempdir, 0755) + os.RemoveAll(c.TempDir) + os.MkdirAll(c.TempDir, 0755) go h.watchClose() return h @@ -120,51 +122,54 @@ func (h *Handler) Close() { h.close <- "" } +func loadConfig(path string, c *Config) { + // load json config + content, err := ioutil.ReadFile(path) + if err != nil { + log.Fatal("Error when opening file: ", err) + } + + err = json.Unmarshal(content, &c) + if err != nil { + log.Fatal("Error loading config file", err) + } + + // Print loaded config + fmt.Printf("%+v\n", c) +} + func main() { if len(os.Args) >= 2 && os.Args[1] == "test" { fmt.Println("test successful") return } + c := &Config{ + Bind: ":47788", + ChunkSize: 3, + LookBehind: 5, + GoalBufferMin: 1, + GoalBufferMax: 4, + StreamIdleTime: 60, + ManagerIdleTime: 60, + } + + // Load config file from second argument + if len(os.Args) >= 2 { + loadConfig(os.Args[1], c) + } else { + log.Fatal("Missing config file") + } + + if c.FFmpeg == "" || c.FFprobe == "" || c.TempDir == "" { + log.Fatal("Missing critical param -- check config file") + } + log.Println("Starting VOD server") - // get executable paths - ffmpeg := os.Getenv("FFMPEG") - if ffmpeg == "" { - ffmpeg = "ffmpeg" - } - - ffprobe := os.Getenv("FFPROBE") - if ffprobe == "" { - ffprobe = "ffprobe" - } - - // get tempdir - tempdir := os.Getenv("GOVOD_TEMPDIR") - if tempdir == "" { - tempdir = "/tmp/go-vod" - } - - // get port - bind := os.Getenv("GOVOD_BIND") - if bind == "" { - bind = ":47788" - } - - h := NewHandler(&Config{ - ffmpeg: ffmpeg, - ffprobe: ffprobe, - tempdir: tempdir, - chunkSize: 3, - lookBehind: 5, - goalBufferMin: 1, - goalBufferMax: 4, - streamIdleTime: 60, - managerIdleTime: 60, - }) - + h := NewHandler(c) http.Handle("/", h) - http.ListenAndServe(bind, nil) + http.ListenAndServe(c.Bind, nil) log.Println("Exiting VOD server") h.Close() diff --git a/manager.go b/manager.go index d4f30110..254bcf68 100644 --- a/manager.go +++ b/manager.go @@ -49,7 +49,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, h := fnv.New32a() h.Write([]byte(path)) ph := fmt.Sprint(h.Sum32()) - m.tempDir = fmt.Sprintf("%s/%s-%s", m.c.tempdir, id, ph) + m.tempDir = fmt.Sprintf("%s/%s-%s", m.c.TempDir, id, ph) // Delete temp dir if exists os.RemoveAll(m.tempDir) @@ -64,7 +64,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m.probe.BitRate *= 2 } - m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.chunkSize))) + m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.ChunkSize))) // Possible streams m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 500000} @@ -134,7 +134,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, } // Nothing done for 5 minutes - if m.inactive >= m.c.managerIdleTime/5 { + if m.inactive >= m.c.ManagerIdleTime/5 { t.Stop() m.Destroy() m.close <- m.id @@ -247,7 +247,7 @@ func (m *Manager) ffprobe() error { } ctx, _ := context.WithDeadline(context.TODO(), time.Now().Add(5*time.Second)) - cmd := exec.CommandContext(ctx, m.c.ffprobe, args...) + cmd := exec.CommandContext(ctx, m.c.FFprobe, args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout diff --git a/stream.go b/stream.go index dfd40cce..86ad421c 100644 --- a/stream.go +++ b/stream.go @@ -63,7 +63,7 @@ func (s *Stream) Run() { s.mutex.Lock() // Prune chunks for id := range s.chunks { - if id < s.goal-s.c.goalBufferMax { + if id < s.goal-s.c.GoalBufferMax { s.pruneChunk(id) } } @@ -71,7 +71,7 @@ func (s *Stream) Run() { s.inactive++ // Nothing done for 2 minutes - if s.inactive >= s.c.streamIdleTime/5 && s.coder != nil { + if s.inactive >= s.c.StreamIdleTime/5 && s.coder != nil { t.Stop() s.clear() } @@ -119,14 +119,14 @@ func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { w.Write([]byte("#EXT-X-VERSION:4\n")) w.Write([]byte("#EXT-X-MEDIA-SEQUENCE:0\n")) w.Write([]byte("#EXT-X-PLAYLIST-TYPE:VOD\n")) - w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", s.c.chunkSize))) + w.Write([]byte(fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", s.c.ChunkSize))) query := GetQueryString(r) duration := s.m.probe.Duration.Seconds() i := 0 for duration > 0 { - size := float64(s.c.chunkSize) + size := float64(s.c.ChunkSize) if duration < size { size = duration } @@ -134,7 +134,7 @@ func (s *Stream) ServeList(w http.ResponseWriter, r *http.Request) error { w.Write([]byte(fmt.Sprintf("#EXTINF:%.3f, nodesc\n", size))) w.Write([]byte(fmt.Sprintf("%s-%06d.ts%s\n", s.quality, i, query))) - duration -= float64(s.c.chunkSize) + duration -= float64(s.c.ChunkSize) i++ } @@ -165,7 +165,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { // Will have this soon enough foundBehind := false - for i := id - 1; i > id-s.c.lookBehind && i >= 0; i-- { + for i := id - 1; i > id-s.c.LookBehind && i >= 0; i-- { if _, ok := s.chunks[i]; ok { foundBehind = true } @@ -201,7 +201,7 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { "-movflags", "frag_keyframe+empty_moov+faststart", "-f", "mov", "pipe:1", }...) - coder := exec.Command(s.c.ffmpeg, args...) + coder := exec.Command(s.c.FFmpeg, args...) log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(coder.Args[:], " ")) cmdStdOut, err := coder.StdoutPipe() @@ -350,7 +350,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { chunk := s.createChunk(id) // create first chunk // Start the transcoder - s.goal = id + s.c.goalBufferMax + s.goal = id + s.c.GoalBufferMax s.transcode(id) s.waitForChunk(w, chunk) // this is also a request @@ -372,11 +372,11 @@ func (s *Stream) transcodeArgs(startAt float64) []string { CV := "libx264" // Check whether hwaccel should be used - if os.Getenv("VAAPI") == "1" { + if s.c.VAAPI { CV = "h264_vaapi" extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" args = append(args, strings.Split(extra, " ")...) - } else if os.Getenv("NVENC") == "1" { + } else if s.c.NVENC { CV = "h264_nvenc" extra := "-hwaccel cuda -hwaccel_output_format cuda" args = append(args, strings.Split(extra, " ")...) @@ -481,7 +481,7 @@ func (s *Stream) transcode(startId int) { // This ensures that the keyframes are aligned startId-- } - startAt := float64(startId * s.c.chunkSize) + startAt := float64(startId * s.c.ChunkSize) args := s.transcodeArgs(startAt) @@ -489,15 +489,15 @@ func (s *Stream) transcode(startId int) { args = append(args, []string{ "-avoid_negative_ts", "disabled", "-f", "hls", - "-hls_time", fmt.Sprintf("%d", s.c.chunkSize), - "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.chunkSize), + "-hls_time", fmt.Sprintf("%d", s.c.ChunkSize), + "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.ChunkSize), "-hls_segment_type", "mpegts", "-start_number", fmt.Sprintf("%d", startId), "-hls_segment_filename", s.getTsPath(-1), "-", }...) - s.coder = exec.Command(s.c.ffmpeg, args...) + s.coder = exec.Command(s.c.FFmpeg, args...) log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(s.coder.Args[:], " ")) cmdStdOut, err := s.coder.StdoutPipe() @@ -520,9 +520,9 @@ func (s *Stream) transcode(startId int) { } func (s *Stream) checkGoal(id int) { - goal := id + s.c.goalBufferMin + goal := id + s.c.GoalBufferMin if goal > s.goal { - s.goal = id + s.c.goalBufferMax + s.goal = id + s.c.GoalBufferMax // resume encoding if s.coder != nil { From a38fed19833dfc6b240ae674982259e88575e299 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 9 Mar 2023 12:39:43 -0800 Subject: [PATCH 065/149] vaapi: make lowpower configurable --- config.go | 4 +++- stream.go | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/config.go b/config.go index 688cfc2e..077464c3 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,8 @@ type Config struct { ManagerIdleTime int `json:"managerIdleTime"` // Hardware acceleration configuration - VAAPI bool `json:"vaapi"` + VAAPI bool `json:"vaapi"` + VAAPILowPower bool `json:"vaapiLowPower"` + NVENC bool `json:"nvenc"` } diff --git a/stream.go b/stream.go index 86ad421c..c644daba 100644 --- a/stream.go +++ b/stream.go @@ -392,11 +392,11 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // Scaling for output var scale string var format string - if CV == "h264_vaapi" { + if s.c.VAAPI { // VAAPI format = "format=nv12|vaapi,hwupload" scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) - } else if CV == "h264_nvenc" { + } else if s.c.NVENC { // NVENC format = "format=nv12|cuda,hwupload" scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) @@ -412,7 +412,7 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // do not scale or set bitrate for full quality if s.quality == "max" { - if CV == "h264_nvenc" { + if s.c.NVENC { // Due to a bug(?) in NVENC, passthrough=0 must be set args = append(args, []string{ "-vf", fmt.Sprintf("%s,%s", format, "scale_cuda=passthrough=0"), @@ -437,12 +437,13 @@ func (s *Stream) transcodeArgs(startAt float64) []string { }...) // Device specific output args - if CV == "h264_vaapi" { - args = append(args, []string{ - "-low_power", "1", - "-global_quality", "25", - }...) - } else if CV == "h264_nvenc" { + if s.c.VAAPI { + args = append(args, []string{"-global_quality", "25"}...) + + if s.c.VAAPILowPower { + args = append(args, []string{"-low_power", "1"}...) + } + } else if s.c.NVENC { args = append(args, []string{ "-preset", "p6", "-tune", "ll", From 95cfe3fd57961d30734fd51a476de9e2e64d966e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 9 Mar 2023 13:02:57 -0800 Subject: [PATCH 066/149] Make NVENC more configurable --- config.go | 7 ++++++- stream.go | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 077464c3..56a44895 100644 --- a/config.go +++ b/config.go @@ -26,8 +26,13 @@ type Config struct { ManagerIdleTime int `json:"managerIdleTime"` // Hardware acceleration configuration + + // VA-API VAAPI bool `json:"vaapi"` VAAPILowPower bool `json:"vaapiLowPower"` - NVENC bool `json:"nvenc"` + // NVENC + NVENC bool `json:"nvenc"` + NVENCTemporalAQ bool `json:"nvencTemporalAQ"` + NVENCScale string `json:"nvencScale"` // cuda, npp } diff --git a/stream.go b/stream.go index c644daba..a4dc8587 100644 --- a/stream.go +++ b/stream.go @@ -399,7 +399,13 @@ func (s *Stream) transcodeArgs(startAt float64) []string { } else if s.c.NVENC { // NVENC format = "format=nv12|cuda,hwupload" - scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) + + if s.c.NVENCScale == "cuda" { + scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) + } else { + // default to "npp" + scale = fmt.Sprintf("scale_npp=%d:%d", s.width, s.height) + } } else { // x264 format = "format=nv12" @@ -447,11 +453,14 @@ func (s *Stream) transcodeArgs(startAt float64) []string { args = append(args, []string{ "-preset", "p6", "-tune", "ll", - "-temporal-aq", "1", "-rc", "vbr", "-rc-lookahead", "30", "-cq", "24", }...) + + if s.c.NVENCTemporalAQ { + args = append(args, []string{"-temporal-aq", "1"}...) + } } else if CV == "libx264" { args = append(args, []string{ "-preset", "faster", From 6710930109cb64031f609636d5254cd960b0bef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20B=C3=A9dard-Couture?= Date: Tue, 14 Mar 2023 14:27:44 -0400 Subject: [PATCH 067/149] Drop the hwaccel_output_format argument to fix issue #6 --- stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream.go b/stream.go index a4dc8587..30fcca52 100644 --- a/stream.go +++ b/stream.go @@ -378,7 +378,7 @@ func (s *Stream) transcodeArgs(startAt float64) []string { args = append(args, strings.Split(extra, " ")...) } else if s.c.NVENC { CV = "h264_nvenc" - extra := "-hwaccel cuda -hwaccel_output_format cuda" + extra := "-hwaccel cuda" args = append(args, strings.Split(extra, " ")...) } From 42c971ed647aa0f5fe71402042c90dd33fd18190 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 14 Mar 2023 12:06:44 -0700 Subject: [PATCH 068/149] Remove comment --- stream.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/stream.go b/stream.go index 30fcca52..53e7e658 100644 --- a/stream.go +++ b/stream.go @@ -188,9 +188,6 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { args := s.transcodeArgs(0) if s.m.probe.CodecName == "h264" && s.quality == "max" { - // no need to transcode, just copy - // args = []string{"-loglevel", "warning", "-i", s.m.path, "-c", "copy"} - // try to just send the original file http.ServeFile(w, r, s.m.path) return nil From dbc2e516ea6e9a7ec5336d79ffe9ac862c115358 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 14 Mar 2023 12:10:50 -0700 Subject: [PATCH 069/149] Update circle config --- .circleci/config.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c86359bb..66c04bb7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,15 +34,11 @@ workflows: jobs: - build: filters: - branches: - ignore: /.*/ tags: only: /^.*/ - publish-github-release: requires: - build filters: - branches: - ignore: /.*/ tags: only: /^.*/ From 5a6d0f8d0ca1e39c96a6828cb055959cad38bdc7 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 14 Mar 2023 12:12:16 -0700 Subject: [PATCH 070/149] Revert "Update circle config" This reverts commit dbc2e516ea6e9a7ec5336d79ffe9ac862c115358. --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 66c04bb7..c86359bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,11 +34,15 @@ workflows: jobs: - build: filters: + branches: + ignore: /.*/ tags: only: /^.*/ - publish-github-release: requires: - build filters: + branches: + ignore: /.*/ tags: only: /^.*/ From 9ea4f3ad2de8879c157c20ffb607819595b7c39a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 15 Mar 2023 10:21:44 -0700 Subject: [PATCH 071/149] Reduce lookBehind to 3 (fix #5) --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 896db9ad..d8f8578b 100644 --- a/main.go +++ b/main.go @@ -147,7 +147,7 @@ func main() { c := &Config{ Bind: ":47788", ChunkSize: 3, - LookBehind: 5, + LookBehind: 3, GoalBufferMin: 1, GoalBufferMax: 4, StreamIdleTime: 60, From 7f2dc535e4268d8c9ce80d86c67861bbf60056d6 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 17 Mar 2023 10:41:09 -0700 Subject: [PATCH 072/149] Add dockerfile --- .dockerignore | 1 + Dockerfile | 14 +++++++++++++ build-ffmpeg.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100755 build-ffmpeg.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..94143827 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..bb3e4fc2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:bullseye AS builder +WORKDIR /app +COPY . . +RUN CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" + +FROM ubuntu:22.04 +WORKDIR /app +ENV DEBIAN_FRONTEND=noninteractive +COPY ./build-ffmpeg.sh . +RUN ./build-ffmpeg.sh + +COPY --from=builder /app/go-vod . +EXPOSE 47788 +CMD ["/app/go-vod", "config.json"] diff --git a/build-ffmpeg.sh b/build-ffmpeg.sh new file mode 100755 index 00000000..fe642dd8 --- /dev/null +++ b/build-ffmpeg.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +apt-get update +apt-get remove -y libva ffmpeg +apt-get install -y \ + sudo curl wget \ + autoconf libtool libdrm-dev xorg xorg-dev openbox \ + libx11-dev libgl1-mesa-glx libgl1-mesa-dev \ + xcb libxcb-xkb-dev x11-xkb-utils libx11-xcb-dev \ + libxkbcommon-x11-dev libxcb-dri3-dev \ + cmake git nasm build-essential + +mkdir qsvbuild +cd qsvbuild + +git clone --depth 1 --branch 2.17.0 https://github.com/intel/libva +cd libva +./autogen.sh +make +sudo make install +sudo ldconfig +cd .. + +git clone --depth 1 --branch intel-gmmlib-22.3.4 https://github.com/intel/gmmlib +cd gmmlib +mkdir build && cd build +cmake .. +make -j"$(nproc)" +sudo make install +sudo ldconfig +cd ../.. + +git clone --depth 1 --branch intel-media-22.6.6 https://github.com/intel/media-driver +mkdir -p build_media +cd build_media +cmake ../media-driver +make -j"$(nproc)" +sudo make install +sudo ldconfig +cd .. + +git clone --depth 1 --branch n6.0 https://github.com/FFmpeg/FFmpeg +cd FFmpeg +./configure --enable-nonfree +make -j"$(nproc)" +sudo make install +sudo ldconfig +cd .. + +cd .. +rm -rf qsvbuild + +rm -rf /var/lib/apt/lists/* From 7dde1e145cf2bd895f1d19bb65f0ba52a9a27aa5 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 17 Mar 2023 14:06:54 -0700 Subject: [PATCH 073/149] Add temp post support --- main.go | 26 +++++++++++++++++++++++--- manager.go | 3 +++ temp.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 temp.go diff --git a/main.go b/main.go index d8f8578b..858e7f7c 100644 --- a/main.go +++ b/main.go @@ -37,39 +37,58 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { url := r.URL.Path parts := make([]string, 0) + // log.Println("Serving", url) + + // Break url into parts for _, part := range strings.Split(url, "/") { if part != "" { parts = append(parts, part) } } + // Serve actual file from manager if len(parts) < 3 { log.Println("Invalid URL", url) w.WriteHeader(http.StatusBadRequest) return } + // Get streamid and chunk streamid := parts[0] path := "/" + strings.Join(parts[1:len(parts)-1], "/") chunk := parts[len(parts)-1] - // log.Println("Serving", path, streamid, chunk) + // Check if POST request to create temp file + if r.Method == "POST" && len(parts) >= 2 && parts[1] == "create" { + var err error + path, err = h.createTempFile(w, r, parts) + if err != nil { + return + } + } - if streamid == "" || chunk == "" || path == "" { + // Check if valid + if streamid == "" || path == "" { w.WriteHeader(http.StatusBadRequest) return } + // Get existing manager or create new one manager := h.getManager(path, streamid) if manager == nil { manager = h.createManager(path, streamid) } + // Failed to create manager if manager == nil { w.WriteHeader(http.StatusInternalServerError) return } - manager.ServeHTTP(w, r, chunk) + + // Serve chunk if asked for + if chunk != "" && chunk != "ignore" { + manager.ServeHTTP(w, r, chunk) + } } func (h *Handler) getManager(path string, streamid string) *Manager { @@ -87,6 +106,7 @@ func (h *Handler) createManager(path string, streamid string) *Manager { manager, err := NewManager(h.c, path, streamid, h.close) if err != nil { log.Println("Error creating manager", err) + freeIfTemp(path) return nil } diff --git a/manager.go b/manager.go index 254bcf68..f9a8dee7 100644 --- a/manager.go +++ b/manager.go @@ -157,6 +157,9 @@ func (m *Manager) Destroy() { // Delete temp dir os.RemoveAll(m.tempDir) + + // Delete file if temp + freeIfTemp(m.path) } func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string) error { diff --git a/temp.go b/temp.go new file mode 100644 index 00000000..4a40fc52 --- /dev/null +++ b/temp.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "os" + "strings" +) + +func (h *Handler) createTempFile(w http.ResponseWriter, r *http.Request, parts []string) (string, error) { + streamid := parts[0] + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading body", err) + w.WriteHeader(http.StatusInternalServerError) + return "", err + } + + // Create temporary file + file, err := ioutil.TempFile(h.c.TempDir, streamid+"-govod-temp-") + if err != nil { + log.Println("Error creating temp file", err) + w.WriteHeader(http.StatusInternalServerError) + return "", err + } + defer file.Close() + + // Write data to file + if _, err := file.Write(body); err != nil { + log.Println("Error writing to temp file", err) + w.WriteHeader(http.StatusInternalServerError) + return "", err + } + + // Return full path to file in JSON + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"path": file.Name()}) + + // Return path to file + return file.Name(), nil +} + +func freeIfTemp(path string) { + if strings.Contains(path, "-govod-temp-") { + os.Remove(path) + } +} From 5cb165d15f31b92165b1a7440041d9322fb2c6e7 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 17 Mar 2023 14:30:30 -0700 Subject: [PATCH 074/149] stream: don't die on stdout error --- stream.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stream.go b/stream.go index 53e7e658..8053f5e9 100644 --- a/stream.go +++ b/stream.go @@ -565,10 +565,10 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 if len(line) == 0 { break } + } else if err != nil { + log.Println(err) + break } else { - if err != nil { - log.Fatal(err) - } line = line[:(len(line) - 1)] } @@ -631,10 +631,10 @@ func (s *Stream) monitorStderr(cmdStdErr io.ReadCloser) { if len(line) == 0 { break } + } else if err != nil { + log.Println(err) + break } else { - if err != nil { - log.Fatal(err) - } line = line[:(len(line) - 1)] } log.Println("ffmpeg-error:", string(line)) From cbad0bb175a5a4d4a13f678a6d9ef98662f9270d Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 17 Mar 2023 14:44:59 -0700 Subject: [PATCH 075/149] Trap exit code for ffmpeg --- stream.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/stream.go b/stream.go index 8053f5e9..76d02ec0 100644 --- a/stream.go +++ b/stream.go @@ -524,6 +524,7 @@ func (s *Stream) transcode(startId int) { go s.monitorTranscodeOutput(cmdStdOut, startAt) go s.monitorStderr(cmdStdErr) + go s.monitorExit() } func (s *Stream) checkGoal(id int) { @@ -617,9 +618,6 @@ func (s *Stream) monitorTranscodeOutput(cmdStdOut io.ReadCloser, startAt float64 }() } } - - // Join the process - coder.Wait() } func (s *Stream) monitorStderr(cmdStdErr io.ReadCloser) { @@ -640,3 +638,28 @@ func (s *Stream) monitorStderr(cmdStdErr io.ReadCloser) { log.Println("ffmpeg-error:", string(line)) } } + +func (s *Stream) monitorExit() { + // Join the process + coder := s.coder + err := coder.Wait() + + // Try to get exit status + if exitError, ok := err.(*exec.ExitError); ok { + exitcode := exitError.ExitCode() + log.Printf("%s-%s: ffmpeg exited with status: %d", s.m.id, s.quality, exitcode) + + s.mutex.Lock() + defer s.mutex.Unlock() + + // If error code is >0, there was an error in transcoding + if exitcode > 0 && s.coder == coder { + // Notify all outstanding chunks + for _, chunk := range s.chunks { + for _, n := range chunk.notifs { + n <- true + } + } + } + } +} From b4ad9d91be9de5fc21f28963834a59200828c796 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 17 Mar 2023 16:07:33 -0700 Subject: [PATCH 076/149] Switch to main profile --- stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream.go b/stream.go index 76d02ec0..26bbf516 100644 --- a/stream.go +++ b/stream.go @@ -436,7 +436,7 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // Output specs args = append(args, []string{ "-c:v", CV, - "-profile:v", "high", + "-profile:v", "main", }...) // Device specific output args From 38384b0d6d14c7955cb2126a1af5311206cd7d81 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 17 Mar 2023 16:40:31 -0700 Subject: [PATCH 077/149] Revamp scaler + add nv12 to vaapi --- stream.go | 63 ++++++++++++++++++++++--------------------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/stream.go b/stream.go index 26bbf516..57e8032e 100644 --- a/stream.go +++ b/stream.go @@ -386,48 +386,35 @@ func (s *Stream) transcodeArgs(startAt float64) []string { "-copyts", // So the "-to" refers to the original TS }...) - // Scaling for output - var scale string - var format string - if s.c.VAAPI { - // VAAPI - format = "format=nv12|vaapi,hwupload" - scale = fmt.Sprintf("scale_vaapi=w=%d:h=%d:force_original_aspect_ratio=decrease", s.width, s.height) - } else if s.c.NVENC { - // NVENC - format = "format=nv12|cuda,hwupload" + // Filters + format := "format=nv12" + scaler := "scale" + scalerArgs := make([]string, 0) + scalerArgs = append(scalerArgs, "force_original_aspect_ratio=decrease") - if s.c.NVENCScale == "cuda" { - scale = fmt.Sprintf("scale_cuda=w=%d:h=%d:force_original_aspect_ratio=decrease:passthrough=0", s.width, s.height) - } else { - // default to "npp" - scale = fmt.Sprintf("scale_npp=%d:%d", s.width, s.height) - } - } else { - // x264 - format = "format=nv12" - if s.width >= s.height { - scale = fmt.Sprintf("scale=-2:%d", s.height) - } else { - scale = fmt.Sprintf("scale=%d:-2", s.width) - } + if s.c.VAAPI { + format = "format=nv12|vaapi,hwupload" + scaler = "scale_vaapi" + scalerArgs = append(scalerArgs, "format=nv12") + } else if s.c.NVENC { + format = "format=nv12|cuda,hwupload" + scaler = fmt.Sprintf("scale_%s", s.c.NVENCScale) + scalerArgs = append(scalerArgs, "passthrough=0") } - // do not scale or set bitrate for full quality - if s.quality == "max" { - if s.c.NVENC { - // Due to a bug(?) in NVENC, passthrough=0 must be set - args = append(args, []string{ - "-vf", fmt.Sprintf("%s,%s", format, "scale_cuda=passthrough=0"), - }...) - } else { - args = append(args, []string{ - "-vf", format, - }...) - } - } else { + // Scale height and width if not max quality + if s.quality != "max" { + scalerArgs = append(scalerArgs, fmt.Sprintf("w=%d", s.width)) + scalerArgs = append(scalerArgs, fmt.Sprintf("h=%d", s.height)) + } + + // Apply filter + filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) + args = append(args, []string{"-vf", filter}...) + + // Apply bitrate cap if not max quality + if s.quality != "max" { args = append(args, []string{ - "-vf", fmt.Sprintf("%s,%s", format, scale), "-maxrate", fmt.Sprintf("%d", s.bitrate), "-bufsize", fmt.Sprintf("%d", s.bitrate*2), }...) From 1c9d46130e1835594d8f999cee12a170c8783009 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 29 Mar 2023 15:50:01 -0700 Subject: [PATCH 078/149] Don't set audio bitrate --- stream.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/stream.go b/stream.go index 57e8032e..35e7ee43 100644 --- a/stream.go +++ b/stream.go @@ -454,16 +454,9 @@ func (s *Stream) transcodeArgs(startAt float64) []string { } // Audio - ab := "192k" - if s.bitrate < 1000000 { - ab = "64k" - } else if s.bitrate < 3000000 { - ab = "128k" - } args = append(args, []string{ "-c:a", "aac", "-ac", "1", - "-b:a", ab, }...) return args From 2684f705f96af1ced0d04d9c80f31d253a559c62 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 29 Mar 2023 15:54:53 -0700 Subject: [PATCH 079/149] output: choose zero streams --- stream.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stream.go b/stream.go index 35e7ee43..31786671 100644 --- a/stream.go +++ b/stream.go @@ -420,8 +420,9 @@ func (s *Stream) transcodeArgs(startAt float64) []string { }...) } - // Output specs + // Output specs for video args = append(args, []string{ + "-map", "0:v:0", "-c:v", CV, "-profile:v", "main", }...) @@ -453,8 +454,9 @@ func (s *Stream) transcodeArgs(startAt float64) []string { }...) } - // Audio + // Audio output specs args = append(args, []string{ + "-map", "0:a:0?", "-c:a", "aac", "-ac", "1", }...) From c9198e0218010d59e27b16dea021e3632e91c08b Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 29 Mar 2023 15:57:23 -0700 Subject: [PATCH 080/149] x264: remove level --- stream.go | 1 - 1 file changed, 1 deletion(-) diff --git a/stream.go b/stream.go index 31786671..d41e01ec 100644 --- a/stream.go +++ b/stream.go @@ -449,7 +449,6 @@ func (s *Stream) transcodeArgs(startAt float64) []string { } else if CV == "libx264" { args = append(args, []string{ "-preset", "faster", - "-level:v", "4.0", "-crf", "24", }...) } From b1aba2284f508e49887f4c3567c4ecd45910d570 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 3 Apr 2023 18:44:11 -0700 Subject: [PATCH 081/149] refactor: consts --- chunk.go | 15 +++++++++++++++ stream.go | 41 ++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 chunk.go diff --git a/chunk.go b/chunk.go new file mode 100644 index 00000000..662c9c75 --- /dev/null +++ b/chunk.go @@ -0,0 +1,15 @@ +package main + +type Chunk struct { + id int + done bool + notifs []chan bool +} + +func NewChunk(id int) *Chunk { + return &Chunk{ + id: id, + done: false, + notifs: make([]chan bool, 0), + } +} \ No newline at end of file diff --git a/stream.go b/stream.go index d41e01ec..b568474d 100644 --- a/stream.go +++ b/stream.go @@ -15,19 +15,14 @@ import ( "time" ) -type Chunk struct { - id int - done bool - notifs []chan bool -} +const ( + ENCODER_X264 = "libx264" + ENCODER_VAAPI = "h264_vaapi" + ENCODER_NVENC = "h264_nvenc" -func NewChunk(id int) *Chunk { - return &Chunk{ - id: id, - done: false, - notifs: make([]chan bool, 0), - } -} + QUALITY_MAX = "max" + CODEC_H264 = "h264" +) type Stream struct { c *Config @@ -187,7 +182,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { args := s.transcodeArgs(0) - if s.m.probe.CodecName == "h264" && s.quality == "max" { + if s.m.probe.CodecName == CODEC_H264 && s.quality == QUALITY_MAX { // try to just send the original file http.ServeFile(w, r, s.m.path) return nil @@ -366,15 +361,15 @@ func (s *Stream) transcodeArgs(startAt float64) []string { } // encoder selection - CV := "libx264" + CV := ENCODER_X264 // Check whether hwaccel should be used if s.c.VAAPI { - CV = "h264_vaapi" + CV = ENCODER_VAAPI extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" args = append(args, strings.Split(extra, " ")...) } else if s.c.NVENC { - CV = "h264_nvenc" + CV = ENCODER_NVENC extra := "-hwaccel cuda" args = append(args, strings.Split(extra, " ")...) } @@ -392,18 +387,18 @@ func (s *Stream) transcodeArgs(startAt float64) []string { scalerArgs := make([]string, 0) scalerArgs = append(scalerArgs, "force_original_aspect_ratio=decrease") - if s.c.VAAPI { + if CV == ENCODER_VAAPI { format = "format=nv12|vaapi,hwupload" scaler = "scale_vaapi" scalerArgs = append(scalerArgs, "format=nv12") - } else if s.c.NVENC { + } else if CV == ENCODER_NVENC { format = "format=nv12|cuda,hwupload" scaler = fmt.Sprintf("scale_%s", s.c.NVENCScale) scalerArgs = append(scalerArgs, "passthrough=0") } // Scale height and width if not max quality - if s.quality != "max" { + if s.quality != QUALITY_MAX { scalerArgs = append(scalerArgs, fmt.Sprintf("w=%d", s.width)) scalerArgs = append(scalerArgs, fmt.Sprintf("h=%d", s.height)) } @@ -413,7 +408,7 @@ func (s *Stream) transcodeArgs(startAt float64) []string { args = append(args, []string{"-vf", filter}...) // Apply bitrate cap if not max quality - if s.quality != "max" { + if s.quality != QUALITY_MAX { args = append(args, []string{ "-maxrate", fmt.Sprintf("%d", s.bitrate), "-bufsize", fmt.Sprintf("%d", s.bitrate*2), @@ -428,13 +423,13 @@ func (s *Stream) transcodeArgs(startAt float64) []string { }...) // Device specific output args - if s.c.VAAPI { + if CV == ENCODER_VAAPI { args = append(args, []string{"-global_quality", "25"}...) if s.c.VAAPILowPower { args = append(args, []string{"-low_power", "1"}...) } - } else if s.c.NVENC { + } else if CV == ENCODER_NVENC { args = append(args, []string{ "-preset", "p6", "-tune", "ll", @@ -446,7 +441,7 @@ func (s *Stream) transcodeArgs(startAt float64) []string { if s.c.NVENCTemporalAQ { args = append(args, []string{"-temporal-aq", "1"}...) } - } else if CV == "libx264" { + } else if CV == ENCODER_X264 { args = append(args, []string{ "-preset", "faster", "-crf", "24", From a2a8fda91e6c3e5fa39e2f2c9ca6f4f030b04da5 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 3 Apr 2023 18:55:26 -0700 Subject: [PATCH 082/149] Use copy for H264 --- stream.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/stream.go b/stream.go index b568474d..a8339016 100644 --- a/stream.go +++ b/stream.go @@ -16,12 +16,13 @@ import ( ) const ( - ENCODER_X264 = "libx264" + ENCODER_COPY = "copy" + ENCODER_X264 = "libx264" ENCODER_VAAPI = "h264_vaapi" ENCODER_NVENC = "h264_nvenc" QUALITY_MAX = "max" - CODEC_H264 = "h264" + CODEC_H264 = "h264" ) type Stream struct { @@ -363,15 +364,20 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // encoder selection CV := ENCODER_X264 - // Check whether hwaccel should be used - if s.c.VAAPI { - CV = ENCODER_VAAPI - extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" - args = append(args, strings.Split(extra, " ")...) - } else if s.c.NVENC { - CV = ENCODER_NVENC - extra := "-hwaccel cuda" - args = append(args, strings.Split(extra, " ")...) + if s.m.probe.CodecName == CODEC_H264 && s.quality == QUALITY_MAX { + // Use original stream + CV = ENCODER_COPY + } else { + // Check whether hwaccel should be used + if s.c.VAAPI { + CV = ENCODER_VAAPI + extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" + args = append(args, strings.Split(extra, " ")...) + } else if s.c.NVENC { + CV = ENCODER_NVENC + extra := "-hwaccel cuda" + args = append(args, strings.Split(extra, " ")...) + } } // Input specs @@ -404,8 +410,11 @@ func (s *Stream) transcodeArgs(startAt float64) []string { } // Apply filter - filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) - args = append(args, []string{"-vf", filter}...) + if CV != ENCODER_COPY { + filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) + args = append(args, []string{"-vf", filter}...) + args = append(args, []string{"-profile:v", "main"}...) + } // Apply bitrate cap if not max quality if s.quality != QUALITY_MAX { @@ -419,7 +428,6 @@ func (s *Stream) transcodeArgs(startAt float64) []string { args = append(args, []string{ "-map", "0:v:0", "-c:v", CV, - "-profile:v", "main", }...) // Device specific output args From 8748f4a6e07bd6507fae9a5b19e30f312988ac99 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 3 Apr 2023 19:37:10 -0700 Subject: [PATCH 083/149] manager: avoid pointless streams --- manager.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/manager.go b/manager.go index f9a8dee7..5b2ca3fc 100644 --- a/manager.go +++ b/manager.go @@ -81,23 +81,16 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // scale bitrate by frame rate with reference 30 stream.bitrate = int(float64(stream.bitrate) * float64(m.probe.FrameRate) / 30.0) - if stream.height > m.probe.Height || stream.width > m.probe.Width { + // these streams are pointless + if stream.height > m.probe.Height || stream.width > m.probe.Width || float64(stream.bitrate) > float64(m.probe.BitRate)*0.8 { delete(m.streams, k) } - - if stream.height == m.probe.Height || stream.width == m.probe.Width { - if stream.bitrate > m.probe.BitRate || float64(stream.bitrate) > float64(m.probe.BitRate)*0.8 { - // no point in "upscaling" - // should have at least 20% difference - delete(m.streams, k) - } - } } // Original stream - m.streams["max"] = &Stream{ + m.streams[QUALITY_MAX] = &Stream{ c: c, m: m, - quality: "max", + quality: QUALITY_MAX, height: m.probe.Height, width: m.probe.Width, bitrate: m.probe.BitRate, From 5548498453306ce4db397c6c299d585f4627e358 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 10 Apr 2023 16:44:39 -0700 Subject: [PATCH 084/149] Add config POST --- config.go | 3 ++ main.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/config.go b/config.go index 56a44895..c8de3e38 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,9 @@ package main type Config struct { + // Is this server configured? + Configured bool + // Bind address Bind string `json:"bind"` diff --git a/main.go b/main.go index 858e7f7c..7dbec629 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,15 @@ import ( "log" "net/http" "os" + "os/exec" "strings" "sync" ) +const ( + VERSION = "0.0.34" +) + type Handler struct { c *Config managers map[string]*Manager @@ -67,6 +72,56 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + // Check if test request + if chunk == "test" { + w.Header().Set("Content-Type", "application/json") + + // check if test file is readable + size := 0 + info, err := os.Stat(path) + if err == nil { + size = int(info.Size()) + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "version": VERSION, + "size": size, + }) + return + } + + // Check if configuration request + if r.Method == "POST" && chunk == "config" { + w.Header().Set("Content-Type", "application/json") + // read new config + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading body", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Unmarshal config + if err := json.Unmarshal(body, h.c); err != nil { + log.Println("Error unmarshaling config", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Set config as loaded + h.c.Configured = true + + // Print loaded config + fmt.Printf("%+v\n", h.c) + return + } + + // Check if configured + if !h.c.Configured { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + // Check if valid if streamid == "" || path == "" { w.WriteHeader(http.StatusBadRequest) @@ -154,6 +209,9 @@ func loadConfig(path string, c *Config) { log.Fatal("Error loading config file", err) } + // Set config as loaded + c.Configured = true + // Print loaded config fmt.Printf("%+v\n", c) } @@ -177,19 +235,41 @@ func main() { // Load config file from second argument if len(os.Args) >= 2 { loadConfig(os.Args[1], c) - } else { - log.Fatal("Missing config file") } - if c.FFmpeg == "" || c.FFprobe == "" || c.TempDir == "" { - log.Fatal("Missing critical param -- check config file") + // Auto-detect ffmpeg and ffprobe paths + if c.FFmpeg == "" || c.FFprobe == "" { + ffmpeg, err := exec.LookPath("ffmpeg") + if err != nil { + log.Fatal("Could not find ffmpeg") + } + + ffprobe, err := exec.LookPath("ffprobe") + if err != nil { + log.Fatal("Could not find ffprobe") + } + + c.FFmpeg = ffmpeg + c.FFprobe = ffprobe } + // Auto-choose tempdir + if c.TempDir == "" { + c.TempDir = os.TempDir() + "/go-vod" + } + + // Print config + fmt.Printf("%+v\n", c) + + // Start server log.Println("Starting VOD server") h := NewHandler(c) http.Handle("/", h) - http.ListenAndServe(c.Bind, nil) + err := http.ListenAndServe(c.Bind, nil) + if err != nil { + log.Fatal("Error starting server", err) + } log.Println("Exiting VOD server") h.Close() From 23be0f6d17ab252573174d2caa9bdf74010ed889 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 10 Apr 2023 16:59:54 -0700 Subject: [PATCH 085/149] Bump up to 0.1.0 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 7dbec629..6a70bec7 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.0.34" + VERSION = "0.1.0" ) type Handler struct { From e685d114bf5e882d0a460fc014a19f94fab17869 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 12 Apr 2023 13:20:57 -0700 Subject: [PATCH 086/149] Update build files --- Dockerfile | 2 +- build-ffmpeg.sh | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index bb3e4fc2..a1914c0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ RUN ./build-ffmpeg.sh COPY --from=builder /app/go-vod . EXPOSE 47788 -CMD ["/app/go-vod", "config.json"] +CMD ["/app/go-vod"] diff --git a/build-ffmpeg.sh b/build-ffmpeg.sh index fe642dd8..12536795 100755 --- a/build-ffmpeg.sh +++ b/build-ffmpeg.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + apt-get update apt-get remove -y libva ffmpeg apt-get install -y \ @@ -8,12 +10,13 @@ apt-get install -y \ libx11-dev libgl1-mesa-glx libgl1-mesa-dev \ xcb libxcb-xkb-dev x11-xkb-utils libx11-xcb-dev \ libxkbcommon-x11-dev libxcb-dri3-dev \ - cmake git nasm build-essential + cmake git nasm build-essential \ + libx264-dev mkdir qsvbuild cd qsvbuild -git clone --depth 1 --branch 2.17.0 https://github.com/intel/libva +git clone --depth 1 --branch 2.18.0 https://github.com/intel/libva cd libva ./autogen.sh make @@ -21,7 +24,7 @@ sudo make install sudo ldconfig cd .. -git clone --depth 1 --branch intel-gmmlib-22.3.4 https://github.com/intel/gmmlib +git clone --depth 1 --branch intel-gmmlib-22.3.5 https://github.com/intel/gmmlib cd gmmlib mkdir build && cd build cmake .. @@ -30,7 +33,7 @@ sudo make install sudo ldconfig cd ../.. -git clone --depth 1 --branch intel-media-22.6.6 https://github.com/intel/media-driver +git clone --depth 1 --branch intel-media-23.1.6 https://github.com/intel/media-driver mkdir -p build_media cd build_media cmake ../media-driver @@ -41,7 +44,11 @@ cd .. git clone --depth 1 --branch n6.0 https://github.com/FFmpeg/FFmpeg cd FFmpeg -./configure --enable-nonfree +./configure \ + --enable-nonfree \ + --enable-gpl \ + --enable-libx264 + make -j"$(nproc)" sudo make install sudo ldconfig From df7122fe076fe24bd468644e7129a3b5d61aace8 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 17 Apr 2023 00:36:36 -0700 Subject: [PATCH 087/149] Disable ENCODER_COPY --- main.go | 2 +- stream.go | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index 6a70bec7..ddfe717f 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.0" + VERSION = "0.1.1" ) type Handler struct { diff --git a/stream.go b/stream.go index a8339016..f303443a 100644 --- a/stream.go +++ b/stream.go @@ -364,20 +364,15 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // encoder selection CV := ENCODER_X264 - if s.m.probe.CodecName == CODEC_H264 && s.quality == QUALITY_MAX { - // Use original stream - CV = ENCODER_COPY - } else { - // Check whether hwaccel should be used - if s.c.VAAPI { - CV = ENCODER_VAAPI - extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" - args = append(args, strings.Split(extra, " ")...) - } else if s.c.NVENC { - CV = ENCODER_NVENC - extra := "-hwaccel cuda" - args = append(args, strings.Split(extra, " ")...) - } + // Check whether hwaccel should be used + if s.c.VAAPI { + CV = ENCODER_VAAPI + extra := "-hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi" + args = append(args, strings.Split(extra, " ")...) + } else if s.c.NVENC { + CV = ENCODER_NVENC + extra := "-hwaccel cuda" + args = append(args, strings.Split(extra, " ")...) } // Input specs From c2a7be94806a07dfe836e8c122bd3e56d2697ed5 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 5 May 2023 12:12:47 -0700 Subject: [PATCH 088/149] add nvidia Dockfile --- Dockerfile.nvidia | 17 +++++++++++++++++ build-ffmpeg-nvidia.sh | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 Dockerfile.nvidia create mode 100755 build-ffmpeg-nvidia.sh diff --git a/Dockerfile.nvidia b/Dockerfile.nvidia new file mode 100644 index 00000000..e81b5d5b --- /dev/null +++ b/Dockerfile.nvidia @@ -0,0 +1,17 @@ +FROM golang:bullseye AS builder +WORKDIR /app +COPY . . +RUN CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" + +FROM nvidia/cuda:11.1.1-base-ubuntu20.04 +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES=all + +WORKDIR /app +ENV DEBIAN_FRONTEND=noninteractive +COPY ./build-ffmpeg-nvidia.sh . +RUN ./build-ffmpeg-nvidia.sh + +COPY --from=builder /app/go-vod . +EXPOSE 47788 +CMD ["/app/go-vod"] diff --git a/build-ffmpeg-nvidia.sh b/build-ffmpeg-nvidia.sh new file mode 100755 index 00000000..894efe80 --- /dev/null +++ b/build-ffmpeg-nvidia.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e + +apt-get update +apt-get remove -y libva ffmpeg +apt-get install -y \ + sudo curl wget \ + autoconf libtool libdrm-dev xorg xorg-dev openbox \ + libx11-dev libgl1-mesa-glx libgl1-mesa-dev \ + xcb libxcb-xkb-dev x11-xkb-utils libx11-xcb-dev \ + libxkbcommon-x11-dev libxcb-dri3-dev \ + cmake git nasm build-essential \ + libx264-dev \ + libffmpeg-nvenc-dev clang + +git clone --branch sdk/11.1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git +cd nv-codec-headers +sudo make install +cd .. + +git clone --depth 1 --branch n5.1.3 https://github.com/FFmpeg/FFmpeg +cd FFmpeg +./configure \ + --enable-nonfree \ + --enable-gpl \ + --enable-libx264 \ + --enable-nvenc \ + --enable-ffnvcodec \ + --enable-cuda-llvm + +make -j"$(nproc)" +sudo make install +sudo ldconfig +cd .. + +rm -rf /var/lib/apt/lists/* From dd637f9b1f53810506b8b7cf455f32e2ecd1eeef Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 14 May 2023 18:10:17 -0700 Subject: [PATCH 089/149] Rotate videos --- manager.go | 12 ++++++++++++ stream.go | 20 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/manager.go b/manager.go index 5b2ca3fc..a3c3b29c 100644 --- a/manager.go +++ b/manager.go @@ -40,6 +40,7 @@ type ProbeVideoData struct { FrameRate int CodecName string BitRate int + Rotation int } func NewManager(c *Config, path string, id string, close chan string) (*Manager, error) { @@ -236,6 +237,7 @@ func (m *Manager) ffprobe() error { // video "-show_entries", "format=duration", "-show_entries", "stream=duration,width,height,avg_frame_rate,codec_name,bit_rate", + "-show_entries", "stream_tags=rotate", "-select_streams", "v", // Video stream only, we're not interested in audio "-of", "json", @@ -263,6 +265,9 @@ func (m *Manager) ffprobe() error { FrameRate string `json:"avg_frame_rate"` CodecName string `json:"codec_name"` BitRate string `json:"bit_rate"` + Tags struct { + Rotate string `json:"rotate"` + } `json:"tags"` } `json:"streams"` Format struct { Duration string `json:"duration"` @@ -310,6 +315,12 @@ func (m *Manager) ffprobe() error { bitRate = 5000000 } + // Rotation is a string + rotation, err := strconv.Atoi(out.Streams[0].Tags.Rotate) + if err != nil { + rotation = 0 + } + m.probe = &ProbeVideoData{ Width: out.Streams[0].Width, Height: out.Streams[0].Height, @@ -317,6 +328,7 @@ func (m *Manager) ffprobe() error { FrameRate: int(frameRate), CodecName: out.Streams[0].CodecName, BitRate: bitRate, + Rotation: rotation, } return nil diff --git a/stream.go b/stream.go index f303443a..1228a547 100644 --- a/stream.go +++ b/stream.go @@ -377,8 +377,8 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // Input specs args = append(args, []string{ - "-autorotate", "0", // consistent behavior - "-i", s.m.path, // Input file + "-noautorotate", // Rotate manually + "-i", s.m.path, // Input file "-copyts", // So the "-to" refers to the original TS }...) @@ -407,6 +407,22 @@ func (s *Stream) transcodeArgs(startAt float64) []string { // Apply filter if CV != ENCODER_COPY { filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) + + // Add transpose filter if needed + transposer := "transpose" + if CV == ENCODER_VAAPI { + transposer = "transpose_vaapi" + } else if CV == ENCODER_NVENC { + transposer = "transpose_npp" + } + if s.m.probe.Rotation == 90 { + filter = fmt.Sprintf("%s,%s=1", filter, transposer) + } else if s.m.probe.Rotation == 270 { + filter = fmt.Sprintf("%s,%s=2", filter, transposer) + } else if s.m.probe.Rotation == 180 { + filter = fmt.Sprintf("%s,%s=1,%s=1", filter, transposer, transposer) + } + args = append(args, []string{"-vf", filter}...) args = append(args, []string{"-profile:v", "main"}...) } From 52cef01dcac487fac5b350f82a984869721976f1 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 14 May 2023 18:38:50 -0700 Subject: [PATCH 090/149] manager: read rotation from side data --- manager.go | 40 ++++++++++++++++++++++------------------ stream.go | 6 +++--- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/manager.go b/manager.go index a3c3b29c..7eaf98c3 100644 --- a/manager.go +++ b/manager.go @@ -234,10 +234,8 @@ func (m *Manager) ffprobe() error { // Hide debug information "-v", "error", - // video - "-show_entries", "format=duration", - "-show_entries", "stream=duration,width,height,avg_frame_rate,codec_name,bit_rate", - "-show_entries", "stream_tags=rotate", + // Show everything + "-show_entries", "stream", "-select_streams", "v", // Video stream only, we're not interested in audio "-of", "json", @@ -259,22 +257,26 @@ func (m *Manager) ffprobe() error { out := struct { Streams []struct { - Width int `json:"width"` - Height int `json:"height"` - Duration string `json:"duration"` - FrameRate string `json:"avg_frame_rate"` - CodecName string `json:"codec_name"` - BitRate string `json:"bit_rate"` - Tags struct { - Rotate string `json:"rotate"` - } `json:"tags"` + Width int `json:"width"` + Height int `json:"height"` + Duration string `json:"duration"` + FrameRate string `json:"avg_frame_rate"` + CodecName string `json:"codec_name"` + BitRate string `json:"bit_rate"` + SideDataList []struct { + SideDataType string `json:"side_data_type"` + Rotation int `json:"rotation"` + } `json:"side_data_list"` } `json:"streams"` Format struct { Duration string `json:"duration"` } `json:"format"` }{} - if err := json.Unmarshal(stdout.Bytes(), &out); err != nil { + byts := stdout.Bytes() + log.Println(string(byts)) + + if err := json.Unmarshal(byts, &out); err != nil { return err } @@ -315,10 +317,12 @@ func (m *Manager) ffprobe() error { bitRate = 5000000 } - // Rotation is a string - rotation, err := strconv.Atoi(out.Streams[0].Tags.Rotate) - if err != nil { - rotation = 0 + // Get rotation from side data + rotation := 0 + for _, sideData := range out.Streams[0].SideDataList { + if sideData.SideDataType == "Display Matrix" { + rotation = sideData.Rotation + } } m.probe = &ProbeVideoData{ diff --git a/stream.go b/stream.go index 1228a547..f8b658c1 100644 --- a/stream.go +++ b/stream.go @@ -415,11 +415,11 @@ func (s *Stream) transcodeArgs(startAt float64) []string { } else if CV == ENCODER_NVENC { transposer = "transpose_npp" } - if s.m.probe.Rotation == 90 { + if s.m.probe.Rotation == -90 { filter = fmt.Sprintf("%s,%s=1", filter, transposer) - } else if s.m.probe.Rotation == 270 { + } else if s.m.probe.Rotation == 90 { filter = fmt.Sprintf("%s,%s=2", filter, transposer) - } else if s.m.probe.Rotation == 180 { + } else if s.m.probe.Rotation == 180 || s.m.probe.Rotation == -180 { filter = fmt.Sprintf("%s,%s=1,%s=1", filter, transposer, transposer) } From 3ad8606d8dae3169af8898dff448580915e6d51f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 14 May 2023 18:39:30 -0700 Subject: [PATCH 091/149] Remove log --- manager.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/manager.go b/manager.go index 7eaf98c3..3851cda0 100644 --- a/manager.go +++ b/manager.go @@ -273,10 +273,7 @@ func (m *Manager) ffprobe() error { } `json:"format"` }{} - byts := stdout.Bytes() - log.Println(string(byts)) - - if err := json.Unmarshal(byts, &out); err != nil { + if err := json.Unmarshal(stdout.Bytes(), &out); err != nil { return err } From c4ad66567fc6ba619e2bef8a18bb9dda51024544 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 14 May 2023 19:38:13 -0700 Subject: [PATCH 092/149] Do not rotate MOV --- stream.go | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/stream.go b/stream.go index f8b658c1..7ee8ce80 100644 --- a/stream.go +++ b/stream.go @@ -181,7 +181,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { } func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { - args := s.transcodeArgs(0) + args := s.transcodeArgs(0, false) if s.m.probe.CodecName == CODEC_H264 && s.quality == QUALITY_MAX { // try to just send the original file @@ -191,7 +191,8 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { // Output mov args = append(args, []string{ - "-movflags", "frag_keyframe+empty_moov+faststart", "-f", "mov", "pipe:1", + "-movflags", "frag_keyframe+empty_moov+faststart", + "-f", "mov", "pipe:1", }...) coder := exec.Command(s.c.FFmpeg, args...) @@ -350,7 +351,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { } // Get arguments to ffmpeg -func (s *Stream) transcodeArgs(startAt float64) []string { +func (s *Stream) transcodeArgs(startAt float64, rotate bool) []string { args := []string{ "-loglevel", "warning", } @@ -375,10 +376,16 @@ func (s *Stream) transcodeArgs(startAt float64) []string { args = append(args, strings.Split(extra, " ")...) } + // Manual rotation: disable autorotate + if rotate { + args = append(args, []string{ + "-noautorotate", // Rotate manually + }...) + } + // Input specs args = append(args, []string{ - "-noautorotate", // Rotate manually - "-i", s.m.path, // Input file + "-i", s.m.path, // Input file "-copyts", // So the "-to" refers to the original TS }...) @@ -409,18 +416,20 @@ func (s *Stream) transcodeArgs(startAt float64) []string { filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) // Add transpose filter if needed - transposer := "transpose" - if CV == ENCODER_VAAPI { - transposer = "transpose_vaapi" - } else if CV == ENCODER_NVENC { - transposer = "transpose_npp" - } - if s.m.probe.Rotation == -90 { - filter = fmt.Sprintf("%s,%s=1", filter, transposer) - } else if s.m.probe.Rotation == 90 { - filter = fmt.Sprintf("%s,%s=2", filter, transposer) - } else if s.m.probe.Rotation == 180 || s.m.probe.Rotation == -180 { - filter = fmt.Sprintf("%s,%s=1,%s=1", filter, transposer, transposer) + if rotate { + transposer := "transpose" + if CV == ENCODER_VAAPI { + transposer = "transpose_vaapi" + } else if CV == ENCODER_NVENC { + transposer = "transpose_npp" + } + if s.m.probe.Rotation == -90 { + filter = fmt.Sprintf("%s,%s=1", filter, transposer) + } else if s.m.probe.Rotation == 90 { + filter = fmt.Sprintf("%s,%s=2", filter, transposer) + } else if s.m.probe.Rotation == 180 || s.m.probe.Rotation == -180 { + filter = fmt.Sprintf("%s,%s=1,%s=1", filter, transposer, transposer) + } } args = append(args, []string{"-vf", filter}...) @@ -485,7 +494,7 @@ func (s *Stream) transcode(startId int) { } startAt := float64(startId * s.c.ChunkSize) - args := s.transcodeArgs(startAt) + args := s.transcodeArgs(startAt, true) // Segmenting specs args = append(args, []string{ From 0dedd2acb826f2b8f18e7b381472a25f63bf9892 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Mon, 22 May 2023 22:42:52 -0700 Subject: [PATCH 093/149] Bump up version --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index ddfe717f..5c9d48df 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.1" + VERSION = "0.1.6" ) type Handler struct { From 0e09eb1d760067156dfe5216dfd9f321e41c2d6e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 27 May 2023 23:42:58 -0700 Subject: [PATCH 094/149] Another attempt at fixiing autorotation --- stream.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/stream.go b/stream.go index 7ee8ce80..7e474f48 100644 --- a/stream.go +++ b/stream.go @@ -181,7 +181,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { } func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { - args := s.transcodeArgs(0, false) + args := s.transcodeArgs(0) if s.m.probe.CodecName == CODEC_H264 && s.quality == QUALITY_MAX { // try to just send the original file @@ -351,7 +351,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { } // Get arguments to ffmpeg -func (s *Stream) transcodeArgs(startAt float64, rotate bool) []string { +func (s *Stream) transcodeArgs(startAt float64) []string { args := []string{ "-loglevel", "warning", } @@ -376,13 +376,6 @@ func (s *Stream) transcodeArgs(startAt float64, rotate bool) []string { args = append(args, strings.Split(extra, " ")...) } - // Manual rotation: disable autorotate - if rotate { - args = append(args, []string{ - "-noautorotate", // Rotate manually - }...) - } - // Input specs args = append(args, []string{ "-i", s.m.path, // Input file @@ -415,8 +408,12 @@ func (s *Stream) transcodeArgs(startAt float64, rotate bool) []string { if CV != ENCODER_COPY { filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) - // Add transpose filter if needed - if rotate { + // Rotation is a mess: https://trac.ffmpeg.org/ticket/8329 + // 1/ autorotate=1 is needed, otherwise the sidecar metadata gets copied to the output + // 2/ But autorotation doesn't seem to work with HW (at least not with VAAPI) + // So keep autorotation enabled, but manually rotate for HW anyway. + // Also, the sidecar metadata only exists for MOV/MP4, so it's not a problem for TS. + if CV == ENCODER_VAAPI || CV == ENCODER_NVENC { transposer := "transpose" if CV == ENCODER_VAAPI { transposer = "transpose_vaapi" @@ -494,7 +491,7 @@ func (s *Stream) transcode(startId int) { } startAt := float64(startId * s.c.ChunkSize) - args := s.transcodeArgs(startAt, true) + args := s.transcodeArgs(startAt) // Segmenting specs args = append(args, []string{ From 4d0020a593f3a86db28394a8a1cc27933e3f2c97 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 27 May 2023 23:43:09 -0700 Subject: [PATCH 095/149] 0.1.7 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 5c9d48df..d8302208 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.6" + VERSION = "0.1.7" ) type Handler struct { From 3b0e91b1dfa8cead6fae1e113d79ebc6e36734ec Mon Sep 17 00:00:00 2001 From: lastlink Date: Wed, 31 May 2023 11:59:28 -0400 Subject: [PATCH 096/149] get dockerfile working on windows --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a1914c0f..72fc8809 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ FROM ubuntu:22.04 WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive COPY ./build-ffmpeg.sh . -RUN ./build-ffmpeg.sh +RUN bash ./build-ffmpeg.sh COPY --from=builder /app/go-vod . EXPOSE 47788 From 92413eaa295747365897c0ae6c5cacb3f17ad57a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 4 Jun 2023 11:25:55 -0700 Subject: [PATCH 097/149] build: remove step is not necessary --- build-ffmpeg-nvidia.sh | 3 ++- build-ffmpeg.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build-ffmpeg-nvidia.sh b/build-ffmpeg-nvidia.sh index 894efe80..29757acf 100755 --- a/build-ffmpeg-nvidia.sh +++ b/build-ffmpeg-nvidia.sh @@ -2,8 +2,9 @@ set -e +# apt-get remove -y libva ffmpeg # not needed for Docker + apt-get update -apt-get remove -y libva ffmpeg apt-get install -y \ sudo curl wget \ autoconf libtool libdrm-dev xorg xorg-dev openbox \ diff --git a/build-ffmpeg.sh b/build-ffmpeg.sh index 12536795..ed37191b 100755 --- a/build-ffmpeg.sh +++ b/build-ffmpeg.sh @@ -2,8 +2,9 @@ set -e +# apt-get remove -y libva ffmpeg # not needed for Docker + apt-get update -apt-get remove -y libva ffmpeg apt-get install -y \ sudo curl wget \ autoconf libtool libdrm-dev xorg xorg-dev openbox \ From ff9c27a74e4dbafeb8419e5677ec2216e7f334f3 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 21 Jul 2023 19:02:31 -0700 Subject: [PATCH 098/149] Fix invalid rotation with CPU --- stream.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/stream.go b/stream.go index 7e474f48..20db54bc 100644 --- a/stream.go +++ b/stream.go @@ -181,7 +181,7 @@ func (s *Stream) ServeChunk(w http.ResponseWriter, id int) error { } func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { - args := s.transcodeArgs(0) + args := s.transcodeArgs(0, false) if s.m.probe.CodecName == CODEC_H264 && s.quality == QUALITY_MAX { // try to just send the original file @@ -351,7 +351,7 @@ func (s *Stream) restartAtChunk(w http.ResponseWriter, id int) { } // Get arguments to ffmpeg -func (s *Stream) transcodeArgs(startAt float64) []string { +func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { args := []string{ "-loglevel", "warning", } @@ -376,6 +376,9 @@ func (s *Stream) transcodeArgs(startAt float64) []string { args = append(args, strings.Split(extra, " ")...) } + // Disable autorotation + args = append(args, []string{"-noautorotate"}...) + // Input specs args = append(args, []string{ "-i", s.m.path, // Input file @@ -409,11 +412,12 @@ func (s *Stream) transcodeArgs(startAt float64) []string { filter := fmt.Sprintf("%s,%s=%s", format, scaler, strings.Join(scalerArgs, ":")) // Rotation is a mess: https://trac.ffmpeg.org/ticket/8329 - // 1/ autorotate=1 is needed, otherwise the sidecar metadata gets copied to the output - // 2/ But autorotation doesn't seem to work with HW (at least not with VAAPI) + // 1/ -noautorotate copies the sidecar metadata to the output + // 2/ autorotation doesn't seem to work with HW anyway (at least not with VAAPI) + // 3/ autorotation doesn't work with HLS streams // So keep autorotation enabled, but manually rotate for HW anyway. // Also, the sidecar metadata only exists for MOV/MP4, so it's not a problem for TS. - if CV == ENCODER_VAAPI || CV == ENCODER_NVENC { + if isHls { transposer := "transpose" if CV == ENCODER_VAAPI { transposer = "transpose_vaapi" @@ -491,7 +495,7 @@ func (s *Stream) transcode(startId int) { } startAt := float64(startId * s.c.ChunkSize) - args := s.transcodeArgs(startAt) + args := s.transcodeArgs(startAt, true) // Segmenting specs args = append(args, []string{ From f2d1a66a2a534f36ab1008eec3f24847b0972cff Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 21 Jul 2023 20:21:39 -0700 Subject: [PATCH 099/149] Disable transpose for CUDA --- stream.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/stream.go b/stream.go index 20db54bc..889931a2 100644 --- a/stream.go +++ b/stream.go @@ -422,14 +422,17 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { if CV == ENCODER_VAAPI { transposer = "transpose_vaapi" } else if CV == ENCODER_NVENC { - transposer = "transpose_npp" + transposer = fmt.Sprintf("transpose_%s", s.c.NVENCScale) } - if s.m.probe.Rotation == -90 { - filter = fmt.Sprintf("%s,%s=1", filter, transposer) - } else if s.m.probe.Rotation == 90 { - filter = fmt.Sprintf("%s,%s=2", filter, transposer) - } else if s.m.probe.Rotation == 180 || s.m.probe.Rotation == -180 { - filter = fmt.Sprintf("%s,%s=1,%s=1", filter, transposer, transposer) + + if transposer != "transpose_cuda" { // does not exist + if s.m.probe.Rotation == -90 { + filter = fmt.Sprintf("%s,%s=1", filter, transposer) + } else if s.m.probe.Rotation == 90 { + filter = fmt.Sprintf("%s,%s=2", filter, transposer) + } else if s.m.probe.Rotation == 180 || s.m.probe.Rotation == -180 { + filter = fmt.Sprintf("%s,%s=1,%s=1", filter, transposer, transposer) + } } } From 7a67cf56e24e8d25a6e28a1d964738e5e9d1da0a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 3 Aug 2023 13:06:31 -0700 Subject: [PATCH 100/149] Make transpose optional and configurable Close #15 Close #16 Close #17 --- config.go | 3 +++ main.go | 2 +- stream.go | 14 ++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/config.go b/config.go index c8de3e38..68ac20a6 100644 --- a/config.go +++ b/config.go @@ -38,4 +38,7 @@ type Config struct { NVENC bool `json:"nvenc"` NVENCTemporalAQ bool `json:"nvencTemporalAQ"` NVENCScale string `json:"nvencScale"` // cuda, npp + + // Use transpose for streaming + UseTranspose bool `json:"useTranspose"` } diff --git a/main.go b/main.go index d8302208..95c1d9fe 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.7" + VERSION = "0.1.9" ) type Handler struct { diff --git a/stream.go b/stream.go index 889931a2..11aecff8 100644 --- a/stream.go +++ b/stream.go @@ -376,8 +376,10 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { args = append(args, strings.Split(extra, " ")...) } - // Disable autorotation - args = append(args, []string{"-noautorotate"}...) + // Disable autorotation (see transpose comments below) + if isHls && s.c.UseTranspose { + args = append(args, []string{"-noautorotate"}...) + } // Input specs args = append(args, []string{ @@ -413,11 +415,11 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { // Rotation is a mess: https://trac.ffmpeg.org/ticket/8329 // 1/ -noautorotate copies the sidecar metadata to the output - // 2/ autorotation doesn't seem to work with HW anyway (at least not with VAAPI) + // 2/ autorotation doesn't seem to work with some types of HW (at least not with VAAPI) // 3/ autorotation doesn't work with HLS streams - // So keep autorotation enabled, but manually rotate for HW anyway. - // Also, the sidecar metadata only exists for MOV/MP4, so it's not a problem for TS. - if isHls { + // 4/ VAAPI cannot transport on AMD GPUs + // So: give the user to disable autorotation for HLS and use a manual transpose + if isHls && s.c.UseTranspose { transposer := "transpose" if CV == ENCODER_VAAPI { transposer = "transpose_vaapi" From 478d62930dda70179e83b5b7ea35d0ffbb2e72fc Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 3 Aug 2023 21:08:29 -0700 Subject: [PATCH 101/149] stream: fix odd numbers in size --- main.go | 2 +- stream.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 95c1d9fe..24af3769 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.9" + VERSION = "0.1.10" ) type Handler struct { diff --git a/stream.go b/stream.go index 11aecff8..52e6a227 100644 --- a/stream.go +++ b/stream.go @@ -405,8 +405,13 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { // Scale height and width if not max quality if s.quality != QUALITY_MAX { - scalerArgs = append(scalerArgs, fmt.Sprintf("w=%d", s.width)) - scalerArgs = append(scalerArgs, fmt.Sprintf("h=%d", s.height)) + maxDim := s.height + if s.width > s.height { + maxDim = s.width + } + + scalerArgs = append(scalerArgs, fmt.Sprintf("w=%d", maxDim)) + scalerArgs = append(scalerArgs, fmt.Sprintf("h=%d", maxDim)) } // Apply filter From f347b996d2f50a587e8044887bbbf2e3f6d232a0 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 5 Aug 2023 11:24:07 -0700 Subject: [PATCH 102/149] manager: fix size calculation --- manager.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/manager.go b/manager.go index 3851cda0..cad79d36 100644 --- a/manager.go +++ b/manager.go @@ -69,12 +69,20 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Possible streams m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 500000} - m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 640, bitrate: 1200000} + m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 854, bitrate: 1200000} m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2200000} m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 3600000} m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 6000000} m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 10000000} + // height is our primary dimension for scaling + // using the probed size, we adjust the width of the stream + // the smaller dimemension of the output should match the height here + smDim, lgDim := m.probe.Height, m.probe.Width + if m.probe.Height > m.probe.Width { + smDim, lgDim = lgDim, smDim + } + // Only keep streams that are smaller than the video for k, stream := range m.streams { stream.order = 0 @@ -82,9 +90,17 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // scale bitrate by frame rate with reference 30 stream.bitrate = int(float64(stream.bitrate) * float64(m.probe.FrameRate) / 30.0) - // these streams are pointless - if stream.height > m.probe.Height || stream.width > m.probe.Width || float64(stream.bitrate) > float64(m.probe.BitRate)*0.8 { + // now store the width of the stream as the larger dimension + stream.width = int(math.Ceil(float64(lgDim) * float64(stream.height) / float64(smDim))) + + // remove invalid streams + if (stream.height > smDim || stream.width > lgDim) || // no upscaling; we're not AI + (float64(stream.bitrate) > float64(m.probe.BitRate)*0.8) || // no more than 80% of original bitrate + (stream.height%2 != 0 || stream.width%2 != 0) { // no odd dimensions + + // remove stream delete(m.streams, k) + continue } } From df534ae500dbcbcb902a966b4b04f6bc222c84ee Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 5 Aug 2023 11:24:20 -0700 Subject: [PATCH 103/149] Bump up version --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 24af3769..bb0746eb 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.10" + VERSION = "0.1.11" ) type Handler struct { From eb5ce7feddbf54e39f90ace695bba3cf42e80c8a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 24 Aug 2023 22:12:35 -0700 Subject: [PATCH 104/149] manager: use container duration --- manager.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/manager.go b/manager.go index cad79d36..49479081 100644 --- a/manager.go +++ b/manager.go @@ -251,7 +251,7 @@ func (m *Manager) ffprobe() error { "-v", "error", // Show everything - "-show_entries", "stream", + "-show_entries", "format:stream", "-select_streams", "v", // Video stream only, we're not interested in audio "-of", "json", @@ -299,16 +299,9 @@ func (m *Manager) ffprobe() error { var duration time.Duration if out.Streams[0].Duration != "" { - duration, err = time.ParseDuration(out.Streams[0].Duration + "s") - if err != nil { - return err - } - } - if out.Format.Duration != "" { - duration, err = time.ParseDuration(out.Format.Duration + "s") - if err != nil { - return err - } + duration, _ = time.ParseDuration(out.Streams[0].Duration + "s") + } else if out.Format.Duration != "" { + duration, _ = time.ParseDuration(out.Format.Duration + "s") } // FrameRate is a fraction string From 814af27881a7524b0f100e5d3e7f85bb2c2b4da8 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 24 Aug 2023 22:14:40 -0700 Subject: [PATCH 105/149] Bump up version --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index bb0746eb..dd4e28b0 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.11" + VERSION = "0.1.12" ) type Handler struct { From 58ac632839537f5c8b16124c7ab5c81a52869f76 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 30 Aug 2023 19:59:03 -0700 Subject: [PATCH 106/149] stream: add split_by_time HLS flag --- stream.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stream.go b/stream.go index 52e6a227..0274a755 100644 --- a/stream.go +++ b/stream.go @@ -511,11 +511,12 @@ func (s *Stream) transcode(startId int) { args = append(args, []string{ "-avoid_negative_ts", "disabled", "-f", "hls", + "-hls_flags", "split_by_time", "-hls_time", fmt.Sprintf("%d", s.c.ChunkSize), - "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.ChunkSize), "-hls_segment_type", "mpegts", - "-start_number", fmt.Sprintf("%d", startId), "-hls_segment_filename", s.getTsPath(-1), + "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.ChunkSize), + "-start_number", fmt.Sprintf("%d", startId), "-", }...) From 8d46582c11e3f3598ebf0ad89cb8e79169a842bf Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 30 Aug 2023 20:05:13 -0700 Subject: [PATCH 107/149] stream: use passtrough only for scale_cuda --- stream.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stream.go b/stream.go index 0274a755..5c09ee69 100644 --- a/stream.go +++ b/stream.go @@ -400,7 +400,11 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { } else if CV == ENCODER_NVENC { format = "format=nv12|cuda,hwupload" scaler = fmt.Sprintf("scale_%s", s.c.NVENCScale) - scalerArgs = append(scalerArgs, "passthrough=0") + + // workaround to force scale_cuda to examine all input frames + if s.c.NVENCScale == "cuda" { + scalerArgs = append(scalerArgs, "passthrough=0") + } } // Scale height and width if not max quality From 33714bb36a9373fed9efeb1a84060ab39992889c Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 30 Aug 2023 20:06:11 -0700 Subject: [PATCH 108/149] 0.1.13 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index dd4e28b0..ecf6d454 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.12" + VERSION = "0.1.13" ) type Handler struct { From c933613d1ee2db972c71a25b4e2dfb1e356df1c6 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 29 Sep 2023 10:00:22 -0700 Subject: [PATCH 109/149] Fix sidecar metadata in max.mov --- stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream.go b/stream.go index 5c09ee69..d9196196 100644 --- a/stream.go +++ b/stream.go @@ -377,7 +377,7 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { } // Disable autorotation (see transpose comments below) - if isHls && s.c.UseTranspose { + if s.c.UseTranspose { args = append(args, []string{"-noautorotate"}...) } From ec9900e16664fac6d1429392d852de296a0f5d67 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 29 Sep 2023 10:24:41 -0700 Subject: [PATCH 110/149] refactor: use log for log --- main.go | 9 ++++----- stream.go | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index ecf6d454..77b0e14a 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "io/ioutil" "log" "net/http" @@ -112,7 +111,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.c.Configured = true // Print loaded config - fmt.Printf("%+v\n", h.c) + log.Printf("%+v\n", h.c) return } @@ -213,12 +212,12 @@ func loadConfig(path string, c *Config) { c.Configured = true // Print loaded config - fmt.Printf("%+v\n", c) + log.Printf("%+v\n", c) } func main() { if len(os.Args) >= 2 && os.Args[1] == "test" { - fmt.Println("test successful") + log.Println("test successful") return } @@ -259,7 +258,7 @@ func main() { } // Print config - fmt.Printf("%+v\n", c) + log.Printf("%+v\n", c) // Start server log.Println("Starting VOD server") diff --git a/stream.go b/stream.go index d9196196..62f6f704 100644 --- a/stream.go +++ b/stream.go @@ -200,12 +200,12 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { cmdStdOut, err := coder.StdoutPipe() if err != nil { - fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + log.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) } cmdStdErr, err := coder.StderrPipe() if err != nil { - fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + log.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) } err = coder.Start() @@ -529,12 +529,12 @@ func (s *Stream) transcode(startId int) { cmdStdOut, err := s.coder.StdoutPipe() if err != nil { - fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + log.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) } cmdStdErr, err := s.coder.StderrPipe() if err != nil { - fmt.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) + log.Printf("FATAL: ffmpeg command stdout failed with %s\n", err) } err = s.coder.Start() From a7968c92796c648ca330d62b0fb0a117e10b51e2 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 29 Sep 2023 10:25:00 -0700 Subject: [PATCH 111/149] stream: add UseGopSize --- config.go | 5 ++++- stream.go | 22 +++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 68ac20a6..6e9a6efb 100644 --- a/config.go +++ b/config.go @@ -39,6 +39,9 @@ type Config struct { NVENCTemporalAQ bool `json:"nvencTemporalAQ"` NVENCScale string `json:"nvencScale"` // cuda, npp - // Use transpose for streaming + // Use transpose workaround for streaming (VA-API) UseTranspose bool `json:"useTranspose"` + + // Use GOP size workaround for streaming (NVENC) + UseGopSize bool `json:"useGopSize"` } diff --git a/stream.go b/stream.go index 62f6f704..4b536861 100644 --- a/stream.go +++ b/stream.go @@ -513,17 +513,33 @@ func (s *Stream) transcode(startId int) { // Segmenting specs args = append(args, []string{ + "-start_number", fmt.Sprintf("%d", startId), "-avoid_negative_ts", "disabled", "-f", "hls", "-hls_flags", "split_by_time", "-hls_time", fmt.Sprintf("%d", s.c.ChunkSize), "-hls_segment_type", "mpegts", "-hls_segment_filename", s.getTsPath(-1), - "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.ChunkSize), - "-start_number", fmt.Sprintf("%d", startId), - "-", }...) + // Keyframe specs + if s.c.UseGopSize && s.m.probe.FrameRate > 0 { + // Fix GOP size + args = append(args, []string{ + "-g", fmt.Sprintf("%d", s.c.ChunkSize*s.m.probe.FrameRate), + "-keyint_min", fmt.Sprintf("%d", s.c.ChunkSize*s.m.probe.FrameRate), + }...) + } else { + // Force keyframes every chunk + args = append(args, []string{ + "-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", s.c.ChunkSize), + }...) + } + + // Output to stdout + args = append(args, "-") + + // Start the process s.coder = exec.Command(s.c.FFmpeg, args...) log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(s.coder.Args[:], " ")) From ebb9b5373b3c3421f1b47a13e27cc20bb68b31d9 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 29 Sep 2023 10:25:17 -0700 Subject: [PATCH 112/149] Bump up version --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 77b0e14a..b3edd5ed 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - VERSION = "0.1.13" + VERSION = "0.1.14" ) type Handler struct { From 7a8c23d4e9cd04d085d9ed37546e5429f3a66059 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 30 Sep 2023 11:34:18 -0700 Subject: [PATCH 113/149] Allow multi resolution full videos --- main.go | 2 +- manager.go | 9 ++++++--- stream.go | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index b3edd5ed..f7c7f2c3 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - VERSION = "0.1.14" + VERSION = "0.1.5" ) type Handler struct { diff --git a/manager.go b/manager.go index 49479081..3d7ee776 100644 --- a/manager.go +++ b/manager.go @@ -210,12 +210,15 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request, chunk string } // Stream full video - movSfx := ".mov" - if strings.HasSuffix(chunk, movSfx) { - quality := strings.TrimSuffix(chunk, movSfx) + mp4Sfx := ".mp4" + if strings.HasSuffix(chunk, mp4Sfx) { + quality := strings.TrimSuffix(chunk, mp4Sfx) if stream, ok := m.streams[quality]; ok { return stream.ServeFullVideo(w, r) } + + // Fall back to original + return m.streams[QUALITY_MAX].ServeFullVideo(w, r) } w.WriteHeader(http.StatusNotFound) diff --git a/stream.go b/stream.go index 4b536861..89cba35c 100644 --- a/stream.go +++ b/stream.go @@ -192,7 +192,7 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { // Output mov args = append(args, []string{ "-movflags", "frag_keyframe+empty_moov+faststart", - "-f", "mov", "pipe:1", + "-f", "mp4", "pipe:1", }...) coder := exec.Command(s.c.FFmpeg, args...) @@ -219,7 +219,7 @@ func (s *Stream) ServeFullVideo(w http.ResponseWriter, r *http.Request) error { stdoutReader := bufio.NewReader(cmdStdOut) // Write mov headers - w.Header().Set("Content-Type", "video/quicktime") + w.Header().Set("Content-Type", "video/mp4") w.WriteHeader(http.StatusOK) flusher, ok := w.(http.Flusher) if !ok { From 54bfe16c28c1fd6b8968618f880dc2384ca78d80 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 30 Sep 2023 14:09:38 -0700 Subject: [PATCH 114/149] Oops --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index f7c7f2c3..d2f4994d 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - VERSION = "0.1.5" + VERSION = "0.1.16" ) type Handler struct { From 770f1e92dab7be92cc9cd206fed2562a0d9f5b56 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 17 Oct 2023 11:43:51 -0700 Subject: [PATCH 115/149] docs: disable issues --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index cfaa8399..3be9c5f4 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,14 @@ Extremely minimal on-demand video transcoding server in go. Used by the FOSS photos app, [Memories](https://github.com/pulsejet/memories). +## Filing Issues + +Please file issues at the [Memories](https://github.com/pulsejet/memories) repository. + ## Usage +Note: this package provides bespoke functionality for Memories. As such it is not intended to be used as a library. + You need go and ffmpeg/ffprobe installed ```bash From 11bca6cc533164eb19b79fd41c27c03fc8d58a39 Mon Sep 17 00:00:00 2001 From: Simon L Date: Thu, 19 Oct 2023 20:50:45 +0200 Subject: [PATCH 116/149] Create docker-build.yml Signed-off-by: Simon L --- .github/workflows/docker-build.yml | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..1b43e031 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,42 @@ +name: Docker Build and Publish + +on: + workflow_dispatch: + inputs: + tagName: + description: "Tag name" + required: true + default: 'v1' + +jobs: + push_to_registry: + runs-on: ubuntu-latest + + name: Build docker image and push to dockerhub + + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build container image + uses: docker/build-push-action@v5 + with: + push: true + platforms: linux/amd64,linux/arm64 + context: './' + no-cache: true + file: 'Dockerfile' + tags: pulsejet/go-vod:${{ github.event.inputs.tagName }} + provenance: false From d388b6f2334363be704326a74d6145087bf8490e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 14:12:09 -0700 Subject: [PATCH 117/149] Add version monitor --- config.go | 3 +++ main.go | 68 ++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/config.go b/config.go index 6e9a6efb..18ce5962 100644 --- a/config.go +++ b/config.go @@ -4,6 +4,9 @@ type Config struct { // Is this server configured? Configured bool + // Restart the server if incorrect version detected + VersionMonitor bool + // Bind address Bind string `json:"bind"` diff --git a/main.go b/main.go index d2f4994d..6e31f0e0 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - VERSION = "0.1.16" + VERSION = "0.1.17" ) type Handler struct { @@ -20,6 +20,8 @@ type Handler struct { managers map[string]*Manager mutex sync.RWMutex close chan string + server *http.Server + exitCode int } func NewHandler(c *Config) *Handler { @@ -27,13 +29,13 @@ func NewHandler(c *Config) *Handler { c: c, managers: make(map[string]*Manager), close: make(chan string), + exitCode: 0, } // Recreate tempdir os.RemoveAll(c.TempDir) os.MkdirAll(c.TempDir, 0755) - go h.watchClose() return h } @@ -43,6 +45,22 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // log.Println("Serving", url) + // Check version if monitoring is enabled + if h.c.VersionMonitor { + expected := r.Header.Get("X-Go-Vod-Version") + if len(expected) > 0 && expected != VERSION { + log.Println("Version mismatch", expected, VERSION) + + // Try again in some time + w.WriteHeader(http.StatusServiceUnavailable) + + // Exit with status code 65 + h.exitCode = 12 + h.Close() + return + } + } + // Break url into parts for _, part := range strings.Split(url, "/") { if part != "" { @@ -216,12 +234,9 @@ func loadConfig(path string, c *Config) { } func main() { - if len(os.Args) >= 2 && os.Args[1] == "test" { - log.Println("test successful") - return - } - + // Build initial configuration c := &Config{ + VersionMonitor: false, Bind: ":47788", ChunkSize: 3, LookBehind: 3, @@ -231,9 +246,18 @@ func main() { ManagerIdleTime: 60, } - // Load config file from second argument - if len(os.Args) >= 2 { - loadConfig(os.Args[1], c) + // Parse arguments + for _, arg := range os.Args[1:] { + if arg == "-version-monitor" { + c.VersionMonitor = true + } else if arg == "-test" { + // Just run the binary for test + log.Println("go-vod " + VERSION) + return + } else { + // Config file + loadConfig(arg, c) + } } // Auto-detect ffmpeg and ffprobe paths @@ -261,15 +285,23 @@ func main() { log.Printf("%+v\n", c) // Start server - log.Println("Starting VOD server") + log.Println("Starting go-vod " + VERSION + " on " + c.Bind) + handler := NewHandler(c) + server := &http.Server{Addr: c.Bind, Handler: handler} + handler.server = server - h := NewHandler(c) - http.Handle("/", h) - err := http.ListenAndServe(c.Bind, nil) - if err != nil { - log.Fatal("Error starting server", err) - } + // Start listening on different thread + go func() { + err := server.ListenAndServe() + if err != nil { + log.Fatal("Error starting server", err) + } + }() + // Wait for handler exit + handler.watchClose() log.Println("Exiting VOD server") - h.Close() + + // Exit with status code + os.Exit(handler.exitCode) } From ad79aa5827a3a4c554b526d8da46b860d87f633e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 14:14:28 -0700 Subject: [PATCH 118/149] Add run script --- run.sh | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 run.sh diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..9811dc3c --- /dev/null +++ b/run.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# This script fetches the current version of go-vod from Nextcloud +# to the working directory and runs it. If go-vod exits with a restart +# code, the script will restart it. + +# This script is intended to be run by systemd if running on bare metal. + +HOST=$NEXTCLOUD_HOST # passed as environment variable + +# check if host is set +if [[ -z $HOST ]]; then + echo "fatal: NEXTCLOUD_HOST is not set" + exit 1 +fi + +# add http:// if not present +if [[ ! $HOST == http://* ]] && [[ ! $HOST == https://* ]]; then + HOST="http://$HOST" +fi + +# build URL to fetch binary from Nextcloud +ARCH=$(uname -m) +URL="$HOST/index.php/apps/memories/static/go-vod?arch=$ARCH" + +# fetch binary, sleeping 10 seconds between retries +function fetch_binary { + while true; do + rm -f go-vod + curl -m 10 -s -o go-vod $URL + if [[ $? == 0 ]]; then + chmod +x go-vod + echo "Fetched $URL successfully!" + break + fi + echo "Failed to fetch $URL, retrying in 10 seconds" + echo "Are you sure the host is reachable and running Memories v6+?" + sleep 10 + done +} + +# infinite loop +while true; do + fetch_binary + ./go-vod -version-monitor + if [[ $? != 12 ]]; then + break + fi + + sleep 3 # throttle +done \ No newline at end of file From d678fda96e6878a514f16663a104ed2bacd84e31 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 14:24:49 -0700 Subject: [PATCH 119/149] main: print version to stdout --- main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 6e31f0e0..30252c97 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "io/ioutil" "log" "net/http" @@ -12,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.17" + VERSION = "0.1.18" ) type Handler struct { @@ -250,9 +251,8 @@ func main() { for _, arg := range os.Args[1:] { if arg == "-version-monitor" { c.VersionMonitor = true - } else if arg == "-test" { - // Just run the binary for test - log.Println("go-vod " + VERSION) + } else if arg == "-version" { + fmt.Print("go-vod " + VERSION) return } else { // Config file From d98d0b9522591b00aa6711331b2210f4fba1f8b7 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 14:53:06 -0700 Subject: [PATCH 120/149] run: add missing params to curl --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index 9811dc3c..aa20c708 100755 --- a/run.sh +++ b/run.sh @@ -27,7 +27,7 @@ URL="$HOST/index.php/apps/memories/static/go-vod?arch=$ARCH" function fetch_binary { while true; do rm -f go-vod - curl -m 10 -s -o go-vod $URL + curl -L -k -f -m 10 -s -o go-vod $URL if [[ $? == 0 ]]; then chmod +x go-vod echo "Fetched $URL successfully!" From 899098bc51e5d1d11d38e9078284b374b5fc13da Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 14:54:01 -0700 Subject: [PATCH 121/149] run: improve logging --- run.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run.sh b/run.sh index aa20c708..50a65bc3 100755 --- a/run.sh +++ b/run.sh @@ -33,8 +33,9 @@ function fetch_binary { echo "Fetched $URL successfully!" break fi - echo "Failed to fetch $URL, retrying in 10 seconds" + echo "Failed to fetch $URL" echo "Are you sure the host is reachable and running Memories v6+?" + echo "Retrying in 10 seconds..." sleep 10 done } From edbcaaa06415726cf46bfb3140a367307525108f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 15:02:59 -0700 Subject: [PATCH 122/149] dockerfile: swith to inheriting build --- Dockerfile | 15 ++++----------- build-ffmpeg-nvidia.sh | 5 ++++- build-ffmpeg.sh | 5 ++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 72fc8809..360e1d88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,7 @@ -FROM golang:bullseye AS builder -WORKDIR /app -COPY . . -RUN CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" +FROM linuxserver/ffmpeg:latest -FROM ubuntu:22.04 -WORKDIR /app -ENV DEBIAN_FRONTEND=noninteractive -COPY ./build-ffmpeg.sh . -RUN bash ./build-ffmpeg.sh +COPY run.sh /go-vod.sh -COPY --from=builder /app/go-vod . EXPOSE 47788 -CMD ["/app/go-vod"] + +ENTRYPOINT ["/go-vod.sh"] diff --git a/build-ffmpeg-nvidia.sh b/build-ffmpeg-nvidia.sh index 29757acf..0bf9c1cd 100755 --- a/build-ffmpeg-nvidia.sh +++ b/build-ffmpeg-nvidia.sh @@ -2,7 +2,10 @@ set -e -# apt-get remove -y libva ffmpeg # not needed for Docker +# This script is intended for bare-metal installations. +# It builds ffmpeg and NVENC drivers from source. + +apt-get remove -y ffmpeg apt-get update apt-get install -y \ diff --git a/build-ffmpeg.sh b/build-ffmpeg.sh index ed37191b..221c859c 100755 --- a/build-ffmpeg.sh +++ b/build-ffmpeg.sh @@ -1,8 +1,11 @@ #!/bin/bash +# This script is intended for bare-metal installations. +# It builds ffmpeg and VA-API drivers from source. + set -e -# apt-get remove -y libva ffmpeg # not needed for Docker +apt-get remove -y libva ffmpeg apt-get update apt-get install -y \ From 36832c731c252160ba542e138cf9accf52714750 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 15:12:58 -0700 Subject: [PATCH 123/149] build: don't remove lists --- build-ffmpeg-nvidia.sh | 2 -- build-ffmpeg.sh | 2 -- 2 files changed, 4 deletions(-) diff --git a/build-ffmpeg-nvidia.sh b/build-ffmpeg-nvidia.sh index 0bf9c1cd..107ed36a 100755 --- a/build-ffmpeg-nvidia.sh +++ b/build-ffmpeg-nvidia.sh @@ -37,5 +37,3 @@ make -j"$(nproc)" sudo make install sudo ldconfig cd .. - -rm -rf /var/lib/apt/lists/* diff --git a/build-ffmpeg.sh b/build-ffmpeg.sh index 221c859c..96bf7288 100755 --- a/build-ffmpeg.sh +++ b/build-ffmpeg.sh @@ -60,5 +60,3 @@ cd .. cd .. rm -rf qsvbuild - -rm -rf /var/lib/apt/lists/* From d4bdc7bd81c963189f9d2c1b56fe30d64ce0824a Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 15:17:55 -0700 Subject: [PATCH 124/149] Remove NVIDIA Dockerfile --- Dockerfile.nvidia | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 Dockerfile.nvidia diff --git a/Dockerfile.nvidia b/Dockerfile.nvidia deleted file mode 100644 index e81b5d5b..00000000 --- a/Dockerfile.nvidia +++ /dev/null @@ -1,17 +0,0 @@ -FROM golang:bullseye AS builder -WORKDIR /app -COPY . . -RUN CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" - -FROM nvidia/cuda:11.1.1-base-ubuntu20.04 -ENV NVIDIA_VISIBLE_DEVICES=all -ENV NVIDIA_DRIVER_CAPABILITIES=all - -WORKDIR /app -ENV DEBIAN_FRONTEND=noninteractive -COPY ./build-ffmpeg-nvidia.sh . -RUN ./build-ffmpeg-nvidia.sh - -COPY --from=builder /app/go-vod . -EXPOSE 47788 -CMD ["/app/go-vod"] From 07fff59353c7fb5c9da006ad4b06e7a249cbb617 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 16:01:14 -0700 Subject: [PATCH 125/149] ci: update image name --- .github/workflows/docker-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1b43e031..8fa522d0 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -7,7 +7,7 @@ on: description: "Tag name" required: true default: 'v1' - + jobs: push_to_registry: runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -38,5 +38,5 @@ jobs: context: './' no-cache: true file: 'Dockerfile' - tags: pulsejet/go-vod:${{ github.event.inputs.tagName }} + tags: radialapps/go-vod:${{ github.event.inputs.tagName }} provenance: false From 432b7a47914978381f812448eada5f8eccd6b2c3 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 16:02:15 -0700 Subject: [PATCH 126/149] 0.1.19 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 30252c97..4e920175 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = "0.1.18" + VERSION = "0.1.19" ) type Handler struct { From 9c3fca950cb2960d953ad3dad88b9ff3ef8e6c13 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 20:28:16 -0700 Subject: [PATCH 127/149] ci: add latest tag --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 8fa522d0..e2e37ef9 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -38,5 +38,5 @@ jobs: context: './' no-cache: true file: 'Dockerfile' - tags: radialapps/go-vod:${{ github.event.inputs.tagName }} + tags: radialapps/go-vod:${{ github.event.inputs.tagName }} , radialapps/go-vod:latest provenance: false From 9f50cae0b17e9e9c3c58592d29a521e7c8fbe827 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 20 Oct 2023 20:47:09 -0700 Subject: [PATCH 128/149] run: more opts --- run.sh | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/run.sh b/run.sh index 50a65bc3..0feb5d2a 100755 --- a/run.sh +++ b/run.sh @@ -6,7 +6,9 @@ # This script is intended to be run by systemd if running on bare metal. -HOST=$NEXTCLOUD_HOST # passed as environment variable +# Environment variables +HOST=$NEXTCLOUD_HOST +ALLOW_INSECURE=$NEXTCLOUD_ALLOW_INSECURE # check if host is set if [[ -z $HOST ]]; then @@ -14,20 +16,33 @@ if [[ -z $HOST ]]; then exit 1 fi -# add http:// if not present +# check if scheme is set if [[ ! $HOST == http://* ]] && [[ ! $HOST == https://* ]]; then - HOST="http://$HOST" + echo "fatal: NEXTCLOUD_HOST must start with http:// or https://" + exit 1 +fi + +# check if scheme is http and allow_insecure is not set +if [[ $HOST == http://* ]] && [[ -z $ALLOW_INSECURE ]]; then + echo "fatal: NEXTCLOUD_HOST is set to http:// but NEXTCLOUD_ALLOW_INSECURE is not set" + exit 1 fi # build URL to fetch binary from Nextcloud ARCH=$(uname -m) URL="$HOST/index.php/apps/memories/static/go-vod?arch=$ARCH" +# set the -k option in curl if allow_insecure is set +EXTRA_CURL_ARGS="" +if [[ $ALLOW_INSECURE == true ]]; then + EXTRA_CURL_ARGS="$EXTRA_CURL_ARGS -k" +fi + # fetch binary, sleeping 10 seconds between retries function fetch_binary { while true; do rm -f go-vod - curl -L -k -f -m 10 -s -o go-vod $URL + curl $EXTRA_CURL_ARGS -L -f -m 10 -s -o go-vod $URL if [[ $? == 0 ]]; then chmod +x go-vod echo "Fetched $URL successfully!" From 48ed209d598977ef0c3f2b3104663fb50e5c0d4c Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 21 Oct 2023 11:27:25 -0700 Subject: [PATCH 129/149] refactor: move around handler --- config.go | 55 +++++++++++ handler.go | 223 +++++++++++++++++++++++++++++++++++++++++++ main.go | 272 ++--------------------------------------------------- 3 files changed, 286 insertions(+), 264 deletions(-) create mode 100644 handler.go diff --git a/config.go b/config.go index 18ce5962..e7fcf6ec 100644 --- a/config.go +++ b/config.go @@ -1,5 +1,13 @@ package main +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "os/exec" +) + type Config struct { // Is this server configured? Configured bool @@ -48,3 +56,50 @@ type Config struct { // Use GOP size workaround for streaming (NVENC) UseGopSize bool `json:"useGopSize"` } + +func (c *Config) FromFile(path string) { + // load json config + content, err := ioutil.ReadFile(path) + if err != nil { + log.Fatal("Error when opening file: ", err) + } + + err = json.Unmarshal(content, &c) + if err != nil { + log.Fatal("Error loading config file", err) + } + + // Set config as loaded + c.Configured = true + c.Print() +} + +func (c *Config) AutoDetect() { + // Auto-detect ffmpeg and ffprobe paths + if c.FFmpeg == "" || c.FFprobe == "" { + ffmpeg, err := exec.LookPath("ffmpeg") + if err != nil { + log.Fatal("Could not find ffmpeg") + } + + ffprobe, err := exec.LookPath("ffprobe") + if err != nil { + log.Fatal("Could not find ffprobe") + } + + c.FFmpeg = ffmpeg + c.FFprobe = ffprobe + } + + // Auto-choose tempdir + if c.TempDir == "" { + c.TempDir = os.TempDir() + "/go-vod" + } + + // Print updated config + c.Print() +} + +func (c *Config) Print() { + log.Printf("%+v\n", c) +} diff --git a/handler.go b/handler.go new file mode 100644 index 00000000..789d37ab --- /dev/null +++ b/handler.go @@ -0,0 +1,223 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "sync" +) + +type Handler struct { + c *Config + server *http.Server + managers map[string]*Manager + mutex sync.RWMutex + close chan string + exitCode int +} + +func NewHandler(c *Config) *Handler { + h := &Handler{ + c: c, + managers: make(map[string]*Manager), + close: make(chan string), + exitCode: 0, + } + + // Recreate tempdir + os.RemoveAll(c.TempDir) + os.MkdirAll(c.TempDir, 0755) + + return h +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Check version if monitoring is enabled + if h.c.VersionMonitor && !h.versionOk(w, r) { + return + } + + url := r.URL.Path + parts := make([]string, 0) + + // log.Println("Serving", url) + + // Break url into parts + for _, part := range strings.Split(url, "/") { + if part != "" { + parts = append(parts, part) + } + } + + // Serve actual file from manager + if len(parts) < 3 { + log.Println("Invalid URL", url) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Get streamid and chunk + streamid := parts[0] + path := "/" + strings.Join(parts[1:len(parts)-1], "/") + chunk := parts[len(parts)-1] + + // Check if POST request to create temp file + if r.Method == "POST" && len(parts) >= 2 && parts[1] == "create" { + var err error + path, err = h.createTempFile(w, r, parts) + if err != nil { + return + } + } + + // Check if test request + if chunk == "test" { + w.Header().Set("Content-Type", "application/json") + + // check if test file is readable + size := 0 + info, err := os.Stat(path) + if err == nil { + size = int(info.Size()) + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "version": VERSION, + "size": size, + }) + return + } + + // Check if configuration request + if r.Method == "POST" && chunk == "config" { + w.Header().Set("Content-Type", "application/json") + // read new config + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading body", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Unmarshal config + if err := json.Unmarshal(body, h.c); err != nil { + log.Println("Error unmarshaling config", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Set config as loaded + h.c.Configured = true + + // Print loaded config + log.Printf("%+v\n", h.c) + return + } + + // Check if configured + if !h.c.Configured { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + // Check if valid + if streamid == "" || path == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Get existing manager or create new one + manager := h.getManager(path, streamid) + if manager == nil { + manager = h.createManager(path, streamid) + } + + // Failed to create manager + if manager == nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Serve chunk if asked for + if chunk != "" && chunk != "ignore" { + manager.ServeHTTP(w, r, chunk) + } +} + +func (h *Handler) versionOk(w http.ResponseWriter, r *http.Request) bool { + expected := r.Header.Get("X-Go-Vod-Version") + if len(expected) > 0 && expected != VERSION { + log.Println("Version mismatch", expected, VERSION) + + // Try again in some time + w.WriteHeader(http.StatusServiceUnavailable) + + // Exit with status code 12 + h.exitCode = 12 + h.Close() + return false + } + + return true +} + +func (h *Handler) getManager(path string, streamid string) *Manager { + h.mutex.RLock() + defer h.mutex.RUnlock() + + m := h.managers[streamid] + if m == nil || m.path != path { + return nil + } + return m +} + +func (h *Handler) createManager(path string, streamid string) *Manager { + manager, err := NewManager(h.c, path, streamid, h.close) + if err != nil { + log.Println("Error creating manager", err) + freeIfTemp(path) + return nil + } + + h.mutex.Lock() + defer h.mutex.Unlock() + + old := h.managers[streamid] + if old != nil { + old.Destroy() + } + + h.managers[streamid] = manager + return manager +} + +func (h *Handler) removeManager(streamid string) { + h.mutex.Lock() + defer h.mutex.Unlock() + delete(h.managers, streamid) +} + +func (h *Handler) Start() { + go func() { + err := h.server.ListenAndServe() + if err != nil { + log.Fatal("Error starting server", err) + } + }() + + for { + id := <-h.close + if id == "" { + return + } + h.removeManager(id) + } +} + +func (h *Handler) Close() { + h.close <- "" +} diff --git a/main.go b/main.go index 4e920175..2da49188 100644 --- a/main.go +++ b/main.go @@ -1,238 +1,13 @@ package main import ( - "encoding/json" "fmt" - "io/ioutil" "log" "net/http" "os" - "os/exec" - "strings" - "sync" ) -const ( - VERSION = "0.1.19" -) - -type Handler struct { - c *Config - managers map[string]*Manager - mutex sync.RWMutex - close chan string - server *http.Server - exitCode int -} - -func NewHandler(c *Config) *Handler { - h := &Handler{ - c: c, - managers: make(map[string]*Manager), - close: make(chan string), - exitCode: 0, - } - - // Recreate tempdir - os.RemoveAll(c.TempDir) - os.MkdirAll(c.TempDir, 0755) - - return h -} - -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - url := r.URL.Path - parts := make([]string, 0) - - // log.Println("Serving", url) - - // Check version if monitoring is enabled - if h.c.VersionMonitor { - expected := r.Header.Get("X-Go-Vod-Version") - if len(expected) > 0 && expected != VERSION { - log.Println("Version mismatch", expected, VERSION) - - // Try again in some time - w.WriteHeader(http.StatusServiceUnavailable) - - // Exit with status code 65 - h.exitCode = 12 - h.Close() - return - } - } - - // Break url into parts - for _, part := range strings.Split(url, "/") { - if part != "" { - parts = append(parts, part) - } - } - - // Serve actual file from manager - if len(parts) < 3 { - log.Println("Invalid URL", url) - w.WriteHeader(http.StatusBadRequest) - return - } - - // Get streamid and chunk - streamid := parts[0] - path := "/" + strings.Join(parts[1:len(parts)-1], "/") - chunk := parts[len(parts)-1] - - // Check if POST request to create temp file - if r.Method == "POST" && len(parts) >= 2 && parts[1] == "create" { - var err error - path, err = h.createTempFile(w, r, parts) - if err != nil { - return - } - } - - // Check if test request - if chunk == "test" { - w.Header().Set("Content-Type", "application/json") - - // check if test file is readable - size := 0 - info, err := os.Stat(path) - if err == nil { - size = int(info.Size()) - } - - json.NewEncoder(w).Encode(map[string]interface{}{ - "version": VERSION, - "size": size, - }) - return - } - - // Check if configuration request - if r.Method == "POST" && chunk == "config" { - w.Header().Set("Content-Type", "application/json") - // read new config - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Println("Error reading body", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Unmarshal config - if err := json.Unmarshal(body, h.c); err != nil { - log.Println("Error unmarshaling config", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Set config as loaded - h.c.Configured = true - - // Print loaded config - log.Printf("%+v\n", h.c) - return - } - - // Check if configured - if !h.c.Configured { - w.WriteHeader(http.StatusServiceUnavailable) - return - } - - // Check if valid - if streamid == "" || path == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - // Get existing manager or create new one - manager := h.getManager(path, streamid) - if manager == nil { - manager = h.createManager(path, streamid) - } - - // Failed to create manager - if manager == nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Serve chunk if asked for - if chunk != "" && chunk != "ignore" { - manager.ServeHTTP(w, r, chunk) - } -} - -func (h *Handler) getManager(path string, streamid string) *Manager { - h.mutex.RLock() - defer h.mutex.RUnlock() - - m := h.managers[streamid] - if m == nil || m.path != path { - return nil - } - return m -} - -func (h *Handler) createManager(path string, streamid string) *Manager { - manager, err := NewManager(h.c, path, streamid, h.close) - if err != nil { - log.Println("Error creating manager", err) - freeIfTemp(path) - return nil - } - - h.mutex.Lock() - defer h.mutex.Unlock() - - old := h.managers[streamid] - if old != nil { - old.Destroy() - } - - h.managers[streamid] = manager - return manager -} - -func (h *Handler) removeManager(streamid string) { - h.mutex.Lock() - defer h.mutex.Unlock() - delete(h.managers, streamid) -} - -func (h *Handler) watchClose() { - for { - id := <-h.close - if id == "" { - return - } - h.removeManager(id) - } -} - -func (h *Handler) Close() { - h.close <- "" -} - -func loadConfig(path string, c *Config) { - // load json config - content, err := ioutil.ReadFile(path) - if err != nil { - log.Fatal("Error when opening file: ", err) - } - - err = json.Unmarshal(content, &c) - if err != nil { - log.Fatal("Error loading config file", err) - } - - // Set config as loaded - c.Configured = true - - // Print loaded config - log.Printf("%+v\n", c) -} +const VERSION = "0.1.19" func main() { // Build initial configuration @@ -255,51 +30,20 @@ func main() { fmt.Print("go-vod " + VERSION) return } else { - // Config file - loadConfig(arg, c) + c.FromFile(arg) // config file } } - // Auto-detect ffmpeg and ffprobe paths - if c.FFmpeg == "" || c.FFprobe == "" { - ffmpeg, err := exec.LookPath("ffmpeg") - if err != nil { - log.Fatal("Could not find ffmpeg") - } + // Auto detect ffmpeg and ffprobe + c.AutoDetect() - ffprobe, err := exec.LookPath("ffprobe") - if err != nil { - log.Fatal("Could not find ffprobe") - } - - c.FFmpeg = ffmpeg - c.FFprobe = ffprobe - } - - // Auto-choose tempdir - if c.TempDir == "" { - c.TempDir = os.TempDir() + "/go-vod" - } - - // Print config - log.Printf("%+v\n", c) - - // Start server + // Build HTTP server log.Println("Starting go-vod " + VERSION + " on " + c.Bind) handler := NewHandler(c) - server := &http.Server{Addr: c.Bind, Handler: handler} - handler.server = server + handler.server = &http.Server{Addr: c.Bind, Handler: handler} - // Start listening on different thread - go func() { - err := server.ListenAndServe() - if err != nil { - log.Fatal("Error starting server", err) - } - }() - - // Wait for handler exit - handler.watchClose() + // Start server and wait for handler exit + handler.Start() log.Println("Exiting VOD server") // Exit with status code From ea9e620de3588c5fdfbaa19a787e944821832b7f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 21 Oct 2023 11:36:22 -0700 Subject: [PATCH 130/149] refactor: create package --- chunk.go => go_vod/chunk.go | 4 ++-- config.go => go_vod/config.go | 5 ++++- handler.go => go_vod/handler.go | 19 ++++++++++++++----- manager.go => go_vod/manager.go | 5 +++-- stream.go => go_vod/stream.go | 2 +- temp.go => go_vod/temp.go | 2 +- util.go => go_vod/util.go | 2 +- main.go | 18 ++++++------------ 8 files changed, 32 insertions(+), 25 deletions(-) rename chunk.go => go_vod/chunk.go (91%) rename config.go => go_vod/config.go (97%) rename handler.go => go_vod/handler.go (92%) rename manager.go => go_vod/manager.go (98%) rename stream.go => go_vod/stream.go (99%) rename temp.go => go_vod/temp.go (98%) rename util.go => go_vod/util.go (94%) diff --git a/chunk.go b/go_vod/chunk.go similarity index 91% rename from chunk.go rename to go_vod/chunk.go index 662c9c75..7cb13fa6 100644 --- a/chunk.go +++ b/go_vod/chunk.go @@ -1,4 +1,4 @@ -package main +package go_vod type Chunk struct { id int @@ -12,4 +12,4 @@ func NewChunk(id int) *Chunk { done: false, notifs: make([]chan bool, 0), } -} \ No newline at end of file +} diff --git a/config.go b/go_vod/config.go similarity index 97% rename from config.go rename to go_vod/config.go index e7fcf6ec..5c6d72ae 100644 --- a/config.go +++ b/go_vod/config.go @@ -1,4 +1,4 @@ -package main +package go_vod import ( "encoding/json" @@ -9,6 +9,9 @@ import ( ) type Config struct { + // Current version of go-vod + Version string + // Is this server configured? Configured bool diff --git a/handler.go b/go_vod/handler.go similarity index 92% rename from handler.go rename to go_vod/handler.go index 789d37ab..76b20c63 100644 --- a/handler.go +++ b/go_vod/handler.go @@ -1,4 +1,4 @@ -package main +package go_vod import ( "encoding/json" @@ -85,7 +85,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(map[string]interface{}{ - "version": VERSION, + "version": h.c.Version, "size": size, }) return @@ -149,8 +149,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *Handler) versionOk(w http.ResponseWriter, r *http.Request) bool { expected := r.Header.Get("X-Go-Vod-Version") - if len(expected) > 0 && expected != VERSION { - log.Println("Version mismatch", expected, VERSION) + if len(expected) > 0 && expected != h.c.Version { + log.Println("Version mismatch", expected, h.c.Version) // Try again in some time w.WriteHeader(http.StatusServiceUnavailable) @@ -202,6 +202,8 @@ func (h *Handler) removeManager(streamid string) { } func (h *Handler) Start() { + h.server = &http.Server{Addr: h.c.Bind, Handler: h} + go func() { err := h.server.ListenAndServe() if err != nil { @@ -212,10 +214,17 @@ func (h *Handler) Start() { for { id := <-h.close if id == "" { - return + break } h.removeManager(id) } + + // Stop server + log.Println("Exiting VOD server") + h.server.Close() + + // Exit with correct status code + os.Exit(h.exitCode) } func (h *Handler) Close() { diff --git a/manager.go b/go_vod/manager.go similarity index 98% rename from manager.go rename to go_vod/manager.go index 3d7ee776..b20a48a3 100644 --- a/manager.go +++ b/go_vod/manager.go @@ -1,4 +1,4 @@ -package main +package go_vod import ( "bytes" @@ -261,7 +261,8 @@ func (m *Manager) ffprobe() error { m.path, } - ctx, _ := context.WithDeadline(context.TODO(), time.Now().Add(5*time.Second)) + ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(5*time.Second)) + defer cancel() cmd := exec.CommandContext(ctx, m.c.FFprobe, args...) var stdout, stderr bytes.Buffer diff --git a/stream.go b/go_vod/stream.go similarity index 99% rename from stream.go rename to go_vod/stream.go index 89cba35c..7b6023bb 100644 --- a/stream.go +++ b/go_vod/stream.go @@ -1,4 +1,4 @@ -package main +package go_vod import ( "bufio" diff --git a/temp.go b/go_vod/temp.go similarity index 98% rename from temp.go rename to go_vod/temp.go index 4a40fc52..43eac11d 100644 --- a/temp.go +++ b/go_vod/temp.go @@ -1,4 +1,4 @@ -package main +package go_vod import ( "encoding/json" diff --git a/util.go b/go_vod/util.go similarity index 94% rename from util.go rename to go_vod/util.go index cf2e1551..4e52606f 100644 --- a/util.go +++ b/go_vod/util.go @@ -1,4 +1,4 @@ -package main +package go_vod import "net/http" diff --git a/main.go b/main.go index 2da49188..e134691a 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,18 @@ package main import ( "fmt" "log" - "net/http" "os" + + "github.com/pulsejet/go-vod/go_vod" ) const VERSION = "0.1.19" func main() { // Build initial configuration - c := &Config{ + c := &go_vod.Config{ VersionMonitor: false, + Version: VERSION, Bind: ":47788", ChunkSize: 3, LookBehind: 3, @@ -37,15 +39,7 @@ func main() { // Auto detect ffmpeg and ffprobe c.AutoDetect() - // Build HTTP server + // Start server log.Println("Starting go-vod " + VERSION + " on " + c.Bind) - handler := NewHandler(c) - handler.server = &http.Server{Addr: c.Bind, Handler: handler} - - // Start server and wait for handler exit - handler.Start() - log.Println("Exiting VOD server") - - // Exit with status code - os.Exit(handler.exitCode) + go_vod.NewHandler(c).Start() } From 4e2b1ddb18110269f38a2f555b1ac65ada7452d8 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 21 Oct 2023 11:45:30 -0700 Subject: [PATCH 131/149] handler: fix exit --- go_vod/handler.go | 21 ++++++++++++++------- main.go | 7 +++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/go_vod/handler.go b/go_vod/handler.go index 76b20c63..c3c8baf4 100644 --- a/go_vod/handler.go +++ b/go_vod/handler.go @@ -1,6 +1,7 @@ package go_vod import ( + "context" "encoding/json" "io/ioutil" "log" @@ -8,6 +9,7 @@ import ( "os" "strings" "sync" + "time" ) type Handler struct { @@ -201,13 +203,16 @@ func (h *Handler) removeManager(streamid string) { delete(h.managers, streamid) } -func (h *Handler) Start() { +func (h *Handler) Start() int { + log.Println("Starting go-vod " + h.c.Version + " on " + h.c.Bind) h.server = &http.Server{Addr: h.c.Bind, Handler: h} go func() { err := h.server.ListenAndServe() - if err != nil { - log.Fatal("Error starting server", err) + if err == http.ErrServerClosed { + log.Println("HTTP server closed") + } else if err != nil { + log.Fatal("Error starting server: ", err) } }() @@ -220,11 +225,13 @@ func (h *Handler) Start() { } // Stop server - log.Println("Exiting VOD server") - h.server.Close() + log.Println("Shutting down HTTP server") + ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(5*time.Second)) + defer cancel() + h.server.Shutdown(ctx) - // Exit with correct status code - os.Exit(h.exitCode) + // Return status code + return h.exitCode } func (h *Handler) Close() { diff --git a/main.go b/main.go index e134691a..3bdec69a 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,9 @@ func main() { c.AutoDetect() // Start server - log.Println("Starting go-vod " + VERSION + " on " + c.Bind) - go_vod.NewHandler(c).Start() + code := go_vod.NewHandler(c).Start() + + // Exit + log.Println("Exiting go-vod with status code", code) + os.Exit(code) } From 27d332eff50628a5cc3e50692e0a5deeb223c240 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 21 Oct 2023 11:46:08 -0700 Subject: [PATCH 132/149] 0.1.20 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 3bdec69a..316edb26 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.19" +const VERSION = "0.1.20" func main() { // Build initial configuration From 2438a2946ddc25389fa2ece1fc46d268b53e43c1 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 24 Oct 2023 17:17:36 -0700 Subject: [PATCH 133/149] stream: make QF configurable --- go_vod/config.go | 3 +++ go_vod/stream.go | 14 +++----------- main.go | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/go_vod/config.go b/go_vod/config.go index 5c6d72ae..9514da68 100644 --- a/go_vod/config.go +++ b/go_vod/config.go @@ -42,6 +42,9 @@ type Config struct { // Number of seconds to wait before shutting down a client ManagerIdleTime int `json:"managerIdleTime"` + // Quality Factor (e.g. CRF / global_quality) + QF int `json:"qf"` + // Hardware acceleration configuration // VA-API diff --git a/go_vod/stream.go b/go_vod/stream.go index 7b6023bb..7a5b51c8 100644 --- a/go_vod/stream.go +++ b/go_vod/stream.go @@ -451,14 +451,6 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { args = append(args, []string{"-profile:v", "main"}...) } - // Apply bitrate cap if not max quality - if s.quality != QUALITY_MAX { - args = append(args, []string{ - "-maxrate", fmt.Sprintf("%d", s.bitrate), - "-bufsize", fmt.Sprintf("%d", s.bitrate*2), - }...) - } - // Output specs for video args = append(args, []string{ "-map", "0:v:0", @@ -467,7 +459,7 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { // Device specific output args if CV == ENCODER_VAAPI { - args = append(args, []string{"-global_quality", "25"}...) + args = append(args, []string{"-global_quality", fmt.Sprintf("%d", s.c.QF)}...) if s.c.VAAPILowPower { args = append(args, []string{"-low_power", "1"}...) @@ -478,7 +470,7 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { "-tune", "ll", "-rc", "vbr", "-rc-lookahead", "30", - "-cq", "24", + "-cq", fmt.Sprintf("%d", s.c.QF), }...) if s.c.NVENCTemporalAQ { @@ -487,7 +479,7 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { } else if CV == ENCODER_X264 { args = append(args, []string{ "-preset", "faster", - "-crf", "24", + "-crf", fmt.Sprintf("%d", s.c.QF), }...) } diff --git a/main.go b/main.go index 316edb26..f43ad6ad 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.20" +const VERSION = "0.1.21" func main() { // Build initial configuration From a8ad3e95c68af01f3104d0409d388f8274cd1e4f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 24 Oct 2023 18:01:57 -0700 Subject: [PATCH 134/149] manager: prevent duplicate max stream --- go_vod/manager.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go_vod/manager.go b/go_vod/manager.go index b20a48a3..2e1aac0a 100644 --- a/go_vod/manager.go +++ b/go_vod/manager.go @@ -94,7 +94,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, stream.width = int(math.Ceil(float64(lgDim) * float64(stream.height) / float64(smDim))) // remove invalid streams - if (stream.height > smDim || stream.width > lgDim) || // no upscaling; we're not AI + if (stream.height >= smDim || stream.width >= lgDim) || // no upscaling; we're not AI (float64(stream.bitrate) > float64(m.probe.BitRate)*0.8) || // no more than 80% of original bitrate (stream.height%2 != 0 || stream.width%2 != 0) { // no odd dimensions diff --git a/main.go b/main.go index f43ad6ad..fa676c00 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.21" +const VERSION = "0.1.22" func main() { // Build initial configuration From 029d3bf1cbae7914018a8542b3947d48f8c5957c Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 24 Oct 2023 18:45:40 -0700 Subject: [PATCH 135/149] manager: improve bitrate scaling --- go_vod/manager.go | 55 +++++++++++++++++++++++++++++++++++------------ main.go | 2 +- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/go_vod/manager.go b/go_vod/manager.go index 2e1aac0a..a147d790 100644 --- a/go_vod/manager.go +++ b/go_vod/manager.go @@ -60,20 +60,15 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, return nil, err } - // heuristic - if m.probe.CodecName != "h264" { - m.probe.BitRate *= 2 - } - m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.ChunkSize))) // Possible streams - m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 500000} - m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 854, bitrate: 1200000} - m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 2200000} - m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 3600000} - m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 6000000} - m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 10000000} + m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 300} + m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 854, bitrate: 400} + m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 700} + m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 1000} + m.streams["1440p"] = &Stream{c: c, m: m, quality: "1440p", height: 1440, width: 2560, bitrate: 1400} + m.streams["2160p"] = &Stream{c: c, m: m, quality: "2160p", height: 2160, width: 3840, bitrate: 3000} // height is our primary dimension for scaling // using the probed size, we adjust the width of the stream @@ -83,12 +78,44 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, smDim, lgDim = lgDim, smDim } + // Get the reference bitrate. This is the same as the current bitrate + // if the video is H.264, otherwise use double the current bitrate. + refBitrate := m.probe.BitRate + if m.probe.CodecName != CODEC_H264 { + refBitrate *= 2 + } + + // If bitrate could not be read, use 10Mbps + if refBitrate == 0 { + refBitrate = 10000000 + } + + // Get the multiplier for the reference bitrate. + // For this get the nearest stream size to the original. + origPixels := float64(m.probe.Height * m.probe.Width) + nearestPixels := float64(0) + nearestStream := "" + for key, stream := range m.streams { + streamPixels := float64(stream.height * stream.width) + if nearestPixels == 0 || math.Abs(origPixels-streamPixels) < math.Abs(origPixels-nearestPixels) { + nearestPixels = streamPixels + nearestStream = key + } + } + + // Get the bitrate multiplier. This is the ratio of the reference + // bitrate to the nearest stream bitrate, so we can scale all streams. + bitrateMultiplier := 1.0 + if nearestStream != "" { + bitrateMultiplier = float64(refBitrate) / float64(m.streams[nearestStream].bitrate) + } + // Only keep streams that are smaller than the video for k, stream := range m.streams { stream.order = 0 - // scale bitrate by frame rate with reference 30 - stream.bitrate = int(float64(stream.bitrate) * float64(m.probe.FrameRate) / 30.0) + // scale bitrate using the multiplier + stream.bitrate = int(math.Ceil(float64(stream.bitrate) * bitrateMultiplier)) // now store the width of the stream as the larger dimension stream.width = int(math.Ceil(float64(lgDim) * float64(stream.height) / float64(smDim))) @@ -110,7 +137,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, quality: QUALITY_MAX, height: m.probe.Height, width: m.probe.Width, - bitrate: m.probe.BitRate, + bitrate: refBitrate, order: 1, } diff --git a/main.go b/main.go index fa676c00..0232c5c6 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.22" +const VERSION = "0.1.23" func main() { // Build initial configuration From 0223d5f6efa590a65554b1bd66bebf595838f73f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 24 Oct 2023 19:06:55 -0700 Subject: [PATCH 136/149] stream: add genpts fflags --- go_vod/stream.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go_vod/stream.go b/go_vod/stream.go index 7a5b51c8..5b524315 100644 --- a/go_vod/stream.go +++ b/go_vod/stream.go @@ -385,6 +385,7 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { args = append(args, []string{ "-i", s.m.path, // Input file "-copyts", // So the "-to" refers to the original TS + "-fflags", "+genpts", }...) // Filters From 791937215d7291db50566ff14a98f56d49881ce2 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 24 Oct 2023 19:10:20 -0700 Subject: [PATCH 137/149] stream: escape printed command --- go_vod/stream.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/go_vod/stream.go b/go_vod/stream.go index 5b524315..57be0730 100644 --- a/go_vod/stream.go +++ b/go_vod/stream.go @@ -534,7 +534,18 @@ func (s *Stream) transcode(startId int) { // Start the process s.coder = exec.Command(s.c.FFmpeg, args...) - log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(s.coder.Args[:], " ")) + + // Log command, quoting the args as needed + quotedArgs := make([]string, len(s.coder.Args)) + invalidChars := strings.Join([]string{" ", "=", ":", "\"", "\\", "\n", "\t"}, "") + for i, arg := range s.coder.Args { + if strings.ContainsAny(arg, invalidChars) { + quotedArgs[i] = fmt.Sprintf("\"%s\"", arg) + } else { + quotedArgs[i] = arg + } + } + log.Printf("%s-%s: %s", s.m.id, s.quality, strings.Join(quotedArgs[:], " ")) cmdStdOut, err := s.coder.StdoutPipe() if err != nil { From c6e9cca896bb4611ddba5f7a5e9659ded488ea45 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 24 Oct 2023 19:35:26 -0700 Subject: [PATCH 138/149] 0.1.24 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 0232c5c6..a6a0e96e 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.23" +const VERSION = "0.1.24" func main() { // Build initial configuration From 10bc4362ba9407c0834198d50c51ffdc06e72121 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 25 Oct 2023 14:35:03 -0700 Subject: [PATCH 139/149] add dev dockerfile --- dev.Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 dev.Dockerfile diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 00000000..d3aececf --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,12 @@ +FROM golang:bullseye AS builder +WORKDIR /app +COPY . . +RUN CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" + +FROM linuxserver/ffmpeg:latest + +COPY --from=builder /app/go-vod . + +EXPOSE 47788 + +ENTRYPOINT ["/go-vod"] \ No newline at end of file From c743d14e5dd56c079c0e6c52ca47a8ad1ed34328 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 26 Oct 2023 21:56:22 -0700 Subject: [PATCH 140/149] Remove 360p --- go_vod/manager.go | 1 - 1 file changed, 1 deletion(-) diff --git a/go_vod/manager.go b/go_vod/manager.go index a147d790..948b6d54 100644 --- a/go_vod/manager.go +++ b/go_vod/manager.go @@ -63,7 +63,6 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, m.numChunks = int(math.Ceil(m.probe.Duration.Seconds() / float64(c.ChunkSize))) // Possible streams - m.streams["360p"] = &Stream{c: c, m: m, quality: "360p", height: 360, width: 640, bitrate: 300} m.streams["480p"] = &Stream{c: c, m: m, quality: "480p", height: 480, width: 854, bitrate: 400} m.streams["720p"] = &Stream{c: c, m: m, quality: "720p", height: 720, width: 1280, bitrate: 700} m.streams["1080p"] = &Stream{c: c, m: m, quality: "1080p", height: 1080, width: 1920, bitrate: 1000} From 3513b543b318024a24ec66a27276ee6205c099e1 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 26 Oct 2023 21:56:31 -0700 Subject: [PATCH 141/149] Reduce ref bitrate --- go_vod/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go_vod/manager.go b/go_vod/manager.go index 948b6d54..31e2b706 100644 --- a/go_vod/manager.go +++ b/go_vod/manager.go @@ -79,7 +79,7 @@ func NewManager(c *Config, path string, id string, close chan string) (*Manager, // Get the reference bitrate. This is the same as the current bitrate // if the video is H.264, otherwise use double the current bitrate. - refBitrate := m.probe.BitRate + refBitrate := int(float64(m.probe.BitRate) / 2.0) if m.probe.CodecName != CODEC_H264 { refBitrate *= 2 } From 61b217d08caa15cb6f1c1e9a067984e74d690d24 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 26 Oct 2023 23:18:41 -0700 Subject: [PATCH 142/149] stream: omit profile --- go_vod/stream.go | 1 - 1 file changed, 1 deletion(-) diff --git a/go_vod/stream.go b/go_vod/stream.go index 57be0730..4d88bea5 100644 --- a/go_vod/stream.go +++ b/go_vod/stream.go @@ -449,7 +449,6 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string { } args = append(args, []string{"-vf", filter}...) - args = append(args, []string{"-profile:v", "main"}...) } // Output specs for video From e2e6dafee4d6b9ecb2e509c6780cd03900455b5b Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 27 Oct 2023 03:00:51 -0700 Subject: [PATCH 143/149] 0.1.25 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index a6a0e96e..693717cb 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.24" +const VERSION = "0.1.25" func main() { // Build initial configuration From a40b1576bbe3f56b3cba86dcb4659569c258b83c Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 1 Nov 2023 00:15:28 -0700 Subject: [PATCH 144/149] Cross compile for ARM --- .circleci/config.yml | 48 -------------------- .github/workflows/{amd64.yml => release.yml} | 14 +++--- main.go | 2 +- 3 files changed, 9 insertions(+), 55 deletions(-) delete mode 100644 .circleci/config.yml rename .github/workflows/{amd64.yml => release.yml} (55%) diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c86359bb..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: 2.1 - -jobs: - build: - machine: - image: ubuntu-2004:current - resource_class: arm.medium - steps: - - checkout - - - run: | - docker run -it --rm -v "$PWD":/work -w /work golang:1.20-bullseye bash -c 'CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w"' - sudo mv go-vod go-vod-aarch64 - - - persist_to_workspace: - root: . - paths: - - go-vod-aarch64 - - publish-github-release: - docker: - - image: cimg/go:1.17 - steps: - - attach_workspace: - at: ./artifacts - - run: - name: "Publish Release on GitHub" - command: | - go get github.com/tcnksm/ghr - ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace ${CIRCLE_TAG} ./artifacts/ - -workflows: - aarch64: - jobs: - - build: - filters: - branches: - ignore: /.*/ - tags: - only: /^.*/ - - publish-github-release: - requires: - - build - filters: - branches: - ignore: /.*/ - tags: - only: /^.*/ diff --git a/.github/workflows/amd64.yml b/.github/workflows/release.yml similarity index 55% rename from .github/workflows/amd64.yml rename to .github/workflows/release.yml index 98dfe89a..fad0484c 100644 --- a/.github/workflows/amd64.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: amd64 +name: release on: push: @@ -6,8 +6,8 @@ on: - "*" jobs: - build-amd64: - name: amd64 + build: + name: Build runs-on: ubuntu-latest container: @@ -18,13 +18,15 @@ jobs: uses: actions/checkout@v3 - name: Build - run: CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" + run: | + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs=false -ldflags="-s -w" -o go-vod-amd64 + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -buildvcs=false -ldflags="-s -w" -o go-vod-arm64 - name: Upload to releases uses: svenstaro/upload-release-action@v2 id: attach_to_release with: - file: go-vod - asset_name: go-vod-amd64 + file: go-vod-* + file_glob: true tag: ${{ github.ref }} overwrite: true diff --git a/main.go b/main.go index 693717cb..02bae723 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.25" +const VERSION = "0.1.26" func main() { // Build initial configuration From bb3c44acbca30d6949a6378269099d95acaae955 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 1 Nov 2023 00:36:38 -0700 Subject: [PATCH 145/149] ci: combine workflows --- .github/workflows/docker-build.yml | 42 ------------------------------ .github/workflows/release.yml | 36 +++++++++++++++++++++++++ main.go | 2 +- 3 files changed, 37 insertions(+), 43 deletions(-) delete mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index e2e37ef9..00000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Docker Build and Publish - -on: - workflow_dispatch: - inputs: - tagName: - description: "Tag name" - required: true - default: 'v1' - -jobs: - push_to_registry: - runs-on: ubuntu-latest - - name: Build docker image and push to dockerhub - - steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build container image - uses: docker/build-push-action@v5 - with: - push: true - platforms: linux/amd64,linux/arm64 - context: './' - no-cache: true - file: 'Dockerfile' - tags: radialapps/go-vod:${{ github.event.inputs.tagName }} , radialapps/go-vod:latest - provenance: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fad0484c..917d3500 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,3 +30,39 @@ jobs: file_glob: true tag: ${{ github.ref }} overwrite: true + + docker: + runs-on: ubuntu-latest + + name: Build Docker image and push to Docker Hub + + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Get image label + id: image_label + run: echo "label=${GITHUB_REF#refs/tags/}" >> $GITHUB_STATE + + - name: Build container image + uses: docker/build-push-action@v5 + with: + push: true + platforms: linux/amd64,linux/arm64 + context: './' + no-cache: true + file: 'Dockerfile' + tags: radialapps/go-vod:${{ steps.image_label.outputs.label }} , radialapps/go-vod:latest + provenance: false diff --git a/main.go b/main.go index 02bae723..d6dfab38 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.26" +const VERSION = "0.1.27" func main() { // Build initial configuration From 11c519c654b6c07f28d819b56f7b1dfbb0067680 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 1 Nov 2023 00:41:15 -0700 Subject: [PATCH 146/149] Fix typo in pipeline --- .github/workflows/release.yml | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 917d3500..e7a4e821 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: - name: Get image label id: image_label - run: echo "label=${GITHUB_REF#refs/tags/}" >> $GITHUB_STATE + run: echo "label=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Build container image uses: docker/build-push-action@v5 diff --git a/main.go b/main.go index d6dfab38..194a782b 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/pulsejet/go-vod/go_vod" ) -const VERSION = "0.1.27" +const VERSION = "0.1.28" func main() { // Build initial configuration From ddc265b01982c6e10681ae92c22855d5c8368023 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 1 Nov 2023 00:43:06 -0700 Subject: [PATCH 147/149] Rename workflows --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7a4e821..b2ac0149 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,8 @@ on: - "*" jobs: - build: - name: Build + binary: + name: Binary build runs-on: ubuntu-latest container: @@ -34,7 +34,7 @@ jobs: docker: runs-on: ubuntu-latest - name: Build Docker image and push to Docker Hub + name: Docker build steps: - name: Check out the repo From 2f8019cb5a83dffe1a5df7fce99f63764f97b15e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 1 Nov 2023 00:45:33 -0700 Subject: [PATCH 148/149] Rename workflows (again) --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2ac0149..54dd8f0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: binary: - name: Binary build + name: Binary runs-on: ubuntu-latest container: @@ -34,7 +34,7 @@ jobs: docker: runs-on: ubuntu-latest - name: Docker build + name: Docker steps: - name: Check out the repo From a37c11daf8c4aa207186c4ef030d8a35f251d433 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 1 Nov 2023 01:03:28 -0700 Subject: [PATCH 149/149] refactor: rename package to transcoder --- main.go | 6 +++--- {go_vod => transcoder}/chunk.go | 2 +- {go_vod => transcoder}/config.go | 2 +- {go_vod => transcoder}/handler.go | 2 +- {go_vod => transcoder}/manager.go | 2 +- {go_vod => transcoder}/stream.go | 2 +- {go_vod => transcoder}/temp.go | 2 +- {go_vod => transcoder}/util.go | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) rename {go_vod => transcoder}/chunk.go (90%) rename {go_vod => transcoder}/config.go (99%) rename {go_vod => transcoder}/handler.go (99%) rename {go_vod => transcoder}/manager.go (99%) rename {go_vod => transcoder}/stream.go (99%) rename {go_vod => transcoder}/temp.go (98%) rename {go_vod => transcoder}/util.go (93%) diff --git a/main.go b/main.go index 194a782b..d84db95c 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,14 @@ import ( "log" "os" - "github.com/pulsejet/go-vod/go_vod" + "github.com/pulsejet/go-vod/transcoder" ) const VERSION = "0.1.28" func main() { // Build initial configuration - c := &go_vod.Config{ + c := &transcoder.Config{ VersionMonitor: false, Version: VERSION, Bind: ":47788", @@ -40,7 +40,7 @@ func main() { c.AutoDetect() // Start server - code := go_vod.NewHandler(c).Start() + code := transcoder.NewHandler(c).Start() // Exit log.Println("Exiting go-vod with status code", code) diff --git a/go_vod/chunk.go b/transcoder/chunk.go similarity index 90% rename from go_vod/chunk.go rename to transcoder/chunk.go index 7cb13fa6..77018491 100644 --- a/go_vod/chunk.go +++ b/transcoder/chunk.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder type Chunk struct { id int diff --git a/go_vod/config.go b/transcoder/config.go similarity index 99% rename from go_vod/config.go rename to transcoder/config.go index 9514da68..526ed890 100644 --- a/go_vod/config.go +++ b/transcoder/config.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder import ( "encoding/json" diff --git a/go_vod/handler.go b/transcoder/handler.go similarity index 99% rename from go_vod/handler.go rename to transcoder/handler.go index c3c8baf4..8c5bb085 100644 --- a/go_vod/handler.go +++ b/transcoder/handler.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder import ( "context" diff --git a/go_vod/manager.go b/transcoder/manager.go similarity index 99% rename from go_vod/manager.go rename to transcoder/manager.go index 31e2b706..7e0e2a60 100644 --- a/go_vod/manager.go +++ b/transcoder/manager.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder import ( "bytes" diff --git a/go_vod/stream.go b/transcoder/stream.go similarity index 99% rename from go_vod/stream.go rename to transcoder/stream.go index 4d88bea5..721f071d 100644 --- a/go_vod/stream.go +++ b/transcoder/stream.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder import ( "bufio" diff --git a/go_vod/temp.go b/transcoder/temp.go similarity index 98% rename from go_vod/temp.go rename to transcoder/temp.go index 43eac11d..08c53733 100644 --- a/go_vod/temp.go +++ b/transcoder/temp.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder import ( "encoding/json" diff --git a/go_vod/util.go b/transcoder/util.go similarity index 93% rename from go_vod/util.go rename to transcoder/util.go index 4e52606f..c2d7940f 100644 --- a/go_vod/util.go +++ b/transcoder/util.go @@ -1,4 +1,4 @@ -package go_vod +package transcoder import "net/http"