Start working on the gapless playback

This commit is contained in:
Alex Ballas
2023-01-13 01:27:05 +02:00
parent e3264f6d6d
commit 5e28f6b017
7 changed files with 440 additions and 18 deletions

View File

@@ -364,7 +364,7 @@ func previewmedia(screen *NewScreen) {
switch mediaTypeSlice[0] {
case "image":
img := canvas.NewImageFromFile(screen.mediafile)
img.FillMode = 1
img.FillMode = canvas.ImageFillContain
imgw := fyne.CurrentApp().NewWindow(filepath.Base(screen.mediafile))
imgw.SetContent(img)
imgw.Resize(fyne.NewSize(800, 600))

View File

@@ -167,7 +167,7 @@ func InitFyneNewScreen(v string) *NewScreen {
return &NewScreen{
Current: w,
currentmfolder: currentdir,
mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".dv", ".mp3", ".flac", ".wav", ".jpg", ".jpeg", ".png"},
mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".dv", ".mp3", ".flac", ".wav", ".m4a", ".jpg", ".jpeg", ".png"},
version: v,
Debug: dw,
}

View File

@@ -127,7 +127,7 @@ func InitFyneNewScreen(v string) *NewScreen {
return &NewScreen{
Current: w,
mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".dv", ".mp3", ".flac", ".wav", ".jpg", ".jpeg", ".png"},
mediaFormats: []string{".mp4", ".avi", ".mkv", ".mpeg", ".mov", ".webm", ".m4v", ".mpv", ".dv", ".mp3", ".flac", ".wav", ".m4a", ".jpg", ".jpeg", ".png"},
version: v,
}
}

View File

