Source: lib/text/vtt_text_parser.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.VttTextParser');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.text.Cue');
  21. goog.require('shaka.text.TextEngine');
  22. goog.require('shaka.util.Error');
  23. goog.require('shaka.util.StringUtils');
  24. goog.require('shaka.util.TextParser');
  25. /**
  26. * @constructor
  27. * @implements {shakaExtern.TextParser}
  28. */
  29. shaka.text.VttTextParser = function() { };
  30. /** @override */
  31. shaka.text.VttTextParser.prototype.parseInit = function(data) {
  32. goog.asserts.assert(false, 'VTT does not have init segments');
  33. };
  34. /**
  35. * @override
  36. * @throws {shaka.util.Error}
  37. */
  38. shaka.text.VttTextParser.prototype.parseMedia = function(data, time) {
  39. var VttTextParser = shaka.text.VttTextParser;
  40. // Get the input as a string. Normalize newlines to \n.
  41. var str = shaka.util.StringUtils.fromUTF8(data);
  42. str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n');
  43. var blocks = str.split(/\n{2,}/m);
  44. if (!/^WEBVTT($|[ \t\n])/m.test(blocks[0])) {
  45. throw new shaka.util.Error(
  46. shaka.util.Error.Severity.CRITICAL,
  47. shaka.util.Error.Category.TEXT,
  48. shaka.util.Error.Code.INVALID_TEXT_HEADER);
  49. }
  50. var offset = time.segmentStart;
  51. // Parse X-TIMESTAMP-MAP metadata header if it's present to get
  52. // time offset information.
  53. // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
  54. if (blocks[0].indexOf('X-TIMESTAMP-MAP') >= 0) {
  55. // 'X-TIMESTAMP-MAP' header is used in HLS to align text with
  56. // the rest of the media.
  57. // The header format is 'X-TIMESTAMP-MAP=MPEGTS:n,LOCAL:m'
  58. // (the attributes can go in any order)
  59. // where n is MPEG-2 time and m is cue time it maps to.
  60. // For example 'X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000'
  61. // means an offset of 10 seconds
  62. // 900000/MPEG_TIMESCALE - cue time.
  63. var cueTimeMatch =
  64. blocks[0].match(/LOCAL:((?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3}))/m);
  65. var mpegTimeMatch = blocks[0].match(/MPEGTS:(\d+)/m);
  66. if (cueTimeMatch && mpegTimeMatch) {
  67. var parser = new shaka.util.TextParser(cueTimeMatch[1]);
  68. var cueTime = shaka.text.VttTextParser.parseTime_(parser);
  69. var mpegTime = Number(mpegTimeMatch[1]);
  70. var mpegTimescale = shaka.text.VttTextParser.MPEG_TIMESCALE_;
  71. // Apple-encoded HLS content uses absolute timestamps, so assume
  72. // the presence of the map tag means the content uses absolute
  73. // timestamps.
  74. offset = time.periodStart + (mpegTime / mpegTimescale - cueTime);
  75. }
  76. }
  77. var ret = [];
  78. for (var i = 1; i < blocks.length; i++) {
  79. var lines = blocks[i].split('\n');
  80. var cue = VttTextParser.parseCue_(lines, offset);
  81. if (cue)
  82. ret.push(cue);
  83. }
  84. return ret;
  85. };
  86. /**
  87. * Parses a text block into a Cue object.
  88. *
  89. * @param {!Array.<string>} text
  90. * @param {number} timeOffset
  91. * @return {shaka.text.Cue}
  92. * @private
  93. */
  94. shaka.text.VttTextParser.parseCue_ = function(text, timeOffset) {
  95. // Skip empty blocks.
  96. if (text.length == 1 && !text[0])
  97. return null;
  98. // Skip comment blocks.
  99. if (/^NOTE($|[ \t])/.test(text[0]))
  100. return null;
  101. var id = null;
  102. var index = text[0].indexOf('-->');
  103. if (index < 0) {
  104. id = text[0];
  105. text.splice(0, 1);
  106. }
  107. // Parse the times.
  108. var parser = new shaka.util.TextParser(text[0]);
  109. var start = shaka.text.VttTextParser.parseTime_(parser);
  110. var expect = parser.readRegex(/[ \t]+-->[ \t]+/g);
  111. var end = shaka.text.VttTextParser.parseTime_(parser);
  112. if (start == null || expect == null || end == null) {
  113. throw new shaka.util.Error(
  114. shaka.util.Error.Severity.CRITICAL,
  115. shaka.util.Error.Category.TEXT,
  116. shaka.util.Error.Code.INVALID_TEXT_CUE);
  117. }
  118. start += timeOffset;
  119. end += timeOffset;
  120. // Get the payload.
  121. var payload = text.slice(1).join('\n').trim();
  122. var cue = new shaka.text.Cue(start, end, payload);
  123. // Parse optional settings.
  124. parser.skipWhitespace();
  125. var word = parser.readWord();
  126. while (word) {
  127. if (!shaka.text.VttTextParser.parseSetting(cue, word)) {
  128. shaka.log.warning('VTT parser encountered an invalid VTT setting: ',
  129. word,
  130. ' The setting will be ignored.');
  131. }
  132. parser.skipWhitespace();
  133. word = parser.readWord();
  134. }
  135. if (id != null)
  136. cue.id = id;
  137. return cue;
  138. };
  139. /**
  140. * Parses a WebVTT setting from the given word.
  141. *
  142. * @param {!shaka.text.Cue} cue
  143. * @param {string} word
  144. * @return {boolean} True on success.
  145. */
  146. shaka.text.VttTextParser.parseSetting = function(cue, word) {
  147. var VttTextParser = shaka.text.VttTextParser;
  148. var results = null;
  149. if ((results = /^align:(start|middle|center|end|left|right)$/.exec(word))) {
  150. VttTextParser.setTextAlign_(cue, results[1]);
  151. } else if ((results = /^vertical:(lr|rl)$/.exec(word))) {
  152. VttTextParser.setVerticalWritingDirection_(cue, results[1]);
  153. } else if ((results = /^size:([\d\.]+)%$/.exec(word))) {
  154. cue.size = Number(results[1]);
  155. } else if ((results =
  156. /^position:([\d\.]+)%(?:,(line-left|line-right|center|start|end))?$/
  157. .exec(word))) {
  158. cue.position = Number(results[1]);
  159. if (results[2]) {
  160. VttTextParser.setPositionAlign_(cue, results[2]);
  161. }
  162. } else {
  163. return VttTextParser.parsedLineValueAndInterpretation_(cue, word);
  164. }
  165. return true;
  166. };
  167. /**
  168. * @param {!shaka.text.Cue} cue
  169. * @param {!string} align
  170. * @private
  171. */
  172. shaka.text.VttTextParser.setTextAlign_ = function(cue, align) {
  173. var Cue = shaka.text.Cue;
  174. if (align == 'middle') {
  175. cue.textAlign = Cue.textAlign.CENTER;
  176. } else {
  177. goog.asserts.assert(align.toUpperCase() in Cue.textAlign,
  178. align.toUpperCase() +
  179. ' Should be in Cue.textAlign values!');
  180. cue.textAlign = Cue.textAlign[align.toUpperCase()];
  181. }
  182. };
  183. /**
  184. * @param {!shaka.text.Cue} cue
  185. * @param {!string} align
  186. * @private
  187. */
  188. shaka.text.VttTextParser.setPositionAlign_ = function(cue, align) {
  189. var Cue = shaka.text.Cue;
  190. if (align == 'line-left' || align == 'start')
  191. cue.positionAlign = Cue.positionAlign.LEFT;
  192. else if (align == 'line-right' || align == 'end')
  193. cue.positionAlign = Cue.positionAlign.RIGHT;
  194. else
  195. cue.positionAlign = Cue.positionAlign.CENTER;
  196. };
  197. /**
  198. * @param {!shaka.text.Cue} cue
  199. * @param {!string} value
  200. * @private
  201. */
  202. shaka.text.VttTextParser.setVerticalWritingDirection_ = function(cue, value) {
  203. var Cue = shaka.text.Cue;
  204. if (value == 'lr')
  205. cue.writingDirection = Cue.writingDirection.VERTICAL_LEFT_TO_RIGHT;
  206. else
  207. cue.writingDirection = Cue.writingDirection.VERTICAL_RIGHT_TO_LEFT;
  208. };
  209. /**
  210. * @param {!shaka.text.Cue} cue
  211. * @param {!string} word
  212. * @return {boolean}
  213. * @private
  214. */
  215. shaka.text.VttTextParser.parsedLineValueAndInterpretation_ =
  216. function(cue, word) {
  217. var Cue = shaka.text.Cue;
  218. var results = null;
  219. if ((results = /^line:([\d\.]+)%(?:,(start|end|center))?$/.exec(word))) {
  220. cue.lineInterpretation = Cue.lineInterpretation.PERCENTAGE;
  221. cue.line = Number(results[1]);
  222. if (results[2]) {
  223. goog.asserts.assert(results[2].toUpperCase() in Cue.lineAlign,
  224. results[2].toUpperCase() +
  225. ' Should be in Cue.lineAlign values!');
  226. cue.lineAlign = Cue.lineAlign[results[2].toUpperCase()];
  227. }
  228. } else if ((results = /^line:(-?\d+)(?:,(start|end|center))?$/.exec(word))) {
  229. cue.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER;
  230. cue.line = Number(results[1]);
  231. if (results[2]) {
  232. goog.asserts.assert(results[2].toUpperCase() in Cue.lineAlign,
  233. results[2].toUpperCase() +
  234. ' Should be in Cue.lineAlign values!');
  235. cue.lineAlign = Cue.lineAlign[results[2].toUpperCase()];
  236. }
  237. } else {
  238. return false;
  239. }
  240. return true;
  241. };
  242. /**
  243. * Parses a WebVTT time from the given parser.
  244. *
  245. * @param {!shaka.util.TextParser} parser
  246. * @return {?number}
  247. * @private
  248. */
  249. shaka.text.VttTextParser.parseTime_ = function(parser) {
  250. // 00:00.000 or 00:00:00.000 or 0:00:00.000
  251. var results = parser.readRegex(/(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3})/g);
  252. if (results == null)
  253. return null;
  254. // This capture is optional, but will still be in the array as undefined,
  255. // default to 0.
  256. var hours = Number(results[1]) || 0;
  257. var minutes = Number(results[2]);
  258. var seconds = Number(results[3]);
  259. var miliseconds = Number(results[4]);
  260. if (minutes > 59 || seconds > 59)
  261. return null;
  262. return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
  263. };
  264. /**
  265. * @const {number}
  266. * @private
  267. */
  268. shaka.text.VttTextParser.MPEG_TIMESCALE_ = 90000;
  269. shaka.text.TextEngine.registerParser(
  270. 'text/vtt',
  271. shaka.text.VttTextParser);
  272. shaka.text.TextEngine.registerParser(
  273. 'text/vtt; codecs="vtt"',
  274. shaka.text.VttTextParser);