UNPKG

mediasource

Version:

MediaSource API as a node.js Writable stream

291 lines (244 loc) 8.18 kB
/*! 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() }