UNPKG

rx-player

Version:
409 lines (408 loc) 19.9 kB
"use strict"; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; Object.defineProperty(exports, "__esModule", { value: true }); var assert_1 = require("../../../utils/assert"); var utils_1 = require("./utils"); /** * first or last IHTMLCue in a group can have a slighlty different start * or end time than the start or end time of the ICuesGroup due to parsing * approximation. * DELTA_CUES_GROUP defines the tolerance level when comparing the start/end * of a IHTMLCue to the start/end of a ICuesGroup. * Having this value too high may lead to have unwanted subtitle displayed * Having this value too low may lead to have subtitles not displayed */ var DELTA_CUES_GROUP = 1e-3; /** * segment_duration / RELATIVE_DELTA_RATIO = relative_delta * * relative_delta is the tolerance to determine if two segements are the same */ var RELATIVE_DELTA_RATIO = 5; /** * Manage a buffer of text track cues. * Allows to add, remove and recuperate cues at given times. * @class TextTrackCuesStore */ var TextTrackCuesStore = /** @class */ (function () { function TextTrackCuesStore() { this._cuesBuffer = []; } TextTrackCuesStore.prototype.isEmpty = function () { return this._cuesBuffer.length === 0; }; /** * Get corresponding cue(s) for the given time. * A cue is an object with three properties: * - start {Number}: start time for which the cue should be displayed. * - end {Number}: end time for which the cue should be displayed. * - element {HTMLElement}: The cue to diplay * * We do not mutate individual cue here. * That is, if the ``get`` method returns the same cue's reference than a * previous ``get`` call, its properties are guaranteed to have the exact same * values than before, if you did not mutate it on your side. * The inverse is true, if the values are the same than before, the reference * will stay the same (this is useful to easily check if the DOM should be * updated, for example). * * @param {Number} time * @returns {Array.<HTMLElement>} - The cues that need to be displayed at that * time. */ TextTrackCuesStore.prototype.get = function (time) { var e_1, _a, e_2, _b; var cuesBuffer = this._cuesBuffer; var ret = []; // begins at the end as most of the time the player will ask for the last // CuesGroup for (var cueIdx = cuesBuffer.length - 1; cueIdx >= 0; cueIdx--) { var segment = cuesBuffer[cueIdx]; if (time < segment.end && time >= segment.start) { var cues = segment.cues; try { for (var cues_1 = (e_1 = void 0, __values(cues)), cues_1_1 = cues_1.next(); !cues_1_1.done; cues_1_1 = cues_1.next()) { var cue = cues_1_1.value; if (time >= cue.start && time < cue.end) { ret.push(cue.element); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (cues_1_1 && !cues_1_1.done && (_a = cues_1.return)) _a.call(cues_1); } finally { if (e_1) throw e_1.error; } } // first or last IHTMLCue in a group can have a slighlty different start // or end time than the start or end time of the ICuesGroup due to parsing // approximation. // Add a tolerance of 1ms to fix this issue if (ret.length === 0 && cues.length > 0) { try { for (var cues_2 = (e_2 = void 0, __values(cues)), cues_2_1 = cues_2.next(); !cues_2_1.done; cues_2_1 = cues_2.next()) { var cue = cues_2_1.value; if ((0, utils_1.areNearlyEqual)(time, cue.start, DELTA_CUES_GROUP) || (0, utils_1.areNearlyEqual)(time, cue.end, DELTA_CUES_GROUP)) { ret.push(cue.element); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (cues_2_1 && !cues_2_1.done && (_b = cues_2.return)) _b.call(cues_2); } finally { if (e_2) throw e_2.error; } } } return ret; } } return []; }; /** * Remove cue from a certain range of time. * @param {Number} from * @param {Number} _to */ TextTrackCuesStore.prototype.remove = function (from, _to) { (0, assert_1.default)(from >= 0); (0, assert_1.default)(_to >= 0); (0, assert_1.default)(_to > from); var to = Math.max(from, _to); var cuesBuffer = this._cuesBuffer; for (var cueIdx = 0; cueIdx < cuesBuffer.length; cueIdx++) { if (cuesBuffer[cueIdx].end > from) { // this cuesInfos is concerned by the remove var startCuesInfos = cuesBuffer[cueIdx]; if (startCuesInfos.start >= to) { // our cuesInfos is strictly after this interval, we have nothing to do return; } if (startCuesInfos.end >= to) { // our cuesInfos ends after `to`, we have to keep the end of it if (from <= startCuesInfos.start) { // from -> to only remove the start of startCuesInfos startCuesInfos.cues = (0, utils_1.getCuesAfter)(startCuesInfos.cues, to); startCuesInfos.start = to; } else { // from -> to is in the middle part of startCuesInfos var _a = __read((0, utils_1.removeCuesInfosBetween)(startCuesInfos, from, to), 2), cuesInfos1 = _a[0], cuesInfos2 = _a[1]; this._cuesBuffer[cueIdx] = cuesInfos1; cuesBuffer.splice(cueIdx + 1, 0, cuesInfos2); } // No cuesInfos can be concerned after this one, we can quit return; } // Else remove all part after `from` if (startCuesInfos.start >= from) { // all the segment is concerned cuesBuffer.splice(cueIdx, 1); cueIdx--; // one less element, we have to decrement the loop } else { // only the end is concerned startCuesInfos.cues = (0, utils_1.getCuesBefore)(startCuesInfos.cues, from); startCuesInfos.end = Math.max(from, startCuesInfos.start); } } } }; /** * Insert new cues in our text buffer. * cues is an array of objects with three properties: * - start {Number}: start time for which the cue should be displayed. * - end {Number}: end time for which the cue should be displayed. * - element {HTMLElement}: The cue to diplay * * @param {Array.<Object>} cues - CuesGroups, array of objects with the * following properties: * - start {Number}: the time at which the cue will start to be displayed * - end {Number}: the time at which the cue will end to be displayed * - cue {HTMLElement}: The cue * @param {Number} start - Start time at which the CuesGroup applies. * This is different than the start of the first cue to display in it, this * has more to do with the time at which the _text segment_ starts. * @param {Number} end - End time at which the CuesGroup applies. * This is different than the end of the last cue to display in it, this * has more to do with the time at which the _text segment_ ends. * * TODO add securities to ensure that: * - the start of a CuesGroup is inferior or equal to the start of the first * cue in it * - the end of a CuesGroup is superior or equal to the end of the last * cue in it * If those requirements are not met, we could delete some cues when adding * a CuesGroup before/after. Find a solution. */ TextTrackCuesStore.prototype.insert = function (cues, start, end) { var cuesBuffer = this._cuesBuffer; var cuesInfosToInsert = { start: start, end: end, cues: cues }; // it's preferable to have a delta depending on the duration of the segment // if the delta is one fifth of the length of the segment: // a segment of [0, 2] is the "same" segment as [0, 2.1] // but [0, 0.04] is not the "same" segement as [0,04, 0.08] var relativeDelta = Math.abs(start - end) / RELATIVE_DELTA_RATIO; /** * Called when we found the index of the next cue relative to the cue we * want to insert (that is a cue starting after its start or at the same * time but ending strictly after its end). * Will insert the cue at the right place and update the next cue * accordingly. * @param {number} indexOfNextCue */ function onIndexOfNextCueFound(indexOfNextCue) { var nextCue = cuesBuffer[indexOfNextCue]; if (nextCue === undefined || // no cue (0, utils_1.areNearlyEqual)(cuesInfosToInsert.end, nextCue.end, relativeDelta)) { // samey end // ours: |AAAAA| // the current one: |BBBBB| // Result: |AAAAA| cuesBuffer[indexOfNextCue] = cuesInfosToInsert; } else if (nextCue.start >= cuesInfosToInsert.end) { // Either // ours: |AAAAA| // the current one: |BBBBBB| // Result: |AAAAA| |BBBBBB| // Or: // ours: |AAAAA| // the current one: |BBBBBB| // Result: |AAAAA|BBBBBB| // Add ours before cuesBuffer.splice(indexOfNextCue, 0, cuesInfosToInsert); } else { // Either // ours: |AAAAA| // the current one: |BBBBBBBB| // Result: |AAAAABBB| // Or: // ours: |AAAAA| // the current one: |BBBBB| // Result: |AAAAABBB| nextCue.cues = (0, utils_1.getCuesAfter)(nextCue.cues, cuesInfosToInsert.end); nextCue.start = cuesInfosToInsert.end; cuesBuffer.splice(indexOfNextCue, 0, cuesInfosToInsert); } } for (var cueIdx = 0; cueIdx < cuesBuffer.length; cueIdx++) { var cuesInfos = cuesBuffer[cueIdx]; if (start < cuesInfos.end) { if ((0, utils_1.areCuesStartNearlyEqual)(cuesInfosToInsert, cuesInfos)) { if ((0, utils_1.areNearlyEqual)(end, cuesInfos.end, relativeDelta)) { // exact same segment // ours: |AAAAA| // the current one: |BBBBB| // Result: |AAAAA| // Which means: // 1. replace the current cue with ours cuesBuffer[cueIdx] = cuesInfosToInsert; return; } else if (end < cuesInfos.end) { // our cue overlaps with the current one: // ours: |AAAAA| // the current one: |BBBBBBBB| // Result: |AAAAABBB| // Which means: // 1. remove some cues at the start of the current one // 2. update start of current one // 3. add ours before the current one cuesInfos.cues = (0, utils_1.getCuesAfter)(cuesInfos.cues, end); cuesInfos.start = end; cuesBuffer.splice(cueIdx, 0, cuesInfosToInsert); return; } // our cue goes beyond the current one: // ours: |AAAAAAA| // the current one: |BBBB|... // Result: |AAAAAAA| // Here we have to delete any cuesInfos which end before ours end, // and see about the following one. do { cuesBuffer.splice(cueIdx, 1); cuesInfos = cuesBuffer[cueIdx]; } while (cuesInfos !== undefined && end > cuesInfos.end); onIndexOfNextCueFound(cueIdx); return; } else if (start < cuesInfos.start) { if (end < cuesInfos.start) { // our cue goes strictly before the current one: // ours: |AAAAAAA| // the current one: |BBBB| // Result: |AAAAAAA| |BBBB| // Which means: // - add ours before the current one cuesBuffer.splice(cueIdx, 0, cuesInfosToInsert); return; } else if ((0, utils_1.areNearlyEqual)(end, cuesInfos.start, relativeDelta)) { // our cue goes just before the current one: // ours: |AAAAAAA| // the current one: |BBBB| // Result: |AAAAAAA|BBBB| // Which means: // - update start time of the current one to be sure // - add ours before the current one cuesInfos.start = end; cuesBuffer.splice(cueIdx, 0, cuesInfosToInsert); return; } else if ((0, utils_1.areNearlyEqual)(end, cuesInfos.end, relativeDelta)) { // ours: |AAAAAAA| // the current one: |BBBB| // Result: |AAAAAAA| // Replace cuesBuffer.splice(cueIdx, 1, cuesInfosToInsert); return; } else if (end < cuesInfos.end) { // ours: |AAAAAAA| // the current one: |BBBBB| // Result: |AAAAAAABB| cuesInfos.cues = (0, utils_1.getCuesAfter)(cuesInfos.cues, end); cuesInfos.start = end; cuesBuffer.splice(cueIdx, 0, cuesInfosToInsert); return; } // ours: |AAAAAAA| // the current one: |BBB|... // Result: |AAAAAAA|... do { cuesBuffer.splice(cueIdx, 1); cuesInfos = cuesBuffer[cueIdx]; } while (cuesInfos !== undefined && end > cuesInfos.end); onIndexOfNextCueFound(cueIdx); return; } // else -> start > cuesInfos.start if ((0, utils_1.areNearlyEqual)(cuesInfos.end, end, relativeDelta)) { // ours: |AAAAAA| // the current one: |BBBBBBBB| // Result: |BBAAAAAA| cuesInfos.cues = (0, utils_1.getCuesBefore)(cuesInfos.cues, start); cuesInfos.end = start; cuesBuffer.splice(cueIdx + 1, 0, cuesInfosToInsert); return; } else if (cuesInfos.end > end) { // ours: |AAAAAA| // the current one: |BBBBBBBBBBB| // Result: |BBAAAAAABBB| var _a = __read((0, utils_1.removeCuesInfosBetween)(cuesInfos, start, end), 2), cuesInfos1 = _a[0], cuesInfos2 = _a[1]; this._cuesBuffer[cueIdx] = cuesInfos1; cuesBuffer.splice(cueIdx + 1, 0, cuesInfosToInsert); cuesBuffer.splice(cueIdx + 2, 0, cuesInfos2); return; } else { // ours: |AAAAAA| // the current one: |BBBBB|... // Result: |BBAAAAAA|... cuesInfos.cues = (0, utils_1.getCuesBefore)(cuesInfos.cues, start); cuesInfos.end = start; var nextCueIdx = cueIdx + 1; cuesInfos = cuesBuffer[nextCueIdx]; while (cuesInfos !== undefined && end > cuesInfos.end) { cuesBuffer.splice(nextCueIdx, 1); cuesInfos = cuesBuffer[nextCueIdx]; } onIndexOfNextCueFound(nextCueIdx); return; } } } if (cuesBuffer.length > 0) { var lastCue = cuesBuffer[cuesBuffer.length - 1]; if ((0, utils_1.areNearlyEqual)(lastCue.end, start, relativeDelta)) { // Match the end of the previous cue to the start of the following one // if they are close enough. If there is a small gap between two segments // it can lead to having no subtitles for a short time, this is noticeable when // two successive segments displays the same text, making it diseappear // and reappear quickly, which gives the impression of blinking // // ours: |AAAAA| // the current one: |BBBBB|... // Result: |BBBBBBBAAAAA| lastCue.end = start; } } // no cues group has the end after our current start. // These cues should be the last one cuesBuffer.push(cuesInfosToInsert); }; return TextTrackCuesStore; }()); exports.default = TextTrackCuesStore;