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
}
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

View File

@ -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
}

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)
cmd := exec.Command(
"ffmpeg",
"-nostats", "-hide_banner", "-loglevel", "warning",
"-dump_attachment:t", "",
"-i", path,
)

View File

@ -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, " "))

View File

@ -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
}

View File

@ -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
}