UNPKG

node-mpv-2

Version:
261 lines (234 loc) 9.98 kB
'use strict'; const net = require('net'); const spawn = require('child_process').spawn; const util = require('../util'); const startStop = { // Starts the MPV player process // // After MPV is started the function listens to the spawned MPV child proecesses' // stdout for see whether it could create and bind the IPC socket or not. // If possible an ipcInterface is created to handle the socket communication // // Observes the properties defined in the observed object // Sets up the event handlers // // mpv_args // arguments that are passed to mpv on start up // // @return // Promise that is resolved when everything went fine or rejected when an // error occured // start: async function (mpv_args = []) { // check if mpv is already running if (this.running) { throw this.errorHandler.errorMessage(6, 'start()'); } // ========================================================= // CHECKING IF THERE IS A MPV INSTANCE RUNNING ON THE SOCKET // ========================================================= // see if there's already an mpv instance running on the specified socket const instance_running = await new Promise((resolve, reject) => { const sock = net.Socket(); sock.connect({path: this.options.socket}, () => { // if this part of the code is reached there is a socket available, but it's not // necessarily one of mpv // send any message to see if there's a MPV instance responding sock.write(JSON.stringify({ 'command': ['get_property', 'mpv-version'] })+'\n'); }) .on('data', (data) => { try{ const res = JSON.parse(data); // if this worked, it's save to say that it's an MPV instance ('data' in res && 'error' in res && res.error === 'success') ? resolve(true) : resolve(false); } // if any error occurred parsing the JSON response, whatever that socket server is, it's not belonging // to any MPV instance catch (err) { resolve(false); } }) // if any error occurs, there's no mpv instance already running on that socket, // in fact there's no socket server available at all .on('error', (err) => { resolve(false); }); }); // these steps are only necessary if a new MPV instance is created, if the module is hooking into an existing // one, there's no need to start a new instance if (!instance_running) { // ========================= // STARTING NEW MPV INSTANCE // ========================= // check if the binary is actually available await util.checkMpvBinary(this.options.binary) // check for the corrrect ipc command const ipcCommand = await util.findIPCCommand(this.options) // check if mpv could be started succesffuly await new Promise ((resolve, reject) => { // add the ipcCommand to the arguments this.mpv_arguments.push(ipcCommand+'='+this.options.socket); // spawns the mpv player this.mpvPlayer = spawn((this.options.binary ? this.options.binary : 'mpv'), this.mpv_arguments.concat(mpv_args)); // callback to listen to stdout + stderr to see, if MPV could bind the IPC socket const stdCallback = (data) => { // stdout/stderr output const output = data.toString(); // "Listening to IPC socket" - message if(output.match(/Listening to IPC (socket|pipe)/)){ // remove the event listener on stdout this.mpvPlayer.stderr.removeAllListeners('data'); this.mpvPlayer.stdout.removeAllListeners('data'); resolve(); } // "Could not bind IPC Socket" - message else if(output.match(/Could not bind IPC (socket|pipe)/)){ // remove the event listener on stdout this.mpvPlayer.stderr.removeAllListeners('data'); this.mpvPlayer.stdout.removeAllListeners('data'); reject(this.errorHandler.errorMessage(4, 'startStop()', [this.options.socket])); } }; // listen to stdout to check if the IPC socket is ready this.mpvPlayer.stdout.on('data', stdCallback); // in some cases on windows, if you pipe your output to a file or another command, the messages that // are usually output via stdout are output via stderr instead. That's why it's required to listen // for the same messages on stderr as well this.mpvPlayer.stderr.on('data', stdCallback); }); // check if mpv went into idle mode and is ready to receive commands await new Promise((resolve, reject) => { // Set up the socket connection this.socket.connect(this.options.socket); // socket to check for the idle event to check if mpv fully loaded and // actually running const observeSocket = net.Socket(); observeSocket.connect({path: this.options.socket}, () => { // send any message to see if there's a MPV instance responding observeSocket.write(JSON.stringify({ 'command': ['get_property', 'idle-active'] })+'\n'); if (this.options.debug || this.options.verbose) console.log('[Node-MPV] sending stimulus'); }).on('data', (data) => { // parse the messages from the socket const messages = data.toString('utf-8').split('\n'); // check every message messages.forEach((message) => { // ignore empty messages if(message.length > 0){ message = JSON.parse(message); // check for the relevant events to see, if mpv has finished loading // idle, idle-active (different between mpv versions) // usually if no special options were added and mpv will go into idle state // file-loaded // for the rare case that somebody would pass files as input via the command line // through the constructor. In that case mpv never goes into idle mode if('event' in message && ['idle','idle-active','file-loaded'].includes(message.event)){ if (this.options.debug || this.options.verbose) console.log('[Node-MPV] idling'); observeSocket.destroy(); resolve(); } // check our stimulus response // Check for our stimulus with idle-active if('data' in message && 'error' in message && message.error === 'success') { if (this.options.debug || this.options.verbose) console.log('[Node-MPV] stimulus received', message.data); observeSocket.destroy(); resolve(); } } }); }); }); } // if the module is hooking into an existing instance of MPV, we still ned to set up the // socket connection for the module else { this.socket.connect(this.options.socket); if(this.options.debug || this.options.verbose){ console.log(`[Node-MPV]: Detected running MPV instance at ${this.options.socket}`); console.log('[Node-MPV]: Hooked into existing MPV instance without starting a new one'); } } // ============================================ // SETTING UP THE PROPERTIES AND EVENT LISTENER // ============================================ // set up the observed properties // sets the Interval to emit the current time position this.observeProperty('time-pos', 0); this.timepositionListenerId = setInterval(async () => { const paused = await this.isPaused().catch(err => { if (this.options.debug) console.log('[Node-MPV] timeposition listener cannot retrieve isPaused', err); if (err.code === 8) { // mpv is not running but the interval was not cleaned out clearInterval(this.timepositionListenerId); return true; } else throw err; // This error is not catcheable, maybe provide a function in options to catch these }); // only emit the time position if there is a file playing and it's not paused if(!paused && this.currentTimePos != null){ this.emit("timeposition", this.currentTimePos); } }, this.options.time_update * 1000); // Observe all the properties defined in the observed JSON object let observePromises = []; util.observedProperties(this.options.audio_only) .forEach((property) => { observePromises.push(this.observeProperty(property)); }); // wait for all observe commands to finish await Promise.all(observePromises); // ### Events ### // events linked to the mpv instance can only be set up, if mpv was started by node js iself // it's not possible for an mpv instance started from a different process if(!instance_running){ // Close Event. Restarts MPV this.mpvPlayer.on('close', (error_code) => this.closeHandler(error_code)); // Output any errors thrown by MPV this.mpvPlayer.on('error', (error) => this.errorHandler(error)); } // if the module is hooking into an existing and running istance of mpv we need an event listener // that is attached directly to the net socket, to clear the interval for the time position else{ this.socket.socket.on('close', () => { clearInterval(this.timepositionListenerId); // it's kind of impossible to tell if an external instance was properly quit or has crashed // that's why both events are emitted this.emit('crashed'); this.emit('quit'); }); } // Handle the JSON messages received from MPV via the ipcInterface this.socket.on('message', (message) => this.messageHandler(message)); // set the running flag this.running = true; // resolve this promise return; }, // Quits the MPV player // // All event handlers are unbound (thus preventing the close event from // restarting MPV // The socket is destroyed quit: async function() { // Clear all the listeners of this module this.mpvPlayer.removeAllListeners('close'); this.mpvPlayer.removeAllListeners('error'); this.mpvPlayer.removeAllListeners('message'); clearInterval(this.timepositionListenerId); // send the quit message to MPV await this.command("quit"); // Quit the socket this.socket.quit(); // unset the running flag this.running = false; return; }, // Shows wheters mpv is running or not // // @return {boolean} isRunning: function() { return this.running; } } module.exports = startStop;