Source: ui/seek_bar.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.SeekBar');
  7. goog.require('shaka.ads.AdManager');
  8. goog.require('shaka.ui.Constants');
  9. goog.require('shaka.ui.Locales');
  10. goog.require('shaka.ui.Localization');
  11. goog.require('shaka.ui.RangeElement');
  12. goog.require('shaka.ui.Utils');
  13. goog.require('shaka.util.Dom');
  14. goog.require('shaka.util.Timer');
  15. goog.requireType('shaka.ads.CuePoint');
  16. goog.requireType('shaka.ui.Controls');
  17. /**
  18. * @extends {shaka.ui.RangeElement}
  19. * @implements {shaka.extern.IUISeekBar}
  20. * @final
  21. * @export
  22. */
  23. shaka.ui.SeekBar = class extends shaka.ui.RangeElement {
  24. /**
  25. * @param {!HTMLElement} parent
  26. * @param {!shaka.ui.Controls} controls
  27. */
  28. constructor(parent, controls) {
  29. super(parent, controls,
  30. [
  31. 'shaka-seek-bar-container',
  32. ],
  33. [
  34. 'shaka-seek-bar',
  35. 'shaka-no-propagation',
  36. 'shaka-show-controls-on-mouse-over',
  37. ]);
  38. /** @private {!HTMLElement} */
  39. this.adMarkerContainer_ = shaka.util.Dom.createHTMLElement('div');
  40. this.adMarkerContainer_.classList.add('shaka-ad-markers');
  41. // Insert the ad markers container as a first child for proper
  42. // positioning.
  43. this.container.insertBefore(
  44. this.adMarkerContainer_, this.container.childNodes[0]);
  45. /** @private {!shaka.extern.UIConfiguration} */
  46. this.config_ = this.controls.getConfig();
  47. /**
  48. * This timer is used to introduce a delay between the user scrubbing across
  49. * the seek bar and the seek being sent to the player.
  50. *
  51. * @private {shaka.util.Timer}
  52. */
  53. this.seekTimer_ = new shaka.util.Timer(() => {
  54. this.video.currentTime = this.getValue();
  55. });
  56. /**
  57. * The timer is activated for live content and checks if
  58. * new ad breaks need to be marked in the current seek range.
  59. *
  60. * @private {shaka.util.Timer}
  61. */
  62. this.adBreaksTimer_ = new shaka.util.Timer(() => {
  63. this.markAdBreaks_();
  64. });
  65. /**
  66. * When user is scrubbing the seek bar - we should pause the video - see
  67. * https://github.com/google/shaka-player/pull/2898#issuecomment-705229215
  68. * but will conditionally pause or play the video after scrubbing
  69. * depending on its previous state
  70. *
  71. * @private {boolean}
  72. */
  73. this.wasPlaying_ = false;
  74. /** @private {!Array.<!shaka.ads.CuePoint>} */
  75. this.adCuePoints_ = [];
  76. this.eventManager.listen(this.localization,
  77. shaka.ui.Localization.LOCALE_UPDATED,
  78. () => this.updateAriaLabel_());
  79. this.eventManager.listen(this.localization,
  80. shaka.ui.Localization.LOCALE_CHANGED,
  81. () => this.updateAriaLabel_());
  82. this.eventManager.listen(
  83. this.adManager, shaka.ads.AdManager.AD_STARTED, () => {
  84. shaka.ui.Utils.setDisplay(this.container, false);
  85. });
  86. this.eventManager.listen(
  87. this.adManager, shaka.ads.AdManager.AD_STOPPED, () => {
  88. if (this.shouldBeDisplayed_()) {
  89. shaka.ui.Utils.setDisplay(this.container, true);
  90. }
  91. });
  92. this.eventManager.listen(
  93. this.adManager, shaka.ads.AdManager.CUEPOINTS_CHANGED, (e) => {
  94. this.adCuePoints_ = (e)['cuepoints'];
  95. this.onAdCuePointsChanged_();
  96. });
  97. this.eventManager.listen(
  98. this.player, 'unloading', () => {
  99. this.adCuePoints_ = [];
  100. this.onAdCuePointsChanged_();
  101. });
  102. // Initialize seek state and label.
  103. this.setValue(this.video.currentTime);
  104. this.update();
  105. this.updateAriaLabel_();
  106. if (this.ad) {
  107. // There was already an ad.
  108. shaka.ui.Utils.setDisplay(this.container, false);
  109. }
  110. }
  111. /** @override */
  112. release() {
  113. if (this.seekTimer_) {
  114. this.seekTimer_.stop();
  115. this.seekTimer_ = null;
  116. this.adBreaksTimer_.stop();
  117. this.adBreaksTimer_ = null;
  118. }
  119. super.release();
  120. }
  121. /**
  122. * Called by the base class when user interaction with the input element
  123. * begins.
  124. *
  125. * @override
  126. */
  127. onChangeStart() {
  128. this.wasPlaying_ = !this.video.paused;
  129. this.controls.setSeeking(true);
  130. this.video.pause();
  131. }
  132. /**
  133. * Update the video element's state to match the input element's state.
  134. * Called by the base class when the input element changes.
  135. *
  136. * @override
  137. */
  138. onChange() {
  139. if (!this.video.duration) {
  140. // Can't seek yet. Ignore.
  141. return;
  142. }
  143. // Update the UI right away.
  144. this.update();
  145. // We want to wait until the user has stopped moving the seek bar for a
  146. // little bit to reduce the number of times we ask the player to seek.
  147. //
  148. // To do this, we will start a timer that will fire in a little bit, but if
  149. // we see another seek bar change, we will cancel that timer and re-start
  150. // it.
  151. //
  152. // Calling |start| on an already pending timer will cancel the old request
  153. // and start the new one.
  154. this.seekTimer_.tickAfter(/* seconds= */ 0.125);
  155. }
  156. /**
  157. * Called by the base class when user interaction with the input element
  158. * ends.
  159. *
  160. * @override
  161. */
  162. onChangeEnd() {
  163. // They just let go of the seek bar, so cancel the timer and manually
  164. // call the event so that we can respond immediately.
  165. this.seekTimer_.tickNow();
  166. this.controls.setSeeking(false);
  167. if (this.wasPlaying_) {
  168. this.video.play();
  169. }
  170. }
  171. /**
  172. * @override
  173. */
  174. isShowing() {
  175. // It is showing by default, so it is hidden if shaka-hidden is in the list.
  176. return !this.container.classList.contains('shaka-hidden');
  177. }
  178. /**
  179. * @override
  180. */
  181. update() {
  182. const colors = this.config_.seekBarColors;
  183. const currentTime = this.getValue();
  184. const bufferedLength = this.video.buffered.length;
  185. const bufferedStart = bufferedLength ? this.video.buffered.start(0) : 0;
  186. const bufferedEnd =
  187. bufferedLength ? this.video.buffered.end(bufferedLength - 1) : 0;
  188. const seekRange = this.player.seekRange();
  189. const seekRangeSize = seekRange.end - seekRange.start;
  190. this.setRange(seekRange.start, seekRange.end);
  191. if (!this.shouldBeDisplayed_()) {
  192. shaka.ui.Utils.setDisplay(this.container, false);
  193. } else {
  194. shaka.ui.Utils.setDisplay(this.container, true);
  195. if (bufferedLength == 0) {
  196. this.container.style.background = colors.base;
  197. } else {
  198. const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
  199. const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
  200. const clampedCurrentTime = Math.min(
  201. Math.max(currentTime, seekRange.start),
  202. seekRange.end);
  203. const bufferStartDistance = clampedBufferStart - seekRange.start;
  204. const bufferEndDistance = clampedBufferEnd - seekRange.start;
  205. const playheadDistance = clampedCurrentTime - seekRange.start;
  206. // NOTE: the fallback to zero eliminates NaN.
  207. const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
  208. const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
  209. const playheadFraction = (playheadDistance / seekRangeSize) || 0;
  210. const unbufferedColor =
  211. this.config_.showUnbufferedStart ? colors.base : colors.played;
  212. const gradient = [
  213. 'to right',
  214. this.makeColor_(unbufferedColor, bufferStartFraction),
  215. this.makeColor_(colors.played, bufferStartFraction),
  216. this.makeColor_(colors.played, playheadFraction),
  217. this.makeColor_(colors.buffered, playheadFraction),
  218. this.makeColor_(colors.buffered, bufferEndFraction),
  219. this.makeColor_(colors.base, bufferEndFraction),
  220. ];
  221. this.container.style.background =
  222. 'linear-gradient(' + gradient.join(',') + ')';
  223. }
  224. }
  225. }
  226. /**
  227. * @private
  228. */
  229. markAdBreaks_() {
  230. if (!this.adCuePoints_.length) {
  231. this.adMarkerContainer_.style.background = 'transparent';
  232. return;
  233. }
  234. const seekRange = this.player.seekRange();
  235. const seekRangeSize = seekRange.end - seekRange.start;
  236. const gradient = ['to right'];
  237. const pointsAsFractions = [];
  238. const adBreakColor = this.config_.seekBarColors.adBreaks;
  239. let postRollAd = false;
  240. for (const point of this.adCuePoints_) {
  241. // Post-roll ads are marked as starting at -1 in CS IMA ads.
  242. if (point.start == -1 && !point.end) {
  243. postRollAd = true;
  244. }
  245. // Filter point within the seek range. For points with no endpoint
  246. // (client side ads) check that the start point is within range.
  247. if (point.start >= seekRange.start && point.start < seekRange.end) {
  248. if (point.end && point.end > seekRange.end) {
  249. continue;
  250. }
  251. const startDist = point.start - seekRange.start;
  252. const startFrac = (startDist / seekRangeSize) || 0;
  253. // For points with no endpoint assume a 1% length: not too much,
  254. // but enough to be visible on the timeline.
  255. let endFrac = startFrac + 0.01;
  256. if (point.end) {
  257. const endDist = point.end - seekRange.start;
  258. endFrac = (endDist / seekRangeSize) || 0;
  259. }
  260. pointsAsFractions.push({
  261. start: startFrac,
  262. end: endFrac,
  263. });
  264. }
  265. }
  266. for (const point of pointsAsFractions) {
  267. gradient.push(this.makeColor_('transparent', point.start));
  268. gradient.push(this.makeColor_(adBreakColor, point.start));
  269. gradient.push(this.makeColor_(adBreakColor, point.end));
  270. gradient.push(this.makeColor_('transparent', point.end));
  271. }
  272. if (postRollAd) {
  273. gradient.push(this.makeColor_('transparent', 0.99));
  274. gradient.push(this.makeColor_(adBreakColor, 0.99));
  275. }
  276. this.adMarkerContainer_.style.background =
  277. 'linear-gradient(' + gradient.join(',') + ')';
  278. }
  279. /**
  280. * @param {string} color
  281. * @param {number} fract
  282. * @return {string}
  283. * @private
  284. */
  285. makeColor_(color, fract) {
  286. return color + ' ' + (fract * 100) + '%';
  287. }
  288. /**
  289. * @private
  290. */
  291. onAdCuePointsChanged_() {
  292. this.markAdBreaks_();
  293. const seekRange = this.player.seekRange();
  294. const seekRangeSize = seekRange.end - seekRange.start;
  295. const minSeekBarWindow = shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR;
  296. // Seek range keeps changing for live content and some of the known
  297. // ad breaks might not be in the seek range now, but get into
  298. // it later.
  299. // If we have a LIVE seekable content, keep checking for ad breaks
  300. // every second.
  301. if (this.player.isLive() && seekRangeSize > minSeekBarWindow) {
  302. this.adBreaksTimer_.tickEvery(1);
  303. }
  304. }
  305. /**
  306. * @return {boolean}
  307. * @private
  308. */
  309. shouldBeDisplayed_() {
  310. // The seek bar should be hidden when the seek window's too small or
  311. // there's an ad playing.
  312. const seekRange = this.player.seekRange();
  313. const seekRangeSize = seekRange.end - seekRange.start;
  314. if (this.player.isLive() &&
  315. seekRangeSize < shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
  316. return false;
  317. }
  318. return this.ad == null;
  319. }
  320. /** @private */
  321. updateAriaLabel_() {
  322. this.bar.setAttribute(shaka.ui.Constants.ARIA_LABEL,
  323. this.localization.resolve(shaka.ui.Locales.Ids.SEEK));
  324. }
  325. };
  326. /**
  327. * @implements {shaka.extern.IUISeekBar.Factory}
  328. * @export
  329. */
  330. shaka.ui.SeekBar.Factory = class {
  331. /**
  332. * Creates a shaka.ui.SeekBar. Use this factory to register the default
  333. * SeekBar when needed
  334. *
  335. * @override
  336. */
  337. create(rootElement, controls) {
  338. return new shaka.ui.SeekBar(rootElement, controls);
  339. }
  340. };