Source: lib/media/region_observer.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.RegionObserver');
  7. goog.require('shaka.media.IPlayheadObserver');
  8. goog.require('shaka.media.RegionTimeline');
  9. goog.require('shaka.util.EventManager');
  10. goog.require('shaka.util.FakeEvent');
  11. goog.require('shaka.util.FakeEventTarget');
  12. /**
  13. * The region observer watches a region timeline and playhead, and fires events
  14. * ('enter', 'exit', 'skip') as the playhead moves.
  15. *
  16. * @implements {shaka.media.IPlayheadObserver}
  17. * @template T
  18. * @final
  19. */
  20. shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
  21. /**
  22. * Create a region observer for the given timeline. The observer does not
  23. * own the timeline, only uses it. This means that the observer should NOT
  24. * destroy the timeline.
  25. *
  26. * @param {!shaka.media.RegionTimeline<T>} timeline
  27. * @param {boolean} startsPastZero
  28. */
  29. constructor(timeline, startsPastZero) {
  30. super();
  31. /** @private {shaka.media.RegionTimeline<T>} */
  32. this.timeline_ = timeline;
  33. /**
  34. * Whether the asset is expected to start at a time beyond 0 seconds.
  35. * For example, if the asset is a live stream.
  36. * If true, we will not start polling for regions until the playhead has
  37. * moved past 0 seconds, to avoid bad behaviors where the current time is
  38. * briefly 0 before we have enough data to play.
  39. * @private {boolean}
  40. */
  41. this.startsPastZero_ = startsPastZero;
  42. /**
  43. * A mapping between a region and where we previously were relative to it.
  44. * When the value here differs from what we calculate, it means we moved and
  45. * should fire an event.
  46. *
  47. * @private {!Map<T, shaka.media.RegionObserver.RelativePosition_>}
  48. */
  49. this.oldPosition_ = new Map();
  50. // To make the rules easier to read, alias all the relative positions.
  51. const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
  52. const BEFORE_THE_REGION = RelativePosition.BEFORE_THE_REGION;
  53. const IN_THE_REGION = RelativePosition.IN_THE_REGION;
  54. const AFTER_THE_REGION = RelativePosition.AFTER_THE_REGION;
  55. /**
  56. * A read-only collection of rules for what to do when we change position
  57. * relative to a region.
  58. *
  59. * @private {!Iterable<shaka.media.RegionObserver.Rule_>}
  60. */
  61. this.rules_ = [
  62. {
  63. weWere: null,
  64. weAre: IN_THE_REGION,
  65. invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
  66. },
  67. {
  68. weWere: BEFORE_THE_REGION,
  69. weAre: IN_THE_REGION,
  70. invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
  71. },
  72. {
  73. weWere: AFTER_THE_REGION,
  74. weAre: IN_THE_REGION,
  75. invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
  76. },
  77. {
  78. weWere: IN_THE_REGION,
  79. weAre: BEFORE_THE_REGION,
  80. invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
  81. },
  82. {
  83. weWere: IN_THE_REGION,
  84. weAre: AFTER_THE_REGION,
  85. invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
  86. },
  87. {
  88. weWere: BEFORE_THE_REGION,
  89. weAre: AFTER_THE_REGION,
  90. invoke: (region, seeking) => {
  91. if (seeking) {
  92. this.onEvent_('skip', region, seeking);
  93. } else {
  94. // This is the case of regions whose duration is smaller than the
  95. // resolution of our polling.
  96. this.onEvent_('enter', region, seeking);
  97. this.onEvent_('exit', region, seeking);
  98. }
  99. },
  100. },
  101. {
  102. weWere: AFTER_THE_REGION,
  103. weAre: BEFORE_THE_REGION,
  104. invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
  105. },
  106. ];
  107. /** @private {shaka.util.EventManager} */
  108. this.eventManager_ = new shaka.util.EventManager();
  109. this.eventManager_.listen(this.timeline_, 'regionremove', (event) => {
  110. /** @type {T} */
  111. const region = event['region'];
  112. this.oldPosition_.delete(region);
  113. });
  114. }
  115. /** @override */
  116. release() {
  117. this.timeline_ = null;
  118. // Clear our maps so that we are not holding onto any more information than
  119. // needed.
  120. this.oldPosition_.clear();
  121. this.eventManager_.release();
  122. this.eventManager_ = null;
  123. super.release();
  124. }
  125. /** @override */
  126. poll(positionInSeconds, wasSeeking) {
  127. const RegionObserver = shaka.media.RegionObserver;
  128. if (this.startsPastZero_ && positionInSeconds == 0) {
  129. // Don't start checking regions until the timeline has begun moving.
  130. return;
  131. }
  132. // Now that we have seen the playhead go past 0, it's okay if it goes
  133. // back there (e.g. seeking back to the start).
  134. this.startsPastZero_ = false;
  135. for (const region of this.timeline_.regions()) {
  136. const previousPosition = this.oldPosition_.get(region);
  137. const currentPosition = RegionObserver.determinePositionRelativeTo_(
  138. region, positionInSeconds);
  139. // We will only use |previousPosition| and |currentPosition|, so we can
  140. // update our state now.
  141. this.oldPosition_.set(region, currentPosition);
  142. for (const rule of this.rules_) {
  143. if (rule.weWere == previousPosition && rule.weAre == currentPosition) {
  144. rule.invoke(region, wasSeeking);
  145. }
  146. }
  147. }
  148. }
  149. /**
  150. * Dispatch events of the given type. All event types in this class have the
  151. * same parameters: region and seeking.
  152. *
  153. * @param {string} eventType
  154. * @param {T} region
  155. * @param {boolean} seeking
  156. * @private
  157. */
  158. onEvent_(eventType, region, seeking) {
  159. const event = new shaka.util.FakeEvent(eventType, new Map([
  160. ['region', region],
  161. ['seeking', seeking],
  162. ]));
  163. this.dispatchEvent(event);
  164. }
  165. /**
  166. * Get the relative position of the playhead to |region| when the playhead is
  167. * at |seconds|. We treat the region's start and end times as inclusive
  168. * bounds.
  169. *
  170. * @template T
  171. * @param {T} region
  172. * @param {number} seconds
  173. * @return {shaka.media.RegionObserver.RelativePosition_}
  174. * @private
  175. */
  176. static determinePositionRelativeTo_(region, seconds) {
  177. const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
  178. if (seconds < region.startTime) {
  179. return RelativePosition.BEFORE_THE_REGION;
  180. }
  181. if (seconds > region.endTime) {
  182. return RelativePosition.AFTER_THE_REGION;
  183. }
  184. return RelativePosition.IN_THE_REGION;
  185. }
  186. };
  187. /**
  188. * An enum of relative positions between the playhead and a region. Each is
  189. * phrased so that it works in "The playhead is X" where "X" is any value in
  190. * the enum.
  191. *
  192. * @enum {number}
  193. * @private
  194. */
  195. shaka.media.RegionObserver.RelativePosition_ = {
  196. BEFORE_THE_REGION: 1,
  197. IN_THE_REGION: 2,
  198. AFTER_THE_REGION: 3,
  199. };
  200. /**
  201. * All region observer events (onEnter, onExit, and onSkip) will be passed the
  202. * region that the playhead is interacting with and whether or not the playhead
  203. * moving is part of a seek event.
  204. *
  205. * @typedef {function(?, boolean)}
  206. */
  207. shaka.media.RegionObserver.EventListener;
  208. /**
  209. * @typedef {{
  210. * weWere: ?shaka.media.RegionObserver.RelativePosition_,
  211. * weAre: ?shaka.media.RegionObserver.RelativePosition_,
  212. * invoke: shaka.media.RegionObserver.EventListener
  213. * }}
  214. *
  215. * @private
  216. */
  217. shaka.media.RegionObserver.Rule_;