Source: lib/media/video_wrapper.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.media.VideoWrapper');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.util.EventManager');
  21. goog.require('shaka.util.IReleasable');
  22. goog.require('shaka.util.Timer');
  23. /**
  24. * Creates a new VideoWrapper that manages setting current time and playback
  25. * rate. This handles seeks before content is loaded and ensuring the video
  26. * time is set properly. This doesn't handle repositioning within the
  27. * presentation window.
  28. *
  29. * @param {!HTMLMediaElement} video
  30. * @param {function()} onSeek Called when the video seeks.
  31. * @param {number} startTime The time to start at.
  32. *
  33. * @constructor
  34. * @struct
  35. * @implements {shaka.util.IReleasable}
  36. */
  37. shaka.media.VideoWrapper = function(video, onSeek, startTime) {
  38. /** @private {HTMLMediaElement} */
  39. this.video_ = video;
  40. /** @private {?function()} */
  41. this.onSeek_ = onSeek;
  42. /** @private {number} */
  43. this.startTime_ = startTime;
  44. /** @private {shaka.util.EventManager} */
  45. this.eventManager_ = new shaka.util.EventManager();
  46. /** @private {number} */
  47. this.playbackRate_ = 1;
  48. /** @private {boolean} */
  49. this.buffering_ = false;
  50. /** @private {shaka.util.Timer} */
  51. this.trickPlayTimer_ = null;
  52. // Check if the video has already loaded some metadata.
  53. if (video.readyState > 0) {
  54. this.onLoadedMetadata_();
  55. } else {
  56. this.eventManager_.listenOnce(
  57. video, 'loadedmetadata', this.onLoadedMetadata_.bind(this));
  58. }
  59. this.eventManager_.listen(video, 'ratechange', this.onRateChange_.bind(this));
  60. };
  61. /** @override */
  62. shaka.media.VideoWrapper.prototype.release = function() {
  63. if (this.eventManager_) {
  64. this.eventManager_.release();
  65. this.eventManager_ = null;
  66. }
  67. if (this.trickPlayTimer_ != null) {
  68. this.trickPlayTimer_.stop();
  69. this.trickPlayTimer_ = null;
  70. }
  71. this.onSeek_ = null;
  72. this.video_ = null;
  73. };
  74. /**
  75. * Gets the video's current (logical) position.
  76. *
  77. * @return {number}
  78. */
  79. shaka.media.VideoWrapper.prototype.getTime = function() {
  80. if (this.video_.readyState > 0) {
  81. return this.video_.currentTime;
  82. } else {
  83. return this.startTime_;
  84. }
  85. };
  86. /**
  87. * Sets the current time of the video.
  88. *
  89. * @param {number} time
  90. */
  91. shaka.media.VideoWrapper.prototype.setTime = function(time) {
  92. if (this.video_.readyState > 0) {
  93. this.movePlayhead_(this.video_.currentTime, time);
  94. } else {
  95. this.startTime_ = time;
  96. setTimeout(this.onSeek_, 0);
  97. }
  98. };
  99. /**
  100. * Gets the current effective playback rate. This may be negative even if the
  101. * browser does not directly support rewinding.
  102. * @return {number}
  103. */
  104. shaka.media.VideoWrapper.prototype.getPlaybackRate = function() {
  105. return this.playbackRate_;
  106. };
  107. /**
  108. * Sets the playback rate.
  109. * @param {number} rate
  110. */
  111. shaka.media.VideoWrapper.prototype.setPlaybackRate = function(rate) {
  112. if (this.trickPlayTimer_ != null) {
  113. this.trickPlayTimer_.stop();
  114. this.trickPlayTimer_ = null;
  115. }
  116. this.playbackRate_ = rate;
  117. // All major browsers support playback rates above zero. Only need fake
  118. // trick play for negative rates.
  119. this.video_.playbackRate = (this.buffering_ || rate < 0) ? 0 : rate;
  120. if (!this.buffering_ && rate < 0) {
  121. // Defer creating the timer until we stop buffering. This function will be
  122. // called again from setBuffering().
  123. let trickPlay = () => { this.video_.currentTime += rate / 4; };
  124. this.trickPlayTimer_ = new shaka.util.Timer(trickPlay);
  125. this.trickPlayTimer_.start(/* seconds= */ 0.25, /* repeating= */ true);
  126. }
  127. };
  128. /**
  129. * Stops the playhead for buffering, or resumes the playhead after buffering.
  130. *
  131. * @param {boolean} buffering True to stop the playhead; false to allow it to
  132. * continue.
  133. */
  134. shaka.media.VideoWrapper.prototype.setBuffering = function(buffering) {
  135. if (buffering != this.buffering_) {
  136. this.buffering_ = buffering;
  137. this.setPlaybackRate(this.playbackRate_);
  138. }
  139. };
  140. /**
  141. * Handles a 'ratechange' event.
  142. *
  143. * @private
  144. */
  145. shaka.media.VideoWrapper.prototype.onRateChange_ = function() {
  146. // NOTE: This will not allow explicitly setting the playback rate to 0 while
  147. // the playback rate is negative. Pause will still work.
  148. let expectedRate =
  149. this.buffering_ || this.playbackRate_ < 0 ? 0 : this.playbackRate_;
  150. // Native controls in Edge trigger a change to playbackRate and set it to 0
  151. // when seeking. If we don't exclude 0 from this check, we will force the
  152. // rate to stay at 0 after a seek with Edge native controls.
  153. // https://github.com/google/shaka-player/issues/951
  154. if (this.video_.playbackRate && this.video_.playbackRate != expectedRate) {
  155. shaka.log.debug('Video playback rate changed to', this.video_.playbackRate);
  156. this.setPlaybackRate(this.video_.playbackRate);
  157. }
  158. };
  159. /**
  160. * Handles a 'loadedmetadata' event.
  161. *
  162. * @private
  163. */
  164. shaka.media.VideoWrapper.prototype.onLoadedMetadata_ = function() {
  165. if (Math.abs(this.video_.currentTime - this.startTime_) < 0.001) {
  166. this.onSeekingToStartTime_();
  167. } else {
  168. this.eventManager_.listenOnce(
  169. this.video_, 'seeking', this.onSeekingToStartTime_.bind(this));
  170. // If the currentTime != 0, it indicates that the user has seeked after
  171. // calling load(), so it is intended to start from a specific timestamp
  172. // when playback, and should not be overriden by the startTime.
  173. if (this.video_.currentTime == 0) {
  174. this.video_.currentTime = this.startTime_;
  175. } else {
  176. // This is a workaround solution. If the currentTime is not set again, the
  177. // video is stuck and could not be played.
  178. // TODO: Need further investigation why it happens. Before and after
  179. // setting the current time, video.readyState is 1, video.paused is true,
  180. // and video.buffered's TimeRanges length is 0.
  181. // See: https://github.com/google/shaka-player/issues/1298
  182. this.video_.currentTime = this.video_.currentTime;
  183. }
  184. }
  185. };
  186. /**
  187. * Handles the 'seeking' event from the initial jump to the start time (if
  188. * there is one).
  189. *
  190. * @private
  191. */
  192. shaka.media.VideoWrapper.prototype.onSeekingToStartTime_ = function() {
  193. goog.asserts.assert(this.video_.readyState > 0,
  194. 'readyState should be greater than 0');
  195. this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
  196. };
  197. /**
  198. * Moves the playhead to the target time, triggering a call to onSeeking_().
  199. *
  200. * @param {number} currentTime
  201. * @param {number} targetTime
  202. * @private
  203. */
  204. shaka.media.VideoWrapper.prototype.movePlayhead_ = function(
  205. currentTime, targetTime) {
  206. shaka.log.debug('Moving playhead...',
  207. 'currentTime=' + currentTime,
  208. 'targetTime=' + targetTime);
  209. this.video_.currentTime = targetTime;
  210. // Sometimes, IE and Edge ignore re-seeks. Check every 100ms and try
  211. // again if need be, up to 10 tries.
  212. // Delay stats over 100 runs of a re-seeking integration test:
  213. // IE - 0ms - 47%
  214. // IE - 100ms - 63%
  215. // Edge - 0ms - 2%
  216. // Edge - 100ms - 40%
  217. // Edge - 200ms - 32%
  218. // Edge - 300ms - 24%
  219. // Edge - 400ms - 2%
  220. // Chrome - 0ms - 100%
  221. // TODO: File a bug on IE/Edge about this.
  222. let tries = 0;
  223. let recheck = () => {
  224. if (!this.video_) return;
  225. if (tries++ >= 10) return;
  226. if (this.video_.currentTime == currentTime) {
  227. // Sigh. Try again.
  228. this.video_.currentTime = targetTime;
  229. setTimeout(recheck, 100);
  230. }
  231. };
  232. setTimeout(recheck, 100);
  233. };