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:
19
go2tv.go
19
go2tv.go
@@ -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
|
||||
|
||||
|
@@ -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{
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
}
|
Reference in New Issue
Block a user