That was one weird dev work. I hate the upnp protocol. I left some debug lines as I need to have a second look. Just commiting my latest changes

This commit is contained in:
Alex Ballas
2021-02-14 02:27:34 +02:00
parent 4916540d24
commit 2f14f6052e
4 changed files with 244 additions and 24 deletions

View File

@@ -8,10 +8,11 @@ import (
"os/signal"
"path/filepath"
"sort"
"sync"
"syscall"
"github.com/alexballas/go2tv/httphandlers"
"github.com/alexballas/go2tv/iptools"
"github.com/alexballas/go2tv/servefiles"
"github.com/alexballas/go2tv/soapcalls"
"github.com/koron/go-ssdp"
)
@@ -51,7 +52,7 @@ func main() {
os.Exit(1)
}
transportURL, _, err := soapcalls.DMRextractor(dmrURL)
transportURL, controlURL, err := soapcalls.DMRextractor(dmrURL)
if err != nil {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
os.Exit(1)
@@ -62,11 +63,14 @@ func main() {
fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
os.Exit(1)
}
var mu sync.Mutex
tvdata := &soapcalls.TVPayload{
TransportURL: transportURL,
ControlURL: controlURL,
CallbackURL: "http://" + whereToListen + "/callback",
VideoURL: "http://" + whereToListen + "/" + filepath.Base(absVideoFile),
SubtitlesURL: "http://" + whereToListen + "/" + filepath.Base(absSubtitlesFile),
Mu: &mu,
}
if err != nil {
@@ -74,8 +78,13 @@ func main() {
os.Exit(1)
}
s := servefiles.NewServer(whereToListen)
go func() { s.ServeFiles(serverStarted, absVideoFile, absSubtitlesFile) }()
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.TVPayload{Soapcalls: *tvdata})
}()
// Wait for HTTP server to properly initialize
<-serverStarted

View File

