Add offline route

This commit is contained in:
Zoe Roux 2024-01-19 17:42:27 +01:00
parent c0f6b5a85f
commit 2551d5071b
No known key found for this signature in database
6 changed files with 175 additions and 16 deletions

View File

@ -48,11 +48,11 @@ func (h *Handler) GetOffline(c echo.Context) error {
return err return err
} }
ret, err := h.downloader.GetOffline(path, quality) ret, path, err := h.downloader.GetOffline(path, quality)
if err != nil { if err != nil {
return err return err
} }
return c.String(http.StatusOK, ret) return ServeOfflineFile(path, ret, c)
} }
// Get master playlist // Get master playlist

View File

@ -1,11 +1,94 @@
package src 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 { func NewDownloader() *Downloader {
return nil return &Downloader{
processing: make(map[Key]Value),
}
} }
func (d *Downloader) GetOffline(path string, quality Quality) (string, error) { func (d *Downloader) GetOffline(path string, quality Quality) (<-chan struct{}, string, error) {
return "", nil 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
} }

View File

@ -57,6 +57,7 @@ func (e *Extractor) RunExtractor(path string, sha string, subs *[]Subtitle) <-ch
fmt.Printf("Extract subs and fonts for %s", path) fmt.Printf("Extract subs and fonts for %s", path)
cmd := exec.Command( cmd := exec.Command(
"ffmpeg", "ffmpeg",
"-nostats", "-hide_banner", "-loglevel", "warning",
"-dump_attachment:t", "", "-dump_attachment:t", "",
"-i", path, "-i", path,
) )

View File

@ -111,7 +111,7 @@ func (ts *Stream) run(start int32) error {
"-copyts", "-copyts",
} }
args = append(args, ts.handle.getTranscodeArgs(segments_str)...) args = append(args, ts.handle.getTranscodeArgs(segments_str)...)
args = append(args, []string{ args = append(args,
"-f", "segment", "-f", "segment",
"-segment_time_delta", "0.2", "-segment_time_delta", "0.2",
"-segment_format", "mpegts", "-segment_format", "mpegts",
@ -120,7 +120,7 @@ func (ts *Stream) run(start int32) error {
"-segment_list_type", "flat", "-segment_list_type", "flat",
"-segment_list", "pipe:1", "-segment_list", "pipe:1",
outpath, outpath,
}...) )
cmd := exec.Command("ffmpeg", args...) cmd := exec.Command("ffmpeg", args...)
log.Printf("Running %s", strings.Join(cmd.Args, " ")) log.Printf("Running %s", strings.Join(cmd.Args, " "))

View File

@ -23,22 +23,32 @@ func (vs *VideoStream) getOutPath() string {
} }
func (vs *VideoStream) getTranscodeArgs(segments string) []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{"-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. // 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", "-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. // 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 // Even less sure but bufsize are 5x the avergae bitrate since the average bitrate is only
// useful for hls segments. // useful for hls segments.
"-bufsize", fmt.Sprint(vs.quality.MaxBitrate() * 5), "-bufsize", fmt.Sprint(quality.MaxBitrate() * 5),
"-b:v", fmt.Sprint(vs.quality.AverageBitrate()), "-b:v", fmt.Sprint(quality.AverageBitrate()),
"-maxrate", fmt.Sprint(vs.quality.MaxBitrate()), "-maxrate", fmt.Sprint(quality.MaxBitrate()),
// Force segments to be split exactly on keyframes (only works when transcoding)
"-force_key_frames", segments,
"-strict", "-2", "-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
} }

View File

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@ -94,3 +96,66 @@ func ErrorHandler(err error, c echo.Context) {
Errors []string `json:"errors"` Errors []string `json:"errors"`
}{Errors: []string{message}}) }{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
}