mediasource
Version:
MediaSource API as a node.js Writable stream
291 lines (244 loc) • 8.18 kB
JavaScript
/*! mediasource. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
module.exports = MediaElementWrapper
var inherits = require('inherits')
var stream = require('readable-stream')
var toArrayBuffer = require('to-arraybuffer')
var MediaSource = typeof window !== 'undefined' && window.MediaSource
var DEFAULT_BUFFER_DURATION = 60 // seconds
function MediaElementWrapper (elem, opts) {
var self = this
if (!(self instanceof MediaElementWrapper)) return new MediaElementWrapper(elem, opts)
if (!MediaSource) throw new Error('web browser lacks MediaSource support')
if (!opts) opts = {}
self._debug = opts.debug
self._bufferDuration = opts.bufferDuration || DEFAULT_BUFFER_DURATION
self._elem = elem
self._mediaSource = new MediaSource()
self._streams = []
self.detailedError = null
self._errorHandler = function () {
self._elem.removeEventListener('error', self._errorHandler)
var streams = self._streams.slice()
streams.forEach(function (stream) {
stream.destroy(self._elem.error)
})
}
self._elem.addEventListener('error', self._errorHandler)
self._elem.src = window.URL.createObjectURL(self._mediaSource)
}
/*
* `obj` can be a previous value returned by this function
* or a string
*/
MediaElementWrapper.prototype.createWriteStream = function (obj) {
var self = this
return new MediaSourceStream(self, obj)
}
/*
* Use to trigger an error on the underlying media element
*/
MediaElementWrapper.prototype.error = function (err) {
var self = this
// be careful not to overwrite any existing detailedError values
if (!self.detailedError) {
self.detailedError = err
}
self._dumpDebugData()
try {
self._mediaSource.endOfStream('decode')
} catch (err) {}
try {
// Attempt to clean up object URL
window.URL.revokeObjectURL(self._elem.src)
} catch (err) {}
}
/*
* When self._debug is set, dump all data to files
*/
MediaElementWrapper.prototype._dumpDebugData = function () {
var self = this
if (self._debug) {
self._debug = false // prevent multiple dumps on multiple errors
self._streams.forEach(function (stream, i) {
downloadBuffers(stream._debugBuffers, 'mediasource-stream-' + i)
})
}
}
inherits(MediaSourceStream, stream.Writable)
function MediaSourceStream (wrapper, obj) {
var self = this
stream.Writable.call(self)
self._wrapper = wrapper
self._elem = wrapper._elem
self._mediaSource = wrapper._mediaSource
self._allStreams = wrapper._streams
self._allStreams.push(self)
self._bufferDuration = wrapper._bufferDuration
self._sourceBuffer = null
self._debugBuffers = []
self._openHandler = function () {
self._onSourceOpen()
}
self._flowHandler = function () {
self._flow()
}
self._errorHandler = function (err) {
if (!self.destroyed) {
self.emit('error', err)
}
}
if (typeof obj === 'string') {
self._type = obj
// Need to create a new sourceBuffer
if (self._mediaSource.readyState === 'open') {
self._createSourceBuffer()
} else {
self._mediaSource.addEventListener('sourceopen', self._openHandler)
}
} else if (obj._sourceBuffer === null) {
obj.destroy()
self._type = obj._type // The old stream was created but hasn't finished initializing
self._mediaSource.addEventListener('sourceopen', self._openHandler)
} else if (obj._sourceBuffer) {
obj.destroy()
self._type = obj._type
self._sourceBuffer = obj._sourceBuffer // Copy over the old sourceBuffer
self._debugBuffers = obj._debugBuffers // Copy over previous debug data
self._sourceBuffer.addEventListener('updateend', self._flowHandler)
self._sourceBuffer.addEventListener('error', self._errorHandler)
} else {
throw new Error('The argument to MediaElementWrapper.createWriteStream must be a string or a previous stream returned from that function')
}
self._elem.addEventListener('timeupdate', self._flowHandler)
self.on('error', function (err) {
self._wrapper.error(err)
})
self.on('finish', function () {
if (self.destroyed) return
self._finished = true
if (self._allStreams.every(function (other) { return other._finished })) {
self._wrapper._dumpDebugData()
try {
self._mediaSource.endOfStream()
} catch (err) {}
}
})
}
MediaSourceStream.prototype._onSourceOpen = function () {
var self = this
if (self.destroyed) return
self._mediaSource.removeEventListener('sourceopen', self._openHandler)
self._createSourceBuffer()
}
MediaSourceStream.prototype.destroy = function (err) {
var self = this
if (self.destroyed) return
self.destroyed = true
// Remove from allStreams
self._allStreams.splice(self._allStreams.indexOf(self), 1)
self._mediaSource.removeEventListener('sourceopen', self._openHandler)
self._elem.removeEventListener('timeupdate', self._flowHandler)
if (self._sourceBuffer) {
self._sourceBuffer.removeEventListener('updateend', self._flowHandler)
self._sourceBuffer.removeEventListener('error', self._errorHandler)
if (self._mediaSource.readyState === 'open') {
self._sourceBuffer.abort()
}
}
if (err) self.emit('error', err)
self.emit('close')
}
MediaSourceStream.prototype._createSourceBuffer = function () {
var self = this
if (self.destroyed) return
if (MediaSource.isTypeSupported(self._type)) {
self._sourceBuffer = self._mediaSource.addSourceBuffer(self._type)
self._sourceBuffer.addEventListener('updateend', self._flowHandler)
self._sourceBuffer.addEventListener('error', self._errorHandler)
if (self._cb) {
var cb = self._cb
self._cb = null
cb()
}
} else {
self.destroy(new Error('The provided type is not supported'))
}
}
MediaSourceStream.prototype._write = function (chunk, encoding, cb) {
var self = this
if (self.destroyed) return
if (!self._sourceBuffer) {
self._cb = function (err) {
if (err) return cb(err)
self._write(chunk, encoding, cb)
}
return
}
if (self._sourceBuffer.updating) {
return cb(new Error('Cannot append buffer while source buffer updating'))
}
var arr = toArrayBuffer(chunk)
if (self._wrapper._debug) {
self._debugBuffers.push(arr)
}
try {
self._sourceBuffer.appendBuffer(arr)
} catch (err) {
// appendBuffer can throw for a number of reasons, most notably when the data
// being appended is invalid or if appendBuffer is called after another error
// already occurred on the media element. In Chrome, there may be useful debugging
// info in chrome://media-internals
self.destroy(err)
return
}
self._cb = cb
}
MediaSourceStream.prototype._flow = function () {
var self = this
if (self.destroyed || !self._sourceBuffer || self._sourceBuffer.updating) {
return
}
if (self._mediaSource.readyState === 'open') {
// check buffer size
if (self._getBufferDuration() > self._bufferDuration) {
return
}
}
if (self._cb) {
var cb = self._cb
self._cb = null
cb()
}
}
// TODO: if zero actually works in all browsers, remove the logic associated with this below
var EPSILON = 0
MediaSourceStream.prototype._getBufferDuration = function () {
var self = this
var buffered = self._sourceBuffer.buffered
var currentTime = self._elem.currentTime
var bufferEnd = -1 // end of the buffer
// This is a little over complex because some browsers seem to separate the
// buffered region into multiple sections with slight gaps.
for (var i = 0; i < buffered.length; i++) {
var start = buffered.start(i)
var end = buffered.end(i) + EPSILON
if (start > currentTime) {
// Reached past the joined buffer
break
} else if (bufferEnd >= 0 || currentTime <= end) {
// Found the start/continuation of the joined buffer
bufferEnd = end
}
}
var bufferedTime = bufferEnd - currentTime
if (bufferedTime < 0) {
bufferedTime = 0
}
return bufferedTime
}
function downloadBuffers (bufs, name) {
var a = document.createElement('a')
a.href = window.URL.createObjectURL(new window.Blob(bufs))
a.download = name
a.click()
}