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