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