Custom external media player

This commit is contained in:
Mathias Rasmussen
2016-06-27 22:11:05 +02:00
parent 1605d23509
commit 6c68645b0f
9 changed files with 126 additions and 88 deletions

View File

@@ -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)
})
}

View File

@@ -13,16 +13,13 @@ var menu = require('./menu')
var powerSaveBlocker = require('./power-save-blocker') var powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell') var shell = require('./shell')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var vlc = require('./vlc') var externalPlayer = require('./external-player')
var windows = require('./windows') var windows = require('./windows')
var thumbar = require('./thumbar') var thumbar = require('./thumbar')
// Messages from the main process, to be sent once the WebTorrent process starts // Messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = [] var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess
function init () { function init () {
var ipc = electron.ipcMain var ipc = electron.ipcMain
@@ -105,52 +102,17 @@ function init () {
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args)) ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
/** /**
* VLC * External Media Player
* TODO: Move most of this code to vlc.js
*/ */
ipc.on('checkForVLC', function (e) { ipc.on('checkForExternalPlayer', function (e, path) {
vlc.checkForVLC(function (isInstalled) { externalPlayer.checkInstall(path, function (isInstalled) {
windows.main.send('checkForVLC', isInstalled) windows.main.send('checkForExternalPlayer', isInstalled)
}) })
}) })
ipc.on('vlcPlay', function (e, url) { ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url] ipc.on('quitExternalPlayer', () => externalPlayer.kill())
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
})
// Capture all events // Capture all events
var oldEmit = ipc.emit var oldEmit = ipc.emit

View File

@@ -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))
})
}

View File

@@ -22,12 +22,12 @@ module.exports = class MediaController {
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
state.playing.result = 'error' state.playing.result = 'error'
state.playing.location = 'error' state.playing.location = 'error'
ipcRenderer.send('checkForVLC') ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForVLC', function (e, isInstalled) { ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
state.modal = { state.modal = {
id: 'unsupported-media-modal', id: 'unsupported-media-modal',
error: error, error: error,
vlcInstalled: isInstalled externalPlayerInstalled: isInstalled
} }
}) })
} }
@@ -42,15 +42,16 @@ module.exports = class MediaController {
this.state.playing.mouseStationarySince = new Date().getTime() this.state.playing.mouseStationarySince = new Date().getTime()
} }
vlcPlay () { openExternalPlayer () {
ipcRenderer.send('vlcPlay', this.state.server.localURL) var state = this.state
this.state.playing.location = 'vlc' ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL)
state.playing.location = 'external'
} }
vlcNotFound () { externalPlayerNotFound () {
var modal = this.state.modal var modal = this.state.modal
if (modal && modal.id === 'unsupported-media-modal') { if (modal && modal.id === 'unsupported-media-modal') {
modal.vlcNotFound = true modal.externalPlayerNotFound = true
} }
} }
} }

View File

@@ -259,8 +259,8 @@ module.exports = class PlaybackController {
if (isCasting(state)) { if (isCasting(state)) {
Cast.stop() Cast.stop()
} }
if (state.playing.location === 'vlc') { if (state.playing.location === 'external') {
ipcRenderer.send('vlcQuit') ipcRenderer.send('quitExternalPlayer')
} }
// Save volume (this session only, not in state.saved) // Save volume (this session only, not in state.saved)

View File

@@ -192,8 +192,8 @@ const dispatchHandlers = {
'mediaSuccess': () => controllers.media.mediaSuccess(), 'mediaSuccess': () => controllers.media.mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(), 'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(), 'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
'vlcPlay': () => controllers.media.vlcPlay(), 'openExternalPlayer': () => controllers.media.openExternalPlayer(),
'vlcNotFound': () => controllers.media.vlcNotFound(), 'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
// Remote casting: Chromecast, Airplay, etc // Remote casting: Chromecast, Airplay, etc
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType), 'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),

View File

@@ -2,6 +2,7 @@ const React = require('react')
const Bitfield = require('bitfield') const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes') const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill') const zeroFill = require('zero-fill')
const path = require('path')
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const {dispatch, dispatcher} = require('../lib/dispatcher') const {dispatch, dispatcher} = require('../lib/dispatcher')
@@ -281,9 +282,12 @@ function renderCastScreen (state) {
castIcon = 'tv' castIcon = 'tv'
castType = 'DLNA' castType = 'DLNA'
isCast = true 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' castIcon = 'tv'
castType = 'VLC' castType = playerName
isCast = false isCast = false
} else if (state.playing.location === 'error') { } else if (state.playing.location === 'error') {
castIcon = 'error_outline' castIcon = 'error_outline'

View File

@@ -1,6 +1,7 @@
const React = require('react') const React = require('react')
const remote = require('electron').remote const remote = require('electron').remote
const dialog = remote.dialog const dialog = remote.dialog
const path = require('path')
const {dispatch} = require('../lib/dispatcher') const {dispatch} = require('../lib/dispatcher')
@@ -22,7 +23,8 @@ function renderGeneralSection (state) {
description: '', description: '',
icon: 'settings' 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 || '<VLC>', // 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. // Renders a prefs section.
// - definition should be {icon, title, description} // - definition should be {icon, title, description}
// - controls should be an array of vdom elements // - controls should be an array of vdom elements

View File

@@ -1,5 +1,6 @@
const React = require('react') const React = require('react')
const electron = require('electron') const electron = require('electron')
const path = require('path')
const {dispatcher} = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
@@ -10,11 +11,15 @@ module.exports = class UnsupportedMediaModal extends React.Component {
var message = (err && err.getMessage) var message = (err && err.getMessage)
? err.getMessage() ? err.getMessage()
: err : err
var actionButton = state.modal.vlcInstalled var playerPath = state.saved.prefs.externalPlayerPath
? (<button className='button-raised' onClick={dispatcher('vlcPlay')}>Play in VLC</button>) var playerName = playerPath
? path.basename(playerPath).split('.')[0]
: 'VLC'
var actionButton = state.modal.externalPlayerInstalled
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>) : (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
var vlcMessage = state.modal.vlcNotFound var playerMessage = state.modal.externalPlayerNotFound
? 'Couldn\'t run VLC. Please make sure it\'s installed.' ? 'Couldn\'t run external player. Please make sure it\'s installed.'
: '' : ''
return ( return (
<div> <div>
@@ -24,7 +29,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button> <button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
{actionButton} {actionButton}
</p> </p>
<p className='error-text'>{vlcMessage}</p> <p className='error-text'>{playerMessage}</p>
</div> </div>
) )
} }
@@ -34,6 +39,6 @@ module.exports = class UnsupportedMediaModal extends React.Component {
// TODO: dcposch send a dispatch rather than modifying state directly // TODO: dcposch send a dispatch rather than modifying state directly
var state = this.props.state 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
} }
} }