Preload sound files for instant playback (#374)

* rm dist at start of build

* renderer style

* preload sound files for instant playback

The first time a sound file is played, the Audio object is cached.

5s after startup, all sound files are automatically preloaded.
This commit is contained in:
Feross Aboukhadijeh
2016-04-10 16:46:46 -07:00
parent 69460db294
commit 45f6cc5247
4 changed files with 108 additions and 67 deletions

View File

@@ -15,6 +15,7 @@ var rimraf = require('rimraf')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
function build () { function build () {
rimraf.sync(path.join(config.ROOT_PATH, 'dist'))
var platform = process.argv[2] var platform = process.argv[2]
var packageType = process.argv.length > 3 ? process.argv[3] : 'all' var packageType = process.argv.length > 3 ? process.argv[3] : 'all'
if (platform === 'darwin') { if (platform === 'darwin') {

View File

@@ -32,39 +32,6 @@ module.exports = {
ROOT_PATH: __dirname, ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'), STATIC_PATH: path.join(__dirname, 'static'),
SOUND_ADD: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
volume: 0.2
},
SOUND_DELETE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
volume: 0.1
},
SOUND_DISABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
volume: 0.2
},
SOUND_DONE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
volume: 0.2
},
SOUND_ENABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
volume: 0.2
},
SOUND_ERROR: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
volume: 0.2
},
SOUND_PLAY: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
volume: 0.2
},
SOUND_STARTUP: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav'),
volume: 0.4
},
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'), WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'), WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'), WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'),

View File

