mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-05 05:27:37 +03:00
feat: select correct transcoding for streaming
This commit is contained in:
parent
39993810b3
commit
c8b0d2bfae
9 changed files with 204 additions and 54 deletions
|
@ -14,12 +14,11 @@ import (
|
||||||
"github.com/deluan/navidrome/engine/transcoder"
|
"github.com/deluan/navidrome/engine/transcoder"
|
||||||
"github.com/deluan/navidrome/log"
|
"github.com/deluan/navidrome/log"
|
||||||
"github.com/deluan/navidrome/model"
|
"github.com/deluan/navidrome/model"
|
||||||
"github.com/deluan/navidrome/utils"
|
|
||||||
"github.com/djherbis/fscache"
|
"github.com/djherbis/fscache"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaStreamer interface {
|
type MediaStreamer interface {
|
||||||
NewStream(ctx context.Context, id string, maxBitRate int, format string) (*Stream, error)
|
NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMediaStreamer(ds model.DataStore, ffm transcoder.Transcoder, cache fscache.Cache) MediaStreamer {
|
func NewMediaStreamer(ds model.DataStore, ffm transcoder.Transcoder, cache fscache.Cache) MediaStreamer {
|
||||||
|
@ -32,18 +31,23 @@ type mediaStreamer struct {
|
||||||
cache fscache.Cache
|
cache fscache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, maxBitRate int, reqFormat string) (*Stream, error) {
|
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, reqFormat string, reqBitRate int) (*Stream, error) {
|
||||||
mf, err := ms.ds.MediaFile(ctx).Get(id)
|
mf, err := ms.ds.MediaFile(ctx).Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bitRate, format := selectTranscodingOptions(mf, maxBitRate, reqFormat)
|
format, bitRate := selectTranscodingOptions(ctx, ms.ds, mf, reqFormat, reqBitRate)
|
||||||
|
log.Trace(ctx, "Selected transcoding options",
|
||||||
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
||||||
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix,
|
||||||
|
"selectedBitrate", bitRate, "selectedFormat", format,
|
||||||
|
)
|
||||||
s := &Stream{ctx: ctx, mf: mf, format: format, bitRate: bitRate}
|
s := &Stream{ctx: ctx, mf: mf, format: format, bitRate: bitRate}
|
||||||
|
|
||||||
if format == "raw" {
|
if format == "raw" {
|
||||||
log.Debug(ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
|
log.Debug(ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
|
||||||
"requestBitrate", maxBitRate, "requestFormat", reqFormat,
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
||||||
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
||||||
f, err := os.Open(mf.Path)
|
f, err := os.Open(mf.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -66,7 +70,12 @@ func (ms *mediaStreamer) NewStream(ctx context.Context, id string, maxBitRate in
|
||||||
// If this is a brand new transcoding request, not in the cache, start transcoding
|
// If this is a brand new transcoding request, not in the cache, start transcoding
|
||||||
if w != nil {
|
if w != nil {
|
||||||
log.Trace(ctx, "Cache miss. Starting new transcoding session", "id", mf.ID)
|
log.Trace(ctx, "Cache miss. Starting new transcoding session", "id", mf.ID)
|
||||||
out, err := ms.ffm.Start(ctx, mf.Path, bitRate, format)
|
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
|
||||||
|
}
|
||||||
|
out, err := ms.ffm.Start(ctx, t.Command, mf.Path, bitRate, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error starting transcoder", "id", mf.ID, err)
|
log.Error(ctx, "Error starting transcoder", "id", mf.ID, err)
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
|
@ -79,7 +88,7 @@ func (ms *mediaStreamer) NewStream(ctx context.Context, id string, maxBitRate in
|
||||||
size := getFinalCachedSize(r)
|
size := getFinalCachedSize(r)
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
log.Debug(ctx, "Streaming cached file", "id", mf.ID, "path", mf.Path,
|
log.Debug(ctx, "Streaming cached file", "id", mf.ID, "path", mf.Path,
|
||||||
"requestBitrate", maxBitRate, "requestFormat", reqFormat,
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
||||||
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix, "size", size)
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix, "size", size)
|
||||||
sr := io.NewSectionReader(r, 0, size)
|
sr := io.NewSectionReader(r, 0, size)
|
||||||
s.Reader = sr
|
s.Reader = sr
|
||||||
|
@ -91,7 +100,7 @@ func (ms *mediaStreamer) NewStream(ctx context.Context, id string, maxBitRate in
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
|
log.Debug(ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
|
||||||
"requestBitrate", maxBitRate, "requestFormat", reqFormat,
|
"requestBitrate", reqBitRate, "requestFormat", reqFormat,
|
||||||
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
||||||
// All other cases, just return a ReadCloser, without Seek capabilities
|
// All other cases, just return a ReadCloser, without Seek capabilities
|
||||||
s.Reader = r
|
s.Reader = r
|
||||||
|
@ -131,27 +140,46 @@ func (s *Stream) ContentType() string { return mime.TypeByExtension("." + s.form
|
||||||
func (s *Stream) Name() string { return s.mf.Path }
|
func (s *Stream) Name() string { return s.mf.Path }
|
||||||
func (s *Stream) ModTime() time.Time { return s.mf.UpdatedAt }
|
func (s *Stream) ModTime() time.Time { return s.mf.UpdatedAt }
|
||||||
|
|
||||||
func selectTranscodingOptions(mf *model.MediaFile, maxBitRate int, format string) (int, string) {
|
// TODO This function deserves some love (refactoring)
|
||||||
var bitRate int
|
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
|
||||||
|
format = "raw"
|
||||||
if format == "raw" || !conf.Server.EnableDownsampling {
|
if reqFormat == "raw" {
|
||||||
return bitRate, "raw"
|
return
|
||||||
|
}
|
||||||
|
trc, hasDefault := ctx.Value("transcoding").(model.Transcoding)
|
||||||
|
var cFormat string
|
||||||
|
var cBitRate int
|
||||||
|
if reqFormat != "" {
|
||||||
|
cFormat = reqFormat
|
||||||
} else {
|
} else {
|
||||||
if maxBitRate == 0 {
|
if hasDefault {
|
||||||
bitRate = mf.BitRate
|
cFormat = trc.TargetFormat
|
||||||
} else {
|
cBitRate = trc.DefaultBitRate
|
||||||
bitRate = utils.MinInt(mf.BitRate, maxBitRate)
|
if p, ok := ctx.Value("player").(model.Player); ok {
|
||||||
|
cBitRate = p.MaxBitRate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
format = "mp3" //mf.Suffix
|
|
||||||
}
|
}
|
||||||
if conf.Server.MaxBitRate != 0 {
|
if reqBitRate > 0 {
|
||||||
bitRate = utils.MinInt(bitRate, conf.Server.MaxBitRate)
|
cBitRate = reqBitRate
|
||||||
}
|
}
|
||||||
|
if cBitRate == 0 && cFormat == "" {
|
||||||
if bitRate == mf.BitRate {
|
return
|
||||||
return bitRate, "raw"
|
|
||||||
}
|
}
|
||||||
return bitRate, format
|
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
|
||||||
|
if err == nil {
|
||||||
|
format = t.TargetFormat
|
||||||
|
if cBitRate != 0 {
|
||||||
|
bitRate = cBitRate
|
||||||
|
} else {
|
||||||
|
bitRate = t.DefaultBitRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if format == mf.Suffix && bitRate > mf.BitRate {
|
||||||
|
format = "raw"
|
||||||
|
bitRate = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheKey(id string, bitRate int, format string) string {
|
func cacheKey(id string, bitRate int, format string) string {
|
||||||
|
|
|
@ -32,8 +32,8 @@ var _ = Describe("MediaStreamer", func() {
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
conf.Server.EnableDownsampling = true
|
conf.Server.EnableDownsampling = true
|
||||||
ds = &persistence.MockDataStore{}
|
ds = &persistence.MockDataStore{MockedTranscoding: &mockTranscodingRepository{}}
|
||||||
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetData(`[{"id": "123", "path": "tests/fixtures/test.mp3", "bitRate": 128, "duration": 257.0}]`, 1)
|
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetData(`[{"id": "123", "path": "tests/fixtures/test.mp3", "suffix": "mp3", "bitRate": 128, "duration": 257.0}]`, 1)
|
||||||
streamer = NewMediaStreamer(ds, ffmpeg, cache)
|
streamer = NewMediaStreamer(ds, ffmpeg, cache)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -43,33 +43,140 @@ var _ = Describe("MediaStreamer", func() {
|
||||||
|
|
||||||
Context("NewStream", func() {
|
Context("NewStream", func() {
|
||||||
It("returns a seekable stream if format is 'raw'", func() {
|
It("returns a seekable stream if format is 'raw'", func() {
|
||||||
s, err := streamer.NewStream(ctx, "123", 0, "raw")
|
s, err := streamer.NewStream(ctx, "123", "raw", 0)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
It("returns a seekable stream if maxBitRate is 0", func() {
|
It("returns a seekable stream if maxBitRate is 0", func() {
|
||||||
s, err := streamer.NewStream(ctx, "123", 0, "mp3")
|
s, err := streamer.NewStream(ctx, "123", "mp3", 0)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
It("returns a seekable stream if maxBitRate is higher than file bitRate", func() {
|
It("returns a seekable stream if maxBitRate is higher than file bitRate", func() {
|
||||||
s, err := streamer.NewStream(ctx, "123", 320, "mp3")
|
s, err := streamer.NewStream(ctx, "123", "mp3", 320)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
It("returns a NON seekable stream if transcode is required", func() {
|
It("returns a NON seekable stream if transcode is required", func() {
|
||||||
s, err := streamer.NewStream(ctx, "123", 64, "mp3")
|
s, err := streamer.NewStream(ctx, "123", "mp3", 64)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(s.Seekable()).To(BeFalse())
|
Expect(s.Seekable()).To(BeFalse())
|
||||||
Expect(s.Duration()).To(Equal(float32(257.0)))
|
Expect(s.Duration()).To(Equal(float32(257.0)))
|
||||||
})
|
})
|
||||||
It("returns a seekable stream if the file is complete in the cache", func() {
|
It("returns a seekable stream if the file is complete in the cache", func() {
|
||||||
Eventually(func() bool { return ffmpeg.closed }).Should(BeTrue())
|
Eventually(func() bool { return ffmpeg.closed }).Should(BeTrue())
|
||||||
s, err := streamer.NewStream(ctx, "123", 64, "mp3")
|
s, err := streamer.NewStream(ctx, "123", "mp3", 64)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(s.Seekable()).To(BeTrue())
|
Expect(s.Seekable()).To(BeTrue())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("selectTranscodingOptions", func() {
|
||||||
|
mf := &model.MediaFile{}
|
||||||
|
Context("player is not configured", func() {
|
||||||
|
It("returns raw if raw is requested", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0)
|
||||||
|
Expect(format).To(Equal("raw"))
|
||||||
|
})
|
||||||
|
It("returns raw if a transcoder does not exists", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, _ := selectTranscodingOptions(ctx, ds, mf, "m4a", 0)
|
||||||
|
Expect(format).To(Equal("raw"))
|
||||||
|
})
|
||||||
|
It("returns the requested format if a transcoder exists", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
|
||||||
|
Expect(format).To(Equal("mp3"))
|
||||||
|
Expect(bitRate).To(Equal(160)) // Default Bit Rate
|
||||||
|
})
|
||||||
|
It("returns raw if requested format is the same as the original and it is not necessary to downsample", func() {
|
||||||
|
mf.Suffix = "mp3"
|
||||||
|
mf.BitRate = 112
|
||||||
|
format, _ := selectTranscodingOptions(ctx, ds, mf, "mp3", 128)
|
||||||
|
Expect(format).To(Equal("raw"))
|
||||||
|
})
|
||||||
|
It("returns the requested format if requested BitRate is lower than original", func() {
|
||||||
|
mf.Suffix = "mp3"
|
||||||
|
mf.BitRate = 320
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 192)
|
||||||
|
Expect(format).To(Equal("mp3"))
|
||||||
|
Expect(bitRate).To(Equal(192))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("player has format configured", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96}
|
||||||
|
ctx = context.WithValue(ctx, "transcoding", t)
|
||||||
|
})
|
||||||
|
It("returns raw if raw is requested", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0)
|
||||||
|
Expect(format).To(Equal("raw"))
|
||||||
|
})
|
||||||
|
It("returns configured format/bitrate as default", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0)
|
||||||
|
Expect(format).To(Equal("oga"))
|
||||||
|
Expect(bitRate).To(Equal(96))
|
||||||
|
})
|
||||||
|
It("returns requested format", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
|
||||||
|
Expect(format).To(Equal("mp3"))
|
||||||
|
Expect(bitRate).To(Equal(160)) // Default Bit Rate
|
||||||
|
})
|
||||||
|
It("returns requested bitrate", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 80)
|
||||||
|
Expect(format).To(Equal("oga"))
|
||||||
|
Expect(bitRate).To(Equal(80))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Context("player has maxBitRate configured", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
t := model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 96}
|
||||||
|
p := model.Player{ID: "player1", TranscodingId: t.ID, MaxBitRate: 80}
|
||||||
|
ctx = context.WithValue(ctx, "transcoding", t)
|
||||||
|
ctx = context.WithValue(ctx, "player", p)
|
||||||
|
})
|
||||||
|
It("returns raw if raw is requested", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, _ := selectTranscodingOptions(ctx, ds, mf, "raw", 0)
|
||||||
|
Expect(format).To(Equal("raw"))
|
||||||
|
})
|
||||||
|
It("returns configured format/bitrate as default", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 0)
|
||||||
|
Expect(format).To(Equal("oga"))
|
||||||
|
Expect(bitRate).To(Equal(80))
|
||||||
|
})
|
||||||
|
It("returns requested format", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "mp3", 0)
|
||||||
|
Expect(format).To(Equal("mp3"))
|
||||||
|
Expect(bitRate).To(Equal(160)) // Default Bit Rate
|
||||||
|
})
|
||||||
|
It("returns requested bitrate", func() {
|
||||||
|
mf.Suffix = "flac"
|
||||||
|
mf.BitRate = 1000
|
||||||
|
format, bitRate := selectTranscodingOptions(ctx, ds, mf, "", 80)
|
||||||
|
Expect(format).To(Equal("oga"))
|
||||||
|
Expect(bitRate).To(Equal(80))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
type fakeFFmpeg struct {
|
type fakeFFmpeg struct {
|
||||||
|
@ -78,7 +185,7 @@ type fakeFFmpeg struct {
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ff *fakeFFmpeg) Start(ctx context.Context, path string, maxBitRate int, format string) (f io.ReadCloser, err error) {
|
func (ff *fakeFFmpeg) Start(ctx context.Context, cmd, path string, maxBitRate int, format string) (f io.ReadCloser, err error) {
|
||||||
ff.r = strings.NewReader(ff.Data)
|
ff.r = strings.NewReader(ff.Data)
|
||||||
return ff, nil
|
return ff, nil
|
||||||
}
|
}
|
||||||
|
|
22
engine/mock_transcoding_repo_test.go
Normal file
22
engine/mock_transcoding_repo_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import "github.com/deluan/navidrome/model"
|
||||||
|
|
||||||
|
type mockTranscodingRepository struct {
|
||||||
|
model.TranscodingRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTranscodingRepository) Get(id string) (*model.Transcoding, error) {
|
||||||
|
return &model.Transcoding{ID: id, TargetFormat: "mp3", DefaultBitRate: 160}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTranscodingRepository) FindByFormat(format string) (*model.Transcoding, error) {
|
||||||
|
switch format {
|
||||||
|
case "mp3":
|
||||||
|
return &model.Transcoding{ID: "mp31", TargetFormat: "mp3", DefaultBitRate: 160}, nil
|
||||||
|
case "oga":
|
||||||
|
return &model.Transcoding{ID: "oga1", TargetFormat: "oga", DefaultBitRate: 128}, nil
|
||||||
|
default:
|
||||||
|
return nil, model.ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,14 +91,6 @@ var _ = Describe("Players", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
type mockTranscodingRepository struct {
|
|
||||||
model.TranscodingRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockTranscodingRepository) Get(id string) (*model.Transcoding, error) {
|
|
||||||
return &model.Transcoding{ID: id, TargetFormat: "mp3"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockPlayerRepository struct {
|
type mockPlayerRepository struct {
|
||||||
model.PlayerRepository
|
model.PlayerRepository
|
||||||
lastSaved *model.Player
|
lastSaved *model.Player
|
||||||
|
|
|
@ -8,12 +8,11 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/deluan/navidrome/conf"
|
|
||||||
"github.com/deluan/navidrome/log"
|
"github.com/deluan/navidrome/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transcoder interface {
|
type Transcoder interface {
|
||||||
Start(ctx context.Context, path string, maxBitRate int, format string) (f io.ReadCloser, err error)
|
Start(ctx context.Context, command, path string, maxBitRate int, format string) (f io.ReadCloser, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() Transcoder {
|
func New() Transcoder {
|
||||||
|
@ -22,8 +21,8 @@ func New() Transcoder {
|
||||||
|
|
||||||
type ffmpeg struct{}
|
type ffmpeg struct{}
|
||||||
|
|
||||||
func (ff *ffmpeg) Start(ctx context.Context, path string, maxBitRate int, format string) (f io.ReadCloser, err error) {
|
func (ff *ffmpeg) Start(ctx context.Context, command, path string, maxBitRate int, format string) (f io.ReadCloser, err error) {
|
||||||
arg0, args := createTranscodeCommand(path, maxBitRate, format)
|
arg0, args := createTranscodeCommand(command, path, maxBitRate, format)
|
||||||
|
|
||||||
log.Trace(ctx, "Executing ffmpeg command", "cmd", arg0, "args", args)
|
log.Trace(ctx, "Executing ffmpeg command", "cmd", arg0, "args", args)
|
||||||
cmd := exec.Command(arg0, args...)
|
cmd := exec.Command(arg0, args...)
|
||||||
|
@ -38,9 +37,7 @@ func (ff *ffmpeg) Start(ctx context.Context, path string, maxBitRate int, format
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTranscodeCommand(path string, maxBitRate int, format string) (string, []string) {
|
func createTranscodeCommand(cmd, path string, maxBitRate int, format string) (string, []string) {
|
||||||
cmd := conf.Server.DownsampleCommand
|
|
||||||
|
|
||||||
split := strings.Split(cmd, " ")
|
split := strings.Split(cmd, " ")
|
||||||
for i, s := range split {
|
for i, s := range split {
|
||||||
s = strings.Replace(s, "%s", path, -1)
|
s = strings.Replace(s, "%s", path, -1)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package transcoder
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/deluan/navidrome/conf"
|
|
||||||
"github.com/deluan/navidrome/log"
|
"github.com/deluan/navidrome/log"
|
||||||
"github.com/deluan/navidrome/tests"
|
"github.com/deluan/navidrome/tests"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
|
@ -18,11 +17,8 @@ func TestTranscoder(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = Describe("createTranscodeCommand", func() {
|
var _ = Describe("createTranscodeCommand", func() {
|
||||||
BeforeEach(func() {
|
|
||||||
conf.Server.DownsampleCommand = "ffmpeg -i %s -b:a %bk mp3 -"
|
|
||||||
})
|
|
||||||
It("creates a valid command line", func() {
|
It("creates a valid command line", func() {
|
||||||
cmd, args := createTranscodeCommand("/music library/file.mp3", 123, "")
|
cmd, args := createTranscodeCommand("ffmpeg -i %s -b:a %bk mp3 -", "/music library/file.mp3", 123, "")
|
||||||
Expect(cmd).To(Equal("ffmpeg"))
|
Expect(cmd).To(Equal("ffmpeg"))
|
||||||
Expect(args).To(Equal([]string{"-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
|
Expect(args).To(Equal([]string{"-i", "/music library/file.mp3", "-b:a", "123k", "mp3", "-"}))
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,4 +13,5 @@ type Transcodings []Transcoding
|
||||||
type TranscodingRepository interface {
|
type TranscodingRepository interface {
|
||||||
Get(id string) (*Transcoding, error)
|
Get(id string) (*Transcoding, error)
|
||||||
Put(*Transcoding) error
|
Put(*Transcoding) error
|
||||||
|
FindByFormat(format string) (*Transcoding, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,13 @@ func (r *transcodingRepository) Get(id string) (*model.Transcoding, error) {
|
||||||
return &res, err
|
return &res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *transcodingRepository) FindByFormat(format string) (*model.Transcoding, error) {
|
||||||
|
sel := r.newSelect().Columns("*").Where(Eq{"target_format": format})
|
||||||
|
var res model.Transcoding
|
||||||
|
err := r.queryOne(sel, &res)
|
||||||
|
return &res, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *transcodingRepository) Put(t *model.Transcoding) error {
|
func (r *transcodingRepository) Put(t *model.Transcoding) error {
|
||||||
_, err := r.put(t.ID, t)
|
_, err := r.put(t.ID, t)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (c *StreamController) Stream(w http.ResponseWriter, r *http.Request) (*resp
|
||||||
maxBitRate := utils.ParamInt(r, "maxBitRate", 0)
|
maxBitRate := utils.ParamInt(r, "maxBitRate", 0)
|
||||||
format := utils.ParamString(r, "format")
|
format := utils.ParamString(r, "format")
|
||||||
|
|
||||||
stream, err := c.streamer.NewStream(r.Context(), id, maxBitRate, format)
|
stream, err := c.streamer.NewStream(r.Context(), id, format, maxBitRate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ func (c *StreamController) Download(w http.ResponseWriter, r *http.Request) (*re
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := c.streamer.NewStream(r.Context(), id, 0, "raw")
|
stream, err := c.streamer.NewStream(r.Context(), id, "raw", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue