diff --git a/transcoder/main.go b/transcoder/main.go index 8d462348..63db25de 100644 --- a/transcoder/main.go +++ b/transcoder/main.go @@ -48,11 +48,11 @@ func (h *Handler) GetOffline(c echo.Context) error { return err } - ret, err := h.downloader.GetOffline(path, quality) + ret, path, err := h.downloader.GetOffline(path, quality) if err != nil { return err } - return c.String(http.StatusOK, ret) + return ServeOfflineFile(path, ret, c) } // Get master playlist diff --git a/transcoder/src/downloader.go b/transcoder/src/downloader.go index 4cb9e5f1..6c854e3c 100644 --- a/transcoder/src/downloader.go +++ b/transcoder/src/downloader.go @@ -1,11 +1,94 @@ package src -type Downloader struct{} +import ( + "fmt" + "log" + "os/exec" + "sync" +) + +type Key struct { + path string + quality Quality +} + +type Value struct { + done chan struct{} + path string +} + +type Downloader struct { + processing map[Key]Value + lock sync.Mutex +} func NewDownloader() *Downloader { - return nil + return &Downloader{ + processing: make(map[Key]Value), + } } -func (d *Downloader) GetOffline(path string, quality Quality) (string, error) { - return "", nil +func (d *Downloader) GetOffline(path string, quality Quality) (<-chan struct{}, string, error) { + key := Key{path, quality} + d.lock.Lock() + defer d.lock.Unlock() + existing, ok := d.processing[key] + + if ok { + return existing.done, existing.path, nil + } + + info, err := GetInfo(path) + if err != nil { + return nil, "", err + } + outpath := fmt.Sprintf("%s/dl-%s-%s.mkv", GetOutPath(), info.Sha, quality) + + ret := make(chan struct{}) + d.processing[key] = Value{ret, outpath} + + go func() { + cmd := exec.Command( + "ffmpeg", + "-nostats", "-hide_banner", "-loglevel", "warning", + "-i", path, + ) + cmd.Args = append(cmd.Args, quality.getTranscodeArgs(nil)...) + // TODO: add custom audio settings depending on quality + cmd.Args = append(cmd.Args, + "-map", "0:a?", + "-c:a", "aac", + "-ac", "2", + "-b:a", "128k", + ) + // also include subtitles, font attachments and chapters. + cmd.Args = append(cmd.Args, + "-map", "0:s?", "-c:s", "copy", + "-map", "0:d?", + "-map", "0:t?", + ) + cmd.Args = append(cmd.Args, outpath) + + log.Printf( + "Starting offline transcode (quality %s) of %s with the command: %s", + quality, + path, + cmd, + ) + cmd.Stdout = nil + err := cmd.Run() + if err != nil { + log.Println("Error starting ffmpeg extract:", err) + // TODO: find a way to inform listeners that there was an error + + d.lock.Lock() + delete(d.processing, key) + d.lock.Unlock() + } else { + log.Println("Transcode finished") + } + + close(ret) + }() + return ret, outpath, nil } diff --git a/transcoder/src/extract.go b/transcoder/src/extract.go index 41878069..f73f4e5a 100644 --- a/transcoder/src/extract.go +++ b/transcoder/src/extract.go @@ -57,6 +57,7 @@ func (e *Extractor) RunExtractor(path string, sha string, subs *[]Subtitle) <-ch fmt.Printf("Extract subs and fonts for %s", path) cmd := exec.Command( "ffmpeg", + "-nostats", "-hide_banner", "-loglevel", "warning", "-dump_attachment:t", "", "-i", path, ) diff --git a/transcoder/src/stream.go b/transcoder/src/stream.go index ebd31a06..bc7baf0e 100644 --- a/transcoder/src/stream.go +++ b/transcoder/src/stream.go @@ -111,7 +111,7 @@ func (ts *Stream) run(start int32) error { "-copyts", } args = append(args, ts.handle.getTranscodeArgs(segments_str)...) - args = append(args, []string{ + args = append(args, "-f", "segment", "-segment_time_delta", "0.2", "-segment_format", "mpegts", @@ -120,7 +120,7 @@ func (ts *Stream) run(start int32) error { "-segment_list_type", "flat", "-segment_list", "pipe:1", outpath, - }...) + ) cmd := exec.Command("ffmpeg", args...) log.Printf("Running %s", strings.Join(cmd.Args, " ")) diff --git a/transcoder/src/videostream.go b/transcoder/src/videostream.go index e692cb84..691206fa 100644 --- a/transcoder/src/videostream.go +++ b/transcoder/src/videostream.go @@ -23,22 +23,32 @@ func (vs *VideoStream) getOutPath() string { } func (vs *VideoStream) getTranscodeArgs(segments string) []string { - if vs.quality == Original { + return vs.quality.getTranscodeArgs(&segments) +} + +func (quality Quality) getTranscodeArgs(segments *string) []string { + if quality == Original { return []string{"-map", "0:V:0", "-c:v", "copy"} } - return []string{ + ret := []string{ // superfast or ultrafast would produce a file extremly big so we prever veryfast or faster. "-map", "0:V:0", "-c:v", "libx264", "-crf", "21", "-preset", "faster", // resize but keep aspect ratio (also force a width that is a multiple of two else some apps behave badly. - "-vf", fmt.Sprintf("scale=-2:'min(%d,ih)'", vs.quality.Height()), + "-vf", fmt.Sprintf("scale=-2:'min(%d,ih)'", quality.Height()), // Even less sure but bufsize are 5x the avergae bitrate since the average bitrate is only // useful for hls segments. - "-bufsize", fmt.Sprint(vs.quality.MaxBitrate() * 5), - "-b:v", fmt.Sprint(vs.quality.AverageBitrate()), - "-maxrate", fmt.Sprint(vs.quality.MaxBitrate()), - // Force segments to be split exactly on keyframes (only works when transcoding) - "-force_key_frames", segments, + "-bufsize", fmt.Sprint(quality.MaxBitrate() * 5), + "-b:v", fmt.Sprint(quality.AverageBitrate()), + "-maxrate", fmt.Sprint(quality.MaxBitrate()), "-strict", "-2", } + if segments != nil { + ret = append(ret, + // Force segments to be split exactly on keyframes (only works when transcoding) + "-force_key_frames", *segments, + ) + } + + return ret } diff --git a/transcoder/utils.go b/transcoder/utils.go index bc3f3901..a0ae8e72 100644 --- a/transcoder/utils.go +++ b/transcoder/utils.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "log" "net/http" "os" "strings" @@ -94,3 +96,66 @@ func ErrorHandler(err error, c echo.Context) { Errors []string `json:"errors"` }{Errors: []string{message}}) } + +func ServeOfflineFile(path string, done <-chan struct{}, c echo.Context) error { + select { + case <-done: + // if the transcode is already finished, no need to do anything, just return the file + return c.File(path) + default: + break + } + + var f *os.File + var err error + for { + // wait for the file to be created by the transcoder. + f, err = os.Open(path) + if err == nil { + break + } + time.Sleep(2 * time.Second) + } + defer f.Close() + + // Offline transcoding always return a mkv and video/webm allow some browser to play a mkv video + c.Response().Header().Set(echo.HeaderContentType, "video/webm") + c.Response().Header().Set("Trailer", echo.HeaderContentLength) + c.Response().WriteHeader(http.StatusOK) + + buffer := make([]byte, 1024) + not_done := true + + for not_done { + select { + case <-done: + info, err := f.Stat() + if err != nil { + log.Printf("Stats error %s", err) + return err + } + c.Response().Header().Set(echo.HeaderContentLength, fmt.Sprint(info.Size())) + c.Response().WriteHeader(http.StatusOK) + not_done = false + case <-time.After(5 * time.Second): + } + read: + for { + size, err := f.Read(buffer) + if size == 0 && err == io.EOF { + break read + } + if err != nil && err != io.EOF { + return err + } + + _, err = c.Response().Writer.Write(buffer[:size]) + if err != nil { + log.Printf("Could not write transcoded file to response.") + return nil + } + } + c.Response().Flush() + } + return nil +}