From 2426643e9b4d0cc366b268dd9a8311a8c127d705 Mon Sep 17 00:00:00 2001 From: Louay Bassbouss Date: Thu, 10 Jul 2014 12:26:28 +0200 Subject: [PATCH] DIAL node.js module first commit --- .gitignore | 2 + LICENSE | 165 +++++++++++++++++++++++++ Readme.md | 25 ++++ index.js | 21 ++++ lib/peer-dial.js | 294 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 20 +++ test/dial-client.js | 28 +++++ test/dial-server.js | 93 ++++++++++++++ xml/app-desc.xml | 9 ++ xml/device-desc.xml | 33 +++++ 10 files changed, 690 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Readme.md create mode 100644 index.js create mode 100644 lib/peer-dial.js create mode 100644 package.json create mode 100644 test/dial-client.js create mode 100644 test/dial-server.js create mode 100644 xml/app-desc.xml create mode 100644 xml/device-desc.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b09d240 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.project +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6600f1c --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..e752c25 --- /dev/null +++ b/Readme.md @@ -0,0 +1,25 @@ +peer-dial +========= + +peer-ssdp is a simple Node.js module implementing the Discovery and Launch Protocol DIAL as described in the +[Protocol Specification Document](http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf) + +Setup +===== + + * use `npm install peer-dial` to install the module. + * run DIAL Server Example with `node node_modules/peer-dial/test/dial-server.js` + * run DIAL Client Example with `node node_modules/peer-dial/test/dial-client.js` +Usage +===== +TODO + +License +======= + +Free for non commercial use released under the GNU Lesser General Public License v3.0 +, See LICENSE file. + +Contact us for commecial use famecontact@fokus.fraunhofer.de + +Copyright (c) 2013 Fraunhofer FOKUS diff --git a/index.js b/index.js new file mode 100644 index 0000000..f6f0991 --- /dev/null +++ b/index.js @@ -0,0 +1,21 @@ +/******************************************************************************* + * + * Copyright (c) 2013 Louay Bassbouss, Fraunhofer FOKUS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * AUTHORS: Louay Bassbouss (louay.bassbouss@fokus.fraunhofer.de) + * + ******************************************************************************/ + module.exports = require('./lib/peer-dial'); \ No newline at end of file diff --git a/lib/peer-dial.js b/lib/peer-dial.js new file mode 100644 index 0000000..8f5de2f --- /dev/null +++ b/lib/peer-dial.js @@ -0,0 +1,294 @@ +/******************************************************************************* + * + * Copyright (c) 2013 Louay Bassbouss, Fraunhofer FOKUS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * AUTHORS: Louay Bassbouss (louay.bassbouss@fokus.fraunhofer.de) + * + ******************************************************************************/ +var uuid = require('node-uuid'); +var ssdp = require('peer-ssdp'); +var fs = require('fs'); +var ejs = require('ejs'); +var os = require("os"); +var util = require('util'); +var events = require('events'); + +var DEVICE_DESC_TEMPLATE = fs.readFileSync(__dirname + '/../xml/device-desc.xml', 'utf8'); +var APP_DESC_TEMPLATE = fs.readFileSync(__dirname + '/../xml/app-desc.xml', 'utf8'); +var DEVICE_DESC_RENDERER = ejs.compile(DEVICE_DESC_TEMPLATE, { open: '{{', close: '}}' }); +var APP_DESC_RENDERER = ejs.compile(APP_DESC_TEMPLATE, { open: '{{', close: '}}' }); +var SERVER = os.type() + "/" + os.release() + " UPnP/1.1 famium/0.0.1"; +var setupServer = function(){ + var self = this; + var pref = self.prefix; + var peer = self.ssdpPeer; + var serviceTypes = ["urn:dial-multiscreen-org:service:dial:1","urn:dial-multiscreen-org:device:dial:1","upnp:rootdevice","ssdp:all","uuid:"+self.uuid]; + var appStates = ["stopped", "starting", "running"]; + var app = self.expressApp; + app.use(function(req, res, next){ + if (req.is('text/plain')) { + req.text = ''; + req.length = 0; + req.setEncoding('utf8'); + req.on('data', function(chunk){ req.text += chunk; req.length += chunk.length; }); + req.on('end', next); + } else { + next(); + } + }); + app.get(pref+"/apps",function(req,rsp){ + rsp.send(204); + }); + app.get(pref+"/apps/:appName",function(req,rsp){ + var baseURL = req.protocol + "://" + (req.host || req.ip || self.host)+":"+self.port+pref; + var appName = req.param("appName"); + var app = self.delegate.getApp(appName); + if (app) { + var state = app.state || (app.pid && "running") || "stopped"; + var xml = APP_DESC_RENDERER({ + name: appName, + state: state, + allowStop: app.allowStop == true, + rel: "run", + href: app.pid? baseURL+"/apps/"+appName+"/"+app.pid: null + }); + rsp.type('application/xml'); + rsp.send(xml); + } + else { + rsp.send(404); + } + }); + app.post(pref+"/apps/:appName",function(req,rsp){ + var baseURL = req.protocol + "://" + (req.host || req.ip || self.host)+":"+self.port+pref; + var appName = req.param("appName"); + var app = self.delegate.getApp(appName); + if (!app) { + rsp.send(404); + } + else if(req.length && req.length > self.maxContentLength){ + rsp.send(413); // Request Entity Too Large + } + else { + var state = app.state || (app.pid && "running") || "stopped"; + self.delegate.launchApp(appName,req.text || null,function(pid){ + if (pid) { + rsp.setHeader('LOCATION', baseURL+"/apps/"+appName+"/"+pid); + rsp.send(state == "stopped"? 201: 200); + } + else { + rsp.send(500); + } + }); + } + }); + app.post(pref+"/apps/:appName/dial_data",function(req,rsp){ + var baseURL = req.protocol + "://" + (req.host || req.ip || self.host)+":"+self.port+pref; + var appName = req.param("appName"); + var app = self.delegate.getApp(appName); + if (!app) { + rsp.send(404); + } + else if(req.length && req.length > self.maxContentLength){ + rsp.send(413); // Request Entity Too Large + } + else { + // TODO + rsp.send(501); + } + }); + + app.delete(pref+"/apps/:appName/:pid",function(req,rsp){ + var baseURL = req.protocol + "://" + (req.host || req.ip || self.host)+":"+self.port+pref; + var appName = req.param("appName"); + var pid = req.param("pid"); + var app = self.delegate.getApp(appName); + if (app) { + if (app.allowStop) { + if (pid) { + self.delegate.stopApp(appName, pid, function(stopped){ + rsp.send(stopped? 200: 404); + }); + } + else{ + rsp.send(400); + } + } + else { + rsp.send(501); + } + } else { + rsp.send(404); + } + }); + app.get(pref+"/ssdp/device-desc.xml",function(req,rsp){ + var baseURL = req.protocol + "://" + (req.host || req.ip || self.host)+":"+self.port+pref; + var xml = DEVICE_DESC_RENDERER({ + URLBase: baseURL, + friendlyName: self.friendlyName, + manufacturer: self.manufacturer, + modelName: self.modelName, + uuid: self.uuid + }); + rsp.setHeader("Access-Control-Allow-Method", "GET, POST, DELETE, OPTIONS"); + rsp.setHeader("Access-Control-Expose-Headers", "Location"); + rsp.setHeader('Content-Type','application/xml'); + rsp.setHeader('Application-URL', baseURL+"/apps"); + rsp.send(xml); + }); + app.get(pref+"/ssdp/notfound",function(req,rsp){ + rsp.send(404); + }); + + var location = "http://"+self.host+":"+self.port+pref+"/ssdp/device-desc.xml"; + peer.on("ready",function(){ + for (var i = 0; i < serviceTypes.length; i++) { + var st = serviceTypes[i]; + peer.alive(merge({ + NT: st, + USN: "uuid:" + self.uuid + "::"+st, + SERVER: SERVER, + LOCATION: location + },self.extraHeaders)); + }; + self.emit("ready"); + }).on("search",function(headers, address){ + if(serviceTypes.indexOf(headers.ST) != -1) { + peer.reply(merge({ + LOCATION: location, + ST: headers.ST, + "CONFIGID.UPNP.ORG": 7337, + "BOOTID.UPNP.ORG": 7337, + USN: "uuid:"+self.uuid + },self.extraHeaders), address); + } + }).on("close",function(){ + console.log("Server Stopped"); + self.emit("stop"); + });; +}; + +var getExtraHeaders = function(dict){ + var extraHeaders = {}; + if (typeof dict == "object") { + for(var key in dict){ + var value = dict[key]; + if (typeof value == "number" || typeof value == "string" || typeof value == "boolean") { + extraHeaders[key] = value; + }; + } + }; + return extraHeaders; +} + +var merge = function(obj1,obj2){ + for(var key in obj2){ + var val1 = obj1[key]; + obj1[key] = val1 || obj2[key]; + } + return obj1; +} +/** + * + */ +var DIALServer = function (options) { + this.expressApp = options.expressApp || null; + this.prefix = options.prefix || ""; + this.port = options.port || null; + this.host = options.host || null; + this.uuid = options.uuid || uuid.v4(); + this.friendlyName = options.friendlyName || os.hostname() || "unknown"; + this.manufacturer = options.manufacturer || "unknown manufacturer"; + this.modelName = options.modelName || "unknown model"; + this.maxContentLength = Math.max(parseInt(options.maxContentLength) || 4096, 4096); + this.extraHeaders = getExtraHeaders(options.extraHeaders); + this.delegate = {}; + this.delegate.getApp = (options.delegate && typeof options.delegate.getApp == "function")? options.delegate.getApp: null; + this.delegate.launchApp = (options.delegate && typeof options.delegate.launchApp == "function")? options.delegate.launchApp: null; + this.delegate.stopApp = (options.delegate && typeof options.delegate.stopApp == "function")? options.delegate.stopApp: null; + this.ssdpPeer = ssdp.createPeer(); + setupServer.call(this); +} +util.inherits(DIALServer, events.EventEmitter); + +DIALServer.prototype.start = function(){ + this.ssdpPeer.start(); +}; + +DIALServer.prototype.stop = function(){ + var self = this; + var pref = self.prefix; + var serviceTypes = ["urn:dial-multiscreen-org:service:dial:1","urn:dial-multiscreen-org:device:dial:1","upnp:rootdevice","ssdp:all","uuid:"+self.uuid]; + var location = "http://"+self.host+":"+self.port+pref+"/ssdp/device-desc.xml"; + var peer = self.ssdpPeer; + for (var i = 0; i < serviceTypes.length; i++) { + var st = serviceTypes[i]; + peer.byebye(merge({ + NT: st, + USN: "uuid:" + self.uuid + "::"+st, + SERVER: SERVER, + LOCATION: location + },self.extraHeaders)); + }; + self.ssdpPeer.close(); +}; + +var DIALClient = function (options) { + var serviceTypes = ["urn:dial-multiscreen-org:service:dial:1","urn:dial-multiscreen-org:device:dial:1"]; + var self = this; + var services = {}; + this.ssdpPeer = new ssdp.createPeer(); + this.ssdpPeer.on("ready",function(){ + self.ssdpPeer.search({ST: "urn:dial-multiscreen-org:device:dial:1"}); + self.ssdpPeer.search({ST: "urn:dial-multiscreen-org:service:dial:1"}); + self.emit("ready"); + }).on("found",function(headers, address){ + var location = headers.LOCATION; + if (location && !services[location]) { + services[location] = headers; + self.emit("found",location,headers); + }; + }).on("notify",function(headers, address){ + var location = headers.LOCATION; + var nts =headers.NTS; + var nt = headers.NT; + if(serviceTypes.indexOf(nt)>=0){ + if (location && nts == "ssdp:alive" && !services[location]) { + services[location] = headers; + self.emit("found",location,headers); + } + else if(location && nts == "ssdp:byebye" && services[location]){ + delete services[location]; + self.emit("disappear",location,headers); + } + } + }).on("close",function(){ + self.emit("stop"); + }); +}; + +util.inherits(DIALClient, events.EventEmitter); + +DIALClient.prototype.start = function(){ + this.ssdpPeer.start(); +} + +DIALClient.prototype.stop = function(){ + this.ssdpPeer.close(); +} + +module.exports.Server = DIALServer; +module.exports.Client = DIALClient; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8c29d05 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "peer-dial", + "description": "Nodejs implementation of the Discovery and Launch Protocol DIAL", + "version": "0.0.1", + "author": { + "name": "Louay Bassbouss", + "email": "louay.bassbouss@fokus.fraunhofer.de" + }, + "keywords": ["ssdp", "upnp", "nsd", "discovery", "launch", "dial"], + "main": "index", + "dependencies" : { + "peer-ssdp": "0.0.2", + "ejs": "1.0.0", + "node-uuid": "1.4.1", + "express": "4.4.5" + }, + "readmeFilename": "Readme.md", + "_id": "peer-dial@0.0.1", + "_from": "peer-dial@*" +} \ No newline at end of file diff --git a/test/dial-client.js b/test/dial-client.js new file mode 100644 index 0000000..431e083 --- /dev/null +++ b/test/dial-client.js @@ -0,0 +1,28 @@ +/******************************************************************************* + * + * Copyright (c) 2013 Louay Bassbouss, Fraunhofer FOKUS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * AUTHORS: Louay Bassbouss (louay.bassbouss@fokus.fraunhofer.de) + * + ******************************************************************************/ +var dial = require("../index.js"); + +var dialClient = new dial.Client(); +dialClient.on("found",function(deviceDesc, headers){ + console.log("found",deviceDesc, headers); +}).on("disappear", function(deviceDesc, headers){ + console.log("disappear", deviceDesc); +}).start(); \ No newline at end of file diff --git a/test/dial-server.js b/test/dial-server.js new file mode 100644 index 0000000..841e204 --- /dev/null +++ b/test/dial-server.js @@ -0,0 +1,93 @@ +/******************************************************************************* + * + * Copyright (c) 2013 Louay Bassbouss, Fraunhofer FOKUS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * AUTHORS: Louay Bassbouss (louay.bassbouss@fokus.fraunhofer.de) + * + ******************************************************************************/ +var dial = require("../index.js"); +var http = require('http'); +var express = require('express'); +var app = express(); +var server = http.createServer(app); + +var HOST = "127.0.0.1";// Please replace with your host +var PORT = 3000; +var MANUFACTURER = "Fraunhofer FOKUS"; +var MODEL_NAME = "FAMIUM Display"; + +var apps = { + "famium": { + name: "famium", + state: "stopped", + allowStop: true, + pid: null + } +} +var dialServer = new dial.Server({ + expressApp: app, + host: HOST, + port: PORT, + manufacturer: MANUFACTURER, + modelName: MODEL_NAME, + extraHeaders: { + "X-FAMIUM-TOKEN": "123456" + }, + delegate: { + getApp: function(appName){ + var app = apps[appName]; + console.log("getApp result",app); + return app; + }, + launchApp: function(appName,lauchData,callback){ + console.log("launchApp request",appName, lauchData); + var app = apps[appName]; + var pid = null; + if (app && app.state == "stopped") { + app.pid = "run"; + app.state = "starting"; + app.timeout = setTimeout(function(){ + app.state = "running"; + app.timeout = null; + }, 5000); + } + console.log("launchApp result",app.pid); + callback(app.pid); + }, + stopApp: function(appName,pid,callback){ + var app = apps[appName]; + if (app && app.pid == pid) { + app.pid = null; + app.timeout && clearTimeout(app.timeout); + app.timeout = null; + app.state = "stopped"; + callback(true); + } + else { + callback(false); + } + } + } +}); + +server.listen(PORT,function(){ + dialServer.start(); + setTimeout(function(){ + dialServer.stop(); + console.log("DIAL Server stopped"); + }, 5000); + console.log("DIAL Server is running on PORT "+PORT); +}); \ No newline at end of file diff --git a/xml/app-desc.xml b/xml/app-desc.xml new file mode 100644 index 0000000..23c5d70 --- /dev/null +++ b/xml/app-desc.xml @@ -0,0 +1,9 @@ + + + {{=name}} + + {{=state}} + {{ if(typeof rel != "undefined" && typeof href != "undefined" && href){ }} + + {{ } }} + diff --git a/xml/device-desc.xml b/xml/device-desc.xml new file mode 100644 index 0000000..33f9a60 --- /dev/null +++ b/xml/device-desc.xml @@ -0,0 +1,33 @@ + + + + 1 + 0 + + {{=URLBase}} + + urn:dial-multiscreen-org:device:dial:1 + {{=friendlyName}} + {{=manufacturer}} + {{=modelName}} + uuid:{{=uuid}} + + + image/png + 144 + 144 + 32 + /img/icon.png + + + + + urn:dial-multiscreen-org:service:dial:1 + urn:dial-multiscreen-org:serviceId:dial + /ssdp/notfound + /ssdp/notfound + /ssdp/notfound + + + +