Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
20
gaia/dev_apps/upnp-test/README
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
UPnP Test App (Bug 1235679)
|
||||
---------------------------
|
||||
UPnP test app search for UPnP media servers in the network and browse and play media files.
|
||||
|
||||
Key features
|
||||
----------------
|
||||
a) Discover uPnP Servers
|
||||
b) Plays media files when browsed and clicked on same screen
|
||||
c) Shows response logs returned from uPnP services
|
||||
|
||||
Links to libraries used
|
||||
------------------------
|
||||
- APP leverages SSDP service API (Bug 751172) that uses UDPSocket to make UPnP requests (Bug 745283).
|
||||
https://github.com/schien/ssdp.js/blob/master/src/ssdp.js
|
||||
|
||||
- A Network Service Messaging framework for use on top of the W3C Network Service Discovery specification.
|
||||
https://github.com/richtr/plug.play.js/blob/master/lib/plug.upnp.js
|
||||
https://github.com/richtr/plug.play.js/blob/master/lib/ContentDirectory/plug.upnp_contentdirectory.js
|
||||
|
||||
Updated plug.upnp.js at Line 830 to add {mozSystem: true} to allow Cross origin XHR requests.
|
||||
40
gaia/dev_apps/upnp-test/index.html
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<title>Device Storage Test</title>
|
||||
<script type="text/javascript" src="js/lib/ssdp/ssdp.js"></script>
|
||||
<script type="text/javascript" src="js/lib/plug.play/plug.upnp.js"></script>
|
||||
<script type="text/javascript" src="js/lib/plug.play/ContentDirectory/plug.upnp_contentdirectory.js"></script>
|
||||
<script type="text/javascript" src="js/app.js"></script>
|
||||
<link href="style/app.css" rel="stylesheet">
|
||||
</head>
|
||||
<body role="application">
|
||||
<section>
|
||||
<header>UPnP Test</header>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div id="folderList"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div>
|
||||
<audio id="audioPlayer" class="player" controls="true"></audio>
|
||||
<video id="videoPlayer" class="player" controls="true"></video>
|
||||
<img id="imagePlayer" class="player"></img>
|
||||
</div>
|
||||
<div id="controlButtons">
|
||||
<button id="discoverButton">Discover</button>
|
||||
<button id="playerToggler">Hide/Show Player</button>
|
||||
<button id="clearLog">Clear Log</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div id="debug">
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
267
gaia/dev_apps/upnp-test/js/app.js
Executable file
|
|
@ -0,0 +1,267 @@
|
|||
(function(exports) {
|
||||
var DEBUG = false;
|
||||
'use strict';
|
||||
|
||||
var savedServices = {};
|
||||
|
||||
var debugEl;
|
||||
var audioPlayer;
|
||||
var videoPlayer;
|
||||
var imagePlayer;
|
||||
var discoverButton;
|
||||
var currentPlayer;
|
||||
var playerToggler;
|
||||
var folderList;
|
||||
|
||||
function debugLog(msg, level) {
|
||||
if (!DEBUG && level === 'debug') {
|
||||
return;
|
||||
}
|
||||
var logEl = document.createElement('div');
|
||||
logEl.textContent = msg;
|
||||
debugEl.appendChild(logEl);
|
||||
}
|
||||
|
||||
function toggleFolder(evt) {
|
||||
evt.preventDefault();
|
||||
var elem = evt.target;
|
||||
|
||||
if (elem.nextSibling && elem.nextSibling.tagName == 'DIV') {
|
||||
// folder is opened. Close it.
|
||||
elem.parentElement.removeChild(elem.nextSibling);
|
||||
elem.classList.remove('opened');
|
||||
} else {
|
||||
// folder is closed. Open it.
|
||||
debugLog(elem.serviceId, 'debug');
|
||||
elem.classList.add('opened');
|
||||
browseFolder(
|
||||
elem.dataset.serviceId, elem.hash.substr(1), evt.target);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlayer(value) {
|
||||
if (typeof value != 'boolean') {
|
||||
value = currentPlayer.classList.contains('hide');
|
||||
}
|
||||
audioPlayer.classList.add('hide');
|
||||
videoPlayer.classList.add('hide');
|
||||
imagePlayer.classList.add('hide');
|
||||
if (value) {
|
||||
currentPlayer.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function switchPlayer(type) {
|
||||
audioPlayer.pause();
|
||||
videoPlayer.pause();
|
||||
|
||||
if (type == 'audio') {
|
||||
currentPlayer = audioPlayer;
|
||||
} else if (type == 'video') {
|
||||
currentPlayer = videoPlayer;
|
||||
} else if (type == 'image') {
|
||||
currentPlayer = imagePlayer;
|
||||
}
|
||||
togglePlayer(true);
|
||||
}
|
||||
|
||||
function playFile(evt) {
|
||||
evt.preventDefault();
|
||||
var fileType = evt.target.dataset.mime.split('/')[0].toLowerCase();
|
||||
var mimeType = evt.target.dataset.mime.split('/')[1].toLowerCase();
|
||||
|
||||
if (evt.target.href.substr(-3) == 'ogv') {
|
||||
mimeType = 'video/ogg';
|
||||
} else if (evt.target.href.substr(-3) == 'ogg') {
|
||||
mimeType = 'audio/ogg';
|
||||
}
|
||||
switchPlayer(fileType);
|
||||
|
||||
currentPlayer.hidden = false;
|
||||
currentPlayer.src = evt.target.href;
|
||||
currentPlayer.setAttribute('type', mimeType);
|
||||
currentPlayer.play();
|
||||
}
|
||||
|
||||
function browseFolder(serviceId, folderId, folderElement) {
|
||||
var mediaServer = savedServices[serviceId];
|
||||
if (!mediaServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
folderId = folderId || '';
|
||||
|
||||
mediaServer.browse(folderId).then(function(response) {
|
||||
if (!response || !response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = response.data.Result.replace(/&/g, "&");
|
||||
debugLog(data);
|
||||
|
||||
if (data.indexOf('xmlns:dlna') == -1) {
|
||||
data = data.replace('<DIDL-Lite ',
|
||||
'<DIDL-Lite xmlns:dlna="urn:schemas-dlna-org:device-1-0" ');
|
||||
}
|
||||
var parser = new DOMParser();
|
||||
var xmlResponse = parser.parseFromString(data, 'application/xml');
|
||||
var lists = xmlResponse.documentElement.children;
|
||||
|
||||
var sublist = document.createElement('div');
|
||||
sublist.className = 'sublist';
|
||||
|
||||
for (var i = 0; i < lists.length; i++) {
|
||||
var item = lists[i];
|
||||
|
||||
var titleElem = item.getElementsByTagName('title')[0] ||
|
||||
item.getElementsByTagName('dc:title')[0];
|
||||
var title;
|
||||
if (titleElem) {
|
||||
title = titleElem.textContent;
|
||||
}
|
||||
|
||||
var newElem;
|
||||
if (item.tagName == 'container') {
|
||||
newElem = document.createElement('a');
|
||||
|
||||
newElem.dataset.serviceId = serviceId;
|
||||
newElem.addEventListener('click', toggleFolder);
|
||||
newElem.href = '#' + item.getAttribute('id');
|
||||
newElem.textContent = title;
|
||||
newElem.className = 'folder';
|
||||
|
||||
sublist.appendChild(newElem);
|
||||
} else if (item.tagName == 'item') {
|
||||
var linkElem = item.getElementsByTagName('res')[0];
|
||||
var link, mime;
|
||||
if (linkElem) {
|
||||
link = linkElem.textContent;
|
||||
mime = linkElem.getAttribute('protocolInfo').split(':')[2];
|
||||
}
|
||||
newElem = document.createElement('a');
|
||||
newElem.addEventListener('click', playFile);
|
||||
newElem.dataset.mime = mime;
|
||||
newElem.href = link;
|
||||
newElem.textContent = title;
|
||||
newElem.className = mime.split('/')[0];
|
||||
sublist.appendChild(newElem);
|
||||
}
|
||||
}
|
||||
if (!folderElement) {
|
||||
folderList.appendChild(sublist);
|
||||
} else {
|
||||
folderElement.parentElement.
|
||||
insertBefore(sublist, folderElement.nextSibling);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processService(service, i) {
|
||||
service._index = i;
|
||||
if (savedServices[service.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaServer =
|
||||
new Plug.UPnP_ContentDirectory(service, { debug: false });
|
||||
if (!savedServices[service.id]) {
|
||||
savedServices[service.id] = mediaServer;
|
||||
}
|
||||
|
||||
mediaServer.getSystemUpdateId().then(function(response) {
|
||||
if (response && response.data) {
|
||||
debugLog('Service[' + service._index + '] is reporting UpdateId = ' +
|
||||
response.data.Id );
|
||||
} else {
|
||||
debugLog('Service[' + service._index + '] is reporting no response');
|
||||
}
|
||||
|
||||
}).then(null, function(error) { // Handle any errors
|
||||
|
||||
debugLog('An error occurred: ' + error.description);
|
||||
|
||||
});
|
||||
|
||||
var serverElem = document.createElement('div');
|
||||
serverElem.className = 'server';
|
||||
|
||||
var parser = new DOMParser();
|
||||
var configDocument = parser.parseFromString(mediaServer.svc.config, 'text/xml');
|
||||
serverElem.textContent = configDocument.getElementsByTagName('friendlyName')[0].textContent;
|
||||
folderList.appendChild(serverElem);
|
||||
|
||||
browseFolder(service.id, null, serverElem);
|
||||
}
|
||||
|
||||
function onServices(services) {
|
||||
|
||||
var idx = services.length;
|
||||
services.addEventListener('servicefound', function _servicefound(e) {
|
||||
debugLog(services.length + ' UPnP service' +
|
||||
(services.length !== 1 ? 's' : '') +
|
||||
' found in the network');
|
||||
|
||||
processService(services[idx], idx);
|
||||
idx++;
|
||||
});
|
||||
|
||||
debugLog(services.length + ' UPnP service' +
|
||||
(services.length !== 1 ? 's' : '') +
|
||||
' found in the network');
|
||||
|
||||
if (services.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
var service = services[i];
|
||||
processService(service, i);
|
||||
}
|
||||
}
|
||||
|
||||
function discover() {
|
||||
if (navigator.getNetworkServices) {
|
||||
debugLog('Search for UPnP services in the network');
|
||||
navigator.getNetworkServices('upnp:urn:schemas-upnp-org:service:ContentDirectory:1')
|
||||
.then(onServices, function(e) {
|
||||
debugLog('An error occurred obtaining UPnP Services [CODE: ' +
|
||||
error.code + ']');
|
||||
});
|
||||
} else {
|
||||
debugLog(
|
||||
'navigator.getNetworkServices API is not supported in this browser');
|
||||
}
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
debugEl.innerHTML = '';
|
||||
}
|
||||
|
||||
function init() {
|
||||
folderList = document.getElementById('folderList');
|
||||
debugEl = document.getElementById('debug');
|
||||
audioPlayer = document.getElementById('audioPlayer');
|
||||
videoPlayer = document.getElementById('videoPlayer');
|
||||
imagePlayer = document.getElementById('imagePlayer');
|
||||
playerToggler = document.getElementById('playerToggler');
|
||||
discoverButton = document.getElementById('discoverButton');
|
||||
clearLogButton = document.getElementById('clearLog');
|
||||
|
||||
currentPlayer = imagePlayer;
|
||||
togglePlayer(false);
|
||||
|
||||
playerToggler.addEventListener('click', togglePlayer);
|
||||
discoverButton.addEventListener('click', discover);
|
||||
clearLogButton.addEventListener('click', clearLog);
|
||||
|
||||
discover();
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
init();
|
||||
});
|
||||
|
||||
exports.ServiceManager = {
|
||||
savedServices: savedServices
|
||||
};
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/* Copyright (c) 2012 Opera Software ASA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A UPnP Media Server API for the following service types:
|
||||
*
|
||||
* - urn:schemas-upnp-org:service:ContentDirectory:1
|
||||
* - urn:schemas-upnp-org:service:ContentDirectory:2
|
||||
* - urn:schemas-upnp-org:service:ContentDirectory:3
|
||||
* - urn:schemas-upnp-org:service:ContentDirectory:4
|
||||
*
|
||||
*/
|
||||
!(function( global, undefined ) {
|
||||
|
||||
if( !global.Plug || !global.Plug.UPnP ) {
|
||||
console.error("plug.play.js must be loaded before you can use this library");
|
||||
return;
|
||||
}
|
||||
|
||||
var allowedUPnPTypes = {
|
||||
'urn:schemas-upnp-org:service:ContentDirectory:1': true,
|
||||
'urn:schemas-upnp-org:service:ContentDirectory:2': true,
|
||||
'urn:schemas-upnp-org:service:ContentDirectory:3': true,
|
||||
'urn:schemas-upnp-org:service:ContentDirectory:4': true
|
||||
};
|
||||
|
||||
var UPnPContentDirectory = function( serviceObj, opts ) {
|
||||
|
||||
Plug.UPnP.apply( this, [ serviceObj, opts ] );
|
||||
|
||||
this.upnpType = serviceObj.type;
|
||||
if( serviceObj.type.indexOf('upnp:') === 0 ) {
|
||||
this.upnpType = this.upnpType.replace('upnp:', '');
|
||||
}
|
||||
|
||||
if( !allowedUPnPTypes[ this.upnpType ] ) {
|
||||
console.error("Provided service is not a UPnP Media Server Service");
|
||||
return;
|
||||
}
|
||||
|
||||
// *** ContentDirectory:1 methods
|
||||
|
||||
this.getSystemUpdateId = function( callback ) {
|
||||
|
||||
return this.action('GetSystemUpdateID', {
|
||||
request: {},
|
||||
response: {
|
||||
Id: {
|
||||
type: this.types.ui4
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
|
||||
};
|
||||
|
||||
this.getSearchCapabilities = function( callback ) {
|
||||
|
||||
return this.action('GetSearchCapabilities', {
|
||||
request: {},
|
||||
response: {
|
||||
SearchCaps: {
|
||||
type: this.types.string
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
|
||||
};
|
||||
|
||||
this.getSortCapabilities = function( callback ) {
|
||||
|
||||
return this.action('GetSortCapabilities', {
|
||||
request: {},
|
||||
response: {
|
||||
SortCaps: {
|
||||
type: this.types.string
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
|
||||
};
|
||||
|
||||
this.browse = function( objectId, browseFlag, filter, startingIndex, requestedCount, sortCriteria, callback ) {
|
||||
|
||||
return this.action('Browse', {
|
||||
request: {
|
||||
ObjectID: {
|
||||
value: objectId || '0',
|
||||
type: this.types.string
|
||||
},
|
||||
BrowseFlag: {
|
||||
value: browseFlag || 'BrowseDirectChildren',
|
||||
type: this.types.string
|
||||
},
|
||||
Filter: {
|
||||
value: filter || '*',
|
||||
type: this.types.string
|
||||
},
|
||||
StartingIndex: {
|
||||
value: startingIndex || '0',
|
||||
type: this.types.ui4
|
||||
},
|
||||
RequestedCount: {
|
||||
value: requestedCount || '100000',
|
||||
type: this.types.ui4
|
||||
},
|
||||
SortCriteria: {
|
||||
value: sortCriteria,
|
||||
type: this.types.string
|
||||
}
|
||||
},
|
||||
response: {
|
||||
Result: {
|
||||
type: this.types.string
|
||||
},
|
||||
NumberReturned: {
|
||||
type: this.types.ui4
|
||||
},
|
||||
TotalMatches: {
|
||||
type: this.types.ui4
|
||||
},
|
||||
UpdateID: {
|
||||
type: this.types.ui4
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
|
||||
};
|
||||
|
||||
this.search = function( objectId, browseFlag, filter, startingIndex, requestedCount, sortCriteria, callback ) {
|
||||
|
||||
return this.action('Search', {
|
||||
request: {
|
||||
ObjectID: {
|
||||
value: objectId || '0',
|
||||
type: this.types.string
|
||||
},
|
||||
BrowseFlag: {
|
||||
value: browseFlag || 'BrowseDirectChildren',
|
||||
type: this.types.string
|
||||
},
|
||||
Filter: {
|
||||
value: filter || '*',
|
||||
type: this.types.string
|
||||
},
|
||||
StartingIndex: {
|
||||
value: startingIndex || '0',
|
||||
type: this.types.ui4
|
||||
},
|
||||
RequestedCount: {
|
||||
value: requestedCount || '100000',
|
||||
type: this.types.ui4
|
||||
},
|
||||
SortCriteria: {
|
||||
value: sortCriteria,
|
||||
type: this.types.string
|
||||
}
|
||||
},
|
||||
response: {
|
||||
Result: {
|
||||
type: this.types.string
|
||||
},
|
||||
NumberReturned: {
|
||||
type: this.types.ui4
|
||||
},
|
||||
TotalMatches: {
|
||||
type: this.types.ui4
|
||||
},
|
||||
UpdateID: {
|
||||
type: this.types.ui4
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
|
||||
};
|
||||
|
||||
|
||||
// *** ContentDirectory:2 methods
|
||||
|
||||
if(this.upnpType === 'urn:schemas-upnp-org:service:ContentDirectory:2') {
|
||||
|
||||
// TODO: GetFeatureList, GetSortExtensionCapabilities, MoveObject
|
||||
|
||||
}
|
||||
|
||||
|
||||
// *** ContentDirectory:3 methods
|
||||
|
||||
if(this.upnpType === 'urn:schemas-upnp-org:service:ContentDirectory:3') {
|
||||
|
||||
// TODO
|
||||
|
||||
}
|
||||
|
||||
// *** ContentDirectory:4 methods
|
||||
|
||||
if(this.upnpType === 'urn:schemas-upnp-org:service:ContentDirectory:4') {
|
||||
|
||||
// TODO
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
UPnPContentDirectory.prototype = Object.create( Plug.UPnP.prototype );
|
||||
|
||||
// *** GLOBAL ASSIGNMENT ***
|
||||
|
||||
global.Plug.UPnP_ContentDirectory = UPnPContentDirectory;
|
||||
|
||||
})( this );
|
||||
1049
gaia/dev_apps/upnp-test/js/lib/plug.play/plug.upnp.js
Executable file
521
gaia/dev_apps/upnp-test/js/lib/ssdp/ssdp.js
Executable file
|
|
@ -0,0 +1,521 @@
|
|||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* jshint ignore:start */
|
||||
(function(global) {
|
||||
// Spec information:
|
||||
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
||||
|
||||
const SSDP_PORT = 1900;
|
||||
const SSDP_ADDRESS = "239.255.255.250";
|
||||
const SSDP_DISCOVER_MX = 2;
|
||||
|
||||
const SSDP_DISCOVER_PACKET =
|
||||
"M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: " + SSDP_ADDRESS + ":" + SSDP_PORT + "\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: " + SSDP_DISCOVER_MX + "\r\n" +
|
||||
"ST: %SEARCH_TARGET%\r\n\r\n";
|
||||
|
||||
const SSDP_RESPONSE_HEADER = /HTTP\/\d{1}\.\d{1} \d+ .*/;
|
||||
const SSDP_HEADER = /^([^:]+):\s*(.*)$/;
|
||||
|
||||
var SimpleServiceDiscovery = {
|
||||
_targets: [],
|
||||
_commands: {},
|
||||
_searching: false,
|
||||
search: function _search(aInterval) {
|
||||
aInterval = aInterval || 0;
|
||||
if (aInterval > 0) {
|
||||
this._searchRepeat = setInterval(this._search.bind(this), aInterval);
|
||||
}
|
||||
this._search();
|
||||
},
|
||||
stopSearch: function _stopSearch() {
|
||||
if (this._searchRepeat) {
|
||||
clearInterval(this._searchRepeat);
|
||||
}
|
||||
},
|
||||
registerTarget: function _registerTarget(target) {
|
||||
if (this._targets.indexOf(target) < 0) {
|
||||
this._targets.push(target);
|
||||
}
|
||||
},
|
||||
|
||||
// internal function
|
||||
_search: function _search() {
|
||||
// create socket if not exist
|
||||
if (!this._searchSocket) {
|
||||
this._searchSocket = new UDPSocket({loopback: true, localPort: SSDP_PORT});
|
||||
this._searchSocket.joinMulticastGroup(SSDP_ADDRESS);
|
||||
this._searchSocket.onmessage = this._onmessage.bind(this);
|
||||
window.addEventListener('beforeunload', function(evt) {
|
||||
this._searchSocket.close();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this._searchSocket.opened.then((function() {
|
||||
// Perform a UDP broadcast to search for SSDP devices
|
||||
this._searchTimeout = setTimeout(this._searchShutdown.bind(this), SSDP_DISCOVER_MX * 1000);
|
||||
this._searching = true;
|
||||
|
||||
var data = SSDP_DISCOVER_PACKET;
|
||||
this._targets.forEach((function(target) {
|
||||
var msgData = data.replace("%SEARCH_TARGET%", target);
|
||||
var ok = this._searchSocket.send(msgData, SSDP_ADDRESS, SSDP_PORT);
|
||||
}).bind(this));
|
||||
}).bind(this)).catch(function(evt) {
|
||||
});
|
||||
},
|
||||
_searchShutdown: function _searchShutdown() {
|
||||
this._searching = false;
|
||||
},
|
||||
_onmessage: function _onmessage(e) {
|
||||
// Listen for responses from specific targets. There could be more than
|
||||
// one available.
|
||||
|
||||
var msg = String.fromCharCode.apply(null, new Uint8Array(e.data));
|
||||
var lines = msg.toString().split("\r\n");
|
||||
var firstLine = lines.shift();
|
||||
var method = SSDP_RESPONSE_HEADER.test(firstLine) ? 'RESPONSE'
|
||||
: firstLine.split(' ')[0].toUpperCase();
|
||||
var headers = {};
|
||||
lines.forEach(function (line) {
|
||||
if (line.length) {
|
||||
var pairs = line.match(/^([^:]+):\s*(.*)$/);
|
||||
if (pairs) {
|
||||
headers[pairs[1].toLowerCase()] = pairs[2];
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this._commands[method]) {
|
||||
this._commands[method].apply(this, [headers]);
|
||||
}
|
||||
},
|
||||
_found: function _found(aService) {
|
||||
// Use the REST api to request more information about this service
|
||||
var xhr = new XMLHttpRequest({mozSystem: true});
|
||||
xhr.open("GET", aService.location, true);
|
||||
xhr.overrideMimeType("text/xml");
|
||||
|
||||
xhr.addEventListener("load", (function() {
|
||||
if (xhr.status == 200) {
|
||||
// walk through root device and all the embedded devices
|
||||
var devices = xhr.responseXML.querySelectorAll('device');
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
this._parseDescriptor(devices[i], aService.location);
|
||||
}
|
||||
}
|
||||
}).bind(this), false);
|
||||
|
||||
xhr.send(null);
|
||||
},
|
||||
_parseDescriptor: function _parseDescriptor(device, refUrl) {
|
||||
var udn = device.querySelector("UDN").innerHTML;
|
||||
|
||||
var serviceList = device.querySelector('serviceList').querySelectorAll('service');
|
||||
for (var i = 0; i < serviceList.length; i++) {
|
||||
var service = serviceList[i];
|
||||
var serviceType = service.querySelector('serviceType').innerHTML;
|
||||
var eventsUrl = this._getAbsoluteURL(service.querySelector('eventSubURL'), refUrl);
|
||||
var options = {};
|
||||
options.id = udn + '::' + serviceType;
|
||||
options.deviceId = udn;
|
||||
options.name = service.querySelector('serviceId').innerHTML;
|
||||
options.type = 'upnp:' + serviceType;
|
||||
options.url = this._getAbsoluteURL(service.querySelector('controlURL').innerHTML, refUrl);
|
||||
options.config = device.outerHTML;
|
||||
if (eventsUrl) {
|
||||
options.eventsUrl = eventsUrl.innerHTML;
|
||||
}
|
||||
options.expiryTimestamp = ''; //TODO
|
||||
serviceHelper.add(new SSDPServiceRecord(options));
|
||||
}
|
||||
},
|
||||
_getAbsoluteURL: function _getAbsoluteURL(url, refUrl) {
|
||||
if (/^https?:\/\//.test(url)) {
|
||||
return url;
|
||||
} else {
|
||||
var absURL = new URL(url, refUrl);
|
||||
return absURL.toString();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SimpleServiceDiscovery._commands['RESPONSE'] = function _response(headers) {
|
||||
if (this._searching && headers.location && this._targets.indexOf(headers.st) >= 0) {
|
||||
this._found(headers);
|
||||
}
|
||||
};
|
||||
SimpleServiceDiscovery._commands['NOTIFY'] = function _notify(headers) {
|
||||
switch(headers.nts) {
|
||||
case 'ssdp:alive':
|
||||
var old_searching = this._searching;
|
||||
this._searching = true;
|
||||
headers['st'] = headers.nt;
|
||||
this._commands['RESPONSE'].apply(this, [headers]);
|
||||
this._searching = old_searching;
|
||||
break;
|
||||
case 'ssdp:byebye':
|
||||
serviceHelper.remove(new SSDPServiceRecord({id: headers.usn}));
|
||||
break;
|
||||
}
|
||||
};
|
||||
SimpleServiceDiscovery._commands['M-SEARCH'] = function _msearch(headers) {
|
||||
};
|
||||
|
||||
function SSDPServiceRecord(options) {
|
||||
this.update(options);
|
||||
}
|
||||
|
||||
SSDPServiceRecord.prototype = {
|
||||
id: '',
|
||||
deviceId: '',
|
||||
name: '',
|
||||
type: '',
|
||||
url: '',
|
||||
eventsUrl: '',
|
||||
config: '',
|
||||
expiryTimestamp: '',
|
||||
update: function(options) {
|
||||
var self = this;
|
||||
['id', 'deviceId', 'name', 'type', 'url', 'config', 'expiryTimestamp'].forEach(function(option) {
|
||||
self[option] = options[option];
|
||||
});
|
||||
|
||||
if (options.eventsUrl) {
|
||||
this.eventsUrl = options.eventsUrl;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// polyfill Network Service Discovery API
|
||||
function NetworkService(record) {
|
||||
this._record = record;
|
||||
//XXX need weak reference
|
||||
this._callbacks = { 'available': [], 'unavailable': [], 'notify': [] };
|
||||
}
|
||||
|
||||
NetworkService.prototype = {
|
||||
online: true,
|
||||
get id() { return this._record.id; },
|
||||
get name() { return this._record.name; },
|
||||
get type() { return this._record.type; },
|
||||
get url() { return this._record.url; },
|
||||
get config() { return this._record.config; },
|
||||
|
||||
set onavailable(callback) {
|
||||
this.addEventListener('available', callback);
|
||||
},
|
||||
|
||||
set onunavailable(callback) {
|
||||
this.addEventListener('unavailable', callback);
|
||||
},
|
||||
|
||||
set onnotify(callback) {
|
||||
this.addEventListener('notify', callback);
|
||||
},
|
||||
|
||||
// EventTarget
|
||||
addEventListener: function(type, callback, capture) {
|
||||
if (!this._callbacks.hasOwnProperty(type)) {
|
||||
return;
|
||||
}
|
||||
this._callbacks[type].push(callback);
|
||||
},
|
||||
removeEventListener: function(type, callback, capture) {
|
||||
if (!this._callbacks.hasOwnProperty(type)) {
|
||||
return;
|
||||
}
|
||||
var callbacks = this._callbacks[type];
|
||||
|
||||
var index = callbacks.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
},
|
||||
dispatchEvent: function(event) {
|
||||
if (this._callbacks.hasOwnProperty(event.type)) {
|
||||
var callbacks = this._callbacks[event.type];
|
||||
if (!event.detail || !event.detail.target) {
|
||||
event.detail = { target: this };
|
||||
}
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i](event);
|
||||
}
|
||||
}
|
||||
return event.cancelable === false || event.defaultPrevented === false;
|
||||
},
|
||||
};
|
||||
|
||||
function NetworkServices(types, services) {
|
||||
this._services = services || [];
|
||||
this._types = types || [];
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
this[i] = services[i];
|
||||
}
|
||||
//XXX need weak reference
|
||||
this._callbacks = { 'servicefound': [], 'servicelost': [] };
|
||||
}
|
||||
|
||||
NetworkServices.prototype = {
|
||||
_append: function(service) {
|
||||
var i = this._services.length;
|
||||
this[i] = service;
|
||||
this._services.push(service);
|
||||
},
|
||||
|
||||
servicesAvailable: 0,
|
||||
|
||||
get length() {
|
||||
return this._services.length;
|
||||
},
|
||||
|
||||
getServiceById: function(id) {
|
||||
for (var i = 0; i < this._services.length; i++) {
|
||||
if (this._services[i].id === id) {
|
||||
return this._services[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
set onservicefound(callback) {
|
||||
this.addEventListener('servicefound', callback);
|
||||
},
|
||||
|
||||
set onservicelost(callback) {
|
||||
this.addEventListener('servicelost', callback);
|
||||
},
|
||||
|
||||
// EventTarget
|
||||
addEventListener: function(type, callback) {
|
||||
if (!this._callbacks.hasOwnProperty(type)) {
|
||||
return;
|
||||
}
|
||||
this._callbacks[type].push(callback);
|
||||
},
|
||||
removeEventListener: function(type, callback, capture) {
|
||||
if (!this._callbacks.hasOwnProperty(type)) {
|
||||
return;
|
||||
}
|
||||
var callbacks = this._callbacks[type];
|
||||
|
||||
var index = callbacks.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
},
|
||||
dispatchEvent: function(event) {
|
||||
if (this._callbacks.hasOwnProperty(event.type)) {
|
||||
var callbacks = this._callbacks[event.type];
|
||||
if (!event.detail || !event.detail.target) {
|
||||
event.detail = {target: this};
|
||||
}
|
||||
for (var i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i](event);
|
||||
}
|
||||
}
|
||||
return event.cancelable === false || event.defaultPrevented === false;
|
||||
},
|
||||
};
|
||||
|
||||
function _hasRequestedType(service, types) {
|
||||
return types.some(function(type) {
|
||||
return type === service.type;
|
||||
});
|
||||
}
|
||||
|
||||
var serviceHelper = (function() {
|
||||
return {
|
||||
add: function(record) {
|
||||
// try update existing service
|
||||
var index = availableServiceRecords.findIndex(function(activeService) {
|
||||
return activeService.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
availableServiceRecords[index].update(record);
|
||||
return;
|
||||
}
|
||||
|
||||
// new service available
|
||||
availableServiceRecords.push(record);
|
||||
|
||||
activeServiceManagers.forEach(function(manager) {
|
||||
// wiring to existed NetworkService objects
|
||||
var index = manager._services.findIndex(function(service) {
|
||||
return service.id === record.id;
|
||||
});
|
||||
if (index >= 0 && manager._services[index].online === false) {
|
||||
var service = manager._services[index];
|
||||
service._record = record;
|
||||
service.online = true;
|
||||
setTimeout(function() {
|
||||
service.dispatchEvent(new CustomEvent('available',
|
||||
{ bubbles: false,
|
||||
cancelable: false,
|
||||
detail: {target: service},
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// notify correspoding service managers
|
||||
if (_hasRequestedType(record, manager._types)) {
|
||||
// XXX should we append new service to existing manager?
|
||||
if (index < 0) {
|
||||
var netService = new NetworkService(record);
|
||||
manager._append(netService);
|
||||
}
|
||||
manager.servicesAvailable++;
|
||||
setTimeout(function() {
|
||||
manager.dispatchEvent(new CustomEvent('servicefound',
|
||||
{ bubbles: false,
|
||||
cancelable: false,
|
||||
detail: {target: manager},
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
remove: function(record) {
|
||||
var index = availableServiceRecords.findIndex(function(activeService) {
|
||||
return activeService.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
var activeService = availableServiceRecords[index];
|
||||
|
||||
activeServiceManagers.forEach(function(manager) {
|
||||
var index = manager._services.findIndex(function(service) {
|
||||
return service.id === record.id;
|
||||
});
|
||||
if (index >= 0 && manager._services[index].online === true) {
|
||||
// notify service unavailable
|
||||
var service = manager._services[index];
|
||||
service.online = false;
|
||||
setTimeout(function() {
|
||||
service.dispatchEvent(new CustomEvent('unavailable',
|
||||
{ bubbles: false,
|
||||
cancelable: false,
|
||||
detail: {target: service},
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// notify service lost
|
||||
if (_hasRequestedType(activeService, manager._types)) {
|
||||
manager.servicesAvailable--;
|
||||
setTimeout(function() {
|
||||
manager.dispatchEvent(new CustomEvent('servicelost',
|
||||
{ bubbles: false,
|
||||
cancelable: false,
|
||||
detail: {target: manager},
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// XXX unregister UPnP event
|
||||
if (activeService.eventsUrl) {
|
||||
console.log('unregister event for ' + activeService.eventsUrl);
|
||||
}
|
||||
|
||||
// remove from available service list
|
||||
availableServiceRecords.splice(index, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
var availableServiceRecords = []; // list of NetworkService
|
||||
var activeServiceManagers = []; // list of NetworkServices //XXX need weak reference
|
||||
var targetRegister = {
|
||||
'upnp:': {
|
||||
register: function(target) {
|
||||
SimpleServiceDiscovery.registerTarget(target.substr(5)); // remove upnp: prefix
|
||||
},
|
||||
search: function() {
|
||||
SimpleServiceDiscovery.search();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getNetworkServices(type) {
|
||||
const UNKNOWN_TYPE_PREFIX_ERR = 'UnknownTypePrefixError';
|
||||
const PERMISSION_DENIED_ERR = 'PermissionDeniedError';
|
||||
|
||||
function _isValidServiceType (type) {
|
||||
return type && Object.keys(targetRegister).some(function(prefix) { return type.startsWith(prefix); });
|
||||
}
|
||||
|
||||
function _register(types) {
|
||||
var needSearch = {};
|
||||
types.forEach(function(type) {
|
||||
var found = Object.keys(targetRegister).find(function(prefix) { return type.startsWith(prefix); });
|
||||
if (found) {
|
||||
targetRegister[found].register(type);
|
||||
if (!needSearch.hasOwnProperty(found)) {
|
||||
needSearch[found] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
// trigger search on the discovery service if new search target
|
||||
// is registered
|
||||
Object.keys(needSearch).forEach(function(type) {
|
||||
targetRegister[type].search();
|
||||
});
|
||||
}
|
||||
|
||||
var requestedControlTypes = [].concat(type).filter(_isValidServiceType);
|
||||
|
||||
if (requestedControlTypes.length === 0) {
|
||||
return Promise.reject(new DOMError(UNKNOWN_TYPE_PREFIX_ERR));
|
||||
}
|
||||
|
||||
_register(requestedControlTypes);
|
||||
|
||||
var allPromises = [];
|
||||
availableServiceRecords.forEach(function(service) {
|
||||
if (_hasRequestedType(service, requestedControlTypes)) {
|
||||
// XXX doesn't check CORS yet
|
||||
// create a new instance of NetworkService
|
||||
allPromises.push(Promise.resolve(new NetworkService(service)));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(allPromises).then(function(servicesFound) {
|
||||
var services = servicesFound.filter(function(service) {
|
||||
//XXX might also filter by user preference
|
||||
return service !== null;
|
||||
});
|
||||
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
if (services[i]._record.eventsUrl) {
|
||||
//XXX perform UPnP event subscription
|
||||
console.log('register for ' + services[i]._record.eventsUrl);
|
||||
}
|
||||
}
|
||||
|
||||
var serviceManager = new NetworkServices(requestedControlTypes, services);
|
||||
serviceManager.servicesAvailable = availableServiceRecords.reduce(function(count, service) {
|
||||
return (_hasRequestedType(service, requestedControlTypes)) ? count + 1 : count;
|
||||
}, 0);
|
||||
activeServiceManagers.push(serviceManager);
|
||||
|
||||
return Promise.resolve(serviceManager);
|
||||
});
|
||||
}
|
||||
|
||||
// export NetworkServices and NetworkServices as pre-defined types
|
||||
global.NetworkService = NetworkService;
|
||||
global.NetworkServices = NetworkServices;
|
||||
navigator.getNetworkServices = getNetworkServices;
|
||||
|
||||
global.addEventListener('unload', function() {
|
||||
delete navigator.getNetworkServices;
|
||||
});
|
||||
})(window);
|
||||
14
gaia/dev_apps/upnp-test/manifest.webapp
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "uPnP Test",
|
||||
"description": "uPnP Test App",
|
||||
"type": "certified",
|
||||
"launch_path": "/index.html",
|
||||
"permissions": {
|
||||
"systemXHR": {},
|
||||
"udp-socket": {}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"icons": {
|
||||
"126": "/style/icons/default_126.png"
|
||||
}
|
||||
}
|
||||
80
gaia/dev_apps/upnp-test/style/app.css
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
header {
|
||||
font: 25px bold;
|
||||
}
|
||||
button {
|
||||
font-size: 15px;
|
||||
}
|
||||
section {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#folderList {
|
||||
width: 300px;
|
||||
height: 280px;
|
||||
padding: 0 20px;
|
||||
overflow: auto;
|
||||
border: 1px solid;
|
||||
}
|
||||
#debug {
|
||||
padding: 0.5em;
|
||||
border: thin inset;
|
||||
font: small monospace;
|
||||
color: black;
|
||||
}
|
||||
.audio, .video, .image, .folder, #folderlist a {
|
||||
display: block;
|
||||
padding-left: 25px;
|
||||
margin: 25px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
a.folder {
|
||||
color: #FF9900;
|
||||
padding-left: 40px;
|
||||
background: url(image/folder_closed.png) left center no-repeat,
|
||||
url(image/icon_folder.png) left 20px center no-repeat;
|
||||
}
|
||||
a.folder.opened {
|
||||
background: url(image/folder_opened.png) left center no-repeat,
|
||||
url(image/icon_folder.png) left 20px center no-repeat;
|
||||
}
|
||||
.audio {
|
||||
color: #00CC66;
|
||||
background: url(image/icon_music.png) left center no-repeat;
|
||||
}
|
||||
.video {
|
||||
color: #00CC66;
|
||||
background: url(image/icon_video.png) left center no-repeat;
|
||||
}
|
||||
.image {
|
||||
color: #00CC66;
|
||||
background: url(image/icon_image.png) left center no-repeat;
|
||||
}
|
||||
.server {
|
||||
background: url(image/icon_server.png) left center no-repeat;
|
||||
font: 15px bold;
|
||||
padding: 0 25px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.sublist {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.player {
|
||||
height: 150px;
|
||||
}
|
||||
a:hover {
|
||||
color: #003399;
|
||||
}
|
||||
a:visited {
|
||||
color: #999999;
|
||||
}
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
BIN
gaia/dev_apps/upnp-test/style/icons/default_126.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/dev_apps/upnp-test/style/image/folder_closed.png
Executable file
|
After Width: | Height: | Size: 185 B |
BIN
gaia/dev_apps/upnp-test/style/image/folder_opened.png
Executable file
|
After Width: | Height: | Size: 181 B |
BIN
gaia/dev_apps/upnp-test/style/image/icon_folder.png
Executable file
|
After Width: | Height: | Size: 244 B |
BIN
gaia/dev_apps/upnp-test/style/image/icon_image.png
Executable file
|
After Width: | Height: | Size: 419 B |
BIN
gaia/dev_apps/upnp-test/style/image/icon_music.png
Executable file
|
After Width: | Height: | Size: 190 B |
BIN
gaia/dev_apps/upnp-test/style/image/icon_server.png
Executable file
|
After Width: | Height: | Size: 207 B |
BIN
gaia/dev_apps/upnp-test/style/image/icon_unknown.png
Executable file
|
After Width: | Height: | Size: 400 B |
BIN
gaia/dev_apps/upnp-test/style/image/icon_video.png
Executable file
|
After Width: | Height: | Size: 182 B |