Custom external media player
This commit is contained in:
65
src/main/external-player.js
Normal file
65
src/main/external-player.js
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user