node-red-contrib-cast
Version:
NodeRED nodes to for Chromecast and Google Home
942 lines (871 loc) • 39.1 kB
JavaScript
/********************************************
* Cast-to-client:
*********************************************/
const util = require('util');
const Client = require('castv2-client').Client;
const DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver;
// Const path = require('path');
// const YoutubeMediaReceiver = require(path.join(__dirname, '/lib/Youtube.js')).Youtube;
const googleTTS = require('google-tts-api');
const isTrue = function(val) {
val = (val+'').toLowerCase();
return (val === 'true' || val === 'yes' || val === 'on' || val === 'ja' || val === '1' || (!isNaN(val) && (Number(val) > 0)));
};
const isFalse = function(val) {
val = (val+'').toLowerCase();
return (val === 'false' || val === 'no' || val === 'off' || val === 'nein' || val === '0' || (!isNaN(val) && (Number(val) <= 0)));
};
const errorHandler = function (node, err, msg, messageText, stateText) {
if (!err) {
return true;
}
if (err.message) {
const msg = err.message.toLowerCase();
if (msg.indexOf('invalid player state') >= 0) {
// Sent when the request by the sender can not be fulfilled because the player is not in a valid state. For example, if the application has not created a media element yet.
// https://developers.google.com/cast/docs/reference/messages#InvalidPlayerState
messageText += ':' + err.message + ' The request can not be fulfilled because the player is not in a valid state.';
} else if (msg.indexOf('load failed') >= 0) {
// Sent when the load request failed. The player state will be IDLE.
// https://developers.google.com/cast/docs/reference/messages#LoadFailed
messageText += ':' + err.message + ' Not able to load the media.';
} else if (msg.indexOf('load cancelled') >= 0) {
// Sent when the load request was cancelled (a second load request was received).
// https://developers.google.com/cast/docs/reference/messages#LoadCancelled
messageText += ':' + err.message + ' The request was cancelled (a second load request was received).';
} else if (msg.indexOf('invalid request') >= 0) {
// Sent when the request is invalid (an unknown request type, for example).
// https://developers.google.com/cast/docs/reference/messages#InvalidRequest
messageText += ':' + err.message + ' The request is invalid (example: an unknown request type).';
} else {
messageText += ':' + err.message;
}
} else {
messageText += '! (No error message given!)';
}
node.error(messageText, msg || {});
node.log(util.inspect(err, Object.getOwnPropertyNames(err)));
node.status({
fill: 'red',
shape: 'ring',
text: stateText
});
return false;
};
const getContentType = function (data, node, fileName) {
// Node property wins!
if (data.contentType) {
return data.contentType;
}
if (!fileName) {
fileName = data.url;
}
let contentType;
if (fileName) {
const contentTypeMap = {
youtube: 'youtube/video',
// official supported https://developers.google.com/cast/docs/media
aac:'video/mp4',
mp3: 'audio/mp3',
m4a: 'audio/mp4',
mpa: 'audio/mpeg',
mp4: 'audio/mp4',
webm: 'video/webm',
vp8: 'video/webm',
wav: 'audio/vnd.wav',
bmp: 'image/bmp',
gif: 'image/gif',
jpeg: 'image/jpeg',
jpg: 'image/jpeg ',
jpe: 'image/jpeg',
png: 'image/png',
webp: 'image/webp',
// additional other formats
au: 'audio/basic',
snd: 'audio/basic',
mp2: 'audio/x-mpeg',
mid: 'audio/mid',
midi: 'audio/mid',
rmi: 'audio/mid',
aif: 'audio/x-aiff',
aiff: 'audio/x-aiff',
aifc: 'audio/x-aiff',
mov: 'video/quicktime',
qt: 'video/quicktime',
flv: 'video/x-flv',
mpeg: 'video/mpeg',
mpg: 'video/mpeg',
mpe: 'video/mpeg',
mjpg: 'video/x-motion-jpeg',
mjpeg: 'video/x-motion-jpeg',
'3gp': 'video/3gpp',
avi: 'video/x-msvideo',
wmv: 'video/x-ms-wmv',
movie: 'video/x-sgi-movie',
m3u: 'audio/x-mpegurl',
ogg: 'audio/ogg',
ogv: 'audio/ogg',
ra: 'audio/vnd.rn-realaudio', // audio/x-pn-realaudio'
stream: 'audio/x-qt-stream',
rpm: 'audio/x-pn-realaudio-plugin',
ram: 'audio/x-pn-realaudio',
m3u8: 'application/x-mpegURL',
svg: 'image/svg',
tiff: 'image/tiff',
tif: 'image/tiff',
ico: 'image/x-icon'
};
const ext = fileName.split('.')[fileName.split('.').length - 1];
contentType = contentTypeMap[ext.toLowerCase()];
node.debug('contentType for ext ' + ext + ' is ' + contentType + Number('(') + fileName + ')');
}
if (!contentType) {
node.warn('No contentType given!, using "audio/basic" which is maybe wrong! (' + fileName + ')');
contentType = 'audio/basic';
}
return contentType;
};
const addGenericMetadata = function (media, imageUrl, contentTitle) {
if (!contentTitle) {
contentTitle = media.contentTitle;
}
if (!contentTitle) {
// Default from url
contentTitle = media.contentId;
if (contentTitle.indexOf('/') > -1) {
try {
let paths = contentTitle.split('/');
if (paths.length > 2) {
paths = paths.slice(paths.length - 2, paths.length);
}
contentTitle = paths.join(' - ');
} catch (e) {
// Not used
}
}
}
if (!imageUrl) {
imageUrl = (media) ? (media.imageUrl || 'https://nodered.org/node-red-icon.png') : 'https://nodered.org/node-red-icon.png';
}
media.metadata = {
type: 0,
metadataType: 0,
title: contentTitle,
images: [{
url: imageUrl
}]
};
};
const getSpeechUrl = function (node, text, language, options, callback, msg) {
try {
const url = googleTTS.getAudioUrl(text, {
lang: language,
slow: options.slow, // speed (number) is changed to slow (boolean)
host: options.ttsHost || 'https://translate.google.com' // allow to change the host
});
node.debug('returned tts media url=\'' + url + '\'');
const media = {
contentId: url,
contentType: 'audio/mp3',
imageUrl: (options.media) ? options.media.imageUrl : 'https://nodered.org/node-red-icon.png',
contentTitle: (options.media) ? (options.media.contentTitle || options.topic) : options.topic
};
doCast(node, media, options, (res, data) => {
callback(url, data);
}, msg);
} catch (err) {
errorHandler(node, err, msg, 'Not able to get media file via google-tts', 'error in tts');
}
};
const doCast = function (node, media, options, callbackResult, msg) {
const client = new Client();
const onStatus = function (status) {
node.debug('onStatus ' + util.inspect(status, { colors: true, compact: 10, breakLength: Infinity }));
if (node) {
node.send({
payload: status,
type: 'status',
topic: options.topic + '/status'
});
}
};
const onClose = function () {
node.debug('Player Close');
client.close();
/*
Reconnect(function(newClient, newPlayer) {
chcClient = newClient;
chcPlayer = newPlayer;
});
*/
};
const onError = function (err) {
client.close();
errorHandler(node, err, msg, 'Client error reported', 'client error');
};
const doGetVolume = function (fkt) {
client.getVolume((err, vol) => {
if (err) {
errorHandler(node, err, msg, 'Not able to get the volume');
} else {
node.context().set('volume', vol.level);
node.debug('volume get from client ' + util.inspect(vol, { colors: true, compact: 10, breakLength: Infinity }));
if (fkt) {
fkt(vol);
}
}
});
};
const doSetVolume = function (volume) {
node.debug('doSetVolume ' + util.inspect(volume, { colors: true, compact: 10, breakLength: Infinity }));
let obj = {};
if (typeof volume === 'object') {
obj = volume;
} else if (volume < 0.01) {
obj = {
muted: true
};
} else if (volume > 0.99) {
obj = {
level: 1
};
} else {
obj = {
level: volume
};
}
node.debug('try to set volume ' + util.inspect(obj, { colors: true, compact: 10, breakLength: Infinity }));
client.setVolume(obj, (err, newvol) => {
const oldVol = node.context().get('volume');
if (oldVol !== newvol.level) {
node.context().set('oldVolume', oldVol);
node.context().set('volume', newvol.level);
}
if (err) {
errorHandler(node, err, msg, 'Not able to set the volume');
} else if (node) {
node.log('volume changed to ' + Math.round(newvol.level * 100));
}
});
};
const checkVolume = function (options) {
node.debug('checkVolume ' + util.inspect(options, { colors: true, compact: 10, breakLength: Infinity }));
if (isTrue(options.muted)) {
doSetVolume({
muted: true
});
} else if (isFalse(options.muted)) {
doSetVolume({
muted: false
});
} else if (typeof options.volume !== 'undefined' && options.volume !== null) {
doSetVolume(options.volume);
} else if (typeof options.lowerVolumeLimit !== 'undefined' || typeof options.upperVolumeLimit !== 'undefined') {
// Eventually getVolume --> https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media.Player
doGetVolume( (newvol) => {
options.oldVolume = newvol.level * 100;
if (options.upperVolumeLimit !== 'undefined' && (newvol.level > options.upperVolumeLimit)) {
doSetVolume(options.upperVolumeLimit);
} else if (typeof options.lowerVolumeLimit !== 'undefined' && (newvol.level < options.lowerVolumeLimit)) {
doSetVolume(options.lowerVolumeLimit);
}
});
}
};
const checkOptions = function (options) {
if (isTrue(options.pause)) {
node.debug('sending pause signal to player');
client.getSessions((err, sessions) => {
if (err) {
errorHandler(node, err, msg, 'Not able to get sessions');
} else {
client.join(sessions[0], DefaultMediaReceiver, (err, app) => {
if (!app.media.currentSession) {
app.getStatus(() => {
app.pause();
});
} else {
app.pause();
}
});
}
});
}
if (isTrue(options.stop)) {
node.debug('sending stop signal to player');
client.getSessions((err, sessions) => {
node.debug('session data response' + util.inspect(sessions, { colors: true, compact: 10, breakLength: Infinity }));
if (err) {
errorHandler(node, err, msg, 'Not able to get sessions');
} else {
client.join(sessions[0], DefaultMediaReceiver, (err, app) => {
node.debug('join session response' + util.inspect(app, { colors: true, compact: 10, breakLength: Infinity }));
if (err) {
errorHandler(node, err, msg, 'error joining session');
} else if (!app.media.currentSession) {
node.debug('send stop 1');
app.getStatus(() => {
node.debug('send stop 2');
app.stop();
});
} else {
node.debug('send stop 3');
app.stop();
}
});
}
});
}
};
const statusCallback = function () {
node.debug('statusCallback');
client.getSessions((err, sessions) => {
node.debug('getSessions Callback');
if (err) {
errorHandler(node, err, msg, 'error getting session');
} else {
try {
checkVolume(options);
const session = sessions[0]; // For now only one app runs at once, so using the first session should be ok
node.debug('session ' + util.inspect(sessions, { colors: true, compact: 10, breakLength: Infinity }));
if (typeof sessions !== 'undefined' && sessions.length > 0) {
client.join(session, DefaultMediaReceiver, (err, player) => {
node.debug('join Callback');
if (err) {
errorHandler(node, err, msg, 'error joining session');
} else {
node.debug('session joined ...');
player.on('status', onStatus);
player.on('close', onClose);
if (isTrue(options.pause)) {
node.debug('sending pause ...');
if (!player.media.currentSession){
player.getStatus(() => {
player.pause();
});
} else {
player.pause();
}
}
if (isTrue(options.stop)) {
node.debug('sending stop ...');
if (!player.media.currentSession){
node.debug('send stop 1');
player.getStatus(() => {
node.debug('send stop 2');
player.stop();
});
} else {
node.debug('send stop 3');
player.stop();
}
}
node.debug('do get Status from player');
client.getStatus((err, status) => {
if (err) {
errorHandler(node, err, msg, 'Not able to get status');
} else {
callbackResult(status, options);
}
client.close();
});
}
});
} else {
// Nothing is playing
client.close();
callbackResult({
applications: []
}, options);
}
} catch (err) {
errorHandler(node, err, msg, 'Exception occurred on load media', 'exception load media');
}
}
});
};
/*
Const launchYTCallback = function () {
node.debug('launchYTCallback');
client.launch(YoutubeMediaReceiver, function (err, player) {
if (err) {
errorHandler(node, err, msg, 'Not able to launch YoutubeMediaReceiver');
}
try {
checkOptions(options)
if (media.videoId) {
media.contentId = videoId;
}
node.debug('experimental implementation playing youtube videos media=\'' + util.inspect(media, Object.getOwnPropertyNames(media)) + '\'');
player.load(media.contentId, (err) => {
if (err) {
errorHandler(node, err, msg, 'Not able to load youtube video', 'error load youtube video');
}
client.close();
});
} catch (err) {
errorHandler(node, err, msg, 'Exception occurred on load youtube video', 'exception load youtube video');
}
});
};
*/
const launchDefCallback = function () {
node.debug('launchDefCallback');
client.launch(DefaultMediaReceiver, (err, player) => {
if (err) {
errorHandler(node, err, msg, 'Not able to launch DefaultMediaReceiver');
return;
}
if (!player) {
errorHandler(node, null, msg, 'Not able to load player from DefaultMediaReceiver');
return;
}
try {
checkVolume(options);
checkOptions(options);
if (media !== null &&
typeof media === 'object' &&
typeof media.contentId !== 'undefined') {
if (typeof media.contentType !== 'string' ||
media.contentType === '') {
media.contentType = 'audio/basic';
}
if (typeof media.streamType !== 'string' ||
media.streamType === '') {
media.streamType = 'BUFFERED'; // Or LIVE
}
node.debug('loading player with media=\'' + util.inspect(media, { colors: true, compact: 10, breakLength: Infinity }) + '\' streamType=' + media.streamType);
addGenericMetadata(media);
player.load(media, {
autoplay: true
}, (err, status) => {
if (err) {
errorHandler(node, err, msg, 'Not able to load media', 'error load media');
} else if (options && typeof options.seek !== 'undefined' && !isNaN(options.seek)) {
if (options && typeof options.seek !== 'undefined' && !isNaN(options.seek)) {
node.debug('seek to position ' + String(options.seek));
player.seek(options.seek, (err, status) => {
if (err) {
errorHandler(node, err, msg, 'Not able to seek to position ' + String(options.seek));
} else {
callbackResult(status, options);
}
});
}
} else {
callbackResult(status, options);
}
client.close();
});
} else {
node.debug('get Status from player');
client.getStatus((err, status) => {
if (err) {
errorHandler(node, err, msg, 'Not able to get status');
} else {
callbackResult(status, options);
}
client.close();
});
}
} catch (err) {
errorHandler(node, err, msg, 'Exception occurred on load media', 'exception load media');
}
});
};
const launchQueueCallback = function () {
client.launch(DefaultMediaReceiver, (err, player) => {
if (err) {
errorHandler(node, err, msg, 'Not able to launch DefaultMediaReceiver');
}
player.on('status', status => {
node.debug('QUEUE STATUS ' + util.inspect(status, { colors: true, compact: 10, breakLength: Infinity }));
});
try {
checkVolume(options);
checkOptions(options);
if (media !== null &&
typeof media === 'object') {
node.log('loading player with queue= ' + media.mediaList.length + ' items');
node.debug('queue data=\'' + util.inspect(media, { colors: true, compact: 10, breakLength: Infinity }) + '\'');
for (let i = 0; i < media.mediaList.length; i++) {
addGenericMetadata(media.mediaList[i].media, media.imageUrl, media.contentTitle);
}
player.queueLoad(
media.mediaList, {
startIndex: 0,
repeatMode: 'REPEAT_OFF'
},
(err, status) => {
node.log('Loaded QUEUE of ' + media.mediaList.length + ' items');
if (err) {
errorHandler(node, err, msg, 'Not able to load media', 'error load media');
} else if (options && typeof options.seek !== 'undefined' && !isNaN(options.seek)) {
if (options && typeof options.seek !== 'undefined' && !isNaN(options.seek)) {
node.debug('seek to position ' + String(options.seek));
player.seek(options.seek, (err, status) => {
if (err) {
errorHandler(node, err, msg, 'Not able to seek to position ' + String(options.seek));
} else {
callbackResult(status, options);
}
});
}
} else {
callbackResult(status, options);
}
client.close();
}
);
} else {
node.debug('get Status from player');
client.getStatus((err, status) => {
if (err) {
errorHandler(node, err, msg, 'Not able to get status');
} else {
callbackResult(status, options);
}
client.close();
});
}
} catch (err) {
errorHandler(node, err, msg, 'Exception occurred on load media', 'exception load media');
}
});
};
try {
client.on('error', onError);
client.on('status', onStatus);
if (typeof options.host === 'undefined') {
options.host = options.ip;
}
node.debug('connect to client ' + util.inspect(options, { colors: true, compact: 10, breakLength: Infinity }));
if ((options && typeof options.status !== 'undefined' && options.status === true) ||
(typeof media === 'undefined') || (media === null)) {
client.connect(options, statusCallback);
} else if (media.mediaList && media.mediaList.length > 0) {
client.connect(options, launchQueueCallback);
} else if (media.contentType.indexOf('youtube') !== -1) {
node.error('currently not supported', {});
// Client.connect(options, launchYTCallback);
} else {
client.connect(options, launchDefCallback);
}
} catch (err) {
errorHandler(node, err, msg, 'Exception occurred on load media', 'exception load media');
}
};
module.exports = function (RED) {
function CastNode(config) {
RED.nodes.createNode(this, config);
const node = this;
this.on('input', function (msg, send, done) {
// If this is pre-1.0, 'send' will be undefined, so fallback to node.send
// If this is pre-1.0, 'done' will be undefined
done = done || function (text, msg) { if (text) { return node.error(text, msg); } return null; };
send = send || function (...args) { node.send.apply(node, args); };
this.debug('getting message ' + util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity }));
//-----------------------------------------
// Error Handling
if (!Client) {
this.status({
fill: 'red',
shape: 'dot',
text: 'installation error'
});
done('Client not defined!! - Installation Problem, Please reinstall!', msg);
return;
}
if (!DefaultMediaReceiver) {
this.status({
fill: 'red',
shape: 'dot',
text: 'installation error'
});
done('DefaultMediaReceiver not defined!! - Installation Problem, Please reinstall!', msg);
return;
}
if (!googleTTS) {
this.status({
fill: 'red',
shape: 'dot',
text: 'installation error'
});
done('googleTTS not defined!! - Installation Problem, Please reinstall!', msg);
return;
}
/********************************************
* Versenden:
*********************************************/
// var creds = RED.nodes.getNode(config.creds); - not used
const attrs = [
'media', 'url', 'urlList', 'imageUrl', 'contentType', 'contentTitle',
'streamType', 'message', 'language', 'ip', 'port', 'volume',
'lowerVolumeLimit', 'upperVolumeLimit', 'muted', 'mute',
'delay', 'stop', 'pause', 'seek', 'duration', 'status', 'topic'
];
if (!msg.topic) {
msg.topic = 'cast';
}
const data = {};
for (const attr of attrs) {
// Value === 'undefined' || value === null --> value == null
if ((config[attr] != null) && (config[attr] !== '')) { // eslint-disable-line
data[attr] = config[attr];
}
if ((msg[attr] != null) && (msg[attr] !== '')) { // eslint-disable-line
data[attr] = msg[attr];
}
}
if (typeof msg.payload === 'object') {
for (const attr of attrs) {
if ((msg.payload[attr] != null) && (msg.payload[attr] !== '')) { // eslint-disable-line
data[attr] = msg.payload[attr];
}
}
} else if (typeof msg.payload === 'string' && msg.payload.trim() !== '') {
if (data.contentType && !msg.url && !config.url && !data.media) {
data.url = msg.payload;
} else {
data.message = msg.payload;
}
}
//-------------------------------------------------------------------
if (typeof data.ip === 'undefined') {
this.status({
fill: 'red',
shape: 'dot',
text: 'No IP given!'
});
done('configuration error: IP is missing!', msg);
return;
}
if (typeof data.language === 'undefined' || data.language === '') {
data.language = 'en';
}
if (typeof data.volume !== 'undefined') {
if (!isNaN(data.volume) &&
data.volume !== '') {
data.volume = parseInt(data.volume) / 100;
} else if ((data.volume.toLowerCase() === 'max') ||
(data.volume.toLowerCase() === 'full') ||
(data.volume.toLowerCase() === 'loud')) {
data.volume = 100;
} else if ((data.volume.toLowerCase() === 'min') ||
(data.volume.toLowerCase() === 'mute') ||
(data.volume.toLowerCase() === 'muted')) {
data.volume = 0;
} else if (data.volume !== 0) {
delete data.volume;
}
}
if (typeof data.mute !== 'undefined') {
data.muted = data.mute;
delete data.mute;
}
if (typeof data.lowerVolumeLimit !== 'undefined' &&
!isNaN(data.lowerVolumeLimit) &&
data.lowerVolumeLimit !== '') {
data.lowerVolumeLimit = parseFloat(data.lowerVolumeLimit);
if (data.lowerVolumeLimit > 1) {
data.lowerVolumeLimit = data.lowerVolumeLimit / 100;
}
} else {
delete data.lowerVolumeLimit;
}
if (typeof data.upperVolumeLimit !== 'undefined' &&
!isNaN(data.upperVolumeLimit) &&
data.upperVolumeLimit !== '') {
data.upperVolumeLimit = parseFloat(data.upperVolumeLimit);
if (data.upperVolumeLimit > 1) {
data.upperVolumeLimit = data.upperVolumeLimit / 100;
}
} else {
delete data.upperVolumeLimit;
}
if (typeof data.delay !== 'undefined' && !isNaN(data.delay) && data.delay !== '') {
data.delay = parseInt(data.delay);
} else {
data.delay = 250;
}
if (typeof data.url !== 'undefined' &&
data.url != null) { // eslint-disable-line
// this.debug('initialize playing url \'' + util.inspect(data, { colors: true, compact: 10, breakLength: Infinity }) + '\'');
if (typeof data.contentType !== 'string' || data.contentType === '') {
data.contentType = getContentType(data, this);
}
data.media = {
contentId: data.url,
contentType: data.contentType
};
} else if (typeof data.urlList !== 'undefined') {
if (typeof data.urlList === 'string') {
data.urlList = data.urlList.split(/,|;\r\n/).filter((el) => el);
}
const listSize = data.urlList.length;
if (listSize > 0) {
// If is a list of files
this.debug('initialize playing queue=\'' + listSize + '\'');
data.media = {};
data.media.mediaList = [];
for (let i = 0; i < listSize; i++) {
const item = data.urlList[i];
const contentType = getContentType(data, this, item);
const mediaItem = {
autoplay: true,
preloadTime: listSize,
startTime: i + 1,
activeTrackIds: [],
playbackDuration: 2,
media: {
contentId: item,
contentType
}
};
data.media.mediaList.push(mediaItem);
}
}
}
if (typeof data.media === 'object' &&
data.media != null) { // eslint-disable-line
if (typeof data.contentType !== 'undefined' &&
data.contentType != null) { // eslint-disable-line
data.media.contentType = data.contentType;
}
if (typeof data.streamType !== 'undefined' &&
data.streamType != null) { // eslint-disable-line
data.media.streamType = data.streamType;
}
if (typeof data.duration !== 'undefined' &&
!isNaN(data.duration)) {
data.media.duration = data.duration;
}
if (typeof data.imageUrl !== 'undefined' &&
data.imageUrl != null) { // eslint-disable-line
data.media.imageUrl = data.imageUrl;
}
if (typeof data.contentTitle !== 'undefined' &&
data.contentTitle != null) { // eslint-disable-line
data.media.contentTitle = data.contentTitle;
}
}
try {
msg.data = data;
this.debug(util.inspect(data, { colors: true, compact: 10, breakLength: Infinity }));
if (data.media) {
this.debug('initialize playing');
this.status({
fill: 'green',
shape: 'dot',
text: 'play (' + data.contentType + ') on ' + data.ip + ' [url]'
});
doCast(this, data.media, data, (res, data2) => {
msg.payload = res;
if (data2 && data2.message) {
setTimeout(data3 => {
this.status({
fill: 'green',
shape: 'ring',
text: 'play message on ' + data3.ip
});
getSpeechUrl(data3.message, data3.language, data3, sres => {
msg.speechResult = sres;
this.status({
fill: 'green',
shape: 'dot',
text: 'ok'
});
msg.type = 'speechResult';
send(msg);
}, msg);
}, data2.delay, data2);
done();
return null;
}
this.status({
fill: 'green',
shape: 'dot',
text: 'ok'
});
msg.type = 'outResult';
send(msg);
}, (errMsg) => {
done(errMsg);
}, msg);
done();
return null;
}
if (data.message) {
this.debug('initialize getting tts');
this.status({
fill: 'green',
shape: 'ring',
text: 'play message on ' + data.ip
});
getSpeechUrl(this, data.message, data.language, data, sres => {
msg.payload = sres;
this.status({
fill: 'green',
shape: 'dot',
text: 'ok'
});
msg.type = 'messageDirect';
send(msg);
}, msg);
done();
return null;
}
this.debug('only sending unspecified request');
this.status({
fill: 'yellow',
shape: 'dot',
text: 'connect to ' + data.ip
});
doCast(this, null, data, status => {
this.debug('status = ' + util.inspect(status, { colors: true, compact: 10, breakLength: Infinity }));
msg.payload = status;
this.status({
fill: 'green',
shape: 'dot',
text: 'ok'
});
msg.type = 'castCommand';
this.send(msg);
}, msg);
} catch (err) {
errorHandler(this, err, msg, 'Exception occurred on cast media to output', 'internal error');
}
done();
return null;
});
discoverIpAddresses('googlecast', (ipaddresses) => {
RED.httpAdmin.get('/ipaddresses', (req, res) => {
res.json(ipaddresses);
});
});
}
function discoverIpAddresses(serviceType, discoveryCallback) {
const ipaddresses = [];
const bonjour = require('bonjour')();
bonjour.find({
type: serviceType
}, (service) => {
service.addresses.forEach((ip) => {
if (ip.split('.').length === 4) {
ipaddresses.push({
ip: ip,
port: service.port,
label: service.txt.md
});
}
});
// delay for all services to be discovered
if (discoveryCallback) {
setTimeout(() => {
discoveryCallback(ipaddresses);
}, 2000);
}
});
}
RED.nodes.registerType('cast-to-client', CastNode);
};