add DLNA support
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user