diff options
| author | 8cy <[email protected]> | 2020-04-30 15:46:16 -0700 |
|---|---|---|
| committer | 8cy <[email protected]> | 2020-04-30 15:46:16 -0700 |
| commit | 3a4deac89054021b56ad5bd8005b2044cc085c98 (patch) | |
| tree | 3dd6af8503e497e46180b6b5231674f36bdce9f2 /node_modules/prism-media/src/core | |
| download | uppity-3a4deac89054021b56ad5bd8005b2044cc085c98.tar.xz uppity-3a4deac89054021b56ad5bd8005b2044cc085c98.zip | |
Up, up, uppity.
Diffstat (limited to 'node_modules/prism-media/src/core')
| -rw-r--r-- | node_modules/prism-media/src/core/FFmpeg.js | 147 | ||||
| -rw-r--r-- | node_modules/prism-media/src/core/VolumeTransformer.js | 129 | ||||
| -rw-r--r-- | node_modules/prism-media/src/core/WebmBase.js | 199 | ||||
| -rw-r--r-- | node_modules/prism-media/src/core/index.js | 9 |
4 files changed, 484 insertions, 0 deletions
diff --git a/node_modules/prism-media/src/core/FFmpeg.js b/node_modules/prism-media/src/core/FFmpeg.js new file mode 100644 index 0000000..fa156cc --- /dev/null +++ b/node_modules/prism-media/src/core/FFmpeg.js @@ -0,0 +1,147 @@ +const ChildProcess = require('child_process'); +const { Duplex } = require('stream'); + +let FFMPEG = { + command: null, + output: null, +}; + +const VERSION_REGEX = /version (.+) Copyright/mi; + +Object.defineProperty(FFMPEG, 'version', { + get() { + return VERSION_REGEX.exec(FFMPEG.output)[1]; + }, + enumerable: true, +}); + +/** + * An FFmpeg transform stream that provides an interface to FFmpeg. + * @memberof core + */ +class FFmpeg extends Duplex { + /** + * Creates a new FFmpeg transform stream + * @memberof core + * @param {Object} options Options you would pass to a regular Transform stream, plus an `args` option + * @param {Array<string>} options.args Arguments to pass to FFmpeg + * @example + * // By default, if you don't specify an input (`-i ...`) prism will assume you're piping a stream into it. + * const transcoder = new prism.FFmpeg({ + * args: [ + * '-analyzeduration', '0', + * '-loglevel', '0', + * '-f', 's16le', + * '-ar', '48000', + * '-ac', '2', + * ] + * }); + * const s16le = mp3File.pipe(transcoder); + * const opus = s16le.pipe(new prism.opus.Encoder({ rate: 48000, channels: 2, frameSize: 960 })); + */ + constructor(options = {}) { + super(); + this.process = FFmpeg.create(options); + const EVENTS = { + readable: this._reader, + data: this._reader, + end: this._reader, + unpipe: this._reader, + finish: this._writer, + drain: this._writer, + }; + + this._readableState = this._reader._readableState; + this._writableState = this._writer._writableState; + + this._copy(['write', 'end'], this._writer); + this._copy(['read', 'setEncoding', 'pipe', 'unpipe'], this._reader); + + for (const method of ['on', 'once', 'removeListener', 'removeListeners', 'listeners']) { + this[method] = (ev, fn) => EVENTS[ev] ? EVENTS[ev][method](ev, fn) : Duplex.prototype[method].call(this, ev, fn); + } + + const processError = error => this.emit('error', error); + this._reader.on('error', processError); + this._writer.on('error', processError); + } + + get _reader() { return this.process.stdout; } + get _writer() { return this.process.stdin; } + + _copy(methods, target) { + for (const method of methods) { + this[method] = target[method].bind(target); + } + } + + _destroy(err, cb) { + super._destroy(err, cb); + this.once('error', () => {}); + this.process.kill('SIGKILL'); + } + + + /** + * The available FFmpeg information + * @typedef {Object} FFmpegInfo + * @memberof core + * @property {string} command The command used to launch FFmpeg + * @property {string} output The output from running `ffmpeg -h` + * @property {string} version The version of FFmpeg being used, determined from `output`. + */ + + /** + * Finds a suitable FFmpeg command and obtains the debug information from it. + * @param {boolean} [force=false] If true, will ignore any cached results and search for the command again + * @returns {FFmpegInfo} + * @throws Will throw an error if FFmpeg cannot be found. + * @example + * const ffmpeg = prism.FFmpeg.getInfo(); + * + * console.log(`Using FFmpeg version ${ffmpeg.version}`); + * + * if (ffmpeg.output.includes('--enable-libopus')) { + * console.log('libopus is available!'); + * } else { + * console.log('libopus is unavailable!'); + * } + */ + static getInfo(force = false) { + if (FFMPEG.command && !force) return FFMPEG; + const sources = [() => { + const ffmpegStatic = require('ffmpeg-static'); + return ffmpegStatic.path || ffmpegStatic; + }, 'ffmpeg', 'avconv', './ffmpeg', './avconv']; + for (let source of sources) { + try { + if (typeof source === 'function') source = source(); + const result = ChildProcess.spawnSync(source, ['-h'], { windowsHide: true }); + if (result.error) throw result.error; + Object.assign(FFMPEG, { + command: source, + output: Buffer.concat(result.output.filter(Boolean)).toString(), + }); + return FFMPEG; + } catch (error) { + // Do nothing + } + } + throw new Error('FFmpeg/avconv not found!'); + } + + /** + * Creates a new FFmpeg instance. If you do not include `-i ...` it will be assumed that `-i -` should be prepended + * to the options and that you'll be piping data into the process. + * @param {String[]} [args=[]] Arguments to pass to FFmpeg + * @returns {ChildProcess} + * @private + * @throws Will throw an error if FFmpeg cannot be found. + */ + static create({ args = [] } = {}) { + if (!args.includes('-i')) args.unshift('-i', '-'); + return ChildProcess.spawn(FFmpeg.getInfo().command, args.concat(['pipe:1']), { windowsHide: true }); + } +} + +module.exports = FFmpeg; diff --git a/node_modules/prism-media/src/core/VolumeTransformer.js b/node_modules/prism-media/src/core/VolumeTransformer.js new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/node_modules/prism-media/src/core/VolumeTransformer.js @@ -0,0 +1,129 @@ +// Based on discord.js' old volume system + +const { Transform } = require('stream'); + +/** + * Transforms a stream of PCM volume. + * @memberof core + * @extends TransformStream + */ +class VolumeTransformer extends Transform { + /** + * @memberof core + * @param {Object} options Any optional TransformStream options plus some extra: + * @param {string} options.type The type of transformer: s16le (signed 16-bit little-endian), s16be, s32le, s32be + * @param {number} [options.volume=1] The output volume of the stream + * @example + * // Half the volume of a signed 16-bit little-endian PCM stream + * input + * .pipe(new prism.VolumeTransformer({ type: 's16le', volume: 0.5 })) + * .pipe(writeStream); + */ + constructor(options = {}) { + super(options); + switch (options.type) { + case 's16le': + this._readInt = (buffer, index) => buffer.readInt16LE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt16LE(int, index); + this._bits = 16; + break; + case 's16be': + this._readInt = (buffer, index) => buffer.readInt16BE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt16BE(int, index); + this._bits = 16; + break; + case 's32le': + this._readInt = (buffer, index) => buffer.readInt32LE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt32LE(int, index); + this._bits = 32; + break; + case 's32be': + this._readInt = (buffer, index) => buffer.readInt32BE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt32BE(int, index); + this._bits = 32; + break; + default: + throw new Error('VolumeTransformer type should be one of s16le, s16be, s32le, s32be'); + } + this._bytes = this._bits / 8; + this._extremum = Math.pow(2, this._bits - 1); + this.volume = typeof options.volume === 'undefined' ? 1 : options.volume; + this._chunk = Buffer.alloc(0); + } + + _readInt(buffer, index) { return index; } + _writeInt(buffer, int, index) { return index; } + + _transform(chunk, encoding, done) { + // If the volume is 1, act like a passthrough stream + if (this.volume === 1) { + this.push(chunk); + return done(); + } + + const { _bytes, _extremum } = this; + + chunk = this._chunk = Buffer.concat([this._chunk, chunk]); + if (chunk.length < _bytes) return done(); + + const transformed = Buffer.allocUnsafe(chunk.length); + const complete = Math.floor(chunk.length / _bytes) * _bytes; + + for (let i = 0; i < complete; i += _bytes) { + const int = Math.min(_extremum - 1, Math.max(-_extremum, Math.floor(this.volume * this._readInt(chunk, i)))); + this._writeInt(transformed, int, i); + } + + this._chunk = chunk.slice(complete); + this.push(transformed); + return done(); + } + + _destroy(err, cb) { + super._destroy(err, cb); + this._chunk = null; + } + + /** + * Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double. + * @param {number} volume The volume that you want to set + */ + setVolume(volume) { + this.volume = volume; + } + + /** + * Sets the volume in decibels. + * @param {number} db The decibels + */ + setVolumeDecibels(db) { + this.setVolume(Math.pow(10, db / 20)); + } + + /** + * Sets the volume so that a perceived value of 0.5 is half the perceived volume etc. + * @param {number} value The value for the volume + */ + setVolumeLogarithmic(value) { + this.setVolume(Math.pow(value, 1.660964)); + } + + /** + * The current volume of the stream in decibels + * @readonly + * @type {number} + */ + get volumeDecibels() { + return Math.log10(this._volume) * 20; + } + /** + * The current volume of the stream from a logarithmic scale + * @readonly + * @type {number} + */ + get volumeLogarithmic() { + return Math.pow(this._volume, 1 / 1.660964); + } +} + +module.exports = VolumeTransformer; diff --git a/node_modules/prism-media/src/core/WebmBase.js b/node_modules/prism-media/src/core/WebmBase.js new file mode 100644 index 0000000..76dcdca --- /dev/null +++ b/node_modules/prism-media/src/core/WebmBase.js @@ -0,0 +1,199 @@ +const { Transform } = require('stream'); + +/** + * Base class for WebmOpusDemuxer and WebmVorbisDemuxer. + * **You shouldn't directly instantiate this class, use the opus.WebmDemuxer and vorbis.WebmDemuxer + * implementations instead!** + * @memberof core + * @protected + * @extends TransformStream + */ +class WebmBaseDemuxer extends Transform { + /** + * Creates a new Webm demuxer. + * @private + * @memberof core + * @param {Object} [options] options that you would pass to a regular Transform stream. + */ + constructor(options = {}) { + super(Object.assign({ readableObjectMode: true }, options)); + this._remainder = null; + this._length = 0; + this._count = 0; + this._skipUntil = null; + this._track = null; + this._incompleteTrack = {}; + this._ebmlFound = false; + } + + _transform(chunk, encoding, done) { + this._length += chunk.length; + if (this._remainder) { + chunk = Buffer.concat([this._remainder, chunk]); + this._remainder = null; + } + let offset = 0; + if (this._skipUntil && this._length > this._skipUntil) { + offset = this._skipUntil - this._count; + this._skipUntil = null; + } else if (this._skipUntil) { + this._count += chunk.length; + return done(); + } + let result; + while (result !== TOO_SHORT) { + result = this._readTag(chunk, offset); + if (result === TOO_SHORT) break; + if (result._skipUntil) { + this._skipUntil = result._skipUntil; + break; + } + if (result.offset) offset = result.offset; + else break; + } + this._count += offset; + this._remainder = chunk.slice(offset); + return done(); + } + + /** + * Reads an EBML ID from a buffer. + * @private + * @param {Buffer} chunk the buffer to read from. + * @param {number} offset the offset in the buffer. + * @returns {Object|Symbol} contains an `id` property (buffer) and the new `offset` (number). + * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. + */ + _readEBMLId(chunk, offset) { + const idLength = vintLength(chunk, offset); + if (idLength === TOO_SHORT) return TOO_SHORT; + return { + id: chunk.slice(offset, offset + idLength), + offset: offset + idLength, + }; + } + + /** + * Reads a size variable-integer to calculate the length of the data of a tag. + * @private + * @param {Buffer} chunk the buffer to read from. + * @param {number} offset the offset in the buffer. + * @returns {Object|Symbol} contains property `offset` (number), `dataLength` (number) and `sizeLength` (number). + * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. + */ + _readTagDataSize(chunk, offset) { + const sizeLength = vintLength(chunk, offset); + if (sizeLength === TOO_SHORT) return TOO_SHORT; + const dataLength = expandVint(chunk, offset, offset + sizeLength); + return { offset: offset + sizeLength, dataLength, sizeLength }; + } + + /** + * Takes a buffer and attempts to read and process a tag. + * @private + * @param {Buffer} chunk the buffer to read from. + * @param {number} offset the offset in the buffer. + * @returns {Object|Symbol} contains the new `offset` (number) and optionally the `_skipUntil` property, + * indicating that the stream should ignore any data until a certain length is reached. + * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. + */ + _readTag(chunk, offset) { + const idData = this._readEBMLId(chunk, offset); + if (idData === TOO_SHORT) return TOO_SHORT; + const ebmlID = idData.id.toString('hex'); + if (!this._ebmlFound) { + if (ebmlID === '1a45dfa3') this._ebmlFound = true; + else throw Error('Did not find the EBML tag at the start of the stream'); + } + offset = idData.offset; + const sizeData = this._readTagDataSize(chunk, offset); + if (sizeData === TOO_SHORT) return TOO_SHORT; + const { dataLength } = sizeData; + offset = sizeData.offset; + // If this tag isn't useful, tell the stream to stop processing data until the tag ends + if (typeof TAGS[ebmlID] === 'undefined') { + if (chunk.length > offset + dataLength) { + return { offset: offset + dataLength }; + } + return { offset, _skipUntil: this._count + offset + dataLength }; + } + + const tagHasChildren = TAGS[ebmlID]; + if (tagHasChildren) { + return { offset }; + } + + if (offset + dataLength > chunk.length) return TOO_SHORT; + const data = chunk.slice(offset, offset + dataLength); + if (!this._track) { + if (ebmlID === 'ae') this._incompleteTrack = {}; + if (ebmlID === 'd7') this._incompleteTrack.number = data[0]; + if (ebmlID === '83') this._incompleteTrack.type = data[0]; + if (this._incompleteTrack.type === 2 && typeof this._incompleteTrack.number !== 'undefined') { + this._track = this._incompleteTrack; + } + } + if (ebmlID === '63a2') { + this._checkHead(data); + } else if (ebmlID === 'a3') { + if (!this._track) throw Error('No audio track in this webm!'); + if ((data[0] & 0xF) === this._track.number) { + this.push(data.slice(4)); + } + } + return { offset: offset + dataLength }; + } +} + +/** + * A symbol that is returned by some functions that indicates the buffer it has been provided is not large enough + * to facilitate a request. + * @name WebmBaseDemuxer#TOO_SHORT + * @memberof core + * @private + * @type {Symbol} + */ +const TOO_SHORT = WebmBaseDemuxer.TOO_SHORT = Symbol('TOO_SHORT'); + +/** + * A map that takes a value of an EBML ID in hex string form, with the value being a boolean that indicates whether + * this tag has children. + * @name WebmBaseDemuxer#TAGS + * @memberof core + * @private + * @type {Object} + */ +const TAGS = WebmBaseDemuxer.TAGS = { // value is true if the element has children + '1a45dfa3': true, // EBML + '18538067': true, // Segment + '1f43b675': true, // Cluster + '1654ae6b': true, // Tracks + 'ae': true, // TrackEntry + 'd7': false, // TrackNumber + '83': false, // TrackType + 'a3': false, // SimpleBlock + '63a2': false, +}; + +module.exports = WebmBaseDemuxer; + +function vintLength(buffer, index) { + let i = 0; + for (; i < 8; i++) if ((1 << (7 - i)) & buffer[index]) break; + i++; + if (index + i > buffer.length) { + return TOO_SHORT; + } + return i; +} + +function expandVint(buffer, start, end) { + const length = vintLength(buffer, start); + if (end > buffer.length || length === TOO_SHORT) return TOO_SHORT; + let mask = (1 << (8 - length)) - 1; + let value = buffer[start] & mask; + for (let i = start + 1; i < end; i++) { + value = (value << 8) + buffer[i]; + } + return value; +} diff --git a/node_modules/prism-media/src/core/index.js b/node_modules/prism-media/src/core/index.js new file mode 100644 index 0000000..ae9967f --- /dev/null +++ b/node_modules/prism-media/src/core/index.js @@ -0,0 +1,9 @@ +/** + * Core features. + * **You shouldn't prefix imports from this namespace with `core`.** + * @namespace core + */ +module.exports = { + FFmpeg: require('./FFmpeg'), + VolumeTransformer: require('./VolumeTransformer'), +}; |