flash-source-buffer.js.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JSDoc: Source: flash-source-buffer.js</title>
  6. <script src="scripts/prettify/prettify.js"> </script>
  7. <script src="scripts/prettify/lang-css.js"> </script>
  8. <!--[if lt IE 9]>
  9. <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  10. <![endif]-->
  11. <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
  12. <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
  13. </head>
  14. <body>
  15. <div id="main">
  16. <h1 class="page-title">Source: flash-source-buffer.js</h1>
  17. <section>
  18. <article>
  19. <pre class="prettyprint source linenums"><code>/**
  20. * @file flash-source-buffer.js
  21. */
  22. import window from 'global/window';
  23. import videojs from 'video.js';
  24. import flv from 'mux.js/lib/flv';
  25. import removeCuesFromTrack from './remove-cues-from-track';
  26. import createTextTracksIfNecessary from './create-text-tracks-if-necessary';
  27. import {addTextTrackData} from './add-text-track-data';
  28. import transmuxWorker from './flash-transmuxer-worker';
  29. import work from 'webworkify';
  30. import FlashConstants from './flash-constants';
  31. /**
  32. * A wrapper around the setTimeout function that uses
  33. * the flash constant time between ticks value.
  34. *
  35. * @param {Function} func the function callback to run
  36. * @private
  37. */
  38. const scheduleTick = function(func) {
  39. // Chrome doesn't invoke requestAnimationFrame callbacks
  40. // in background tabs, so use setTimeout.
  41. window.setTimeout(func, FlashConstants.TIME_BETWEEN_CHUNKS);
  42. };
  43. /**
  44. * Generates a random string of max length 6
  45. *
  46. * @return {String} the randomly generated string
  47. * @function generateRandomString
  48. * @private
  49. */
  50. const generateRandomString = function() {
  51. return (Math.random().toString(36)).slice(2, 8);
  52. };
  53. /**
  54. * Round a number to a specified number of places much like
  55. * toFixed but return a number instead of a string representation.
  56. *
  57. * @param {Number} num A number
  58. * @param {Number} places The number of decimal places which to
  59. * round
  60. * @private
  61. */
  62. const toDecimalPlaces = function(num, places) {
  63. if (typeof places !== 'number' || places &lt; 0) {
  64. places = 0;
  65. }
  66. let scale = Math.pow(10, places);
  67. return Math.round(num * scale) / scale;
  68. };
  69. /**
  70. * A SourceBuffer implementation for Flash rather than HTML.
  71. *
  72. * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource
  73. * @param {Object} mediaSource the flash media source
  74. * @class FlashSourceBuffer
  75. * @extends videojs.EventTarget
  76. */
  77. export default class FlashSourceBuffer extends videojs.EventTarget {
  78. constructor(mediaSource) {
  79. super();
  80. let encodedHeader;
  81. // Start off using the globally defined value but refine
  82. // as we append data into flash
  83. this.chunkSize_ = FlashConstants.BYTES_PER_CHUNK;
  84. // byte arrays queued to be appended
  85. this.buffer_ = [];
  86. // the total number of queued bytes
  87. this.bufferSize_ = 0;
  88. // to be able to determine the correct position to seek to, we
  89. // need to retain information about the mapping between the
  90. // media timeline and PTS values
  91. this.basePtsOffset_ = NaN;
  92. this.mediaSource_ = mediaSource;
  93. this.audioBufferEnd_ = NaN;
  94. this.videoBufferEnd_ = NaN;
  95. // indicates whether the asynchronous continuation of an operation
  96. // is still being processed
  97. // see https://w3c.github.io/media-source/#widl-SourceBuffer-updating
  98. this.updating = false;
  99. this.timestampOffset_ = 0;
  100. encodedHeader = window.btoa(
  101. String.fromCharCode.apply(
  102. null,
  103. Array.prototype.slice.call(
  104. flv.getFlvHeader()
  105. )
  106. )
  107. );
  108. // create function names with added randomness for the global callbacks flash will use
  109. // to get data from javascript into the swf. Random strings are added as a safety
  110. // measure for pages with multiple players since these functions will be global
  111. // instead of per instance. When making a call to the swf, the browser generates a
  112. // try catch code snippet, but just takes the function name and writes out an unquoted
  113. // call to that function. If the player id has any special characters, this will result
  114. // in an error, so safePlayerId replaces all special characters to '_'
  115. const safePlayerId = this.mediaSource_.player_.id().replace(/[^a-zA-Z0-9]/g, '_');
  116. this.flashEncodedHeaderName_ = 'vjs_flashEncodedHeader_' +
  117. safePlayerId +
  118. generateRandomString();
  119. this.flashEncodedDataName_ = 'vjs_flashEncodedData_' +
  120. safePlayerId +
  121. generateRandomString();
  122. window[this.flashEncodedHeaderName_] = () => {
  123. delete window[this.flashEncodedHeaderName_];
  124. return encodedHeader;
  125. };
  126. this.mediaSource_.swfObj.vjs_appendChunkReady(this.flashEncodedHeaderName_);
  127. this.transmuxer_ = work(transmuxWorker);
  128. this.transmuxer_.postMessage({ action: 'init', options: {} });
  129. this.transmuxer_.onmessage = (event) => {
  130. if (event.data.action === 'data') {
  131. this.receiveBuffer_(event.data.segment);
  132. }
  133. };
  134. this.one('updateend', () => {
  135. this.mediaSource_.tech_.trigger('loadedmetadata');
  136. });
  137. Object.defineProperty(this, 'timestampOffset', {
  138. get() {
  139. return this.timestampOffset_;
  140. },
  141. set(val) {
  142. if (typeof val === 'number' &amp;&amp; val >= 0) {
  143. this.timestampOffset_ = val;
  144. // We have to tell flash to expect a discontinuity
  145. this.mediaSource_.swfObj.vjs_discontinuity();
  146. // the media &lt;-> PTS mapping must be re-established after
  147. // the discontinuity
  148. this.basePtsOffset_ = NaN;
  149. this.audioBufferEnd_ = NaN;
  150. this.videoBufferEnd_ = NaN;
  151. this.transmuxer_.postMessage({ action: 'reset' });
  152. }
  153. }
  154. });
  155. Object.defineProperty(this, 'buffered', {
  156. get() {
  157. if (!this.mediaSource_ ||
  158. !this.mediaSource_.swfObj ||
  159. !('vjs_getProperty' in this.mediaSource_.swfObj)) {
  160. return videojs.createTimeRange();
  161. }
  162. let buffered = this.mediaSource_.swfObj.vjs_getProperty('buffered');
  163. if (buffered &amp;&amp; buffered.length) {
  164. buffered[0][0] = toDecimalPlaces(buffered[0][0], 3);
  165. buffered[0][1] = toDecimalPlaces(buffered[0][1], 3);
  166. }
  167. return videojs.createTimeRanges(buffered);
  168. }
  169. });
  170. // On a seek we remove all text track data since flash has no concept
  171. // of a buffered-range and everything else is reset on seek
  172. this.mediaSource_.player_.on('seeked', () => {
  173. removeCuesFromTrack(0, Infinity, this.metadataTrack_);
  174. if (this.inbandTextTracks_) {
  175. for (let track in this.inbandTextTracks_) {
  176. removeCuesFromTrack(0, Infinity, this.inbandTextTracks_[track]);
  177. }
  178. }
  179. });
  180. let onHlsReset = this.onHlsReset_.bind(this);
  181. // hls-reset is fired by videojs.Hls on to the tech after the main SegmentLoader
  182. // resets its state and flushes the buffer
  183. this.mediaSource_.player_.tech_.on('hls-reset', onHlsReset);
  184. this.mediaSource_.player_.tech_.hls.on('dispose', () => {
  185. this.transmuxer_.terminate();
  186. this.mediaSource_.player_.tech_.off('hls-reset', onHlsReset);
  187. });
  188. }
  189. /**
  190. * Append bytes to the sourcebuffers buffer, in this case we
  191. * have to append it to swf object.
  192. *
  193. * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer
  194. * @param {Array} bytes
  195. */
  196. appendBuffer(bytes) {
  197. let error;
  198. if (this.updating) {
  199. error = new Error('SourceBuffer.append() cannot be called ' +
  200. 'while an update is in progress');
  201. error.name = 'InvalidStateError';
  202. error.code = 11;
  203. throw error;
  204. }
  205. this.updating = true;
  206. this.mediaSource_.readyState = 'open';
  207. this.trigger({ type: 'update' });
  208. this.transmuxer_.postMessage({
  209. action: 'push',
  210. data: bytes.buffer,
  211. byteOffset: bytes.byteOffset,
  212. byteLength: bytes.byteLength
  213. }, [bytes.buffer]);
  214. this.transmuxer_.postMessage({action: 'flush'});
  215. }
  216. /**
  217. * Reset the parser and remove any data queued to be sent to the SWF.
  218. *
  219. * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/abort
  220. */
  221. abort() {
  222. this.buffer_ = [];
  223. this.bufferSize_ = 0;
  224. this.mediaSource_.swfObj.vjs_abort();
  225. // report any outstanding updates have ended
  226. if (this.updating) {
  227. this.updating = false;
  228. this.trigger({ type: 'updateend' });
  229. }
  230. }
  231. /**
  232. * Flash cannot remove ranges already buffered in the NetStream
  233. * but seeking clears the buffer entirely. For most purposes,
  234. * having this operation act as a no-op is acceptable.
  235. *
  236. * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/remove
  237. * @param {Double} start start of the section to remove
  238. * @param {Double} end end of the section to remove
  239. */
  240. remove(start, end) {
  241. removeCuesFromTrack(start, end, this.metadataTrack_);
  242. if (this.inbandTextTracks_) {
  243. for (let track in this.inbandTextTracks_) {
  244. removeCuesFromTrack(start, end, this.inbandTextTracks_[track]);
  245. }
  246. }
  247. this.trigger({ type: 'update' });
  248. this.trigger({ type: 'updateend' });
  249. }
  250. /**
  251. * Receive a buffer from the flv.
  252. *
  253. * @param {Object} segment
  254. * @private
  255. */
  256. receiveBuffer_(segment) {
  257. // create an in-band caption track if one is present in the segment
  258. createTextTracksIfNecessary(this, this.mediaSource_, segment);
  259. addTextTrackData(this, segment.captions, segment.metadata);
  260. // Do this asynchronously since convertTagsToData_ can be time consuming
  261. scheduleTick(() => {
  262. let flvBytes = this.convertTagsToData_(segment);
  263. if (this.buffer_.length === 0) {
  264. scheduleTick(this.processBuffer_.bind(this));
  265. }
  266. if (flvBytes) {
  267. this.buffer_.push(flvBytes);
  268. this.bufferSize_ += flvBytes.byteLength;
  269. }
  270. });
  271. }
  272. /**
  273. * Append a portion of the current buffer to the SWF.
  274. *
  275. * @private
  276. */
  277. processBuffer_() {
  278. let chunkSize = FlashConstants.BYTES_PER_CHUNK;
  279. if (!this.buffer_.length) {
  280. if (this.updating !== false) {
  281. this.updating = false;
  282. this.trigger({ type: 'updateend' });
  283. }
  284. // do nothing if the buffer is empty
  285. return;
  286. }
  287. // concatenate appends up to the max append size
  288. let chunk = this.buffer_[0].subarray(0, chunkSize);
  289. // requeue any bytes that won't make it this round
  290. if (chunk.byteLength &lt; chunkSize ||
  291. this.buffer_[0].byteLength === chunkSize) {
  292. this.buffer_.shift();
  293. } else {
  294. this.buffer_[0] = this.buffer_[0].subarray(chunkSize);
  295. }
  296. this.bufferSize_ -= chunk.byteLength;
  297. // base64 encode the bytes
  298. let binary = [];
  299. let length = chunk.byteLength;
  300. for (let i = 0; i &lt; length; i++) {
  301. binary.push(String.fromCharCode(chunk[i]));
  302. }
  303. let b64str = window.btoa(binary.join(''));
  304. window[this.flashEncodedDataName_] = () => {
  305. // schedule another processBuffer to process any left over data or to
  306. // trigger updateend
  307. scheduleTick(this.processBuffer_.bind(this));
  308. delete window[this.flashEncodedDataName_];
  309. return b64str;
  310. };
  311. // Notify the swf that segment data is ready to be appended
  312. this.mediaSource_.swfObj.vjs_appendChunkReady(this.flashEncodedDataName_);
  313. }
  314. /**
  315. * Turns an array of flv tags into a Uint8Array representing the
  316. * flv data. Also removes any tags that are before the current
  317. * time so that playback begins at or slightly after the right
  318. * place on a seek
  319. *
  320. * @private
  321. * @param {Object} segmentData object of segment data
  322. */
  323. convertTagsToData_(segmentData) {
  324. let segmentByteLength = 0;
  325. let tech = this.mediaSource_.tech_;
  326. let videoTargetPts = 0;
  327. let segment;
  328. let videoTags = segmentData.tags.videoTags;
  329. let audioTags = segmentData.tags.audioTags;
  330. // Establish the media timeline to PTS translation if we don't
  331. // have one already
  332. if (isNaN(this.basePtsOffset_) &amp;&amp; (videoTags.length || audioTags.length)) {
  333. // We know there is at least one video or audio tag, but since we may not have both,
  334. // we use pts: Infinity for the missing tag. The will force the following Math.min
  335. // call will to use the proper pts value since it will always be less than Infinity
  336. const firstVideoTag = videoTags[0] || { pts: Infinity };
  337. const firstAudioTag = audioTags[0] || { pts: Infinity };
  338. this.basePtsOffset_ = Math.min(firstAudioTag.pts, firstVideoTag.pts);
  339. }
  340. if (tech.seeking()) {
  341. // Do not use previously saved buffer end values while seeking since buffer
  342. // is cleared on all seeks
  343. this.videoBufferEnd_ = NaN;
  344. this.audioBufferEnd_ = NaN;
  345. }
  346. if (isNaN(this.videoBufferEnd_)) {
  347. if (tech.buffered().length) {
  348. videoTargetPts = tech.buffered().end(0) - this.timestampOffset;
  349. }
  350. // Trim to currentTime if seeking
  351. if (tech.seeking()) {
  352. videoTargetPts = Math.max(videoTargetPts, tech.currentTime() - this.timestampOffset);
  353. }
  354. // PTS values are represented in milliseconds
  355. videoTargetPts *= 1e3;
  356. videoTargetPts += this.basePtsOffset_;
  357. } else {
  358. // Add a fudge factor of 0.1 to the last video pts appended since a rendition change
  359. // could append an overlapping segment, in which case there is a high likelyhood
  360. // a tag could have a matching pts to videoBufferEnd_, which would cause
  361. // that tag to get appended by the tag.pts >= targetPts check below even though it
  362. // is a duplicate of what was previously appended
  363. videoTargetPts = this.videoBufferEnd_ + 0.1;
  364. }
  365. // filter complete GOPs with a presentation time less than the seek target/end of buffer
  366. let currentIndex = videoTags.length;
  367. // if the last tag is beyond videoTargetPts, then do not search the list for a GOP
  368. // since our videoTargetPts lies in a future segment
  369. if (currentIndex &amp;&amp; videoTags[currentIndex - 1].pts >= videoTargetPts) {
  370. // Start by walking backwards from the end of the list until we reach a tag that
  371. // is equal to or less than videoTargetPts
  372. while (--currentIndex) {
  373. const currentTag = videoTags[currentIndex];
  374. if (currentTag.pts > videoTargetPts) {
  375. continue;
  376. }
  377. // if we see a keyFrame or metadata tag once we've gone below videoTargetPts,
  378. // exit the loop as this is the start of the GOP that we want to append
  379. if (currentTag.keyFrame || currentTag.metaDataTag) {
  380. break;
  381. }
  382. }
  383. // We need to check if there are any metadata tags that come before currentIndex
  384. // as those will be metadata tags associated with the GOP we are appending
  385. // There could be 0 to 2 metadata tags that come before the currentIndex depending
  386. // on what videoTargetPts is and whether the transmuxer prepended metadata tags to this
  387. // key frame
  388. while (currentIndex) {
  389. const nextTag = videoTags[currentIndex - 1];
  390. if (!nextTag.metaDataTag) {
  391. break;
  392. }
  393. currentIndex--;
  394. }
  395. }
  396. const filteredVideoTags = videoTags.slice(currentIndex);
  397. let audioTargetPts;
  398. if (isNaN(this.audioBufferEnd_)) {
  399. audioTargetPts = videoTargetPts;
  400. } else {
  401. // Add a fudge factor of 0.1 to the last video pts appended since a rendition change
  402. // could append an overlapping segment, in which case there is a high likelyhood
  403. // a tag could have a matching pts to videoBufferEnd_, which would cause
  404. // that tag to get appended by the tag.pts >= targetPts check below even though it
  405. // is a duplicate of what was previously appended
  406. audioTargetPts = this.audioBufferEnd_ + 0.1;
  407. }
  408. if (filteredVideoTags.length) {
  409. // If targetPts intersects a GOP and we appended the tags for the GOP that came
  410. // before targetPts, we want to make sure to trim audio tags at the pts
  411. // of the first video tag to avoid brief moments of silence
  412. audioTargetPts = Math.min(audioTargetPts, filteredVideoTags[0].pts);
  413. }
  414. // skip tags with a presentation time less than the seek target/end of buffer
  415. currentIndex = 0;
  416. while (currentIndex &lt; audioTags.length) {
  417. if (audioTags[currentIndex].pts >= audioTargetPts) {
  418. break;
  419. }
  420. currentIndex++;
  421. }
  422. const filteredAudioTags = audioTags.slice(currentIndex);
  423. // update the audio and video buffer ends
  424. if (filteredAudioTags.length) {
  425. this.audioBufferEnd_ = filteredAudioTags[filteredAudioTags.length - 1].pts;
  426. }
  427. if (filteredVideoTags.length) {
  428. this.videoBufferEnd_ = filteredVideoTags[filteredVideoTags.length - 1].pts;
  429. }
  430. let tags = this.getOrderedTags_(filteredVideoTags, filteredAudioTags);
  431. if (tags.length === 0) {
  432. return;
  433. }
  434. // If we are appending data that comes before our target pts, we want to tell
  435. // the swf to adjust its notion of current time to account for the extra tags
  436. // we are appending to complete the GOP that intersects with targetPts
  437. if (tags[0].pts &lt; videoTargetPts &amp;&amp; tech.seeking()) {
  438. const fudgeFactor = 1 / 30;
  439. const currentTime = tech.currentTime();
  440. const diff = (videoTargetPts - tags[0].pts) / 1e3;
  441. let adjustedTime = currentTime - diff;
  442. if (adjustedTime &lt; fudgeFactor) {
  443. adjustedTime = 0;
  444. }
  445. try {
  446. this.mediaSource_.swfObj.vjs_adjustCurrentTime(adjustedTime);
  447. } catch (e) {
  448. // no-op for backwards compatability of swf. If adjustCurrentTime fails,
  449. // the swf may incorrectly report currentTime and buffered ranges
  450. // but should not affect playback over than the time displayed on the
  451. // progress bar is inaccurate
  452. }
  453. }
  454. // concatenate the bytes into a single segment
  455. for (let i = 0; i &lt; tags.length; i++) {
  456. segmentByteLength += tags[i].bytes.byteLength;
  457. }
  458. segment = new Uint8Array(segmentByteLength);
  459. for (let i = 0, j = 0; i &lt; tags.length; i++) {
  460. segment.set(tags[i].bytes, j);
  461. j += tags[i].bytes.byteLength;
  462. }
  463. return segment;
  464. }
  465. /**
  466. * Assemble the FLV tags in decoder order.
  467. *
  468. * @private
  469. * @param {Array} videoTags list of video tags
  470. * @param {Array} audioTags list of audio tags
  471. */
  472. getOrderedTags_(videoTags, audioTags) {
  473. let tag;
  474. let tags = [];
  475. while (videoTags.length || audioTags.length) {
  476. if (!videoTags.length) {
  477. // only audio tags remain
  478. tag = audioTags.shift();
  479. } else if (!audioTags.length) {
  480. // only video tags remain
  481. tag = videoTags.shift();
  482. } else if (audioTags[0].dts &lt; videoTags[0].dts) {
  483. // audio should be decoded next
  484. tag = audioTags.shift();
  485. } else {
  486. // video should be decoded next
  487. tag = videoTags.shift();
  488. }
  489. tags.push(tag);
  490. }
  491. return tags;
  492. }
  493. onHlsReset_() {
  494. this.transmuxer_.postMessage({action: 'resetCaptions'});
  495. }
  496. }
  497. </code></pre>
  498. </article>
  499. </section>
  500. </div>
  501. <nav>
  502. <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>
  503. </nav>
  504. <br class="clear">
  505. <footer>
  506. 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)
  507. </footer>
  508. <script> prettyPrint(); </script>
  509. <script src="scripts/linenumber.js"> </script>
  510. </body>
  511. </html>