Source: lib/media/playhead_observer.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.PlayheadObserver');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.media.TimeRangesUtils');
  20. goog.require('shaka.util.ConfigUtils');
  21. goog.require('shaka.util.EventManager');
  22. goog.require('shaka.util.FakeEvent');
  23. goog.require('shaka.util.IDestroyable');
  24. goog.require('shaka.util.StreamUtils');
  25. /**
  26. * This observes the current playhead position to raise events. This will only
  27. * observe the playhead, {@link shaka.media.Playhead} will modify it. This will:
  28. * <ul>
  29. * <li>Track buffering state and call |onBuffering|.</li>
  30. * <li>Track current Period and call |onChangePeriod|.</li>
  31. * <li>Track timeline regions and raise respective events.</li>
  32. * </ul>
  33. *
  34. * @param {HTMLMediaElement} video
  35. * @param {MediaSource} mediaSource
  36. * @param {shakaExtern.Manifest} manifest
  37. * @param {shakaExtern.StreamingConfiguration} config
  38. * @param {function(boolean)} onBuffering Called and passed true when stopped
  39. * for buffering; called and passed false when proceeding after buffering.
  40. * If passed true, the callback should not set the video's playback rate.
  41. * @param {function(!Event)} onEvent Called when an event is raised to be sent
  42. * to the application.
  43. * @param {function()} onChangePeriod Called when the playhead moves to a
  44. * different Period.
  45. *
  46. * @constructor
  47. * @struct
  48. * @implements {shaka.util.IDestroyable}
  49. */
  50. shaka.media.PlayheadObserver = function(
  51. video, mediaSource, manifest, config, onBuffering, onEvent,
  52. onChangePeriod) {
  53. /** @private {HTMLMediaElement} */
  54. this.video_ = video;
  55. /** @private {MediaSource} */
  56. this.mediaSource_ = mediaSource;
  57. /** @private {?shakaExtern.Manifest} */
  58. this.manifest_ = manifest;
  59. /** @private {?shakaExtern.StreamingConfiguration} */
  60. this.config_ = config;
  61. /** @private {?function(boolean)} */
  62. this.onBuffering_ = onBuffering;
  63. /** @private {?function(!Event)} */
  64. this.onEvent_ = onEvent;
  65. /** @private {?function()} */
  66. this.onChangePeriod_ = onChangePeriod;
  67. /** @private {!Array.<shaka.media.PlayheadObserver.TimelineRegion>} */
  68. this.timelineRegions_ = [];
  69. /** @private {shaka.util.EventManager} */
  70. this.eventManager_ = new shaka.util.EventManager();
  71. /** @private {boolean} */
  72. this.buffering_ = false;
  73. /** @private {number} */
  74. this.curPeriodIndex_ = -1;
  75. /** @private {?number} */
  76. this.watchdogTimer_ = null;
  77. this.startWatchdogTimer_();
  78. };
  79. /**
  80. * The threshold for underflow, in seconds. If there is less than this amount
  81. * of data buffered, we will consider the player to be out of data.
  82. *
  83. * @private {number}
  84. * @const
  85. */
  86. shaka.media.PlayheadObserver.UNDERFLOW_THRESHOLD_ = 0.5;
  87. /**
  88. * @enum {number}
  89. * @private
  90. */
  91. shaka.media.PlayheadObserver.RegionLocation_ = {
  92. FUTURE_REGION: 1,
  93. INSIDE: 2,
  94. PAST_REGION: 3
  95. };
  96. /**
  97. * @typedef {{
  98. * info: shakaExtern.TimelineRegionInfo,
  99. * status: shaka.media.PlayheadObserver.RegionLocation_
  100. * }}
  101. *
  102. * @property {shakaExtern.TimelineRegionInfo} info
  103. * The info for this timeline region.
  104. * @property {shaka.media.PlayheadObserver.RegionLocation_} status
  105. * This tracks where the region is relative to the playhead. This tracks
  106. * whether we are before or after the region so we can raise events if we pass
  107. * it.
  108. */
  109. shaka.media.PlayheadObserver.TimelineRegion;
  110. /** @override */
  111. shaka.media.PlayheadObserver.prototype.destroy = function() {
  112. var p = this.eventManager_ ? this.eventManager_.destroy() : Promise.resolve();
  113. this.eventManager_ = null;
  114. this.cancelWatchdogTimer_();
  115. this.video_ = null;
  116. this.mediaSource_ = null;
  117. this.manifest_ = null;
  118. this.config_ = null;
  119. this.onBuffering_ = null;
  120. this.onEvent_ = null;
  121. this.onChangePeriod_ = null;
  122. this.timelineRegions_ = [];
  123. return p;
  124. };
  125. /** Called when a seek completes. */
  126. shaka.media.PlayheadObserver.prototype.seeked = function() {
  127. this.timelineRegions_.forEach(
  128. this.updateTimelineRegion_.bind(this, /* isSeek */ true));
  129. };
  130. /**
  131. * Adds a new timeline region. Events will be raised whenever the playhead
  132. * enters or exits the given region. This method will raise a
  133. * 'timelineregionadded' event.
  134. * @param {shakaExtern.TimelineRegionInfo} regionInfo
  135. */
  136. shaka.media.PlayheadObserver.prototype.addTimelineRegion = function(
  137. regionInfo) {
  138. // Check there isn't an existing event with the same scheme ID and time range.
  139. // This ensures that the manifest parser doesn't need to also track which
  140. // events have already been added.
  141. var hasExistingRegion = this.timelineRegions_.some(function(existing) {
  142. return existing.info.schemeIdUri == regionInfo.schemeIdUri &&
  143. existing.info.startTime == regionInfo.startTime &&
  144. existing.info.endTime == regionInfo.endTime;
  145. });
  146. if (hasExistingRegion) return;
  147. var region = {
  148. info: regionInfo,
  149. status: shaka.media.PlayheadObserver.RegionLocation_.FUTURE_REGION
  150. };
  151. this.timelineRegions_.push(region);
  152. var cloneTimelineInfo_ = shaka.media.PlayheadObserver.cloneTimelineInfo_;
  153. var event = new shaka.util.FakeEvent(
  154. 'timelineregionadded', {detail: cloneTimelineInfo_(regionInfo)});
  155. this.onEvent_(event);
  156. // Pretend this is a seek so it will ignore if it should be PAST_REGION but
  157. // still fire an event if it should be INSIDE.
  158. this.updateTimelineRegion_(/* isSeek */ true, region);
  159. };
  160. /**
  161. * Clones the given TimelineRegionInfo so the app can modify it without
  162. * modifying our internal objects.
  163. * @param {shakaExtern.TimelineRegionInfo} source
  164. * @return {shakaExtern.TimelineRegionInfo}
  165. * @private
  166. */
  167. shaka.media.PlayheadObserver.cloneTimelineInfo_ = function(source) {
  168. var copy = shaka.util.ConfigUtils.cloneObject(source);
  169. // cloneObject uses JSON to clone, which won't copy the DOM element.
  170. copy.eventElement = source.eventElement;
  171. return copy;
  172. };
  173. /**
  174. * Updates the status of a timeline region and fires any enter/exit events.
  175. * @param {boolean} isSeek
  176. * @param {shaka.media.PlayheadObserver.TimelineRegion} region
  177. * @private
  178. */
  179. shaka.media.PlayheadObserver.prototype.updateTimelineRegion_ = function(
  180. isSeek, region) {
  181. var RegionLocation = shaka.media.PlayheadObserver.RegionLocation_;
  182. var cloneTimelineInfo_ = shaka.media.PlayheadObserver.cloneTimelineInfo_;
  183. // The events are fired when the playhead enters a region. We fire both
  184. // events when passing over a region and not seeking since the playhead was
  185. // in the region but left before we saw it. We don't fire both when seeking
  186. // since the playhead was never in the region.
  187. //
  188. // |--------------------------------------|
  189. // | From \ To | FUTURE | INSIDE | PAST |
  190. // | FUTURE | | enter | both* |
  191. // | INSIDE | exit | | exit |
  192. // | PAST | both* | enter | |
  193. // |--------------------------------------|
  194. // * Only when not seeking.
  195. var newStatus = region.info.startTime > this.video_.currentTime ?
  196. RegionLocation.FUTURE_REGION :
  197. (region.info.endTime < this.video_.currentTime ?
  198. RegionLocation.PAST_REGION :
  199. RegionLocation.INSIDE);
  200. var wasInside = region.status == RegionLocation.INSIDE;
  201. var isInside = newStatus == RegionLocation.INSIDE;
  202. if (newStatus != region.status) {
  203. var passedRegion = !wasInside && !isInside;
  204. if (!(isSeek && passedRegion)) {
  205. if (!wasInside) {
  206. this.onEvent_(new shaka.util.FakeEvent(
  207. 'timelineregionenter',
  208. {'detail': cloneTimelineInfo_(region.info)}));
  209. }
  210. if (!isInside) {
  211. this.onEvent_(new shaka.util.FakeEvent(
  212. 'timelineregionexit', {'detail': cloneTimelineInfo_(region.info)}));
  213. }
  214. }
  215. region.status = newStatus;
  216. }
  217. };
  218. /**
  219. * Starts the watchdog timer.
  220. * @private
  221. */
  222. shaka.media.PlayheadObserver.prototype.startWatchdogTimer_ = function() {
  223. this.cancelWatchdogTimer_();
  224. this.watchdogTimer_ =
  225. window.setTimeout(this.onWatchdogTimer_.bind(this), 250);
  226. };
  227. /**
  228. * Cancels the watchdog timer, if any.
  229. * @private
  230. */
  231. shaka.media.PlayheadObserver.prototype.cancelWatchdogTimer_ = function() {
  232. if (this.watchdogTimer_) {
  233. window.clearTimeout(this.watchdogTimer_);
  234. this.watchdogTimer_ = null;
  235. }
  236. };
  237. /**
  238. * Called on a recurring timer to detect buffering events and Period changes.
  239. * @private
  240. */
  241. shaka.media.PlayheadObserver.prototype.onWatchdogTimer_ = function() {
  242. this.watchdogTimer_ = null;
  243. this.startWatchdogTimer_();
  244. goog.asserts.assert(this.manifest_ && this.config_, 'Must not be destroyed');
  245. var newPeriod = shaka.util.StreamUtils.findPeriodContainingTime(
  246. this.manifest_, this.video_.currentTime);
  247. if (newPeriod != this.curPeriodIndex_) {
  248. // Ignore seek to start time, the first 'trackschanged' event is handled
  249. // during player.load().
  250. if (this.curPeriodIndex_ != -1)
  251. this.onChangePeriod_();
  252. this.curPeriodIndex_ = newPeriod;
  253. }
  254. // This uses an intersection of buffered ranges for both audio and video, so
  255. // it's an accurate way to determine if we are buffering or not.
  256. var bufferedAhead = shaka.media.TimeRangesUtils.bufferedAheadOf(
  257. this.video_.buffered, this.video_.currentTime);
  258. var bufferEnd = shaka.media.TimeRangesUtils.bufferEnd(this.video_.buffered);
  259. var threshold = shaka.media.PlayheadObserver.UNDERFLOW_THRESHOLD_;
  260. var timeline = this.manifest_.presentationTimeline;
  261. var liveEdge = timeline.getSegmentAvailabilityEnd();
  262. var bufferedToLiveEdge = timeline.isLive() && bufferEnd >= liveEdge;
  263. var noMoreSegments = this.mediaSource_.readyState == 'ended';
  264. var atEnd = bufferedToLiveEdge || this.video_.ended || noMoreSegments;
  265. if (!this.buffering_) {
  266. // If there are no buffered ranges but the playhead is at the end of
  267. // the video then we shouldn't enter a buffering state.
  268. if (!atEnd && bufferedAhead < threshold) {
  269. this.setBuffering_(true);
  270. }
  271. } else {
  272. var rebufferingGoal = shaka.util.StreamUtils.getRebufferingGoal(
  273. this.manifest_, this.config_, 1 /* scaleFactor */);
  274. if (atEnd || bufferedAhead >= rebufferingGoal) {
  275. this.setBuffering_(false);
  276. }
  277. }
  278. this.timelineRegions_.forEach(
  279. this.updateTimelineRegion_.bind(this, /* isSeek */ false));
  280. };
  281. /**
  282. * Stops the playhead for buffering, or resumes the playhead after buffering.
  283. *
  284. * @param {boolean} buffering True to stop the playhead; false to allow it to
  285. * continue.
  286. * @private
  287. */
  288. shaka.media.PlayheadObserver.prototype.setBuffering_ = function(buffering) {
  289. if (buffering != this.buffering_) {
  290. this.buffering_ = buffering;
  291. this.onBuffering_(buffering);
  292. }
  293. };