diff --git a/externs/mux.js b/externs/mux.js index eed9222c3d..b85a6f0522 100644 --- a/externs/mux.js +++ b/externs/mux.js @@ -37,6 +37,12 @@ muxjs.mp4 = {}; muxjs.mp4.Transmuxer = function() {}; +/** + * @param {number} time + */ +muxjs.mp4.Transmuxer.prototype.setBaseMediaDecodeTime = function(time) {}; + + /** * @param {!Uint8Array} data */ @@ -62,6 +68,12 @@ muxjs.mp4.Transmuxer.prototype.on = function(type, listener) {}; muxjs.mp4.Transmuxer.prototype.off = function(type, listener) {}; +/** + * Remove all handlers and clean up. + */ +muxjs.mp4.Transmuxer.prototype.dispose = function() {}; + + /** * @typedef {{ * initSegment: !Uint8Array, diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 8f3c90be78..01782e6b85 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -435,10 +435,11 @@ shaka.media.MediaSourceEngine.prototype.getBuffered_ = function(contentType) { shaka.media.MediaSourceEngine.prototype.appendBuffer = function(contentType, data, startTime, endTime) { var ContentType = shaka.util.ManifestParserUtils.ContentType; + if (contentType == ContentType.TEXT) { return this.textEngine_.appendBuffer(data, startTime, endTime); } else if (this.transmuxers_[contentType]) { - return this.transmuxers_[contentType].transmux(data).then( + return this.transmuxers_[contentType].transmux(data, startTime).then( function(transmuxedData) { return this.enqueueOperation_(contentType, this.append_.bind(this, contentType, transmuxedData.buffer)); diff --git a/lib/media/transmuxer.js b/lib/media/transmuxer.js index 778f51491e..18f5eda851 100644 --- a/lib/media/transmuxer.js +++ b/lib/media/transmuxer.js @@ -46,13 +46,12 @@ shaka.media.Transmuxer = function() { /** @private {boolean} */ this.isTransmuxing_ = false; - this.onTransmuxed_ = this.onTransmuxed_.bind(this); - - this.onTransmuxDone_ = this.onTransmuxDone_.bind(this); + /** @private {boolean} */ + this.baseDecodeTimeSet_ = false; - this.muxTransmuxer_.on('data', this.onTransmuxed_); + this.muxTransmuxer_.on('data', this.onTransmuxed_.bind(this)); - this.muxTransmuxer_.on('done', this.onTransmuxDone_); + this.muxTransmuxer_.on('done', this.onTransmuxDone_.bind(this)); }; @@ -60,22 +59,12 @@ shaka.media.Transmuxer = function() { * @override */ shaka.media.Transmuxer.prototype.destroy = function() { - this.cleanUpListeners_(); + this.muxTransmuxer_.dispose(); this.muxTransmuxer_ = null; return Promise.resolve(); }; -/** - * Detaches all event listeners. - * @private - */ -shaka.media.Transmuxer.prototype.cleanUpListeners_ = function() { - this.muxTransmuxer_.off('data', this.onTransmuxed_); - this.muxTransmuxer_.off('done', this.onTransmuxDone_); -}; - - /** * Check if the content type is Transport Stream, and if muxjs is loaded. * @param {string} contentType @@ -118,14 +107,23 @@ shaka.media.Transmuxer.convertTsCodecs = function(contentType, tsMimeType) { /** * Transmux from Transport stream to MP4, using mux.js library. * @param {!ArrayBuffer} data + * @param {?number} startTime * @return {!Promise.} */ -shaka.media.Transmuxer.prototype.transmux = function(data) { +shaka.media.Transmuxer.prototype.transmux = function(data, startTime) { goog.asserts.assert(!this.isTransmuxing_, 'No transmuxing should be in progress.'); this.isTransmuxing_ = true; this.transmuxPromise_ = new shaka.util.PublicPromise(); this.transmuxedData_ = []; + + // TODO: remove this once videojs/mux.js#168 is solved + if (startTime != null && !this.baseDecodeTimeSet_) { + var timescale = shaka.media.Transmuxer.TS_TIMESCALE_; + this.muxTransmuxer_.setBaseMediaDecodeTime(startTime * timescale); + this.baseDecodeTimeSet_ = true; + } + var dataArray = new Uint8Array(data); this.muxTransmuxer_.push(dataArray); this.muxTransmuxer_.flush(); @@ -156,7 +154,15 @@ shaka.media.Transmuxer.prototype.onTransmuxed_ = function(segment) { * @private */ shaka.media.Transmuxer.prototype.onTransmuxDone_ = function() { - this.transmuxPromise_.resolve( - shaka.util.Uint8ArrayUtils.concat.apply(null, this.transmuxedData_)); + var output = + shaka.util.Uint8ArrayUtils.concat.apply(null, this.transmuxedData_); + this.transmuxPromise_.resolve(output); this.isTransmuxing_ = false; }; + + +/** + * @const {number} + * @private + */ +shaka.media.Transmuxer.TS_TIMESCALE_ = 90000; diff --git a/test/media/transmuxer_integration.js b/test/media/transmuxer_integration.js index 1c4952eddf..bd56366053 100644 --- a/test/media/transmuxer_integration.js +++ b/test/media/transmuxer_integration.js @@ -86,48 +86,73 @@ describe('Transmuxer', function() { describe('transmuxing', function() { it('transmux video from TS to MP4', function(done) { var sawMDAT = false; - transmuxer.transmux(videoSegment).then( - function(transmuxedData) { - expect(transmuxedData instanceof Uint8Array).toBe(true); - expect(transmuxedData.length).toBeGreaterThan(0); - new shaka.util.Mp4Parser() - .box('mdat', shaka.util.Mp4Parser.allData(function(data) { - sawMDAT = true; - expect(data.buffer.byteLength).toBeGreaterThan(0); - }.bind(this))).parse(transmuxedData.buffer); - expect(sawMDAT).toBeTruthy(); - }).catch(fail).then(done); + transmuxer.transmux(videoSegment, 0).then(function(transmuxedData) { + expect(transmuxedData instanceof Uint8Array).toBe(true); + expect(transmuxedData.length).toBeGreaterThan(0); + new shaka.util.Mp4Parser() + .box('mdat', shaka.util.Mp4Parser.allData(function(data) { + sawMDAT = true; + expect(data.buffer.byteLength).toBeGreaterThan(0); + })) + .parse(transmuxedData.buffer); + expect(sawMDAT).toBeTruthy(); + }).catch(fail).then(done); }); it('transmux audio from TS to MP4', function(done) { var sawMDAT = false; - transmuxer.transmux(audioSegment).then( - function(transmuxedData) { - expect(transmuxedData instanceof Uint8Array).toBe(true); - expect(transmuxedData.length).toBeGreaterThan(0); - new shaka.util.Mp4Parser() - .box('mdat', shaka.util.Mp4Parser.allData(function(data) { - sawMDAT = true; - expect(data.buffer.byteLength).toBeGreaterThan(0); - }.bind(this))).parse(transmuxedData.buffer); - expect(sawMDAT).toBeTruthy(); - - }).catch(fail).then(done); + transmuxer.transmux(audioSegment, 0).then(function(transmuxedData) { + expect(transmuxedData instanceof Uint8Array).toBe(true); + expect(transmuxedData.length).toBeGreaterThan(0); + new shaka.util.Mp4Parser() + .box('mdat', shaka.util.Mp4Parser.allData(function(data) { + sawMDAT = true; + expect(data.buffer.byteLength).toBeGreaterThan(0); + })) + .parse(transmuxedData.buffer); + expect(sawMDAT).toBeTruthy(); + }).catch(fail).then(done); }); it('transmux empty video from TS to MP4', function(done) { var sawMDAT = false; - transmuxer.transmux(emptySegment).then( - function(transmuxedData) { - expect(transmuxedData instanceof Uint8Array).toBe(true); - expect(transmuxedData.length).toBeGreaterThan(0); - new shaka.util.Mp4Parser() - .box('mdat', shaka.util.Mp4Parser.allData(function(data) { - sawMDAT = true; - }.bind(this))).parse(transmuxedData.buffer); - expect(sawMDAT).toBeFalsy(); - }).catch(fail).then(done); + transmuxer.transmux(emptySegment, 0).then(function(transmuxedData) { + expect(transmuxedData instanceof Uint8Array).toBe(true); + expect(transmuxedData.length).toBeGreaterThan(0); + new shaka.util.Mp4Parser() + .box('mdat', shaka.util.Mp4Parser.allData(function(data) { + sawMDAT = true; + })) + .parse(transmuxedData.buffer); + expect(sawMDAT).toBeFalsy(); + }).catch(fail).then(done); }); + it('offsets output timestamps', function(done) { + var parsed = false; + var expectedMp4Timestamp = 123 * 90000; // timescale units + var mp4Timestamp; + + transmuxer.transmux(videoSegment, 123).then(function(transmuxedData) { + var Mp4Parser = shaka.util.Mp4Parser; + + new Mp4Parser() + .box('moof', Mp4Parser.children) + .box('traf', Mp4Parser.children) + .fullBox('tfdt', function(box) { + goog.asserts.assert( + box.version == 0 || box.version == 1, + 'TFDT version can only be 0 or 1'); + mp4Timestamp = (box.version == 0) ? + box.reader.readUint32() : + box.reader.readUint64(); + parsed = true; + }) + .parse(transmuxedData.buffer); + + expect(parsed).toBe(true); + expect(mp4Timestamp).toEqual(expectedMp4Timestamp); + }).catch(fail).then(done); + }); }); });