Source: lib/text/text_engine.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.text.TextEngine');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.util.Error');
  20. goog.require('shaka.util.IDestroyable');
  21. /**
  22. * Manages text parsers and cues.
  23. *
  24. * @param {shakaExtern.TextDisplayer} displayer
  25. * @struct
  26. * @constructor
  27. * @implements {shaka.util.IDestroyable}
  28. */
  29. shaka.text.TextEngine = function(displayer) {
  30. /** @private {shakaExtern.TextParser} */
  31. this.parser_ = null;
  32. /** @private {shakaExtern.TextDisplayer} */
  33. this.displayer_ = displayer;
  34. /** @private {number} */
  35. this.timestampOffset_ = 0;
  36. /** @private {number} */
  37. this.appendWindowStart_ = 0;
  38. /** @private {number} */
  39. this.appendWindowEnd_ = Infinity;
  40. /** @private {?number} */
  41. this.bufferStart_ = null;
  42. /** @private {?number} */
  43. this.bufferEnd_ = null;
  44. };
  45. /** @private {!Object.<string, !shakaExtern.TextParserPlugin>} */
  46. shaka.text.TextEngine.parserMap_ = {};
  47. /**
  48. * @param {string} mimeType
  49. * @param {!shakaExtern.TextParserPlugin} plugin
  50. * @export
  51. */
  52. shaka.text.TextEngine.registerParser = function(mimeType, plugin) {
  53. shaka.text.TextEngine.parserMap_[mimeType] = plugin;
  54. };
  55. /**
  56. * @param {string} mimeType
  57. * @export
  58. */
  59. shaka.text.TextEngine.unregisterParser = function(mimeType) {
  60. delete shaka.text.TextEngine.parserMap_[mimeType];
  61. };
  62. /**
  63. * @param {string} mimeType
  64. * @return {boolean}
  65. */
  66. shaka.text.TextEngine.isTypeSupported = function(mimeType) {
  67. return !!shaka.text.TextEngine.parserMap_[mimeType];
  68. };
  69. /** @override */
  70. shaka.text.TextEngine.prototype.destroy = function() {
  71. this.parser_ = null;
  72. this.displayer_ = null;
  73. return Promise.resolve();
  74. };
  75. /**
  76. * @param {shakaExtern.TextDisplayer} displayer
  77. * @export
  78. */
  79. shaka.text.TextEngine.prototype.setDisplayer = function(displayer) {
  80. this.displayer_ = displayer;
  81. };
  82. /**
  83. * Initialize the parser. This can be called multiple times, but must be called
  84. * at least once before appendBuffer.
  85. *
  86. * @param {string} mimeType
  87. */
  88. shaka.text.TextEngine.prototype.initParser = function(mimeType) {
  89. let factory = shaka.text.TextEngine.parserMap_[mimeType];
  90. goog.asserts.assert(
  91. factory,
  92. 'Text type negotiation should have happened already');
  93. this.parser_ = new factory();
  94. };
  95. /**
  96. * Parse the start time from the text media segment, if possible.
  97. *
  98. * @param {!ArrayBuffer} buffer
  99. * @return {number}
  100. * @throws {shaka.util.Error} on failure
  101. */
  102. shaka.text.TextEngine.prototype.getStartTime = function(buffer) {
  103. goog.asserts.assert(this.parser_, 'The parser should already be initialized');
  104. /** @type {shakaExtern.TextParser.TimeContext} **/
  105. let time = {
  106. periodStart: 0,
  107. segmentStart: null,
  108. segmentEnd: 0
  109. };
  110. // Parse the buffer and extract the first cue start time.
  111. try {
  112. let allCues = this.parser_.parseMedia(new Uint8Array(buffer), time);
  113. return allCues[0].startTime;
  114. } catch (exception) {
  115. // This could be a failure from the parser itself (init segment required)
  116. // or an exception from allCues.length being zero.
  117. throw new shaka.util.Error(
  118. shaka.util.Error.Severity.CRITICAL,
  119. shaka.util.Error.Category.TEXT,
  120. shaka.util.Error.Code.UNABLE_TO_EXTRACT_CUE_START_TIME,
  121. exception);
  122. }
  123. };
  124. /**
  125. * @param {!ArrayBuffer} buffer
  126. * @param {?number} startTime relative to the start of the presentation
  127. * @param {?number} endTime relative to the start of the presentation
  128. * @return {!Promise}
  129. */
  130. shaka.text.TextEngine.prototype.appendBuffer =
  131. function(buffer, startTime, endTime) {
  132. goog.asserts.assert(this.parser_, 'The parser should already be initialized');
  133. // Start the operation asynchronously to avoid blocking the caller.
  134. return Promise.resolve().then(function() {
  135. // Check that TextEngine hasn't been destroyed.
  136. if (!this.parser_ || !this.displayer_) return;
  137. if (startTime == null || endTime == null) {
  138. this.parser_.parseInit(new Uint8Array(buffer));
  139. return;
  140. }
  141. /** @type {shakaExtern.TextParser.TimeContext} **/
  142. let time = {
  143. periodStart: this.timestampOffset_,
  144. segmentStart: startTime,
  145. segmentEnd: endTime,
  146. };
  147. // Parse the buffer and add the new cues.
  148. let allCues = this.parser_.parseMedia(new Uint8Array(buffer), time);
  149. let cuesToAppend = allCues.filter(function(cue) {
  150. return cue.startTime >= this.appendWindowStart_ &&
  151. cue.startTime < this.appendWindowEnd_;
  152. }.bind(this));
  153. this.displayer_.append(cuesToAppend);
  154. // NOTE: We update the buffered range from the start and end times passed
  155. // down from the segment reference, not with the start and end times of the
  156. // parsed cues. This is important because some segments may contain no
  157. // cues, but we must still consider those ranges buffered.
  158. if (this.bufferStart_ == null) {
  159. this.bufferStart_ = Math.max(startTime, this.appendWindowStart_);
  160. } else {
  161. // We already had something in buffer, and we assume we are extending the
  162. // range from the end.
  163. goog.asserts.assert((startTime - this.bufferEnd_) <= 1,
  164. 'There should not be a gap in text references >1s');
  165. }
  166. this.bufferEnd_ = Math.min(endTime, this.appendWindowEnd_);
  167. }.bind(this));
  168. };
  169. /**
  170. * @param {number} startTime relative to the start of the presentation
  171. * @param {number} endTime relative to the start of the presentation
  172. * @return {!Promise}
  173. */
  174. shaka.text.TextEngine.prototype.remove = function(startTime, endTime) {
  175. // Start the operation asynchronously to avoid blocking the caller.
  176. return Promise.resolve().then(function() {
  177. if (this.displayer_ && this.displayer_.remove(startTime, endTime)) {
  178. if (this.bufferStart_ == null) {
  179. goog.asserts.assert(this.bufferEnd_ == null,
  180. 'end must be null if startTime is null');
  181. } else {
  182. goog.asserts.assert(this.bufferEnd_ != null,
  183. 'end must be non-null if startTime is non-null');
  184. // Update buffered range.
  185. if (endTime <= this.bufferStart_ || startTime >= this.bufferEnd_) {
  186. // No intersection. Nothing was removed.
  187. } else if (startTime <= this.bufferStart_ &&
  188. endTime >= this.bufferEnd_) {
  189. // We wiped out everything.
  190. this.bufferStart_ = this.bufferEnd_ = null;
  191. } else if (startTime <= this.bufferStart_ &&
  192. endTime < this.bufferEnd_) {
  193. // We removed from the beginning of the range.
  194. this.bufferStart_ = endTime;
  195. } else if (startTime > this.bufferStart_ &&
  196. endTime >= this.bufferEnd_) {
  197. // We removed from the end of the range.
  198. this.bufferEnd_ = startTime;
  199. } else {
  200. // We removed from the middle? StreamingEngine isn't supposed to.
  201. goog.asserts.assert(
  202. false, 'removal from the middle is not supported by TextEngine');
  203. }
  204. }
  205. }
  206. }.bind(this));
  207. };
  208. /** @param {number} timestampOffset */
  209. shaka.text.TextEngine.prototype.setTimestampOffset =
  210. function(timestampOffset) {
  211. this.timestampOffset_ = timestampOffset;
  212. };
  213. /**
  214. * @param {number} appendWindowStart
  215. * @param {number} appendWindowEnd
  216. */
  217. shaka.text.TextEngine.prototype.setAppendWindow =
  218. function(appendWindowStart, appendWindowEnd) {
  219. this.appendWindowStart_ = appendWindowStart;
  220. this.appendWindowEnd_ = appendWindowEnd;
  221. };
  222. /**
  223. * @return {?number} Time in seconds of the beginning of the buffered range,
  224. * or null if nothing is buffered.
  225. */
  226. shaka.text.TextEngine.prototype.bufferStart = function() {
  227. return this.bufferStart_;
  228. };
  229. /**
  230. * @return {?number} Time in seconds of the end of the buffered range,
  231. * or null if nothing is buffered.
  232. */
  233. shaka.text.TextEngine.prototype.bufferEnd = function() {
  234. return this.bufferEnd_;
  235. };
  236. /**
  237. * @param {number} t A timestamp
  238. * @return {boolean}
  239. */
  240. shaka.text.TextEngine.prototype.isBuffered = function(t) {
  241. return t >= this.bufferStart_ && t < this.bufferEnd_;
  242. };
  243. /**
  244. * @param {number} t A timestamp
  245. * @return {number} Number of seconds ahead of 't' we have buffered
  246. */
  247. shaka.text.TextEngine.prototype.bufferedAheadOf = function(t) {
  248. if (this.bufferEnd_ == null || this.bufferEnd_ < t) return 0;
  249. goog.asserts.assert(
  250. this.bufferStart_ != null, 'start should not be null if end is not null');
  251. return this.bufferEnd_ - Math.max(t, this.bufferStart_);
  252. };
  253. /**
  254. * Append cues to text displayer.
  255. *
  256. * @param {!Array.<!shaka.text.Cue>} cues
  257. * @export
  258. */
  259. shaka.text.TextEngine.prototype.appendCues = function(cues) {
  260. this.displayer_.append(cues);
  261. };