media-player-controller
Version:
Spawn media player app and control playback
524 lines (424 loc) • 9.99 kB
JavaScript
const noop = () => {};
const helper = require('../helper');
var previous;
var httpOpts = { xml: true };
var playerData =
{
'time-pos': 'time',
'volume': 'volume',
'duration': 'length',
'pause': 'state',
'speed': 'rate',
'eof-reached': 'state'
};
/* Prevent emitting 'eof-reached' at launch */
var launched = false;
var loading = false;
var streams;
var currStreams;
module.exports =
{
init: function()
{
previous = {};
streams = {};
currStreams = {};
launched = false;
loading = false;
this._intervalEnabled = true;
this._getPlayerData();
},
_connectType: 'web',
_forceEnglish: true,
_getPlayerData: function()
{
const onTimeout = () =>
{
this._getDataTimeout = null;
this._getPlayerData();
}
httpOpts.path = '/requests/status.xml';
helper.httpRequest(httpOpts, (err, result) =>
{
var time = 1000;
if(!err && this._intervalEnabled)
{
this._parseRequest(result);
var isPlaying = (result.state === 'playing');
time = this._getProbeTime(isPlaying, result.time, result.rate);
}
if(this._intervalEnabled)
this._getDataTimeout = setTimeout(() => onTimeout(), time);
});
},
_parseRequest: function(result)
{
for(var key in playerData)
{
var value = result[playerData[key]];
switch(key)
{
case 'pause':
value = (value === 'paused');
break;
case 'eof-reached':
value = (launched && !loading && value === 'stopped');
break;
case 'time-pos':
case 'duration':
value = parseInt(value);
if(value < 0)
continue;
else if(!launched && key === 'time-pos' && value > 0)
launched = true;
break;
case 'volume':
value = Math.round(value / 2.56) / 100;
if(value < 0)
continue;
break;
case 'speed':
value = Number(parseFloat(value).toFixed(2));
if(previous.speed && previous.speed != value)
this._onSpeedChanged(value);
break;
default:
if(value == 'true')
value = true;
else if(value == 'false')
value = false;
break;
}
if(
previous.hasOwnProperty(key)
&& previous[key] === value
)
continue;
previous[key] = value;
this.emit('playback', { name: key, value: value });
}
previous.repeat = (result.repeat === true || result.repeat === 'true');
previous.fullscreen = (result.fullscreen > 0);
if(result.currentplid > 0)
previous.id = result.currentplid;
if(
!result.information
|| !result.information.category
|| !result.information.category.length
|| (streams.count
&& streams.count === result.information.category.length)
)
return;
streams = {
count: result.information.category.length,
video: [],
audio: [],
subs: []
};
for(var cat of result.information.category)
{
if(!cat['$'].name.startsWith('Stream'))
continue;
var index = cat['$'].name.split(' ')[1];
var streamType = cat.info.find(inf => inf['$'].name === 'Type');
switch(streamType._)
{
case 'Video':
streams.video.push(index);
break;
case 'Audio':
streams.audio.push(index);
break;
case 'Subtitle':
streams.subs.push(index);
break;
default:
break;
}
}
for(var type in streams)
{
if(type === 'count')
continue;
if(streams[type].length > 1)
streams[type].sort();
}
this.emit('streams-changed');
},
cleanup: function()
{
this._intervalEnabled = false;
if(this._getDataTimeout)
clearTimeout(this._getDataTimeout);
},
_getSpawnArgs: function(opts)
{
/* VLC requires password for web interface */
httpOpts.pass = opts.httpPass || 'vlc';
httpOpts.port = opts.httpPort;
var presetArgs = [
'--no-play-and-exit',
'--no-qt-recentplay',
'--qt-continue', '0',
'--image-duration', '-1',
'--extraintf', 'http',
'--http-port', httpOpts.port,
'--http-password', httpOpts.pass,
opts.media
];
return [ ...opts.args, ...presetArgs ];
},
command: function(params, cb)
{
cb = cb || noop;
var command = null;
if(!Array.isArray(params))
return cb(new Error('No command parameters array!'));
for(var cmd of params)
{
if(!command)
command = cmd;
else
command += `&${cmd}`;
}
httpOpts.path = '/requests/status.xml?command=' + command;
helper.httpRequest(httpOpts, (err, result) =>
{
if(err) return cb(err);
httpOpts.path = '/requests/status.xml';
helper.httpRequest(httpOpts, (err, result) =>
{
if(err) return cb(err);
this._debug(command);
this._parseRequest(result);
cb(null);
});
});
},
play: function(cb)
{
cb = cb || noop;
if(previous.pause)
this.cyclePause(cb);
else
cb(null);
},
pause: function(cb)
{
cb = cb || noop;
if(!previous.pause)
this.cyclePause(cb);
else
cb(null);
},
cyclePause: function(cb)
{
cb = cb || noop;
this.command(['pl_pause'], cb);
},
_load: function(media, cb)
{
cb = cb || noop;
previous.duration = null;
loading = true;
var changeTimeout;
const onStreamsChange = function()
{
if(changeTimeout)
{
clearTimeout(changeTimeout);
changeTimeout = null;
}
loading = false;
cb(null);
}
var delId = previous.id;
this.command(['in_play', `input=${media}`], (err) =>
{
if(err) return cb(err);
currStreams = {};
this.command(['pl_delete', `id=${delId}`], (err) =>
{
if(err) return cb(err);
changeTimeout = setTimeout(() =>
{
changeTimeout = null;
loading = false;
this.removeListener('streams-changed', onStreamsChange);
cb(new Error('Streams change timeout'));
}, 15000);
this.once('streams-changed', onStreamsChange);
streams = {};
httpOpts.path = '/requests/status.xml';
helper.httpRequest(httpOpts, (err, result) =>
{
/* Ignore this req and wait for next */
if(err) return;
this._parseRequest(result);
});
});
});
},
seek: function(position, cb)
{
cb = cb || noop;
position = (position > 0) ? parseInt(position) : 0;
this.command(['seek', `val=${position}`], cb);
},
setVolume: function(value, cb)
{
cb = cb || noop;
value = (value > 0) ? parseInt(value * 256) : 0;
this.command(['volume', `val=${value}`], (err) =>
{
if(err) return cb(err);
/* Setting volume on VLC sometimes requires another refresh */
httpOpts.path = '/requests/status.xml';
helper.httpRequest(httpOpts, (err, result) =>
{
if(err) return cb(err);
this._parseRequest(result);
cb(null);
});
});
},
setSpeed: function(value, cb)
{
cb = cb || noop;
value = (value > 0) ? value : 1;
this.command(['rate', `val=${value}`], cb);
},
setRepeat: function(isEnabled, cb)
{
cb = cb || noop;
switch(isEnabled)
{
case true:
case 'inf':
case 'yes':
case 'on':
isEnabled = true;
break;
default:
isEnabled = false;
break;
}
if(
isEnabled && previous.repeat
|| !isEnabled && !previous.repeat
)
return cb(null);
this.command(['pl_repeat'], cb);
},
cycleVideo: function(cb)
{
cb = cb || noop;
if(!streams.video || !streams.video.length)
return cb(new Error('No video tracks'));
var value = this._cycleStream('video');
this.command(['video_track', `val=${value}`], cb);
},
cycleAudio: function(cb)
{
cb = cb || noop;
if(!streams.audio || !streams.audio.length)
return cb(new Error('No audio tracks'));
var value = this._cycleStream('audio');
this.command(['audio_track', `val=${value}`], cb);
},
cycleSubs: function(cb)
{
cb = cb || noop;
if(!streams.subs || !streams.subs.length)
return cb(new Error('No subtitles tracks'));
var value = this._cycleStream('subs');
this.command(['subtitle_track', `val=${value}`], cb);
},
_cycleStream: function(streamType)
{
if(!currStreams[streamType])
currStreams[streamType] = 0;
currStreams[streamType]++;
if(currStreams[streamType] > streams[streamType].length - 1)
return currStreams[streamType] = -1;
return streams[streamType][currStreams[streamType]];
},
addSubs: function(subsPath, cb)
{
cb = cb || noop;
if(!subsPath)
return cb(new Error('No subtitles path specified'));
var initialSubsLength = streams.subs.length;
const setAddedSubs = () =>
{
if(this.addSubsTimeout)
{
clearTimeout(this.addSubsTimeout);
this.addSubsTimeout = null;
}
if(!streams.subs.length)
return cb(new Error('No subtitles tracks'));
else if(initialSubsLength === streams.subs.length)
return cb(new Error('Subtitles track was not added'));
var lastSubs = Math.max.apply(null, streams.subs);
this.command(['subtitle_track', `val=${lastSubs}`], cb);
}
this.addSubsTimeout = setTimeout(() =>
{
this.addSubsTimeout = null;
this.removeListener('streams-changed', setAddedSubs);
cb(new Error('Add subtitles timeout'));
}, 5000);
this.once('streams-changed', setAddedSubs);
this.command(['addsubtitle', `val=${subsPath}`], (err) =>
{
/* When no error wait for "streams-changed" event */
if(!err) return;
this.removeListener('streams-changed', setAddedSubs);
if(this.addSubsTimeout)
{
clearTimeout(this.addSubsTimeout);
this.addSubsTimeout = null;
}
cb(err);
});
},
setFullscreen: function(isEnabled, cb)
{
cb = cb || noop;
switch(isEnabled)
{
case false:
case 'no':
case 'off':
isEnabled = false;
break;
default:
isEnabled = true;
break;
}
if(
isEnabled && previous.fullscreen
|| !isEnabled && !previous.fullscreen
)
return cb(null);
this.command(['fullscreen'], cb);
},
cycleFullscreen: function(cb)
{
cb = cb || noop;
this.command(['fullscreen'], cb);
},
keepOpen: function(value, cb)
{
cb = cb || noop;
/* VLC always uses keep open */
cb(new Error('VLC does not support keep open command'));
},
_playerQuit: function(cb)
{
cb = cb || noop;
this.cleanup();
cb(new Error('VLC does not support remote quit command'));
}
}