@@ -1,11 +1,11 @@
package gui
import (
"fmt"
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
@@ -66,7 +66,7 @@ func (m go2tvTheme) Size(name fyne.ThemeSizeName) float32 {
func settingsWindow(s *NewScreen) fyne.CanvasObject {
w := s.Current
themeText := canvas.NewText("Theme", nil)
themeText := widget.NewLabel("Theme")
dropdown := widget.NewSelect([]string{"Light", "Dark", "Default"}, parseTheme(s))
theme := fyne.CurrentApp().Preferences().StringWithFallback("Theme", "Default")
switch theme {
@@ -78,7 +78,7 @@ func settingsWindow(s *NewScreen) fyne.CanvasObject {
dropdown.PlaceHolder = "Default"
}
debugText := canvas.NewText("Debug", nil)
debugText := widget.NewLabel("Debug")
debugExport := widget.NewButton("Export Debug Logs", func() {
var itemInRing bool
s.Debug.ring.Do(func(p interface{}) {
@@ -106,8 +106,15 @@ func settingsWindow(s *NewScreen) fyne.CanvasObject {
})
gaplessText := widget.NewLabel("Gapless Playback")
gaplessdropdown := widget.NewSelect([]string{"Enabled", "Disabled"}, func(s string) {
fmt.Println(s)
})
gaplessOption := fyne.CurrentApp().Preferences().StringWithFallback("Gapless", "Disabled")
gaplessdropdown.SetSelected(gaplessOption)
dropdown.Refresh()
settings := container.New(layout.NewFormLayout(), themeText, dropdown, debugText, debugExport)
settings := container.New(layout.NewFormLayout(), themeText, dropdown, gaplessText, gaplessdropdown, debugText, debugExport)
return settings
}

View File

@@ -98,6 +98,31 @@ type currentURIMetaData struct {
Value []byte `xml:",chardata"`
}
type setNextAVTransportEnvelope struct {
XMLName xml.Name `xml:"s:Envelope"`
Schema string `xml:"xmlns:s,attr"`
Encoding string `xml:"s:encodingStyle,attr"`
Body setNextAVTransportBody `xml:"s:Body"`
}
type setNextAVTransportBody struct {
XMLName xml.Name `xml:"s:Body"`
SetNextAVTransportURI setNextAVTransportURI `xml:"u:SetNextAVTransportURI"`
}
type setNextAVTransportURI struct {
XMLName xml.Name `xml:"u:SetNextAVTransportURI"`
AVTransport string `xml:"xmlns:u,attr"`
InstanceID string
NextURI string
NextURIMetaData nextURIMetaData `xml:"NextURIMetaData"`
}
type nextURIMetaData struct {
XMLName xml.Name `xml:"NextURIMetaData"`
Value []byte `xml:",chardata"`
}
type didLLite struct {
XMLName xml.Name `xml:"DIDL-Lite"`
SchemaDIDL string `xml:"xmlns,attr"`
@@ -224,8 +249,8 @@ type getProtocolInfoEnvelope struct {
}
type getProtocolInfoBody struct {
XMLName xml.Name `xml:"s:Body"`
GetProtocolInfoction getProtocolInfoAction `xml:"u:GetProtocolInfo"`
XMLName xml.Name `xml:"s:Body"`
GetProtocolInfoAction getProtocolInfoAction `xml:"u:GetProtocolInfo"`
}
type getProtocolInfoAction struct {
@@ -233,6 +258,24 @@ type getProtocolInfoAction struct {
ConnectionManager string `xml:"xmlns:u,attr"`
}
type getMediaInfoEnvelope struct {
XMLName xml.Name `xml:"s:Envelope"`
Schema string `xml:"xmlns:s,attr"`
Encoding string `xml:"s:encodingStyle,attr"`
GetMediaInfoBody getMediaInfoBody `xml:"s:Body"`
}
type getMediaInfoBody struct {
XMLName xml.Name `xml:"s:Body"`
GetMediaInfoAction getMediaInfoAction `xml:"u:GetMediaInfo"`
}
type getMediaInfoAction struct {
XMLName xml.Name `xml:"u:GetMediaInfo"`
AVTransport string `xml:"xmlns:u,attr"`
InstanceID string
}
func setAVTransportSoapBuild(tvdata *TVPayload) ([]byte, error) {
mediaTypeSlice := strings.Split(tvdata.MediaType, "/")
seekflag := "00"
@@ -371,6 +414,144 @@ func setAVTransportSoapBuild(tvdata *TVPayload) ([]byte, error) {
return append(xmlStart, b...), nil
}
func setNextAVTransportSoapBuild(tvdata *TVPayload) ([]byte, error) {
mediaTypeSlice := strings.Split(tvdata.MediaType, "/")
seekflag := "00"
if tvdata.Seekable {
seekflag = "01"
}
contentFeatures, err := utils.BuildContentFeatures(tvdata.MediaType, seekflag, tvdata.Transcode)
if err != nil {
return nil, fmt.Errorf("setNextAVTransportSoapBuild failed to build contentFeatures: %w", err)
}
var class string
switch mediaTypeSlice[0] {
case "audio":
class = "object.item.audioItem.musicTrack"
case "image":
class = "object.item.imageItem.photo"
default:
class = "object.item.videoItem.movie"
}
mediaTitlefromURL, err := url.Parse(tvdata.MediaURL)
if err != nil {
return nil, fmt.Errorf("setNextAVTransportSoapBuild url parse error: %w", err)
}
mediaTitle := strings.TrimLeft(mediaTitlefromURL.Path, "/")
re, err := regexp.Compile(`[&<>\\]+`)
if err != nil {
return nil, fmt.Errorf("setNextAVTransportSoapBuild regex compile error: %w", err)
}
mediaTitle = re.ReplaceAllString(mediaTitle, "")
var didl didLLiteItem
resNodeData := []resNode{}
duration, _ := utils.DurationForMedia(tvdata.MediaPath)
switch duration {
case "":
resNodeData = append(resNodeData, resNode{
XMLName: xml.Name{},
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", tvdata.MediaType, contentFeatures),
Value: tvdata.MediaURL,
})
default:
resNodeData = append(resNodeData, resNode{
XMLName: xml.Name{},
Duration: duration,
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", tvdata.MediaType, contentFeatures),
Value: tvdata.MediaURL,
})
}
didl = didLLiteItem{
XMLName: xml.Name{},
ID: "1",
ParentID: "0",
Restricted: "1",
UPNPClass: class,
DCtitle: mediaTitle,
ResNode: resNodeData,
}
if strings.Contains(tvdata.SubtitlesURL, "srt") {
resNodeData = append(resNodeData, resNode{
XMLName: xml.Name{},
ProtocolInfo: "http-get:*:text/srt:*",
Value: tvdata.SubtitlesURL,
})
didl = didLLiteItem{
XMLName: xml.Name{},
ID: "1",
ParentID: "0",
Restricted: "1",
DCtitle: mediaTitle,
UPNPClass: class,
ResNode: resNodeData,
SecCaptionInfo: &secCaptionInfo{
XMLName: xml.Name{},
Type: "srt",
Value: tvdata.SubtitlesURL,
},
SecCaptionInfoEx: &secCaptionInfoEx{
XMLName: xml.Name{},
Type: "srt",
Value: tvdata.SubtitlesURL,
},
}
}
l := didLLite{
XMLName: xml.Name{},
SchemaDIDL: "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
DC: "http://purl.org/dc/elements/1.1/",
Sec: "http://www.sec.co.kr/",
SchemaUPNP: "urn:schemas-upnp-org:metadata-1-0/upnp/",
DIDLLiteItem: didl,
}
a, err := xml.Marshal(l)
if err != nil {
return nil, fmt.Errorf("setNextAVTransportSoapBuild #1 Marshal error: %w", err)
}
d := setNextAVTransportEnvelope{
XMLName: xml.Name{},
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
Body: setNextAVTransportBody{
XMLName: xml.Name{},
SetNextAVTransportURI: setNextAVTransportURI{
XMLName: xml.Name{},
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
InstanceID: "0",
NextURI: tvdata.MediaURL,
NextURIMetaData: nextURIMetaData{
XMLName: xml.Name{},
Value: a,
},
},
},
}
xmlStart := []byte(`<?xml version="1.0" encoding="utf-8"?>`)
b, err := xml.Marshal(d)
if err != nil {
return nil, fmt.Errorf("setNextAVTransportSoapBuild #2 Marshal error: %w", err)
}
// Samsung TV hack.
b = bytes.ReplaceAll(b, []byte("&#34;"), []byte(`"`))
b = bytes.ReplaceAll(b, []byte("&amp;"), []byte("&"))
return append(xmlStart, b...), nil
}
func playSoapBuild() ([]byte, error) {
d := playEnvelope{
XMLName: xml.Name{},
@@ -552,7 +733,7 @@ func getProtocolInfoSoapBuild() ([]byte, error) {
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
GetProtocolInfoBody: getProtocolInfoBody{
XMLName: xml.Name{},
GetProtocolInfoction: getProtocolInfoAction{
GetProtocolInfoAction: getProtocolInfoAction{
XMLName: xml.Name{},
ConnectionManager: "urn:schemas-upnp-org:service:ConnectionManager:1",
},
@@ -561,7 +742,30 @@ func getProtocolInfoSoapBuild() ([]byte, error) {
xmlStart := []byte(`<?xml version="1.0" encoding="utf-8"?>`)
b, err := xml.Marshal(d)
if err != nil {
return nil, fmt.Errorf("setVolumeSoapBuild Marshal error: %w", err)
return nil, fmt.Errorf("getProtocolInfoSoapBuild Marshal error: %w", err)
}
return append(xmlStart, b...), nil
}
func getMediaInfoSoapBuild() ([]byte, error) {
d := getMediaInfoEnvelope{
XMLName: xml.Name{},
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
GetMediaInfoBody: getMediaInfoBody{
XMLName: xml.Name{},
GetMediaInfoAction: getMediaInfoAction{
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("getMediaInfoSoapBuild Marshal error: %w", err)
}
return append(xmlStart, b...), nil

View File

@@ -7,7 +7,6 @@ import (
)
func TestSetAVTransportSoapBuild(t *testing.T) {
tt := []struct {
name string
tv *TVPayload
@@ -49,6 +48,48 @@ func TestSetAVTransportSoapBuild(t *testing.T) {
}
}
func TestSetNextAVTransportSoapBuild(t *testing.T) {
tt := []struct {
name string
tv *TVPayload
}{
{
`setNextAVTransportSoapBuild Test #1`,
&TVPayload{
MediaURL: `http://192.168.88.250:3500/video%20%26%20%27example%27.mp4`,
MediaType: "video/mp4",
SubtitlesURL: "http://192.168.88.250:3500/video_example.srt",
Transcode: false,
Seekable: true,
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
seekflag := "00"
if tc.tv.Seekable {
seekflag = "01"
}
contentFeatures, err := utils.BuildContentFeatures(tc.tv.MediaType, seekflag, tc.tv.Transcode)
if err != nil {
t.Fatalf("%s: setNextAVTransportSoapBuild failed to build contentFeatures: %s", tc.name, err.Error())
}
want := `<?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:SetNextAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><NextURI>http://192.168.88.250:3500/video%20%26%20%27example%27.mp4</NextURI><NextURIMetaData>&lt;DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sec="http://www.sec.co.kr/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"&gt;&lt;item id="1" parentID="0" restricted="1"&gt;&lt;sec:CaptionInfo sec:type="srt"&gt;http://192.168.88.250:3500/video_example.srt&lt;/sec:CaptionInfo&gt;&lt;sec:CaptionInfoEx sec:type="srt"&gt;http://192.168.88.250:3500/video_example.srt&lt;/sec:CaptionInfoEx&gt;&lt;dc:title&gt;video &#39;example&#39;.mp4&lt;/dc:title&gt;&lt;upnp:class&gt;object.item.videoItem.movie&lt;/upnp:class&gt;&lt;res protocolInfo="http-get:*:video/mp4:` + contentFeatures + `"&gt;http://192.168.88.250:3500/video%20%26%20%27example%27.mp4&lt;/res&gt;&lt;res protocolInfo="http-get:*:text/srt:*"&gt;http://192.168.88.250:3500/video_example.srt&lt;/res&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</NextURIMetaData></u:SetNextAVTransportURI></s:Body></s:Envelope>`
out, err := setNextAVTransportSoapBuild(tc.tv)
if err != nil {
t.Fatalf("%s: Failed to call setNextAVTransportSoapBuild due to %s", tc.name, err.Error())
}
if string(out) != want {
t.Fatalf("%s: got: %s, want: %s.", tc.name, out, want)
}
})
}
}
func TestSetMuteSoapBuild(t *testing.T) {
tt := []struct {
name string

View File

@@ -31,26 +31,26 @@ var (
ErrZombieCallbacks = errors.New("zombie callbacks, we should ignore those")
)
// TVPayload this is the heart of Go2TV. We pass that type to the
// TVPayload is the heart of Go2TV. We pass that type to the
// webserver. We need to explicitly initialize it.
type TVPayload struct {
mu sync.RWMutex
initLogOnce sync.Once
Logging io.Writer
CurrentTimers map[string]*time.Timer
InitialMediaRenderersStates map[string]bool
mu sync.RWMutex
MediaRenderersStates map[string]*States
RenderingControlURL string
ConnectionManagerURL string
EventURL string
ControlURL string
EventURL string
MediaURL string
MediaType string
MediaPath string
SubtitlesURL string
CallbackURL string
ConnectionManagerURL string
RenderingControlURL string
Transcode bool
Seekable bool
initLogOnce sync.Once
}
type getMuteRespBody struct {
@@ -99,6 +99,29 @@ type protocolInfoResponse struct {
} `xml:"Body"`
}
type getMediaInfoResponse struct {
XMLName xml.Name `xml:"Envelope"`
Text string `xml:",chardata"`
EncodingStyle string `xml:"encodingStyle,attr"`
S string `xml:"s,attr"`
Body struct {
Text string `xml:",chardata"`
GetMediaInfoResponse struct {
Text string `xml:",chardata"`
U string `xml:"u,attr"`
NrTracks string `xml:"NrTracks"`
MediaDuration string `xml:"MediaDuration"`
CurrentURI string `xml:"CurrentURI"`
CurrentURIMetaData string `xml:"CurrentURIMetaData"`
NextURI string `xml:"NextURI"`
NextURIMetaData string `xml:"NextURIMetaData"`
PlayMedium string `xml:"PlayMedium"`
RecordMedium string `xml:"RecordMedium"`
WriteStatus string `xml:"WriteStatus"`
} `xml:"GetMediaInfoResponse"`
} `xml:"Body"`
}
func (p *TVPayload) setAVTransportSoapCall() error {
parsedURLtransport, err := url.Parse(p.ControlURL)
if err != nil {
@@ -168,6 +191,75 @@ func (p *TVPayload) setAVTransportSoapCall() error {
return nil
}
func (p *TVPayload) setNextAVTransportSoapCall() error {
parsedURLtransport, err := url.Parse(p.ControlURL)
if err != nil {
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall parse error: %w", err)
}
xml, err := setNextAVTransportSoapBuild(p)
if err != nil {
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "setNextAVTransportSoapBuild").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall soap build error: %w", err)
}
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 3
retryClient.Logger = nil
client := retryClient.StandardClient()
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
if err != nil {
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall POST error: %w", err)
}
req.Header = http.Header{
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#SetNextAVTransportURI"`},
"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", "setNextAVTransportSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall Request Marshaling error: %w", err)
}
p.Log().Debug().
Str("Method", "setNextAVTransportSoapCall").Str("Action", "Request").
RawJSON("Headers", headerBytesReq).
Msg(string(xml))
res, err := client.Do(req)
if err != nil {
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Do POST").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall Do POST error: %w", err)
}
defer res.Body.Close()
resBytes, err := io.ReadAll(res.Body)
if err != nil {
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Readall").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall Failed to read response: %w", err)
}
headerBytesRes, err := json.Marshal(res.Header)
if err != nil {
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
return fmt.Errorf("setNextAVTransportSoapCall Response Marshaling error: %w", err)
}
p.Log().Debug().
Str("Method", "setNextAVTransportSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
RawJSON("Headers", headerBytesRes).
Msg(string(resBytes))
return nil
}
// AVTransportActionSoapCall builds and sends the AVTransport actions
func (p *TVPayload) AVTransportActionSoapCall(action string) error {
parsedURLtransport, err := url.Parse(p.ControlURL)
@@ -801,6 +893,84 @@ 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) {
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)
}
var xmlbuilder []byte
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)
}
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)
}
req.Header = http.Header{
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetMediaInfo"`},
"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", "Gapless").Str("Action", "Header Marshaling").Err(err).Msg("")
return false, fmt.Errorf("Gapless Request Marshaling error: %w", err)
}
p.Log().Debug().
Str("Method", "Gapless").Str("Action", "Request").
RawJSON("Headers", headerBytesReq).
Msg(string(xmlbuilder))
res, err := client.Do(req)
if err != nil {
return false, 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)
}
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)
}
p.Log().Debug().
Str("Method", "Gapless").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
RawJSON("Headers", headerBytesRes).
Msg(string(resBytes))
var respMedialInfo getMediaInfoResponse
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)
}
nextURI := respMedialInfo.Body.GetMediaInfoResponse.NextURI
if nextURI != "NOT_IMPLEMENTED" && nextURI != "" {
return true, nil
}
return false, nil
}
// SendtoTV is a higher level method that gracefully handles the various
// states when communicating with the DMR devices.
func (p *TVPayload) SendtoTV(action string) error {