From 2701bb3f7f646f9d56e4ca268a2f32b09be5667f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 30 Mar 2025 13:08:18 -0700 Subject: [PATCH] sync playback time when seeking and starting new tracks --- backend/player/dlna/dlnaplayer.go | 57 +++++++++++++++++++++++++++++-- go.mod | 10 +++--- go.sum | 20 +++++++---- res/translations/en.json | 1 + ui/controller/controller.go | 2 +- 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/backend/player/dlna/dlnaplayer.go b/backend/player/dlna/dlnaplayer.go index dd87e34..3f9c168 100644 --- a/backend/player/dlna/dlnaplayer.go +++ b/backend/player/dlna/dlnaplayer.go @@ -41,6 +41,8 @@ type proxyMapEntry struct { type DLNAPlayer struct { player.BasePlayerCallbackImpl + destroyed bool + avTransport *avtransport.Client renderControl *renderingcontrol.Client @@ -51,8 +53,13 @@ type DLNAPlayer struct { curTrackMeta mediaprovider.MediaItemMetadata nextTrackMeta mediaprovider.MediaItemMetadata + // if true, report playback time 00:00 + // pending time sync with player after beginning playback + pendingPlayStart bool + // start playback position in seconds of the last seek/time sync lastStartTime int - stopwatch util.Stopwatch + // how long the track has been playing since last time sync + stopwatch util.Stopwatch proxyServer *http.Server proxyActive atomic.Bool @@ -139,10 +146,22 @@ func (d *DLNAPlayer) PlayFile(urlstr string, meta mediaprovider.MediaItemMetadat if err := d.playAVTransportMedia(&media); err != nil { return err } + d.pendingPlayStart = true if startTime > 0 { // TODO: do something better than this!! time.Sleep(2 * time.Second) - d.sendSeekCmd(startTime) + if !d.destroyed { + d.sendSeekCmd(startTime) + } + d.pendingPlayStart = false + } else { + go func() { + time.Sleep(2 * time.Second) + if !d.destroyed { + d.syncPlaybackTime() + } + d.pendingPlayStart = false + }() } d.state = playing remainingDur := meta.Duration - int(startTime) @@ -278,6 +297,13 @@ func (d *DLNAPlayer) SeekSeconds(secs float64) error { } d.InvokeOnSeek() + + go func() { + time.Sleep(4 * time.Second) + if !d.destroyed { + d.syncPlaybackTime() + } + }() return nil } @@ -303,9 +329,13 @@ func (d *DLNAPlayer) GetStatus() player.Status { state = player.Paused } + var timePos float64 + if !d.pendingPlayStart { + timePos = d.curPlayPos().Seconds() + } return player.Status{ State: state, - TimePos: d.curPlayPos().Seconds(), + TimePos: timePos, Duration: float64(d.curTrackMeta.Duration), } } @@ -315,11 +345,25 @@ func (d *DLNAPlayer) curPlayPos() time.Duration { } func (d *DLNAPlayer) Destroy() { + d.destroyed = true if d.proxyServer != nil { go d.proxyServer.Shutdown(context.Background()) } } +func (d *DLNAPlayer) syncPlaybackTime() { + start := time.Now() + if pos, err := d.avTransport.GetPositionInfo(context.Background()); err == nil { + d.lastStartTime = int(pos.RelTime.Seconds() + (time.Since(start) / 2).Seconds()) + d.stopwatch.Reset() + if d.state == playing { + d.stopwatch.Start() + } + d.setTrackChangeTimer(time.Duration(d.curTrackMeta.Duration-d.lastStartTime) * time.Second) + d.InvokeOnSeek() + } +} + func (d *DLNAPlayer) ensureSetupProxy() error { if d.proxyActive.Swap(true) { return nil // already active @@ -422,6 +466,13 @@ func (d *DLNAPlayer) handleOnTrackChange() { d.stopwatch.Start() d.setTrackChangeTimer(nextTrackChange) d.InvokeOnTrackChange() + + go func() { + time.Sleep(5 * time.Second) + if !d.destroyed { + d.syncPlaybackTime() + } + }() } } diff --git a/go.mod b/go.mod index 1fbe980..91c3880 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dweymouth/supersonic -go 1.21 +go 1.21.9 require ( fyne.io/fyne/v2 v2.6.0-beta1 @@ -14,13 +14,14 @@ require ( github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 + github.com/hashicorp/go-retryablehttp v0.7.7 github.com/pelletier/go-toml/v2 v2.0.8 github.com/quarckster/go-mpris-server v1.0.3 github.com/supersonic-app/go-mpv v0.1.0 github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d - github.com/supersonic-app/go-upnpcast v0.0.0-20250312000014-e4f7242a07ce + github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5 github.com/zalando/go-keyring v0.2.6 - golang.org/x/net v0.25.0 + golang.org/x/net v0.33.0 golang.org/x/sys v0.30.0 golang.org/x/text v0.22.0 ) @@ -42,10 +43,9 @@ require ( github.com/go-text/typesetting v0.2.1 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/koron/go-ssdp v0.0.4 // indirect + github.com/koron/go-ssdp v0.0.5 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index a28ff24..e4cd2e6 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250308154116-c32a0de49bde h1:vSJQkJW github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250308154116-c32a0de49bde/go.mod h1:ON11afuS9jVyN+nfEtITv3jX1H+JD+Z50gxCiEC1zLw= github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 h1:KzqSaQwG3HsTZQlEtkp0BeUy9vmYZ0rq0B15qIPSiBs= github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= @@ -61,16 +63,22 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc= github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= -github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= -github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= +github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= @@ -105,8 +113,8 @@ github.com/supersonic-app/go-mpv v0.1.0 h1:U+cCnLQxmpqx5mY6nMlC0J4uIdCCXUbAjpjS0 github.com/supersonic-app/go-mpv v0.1.0/go.mod h1:1bQz6kBQumJopXEbkiqoLxIXLy7F7yWFBvknvpAtIC0= github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d h1:70+Nn7yh+cfeKqqXVTdpneFqXuvrBLyP7U6GVUsjTU4= github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d/go.mod h1:D+OWPXeD9owcdcoXATv5YPBGWxxVvn5k98rt5B4wMc4= -github.com/supersonic-app/go-upnpcast v0.0.0-20250312000014-e4f7242a07ce h1:6R8/JRwRVrR9t2YPTgikS39lRf3gPp/MPcbx+lIf3rA= -github.com/supersonic-app/go-upnpcast v0.0.0-20250312000014-e4f7242a07ce/go.mod h1:Wscg4vEzF9x6i4ltL2Qkvx4VhzXfkBBl8uoG0YSAJMU= +github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5 h1:aoUJKPFD/ZrNZjK6fl2Xhazwsttx42esKxhSSzEE3Bo= +github.com/supersonic-app/go-upnpcast v0.0.0-20250330154256-b957204209a5/go.mod h1:ibt19zDV5/vvF14jHJpTv3AOorq1EbmrMAubxnuvR5Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= @@ -123,8 +131,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/res/translations/en.json b/res/translations/en.json index 5c10897..d2cfee6 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -207,6 +207,7 @@ "Testing connection": "Testing connection", "The request timed out": "The request timed out", "Theme": "Theme", + "This computer": "This computer", "Time": "Time", "Title": "Title", "Title (A-Z)": "Title (A-Z)", diff --git a/ui/controller/controller.go b/ui/controller/controller.go index a3d3186..67dde41 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -154,7 +154,7 @@ func (m *Controller) HaveModal() bool { func (m *Controller) ShowCastMenu(onPendingPlayerChange func()) { rp := m.App.PlaybackManager.CurrentRemotePlayer() devices := m.App.PlaybackManager.RemotePlayers() - local := fyne.NewMenuItem(lang.L("Local player"), func() { + local := fyne.NewMenuItem(lang.L("This computer"), func() { onPendingPlayerChange() go func() { if err := m.App.PlaybackManager.SetRemotePlayer(nil); err != nil {