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. */
  27. constructor(timeline) {
  28. super();
  29. /** @private {shaka.media.RegionTimeline} */
  30. this.timeline_ = timeline;
  31. /**
  32. * A mapping between a region and where we previously were relative to it.
  33. * When the value here differs from what we calculate, it means we moved and
  34. * should fire an event.
  35. *
  36. * @private {!Map.<shaka.extern.TimelineRegionInfo,
  37. * shaka.media.RegionObserver.RelativePosition_>}
  38. */
  39. this.oldPosition_ = new Map();
  40. // To make the rules easier to read, alias all the relative positions.
  41. const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
  42. const BEFORE_THE_REGION = RelativePosition.BEFORE_THE_REGION;
  43. const IN_THE_REGION = RelativePosition.IN_THE_REGION;
  44. const AFTER_THE_REGION = RelativePosition.AFTER_THE_REGION;
  45. /**
  46. * A read-only collection of rules for what to do when we change position
  47. * relative to a region.
  48. *
  49. * @private {!Iterable.<shaka.media.RegionObserver.Rule_>}
  50. */
  51. this.rules_ = [
  52. {
  53. weWere: null,
  54. weAre: IN_THE_REGION,
  55. invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
  56. },
  57. {
  58. weWere: BEFORE_THE_REGION,
  59. weAre: IN_THE_REGION,
  60. invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
  61. },
  62. {
  63. weWere: AFTER_THE_REGION,
  64. weAre: IN_THE_REGION,
  65. invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
  66. },
  67. {
  68. weWere: IN_THE_REGION,
  69. weAre: BEFORE_THE_REGION,
  70. invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
  71. },
  72. {
  73. weWere: IN_THE_REGION,
  74. weAre: AFTER_THE_REGION,
  75. invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
  76. },
  77. {
  78. weWere: BEFORE_THE_REGION,
  79. weAre: AFTER_THE_REGION,
  80. invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
  81. },
  82. {
  83. weWere: AFTER_THE_REGION,
  84. weAre: BEFORE_THE_REGION,
  85. invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
  86. },
  87. ];
  88. /** @private {shaka.util.EventManager} */
  89. this.eventManager_ = new shaka.util.EventManager();
  90. this.eventManager_.listen(this.timeline_, 'regionremove', (event) => {
  91. /** @type {shaka.extern.TimelineRegionInfo} */
  92. const region = event['region'];
  93. this.oldPosition_.delete(region);
  94. });
  95. }
  96. /** @override */
  97. release() {
  98. this.timeline_ = null;
  99. // Clear our maps so that we are not holding onto any more information than
  100. // needed.
  101. this.oldPosition_.clear();
  102. this.eventManager_.release();
  103. this.eventManager_ = null;
  104. super.release();
  105. }
  106. /** @override */
  107. poll(positionInSeconds, wasSeeking) {
  108. const RegionObserver = shaka.media.RegionObserver;
  109. for (const region of this.timeline_.regions()) {
  110. const previousPosition = this.oldPosition_.get(region);
  111. const currentPosition = RegionObserver.determinePositionRelativeTo_(
  112. region, positionInSeconds);
  113. // We will only use |previousPosition| and |currentPosition|, so we can
  114. // update our state now.
  115. this.oldPosition_.set(region, currentPosition);
  116. for (const rule of this.rules_) {
  117. if (rule.weWere == previousPosition && rule.weAre == currentPosition) {
  118. rule.invoke(region, wasSeeking);
  119. }
  120. }
  121. }
  122. }
  123. /**
  124. * Dispatch events of the given type. All event types in this class have the
  125. * same parameters: region and seeking.
  126. *
  127. * @param {string} eventType
  128. * @param {shaka.extern.TimelineRegionInfo} region
  129. * @param {boolean} seeking
  130. * @private
  131. */
  132. onEvent_(eventType, region, seeking) {
  133. const event = new shaka.util.FakeEvent(eventType, new Map([
  134. ['region', region],
  135. ['seeking', seeking],
  136. ]));
  137. this.dispatchEvent(event);
  138. }
  139. /**
  140. * Get the relative position of the playhead to |region| when the playhead is
  141. * at |seconds|. We treat the region's start and end times as inclusive
  142. * bounds.
  143. *
  144. * @param {shaka.extern.TimelineRegionInfo} region
  145. * @param {number} seconds
  146. * @return {shaka.media.RegionObserver.RelativePosition_}
  147. * @private
  148. */
  149. static determinePositionRelativeTo_(region, seconds) {
  150. const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
  151. if (seconds < region.startTime) {
  152. return RelativePosition.BEFORE_THE_REGION;
  153. }
  154. if (seconds > region.endTime) {
  155. return RelativePosition.AFTER_THE_REGION;
  156. }
  157. return RelativePosition.IN_THE_REGION;
  158. }
  159. };
  160. /**
  161. * An enum of relative positions between the playhead and a region. Each is
  162. * phrased so that it works in "The playhead is X" where "X" is any value in
  163. * the enum.
  164. *
  165. * @enum {number}
  166. * @private
  167. */
  168. shaka.media.RegionObserver.RelativePosition_ = {
  169. BEFORE_THE_REGION: 1,
  170. IN_THE_REGION: 2,
  171. AFTER_THE_REGION: 3,
  172. };
  173. /**
  174. * All region observer events (onEnter, onExit, and onSkip) will be passed the
  175. * region that the playhead is interacting with and whether or not the playhead
  176. * moving is part of a seek event.
  177. *
  178. * @typedef {function(shaka.extern.TimelineRegionInfo, boolean)}
  179. */
  180. shaka.media.RegionObserver.EventListener;
  181. /**
  182. * @typedef {{
  183. * weWere: ?shaka.media.RegionObserver.RelativePosition_,
  184. * weAre: ?shaka.media.RegionObserver.RelativePosition_,
  185. * invoke: shaka.media.RegionObserver.EventListener
  186. * }}
  187. *
  188. * @private
  189. */
  190. shaka.media.RegionObserver.Rule_;