add DLNA support

This commit is contained in:
grunjol
2016-04-04 13:00:25 -03:00
parent 2509c0c951
commit 6aa7058184
5 changed files with 122 additions and 24 deletions

View File

@@ -19,6 +19,7 @@
"application-config-path": "^0.1.0", "application-config-path": "^0.1.0",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"create-torrent": "^3.22.1", "create-torrent": "^3.22.1",
"dlnacasts": "^0.0.1",
"drag-drop": "^2.11.0", "drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0", "electron-localshortcut": "^0.6.0",
"electron-prebuilt": "0.37.3", "electron-prebuilt": "0.37.3",

View File

@@ -668,8 +668,7 @@ body.drag .app::after {
margin: 0 auto; margin: 0 auto;
} }
.player-controls .chromecast, .player-controls .device,
.player-controls .airplay,
.player-controls .fullscreen, .player-controls .fullscreen,
.player-controls .back { .player-controls .back {
display: block; display: block;
@@ -682,8 +681,7 @@ body.drag .app::after {
float: left; float: left;
} }
.player-controls .chromecast, .player-controls .device,
.player-controls .airplay,
.player-controls .fullscreen { .player-controls .fullscreen {
float: right; float: right;
} }
@@ -692,14 +690,12 @@ body.drag .app::after {
margin-right: 15px; margin-right: 15px;
} }
.player-controls .chromecast, .player-controls .device {
.player-controls .airplay {
font-size: 18px; /* make the cast icons less huge */ font-size: 18px; /* make the cast icons less huge */
margin-top: 8px !important; margin-top: 8px !important;
} }
.player-controls .chromecast.active, .player-controls .device.active {
.player-controls .airplay.active {
color: #9af; color: #9af;
} }

View File

