add status line with media info to bottom of now playing page
This commit is contained in:
@@ -116,6 +116,10 @@ func (p *PlaybackManager) NowPlaying() *mediaprovider.Track {
|
||||
return p.playQueue[p.nowPlayingIdx]
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) NowPlayingIndex() int {
|
||||
return int(p.nowPlayingIdx)
|
||||
}
|
||||
|
||||
// Sets a callback that is notified whenever a new song begins playing.
|
||||
func (p *PlaybackManager) OnSongChange(cb func(nowPlaying *mediaprovider.Track, justScrobbledIfAny *mediaprovider.Track)) {
|
||||
p.onSongChange = append(p.onSongChange, cb)
|
||||
|
||||
@@ -70,6 +70,25 @@ type AudioDevice struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
// Media information about the currently playing media.
|
||||
type MediaInfo struct {
|
||||
// The sample format as string. This uses the same names as used in other places of mpv.
|
||||
// NOTE: this is the format that the decoder outputs, NOT necessarily the format of the file.
|
||||
Format string
|
||||
|
||||
// Audio samplerate.
|
||||
Samplerate int
|
||||
|
||||
// The number of channels.
|
||||
ChannelCount int
|
||||
|
||||
// The audio codec.
|
||||
Codec string
|
||||
|
||||
// The average bit rate in bits per second.
|
||||
Bitrate int
|
||||
}
|
||||
|
||||
// Player encapsulates the mpv instance and provides functions
|
||||
// to control it and to check its status.
|
||||
type Player struct {
|
||||
@@ -413,6 +432,29 @@ func (p *Player) SetAudioDevice(deviceName string) error {
|
||||
return p.mpv.SetPropertyString("audio-device", deviceName)
|
||||
}
|
||||
|
||||
func (p *Player) GetMediaInfo() (MediaInfo, error) {
|
||||
var info MediaInfo
|
||||
n, err := p.mpv.GetProperty("audio-params", mpv.FORMAT_NODE)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
nodeMap := n.(*mpv.Node).Data.(map[string]*mpv.Node)
|
||||
info.Format = nodeMap["format"].Data.(string)
|
||||
info.Samplerate = int(nodeMap["samplerate"].Data.(int64))
|
||||
info.ChannelCount = int(nodeMap["channel-count"].Data.(int64))
|
||||
|
||||
br, err := p.mpv.GetProperty("audio-bitrate", mpv.FORMAT_INT64)
|
||||
if err == nil {
|
||||
info.Bitrate = int(br.(int64))
|
||||
}
|
||||
codec, err := p.mpv.GetProperty("track-list/0/codec", mpv.FORMAT_STRING)
|
||||
if err == nil {
|
||||
info.Codec = codec.(string)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *Player) getInt64Property(propName string) (int64, error) {
|
||||
playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,6 +39,10 @@ type CanShowNowPlaying interface {
|
||||
OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track)
|
||||
}
|
||||
|
||||
type CanShowPlayTime interface {
|
||||
OnPlayTimeUpdate(curTime, totalTime float64)
|
||||
}
|
||||
|
||||
type BrowsingPane struct {
|
||||
widget.BaseWidget
|
||||
|
||||
@@ -66,6 +70,7 @@ func NewBrowsingPane(app *backend.App) *BrowsingPane {
|
||||
b.forward = widget.NewButtonWithIcon("", theme.NavigateNextIcon(), b.GoForward)
|
||||
b.reload = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), b.Reload)
|
||||
b.app.PlaybackManager.OnSongChange(b.onSongChange)
|
||||
b.app.PlaybackManager.OnPlayTimeUpdate(b.onPlayTimeUpdate)
|
||||
bkgrnd := myTheme.NewThemedRectangle(myTheme.ColorNamePageBackground)
|
||||
b.pageContainer = container.NewMax(bkgrnd, layout.NewSpacer())
|
||||
b.settingsBtn = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
|
||||
@@ -175,6 +180,15 @@ func (b *BrowsingPane) onSongChange(song, lastScrobbledIfAny *mediaprovider.Trac
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) onPlayTimeUpdate(cur, total float64) {
|
||||
if b.curPage == nil {
|
||||
return
|
||||
}
|
||||
if p, ok := b.curPage.(CanShowPlayTime); ok {
|
||||
p.OnPlayTimeUpdate(cur, total)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) addPageToHistory(p Page, truncate bool) {
|
||||
if truncate {
|
||||
// allow garbage collection of pages that will be removed from the history
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package browsing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend"
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/player"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
"github.com/dweymouth/supersonic/ui/controller"
|
||||
"github.com/dweymouth/supersonic/ui/layouts"
|
||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||
"github.com/dweymouth/supersonic/ui/util"
|
||||
"github.com/dweymouth/supersonic/ui/widgets"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
@@ -18,8 +26,12 @@ type NowPlayingPage struct {
|
||||
|
||||
nowPlayingPageState
|
||||
|
||||
queue []*mediaprovider.Track
|
||||
totalTime float64
|
||||
|
||||
title *widget.RichText
|
||||
tracklist *widgets.Tracklist
|
||||
statusLabel *widget.RichText
|
||||
nowPlayingID string
|
||||
container *fyne.Container
|
||||
}
|
||||
@@ -28,6 +40,7 @@ type nowPlayingPageState struct {
|
||||
contr *controller.Controller
|
||||
conf *backend.NowPlayingPageConfig
|
||||
pm *backend.PlaybackManager
|
||||
p *player.Player
|
||||
}
|
||||
|
||||
func NewNowPlayingPage(
|
||||
@@ -35,9 +48,15 @@ func NewNowPlayingPage(
|
||||
contr *controller.Controller,
|
||||
conf *backend.NowPlayingPageConfig,
|
||||
pm *backend.PlaybackManager,
|
||||
p *player.Player, // TODO: once other player backends are supported (eg uPnP), refactor
|
||||
) *NowPlayingPage {
|
||||
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, conf: conf, pm: pm}}
|
||||
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, conf: conf, pm: pm, p: p}}
|
||||
a.ExtendBaseWidget(a)
|
||||
|
||||
p.OnPaused(a.formatStatusLine)
|
||||
p.OnPlaying(a.formatStatusLine)
|
||||
p.OnStopped(a.formatStatusLine)
|
||||
|
||||
a.tracklist = widgets.NewTracklist(nil)
|
||||
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
||||
a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
|
||||
@@ -53,8 +72,13 @@ func NewNowPlayingPage(
|
||||
}
|
||||
a.title = widget.NewRichTextWithText("Now Playing")
|
||||
a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName
|
||||
a.statusLabel = widget.NewRichTextWithText("Stopped")
|
||||
statusLabelCtr := container.New(&layouts.VboxCustomPadding{ExtraPad: -5},
|
||||
myTheme.NewThemedRectangle(theme.ColorNameInputBorder),
|
||||
a.statusLabel,
|
||||
)
|
||||
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
|
||||
container.NewBorder(a.title, nil, nil, nil, a.tracklist))
|
||||
container.NewBorder(a.title, statusLabelCtr, nil, nil, a.tracklist))
|
||||
a.load(highlightedTrackID)
|
||||
return a
|
||||
}
|
||||
@@ -80,6 +104,8 @@ func (a *NowPlayingPage) SelectAll() {
|
||||
a.tracklist.SelectAll()
|
||||
}
|
||||
|
||||
var _ CanShowNowPlaying = (*NowPlayingPage)(nil)
|
||||
|
||||
func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
|
||||
if song == nil {
|
||||
a.nowPlayingID = ""
|
||||
@@ -90,6 +116,62 @@ func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr
|
||||
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
|
||||
}
|
||||
|
||||
var _ CanShowPlayTime = (*NowPlayingPage)(nil)
|
||||
|
||||
func (a *NowPlayingPage) OnPlayTimeUpdate(_, _ float64) {
|
||||
a.formatStatusLine()
|
||||
}
|
||||
|
||||
func (a *NowPlayingPage) formatStatusLine() {
|
||||
playerStats := a.p.GetStatus()
|
||||
ts := a.statusLabel.Segments[0].(*widget.TextSegment)
|
||||
state := "Stopped"
|
||||
switch playerStats.State {
|
||||
case player.Paused:
|
||||
state = "Paused"
|
||||
case player.Playing:
|
||||
state = "Playing"
|
||||
}
|
||||
|
||||
dur := 0.0
|
||||
if np := a.pm.NowPlaying(); np != nil {
|
||||
dur = float64(np.Duration)
|
||||
}
|
||||
statusSuffix := ""
|
||||
trackNum := 0
|
||||
if state != "Stopped" {
|
||||
trackNum = a.pm.NowPlayingIndex() + 1
|
||||
statusSuffix = fmt.Sprintf(" %s/%s",
|
||||
util.SecondsToTimeString(playerStats.TimePos),
|
||||
util.SecondsToTimeString(dur))
|
||||
}
|
||||
status := fmt.Sprintf("%s (%d/%d)%s", state, trackNum,
|
||||
len(a.queue), statusSuffix)
|
||||
|
||||
if state == "Stopped" {
|
||||
ts.Text = fmt.Sprintf("%s | Total time: %s", status, util.SecondsToTimeString(a.totalTime))
|
||||
} else {
|
||||
audioInfo, err := a.p.GetMediaInfo()
|
||||
if err != nil {
|
||||
log.Printf("error getting playback status: %s", err.Error())
|
||||
}
|
||||
codec := audioInfo.Codec
|
||||
if len(codec) <= 4 && !strings.EqualFold(codec, "opus") {
|
||||
codec = strings.ToUpper(codec) // FLAC, MP3, AAC, etc
|
||||
}
|
||||
|
||||
// Note: bit depth intentionally omitted since MPV reports the decoded bit depth
|
||||
// i.e. 24 bit files get reported as 32 bit. Also b/c bit depth isn't meaningful for lossy.
|
||||
ts.Text = fmt.Sprintf("%s · %s %g kHz, %d kbps | Total time: %s",
|
||||
status,
|
||||
codec,
|
||||
float64(audioInfo.Samplerate)/1000,
|
||||
audioInfo.Bitrate/1000,
|
||||
util.SecondsToTimeString(a.totalTime))
|
||||
}
|
||||
a.statusLabel.Refresh()
|
||||
}
|
||||
|
||||
func (a *NowPlayingPage) Reload() {
|
||||
a.load("")
|
||||
}
|
||||
@@ -106,14 +188,19 @@ func (a *NowPlayingPage) onRemoveSelectedFromQueue() {
|
||||
|
||||
// does not make calls to server - can safely be run in UI callbacks
|
||||
func (a *NowPlayingPage) load(highlightedTrackID string) {
|
||||
queue := a.pm.GetPlayQueue()
|
||||
a.tracklist.SetTracks(queue)
|
||||
a.queue = a.pm.GetPlayQueue()
|
||||
a.tracklist.SetTracks(a.queue)
|
||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||
if highlightedTrackID != "" {
|
||||
a.tracklist.SelectAndScrollToTrack(highlightedTrackID)
|
||||
}
|
||||
a.totalTime = 0.0
|
||||
for _, tr := range a.queue {
|
||||
a.totalTime += float64(tr.Duration)
|
||||
}
|
||||
a.formatStatusLine()
|
||||
}
|
||||
|
||||
func (s *nowPlayingPageState) Restore() Page {
|
||||
return NewNowPlayingPage("", s.contr, s.conf, s.pm)
|
||||
return NewNowPlayingPage("", s.contr, s.conf, s.pm, s.p)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
|
||||
case controller.Genres:
|
||||
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
|
||||
case controller.NowPlaying:
|
||||
return NewNowPlayingPage(rte.Arg, r.Controller, &r.App.Config.NowPlayingPage, r.App.PlaybackManager)
|
||||
return NewNowPlayingPage(rte.Arg, r.Controller, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, r.App.Player)
|
||||
case controller.Playlist:
|
||||
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
|
||||
case controller.Playlists:
|
||||
|
||||
Reference in New Issue
Block a user