Source: master-playlist-controller.js

  1. /**
  2. * @file master-playlist-controller.js
  3. */
  4. import PlaylistLoader from './playlist-loader';
  5. import SegmentLoader from './segment-loader';
  6. import Ranges from './ranges';
  7. import videojs from 'video.js';
  8. import AdCueTags from './ad-cue-tags';
  9. // 5 minute blacklist
  10. const BLACKLIST_DURATION = 5 * 60 * 1000;
  11. let Hls;
  12. /**
  13. * determine if an object a is differnt from
  14. * and object b. both only having one dimensional
  15. * properties
  16. *
  17. * @param {Object} a object one
  18. * @param {Object} b object two
  19. * @return {Boolean} if the object has changed or not
  20. */
  21. const objectChanged = function(a, b) {
  22. if (typeof a !== typeof b) {
  23. return true;
  24. }
  25. // if we have a different number of elements
  26. // something has changed
  27. if (Object.keys(a).length !== Object.keys(b).length) {
  28. return true;
  29. }
  30. for (let prop in a) {
  31. if (a[prop] !== b[prop]) {
  32. return true;
  33. }
  34. }
  35. return false;
  36. };
  37. /**
  38. * Parses a codec string to retrieve the number of codecs specified,
  39. * the video codec and object type indicator, and the audio profile.
  40. *
  41. * @private
  42. */
  43. const parseCodecs = function(codecs) {
  44. let result = {
  45. codecCount: 0,
  46. videoCodec: null,
  47. videoObjectTypeIndicator: null,
  48. audioProfile: null
  49. };
  50. let parsed;
  51. result.codecCount = codecs.split(',').length;
  52. result.codecCount = result.codecCount || 2;
  53. // parse the video codec
  54. parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs);
  55. if (parsed) {
  56. result.videoCodec = parsed[2];
  57. result.videoObjectTypeIndicator = parsed[3];
  58. }
  59. // parse the last field of the audio codec
  60. result.audioProfile =
  61. (/(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i).exec(codecs);
  62. result.audioProfile = result.audioProfile && result.audioProfile[2];
  63. return result;
  64. };
  65. /**
  66. * Calculates the MIME type strings for a working configuration of
  67. * SourceBuffers to play variant streams in a master playlist. If
  68. * there is no possible working configuration, an empty array will be
  69. * returned.
  70. *
  71. * @param master {Object} the m3u8 object for the master playlist
  72. * @param media {Object} the m3u8 object for the variant playlist
  73. * @return {Array} the MIME type strings. If the array has more than
  74. * one entry, the first element should be applied to the video
  75. * SourceBuffer and the second to the audio SourceBuffer.
  76. *
  77. * @private
  78. */
  79. export const mimeTypesForPlaylist_ = function(master, media) {
  80. let container = 'mp2t';
  81. let codecs = {
  82. videoCodec: 'avc1',
  83. videoObjectTypeIndicator: '.4d400d',
  84. audioProfile: '2'
  85. };
  86. let audioGroup = [];
  87. let mediaAttributes;
  88. let previousGroup = null;
  89. if (!media) {
  90. // not enough information, return an error
  91. return [];
  92. }
  93. // An initialization segment means the media playlists is an iframe
  94. // playlist or is using the mp4 container. We don't currently
  95. // support iframe playlists, so assume this is signalling mp4
  96. // fragments.
  97. // the existence check for segments can be removed once
  98. // https://github.com/videojs/m3u8-parser/issues/8 is closed
  99. if (media.segments && media.segments.length && media.segments[0].map) {
  100. container = 'mp4';
  101. }
  102. // if the codecs were explicitly specified, use them instead of the
  103. // defaults
  104. mediaAttributes = media.attributes || {};
  105. if (mediaAttributes.CODECS) {
  106. let parsedCodecs = parseCodecs(mediaAttributes.CODECS);
  107. Object.keys(parsedCodecs).forEach((key) => {
  108. codecs[key] = parsedCodecs[key] || codecs[key];
  109. });
  110. }
  111. if (master.mediaGroups.AUDIO) {
  112. audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
  113. }
  114. // if audio could be muxed or unmuxed, use mime types appropriate
  115. // for both scenarios
  116. for (let groupId in audioGroup) {
  117. if (previousGroup && (!!audioGroup[groupId].uri !== !!previousGroup.uri)) {
  118. // one source buffer with muxed video and audio and another for
  119. // the alternate audio
  120. return [
  121. 'video/' + container + '; codecs="' +
  122. codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"',
  123. 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"'
  124. ];
  125. }
  126. previousGroup = audioGroup[groupId];
  127. }
  128. // if all video and audio is unmuxed, use two single-codec mime
  129. // types
  130. if (previousGroup && previousGroup.uri) {
  131. return [
  132. 'video/' + container + '; codecs="' +
  133. codecs.videoCodec + codecs.videoObjectTypeIndicator + '"',
  134. 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"'
  135. ];
  136. }
  137. // all video and audio are muxed, use a dual-codec mime type
  138. return [
  139. 'video/' + container + '; codecs="' +
  140. codecs.videoCodec + codecs.videoObjectTypeIndicator +
  141. ', mp4a.40.' + codecs.audioProfile + '"'
  142. ];
  143. };
  144. /**
  145. * the master playlist controller controller all interactons
  146. * between playlists and segmentloaders. At this time this mainly
  147. * involves a master playlist and a series of audio playlists
  148. * if they are available
  149. *
  150. * @class MasterPlaylistController
  151. * @extends videojs.EventTarget
  152. */
  153. export class MasterPlaylistController extends videojs.EventTarget {
  154. constructor(options) {
  155. super();
  156. let {
  157. url,
  158. withCredentials,
  159. mode,
  160. tech,
  161. bandwidth,
  162. externHls,
  163. useCueTags
  164. } = options;
  165. if (!url) {
  166. throw new Error('A non-empty playlist URL is required');
  167. }
  168. Hls = externHls;
  169. this.withCredentials = withCredentials;
  170. this.tech_ = tech;
  171. this.hls_ = tech.hls;
  172. this.mode_ = mode;
  173. this.useCueTags_ = useCueTags;
  174. if (this.useCueTags_) {
  175. this.cueTagsTrack_ = this.tech_.addTextTrack('metadata',
  176. 'ad-cues');
  177. this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
  178. this.tech_.textTracks().addTrack_(this.cueTagsTrack_);
  179. }
  180. this.audioTracks_ = [];
  181. this.requestOptions_ = {
  182. withCredentials: this.withCredentials,
  183. timeout: null
  184. };
  185. this.audioGroups_ = {};
  186. this.mediaSource = new videojs.MediaSource({ mode });
  187. this.audioinfo_ = null;
  188. this.mediaSource.on('audioinfo', this.handleAudioinfoUpdate_.bind(this));
  189. // load the media source into the player
  190. this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
  191. this.hasPlayed_ = () => false;
  192. let segmentLoaderOptions = {
  193. hls: this.hls_,
  194. mediaSource: this.mediaSource,
  195. currentTime: this.tech_.currentTime.bind(this.tech_),
  196. seekable: () => this.seekable(),
  197. seeking: () => this.tech_.seeking(),
  198. setCurrentTime: (a) => this.tech_.setCurrentTime(a),
  199. hasPlayed: () => this.hasPlayed_(),
  200. bandwidth
  201. };
  202. // setup playlist loaders
  203. this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials);
  204. this.setupMasterPlaylistLoaderListeners_();
  205. this.audioPlaylistLoader_ = null;
  206. // setup segment loaders
  207. // combined audio/video or just video when alternate audio track is selected
  208. this.mainSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
  209. // alternate audio track
  210. this.audioSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
  211. this.setupSegmentLoaderListeners_();
  212. this.masterPlaylistLoader_.start();
  213. }
  214. /**
  215. * Register event handlers on the master playlist loader. A helper
  216. * function for construction time.
  217. *
  218. * @private
  219. */
  220. setupMasterPlaylistLoaderListeners_() {
  221. this.masterPlaylistLoader_.on('loadedmetadata', () => {
  222. let media = this.masterPlaylistLoader_.media();
  223. let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
  224. this.requestOptions_.timeout = requestTimeout;
  225. // if this isn't a live video and preload permits, start
  226. // downloading segments
  227. if (media.endList && this.tech_.preload() !== 'none') {
  228. this.mainSegmentLoader_.playlist(media, this.requestOptions_);
  229. this.mainSegmentLoader_.load();
  230. }
  231. this.fillAudioTracks_();
  232. this.setupAudio();
  233. try {
  234. this.setupSourceBuffers_();
  235. } catch (e) {
  236. videojs.log.warn('Failed to create SourceBuffers', e);
  237. return this.mediaSource.endOfStream('decode');
  238. }
  239. this.setupFirstPlay();
  240. this.trigger('audioupdate');
  241. this.trigger('selectedinitialmedia');
  242. });
  243. this.masterPlaylistLoader_.on('loadedplaylist', () => {
  244. let updatedPlaylist = this.masterPlaylistLoader_.media();
  245. let seekable;
  246. if (!updatedPlaylist) {
  247. // select the initial variant
  248. this.initialMedia_ = this.selectPlaylist();
  249. this.masterPlaylistLoader_.media(this.initialMedia_);
  250. return;
  251. }
  252. if (this.useCueTags_) {
  253. this.updateAdCues_(updatedPlaylist,
  254. this.masterPlaylistLoader_.expired_);
  255. }
  256. // TODO: Create a new event on the PlaylistLoader that signals
  257. // that the segments have changed in some way and use that to
  258. // update the SegmentLoader instead of doing it twice here and
  259. // on `mediachange`
  260. this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
  261. this.updateDuration();
  262. // update seekable
  263. seekable = this.seekable();
  264. if (!updatedPlaylist.endList && seekable.length !== 0) {
  265. this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
  266. }
  267. });
  268. this.masterPlaylistLoader_.on('error', () => {
  269. this.blacklistCurrentPlaylist(this.masterPlaylistLoader_.error);
  270. });
  271. this.masterPlaylistLoader_.on('mediachanging', () => {
  272. this.mainSegmentLoader_.abort();
  273. this.mainSegmentLoader_.pause();
  274. });
  275. this.masterPlaylistLoader_.on('mediachange', () => {
  276. let media = this.masterPlaylistLoader_.media();
  277. let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000;
  278. let activeAudioGroup;
  279. let activeTrack;
  280. // If we don't have any more available playlists, we don't want to
  281. // timeout the request.
  282. if (this.masterPlaylistLoader_.isLowestEnabledRendition_()) {
  283. this.requestOptions_.timeout = 0;
  284. } else {
  285. this.requestOptions_.timeout = requestTimeout;
  286. }
  287. // TODO: Create a new event on the PlaylistLoader that signals
  288. // that the segments have changed in some way and use that to
  289. // update the SegmentLoader instead of doing it twice here and
  290. // on `loadedplaylist`
  291. this.mainSegmentLoader_.playlist(media, this.requestOptions_);
  292. this.mainSegmentLoader_.load();
  293. // if the audio group has changed, a new audio track has to be
  294. // enabled
  295. activeAudioGroup = this.activeAudioGroup();
  296. activeTrack = activeAudioGroup.filter((track) => track.enabled)[0];
  297. if (!activeTrack) {
  298. this.setupAudio();
  299. this.trigger('audioupdate');
  300. }
  301. this.tech_.trigger({
  302. type: 'mediachange',
  303. bubbles: true
  304. });
  305. });
  306. }
  307. /**
  308. * Register event handlers on the segment loaders. A helper function
  309. * for construction time.
  310. *
  311. * @private
  312. */
  313. setupSegmentLoaderListeners_() {
  314. this.mainSegmentLoader_.on('progress', () => {
  315. // figure out what stream the next segment should be downloaded from
  316. // with the updated bandwidth information
  317. this.masterPlaylistLoader_.media(this.selectPlaylist());
  318. this.trigger('progress');
  319. });
  320. this.mainSegmentLoader_.on('error', () => {
  321. this.blacklistCurrentPlaylist(this.mainSegmentLoader_.error());
  322. });
  323. this.audioSegmentLoader_.on('error', () => {
  324. videojs.log.warn('Problem encountered with the current alternate audio track' +
  325. '. Switching back to default.');
  326. this.audioSegmentLoader_.abort();
  327. this.audioPlaylistLoader_ = null;
  328. this.setupAudio();
  329. });
  330. }
  331. handleAudioinfoUpdate_(event) {
  332. if (Hls.supportsAudioInfoChange_() ||
  333. !this.audioInfo_ ||
  334. !objectChanged(this.audioInfo_, event.info)) {
  335. this.audioInfo_ = event.info;
  336. return;
  337. }
  338. let error = 'had different audio properties (channels, sample rate, etc.) ' +
  339. 'or changed in some other way. This behavior is currently ' +
  340. 'unsupported in Firefox 48 and below due to an issue: \n\n' +
  341. 'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n';
  342. let enabledIndex =
  343. this.activeAudioGroup()
  344. .map((track) => track.enabled)
  345. .indexOf(true);
  346. let enabledTrack = this.activeAudioGroup()[enabledIndex];
  347. let defaultTrack = this.activeAudioGroup().filter((track) => {
  348. return track.properties_ && track.properties_.default;
  349. })[0];
  350. // they did not switch audiotracks
  351. // blacklist the current playlist
  352. if (!this.audioPlaylistLoader_) {
  353. error = `The rendition that we tried to switch to ${error}` +
  354. 'Unfortunately that means we will have to blacklist ' +
  355. 'the current playlist and switch to another. Sorry!';
  356. this.blacklistCurrentPlaylist();
  357. } else {
  358. error = `The audio track '${enabledTrack.label}' that we tried to ` +
  359. `switch to ${error} Unfortunately this means we will have to ` +
  360. `return you to the main track '${defaultTrack.label}'. Sorry!`;
  361. defaultTrack.enabled = true;
  362. this.activeAudioGroup().splice(enabledIndex, 1);
  363. this.trigger('audioupdate');
  364. }
  365. videojs.log.warn(error);
  366. this.setupAudio();
  367. }
  368. /**
  369. * get the total number of media requests from the `audiosegmentloader_`
  370. * and the `mainSegmentLoader_`
  371. *
  372. * @private
  373. */
  374. mediaRequests_() {
  375. return this.audioSegmentLoader_.mediaRequests +
  376. this.mainSegmentLoader_.mediaRequests;
  377. }
  378. /**
  379. * get the total time that media requests have spent trnasfering
  380. * from the `audiosegmentloader_` and the `mainSegmentLoader_`
  381. *
  382. * @private
  383. */
  384. mediaTransferDuration_() {
  385. return this.audioSegmentLoader_.mediaTransferDuration +
  386. this.mainSegmentLoader_.mediaTransferDuration;
  387. }
  388. /**
  389. * get the total number of bytes transfered during media requests
  390. * from the `audiosegmentloader_` and the `mainSegmentLoader_`
  391. *
  392. * @private
  393. */
  394. mediaBytesTransferred_() {
  395. return this.audioSegmentLoader_.mediaBytesTransferred +
  396. this.mainSegmentLoader_.mediaBytesTransferred;
  397. }
  398. mediaSecondsLoaded_() {
  399. return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded +
  400. this.mainSegmentLoader_.mediaSecondsLoaded);
  401. }
  402. /**
  403. * fill our internal list of HlsAudioTracks with data from
  404. * the master playlist or use a default
  405. *
  406. * @private
  407. */
  408. fillAudioTracks_() {
  409. let master = this.master();
  410. let mediaGroups = master.mediaGroups || {};
  411. // force a default if we have none or we are not
  412. // in html5 mode (the only mode to support more than one
  413. // audio track)
  414. if (!mediaGroups ||
  415. !mediaGroups.AUDIO ||
  416. Object.keys(mediaGroups.AUDIO).length === 0 ||
  417. this.mode_ !== 'html5') {
  418. // "main" audio group, track name "default"
  419. mediaGroups.AUDIO = { main: { default: { default: true }}};
  420. }
  421. for (let mediaGroup in mediaGroups.AUDIO) {
  422. if (!this.audioGroups_[mediaGroup]) {
  423. this.audioGroups_[mediaGroup] = [];
  424. }
  425. for (let label in mediaGroups.AUDIO[mediaGroup]) {
  426. let properties = mediaGroups.AUDIO[mediaGroup][label];
  427. let track = new videojs.AudioTrack({
  428. id: label,
  429. kind: properties.default ? 'main' : 'alternative',
  430. enabled: false,
  431. language: properties.language,
  432. label
  433. });
  434. track.properties_ = properties;
  435. this.audioGroups_[mediaGroup].push(track);
  436. }
  437. }
  438. // enable the default active track
  439. (this.activeAudioGroup().filter((audioTrack) => {
  440. return audioTrack.properties_.default;
  441. })[0] || this.activeAudioGroup()[0]).enabled = true;
  442. }
  443. /**
  444. * Call load on our SegmentLoaders
  445. */
  446. load() {
  447. this.mainSegmentLoader_.load();
  448. if (this.audioPlaylistLoader_) {
  449. this.audioSegmentLoader_.load();
  450. }
  451. }
  452. /**
  453. * Returns the audio group for the currently active primary
  454. * media playlist.
  455. */
  456. activeAudioGroup() {
  457. let videoPlaylist = this.masterPlaylistLoader_.media();
  458. let result;
  459. if (videoPlaylist.attributes && videoPlaylist.attributes.AUDIO) {
  460. result = this.audioGroups_[videoPlaylist.attributes.AUDIO];
  461. }
  462. return result || this.audioGroups_.main;
  463. }
  464. /**
  465. * Determine the correct audio rendition based on the active
  466. * AudioTrack and initialize a PlaylistLoader and SegmentLoader if
  467. * necessary. This method is called once automatically before
  468. * playback begins to enable the default audio track and should be
  469. * invoked again if the track is changed.
  470. */
  471. setupAudio() {
  472. // determine whether seperate loaders are required for the audio
  473. // rendition
  474. let audioGroup = this.activeAudioGroup();
  475. let track = audioGroup.filter((audioTrack) => {
  476. return audioTrack.enabled;
  477. })[0];
  478. if (!track) {
  479. track = audioGroup.filter((audioTrack) => {
  480. return audioTrack.properties_.default;
  481. })[0] || audioGroup[0];
  482. track.enabled = true;
  483. }
  484. // stop playlist and segment loading for audio
  485. if (this.audioPlaylistLoader_) {
  486. this.audioPlaylistLoader_.dispose();
  487. this.audioPlaylistLoader_ = null;
  488. }
  489. this.audioSegmentLoader_.pause();
  490. if (!track.properties_.resolvedUri) {
  491. this.mainSegmentLoader_.resetEverything();
  492. return;
  493. }
  494. this.audioSegmentLoader_.resetEverything();
  495. // startup playlist and segment loaders for the enabled audio
  496. // track
  497. this.audioPlaylistLoader_ = new PlaylistLoader(track.properties_.resolvedUri,
  498. this.hls_,
  499. this.withCredentials);
  500. this.audioPlaylistLoader_.start();
  501. this.audioPlaylistLoader_.on('loadedmetadata', () => {
  502. let audioPlaylist = this.audioPlaylistLoader_.media();
  503. this.audioSegmentLoader_.playlist(audioPlaylist, this.requestOptions_);
  504. // if the video is already playing, or if this isn't a live video and preload
  505. // permits, start downloading segments
  506. if (!this.tech_.paused() ||
  507. (audioPlaylist.endList && this.tech_.preload() !== 'none')) {
  508. this.audioSegmentLoader_.load();
  509. }
  510. if (!audioPlaylist.endList) {
  511. // trigger the playlist loader to start "expired time"-tracking
  512. this.audioPlaylistLoader_.trigger('firstplay');
  513. }
  514. });
  515. this.audioPlaylistLoader_.on('loadedplaylist', () => {
  516. let updatedPlaylist;
  517. if (this.audioPlaylistLoader_) {
  518. updatedPlaylist = this.audioPlaylistLoader_.media();
  519. }
  520. if (!updatedPlaylist) {
  521. // only one playlist to select
  522. this.audioPlaylistLoader_.media(
  523. this.audioPlaylistLoader_.playlists.master.playlists[0]);
  524. return;
  525. }
  526. this.audioSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
  527. });
  528. this.audioPlaylistLoader_.on('error', () => {
  529. videojs.log.warn('Problem encountered loading the alternate audio track' +
  530. '. Switching back to default.');
  531. this.audioSegmentLoader_.abort();
  532. this.setupAudio();
  533. });
  534. }
  535. /**
  536. * Re-tune playback quality level for the current player
  537. * conditions. This method may perform destructive actions, like
  538. * removing already buffered content, to readjust the currently
  539. * active playlist quickly.
  540. *
  541. * @private
  542. */
  543. fastQualityChange_() {
  544. let media = this.selectPlaylist();
  545. if (media !== this.masterPlaylistLoader_.media()) {
  546. this.masterPlaylistLoader_.media(media);
  547. this.mainSegmentLoader_.resetLoader();
  548. if (this.audiosegmentloader_) {
  549. this.audioSegmentLoader_.resetLoader();
  550. }
  551. }
  552. }
  553. /**
  554. * Begin playback.
  555. */
  556. play() {
  557. if (this.setupFirstPlay()) {
  558. return;
  559. }
  560. if (this.tech_.ended()) {
  561. this.tech_.setCurrentTime(0);
  562. }
  563. if (this.hasPlayed_()) {
  564. this.load();
  565. }
  566. // if the viewer has paused and we fell out of the live window,
  567. // seek forward to the earliest available position
  568. if (this.tech_.duration() === Infinity) {
  569. if (this.tech_.currentTime() < this.tech_.seekable().start(0)) {
  570. return this.tech_.setCurrentTime(this.tech_.seekable().start(0));
  571. }
  572. }
  573. }
  574. /**
  575. * Seek to the latest media position if this is a live video and the
  576. * player and video are loaded and initialized.
  577. */
  578. setupFirstPlay() {
  579. let seekable;
  580. let media = this.masterPlaylistLoader_.media();
  581. // check that everything is ready to begin buffering in the live
  582. // scenario
  583. // 1) the active media playlist is available
  584. if (media &&
  585. // 2) the player is not paused
  586. !this.tech_.paused() &&
  587. // 3) the player has not started playing
  588. !this.hasPlayed_()) {
  589. // when the video is a live stream
  590. if (!media.endList) {
  591. // trigger the playlist loader to start "expired time"-tracking
  592. this.masterPlaylistLoader_.trigger('firstplay');
  593. this.trigger('firstplay');
  594. // seek to the latest media position for live videos
  595. seekable = this.seekable();
  596. if (seekable.length) {
  597. this.tech_.setCurrentTime(seekable.end(0));
  598. }
  599. }
  600. this.hasPlayed_ = () => true;
  601. // now that we are ready, load the segment
  602. this.load();
  603. return true;
  604. }
  605. return false;
  606. }
  607. /**
  608. * handle the sourceopen event on the MediaSource
  609. *
  610. * @private
  611. */
  612. handleSourceOpen_() {
  613. // Only attempt to create the source buffer if none already exist.
  614. // handleSourceOpen is also called when we are "re-opening" a source buffer
  615. // after `endOfStream` has been called (in response to a seek for instance)
  616. try {
  617. this.setupSourceBuffers_();
  618. } catch (e) {
  619. videojs.log.warn('Failed to create Source Buffers', e);
  620. return this.mediaSource.endOfStream('decode');
  621. }
  622. // if autoplay is enabled, begin playback. This is duplicative of
  623. // code in video.js but is required because play() must be invoked
  624. // *after* the media source has opened.
  625. if (this.tech_.autoplay()) {
  626. this.tech_.play();
  627. }
  628. this.trigger('sourceopen');
  629. }
  630. /**
  631. * Blacklists a playlist when an error occurs for a set amount of time
  632. * making it unavailable for selection by the rendition selection algorithm
  633. * and then forces a new playlist (rendition) selection.
  634. *
  635. * @param {Object=} error an optional error that may include the playlist
  636. * to blacklist
  637. */
  638. blacklistCurrentPlaylist(error = {}) {
  639. let currentPlaylist;
  640. let nextPlaylist;
  641. // If the `error` was generated by the playlist loader, it will contain
  642. // the playlist we were trying to load (but failed) and that should be
  643. // blacklisted instead of the currently selected playlist which is likely
  644. // out-of-date in this scenario
  645. currentPlaylist = error.playlist || this.masterPlaylistLoader_.media();
  646. // If there is no current playlist, then an error occurred while we were
  647. // trying to load the master OR while we were disposing of the tech
  648. if (!currentPlaylist) {
  649. this.error = error;
  650. return this.mediaSource.endOfStream('network');
  651. }
  652. // Blacklist this playlist
  653. currentPlaylist.excludeUntil = Date.now() + BLACKLIST_DURATION;
  654. // Select a new playlist
  655. nextPlaylist = this.selectPlaylist();
  656. if (nextPlaylist) {
  657. videojs.log.warn('Problem encountered with the current ' +
  658. 'HLS playlist. Switching to another playlist.');
  659. return this.masterPlaylistLoader_.media(nextPlaylist);
  660. }
  661. videojs.log.warn('Problem encountered with the current ' +
  662. 'HLS playlist. No suitable alternatives found.');
  663. // We have no more playlists we can select so we must fail
  664. this.error = error;
  665. return this.mediaSource.endOfStream('network');
  666. }
  667. /**
  668. * Pause all segment loaders
  669. */
  670. pauseLoading() {
  671. this.mainSegmentLoader_.pause();
  672. if (this.audioPlaylistLoader_) {
  673. this.audioSegmentLoader_.pause();
  674. }
  675. }
  676. /**
  677. * set the current time on all segment loaders
  678. *
  679. * @param {TimeRange} currentTime the current time to set
  680. * @return {TimeRange} the current time
  681. */
  682. setCurrentTime(currentTime) {
  683. let buffered = Ranges.findRange(this.tech_.buffered(), currentTime);
  684. if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) {
  685. // return immediately if the metadata is not ready yet
  686. return 0;
  687. }
  688. // it's clearly an edge-case but don't thrown an error if asked to
  689. // seek within an empty playlist
  690. if (!this.masterPlaylistLoader_.media().segments) {
  691. return 0;
  692. }
  693. // if the seek location is already buffered, continue buffering as
  694. // usual
  695. if (buffered && buffered.length) {
  696. return currentTime;
  697. }
  698. // cancel outstanding requests so we begin buffering at the new
  699. // location
  700. this.mainSegmentLoader_.abort();
  701. this.mainSegmentLoader_.resetEverything();
  702. if (this.audioPlaylistLoader_) {
  703. this.audioSegmentLoader_.abort();
  704. this.audioSegmentLoader_.resetEverything();
  705. }
  706. if (!this.tech_.paused()) {
  707. this.mainSegmentLoader_.load();
  708. if (this.audioPlaylistLoader_) {
  709. this.audioSegmentLoader_.load();
  710. }
  711. }
  712. }
  713. /**
  714. * get the current duration
  715. *
  716. * @return {TimeRange} the duration
  717. */
  718. duration() {
  719. if (!this.masterPlaylistLoader_) {
  720. return 0;
  721. }
  722. if (this.mediaSource) {
  723. return this.mediaSource.duration;
  724. }
  725. return Hls.Playlist.duration(this.masterPlaylistLoader_.media());
  726. }
  727. /**
  728. * check the seekable range
  729. *
  730. * @return {TimeRange} the seekable range
  731. */
  732. seekable() {
  733. let media;
  734. let mainSeekable;
  735. let audioSeekable;
  736. if (!this.masterPlaylistLoader_) {
  737. return videojs.createTimeRanges();
  738. }
  739. media = this.masterPlaylistLoader_.media();
  740. if (!media) {
  741. return videojs.createTimeRanges();
  742. }
  743. mainSeekable = Hls.Playlist.seekable(media,
  744. this.masterPlaylistLoader_.expired_);
  745. if (mainSeekable.length === 0) {
  746. return mainSeekable;
  747. }
  748. if (this.audioPlaylistLoader_) {
  749. audioSeekable = Hls.Playlist.seekable(this.audioPlaylistLoader_.media(),
  750. this.audioPlaylistLoader_.expired_);
  751. if (audioSeekable.length === 0) {
  752. return audioSeekable;
  753. }
  754. }
  755. if (!audioSeekable) {
  756. // seekable has been calculated based on buffering video data so it
  757. // can be returned directly
  758. return mainSeekable;
  759. }
  760. return videojs.createTimeRanges([[
  761. (audioSeekable.start(0) > mainSeekable.start(0)) ? audioSeekable.start(0) :
  762. mainSeekable.start(0),
  763. (audioSeekable.end(0) < mainSeekable.end(0)) ? audioSeekable.end(0) :
  764. mainSeekable.end(0)
  765. ]]);
  766. }
  767. /**
  768. * Update the player duration
  769. */
  770. updateDuration() {
  771. let oldDuration = this.mediaSource.duration;
  772. let newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media());
  773. let buffered = this.tech_.buffered();
  774. let setDuration = () => {
  775. this.mediaSource.duration = newDuration;
  776. this.tech_.trigger('durationchange');
  777. this.mediaSource.removeEventListener('sourceopen', setDuration);
  778. };
  779. if (buffered.length > 0) {
  780. newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
  781. }
  782. // if the duration has changed, invalidate the cached value
  783. if (oldDuration !== newDuration) {
  784. // update the duration
  785. if (this.mediaSource.readyState !== 'open') {
  786. this.mediaSource.addEventListener('sourceopen', setDuration);
  787. } else {
  788. setDuration();
  789. }
  790. }
  791. }
  792. /**
  793. * dispose of the MasterPlaylistController and everything
  794. * that it controls
  795. */
  796. dispose() {
  797. this.masterPlaylistLoader_.dispose();
  798. this.mainSegmentLoader_.dispose();
  799. this.audioSegmentLoader_.dispose();
  800. }
  801. /**
  802. * return the master playlist object if we have one
  803. *
  804. * @return {Object} the master playlist object that we parsed
  805. */
  806. master() {
  807. return this.masterPlaylistLoader_.master;
  808. }
  809. /**
  810. * return the currently selected playlist
  811. *
  812. * @return {Object} the currently selected playlist object that we parsed
  813. */
  814. media() {
  815. // playlist loader will not return media if it has not been fully loaded
  816. return this.masterPlaylistLoader_.media() || this.initialMedia_;
  817. }
  818. /**
  819. * setup our internal source buffers on our segment Loaders
  820. *
  821. * @private
  822. */
  823. setupSourceBuffers_() {
  824. let media = this.masterPlaylistLoader_.media();
  825. let mimeTypes;
  826. // wait until a media playlist is available and the Media Source is
  827. // attached
  828. if (!media || this.mediaSource.readyState !== 'open') {
  829. return;
  830. }
  831. mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media);
  832. if (mimeTypes.length < 1) {
  833. this.error =
  834. 'No compatible SourceBuffer configuration for the variant stream:' +
  835. media.resolvedUri;
  836. return this.mediaSource.endOfStream('decode');
  837. }
  838. this.mainSegmentLoader_.mimeType(mimeTypes[0]);
  839. if (mimeTypes[1]) {
  840. this.audioSegmentLoader_.mimeType(mimeTypes[1]);
  841. }
  842. // exclude any incompatible variant streams from future playlist
  843. // selection
  844. this.excludeIncompatibleVariants_(media);
  845. }
  846. /**
  847. * Blacklist playlists that are known to be codec or
  848. * stream-incompatible with the SourceBuffer configuration. For
  849. * instance, Media Source Extensions would cause the video element to
  850. * stall waiting for video data if you switched from a variant with
  851. * video and audio to an audio-only one.
  852. *
  853. * @param {Object} media a media playlist compatible with the current
  854. * set of SourceBuffers. Variants in the current master playlist that
  855. * do not appear to have compatible codec or stream configurations
  856. * will be excluded from the default playlist selection algorithm
  857. * indefinitely.
  858. * @private
  859. */
  860. excludeIncompatibleVariants_(media) {
  861. let master = this.masterPlaylistLoader_.master;
  862. let codecCount = 2;
  863. let videoCodec = null;
  864. let audioProfile = null;
  865. let codecs;
  866. if (media.attributes && media.attributes.CODECS) {
  867. codecs = parseCodecs(media.attributes.CODECS);
  868. videoCodec = codecs.videoCodec;
  869. audioProfile = codecs.audioProfile;
  870. codecCount = codecs.codecCount;
  871. }
  872. master.playlists.forEach(function(variant) {
  873. let variantCodecs = {
  874. codecCount: 2,
  875. videoCodec: null,
  876. audioProfile: null
  877. };
  878. if (variant.attributes && variant.attributes.CODECS) {
  879. let codecString = variant.attributes.CODECS;
  880. variantCodecs = parseCodecs(codecString);
  881. if (window.MediaSource &&
  882. window.MediaSource.isTypeSupported &&
  883. !window.MediaSource.isTypeSupported('video/mp4; codecs="' + codecString + '"')) {
  884. variant.excludeUntil = Infinity;
  885. }
  886. }
  887. // if the streams differ in the presence or absence of audio or
  888. // video, they are incompatible
  889. if (variantCodecs.codecCount !== codecCount) {
  890. variant.excludeUntil = Infinity;
  891. }
  892. // if h.264 is specified on the current playlist, some flavor of
  893. // it must be specified on all compatible variants
  894. if (variantCodecs.videoCodec !== videoCodec) {
  895. variant.excludeUntil = Infinity;
  896. }
  897. // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
  898. // AAC audio in Chrome 46. Don't mix the two.
  899. if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
  900. (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
  901. variant.excludeUntil = Infinity;
  902. }
  903. });
  904. }
  905. updateAdCues_(media, offset = 0) {
  906. AdCueTags.updateAdCues(media, this.cueTagsTrack_, offset);
  907. }
  908. }