Source: lib/text/ssa_text_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.text.SsaTextParser');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.text.Cue');
  10. goog.require('shaka.text.TextEngine');
  11. goog.require('shaka.util.StringUtils');
  12. /**
  13. * Documentation: http://moodub.free.fr/video/ass-specs.doc
  14. * https://en.wikipedia.org/wiki/SubStation_Alpha
  15. * @implements {shaka.extern.TextParser}
  16. * @export
  17. */
  18. shaka.text.SsaTextParser = class {
  19. /**
  20. * @override
  21. * @export
  22. */
  23. parseInit(data) {
  24. goog.asserts.assert(false, 'SSA does not have init segments');
  25. }
  26. /**
  27. * @override
  28. * @export
  29. */
  30. setSequenceMode(sequenceMode) {
  31. // Unused.
  32. }
  33. /**
  34. * @override
  35. * @export
  36. */
  37. setManifestType(manifestType) {
  38. // Unused.
  39. }
  40. /**
  41. * @override
  42. * @export
  43. */
  44. parseMedia(data, time) {
  45. const StringUtils = shaka.util.StringUtils;
  46. const SsaTextParser = shaka.text.SsaTextParser;
  47. // Get the input as a string.
  48. const str = StringUtils.fromUTF8(data);
  49. const section = {
  50. styles: '',
  51. events: '',
  52. };
  53. const parts = str.split(/\r?\n\s*\r?\n/);
  54. for (const part of parts) {
  55. // SSA content
  56. const match = SsaTextParser.ssaContent_.exec(part);
  57. if (match) {
  58. const tag = match[1];
  59. const lines = match[2];
  60. if (tag == 'V4 Styles' || tag == 'V4+ Styles') {
  61. section.styles = lines;
  62. continue;
  63. }
  64. if (tag == 'Events') {
  65. section.events = lines;
  66. continue;
  67. }
  68. }
  69. shaka.log.warning('SsaTextParser parser encountered an unknown part.',
  70. part);
  71. }
  72. // Process styles
  73. const styles = [];
  74. // Used to be able to iterate over the style parameters.
  75. let styleColumns = null;
  76. const styleLines = section.styles.split(/\r?\n/);
  77. for (const line of styleLines) {
  78. if (/^\s*;/.test(line)) {
  79. // Skip comment
  80. continue;
  81. }
  82. const lineParts = SsaTextParser.lineParts_.exec(line);
  83. if (lineParts) {
  84. const name = lineParts[1].trim();
  85. const value = lineParts[2].trim();
  86. if (name == 'Format') {
  87. styleColumns = value.split(SsaTextParser.valuesFormat_);
  88. continue;
  89. }
  90. if (name == 'Style') {
  91. const values = value.split(SsaTextParser.valuesFormat_);
  92. const style = {};
  93. for (let c = 0; c < styleColumns.length && c < values.length; c++) {
  94. style[styleColumns[c]] = values[c];
  95. }
  96. styles.push(style);
  97. continue;
  98. }
  99. }
  100. }
  101. // Process cues
  102. /** @type {!Array.<!shaka.text.Cue>} */
  103. const cues = [];
  104. // Used to be able to iterate over the event parameters.
  105. let eventColumns = null;
  106. const eventLines = section.events.split(/\r?\n/);
  107. for (const line of eventLines) {
  108. if (/^\s*;/.test(line)) {
  109. // Skip comment
  110. continue;
  111. }
  112. const lineParts = SsaTextParser.lineParts_.exec(line);
  113. if (lineParts) {
  114. const name = lineParts[1].trim();
  115. const value = lineParts[2].trim();
  116. if (name == 'Format') {
  117. eventColumns = value.split(SsaTextParser.valuesFormat_);
  118. continue;
  119. }
  120. if (name == 'Dialogue') {
  121. const values = value.split(SsaTextParser.valuesFormat_);
  122. const data = {};
  123. for (let c = 0; c < eventColumns.length && c < values.length; c++) {
  124. data[eventColumns[c]] = values[c];
  125. }
  126. const startTime = SsaTextParser.parseTime_(data['Start']);
  127. const endTime = SsaTextParser.parseTime_(data['End']);
  128. // Note: Normally, you should take the "Text" field, but if it
  129. // has a comma, it fails.
  130. const payload = values.slice(eventColumns.length - 1).join(',')
  131. .replace(/\\N/g, '\n') // '\n' for new line
  132. .replace(/\{[^}]+\}/g, ''); // {\pos(400,570)}
  133. const cue = new shaka.text.Cue(startTime, endTime, payload);
  134. const styleName = data['Style'];
  135. const styleData = styles.find((s) => s['Name'] == styleName);
  136. if (styleData) {
  137. SsaTextParser.addStyle_(cue, styleData);
  138. }
  139. cues.push(cue);
  140. continue;
  141. }
  142. }
  143. }
  144. return cues;
  145. }
  146. /**
  147. * Adds applicable style properties to a cue.
  148. *
  149. * @param {shaka.text.Cue} cue
  150. * @param {Object} style
  151. * @private
  152. */
  153. static addStyle_(cue, style) {
  154. const Cue = shaka.text.Cue;
  155. const SsaTextParser = shaka.text.SsaTextParser;
  156. const fontFamily = style['Fontname'];
  157. if (fontFamily) {
  158. cue.fontFamily = fontFamily;
  159. }
  160. const fontSize = style['Fontsize'];
  161. if (fontSize) {
  162. cue.fontSize = fontSize + 'px';
  163. }
  164. const color = style['PrimaryColour'];
  165. if (color) {
  166. const ccsColor = SsaTextParser.parseSsaColor_(color);
  167. if (ccsColor) {
  168. cue.color = ccsColor;
  169. }
  170. }
  171. const backgroundColor = style['BackColour'];
  172. if (backgroundColor) {
  173. const cssBackgroundColor = SsaTextParser.parseSsaColor_(backgroundColor);
  174. if (cssBackgroundColor) {
  175. cue.backgroundColor = cssBackgroundColor;
  176. }
  177. }
  178. const bold = style['Bold'];
  179. if (bold) {
  180. cue.fontWeight = Cue.fontWeight.BOLD;
  181. }
  182. const italic = style['Italic'];
  183. if (italic) {
  184. cue.fontStyle = Cue.fontStyle.ITALIC;
  185. }
  186. const underline = style['Underline'];
  187. if (underline) {
  188. cue.textDecoration.push(Cue.textDecoration.UNDERLINE);
  189. }
  190. const letterSpacing = style['Spacing'];
  191. if (letterSpacing) {
  192. cue.letterSpacing = letterSpacing + 'px';
  193. }
  194. const alignment = style['Alignment'];
  195. if (alignment) {
  196. const alignmentInt = parseInt(alignment, 10);
  197. switch (alignmentInt) {
  198. case 1:
  199. cue.displayAlign = Cue.displayAlign.AFTER;
  200. cue.textAlign = Cue.textAlign.START;
  201. break;
  202. case 2:
  203. cue.displayAlign = Cue.displayAlign.AFTER;
  204. cue.textAlign = Cue.textAlign.CENTER;
  205. break;
  206. case 3:
  207. cue.displayAlign = Cue.displayAlign.AFTER;
  208. cue.textAlign = Cue.textAlign.END;
  209. break;
  210. case 5:
  211. cue.displayAlign = Cue.displayAlign.BEFORE;
  212. cue.textAlign = Cue.textAlign.START;
  213. break;
  214. case 6:
  215. cue.displayAlign = Cue.displayAlign.BEFORE;
  216. cue.textAlign = Cue.textAlign.CENTER;
  217. break;
  218. case 7:
  219. cue.displayAlign = Cue.displayAlign.BEFORE;
  220. cue.textAlign = Cue.textAlign.END;
  221. break;
  222. case 9:
  223. cue.displayAlign = Cue.displayAlign.CENTER;
  224. cue.textAlign = Cue.textAlign.START;
  225. break;
  226. case 10:
  227. cue.displayAlign = Cue.displayAlign.CENTER;
  228. cue.textAlign = Cue.textAlign.CENTER;
  229. break;
  230. case 11:
  231. cue.displayAlign = Cue.displayAlign.CENTER;
  232. cue.textAlign = Cue.textAlign.END;
  233. break;
  234. }
  235. }
  236. const opacity = style['AlphaLevel'];
  237. if (opacity) {
  238. cue.opacity = parseFloat(opacity);
  239. }
  240. }
  241. /**
  242. * Parses a SSA color .
  243. *
  244. * @param {string} colorString
  245. * @return {?string}
  246. * @private
  247. */
  248. static parseSsaColor_(colorString) {
  249. // The SSA V4+ color can be represented in hex (&HAABBGGRR) or in decimal
  250. // format (byte order AABBGGRR) and in both cases the alpha channel's
  251. // value needs to be inverted as in case of SSA the 0xFF alpha value means
  252. // transparent and 0x00 means opaque
  253. /** @type {number} */
  254. const abgr = parseInt(colorString.replace('&H', ''), 16);
  255. if (abgr >= 0) {
  256. const a = ((abgr >> 24) & 0xFF) ^ 0xFF; // Flip alpha.
  257. const alpha = a / 255;
  258. const b = (abgr >> 16) & 0xFF;
  259. const g = (abgr >> 8) & 0xFF;
  260. const r = abgr & 0xff;
  261. return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
  262. }
  263. return null;
  264. }
  265. /**
  266. * Parses a SSA time from the given parser.
  267. *
  268. * @param {string} string
  269. * @return {number}
  270. * @private
  271. */
  272. static parseTime_(string) {
  273. const SsaTextParser = shaka.text.SsaTextParser;
  274. const match = SsaTextParser.timeFormat_.exec(string);
  275. const hours = match[1] ? parseInt(match[1].replace(':', ''), 10) : 0;
  276. const minutes = parseInt(match[2], 10);
  277. const seconds = parseFloat(match[3]);
  278. return hours * 3600 + minutes * 60 + seconds;
  279. }
  280. };
  281. /**
  282. * @const
  283. * @private {!RegExp}
  284. * @example [V4 Styles]\nFormat: Name\nStyle: DefaultVCD
  285. */
  286. shaka.text.SsaTextParser.ssaContent_ =
  287. /^\s*\[([^\]]+)\]\r?\n([\s\S]*)/;
  288. /**
  289. * @const
  290. * @private {!RegExp}
  291. * @example Style: DefaultVCD,...
  292. */
  293. shaka.text.SsaTextParser.lineParts_ =
  294. /^\s*([^:]+):\s*(.*)/;
  295. /**
  296. * @const
  297. * @private {!RegExp}
  298. * @example Style: DefaultVCD,...
  299. */
  300. shaka.text.SsaTextParser.valuesFormat_ = /\s*,\s*/;
  301. /**
  302. * @const
  303. * @private {!RegExp}
  304. * @example 0:00:01.1 or 0:00:01.18 or 0:00:01.180
  305. */
  306. shaka.text.SsaTextParser.timeFormat_ =
  307. /^(\d+:)?(\d{1,2}):(\d{1,2}(?:[.]\d{1,3})?)?$/;
  308. shaka.text.TextEngine.registerParser(
  309. 'text/x-ssa', () => new shaka.text.SsaTextParser());