Merge pull request #582 from dweymouth/feat/dlnacast

Add ability to cast playback to DLNA devices
This commit is contained in:
Drew Weymouth
2025-04-05 11:01:10 -07:00
committed by GitHub
25 changed files with 1199 additions and 132 deletions
+6
View File
@@ -3,6 +3,7 @@ package ui
import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/supersonic/ui/util"
@@ -111,6 +112,10 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
pm.OnVolumeChange(func(vol int) {
fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) })
})
pm.OnPlayerChange(func() {
_, local := pm.CurrentPlayer().(*mpv.Player)
fyne.Do(func() { bp.AuxControls.SetIsRemotePlayer(!local) })
})
bp.AuxControls.VolumeControl.OnSetVolume = func(v int) {
pm.SetVolume(v)
}
@@ -121,6 +126,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
pm.SetAutoplay(autoplay)
}
bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue)
bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu)
bp.imageLoader = util.NewThumbnailLoader(im, bp.NowPlaying.SetImage)
+2 -2
View File
@@ -443,7 +443,7 @@ func (a *NowPlayingPage) Reload() {
}
switch a.tabs.SelectedIndex() {
case 1: /*lyrics*/
a.lastPlayPos = a.pm.CurrentPlayer().GetStatus().TimePos
a.lastPlayPos = a.pm.PlaybackStatus().TimePos
a.updateLyrics()
case 2: /*related*/
a.updateRelatedList()
@@ -527,7 +527,7 @@ func (a *NowPlayingPage) saveSelectedTab(tabNum int) {
func (a *NowPlayingPage) formatStatusLine() {
curPlayer := a.pm.CurrentPlayer()
playerStats := curPlayer.GetStatus()
playerStats := a.pm.PlaybackStatus()
lastStatus := a.statusLabel.Text
stopped := lang.L("Stopped")
state := stopped
+47
View File
@@ -151,6 +151,50 @@ func (m *Controller) HaveModal() bool {
return m.haveModal
}
func (m *Controller) ShowCastMenu(onPendingPlayerChange func()) {
rp := m.App.PlaybackManager.CurrentRemotePlayer()
devices := m.App.PlaybackManager.RemotePlayers()
local := fyne.NewMenuItem(lang.L("This computer"), func() {
if rp == nil {
return // no-op. already not casting
}
onPendingPlayerChange()
go func() {
if err := m.App.PlaybackManager.SetRemotePlayer(nil); err != nil {
fyne.Do(func() { m.ToastProvider.ShowErrorToast("Failed to disconnect from remote player") })
}
}()
})
local.Icon = theme.ComputerIcon()
local.Checked = rp == nil
menu := fyne.NewMenu("", local)
for _, d := range devices {
_d := d
isCurrent := rp != nil && _d.URL == rp.URL
item := fyne.NewMenuItem(d.Name, func() {
if isCurrent {
return // no-op.
}
onPendingPlayerChange()
go func() {
if err := m.App.PlaybackManager.SetRemotePlayer(&_d); err != nil {
fyne.Do(func() { m.ToastProvider.ShowErrorToast("Failed to connect to " + _d.Name) })
}
}()
})
item.Icon = myTheme.CastIcon
item.Checked = isCurrent
menu.Items = append(menu.Items, item)
}
pop := widget.NewPopUpMenu(menu, m.MainWindow.Canvas())
canvasSize := m.MainWindow.Canvas().Size()
pop.ShowAtPosition(fyne.NewPos(
canvasSize.Width-pop.MinSize().Width-10,
canvasSize.Height-pop.MinSize().Height-100,
))
}
func (m *Controller) ShowPopUpPlayQueue() {
m.popUpQueueMutex.Lock()
if m.popUpQueue == nil {
@@ -270,6 +314,7 @@ func (c *Controller) ShowAboutDialog() {
}
func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) {
devs, err := c.App.LocalPlayer.ListAudioDevices()
if err != nil {
log.Printf("error listing audio devices: %v", err)
@@ -282,6 +327,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
_, canSavePlayQueue := c.App.ServerManager.Server.(mediaprovider.CanSavePlayQueue)
isLocalPlayer := isEqualizerPlayer
bands := c.App.LocalPlayer.Equalizer().BandFrequencies()
dlg := dialogs.NewSettingsDialog(c.App.Config,
devs, themeFiles, bands,
c.App.ServerManager.Server.ClientDecidesScrobble(),
@@ -317,6 +363,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
c.ClosePopUpOnEscape(pop)
c.haveModal = true
pop.Show()
}
func (c *Controller) doModalClosed() {
+12 -9
View File
@@ -7,6 +7,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/ui/shortcuts"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/visualizations"
@@ -21,11 +22,13 @@ type visualizationData struct {
}
func (c *Controller) initVisualizations() {
c.App.LocalPlayer.OnStopped(c.stopVisualizationAnim)
c.App.LocalPlayer.OnPaused(c.stopVisualizationAnim)
c.App.LocalPlayer.OnPlaying(func() {
if c.peakMeter != nil {
c.startVisualizationAnim()
c.App.PlaybackManager.OnStopped(c.stopVisualizationAnim)
c.App.PlaybackManager.OnPaused(c.stopVisualizationAnim)
c.App.PlaybackManager.OnPlaying(func() {
if _, ok := c.App.PlaybackManager.CurrentPlayer().(*mpv.Player); ok {
if c.peakMeter != nil {
c.startVisualizationAnim()
}
}
})
}
@@ -71,13 +74,13 @@ func (c *Controller) stopVisualizationAnim() {
if c.visualizationAnim != nil {
c.visualizationAnim.Stop()
c.visualizationAnim = nil
c.App.LocalPlayer.SetPeaksEnabled(false)
// c.App.LocalPlayer.SetPeaksEnabled(false)
}
}
func (c *Controller) startVisualizationAnim() {
if c.visualizationAnim == nil {
c.App.LocalPlayer.SetPeaksEnabled(true)
//c.App.LocalPlayer.SetPeaksEnabled(true)
c.visualizationAnim = fyne.NewAnimation(
time.Duration(math.MaxInt64), /*until stopped*/
c.tickVisualizations)
@@ -86,8 +89,8 @@ func (c *Controller) startVisualizationAnim() {
}
func (c *Controller) tickVisualizations(_ float32) {
lP, rP, lRMS, rRMS := c.App.LocalPlayer.GetPeaks()
//lP, rP, lRMS, rRMS := c.App.LocalPlayer.GetPeaks()
if c.visualizationData.peakMeter != nil {
c.visualizationData.peakMeter.UpdatePeaks(lP, rP, lRMS, rRMS)
//c.visualizationData.peakMeter.UpdatePeaks(lP, rP, lRMS, rRMS)
}
}
+1
View File
@@ -39,6 +39,7 @@ var (
AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg)
ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg)
AutoplayIcon fyne.Resource = theme.NewThemedResource(res.ResInfinitySvg)
CastIcon fyne.Resource = theme.NewThemedResource(res.ResCastSvg)
RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg)
FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg)
NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg)
+48 -2
View File
@@ -2,6 +2,7 @@ package widgets
import (
"math"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -25,6 +26,7 @@ type AuxControls struct {
VolumeControl *VolumeControl
autoplay *IconButton
loop *IconButton
cast *IconButton
showQueue *IconButton
container *fyne.Container
@@ -35,10 +37,17 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial
VolumeControl: NewVolumeControl(initialVolume),
autoplay: NewIconButton(myTheme.AutoplayIcon, nil),
loop: NewIconButton(myTheme.RepeatIcon, nil),
cast: NewIconButton(myTheme.CastIcon, nil),
showQueue: NewIconButton(myTheme.PlayQueueIcon, nil),
}
a.loop.IconSize = IconButtonSizeSmaller
a.loop.SetToolTip(lang.L("Repeat"))
a.SetLoopMode(initialLoopMode)
a.cast.IconSize = IconButtonSizeSmaller
a.cast.SetToolTip(lang.L("Cast to device"))
a.autoplay.Highlighted = initialAutoplay
//a.autoplay.IconSize = IconButtonSizeSmaller
a.autoplay.SetToolTip(lang.L("Autoplay"))
@@ -48,9 +57,10 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial
a.OnChangeAutoplay(a.autoplay.Highlighted)
}
}
a.SetLoopMode(initialLoopMode)
a.showQueue.IconSize = IconButtonSizeSmaller
a.showQueue.SetToolTip(lang.L("Show play queue"))
a.container = container.NewHBox(
layout.NewSpacer(),
container.NewVBox(
@@ -58,7 +68,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial
a.VolumeControl,
container.New(
layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5),
layout.NewSpacer(), a.autoplay, a.loop, a.showQueue, util.NewHSpace(5)),
layout.NewSpacer(), a.autoplay, a.loop, a.cast, a.showQueue, util.NewHSpace(5)),
layout.NewSpacer(),
),
)
@@ -88,6 +98,16 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) {
}
}
func (a *AuxControls) DisableCastButton() {
a.cast.Disable()
}
func (a *AuxControls) SetIsRemotePlayer(isRemote bool) {
a.cast.Enable()
a.cast.Highlighted = isRemote
a.cast.Refresh()
}
func (a *AuxControls) SetAutoplay(autoplay bool) {
if autoplay == a.autoplay.Highlighted {
return
@@ -100,6 +120,12 @@ func (a *AuxControls) OnShowPlayQueue(f func()) {
a.showQueue.OnTapped = f
}
func (a *AuxControls) OnShowCastMenu(f func(func())) {
a.cast.OnTapped = func() {
f(a.DisableCastButton)
}
}
type volumeSlider struct {
widget.Slider
@@ -145,6 +171,10 @@ type VolumeControl struct {
muted bool
lastVol int
setVolDebouncer func()
delaySetVolume bool
pendingVolume int
container *fyne.Container
}
@@ -160,6 +190,15 @@ func NewVolumeControl(initialVol int) *VolumeControl {
v.slider.Orientation = widget.Horizontal
v.slider.Value = float64(v.lastVol)
v.slider.OnChanged = v.onChanged
// for players that are slow to respond to volume changes
// (e.g. DLNA), delay responding to the SetVolume call
// to avoid hiccuping from callback "echoes"
v.setVolDebouncer = util.NewDebouncer(100*time.Millisecond, func() {
v.delaySetVolume = false
v.SetVolume(v.pendingVolume)
})
v.container = container.NewHBox(container.NewCenter(v.icon), v.slider)
return v
}
@@ -167,6 +206,11 @@ func NewVolumeControl(initialVol int) *VolumeControl {
// Sets the volume that is displayed in the slider.
// Does not invoke OnSetVolume callback.
func (v *VolumeControl) SetVolume(vol int) {
if v.delaySetVolume {
v.pendingVolume = vol
return
}
if (vol == v.lastVol && !v.muted) || (v.muted && vol == 0) {
return
}
@@ -177,6 +221,8 @@ func (v *VolumeControl) SetVolume(vol int) {
func (v *VolumeControl) onChanged(volume float64) {
vol := int(volume)
v.delaySetVolume = true
v.setVolDebouncer()
v.lastVol = vol
v.muted = false
v.updateIconForVolume(vol)