@@ -8,7 +8,6 @@ var EventEmitter = require('events')
var fs = require('fs') var fs = require('fs')
var mainLoop = require('main-loop') var mainLoop = require('main-loop')
var path = require('path') var path = require('path')
var remote = require('remote')
var srtToVtt = require('srt-to-vtt') var srtToVtt = require('srt-to-vtt')
var createElement = require('virtual-dom/create-element') var createElement = require('virtual-dom/create-element')
@@ -16,25 +15,30 @@ var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch') var patch = require('virtual-dom/patch')
var App = require('./views/app') var App = require('./views/app')
var errors = require('./lib/errors')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
var errors = require('./lib/errors')
var sound = require('./lib/sound')
var State = require('./state')
var TorrentPlayer = require('./lib/torrent-player') var TorrentPlayer = require('./lib/torrent-player')
var util = require('./util') var util = require('./util')
var {setDispatch} = require('./lib/dispatcher') var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch) setDispatch(dispatch)
var State = require('./state')
// This dependency is the slowest-loading, so we lazy load it
var Cast = null
// Electron apps have two processes: a main process (node) runs first and starts // 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, // 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 // and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer var ipcRenderer = electron.ipcRenderer
var clipboard = electron.clipboard var clipboard = electron.clipboard
var dialog = remote.require('dialog')
var dialog = electron.remote.dialog
var Menu = electron.remote.Menu
var MenuItem = electron.remote.MenuItem
var remote = electron.remote
// This dependency is the slowest-loading, so we lazy load it
var Cast = null
// For easy debugging in Developer Tools // For easy debugging in Developer Tools
var state = global.state = State.getInitialState() var state = global.state = State.getInitialState()
@@ -60,8 +64,7 @@ function init () {
initWebTorrent() initWebTorrent()
// Lazily load the Chromecast/Airplay/DLNA modules window.setTimeout(delayedInit, 5000)
window.setTimeout(lazyLoadCast, 5000)
// The UI is built with virtual-dom, a minimalist library extracted from React // 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 // The concepts--one way data flow, a pure function that renders state to a
@@ -112,11 +115,16 @@ function init () {
setupIpc() setupIpc()
// Done! Ideally we want to get here <100ms after the user clicks the app // Done! Ideally we want to get here <100ms after the user clicks the app
playInterfaceSound('STARTUP') sound.play('STARTUP')
console.timeEnd('init') console.timeEnd('init')
} }
function delayedInit () {
lazyLoadCast()
sound.preload()
}
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
function lazyLoadCast () { function lazyLoadCast () {
if (!Cast) { if (!Cast) {
@@ -651,7 +659,7 @@ function torrentInfoHash (torrentKey, infoHash) {
status: 'new' status: 'new'
} }
state.saved.torrents.push(torrentSummary) state.saved.torrents.push(torrentSummary)
playInterfaceSound('ADD') sound.play('ADD')
} }
torrentSummary.infoHash = infoHash torrentSummary.infoHash = infoHash
@@ -800,13 +808,13 @@ function openPlayer (infoHash, index, cb) {
if (index === undefined) return cb(new errors.UnplayableError()) if (index === undefined) return cb(new errors.UnplayableError())
// update UI to show pending playback // update UI to show pending playback
if (torrentSummary.progress !== 1) playInterfaceSound('PLAY') if (torrentSummary.progress !== 1) sound.play('PLAY')
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
update() update()
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
playInterfaceSound('ERROR') sound.play('ERROR')
cb(new Error('playback timed out')) cb(new Error('playback timed out'))
update() update()
}, 10000) /* give it a few seconds */ }, 10000) /* give it a few seconds */
@@ -903,11 +911,11 @@ function toggleTorrent (infoHash) {
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new' torrentSummary.status = 'new'
startTorrentingSummary(torrentSummary) startTorrentingSummary(torrentSummary)
playInterfaceSound('ENABLE') sound.play('ENABLE')
} else { } else {
torrentSummary.status = 'paused' torrentSummary.status = 'paused'
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash) ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
playInterfaceSound('DISABLE') sound.play('DISABLE')
} }
} }
@@ -919,7 +927,7 @@ function deleteTorrent (infoHash) {
if (index > -1) state.saved.torrents.splice(index, 1) if (index > -1) state.saved.torrents.splice(index, 1)
saveStateThrottled() saveStateThrottled()
state.location.clearForward() // prevent user from going forward to a deleted torrent state.location.clearForward() // prevent user from going forward to a deleted torrent
playInterfaceSound('DELETE') sound.play('DELETE')
} }
function toggleSelectTorrent (infoHash) { function toggleSelectTorrent (infoHash) {
@@ -930,18 +938,18 @@ function toggleSelectTorrent (infoHash) {
function openTorrentContextMenu (infoHash) { function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var menu = new remote.Menu() var menu = new Menu()
menu.append(new remote.MenuItem({ menu.append(new MenuItem({
label: 'Save Torrent File As...', label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary) click: () => saveTorrentFileAs(torrentSummary)
})) }))
menu.append(new remote.MenuItem({ menu.append(new MenuItem({
label: 'Copy Instant.io Link to Clipboard', label: 'Copy Instant.io Link to Clipboard',
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`) click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
})) }))
menu.append(new remote.MenuItem({ menu.append(new MenuItem({
label: 'Copy Magnet Link to Clipboard', label: 'Copy Magnet Link to Clipboard',
click: () => clipboard.writeText(torrentSummary.magnetURI) click: () => clipboard.writeText(torrentSummary.magnetURI)
})) }))
@@ -959,7 +967,7 @@ function saveTorrentFileAs (torrentSummary) {
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
] ]
} }
dialog.showSaveDialog(remote.getCurrentWindow(), opts, (savePath) => { dialog.showSaveDialog(remote.getCurrentWindow(), opts, function (savePath) {
var torrentPath = util.getAbsoluteStaticPath(torrentSummary.torrentPath) var torrentPath = util.getAbsoluteStaticPath(torrentSummary.torrentPath)
fs.readFile(torrentPath, function (err, torrentFile) { fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return onError(err) if (err) return onError(err)
@@ -1021,7 +1029,7 @@ function restoreBounds () {
function onError (err) { function onError (err) {
console.error(err.stack || err) console.error(err.stack || err)
playInterfaceSound('ERROR') sound.play('ERROR')
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
message: err.message || err message: err.message || err
@@ -1043,17 +1051,7 @@ function showDoneNotification (torrent) {
ipcRenderer.send('focusWindow', 'main') ipcRenderer.send('focusWindow', 'main')
} }
playInterfaceSound('DONE') sound.play('DONE')
}
function playInterfaceSound (name) {
var sound = config[`SOUND_${name}`]
if (!sound) throw new Error('Invalid sound name')
var audio = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
audio.play()
} }
// Finds the longest common prefix // Finds the longest common prefix

75
renderer/lib/sound.js Normal file
View File

@@ -0,0 +1,75 @@
module.exports = {
preload,
play
}
var config = require('../../config')
var path = require('path')
/* Cache of Audio elements, for instant playback */
var cache = {}
var sounds = {
ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: 0.2
},
DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: 0.1
},
DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: 0.2
},
DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
volume: 0.2
},
ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: 0.2
},
ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
volume: 0.2
},
POP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'pop.wav'),
volume: 0.2
},
PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: 0.2
},
STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: 0.4
}
}
function preload () {
for (var name in sounds) {
if (!cache[name]) {
var sound = sounds[name]
var audio = cache[name] = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
}
}
}
function play (name) {
var audio = cache[name]
if (!audio) {
var sound = sounds[name]
if (!sound) {
throw new Error('Invalid sound name')
}
audio = cache[name] = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
}
audio.currentTime = 0
audio.play()
}