videojs-http-streaming.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402
  1. /**
  2. * @file videojs-http-streaming.js
  3. *
  4. * The main file for the VHS project.
  5. * License: https://github.com/videojs/videojs-http-streaming/blob/main/LICENSE
  6. */
  7. import document from 'global/document';
  8. import window from 'global/window';
  9. import PlaylistLoader from './playlist-loader';
  10. import Playlist from './playlist';
  11. import xhrFactory from './xhr';
  12. import { simpleTypeFromSourceType } from '@videojs/vhs-utils/es/media-types.js';
  13. import * as utils from './bin-utils';
  14. import {
  15. getProgramTime,
  16. seekToProgramTime
  17. } from './util/time';
  18. import { timeRangesToArray } from './ranges';
  19. import videojs from 'video.js';
  20. import { PlaylistController } from './playlist-controller';
  21. import Config from './config';
  22. import renditionSelectionMixin from './rendition-mixin';
  23. import PlaybackWatcher from './playback-watcher';
  24. import SourceUpdater from './source-updater';
  25. import reloadSourceOnError from './reload-source-on-error';
  26. import {
  27. lastBandwidthSelector,
  28. lowestBitrateCompatibleVariantSelector,
  29. movingAverageBandwidthSelector,
  30. comparePlaylistBandwidth,
  31. comparePlaylistResolution
  32. } from './playlist-selectors.js';
  33. import {
  34. browserSupportsCodec,
  35. getMimeForCodec,
  36. parseCodecs
  37. } from '@videojs/vhs-utils/es/codecs.js';
  38. import { unwrapCodecList } from './util/codecs.js';
  39. import logger from './util/logger';
  40. import {SAFE_TIME_DELTA} from './ranges';
  41. import {merge} from './util/vjs-compat';
  42. // IMPORTANT:
  43. // keep these at the bottom they are replaced at build time
  44. // because webpack and rollup without plugins do not support json
  45. // and we do not want to break our users
  46. import {version as vhsVersion} from '../package.json';
  47. import {version as muxVersion} from 'mux.js/package.json';
  48. import {version as mpdVersion} from 'mpd-parser/package.json';
  49. import {version as m3u8Version} from 'm3u8-parser/package.json';
  50. import {version as aesVersion} from 'aes-decrypter/package.json';
  51. const Vhs = {
  52. PlaylistLoader,
  53. Playlist,
  54. utils,
  55. STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
  56. INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
  57. lastBandwidthSelector,
  58. movingAverageBandwidthSelector,
  59. comparePlaylistBandwidth,
  60. comparePlaylistResolution,
  61. xhr: xhrFactory()
  62. };
  63. // Define getter/setters for config properties
  64. Object.keys(Config).forEach((prop) => {
  65. Object.defineProperty(Vhs, prop, {
  66. get() {
  67. videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
  68. return Config[prop];
  69. },
  70. set(value) {
  71. videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
  72. if (typeof value !== 'number' || value < 0) {
  73. videojs.log.warn(`value of Vhs.${prop} must be greater than or equal to 0`);
  74. return;
  75. }
  76. Config[prop] = value;
  77. }
  78. });
  79. });
  80. export const LOCAL_STORAGE_KEY = 'videojs-vhs';
  81. /**
  82. * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs.
  83. *
  84. * @param {QualityLevelList} qualityLevels The QualityLevelList to update.
  85. * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
  86. * @function handleVhsMediaChange
  87. */
  88. const handleVhsMediaChange = function(qualityLevels, playlistLoader) {
  89. const newPlaylist = playlistLoader.media();
  90. let selectedIndex = -1;
  91. for (let i = 0; i < qualityLevels.length; i++) {
  92. if (qualityLevels[i].id === newPlaylist.id) {
  93. selectedIndex = i;
  94. break;
  95. }
  96. }
  97. qualityLevels.selectedIndex_ = selectedIndex;
  98. qualityLevels.trigger({
  99. selectedIndex,
  100. type: 'change'
  101. });
  102. };
  103. /**
  104. * Adds quality levels to list once playlist metadata is available
  105. *
  106. * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to.
  107. * @param {Object} vhs Vhs object to listen to for media events.
  108. * @function handleVhsLoadedMetadata
  109. */
  110. const handleVhsLoadedMetadata = function(qualityLevels, vhs) {
  111. vhs.representations().forEach((rep) => {
  112. qualityLevels.addQualityLevel(rep);
  113. });
  114. handleVhsMediaChange(qualityLevels, vhs.playlists);
  115. };
  116. // VHS is a source handler, not a tech. Make sure attempts to use it
  117. // as one do not cause exceptions.
  118. Vhs.canPlaySource = function() {
  119. return videojs.log.warn('VHS is no longer a tech. Please remove it from ' +
  120. 'your player\'s techOrder.');
  121. };
  122. const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => {
  123. if (!keySystemOptions) {
  124. return keySystemOptions;
  125. }
  126. let codecs = {};
  127. if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) {
  128. codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS));
  129. }
  130. if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) {
  131. codecs.audio = audioPlaylist.attributes.CODECS;
  132. }
  133. const videoContentType = getMimeForCodec(codecs.video);
  134. const audioContentType = getMimeForCodec(codecs.audio);
  135. // upsert the content types based on the selected playlist
  136. const keySystemContentTypes = {};
  137. for (const keySystem in keySystemOptions) {
  138. keySystemContentTypes[keySystem] = {};
  139. if (audioContentType) {
  140. keySystemContentTypes[keySystem].audioContentType = audioContentType;
  141. }
  142. if (videoContentType) {
  143. keySystemContentTypes[keySystem].videoContentType = videoContentType;
  144. }
  145. // Default to using the video playlist's PSSH even though they may be different, as
  146. // videojs-contrib-eme will only accept one in the options.
  147. //
  148. // This shouldn't be an issue for most cases as early intialization will handle all
  149. // unique PSSH values, and if they aren't, then encrypted events should have the
  150. // specific information needed for the unique license.
  151. if (mainPlaylist.contentProtection &&
  152. mainPlaylist.contentProtection[keySystem] &&
  153. mainPlaylist.contentProtection[keySystem].pssh) {
  154. keySystemContentTypes[keySystem].pssh =
  155. mainPlaylist.contentProtection[keySystem].pssh;
  156. }
  157. // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url'
  158. // so we need to prevent overwriting the URL entirely
  159. if (typeof keySystemOptions[keySystem] === 'string') {
  160. keySystemContentTypes[keySystem].url = keySystemOptions[keySystem];
  161. }
  162. }
  163. return merge(keySystemOptions, keySystemContentTypes);
  164. };
  165. /**
  166. * @typedef {Object} KeySystems
  167. *
  168. * keySystems configuration for https://github.com/videojs/videojs-contrib-eme
  169. * Note: not all options are listed here.
  170. *
  171. * @property {Uint8Array} [pssh]
  172. * Protection System Specific Header
  173. */
  174. /**
  175. * Goes through all the playlists and collects an array of KeySystems options objects
  176. * containing each playlist's keySystems and their pssh values, if available.
  177. *
  178. * @param {Object[]} playlists
  179. * The playlists to look through
  180. * @param {string[]} keySystems
  181. * The keySystems to collect pssh values for
  182. *
  183. * @return {KeySystems[]}
  184. * An array of KeySystems objects containing available key systems and their
  185. * pssh values
  186. */
  187. const getAllPsshKeySystemsOptions = (playlists, keySystems) => {
  188. return playlists.reduce((keySystemsArr, playlist) => {
  189. if (!playlist.contentProtection) {
  190. return keySystemsArr;
  191. }
  192. const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => {
  193. const keySystemOptions = playlist.contentProtection[keySystem];
  194. if (keySystemOptions && keySystemOptions.pssh) {
  195. keySystemsObj[keySystem] = { pssh: keySystemOptions.pssh };
  196. }
  197. return keySystemsObj;
  198. }, {});
  199. if (Object.keys(keySystemsOptions).length) {
  200. keySystemsArr.push(keySystemsOptions);
  201. }
  202. return keySystemsArr;
  203. }, []);
  204. };
  205. /**
  206. * Returns a promise that waits for the
  207. * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session.
  208. *
  209. * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11
  210. * browsers.
  211. *
  212. * As per the above ticket, this is particularly important for Chrome, where, if
  213. * unencrypted content is appended before encrypted content and the key session has not
  214. * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached
  215. * during playback.
  216. *
  217. * @param {Object} player
  218. * The player instance
  219. * @param {Object[]} sourceKeySystems
  220. * The key systems options from the player source
  221. * @param {Object} [audioMedia]
  222. * The active audio media playlist (optional)
  223. * @param {Object[]} mainPlaylists
  224. * The playlists found on the main playlist object
  225. *
  226. * @return {Object}
  227. * Promise that resolves when the key session has been created
  228. */
  229. export const waitForKeySessionCreation = ({
  230. player,
  231. sourceKeySystems,
  232. audioMedia,
  233. mainPlaylists
  234. }) => {
  235. if (!player.eme.initializeMediaKeys) {
  236. return Promise.resolve();
  237. }
  238. // TODO should all audio PSSH values be initialized for DRM?
  239. //
  240. // All unique video rendition pssh values are initialized for DRM, but here only
  241. // the initial audio playlist license is initialized. In theory, an encrypted
  242. // event should be fired if the user switches to an alternative audio playlist
  243. // where a license is required, but this case hasn't yet been tested. In addition, there
  244. // may be many alternate audio playlists unlikely to be used (e.g., multiple different
  245. // languages).
  246. const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists;
  247. const keySystemsOptionsArr = getAllPsshKeySystemsOptions(
  248. playlists,
  249. Object.keys(sourceKeySystems)
  250. );
  251. const initializationFinishedPromises = [];
  252. const keySessionCreatedPromises = [];
  253. // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The
  254. // only place where it should not be deduped is for ms-prefixed APIs, but
  255. // the existence of modern EME APIs in addition to
  256. // ms-prefixed APIs on Edge should prevent this from being a concern.
  257. // initializeMediaKeys also won't use the webkit-prefixed APIs.
  258. keySystemsOptionsArr.forEach((keySystemsOptions) => {
  259. keySessionCreatedPromises.push(new Promise((resolve, reject) => {
  260. player.tech_.one('keysessioncreated', resolve);
  261. }));
  262. initializationFinishedPromises.push(new Promise((resolve, reject) => {
  263. player.eme.initializeMediaKeys({
  264. keySystems: keySystemsOptions
  265. }, (err) => {
  266. if (err) {
  267. reject(err);
  268. return;
  269. }
  270. resolve();
  271. });
  272. }));
  273. });
  274. // The reasons Promise.race is chosen over Promise.any:
  275. //
  276. // * Promise.any is only available in Safari 14+.
  277. // * None of these promises are expected to reject. If they do reject, it might be
  278. // better here for the race to surface the rejection, rather than mask it by using
  279. // Promise.any.
  280. return Promise.race([
  281. // If a session was previously created, these will all finish resolving without
  282. // creating a new session, otherwise it will take until the end of all license
  283. // requests, which is why the key session check is used (to make setup much faster).
  284. Promise.all(initializationFinishedPromises),
  285. // Once a single session is created, the browser knows DRM will be used.
  286. Promise.race(keySessionCreatedPromises)
  287. ]);
  288. };
  289. /**
  290. * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and
  291. * there are keySystems on the source, sets up source options to prepare the source for
  292. * eme.
  293. *
  294. * @param {Object} player
  295. * The player instance
  296. * @param {Object[]} sourceKeySystems
  297. * The key systems options from the player source
  298. * @param {Object} media
  299. * The active media playlist
  300. * @param {Object} [audioMedia]
  301. * The active audio media playlist (optional)
  302. *
  303. * @return {boolean}
  304. * Whether or not options were configured and EME is available
  305. */
  306. const setupEmeOptions = ({
  307. player,
  308. sourceKeySystems,
  309. media,
  310. audioMedia
  311. }) => {
  312. const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia);
  313. if (!sourceOptions) {
  314. return false;
  315. }
  316. player.currentSource().keySystems = sourceOptions;
  317. // eme handles the rest of the setup, so if it is missing
  318. // do nothing.
  319. if (sourceOptions && !player.eme) {
  320. videojs.log.warn('DRM encrypted source cannot be decrypted without a DRM plugin');
  321. return false;
  322. }
  323. return true;
  324. };
  325. const getVhsLocalStorage = () => {
  326. if (!window.localStorage) {
  327. return null;
  328. }
  329. const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY);
  330. if (!storedObject) {
  331. return null;
  332. }
  333. try {
  334. return JSON.parse(storedObject);
  335. } catch (e) {
  336. // someone may have tampered with the value
  337. return null;
  338. }
  339. };
  340. const updateVhsLocalStorage = (options) => {
  341. if (!window.localStorage) {
  342. return false;
  343. }
  344. let objectToStore = getVhsLocalStorage();
  345. objectToStore = objectToStore ? merge(objectToStore, options) : options;
  346. try {
  347. window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore));
  348. } catch (e) {
  349. // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where
  350. // storage is set to 0).
  351. // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions
  352. // No need to perform any operation.
  353. return false;
  354. }
  355. return objectToStore;
  356. };
  357. /**
  358. * Parses VHS-supported media types from data URIs. See
  359. * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
  360. * for information on data URIs.
  361. *
  362. * @param {string} dataUri
  363. * The data URI
  364. *
  365. * @return {string|Object}
  366. * The parsed object/string, or the original string if no supported media type
  367. * was found
  368. */
  369. const expandDataUri = (dataUri) => {
  370. if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) {
  371. return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1));
  372. }
  373. // no known case for this data URI, return the string as-is
  374. return dataUri;
  375. };
  376. /**
  377. * Adds a request hook to an xhr object
  378. *
  379. * @param {Object} xhr object to add the onRequest hook to
  380. * @param {function} callback hook function for an xhr request
  381. */
  382. const addOnRequestHook = (xhr, callback) => {
  383. if (!xhr._requestCallbackSet) {
  384. xhr._requestCallbackSet = new Set();
  385. }
  386. xhr._requestCallbackSet.add(callback);
  387. };
  388. /**
  389. * Adds a response hook to an xhr object
  390. *
  391. * @param {Object} xhr object to add the onResponse hook to
  392. * @param {function} callback hook function for an xhr response
  393. */
  394. const addOnResponseHook = (xhr, callback) => {
  395. if (!xhr._responseCallbackSet) {
  396. xhr._responseCallbackSet = new Set();
  397. }
  398. xhr._responseCallbackSet.add(callback);
  399. };
  400. /**
  401. * Removes a request hook on an xhr object, deletes the onRequest set if empty.
  402. *
  403. * @param {Object} xhr object to remove the onRequest hook from
  404. * @param {function} callback hook function to remove
  405. */
  406. const removeOnRequestHook = (xhr, callback) => {
  407. if (!xhr._requestCallbackSet) {
  408. return;
  409. }
  410. xhr._requestCallbackSet.delete(callback);
  411. if (!xhr._requestCallbackSet.size) {
  412. delete xhr._requestCallbackSet;
  413. }
  414. };
  415. /**
  416. * Removes a response hook on an xhr object, deletes the onResponse set if empty.
  417. *
  418. * @param {Object} xhr object to remove the onResponse hook from
  419. * @param {function} callback hook function to remove
  420. */
  421. const removeOnResponseHook = (xhr, callback) => {
  422. if (!xhr._responseCallbackSet) {
  423. return;
  424. }
  425. xhr._responseCallbackSet.delete(callback);
  426. if (!xhr._responseCallbackSet.size) {
  427. delete xhr._responseCallbackSet;
  428. }
  429. };
  430. /**
  431. * Whether the browser has built-in HLS support.
  432. */
  433. Vhs.supportsNativeHls = (function() {
  434. if (!document || !document.createElement) {
  435. return false;
  436. }
  437. const video = document.createElement('video');
  438. // native HLS is definitely not supported if HTML5 video isn't
  439. if (!videojs.getTech('Html5').isSupported()) {
  440. return false;
  441. }
  442. // HLS manifests can go by many mime-types
  443. const canPlay = [
  444. // Apple santioned
  445. 'application/vnd.apple.mpegurl',
  446. // Apple sanctioned for backwards compatibility
  447. 'audio/mpegurl',
  448. // Very common
  449. 'audio/x-mpegurl',
  450. // Very common
  451. 'application/x-mpegurl',
  452. // Included for completeness
  453. 'video/x-mpegurl',
  454. 'video/mpegurl',
  455. 'application/mpegurl'
  456. ];
  457. return canPlay.some(function(canItPlay) {
  458. return (/maybe|probably/i).test(video.canPlayType(canItPlay));
  459. });
  460. }());
  461. Vhs.supportsNativeDash = (function() {
  462. if (!document || !document.createElement || !videojs.getTech('Html5').isSupported()) {
  463. return false;
  464. }
  465. return (/maybe|probably/i).test(document.createElement('video').canPlayType('application/dash+xml'));
  466. }());
  467. Vhs.supportsTypeNatively = (type) => {
  468. if (type === 'hls') {
  469. return Vhs.supportsNativeHls;
  470. }
  471. if (type === 'dash') {
  472. return Vhs.supportsNativeDash;
  473. }
  474. return false;
  475. };
  476. /**
  477. * VHS is a source handler, not a tech. Make sure attempts to use it
  478. * as one do not cause exceptions.
  479. */
  480. Vhs.isSupported = function() {
  481. return videojs.log.warn('VHS is no longer a tech. Please remove it from ' +
  482. 'your player\'s techOrder.');
  483. };
  484. /**
  485. * A global function for setting an onRequest hook
  486. *
  487. * @param {function} callback for request modifiction
  488. */
  489. Vhs.xhr.onRequest = function(callback) {
  490. addOnRequestHook(Vhs.xhr, callback);
  491. };
  492. /**
  493. * A global function for setting an onResponse hook
  494. *
  495. * @param {callback} callback for response data retrieval
  496. */
  497. Vhs.xhr.onResponse = function(callback) {
  498. addOnResponseHook(Vhs.xhr, callback);
  499. };
  500. /**
  501. * Deletes a global onRequest callback if it exists
  502. *
  503. * @param {function} callback to delete from the global set
  504. */
  505. Vhs.xhr.offRequest = function(callback) {
  506. removeOnRequestHook(Vhs.xhr, callback);
  507. };
  508. /**
  509. * Deletes a global onResponse callback if it exists
  510. *
  511. * @param {function} callback to delete from the global set
  512. */
  513. Vhs.xhr.offResponse = function(callback) {
  514. removeOnResponseHook(Vhs.xhr, callback);
  515. };
  516. const Component = videojs.getComponent('Component');
  517. /**
  518. * The Vhs Handler object, where we orchestrate all of the parts
  519. * of VHS to interact with video.js
  520. *
  521. * @class VhsHandler
  522. * @extends videojs.Component
  523. * @param {Object} source the soruce object
  524. * @param {Tech} tech the parent tech object
  525. * @param {Object} options optional and required options
  526. */
  527. class VhsHandler extends Component {
  528. constructor(source, tech, options) {
  529. super(tech, options.vhs);
  530. // if a tech level `initialBandwidth` option was passed
  531. // use that over the VHS level `bandwidth` option
  532. if (typeof options.initialBandwidth === 'number') {
  533. this.options_.bandwidth = options.initialBandwidth;
  534. }
  535. this.logger_ = logger('VhsHandler');
  536. // we need access to the player in some cases,
  537. // so, get it from Video.js via the `playerId`
  538. if (tech.options_ && tech.options_.playerId) {
  539. const _player = videojs.getPlayer(tech.options_.playerId);
  540. this.player_ = _player;
  541. }
  542. this.tech_ = tech;
  543. this.source_ = source;
  544. this.stats = {};
  545. this.ignoreNextSeekingEvent_ = false;
  546. this.setOptions_();
  547. if (this.options_.overrideNative &&
  548. tech.overrideNativeAudioTracks &&
  549. tech.overrideNativeVideoTracks) {
  550. tech.overrideNativeAudioTracks(true);
  551. tech.overrideNativeVideoTracks(true);
  552. } else if (this.options_.overrideNative &&
  553. (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
  554. // overriding native VHS only works if audio tracks have been emulated
  555. // error early if we're misconfigured
  556. throw new Error('Overriding native VHS requires emulated tracks. ' +
  557. 'See https://git.io/vMpjB');
  558. }
  559. // listen for fullscreenchange events for this player so that we
  560. // can adjust our quality selection quickly
  561. this.on(document, [
  562. 'fullscreenchange', 'webkitfullscreenchange',
  563. 'mozfullscreenchange', 'MSFullscreenChange'
  564. ], (event) => {
  565. const fullscreenElement = document.fullscreenElement ||
  566. document.webkitFullscreenElement ||
  567. document.mozFullScreenElement ||
  568. document.msFullscreenElement;
  569. if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
  570. this.playlistController_.fastQualityChange_();
  571. } else {
  572. // When leaving fullscreen, since the in page pixel dimensions should be smaller
  573. // than full screen, see if there should be a rendition switch down to preserve
  574. // bandwidth.
  575. this.playlistController_.checkABR_();
  576. }
  577. });
  578. this.on(this.tech_, 'seeking', function() {
  579. if (this.ignoreNextSeekingEvent_) {
  580. this.ignoreNextSeekingEvent_ = false;
  581. return;
  582. }
  583. this.setCurrentTime(this.tech_.currentTime());
  584. });
  585. this.on(this.tech_, 'error', function() {
  586. // verify that the error was real and we are loaded
  587. // enough to have pc loaded.
  588. if (this.tech_.error() && this.playlistController_) {
  589. this.playlistController_.pauseLoading();
  590. }
  591. });
  592. this.on(this.tech_, 'play', this.play);
  593. }
  594. /**
  595. * Set VHS options based on options from configuration, as well as partial
  596. * options to be passed at a later time.
  597. *
  598. * @param {Object} options A partial chunk of config options
  599. */
  600. setOptions_(options = {}) {
  601. this.options_ = merge(this.options_, options);
  602. // defaults
  603. this.options_.withCredentials = this.options_.withCredentials || false;
  604. this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true;
  605. this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false;
  606. this.options_.useBandwidthFromLocalStorage =
  607. typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ?
  608. this.source_.useBandwidthFromLocalStorage :
  609. this.options_.useBandwidthFromLocalStorage || false;
  610. this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
  611. this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false;
  612. this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
  613. this.options_.customTagParsers = this.options_.customTagParsers || [];
  614. this.options_.customTagMappers = this.options_.customTagMappers || [];
  615. this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;
  616. this.options_.llhls = this.options_.llhls === false ? false : true;
  617. this.options_.bufferBasedABR = this.options_.bufferBasedABR || false;
  618. if (typeof this.options_.playlistExclusionDuration !== 'number') {
  619. this.options_.playlistExclusionDuration = 60;
  620. }
  621. if (typeof this.options_.bandwidth !== 'number') {
  622. if (this.options_.useBandwidthFromLocalStorage) {
  623. const storedObject = getVhsLocalStorage();
  624. if (storedObject && storedObject.bandwidth) {
  625. this.options_.bandwidth = storedObject.bandwidth;
  626. this.tech_.trigger({type: 'usage', name: 'vhs-bandwidth-from-local-storage'});
  627. }
  628. if (storedObject && storedObject.throughput) {
  629. this.options_.throughput = storedObject.throughput;
  630. this.tech_.trigger({type: 'usage', name: 'vhs-throughput-from-local-storage'});
  631. }
  632. }
  633. }
  634. // if bandwidth was not set by options or pulled from local storage, start playlist
  635. // selection at a reasonable bandwidth
  636. if (typeof this.options_.bandwidth !== 'number') {
  637. this.options_.bandwidth = Config.INITIAL_BANDWIDTH;
  638. }
  639. // If the bandwidth number is unchanged from the initial setting
  640. // then this takes precedence over the enableLowInitialPlaylist option
  641. this.options_.enableLowInitialPlaylist =
  642. this.options_.enableLowInitialPlaylist &&
  643. this.options_.bandwidth === Config.INITIAL_BANDWIDTH;
  644. // grab options passed to player.src
  645. [
  646. 'withCredentials',
  647. 'useDevicePixelRatio',
  648. 'limitRenditionByPlayerDimensions',
  649. 'bandwidth',
  650. 'customTagParsers',
  651. 'customTagMappers',
  652. 'cacheEncryptionKeys',
  653. 'playlistSelector',
  654. 'initialPlaylistSelector',
  655. 'bufferBasedABR',
  656. 'liveRangeSafeTimeDelta',
  657. 'llhls',
  658. 'useForcedSubtitles',
  659. 'useNetworkInformationApi',
  660. 'useDtsForTimestampOffset',
  661. 'exactManifestTimings',
  662. 'leastPixelDiffSelector'
  663. ].forEach((option) => {
  664. if (typeof this.source_[option] !== 'undefined') {
  665. this.options_[option] = this.source_[option];
  666. }
  667. });
  668. this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions;
  669. this.useDevicePixelRatio = this.options_.useDevicePixelRatio;
  670. }
  671. // alias for public method to set options
  672. setOptions(options = {}) {
  673. this.setOptions_(options);
  674. }
  675. /**
  676. * called when player.src gets called, handle a new source
  677. *
  678. * @param {Object} src the source object to handle
  679. */
  680. src(src, type) {
  681. // do nothing if the src is falsey
  682. if (!src) {
  683. return;
  684. }
  685. this.setOptions_();
  686. // add main playlist controller options
  687. this.options_.src = expandDataUri(this.source_.src);
  688. this.options_.tech = this.tech_;
  689. this.options_.externVhs = Vhs;
  690. this.options_.sourceType = simpleTypeFromSourceType(type);
  691. // Whenever we seek internally, we should update the tech
  692. this.options_.seekTo = (time) => {
  693. this.tech_.setCurrentTime(time);
  694. };
  695. this.playlistController_ = new PlaylistController(this.options_);
  696. const playbackWatcherOptions = merge(
  697. {
  698. liveRangeSafeTimeDelta: SAFE_TIME_DELTA
  699. },
  700. this.options_,
  701. {
  702. seekable: () => this.seekable(),
  703. media: () => this.playlistController_.media(),
  704. playlistController: this.playlistController_
  705. }
  706. );
  707. this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions);
  708. this.playlistController_.on('error', () => {
  709. const player = videojs.players[this.tech_.options_.playerId];
  710. let error = this.playlistController_.error;
  711. if (typeof error === 'object' && !error.code) {
  712. error.code = 3;
  713. } else if (typeof error === 'string') {
  714. error = {message: error, code: 3};
  715. }
  716. player.error(error);
  717. });
  718. const defaultSelector = this.options_.bufferBasedABR ?
  719. Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR;
  720. // `this` in selectPlaylist should be the VhsHandler for backwards
  721. // compatibility with < v2
  722. this.playlistController_.selectPlaylist = this.selectPlaylist ?
  723. this.selectPlaylist.bind(this) :
  724. defaultSelector.bind(this);
  725. this.playlistController_.selectInitialPlaylist =
  726. Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this);
  727. // re-expose some internal objects for backwards compatibility with < v2
  728. this.playlists = this.playlistController_.mainPlaylistLoader_;
  729. this.mediaSource = this.playlistController_.mediaSource;
  730. // Proxy assignment of some properties to the main playlist
  731. // controller. Using a custom property for backwards compatibility
  732. // with < v2
  733. Object.defineProperties(this, {
  734. selectPlaylist: {
  735. get() {
  736. return this.playlistController_.selectPlaylist;
  737. },
  738. set(selectPlaylist) {
  739. this.playlistController_.selectPlaylist = selectPlaylist.bind(this);
  740. }
  741. },
  742. throughput: {
  743. get() {
  744. return this.playlistController_.mainSegmentLoader_.throughput.rate;
  745. },
  746. set(throughput) {
  747. this.playlistController_.mainSegmentLoader_.throughput.rate = throughput;
  748. // By setting `count` to 1 the throughput value becomes the starting value
  749. // for the cumulative average
  750. this.playlistController_.mainSegmentLoader_.throughput.count = 1;
  751. }
  752. },
  753. bandwidth: {
  754. get() {
  755. let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth;
  756. const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
  757. const tenMbpsAsBitsPerSecond = 10e6;
  758. if (this.options_.useNetworkInformationApi && networkInformation) {
  759. // downlink returns Mbps
  760. // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink
  761. const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000;
  762. // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player
  763. // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that
  764. // high quality streams are not filtered out.
  765. if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) {
  766. playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec);
  767. } else {
  768. playerBandwidthEst = networkInfoBandwidthEstBitsPerSec;
  769. }
  770. }
  771. return playerBandwidthEst;
  772. },
  773. set(bandwidth) {
  774. this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth;
  775. // setting the bandwidth manually resets the throughput counter
  776. // `count` is set to zero that current value of `rate` isn't included
  777. // in the cumulative average
  778. this.playlistController_.mainSegmentLoader_.throughput = {
  779. rate: 0,
  780. count: 0
  781. };
  782. }
  783. },
  784. /**
  785. * `systemBandwidth` is a combination of two serial processes bit-rates. The first
  786. * is the network bitrate provided by `bandwidth` and the second is the bitrate of
  787. * the entire process after that - decryption, transmuxing, and appending - provided
  788. * by `throughput`.
  789. *
  790. * Since the two process are serial, the overall system bandwidth is given by:
  791. * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
  792. */
  793. systemBandwidth: {
  794. get() {
  795. const invBandwidth = 1 / (this.bandwidth || 1);
  796. let invThroughput;
  797. if (this.throughput > 0) {
  798. invThroughput = 1 / this.throughput;
  799. } else {
  800. invThroughput = 0;
  801. }
  802. const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
  803. return systemBitrate;
  804. },
  805. set() {
  806. videojs.log.error('The "systemBandwidth" property is read-only');
  807. }
  808. }
  809. });
  810. if (this.options_.bandwidth) {
  811. this.bandwidth = this.options_.bandwidth;
  812. }
  813. if (this.options_.throughput) {
  814. this.throughput = this.options_.throughput;
  815. }
  816. Object.defineProperties(this.stats, {
  817. bandwidth: {
  818. get: () => this.bandwidth || 0,
  819. enumerable: true
  820. },
  821. mediaRequests: {
  822. get: () => this.playlistController_.mediaRequests_() || 0,
  823. enumerable: true
  824. },
  825. mediaRequestsAborted: {
  826. get: () => this.playlistController_.mediaRequestsAborted_() || 0,
  827. enumerable: true
  828. },
  829. mediaRequestsTimedout: {
  830. get: () => this.playlistController_.mediaRequestsTimedout_() || 0,
  831. enumerable: true
  832. },
  833. mediaRequestsErrored: {
  834. get: () => this.playlistController_.mediaRequestsErrored_() || 0,
  835. enumerable: true
  836. },
  837. mediaTransferDuration: {
  838. get: () => this.playlistController_.mediaTransferDuration_() || 0,
  839. enumerable: true
  840. },
  841. mediaBytesTransferred: {
  842. get: () => this.playlistController_.mediaBytesTransferred_() || 0,
  843. enumerable: true
  844. },
  845. mediaSecondsLoaded: {
  846. get: () => this.playlistController_.mediaSecondsLoaded_() || 0,
  847. enumerable: true
  848. },
  849. mediaAppends: {
  850. get: () => this.playlistController_.mediaAppends_() || 0,
  851. enumerable: true
  852. },
  853. mainAppendsToLoadedData: {
  854. get: () => this.playlistController_.mainAppendsToLoadedData_() || 0,
  855. enumerable: true
  856. },
  857. audioAppendsToLoadedData: {
  858. get: () => this.playlistController_.audioAppendsToLoadedData_() || 0,
  859. enumerable: true
  860. },
  861. appendsToLoadedData: {
  862. get: () => this.playlistController_.appendsToLoadedData_() || 0,
  863. enumerable: true
  864. },
  865. timeToLoadedData: {
  866. get: () => this.playlistController_.timeToLoadedData_() || 0,
  867. enumerable: true
  868. },
  869. buffered: {
  870. get: () => timeRangesToArray(this.tech_.buffered()),
  871. enumerable: true
  872. },
  873. currentTime: {
  874. get: () => this.tech_.currentTime(),
  875. enumerable: true
  876. },
  877. currentSource: {
  878. get: () => this.tech_.currentSource_,
  879. enumerable: true
  880. },
  881. currentTech: {
  882. get: () => this.tech_.name_,
  883. enumerable: true
  884. },
  885. duration: {
  886. get: () => this.tech_.duration(),
  887. enumerable: true
  888. },
  889. main: {
  890. get: () => this.playlists.main,
  891. enumerable: true
  892. },
  893. playerDimensions: {
  894. get: () => this.tech_.currentDimensions(),
  895. enumerable: true
  896. },
  897. seekable: {
  898. get: () => timeRangesToArray(this.tech_.seekable()),
  899. enumerable: true
  900. },
  901. timestamp: {
  902. get: () => Date.now(),
  903. enumerable: true
  904. },
  905. videoPlaybackQuality: {
  906. get: () => this.tech_.getVideoPlaybackQuality(),
  907. enumerable: true
  908. }
  909. });
  910. this.tech_.one(
  911. 'canplay',
  912. this.playlistController_.setupFirstPlay.bind(this.playlistController_)
  913. );
  914. this.tech_.on('bandwidthupdate', () => {
  915. if (this.options_.useBandwidthFromLocalStorage) {
  916. updateVhsLocalStorage({
  917. bandwidth: this.bandwidth,
  918. throughput: Math.round(this.throughput)
  919. });
  920. }
  921. });
  922. this.playlistController_.on('selectedinitialmedia', () => {
  923. // Add the manual rendition mix-in to VhsHandler
  924. renditionSelectionMixin(this);
  925. });
  926. this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => {
  927. this.setupEme_();
  928. });
  929. // the bandwidth of the primary segment loader is our best
  930. // estimate of overall bandwidth
  931. this.on(this.playlistController_, 'progress', function() {
  932. this.tech_.trigger('progress');
  933. });
  934. // In the live case, we need to ignore the very first `seeking` event since
  935. // that will be the result of the seek-to-live behavior
  936. this.on(this.playlistController_, 'firstplay', function() {
  937. this.ignoreNextSeekingEvent_ = true;
  938. });
  939. this.setupQualityLevels_();
  940. // do nothing if the tech has been disposed already
  941. // this can occur if someone sets the src in player.ready(), for instance
  942. if (!this.tech_.el()) {
  943. return;
  944. }
  945. this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource);
  946. this.tech_.src(this.mediaSourceUrl_);
  947. }
  948. createKeySessions_() {
  949. const audioPlaylistLoader =
  950. this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
  951. this.logger_('waiting for EME key session creation');
  952. waitForKeySessionCreation({
  953. player: this.player_,
  954. sourceKeySystems: this.source_.keySystems,
  955. audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(),
  956. mainPlaylists: this.playlists.main.playlists
  957. }).then(() => {
  958. this.logger_('created EME key session');
  959. this.playlistController_.sourceUpdater_.initializedEme();
  960. }).catch((err) => {
  961. this.logger_('error while creating EME key session', err);
  962. this.player_.error({
  963. message: 'Failed to initialize media keys for EME',
  964. code: 3
  965. });
  966. });
  967. }
  968. handleWaitingForKey_() {
  969. // If waitingforkey is fired, it's possible that the data that's necessary to retrieve
  970. // the key is in the manifest. While this should've happened on initial source load, it
  971. // may happen again in live streams where the keys change, and the manifest info
  972. // reflects the update.
  973. //
  974. // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's
  975. // already requested keys for, we don't have to worry about this generating extraneous
  976. // requests.
  977. this.logger_('waitingforkey fired, attempting to create any new key sessions');
  978. this.createKeySessions_();
  979. }
  980. /**
  981. * If necessary and EME is available, sets up EME options and waits for key session
  982. * creation.
  983. *
  984. * This function also updates the source updater so taht it can be used, as for some
  985. * browsers, EME must be configured before content is appended (if appending unencrypted
  986. * content before encrypted content).
  987. */
  988. setupEme_() {
  989. const audioPlaylistLoader =
  990. this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
  991. const didSetupEmeOptions = setupEmeOptions({
  992. player: this.player_,
  993. sourceKeySystems: this.source_.keySystems,
  994. media: this.playlists.media(),
  995. audioMedia: audioPlaylistLoader && audioPlaylistLoader.media()
  996. });
  997. this.player_.tech_.on('keystatuschange', (e) => {
  998. this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status);
  999. });
  1000. this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this);
  1001. this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_);
  1002. if (!didSetupEmeOptions) {
  1003. // If EME options were not set up, we've done all we could to initialize EME.
  1004. this.playlistController_.sourceUpdater_.initializedEme();
  1005. return;
  1006. }
  1007. this.createKeySessions_();
  1008. }
  1009. /**
  1010. * Initializes the quality levels and sets listeners to update them.
  1011. *
  1012. * @method setupQualityLevels_
  1013. * @private
  1014. */
  1015. setupQualityLevels_() {
  1016. const player = videojs.players[this.tech_.options_.playerId];
  1017. // if there isn't a player or there isn't a qualityLevels plugin
  1018. // or qualityLevels_ listeners have already been setup, do nothing.
  1019. if (!player || !player.qualityLevels || this.qualityLevels_) {
  1020. return;
  1021. }
  1022. this.qualityLevels_ = player.qualityLevels();
  1023. this.playlistController_.on('selectedinitialmedia', () => {
  1024. handleVhsLoadedMetadata(this.qualityLevels_, this);
  1025. });
  1026. this.playlists.on('mediachange', () => {
  1027. handleVhsMediaChange(this.qualityLevels_, this.playlists);
  1028. });
  1029. }
  1030. /**
  1031. * return the version
  1032. */
  1033. static version() {
  1034. return {
  1035. '@videojs/http-streaming': vhsVersion,
  1036. 'mux.js': muxVersion,
  1037. 'mpd-parser': mpdVersion,
  1038. 'm3u8-parser': m3u8Version,
  1039. 'aes-decrypter': aesVersion
  1040. };
  1041. }
  1042. /**
  1043. * return the version
  1044. */
  1045. version() {
  1046. return this.constructor.version();
  1047. }
  1048. canChangeType() {
  1049. return SourceUpdater.canChangeType();
  1050. }
  1051. /**
  1052. * Begin playing the video.
  1053. */
  1054. play() {
  1055. this.playlistController_.play();
  1056. }
  1057. /**
  1058. * a wrapper around the function in PlaylistController
  1059. */
  1060. setCurrentTime(currentTime) {
  1061. this.playlistController_.setCurrentTime(currentTime);
  1062. }
  1063. /**
  1064. * a wrapper around the function in PlaylistController
  1065. */
  1066. duration() {
  1067. return this.playlistController_.duration();
  1068. }
  1069. /**
  1070. * a wrapper around the function in PlaylistController
  1071. */
  1072. seekable() {
  1073. return this.playlistController_.seekable();
  1074. }
  1075. /**
  1076. * Abort all outstanding work and cleanup.
  1077. */
  1078. dispose() {
  1079. if (this.playbackWatcher_) {
  1080. this.playbackWatcher_.dispose();
  1081. }
  1082. if (this.playlistController_) {
  1083. this.playlistController_.dispose();
  1084. }
  1085. if (this.qualityLevels_) {
  1086. this.qualityLevels_.dispose();
  1087. }
  1088. if (this.tech_ && this.tech_.vhs) {
  1089. delete this.tech_.vhs;
  1090. }
  1091. if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) {
  1092. window.URL.revokeObjectURL(this.mediaSourceUrl_);
  1093. this.mediaSourceUrl_ = null;
  1094. }
  1095. if (this.tech_) {
  1096. this.tech_.off('waitingforkey', this.handleWaitingForKey_);
  1097. }
  1098. super.dispose();
  1099. }
  1100. convertToProgramTime(time, callback) {
  1101. return getProgramTime({
  1102. playlist: this.playlistController_.media(),
  1103. time,
  1104. callback
  1105. });
  1106. }
  1107. // the player must be playing before calling this
  1108. seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) {
  1109. return seekToProgramTime({
  1110. programTime,
  1111. playlist: this.playlistController_.media(),
  1112. retryCount,
  1113. pauseAfterSeek,
  1114. seekTo: this.options_.seekTo,
  1115. tech: this.options_.tech,
  1116. callback
  1117. });
  1118. }
  1119. /**
  1120. * Adds the onRequest, onResponse, offRequest and offResponse functions
  1121. * to the VhsHandler xhr Object.
  1122. */
  1123. setupXhrHooks_() {
  1124. /**
  1125. * A player function for setting an onRequest hook
  1126. *
  1127. * @param {function} callback for request modifiction
  1128. */
  1129. this.xhr.onRequest = (callback) => {
  1130. addOnRequestHook(this.xhr, callback);
  1131. };
  1132. /**
  1133. * A player function for setting an onResponse hook
  1134. *
  1135. * @param {callback} callback for response data retrieval
  1136. */
  1137. this.xhr.onResponse = (callback) => {
  1138. addOnResponseHook(this.xhr, callback);
  1139. };
  1140. /**
  1141. * Deletes a player onRequest callback if it exists
  1142. *
  1143. * @param {function} callback to delete from the player set
  1144. */
  1145. this.xhr.offRequest = (callback) => {
  1146. removeOnRequestHook(this.xhr, callback);
  1147. };
  1148. /**
  1149. * Deletes a player onResponse callback if it exists
  1150. *
  1151. * @param {function} callback to delete from the player set
  1152. */
  1153. this.xhr.offResponse = (callback) => {
  1154. removeOnResponseHook(this.xhr, callback);
  1155. };
  1156. // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks.
  1157. // This allows hooks to be set before the source is set to vhs when handleSource is called.
  1158. this.player_.trigger('xhr-hooks-ready');
  1159. }
  1160. }
  1161. /**
  1162. * The Source Handler object, which informs video.js what additional
  1163. * MIME types are supported and sets up playback. It is registered
  1164. * automatically to the appropriate tech based on the capabilities of
  1165. * the browser it is running in. It is not necessary to use or modify
  1166. * this object in normal usage.
  1167. */
  1168. const VhsSourceHandler = {
  1169. name: 'videojs-http-streaming',
  1170. VERSION: vhsVersion,
  1171. canHandleSource(srcObj, options = {}) {
  1172. const localOptions = merge(videojs.options, options);
  1173. return VhsSourceHandler.canPlayType(srcObj.type, localOptions);
  1174. },
  1175. handleSource(source, tech, options = {}) {
  1176. const localOptions = merge(videojs.options, options);
  1177. tech.vhs = new VhsHandler(source, tech, localOptions);
  1178. tech.vhs.xhr = xhrFactory();
  1179. tech.vhs.setupXhrHooks_();
  1180. tech.vhs.src(source.src, source.type);
  1181. return tech.vhs;
  1182. },
  1183. canPlayType(type, options) {
  1184. const simpleType = simpleTypeFromSourceType(type);
  1185. if (!simpleType) {
  1186. return '';
  1187. }
  1188. const overrideNative = VhsSourceHandler.getOverrideNative(options);
  1189. const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType);
  1190. const canUseMsePlayback = !supportsTypeNatively || overrideNative;
  1191. return canUseMsePlayback ? 'maybe' : '';
  1192. },
  1193. getOverrideNative(options = {}) {
  1194. const { vhs = {} } = options;
  1195. const defaultOverrideNative = !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS);
  1196. const { overrideNative = defaultOverrideNative } = vhs;
  1197. return overrideNative;
  1198. }
  1199. };
  1200. /**
  1201. * Check to see if the native MediaSource object exists and supports
  1202. * an MP4 container with both H.264 video and AAC-LC audio.
  1203. *
  1204. * @return {boolean} if native media sources are supported
  1205. */
  1206. const supportsNativeMediaSources = () => {
  1207. return browserSupportsCodec('avc1.4d400d,mp4a.40.2');
  1208. };
  1209. // register source handlers with the appropriate techs
  1210. if (supportsNativeMediaSources()) {
  1211. videojs.getTech('Html5').registerSourceHandler(VhsSourceHandler, 0);
  1212. }
  1213. videojs.VhsHandler = VhsHandler;
  1214. videojs.VhsSourceHandler = VhsSourceHandler;
  1215. videojs.Vhs = Vhs;
  1216. if (!videojs.use) {
  1217. videojs.registerComponent('Vhs', Vhs);
  1218. }
  1219. videojs.options.vhs = videojs.options.vhs || {};
  1220. if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) {
  1221. videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError);
  1222. }
  1223. export {
  1224. Vhs,
  1225. VhsHandler,
  1226. VhsSourceHandler,
  1227. emeKeySystems,
  1228. simpleTypeFromSourceType,
  1229. expandDataUri,
  1230. setupEmeOptions,
  1231. getAllPsshKeySystemsOptions
  1232. };