Update project structure
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
||||
LDFLAGS="-s -w -X main.build=`date -u +%Y%m%d%H%M%S` -X main.version=`cat ./version.txt`"
|
||||
|
||||
build: clean
|
||||
go build -ldflags $(LDFLAGS) -o build/go2tv go2tv.go flagfuncs.go
|
||||
go build -ldflags $(LDFLAGS) -o build/go2tv cmd/go2tv/go2tv.go cmd/go2tv/flagfuncs.go
|
||||
|
||||
install:
|
||||
mkdir -p /usr/local/bin/
|
||||
|
146
flagfuncs.go
146
flagfuncs.go
@@ -1,146 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func listFlagFunction() error {
|
||||
if len(devices) == 0 {
|
||||
err := errors.New("-l and -t can't be used together")
|
||||
return err
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// We loop through this map twice as we need to maintain
|
||||
// the correct order.
|
||||
keys := make([]int, 0)
|
||||
for k := range devices {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Ints(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
boldStart := ""
|
||||
boldEnd := ""
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
boldStart = "\033[1m"
|
||||
boldEnd = "\033[0m"
|
||||
}
|
||||
fmt.Printf("%sDevice %v%s\n", boldStart, k, boldEnd)
|
||||
fmt.Printf("%s--------%s\n", boldStart, boldEnd)
|
||||
fmt.Printf("%sModel:%s %s\n", boldStart, boldEnd, devices[k][0])
|
||||
fmt.Printf("%sURL:%s %s\n", boldStart, boldEnd, devices[k][1])
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkflags() (bool, error) {
|
||||
checkVerflag()
|
||||
|
||||
if err := checkVflag(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := checkTflag(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
list, err := checkLflag()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if list {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err := checkSflag(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func checkVflag() error {
|
||||
if !*listPtr {
|
||||
if *videoArg == "" {
|
||||
err := errors.New("No video file defined")
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(*videoArg); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSflag() error {
|
||||
if *subsArg != "" {
|
||||
if _, err := os.Stat(*subsArg); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// The checkVflag should happen before
|
||||
// checkSflag so we're safe to call *videoArg
|
||||
// here. If *subsArg is empty, try to
|
||||
// automatically find the srt from the
|
||||
// video filename.
|
||||
*subsArg = (*videoArg)[0:len(*videoArg)-
|
||||
len(filepath.Ext(*videoArg))] + ".srt"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkTflag() error {
|
||||
if *targetPtr == "" {
|
||||
err := loadSSDPservices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dmrURL, err = devicePicker(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Validate URL before proceeding.
|
||||
_, err := url.ParseRequestURI(*targetPtr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dmrURL = *targetPtr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkLflag() (bool, error) {
|
||||
if *listPtr {
|
||||
if err := listFlagFunction(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func checkVerflag() {
|
||||
if *versionPtr {
|
||||
fmt.Printf("Go2TV Version: %s, ", version)
|
||||
fmt.Printf("Build: %s\n", build)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
130
go2tv.go
130
go2tv.go
@@ -1,130 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/alexballas/go2tv/httphandlers"
|
||||
"github.com/alexballas/go2tv/interactive"
|
||||
"github.com/alexballas/go2tv/iptools"
|
||||
"github.com/alexballas/go2tv/soapcalls"
|
||||
"github.com/koron/go-ssdp"
|
||||
)
|
||||
|
||||
var (
|
||||
version string
|
||||
build string
|
||||
dmrURL string
|
||||
serverStarted = make(chan struct{})
|
||||
devices = make(map[int][]string)
|
||||
videoArg = flag.String("v", "", "Path to the video file.")
|
||||
subsArg = flag.String("s", "", "Path to the subtitles file.")
|
||||
listPtr = flag.Bool("l", false, "List all available UPnP/DLNA MediaRenderer models and URLs.")
|
||||
targetPtr = flag.String("t", "", "Cast to a specific UPnP/DLNA MediaRenderer URL.")
|
||||
versionPtr = flag.Bool("version", false, "Print version.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
exit, err := checkflags()
|
||||
check(err)
|
||||
|
||||
if exit {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
absVideoFile, err := filepath.Abs(*videoArg)
|
||||
check(err)
|
||||
|
||||
absSubtitlesFile, err := filepath.Abs(*subsArg)
|
||||
check(err)
|
||||
|
||||
transportURL, controlURL, err := soapcalls.DMRextractor(dmrURL)
|
||||
check(err)
|
||||
|
||||
whereToListen, err := iptools.URLtoListenIPandPort(dmrURL)
|
||||
check(err)
|
||||
|
||||
newsc, err := interactive.InitNewScreen()
|
||||
check(err)
|
||||
|
||||
// The String() method of the net/url package will properly escape the URL
|
||||
// compared to the url.QueryEscape() method.
|
||||
videoFileURLencoded := &url.URL{Path: filepath.Base(absVideoFile)}
|
||||
subsFileURLencoded := &url.URL{Path: filepath.Base(absSubtitlesFile)}
|
||||
|
||||
tvdata := &soapcalls.TVPayload{
|
||||
TransportURL: transportURL,
|
||||
ControlURL: controlURL,
|
||||
CallbackURL: "http://" + whereToListen + "/callback",
|
||||
VideoURL: "http://" + whereToListen + "/" + videoFileURLencoded.String(),
|
||||
SubtitlesURL: "http://" + whereToListen + "/" + subsFileURLencoded.String(),
|
||||
}
|
||||
|
||||
s := httphandlers.NewServer(whereToListen)
|
||||
|
||||
// We pass the tvdata here as we need the callback handlers to be able to react
|
||||
// to the different media renderer states.
|
||||
go func() {
|
||||
s.ServeFiles(serverStarted, absVideoFile, absSubtitlesFile, &httphandlers.HTTPPayload{Soapcalls: tvdata, Screen: newsc})
|
||||
}()
|
||||
// Wait for HTTP server to properly initialize
|
||||
<-serverStarted
|
||||
|
||||
err = tvdata.SendtoTV("Play1")
|
||||
check(err)
|
||||
|
||||
newsc.InterInit(*tvdata)
|
||||
}
|
||||
|
||||
func loadSSDPservices() error {
|
||||
list, err := ssdp.Search(ssdp.All, 1, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
counter := 0
|
||||
for _, srv := range list {
|
||||
// We only care about the AVTransport services for basic actions
|
||||
// (stop,play,pause). If we need to extend this to support other functionality
|
||||
// like volume control we need to use the RenderingControl service.
|
||||
if srv.Type == "urn:schemas-upnp-org:service:AVTransport:1" {
|
||||
counter++
|
||||
devices[counter] = []string{srv.Server, srv.Location}
|
||||
}
|
||||
}
|
||||
if counter > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("loadSSDPservices: No available Media Renderers")
|
||||
}
|
||||
|
||||
func devicePicker(i int) (string, error) {
|
||||
if i > len(devices) || len(devices) == 0 || i <= 0 {
|
||||
return "", errors.New("devicePicker: Requested device not available")
|
||||
}
|
||||
keys := make([]int, 0)
|
||||
for k := range devices {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
for _, k := range keys {
|
||||
if i == k {
|
||||
return devices[k][1], nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("devicePicker: Something went terribly wrong")
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@@ -1,168 +0,0 @@
|
||||
package httphandlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alexballas/go2tv/interactive"
|
||||
"github.com/alexballas/go2tv/soapcalls"
|
||||
)
|
||||
|
||||
// filesToServe defines the files we need to serve.
|
||||
type filesToServe struct {
|
||||
Video string
|
||||
Subtitles string
|
||||
}
|
||||
|
||||
// HTTPserver - new http.Server instance.
|
||||
type HTTPserver struct {
|
||||
http *http.Server
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// HTTPPayload - We need some of the soapcalls magic in this package too. We need
|
||||
// to expose the ControlURL to the callback handler.
|
||||
type HTTPPayload struct {
|
||||
Soapcalls *soapcalls.TVPayload
|
||||
Screen *interactive.NewScreen
|
||||
}
|
||||
|
||||
// ServeFiles - Start HTTP server and serve the files.
|
||||
func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, videoPath, subtitlesPath string, tvpayload *HTTPPayload) {
|
||||
files := &filesToServe{
|
||||
Video: videoPath,
|
||||
Subtitles: subtitlesPath,
|
||||
}
|
||||
|
||||
s.mux.HandleFunc("/"+filepath.Base(files.Video), files.serveVideoHandler)
|
||||
s.mux.HandleFunc("/"+filepath.Base(files.Subtitles), files.serveSubtitlesHandler)
|
||||
s.mux.HandleFunc("/callback", tvpayload.callbackHandler)
|
||||
|
||||
ln, err := net.Listen("tcp", s.http.Addr)
|
||||
check(err)
|
||||
|
||||
serverStarted <- struct{}{}
|
||||
s.http.Serve(ln)
|
||||
}
|
||||
|
||||
func (f *filesToServe) serveVideoHandler(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("transferMode.dlna.org", "Streaming")
|
||||
w.Header().Set("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=017000 00000000000000000000000000")
|
||||
|
||||
filePath, err := os.Open(f.Video)
|
||||
check(err)
|
||||
defer filePath.Close()
|
||||
|
||||
fileStat, err := filePath.Stat()
|
||||
check(err)
|
||||
|
||||
http.ServeContent(w, req, filepath.Base(f.Video), fileStat.ModTime(), filePath)
|
||||
}
|
||||
|
||||
func (f *filesToServe) serveSubtitlesHandler(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("transferMode.dlna.org", "Streaming")
|
||||
w.Header().Set("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=017000 00000000000000000000000000")
|
||||
|
||||
filePath, err := os.Open(f.Subtitles)
|
||||
if err != nil {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
defer filePath.Close()
|
||||
|
||||
fileStat, err := filePath.Stat()
|
||||
if err != nil {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, req, filepath.Base(f.Subtitles), fileStat.ModTime(), filePath)
|
||||
}
|
||||
|
||||
func (p *HTTPPayload) callbackHandler(w http.ResponseWriter, req *http.Request) {
|
||||
reqParsed, _ := io.ReadAll(req.Body)
|
||||
sidVal, sidExists := req.Header["Sid"]
|
||||
|
||||
if !sidExists {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
|
||||
if sidVal[0] == "" {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
|
||||
uuid := sidVal[0]
|
||||
uuid = strings.TrimLeft(uuid, "[")
|
||||
uuid = strings.TrimLeft(uuid, "]")
|
||||
uuid = strings.TrimLeft(uuid, "uuid:")
|
||||
|
||||
// Apparently we should ignore the first message
|
||||
// On some media renderers we receive a STOPPED message
|
||||
// even before we start streaming.
|
||||
seq, err := soapcalls.GetSequence(uuid)
|
||||
if err != nil {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
|
||||
if seq == 0 {
|
||||
soapcalls.IncreaseSequence(uuid)
|
||||
_, _ = fmt.Fprintf(w, "OK\n")
|
||||
return
|
||||
}
|
||||
|
||||
reqParsedUnescape := html.UnescapeString(string(reqParsed))
|
||||
previousstate, newstate, err := soapcalls.EventNotifyParser(reqParsedUnescape)
|
||||
if err != nil {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
|
||||
if !soapcalls.UpdateMRstate(previousstate, newstate, uuid) {
|
||||
http.Error(w, "", 404)
|
||||
return
|
||||
}
|
||||
|
||||
if newstate == "PLAYING" {
|
||||
p.Screen.EmmitMsg("Playing")
|
||||
}
|
||||
if newstate == "PAUSED_PLAYBACK" {
|
||||
p.Screen.EmmitMsg("Paused")
|
||||
}
|
||||
if newstate == "STOPPED" {
|
||||
p.Screen.EmmitMsg("Stopped")
|
||||
p.Soapcalls.UnsubscribeSoapCall(uuid)
|
||||
p.Screen.Current.Fini()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// We could just not send anything here
|
||||
// as the core server package would still
|
||||
// default to a 200 OK empty response.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// NewServer - create a new HTTP server.
|
||||
func NewServer(a string) HTTPserver {
|
||||
srv := HTTPserver{
|
||||
http: &http.Server{Addr: a},
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
srv.http.Handler = srv.mux
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@@ -1,128 +0,0 @@
|
||||
package interactive
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alexballas/go2tv/soapcalls"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/encoding"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// NewScreen .
|
||||
type NewScreen struct {
|
||||
Current tcell.Screen
|
||||
videoTitle string
|
||||
lastAction string
|
||||
}
|
||||
|
||||
var flipflop bool = true
|
||||
|
||||
func (p *NewScreen) emitStr(x, y int, style tcell.Style, str string) {
|
||||
s := p.Current
|
||||
for _, c := range str {
|
||||
var comb []rune
|
||||
w := runewidth.RuneWidth(c)
|
||||
if w == 0 {
|
||||
comb = []rune{c}
|
||||
c = ' '
|
||||
w = 1
|
||||
}
|
||||
s.SetContent(x, y, c, comb, style)
|
||||
x += w
|
||||
}
|
||||
}
|
||||
|
||||
// EmmitMsg - Display the actions to the interactive terminal
|
||||
func (p *NewScreen) EmmitMsg(inputtext string) {
|
||||
p.lastAction = inputtext
|
||||
s := p.Current
|
||||
titleLen := len("Title: " + p.videoTitle)
|
||||
w, h := s.Size()
|
||||
boldStyle := tcell.StyleDefault.
|
||||
Background(tcell.ColorBlack).
|
||||
Foreground(tcell.ColorWhite).Bold(true)
|
||||
blinkStyle := tcell.StyleDefault.
|
||||
Background(tcell.ColorBlack).
|
||||
Foreground(tcell.ColorWhite).Blink(true)
|
||||
|
||||
s.Clear()
|
||||
|
||||
p.emitStr(w/2-titleLen/2, h/2-2, tcell.StyleDefault, "Title: "+p.videoTitle)
|
||||
if inputtext == "Waiting for status..." {
|
||||
p.emitStr(w/2-len(inputtext)/2, h/2, blinkStyle, inputtext)
|
||||
} else {
|
||||
p.emitStr(w/2-len(inputtext)/2, h/2, boldStyle, inputtext)
|
||||
}
|
||||
p.emitStr(1, 1, tcell.StyleDefault, "Press ESC to stop and exit.")
|
||||
p.emitStr(w/2-len("Press p to Pause/Play.")/2, h/2+2, tcell.StyleDefault, "Press p to Pause/Play.")
|
||||
|
||||
s.Show()
|
||||
}
|
||||
|
||||
// InterInit - Start the interactive terminal
|
||||
func (p *NewScreen) InterInit(tv soapcalls.TVPayload) {
|
||||
var videoTitle string
|
||||
|
||||
videoTitlefromURL, err := url.Parse(tv.VideoURL)
|
||||
if err != nil {
|
||||
videoTitle = tv.VideoURL
|
||||
} else {
|
||||
videoTitle = strings.TrimLeft(videoTitlefromURL.Path, "/")
|
||||
}
|
||||
p.videoTitle = videoTitle
|
||||
|
||||
encoding.Register()
|
||||
s := p.Current
|
||||
if e := s.Init(); e != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%v\n", e)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defStyle := tcell.StyleDefault.
|
||||
Background(tcell.ColorBlack).
|
||||
Foreground(tcell.ColorWhite)
|
||||
s.SetStyle(defStyle)
|
||||
|
||||
p.lastAction = "Waiting for status..."
|
||||
p.EmmitMsg(p.lastAction)
|
||||
|
||||
for {
|
||||
switch ev := s.PollEvent().(type) {
|
||||
case *tcell.EventResize:
|
||||
s.Sync()
|
||||
p.EmmitMsg(p.lastAction)
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEscape {
|
||||
tv.SendtoTV("Stop")
|
||||
s.Fini()
|
||||
os.Exit(0)
|
||||
} else if ev.Rune() == 'p' {
|
||||
if flipflop {
|
||||
flipflop = false
|
||||
tv.SendtoTV("Pause")
|
||||
} else {
|
||||
flipflop = true
|
||||
tv.SendtoTV("Play")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitNewScreen - Just initializing our new tcell screen
|
||||
func InitNewScreen() (*NewScreen, error) {
|
||||
s, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
return nil, errors.New("can't start new interactive screen")
|
||||
}
|
||||
q := NewScreen{
|
||||
Current: s,
|
||||
}
|
||||
|
||||
return &q, nil
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
package iptools
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// URLtoListenIPandPort for a given internal URL, find the correct IP/Interface to listen to.
|
||||
func URLtoListenIPandPort(u string) (string, error) {
|
||||
parsedURL, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
conn, err := net.Dial("udp", parsedURL.Host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ipToListen := strings.Split(conn.LocalAddr().String(), ":")[0]
|
||||
portToListen, err := checkAndPickPort(ipToListen, 3500)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res := ipToListen + ":" + portToListen
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func checkAndPickPort(ip string, port int) (string, error) {
|
||||
var numberOfchecks int
|
||||
CHECK:
|
||||
numberOfchecks++
|
||||
conn, err := net.Listen("tcp", ip+":"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "address already in use") {
|
||||
if numberOfchecks == 1000 {
|
||||
return "", err
|
||||
}
|
||||
port++
|
||||
goto CHECK
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
conn.Close()
|
||||
return strconv.Itoa(port), nil
|
||||
}
|
@@ -1,304 +0,0 @@
|
||||
package soapcalls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PlayEnvelope - As in Play Pause Stop.
|
||||
type PlayEnvelope struct {
|
||||
XMLName xml.Name `xml:"s:Envelope"`
|
||||
Schema string `xml:"xmlns:s,attr"`
|
||||
Encoding string `xml:"s:encodingStyle,attr"`
|
||||
PlayBody PlayBody `xml:"s:Body"`
|
||||
}
|
||||
|
||||
// PlayBody .
|
||||
type PlayBody struct {
|
||||
XMLName xml.Name `xml:"s:Body"`
|
||||
PlayAction PlayAction `xml:"u:Play"`
|
||||
}
|
||||
|
||||
// PlayAction .
|
||||
type PlayAction struct {
|
||||
XMLName xml.Name `xml:"u:Play"`
|
||||
AVTransport string `xml:"xmlns:u,attr"`
|
||||
InstanceID string
|
||||
Speed string
|
||||
}
|
||||
|
||||
// PauseEnvelope - As in Play Pause Stop.
|
||||
type PauseEnvelope struct {
|
||||
XMLName xml.Name `xml:"s:Envelope"`
|
||||
Schema string `xml:"xmlns:s,attr"`
|
||||
Encoding string `xml:"s:encodingStyle,attr"`
|
||||
PauseBody PauseBody `xml:"s:Body"`
|
||||
}
|
||||
|
||||
// PauseBody .
|
||||
type PauseBody struct {
|
||||
XMLName xml.Name `xml:"s:Body"`
|
||||
PauseAction PauseAction `xml:"u:Pause"`
|
||||
}
|
||||
|
||||
// PauseAction .
|
||||
type PauseAction struct {
|
||||
XMLName xml.Name `xml:"u:Pause"`
|
||||
AVTransport string `xml:"xmlns:u,attr"`
|
||||
InstanceID string
|
||||
Speed string
|
||||
}
|
||||
|
||||
// StopEnvelope - As in Play Pause Stop.
|
||||
type StopEnvelope struct {
|
||||
XMLName xml.Name `xml:"s:Envelope"`
|
||||
Schema string `xml:"xmlns:s,attr"`
|
||||
Encoding string `xml:"s:encodingStyle,attr"`
|
||||
StopBody StopBody `xml:"s:Body"`
|
||||
}
|
||||
|
||||
// StopBody .
|
||||
type StopBody struct {
|
||||
XMLName xml.Name `xml:"s:Body"`
|
||||
StopAction StopAction `xml:"u:Stop"`
|
||||
}
|
||||
|
||||
// StopAction .
|
||||
type StopAction struct {
|
||||
XMLName xml.Name `xml:"u:Stop"`
|
||||
AVTransport string `xml:"xmlns:u,attr"`
|
||||
InstanceID string
|
||||
Speed string
|
||||
}
|
||||
|
||||
// SetAVTransportEnvelope .
|
||||
type SetAVTransportEnvelope struct {
|
||||
XMLName xml.Name `xml:"s:Envelope"`
|
||||
Schema string `xml:"xmlns:s,attr"`
|
||||
Encoding string `xml:"s:encodingStyle,attr"`
|
||||
Body SetAVTransportBody `xml:"s:Body"`
|
||||
}
|
||||
|
||||
// SetAVTransportBody .
|
||||
type SetAVTransportBody struct {
|
||||
XMLName xml.Name `xml:"s:Body"`
|
||||
SetAVTransportURI SetAVTransportURI `xml:"u:SetAVTransportURI"`
|
||||
}
|
||||
|
||||
// SetAVTransportURI .
|
||||
type SetAVTransportURI struct {
|
||||
XMLName xml.Name `xml:"u:SetAVTransportURI"`
|
||||
AVTransport string `xml:"xmlns:u,attr"`
|
||||
InstanceID string
|
||||
CurrentURI string
|
||||
CurrentURIMetaData CurrentURIMetaData `xml:"CurrentURIMetaData"`
|
||||
}
|
||||
|
||||
// CurrentURIMetaData .
|
||||
type CurrentURIMetaData struct {
|
||||
XMLName xml.Name `xml:"CurrentURIMetaData"`
|
||||
Value []byte `xml:",chardata"`
|
||||
}
|
||||
|
||||
// DIDLLite .
|
||||
type DIDLLite struct {
|
||||
XMLName xml.Name `xml:"DIDL-Lite"`
|
||||
SchemaDIDL string `xml:"xmlns,attr"`
|
||||
DC string `xml:"xmlns:dc,attr"`
|
||||
Sec string `xml:"xmlns:sec,attr"`
|
||||
SchemaUPNP string `xml:"xmlns:upnp,attr"`
|
||||
DIDLLiteItem DIDLLiteItem `xml:"item"`
|
||||
}
|
||||
|
||||
// DIDLLiteItem .
|
||||
type DIDLLiteItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
ID string `xml:"id,attr"`
|
||||
ParentID string `xml:"parentID,attr"`
|
||||
Restricted string `xml:"restricted,attr"`
|
||||
UPNPClass string `xml:"upnp:class"`
|
||||
DCtitle string `xml:"dc:title"`
|
||||
ResNode []ResNode `xml:"res"`
|
||||
SecCaptionInfo SecCaptionInfo `xml:"sec:CaptionInfo"`
|
||||
SecCaptionInfoEx SecCaptionInfoEx `xml:"sec:CaptionInfoEx"`
|
||||
}
|
||||
|
||||
// ResNode .
|
||||
type ResNode struct {
|
||||
XMLName xml.Name `xml:"res"`
|
||||
ProtocolInfo string `xml:"protocolInfo,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// SecCaptionInfo .
|
||||
type SecCaptionInfo struct {
|
||||
XMLName xml.Name `xml:"sec:CaptionInfo"`
|
||||
Type string `xml:"sec:type,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// SecCaptionInfoEx .
|
||||
type SecCaptionInfoEx struct {
|
||||
XMLName xml.Name `xml:"sec:CaptionInfoEx"`
|
||||
Type string `xml:"sec:type,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
func setAVTransportSoapBuild(videoURL, subtitleURL string) ([]byte, error) {
|
||||
var videoTitle string
|
||||
|
||||
videoTitlefromURL, err := url.Parse(videoURL)
|
||||
if err != nil {
|
||||
videoTitle = videoURL
|
||||
} else {
|
||||
videoTitle = strings.TrimLeft(videoTitlefromURL.Path, "/")
|
||||
}
|
||||
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: DIDLLiteItem{
|
||||
XMLName: xml.Name{},
|
||||
ID: "0",
|
||||
ParentID: "-1",
|
||||
Restricted: "false",
|
||||
UPNPClass: "object.item.videoItem.movie",
|
||||
DCtitle: videoTitle,
|
||||
ResNode: []ResNode{{
|
||||
XMLName: xml.Name{},
|
||||
ProtocolInfo: "http-get:*:video/mp4:*",
|
||||
Value: videoURL,
|
||||
}, {
|
||||
XMLName: xml.Name{},
|
||||
ProtocolInfo: "http-get:*:text/srt:*",
|
||||
Value: subtitleURL,
|
||||
},
|
||||
},
|
||||
SecCaptionInfo: SecCaptionInfo{
|
||||
XMLName: xml.Name{},
|
||||
Type: "srt",
|
||||
Value: subtitleURL,
|
||||
},
|
||||
SecCaptionInfoEx: SecCaptionInfoEx{
|
||||
XMLName: xml.Name{},
|
||||
Type: "srt",
|
||||
Value: subtitleURL,
|
||||
},
|
||||
},
|
||||
}
|
||||
a, err := xml.Marshal(l)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return make([]byte, 0), err
|
||||
}
|
||||
|
||||
d := SetAVTransportEnvelope{
|
||||
XMLName: xml.Name{},
|
||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
Body: SetAVTransportBody{
|
||||
XMLName: xml.Name{},
|
||||
SetAVTransportURI: SetAVTransportURI{
|
||||
XMLName: xml.Name{},
|
||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
||||
InstanceID: "0",
|
||||
CurrentURI: videoURL,
|
||||
CurrentURIMetaData: CurrentURIMetaData{
|
||||
XMLName: xml.Name{},
|
||||
Value: a,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
||||
b, err := xml.Marshal(d)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return make([]byte, 0), err
|
||||
}
|
||||
// That looks like an issue just with my Samsung TV.
|
||||
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{},
|
||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
PlayBody: PlayBody{
|
||||
XMLName: xml.Name{},
|
||||
PlayAction: PlayAction{
|
||||
XMLName: xml.Name{},
|
||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
||||
InstanceID: "0",
|
||||
Speed: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
||||
b, err := xml.Marshal(d)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return make([]byte, 0), err
|
||||
}
|
||||
|
||||
return append(xmlStart, b...), nil
|
||||
}
|
||||
|
||||
func stopSoapBuild() ([]byte, error) {
|
||||
d := StopEnvelope{
|
||||
XMLName: xml.Name{},
|
||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
StopBody: StopBody{
|
||||
XMLName: xml.Name{},
|
||||
StopAction: StopAction{
|
||||
XMLName: xml.Name{},
|
||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
||||
InstanceID: "0",
|
||||
Speed: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
||||
b, err := xml.Marshal(d)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return make([]byte, 0), err
|
||||
}
|
||||
|
||||
return append(xmlStart, b...), nil
|
||||
}
|
||||
|
||||
func pauseSoapBuild() ([]byte, error) {
|
||||
d := PauseEnvelope{
|
||||
XMLName: xml.Name{},
|
||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
PauseBody: PauseBody{
|
||||
XMLName: xml.Name{},
|
||||
PauseAction: PauseAction{
|
||||
XMLName: xml.Name{},
|
||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
||||
InstanceID: "0",
|
||||
Speed: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
||||
b, err := xml.Marshal(d)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return make([]byte, 0), err
|
||||
}
|
||||
|
||||
return append(xmlStart, b...), nil
|
||||
}
|
@@ -1,318 +0,0 @@
|
||||
package soapcalls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type states struct {
|
||||
previousState string
|
||||
newState string
|
||||
sequence int
|
||||
}
|
||||
|
||||
var mediaRenderersStates = make(map[string]*states)
|
||||
var initialMediaRenderersStates = make(map[string]interface{})
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
// TVPayload - this is the heard of Go2TV.
|
||||
type TVPayload struct {
|
||||
TransportURL string
|
||||
VideoURL string
|
||||
SubtitlesURL string
|
||||
ControlURL string
|
||||
CallbackURL string
|
||||
}
|
||||
|
||||
func (p *TVPayload) setAVTransportSoapCall() error {
|
||||
parsedURLtransport, err := url.Parse(p.TransportURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xml, err := setAVTransportSoapBuild(p.VideoURL, p.SubtitlesURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := http.Header{
|
||||
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"`},
|
||||
"content-type": []string{"text/xml"},
|
||||
"charset": []string{"utf-8"},
|
||||
"Connection": []string{"close"},
|
||||
}
|
||||
req.Header = headers
|
||||
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlayStopSoapCall - Build and call the play soap call.
|
||||
func (p *TVPayload) playStopPauseSoapCall(action string) error {
|
||||
parsedURLtransport, err := url.Parse(p.TransportURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var xml []byte
|
||||
if action == "Play" {
|
||||
xml, err = playSoapBuild()
|
||||
}
|
||||
if action == "Stop" {
|
||||
xml, err = stopSoapBuild()
|
||||
}
|
||||
if action == "Pause" {
|
||||
xml, err = pauseSoapBuild()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := http.Header{
|
||||
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#` + action + `"`},
|
||||
"content-type": []string{"text/xml"},
|
||||
"charset": []string{"utf-8"},
|
||||
"Connection": []string{"close"},
|
||||
}
|
||||
req.Header = headers
|
||||
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubscribeSoapCall - Subscribe to a media renderer
|
||||
// If we explicitly pass the uuid, then we refresh it instead.
|
||||
func (p *TVPayload) SubscribeSoapCall(uuidInput string) error {
|
||||
parsedURLcontrol, err := url.Parse(p.ControlURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedURLcallback, err := url.Parse(p.CallbackURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
req, err := http.NewRequest("SUBSCRIBE", parsedURLcontrol.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var headers http.Header
|
||||
if uuidInput == "" {
|
||||
headers = http.Header{
|
||||
"USER-AGENT": []string{runtime.GOOS + " UPnP/1.1 " + "Go2TV"},
|
||||
"CALLBACK": []string{"<" + parsedURLcallback.String() + ">"},
|
||||
"NT": []string{"upnp:event"},
|
||||
"TIMEOUT": []string{"Second-300"},
|
||||
"Connection": []string{"close"},
|
||||
}
|
||||
} else {
|
||||
headers = http.Header{
|
||||
"SID": []string{"uuid:" + uuidInput},
|
||||
"TIMEOUT": []string{"Second-300"},
|
||||
"Connection": []string{"close"},
|
||||
}
|
||||
}
|
||||
req.Header = headers
|
||||
req.Header.Del("User-Agent")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
uuid := resp.Header["Sid"][0]
|
||||
uuid = strings.TrimLeft(uuid, "[")
|
||||
uuid = strings.TrimLeft(uuid, "]")
|
||||
uuid = strings.TrimLeft(uuid, "uuid:")
|
||||
|
||||
// We don't really need to initialize or set
|
||||
// the State if we're just refreshing the uuid.
|
||||
if uuidInput == "" {
|
||||
CreateMRstate(uuid)
|
||||
}
|
||||
|
||||
timeoutReply := strings.TrimLeft(resp.Header["Timeout"][0], "Second-")
|
||||
p.RefreshLoopUUIDSoapCall(uuid, timeoutReply)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnsubscribeSoapCall - exported that as we use
|
||||
// it for the callback stuff in the httphandlers package.
|
||||
func (p *TVPayload) UnsubscribeSoapCall(uuid string) error {
|
||||
parsedURLcontrol, err := url.Parse(p.ControlURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
req, err := http.NewRequest("UNSUBSCRIBE", parsedURLcontrol.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := http.Header{
|
||||
"SID": []string{"uuid:" + uuid},
|
||||
"Connection": []string{"close"},
|
||||
}
|
||||
|
||||
req.Header = headers
|
||||
req.Header.Del("User-Agent")
|
||||
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
DeleteMRstate(uuid)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshLoopUUIDSoapCall - Refresh the UUID.
|
||||
func (p *TVPayload) RefreshLoopUUIDSoapCall(uuid, timeout string) error {
|
||||
var triggerTime int
|
||||
timeoutInt, err := strconv.Atoi(timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Refresh token after after Timeout / 2 seconds.
|
||||
if timeoutInt > 1 {
|
||||
triggerTime = timeoutInt / 2
|
||||
}
|
||||
|
||||
triggerTimefunc := time.Duration(triggerTime) * time.Second
|
||||
|
||||
// We're doing this as time.AfterFunc can't handle
|
||||
// function arguments.
|
||||
f := p.refreshLoopUUIDAsyncSoapCall(uuid)
|
||||
time.AfterFunc(triggerTimefunc, f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *TVPayload) refreshLoopUUIDAsyncSoapCall(uuid string) func() {
|
||||
return func() {
|
||||
p.SubscribeSoapCall(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// SendtoTV - Send to TV.
|
||||
func (p *TVPayload) SendtoTV(action string) error {
|
||||
if action == "Play1" {
|
||||
if err := p.SubscribeSoapCall(""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.setAVTransportSoapCall(); err != nil {
|
||||
return err
|
||||
}
|
||||
action = "Play"
|
||||
}
|
||||
|
||||
if action == "Stop" {
|
||||
// Cleaning up all uuids on force stop.
|
||||
for uuids := range mediaRenderersStates {
|
||||
p.UnsubscribeSoapCall(uuids)
|
||||
}
|
||||
}
|
||||
if err := p.playStopPauseSoapCall(action); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateMRstate - Update the mediaRenderersStates map
|
||||
// with the state. Return true or false to verify that
|
||||
// the actual update took place.
|
||||
func UpdateMRstate(previous, new, uuid string) bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
// If the uuid is not one of the UUIDs we stored in
|
||||
// soapcalls.InitialMediaRenderersStates it means that
|
||||
// probably it expired and there is not much we can do
|
||||
// with it. Trying to send an unsubscribe for those will
|
||||
// probably result in a 412 error as per the upnpn documentation
|
||||
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
||||
// (page 94).
|
||||
if initialMediaRenderersStates[uuid] == true {
|
||||
mediaRenderersStates[uuid].previousState = previous
|
||||
mediaRenderersStates[uuid].newState = new
|
||||
mediaRenderersStates[uuid].sequence++
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CreateMRstate .
|
||||
func CreateMRstate(uuid string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
initialMediaRenderersStates[uuid] = true
|
||||
mediaRenderersStates[uuid] = &states{
|
||||
previousState: "",
|
||||
newState: "",
|
||||
sequence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteMRstate .
|
||||
func DeleteMRstate(uuid string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
delete(initialMediaRenderersStates, uuid)
|
||||
delete(mediaRenderersStates, uuid)
|
||||
}
|
||||
|
||||
// IncreaseSequence .
|
||||
func IncreaseSequence(uuid string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
mediaRenderersStates[uuid].sequence++
|
||||
}
|
||||
|
||||
// GetSequence .
|
||||
func GetSequence(uuid string) (int, error) {
|
||||
if initialMediaRenderersStates[uuid] == true {
|
||||
return mediaRenderersStates[uuid].sequence, nil
|
||||
}
|
||||
|
||||
return -1, errors.New("zombie callbacks, we should ignore those")
|
||||
}
|
@@ -1,110 +0,0 @@
|
||||
package soapcalls
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Root - root node.
|
||||
type Root struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Device Device `xml:"device"`
|
||||
}
|
||||
|
||||
// Device - device node (we should only expect one?).
|
||||
type Device struct {
|
||||
XMLName xml.Name `xml:"device"`
|
||||
ServiceList ServiceList `xml:"serviceList"`
|
||||
}
|
||||
|
||||
// ServiceList - serviceList node
|
||||
type ServiceList struct {
|
||||
XMLName xml.Name `xml:"serviceList"`
|
||||
Services []Service `xml:"service"`
|
||||
}
|
||||
|
||||
// Service - service node.
|
||||
type Service struct {
|
||||
XMLName xml.Name `xml:"service"`
|
||||
Type string `xml:"serviceType"`
|
||||
ID string `xml:"serviceId"`
|
||||
ControlURL string `xml:"controlURL"`
|
||||
EventSubURL string `xml:"eventSubURL"`
|
||||
}
|
||||
|
||||
// EventPropertySet .
|
||||
type EventPropertySet struct {
|
||||
XMLName xml.Name `xml:"propertyset"`
|
||||
EventInstance EventInstance `xml:"property>LastChange>Event>InstanceID"`
|
||||
}
|
||||
|
||||
// EventInstance .
|
||||
type EventInstance struct {
|
||||
XMLName xml.Name `xml:"InstanceID"`
|
||||
Value string `xml:"val,attr"`
|
||||
EventCurrentTransportActions EventCurrentTransportActions `xml:"CurrentTransportActions"`
|
||||
EventTransportState EventTransportState `xml:"TransportState"`
|
||||
}
|
||||
|
||||
// EventCurrentTransportActions .
|
||||
type EventCurrentTransportActions struct {
|
||||
Value string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
// EventTransportState .
|
||||
type EventTransportState struct {
|
||||
Value string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
// DMRextractor - Get the AVTransport URL from the main DMR xml.
|
||||
func DMRextractor(dmrurl string) (string, string, error) {
|
||||
var root Root
|
||||
|
||||
parsedURL, err := url.Parse(dmrurl)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", dmrurl, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
xmlresp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer xmlresp.Body.Close()
|
||||
|
||||
xmlbody, err := io.ReadAll(xmlresp.Body)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
xml.Unmarshal(xmlbody, &root)
|
||||
for i := 0; i < len(root.Device.ServiceList.Services); i++ {
|
||||
if root.Device.ServiceList.Services[i].ID == "urn:upnp-org:serviceId:AVTransport" {
|
||||
avtransportControlURL := parsedURL.Scheme + "://" + parsedURL.Host + root.Device.ServiceList.Services[i].ControlURL
|
||||
avtransportEventSubURL := parsedURL.Scheme + "://" + parsedURL.Host + root.Device.ServiceList.Services[i].EventSubURL
|
||||
return avtransportControlURL, avtransportEventSubURL, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", errors.New("something broke somewhere - wrong DMR URL?")
|
||||
}
|
||||
|
||||
// EventNotifyParser - Parse the Notify messages from the media renderer.
|
||||
func EventNotifyParser(xmlbody string) (string, string, error) {
|
||||
var root EventPropertySet
|
||||
err := xml.Unmarshal([]byte(xmlbody), &root)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
previousstate := root.EventInstance.EventCurrentTransportActions.Value
|
||||
newstate := root.EventInstance.EventTransportState.Value
|
||||
|
||||
return previousstate, newstate, nil
|
||||
}
|
Reference in New Issue
Block a user