diff --git a/package.json b/package.json index 6c37fad6..e1b3ce74 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.1", "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..2ac8f1ce 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 === 'open') { + lazyLoadCast().open(args[0] /* deviceType */) } if (action === 'stopCasting') { lazyLoadCast().stopCasting() diff --git a/renderer/lib/cast.js b/renderer/lib/cast.js index c34171ff..625b4709 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,79 @@ 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 | 0 + }, function (err) { + if (err) { + state.playing.location = 'local' + } 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 +228,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 +285,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 +295,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..9c9c06df 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`