Gapless playback improvements. UI improvements

This commit is contained in:
Alex Ballas
2023-01-27 00:33:27 +02:00
parent 4d9415779b
commit 6f2c42b176
8 changed files with 331 additions and 54 deletions

View File

@@ -158,6 +158,37 @@ func playAction(screen *NewScreen) {
screen.PlayPause.Disable()
if screen.cancelEnablePlay != nil {
screen.cancelEnablePlay()
}
ctx, cancelEnablePlay := context.WithTimeout(context.Background(), 5*time.Second)
screen.cancelEnablePlay = cancelEnablePlay
go func() {
<-ctx.Done()
defer func() { screen.cancelEnablePlay = nil }()
if errors.Is(ctx.Err(), context.Canceled) {
return
}
out, err := screen.tvdata.GetTransportInfo()
if err != nil {
return
}
switch out[0] {
case "PLAYING":
setPlayPauseView("Pause", screen)
screen.updateScreenState("Playing")
case "PAUSED":
setPlayPauseView("Play", screen)
screen.updateScreenState("Paused")
}
}()
currentState := screen.getScreenState()
if currentState == "Paused" {
@@ -183,20 +214,20 @@ func playAction(screen *NewScreen) {
if screen.mediafile == "" && screen.MediaText.Text == "" {
check(screen, errors.New("please select a media file or enter a media URL"))
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
if screen.controlURL == "" {
check(screen, errors.New("please select a device"))
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
whereToListen, err := utils.URLtoListenIPandPort(screen.controlURL)
check(screen, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -207,14 +238,14 @@ func playAction(screen *NewScreen) {
mfile, err := os.Open(screen.mediafile)
check(screen, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaType, err = utils.GetMimeDetailsFromFile(mfile)
check(screen, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -225,7 +256,7 @@ func playAction(screen *NewScreen) {
callbackPath, err := utils.RandomString()
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -246,21 +277,21 @@ func playAction(screen *NewScreen) {
mediaURL, err := utils.StreamURL(context.Background(), screen.MediaText.Text)
check(screen, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaURLinfo, err := utils.StreamURL(context.Background(), screen.MediaText.Text)
check(screen, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo)
check(screen, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -269,7 +300,7 @@ func playAction(screen *NewScreen) {
readerToBytes, err := io.ReadAll(mediaURL)
mediaURL.Close()
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaFile = readerToBytes
@@ -338,6 +369,15 @@ func playAction(screen *NewScreen) {
}
func startAfreshPlayButton(screen *NewScreen) {
if screen.cancelEnablePlay != nil {
screen.cancelEnablePlay()
}
setPlayPauseView("Play", screen)
screen.updateScreenState("Stopped")
}
func gaplessMediaWatcher(ctx context.Context, screen *NewScreen, payload *soapcalls.TVPayload) {
t := time.NewTicker(1 * time.Second)
out:
@@ -345,9 +385,14 @@ out:
select {
case <-t.C:
gaplessOption := fyne.CurrentApp().Preferences().StringWithFallback("Gapless", "Disabled")
gapless, _ := payload.Gapless()
nextURI, _ := payload.Gapless()
if !gapless && gaplessOption == "Enabled" && screen.NextMediaCheck.Checked {
if nextURI == "NOT_IMPLEMENTED" || gaplessOption == "Disabled" {
screen.GaplessMediaWatcher = nil
break out
}
if nextURI == "" && screen.NextMediaCheck.Checked {
screen.MediaText.Text, screen.mediafile = getNextMedia(screen)
screen.MediaText.Refresh()

View File

@@ -128,6 +128,37 @@ func playAction(screen *NewScreen) {
screen.PlayPause.Disable()
if screen.cancelEnablePlay != nil {
screen.cancelEnablePlay()
}
ctx, cancelEnablePlay := context.WithTimeout(context.Background(), 5*time.Second)
screen.cancelEnablePlay = cancelEnablePlay
go func() {
<-ctx.Done()
defer func() { screen.cancelEnablePlay = nil }()
if errors.Is(ctx.Err(), context.Canceled) {
return
}
out, err := screen.tvdata.GetTransportInfo()
if err != nil {
return
}
switch out[0] {
case "PLAYING":
setPlayPauseView("Pause", screen)
screen.updateScreenState("Playing")
case "PAUSED":
setPlayPauseView("Play", screen)
screen.updateScreenState("Paused")
}
}()
currentState := screen.getScreenState()
if currentState == "Paused" {
@@ -153,20 +184,20 @@ func playAction(screen *NewScreen) {
if screen.mediafile == nil && screen.MediaText.Text == "" {
check(w, errors.New("please select a media file or enter a media URL"))
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
if screen.controlURL == "" {
check(w, errors.New("please select a device"))
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
whereToListen, err := utils.URLtoListenIPandPort(screen.controlURL)
check(w, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -174,7 +205,7 @@ func playAction(screen *NewScreen) {
callbackPath, err := utils.RandomString()
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -182,21 +213,21 @@ func playAction(screen *NewScreen) {
mediaURL, err := storage.OpenFileFromURI(screen.mediafile)
check(screen.Current, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaURLinfo, err := storage.OpenFileFromURI(screen.mediafile)
check(screen.Current, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo)
check(w, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -205,7 +236,7 @@ func playAction(screen *NewScreen) {
readerToBytes, err := io.ReadAll(mediaURL)
mediaURL.Close()
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaFile = readerToBytes
@@ -216,7 +247,7 @@ func playAction(screen *NewScreen) {
subsFile, err = storage.OpenFileFromURI(screen.subsfile)
check(screen.Current, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
}
@@ -230,21 +261,21 @@ func playAction(screen *NewScreen) {
mediaURL, err := utils.StreamURL(context.Background(), screen.MediaText.Text)
check(screen.Current, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaURLinfo, err := utils.StreamURL(context.Background(), screen.MediaText.Text)
check(screen.Current, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaType, err = utils.GetMimeDetailsFromStream(mediaURLinfo)
check(w, err)
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
@@ -253,7 +284,7 @@ func playAction(screen *NewScreen) {
readerToBytes, err := io.ReadAll(mediaURL)
mediaURL.Close()
if err != nil {
screen.PlayPause.Enable()
startAfreshPlayButton(screen)
return
}
mediaFile = readerToBytes
@@ -394,3 +425,12 @@ func volumeAction(screen *NewScreen, up bool) {
check(w, errors.New("could not send volume action"))
}
}
func startAfreshPlayButton(screen *NewScreen) {
if screen.cancelEnablePlay != nil {
screen.cancelEnablePlay()
}
setPlayPauseView("Play", screen)
screen.updateScreenState("Stopped")
}

View File

@@ -24,12 +24,12 @@ import (
// NewScreen .
type NewScreen struct {
muError sync.RWMutex
mu sync.RWMutex
serverStopCTX context.Context
muError sync.RWMutex
Current fyne.Window
VolumeUp *widget.Button
tvdata *soapcalls.TVPayload
VolumeDown *widget.Button
MediaText *widget.Entry
Debug *debugWriter
tabs *container.AppTabs
CheckVersion *widget.Button
SubsText *widget.Entry
@@ -38,26 +38,27 @@ type NewScreen struct {
Stop *widget.Button
DeviceList *widget.List
httpserver *httphandlers.HTTPserver
MediaText *widget.Entry
serverStopCTX context.Context
ExternalMediaURL *widget.Check
GaplessMediaWatcher func(context.Context, *NewScreen, *soapcalls.TVPayload)
cancelEnablePlay context.CancelFunc
MuteUnmute *widget.Button
VolumeDown *widget.Button
Debug *debugWriter
NextMediaCheck *widget.Check
VolumeUp *widget.Button
tvdata *soapcalls.TVPayload
selectedDevice devType
State string
subsfile string
currentmfolder string
mediafile string
eventlURL string
controlURL string
renderingControlURL string
connectionManagerURL string
currentmfolder string
State string
version string
eventlURL string
mediafile string
subsfile string
mediaFormats []string
Transcode bool
ErrorVisible bool
NextMediaCheck *widget.Check
Medialoop bool
Hotkeys bool
}
@@ -289,6 +290,10 @@ func getNextPossibleSubs(v string, screen *NewScreen) (string, string) {
}
func setPlayPauseView(s string, screen *NewScreen) {
if screen.cancelEnablePlay != nil {
screen.cancelEnablePlay()
}
screen.PlayPause.Enable()
switch s {
case "Play":

View File

@@ -32,6 +32,7 @@ type NewScreen struct {
CheckVersion *widget.Button
CustomSubsCheck *widget.Check
ExternalMediaURL *widget.Check
cancelEnablePlay context.CancelFunc
MediaText *widget.Entry
SubsText *widget.Entry
DeviceList *widget.List
@@ -157,6 +158,10 @@ func (p *NewScreen) getScreenState() string {
}
func setPlayPauseView(s string, screen *NewScreen) {
if screen.cancelEnablePlay != nil {
screen.cancelEnablePlay()
}
screen.PlayPause.Enable()
switch s {
case "Play":

View File

@@ -1,3 +1,6 @@
//go:build !(android || ios)
// +build !android,!ios
package gui
import (
@@ -109,8 +112,26 @@ func settingsWindow(s *NewScreen) fyne.CanvasObject {
gaplessdropdown := widget.NewSelect([]string{"Enabled", "Disabled"}, func(ss string) {
fyne.CurrentApp().Preferences().SetString("Gapless", ss)
if s.NextMediaCheck.Checked {
s.NextMediaCheck.SetChecked(false)
s.NextMediaCheck.SetChecked(true)
switch ss {
case "Enabled":
switch s.State {
case "Playing", "Paused":
newTVPayload, err := queueNext(s, false)
if err == nil && s.GaplessMediaWatcher == nil {
s.GaplessMediaWatcher = gaplessMediaWatcher
go s.GaplessMediaWatcher(s.serverStopCTX, s, newTVPayload)
}
}
case "Disabled":
// We're disabling gapless playback. If for some reason
// we fail to clear the NextURI it would be best to stop and
// avoid inconsistent where gapless playback appears disabled
// but in reality it's not.
_, err := queueNext(s, true)
if err != nil {
stopAction(s)
}
}
}
})
gaplessOption := fyne.CurrentApp().Preferences().StringWithFallback("Gapless", "Disabled")

View File

@@ -276,6 +276,24 @@ type getMediaInfoAction struct {
InstanceID string
}
type getTransportInfoEnvelope struct {
XMLName xml.Name `xml:"s:Envelope"`
Schema string `xml:"xmlns:s,attr"`
Encoding string `xml:"s:encodingStyle,attr"`
GetTransportInfoBody getTransportInfoBody `xml:"s:Body"`
}
type getTransportInfoBody struct {
XMLName xml.Name `xml:"s:Body"`
GetTransportInfoAction getTransportInfoAction `xml:"u:GetTransportInfo"`
}
type getTransportInfoAction struct {
XMLName xml.Name `xml:"u:GetTransportInfo"`
AVTransport string `xml:"xmlns:u,attr"`
InstanceID string
}
func setAVTransportSoapBuild(tvdata *TVPayload) ([]byte, error) {
mediaTypeSlice := strings.Split(tvdata.MediaType, "/")
seekflag := "00"
@@ -775,3 +793,26 @@ func getMediaInfoSoapBuild() ([]byte, error) {
return append(xmlStart, b...), nil
}
func getTransportInfoSoapBuild() ([]byte, error) {
d := getTransportInfoEnvelope{
XMLName: xml.Name{},
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
GetTransportInfoBody: getTransportInfoBody{
XMLName: xml.Name{},
GetTransportInfoAction: getTransportInfoAction{
XMLName: xml.Name{},
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
InstanceID: "0",
},
},
}
xmlStart := []byte(`<?xml version="1.0" encoding="utf-8"?>`)
b, err := xml.Marshal(d)
if err != nil {
return nil, fmt.Errorf("getTransportInfoSoapBuild Marshal error: %w", err)
}
return append(xmlStart, b...), nil
}

View File

@@ -266,3 +266,27 @@ func TestSetVolumeSoapBuild(t *testing.T) {
})
}
}
func TestGetTransportInfoSoapBuild(t *testing.T) {
tt := []struct {
name string
want string
}{
{
`getTransportInfoSoapBuildTest #1`,
`<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetTransportInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID></u:GetTransportInfo></s:Body></s:Envelope>`,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
out, err := getTransportInfoSoapBuild()
if err != nil {
t.Fatalf("%s: Failed to call getTransportInfoSoapBuild due to %s", tc.name, err.Error())
}
if string(out) != tc.want {
t.Fatalf("%s: got: %s, want: %s.", tc.name, out, tc.want)
}
})
}
}

View File

@@ -122,6 +122,23 @@ type getMediaInfoResponse struct {
} `xml:"Body"`
}
type getTransportInfoResponse struct {
XMLName xml.Name `xml:"Envelope"`
Text string `xml:",chardata"`
S string `xml:"s,attr"`
EncodingStyle string `xml:"encodingStyle,attr"`
Body struct {
Text string `xml:",chardata"`
GetTransportInfoResponse struct {
Text string `xml:",chardata"`
U string `xml:"u,attr"`
CurrentTransportState string `xml:"CurrentTransportState"`
CurrentTransportStatus string `xml:"CurrentTransportStatus"`
CurrentSpeed string `xml:"CurrentSpeed"`
} `xml:"GetTransportInfoResponse"`
} `xml:"Body"`
}
func (p *TVPayload) setAVTransportSoapCall() error {
parsedURLtransport, err := url.Parse(p.ControlURL)
if err != nil {
@@ -890,16 +907,16 @@ func (p *TVPayload) GetProtocolInfo() error {
return nil
}
// Gapless requests our device's media info and checks if Next URI exists.
func (p *TVPayload) Gapless() (bool, error) {
// Gapless requests our device's media info and returns the Next URI.
func (p *TVPayload) Gapless() (string, error) {
if p == nil {
return false, errors.New("Gapless, nil tvdata")
return "", errors.New("Gapless, nil tvdata")
}
parsedURLtransport, err := url.Parse(p.ControlURL)
if err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "URL Parse").Err(err).Msg("")
return false, fmt.Errorf("Gapless parse error: %w", err)
return "", fmt.Errorf("Gapless parse error: %w", err)
}
var xmlbuilder []byte
@@ -907,14 +924,14 @@ func (p *TVPayload) Gapless() (bool, error) {
xmlbuilder, err = getMediaInfoSoapBuild()
if err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "Build").Err(err).Msg("")
return false, fmt.Errorf("Gapless build error: %w", err)
return "", fmt.Errorf("Gapless build error: %w", err)
}
client := &http.Client{}
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xmlbuilder))
if err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "Prepare POST").Err(err).Msg("")
return false, fmt.Errorf("Gapless POST error: %w", err)
return "", fmt.Errorf("Gapless POST error: %w", err)
}
req.Header = http.Header{
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetMediaInfo"`},
@@ -926,7 +943,7 @@ func (p *TVPayload) Gapless() (bool, error) {
headerBytesReq, err := json.Marshal(req.Header)
if err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "Header Marshaling").Err(err).Msg("")
return false, fmt.Errorf("Gapless Request Marshaling error: %w", err)
return "", fmt.Errorf("Gapless Request Marshaling error: %w", err)
}
p.Log().Debug().
@@ -936,20 +953,20 @@ func (p *TVPayload) Gapless() (bool, error) {
res, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("Gapless Do POST error: %w", err)
return "", fmt.Errorf("Gapless Do POST error: %w", err)
}
defer res.Body.Close()
headerBytesRes, err := json.Marshal(res.Header)
if err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "Header Marshaling #2").Err(err).Msg("")
return false, fmt.Errorf("Gapless Response Marshaling error: %w", err)
return "", fmt.Errorf("Gapless Response Marshaling error: %w", err)
}
resBytes, err := io.ReadAll(res.Body)
if err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "Readall").Err(err).Msg("")
return false, fmt.Errorf("Gapless Failed to read response: %w", err)
return "", fmt.Errorf("Gapless Failed to read response: %w", err)
}
p.Log().Debug().
@@ -961,15 +978,94 @@ func (p *TVPayload) Gapless() (bool, error) {
if err := xml.Unmarshal(resBytes, &respMedialInfo); err != nil {
p.Log().Error().Str("Method", "Gapless").Str("Action", "Unmarshal").Err(err).Msg("")
return false, fmt.Errorf("Gapless Failed to unmarshal response: %w", err)
return "", fmt.Errorf("Gapless Failed to unmarshal response: %w", err)
}
nextURI := respMedialInfo.Body.GetMediaInfoResponse.NextURI
if nextURI != "NOT_IMPLEMENTED" && nextURI != "" {
return true, nil
return nextURI, nil
}
// GetTransportInfo .
func (p *TVPayload) GetTransportInfo() ([]string, error) {
if p == nil {
return nil, errors.New("GetTransportInfo, nil tvdata")
}
return false, nil
parsedURLtransport, err := url.Parse(p.ControlURL)
if err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "URL Parse").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo parse error: %w", err)
}
var xmlbuilder []byte
xmlbuilder, err = getTransportInfoSoapBuild()
if err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Build").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo build error: %w", err)
}
client := &http.Client{}
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xmlbuilder))
if err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Prepare POST").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo POST error: %w", err)
}
req.Header = http.Header{
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo"`},
"content-type": []string{"text/xml"},
"charset": []string{"utf-8"},
"Connection": []string{"close"},
}
headerBytesReq, err := json.Marshal(req.Header)
if err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Header Marshaling").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo Request Marshaling error: %w", err)
}
p.Log().Debug().
Str("Method", "GetTransportInfo").Str("Action", "Request").
RawJSON("Headers", headerBytesReq).
Msg(string(xmlbuilder))
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("GetTransportInfo Do POST error: %w", err)
}
defer res.Body.Close()
headerBytesRes, err := json.Marshal(res.Header)
if err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Header Marshaling #2").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo Response Marshaling error: %w", err)
}
resBytes, err := io.ReadAll(res.Body)
if err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Readall").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo Failed to read response: %w", err)
}
p.Log().Debug().
Str("Method", "GetTransportInfo").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
RawJSON("Headers", headerBytesRes).
Msg(string(resBytes))
var respTransportInfo getTransportInfoResponse
if err := xml.Unmarshal(resBytes, &respTransportInfo); err != nil {
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Unmarshal").Err(err).Msg("")
return nil, fmt.Errorf("GetTransportInfo Failed to unmarshal response: %w", err)
}
r := respTransportInfo.Body.GetTransportInfoResponse
state := r.CurrentTransportState
status := r.CurrentTransportStatus
speed := r.CurrentSpeed
return []string{state, status, speed}, nil
}
// SendtoTV is a higher level method that gracefully handles the various