Home Reference Source

src/utils/webvtt-parser.js

  1. import VTTParser from './vttparser';
  2. import { utf8ArrayToStr } from '../demux/id3';
  3.  
  4. // String.prototype.startsWith is not supported in IE11
  5. const startsWith = function (inputString, searchString, position) {
  6. return inputString.substr(position || 0, searchString.length) === searchString;
  7. };
  8.  
  9. const cueString2millis = function (timeString) {
  10. let ts = parseInt(timeString.substr(-3));
  11. let secs = parseInt(timeString.substr(-6, 2));
  12. let mins = parseInt(timeString.substr(-9, 2));
  13. let hours = timeString.length > 9 ? parseInt(timeString.substr(0, timeString.indexOf(':'))) : 0;
  14.  
  15. if (!Number.isFinite(ts) || !Number.isFinite(secs) || !Number.isFinite(mins) || !Number.isFinite(hours)) {
  16. throw Error(`Malformed X-TIMESTAMP-MAP: Local:${timeString}`);
  17. }
  18.  
  19. ts += 1000 * secs;
  20. ts += 60 * 1000 * mins;
  21. ts += 60 * 60 * 1000 * hours;
  22.  
  23. return ts;
  24. };
  25.  
  26. // From https://github.com/darkskyapp/string-hash
  27. const hash = function (text) {
  28. let hash = 5381;
  29. let i = text.length;
  30. while (i) {
  31. hash = (hash * 33) ^ text.charCodeAt(--i);
  32. }
  33.  
  34. return (hash >>> 0).toString();
  35. };
  36.  
  37. const calculateOffset = function (vttCCs, cc, presentationTime) {
  38. let currCC = vttCCs[cc];
  39. let prevCC = vttCCs[currCC.prevCC];
  40.  
  41. // This is the first discontinuity or cues have been processed since the last discontinuity
  42. // Offset = current discontinuity time
  43. if (!prevCC || (!prevCC.new && currCC.new)) {
  44. vttCCs.ccOffset = vttCCs.presentationOffset = currCC.start;
  45. currCC.new = false;
  46. return;
  47. }
  48.  
  49. // There have been discontinuities since cues were last parsed.
  50. // Offset = time elapsed
  51. while (prevCC && prevCC.new) {
  52. vttCCs.ccOffset += currCC.start - prevCC.start;
  53. currCC.new = false;
  54. currCC = prevCC;
  55. prevCC = vttCCs[currCC.prevCC];
  56. }
  57.  
  58. vttCCs.presentationOffset = presentationTime;
  59. };
  60.  
  61. const WebVTTParser = {
  62. parse: function (vttByteArray, syncPTS, vttCCs, cc, callBack, errorCallBack) {
  63. // Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character.
  64. let re = /\r\n|\n\r|\n|\r/g;
  65. // Uint8Array.prototype.reduce is not implemented in IE11
  66. let vttLines = utf8ArrayToStr(new Uint8Array(vttByteArray)).trim().replace(re, '\n').split('\n');
  67.  
  68. let cueTime = '00:00.000';
  69. let mpegTs = 0;
  70. let localTime = 0;
  71. let presentationTime = 0;
  72. let cues = [];
  73. let parsingError;
  74. let inHeader = true;
  75. let timestampMap = false;
  76. // let VTTCue = VTTCue || window.TextTrackCue;
  77.  
  78. // Create parser object using VTTCue with TextTrackCue fallback on certain browsers.
  79. let parser = new VTTParser();
  80.  
  81. parser.oncue = function (cue) {
  82. // Adjust cue timing; clamp cues to start no earlier than - and drop cues that don't end after - 0 on timeline.
  83. let currCC = vttCCs[cc];
  84. let cueOffset = vttCCs.ccOffset;
  85.  
  86. // Update offsets for new discontinuities
  87. if (currCC && currCC.new) {
  88. if (localTime !== undefined) {
  89. // When local time is provided, offset = discontinuity start time - local time
  90. cueOffset = vttCCs.ccOffset = currCC.start;
  91. } else {
  92. calculateOffset(vttCCs, cc, presentationTime);
  93. }
  94. }
  95.  
  96. if (presentationTime) {
  97. // If we have MPEGTS, offset = presentation time + discontinuity offset
  98. cueOffset = presentationTime - vttCCs.presentationOffset;
  99. }
  100.  
  101. if (timestampMap) {
  102. cue.startTime += cueOffset - localTime;
  103. cue.endTime += cueOffset - localTime;
  104. }
  105.  
  106. // Create a unique hash id for a cue based on start/end times and text.
  107. // This helps timeline-controller to avoid showing repeated captions.
  108. cue.id = hash(cue.startTime.toString()) + hash(cue.endTime.toString()) + hash(cue.text);
  109.  
  110. // Fix encoding of special characters. TODO: Test with all sorts of weird characters.
  111. cue.text = decodeURIComponent(encodeURIComponent(cue.text));
  112. if (cue.endTime > 0) {
  113. cues.push(cue);
  114. }
  115. };
  116.  
  117. parser.onparsingerror = function (e) {
  118. parsingError = e;
  119. };
  120.  
  121. parser.onflush = function () {
  122. if (parsingError && errorCallBack) {
  123. errorCallBack(parsingError);
  124. return;
  125. }
  126. callBack(cues);
  127. };
  128.  
  129. // Go through contents line by line.
  130. vttLines.forEach(line => {
  131. if (inHeader) {
  132. // Look for X-TIMESTAMP-MAP in header.
  133. if (startsWith(line, 'X-TIMESTAMP-MAP=')) {
  134. // Once found, no more are allowed anyway, so stop searching.
  135. inHeader = false;
  136. timestampMap = true;
  137. // Extract LOCAL and MPEGTS.
  138. line.substr(16).split(',').forEach(timestamp => {
  139. if (startsWith(timestamp, 'LOCAL:')) {
  140. cueTime = timestamp.substr(6);
  141. } else if (startsWith(timestamp, 'MPEGTS:')) {
  142. mpegTs = parseInt(timestamp.substr(7));
  143. }
  144. });
  145. try {
  146. // Calculate subtitle offset in milliseconds.
  147. if (syncPTS + ((vttCCs[cc].start * 90000) || 0) < 0) {
  148. syncPTS += 8589934592;
  149. }
  150. // Adjust MPEGTS by sync PTS.
  151. mpegTs -= syncPTS;
  152. // Convert cue time to seconds
  153. localTime = cueString2millis(cueTime) / 1000;
  154. // Convert MPEGTS to seconds from 90kHz.
  155. presentationTime = mpegTs / 90000;
  156. } catch (e) {
  157. timestampMap = false;
  158. parsingError = e;
  159. }
  160. // Return without parsing X-TIMESTAMP-MAP line.
  161. return;
  162. } else if (line === '') {
  163. inHeader = false;
  164. }
  165. }
  166. // Parse line by default.
  167. parser.parse(line + '\n');
  168. });
  169.  
  170. parser.flush();
  171. }
  172. };
  173.  
  174. export default WebVTTParser;