/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.media.RegionObserver');
goog.require('shaka.media.IPlayheadObserver');
goog.require('shaka.media.RegionTimeline');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
/**
* The region observer watches a region timeline and playhead, and fires events
* ('enter', 'exit', 'skip') as the playhead moves.
*
* @implements {shaka.media.IPlayheadObserver}
* @final
*/
shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
/**
* Create a region observer for the given timeline. The observer does not
* own the timeline, only uses it. This means that the observer should NOT
* destroy the timeline.
*
* @param {!shaka.media.RegionTimeline} timeline
*/
constructor(timeline) {
super();
/** @private {shaka.media.RegionTimeline} */
this.timeline_ = timeline;
/**
* A mapping between a region and where we previously were relative to it.
* When the value here differs from what we calculate, it means we moved and
* should fire an event.
*
* @private {!Map.<shaka.extern.TimelineRegionInfo,
* shaka.media.RegionObserver.RelativePosition_>}
*/
this.oldPosition_ = new Map();
// To make the rules easier to read, alias all the relative positions.
const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
const BEFORE_THE_REGION = RelativePosition.BEFORE_THE_REGION;
const IN_THE_REGION = RelativePosition.IN_THE_REGION;
const AFTER_THE_REGION = RelativePosition.AFTER_THE_REGION;
/**
* A read-only collection of rules for what to do when we change position
* relative to a region.
*
* @private {!Iterable.<shaka.media.RegionObserver.Rule_>}
*/
this.rules_ = [
{
weWere: null,
weAre: IN_THE_REGION,
invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
},
{
weWere: BEFORE_THE_REGION,
weAre: IN_THE_REGION,
invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
},
{
weWere: AFTER_THE_REGION,
weAre: IN_THE_REGION,
invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
},
{
weWere: IN_THE_REGION,
weAre: BEFORE_THE_REGION,
invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
},
{
weWere: IN_THE_REGION,
weAre: AFTER_THE_REGION,
invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
},
{
weWere: BEFORE_THE_REGION,
weAre: AFTER_THE_REGION,
invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
},
{
weWere: AFTER_THE_REGION,
weAre: BEFORE_THE_REGION,
invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
},
];
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
this.eventManager_.listen(this.timeline_, 'regionremove', (event) => {
/** @type {shaka.extern.TimelineRegionInfo} */
const region = event['region'];
this.oldPosition_.delete(region);
});
}
/** @override */
release() {
this.timeline_ = null;
// Clear our maps so that we are not holding onto any more information than
// needed.
this.oldPosition_.clear();
this.eventManager_.release();
this.eventManager_ = null;
super.release();
}
/** @override */
poll(positionInSeconds, wasSeeking) {
const RegionObserver = shaka.media.RegionObserver;
for (const region of this.timeline_.regions()) {
const previousPosition = this.oldPosition_.get(region);
const currentPosition = RegionObserver.determinePositionRelativeTo_(
region, positionInSeconds);
// We will only use |previousPosition| and |currentPosition|, so we can
// update our state now.
this.oldPosition_.set(region, currentPosition);
for (const rule of this.rules_) {
if (rule.weWere == previousPosition && rule.weAre == currentPosition) {
rule.invoke(region, wasSeeking);
}
}
}
}
/**
* Dispatch events of the given type. All event types in this class have the
* same parameters: region and seeking.
*
* @param {string} eventType
* @param {shaka.extern.TimelineRegionInfo} region
* @param {boolean} seeking
* @private
*/
onEvent_(eventType, region, seeking) {
const event = new shaka.util.FakeEvent(eventType, new Map([
['region', region],
['seeking', seeking],
]));
this.dispatchEvent(event);
}
/**
* Get the relative position of the playhead to |region| when the playhead is
* at |seconds|. We treat the region's start and end times as inclusive
* bounds.
*
* @param {shaka.extern.TimelineRegionInfo} region
* @param {number} seconds
* @return {shaka.media.RegionObserver.RelativePosition_}
* @private
*/
static determinePositionRelativeTo_(region, seconds) {
const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
if (seconds < region.startTime) {
return RelativePosition.BEFORE_THE_REGION;
}
if (seconds > region.endTime) {
return RelativePosition.AFTER_THE_REGION;
}
return RelativePosition.IN_THE_REGION;
}
};
/**
* An enum of relative positions between the playhead and a region. Each is
* phrased so that it works in "The playhead is X" where "X" is any value in
* the enum.
*
* @enum {number}
* @private
*/
shaka.media.RegionObserver.RelativePosition_ = {
BEFORE_THE_REGION: 1,
IN_THE_REGION: 2,
AFTER_THE_REGION: 3,
};
/**
* All region observer events (onEnter, onExit, and onSkip) will be passed the
* region that the playhead is interacting with and whether or not the playhead
* moving is part of a seek event.
*
* @typedef {function(shaka.extern.TimelineRegionInfo, boolean)}
*/
shaka.media.RegionObserver.EventListener;
/**
* @typedef {{
* weWere: ?shaka.media.RegionObserver.RelativePosition_,
* weAre: ?shaka.media.RegionObserver.RelativePosition_,
* invoke: shaka.media.RegionObserver.EventListener
* }}
*
* @private
*/
shaka.media.RegionObserver.Rule_;