web-audio-player
Version:
a cross-browser WebAudio player
188 lines (166 loc) • 5.54 kB
JavaScript
var EventEmitter = require('events').EventEmitter
var createAudio = require('simple-media-element').audio
var assign = require('object-assign')
var resume = require('./resume-context')
var createAudioContext = require('./audio-context')
var canPlaySrc = require('./can-play-src')
var addOnce = require('./event-add-once')
module.exports = createMediaSource
function createMediaSource (src, opt) {
opt = assign({}, opt)
var emitter = new EventEmitter()
// Default to Audio instead of HTMLAudioElement
// There is not much difference except in the following:
// x instanceof Audio
// x instanceof HTMLAudioElement
// And in my experience Audio has better support on various
// platforms like CocoonJS.
// Please open an issue if there is a concern with this.
if (!opt.element) opt.element = new window.Audio()
var desiredVolume = opt.volume
delete opt.volume // make sure <audio> tag receives full volume
var audio = createAudio(src, opt)
var audioContext = opt.context || createAudioContext()
var node = audioContext.createGain()
var mediaNode = audioContext.createMediaElementSource(audio)
mediaNode.connect(node)
audio.addEventListener('ended', function () {
emitter.emit('end')
})
var loopStart = opt.loopStart
var loopEnd = opt.loopEnd
var hasLoopStart = typeof loopStart === 'number' && isFinite(loopStart)
var hasLoopEnd = typeof loopEnd === 'number' && isFinite(loopEnd)
var isLoopReady = false
if (hasLoopStart || hasLoopEnd) {
window.requestAnimationFrame(function update () {
// audio hasn't been loaded yet...
if (typeof audio.duration !== 'number') return
var currentTime = audio.currentTime
// where to end the buffer
var endTime = hasLoopEnd ? Math.min(audio.duration, loopEnd) : audio.duration
if (currentTime > (loopStart || 0)) {
isLoopReady = true
}
// jump ahead to loop start point
if (hasLoopStart && isLoopReady && currentTime < loopStart) {
audio.currentTime = loopStart
}
// if we've hit the end of the buffer
if (currentTime >= endTime) {
// if there is no loop end point, let native looping take over
// if we have a loop end point, jump back to start point or zero
if (hasLoopEnd) {
audio.currentTime = hasLoopStart ? loopStart : 0
}
}
window.requestAnimationFrame(update)
});
}
emitter.element = audio
emitter.context = audioContext
emitter.node = node
emitter.pause = audio.pause.bind(audio)
emitter.play = function () {
if (opt.autoResume !== false) resume(emitter.context)
return audio.play()
}
// This exists currently for parity with Buffer source
// Open to suggestions for what this should dispose...
emitter.dispose = function () {}
emitter.stop = function () {
var wasPlaying = emitter.playing
audio.pause()
audio.currentTime = 0
isLoopReady = false
if (wasPlaying) {
emitter.emit('end')
}
}
Object.defineProperties(emitter, {
duration: {
enumerable: true, configurable: true,
get: function () {
return audio.duration
}
},
currentTime: {
enumerable: true, configurable: true,
get: function () {
return audio.currentTime
}
},
playing: {
enumerable: true, configurable: true,
get: function () {
return !audio.paused
}
},
volume: {
enumerable: true, configurable: true,
get: function () {
return node.gain.value
},
set: function (n) {
node.gain.value = n
}
}
})
// Set initial volume
if (typeof desiredVolume === 'number') {
emitter.volume = desiredVolume
}
// Check if all sources are unplayable,
// if so we emit an error since the browser
// might not.
var sources = Array.isArray(src) ? src : [ src ]
sources = sources.filter(Boolean)
var playable = sources.some(canPlaySrc)
if (playable) {
// At least one source is probably/maybe playable
startLoad()
} else {
// emit error on next tick so user can catch it
process.nextTick(function () {
emitter.emit('error', canPlaySrc.createError(sources))
})
}
return emitter
function startLoad () {
// The file errors (like decoding / 404s) appear on <source>
var srcElements = Array.prototype.slice.call(audio.children)
var remainingSrcErrors = srcElements.length
var hasErrored = false
var sourceError = function (err, el) {
if (hasErrored) return
remainingSrcErrors--
console.warn('Error loading source: ' + el.getAttribute('src'))
if (remainingSrcErrors <= 0) {
hasErrored = true
srcElements.forEach(function (el) {
el.removeEventListener('error', sourceError, false)
})
emitter.emit('error', new Error('Could not play any of the supplied sources'))
}
}
var done = function () {
emitter.emit('load')
}
if (audio.readyState >= audio.HAVE_ENOUGH_DATA) {
process.nextTick(done)
} else {
addOnce(audio, 'canplay', done)
addOnce(audio, 'error', function (ev) {
emitter.emit(new Error('Unknown error while loading <audio>'))
})
srcElements.forEach(function (el) {
addOnce(el, 'error', sourceError)
})
}
// On most browsers the loading begins
// immediately. However, on iOS 9.2 Safari,
// you need to call load() for events
// to be triggered.
audio.load()
}
}