mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add offline route
This commit is contained in:
parent
c0f6b5a85f
commit
2551d5071b
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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, " "))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user