@@ -246,11 +246,8 @@ function dispatch (action, ...args) {
if (action === 'openTorrentContextMenu') { if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */) openTorrentContextMenu(args[0] /* infoHash */)
} }
if (action === 'openChromecast') { if (action === 'open') {
lazyLoadCast().open('chromecast') lazyLoadCast().open(args[0] /* deviceType */)
}
if (action === 'openAirplay') {
lazyLoadCast().open('airplay')
} }
if (action === 'stopCasting') { if (action === 'stopCasting') {
lazyLoadCast().stopCasting() lazyLoadCast().stopCasting()

View File

@@ -1,5 +1,6 @@
var chromecasts = require('chromecasts')() var chromecasts = require('chromecasts')()
var airplay = require('airplay-js') var airplay = require('airplay-js')
var dlnacasts = require('dlnacasts')()
var config = require('../../config') var config = require('../../config')
var state = require('../state') var state = require('../state')
@@ -37,7 +38,7 @@ function chromecastPlayer (player) {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, { player.play(state.server.networkURL, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' ' + torrentSummary.name title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) { }, function (err) {
if (err) { if (err) {
state.playing.location = 'local' 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 // start export functions
function init (callback) { function init (callback) {
update = callback update = callback
@@ -154,11 +228,15 @@ function init (callback) {
// Start polling Chromecast or Airplay, whenever we're connected // Start polling Chromecast or Airplay, whenever we're connected
setInterval(() => pollCastStatus(state), 1000) setInterval(() => pollCastStatus(state), 1000)
// Listen for devices: Chromecast and Airplay // Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', function (player) { chromecasts.on('update', function (player) {
state.devices.chromecast = chromecastPlayer(player) state.devices.chromecast = chromecastPlayer(player)
}) })
dlnacasts.on('update', function (player) {
state.devices.dlna = dlnaPlayer(player)
})
var browser = airplay.createBrowser() var browser = airplay.createBrowser()
browser.on('deviceOn', function (player) { browser.on('deviceOn', function (player) {
state.devices.airplay = airplayPlayer(player) state.devices.airplay = airplayPlayer(player)
@@ -207,7 +285,7 @@ function stoppedCasting () {
// Returns false if we not casting (state.playing.location === 'local') // Returns false if we not casting (state.playing.location === 'local')
// or if we're trying to connect but haven't yet ('chromecast-pending', etc) // or if we're trying to connect but haven't yet ('chromecast-pending', etc)
function isCasting () { 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) { function getDevice (location) {
@@ -217,6 +295,8 @@ function getDevice (location) {
return state.devices.chromecast return state.devices.chromecast
} else if (state.playing.location === 'airplay') { } else if (state.playing.location === 'airplay') {
return state.devices.airplay return state.devices.airplay
} else if (state.playing.location === 'dlna') {
return state.devices.dlna
} }
} }

View File

@@ -181,21 +181,22 @@ function renderLoadingSpinner (state) {
function renderCastScreen (state) { function renderCastScreen (state) {
var isChromecast = state.playing.location.startsWith('chromecast') var isChromecast = state.playing.location.startsWith('chromecast')
var isAirplay = state.playing.location.startsWith('airplay') var isAirplay = state.playing.location.startsWith('airplay')
var isDlna = state.playing.location.startsWith('dlna')
var isStarting = state.playing.location.endsWith('-pending') 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 // Show a nice title image, if possible
var style = { var style = {
backgroundImage: cssBackgroundImagePoster(state) 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' var castStatus = isStarting ? 'Connecting...' : 'Connected'
return hx` return hx`
<div class='letterbox' style=${style}> <div class='letterbox' style=${style}>
<div class='cast-screen'> <div class='cast-screen'>
<i class='icon'>${isAirplay ? 'airplay' : 'cast'}</i> <i class='icon'>${isAirplay ? 'airplay' : 'cast'}</i>
<div class='cast-type'>${isAirplay ? 'AirPlay' : 'Chromecast'}</div> <div class='cast-type'>${isAirplay ? 'AirPlay' : (isDlna ? 'DLNA' : 'Chromecast')}</div>
<div class='cast-status'>${castStatus}</div> <div class='cast-status'>${castStatus}</div>
</div> </div>
</div> </div>
@@ -247,26 +248,40 @@ function renderPlayerControls (state) {
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast') var isOnChromecast = state.playing.location.startsWith('chromecast')
var isOnAirplay = state.playing.location.startsWith('airplay') 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) { if (isOnChromecast) {
chromecastClass = 'active' chromecastClass = 'active'
dlnaClass = 'disabled'
airplayClass = 'disabled' airplayClass = 'disabled'
chromecastHandler = dispatcher('stopCasting') chromecastHandler = dispatcher('stopCasting')
airplayHandler = undefined airplayHandler = undefined
dlnaHandler = undefined
} else if (isOnAirplay) { } else if (isOnAirplay) {
chromecastClass = 'disabled' chromecastClass = 'disabled'
dlnaClass = 'disabled'
airplayClass = 'active' airplayClass = 'active'
chromecastHandler = undefined chromecastHandler = undefined
airplayHandler = dispatcher('stopCasting') airplayHandler = dispatcher('stopCasting')
dlnaHandler = undefined
} else if (isOnDlna) {
chromecastClass = 'disabled'
dlnaClass = 'active'
airplayClass = 'disabled'
chromecastHandler = undefined
airplayHandler = undefined
dlnaHandler = dispatcher('stopCasting')
} else { } else {
chromecastClass = '' chromecastClass = ''
airplayClass = '' airplayClass = ''
chromecastHandler = dispatcher('openChromecast') dlnaClass = ''
airplayHandler = dispatcher('openAirplay') chromecastHandler = dispatcher('open', 'chromecast')
airplayHandler = dispatcher('open', 'airplay')
dlnaHandler = dispatcher('open', 'dlna')
} }
if (state.devices.chromecast || isOnChromecast) { if (state.devices.chromecast || isOnChromecast) {
elements.push(hx` elements.push(hx`
<i.icon.chromecast <i.icon.device
class=${chromecastClass} class=${chromecastClass}
onclick=${chromecastHandler}> onclick=${chromecastHandler}>
cast cast
@@ -275,13 +290,22 @@ function renderPlayerControls (state) {
} }
if (state.devices.airplay || isOnAirplay) { if (state.devices.airplay || isOnAirplay) {
elements.push(hx` elements.push(hx`
<i.icon.airplay <i.icon.device
class=${airplayClass} class=${airplayClass}
onclick=${airplayHandler}> onclick=${airplayHandler}>
airplay airplay
</i> </i>
`) `)
} }
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i.icon.device
class=${dlnaClass}
onclick=${dlnaHandler}>
cast
</i>
`)
}
// On OSX, the back button is in the title bar of the window; see app.js // 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 // On other platforms, we render one over the video on mouseover