Start working on the gapless playback
This commit is contained in:
@@ -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))
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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("""), []byte(`"`))
|
||||
b = bytes.ReplaceAll(b, []byte("&"), []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
|
||||
|
@@ -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><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/"><item id="1" parentID="0" restricted="1"><sec:CaptionInfo sec:type="srt">http://192.168.88.250:3500/video_example.srt</sec:CaptionInfo><sec:CaptionInfoEx sec:type="srt">http://192.168.88.250:3500/video_example.srt</sec:CaptionInfoEx><dc:title>video 'example'.mp4</dc:title><upnp:class>object.item.videoItem.movie</upnp:class><res protocolInfo="http-get:*:video/mp4:` + contentFeatures + `">http://192.168.88.250:3500/video%20%26%20%27example%27.mp4</res><res protocolInfo="http-get:*:text/srt:*">http://192.168.88.250:3500/video_example.srt</res></item></DIDL-Lite></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
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user