diff --git a/package.json b/package.json index 6c37fad6..ba2889b8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "application-config-path": "^0.1.0", "chromecasts": "^1.8.0", "create-torrent": "^3.22.1", + "dlnacasts": "^0.0.2", "drag-drop": "^2.11.0", "electron-localshortcut": "^0.6.0", "electron-prebuilt": "0.37.3", diff --git a/renderer/index.css b/renderer/index.css index af498d56..f7aece21 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -668,8 +668,7 @@ body.drag .app::after { margin: 0 auto; } -.player-controls .chromecast, -.player-controls .airplay, +.player-controls .device, .player-controls .fullscreen, .player-controls .back { display: block; @@ -682,8 +681,7 @@ body.drag .app::after { float: left; } -.player-controls .chromecast, -.player-controls .airplay, +.player-controls .device, .player-controls .fullscreen { float: right; } @@ -692,14 +690,12 @@ body.drag .app::after { margin-right: 15px; } -.player-controls .chromecast, -.player-controls .airplay { +.player-controls .device { font-size: 18px; /* make the cast icons less huge */ margin-top: 8px !important; } -.player-controls .chromecast.active, -.player-controls .airplay.active { +.player-controls .device.active { color: #9af; } diff --git a/renderer/index.js b/renderer/index.js index b22ee1d6..f7645ea2 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -246,11 +246,8 @@ function dispatch (action, ...args) { if (action === 'openTorrentContextMenu') { openTorrentContextMenu(args[0] /* infoHash */) } - if (action === 'openChromecast') { - lazyLoadCast().open('chromecast') - } - if (action === 'openAirplay') { - lazyLoadCast().open('airplay') + if (action === 'openDevice') { + lazyLoadCast().open(args[0] /* deviceType */) } if (action === 'stopCasting') { lazyLoadCast().stopCasting() diff --git a/renderer/lib/cast.js b/renderer/lib/cast.js index c34171ff..e755855c 100644 --- a/renderer/lib/cast.js +++ b/renderer/lib/cast.js @@ -1,5 +1,6 @@ var chromecasts = require('chromecasts')() var airplay = require('airplay-js') +var dlnacasts = require('dlnacasts')() var config = require('../../config') var state = require('../state') @@ -37,7 +38,7 @@ function chromecastPlayer (player) { var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) player.play(state.server.networkURL, { type: 'video/mp4', - title: config.APP_NAME + ' — ' + torrentSummary.name + title: config.APP_NAME + ' - ' + torrentSummary.name }, function (err) { if (err) { state.playing.location = 'local' @@ -147,6 +148,83 @@ function airplayPlayer (player) { } } +// DLNA player implementation +function dlnaPlayer (player) { + function addEvents () { + player.on('error', function (err) { + player.errorMessage = err.message + update() + }) + player.on('disconnect', function () { + state.playing.location = 'local' + update() + }) + } + + function open () { + var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) + player.play(state.server.networkURL, { + type: 'video/mp4', + title: config.APP_NAME + ' - ' + torrentSummary.name, + seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 + }, function (err) { + if (err) { + state.playing.location = 'local' + state.errors.push({ + time: new Date().getTime(), + message: 'Couldn\'t connect to DLNA. ' + err + }) + } else { + state.playing.location = 'dlna' + } + update() + }) + } + + function playPause (callback) { + if (!state.playing.isPaused) player.pause(callback) + else player.play(null, null, callback) + } + + function stop (callback) { + player.stop(callback) + } + + function status (state) { + player.status(function (err, status) { + if (err) return console.log('error getting %s status: %o', state.playing.location, err) + state.playing.isPaused = status.playerState === 'PAUSED' + state.playing.currentTime = status.currentTime + state.playing.volume = status.volume.level + update() + }) + } + + function seek (time, callback) { + player.seek(time, callback) + } + + function volume (volume, callback) { + player.volume(volume, function (err) { + // quick volume update + state.playing.volume = volume + callback(err) + }) + } + + addEvents() + + return { + player: player, + open: open, + playPause: playPause, + stop: stop, + status: status, + seek: seek, + volume: volume + } +} + // start export functions function init (callback) { update = callback @@ -154,11 +232,15 @@ function init (callback) { // Start polling Chromecast or Airplay, whenever we're connected setInterval(() => pollCastStatus(state), 1000) - // Listen for devices: Chromecast and Airplay + // Listen for devices: Chromecast, DLNA and Airplay chromecasts.on('update', function (player) { state.devices.chromecast = chromecastPlayer(player) }) + dlnacasts.on('update', function (player) { + state.devices.dlna = dlnaPlayer(player) + }) + var browser = airplay.createBrowser() browser.on('deviceOn', function (player) { state.devices.airplay = airplayPlayer(player) @@ -207,7 +289,7 @@ function stoppedCasting () { // Returns false if we not casting (state.playing.location === 'local') // or if we're trying to connect but haven't yet ('chromecast-pending', etc) function isCasting () { - return state.playing.location === 'chromecast' || state.playing.location === 'airplay' + return state.playing.location === 'chromecast' || state.playing.location === 'airplay' || state.playing.location === 'dlna' } function getDevice (location) { @@ -217,6 +299,8 @@ function getDevice (location) { return state.devices.chromecast } else if (state.playing.location === 'airplay') { return state.devices.airplay + } else if (state.playing.location === 'dlna') { + return state.devices.dlna } } diff --git a/renderer/views/player.js b/renderer/views/player.js index 023ebf58..76af7270 100644 --- a/renderer/views/player.js +++ b/renderer/views/player.js @@ -181,21 +181,22 @@ function renderLoadingSpinner (state) { function renderCastScreen (state) { var isChromecast = state.playing.location.startsWith('chromecast') var isAirplay = state.playing.location.startsWith('airplay') + var isDlna = state.playing.location.startsWith('dlna') var isStarting = state.playing.location.endsWith('-pending') - if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type') + if (!isChromecast && !isAirplay && !isDlna) throw new Error('Unimplemented cast type') // Show a nice title image, if possible var style = { backgroundImage: cssBackgroundImagePoster(state) } - // Show whether we're connected to Chromecast / Airplay + // Show whether we're connected to Chromecast / Airplay / DLNA var castStatus = isStarting ? 'Connecting...' : 'Connected' return hx`
${isAirplay ? 'airplay' : 'cast'} -
${isAirplay ? 'AirPlay' : 'Chromecast'}
+
${isAirplay ? 'AirPlay' : (isDlna ? 'DLNA' : 'Chromecast')}
${castStatus}
@@ -247,26 +248,40 @@ function renderPlayerControls (state) { // If we've detected a Chromecast or AppleTV, the user can play video there var isOnChromecast = state.playing.location.startsWith('chromecast') var isOnAirplay = state.playing.location.startsWith('airplay') - var chromecastClass, chromecastHandler, airplayClass, airplayHandler + var isOnDlna = state.playing.location.startsWith('dlna') + var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler if (isOnChromecast) { chromecastClass = 'active' + dlnaClass = 'disabled' airplayClass = 'disabled' chromecastHandler = dispatcher('stopCasting') airplayHandler = undefined + dlnaHandler = undefined } else if (isOnAirplay) { chromecastClass = 'disabled' + dlnaClass = 'disabled' airplayClass = 'active' chromecastHandler = undefined airplayHandler = dispatcher('stopCasting') + dlnaHandler = undefined + } else if (isOnDlna) { + chromecastClass = 'disabled' + dlnaClass = 'active' + airplayClass = 'disabled' + chromecastHandler = undefined + airplayHandler = undefined + dlnaHandler = dispatcher('stopCasting') } else { chromecastClass = '' airplayClass = '' - chromecastHandler = dispatcher('openChromecast') - airplayHandler = dispatcher('openAirplay') + dlnaClass = '' + chromecastHandler = dispatcher('openDevice', 'chromecast') + airplayHandler = dispatcher('openDevice', 'airplay') + dlnaHandler = dispatcher('openDevice', 'dlna') } if (state.devices.chromecast || isOnChromecast) { elements.push(hx` - cast @@ -275,13 +290,22 @@ function renderPlayerControls (state) { } if (state.devices.airplay || isOnAirplay) { elements.push(hx` - airplay `) } + if (state.devices.dlna || isOnDlna) { + elements.push(hx` + + tv + + `) + } // On OSX, the back button is in the title bar of the window; see app.js // On other platforms, we render one over the video on mouseover