Speed up init() by >= 2x

Lazy load the WebTorrent, Chromecast, and Airplay modules
This commit is contained in:
DC
2016-03-26 22:46:55 -07:00
parent 1e6e101c4e
commit 906da4d977
6 changed files with 113 additions and 95 deletions

View File

@@ -49,34 +49,6 @@ table {
background-color: rgb(40, 40, 40);
}
.loading {
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.loading .icon {
font-size: 42px;
display: block;
text-align: center;
animation: spin-ccw 2s infinite linear;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-ccw {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }

View File

@@ -6,7 +6,6 @@
<link rel="stylesheet" href="index.css" charset="utf-8">
</head>
<body>
<div class="loading"><i class="icon">sync</i></div>
<script async src="index.js"></script>
</body>
</html>

View File

@@ -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)

View File

@@ -57,7 +57,7 @@ function addAirplayEvents () {}
function pollCastStatus (state) {
if (state.playing.location === 'chromecast') {
state.devices.chromecast.status(function (err, status) {
if (err) return console.log('Error getting %s status: %o', state.playing.location, err)
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

View File

@@ -30,6 +30,7 @@ module.exports = {
isPaused: true,
mouseStationarySince: 0 /* Unix time in ms */
},
pendingTorrents: {}, /* infohash to WebTorrent handle */
devices: { /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */

View File

@@ -24,7 +24,9 @@ function TorrentList (state, dispatch) {
function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash
var torrent = state.client.torrents.find((x) => x.infoHash === infoHash)
var torrent = state.client
? state.client.torrents.find((x) => x.infoHash === infoHash)
: null
var isSelected = state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible