gapless DLNA working
This commit is contained in:
+2
-2
@@ -404,7 +404,7 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) {
|
||||
})
|
||||
a.PlaybackManager.OnSeek(func() {
|
||||
dur := a.PlaybackManager.NowPlaying().Metadata().Duration
|
||||
smtc.UpdatePosition(int(a.PlaybackManager.CurrentPlayer().GetStatus().TimePos*1000), dur*1000)
|
||||
smtc.UpdatePosition(int(a.PlaybackManager.PlayerStatus().TimePos*1000), dur*1000)
|
||||
})
|
||||
a.PlaybackManager.OnPlaying(func() {
|
||||
smtc.SetEnabled(true)
|
||||
@@ -465,7 +465,7 @@ func (a *App) Shutdown() {
|
||||
a.PlaybackManager.DisableCallbacks()
|
||||
a.PlaybackManager.Stop() // will trigger scrobble check
|
||||
a.cancel()
|
||||
//a.LocalPlayer.Destroy()
|
||||
a.LocalPlayer.Destroy()
|
||||
}
|
||||
|
||||
func (a *App) SavePlayQueueIfEnabled() {
|
||||
|
||||
+29
-17
@@ -54,9 +54,13 @@ type playbackEngine struct {
|
||||
loopMode LoopMode
|
||||
|
||||
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
|
||||
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
|
||||
noIncrementNextTrackChange bool // true iff the nowPlayingIndex should not be incremented on the next onTrackChange
|
||||
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
|
||||
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
|
||||
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
|
||||
|
||||
// if >= 0, track number that was requested by PlayTrackAt
|
||||
// onTrackChange callback should set nowPlayingIdx to this,
|
||||
// and reset this to -1
|
||||
pendingTrackChangeNum int
|
||||
|
||||
pendingPlayerChange bool
|
||||
pendingPlayerChangeTimePos float64
|
||||
@@ -136,7 +140,7 @@ func (p *playbackEngine) registerPlayerCallbacks(pl player.BasePlayer) {
|
||||
func (p *playbackEngine) SetPlayer(pl player.BasePlayer) {
|
||||
needToUnpause := false
|
||||
|
||||
stat := p.player.GetStatus()
|
||||
stat := p.PlayerStatus()
|
||||
switch stat.State {
|
||||
case player.Stopped:
|
||||
// nothing
|
||||
@@ -149,6 +153,7 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) {
|
||||
needToUnpause = true
|
||||
}
|
||||
|
||||
oldVol := p.player.GetVolume()
|
||||
p.player = pl
|
||||
p.registerPlayerCallbacks(pl)
|
||||
|
||||
@@ -158,6 +163,12 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) {
|
||||
p.pendingPlayerChange = false
|
||||
}
|
||||
p.invokeNoArgCallbacks(p.onPlayerChange)
|
||||
vol := pl.GetVolume()
|
||||
if oldVol != vol {
|
||||
for _, cb := range p.onVolumeChange {
|
||||
cb(vol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playbackEngine) PlayTrackAt(idx int) error {
|
||||
@@ -167,11 +178,8 @@ func (p *playbackEngine) PlayTrackAt(idx int) error {
|
||||
// scrobble current track if needed
|
||||
p.checkScrobble()
|
||||
p.alreadyScrobbled = true
|
||||
p.noIncrementNextTrackChange = true
|
||||
p.pendingTrackChangeNum = idx
|
||||
err := p.setTrack(idx, false)
|
||||
if err == nil {
|
||||
p.nowPlayingIdx = idx
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -222,14 +230,14 @@ func (p *playbackEngine) CurrentPlayer() player.BasePlayer {
|
||||
}
|
||||
|
||||
func (p *playbackEngine) SeekNext() error {
|
||||
if p.CurrentPlayer().GetStatus().State == player.Stopped {
|
||||
if p.PlayerStatus().State == player.Stopped {
|
||||
return nil
|
||||
}
|
||||
return p.PlayTrackAt(p.nowPlayingIdx + 1)
|
||||
}
|
||||
|
||||
func (p *playbackEngine) SeekBackOrPrevious() error {
|
||||
if p.nowPlayingIdx == 0 || p.player.GetStatus().TimePos > 3 {
|
||||
if p.nowPlayingIdx == 0 || p.PlayerStatus().TimePos > 3 {
|
||||
return p.player.SeekSeconds(0)
|
||||
}
|
||||
return p.PlayTrackAt(p.nowPlayingIdx - 1)
|
||||
@@ -237,7 +245,7 @@ func (p *playbackEngine) SeekBackOrPrevious() error {
|
||||
|
||||
func (p *playbackEngine) SeekFwdBackN(n int) error {
|
||||
idx := p.nowPlayingIdx
|
||||
if n < 0 && p.player.GetStatus().TimePos > 3 {
|
||||
if n < 0 && p.PlayerStatus().TimePos > 3 {
|
||||
n += 1 // first seek back is just seek to beginning of current
|
||||
}
|
||||
if n == 0 || (idx == 0 && n < 0) {
|
||||
@@ -511,21 +519,23 @@ func (p *playbackEngine) handleOnTrackChange() {
|
||||
p.checkScrobble()
|
||||
}
|
||||
|
||||
if p.player.GetStatus().State == player.Playing {
|
||||
if p.PlayerStatus().State == player.Playing {
|
||||
p.playTimeStopwatch.Start()
|
||||
}
|
||||
if !p.noIncrementNextTrackChange && (p.wasStopped || p.loopMode != LoopOne) {
|
||||
if p.pendingTrackChangeNum < 0 && (p.wasStopped || p.loopMode != LoopOne) {
|
||||
p.nowPlayingIdx++
|
||||
if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) {
|
||||
p.nowPlayingIdx = 0 // wrapped around
|
||||
}
|
||||
} else if p.pendingTrackChangeNum >= 0 {
|
||||
p.nowPlayingIdx = p.pendingTrackChangeNum
|
||||
p.pendingTrackChangeNum = -1
|
||||
}
|
||||
nowPlaying := p.playQueue[p.nowPlayingIdx]
|
||||
_, isRadio := nowPlaying.(*mediaprovider.RadioStation)
|
||||
p.isRadio = isRadio
|
||||
|
||||
// reset flags
|
||||
p.noIncrementNextTrackChange = false
|
||||
p.wasStopped = false
|
||||
p.alreadyScrobbled = false
|
||||
|
||||
@@ -594,9 +604,11 @@ func (p *playbackEngine) setNextTrackAfterQueueUpdate() {
|
||||
func (p *playbackEngine) setTrack(idx int, next bool) error {
|
||||
if urlP, ok := p.player.(player.URLPlayer); ok {
|
||||
url := ""
|
||||
var meta mediaprovider.MediaItemMetadata
|
||||
if idx >= 0 {
|
||||
var err error
|
||||
item := p.playQueue[idx]
|
||||
meta = item.Metadata()
|
||||
if tr, ok := item.(*mediaprovider.Track); ok {
|
||||
url, err = p.sm.Server.GetStreamURL(tr.ID, p.transcodeCfg.ForceRawFile)
|
||||
} else {
|
||||
@@ -607,9 +619,9 @@ func (p *playbackEngine) setTrack(idx int, next bool) error {
|
||||
}
|
||||
}
|
||||
if next {
|
||||
return urlP.SetNextFile(url)
|
||||
return urlP.SetNextFile(url, meta)
|
||||
}
|
||||
return urlP.PlayFile(url)
|
||||
return urlP.PlayFile(url, meta)
|
||||
} else if trP, ok := p.player.(player.TrackPlayer); ok {
|
||||
var track *mediaprovider.Track
|
||||
if idx >= 0 {
|
||||
@@ -744,7 +756,7 @@ func (p *playbackEngine) doUpdateTimePos(seeked bool) {
|
||||
if p.callbacksDisabled {
|
||||
return
|
||||
}
|
||||
s := p.player.GetStatus()
|
||||
s := p.PlayerStatus()
|
||||
if s.TimePos > p.latestTrackPosition {
|
||||
p.latestTrackPosition = s.TimePos
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package dlna
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -9,12 +11,16 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
"github.com/supersonic-app/go-upnpcast/device"
|
||||
"github.com/supersonic-app/go-upnpcast/services/avtransport"
|
||||
"github.com/supersonic-app/go-upnpcast/services/renderingcontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,10 +34,25 @@ var unimplemented = errors.New("unimplemented")
|
||||
type DLNAPlayer struct {
|
||||
player.BasePlayerCallbackImpl
|
||||
|
||||
avTransport *avtransport.Client
|
||||
avTransport *avtransport.Client
|
||||
renderControl *renderingcontrol.Client
|
||||
|
||||
state int // stopped, playing, paused
|
||||
seeking bool
|
||||
|
||||
curTrackMeta mediaprovider.MediaItemMetadata
|
||||
nextTrackMeta mediaprovider.MediaItemMetadata
|
||||
|
||||
lastSeekSecs int
|
||||
seekedAt time.Time
|
||||
|
||||
proxyServer *http.Server
|
||||
proxyActive atomic.Bool
|
||||
localIP string
|
||||
proxyPort int
|
||||
|
||||
proxyURLLock sync.Mutex
|
||||
proxyURLs map[string]string
|
||||
}
|
||||
|
||||
func NewDLNAPlayer(device *device.MediaRenderer) (*DLNAPlayer, error) {
|
||||
@@ -39,29 +60,38 @@ func NewDLNAPlayer(device *device.MediaRenderer) (*DLNAPlayer, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DLNAPlayer{avTransport: avt}, nil
|
||||
rc, err := device.RenderingControlClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DLNAPlayer{
|
||||
avTransport: avt,
|
||||
renderControl: rc,
|
||||
proxyURLs: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) SetVolume(vol int) error {
|
||||
return unimplemented
|
||||
return d.renderControl.SetVolume(context.Background(), vol)
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) GetVolume() int {
|
||||
return 0
|
||||
vol, _ := d.renderControl.GetVolume(context.Background())
|
||||
return vol
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) PlayFile(urlstr string) error {
|
||||
ensureSetupProxies()
|
||||
func (d *DLNAPlayer) PlayFile(urlstr string, meta mediaprovider.MediaItemMetadata) error {
|
||||
d.ensureSetupProxy()
|
||||
|
||||
proxyURLLock.Lock()
|
||||
dlnaProxyCurrent.url = urlstr
|
||||
proxyURLLock.Unlock()
|
||||
log.Println("playing track " + meta.Name)
|
||||
|
||||
d.curTrackMeta = meta
|
||||
key := d.addURLToProxy(urlstr)
|
||||
|
||||
media := avtransport.MediaItem{
|
||||
URL: "http://" + localIP + ":8080/current",
|
||||
Title: "Supersonic media item",
|
||||
URL: d.urlForItem(key),
|
||||
Title: meta.Name,
|
||||
}
|
||||
log.Printf("URL %s", media.URL)
|
||||
|
||||
err := d.avTransport.SetAVTransportMedia(context.Background(), &media)
|
||||
if err != nil {
|
||||
@@ -71,26 +101,41 @@ func (d *DLNAPlayer) PlayFile(urlstr string) error {
|
||||
return err
|
||||
}
|
||||
d.state = playing
|
||||
d.seekedAt = time.Now()
|
||||
d.lastSeekSecs = 0
|
||||
d.InvokeOnPlaying()
|
||||
d.InvokeOnTrackChange()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) SetNextFile(url string) error {
|
||||
func (d *DLNAPlayer) SetNextFile(url string, meta mediaprovider.MediaItemMetadata) error {
|
||||
var media *avtransport.MediaItem
|
||||
d.nextTrackMeta = meta
|
||||
if url != "" {
|
||||
ensureSetupProxies()
|
||||
|
||||
proxyURLLock.Lock()
|
||||
dlnaProxyCurrent.url = url
|
||||
proxyURLLock.Unlock()
|
||||
d.ensureSetupProxy()
|
||||
|
||||
key := d.addURLToProxy(url)
|
||||
media = &avtransport.MediaItem{
|
||||
URL: "http://" + localIP + ":8080/next",
|
||||
URL: d.urlForItem(key),
|
||||
Title: meta.Name,
|
||||
}
|
||||
}
|
||||
return d.avTransport.SetNextAVTransportMedia(context.Background(), media)
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) addURLToProxy(url string) string {
|
||||
hash := md5.Sum([]byte(url))
|
||||
key := base64.StdEncoding.EncodeToString(hash[:])
|
||||
d.proxyURLLock.Lock()
|
||||
d.proxyURLs[key] = url
|
||||
d.proxyURLLock.Unlock()
|
||||
return key
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) urlForItem(key string) string {
|
||||
return fmt.Sprintf("http://%s:%d/%s", d.localIP, d.proxyPort, key)
|
||||
}
|
||||
|
||||
func (d *DLNAPlayer) Continue() error {
|
||||
if err := d.avTransport.Play(context.Background()); err != nil {
|
||||
return err
|
||||
@@ -125,6 +170,8 @@ func (d *DLNAPlayer) SeekSeconds(secs float64) error {
|
||||
return err
|
||||
}
|
||||
d.seeking = false
|
||||
d.seekedAt = time.Now()
|
||||
d.lastSeekSecs = int(secs)
|
||||
d.InvokeOnSeek()
|
||||
return nil
|
||||
}
|
||||
@@ -141,10 +188,12 @@ func (d *DLNAPlayer) GetStatus() player.Status {
|
||||
state = player.Paused
|
||||
}
|
||||
|
||||
// TODO - the rest
|
||||
time := time.Now().Sub(d.seekedAt) + time.Duration(d.lastSeekSecs)*time.Second
|
||||
|
||||
return player.Status{
|
||||
State: state,
|
||||
State: state,
|
||||
TimePos: time.Seconds(),
|
||||
Duration: float64(d.curTrackMeta.Duration),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,38 +224,51 @@ func getLocalIP() (string, error) {
|
||||
return "", fmt.Errorf("no suitable interface found")
|
||||
}
|
||||
|
||||
var (
|
||||
localIP string
|
||||
proxyURLLock sync.Mutex
|
||||
dlnaProxyCurrent proxy
|
||||
dlnaProxyNext proxy
|
||||
proxyActive atomic.Bool
|
||||
)
|
||||
|
||||
func ensureSetupProxies() {
|
||||
if proxyActive.Swap(true) {
|
||||
return // already active
|
||||
func (d *DLNAPlayer) ensureSetupProxy() error {
|
||||
if d.proxyActive.Swap(true) {
|
||||
return nil // already active
|
||||
}
|
||||
|
||||
localIP, _ = getLocalIP()
|
||||
log.Println(localIP)
|
||||
var err error
|
||||
d.localIP, err = getLocalIP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/current", dlnaProxyCurrent.handleRequest)
|
||||
mux.HandleFunc("/next", dlnaProxyNext.handleRequest)
|
||||
go http.ListenAndServe(":8080", mux)
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.proxyPort = listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
d.proxyServer = &http.Server{
|
||||
Handler: http.HandlerFunc(d.handleRequest),
|
||||
}
|
||||
|
||||
go d.proxyServer.Serve(listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
type proxy struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func (p *proxy) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DLNAPlayer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
var url string
|
||||
key := strings.TrimPrefix(r.URL.Path, "/")
|
||||
d.proxyURLLock.Lock()
|
||||
if u, ok := d.proxyURLs[key]; ok {
|
||||
url = u
|
||||
}
|
||||
d.proxyURLLock.Unlock()
|
||||
|
||||
if url == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("404"))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new request to the target server
|
||||
proxyURLLock.Lock()
|
||||
url := p.url
|
||||
proxyURLLock.Unlock()
|
||||
log.Println("Got request for " + url)
|
||||
proxyReq, err := http.NewRequest(r.Method, url, r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
"github.com/supersonic-app/go-mpv"
|
||||
)
|
||||
@@ -133,7 +134,7 @@ func (p *Player) Init(maxCacheMB int) error {
|
||||
}
|
||||
|
||||
// Plays the specified file, clearing the previous play queue, if any.
|
||||
func (p *Player) PlayFile(url string) error {
|
||||
func (p *Player) PlayFile(url string, _ mediaprovider.MediaItemMetadata) error {
|
||||
if !p.initialized {
|
||||
return ErrUnitialized
|
||||
}
|
||||
@@ -169,7 +170,7 @@ func (p *Player) Stop() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Player) SetNextFile(url string) error {
|
||||
func (p *Player) SetNextFile(url string, _ mediaprovider.MediaItemMetadata) error {
|
||||
if p.lenPlaylist > p.curPlaylistPos+1 {
|
||||
if err := p.mpv.Command([]string{"playlist-remove", strconv.Itoa(int(p.curPlaylistPos) + 1)}); err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,8 +4,8 @@ import "github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
|
||||
type URLPlayer interface {
|
||||
BasePlayer
|
||||
PlayFile(url string) error
|
||||
SetNextFile(url string) error
|
||||
PlayFile(url string, metadata mediaprovider.MediaItemMetadata) error
|
||||
SetNextFile(url string, metadata mediaprovider.MediaItemMetadata) error
|
||||
}
|
||||
|
||||
type TrackPlayer interface {
|
||||
|
||||
@@ -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.PlayerStatus().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.PlayerStatus()
|
||||
lastStatus := a.statusLabel.Text
|
||||
stopped := lang.L("Stopped")
|
||||
state := stopped
|
||||
|
||||
Reference in New Issue
Block a user