mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 05:34:23 -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
|
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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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, " "))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user