UNPKG

videojs-contrib-media-sources

Version:

A Media Source Extensions plugin for video.js

1,396 lines (1,147 loc) 1.17 MB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /** * @file add-text-track-data.js */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); var _videoJs = require('video.js'); var _videoJs2 = _interopRequireDefault(_videoJs); /** * Define properties on a cue for backwards compatability, * but warn the user that the way that they are using it * is depricated and will be removed at a later date. * * @param {Cue} cue the cue to add the properties on * @private */ var deprecateOldCue = function deprecateOldCue(cue) { Object.defineProperties(cue.frame, { id: { get: function get() { _videoJs2['default'].log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); return cue.value.key; } }, value: { get: function get() { _videoJs2['default'].log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); return cue.value.data; } }, privateData: { get: function get() { _videoJs2['default'].log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); return cue.value.data; } } }); }; var durationOfVideo = function durationOfVideo(duration) { var dur = undefined; if (isNaN(duration) || Math.abs(duration) === Infinity) { dur = Number.MAX_VALUE; } else { dur = duration; } return dur; }; /** * Add text track data to a source handler given the captions and * metadata from the buffer. * * @param {Object} sourceHandler the flash or virtual source buffer * @param {Array} captionArray an array of caption data * @param {Array} metadataArray an array of meta data * @private */ var addTextTrackData = function addTextTrackData(sourceHandler, captionArray, metadataArray) { var Cue = _globalWindow2['default'].WebKitDataCue || _globalWindow2['default'].VTTCue; if (captionArray) { captionArray.forEach(function (caption) { var track = caption.stream; this.inbandTextTracks_[track].addCue(new Cue(caption.startTime + this.timestampOffset, caption.endTime + this.timestampOffset, caption.text)); }, sourceHandler); } if (metadataArray) { (function () { var videoDuration = durationOfVideo(sourceHandler.mediaSource_.duration); metadataArray.forEach(function (metadata) { var time = metadata.cueTime + this.timestampOffset; metadata.frames.forEach(function (frame) { var cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); cue.frame = frame; cue.value = frame; deprecateOldCue(cue); this.metadataTrack_.addCue(cue); }, this); }, sourceHandler); // Updating the metadeta cues so that // the endTime of each cue is the startTime of the next cue // the endTime of last cue is the duration of the video if (sourceHandler.metadataTrack_ && sourceHandler.metadataTrack_.cues && sourceHandler.metadataTrack_.cues.length) { (function () { var cues = sourceHandler.metadataTrack_.cues; var cuesArray = []; // Create a copy of the TextTrackCueList... // ...disregarding cues with a falsey value for (var i = 0; i < cues.length; i++) { if (cues[i]) { cuesArray.push(cues[i]); } } // Group cues by their startTime value var cuesGroupedByStartTime = cuesArray.reduce(function (obj, cue) { var timeSlot = obj[cue.startTime] || []; timeSlot.push(cue); obj[cue.startTime] = timeSlot; return obj; }, {}); // Sort startTimes by ascending order var sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort(function (a, b) { return Number(a) - Number(b); }); // Map each cue group's endTime to the next group's startTime sortedStartTimes.forEach(function (startTime, idx) { var cueGroup = cuesGroupedByStartTime[startTime]; var nextTime = Number(sortedStartTimes[idx + 1]) || videoDuration; // Map each cue's endTime the next group's startTime cueGroup.forEach(function (cue) { cue.endTime = nextTime; }); }); })(); } })(); } }; exports['default'] = { addTextTrackData: addTextTrackData, durationOfVideo: durationOfVideo }; module.exports = exports['default']; },{"global/window":16,"video.js":135}],2:[function(require,module,exports){ /** * @file codec-utils.js */ /** * Check if a codec string refers to an audio codec. * * @param {String} codec codec string to check * @return {Boolean} if this is an audio codec * @private */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var isAudioCodec = function isAudioCodec(codec) { return (/mp4a\.\d+.\d+/i.test(codec) ); }; /** * Check if a codec string refers to a video codec. * * @param {String} codec codec string to check * @return {Boolean} if this is a video codec * @private */ var isVideoCodec = function isVideoCodec(codec) { return (/avc1\.[\da-f]+/i.test(codec) ); }; /** * Parse a content type header into a type and parameters * object * * @param {String} type the content type header * @return {Object} the parsed content-type * @private */ var parseContentType = function parseContentType(type) { var object = { type: '', parameters: {} }; var parameters = type.trim().split(';'); // first parameter should always be content-type object.type = parameters.shift().trim(); parameters.forEach(function (parameter) { var pair = parameter.trim().split('='); if (pair.length > 1) { var _name = pair[0].replace(/"/g, '').trim(); var value = pair[1].replace(/"/g, '').trim(); object.parameters[_name] = value; } }); return object; }; /** * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard * `avc1.<hhhhhh>` * * @param {Array} codecs an array of codec strings to fix * @return {Array} the translated codec array * @private */ var translateLegacyCodecs = function translateLegacyCodecs(codecs) { return codecs.map(function (codec) { return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { var profileHex = ('00' + Number(profile).toString(16)).slice(-2); var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); return 'avc1.' + profileHex + '00' + avcLevelHex; }); }); }; exports['default'] = { isAudioCodec: isAudioCodec, parseContentType: parseContentType, isVideoCodec: isVideoCodec, translateLegacyCodecs: translateLegacyCodecs }; module.exports = exports['default']; },{}],3:[function(require,module,exports){ /** * @file create-text-tracks-if-necessary.js */ /** * Create text tracks on video.js if they exist on a segment. * * @param {Object} sourceBuffer the VSB or FSB * @param {Object} mediaSource the HTML or Flash media source * @param {Object} segment the segment that may contain the text track * @private */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var createTextTracksIfNecessary = function createTextTracksIfNecessary(sourceBuffer, mediaSource, segment) { var player = mediaSource.player_; // create an in-band caption track if one is present in the segment if (segment.captions && segment.captions.length) { if (!sourceBuffer.inbandTextTracks_) { sourceBuffer.inbandTextTracks_ = {}; } for (var trackId in segment.captionStreams) { if (!sourceBuffer.inbandTextTracks_[trackId]) { player.tech_.trigger({ type: 'usage', name: 'hls-608' }); var track = player.textTracks().getTrackById(trackId); if (track) { // Resuse an existing track with a CC# id because this was // very likely created by videojs-contrib-hls from information // in the m3u8 for us to use sourceBuffer.inbandTextTracks_[trackId] = track; } else { // Otherwise, create a track with the default `CC#` label and // without a language sourceBuffer.inbandTextTracks_[trackId] = player.addRemoteTextTrack({ kind: 'captions', id: trackId, label: trackId }, false).track; } } } } if (segment.metadata && segment.metadata.length && !sourceBuffer.metadataTrack_) { sourceBuffer.metadataTrack_ = player.addRemoteTextTrack({ kind: 'metadata', label: 'Timed Metadata' }, false).track; sourceBuffer.metadataTrack_.inBandMetadataTrackDispatchType = segment.metadata.dispatchType; } }; exports['default'] = createTextTracksIfNecessary; module.exports = exports['default']; },{}],4:[function(require,module,exports){ /** * @file flash-constants.js */ /** * The maximum size in bytes for append operations to the video.js * SWF. Calling through to Flash blocks and can be expensive so * we chunk data and pass through 4KB at a time, yielding to the * browser between chunks. This gives a theoretical maximum rate of * 1MB/s into Flash. Any higher and we begin to drop frames and UI * responsiveness suffers. * * @private */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var flashConstants = { // times in milliseconds TIME_BETWEEN_CHUNKS: 1, BYTES_PER_CHUNK: 1024 * 32 }; exports["default"] = flashConstants; module.exports = exports["default"]; },{}],5:[function(require,module,exports){ /** * @file flash-media-source.js */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var _globalDocument = require('global/document'); var _globalDocument2 = _interopRequireDefault(_globalDocument); var _videoJs = require('video.js'); var _videoJs2 = _interopRequireDefault(_videoJs); var _flashSourceBuffer = require('./flash-source-buffer'); var _flashSourceBuffer2 = _interopRequireDefault(_flashSourceBuffer); var _flashConstants = require('./flash-constants'); var _flashConstants2 = _interopRequireDefault(_flashConstants); var _codecUtils = require('./codec-utils'); /** * A flash implmentation of HTML MediaSources and a polyfill * for browsers that don't support native or HTML MediaSources.. * * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource * @class FlashMediaSource * @extends videojs.EventTarget */ var FlashMediaSource = (function (_videojs$EventTarget) { _inherits(FlashMediaSource, _videojs$EventTarget); function FlashMediaSource() { var _this = this; _classCallCheck(this, FlashMediaSource); _get(Object.getPrototypeOf(FlashMediaSource.prototype), 'constructor', this).call(this); this.sourceBuffers = []; this.readyState = 'closed'; this.on(['sourceopen', 'webkitsourceopen'], function (event) { // find the swf where we will push media data _this.swfObj = _globalDocument2['default'].getElementById(event.swfId); _this.player_ = (0, _videoJs2['default'])(_this.swfObj.parentNode); _this.tech_ = _this.swfObj.tech; _this.readyState = 'open'; _this.tech_.on('seeking', function () { var i = _this.sourceBuffers.length; while (i--) { _this.sourceBuffers[i].abort(); } }); // trigger load events if (_this.swfObj) { _this.swfObj.vjs_load(); } }); } /** * Set or return the presentation duration. * * @param {Double} value the duration of the media in seconds * @param {Double} the current presentation duration * @link http://www.w3.org/TR/media-source/#widl-MediaSource-duration */ /** * We have this function so that the html and flash interfaces * are the same. * * @private */ _createClass(FlashMediaSource, [{ key: 'addSeekableRange_', value: function addSeekableRange_() {} // intentional no-op /** * Create a new flash source buffer and add it to our flash media source. * * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer * @param {String} type the content-type of the source * @return {Object} the flash source buffer */ }, { key: 'addSourceBuffer', value: function addSourceBuffer(type) { var parsedType = (0, _codecUtils.parseContentType)(type); var sourceBuffer = undefined; // if this is an FLV type, we'll push data to flash if (parsedType.type === 'video/mp2t' || parsedType.type === 'audio/mp2t') { // Flash source buffers sourceBuffer = new _flashSourceBuffer2['default'](this); } else { throw new Error('NotSupportedError (Video.js)'); } this.sourceBuffers.push(sourceBuffer); return sourceBuffer; } /** * Signals the end of the stream. * * @link https://w3c.github.io/media-source/#widl-MediaSource-endOfStream-void-EndOfStreamError-error * @param {String=} error Signals that a playback error * has occurred. If specified, it must be either "network" or * "decode". */ }, { key: 'endOfStream', value: function endOfStream(error) { if (error === 'network') { // MEDIA_ERR_NETWORK this.tech_.error(2); } else if (error === 'decode') { // MEDIA_ERR_DECODE this.tech_.error(3); } if (this.readyState !== 'ended') { this.readyState = 'ended'; this.swfObj.vjs_endOfStream(); } } }]); return FlashMediaSource; })(_videoJs2['default'].EventTarget); exports['default'] = FlashMediaSource; try { Object.defineProperty(FlashMediaSource.prototype, 'duration', { /** * Return the presentation duration. * * @return {Double} the duration of the media in seconds * @link http://www.w3.org/TR/media-source/#widl-MediaSource-duration */ get: function get() { if (!this.swfObj) { return NaN; } // get the current duration from the SWF return this.swfObj.vjs_getProperty('duration'); }, /** * Set the presentation duration. * * @param {Double} value the duration of the media in seconds * @return {Double} the duration of the media in seconds * @link http://www.w3.org/TR/media-source/#widl-MediaSource-duration */ set: function set(value) { var i = undefined; var oldDuration = this.swfObj.vjs_getProperty('duration'); this.swfObj.vjs_setProperty('duration', value); if (value < oldDuration) { // In MSE, this triggers the range removal algorithm which causes // an update to occur for (i = 0; i < this.sourceBuffers.length; i++) { this.sourceBuffers[i].remove(value, oldDuration); } } return value; } }); } catch (e) { // IE8 throws if defineProperty is called on a non-DOM node. We // don't support IE8 but we shouldn't throw an error if loaded // there. FlashMediaSource.prototype.duration = NaN; } for (var property in _flashConstants2['default']) { FlashMediaSource[property] = _flashConstants2['default'][property]; } module.exports = exports['default']; },{"./codec-utils":2,"./flash-constants":4,"./flash-source-buffer":6,"global/document":15,"video.js":135}],6:[function(require,module,exports){ /** * @file flash-source-buffer.js */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); var _videoJs = require('video.js'); var _videoJs2 = _interopRequireDefault(_videoJs); var _muxJsLibFlv = require('mux.js/lib/flv'); var _muxJsLibFlv2 = _interopRequireDefault(_muxJsLibFlv); var _removeCuesFromTrack = require('./remove-cues-from-track'); var _removeCuesFromTrack2 = _interopRequireDefault(_removeCuesFromTrack); var _createTextTracksIfNecessary = require('./create-text-tracks-if-necessary'); var _createTextTracksIfNecessary2 = _interopRequireDefault(_createTextTracksIfNecessary); var _addTextTrackData = require('./add-text-track-data'); var _flashTransmuxerWorker = require('./flash-transmuxer-worker'); var _flashTransmuxerWorker2 = _interopRequireDefault(_flashTransmuxerWorker); var _webwackify = require('webwackify'); var _webwackify2 = _interopRequireDefault(_webwackify); var _flashConstants = require('./flash-constants'); var _flashConstants2 = _interopRequireDefault(_flashConstants); var resolveFlashTransmuxWorker = function resolveFlashTransmuxWorker() { var result = undefined; try { result = require.resolve('./flash-transmuxer-worker'); } catch (e) { // no result } return result; }; /** * A wrapper around the setTimeout function that uses * the flash constant time between ticks value. * * @param {Function} func the function callback to run * @private */ var scheduleTick = function scheduleTick(func) { // Chrome doesn't invoke requestAnimationFrame callbacks // in background tabs, so use setTimeout. _globalWindow2['default'].setTimeout(func, _flashConstants2['default'].TIME_BETWEEN_CHUNKS); }; /** * Generates a random string of max length 6 * * @return {String} the randomly generated string * @function generateRandomString * @private */ var generateRandomString = function generateRandomString() { return Math.random().toString(36).slice(2, 8); }; /** * Round a number to a specified number of places much like * toFixed but return a number instead of a string representation. * * @param {Number} num A number * @param {Number} places The number of decimal places which to * round * @private */ var toDecimalPlaces = function toDecimalPlaces(num, places) { if (typeof places !== 'number' || places < 0) { places = 0; } var scale = Math.pow(10, places); return Math.round(num * scale) / scale; }; /** * A SourceBuffer implementation for Flash rather than HTML. * * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource * @param {Object} mediaSource the flash media source * @class FlashSourceBuffer * @extends videojs.EventTarget */ var FlashSourceBuffer = (function (_videojs$EventTarget) { _inherits(FlashSourceBuffer, _videojs$EventTarget); function FlashSourceBuffer(mediaSource) { var _this = this; _classCallCheck(this, FlashSourceBuffer); _get(Object.getPrototypeOf(FlashSourceBuffer.prototype), 'constructor', this).call(this); var encodedHeader = undefined; // Start off using the globally defined value but refine // as we append data into flash this.chunkSize_ = _flashConstants2['default'].BYTES_PER_CHUNK; // byte arrays queued to be appended this.buffer_ = []; // the total number of queued bytes this.bufferSize_ = 0; // to be able to determine the correct position to seek to, we // need to retain information about the mapping between the // media timeline and PTS values this.basePtsOffset_ = NaN; this.mediaSource_ = mediaSource; this.audioBufferEnd_ = NaN; this.videoBufferEnd_ = NaN; // indicates whether the asynchronous continuation of an operation // is still being processed // see https://w3c.github.io/media-source/#widl-SourceBuffer-updating this.updating = false; this.timestampOffset_ = 0; encodedHeader = _globalWindow2['default'].btoa(String.fromCharCode.apply(null, Array.prototype.slice.call(_muxJsLibFlv2['default'].getFlvHeader()))); // create function names with added randomness for the global callbacks flash will use // to get data from javascript into the swf. Random strings are added as a safety // measure for pages with multiple players since these functions will be global // instead of per instance. When making a call to the swf, the browser generates a // try catch code snippet, but just takes the function name and writes out an unquoted // call to that function. If the player id has any special characters, this will result // in an error, so safePlayerId replaces all special characters to '_' var safePlayerId = this.mediaSource_.player_.id().replace(/[^a-zA-Z0-9]/g, '_'); this.flashEncodedHeaderName_ = 'vjs_flashEncodedHeader_' + safePlayerId + generateRandomString(); this.flashEncodedDataName_ = 'vjs_flashEncodedData_' + safePlayerId + generateRandomString(); _globalWindow2['default'][this.flashEncodedHeaderName_] = function () { delete _globalWindow2['default'][_this.flashEncodedHeaderName_]; return encodedHeader; }; this.mediaSource_.swfObj.vjs_appendChunkReady(this.flashEncodedHeaderName_); this.transmuxer_ = (0, _webwackify2['default'])(_flashTransmuxerWorker2['default'], resolveFlashTransmuxWorker()); this.transmuxer_.postMessage({ action: 'init', options: {} }); this.transmuxer_.onmessage = function (event) { if (event.data.action === 'data') { _this.receiveBuffer_(event.data.segment); } }; this.one('updateend', function () { _this.mediaSource_.tech_.trigger('loadedmetadata'); }); Object.defineProperty(this, 'timestampOffset', { get: function get() { return this.timestampOffset_; }, set: function set(val) { if (typeof val === 'number' && val >= 0) { this.timestampOffset_ = val; // We have to tell flash to expect a discontinuity this.mediaSource_.swfObj.vjs_discontinuity(); // the media <-> PTS mapping must be re-established after // the discontinuity this.basePtsOffset_ = NaN; this.audioBufferEnd_ = NaN; this.videoBufferEnd_ = NaN; this.transmuxer_.postMessage({ action: 'reset' }); } } }); Object.defineProperty(this, 'buffered', { get: function get() { if (!this.mediaSource_ || !this.mediaSource_.swfObj || !('vjs_getProperty' in this.mediaSource_.swfObj)) { return _videoJs2['default'].createTimeRange(); } var buffered = this.mediaSource_.swfObj.vjs_getProperty('buffered'); if (buffered && buffered.length) { buffered[0][0] = toDecimalPlaces(buffered[0][0], 3); buffered[0][1] = toDecimalPlaces(buffered[0][1], 3); } return _videoJs2['default'].createTimeRanges(buffered); } }); // On a seek we remove all text track data since flash has no concept // of a buffered-range and everything else is reset on seek this.mediaSource_.player_.on('seeked', function () { (0, _removeCuesFromTrack2['default'])(0, Infinity, _this.metadataTrack_); if (_this.inbandTextTracks_) { for (var track in _this.inbandTextTracks_) { (0, _removeCuesFromTrack2['default'])(0, Infinity, _this.inbandTextTracks_[track]); } } }); var onHlsReset = this.onHlsReset_.bind(this); // hls-reset is fired by videojs.Hls on to the tech after the main SegmentLoader // resets its state and flushes the buffer this.mediaSource_.player_.tech_.on('hls-reset', onHlsReset); this.mediaSource_.player_.tech_.hls.on('dispose', function () { _this.transmuxer_.terminate(); _this.mediaSource_.player_.tech_.off('hls-reset', onHlsReset); }); } /** * Append bytes to the sourcebuffers buffer, in this case we * have to append it to swf object. * * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer * @param {Array} bytes */ _createClass(FlashSourceBuffer, [{ key: 'appendBuffer', value: function appendBuffer(bytes) { var error = undefined; if (this.updating) { error = new Error('SourceBuffer.append() cannot be called ' + 'while an update is in progress'); error.name = 'InvalidStateError'; error.code = 11; throw error; } this.updating = true; this.mediaSource_.readyState = 'open'; this.trigger({ type: 'update' }); this.transmuxer_.postMessage({ action: 'push', data: bytes.buffer, byteOffset: bytes.byteOffset, byteLength: bytes.byteLength }, [bytes.buffer]); this.transmuxer_.postMessage({ action: 'flush' }); } /** * Reset the parser and remove any data queued to be sent to the SWF. * * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/abort */ }, { key: 'abort', value: function abort() { this.buffer_ = []; this.bufferSize_ = 0; this.mediaSource_.swfObj.vjs_abort(); // report any outstanding updates have ended if (this.updating) { this.updating = false; this.trigger({ type: 'updateend' }); } } /** * Flash cannot remove ranges already buffered in the NetStream * but seeking clears the buffer entirely. For most purposes, * having this operation act as a no-op is acceptable. * * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/remove * @param {Double} start start of the section to remove * @param {Double} end end of the section to remove */ }, { key: 'remove', value: function remove(start, end) { (0, _removeCuesFromTrack2['default'])(start, end, this.metadataTrack_); if (this.inbandTextTracks_) { for (var track in this.inbandTextTracks_) { (0, _removeCuesFromTrack2['default'])(start, end, this.inbandTextTracks_[track]); } } this.trigger({ type: 'update' }); this.trigger({ type: 'updateend' }); } /** * Receive a buffer from the flv. * * @param {Object} segment * @private */ }, { key: 'receiveBuffer_', value: function receiveBuffer_(segment) { var _this2 = this; // create an in-band caption track if one is present in the segment (0, _createTextTracksIfNecessary2['default'])(this, this.mediaSource_, segment); (0, _addTextTrackData.addTextTrackData)(this, segment.captions, segment.metadata); // Do this asynchronously since convertTagsToData_ can be time consuming scheduleTick(function () { var flvBytes = _this2.convertTagsToData_(segment); if (_this2.buffer_.length === 0) { scheduleTick(_this2.processBuffer_.bind(_this2)); } if (flvBytes) { _this2.buffer_.push(flvBytes); _this2.bufferSize_ += flvBytes.byteLength; } }); } /** * Append a portion of the current buffer to the SWF. * * @private */ }, { key: 'processBuffer_', value: function processBuffer_() { var _this3 = this; var chunkSize = _flashConstants2['default'].BYTES_PER_CHUNK; if (!this.buffer_.length) { if (this.updating !== false) { this.updating = false; this.trigger({ type: 'updateend' }); } // do nothing if the buffer is empty return; } // concatenate appends up to the max append size var chunk = this.buffer_[0].subarray(0, chunkSize); // requeue any bytes that won't make it this round if (chunk.byteLength < chunkSize || this.buffer_[0].byteLength === chunkSize) { this.buffer_.shift(); } else { this.buffer_[0] = this.buffer_[0].subarray(chunkSize); } this.bufferSize_ -= chunk.byteLength; // base64 encode the bytes var binary = []; var length = chunk.byteLength; for (var i = 0; i < length; i++) { binary.push(String.fromCharCode(chunk[i])); } var b64str = _globalWindow2['default'].btoa(binary.join('')); _globalWindow2['default'][this.flashEncodedDataName_] = function () { // schedule another processBuffer to process any left over data or to // trigger updateend scheduleTick(_this3.processBuffer_.bind(_this3)); delete _globalWindow2['default'][_this3.flashEncodedDataName_]; return b64str; }; // Notify the swf that segment data is ready to be appended this.mediaSource_.swfObj.vjs_appendChunkReady(this.flashEncodedDataName_); } /** * Turns an array of flv tags into a Uint8Array representing the * flv data. Also removes any tags that are before the current * time so that playback begins at or slightly after the right * place on a seek * * @private * @param {Object} segmentData object of segment data */ }, { key: 'convertTagsToData_', value: function convertTagsToData_(segmentData) { var segmentByteLength = 0; var tech = this.mediaSource_.tech_; var videoTargetPts = 0; var segment = undefined; var videoTags = segmentData.tags.videoTags; var audioTags = segmentData.tags.audioTags; // Establish the media timeline to PTS translation if we don't // have one already if (isNaN(this.basePtsOffset_) && (videoTags.length || audioTags.length)) { // We know there is at least one video or audio tag, but since we may not have both, // we use pts: Infinity for the missing tag. The will force the following Math.min // call will to use the proper pts value since it will always be less than Infinity var firstVideoTag = videoTags[0] || { pts: Infinity }; var firstAudioTag = audioTags[0] || { pts: Infinity }; this.basePtsOffset_ = Math.min(firstAudioTag.pts, firstVideoTag.pts); } if (tech.seeking()) { // Do not use previously saved buffer end values while seeking since buffer // is cleared on all seeks this.videoBufferEnd_ = NaN; this.audioBufferEnd_ = NaN; } if (isNaN(this.videoBufferEnd_)) { if (tech.buffered().length) { videoTargetPts = tech.buffered().end(0) - this.timestampOffset; } // Trim to currentTime if seeking if (tech.seeking()) { videoTargetPts = Math.max(videoTargetPts, tech.currentTime() - this.timestampOffset); } // PTS values are represented in milliseconds videoTargetPts *= 1e3; videoTargetPts += this.basePtsOffset_; } else { // Add a fudge factor of 0.1 to the last video pts appended since a rendition change // could append an overlapping segment, in which case there is a high likelyhood // a tag could have a matching pts to videoBufferEnd_, which would cause // that tag to get appended by the tag.pts >= targetPts check below even though it // is a duplicate of what was previously appended videoTargetPts = this.videoBufferEnd_ + 0.1; } // filter complete GOPs with a presentation time less than the seek target/end of buffer var currentIndex = videoTags.length; // if the last tag is beyond videoTargetPts, then do not search the list for a GOP // since our videoTargetPts lies in a future segment if (currentIndex && videoTags[currentIndex - 1].pts >= videoTargetPts) { // Start by walking backwards from the end of the list until we reach a tag that // is equal to or less than videoTargetPts while (--currentIndex) { var currentTag = videoTags[currentIndex]; if (currentTag.pts > videoTargetPts) { continue; } // if we see a keyFrame or metadata tag once we've gone below videoTargetPts, // exit the loop as this is the start of the GOP that we want to append if (currentTag.keyFrame || currentTag.metaDataTag) { break; } } // We need to check if there are any metadata tags that come before currentIndex // as those will be metadata tags associated with the GOP we are appending // There could be 0 to 2 metadata tags that come before the currentIndex depending // on what videoTargetPts is and whether the transmuxer prepended metadata tags to this // key frame while (currentIndex) { var nextTag = videoTags[currentIndex - 1]; if (!nextTag.metaDataTag) { break; } currentIndex--; } } var filteredVideoTags = videoTags.slice(currentIndex); var audioTargetPts = undefined; if (isNaN(this.audioBufferEnd_)) { audioTargetPts = videoTargetPts; } else { // Add a fudge factor of 0.1 to the last video pts appended since a rendition change // could append an overlapping segment, in which case there is a high likelyhood // a tag could have a matching pts to videoBufferEnd_, which would cause // that tag to get appended by the tag.pts >= targetPts check below even though it // is a duplicate of what was previously appended audioTargetPts = this.audioBufferEnd_ + 0.1; } if (filteredVideoTags.length) { // If targetPts intersects a GOP and we appended the tags for the GOP that came // before targetPts, we want to make sure to trim audio tags at the pts // of the first video tag to avoid brief moments of silence audioTargetPts = Math.min(audioTargetPts, filteredVideoTags[0].pts); } // skip tags with a presentation time less than the seek target/end of buffer currentIndex = 0; while (currentIndex < audioTags.length) { if (audioTags[currentIndex].pts >= audioTargetPts) { break; } currentIndex++; } var filteredAudioTags = audioTags.slice(currentIndex); // update the audio and video buffer ends if (filteredAudioTags.length) { this.audioBufferEnd_ = filteredAudioTags[filteredAudioTags.length - 1].pts; } if (filteredVideoTags.length) { this.videoBufferEnd_ = filteredVideoTags[filteredVideoTags.length - 1].pts; } var tags = this.getOrderedTags_(filteredVideoTags, filteredAudioTags); if (tags.length === 0) { return; } // If we are appending data that comes before our target pts, we want to tell // the swf to adjust its notion of current time to account for the extra tags // we are appending to complete the GOP that intersects with targetPts if (tags[0].pts < videoTargetPts && tech.seeking()) { var fudgeFactor = 1 / 30; var currentTime = tech.currentTime(); var diff = (videoTargetPts - tags[0].pts) / 1e3; var adjustedTime = currentTime - diff; if (adjustedTime < fudgeFactor) { adjustedTime = 0; } try { this.mediaSource_.swfObj.vjs_adjustCurrentTime(adjustedTime); } catch (e) { // no-op for backwards compatability of swf. If adjustCurrentTime fails, // the swf may incorrectly report currentTime and buffered ranges // but should not affect playback over than the time displayed on the // progress bar is inaccurate } } // concatenate the bytes into a single segment for (var i = 0; i < tags.length; i++) { segmentByteLength += tags[i].bytes.byteLength; } segment = new Uint8Array(segmentByteLength); for (var i = 0, j = 0; i < tags.length; i++) { segment.set(tags[i].bytes, j); j += tags[i].bytes.byteLength; } return segment; } /** * Assemble the FLV tags in decoder order. * * @private * @param {Array} videoTags list of video tags * @param {Array} audioTags list of audio tags */ }, { key: 'getOrderedTags_', value: function getOrderedTags_(videoTags, audioTags) { var tag = undefined; var tags = []; while (videoTags.length || audioTags.length) { if (!videoTags.length) { // only audio tags remain tag = audioTags.shift(); } else if (!audioTags.length) { // only video tags remain tag = videoTags.shift(); } else if (audioTags[0].dts < videoTags[0].dts) { // audio should be decoded next tag = audioTags.shift(); } else { // video should be decoded next tag = videoTags.shift(); } tags.push(tag); } return tags; } }, { key: 'onHlsReset_', value: function onHlsReset_() { this.transmuxer_.postMessage({ action: 'resetCaptions' }); } }]); return FlashSourceBuffer; })(_videoJs2['default'].EventTarget); exports['default'] = FlashSourceBuffer; module.exports = exports['default']; },{"./add-text-track-data":1,"./create-text-tracks-if-necessary":3,"./flash-constants":4,"./flash-transmuxer-worker":7,"./remove-cues-from-track":9,"global/window":16,"mux.js/lib/flv":25,"video.js":135,"webwackify":142}],7:[function(require,module,exports){ /** * @file flash-transmuxer-worker.js */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); var _muxJsLibFlv = require('mux.js/lib/flv'); var _muxJsLibFlv2 = _interopRequireDefault(_muxJsLibFlv); /** * Re-emits transmuxer events by converting them into messages to the * world outside the worker. * * @param {Object} transmuxer the transmuxer to wire events on * @private */ var wireTransmuxerEvents = function wireTransmuxerEvents(transmuxer) { transmuxer.on('data', function (segment) { _globalWindow2['default'].postMessage({ action: 'data', segment: segment }); }); transmuxer.on('done', function (data) { _globalWindow2['default'].postMessage({ action: 'done' }); }); }; /** * All incoming messages route through this hash. If no function exists * to handle an incoming message, then we ignore the message. * * @class MessageHandlers * @param {Object} options the options to initialize with */ var MessageHandlers = (function () { function MessageHandlers(options) { _classCallCheck(this, MessageHandlers); this.options = options || {}; this.init(); } /** * Our web wroker interface so that things can talk to mux.js * that will be running in a web worker. The scope is passed to this by * webworkify. * * @param {Object} self the scope for the web worker */ /** * initialize our web worker and wire all the events. */ _createClass(MessageHandlers, [{ key: 'init', value: function init() { if (this.transmuxer) { this.transmuxer.dispose(); } this.transmuxer = new _muxJsLibFlv2['default'].Transmuxer(this.options); wireTransmuxerEvents(this.transmuxer); } /** * Adds data (a ts segment) to the start of the transmuxer pipeline for * processing. * * @param {ArrayBuffer} data data to push into the muxer */ }, { key: 'push', value: function push(data) { // Cast array buffer to correct type for transmuxer var segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); this.transmuxer.push(segment); } /** * Recreate the transmuxer so that the next segment added via `push` * start with a fresh transmuxer. */ }, { key: 'reset', value: function reset() { this.init(); } /** * Forces the pipeline to finish processing the last segment and emit its * results. */ }, { key: 'flush', value: function flush() { this.transmuxer.flush(); } }, { key: 'resetCaptions', value: function resetCaptions() { this.transmuxer.resetCaptions(); } }]); return MessageHandlers; })(); var FlashTransmuxerWorker = function FlashTransmuxerWorker(self) { self.onmessage = function (event) { if (event.data.action === 'init' && event.data.options) { this.messageHandlers = new MessageHandlers(event.data.options); return; } if (!this.messageHandlers) { this.messageHandlers = new MessageHandlers(); } if (event.data && event.data.action && event.data.action !== 'init') { if (this.messageHandlers[event.data.action]) { this.messageHandlers[event.data.action](event.data); } } }; }; exports['default'] = function (self) { return new FlashTransmuxerWorker(self); }; module.exports = exports['default']; },{"global/window":16,"mux.js/lib/flv":25}],8:[function(require,module,exports){ /** * @file html-media-source.js */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); var _globalDocument = require('global/document'); var _globalDocument2 = _interopRequireDefault(_globalDocument); var _videoJs = require('video.js'); var _videoJs2 = _interopRequireDefault(_videoJs); var _virtualSourceBuffer = require('./virtual-source-buffer'); var _virtualSourceBuffer2 = _interopRequireDefault(_virtualSourceBuffer); var _addTextTrackData = require('./add-text-track-data'); var _codecUtils = require('./codec-utils'); /** * Our MediaSource implementation in HTML, mimics native * MediaSource where/if possible. * * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource * @class HtmlMediaSource * @extends videojs.EventTarget */ var HtmlMediaSource = (function (_videojs$EventTarget) { _inherits(HtmlMediaSource, _videojs$EventTarget); function HtmlMediaSource() { var _this = this; _classCallCheck(this, HtmlMediaSource); _get(Object.getPrototypeOf(HtmlMediaSource.prototype), 'constructor', this).call(this); var property = undefined; this.nativeMediaSource_ = new _globalWindow2['default'].MediaSource(); // delegate to the native MediaSource's methods by default for (property in this.nativeMediaSource_) { if (!(property in HtmlMediaSource.prototype) && typeof this.nativeMediaSource_[property] === 'function') { this[property] = this.nativeMediaSource_[property].bind(this.nativeMediaSource_); } } // emulate `duration` and `seekable` until seeking can be // handled uniformly for live streams // see https://github.com/w3c/media-source/issues/5 this.duration_ = NaN; Object.defineProperty(this, 'duration', { get: function get() { if (this.duration_ === Infinity) { return this.duration_; } return this.nativeMediaSource_.duration; }, set: function set(duration) { this.duration_