videojs-contrib-hls.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /**
  2. * @file videojs-contrib-hls.js
  3. *
  4. * The main file for the HLS project.
  5. * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE
  6. */
  7. import document from 'global/document';
  8. import PlaylistLoader from './playlist-loader';
  9. import Playlist from './playlist';
  10. import xhrFactory from './xhr';
  11. import {Decrypter, AsyncStream, decrypt} from 'aes-decrypter';
  12. import utils from './bin-utils';
  13. import {MediaSource, URL} from 'videojs-contrib-media-sources';
  14. import m3u8 from 'm3u8-parser';
  15. import videojs from 'video.js';
  16. import { MasterPlaylistController } from './master-playlist-controller';
  17. import Config from './config';
  18. import renditionSelectionMixin from './rendition-mixin';
  19. import window from 'global/window';
  20. import PlaybackWatcher from './playback-watcher';
  21. import reloadSourceOnError from './reload-source-on-error';
  22. import {
  23. lastBandwidthSelector,
  24. lowestBitrateCompatibleVariantSelector,
  25. comparePlaylistBandwidth,
  26. comparePlaylistResolution
  27. } from './playlist-selectors.js';
  28. const Hls = {
  29. PlaylistLoader,
  30. Playlist,
  31. Decrypter,
  32. AsyncStream,
  33. decrypt,
  34. utils,
  35. STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
  36. INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
  37. comparePlaylistBandwidth,
  38. comparePlaylistResolution,
  39. xhr: xhrFactory()
  40. };
  41. // 0.5 MB/s
  42. const INITIAL_BANDWIDTH = 4194304;
  43. // Define getter/setters for config properites
  44. [
  45. 'GOAL_BUFFER_LENGTH',
  46. 'MAX_GOAL_BUFFER_LENGTH',
  47. 'GOAL_BUFFER_LENGTH_RATE',
  48. 'BUFFER_LOW_WATER_LINE',
  49. 'MAX_BUFFER_LOW_WATER_LINE',
  50. 'BUFFER_LOW_WATER_LINE_RATE',
  51. 'BANDWIDTH_VARIANCE'
  52. ].forEach((prop) => {
  53. Object.defineProperty(Hls, prop, {
  54. get() {
  55. videojs.log.warn(`using Hls.${prop} is UNSAFE be sure you know what you are doing`);
  56. return Config[prop];
  57. },
  58. set(value) {
  59. videojs.log.warn(`using Hls.${prop} is UNSAFE be sure you know what you are doing`);
  60. if (typeof value !== 'number' || value < 0) {
  61. videojs.log.warn(`value of Hls.${prop} must be greater than or equal to 0`);
  62. return;
  63. }
  64. Config[prop] = value;
  65. }
  66. });
  67. });
  68. /**
  69. * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls.
  70. *
  71. * @param {QualityLevelList} qualityLevels The QualityLevelList to update.
  72. * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
  73. * @function handleHlsMediaChange
  74. */
  75. const handleHlsMediaChange = function(qualityLevels, playlistLoader) {
  76. let newPlaylist = playlistLoader.media();
  77. let selectedIndex = -1;
  78. for (let i = 0; i < qualityLevels.length; i++) {
  79. if (qualityLevels[i].id === newPlaylist.uri) {
  80. selectedIndex = i;
  81. break;
  82. }
  83. }
  84. qualityLevels.selectedIndex_ = selectedIndex;
  85. qualityLevels.trigger({
  86. selectedIndex,
  87. type: 'change'
  88. });
  89. };
  90. /**
  91. * Adds quality levels to list once playlist metadata is available
  92. *
  93. * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to.
  94. * @param {Object} hls Hls object to listen to for media events.
  95. * @function handleHlsLoadedMetadata
  96. */
  97. const handleHlsLoadedMetadata = function(qualityLevels, hls) {
  98. hls.representations().forEach((rep) => {
  99. qualityLevels.addQualityLevel(rep);
  100. });
  101. handleHlsMediaChange(qualityLevels, hls.playlists);
  102. };
  103. // HLS is a source handler, not a tech. Make sure attempts to use it
  104. // as one do not cause exceptions.
  105. Hls.canPlaySource = function() {
  106. return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
  107. 'your player\'s techOrder.');
  108. };
  109. /**
  110. * Whether the browser has built-in HLS support.
  111. */
  112. Hls.supportsNativeHls = (function() {
  113. let video = document.createElement('video');
  114. // native HLS is definitely not supported if HTML5 video isn't
  115. if (!videojs.getTech('Html5').isSupported()) {
  116. return false;
  117. }
  118. // HLS manifests can go by many mime-types
  119. let canPlay = [
  120. // Apple santioned
  121. 'application/vnd.apple.mpegurl',
  122. // Apple sanctioned for backwards compatibility
  123. 'audio/mpegurl',
  124. // Very common
  125. 'audio/x-mpegurl',
  126. // Very common
  127. 'application/x-mpegurl',
  128. // Included for completeness
  129. 'video/x-mpegurl',
  130. 'video/mpegurl',
  131. 'application/mpegurl'
  132. ];
  133. return canPlay.some(function(canItPlay) {
  134. return (/maybe|probably/i).test(video.canPlayType(canItPlay));
  135. });
  136. }());
  137. /**
  138. * HLS is a source handler, not a tech. Make sure attempts to use it
  139. * as one do not cause exceptions.
  140. */
  141. Hls.isSupported = function() {
  142. return videojs.log.warn('HLS is no longer a tech. Please remove it from ' +
  143. 'your player\'s techOrder.');
  144. };
  145. const Component = videojs.getComponent('Component');
  146. /**
  147. * The Hls Handler object, where we orchestrate all of the parts
  148. * of HLS to interact with video.js
  149. *
  150. * @class HlsHandler
  151. * @extends videojs.Component
  152. * @param {Object} source the soruce object
  153. * @param {Tech} tech the parent tech object
  154. * @param {Object} options optional and required options
  155. */
  156. class HlsHandler extends Component {
  157. constructor(source, tech, options) {
  158. super(tech, options.hls);
  159. // tech.player() is deprecated but setup a reference to HLS for
  160. // backwards-compatibility
  161. if (tech.options_ && tech.options_.playerId) {
  162. let _player = videojs(tech.options_.playerId);
  163. if (!_player.hasOwnProperty('hls')) {
  164. Object.defineProperty(_player, 'hls', {
  165. get: () => {
  166. videojs.log.warn('player.hls is deprecated. Use player.tech_.hls instead.');
  167. tech.trigger({type: 'usage', name: 'hls-player-access'});
  168. return this;
  169. }
  170. });
  171. }
  172. }
  173. this.tech_ = tech;
  174. this.source_ = source;
  175. this.stats = {};
  176. this.ignoreNextSeekingEvent_ = false;
  177. this.setOptions_();
  178. // overriding native HLS only works if audio tracks have been emulated
  179. // error early if we're misconfigured:
  180. if (this.options_.overrideNative &&
  181. (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
  182. throw new Error('Overriding native HLS requires emulated tracks. ' +
  183. 'See https://git.io/vMpjB');
  184. }
  185. // listen for fullscreenchange events for this player so that we
  186. // can adjust our quality selection quickly
  187. this.on(document, [
  188. 'fullscreenchange', 'webkitfullscreenchange',
  189. 'mozfullscreenchange', 'MSFullscreenChange'
  190. ], (event) => {
  191. let fullscreenElement = document.fullscreenElement ||
  192. document.webkitFullscreenElement ||
  193. document.mozFullScreenElement ||
  194. document.msFullscreenElement;
  195. if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
  196. this.masterPlaylistController_.fastQualityChange_();
  197. }
  198. });
  199. this.on(this.tech_, 'seeking', function() {
  200. if (this.ignoreNextSeekingEvent_) {
  201. this.ignoreNextSeekingEvent_ = false;
  202. return;
  203. }
  204. this.setCurrentTime(this.tech_.currentTime());
  205. });
  206. this.on(this.tech_, 'error', function() {
  207. if (this.masterPlaylistController_) {
  208. this.masterPlaylistController_.pauseLoading();
  209. }
  210. });
  211. this.on(this.tech_, 'play', this.play);
  212. }
  213. setOptions_() {
  214. // defaults
  215. this.options_.withCredentials = this.options_.withCredentials || false;
  216. if (typeof this.options_.blacklistDuration !== 'number') {
  217. this.options_.blacklistDuration = 5 * 60;
  218. }
  219. // start playlist selection at a reasonable bandwidth for
  220. // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s)
  221. if (typeof this.options_.bandwidth !== 'number') {
  222. this.options_.bandwidth = INITIAL_BANDWIDTH;
  223. }
  224. // If the bandwidth number is unchanged from the initial setting
  225. // then this takes precedence over the enableLowInitialPlaylist option
  226. this.options_.enableLowInitialPlaylist =
  227. this.options_.enableLowInitialPlaylist &&
  228. this.options_.bandwidth === INITIAL_BANDWIDTH;
  229. // grab options passed to player.src
  230. ['withCredentials', 'bandwidth', 'handleManifestRedirects'].forEach((option) => {
  231. if (typeof this.source_[option] !== 'undefined') {
  232. this.options_[option] = this.source_[option];
  233. }
  234. });
  235. this.bandwidth = this.options_.bandwidth;
  236. }
  237. /**
  238. * called when player.src gets called, handle a new source
  239. *
  240. * @param {Object} src the source object to handle
  241. */
  242. src(src) {
  243. // do nothing if the src is falsey
  244. if (!src) {
  245. return;
  246. }
  247. this.setOptions_();
  248. // add master playlist controller options
  249. this.options_.url = this.source_.src;
  250. this.options_.tech = this.tech_;
  251. this.options_.externHls = Hls;
  252. this.masterPlaylistController_ = new MasterPlaylistController(this.options_);
  253. this.playbackWatcher_ = new PlaybackWatcher(
  254. videojs.mergeOptions(this.options_, {
  255. seekable: () => this.seekable()
  256. }));
  257. this.masterPlaylistController_.on('error', () => {
  258. let player = videojs.players[this.tech_.options_.playerId];
  259. player.error(this.masterPlaylistController_.error);
  260. });
  261. // `this` in selectPlaylist should be the HlsHandler for backwards
  262. // compatibility with < v2
  263. this.masterPlaylistController_.selectPlaylist =
  264. this.selectPlaylist ?
  265. this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this);
  266. this.masterPlaylistController_.selectInitialPlaylist =
  267. Hls.INITIAL_PLAYLIST_SELECTOR.bind(this);
  268. // re-expose some internal objects for backwards compatibility with < v2
  269. this.playlists = this.masterPlaylistController_.masterPlaylistLoader_;
  270. this.mediaSource = this.masterPlaylistController_.mediaSource;
  271. // Proxy assignment of some properties to the master playlist
  272. // controller. Using a custom property for backwards compatibility
  273. // with < v2
  274. Object.defineProperties(this, {
  275. selectPlaylist: {
  276. get() {
  277. return this.masterPlaylistController_.selectPlaylist;
  278. },
  279. set(selectPlaylist) {
  280. this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this);
  281. }
  282. },
  283. throughput: {
  284. get() {
  285. return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate;
  286. },
  287. set(throughput) {
  288. this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput;
  289. // By setting `count` to 1 the throughput value becomes the starting value
  290. // for the cumulative average
  291. this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1;
  292. }
  293. },
  294. bandwidth: {
  295. get() {
  296. return this.masterPlaylistController_.mainSegmentLoader_.bandwidth;
  297. },
  298. set(bandwidth) {
  299. this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth;
  300. // setting the bandwidth manually resets the throughput counter
  301. // `count` is set to zero that current value of `rate` isn't included
  302. // in the cumulative average
  303. this.masterPlaylistController_.mainSegmentLoader_.throughput = {
  304. rate: 0,
  305. count: 0
  306. };
  307. }
  308. },
  309. /**
  310. * `systemBandwidth` is a combination of two serial processes bit-rates. The first
  311. * is the network bitrate provided by `bandwidth` and the second is the bitrate of
  312. * the entire process after that - decryption, transmuxing, and appending - provided
  313. * by `throughput`.
  314. *
  315. * Since the two process are serial, the overall system bandwidth is given by:
  316. * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
  317. */
  318. systemBandwidth: {
  319. get() {
  320. let invBandwidth = 1 / (this.bandwidth || 1);
  321. let invThroughput;
  322. if (this.throughput > 0) {
  323. invThroughput = 1 / this.throughput;
  324. } else {
  325. invThroughput = 0;
  326. }
  327. let systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
  328. return systemBitrate;
  329. },
  330. set() {
  331. videojs.log.error('The "systemBandwidth" property is read-only');
  332. }
  333. }
  334. });
  335. Object.defineProperties(this.stats, {
  336. bandwidth: {
  337. get: () => this.bandwidth || 0,
  338. enumerable: true
  339. },
  340. mediaRequests: {
  341. get: () => this.masterPlaylistController_.mediaRequests_() || 0,
  342. enumerable: true
  343. },
  344. mediaRequestsAborted: {
  345. get: () => this.masterPlaylistController_.mediaRequestsAborted_() || 0,
  346. enumerable: true
  347. },
  348. mediaRequestsTimedout: {
  349. get: () => this.masterPlaylistController_.mediaRequestsTimedout_() || 0,
  350. enumerable: true
  351. },
  352. mediaRequestsErrored: {
  353. get: () => this.masterPlaylistController_.mediaRequestsErrored_() || 0,
  354. enumerable: true
  355. },
  356. mediaTransferDuration: {
  357. get: () => this.masterPlaylistController_.mediaTransferDuration_() || 0,
  358. enumerable: true
  359. },
  360. mediaBytesTransferred: {
  361. get: () => this.masterPlaylistController_.mediaBytesTransferred_() || 0,
  362. enumerable: true
  363. },
  364. mediaSecondsLoaded: {
  365. get: () => this.masterPlaylistController_.mediaSecondsLoaded_() || 0,
  366. enumerable: true
  367. }
  368. });
  369. this.tech_.one('canplay',
  370. this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_));
  371. this.masterPlaylistController_.on('selectedinitialmedia', () => {
  372. // Add the manual rendition mix-in to HlsHandler
  373. renditionSelectionMixin(this);
  374. });
  375. // the bandwidth of the primary segment loader is our best
  376. // estimate of overall bandwidth
  377. this.on(this.masterPlaylistController_, 'progress', function() {
  378. this.tech_.trigger('progress');
  379. });
  380. // In the live case, we need to ignore the very first `seeking` event since
  381. // that will be the result of the seek-to-live behavior
  382. this.on(this.masterPlaylistController_, 'firstplay', function() {
  383. this.ignoreNextSeekingEvent_ = true;
  384. });
  385. this.tech_.ready(() => this.setupQualityLevels_());
  386. // do nothing if the tech has been disposed already
  387. // this can occur if someone sets the src in player.ready(), for instance
  388. if (!this.tech_.el()) {
  389. return;
  390. }
  391. this.tech_.src(videojs.URL.createObjectURL(
  392. this.masterPlaylistController_.mediaSource));
  393. }
  394. /**
  395. * Initializes the quality levels and sets listeners to update them.
  396. *
  397. * @method setupQualityLevels_
  398. * @private
  399. */
  400. setupQualityLevels_() {
  401. let player = videojs.players[this.tech_.options_.playerId];
  402. if (player && player.qualityLevels) {
  403. this.qualityLevels_ = player.qualityLevels();
  404. this.masterPlaylistController_.on('selectedinitialmedia', () => {
  405. handleHlsLoadedMetadata(this.qualityLevels_, this);
  406. });
  407. this.playlists.on('mediachange', () => {
  408. handleHlsMediaChange(this.qualityLevels_, this.playlists);
  409. });
  410. }
  411. }
  412. /**
  413. * Begin playing the video.
  414. */
  415. play() {
  416. this.masterPlaylistController_.play();
  417. }
  418. /**
  419. * a wrapper around the function in MasterPlaylistController
  420. */
  421. setCurrentTime(currentTime) {
  422. this.masterPlaylistController_.setCurrentTime(currentTime);
  423. }
  424. /**
  425. * a wrapper around the function in MasterPlaylistController
  426. */
  427. duration() {
  428. return this.masterPlaylistController_.duration();
  429. }
  430. /**
  431. * a wrapper around the function in MasterPlaylistController
  432. */
  433. seekable() {
  434. return this.masterPlaylistController_.seekable();
  435. }
  436. /**
  437. * Abort all outstanding work and cleanup.
  438. */
  439. dispose() {
  440. if (this.playbackWatcher_) {
  441. this.playbackWatcher_.dispose();
  442. }
  443. if (this.masterPlaylistController_) {
  444. this.masterPlaylistController_.dispose();
  445. }
  446. if (this.qualityLevels_) {
  447. this.qualityLevels_.dispose();
  448. }
  449. super.dispose();
  450. }
  451. }
  452. /**
  453. * The Source Handler object, which informs video.js what additional
  454. * MIME types are supported and sets up playback. It is registered
  455. * automatically to the appropriate tech based on the capabilities of
  456. * the browser it is running in. It is not necessary to use or modify
  457. * this object in normal usage.
  458. */
  459. const HlsSourceHandler = function(mode) {
  460. return {
  461. canHandleSource(srcObj, options = {}) {
  462. let localOptions = videojs.mergeOptions(videojs.options, options);
  463. // this forces video.js to skip this tech/mode if its not the one we have been
  464. // overriden to use, by returing that we cannot handle the source.
  465. if (localOptions.hls &&
  466. localOptions.hls.mode &&
  467. localOptions.hls.mode !== mode) {
  468. return false;
  469. }
  470. return HlsSourceHandler.canPlayType(srcObj.type, localOptions);
  471. },
  472. handleSource(source, tech, options = {}) {
  473. let localOptions = videojs.mergeOptions(videojs.options, options, {hls: {mode}});
  474. if (mode === 'flash') {
  475. // We need to trigger this asynchronously to give others the chance
  476. // to bind to the event when a source is set at player creation
  477. tech.setTimeout(function() {
  478. tech.trigger('loadstart');
  479. }, 1);
  480. }
  481. tech.hls = new HlsHandler(source, tech, localOptions);
  482. tech.hls.xhr = xhrFactory();
  483. tech.hls.src(source.src);
  484. return tech.hls;
  485. },
  486. canPlayType(type, options = {}) {
  487. let localOptions = videojs.mergeOptions(videojs.options, options);
  488. if (HlsSourceHandler.canPlayType(type, localOptions)) {
  489. return 'maybe';
  490. }
  491. return '';
  492. }
  493. };
  494. };
  495. HlsSourceHandler.canPlayType = function(type, options) {
  496. // No support for IE 10 or below
  497. if (videojs.browser.IE_VERSION && videojs.browser.IE_VERSION <= 10) {
  498. return false;
  499. }
  500. let mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
  501. // favor native HLS support if it's available
  502. if (!options.hls.overrideNative && Hls.supportsNativeHls) {
  503. return false;
  504. }
  505. return mpegurlRE.test(type);
  506. };
  507. if (typeof videojs.MediaSource === 'undefined' ||
  508. typeof videojs.URL === 'undefined') {
  509. videojs.MediaSource = MediaSource;
  510. videojs.URL = URL;
  511. }
  512. const flashTech = videojs.getTech('Flash');
  513. // register source handlers with the appropriate techs
  514. if (MediaSource.supportsNativeMediaSources()) {
  515. videojs.getTech('Html5').registerSourceHandler(HlsSourceHandler('html5'), 0);
  516. }
  517. if (window.Uint8Array && flashTech) {
  518. flashTech.registerSourceHandler(HlsSourceHandler('flash'));
  519. }
  520. videojs.HlsHandler = HlsHandler;
  521. videojs.HlsSourceHandler = HlsSourceHandler;
  522. videojs.Hls = Hls;
  523. if (!videojs.use) {
  524. videojs.registerComponent('Hls', Hls);
  525. }
  526. videojs.m3u8 = m3u8;
  527. videojs.options.hls = videojs.options.hls || {};
  528. if (videojs.registerPlugin) {
  529. videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError);
  530. } else {
  531. videojs.plugin('reloadSourceOnError', reloadSourceOnError);
  532. }
  533. module.exports = {
  534. Hls,
  535. HlsHandler,
  536. HlsSourceHandler
  537. };