2020-02-03 17:54:59 +01:00
|
|
|
package engine
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-02-19 20:53:35 +01:00
|
|
|
"fmt"
|
2020-02-03 17:54:59 +01:00
|
|
|
"io"
|
2020-02-24 19:31:05 +01:00
|
|
|
"mime"
|
2020-02-03 17:54:59 +01:00
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/deluan/navidrome/conf"
|
2020-02-21 15:36:29 +01:00
|
|
|
"github.com/deluan/navidrome/consts"
|
2020-02-25 16:01:39 +01:00
|
|
|
"github.com/deluan/navidrome/engine/transcoder"
|
2020-02-03 17:54:59 +01:00
|
|
|
"github.com/deluan/navidrome/log"
|
|
|
|
"github.com/deluan/navidrome/model"
|
2020-05-13 22:49:55 +02:00
|
|
|
"github.com/deluan/navidrome/model/request"
|
2020-02-24 19:31:05 +01:00
|
|
|
"github.com/djherbis/fscache"
|
2020-02-03 17:54:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type MediaStreamer interface {
|
2020-03-16 19:28:13 +01:00
|
|
|
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-04-05 04:23:20 +02:00
|
|
|
type TranscodingCache fscache.Cache
|
|
|
|
|
|
|
|
func NewMediaStreamer(ds model.DataStore, ffm transcoder.Transcoder, cache TranscodingCache) MediaStreamer {
|
2020-02-21 01:08:10 +01:00
|
|
|
return &mediaStreamer{ds: ds, ffm: ffm, cache: cache}
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type mediaStreamer struct {
|
2020-02-21 01:08:10 +01:00
|
|
|
ds model.DataStore
|
2020-02-25 16:01:39 +01:00
|
|
|
ffm transcoder.Transcoder
|
2020-02-21 01:08:10 +01:00
|
|
|
cache fscache.Cache
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-03-16 19:28:13 +01:00
|
|
|
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error) {
|
2020-02-24 19:31:05 +01:00
|
|
|
mf, err := ms.ds.MediaFile(ctx).Get(id)
|
2020-02-19 20:53:35 +01:00
|
|
|
if err != nil {
|
2020-02-24 19:31:05 +01:00
|
|
|
return nil, err
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-04-06 06:26:51 +02:00
|
|
|
var format string
|
|
|
|
var bitRate int
|
2020-05-22 03:26:48 +02:00
|
|
|
var cached bool
|
2020-04-06 06:26:51 +02:00
|
|
|
defer func() {
|
2020-05-22 03:26:48 +02:00
|
|
|
log.Info("Streaming file", "title", mf.Title, "artist", mf.Artist, "format", format, "cached", cached,
|
|
|
|
"bitRate", bitRate, "user", userName(ctx), "transcoding", format != "raw",
|
|
|
|
"originalFormat", mf.Suffix, "originalBitRate", mf.BitRate)
|
2020-04-06 06:26:51 +02:00
|
|
|
}()
|
|
|
|
|
|
|
|
format, bitRate = selectTranscodingOptions(ctx, ms.ds, mf, reqFormat, reqBitRate)
|
2020-03-16 19:28:13 +01:00
|
|
|
log.Trace(ctx, "Selected transcoding options",
|
|
|
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
|
|
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
|
|
|
|
"selectedBitrate", bitRate, "selectedFormat", format,
|
|
|
|
)
|
2020-02-24 19:31:05 +01:00
|
|
|
s := &Stream{ctx: ctx, mf: mf, format: format, bitRate: bitRate}
|
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
if format == "raw" {
|
2020-02-24 19:31:05 +01:00
|
|
|
log.Debug(ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
|
2020-03-16 19:28:13 +01:00
|
|
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
2020-02-03 17:54:59 +01:00
|
|
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
2020-02-24 19:31:05 +01:00
|
|
|
f, err := os.Open(mf.Path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
s.Reader = f
|
|
|
|
s.Closer = f
|
|
|
|
s.Seeker = f
|
|
|
|
s.format = mf.Suffix
|
|
|
|
return s, nil
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
key := cacheKey(id, bitRate, format)
|
|
|
|
r, w, err := ms.cache.Get(key)
|
2020-02-19 20:53:35 +01:00
|
|
|
if err != nil {
|
2020-02-24 19:31:05 +01:00
|
|
|
log.Error(ctx, "Error creating stream caching buffer", "id", mf.ID, err)
|
|
|
|
return nil, err
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-21 01:08:10 +01:00
|
|
|
|
2020-05-22 03:26:48 +02:00
|
|
|
cached = w == nil
|
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
// If this is a brand new transcoding request, not in the cache, start transcoding
|
2020-05-22 03:26:48 +02:00
|
|
|
if !cached {
|
2020-02-24 19:31:05 +01:00
|
|
|
log.Trace(ctx, "Cache miss. Starting new transcoding session", "id", mf.ID)
|
2020-03-16 19:28:13 +01:00
|
|
|
t, err := ms.ds.Transcoding(ctx).FindByFormat(format)
|
|
|
|
if err != nil {
|
|
|
|
log.Error(ctx, "Error loading transcoding command", "format", format, err)
|
|
|
|
return nil, os.ErrInvalid
|
|
|
|
}
|
2020-04-10 18:00:25 +02:00
|
|
|
out, err := ms.ffm.Start(ctx, t.Command, mf.Path, bitRate)
|
2020-02-21 01:08:10 +01:00
|
|
|
if err != nil {
|
2020-02-24 19:31:05 +01:00
|
|
|
log.Error(ctx, "Error starting transcoder", "id", mf.ID, err)
|
2020-02-21 01:08:10 +01:00
|
|
|
return nil, os.ErrInvalid
|
|
|
|
}
|
2020-03-06 22:28:20 +01:00
|
|
|
go copyAndClose(ctx, w, out)
|
2020-02-24 19:31:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// If it is in the cache, check if the stream is done being written. If so, return a ReaderSeeker
|
2020-05-22 03:26:48 +02:00
|
|
|
if cached {
|
2020-02-24 19:31:05 +01:00
|
|
|
size := getFinalCachedSize(r)
|
|
|
|
if size > 0 {
|
|
|
|
log.Debug(ctx, "Streaming cached file", "id", mf.ID, "path", mf.Path,
|
2020-03-16 19:28:13 +01:00
|
|
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
2020-02-24 19:31:05 +01:00
|
|
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix, "size", size)
|
|
|
|
sr := io.NewSectionReader(r, 0, size)
|
|
|
|
s.Reader = sr
|
|
|
|
s.Closer = r
|
|
|
|
s.Seeker = sr
|
|
|
|
s.format = format
|
|
|
|
return s, nil
|
|
|
|
}
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
log.Debug(ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
|
2020-03-16 19:28:13 +01:00
|
|
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
2020-02-24 19:31:05 +01:00
|
|
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
|
|
|
// All other cases, just return a ReadCloser, without Seek capabilities
|
|
|
|
s.Reader = r
|
|
|
|
s.Closer = r
|
|
|
|
s.format = format
|
|
|
|
return s, nil
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-03-06 22:28:20 +01:00
|
|
|
func copyAndClose(ctx context.Context, w io.WriteCloser, r io.ReadCloser) {
|
|
|
|
_, err := io.Copy(w, r)
|
|
|
|
if err != nil {
|
|
|
|
log.Error(ctx, "Error copying data to cache", err)
|
|
|
|
}
|
|
|
|
err = r.Close()
|
|
|
|
if err != nil {
|
|
|
|
log.Error(ctx, "Error closing transcode output", err)
|
|
|
|
}
|
|
|
|
err = w.Close()
|
|
|
|
if err != nil {
|
|
|
|
log.Error(ctx, "Error closing cache", err)
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
type Stream struct {
|
2020-02-21 01:08:10 +01:00
|
|
|
ctx context.Context
|
2020-02-19 20:53:35 +01:00
|
|
|
mf *model.MediaFile
|
|
|
|
bitRate int
|
2020-02-24 19:31:05 +01:00
|
|
|
format string
|
|
|
|
io.Reader
|
|
|
|
io.Closer
|
|
|
|
io.Seeker
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
func (s *Stream) Seekable() bool { return s.Seeker != nil }
|
|
|
|
func (s *Stream) Duration() float32 { return s.mf.Duration }
|
|
|
|
func (s *Stream) ContentType() string { return mime.TypeByExtension("." + s.format) }
|
2020-04-03 06:24:40 +02:00
|
|
|
func (s *Stream) Name() string { return s.mf.Title + "." + s.format }
|
2020-02-24 19:31:05 +01:00
|
|
|
func (s *Stream) ModTime() time.Time { return s.mf.UpdatedAt }
|
2020-04-03 06:24:40 +02:00
|
|
|
func (s *Stream) EstimatedContentLength() int {
|
|
|
|
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
|
|
|
|
}
|
2020-02-10 04:09:18 +01:00
|
|
|
|
2020-03-16 19:28:13 +01:00
|
|
|
// TODO This function deserves some love (refactoring)
|
|
|
|
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
|
|
|
|
format = "raw"
|
|
|
|
if reqFormat == "raw" {
|
|
|
|
return
|
|
|
|
}
|
2020-04-03 04:17:52 +02:00
|
|
|
if reqFormat == mf.Suffix && reqBitRate == 0 {
|
|
|
|
bitRate = mf.BitRate
|
|
|
|
return
|
|
|
|
}
|
2020-05-13 22:49:55 +02:00
|
|
|
trc, hasDefault := request.TranscodingFrom(ctx)
|
2020-03-16 19:28:13 +01:00
|
|
|
var cFormat string
|
|
|
|
var cBitRate int
|
|
|
|
if reqFormat != "" {
|
|
|
|
cFormat = reqFormat
|
2020-02-24 19:31:05 +01:00
|
|
|
} else {
|
2020-03-16 19:28:13 +01:00
|
|
|
if hasDefault {
|
|
|
|
cFormat = trc.TargetFormat
|
|
|
|
cBitRate = trc.DefaultBitRate
|
2020-05-13 22:49:55 +02:00
|
|
|
if p, ok := request.PlayerFrom(ctx); ok {
|
2020-03-16 19:28:13 +01:00
|
|
|
cBitRate = p.MaxBitRate
|
|
|
|
}
|
2020-02-24 19:31:05 +01:00
|
|
|
}
|
|
|
|
}
|
2020-03-16 19:28:13 +01:00
|
|
|
if reqBitRate > 0 {
|
|
|
|
cBitRate = reqBitRate
|
2020-02-24 19:31:05 +01:00
|
|
|
}
|
2020-03-16 19:28:13 +01:00
|
|
|
if cBitRate == 0 && cFormat == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
|
|
|
|
if err == nil {
|
|
|
|
format = t.TargetFormat
|
|
|
|
if cBitRate != 0 {
|
|
|
|
bitRate = cBitRate
|
|
|
|
} else {
|
|
|
|
bitRate = t.DefaultBitRate
|
|
|
|
}
|
|
|
|
}
|
2020-04-09 01:10:55 +02:00
|
|
|
if format == mf.Suffix && bitRate >= mf.BitRate {
|
2020-03-16 19:28:13 +01:00
|
|
|
format = "raw"
|
|
|
|
bitRate = 0
|
2020-02-24 19:31:05 +01:00
|
|
|
}
|
2020-03-16 19:28:13 +01:00
|
|
|
return
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
func cacheKey(id string, bitRate int, format string) string {
|
|
|
|
return fmt.Sprintf("%s.%d.%s", id, bitRate, format)
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
2020-02-21 01:08:10 +01:00
|
|
|
|
2020-02-24 19:31:05 +01:00
|
|
|
func getFinalCachedSize(r fscache.ReadAtCloser) int64 {
|
|
|
|
cr, ok := r.(*fscache.CacheReader)
|
|
|
|
if ok {
|
|
|
|
size, final, err := cr.Size()
|
|
|
|
if final && err == nil {
|
|
|
|
return size
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|
2020-02-21 15:36:29 +01:00
|
|
|
|
2020-04-05 04:23:20 +02:00
|
|
|
func NewTranscodingCache() (TranscodingCache, error) {
|
2020-04-15 01:15:47 +02:00
|
|
|
return newFileCache("Transcoding", conf.Server.TranscodingCacheSize, consts.TranscodingCacheDir, consts.DefaultTranscodingCacheMaxItems)
|
2020-02-21 15:36:29 +01:00
|
|
|
}
|