very WIP for DLNA casting
This commit is contained in:
+57
-49
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user