Merge pull request #582 from dweymouth/feat/dlnacast
Add ability to cast playback to DLNA devices
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user