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-19 20:53:35 +01:00
|
|
|
"net/http"
|
2020-02-03 17:54:59 +01:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/deluan/navidrome/conf"
|
2020-02-19 20:53:35 +01:00
|
|
|
"github.com/deluan/navidrome/engine/ffmpeg"
|
2020-02-03 17:54:59 +01:00
|
|
|
"github.com/deluan/navidrome/log"
|
|
|
|
"github.com/deluan/navidrome/model"
|
|
|
|
"github.com/deluan/navidrome/utils"
|
2020-02-21 01:08:10 +01:00
|
|
|
"gopkg.in/djherbis/fscache.v0"
|
2020-02-03 17:54:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type MediaStreamer interface {
|
2020-02-19 20:53:35 +01:00
|
|
|
NewFileSystem(ctx context.Context, maxBitRate int, format string) (http.FileSystem, error)
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
func NewMediaStreamer(ds model.DataStore, ffm ffmpeg.FFmpeg, cache fscache.Cache) MediaStreamer {
|
|
|
|
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
|
|
|
|
ffm ffmpeg.FFmpeg
|
|
|
|
cache fscache.Cache
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
func (ms *mediaStreamer) NewFileSystem(ctx context.Context, maxBitRate int, format string) (http.FileSystem, error) {
|
2020-02-21 01:08:10 +01:00
|
|
|
return &mediaFileSystem{ctx: ctx, ds: ms.ds, ffm: ms.ffm, cache: ms.cache, maxBitRate: maxBitRate, format: format}, nil
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
type mediaFileSystem struct {
|
2020-02-21 01:08:10 +01:00
|
|
|
ctx context.Context
|
|
|
|
ds model.DataStore
|
|
|
|
maxBitRate int
|
|
|
|
format string
|
|
|
|
ffm ffmpeg.FFmpeg
|
|
|
|
cache fscache.Cache
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (fs *mediaFileSystem) selectTranscodingOptions(mf *model.MediaFile) (string, int) {
|
2020-02-03 17:54:59 +01:00
|
|
|
var bitRate int
|
2020-02-19 20:53:35 +01:00
|
|
|
var format string
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
if fs.format == "raw" || !conf.Server.EnableDownsampling {
|
|
|
|
return "raw", bitRate
|
2020-02-03 17:54:59 +01:00
|
|
|
} else {
|
2020-02-19 20:53:35 +01:00
|
|
|
if fs.maxBitRate == 0 {
|
2020-02-03 17:54:59 +01:00
|
|
|
bitRate = mf.BitRate
|
|
|
|
} else {
|
2020-02-19 20:53:35 +01:00
|
|
|
bitRate = utils.MinInt(mf.BitRate, fs.maxBitRate)
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
2020-02-19 20:53:35 +01:00
|
|
|
format = "mp3" //mf.Suffix
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
if conf.Server.MaxBitRate != 0 {
|
|
|
|
bitRate = utils.MinInt(bitRate, conf.Server.MaxBitRate)
|
|
|
|
}
|
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
if bitRate == mf.BitRate {
|
|
|
|
return "raw", bitRate
|
|
|
|
}
|
|
|
|
return format, bitRate
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fs *mediaFileSystem) Open(name string) (http.File, error) {
|
|
|
|
id := strings.Trim(name, "/")
|
|
|
|
mf, err := fs.ds.MediaFile(fs.ctx).Get(id)
|
|
|
|
if err == model.ErrNotFound {
|
|
|
|
return nil, os.ErrNotExist
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Error opening mediaFile", "id", id, err)
|
|
|
|
return nil, os.ErrInvalid
|
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
format, bitRate := fs.selectTranscodingOptions(mf)
|
|
|
|
if format == "raw" {
|
|
|
|
log.Debug(fs.ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
|
|
|
|
"requestBitrate", bitRate, "requestFormat", format,
|
2020-02-03 17:54:59 +01:00
|
|
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
2020-02-19 20:53:35 +01:00
|
|
|
return os.Open(mf.Path)
|
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-19 20:53:35 +01:00
|
|
|
log.Debug(fs.ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
|
2020-02-03 17:54:59 +01:00
|
|
|
"requestBitrate", bitRate, "requestFormat", format,
|
|
|
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
return fs.transcodeFile(mf, bitRate, format)
|
2020-02-10 04:09:18 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
func (fs *mediaFileSystem) transcodeFile(mf *model.MediaFile, bitRate int, format string) (*transcodingFile, error) {
|
|
|
|
key := fmt.Sprintf("%s.%d.%s", mf.ID, bitRate, format)
|
|
|
|
r, w, err := fs.cache.Get(key)
|
2020-02-19 20:53:35 +01:00
|
|
|
if err != nil {
|
2020-02-21 01:08:10 +01:00
|
|
|
log.Error("Error creating stream caching buffer", "id", mf.ID, err)
|
2020-02-19 20:53:35 +01:00
|
|
|
return nil, os.ErrInvalid
|
|
|
|
}
|
2020-02-21 01:08:10 +01:00
|
|
|
|
|
|
|
// If it is a new file (not found in the cached), start a new transcoding session
|
|
|
|
if w != nil {
|
|
|
|
log.Debug("File not found in cache. Starting new transcoding session", "id", mf.ID)
|
|
|
|
out, err := fs.ffm.StartTranscoding(fs.ctx, mf.Path, bitRate, format)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Error starting transcoder", "id", mf.ID, err)
|
|
|
|
return nil, os.ErrInvalid
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
io.Copy(w, out)
|
|
|
|
out.Close()
|
|
|
|
w.Close()
|
|
|
|
}()
|
|
|
|
} else {
|
|
|
|
log.Debug("Reading transcoded file from cache", "id", mf.ID)
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
return newTranscodingFile(fs.ctx, r, mf, bitRate), nil
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
// transcodingFile Implements http.File interface, required for the FileSystem. It needs a Closer, a Reader and
|
|
|
|
// a Seeker for the same stream. Because the fscache package only provides a ReaderAtCloser (without the Seek()
|
|
|
|
// method), we wrap that reader with a SectionReader, which provides a Seek(). But we still need the original
|
|
|
|
// reader, as we need to close the stream when the transfer is complete
|
|
|
|
func newTranscodingFile(ctx context.Context, reader fscache.ReadAtCloser,
|
|
|
|
mf *model.MediaFile, bitRate int) *transcodingFile {
|
2020-02-03 17:54:59 +01:00
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
size := int64(mf.Duration*float32(bitRate*1000)) / 8
|
|
|
|
return &transcodingFile{
|
|
|
|
ctx: ctx,
|
|
|
|
mf: mf,
|
|
|
|
bitRate: bitRate,
|
|
|
|
size: size,
|
|
|
|
closer: reader,
|
|
|
|
ReadSeeker: io.NewSectionReader(reader, 0, size),
|
2020-02-19 20:53:35 +01:00
|
|
|
}
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
type transcodingFile struct {
|
|
|
|
ctx context.Context
|
2020-02-19 20:53:35 +01:00
|
|
|
mf *model.MediaFile
|
|
|
|
bitRate int
|
2020-02-21 01:08:10 +01:00
|
|
|
size int64
|
|
|
|
closer io.Closer
|
|
|
|
io.ReadSeeker
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
func (tf *transcodingFile) Stat() (os.FileInfo, error) {
|
|
|
|
return &streamHandlerFileInfo{f: tf}, nil
|
|
|
|
}
|
2020-02-10 04:09:18 +01:00
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
func (tf *transcodingFile) Close() error {
|
|
|
|
return tf.closer.Close()
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
func (tf *transcodingFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
|
|
return nil, nil
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:08:10 +01:00
|
|
|
type streamHandlerFileInfo struct {
|
|
|
|
f *transcodingFile
|
2020-02-03 17:54:59 +01:00
|
|
|
}
|
2020-02-21 01:08:10 +01:00
|
|
|
|
|
|
|
func (fi *streamHandlerFileInfo) Name() string { return fi.f.mf.Title }
|
|
|
|
func (fi *streamHandlerFileInfo) ModTime() time.Time { return fi.f.mf.UpdatedAt }
|
|
|
|
func (fi *streamHandlerFileInfo) Size() int64 { return fi.f.size }
|
|
|
|
func (fi *streamHandlerFileInfo) Mode() os.FileMode { return os.FileMode(0777) }
|
|
|
|
func (fi *streamHandlerFileInfo) IsDir() bool { return false }
|
|
|
|
func (fi *streamHandlerFileInfo) Sys() interface{} { return nil }
|