diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index 9bad487c..b6448041 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -37,7 +37,9 @@ func (c *Controller) ServePing(_ *http.Request) *spec.Response { func (c *Controller) ServeGetOpenSubsonicExtensions(_ *http.Request) *spec.Response { sub := spec.NewResponse() - sub.OpenSubsonicExtensions = &spec.OpenSubsonicExtensions{} + sub.OpenSubsonicExtensions = &spec.OpenSubsonicExtensions{ + {Name: "transcodeOffset", Versions: []int{1}}, + } return sub } diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 7146404a..a84767dd 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -218,6 +218,7 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R maxBitRate, _ := params.GetInt("maxBitRate") format, _ := params.Get("format") + timeOffset, _ := params.GetInt("timeOffset") if format == "raw" || maxBitRate >= audioFile.AudioBitrate() { http.ServeFile(w, r, file.AbsPath()) @@ -240,6 +241,9 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R if maxBitRate > 0 && int(profile.BitRate()) > maxBitRate { profile = transcode.WithBitrate(profile, transcode.BitRate(maxBitRate)) } + if timeOffset > 0 { + profile = transcode.WithSeek(profile, time.Second*time.Duration(timeOffset)) + } log.Printf("trancoding to %q with max bitrate %dk", profile.MIME(), profile.BitRate()) diff --git a/transcode/transcode_test.go b/transcode/transcode_test.go index df21c30a..20119768 100644 --- a/transcode/transcode_test.go +++ b/transcode/transcode_test.go @@ -64,3 +64,39 @@ func TestTranscode(t *testing.T) { // we should have 5 seconds of PCM data require.Equal(t, testFileLen*bytesPerSec, buf.Len()) } + +// TestTranscodeWithSeek starts a web server that transcodes a 5s FLAC file to PCM audio, but with a 2 second offset. +// A client consumes the result over a 3 second period. +func TestTranscodeWithSeek(t *testing.T) { + t.Parallel() + + testFile := "testdata/5s.flac" + testFileLen := 5 + + seekSecs := 2 + profile := transcode.WithSeek(testProfile, time.Duration(seekSecs)*time.Second) + + tr := transcode.NewFFmpegTranscoder() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, tr.Transcode(r.Context(), profile, testFile, w)) + w.(http.Flusher).Flush() + })) + defer server.Close() + + resp, err := server.Client().Get(server.URL) + require.NoError(t, err) + defer resp.Body.Close() + + var buf bytes.Buffer + for { + n, err := io.Copy(&buf, io.LimitReader(resp.Body, bytesPerSec)) + require.NoError(t, err) + if n == 0 { + break + } + time.Sleep(1 * time.Second) + } + + // since we seeked 2 seconds, we should have 5-2 = 3 seconds of PCM data + require.Equal(t, (testFileLen-seekSecs)*bytesPerSec, buf.Len()) +} diff --git a/transcode/transcoder_caching.go b/transcode/transcoder_caching.go index f1043b2d..2f744ebb 100644 --- a/transcode/transcoder_caching.go +++ b/transcode/transcoder_caching.go @@ -23,6 +23,11 @@ func NewCachingTranscoder(t Transcoder, cachePath string) *CachingTranscoder { } func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error { + // don't try cache partial transcodes + if profile.Seek() > 0 { + return t.transcoder.Transcode(ctx, profile, in, out) + } + if err := os.MkdirAll(t.cachePath, perm^0o111); err != nil { return fmt.Errorf("make cache path: %w", err) }