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. * @final
  18. */
  19. shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
  20. /**
  21. * Create a region observer for the given timeline. The observer does not
  22. * own the timeline, only uses it. This means that the observer should NOT
  23. * destroy the timeline.
  24. *
  25. * @param {!shaka.media.RegionTimeline} timeline
  26. * @param {boolean} startsPastZero
  27. */
  28. constructor(timeline, startsPastZero) {
  29. super();
  30. /** @private {shaka.media.RegionTimeline} */
  31. this.timeline_ = timeline;
  32. /**
  33. * Whether the asset is expected to start at a time beyond 0 seconds.
  34. * For example, if the asset is a live stream.
  35. * If true, we will not start polling for regions until the playhead has
  36. * moved past 0 seconds, to avoid bad behaviors where the current time is
  37. * briefly 0 before we have enough data to play.
  38. * @private {boolean}
  39. */
  40. this.startsPastZero_ = startsPastZero;
  41. /**
  42. * A mapping between a region and where we previously were relative to it.
  43. * When the value here differs from what we calculate, it means we moved and
  44. * should fire an event.
  45. *
  46. * @private {!Map.<shaka.extern.TimelineRegionInfo,
  47. * 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) => this.onEvent_('skip', region, seeking),
  91. },
  92. {
  93. weWere: AFTER_THE_REGION,
  94. weAre: BEFORE_THE_REGION,
  95. invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
  96. },
  97. ];
  98. /** @private {shaka.util.EventManager} */
  99. this.eventManager_ = new shaka.util.EventManager();
  100. this.eventManager_.listen(this.timeline_, 'regionremove', (event) => {
  101. /** @type {shaka.extern.TimelineRegionInfo} */
  102. const region = event['region'];
  103. this.oldPosition_.delete(region);
  104. });
  105. }
  106. /** @override */
  107. release() {
  108. this.timeline_ = null;
  109. // Clear our maps so that we are not holding onto any more information than
  110. // needed.
  111. this.oldPosition_.clear();
  112. this.eventManager_.release();
  113. this.eventManager_ = null;
  114. super.release();
  115. }
  116. /** @override */
  117. poll(positionInSeconds, wasSeeking) {
  118. const RegionObserver = shaka.media.RegionObserver;
  119. if (this.startsPastZero_ && positionInSeconds == 0) {
  120. // Don't start checking regions until the timeline has begun moving.
  121. return;
  122. }
  123. // Now that we have seen the playhead go past 0, it's okay if it goes
  124. // back there (e.g. seeking back to the start).
  125. this.startsPastZero_ = false;
  126. for (const region of this.timeline_.regions()) {
  127. const previousPosition = this.oldPosition_.get(region);
  128. const currentPosition = RegionObserver.determinePositionRelativeTo_(
  129. region, positionInSeconds);
  130. // We will only use |previousPosition| and |currentPosition|, so we can
  131. // update our state now.
  132. this.oldPosition_.set(region, currentPosition);
  133. for (const rule of this.rules_) {
  134. if (rule.weWere == previousPosition && rule.weAre == currentPosition) {
  135. rule.invoke(region, wasSeeking);
  136. }
  137. }
  138. }
  139. }
  140. /**
  141. * Dispatch events of the given type. All event types in this class have the
  142. * same parameters: region and seeking.
  143. *
  144. * @param {string} eventType
  145. * @param {shaka.extern.TimelineRegionInfo} region
  146. * @param {boolean} seeking
  147. * @private
  148. */
  149. onEvent_(eventType, region, seeking) {
  150. const event = new shaka.util.FakeEvent(eventType, new Map([
  151. ['region', region],
  152. ['seeking', seeking],
  153. ]));
  154. this.dispatchEvent(event);
  155. }
  156. /**
  157. * Get the relative position of the playhead to |region| when the playhead is
  158. * at |seconds|. We treat the region's start and end times as inclusive
  159. * bounds.
  160. *
  161. * @param {shaka.extern.TimelineRegionInfo} region
  162. * @param {number} seconds
  163. * @return {shaka.media.RegionObserver.RelativePosition_}
  164. * @private
  165. */
  166. static determinePositionRelativeTo_(region, seconds) {
  167. const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
  168. if (seconds < region.startTime) {
  169. return RelativePosition.BEFORE_THE_REGION;
  170. }
  171. if (seconds > region.endTime) {
  172. return RelativePosition.AFTER_THE_REGION;
  173. }
  174. return RelativePosition.IN_THE_REGION;
  175. }
  176. };
  177. /**
  178. * An enum of relative positions between the playhead and a region. Each is
  179. * phrased so that it works in "The playhead is X" where "X" is any value in
  180. * the enum.
  181. *
  182. * @enum {number}
  183. * @private
  184. */
  185. shaka.media.RegionObserver.RelativePosition_ = {
  186. BEFORE_THE_REGION: 1,
  187. IN_THE_REGION: 2,
  188. AFTER_THE_REGION: 3,
  189. };
  190. /**
  191. * All region observer events (onEnter, onExit, and onSkip) will be passed the
  192. * region that the playhead is interacting with and whether or not the playhead
  193. * moving is part of a seek event.
  194. *
  195. * @typedef {function(shaka.extern.TimelineRegionInfo, boolean)}
  196. */
  197. shaka.media.RegionObserver.EventListener;
  198. /**
  199. * @typedef {{
  200. * weWere: ?shaka.media.RegionObserver.RelativePosition_,
  201. * weAre: ?shaka.media.RegionObserver.RelativePosition_,
  202. * invoke: shaka.media.RegionObserver.EventListener
  203. * }}
  204. *
  205. * @private
  206. */
  207. shaka.media.RegionObserver.Rule_;