upnp-client-ts
Version:
A modern UPNP client made in Typescript.
1,044 lines (1,000 loc) • 46.8 kB
JavaScript
import $gLCr3$http from "http";
import {EventEmitter as $gLCr3$EventEmitter} from "events";
import $gLCr3$elementtree from "elementtree";
import $gLCr3$os from "os";
import {urlToHttpOptions as $gLCr3$urlToHttpOptions} from "url";
function $parcel$interopDefault(a) {
return a && a.__esModule ? a.default : a;
}
const $507f4ad1d2498db3$var$pickInterface = (interfaces, family)=>{
for(const i in interfaces)for(let j = interfaces[i].length - 1; j >= 0; j--){
const face = interfaces[i][j];
const reachable = family === "IPv4" || face.scopeid === 0;
if (!face.internal && face.family === family && reachable) return face.address;
}
return family === "IPv4" ? "127.0.0.1" : "::1";
};
const $507f4ad1d2498db3$var$reduceInterfaces = (interfaces, iface)=>{
const ifaces = {};
for(const i in interfaces)if (i === iface) ifaces[i] = interfaces[i];
return ifaces;
};
const $507f4ad1d2498db3$export$291a17a16047f284 = (iface)=>{
let interfaces = (0, $gLCr3$os).networkInterfaces();
if (iface) interfaces = $507f4ad1d2498db3$var$reduceInterfaces(interfaces, iface);
return $507f4ad1d2498db3$var$pickInterface(interfaces, "IPv4");
};
const $507f4ad1d2498db3$export$4e0c86a59c658526 = (iface)=>{
let interfaces = (0, $gLCr3$os).networkInterfaces();
if (iface) interfaces = $507f4ad1d2498db3$var$reduceInterfaces(interfaces, iface);
return $507f4ad1d2498db3$var$pickInterface(interfaces, "IPv6");
};
var $337cb25062390681$exports = {};
$337cb25062390681$exports = JSON.parse('{"name":"upnp-client-ts","version":"1.1.1","description":"A modern UPNP client made in Typescript.","source":"src/index.ts","main":"dist/main.js","module":"dist/module.js","types":"dist/types.d.ts","scripts":{"watch":"parcel watch","build":"parcel build","lint":"tsc --noEmit && eslint \\"src/**/*.ts{,x}\\""},"targets":{"module":{"isLibrary":true,"context":"node"},"main":{"isLibrary":true,"context":"node"}},"keywords":["upnp","client","mediarenderer","typescript","module"],"author":"Corentin Mors <corentin@pixelswap.fr>","license":"MIT","devDependencies":{"@parcel/packager-ts":"^2.10.3","@parcel/transformer-typescript-tsc":"^2.10.3","@parcel/transformer-typescript-types":"^2.10.3","@types/elementtree":"^0.1.5","@types/node":"^18.11.18","@typescript-eslint/eslint-plugin":"^6.16.0","@typescript-eslint/parser":"^6.16.0","eslint":"^8.56.0","eslint-config-prettier":"^9.1.0","eslint-plugin-prettier":"^5.1.2","parcel":"^2.10.3","prettier":"^3.1.1","typescript":"^5.3.3"},"dependencies":{"elementtree":"^0.1.7"},"@parcel/transformer-js":{"inlineFS":false},"files":["dist"],"repository":{"type":"git","url":"https://github.com/Mikescops/upnp-client-ts.git"},"publishConfig":{"registry":"https://registry.npmjs.org"},"engines":{"node":">=18.0.0"}}');
const $726bf4599eb30a05$export$e7aa7bc5c1b3cfb3 = (url)=>{
return new Promise((resolve, reject)=>{
const req = (0, $gLCr3$http).get(url, (res)=>{
const chunks = [];
res.on("data", (chunk)=>{
chunks.push(chunk);
});
res.on("end", ()=>{
resolve({
headers: res.headers,
body: Buffer.concat(chunks),
statusCode: res.statusCode ?? 500
});
});
res.on("error", (error)=>{
reject(error);
});
});
req.on("error", (error)=>{
reject(error);
});
});
};
const $726bf4599eb30a05$export$707dc63f85edc8be = (params)=>{
const { requestOptions: requestOptions, bodyString: bodyString } = params;
return new Promise((resolve, reject)=>{
const req = (0, $gLCr3$http).request(requestOptions, (res)=>{
const chunks = [];
res.on("data", (chunk)=>{
chunks.push(chunk);
});
res.on("end", ()=>{
resolve({
headers: res.headers,
body: Buffer.concat(chunks),
statusCode: res.statusCode ?? 500
});
});
res.on("error", (error)=>{
reject(error);
});
});
req.on("error", (error)=>{
reject(error);
});
if (bodyString == null) req.end();
else req.end(bodyString);
});
};
const $ecfbf6a852be27bd$export$594a403c6b2e822 = (node, fields)=>{
const data = {};
fields.forEach((field)=>{
const value = node.findtext("./" + field);
if (typeof value !== "undefined") data[field] = value.toString();
});
return data;
};
const $ecfbf6a852be27bd$export$1f2583822e2f26e9 = (url)=>{
return new URL(url).href;
};
const $ecfbf6a852be27bd$export$951d0fb1057358a1 = (baseUrl, url)=>{
return new URL(url, baseUrl).toString();
};
const $ecfbf6a852be27bd$export$388f24d19a29b0a2 = (serviceId)=>{
return serviceId.indexOf(":") === -1 ? "urn:upnp-org:serviceId:" + serviceId : serviceId;
};
const $ce638303bdccc510$export$3aeadf0c5d028ed = (eventsBuffer)=>{
const events = [];
let doc = (0, $gLCr3$elementtree).parse(eventsBuffer.toString());
const lastChange = doc.findtext(".//LastChange");
if (lastChange) {
// AVTransport and RenderingControl services embed event data
// in an `<Event></Event>` element stored as an URIencoded string.
doc = (0, $gLCr3$elementtree).parse(lastChange.toString());
// The `<Event></Event>` element contains one `<InstanceID></InstanceID>`
// subtree per stream instance reporting its status.
const instances = doc.findall("./InstanceID");
instances.forEach((instance)=>{
const data = {
InstanceID: Number(instance.get("val"))
};
instance.findall("./*").forEach((node)=>{
data[node.tag.toString()] = node.get("val");
});
events.push(data);
});
} else {
// In any other case, each variable is stored separately in a
// `<property></property>` tag
const data = {};
doc.findall("./property/*").forEach((node)=>{
data[node.tag.toString()] = node.text.toString();
});
events.push(data);
}
return events;
};
const $ce638303bdccc510$export$7bf9f14fa3bf6734 = (header)=>{
return Number(header.split("-")[1]);
};
const $ce638303bdccc510$export$c556cc5e0d27daa3 = (xml, url)=>{
const doc = (0, $gLCr3$elementtree).parse(xml);
const desc = {
icons: [],
services: {},
...(0, $ecfbf6a852be27bd$export$594a403c6b2e822)(doc.find("./device"), [
"deviceType",
"friendlyName",
"manufacturer",
"manufacturerURL",
"modelName",
"modelNumber",
"modelDescription",
"UDN"
])
};
const iconNodes = doc.findall("./device/iconList/icon");
desc.icons = iconNodes.map((icon)=>{
return (0, $ecfbf6a852be27bd$export$594a403c6b2e822)(icon, [
"mimetype",
"width",
"height",
"depth",
"url"
]);
});
const serviceNodes = doc.findall("./device/serviceList/service");
serviceNodes.forEach((service)=>{
const tmp = (0, $ecfbf6a852be27bd$export$594a403c6b2e822)(service, [
"serviceType",
"serviceId",
"SCPDURL",
"controlURL",
"eventSubURL"
]);
const id = tmp.serviceId;
delete tmp.serviceId;
desc.services[id] = tmp;
});
// Make URLs absolute
const baseUrl = (0, $ecfbf6a852be27bd$export$1f2583822e2f26e9)(url);
desc.icons.map((icon)=>{
icon.url = (0, $ecfbf6a852be27bd$export$951d0fb1057358a1)(baseUrl, icon.url);
return icon;
});
Object.keys(desc.services).forEach((id)=>{
const service = desc.services[id];
service.SCPDURL = (0, $ecfbf6a852be27bd$export$951d0fb1057358a1)(baseUrl, service.SCPDURL);
service.controlURL = (0, $ecfbf6a852be27bd$export$951d0fb1057358a1)(baseUrl, service.controlURL);
service.eventSubURL = (0, $ecfbf6a852be27bd$export$951d0fb1057358a1)(baseUrl, service.eventSubURL);
});
return desc;
};
const $ce638303bdccc510$export$ed28eb71bbed0619 = (xml)=>{
const doc = (0, $gLCr3$elementtree).parse(xml);
const desc = {
actions: null,
stateVariables: null
};
desc.actions = {};
const actionNodes = doc.findall("./actionList/action");
actionNodes.forEach((action)=>{
const name = action.findtext("./name").toString();
const inputs = [];
const outputs = [];
const nodes = action.findall("./argumentList/argument");
nodes.forEach((argument)=>{
const arg = (0, $ecfbf6a852be27bd$export$594a403c6b2e822)(argument, [
"name",
"direction",
"relatedStateVariable"
]);
const direction = arg.direction;
delete arg.direction;
if (direction === "in") inputs.push(arg);
else outputs.push(arg);
});
desc.actions[name] = {
inputs: inputs,
outputs: outputs
};
});
desc.stateVariables = {};
const nodes = doc.findall("./serviceStateTable/stateVariable");
nodes.forEach((stateVariable)=>{
const name = stateVariable.findtext("./name").toString();
const nodes = stateVariable.findall("./allowedValueList/allowedValue");
const allowedValues = nodes.map((allowedValue)=>{
return allowedValue.text.toString();
});
desc.stateVariables[name] = {
dataType: stateVariable.findtext("./dataType").toString(),
sendEvents: stateVariable.get("sendEvents"),
allowedValues: allowedValues,
defaultValue: stateVariable.findtext("./defaultValue")?.toString()
};
});
return desc;
};
class $b6d78e8c5aad4348$export$a9a483eb6d8951f9 extends Error {
constructor(code, message, extra){
super(`${code}: ${message}`);
this.code = code;
this.extra = extra;
}
}
class $b6d78e8c5aad4348$export$f19ee4bfd5d67d71 extends Error {
constructor(errorCode){
super(`AVTransportError: ${errorCode} - ${$b6d78e8c5aad4348$export$b954b5aa4d9dfc64[errorCode].message} - ${$b6d78e8c5aad4348$export$b954b5aa4d9dfc64[errorCode].description}`);
}
}
const $b6d78e8c5aad4348$export$b954b5aa4d9dfc64 = {
401: {
message: "Invalid Action",
description: "No action by that name at this service."
},
402: {
message: "Invalid Args",
description: "Could be any of the following: not enough in args, too many in args, no in arg by that name, one or more in args are of the wrong data type."
},
404: {
message: "Invalid Var",
description: "No state variable by that name at this service."
},
501: {
message: "Action Failed",
description: "May be returned in current state of service prevents invoking that action."
},
701: {
message: "Transition not available",
description: "The immediate transition from current transport state to desired transport state is not supported by this device."
},
702: {
message: "No contents",
description: "The media does not contain any contents that can be played."
},
703: {
message: "Read error ",
description: "The media cannot be read (e.g., because of dust or a scratch)."
},
704: {
message: "Format not supported for playback",
description: "The storage format of the currently loaded media is not supported for playback by this device."
},
705: {
message: "Transport is locked",
description: "The transport is \u201Chold locked\u201D. (Some portable mobile devices have a small mechanical toggle switch called a \u201Chold lock switch\u201D. While this switch is ON, i.e., the transport is hold locked, the device is guarded against operations such as accidental power on when not in use, or interruption of play or record from accidental pressing of a front panel button or a GUI button.)"
},
706: {
message: "Write error",
description: "The media cannot be written (e.g., because of dust or a scratch)."
},
707: {
message: "Media is protected or not writable",
description: "The media is write-protected or is of a not writable type."
},
708: {
message: "Format not supported for recording",
description: "The storage format of the currently loaded media is not supported for recording by this device."
},
709: {
message: "Media is full",
description: "There is no free space left on the loaded media."
},
710: {
message: "Seek mode not supported",
description: "The specified seek mode is not supported by the device."
},
711: {
message: "Illegal seek target",
description: "The specified seek target is not specified in terms of the seek mode, or is not present on the media."
},
712: {
message: "Play mode not supported",
description: "The specified play mode is not supported by the device."
},
713: {
message: "Record quality not supported",
description: "The specified record quality is not supported by the device."
},
714: {
message: "Illegal MIME-type",
description: "The resource to be played has a MIME-type which is not supported by the AVTransport service."
},
715: {
message: 'Content "BUSY"',
description: "This indicates the resource is already being played by other means. The actual implementation might detect through HTTP Busy, and returns this error code."
},
716: {
message: "Resource not found",
description: "The resource to be played cannot be found in the network."
},
717: {
message: "Play speed not supported",
description: "The specified playback speed is not supported by the AVTransport service."
},
718: {
message: "Invalid InstanceID",
description: "The specified instanceID is invalid for this AVTransport."
},
737: {
message: "No DNS Server",
description: "The DNS Server is not available (HTTP error 503)."
},
738: {
message: "Bad Domain Name",
description: "Unable to resolve the Fully Qualified Domain Name (HTTP error 502)."
},
739: {
message: "Server Error",
description: "The server that hosts the resource is unreachable or unresponsive (HTTP error 404/410)."
}
};
const $4caa021e1eac66fe$var$cliColors = {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
FgBlack: "\x1b[30m",
FgRed: "\x1b[31m",
FgGreen: "\x1b[32m",
FgYellow: "\x1b[33m",
FgBlue: "\x1b[34m",
FgMagenta: "\x1b[35m",
FgCyan: "\x1b[36m",
FgWhite: "\x1b[37m",
BgBlack: "\x1b[40m",
BgRed: "\x1b[41m",
BgGreen: "\x1b[42m",
BgYellow: "\x1b[43m",
BgBlue: "\x1b[44m",
BgMagenta: "\x1b[45m",
BgCyan: "\x1b[46m",
BgWhite: "\x1b[47m"
};
const $4caa021e1eac66fe$var$getLogTimestamp = ()=>{
const now = new Date();
const M = now.toLocaleString("default", {
month: "short"
});
const d = now.getDate();
const H = now.getHours();
const m = now.getMinutes();
const s = now.getSeconds();
return `[${M} ${d} ${H}:${m}:${s}]`;
};
const $4caa021e1eac66fe$export$1c9f709888824e05 = (...rest)=>{
const shouldLog = process.env.DEBUG === "true";
if (!shouldLog) return;
console.log($4caa021e1eac66fe$export$acf44fc0058220b7($4caa021e1eac66fe$var$getLogTimestamp(), "FgBlue"), ...rest);
};
const $4caa021e1eac66fe$export$acf44fc0058220b7 = (text, color)=>{
return `${$4caa021e1eac66fe$var$cliColors[color]}${text}${$4caa021e1eac66fe$var$cliColors.Reset}`;
};
const $be68d0d29cb5b615$var$OS_VERSION = [
(0, $gLCr3$os).platform(),
(0, $gLCr3$os).release()
].join("/");
const $be68d0d29cb5b615$var$PACKAGE_VERSION = [
(0, (/*@__PURE__*/$parcel$interopDefault($337cb25062390681$exports))).name,
(0, (/*@__PURE__*/$parcel$interopDefault($337cb25062390681$exports))).version
].join("/");
const $be68d0d29cb5b615$var$SUBSCRIPTION_TIMEOUT = 300;
class $be68d0d29cb5b615$export$46e1cecbf9b81586 extends (0, $gLCr3$EventEmitter) {
constructor(url){
super();
this.getDeviceDescription = async ()=>{
// Use cache if available
if (this.deviceDescription) return this.deviceDescription;
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("Fetch device description");
const response = await (0, $726bf4599eb30a05$export$e7aa7bc5c1b3cfb3)(this.url);
const desc = (0, $ce638303bdccc510$export$c556cc5e0d27daa3)(response.body.toString(), this.url);
this.deviceDescription = desc; // Store in cache for next call
return desc;
};
this.getServiceDescription = async (serviceId)=>{
serviceId = (0, $ecfbf6a852be27bd$export$388f24d19a29b0a2)(serviceId);
const deviceDescription = await this.getDeviceDescription();
const service = deviceDescription.services[serviceId];
if (!service) throw new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("ENOSERVICE", `Service ${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")} not provided by device`);
// Use cache if available
if (this.serviceDescriptions[serviceId]) return this.serviceDescriptions[serviceId];
(0, $4caa021e1eac66fe$export$1c9f709888824e05)(`Fetch service description (${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")})`);
const response = await (0, $726bf4599eb30a05$export$e7aa7bc5c1b3cfb3)(service.SCPDURL);
const desc = (0, $ce638303bdccc510$export$ed28eb71bbed0619)(response.body.toString());
this.serviceDescriptions[serviceId] = desc; // Store in cache for next call
return desc;
};
this.callAction = async (serviceId, actionName, params)=>{
serviceId = (0, $ecfbf6a852be27bd$export$388f24d19a29b0a2)(serviceId);
const serviceDescription = await this.getServiceDescription(serviceId);
if (!serviceDescription.actions[actionName]) throw new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("ENOACTION", `Action ${actionName} not implemented by service`);
const service = this.deviceDescription.services[serviceId];
// Build SOAP action body
const envelope = (0, $gLCr3$elementtree).Element("s:Envelope");
envelope.set("xmlns:s", "http://schemas.xmlsoap.org/soap/envelope/");
envelope.set("s:encodingStyle", "http://schemas.xmlsoap.org/soap/encoding/");
const body = (0, $gLCr3$elementtree).SubElement(envelope, "s:Body");
const action = (0, $gLCr3$elementtree).SubElement(body, "u:" + actionName);
action.set("xmlns:u", service.serviceType);
Object.keys(params).forEach((paramName)=>{
const tmp = (0, $gLCr3$elementtree).SubElement(action, paramName);
const value = params[paramName];
tmp.text = value === null ? "" : params[paramName].toString();
});
const doc = new (0, $gLCr3$elementtree).ElementTree(envelope);
const xml = doc.write({
xml_declaration: true
});
// Send action request
const options = (0, $gLCr3$urlToHttpOptions)(new URL(service.controlURL));
options.method = "POST";
options.headers = {
"Content-Type": 'text/xml; charset="utf-8"',
"Content-Length": xml.length,
Connection: "close",
SOAPACTION: '"' + service.serviceType + "#" + actionName + '"'
};
(0, $4caa021e1eac66fe$export$1c9f709888824e05)(`Call action ${actionName} on service ${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")} with params:\n`, params);
const serviceDescriptions = this.serviceDescriptions;
const response = await (0, $726bf4599eb30a05$export$707dc63f85edc8be)({
requestOptions: options,
bodyString: xml
});
const responseDoc = (0, $gLCr3$elementtree).parse(response.body.toString());
if (response.statusCode !== 200) {
const errorCode = responseDoc.findtext(".//errorCode").toString() ?? "0";
const errorDescription = responseDoc.findtext(".//errorDescription").toString().trim() ?? "Unknown error";
throw new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("EUPNP", errorDescription, {
errorCode: parseInt(errorCode),
httpCode: response.statusCode
});
}
// Extract response outputs
const serviceDesc = serviceDescriptions[serviceId];
const actionDesc = serviceDesc.actions[actionName];
const outputs = actionDesc.outputs.map((desc)=>{
return desc.name;
});
const result = {};
outputs.forEach((name)=>{
result[name] = responseDoc.findtext(".//" + name).toString() ?? "";
});
return result;
};
this.subscribe = async (serviceId, listener)=>{
serviceId = (0, $ecfbf6a852be27bd$export$388f24d19a29b0a2)(serviceId);
if (this.subscriptions[serviceId]) {
// If we already have a subscription to this service,
// add the provided callback to the listeners and return
this.subscriptions[serviceId].listeners.push(listener);
return;
}
// If there's no subscription to this service, create one
// by first fetching the event subscription URL ...
const deviceDescription = await this.getDeviceDescription();
const service = deviceDescription.services[serviceId];
if (!service) throw new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("ENOSERVICE", `Service ${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")} not provided by device`);
// ... and ensuring the event server is created and listening
await this.ensureEventingServer();
const options = (0, $gLCr3$urlToHttpOptions)(new URL(service.eventSubURL));
const serverAddress = this.server.address();
options.method = "SUBSCRIBE";
options.headers = {
HOST: options.hostname,
"USER-AGENT": `${$be68d0d29cb5b615$var$OS_VERSION} UPnP/1.1 ${$be68d0d29cb5b615$var$PACKAGE_VERSION}`,
CALLBACK: `<http://${serverAddress["address"]}:${serverAddress["port"]}/>`,
NT: "upnp:event",
TIMEOUT: `Second-${$be68d0d29cb5b615$var$SUBSCRIPTION_TIMEOUT}`
};
try {
const response = await (0, $726bf4599eb30a05$export$707dc63f85edc8be)({
requestOptions: options,
bodyString: null
});
if (response.statusCode !== 200) {
const error = new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("ESUB", `${response.statusCode} - SUBSCRIBE error`, {
httpCode: response.statusCode
});
this.releaseEventingServer();
this.emit("error", error);
throw error;
}
(0, $4caa021e1eac66fe$export$1c9f709888824e05)(`Subscribed to ${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")}`);
const sid = response.headers["sid"];
const timeout = (0, $ce638303bdccc510$export$7bf9f14fa3bf6734)(response.headers["timeout"]);
const renewTimeout = Math.max(timeout - 30, 30); // renew 30 seconds before expiration
(0, $4caa021e1eac66fe$export$1c9f709888824e05)(`Renewing subscription to ${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")} in ${renewTimeout} seconds`);
const timer = setTimeout(()=>void this.renew(serviceId, sid, service), renewTimeout * 1000);
this.subscriptions[serviceId] = {
sid: sid,
url: service.eventSubURL,
timer: timer,
listeners: [
listener
]
};
} catch (error) {
this.emit("error", error);
}
};
this.renew = async (serviceId, sid, service)=>{
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("Renew subscription to", serviceId);
const options = (0, $gLCr3$urlToHttpOptions)(new URL(service.eventSubURL));
options.method = "SUBSCRIBE";
options.headers = {
HOST: options.hostname,
SID: sid,
TIMEOUT: `Second-${$be68d0d29cb5b615$var$SUBSCRIPTION_TIMEOUT}`
};
const response = await (0, $726bf4599eb30a05$export$707dc63f85edc8be)({
requestOptions: options,
bodyString: null
});
if (response.statusCode !== 200) {
const error = new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("ESUB", `${response.statusCode} - SUBSCRIBE renewal error`, {
httpCode: response.statusCode
});
// XXX: should we clear the subscription and release the server here ?
this.emit("error", error);
return;
}
const timeout = (0, $ce638303bdccc510$export$7bf9f14fa3bf6734)(response.headers["timeout"]);
const renewTimeout = Math.max(timeout - 30, 30); // renew 30 seconds before expiration
(0, $4caa021e1eac66fe$export$1c9f709888824e05)(`Renewing subscription to ${(0, $4caa021e1eac66fe$export$acf44fc0058220b7)(serviceId, "FgMagenta")} in ${renewTimeout} seconds`);
const timer = setTimeout(()=>void this.renew(serviceId, sid, service), renewTimeout * 1000);
this.subscriptions[serviceId].timer = timer;
};
this.unsubscribe = async (serviceId, listener)=>{
serviceId = (0, $ecfbf6a852be27bd$export$388f24d19a29b0a2)(serviceId);
// First make sure there are subscriptions for this service ...
const subscription = this.subscriptions[serviceId];
if (!subscription) return;
// ... and we know about this listener
const idx = subscription.listeners.indexOf(listener);
if (idx === -1) return;
// Remove the listener from the list
subscription.listeners.splice(idx, 1);
if (subscription.listeners.length === 0) {
// If there's no listener left for this service, unsubscribe from it
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("Unsubscribe from service", serviceId);
const options = (0, $gLCr3$urlToHttpOptions)(new URL(subscription.url));
options.method = "UNSUBSCRIBE";
options.headers = {
HOST: options.hostname,
SID: subscription.sid
};
try {
const response = await (0, $726bf4599eb30a05$export$707dc63f85edc8be)({
requestOptions: options,
bodyString: null
});
if (response.statusCode !== 200) {
const error = new (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9)("EUNSUB", `${response.statusCode} - UNSUBSCRIBE error`, {
httpCode: response.statusCode
});
return this.emit("error", error);
}
clearTimeout(this.subscriptions[serviceId].timer);
delete this.subscriptions[serviceId];
// Make sure the eventing server is shutdown if there is no
// subscription left for any service
this.releaseEventingServer();
} catch (error) {
this.emit("error", error);
}
}
};
this.ensureEventingServer = ()=>{
return new Promise((resolve)=>{
if (!this.listening) {
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("Create eventing server");
this.server.listen(0, $507f4ad1d2498db3$export$291a17a16047f284());
return this.server.on("listening", ()=>{
this.listening = true;
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("Eventing server started to listen");
return resolve();
});
} else process.nextTick(()=>resolve());
});
};
this.releaseEventingServer = ()=>{
if (Object.keys(this.subscriptions).length === 0) {
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("Shutdown eventing server");
this.server.close();
this.server = null;
this.listening = false;
}
};
this.url = url;
this.deviceDescription = null;
this.serviceDescriptions = {};
this.listening = false;
this.subscriptions = {};
this.server = (0, $gLCr3$http).createServer((req)=>{
const chunks = [];
req.on("data", (chunk)=>{
chunks.push(chunk);
});
req.on("end", ()=>{
const sid = req.headers["sid"];
const seq = req.headers["seq"];
const events = (0, $ce638303bdccc510$export$3aeadf0c5d028ed)(Buffer.concat(chunks));
(0, $4caa021e1eac66fe$export$1c9f709888824e05)(`Received events from ${sid}, number ${seq}:\n`, events);
const keys = Object.keys(this.subscriptions);
const sids = keys.map((key)=>{
return this.subscriptions[key].sid;
});
const idx = sids.indexOf(sid);
if (idx === -1) {
(0, $4caa021e1eac66fe$export$1c9f709888824e05)("WARNING unknown SID:", sid);
// silently ignore unknown SIDs
return;
}
const serviceId = keys[idx];
const listeners = this.subscriptions[serviceId].listeners;
// Dispatch each event to each listener registered for
// this service's events
listeners.forEach((listener)=>{
events.forEach((e)=>{
listener(e);
});
});
});
});
}
}
const $8b2172c32d75e0db$export$9f2623f1b4c8cf8 = (url, metadata, options)=>{
const dlnaFeatures = options.dlnaFeatures ?? "*";
const contentType = options.contentType ?? "video/mpeg"; // Default to something generic
const protocolInfo = "http-get:*:" + contentType + ":" + dlnaFeatures;
metadata;
metadata.url = url;
metadata.protocolInfo = protocolInfo;
const didl = (0, $gLCr3$elementtree).Element("DIDL-Lite");
didl.set("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
didl.set("xmlns:dc", "http://purl.org/dc/elements/1.1/");
didl.set("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
didl.set("xmlns:sec", "http://www.sec.co.kr/");
const item = (0, $gLCr3$elementtree).SubElement(didl, "item");
item.set("id", "0");
item.set("parentID", "-1");
item.set("restricted", "false");
const OBJECT_CLASSES = {
audio: "object.item.audioItem.musicTrack",
video: "object.item.videoItem.movie",
image: "object.item.imageItem.photo"
};
if (metadata.type) {
const klass = (0, $gLCr3$elementtree).SubElement(item, "upnp:class");
klass.text = OBJECT_CLASSES[metadata.type];
}
if (metadata.title) {
const title = (0, $gLCr3$elementtree).SubElement(item, "dc:title");
title.text = metadata.title;
}
if (metadata.creator) {
const creator = (0, $gLCr3$elementtree).SubElement(item, "dc:creator");
creator.text = metadata.creator;
}
if (metadata.artist) {
const artist = (0, $gLCr3$elementtree).SubElement(item, "upnp:artist");
artist.text = metadata.artist;
}
if (metadata.album) {
const album = (0, $gLCr3$elementtree).SubElement(item, "upnp:album");
album.text = metadata.album;
}
if (metadata.albumArtURI) {
const albumArtURI = (0, $gLCr3$elementtree).SubElement(item, "upnp:albumArtURI");
albumArtURI.text = metadata.albumArtURI;
}
if (metadata.genre) {
const genre = (0, $gLCr3$elementtree).SubElement(item, "upnp:genre");
genre.text = metadata.genre;
}
if (metadata.url && metadata.protocolInfo) {
const res = (0, $gLCr3$elementtree).SubElement(item, "res");
res.set("protocolInfo", metadata.protocolInfo);
res.text = metadata.url;
}
if (metadata.subtitlesUrl) {
const captionInfo = (0, $gLCr3$elementtree).SubElement(item, "sec:CaptionInfo");
captionInfo.set("sec:type", "srt");
captionInfo.text = metadata.subtitlesUrl;
const captionInfoEx = (0, $gLCr3$elementtree).SubElement(item, "sec:CaptionInfoEx");
captionInfoEx.set("sec:type", "srt");
captionInfoEx.text = metadata.subtitlesUrl;
// Create a second `res` for the subtitles
const res = (0, $gLCr3$elementtree).SubElement(item, "res");
res.set("protocolInfo", "http-get:*:text/srt:*");
res.text = metadata.subtitlesUrl;
}
const doc = new (0, $gLCr3$elementtree).ElementTree(didl);
const xml = doc.write({
xml_declaration: false
});
return {
metadata: metadata,
xml: xml
};
};
const $481f300f372bb164$var$pad = (v)=>{
return v < 10 ? "0" + v.toString() : v.toString();
};
const $481f300f372bb164$export$3203edd9e5edd663 = (seconds)=>{
let h = 0;
let m = 0;
let s = 0;
h = Math.floor((seconds - h * 0 - m * 0) / 3600);
m = Math.floor((seconds - h * 3600 - m * 0) / 60);
s = seconds - h * 3600 - m * 60;
return `${$481f300f372bb164$var$pad(h)}:${$481f300f372bb164$var$pad(m)}:${$481f300f372bb164$var$pad(s)}`;
};
const $481f300f372bb164$export$c9698ec7f05a07e1 = (time)=>{
if (!time) return 0;
const parts = time.split(":").map(Number);
return parts[0] * 3600 + parts[1] * 60 + parts[2];
};
const $4681b0383e6659e8$var$MEDIA_EVENTS = [
"status",
"loading",
"playing",
"paused",
"stopped",
"speedChanged"
];
class $4681b0383e6659e8$export$4ac5edbbf5e258df extends (0, $be68d0d29cb5b615$export$46e1cecbf9b81586) {
constructor(url){
super(url);
this.callAVTransport = (actionName, params)=>this.callAction("AVTransport", actionName, params).catch((error)=>{
if (error instanceof (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9) && error.extra.errorCode) throw new (0, $b6d78e8c5aad4348$export$f19ee4bfd5d67d71)(error.extra.errorCode);
throw error;
});
this.getSupportedProtocols = async ()=>{
const response = await this.callAction("ConnectionManager", "GetProtocolInfo", {});
//
// Here we leave off the `Source` field as we're hopefuly dealing with a Sink-only device.
//
const lines = response.Sink.split(",");
const protocols = lines.map((line)=>{
const tmp = line.split(":");
return {
protocol: tmp[0],
network: tmp[1],
contentFormat: tmp[2],
additionalInfo: tmp[3]
};
});
return protocols;
};
this.getPosition = async ()=>{
const response = await this.callAVTransport("GetPositionInfo", {
InstanceID: this.instanceId
});
return (0, $481f300f372bb164$export$c9698ec7f05a07e1)(response.RelCount);
};
this.getPositionInfo = ()=>{
return this.callAVTransport("GetPositionInfo", {
InstanceID: this.instanceId
});
};
this.getDuration = async ()=>{
const response = await this.callAVTransport("GetMediaInfo", {
InstanceID: this.instanceId
});
return (0, $481f300f372bb164$export$c9698ec7f05a07e1)(response.MediaDuration);
};
this.getMediaInfo = ()=>{
return this.callAVTransport("GetMediaInfo", {
InstanceID: this.instanceId
});
};
this.load = async (url, options)=>{
const metadata = (0, $8b2172c32d75e0db$export$9f2623f1b4c8cf8)(url, options.metadata, options);
const paramsPrepareForConnection = {
RemoteProtocolInfo: metadata.metadata.protocolInfo,
PeerConnectionManager: null,
PeerConnectionID: -1,
Direction: "Input"
};
await this.callAction("ConnectionManager", "PrepareForConnection", paramsPrepareForConnection).catch((error)=>{
if (error instanceof (0, $b6d78e8c5aad4348$export$a9a483eb6d8951f9) && error.code !== "ENOACTION") throw error;
}).then((connection)=>{
if (connection) this.instanceId = Number(connection["AVTransportID"]);
});
const paramsSetAVTransportURI = {
InstanceID: this.instanceId,
CurrentURI: url,
CurrentURIMetaData: metadata.xml
};
const response = await this.callAVTransport("SetAVTransportURI", paramsSetAVTransportURI);
if (options.autoplay) return this.play();
return response;
};
this.loadNext = (url, options)=>{
if (!this.listening) throw new Error("No media was loaded first, use load method.");
const params = {
InstanceID: this.instanceId,
NextURI: url,
NextURIMetaData: (0, $8b2172c32d75e0db$export$9f2623f1b4c8cf8)(url, options.metadata, options).xml
};
return this.callAVTransport("SetNextAVTransportURI", params);
};
this.play = ()=>{
const params = {
InstanceID: this.instanceId,
Speed: 1
};
return this.callAVTransport("Play", params);
};
this.pause = async ()=>{
const params = {
InstanceID: this.instanceId
};
await this.callAVTransport("Pause", params);
};
this.stop = async ()=>{
const params = {
InstanceID: this.instanceId
};
await this.callAVTransport("Stop", params);
};
this.next = async ()=>{
const params = {
InstanceID: this.instanceId
};
await this.callAVTransport("Next", params);
};
this.previous = async ()=>{
const params = {
InstanceID: this.instanceId
};
await this.callAVTransport("Previous", params);
};
this.seek = (seconds)=>{
const params = {
InstanceID: this.instanceId,
Unit: "REL_TIME",
Target: (0, $481f300f372bb164$export$3203edd9e5edd663)(seconds)
};
return this.callAVTransport("Seek", params);
};
this.getVolume = async ()=>{
const response = await this.callAction("RenderingControl", "GetVolume", {
InstanceID: this.instanceId,
Channel: "Master"
});
return parseInt(response.CurrentVolume);
};
this.setVolume = async (volume)=>{
const params = {
InstanceID: this.instanceId,
Channel: "Master",
DesiredVolume: volume
};
await this.callAction("RenderingControl", "SetVolume", params);
};
this.getTransportInfo = ()=>{
return this.callAVTransport("GetTransportInfo", {
InstanceID: this.instanceId
});
};
this.instanceId = 0;
// Subscribe / unsubscribe from AVTransport depending
// on relevant registered / removed event listeners.
let refs = 0;
let receivedState;
this.addListener("newListener", (eventName)=>{
if ($4681b0383e6659e8$var$MEDIA_EVENTS.indexOf(eventName) === -1) return;
if (refs === 0) {
receivedState = false;
this.subscribe("AVTransport", onstatus);
}
refs++;
});
this.addListener("removeListener", (eventName)=>{
if ($4681b0383e6659e8$var$MEDIA_EVENTS.indexOf(eventName) === -1) return;
refs--;
if (refs === 0) this.unsubscribe("AVTransport", onstatus);
});
const onstatus = (event)=>{
this.emit("status", event);
if (!receivedState) {
// Starting from here we only want state updates.
// As the first received event is the full service state, we ignore it.
receivedState = true;
return;
}
if (Object.prototype.hasOwnProperty.call(event, "TransportState")) switch(event.TransportState){
case "TRANSITIONING":
this.emit("loading");
break;
case "PLAYING":
this.emit("playing");
break;
case "PAUSED_PLAYBACK":
this.emit("paused");
break;
case "STOPPED":
this.emit("stopped");
break;
case "NO_MEDIA_PRESENT":
this.emit("nomedia");
break;
}
if (Object.prototype.hasOwnProperty.call(event, "TransportPlaySpeed")) this.emit("speedChanged", Number(event.TransportPlaySpeed));
};
}
}
/** Convert flag byte codes to a final flags string */ const $cdccfe8ad1b546f2$var$toDlnaFlagString = (flags)=>{
return "DLNA.ORG_FLAGS=" + flags.toString(16) + "000000000000000000000000";
};
const $cdccfe8ad1b546f2$var$flagByteCodes = {
/** Content source is the clock source during transport */ DLNA_ORG_FLAG_SENDER_PACED: -2147483648,
/** Limited Operation: time-seek supported */ DLNA_ORG_FLAG_TIME_BASED_SEEK: 1073741824,
/** Limited Operation: byte-seek supported */ DLNA_ORG_FLAG_BYTE_BASED_SEEK: 536870912,
/** Resource supports 'Container Playback' */ DLNA_ORG_FLAG_PLAY_CONTAINER: 268435456,
/** Content does not have a fixed beginning */ DLNA_ORG_FLAG_S0_INCREASE: 134217728,
/** Content does not have a fixed end */ DLNA_ORG_FLAG_SN_INCREASE: 67108864,
/** RTSP resource supports pausing of media transfer */ DLNA_ORG_FLAG_RTSP_PAUSE: 33554432,
/** Streaming transfer mode supported */ DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE: 16777216,
/** Interactive transfer mode supported */ DLNA_ORG_FLAG_INTERACTIVE_TRANSFERT_MODE: 8388608,
/** Background transfer mode supported */ DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE: 4194304,
/** No content transfer when paused */ DLNA_ORG_FLAG_CONNECTION_STALL: 2097152,
/** DLNAv1.5 version flag */ DLNA_ORG_FLAG_DLNA_V15: 1048576
};
const $cdccfe8ad1b546f2$var$defaultFlags = {
DLNA_STREAMING_BYTE_BASED_FLAGS: $cdccfe8ad1b546f2$var$toDlnaFlagString($cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_DLNA_V15 | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_BYTE_BASED_SEEK | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE),
DLNA_STREAMING_TIME_BASED_FLAGS: $cdccfe8ad1b546f2$var$toDlnaFlagString($cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_DLNA_V15 | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_TIME_BASED_SEEK | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE),
DLNA_ORIGIN_FLAGS: $cdccfe8ad1b546f2$var$toDlnaFlagString($cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_DLNA_V15 | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_CONNECTION_STALL | $cdccfe8ad1b546f2$var$flagByteCodes.DLNA_ORG_FLAG_INTERACTIVE_TRANSFERT_MODE)
};
const $cdccfe8ad1b546f2$var$getDlnaSeekModeFeature = (seekMode)=>{
let feature = "DLNA.ORG_OP=";
switch(seekMode){
case "none":
default:
feature += "00";
break;
case "range":
feature += "01";
break;
case "time":
feature += "10";
break;
case "both":
feature += "11";
break;
}
return feature;
};
const $cdccfe8ad1b546f2$var$getDlnaTranscodeFeature = (transcodeEnabled)=>{
return "DLNA.ORG_CI=" + (transcodeEnabled ? "1" : "0");
};
const $cdccfe8ad1b546f2$export$9cb65a7c0890271b = {
defaultFlags: $cdccfe8ad1b546f2$var$defaultFlags,
flagByteCodes: $cdccfe8ad1b546f2$var$flagByteCodes,
getDlnaSeekModeFeature: $cdccfe8ad1b546f2$var$getDlnaSeekModeFeature,
getDlnaTranscodeFeature: $cdccfe8ad1b546f2$var$getDlnaTranscodeFeature,
toDlnaFlagString: $cdccfe8ad1b546f2$var$toDlnaFlagString
};
var $dc8adb69e96e43cd$exports = {};
export {$be68d0d29cb5b615$export$46e1cecbf9b81586 as UpnpDeviceClient, $4681b0383e6659e8$export$4ac5edbbf5e258df as UpnpMediaRendererClient, $cdccfe8ad1b546f2$export$9cb65a7c0890271b as dlnaHelpers, $481f300f372bb164$export$3203edd9e5edd663 as formatTime, $481f300f372bb164$export$c9698ec7f05a07e1 as parseTime};
//# sourceMappingURL=module.js.map