@@ -1,11 +1,16 @@
package servefiles
package httphandlers
import (
"fmt"
"html"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/alexballas/go2tv/soapcalls"
)
// filesToServe defines the files we need to serve
@@ -19,8 +24,16 @@ type HTTPserver struct {
http *http.Server
}
// TVPayload - We need some of the soapcalls magic in
// this package too. We need to expose the ControlURL
// to the callback handler
type TVPayload struct {
Soapcalls soapcalls.TVPayload
}
// ServeFiles - Start HTTP server and serve file
func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, videoPath, subtitlesPath string) {
func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, videoPath, subtitlesPath string, tvpayload *TVPayload) {
files := &filesToServe{
Video: videoPath,
Subtitles: subtitlesPath,
@@ -28,6 +41,7 @@ func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, videoPath, subtit
http.HandleFunc("/"+filepath.Base(files.Video), files.serveVideoHandler)
http.HandleFunc("/"+filepath.Base(files.Subtitles), files.serveSubtitlesHandler)
http.HandleFunc("/callback", tvpayload.callbackHandler)
ln, err := net.Listen("tcp", s.http.Addr)
if err != nil {
@@ -85,6 +99,51 @@ func (f *filesToServe) serveSubtitlesHandler(w http.ResponseWriter, req *http.Re
}
func (p *TVPayload) callbackHandler(w http.ResponseWriter, req *http.Request) {
reqParsed, _ := ioutil.ReadAll(req.Body)
// I'm sure there's a smarter way to do it.
// I'm not smart.
uuid := req.Header["Sid"][0]
uuid = strings.TrimLeft(uuid, "[")
uuid = strings.TrimLeft(uuid, "]")
uuid = strings.TrimLeft(uuid, "uuid:")
previousstate, newstate, err := soapcalls.EventNotifyParser(html.UnescapeString(string(reqParsed)))
if err != nil {
http.Error(w, "", 404)
return
}
// If the uuid is not one of the UUIDs we storred 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 soapcalls.InitialMediaRenderersStates[uuid] == true {
p.Soapcalls.Mu.Lock()
soapcalls.MediaRenderersStates[uuid] = map[string]string{
"PreviousState": previousstate,
"NewState": newstate,
}
p.Soapcalls.Mu.Unlock()
} else {
http.Error(w, "", 404)
return
}
fmt.Println("ALEX CALLBACK")
fmt.Println(soapcalls.MediaRenderersStates)
fmt.Println("-------")
// TODO - Properly reply to that
fmt.Fprintf(w, "OK\n")
return
}
// NewServer - create a new HTTP server
func NewServer(a string) HTTPserver {
return HTTPserver{

View File

@@ -5,22 +5,34 @@ import (
"fmt"
"net/http"
"net/url"
"runtime"
"strings"
"sync"
)
// MediaRenderersStates - We hold the states and uuids here
var MediaRenderersStates = make(map[string]map[string]string)
// InitialMediaRenderersStates - Just storing the subscription uuids here
var InitialMediaRenderersStates = make(map[string]interface{})
// TVPayload - we need this populated in order
type TVPayload struct {
TransportURL string
VideoURL string
SubtitlesURL string
ControlURL string
CallbackURL string
Mu *sync.Mutex
}
func setAVTransportSoapCall(videoURL, subtitleURL, transporturl string) error {
parsedURLtransport, err := url.Parse(transporturl)
func (p *TVPayload) setAVTransportSoapCall() error {
parsedURLtransport, err := url.Parse(p.TransportURL)
if err != nil {
return err
}
xml, err := setAVTransportSoapBuild(videoURL, subtitleURL)
xml, err := setAVTransportSoapBuild(p.VideoURL, p.SubtitlesURL)
client := &http.Client{}
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
if err != nil {
@@ -43,8 +55,8 @@ func setAVTransportSoapCall(videoURL, subtitleURL, transporturl string) error {
}
// PlayStopSoapCall - Build and call the play soap call
func playStopSoapCall(action, transporturl string) error {
parsedURLtransport, err := url.Parse(transporturl)
func (p *TVPayload) playStopSoapCall(action string) error {
parsedURLtransport, err := url.Parse(p.TransportURL)
if err != nil {
return err
}
@@ -79,22 +91,124 @@ func playStopSoapCall(action, transporturl string) error {
return nil
}
// SendtoTV - Send to tv
func (p *TVPayload) SendtoTV(action string) error {
if action == "Play" {
if err := setAVTransportSoapCall(p.VideoURL, p.SubtitlesURL, p.TransportURL); err != nil {
// SubscribeSoapCall - Let's export that too,
// why not
func (p *TVPayload) SubscribeSoapCall() error {
parsedURLcontrol, err := url.Parse(p.ControlURL)
if err != nil {
return err
}
}
if err := playStopSoapCall(action, p.TransportURL); err != nil {
parsedURLcallback, err := url.Parse(p.CallbackURL)
if err != nil {
return err
}
if action == "Play" {
fmt.Println("Streaming to Device..")
client := &http.Client{}
req, err := http.NewRequest("SUBSCRIBE", parsedURLcontrol.String(), nil)
if err != nil {
return err
}
if action == "Stop" {
fmt.Println("Stopping stream..")
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"},
}
req.Header = headers
req.Header.Del("User-Agent")
resp, err := client.Do(req)
if err != nil {
return err
}
uuid := resp.Header["Sid"][0]
uuid = strings.TrimLeft(uuid, "[")
uuid = strings.TrimLeft(uuid, "]")
uuid = strings.TrimLeft(uuid, "uuid:")
p.Mu.Lock()
InitialMediaRenderersStates[uuid] = true
MediaRenderersStates[uuid] = map[string]string{
"PreviousState": "",
"NewState": "",
}
p.Mu.Unlock()
fmt.Println("ALEX SUBSCRIBE")
fmt.Println(MediaRenderersStates)
fmt.Println("-------")
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")
resp, err := client.Do(req)
if err != nil {
return err
}
p.Mu.Lock()
delete(InitialMediaRenderersStates, uuid)
delete(MediaRenderersStates, uuid)
p.Mu.Unlock()
fmt.Println("ALEX UNSUBSCRIBE")
fmt.Println(MediaRenderersStates)
fmt.Println(resp.StatusCode)
fmt.Println("-------")
return nil
}
// SendtoTV - Send to tv
func (p *TVPayload) SendtoTV(action string) error {
if action == "Play" {
if err := p.SubscribeSoapCall(); err != nil {
return err
}
if err := p.setAVTransportSoapCall(); err != nil {
return err
}
fmt.Println("Streaming to Device..")
}
if action == "Stop" {
// Cleaning up all uuids until we start
// supporting multiple streaming devices
for uuids := range MediaRenderersStates {
p.UnsubscribeSoapCall(uuids)
}
fmt.Println("Stopping stream..")
}
if err := p.playStopSoapCall(action); err != nil {
return err
}
return nil
}

View File

@@ -35,6 +35,30 @@ type Service struct {
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
@@ -62,3 +86,17 @@ func DMRextractor(dmrurl string) (string, string, error) {
}
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
}