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.IDestroyable');
  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.IDestroyable}
  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.destroy = function() {
  63. let p = this.eventManager_.destroy();
  64. this.eventManager_ = null;
  65. if (this.trickPlayTimer_ != null) {
  66. this.trickPlayTimer_.cancel();
  67. this.trickPlayTimer_ = null;
  68. }
  69. this.video_ = null;
  70. this.onSeek_ = null;
  71. return p;
  72. };
  73. /**
  74. * Gets the video's current (logical) position.
  75. *
  76. * @return {number}
  77. */
  78. shaka.media.VideoWrapper.prototype.getTime = function() {
  79. if (this.video_.readyState > 0) {
  80. return this.video_.currentTime;
  81. } else {
  82. return this.startTime_;
  83. }
  84. };
  85. /**
  86. * Sets the current time of the video.
  87. *
  88. * @param {number} time
  89. */
  90. shaka.media.VideoWrapper.prototype.setTime = function(time) {
  91. if (this.video_.readyState > 0) {
  92. this.movePlayhead_(this.video_.currentTime, time);
  93. } else {
  94. this.startTime_ = time;
  95. setTimeout(this.onSeek_, 0);
  96. }
  97. };
  98. /**
  99. * Gets the current effective playback rate. This may be negative even if the
  100. * browser does not directly support rewinding.
  101. * @return {number}
  102. */
  103. shaka.media.VideoWrapper.prototype.getPlaybackRate = function() {
  104. return this.playbackRate_;
  105. };
  106. /**
  107. * Sets the playback rate.
  108. * @param {number} rate
  109. */
  110. shaka.media.VideoWrapper.prototype.setPlaybackRate = function(rate) {
  111. if (this.trickPlayTimer_ != null) {
  112. this.trickPlayTimer_.cancel();
  113. this.trickPlayTimer_ = null;
  114. }
  115. this.playbackRate_ = rate;
  116. // All major browsers support playback rates above zero. Only need fake
  117. // trick play for negative rates.
  118. this.video_.playbackRate = (this.buffering_ || rate < 0) ? 0 : rate;
  119. if (!this.buffering_ && rate < 0) {
  120. // Defer creating the timer until we stop buffering. This function will be
  121. // called again from setBuffering().
  122. let trickPlay = () => { this.video_.currentTime += rate / 4; };
  123. this.trickPlayTimer_ = new shaka.util.Timer(trickPlay);
  124. this.trickPlayTimer_.scheduleRepeated(0.25);
  125. }
  126. };
  127. /**
  128. * Stops the playhead for buffering, or resumes the playhead after buffering.
  129. *
  130. * @param {boolean} buffering True to stop the playhead; false to allow it to
  131. * continue.
  132. */
  133. shaka.media.VideoWrapper.prototype.setBuffering = function(buffering) {
  134. if (buffering != this.buffering_) {
  135. this.buffering_ = buffering;
  136. this.setPlaybackRate(this.playbackRate_);
  137. }
  138. };
  139. /**
  140. * Handles a 'ratechange' event.
  141. *
  142. * @private
  143. */
  144. shaka.media.VideoWrapper.prototype.onRateChange_ = function() {
  145. // NOTE: This will not allow explicitly setting the playback rate to 0 while
  146. // the playback rate is negative. Pause will still work.
  147. let expectedRate =
  148. this.buffering_ || this.playbackRate_ < 0 ? 0 : this.playbackRate_;
  149. // Native controls in Edge trigger a change to playbackRate and set it to 0
  150. // when seeking. If we don't exclude 0 from this check, we will force the
  151. // rate to stay at 0 after a seek with Edge native controls.
  152. // https://github.com/google/shaka-player/issues/951
  153. if (this.video_.playbackRate && this.video_.playbackRate != expectedRate) {
  154. shaka.log.debug('Video playback rate changed to', this.video_.playbackRate);
  155. this.setPlaybackRate(this.video_.playbackRate);
  156. }
  157. };
  158. /**
  159. * Handles a 'loadedmetadata' event.
  160. *
  161. * @private
  162. */
  163. shaka.media.VideoWrapper.prototype.onLoadedMetadata_ = function() {
  164. if (Math.abs(this.video_.currentTime - this.startTime_) < 0.001) {
  165. this.onSeekingToStartTime_();
  166. } else {
  167. this.eventManager_.listenOnce(
  168. this.video_, 'seeking', this.onSeekingToStartTime_.bind(this));
  169. // If the currentTime != 0, it indicates that the user has seeked after
  170. // calling load(), so it is intended to start from a specific timestamp
  171. // when playback, and should not be overriden by the startTime.
  172. if (this.video_.currentTime == 0) {
  173. this.video_.currentTime = this.startTime_;
  174. } else {
  175. // This is a workaround solution. If the currentTime is not set again, the
  176. // video is stuck and could not be played.
  177. // TODO: Need further investigation why it happens. Before and after
  178. // setting the current time, video.readyState is 1, video.paused is true,
  179. // and video.buffered's TimeRanges length is 0.
  180. // See: https://github.com/google/shaka-player/issues/1298
  181. this.video_.currentTime = this.video_.currentTime;
  182. }
  183. }
  184. };
  185. /**
  186. * Handles the 'seeking' event from the initial jump to the start time (if
  187. * there is one).
  188. *
  189. * @private
  190. */
  191. shaka.media.VideoWrapper.prototype.onSeekingToStartTime_ = function() {
  192. goog.asserts.assert(this.video_.readyState > 0,
  193. 'readyState should be greater than 0');
  194. this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
  195. };
  196. /**
  197. * Moves the playhead to the target time, triggering a call to onSeeking_().
  198. *
  199. * @param {number} currentTime
  200. * @param {number} targetTime
  201. * @private
  202. */
  203. shaka.media.VideoWrapper.prototype.movePlayhead_ = function(
  204. currentTime, targetTime) {
  205. shaka.log.debug('Moving playhead...',
  206. 'currentTime=' + currentTime,
  207. 'targetTime=' + targetTime);
  208. this.video_.currentTime = targetTime;
  209. // Sometimes, IE and Edge ignore re-seeks. Check every 100ms and try
  210. // again if need be, up to 10 tries.
  211. // Delay stats over 100 runs of a re-seeking integration test:
  212. // IE - 0ms - 47%
  213. // IE - 100ms - 63%
  214. // Edge - 0ms - 2%
  215. // Edge - 100ms - 40%
  216. // Edge - 200ms - 32%
  217. // Edge - 300ms - 24%
  218. // Edge - 400ms - 2%
  219. // Chrome - 0ms - 100%
  220. // TODO: File a bug on IE/Edge about this.
  221. let tries = 0;
  222. let recheck = () => {
  223. if (!this.video_) return;
  224. if (tries++ >= 10) return;
  225. if (this.video_.currentTime == currentTime) {
  226. // Sigh. Try again.
  227. this.video_.currentTime = targetTime;
  228. setTimeout(recheck, 100);
  229. }
  230. };
  231. setTimeout(recheck, 100);
  232. };