Repository: mafintosh/mp4-stream Branch: master Commit: 2bdf2aad5fcc Files: 10 Total size: 14.7 KB Directory structure: gitextract_vw7dh3fz/ ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── decode.js ├── encode.js ├── index.js ├── package.json └── test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules sandbox ================================================ FILE: .npmignore ================================================ test.js .travis.yml ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '6' - '8' - '10' - '12' ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Mathias Buus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # mp4-stream Streaming mp4 encoder and decoder ``` npm install mp4-stream ``` [![build status](http://img.shields.io/travis/mafintosh/mp4-stream.svg?style=flat)](http://travis-ci.org/mafintosh/mp4-stream) ## Usage ``` js var mp4 = require('mp4-stream') var fs = require('fs') var decode = mp4.decode() fs.createReadStream('video.mp4') .pipe(decode) .on('box', function (headers) { console.log('found box (' + headers.type + ') (' + headers.length + ')') if (headers.type === 'mdat') { // you can get the contents as a stream console.log('box has stream data (consume stream to continue)') decode.stream().resume() } else if (headers.type === 'moof') { // you can ignore some boxes decode.ignore() } else { // or you can fully decode them decode.decode(function (box) { console.log('box contents:', box) }) } } }) ``` All boxes have a type thats a 4 char string with a type name. ## API #### `var stream = mp4.decode()` Create a new decoder. The decoder is a writable stream you should write a mp4 file to. It emits the following additional events: * `on('box', headers)` - emitted when a new box is found. Each time the `box` event fires, you must call one of these three functions: * `stream.ignore()` - ignore the entire box and continue parsing after its end * `stream.stream()` - get a readable stream of the box contents * `stream.decode(callback)` - decode the box, including all childeren in the case of containers, and pass the resulting box object to the callback ``` js var fs = require('fs') var stream = mp4.decode() stream.on('box', function (headers) { console.log('found new box:', headers) }) fs.createReadStream('my-video.mp4').pipe(stream) ``` #### `var stream = mp4.encode()` Create a new encoder. The encoder is a readable stream you can use to generate a mp4 file. It has the following API: * `stream.box(box, [callback])` - adds a new mp4 box to the stream. * `var ws = stream.mediaData(size)` - helper that adds an `mdat` box. write the media content to this stream. * `stream.finalize()` - finalizes the mp4 stream. call this when you're done. ``` js var fs = require('fs') var stream = mp4.encode() stream.pipe(fs.createWriteStream('my-new-video.mp4')) stream.box(anMP4Box, function (err) { // box flushed var content = stream.mediaData(lengthOfStream, function () { // wrote media data stream.finalize() }) someContent.pipe(content) }) ``` ## Decode and encode a file To decode and encode an mp4 file with this module do ``` js var encoder = mp4.encode() var decoder = mp4.decode() decoder.on('box', function (headers) { decoder.decode(function (box) { encoder.box(box, next) }) }) fs.createReadStream('my-movie.mp4').pipe(decoder) encoder.pipe(fs.createWriteStream('my-movie-copy.mp4')) ``` ## Boxes Mp4 supports a wide range of boxes, implemented in [mp4-box-encoding](https://github.com/jhiesey/mp4-box-encoding). ## License MIT ================================================ FILE: decode.js ================================================ var stream = require('readable-stream') var nextEvent = require('next-event') var Box = require('mp4-box-encoding') var EMPTY = Buffer.alloc(0) class Decoder extends stream.Writable { constructor (opts) { super(opts) this.destroyed = false this._pending = 0 this._missing = 0 this._ignoreEmpty = false this._buf = null this._str = null this._cb = null this._ondrain = null this._writeBuffer = null this._writeCb = null this._ondrain = null this._kick() } destroy (err) { if (this.destroyed) return this.destroyed = true if (err) this.emit('error', err) this.emit('close') } _write (data, enc, next) { if (this.destroyed) return var drained = !this._str || !this._str._writableState.needDrain while (data.length && !this.destroyed) { if (!this._missing && !this._ignoreEmpty) { this._writeBuffer = data this._writeCb = next return } var consumed = data.length < this._missing ? data.length : this._missing if (this._buf) data.copy(this._buf, this._buf.length - this._missing) else if (this._str) drained = this._str.write(consumed === data.length ? data : data.slice(0, consumed)) this._missing -= consumed if (!this._missing) { var buf = this._buf var cb = this._cb var stream = this._str this._buf = this._cb = this._str = this._ondrain = null drained = true this._ignoreEmpty = false if (stream) stream.end() if (cb) cb(buf) } data = consumed === data.length ? EMPTY : data.slice(consumed) } if (this._pending && !this._missing) { this._writeBuffer = data this._writeCb = next return } if (drained) next() else this._ondrain(next) } _buffer (size, cb) { this._missing = size this._buf = Buffer.alloc(size) this._cb = cb } _stream (size, cb) { this._missing = size this._str = new MediaData(this) this._ondrain = nextEvent(this._str, 'drain') this._pending++ this._str.on('end', () => { this._pending-- this._kick() }) this._cb = cb return this._str } _readBox () { const bufferHeaders = (len, buf) => { this._buffer(len, additionalBuf => { if (buf) { buf = Buffer.concat([buf, additionalBuf]) } else { buf = additionalBuf } var headers = Box.readHeaders(buf) if (typeof headers === 'number') { bufferHeaders(headers - buf.length, buf) } else { this._pending++ this._headers = headers this.emit('box', headers) } }) } bufferHeaders(8) } stream () { if (!this._headers) throw new Error('this function can only be called once after \'box\' is emitted') var headers = this._headers this._headers = null return this._stream(headers.contentLen, () => { this._pending-- this._kick() }) } decode (cb) { if (!this._headers) throw new Error('this function can only be called once after \'box\' is emitted') var headers = this._headers this._headers = null this._buffer(headers.contentLen, buf => { var box = Box.decodeWithoutHeaders(headers, buf) cb(box) this._pending-- this._kick() }) } ignore () { if (!this._headers) throw new Error('this function can only be called once after \'box\' is emitted') var headers = this._headers this._headers = null this._missing = headers.contentLen if (this._missing === 0) { this._ignoreEmpty = true } this._cb = () => { this._pending-- this._kick() } } _kick () { if (this._pending) return if (!this._buf && !this._str) this._readBox() if (this._writeBuffer) { var next = this._writeCb var buffer = this._writeBuffer this._writeBuffer = null this._writeCb = null this._write(buffer, null, next) } } } class MediaData extends stream.PassThrough { constructor (parent) { super() this._parent = parent this.destroyed = false } destroy (err) { if (this.destroyed) return this.destroyed = true this._parent.destroy(err) if (err) this.emit('error', err) this.emit('close') } } module.exports = Decoder ================================================ FILE: encode.js ================================================ var stream = require('readable-stream') var Box = require('mp4-box-encoding') var queueMicrotask = require('queue-microtask') function noop () {} class Encoder extends stream.Readable { constructor (opts) { super(opts) this.destroyed = false this._finalized = false this._reading = false this._stream = null this._drain = null this._want = false this._onreadable = () => { if (!this._want) return this._want = false this._read() } this._onend = () => { this._stream = null } } mdat (size, cb) { this.mediaData(size, cb) } mediaData (size, cb) { var stream = new MediaData(this) this.box({ type: 'mdat', contentLength: size, encodeBufferLen: 8, stream: stream }, cb) return stream } box (box, cb) { if (!cb) cb = noop if (this.destroyed) return cb(new Error('Encoder is destroyed')) var buf if (box.encodeBufferLen) { buf = Buffer.alloc(box.encodeBufferLen) } if (box.stream) { box.buffer = null buf = Box.encode(box, buf) this.push(buf) this._stream = box.stream this._stream.on('readable', this._onreadable) this._stream.on('end', this._onend) this._stream.on('end', cb) this._forward() } else { buf = Box.encode(box, buf) var drained = this.push(buf) if (drained) return queueMicrotask(cb) this._drain = cb } } destroy (err) { if (this.destroyed) return this.destroyed = true if (this._stream && this._stream.destroy) this._stream.destroy() this._stream = null if (this._drain) { var cb = this._drain this._drain = null cb(err) } if (err) this.emit('error', err) this.emit('close') } finalize () { this._finalized = true if (!this._stream && !this._drain) { this.push(null) } } _forward () { if (!this._stream) return while (!this.destroyed) { var buf = this._stream.read() if (!buf) { this._want = !!this._stream return } if (!this.push(buf)) return } } _read () { if (this._reading || this.destroyed) return this._reading = true if (this._stream) this._forward() if (this._drain) { var drain = this._drain this._drain = null drain() } this._reading = false if (this._finalized) { this.push(null) } } } class MediaData extends stream.PassThrough { constructor (parent) { super() this._parent = parent this.destroyed = false } destroy (err) { if (this.destroyed) return this.destroyed = true this._parent.destroy(err) if (err) this.emit('error', err) this.emit('close') } } module.exports = Encoder ================================================ FILE: index.js ================================================ const Decoder = require('./decode') const Encoder = require('./encode') exports.decode = opts => new Decoder(opts) exports.encode = opts => new Encoder(opts) ================================================ FILE: package.json ================================================ { "name": "mp4-stream", "version": "3.1.3", "description": "Streaming mp4 encoder and decoder", "main": "index.js", "dependencies": { "mp4-box-encoding": "^1.3.0", "next-event": "^1.0.0", "queue-microtask": "^1.2.2", "readable-stream": "^3.0.6" }, "devDependencies": { "standard": "^12.0.1", "tape": "^4.9.1" }, "scripts": { "test": "standard && tape test.js" }, "repository": { "type": "git", "url": "https://github.com/mafintosh/mp4-stream.git" }, "author": "Mathias Buus (@mafintosh)", "license": "MIT", "bugs": { "url": "https://github.com/mafintosh/mp4-stream/issues" }, "homepage": "https://github.com/mafintosh/mp4-stream" } ================================================ FILE: test.js ================================================ var tape = require('tape') var mp4 = require('./') tape('all boxes are decoded', function (t) { var encode = mp4.encode() var decode = mp4.decode() var count = 0 decode.on('box', function () { if (count === 0) { decode.decode(function () { }) } else if (count === 1) { decode.stream().on('data', function () { }) } else { decode.ignore() } count++ }) decode.on('finish', function () { t.same(count, 4) t.end() }) for (let i = 0; i < 4; i++) { encode.box({ type: 'ftyp', brand: 'mafi', brandVersion: 1 }) } encode.finalize() encode.pipe(decode) }) tape('generates and parses', function (t) { var encode = mp4.encode() var decode = mp4.decode() decode.on('box', function (headers) { if (headers.type === 'ftyp') { decode.decode(function (box) { t.same(box.type, 'ftyp') t.same(box.brand, 'mafi') t.same(box.brandVersion, 1) }) } else if (headers.type === 'mdat') { t.same(headers.type, 'mdat') t.same(headers.length, 8 + 11) var buffer = [] var stream = decode.stream() stream.on('data', function (data) { buffer.push(data) }) stream.on('end', function () { t.same(Buffer.concat(buffer).toString(), 'hello world') t.end() }) } else { t.fail('unexpected box') } }) encode.box({ type: 'ftyp', brand: 'mafi', brandVersion: 1 }) var stream = encode.mediaData(11) stream.end('hello world') encode.finalize() encode.pipe(decode) }) tape('generates and parses with decoder/encoder in between', function (t) { var encode = mp4.encode() var decode = mp4.decode() var encode2 = mp4.encode() var decode2 = mp4.decode() decode.on('box', function (headers) { if (headers.type === 'ftyp') { decode.decode(function (box) { t.same(box.type, 'ftyp') t.same(box.brand, 'mafi') t.same(box.brandVersion, 1) }) } else if (headers.type === 'mdat') { t.same(headers.type, 'mdat') t.same(headers.length, 8 + 11) var buffer = [] var stream = decode.stream() stream.on('data', function (data) { buffer.push(data) }) stream.on('end', function () { t.same(Buffer.concat(buffer).toString(), 'hello world') t.end() }) } else { t.fail('unexpected box') } }) encode.box({ type: 'ftyp', brand: 'mafi', brandVersion: 1 }) var stream = encode.mediaData(11) stream.end('hello world') encode.finalize() encode.pipe(decode2) decode2.on('box', function (headers) { decode2.decode(function (box) { encode2.box(box) }) }) decode2.on('end', function () { encode2.finalize() }) encode2.pipe(decode) })