gapless DLNA working

This commit is contained in:
Drew Weymouth
2025-03-29 15:15:13 -07:00
parent 1ad45a33db
commit 5a37b1e39f
6 changed files with 143 additions and 68 deletions
+2 -2
View File
@@ -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
View File
@@ -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
}
+105 -43
View File
@@ -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)
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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 {