From 6c68645b0f1e64dfcbfecd803d9c2bcb08a0f2dd Mon Sep 17 00:00:00 2001 From: Mathias Rasmussen Date: Mon, 27 Jun 2016 22:11:05 +0200 Subject: [PATCH] Custom external media player --- src/main/external-player.js | 65 +++++++++++++++++++ src/main/ipc.js | 52 ++------------- src/main/vlc.js | 22 ------- src/renderer/controllers/media-controller.js | 17 ++--- .../controllers/playback-controller.js | 4 +- src/renderer/main.js | 4 +- src/renderer/views/player.js | 8 ++- src/renderer/views/preferences.js | 25 ++++++- src/renderer/views/unsupported-media-modal.js | 17 +++-- 9 files changed, 126 insertions(+), 88 deletions(-) create mode 100644 src/main/external-player.js delete mode 100644 src/main/vlc.js diff --git a/src/main/external-player.js b/src/main/external-player.js new file mode 100644 index 00000000..5870712d --- /dev/null +++ b/src/main/external-player.js @@ -0,0 +1,65 @@ +module.exports = { + spawn, + kill, + checkInstall +} + +var cp = require('child_process') +var vlcCommand = require('vlc-command') + +var log = require('./log') +var windows = require('./windows') + +// holds a ChildProcess while we're playing a video in an external player, null otherwise +var proc + +function checkInstall (path, cb) { + // check for VLC if external player has not been specified by the user + // otherwise assume the player is installed + if (path == null) return vlcCommand((err) => cb(!err)) + process.nextTick(() => cb(true)) +} + +function spawn (path, url) { + if (path != null) return spawnExternal(path, [url]) + + // Try to find and use VLC if external player is not specified + vlcCommand(function (err, vlcPath) { + if (err) return windows.main.dispatch('externalPlayerNotFound') + var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url] + spawnExternal(vlcPath, args) + }) +} + +function kill () { + if (!proc) return + log('Killing external player, pid ' + proc.pid) + proc.kill('SIGKILL') // kill -9 + proc = null +} + +function spawnExternal (path, args) { + log('Running external media player:', path + ' ' + args.join(' ')) + + proc = cp.spawn(path, args) + + // If it works, close the modal after a second + var closeModalTimeout = setTimeout(() => + windows.main.dispatch('exitModal'), 1000) + + proc.on('close', function (code) { + clearTimeout(closeModalTimeout) + if (!proc) return // Killed + log('External player exited with code ', code) + if (code === 0) { + windows.main.dispatch('backToList') + } else { + windows.main.dispatch('externalPlayerNotFound') + } + proc = null + }) + + proc.on('error', function (e) { + log('External player error', e) + }) +} diff --git a/src/main/ipc.js b/src/main/ipc.js index 99b76a6a..b89d69cc 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -13,16 +13,13 @@ var menu = require('./menu') var powerSaveBlocker = require('./power-save-blocker') var shell = require('./shell') var shortcuts = require('./shortcuts') -var vlc = require('./vlc') +var externalPlayer = require('./external-player') var windows = require('./windows') var thumbar = require('./thumbar') // Messages from the main process, to be sent once the WebTorrent process starts var messageQueueMainToWebTorrent = [] -// holds a ChildProcess while we're playing a video in VLC, null otherwise -var vlcProcess - function init () { var ipc = electron.ipcMain @@ -105,52 +102,17 @@ function init () { ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args)) /** - * VLC - * TODO: Move most of this code to vlc.js + * External Media Player */ - ipc.on('checkForVLC', function (e) { - vlc.checkForVLC(function (isInstalled) { - windows.main.send('checkForVLC', isInstalled) + ipc.on('checkForExternalPlayer', function (e, path) { + externalPlayer.checkInstall(path, function (isInstalled) { + windows.main.send('checkForExternalPlayer', isInstalled) }) }) - ipc.on('vlcPlay', function (e, url) { - var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url] - log('Running vlc ' + args.join(' ')) - - vlc.spawn(args, function (err, proc) { - if (err) return windows.main.dispatch('vlcNotFound') - vlcProcess = proc - - // If it works, close the modal after a second - var closeModalTimeout = setTimeout(() => - windows.main.dispatch('exitModal'), 1000) - - vlcProcess.on('close', function (code) { - clearTimeout(closeModalTimeout) - if (!vlcProcess) return // Killed - log('VLC exited with code ', code) - if (code === 0) { - windows.main.dispatch('backToList') - } else { - windows.main.dispatch('vlcNotFound') - } - vlcProcess = null - }) - - vlcProcess.on('error', function (e) { - log('VLC error', e) - }) - }) - }) - - ipc.on('vlcQuit', function () { - if (!vlcProcess) return - log('Killing VLC, pid ' + vlcProcess.pid) - vlcProcess.kill('SIGKILL') // kill -9 - vlcProcess = null - }) + ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args)) + ipc.on('quitExternalPlayer', () => externalPlayer.kill()) // Capture all events var oldEmit = ipc.emit diff --git a/src/main/vlc.js b/src/main/vlc.js deleted file mode 100644 index b2ff659b..00000000 --- a/src/main/vlc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - checkForVLC, - spawn -} - -var cp = require('child_process') -var vlcCommand = require('vlc-command') - -// Finds if VLC is installed on Mac, Windows, or Linux. -// Calls back with true or false: whether VLC was detected -function checkForVLC (cb) { - vlcCommand((err) => cb(!err)) -} - -// Spawns VLC with child_process.spawn() to return a ChildProcess object -// Calls back with (err, childProcess) -function spawn (args, cb) { - vlcCommand(function (err, vlcPath) { - if (err) return cb(err) - cb(null, cp.spawn(vlcPath, args)) - }) -} diff --git a/src/renderer/controllers/media-controller.js b/src/renderer/controllers/media-controller.js index 785b168b..b6010b8b 100644 --- a/src/renderer/controllers/media-controller.js +++ b/src/renderer/controllers/media-controller.js @@ -22,12 +22,12 @@ module.exports = class MediaController { if (state.location.url() === 'player') { state.playing.result = 'error' state.playing.location = 'error' - ipcRenderer.send('checkForVLC') - ipcRenderer.once('checkForVLC', function (e, isInstalled) { + ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath) + ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) { state.modal = { id: 'unsupported-media-modal', error: error, - vlcInstalled: isInstalled + externalPlayerInstalled: isInstalled } }) } @@ -42,15 +42,16 @@ module.exports = class MediaController { this.state.playing.mouseStationarySince = new Date().getTime() } - vlcPlay () { - ipcRenderer.send('vlcPlay', this.state.server.localURL) - this.state.playing.location = 'vlc' + openExternalPlayer () { + var state = this.state + ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL) + state.playing.location = 'external' } - vlcNotFound () { + externalPlayerNotFound () { var modal = this.state.modal if (modal && modal.id === 'unsupported-media-modal') { - modal.vlcNotFound = true + modal.externalPlayerNotFound = true } } } diff --git a/src/renderer/controllers/playback-controller.js b/src/renderer/controllers/playback-controller.js index 8a84cefa..f7013658 100644 --- a/src/renderer/controllers/playback-controller.js +++ b/src/renderer/controllers/playback-controller.js @@ -259,8 +259,8 @@ module.exports = class PlaybackController { if (isCasting(state)) { Cast.stop() } - if (state.playing.location === 'vlc') { - ipcRenderer.send('vlcQuit') + if (state.playing.location === 'external') { + ipcRenderer.send('quitExternalPlayer') } // Save volume (this session only, not in state.saved) diff --git a/src/renderer/main.js b/src/renderer/main.js index d83295b9..0ab4b78b 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -192,8 +192,8 @@ const dispatchHandlers = { 'mediaSuccess': () => controllers.media.mediaSuccess(), 'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(), 'mediaMouseMoved': () => controllers.media.mediaMouseMoved(), - 'vlcPlay': () => controllers.media.vlcPlay(), - 'vlcNotFound': () => controllers.media.vlcNotFound(), + 'openExternalPlayer': () => controllers.media.openExternalPlayer(), + 'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(), // Remote casting: Chromecast, Airplay, etc 'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType), diff --git a/src/renderer/views/player.js b/src/renderer/views/player.js index 09f2df51..ee49fff9 100644 --- a/src/renderer/views/player.js +++ b/src/renderer/views/player.js @@ -2,6 +2,7 @@ const React = require('react') const Bitfield = require('bitfield') const prettyBytes = require('prettier-bytes') const zeroFill = require('zero-fill') +const path = require('path') const TorrentSummary = require('../lib/torrent-summary') const {dispatch, dispatcher} = require('../lib/dispatcher') @@ -281,9 +282,12 @@ function renderCastScreen (state) { castIcon = 'tv' castType = 'DLNA' isCast = true - } else if (state.playing.location === 'vlc') { + } else if (state.playing.location === 'external') { + // TODO: get the player name in a more reliable way + var playerPath = state.saved.prefs.externalPlayerPath + var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC' castIcon = 'tv' - castType = 'VLC' + castType = playerName isCast = false } else if (state.playing.location === 'error') { castIcon = 'error_outline' diff --git a/src/renderer/views/preferences.js b/src/renderer/views/preferences.js index 3caaac93..508718e7 100644 --- a/src/renderer/views/preferences.js +++ b/src/renderer/views/preferences.js @@ -1,6 +1,7 @@ const React = require('react') const remote = require('electron').remote const dialog = remote.dialog +const path = require('path') const {dispatch} = require('../lib/dispatcher') @@ -22,7 +23,8 @@ function renderGeneralSection (state) { description: '', icon: 'settings' }, [ - renderDownloadDirSelector(state) + renderDownloadDirSelector(state), + renderExternalPlayerSelector(state) ]) } @@ -43,6 +45,27 @@ function renderDownloadDirSelector (state) { }) } +function renderExternalPlayerSelector (state) { + return renderFileSelector({ + label: 'External Media Player', + description: 'Progam that will be used to play media externally', + property: 'externalPlayerPath', + options: { + title: 'Select media player executable', + properties: [ 'openFile' ] + } + }, + state.unsaved.prefs.externalPlayerPath || '', // TODO: should we get/store vlc path instead? + function (filePath) { + if (path.extname(filePath) === '.app') { + // Get executable in packaged mac app + var name = path.basename(filePath, '.app') + filePath += '/Contents/MacOS/' + name + } + setStateValue('externalPlayerPath', filePath) + }) +} + // Renders a prefs section. // - definition should be {icon, title, description} // - controls should be an array of vdom elements diff --git a/src/renderer/views/unsupported-media-modal.js b/src/renderer/views/unsupported-media-modal.js index 52654c32..ad379b62 100644 --- a/src/renderer/views/unsupported-media-modal.js +++ b/src/renderer/views/unsupported-media-modal.js @@ -1,5 +1,6 @@ const React = require('react') const electron = require('electron') +const path = require('path') const {dispatcher} = require('../lib/dispatcher') @@ -10,11 +11,15 @@ module.exports = class UnsupportedMediaModal extends React.Component { var message = (err && err.getMessage) ? err.getMessage() : err - var actionButton = state.modal.vlcInstalled - ? () + var playerPath = state.saved.prefs.externalPlayerPath + var playerName = playerPath + ? path.basename(playerPath).split('.')[0] + : 'VLC' + var actionButton = state.modal.externalPlayerInstalled + ? () : () - var vlcMessage = state.modal.vlcNotFound - ? 'Couldn\'t run VLC. Please make sure it\'s installed.' + var playerMessage = state.modal.externalPlayerNotFound + ? 'Couldn\'t run external player. Please make sure it\'s installed.' : '' return (
@@ -24,7 +29,7 @@ module.exports = class UnsupportedMediaModal extends React.Component { {actionButton}

-

{vlcMessage}

+

{playerMessage}

) } @@ -34,6 +39,6 @@ module.exports = class UnsupportedMediaModal extends React.Component { // TODO: dcposch send a dispatch rather than modifying state directly var state = this.props.state - state.modal.vlcInstalled = true // Assume they'll install it successfully + state.modal.externalPlayerInstalled = true // Assume they'll install it successfully } }