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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
267
gaia/dev_apps/upnp-test/js/app.js
Executable file
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
1049
gaia/dev_apps/upnp-test/js/lib/plug.play/plug.upnp.js
Executable file
File diff suppressed because it is too large
Load diff
521
gaia/dev_apps/upnp-test/js/lib/ssdp/ssdp.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);
|
||||
Loading…
Reference in a new issue