Speed up init() by >= 2x
Lazy load the WebTorrent, Chromecast, and Airplay modules
This commit is contained in:
@@ -12,19 +12,22 @@ var musicmetadata = require('musicmetadata')
|
||||
var networkAddress = require('network-address')
|
||||
var path = require('path')
|
||||
var remote = require('remote')
|
||||
var WebTorrent = require('webtorrent')
|
||||
|
||||
var createElement = require('virtual-dom/create-element')
|
||||
var diff = require('virtual-dom/diff')
|
||||
var patch = require('virtual-dom/patch')
|
||||
|
||||
var App = require('./views/app')
|
||||
var Cast = require('./lib/cast')
|
||||
var errors = require('./lib/errors')
|
||||
var config = require('../config')
|
||||
var TorrentPlayer = require('./lib/torrent-player')
|
||||
var torrentPoster = require('./lib/torrent-poster')
|
||||
|
||||
// These two dependencies are the slowest-loading, so we lazy load them
|
||||
// This cuts time from icon click to rendered window from ~550ms to ~150ms on my laptop
|
||||
var WebTorrent = null
|
||||
var Cast = null
|
||||
|
||||
// Electron apps have two processes: a main process (node) runs first and starts
|
||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||
// and this IPC channel receives from and sends messages to the main process
|
||||
@@ -53,20 +56,11 @@ loadState(init)
|
||||
function init () {
|
||||
state.location.go({ url: 'home' })
|
||||
|
||||
// Connect to the WebTorrent and BitTorrent networks
|
||||
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq
|
||||
state.client = new WebTorrent()
|
||||
state.client.on('warning', onWarning)
|
||||
state.client.on('error', function (err) {
|
||||
// TODO: WebTorrent should have semantic errors
|
||||
if (err.message.startsWith('There is already a swarm')) {
|
||||
onError(new Error('Couldn\'t add duplicate torrent'))
|
||||
} else {
|
||||
onError(err)
|
||||
}
|
||||
})
|
||||
resumeTorrents() /* restart everything we were torrenting last time the app ran */
|
||||
setInterval(updateTorrentProgress, 1000)
|
||||
// Lazily load the WebTorrent, Chromecast, and Airplay modules
|
||||
window.setTimeout(function () {
|
||||
lazyLoadClient()
|
||||
lazyLoadCast()
|
||||
}, 100)
|
||||
|
||||
// The UI is built with virtual-dom, a minimalist library extracted from React
|
||||
// The concepts--one way data flow, a pure function that renders state to a
|
||||
@@ -79,23 +73,13 @@ function init () {
|
||||
})
|
||||
document.body.appendChild(vdomLoop.target)
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to show latest state for each torrent
|
||||
// (eg % downloaded) and to keep the cursor in sync when playing a video
|
||||
setInterval(function () {
|
||||
update()
|
||||
updateClientProgress()
|
||||
}, 1000)
|
||||
|
||||
// Save state on exit
|
||||
window.addEventListener('beforeunload', saveState)
|
||||
|
||||
// listen for messages from the main process
|
||||
// Listen for messages from the main process
|
||||
setupIpc()
|
||||
|
||||
// OS integrations:
|
||||
// ...Chromecast and Airplay
|
||||
Cast.init(update)
|
||||
|
||||
// ...drag and drop a torrent or video file to play or seed
|
||||
dragDrop('body', (files) => dispatch('onOpen', files))
|
||||
|
||||
@@ -130,11 +114,56 @@ function init () {
|
||||
})
|
||||
|
||||
// Done! Ideally we want to get here <100ms after the user clicks the app
|
||||
document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */
|
||||
playInterfaceSound('STARTUP')
|
||||
console.timeEnd('init')
|
||||
}
|
||||
|
||||
// Lazily loads the WebTorrent module and creates the WebTorrent client
|
||||
function lazyLoadClient () {
|
||||
if (!WebTorrent) initWebtorrent()
|
||||
return state.client
|
||||
}
|
||||
|
||||
// Lazily loads Chromecast and Airplay support
|
||||
function lazyLoadCast () {
|
||||
if (!Cast) {
|
||||
Cast = require('./lib/cast')
|
||||
Cast.init(update) // Search the local network for Chromecast and Airplays
|
||||
}
|
||||
return Cast
|
||||
}
|
||||
|
||||
// Load the WebTorrent module, connect to both the WebTorrent and BitTorrent
|
||||
// networks, resume torrents, start monitoring torrent progress
|
||||
function initWebtorrent () {
|
||||
WebTorrent = require('webtorrent')
|
||||
|
||||
// Connect to the WebTorrent and BitTorrent networks
|
||||
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq
|
||||
state.client = new WebTorrent()
|
||||
state.client.on('warning', onWarning)
|
||||
state.client.on('error', function (err) {
|
||||
// TODO: WebTorrent should have semantic errors
|
||||
if (err.message.startsWith('There is already a swarm')) {
|
||||
onError(new Error('Couldn\'t add duplicate torrent'))
|
||||
} else {
|
||||
onError(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Restart everything we were torrenting last time the app ran
|
||||
resumeTorrents()
|
||||
|
||||
// Calling update() updates the UI given the current state
|
||||
// Do this at least once a second to give every file in every torrentSummary
|
||||
// a progress bar and to keep the cursor in sync when playing a video
|
||||
setInterval(function () {
|
||||
if (!updateTorrentProgress()) {
|
||||
update() // If we didn't just update(), do so now, for the video cursor
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
|
||||
// tree. Any events, such as button clicks, will turn into calls to dispatch()
|
||||
function render (state) {
|
||||
@@ -188,7 +217,6 @@ function dispatch (action, ...args) {
|
||||
state.location.go({
|
||||
url: 'player',
|
||||
onbeforeload: function (cb) {
|
||||
// TODO: handle audio. video only for now.
|
||||
openPlayer(args[0] /* torrentSummary */, args[1] /* index */, cb)
|
||||
},
|
||||
onbeforeunload: closePlayer
|
||||
@@ -210,13 +238,13 @@ function dispatch (action, ...args) {
|
||||
toggleSelectTorrent(args[0] /* infoHash */)
|
||||
}
|
||||
if (action === 'openChromecast') {
|
||||
Cast.openChromecast()
|
||||
lazyLoadCast().openChromecast()
|
||||
}
|
||||
if (action === 'openAirplay') {
|
||||
Cast.openAirplay()
|
||||
lazyLoadCast().openAirplay()
|
||||
}
|
||||
if (action === 'stopCasting') {
|
||||
Cast.stopCasting()
|
||||
lazyLoadCast().stopCasting()
|
||||
}
|
||||
if (action === 'setDimensions') {
|
||||
setDimensions(args[0] /* dimensions */)
|
||||
@@ -272,7 +300,7 @@ function playPause (isPaused) {
|
||||
return // Nothing to do
|
||||
}
|
||||
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
|
||||
if (Cast.isCasting()) {
|
||||
if (lazyLoadCast().isCasting()) {
|
||||
Cast.playPause()
|
||||
}
|
||||
state.playing.isPaused = !state.playing.isPaused
|
||||
@@ -280,7 +308,7 @@ function playPause (isPaused) {
|
||||
}
|
||||
|
||||
function jumpToTime (time) {
|
||||
if (Cast.isCasting()) {
|
||||
if (lazyLoadCast().isCasting()) {
|
||||
Cast.seek(time)
|
||||
} else {
|
||||
state.playing.jumpToTime = time
|
||||
@@ -296,7 +324,7 @@ function changeVolume (delta) {
|
||||
function setVolume (volume) {
|
||||
// check if its in [0.0 - 1.0] range
|
||||
volume = Math.max(0, Math.min(1, volume))
|
||||
if (Cast.isCasting()) {
|
||||
if (lazyLoadCast().isCasting()) {
|
||||
Cast.setVolume(volume)
|
||||
} else {
|
||||
state.playing.setVolume = volume
|
||||
@@ -362,18 +390,6 @@ function saveState () {
|
||||
})
|
||||
}
|
||||
|
||||
function updateClientProgress () {
|
||||
var progress = state.client.progress
|
||||
var activeTorrentsExist = state.client.torrents.some(function (torrent) {
|
||||
return torrent.progress !== 1
|
||||
})
|
||||
// Hide progress bar when client has no torrents, or progress is 100%
|
||||
if (!activeTorrentsExist || progress === 1) {
|
||||
progress = -1
|
||||
}
|
||||
state.dock.progress = progress
|
||||
}
|
||||
|
||||
function onOpen (files) {
|
||||
if (!Array.isArray(files)) files = [ files ]
|
||||
|
||||
@@ -417,7 +433,9 @@ function getTorrentSummary (infoHash) {
|
||||
// Get an active torrent from state.client.torrents
|
||||
// Returns undefined if we are not currently torrenting that infoHash
|
||||
function getTorrent (infoHash) {
|
||||
return state.client.torrents.find((x) => x.infoHash === infoHash)
|
||||
var pending = state.pendingTorrents[infoHash]
|
||||
if (pending) return pending
|
||||
return lazyLoadClient().torrents.find((x) => x.infoHash === infoHash)
|
||||
}
|
||||
|
||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||
@@ -454,16 +472,22 @@ function addTorrentToList (torrent) {
|
||||
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
||||
function startTorrentingSummary (torrentSummary) {
|
||||
var s = torrentSummary
|
||||
if (s.torrentPath) return startTorrentingID(s.torrentPath, s.path)
|
||||
else if (s.magnetURI) return startTorrentingID(s.magnetURI, s.path)
|
||||
else return startTorrentingID(s.infoHash, s.path)
|
||||
if (s.torrentPath) {
|
||||
var ret = startTorrentingID(s.torrentPath, s.path)
|
||||
if (s.infoHash) state.pendingTorrents[s.infoHash] = ret
|
||||
return ret
|
||||
} else if (s.magnetURI) {
|
||||
return startTorrentingID(s.magnetURI, s.path)
|
||||
} else {
|
||||
return startTorrentingID(s.infoHash, s.path)
|
||||
}
|
||||
}
|
||||
|
||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||
function startTorrentingID (torrentID, path) {
|
||||
console.log('Starting torrent ' + torrentID)
|
||||
var torrent = state.client.add(torrentID, {
|
||||
console.log('starting torrent ' + torrentID)
|
||||
var torrent = lazyLoadClient().add(torrentID, {
|
||||
path: path || state.saved.downloadPath // Use downloads folder
|
||||
})
|
||||
addTorrentEvents(torrent)
|
||||
@@ -479,13 +503,17 @@ function stopTorrenting (infoHash) {
|
||||
// Creates a torrent for a local file and starts seeding it
|
||||
function seed (files) {
|
||||
if (files.length === 0) return
|
||||
var torrent = state.client.seed(files)
|
||||
var torrent = lazyLoadClient().seed(files)
|
||||
addTorrentToList(torrent)
|
||||
addTorrentEvents(torrent)
|
||||
}
|
||||
|
||||
function addTorrentEvents (torrent) {
|
||||
torrent.on('infoHash', update)
|
||||
torrent.on('infoHash', function () {
|
||||
var infoHash = torrent.infoHash
|
||||
if (state.pendingTorrents[infoHash]) delete state.pendingTorrents[infoHash]
|
||||
update()
|
||||
})
|
||||
torrent.on('ready', torrentReady)
|
||||
torrent.on('done', torrentDone)
|
||||
|
||||
@@ -531,10 +559,25 @@ function addTorrentEvents (torrent) {
|
||||
}
|
||||
|
||||
function updateTorrentProgress () {
|
||||
var changed = false
|
||||
|
||||
// First, track overall progress
|
||||
var progress = lazyLoadClient().progress
|
||||
var activeTorrentsExist = lazyLoadClient().torrents.some(function (torrent) {
|
||||
return torrent.progress !== 1
|
||||
})
|
||||
// Hide progress bar when client has no torrents, or progress is 100%
|
||||
if (!activeTorrentsExist || progress === 1) {
|
||||
progress = -1
|
||||
}
|
||||
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||
if (state.dock.progress !== progress) changed = true
|
||||
state.dock.progress = progress
|
||||
|
||||
// Track progress for every file in each torrentSummary
|
||||
// TODO: ideally this would be tracked by WebTorrent, which could do it
|
||||
// more efficiently than looping over torrent.bitfield
|
||||
var changed = false
|
||||
state.client.torrents.forEach(function (torrent) {
|
||||
lazyLoadClient().torrents.forEach(function (torrent) {
|
||||
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
||||
if (!torrentSummary || !torrent.ready) return
|
||||
torrent.files.forEach(function (file, index) {
|
||||
@@ -554,6 +597,7 @@ function updateTorrentProgress () {
|
||||
})
|
||||
|
||||
if (changed) update()
|
||||
return changed
|
||||
}
|
||||
|
||||
function generateTorrentPoster (torrent, torrentSummary) {
|
||||
@@ -597,8 +641,8 @@ function saveTorrentFile (torrentSummary, torrent) {
|
||||
// Otherwise, save the .torrent file, under the app config folder
|
||||
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
|
||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||
if (err) return console.log('Error saving torrent file %s: %o', torrentPath, err)
|
||||
console.log('Saved torrent file %s', torrentPath)
|
||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||
console.log('saved torrent file %s', torrentPath)
|
||||
torrentSummary.torrentPath = torrentPath
|
||||
saveState()
|
||||
})
|
||||
@@ -639,7 +683,7 @@ function startServerFromReadyTorrent (torrent, index, cb) {
|
||||
// if it's audio, parse out the metadata (artist, title, etc)
|
||||
musicmetadata(file.createReadStream(), function (err, info) {
|
||||
if (err) return
|
||||
console.log('Got audio metadata for %s: %v', file.name, info)
|
||||
console.log('got audio metadata for %s: %v', file.name, info)
|
||||
state.playing.audioInfo = info
|
||||
update()
|
||||
})
|
||||
@@ -690,7 +734,7 @@ function stopServer () {
|
||||
|
||||
// Opens the video player
|
||||
function openPlayer (torrentSummary, index, cb) {
|
||||
var torrent = state.client.get(torrentSummary.infoHash)
|
||||
var torrent = lazyLoadClient().get(torrentSummary.infoHash)
|
||||
if (!torrent || !torrent.done) playInterfaceSound('PLAY')
|
||||
torrentSummary.playStatus = 'requested'
|
||||
update()
|
||||
@@ -743,7 +787,7 @@ function closePlayer (cb) {
|
||||
}
|
||||
|
||||
function openFile (torrentSummary, index) {
|
||||
var torrent = state.client.get(torrentSummary.infoHash)
|
||||
var torrent = lazyLoadClient().get(torrentSummary.infoHash)
|
||||
if (!torrent) return
|
||||
|
||||
var filePath = path.join(torrent.path, torrent.files[index].path)
|
||||
@@ -751,7 +795,7 @@ function openFile (torrentSummary, index) {
|
||||
}
|
||||
|
||||
function openFolder (torrentSummary) {
|
||||
var torrent = state.client.get(torrentSummary.infoHash)
|
||||
var torrent = lazyLoadClient().get(torrentSummary.infoHash)
|
||||
if (!torrent) return
|
||||
|
||||
var folderPath = path.join(torrent.path, torrent.name)
|
||||
|
||||
Reference in New Issue
Block a user