// The Cast module talks to Airplay and Chromecast // * Modifies state when things change // * Starts and stops casting, provides remote video controls module.exports = { init, open, close, play, pause, seek, setVolume, setRate } var airplayer = require('airplayer')() var chromecasts = require('chromecasts')() var dlnacasts = require('dlnacasts')() var config = require('../../config') // App state. Cast modifies state.playing and state.errors in response to events var state // Callback to notify module users when state has changed var update // setInterval() for updating cast status var statusInterval = null // Start looking for cast devices on the local network function init (appState, callback) { state = appState update = callback // 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) }) airplayer.on('update', function (player) { state.devices.airplay = airplayPlayer(player) }) } // chromecast player implementation function chromecastPlayer (player) { function addEvents () { player.on('error', function (err) { state.playing.location = 'local' state.errors.push({ time: new Date().getTime(), message: 'Could not connect to Chromecast. ' + 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 }, function (err) { if (err) { state.playing.location = 'local' state.errors.push({ time: new Date().getTime(), message: 'Could not connect to Chromecast. ' + err.message }) } else { state.playing.location = 'chromecast' } update() }) } function play (callback) { player.play(null, null, callback) } function pause (callback) { player.pause(callback) } function stop (callback) { player.stop(callback) } function status () { 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.muted ? 0 : status.volume.level update() }) } function seek (time, callback) { player.seek(time, callback) } function volume (volume, callback) { player.volume(volume, callback) } addEvents() return { player: player, open: open, play: play, pause: pause, stop: stop, status: status, seek: seek, volume: volume } } // airplay player implementation function airplayPlayer (player) { function addEvents () { player.on('event', function (event) { switch (event.state) { case 'loading': break case 'playing': state.playing.isPaused = false break case 'paused': state.playing.isPaused = true break case 'stopped': break } update() }) } function open () { player.play(state.server.networkURL, function (err, res) { if (err) { state.playing.location = 'local' state.errors.push({ time: new Date().getTime(), message: 'Could not connect to AirPlay. ' + err.message }) } else { state.playing.location = 'airplay' } update() }) } function play (callback) { player.resume(callback) } function pause (callback) { player.pause(callback) } function stop (callback) { player.stop(callback) } function status () { player.playbackInfo(function (err, res, status) { if (err) { state.playing.location = 'local' state.errors.push({ time: new Date().getTime(), message: 'Could not connect to AirPlay. ' + err.message }) } else { state.playing.isPaused = status.rate === 0 state.playing.currentTime = status.position update() } }) } function seek (time, callback) { player.scrub(time, callback) } function volume (volume, callback) { // AirPlay doesn't support volume // TODO: We should just disable the volume slider state.playing.volume = volume } addEvents() return { player: player, open: open, play: play, pause: pause, stop: stop, status: status, seek: seek, volume: volume } } // DLNA player implementation function dlnaPlayer (player) { function addEvents () { player.on('error', function (err) { state.playing.location = 'local' state.errors.push({ time: new Date().getTime(), message: 'Could not connect to DLNA. ' + 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: 'Could not connect to DLNA. ' + err.message }) } else { state.playing.location = 'dlna' } update() }) } function play (callback) { player.play(null, null, callback) } function pause (callback) { player.pause(callback) } function stop (callback) { player.stop(callback) } function status () { 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, play: play, pause: pause, stop: stop, status: status, seek: seek, volume: volume } } // Start polling cast device state, whenever we're connected function startStatusInterval () { statusInterval = setInterval(function () { var device = getDevice() if (device) { device.status() } }, 1000) } function open (location) { if (state.playing.location !== 'local') { throw new Error('You can\'t connect to ' + location + ' when already connected to another device') } state.playing.location = location + '-pending' var device = getDevice(location) if (device) { getDevice(location).open() startStatusInterval() } update() } // Stops casting, move video back to local screen function close () { var device = getDevice() if (device) { device.stop(stoppedCasting) clearInterval(statusInterval) } else { stoppedCasting() } } function stoppedCasting () { state.playing.location = 'local' state.playing.jumpToTime = state.playing.currentTime update() } function getDevice (location) { if (location && state.devices[location]) { return state.devices[location] } else if (state.playing.location === 'chromecast') { return state.devices.chromecast } else if (state.playing.location === 'airplay') { return state.devices.airplay } else if (state.playing.location === 'dlna') { return state.devices.dlna } else { return null } } function play () { var device = getDevice() if (device) { device.play(castCallback) } } function pause () { var device = getDevice() if (device) { device.pause(castCallback) } } function setRate (rate) { var device var result = true if (state.playing.location === 'chromecast') { // TODO find how to control playback rate on chromecast castCallback() result = false } else if (state.playing.location === 'airplay') { device = state.devices.airplay device.rate(rate, castCallback) } else { result = false } return result } function seek (time) { var device = getDevice() if (device) { device.seek(time, castCallback) } } function setVolume (volume) { var device = getDevice() if (device) { device.volume(volume, castCallback) } } function castCallback () { console.log('%s callback: %o', state.playing.location, arguments) }