123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>JSDoc: Source: flash-source-buffer.js</title>
- <script src="scripts/prettify/prettify.js"> </script>
- <script src="scripts/prettify/lang-css.js"> </script>
- <!--[if lt IE 9]>
- <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
- <![endif]-->
- <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
- <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
- </head>
- <body>
- <div id="main">
- <h1 class="page-title">Source: flash-source-buffer.js</h1>
-
-
- <section>
- <article>
- <pre class="prettyprint source linenums"><code>/**
- * @file flash-source-buffer.js
- */
- import window from 'global/window';
- import videojs from 'video.js';
- import flv from 'mux.js/lib/flv';
- import removeCuesFromTrack from './remove-cues-from-track';
- import createTextTracksIfNecessary from './create-text-tracks-if-necessary';
- import {addTextTrackData} from './add-text-track-data';
- import transmuxWorker from './flash-transmuxer-worker';
- import work from 'webworkify';
- import FlashConstants from './flash-constants';
- /**
- * A wrapper around the setTimeout function that uses
- * the flash constant time between ticks value.
- *
- * @param {Function} func the function callback to run
- * @private
- */
- const scheduleTick = function(func) {
- // Chrome doesn't invoke requestAnimationFrame callbacks
- // in background tabs, so use setTimeout.
- window.setTimeout(func, FlashConstants.TIME_BETWEEN_CHUNKS);
- };
- /**
- * Generates a random string of max length 6
- *
- * @return {String} the randomly generated string
- * @function generateRandomString
- * @private
- */
- const generateRandomString = function() {
- 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
- */
- const toDecimalPlaces = function(num, places) {
- if (typeof places !== 'number' || places < 0) {
- places = 0;
- }
- let 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
- */
- export default class FlashSourceBuffer extends videojs.EventTarget {
- constructor(mediaSource) {
- super();
- let encodedHeader;
- // Start off using the globally defined value but refine
- // as we append data into flash
- this.chunkSize_ = FlashConstants.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 = window.btoa(
- String.fromCharCode.apply(
- null,
- Array.prototype.slice.call(
- flv.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 '_'
- const safePlayerId = this.mediaSource_.player_.id().replace(/[^a-zA-Z0-9]/g, '_');
- this.flashEncodedHeaderName_ = 'vjs_flashEncodedHeader_' +
- safePlayerId +
- generateRandomString();
- this.flashEncodedDataName_ = 'vjs_flashEncodedData_' +
- safePlayerId +
- generateRandomString();
- window[this.flashEncodedHeaderName_] = () => {
- delete window[this.flashEncodedHeaderName_];
- return encodedHeader;
- };
- this.mediaSource_.swfObj.vjs_appendChunkReady(this.flashEncodedHeaderName_);
- this.transmuxer_ = work(transmuxWorker);
- this.transmuxer_.postMessage({ action: 'init', options: {} });
- this.transmuxer_.onmessage = (event) => {
- if (event.data.action === 'data') {
- this.receiveBuffer_(event.data.segment);
- }
- };
- this.one('updateend', () => {
- this.mediaSource_.tech_.trigger('loadedmetadata');
- });
- Object.defineProperty(this, 'timestampOffset', {
- get() {
- return this.timestampOffset_;
- },
- 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() {
- if (!this.mediaSource_ ||
- !this.mediaSource_.swfObj ||
- !('vjs_getProperty' in this.mediaSource_.swfObj)) {
- return videojs.createTimeRange();
- }
- let 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 videojs.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', () => {
- removeCuesFromTrack(0, Infinity, this.metadataTrack_);
- if (this.inbandTextTracks_) {
- for (let track in this.inbandTextTracks_) {
- removeCuesFromTrack(0, Infinity, this.inbandTextTracks_[track]);
- }
- }
- });
- let 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', () => {
- 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
- */
- appendBuffer(bytes) {
- let error;
- 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
- */
- 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
- */
- remove(start, end) {
- removeCuesFromTrack(start, end, this.metadataTrack_);
- if (this.inbandTextTracks_) {
- for (let track in this.inbandTextTracks_) {
- removeCuesFromTrack(start, end, this.inbandTextTracks_[track]);
- }
- }
- this.trigger({ type: 'update' });
- this.trigger({ type: 'updateend' });
- }
- /**
- * Receive a buffer from the flv.
- *
- * @param {Object} segment
- * @private
- */
- receiveBuffer_(segment) {
- // create an in-band caption track if one is present in the segment
- createTextTracksIfNecessary(this, this.mediaSource_, segment);
- addTextTrackData(this, segment.captions, segment.metadata);
- // Do this asynchronously since convertTagsToData_ can be time consuming
- scheduleTick(() => {
- let flvBytes = this.convertTagsToData_(segment);
- if (this.buffer_.length === 0) {
- scheduleTick(this.processBuffer_.bind(this));
- }
- if (flvBytes) {
- this.buffer_.push(flvBytes);
- this.bufferSize_ += flvBytes.byteLength;
- }
- });
- }
- /**
- * Append a portion of the current buffer to the SWF.
- *
- * @private
- */
- processBuffer_() {
- let chunkSize = FlashConstants.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
- let 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
- let binary = [];
- let length = chunk.byteLength;
- for (let i = 0; i < length; i++) {
- binary.push(String.fromCharCode(chunk[i]));
- }
- let b64str = window.btoa(binary.join(''));
- window[this.flashEncodedDataName_] = () => {
- // schedule another processBuffer to process any left over data or to
- // trigger updateend
- scheduleTick(this.processBuffer_.bind(this));
- delete window[this.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
- */
- convertTagsToData_(segmentData) {
- let segmentByteLength = 0;
- let tech = this.mediaSource_.tech_;
- let videoTargetPts = 0;
- let segment;
- let videoTags = segmentData.tags.videoTags;
- let 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
- const firstVideoTag = videoTags[0] || { pts: Infinity };
- const 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
- let 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) {
- const 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) {
- const nextTag = videoTags[currentIndex - 1];
- if (!nextTag.metaDataTag) {
- break;
- }
- currentIndex--;
- }
- }
- const filteredVideoTags = videoTags.slice(currentIndex);
- let audioTargetPts;
- 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++;
- }
- const 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;
- }
- let 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()) {
- const fudgeFactor = 1 / 30;
- const currentTime = tech.currentTime();
- const diff = (videoTargetPts - tags[0].pts) / 1e3;
- let 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 (let i = 0; i < tags.length; i++) {
- segmentByteLength += tags[i].bytes.byteLength;
- }
- segment = new Uint8Array(segmentByteLength);
- for (let 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
- */
- getOrderedTags_(videoTags, audioTags) {
- let tag;
- let 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;
- }
- onHlsReset_() {
- this.transmuxer_.postMessage({action: 'resetCaptions'});
- }
- }
- </code></pre>
- </article>
- </section>
- </div>
- <nav>
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FlashMediaSource.html">FlashMediaSource</a></li><li><a href="FlashSourceBuffer.html">FlashSourceBuffer</a></li><li><a href="HtmlMediaSource.html">HtmlMediaSource</a></li><li><a href="MessageHandlers.html">MessageHandlers</a></li><li><a href="VirtualSourceBuffer.html">VirtualSourceBuffer</a></li></ul><h3>Global</h3><ul><li><a href="global.html#abort">abort</a></li><li><a href="global.html#addSourceBuffer">addSourceBuffer</a></li><li><a href="global.html#appendBuffer">appendBuffer</a></li><li><a href="global.html#appendGopInfo_">appendGopInfo_</a></li><li><a href="global.html#endOfStream">endOfStream</a></li><li><a href="global.html#FlashTransmuxerWorker">FlashTransmuxerWorker</a></li><li><a href="global.html#get">get</a></li><li><a href="global.html#gopsSafeToAlignWith">gopsSafeToAlignWith</a></li><li><a href="global.html#MediaSource">MediaSource</a></li><li><a href="global.html#open">open</a></li><li><a href="global.html#remove">remove</a></li><li><a href="global.html#removeGopBuffer">removeGopBuffer</a></li><li><a href="global.html#set">set</a></li><li><a href="global.html#supportsNativeMediaSources">supportsNativeMediaSources</a></li><li><a href="global.html#TransmuxerWorker">TransmuxerWorker</a></li><li><a href="global.html#updateGopBuffer">updateGopBuffer</a></li><li><a href="global.html#URL">URL</a></li></ul>
- </nav>
- <br class="clear">
- <footer>
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.4</a> on Thu Nov 02 2017 12:03:25 GMT-0400 (EDT)
- </footer>
- <script> prettyPrint(); </script>
- <script src="scripts/linenumber.js"> </script>
- </body>
- </html>
|