Every require() that we do before the users sees UI reduces startup
time.
I used the following code (added to index.js) to log every require()
call in the main process:
```js
var Module = require('module')
var required = {}
Module.prototype.require = function (orig) {
return function (id) {
if (!required[id]) {
required[id] = true
console.log(`${id} (from ${this.filename})`)
}
return orig.apply(this, arguments)
}
}(Module.prototype.require)
```
From this I was able to learn that lots of modules were being required
that aren't actually used until later.
I also sent this related PR to eliminate another few require()s:
https://github.com/LinusU/node-application-config/pull/4
This increases startup time by 50ms.
We'll probably realize much bigger gains by following this same
procedure for the renderer process.
234 lines
7.2 KiB
JavaScript
234 lines
7.2 KiB
JavaScript
const appConfig = require('application-config')('WebTorrent')
|
|
const debounce = require('debounce')
|
|
const path = require('path')
|
|
const {EventEmitter} = require('events')
|
|
|
|
const config = require('../../config')
|
|
const migrations = require('./migrations')
|
|
|
|
const SAVE_DEBOUNCE_INTERVAL = 1000
|
|
|
|
const State = module.exports = Object.assign(new EventEmitter(), {
|
|
getDefaultPlayState,
|
|
load,
|
|
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
|
|
save: debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL),
|
|
saveImmediate
|
|
})
|
|
|
|
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
|
|
|
function getDefaultState () {
|
|
const LocationHistory = require('location-history')
|
|
|
|
return {
|
|
/*
|
|
* Temporary state disappears once the program exits.
|
|
* It can contain complex objects like open connections, etc.
|
|
*/
|
|
client: null, /* the WebTorrent client */
|
|
server: null, /* local WebTorrent-to-HTTP server */
|
|
prev: { /* used for state diffing in updateElectron() */
|
|
title: null,
|
|
progress: -1,
|
|
badge: null
|
|
},
|
|
location: new LocationHistory(),
|
|
window: {
|
|
bounds: null, /* {x, y, width, height } */
|
|
isFocused: true,
|
|
isFullScreen: false,
|
|
title: config.APP_WINDOW_TITLE
|
|
},
|
|
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
|
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
|
|
devices: {}, /* playback devices like Chromecast and AppleTV */
|
|
dock: {
|
|
badge: 0,
|
|
progress: 0
|
|
},
|
|
modal: null, /* modal popover */
|
|
errors: [], /* user-facing errors */
|
|
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
|
|
|
|
/*
|
|
* Saved state is read from and written to a file every time the app runs.
|
|
* It should be simple and minimal and must be JSON.
|
|
* It must never contain absolute paths since we have a portable app.
|
|
*
|
|
* Config path:
|
|
*
|
|
* Mac ~/Library/Application Support/WebTorrent/config.json
|
|
* Linux (XDG) $XDG_CONFIG_HOME/WebTorrent/config.json
|
|
* Linux (Legacy) ~/.config/WebTorrent/config.json
|
|
* Windows (> Vista) %LOCALAPPDATA%/WebTorrent/config.json
|
|
* Windows (XP, 2000) %USERPROFILE%/Local Settings/Application Data/WebTorrent/config.json
|
|
*
|
|
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
|
*/
|
|
saved: {},
|
|
|
|
/*
|
|
* Getters, for convenience
|
|
*/
|
|
getPlayingTorrentSummary,
|
|
getPlayingFileSummary,
|
|
getExternalPlayerName
|
|
}
|
|
}
|
|
|
|
/* Whenever we stop playing video or audio, here's what we reset state.playing to */
|
|
function getDefaultPlayState () {
|
|
return {
|
|
infoHash: null, /* the info hash of the torrent we're playing */
|
|
fileIndex: null, /* the zero-based index within the torrent */
|
|
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
|
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
|
|
currentTime: 0, /* seconds */
|
|
duration: 1, /* seconds */
|
|
isPaused: true,
|
|
isStalled: false,
|
|
lastTimeUpdate: 0, /* Unix time in ms */
|
|
mouseStationarySince: 0, /* Unix time in ms */
|
|
playbackRate: 1,
|
|
volume: 1,
|
|
subtitles: {
|
|
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
|
selectedIndex: -1, /* current subtitle track */
|
|
showMenu: false /* popover menu, above the video */
|
|
},
|
|
aspectRatio: 0 /* aspect ratio of the video */
|
|
}
|
|
}
|
|
|
|
/* If the saved state file doesn't exist yet, here's what we use instead */
|
|
function setupStateSaved (cb) {
|
|
const fs = require('fs-extra')
|
|
const parseTorrent = require('parse-torrent')
|
|
const parallel = require('run-parallel')
|
|
|
|
const saved = {
|
|
prefs: {
|
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
|
isFileHandler: false,
|
|
openExternalPlayer: false,
|
|
externalPlayerPath: null,
|
|
startup: false
|
|
},
|
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
|
}
|
|
|
|
const tasks = []
|
|
|
|
config.DEFAULT_TORRENTS.map(function (t, i) {
|
|
const infoHash = saved.torrents[i].infoHash
|
|
tasks.push(function (cb) {
|
|
fs.copy(
|
|
path.join(config.STATIC_PATH, t.posterFileName),
|
|
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
|
cb
|
|
)
|
|
})
|
|
tasks.push(function (cb) {
|
|
fs.copy(
|
|
path.join(config.STATIC_PATH, t.torrentFileName),
|
|
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
|
cb
|
|
)
|
|
})
|
|
})
|
|
|
|
parallel(tasks, function (err) {
|
|
if (err) return cb(err)
|
|
cb(null, saved)
|
|
})
|
|
|
|
function createTorrentObject (t) {
|
|
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
|
const parsedTorrent = parseTorrent(torrent)
|
|
|
|
return {
|
|
status: 'paused',
|
|
infoHash: parsedTorrent.infoHash,
|
|
name: t.name,
|
|
displayName: t.name,
|
|
posterFileName: parsedTorrent.infoHash + path.extname(t.posterFileName),
|
|
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
|
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
|
files: parsedTorrent.files,
|
|
selections: parsedTorrent.files.map((x) => true),
|
|
testID: t.testID
|
|
}
|
|
}
|
|
}
|
|
|
|
function getPlayingTorrentSummary () {
|
|
const infoHash = this.playing.infoHash
|
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
|
}
|
|
|
|
function getPlayingFileSummary () {
|
|
const torrentSummary = this.getPlayingTorrentSummary()
|
|
if (!torrentSummary) return null
|
|
return torrentSummary.files[this.playing.fileIndex]
|
|
}
|
|
|
|
function getExternalPlayerName () {
|
|
const playerPath = this.saved.prefs.externalPlayerPath
|
|
if (!playerPath) return 'VLC'
|
|
return path.basename(playerPath).split('.')[0]
|
|
}
|
|
|
|
function load (cb) {
|
|
appConfig.read(function (err, saved) {
|
|
if (err || !saved.version) {
|
|
console.log('Missing config file: Creating new one')
|
|
setupStateSaved(onSavedState)
|
|
} else {
|
|
onSavedState(null, saved)
|
|
}
|
|
})
|
|
|
|
function onSavedState (err, saved) {
|
|
if (err) return cb(err)
|
|
const state = getDefaultState()
|
|
state.saved = saved
|
|
migrations.run(state)
|
|
cb(null, state)
|
|
}
|
|
}
|
|
|
|
// Write state.saved to the JSON state file
|
|
function saveImmediate (state, cb) {
|
|
console.log('Saving state to ' + appConfig.filePath)
|
|
|
|
// Clean up, so that we're not saving any pending state
|
|
const copy = Object.assign({}, state.saved)
|
|
// Remove torrents pending addition to the list, where we haven't finished
|
|
// reading the torrent file or file(s) to seed & don't have an infohash
|
|
copy.torrents = copy.torrents
|
|
.filter((x) => x.infoHash)
|
|
.map(function (x) {
|
|
const torrent = {}
|
|
for (let key in x) {
|
|
if (key === 'progress' || key === 'torrentKey') {
|
|
continue // Don't save progress info or key for the webtorrent process
|
|
}
|
|
if (key === 'playStatus') {
|
|
continue // Don't save whether a torrent is playing / pending
|
|
}
|
|
if (key === 'error') {
|
|
continue // Don't save error states
|
|
}
|
|
torrent[key] = x[key]
|
|
}
|
|
return torrent
|
|
})
|
|
|
|
appConfig.write(copy, (err) => {
|
|
if (err) console.error(err)
|
|
else State.emit('stateSaved')
|
|
})
|
|
}
|