All files / src/utils webvtt-parser.js

5.81% Statements 5/86
0% Branches 0/45
0% Functions 0/10
5.81% Lines 5/86

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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