very WIP for DLNA casting

This commit is contained in:
Drew Weymouth
2025-03-29 15:15:13 -07:00
parent 31c23490d1
commit 4f731833c0
6 changed files with 363 additions and 101 deletions
+57 -49
View File
@@ -11,16 +11,18 @@ import (
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"time"
"github.com/dweymouth/supersonic/backend/ipc"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/dlna"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/backend/util"
"github.com/google/uuid"
"github.com/supersonic-app/go-upnpcast/device"
"github.com/supersonic-app/go-upnpcast/services"
"github.com/20after4/configdir"
"github.com/zalando/go-keyring"
@@ -43,7 +45,7 @@ type App struct {
ServerManager *ServerManager
ImageManager *ImageManager
PlaybackManager *PlaybackManager
LocalPlayer *mpv.Player
LocalPlayer player.BasePlayer
UpdateChecker UpdateChecker
MPRISHandler *MPRISHandler
WinSMTC *SMTC
@@ -273,7 +275,11 @@ func (a *App) initMPV() error {
if err := p.Init(c.InMemoryCacheSizeMB); err != nil {
return fmt.Errorf("failed to initialize mpv player: %s", err.Error())
}
a.LocalPlayer = p
// a.LocalPlayer = p
devices, _ := device.SearchMediaRenderers(context.Background(), 10, services.AVTransport)
if len(devices) > 0 {
a.LocalPlayer, _ = dlna.NewDLNAPlayer(devices[0])
}
return nil
}
@@ -281,55 +287,57 @@ func (a *App) setupMPV() error {
a.Config.LocalPlayback.Volume = clamp(a.Config.LocalPlayback.Volume, 0, 100)
a.LocalPlayer.SetVolume(a.Config.LocalPlayback.Volume)
devs, err := a.LocalPlayer.ListAudioDevices()
if err != nil {
return err
}
desiredDevice := a.Config.LocalPlayback.AudioDeviceName
var desiredDeviceAvailable bool
for _, dev := range devs {
if dev.Name == desiredDevice {
desiredDeviceAvailable = true
break
/*
devs, err := a.LocalPlayer.ListAudioDevices()
if err != nil {
return err
}
}
if !desiredDeviceAvailable {
// The audio device the user has configured is not available.
// Use the default (autoselect) device but leave the setting unchanged,
// in case the device is later available on a subsequent run of the app
// (e.g. a USB audio device that is currently unplugged)
desiredDevice = "auto"
}
a.LocalPlayer.SetAudioDevice(desiredDevice)
rgainOpts := []string{ReplayGainNone, ReplayGainAlbum, ReplayGainTrack, ReplayGainAuto}
if !slices.Contains(rgainOpts, a.Config.ReplayGain.Mode) {
a.Config.ReplayGain.Mode = ReplayGainNone
}
mode := player.ReplayGainNone
switch a.Config.ReplayGain.Mode {
case ReplayGainAlbum:
mode = player.ReplayGainAlbum
case ReplayGainTrack:
mode = player.ReplayGainTrack
case ReplayGainAuto:
mode = player.ReplayGainTrack
}
desiredDevice := a.Config.LocalPlayback.AudioDeviceName
var desiredDeviceAvailable bool
for _, dev := range devs {
if dev.Name == desiredDevice {
desiredDeviceAvailable = true
break
}
}
if !desiredDeviceAvailable {
// The audio device the user has configured is not available.
// Use the default (autoselect) device but leave the setting unchanged,
// in case the device is later available on a subsequent run of the app
// (e.g. a USB audio device that is currently unplugged)
desiredDevice = "auto"
}
a.LocalPlayer.SetAudioDevice(desiredDevice)
a.LocalPlayer.SetReplayGainOptions(player.ReplayGainOptions{
Mode: mode,
PreventClipping: a.Config.ReplayGain.PreventClipping,
PreampGain: a.Config.ReplayGain.PreampGainDB,
})
a.LocalPlayer.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
rgainOpts := []string{ReplayGainNone, ReplayGainAlbum, ReplayGainTrack, ReplayGainAuto}
if !slices.Contains(rgainOpts, a.Config.ReplayGain.Mode) {
a.Config.ReplayGain.Mode = ReplayGainNone
}
mode := player.ReplayGainNone
switch a.Config.ReplayGain.Mode {
case ReplayGainAlbum:
mode = player.ReplayGainAlbum
case ReplayGainTrack:
mode = player.ReplayGainTrack
case ReplayGainAuto:
mode = player.ReplayGainTrack
}
eq := &mpv.ISO15BandEqualizer{
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
}
copy(eq.BandGains[:], a.Config.LocalPlayback.GraphicEqualizerBands)
a.LocalPlayer.SetEqualizer(eq)
a.LocalPlayer.SetReplayGainOptions(player.ReplayGainOptions{
Mode: mode,
PreventClipping: a.Config.ReplayGain.PreventClipping,
PreampGain: a.Config.ReplayGain.PreampGainDB,
})
a.LocalPlayer.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
eq := &mpv.ISO15BandEqualizer{
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
}
copy(eq.BandGains[:], a.Config.LocalPlayback.GraphicEqualizerBands)
a.LocalPlayer.SetEqualizer(eq)
*/
return nil
}
@@ -450,7 +458,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() {
+243
View File
@@ -0,0 +1,243 @@
package dlna
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"sync"
"sync/atomic"
"github.com/dweymouth/supersonic/backend/player"
"github.com/supersonic-app/go-upnpcast/device"
"github.com/supersonic-app/go-upnpcast/services/avtransport"
)
const (
stopped = 0
playing = 1
paused = 2
)
var unimplemented = errors.New("unimplemented")
type DLNAPlayer struct {
player.BasePlayerCallbackImpl
avTransport *avtransport.Client
state int // stopped, playing, paused
seeking bool
}
func NewDLNAPlayer(device *device.MediaRenderer) (*DLNAPlayer, error) {
avt, err := device.AVTransportClient()
if err != nil {
return nil, err
}
return &DLNAPlayer{avTransport: avt}, nil
}
func (d *DLNAPlayer) SetVolume(vol int) error {
return unimplemented
}
func (d *DLNAPlayer) GetVolume() int {
return 0
}
func (d *DLNAPlayer) PlayFile(urlstr string) error {
ensureSetupProxies()
proxyURLLock.Lock()
dlnaProxyCurrent.url = urlstr
proxyURLLock.Unlock()
media := avtransport.MediaItem{
URL: "http://" + localIP + ":8080/current",
Title: "Supersonic media item",
}
log.Printf("URL %s", media.URL)
err := d.avTransport.SetAVTransportMedia(context.Background(), &media)
if err != nil {
return err
}
if err := d.avTransport.Play(context.Background()); err != nil {
return err
}
d.state = playing
d.InvokeOnPlaying()
return nil
}
func (d *DLNAPlayer) SetNextFile(url string) error {
var media *avtransport.MediaItem
if url != "" {
ensureSetupProxies()
proxyURLLock.Lock()
dlnaProxyCurrent.url = url
proxyURLLock.Unlock()
media = &avtransport.MediaItem{
URL: "http://" + localIP + ":8080/next",
}
}
return d.avTransport.SetNextAVTransportMedia(context.Background(), media)
}
func (d *DLNAPlayer) Continue() error {
if err := d.avTransport.Play(context.Background()); err != nil {
return err
}
d.state = playing
d.InvokeOnPlaying()
return nil
}
func (d *DLNAPlayer) Pause() error {
if err := d.avTransport.Pause(context.Background()); err != nil {
return err
}
d.state = paused
d.InvokeOnPaused()
return nil
}
func (d *DLNAPlayer) Stop() error {
if err := d.avTransport.Pause(context.Background()); err != nil {
return err
}
d.state = stopped
d.InvokeOnStopped()
return nil
}
func (d *DLNAPlayer) SeekSeconds(secs float64) error {
d.seeking = true
if err := d.avTransport.Seek(context.Background(), int(secs)); err != nil {
d.seeking = false
return err
}
d.seeking = false
d.InvokeOnSeek()
return nil
}
func (d *DLNAPlayer) IsSeeking() bool {
return d.seeking
}
func (d *DLNAPlayer) GetStatus() player.Status {
state := player.Stopped
if d.state == playing {
state = player.Playing
} else if d.state == paused {
state = player.Paused
}
// TODO - the rest
return player.Status{
State: state,
}
}
func getLocalIP() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
return ipnet.IP.String(), nil
}
}
}
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
}
localIP, _ = getLocalIP()
log.Println(localIP)
mux := http.NewServeMux()
mux.HandleFunc("/current", dlnaProxyCurrent.handleRequest)
mux.HandleFunc("/next", dlnaProxyNext.handleRequest)
go http.ListenAndServe(":8080", mux)
}
type proxy struct {
url string
}
func (p *proxy) handleRequest(w http.ResponseWriter, r *http.Request) {
// 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)
return
}
// Copy headers from the original request to the new request
proxyReq.Header = r.Header
// Create an HTTP client and send the request
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// Copy headers from the response to the writer
for name, values := range resp.Header {
for _, value := range values {
w.Header().Add(name, value)
}
}
// Set the status code
w.WriteHeader(resp.StatusCode)
// Copy the response body to the writer
_, err = io.Copy(w, resp.Body)
if err != nil {
fmt.Fprintln(os.Stderr, "Error copying response body:", err)
}
}