videojs-contrib-media-sources
Version:
A Media Source Extensions plugin for video.js
1,396 lines (1,147 loc) • 1.17 MB
JavaScript
(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_