From bde5dc14c35be18967864b31dadec2666e330f71 Mon Sep 17 00:00:00 2001 From: DC Date: Tue, 19 Apr 2016 01:02:47 -0700 Subject: [PATCH] Play unsupported files in VLC --- main/ipc.js | 60 ++++++++++++++++++++++- renderer/index.css | 4 ++ renderer/index.js | 36 +++++++++++--- renderer/views/app.js | 3 +- renderer/views/player.js | 57 ++++++++++++++++----- renderer/views/unsupported-media-modal.js | 42 ++++++++++++++++ 6 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 renderer/views/unsupported-media-modal.js diff --git a/main/ipc.js b/main/ipc.js index 155e0d60..0d9c95fb 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -2,6 +2,7 @@ module.exports = { init } +var cp = require('child_process') var electron = require('electron') var app = electron.app @@ -16,6 +17,12 @@ var shortcuts = require('./shortcuts') // has to be a number, not a boolean, and undefined throws an error var powerSaveBlockID = 0 +// 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 () { ipcMain.on('ipcReady', function (e) { app.ipcReady = true @@ -24,7 +31,6 @@ function init () { console.timeEnd('init') }) - var messageQueueMainToWebTorrent = [] ipcMain.on('ipcReadyWebTorrent', function (e) { app.ipcReadyWebTorrent = true log('sending %d queued messages from the main win to the webtorrent window', @@ -36,6 +42,7 @@ function init () { }) ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile) + ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles) ipcMain.on('setBounds', function (e, bounds, maximize) { @@ -68,12 +75,14 @@ function init () { }) ipcMain.on('blockPowerSave', blockPowerSave) + ipcMain.on('unblockPowerSave', unblockPowerSave) ipcMain.on('onPlayerOpen', function () { menu.onPlayerOpen() shortcuts.registerPlayerShortcuts() }) + ipcMain.on('onPlayerClose', function () { menu.onPlayerClose() shortcuts.unregisterPlayerShortcuts() @@ -83,6 +92,55 @@ function init () { windows.focusWindow(windows[windowName]) }) + ipcMain.on('vlcVersion', function (e) { + cp.exec('vlc --version', function (e, stdout, stderr) { + var version + if (e) { + version = null + } else { + // Prints several lines, starting with eg: VLC media player 2.7.0 + if (!stdout.startsWith('VLC media player')) version = 'unknown' + else version = stdout.split(' ')[3] + } + windows.main.send('vlcVersion', version) + }) + }) + + ipcMain.on('vlcPlay', function (e, url) { + // TODO: cross-platform VLC detection + var command = 'vlc' + var args = ['--play-and-exit', '--quiet', url] + console.log('Running ' + command + ' ' + args.join(' ')) + + vlcProcess = cp.spawn(command, args) + + // If it works, close the modal after a second + var closeModalTimeout = setTimeout(() => windows.main.send('dispatch', 'exitModal'), 1000) + + vlcProcess.on('close', function (code) { + clearTimeout(closeModalTimeout) + if (!vlcProcess) return // Killed + console.log('VLC exited with code ', code) + if (code === 0) { + windows.main.send('dispatch', 'backToList') + } else { + windows.main.send('dispatch', 'vlcNotFound') + } + vlcProcess = null + }) + + vlcProcess.on('error', function (e) { + console.log('VLC error', e) + }) + }) + + ipcMain.on('vlcQuit', function () { + if (!vlcProcess) return + console.log('Killing VLC, pid ' + vlcProcess.pid) + vlcProcess.kill('SIGKILL') // kill -9 + vlcProcess = null + }) + // Capture all events var oldEmit = ipcMain.emit ipcMain.emit = function (name, e, ...args) { diff --git a/renderer/index.css b/renderer/index.css index c1b151b8..d3e30f51 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -987,3 +987,7 @@ body.drag .app::after { .error-popover .error:last-child { border-bottom: none; } + +.error-text { + color: #c44; +} diff --git a/renderer/index.js b/renderer/index.js index a7544ff9..9fc22451 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -251,6 +251,8 @@ function dispatch (action, ...args) { setDimensions(args[0] /* dimensions */) } if (action === 'backToList') { + // Exit any modals and screens with a back button + state.modal = null while (state.location.hasBack()) state.location.back() // Work around virtual-dom issue: it doesn't expose its redraw function, @@ -302,20 +304,38 @@ function dispatch (action, ...args) { state.playing.isStalled = true } if (action === 'mediaError') { - state.location.back(function () { - onError(new Error('Unsupported file format')) - }) + if (state.location.current().url === 'player') { + state.playing.location = 'error' + ipcRenderer.send('vlcVersion') + ipcRenderer.once('vlcVersion', function (e, version) { + console.log('vlcVersion', version) + state.modal = { + id: 'unsupported-media-modal', + error: args[0], + vlcInstalled: !!version + } + }) + } } if (action === 'mediaTimeUpdate') { state.playing.lastTimeUpdate = new Date().getTime() state.playing.isStalled = false } - if (action === 'toggleFullScreen') { - ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */) - } if (action === 'mediaMouseMoved') { state.playing.mouseStationarySince = new Date().getTime() } + if (action === 'vlcPlay') { + ipcRenderer.send('vlcPlay', state.server.localURL) + state.playing.location = 'vlc' + } + if (action === 'vlcNotFound') { + if (state.modal && state.modal.id === 'unsupported-media-modal') { + state.modal.vlcNotFound = true + } + } + if (action === 'toggleFullScreen') { + ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */) + } if (action === 'exitModal') { state.modal = null } @@ -624,6 +644,7 @@ function startTorrentingSummary (torrentSummary) { torrentID = s.magnetURI || s.infoHash } + console.log('start torrenting %s %s', s.torrentKey, torrentID) ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes) } @@ -908,6 +929,9 @@ function closePlayer (cb) { if (isCasting()) { Cast.close() } + if (state.playing.location === 'vlc') { + ipcRenderer.send('vlcQuit') + } state.window.title = config.APP_WINDOW_TITLE state.playing = State.getDefaultPlayState() state.server = null diff --git a/renderer/views/app.js b/renderer/views/app.js index 18f7c068..06ac70da 100644 --- a/renderer/views/app.js +++ b/renderer/views/app.js @@ -12,7 +12,8 @@ var Views = { } var Modals = { 'open-torrent-address-modal': require('./open-torrent-address-modal'), - 'update-available-modal': require('./update-available-modal') + 'update-available-modal': require('./update-available-modal'), + 'unsupported-media-modal': require('./unsupported-media-modal') } function App (state) { diff --git a/renderer/views/player.js b/renderer/views/player.js index c6cad1ae..8f30dd53 100644 --- a/renderer/views/player.js +++ b/renderer/views/player.js @@ -89,7 +89,8 @@ function renderMedia (state) { onstalling=${dispatcher('mediaStalled')} onerror=${dispatcher('mediaError')} ontimeupdate=${dispatcher('mediaTimeUpdate')} - autoplay> + onencrypted=${dispatcher('mediaEncrypted')} + oncanplay=${onCanPlay}> ${trackTags} ` @@ -120,6 +121,16 @@ function renderMedia (state) { function onEnded (e) { state.playing.isPaused = true } + + function onCanPlay (e) { + var video = e.target + if (video.webkitVideoDecodedByteCount > 0 && + video.webkitAudioDecodedByteCount === 0) { + dispatch('mediaError', 'Audio codec unsupported') + } else { + video.play() + } + } } function renderOverlay (state) { @@ -207,20 +218,33 @@ function renderLoadingSpinner (state) { } function renderCastScreen (state) { - var castIcon, castType + var castIcon, castType, isCast if (state.playing.location.startsWith('chromecast')) { castIcon = 'cast_connected' castType = 'Chromecast' + isCast = true } else if (state.playing.location.startsWith('airplay')) { castIcon = 'airplay' castType = 'AirPlay' + isCast = true } else if (state.playing.location.startsWith('dlna')) { castIcon = 'tv' castType = 'DLNA' + isCast = true + } else if (state.playing.location === 'vlc') { + castIcon = 'tv' + castType = 'VLC' + isCast = false + } else if (state.playing.location === 'error') { + castIcon = 'error_outline' + castType = 'Error' + isCast = false } var isStarting = state.playing.location.endsWith('-pending') - var castStatus = isStarting ? 'Connecting...' : 'Connected' + var castStatus + if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected' + else castStatus = '' // Show a nice title image, if possible var style = { @@ -240,15 +264,26 @@ function renderCastScreen (state) { function renderSubtitlesOptions (state) { var subtitles = state.playing.subtitles - if (subtitles.tracks.length && subtitles.show) { - return hx` - ${subtitles.tracks.map(function (w, i) { - return hx`
  • ${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}${w.label}
  • ` - })} -
  • ${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}None
  • - + if (!subtitles.tracks.length || !subtitles.show) return + + var items = subtitles.tracks.map(function (track) { + return hx` +
  • + ${track.selected ? 'radio_button_checked' : 'radio_button_unchecked'} + ${track.label} +
  • ` - } + }) + + return hx` + + ${items} +
  • + ${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'} + None +
  • + + ` } function renderPlayerControls (state) { diff --git a/renderer/views/unsupported-media-modal.js b/renderer/views/unsupported-media-modal.js new file mode 100644 index 00000000..9cd04e0e --- /dev/null +++ b/renderer/views/unsupported-media-modal.js @@ -0,0 +1,42 @@ +module.exports = UnsupportedMediaModal + +var h = require('virtual-dom/h') +var hyperx = require('hyperx') +var hx = hyperx(h) + +var electron = require('electron') + +var {dispatch, dispatcher} = require('../lib/dispatcher') + +function UnsupportedMediaModal (state) { + var err = state.modal.error + var message = (err && err.getMessage) + ? err.getMessage() + : err + var actionButton = state.modal.vlcInstalled + ? hx`` + : hx`` + var vlcMessage = state.modal.vlcNotFound + ? 'Couldn\'t run VLC. Please make sure it\'s installed.' + : '' + return hx` +
    +

    Sorry, we can't play that file.

    +

    ${message}

    +

    + + ${actionButton} +

    +

    ${vlcMessage}

    +
    + ` + + function onInstall () { + electron.shell.openExternal('http://www.videolan.org/vlc/') + state.modal.vlcInstalled = true // Assume they'll install it successfully + } + + function onPlay () { + dispatch('vlcPlay') + } +}