Source: lib/media/closed_caption_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.ClosedCaptionParser');
  7. goog.provide('shaka.media.IClosedCaptionParser');
  8. goog.require('shaka.cea.DummyCaptionDecoder');
  9. goog.require('shaka.cea.DummyCeaParser');
  10. goog.require('shaka.log');
  11. goog.require('shaka.util.BufferUtils');
  12. /**
  13. * The IClosedCaptionParser defines the interface to provide all operations for
  14. * parsing the closed captions embedded in Dash videos streams.
  15. * TODO: Remove this interface and move method definitions
  16. * directly to ClosedCaptionParser.
  17. * @interface
  18. * @export
  19. */
  20. shaka.media.IClosedCaptionParser = class {
  21. /**
  22. * Initialize the caption parser. This should be called whenever new init
  23. * segment arrives.
  24. * @param {BufferSource} initSegment
  25. * @param {boolean=} adaptation True if we just automatically switched active
  26. * variant(s).
  27. * @param {number=} continuityTimeline the optional continuity timeline
  28. */
  29. init(initSegment, adaptation = false, continuityTimeline = -1) {}
  30. /**
  31. * Parses embedded CEA closed captions and interacts with the underlying
  32. * CaptionStream, and calls the callback function when there are closed
  33. * captions.
  34. *
  35. * @param {BufferSource} mediaFragment
  36. * @return {!Array<!shaka.extern.ICaptionDecoder.ClosedCaption>}
  37. * An array of parsed closed captions.
  38. */
  39. parseFrom(mediaFragment) {}
  40. /**
  41. * Resets the CaptionStream.
  42. */
  43. reset() {}
  44. /**
  45. * Remove items from the decoder cache based on the provided continuity
  46. * timelines. Caches relating to provided timelines are kept and the rest
  47. * are discarded.
  48. *
  49. * @param {Array<number>} timelinesToKeep
  50. */
  51. remove(timelinesToKeep = []) {}
  52. /**
  53. * Returns the streams that the CEA decoder found.
  54. * @return {!Array<string>}
  55. */
  56. getStreams() {}
  57. };
  58. /**
  59. * Closed Caption Parser provides all operations for parsing the closed captions
  60. * embedded in Dash videos streams.
  61. *
  62. * @implements {shaka.media.IClosedCaptionParser}
  63. * @final
  64. * @export
  65. */
  66. shaka.media.ClosedCaptionParser = class {
  67. /**
  68. * @param {string} mimeType
  69. */
  70. constructor(mimeType) {
  71. /** @private {Map<number, shaka.extern.ICaptionDecoder>} */
  72. this.decoderCache_ = new Map();
  73. /** @private {number} */
  74. this.currentContinuityTimeline_ = 0;
  75. /** @private {!shaka.extern.ICeaParser} */
  76. this.ceaParser_ = new shaka.cea.DummyCeaParser();
  77. const parserFactory =
  78. shaka.media.ClosedCaptionParser.findParser(mimeType.toLowerCase());
  79. if (parserFactory) {
  80. this.ceaParser_ = parserFactory();
  81. }
  82. /**
  83. * Decoder for decoding CEA-X08 data from closed caption packets.
  84. * @private {!shaka.extern.ICaptionDecoder}
  85. */
  86. this.ceaDecoder_ = new shaka.cea.DummyCaptionDecoder();
  87. const decoderFactory = shaka.media.ClosedCaptionParser.findDecoder();
  88. if (decoderFactory) {
  89. this.ceaDecoder_ = decoderFactory();
  90. this.decoderCache_.set(this.currentContinuityTimeline_, this.ceaDecoder_);
  91. }
  92. }
  93. /**
  94. * @override
  95. */
  96. init(initSegment, adaptation = false, continuityTimeline = -1) {
  97. shaka.log.debug('Passing new init segment to CEA parser');
  98. if (continuityTimeline != -1 &&
  99. this.currentContinuityTimeline_ != continuityTimeline) {
  100. // When we get a new init segment associated with a different continuity
  101. // timeline, we should switch to a new decoder until we go back to the
  102. // current continuity timeline.
  103. this.updateDecoder_(continuityTimeline);
  104. } else if (!adaptation) {
  105. // Reset underlying decoder when new init segment arrives
  106. // to clear stored pts values.
  107. // This is necessary when a new Period comes in DASH or a discontinuity
  108. // in HLS.
  109. this.reset();
  110. }
  111. this.ceaParser_.init(initSegment);
  112. if (continuityTimeline != -1) {
  113. this.currentContinuityTimeline_ = continuityTimeline;
  114. }
  115. }
  116. /**
  117. * @override
  118. */
  119. parseFrom(mediaFragment) {
  120. // Parse the fragment.
  121. const captionPackets = this.ceaParser_.parse(mediaFragment);
  122. // Extract the caption packets for decoding.
  123. for (const captionPacket of captionPackets) {
  124. const uint8ArrayData =
  125. shaka.util.BufferUtils.toUint8(captionPacket.packet);
  126. if (uint8ArrayData.length > 0) {
  127. this.ceaDecoder_.extract(uint8ArrayData, captionPacket.pts);
  128. }
  129. }
  130. // Decode and return the parsed captions.
  131. return this.ceaDecoder_.decode();
  132. }
  133. /**
  134. * @private
  135. */
  136. updateDecoder_(continuityTimeline) {
  137. const decoder = this.decoderCache_.get(continuityTimeline);
  138. this.decoderCache_.set(this.currentContinuityTimeline_, this.ceaDecoder_);
  139. if (decoder) {
  140. this.ceaDecoder_ = decoder;
  141. } else {
  142. const decoderFactory = shaka.media.ClosedCaptionParser.findDecoder();
  143. if (decoderFactory) {
  144. this.ceaDecoder_ = decoderFactory();
  145. }
  146. this.decoderCache_.set(continuityTimeline, this.ceaDecoder_);
  147. }
  148. }
  149. /**
  150. * @override
  151. */
  152. reset() {
  153. this.ceaDecoder_.clear();
  154. }
  155. /**
  156. * @override
  157. */
  158. remove(timelinesToKeep = []) {
  159. const timelines = new Set(timelinesToKeep);
  160. for (const key of this.decoderCache_.keys()) {
  161. if (!timelines.has(key)) {
  162. let decoder = this.decoderCache_.get(key);
  163. if (decoder) {
  164. decoder.clear();
  165. }
  166. this.decoderCache_.delete(key);
  167. decoder = null;
  168. }
  169. }
  170. }
  171. /**
  172. * @override
  173. */
  174. getStreams() {
  175. return this.ceaDecoder_.getStreams();
  176. }
  177. /**
  178. * @param {string} mimeType
  179. * @param {!shaka.extern.CeaParserPlugin} plugin
  180. * @export
  181. */
  182. static registerParser(mimeType, plugin) {
  183. shaka.media.ClosedCaptionParser.parserMap_.set(mimeType, plugin);
  184. }
  185. /**
  186. * @param {string} mimeType
  187. * @export
  188. */
  189. static unregisterParser(mimeType) {
  190. shaka.media.ClosedCaptionParser.parserMap_.delete(mimeType);
  191. }
  192. /**
  193. * @param {string} mimeType
  194. * @return {?shaka.extern.CeaParserPlugin}
  195. * @export
  196. */
  197. static findParser(mimeType) {
  198. return shaka.media.ClosedCaptionParser.parserMap_.get(mimeType);
  199. }
  200. /**
  201. * @param {!shaka.extern.CaptionDecoderPlugin} plugin
  202. * @export
  203. */
  204. static registerDecoder(plugin) {
  205. shaka.media.ClosedCaptionParser.decoderFactory_ = plugin;
  206. }
  207. /**
  208. * @export
  209. */
  210. static unregisterDecoder() {
  211. shaka.media.ClosedCaptionParser.decoderFactory_ = null;
  212. }
  213. /**
  214. * @return {?shaka.extern.CaptionDecoderPlugin}
  215. * @export
  216. */
  217. static findDecoder() {
  218. return shaka.media.ClosedCaptionParser.decoderFactory_;
  219. }
  220. };
  221. /** @private {!Map<string, shaka.extern.CeaParserPlugin>} */
  222. shaka.media.ClosedCaptionParser.parserMap_ = new Map();
  223. /** @private {?shaka.extern.CaptionDecoderPlugin} */
  224. shaka.media.ClosedCaptionParser.decoderFactory_ = null;