diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 23cc1f81..dad21cfc 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -144,6 +144,12 @@ type MediaProvider interface { RescanLibrary() error } +type SupportsStreamOffset interface { + CanStreamWithOffset() bool + + GetStreamURLWithOffset(trackID string, timeOffsetSeconds int) (string, error) +} + type SupportsRating interface { SetRating(params RatingFavoriteParameters, rating int) error } diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 9809a281..a0a68f9b 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -4,6 +4,7 @@ import ( "errors" "image" "io" + "log" "math" "strconv" "strings" @@ -28,6 +29,13 @@ type subsonicMediaProvider struct { playlistsCachedAt int64 // unix } +// assert compliance with interfaces +var ( + _ mediaprovider.MediaProvider = (*subsonicMediaProvider)(nil) + _ mediaprovider.SupportsRating = (*subsonicMediaProvider)(nil) + _ mediaprovider.SupportsStreamOffset = (*subsonicMediaProvider)(nil) +) + func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider { return &subsonicMediaProvider{client: subsonicClient} } @@ -324,6 +332,28 @@ func (s *subsonicMediaProvider) RescanLibrary() error { return err } +func (s *subsonicMediaProvider) CanStreamWithOffset() bool { + extensions, err := s.client.GetOpenSubsonicExtensions() + if err != nil { + return false + } + log.Printf("OpenSubsonic extensions: %v", extensions) + for _, ext := range extensions { + if ext.Name == subsonic.TranscodeOffset { + return true + } + } + return false +} + +func (s *subsonicMediaProvider) GetStreamURLWithOffset(trackID string, offsetSeconds int) (string, error) { + u, err := s.client.GetStreamURL(trackID, map[string]string{"timeOffset": strconv.Itoa(offsetSeconds)}) + if err != nil { + return "", err + } + return u.String(), nil +} + func toTrack(ch *subsonic.Child) *mediaprovider.Track { if ch == nil { return nil diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index fe1a343f..5c70bf97 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -4,6 +4,7 @@ import ( "context" "errors" "log" + "math" "math/rand" "time" @@ -53,6 +54,10 @@ type PlaybackManager struct { transcodeCfg *TranscodingConfig replayGainCfg ReplayGainConfig + // OpenSubsonic transcodeOffset extension support + serverCanStreamWithOffset bool + streamOffsetSeconds int + // registered callbacks onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64) @@ -83,6 +88,18 @@ func NewPlaybackManager( nowPlayingIdx: -1, wasStopped: true, } + + // keep track of whether we can use OpenSubsonic's transcodeOffset extension when seeking + updateCanStreamWithOffset := func() { + if so, ok := s.Server.(mediaprovider.SupportsStreamOffset); ok { + pm.serverCanStreamWithOffset = so.CanStreamWithOffset() + } else { + pm.serverCanStreamWithOffset = false + } + } + updateCanStreamWithOffset() + s.OnServerConnected(updateCanStreamWithOffset) + p.OnTrackChange(pm.handleOnTrackChange) p.OnSeek(func() { pm.doUpdateTimePos() @@ -283,11 +300,15 @@ func (p *PlaybackManager) PlayFromBeginning() error { } func (p *PlaybackManager) PlayTrackAt(idx int) error { + return p.playTrackAtWithOffset(idx, 0) +} + +func (p *PlaybackManager) playTrackAtWithOffset(idx, timeOffsetSeconds int) error { if idx < 0 || idx >= len(p.playQueue) { return errors.New("track index out of range") } p.nowPlayingIdx = idx - 1 - return p.setTrack(idx, false) + return p.setTrack(idx, false, timeOffsetSeconds) } func (p *PlaybackManager) PlayRandomSongs(genreName string) { @@ -367,7 +388,7 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { p.Stop() } else { p.nowPlayingIdx -= 1 // will be incremented in newtrack callback from player - p.setTrack(newNowPlaying, false) + p.setTrack(newNowPlaying, false, 0) } // setNextTrack and onSongChange callbacks will be handled // when we receive new track event from player @@ -457,7 +478,10 @@ func (p *PlaybackManager) GetLoopMode() LoopMode { } func (p *PlaybackManager) PlayerStatus() player.Status { - return p.player.GetStatus() + s := p.player.GetStatus() + s.Duration = p.curTrackTime // don't use duration reported by player + s.TimePos = s.TimePos + float64(p.streamOffsetSeconds) + return s } func (p *PlaybackManager) SetVolume(vol int) error { @@ -484,13 +508,16 @@ func (p *PlaybackManager) SeekNext() error { func (p *PlaybackManager) SeekBackOrPrevious() error { if p.nowPlayingIdx == 0 || p.player.GetStatus().TimePos > 3 { - return p.player.SeekSeconds(0) + return p.SeekSeconds(0) } return p.PlayTrackAt(p.nowPlayingIdx - 1) } // Seek to given absolute position in the current track by seconds. func (p *PlaybackManager) SeekSeconds(sec float64) error { + if p.serverCanStreamWithOffset && !p.transcodeCfg.ForceRawFile { + return p.playTrackAtWithOffset(p.nowPlayingIdx, int(math.Floor(sec))) + } return p.player.SeekSeconds(sec) } @@ -502,7 +529,7 @@ func (p *PlaybackManager) SeekFraction(fraction float64) error { fraction = 1 } target := p.curTrackTime * fraction - return p.player.SeekSeconds(target) + return p.SeekSeconds(target) } func (p *PlaybackManager) Stop() error { @@ -603,12 +630,16 @@ func (p *PlaybackManager) setNextTrackAfterQueueUpdate() { } } -func (p *PlaybackManager) setTrack(idx int, next bool) error { +func (p *PlaybackManager) setTrack(idx int, next bool, timeOffset int) error { if urlP, ok := p.player.(player.URLPlayer); ok { url := "" if idx >= 0 { var err error - url, err = p.sm.Server.GetStreamURL(p.playQueue[idx].ID, p.transcodeCfg.ForceRawFile) + if so, ok := p.sm.Server.(mediaprovider.SupportsStreamOffset); ok && timeOffset > 0 { + url, err = so.GetStreamURLWithOffset(p.playQueue[idx].ID, timeOffset) + } else { + url, err = p.sm.Server.GetStreamURL(p.playQueue[idx].ID, p.transcodeCfg.ForceRawFile) + } if err != nil { return err } @@ -631,7 +662,7 @@ func (p *PlaybackManager) setTrack(idx int, next bool) error { } func (p *PlaybackManager) setNextTrack(idx int) error { - return p.setTrack(idx, true) + return p.setTrack(idx, true, 0) } // call BEFORE updating p.nowPlayingIdx @@ -738,6 +769,6 @@ func (p *PlaybackManager) doUpdateTimePos() { p.latestTrackPosition = s.TimePos } for _, cb := range p.onPlayTimeUpdate { - cb(s.TimePos, s.Duration) + cb(s.TimePos, p.curTrackTime) } } diff --git a/go.mod b/go.mod index 46633a3a..ed99f4f1 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002 + github.com/dweymouth/go-subsonic v0.0.0-20240209011949-13593bcf2811 github.com/fsnotify/fsnotify v1.6.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index fd340a16..baa617a4 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,10 @@ github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31 github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002 h1:DhWQZJObkCUSFmHu/eEzHRqy0R33DcR266nPAvZJE34= github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= +github.com/dweymouth/go-subsonic v0.0.0-20240110030314-8df30080ab8d h1:8Lre3j3AYq9S53dcToWm7X21V5A2Z+g4ONSCI7hsWxc= +github.com/dweymouth/go-subsonic v0.0.0-20240110030314-8df30080ab8d/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= +github.com/dweymouth/go-subsonic v0.0.0-20240209011949-13593bcf2811 h1:TgDyZFMtcl38ql2Mj4t0lowftWmw9ZnPE2GbXsY0nx4= +github.com/dweymouth/go-subsonic v0.0.0-20240209011949-13593bcf2811/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=