Changes to be committed:
modified: cmd/pmomusic/main.go new file: internal/netutils/ip_detect.go new file: internal/netutils/list_all_ip.go modified: internal/soap/soap.go modified: internal/ssdp/responder.go modified: internal/ssdp/ssdp.go modified: internal/upnp/http.go new file: internal/upnp/server.go new file: internal/upnp/xml/AVTransport.xml new file: internal/upnp/xml/ConnectionManager.xml new file: internal/upnp/xml/RenderingControl.xml
This commit is contained in:
@@ -3,12 +3,18 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/netutils"
|
||||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/ssdp"
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/ssdp"
|
||||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
localIP := ssdp.GetLocalIP()
|
localIP, err := netutils.GuessLocalIP()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not guess local IP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
location := "http://" + localIP + ":1400/description.xml"
|
location := "http://" + localIP + ":1400/description.xml"
|
||||||
usn := "uuid:pmomusic-renderer-001"
|
usn := "uuid:pmomusic-renderer-001"
|
||||||
|
|
||||||
@@ -18,5 +24,5 @@ func main() {
|
|||||||
|
|
||||||
// Lance le serveur HTTP
|
// Lance le serveur HTTP
|
||||||
log.Println("Starting HTTP server at:", location)
|
log.Println("Starting HTTP server at:", location)
|
||||||
upnp.StartHTTPServer(usn)
|
upnp.StartHTTPServer(usn, localIP, 1400)
|
||||||
}
|
}
|
||||||
|
|||||||
92
internal/netutils/ip_detect.go
Normal file
92
internal/netutils/ip_detect.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package netutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GuessLocalIP returns the best-guess local IP address usable for UPnP location,
|
||||||
|
// in order of preference: eth0 > en* > wl* > any private, non-loopback IP.
|
||||||
|
func GuessLocalIP() (string, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
type scoredIP struct {
|
||||||
|
ip net.IP
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates []scoredIP
|
||||||
|
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
// Skip interfaces that are down or loopback
|
||||||
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip = v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
ip = v.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip non-IPv4 or non-private addresses
|
||||||
|
if ip == nil || ip.IsLoopback() || ip.To4() == nil || !isPrivateIPv4(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
score := scoreInterfaceName(iface.Name)
|
||||||
|
candidates = append(candidates, scoredIP{ip: ip, score: score})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return "", errors.New("no suitable local IP found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer interfaces with higher score
|
||||||
|
sort.SliceStable(candidates, func(i, j int) bool {
|
||||||
|
return candidates[i].score > candidates[j].score
|
||||||
|
})
|
||||||
|
|
||||||
|
return candidates[0].ip.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPrivateIPv4(ip net.IP) bool {
|
||||||
|
private := []string{
|
||||||
|
"10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.2", "192.168.",
|
||||||
|
}
|
||||||
|
ipStr := ip.String()
|
||||||
|
for _, p := range private {
|
||||||
|
if strings.HasPrefix(ipStr, p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func scoreInterfaceName(name string) int {
|
||||||
|
switch {
|
||||||
|
case name == "eth0":
|
||||||
|
return 100
|
||||||
|
case strings.HasPrefix(name, "en"):
|
||||||
|
return 80
|
||||||
|
case name == "wlan0" || strings.HasPrefix(name, "wl"):
|
||||||
|
return 60
|
||||||
|
default:
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/netutils/list_all_ip.go
Normal file
49
internal/netutils/list_all_ip.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package netutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListAllIPs returns a map of interface names to their associated IPv4 addresses.
|
||||||
|
func ListAllIPs() map[string][]string {
|
||||||
|
result := make(map[string][]string)
|
||||||
|
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
result["error"] = []string{err.Error()}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 {
|
||||||
|
continue // Ignore down interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var ips []string
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip = v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
ip = v.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip == nil || ip.To4() == nil || ip.IsLoopback() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ips = append(ips, ip.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ips) > 0 {
|
||||||
|
result[iface.Name] = ips
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,18 +1,69 @@
|
|||||||
package soap
|
package soap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleSOAP(w http.ResponseWriter, r *http.Request) {
|
func HandleSOAP(w http.ResponseWriter, r *http.Request) {
|
||||||
action := r.Header.Get("SOAPACTION")
|
body, err := io.ReadAll(r.Body)
|
||||||
body, _ := io.ReadAll(r.Body)
|
if err != nil {
|
||||||
|
log.Println("Failed to read SOAP request:", err)
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
log.Println("----------")
|
log.Println("Received SOAP request:")
|
||||||
log.Println("SOAPAction:", action)
|
log.Println(string(body))
|
||||||
log.Println("Body:\n", string(body))
|
|
||||||
|
|
||||||
|
// Extract action name from the SOAP body
|
||||||
|
action := extractSOAPAction(body)
|
||||||
|
log.Printf("SOAP Action: %s\n", action)
|
||||||
|
|
||||||
|
// Send dummy response
|
||||||
|
w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(dummySOAPResponse(action)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSOAPAction(body []byte) string {
|
||||||
|
type Envelope struct {
|
||||||
|
Body struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
} `xml:"Body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var env Envelope
|
||||||
|
if err := xml.Unmarshal(body, &env); err != nil {
|
||||||
|
log.Println("SOAP parse error:", err)
|
||||||
|
return "UnknownAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(body))
|
||||||
|
for {
|
||||||
|
tok, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if se, ok := tok.(xml.StartElement); ok && se.Name.Local != "Envelope" && se.Name.Local != "Body" {
|
||||||
|
return se.Name.Local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "UnknownAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
func dummySOAPResponse(action string) string {
|
||||||
|
// You can specialize per action if needed.
|
||||||
|
return `<?xml version="1.0"?>
|
||||||
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
||||||
|
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||||
|
<s:Body>
|
||||||
|
<u:` + action + `Response xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
||||||
|
</u:` + action + `Response>
|
||||||
|
</s:Body>
|
||||||
|
</s:Envelope>`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,19 @@ func StartSSDPResponder(usn, location string) {
|
|||||||
st := extractHeader(data, "ST")
|
st := extractHeader(data, "ST")
|
||||||
log.Printf("🔎 M-SEARCH from %s, ST=%s\n", src.String(), st)
|
log.Printf("🔎 M-SEARCH from %s, ST=%s\n", src.String(), st)
|
||||||
|
|
||||||
if st == "ssdp:all" || st == serviceType {
|
if st == "ssdp:all" ||
|
||||||
go sendSSDPResponse(src, usn, location)
|
st == serviceType ||
|
||||||
|
strings.HasPrefix(st, "urn:schemas-upnp-org:service:") ||
|
||||||
|
strings.HasPrefix(st, "urn:av-openhome-org:service:") ||
|
||||||
|
strings.HasPrefix(st, "urn:bubblesoftapps-com:service:") {
|
||||||
|
|
||||||
|
go sendSSDPResponse(src, usn, location, st)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendSSDPResponse(dst *net.UDPAddr, usn, location string) {
|
func sendSSDPResponse(dst *net.UDPAddr, usn, location, st string) {
|
||||||
resp := fmt.Sprintf(
|
resp := fmt.Sprintf(
|
||||||
"HTTP/1.1 200 OK\r\n"+
|
"HTTP/1.1 200 OK\r\n"+
|
||||||
"CACHE-CONTROL: max-age=1800\r\n"+
|
"CACHE-CONTROL: max-age=1800\r\n"+
|
||||||
@@ -63,11 +68,12 @@ func sendSSDPResponse(dst *net.UDPAddr, usn, location string) {
|
|||||||
"\r\n",
|
"\r\n",
|
||||||
time.Now().Format(time.RFC1123),
|
time.Now().Format(time.RFC1123),
|
||||||
location,
|
location,
|
||||||
serviceType,
|
st,
|
||||||
usn,
|
usn,
|
||||||
serviceType,
|
st,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// envoie UDP
|
||||||
conn, err := net.DialUDP("udp4", nil, dst)
|
conn, err := net.DialUDP("udp4", nil, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to dial UDP to respond:", err)
|
log.Println("Failed to dial UDP to respond:", err)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package ssdp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/koron/go-ssdp"
|
"github.com/koron/go-ssdp"
|
||||||
@@ -32,12 +31,3 @@ func AnnounceRenderer(usn, location string) {
|
|||||||
// Un jour on voudra faire ça à l'arrêt :
|
// Un jour on voudra faire ça à l'arrêt :
|
||||||
// adv.Close() // envoie le ssdp:byebye
|
// adv.Close() // envoie le ssdp:byebye
|
||||||
}
|
}
|
||||||
func GetLocalIP() string {
|
|
||||||
conn, err := net.Dial("udp", "239.255.255.250:1900")
|
|
||||||
if err != nil {
|
|
||||||
return "127.0.0.1"
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
|
||||||
return localAddr.IP.String()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,30 +2,16 @@ package upnp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/soap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartHTTPServer(usn string) {
|
func generateDeviceDescription(usn, ip string, port uint) string {
|
||||||
http.HandleFunc("/description.xml", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/xml")
|
|
||||||
w.Write([]byte(generateDeviceDescription(usn)))
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/upnp/control/AVTransport", soap.HandleSOAP)
|
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":1400", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateDeviceDescription(usn string) string {
|
|
||||||
return fmt.Sprintf(`<?xml version="1.0"?>
|
return fmt.Sprintf(`<?xml version="1.0"?>
|
||||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||||
<specVersion>
|
<specVersion>
|
||||||
<major>1</major>
|
<major>1</major>
|
||||||
<minor>0</minor>
|
<minor>0</minor>
|
||||||
</specVersion>
|
</specVersion>
|
||||||
|
<URLBase>http://%s:%d/</URLBase>
|
||||||
<device>
|
<device>
|
||||||
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
||||||
<friendlyName>pmomusic Fake Renderer</friendlyName>
|
<friendlyName>pmomusic Fake Renderer</friendlyName>
|
||||||
@@ -36,7 +22,7 @@ func generateDeviceDescription(usn string) string {
|
|||||||
<modelNumber>1.0</modelNumber>
|
<modelNumber>1.0</modelNumber>
|
||||||
<modelURL>http://example.com/model</modelURL>
|
<modelURL>http://example.com/model</modelURL>
|
||||||
<UDN>%s</UDN>
|
<UDN>%s</UDN>
|
||||||
<presentationURL>http://%s</presentationURL>
|
<presentationURL>http://%s:%d</presentationURL>
|
||||||
<serviceList>
|
<serviceList>
|
||||||
<service>
|
<service>
|
||||||
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
|
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
|
||||||
@@ -61,5 +47,5 @@ func generateDeviceDescription(usn string) string {
|
|||||||
</service>
|
</service>
|
||||||
</serviceList>
|
</serviceList>
|
||||||
</device>
|
</device>
|
||||||
</root>`, usn, "127.0.0.1:1400") // à adapter à ton IP:port réel
|
</root>`, ip, port, usn, ip, port)
|
||||||
}
|
}
|
||||||
|
|||||||
45
internal/upnp/server.go
Normal file
45
internal/upnp/server.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package upnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/soap"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed xml/*.xml
|
||||||
|
var embeddedXML embed.FS
|
||||||
|
|
||||||
|
// ServeStaticXML mounts handlers for SCPD XML files.
|
||||||
|
func ServeStaticXML(mux *http.ServeMux) {
|
||||||
|
subFS, err := fs.Sub(embeddedXML, "xml")
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to create sub FS: " + err.Error())
|
||||||
|
}
|
||||||
|
mux.Handle("/scpd/", http.StripPrefix("/scpd/", http.FileServer(http.FS(subFS))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartHTTPServer(usn, ip string, port uint) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Device description
|
||||||
|
mux.HandleFunc("/description.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/xml")
|
||||||
|
w.Write([]byte(generateDeviceDescription(usn, ip, port)))
|
||||||
|
})
|
||||||
|
|
||||||
|
// SOAP control endpoints
|
||||||
|
mux.HandleFunc("/upnp/control/AVTransport", soap.HandleSOAP)
|
||||||
|
mux.HandleFunc("/upnp/control/RenderingControl", soap.HandleSOAP)
|
||||||
|
mux.HandleFunc("/upnp/control/ConnectionManager", soap.HandleSOAP)
|
||||||
|
|
||||||
|
// Serve static SCPD XML files
|
||||||
|
ServeStaticXML(mux)
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||||
|
log.Printf("Serving UPnP fake renderer at http://%s", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, mux))
|
||||||
|
}
|
||||||
81
internal/upnp/xml/AVTransport.xml
Normal file
81
internal/upnp/xml/AVTransport.xml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||||
|
<specVersion>
|
||||||
|
<major>1</major>
|
||||||
|
<minor>0</minor>
|
||||||
|
</specVersion>
|
||||||
|
<actionList>
|
||||||
|
<action>
|
||||||
|
<name>SetAVTransportURI</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>InstanceID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>CurrentURI</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>AVTransportURI</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>CurrentURIMetaData</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>AVTransportURIMetaData</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>
|
||||||
|
<name>Play</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>InstanceID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Speed</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>TransportPlaySpeed</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>
|
||||||
|
<name>Stop</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>InstanceID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
</actionList>
|
||||||
|
|
||||||
|
<serviceStateTable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_InstanceID</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>AVTransportURI</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>AVTransportURIMetaData</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>TransportPlaySpeed</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
<allowedValueList>
|
||||||
|
<allowedValue>1</allowedValue>
|
||||||
|
</allowedValueList>
|
||||||
|
</stateVariable>
|
||||||
|
</serviceStateTable>
|
||||||
|
</scpd>
|
||||||
31
internal/upnp/xml/ConnectionManager.xml
Normal file
31
internal/upnp/xml/ConnectionManager.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||||
|
<specVersion><major>1</major><minor>0</minor></specVersion>
|
||||||
|
<actionList>
|
||||||
|
<action>
|
||||||
|
<name>GetProtocolInfo</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>Source</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>SourceProtocolInfo</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Sink</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>SinkProtocolInfo</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
</actionList>
|
||||||
|
<serviceStateTable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>SourceProtocolInfo</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>SinkProtocolInfo</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
</serviceStateTable>
|
||||||
|
</scpd>
|
||||||
43
internal/upnp/xml/RenderingControl.xml
Normal file
43
internal/upnp/xml/RenderingControl.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||||
|
<specVersion><major>1</major><minor>0</minor></specVersion>
|
||||||
|
<actionList>
|
||||||
|
<action>
|
||||||
|
<name>SetVolume</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>InstanceID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Channel</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Channel</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>DesiredVolume</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>Volume</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
</actionList>
|
||||||
|
<serviceStateTable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_InstanceID</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_Channel</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
<allowedValueList>
|
||||||
|
<allowedValue>Master</allowedValue>
|
||||||
|
</allowedValueList>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>Volume</name>
|
||||||
|
<dataType>ui2</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
</serviceStateTable>
|
||||||
|
</scpd>
|
||||||
Reference in New Issue
Block a user