Source: add-subtitles.js

/**
 * @function addSubtitlesToVideo
 * @summary
 * Create subtitles from a standard .vtt file, and add to the input container Movie Clip.
 * @description
 * The .vtt file must be in the directory /src/sounds
 * Center the container Movie Clip at the desired location of the subtitles, and set
 * the alpha of the shape in the container to zero.
 *
 * Set options.showWhenMuted to true if you only want the subtitles displayed
 * when the video is muted.  This cannot detect if the entire browser tab is muted.
 *
 * Set optional styling parameters for the text and background shape.
 *
 * Required parameters:
 * @param {Object} options
 * @param {string} options.vtFileName
 * @param {createjs.MovieClip} options.container
 *
 * Optional parameters:
 * @param {boolean=}  options.allowMutedPlayWithSubtitles (default true)
 * @param {boolean=}  options.showWhenMuted (default false)
 * @param {number=} options.fontSize (default 24) units px
 * @param {string=} options.fontFamily (default "Arial")
 * @param {string=} options.fontColor (default "#CCC")
 * @param {number=} options.fontAlpha (default 0.8)
 * @param {number=} options.fontStyle (default "normal", allowable "italic" and "oblique")
 * @param {number|string=} options.fontWeight (default "normal", allowable 100, 200, ...900)
 * @param {string=} options.backgroundColor (default "#333")
 * @param {number=} options.backgroundAlpha (default 0.3)
 * @param {number=} options.padding (default 10) units px
 * @param {number=} options.lineFactor (default 1.25) lineHeight = lineFactor * fontSize
 */
function addSubtitlesToVideo (options) {
  if (typeof options !== 'object') {
    console.error('addSubtitlesToVideo must be passed an options object')
    return
  }
  if (typeof options.vtFileName !== 'string') {
    console.error('addSubtitlestoVideo must be passed options.vtFileName')
    return
  }
  var createjs = window.createjs
  if (!(options.container instanceof createjs.MovieClip)) {
    console.error('options.container must be a movie clip')
    return
  }
  window.$b.allowMutedPlayWithSubtitles = options.allowMutedPlayWithSubtitles !== false
  if (window.$b.allowMutedPlayWithSubtitles && !window.$b.playVideoWithFallback) {
    console.error('Upgrade blink version to allow muted autoplay with subtitles')
  }
  var container = options.container
  var containerCenter = {
    x: container.nominalBounds.x + container.nominalBounds.width / 2,
    y: container.nominalBounds.y + container.nominalBounds.height / 2
  }
  var showWhenMuted = !!options.showWhenMuted
  console.log('showWhenMuted = ' + showWhenMuted)
  var mcOptions = {
    mode: 'independent',
    startPosition: 0,
    loop: false,
    ignoreGlobalPause: true,
    paused: false,
    labels: { start: 0 }
  }
  var mc = new createjs.MovieClip(mcOptions)
  options.container.addChild(mc)
  mc.x = containerCenter.x
  mc.y = containerCenter.y
  var timeline = new createjs.Timeline({ useTicks: false })
  mc.textTimeline = timeline
  var video = window.$b._videoSlot
  loadVtt(options)
    .then(function (result) {
      var frames = parseVtt({ text: result })
      if (frames instanceof Array) {
        var addFramesAndPlay = function () {
          addFrames(Object.assign({}, options, {
            mc: mc,
            frames: frames,
            timeline: timeline,
            duration: video.duration
          }))
          mc.textTimeline.gotoAndStop()
          /*
           * Sometimes the browser will be stopping autoplay before changing video.paused to false,
           * so add small delay before playing subtitles
           */
          setTimeout(function () {
            playSubtitles()
          }, 10)
        }
        /*
         * Cannot add the subtitles until the video duration is known
         */
        if (video.readyState > 0) {
          addFramesAndPlay()
        } else {
          video.addEventListener('canplay', function (evt) {
            addFramesAndPlay()
          })
        }
      }
    })
  function onMute () {
    if (window.$b.getAdVolume() === 0) {
      mc.alpha = 1
    } else {
      mc.alpha = 0
    }
    window.$b.stage.update()
  }
  function playSubtitles (evt) {
    if (!video.paused) {
      var currentTime = video.currentTime
      mc.textTimeline.gotoAndPlay(currentTime * 1000)
      mc.alpha = 1
      showWhenMuted && onMute()
    }
  }
  function pauseSubtitles (evt) {
    mc.textTimeline.gotoAndStop()
    mc.alpha = 0
  }
  video.addEventListener('play', playSubtitles)
  video.addEventListener('playing', playSubtitles)
  video.addEventListener('seeked', playSubtitles)
  video.addEventListener('pause', pauseSubtitles)
  if (showWhenMuted) {
    onMute()
    window.$b.on('AdVolumeChange', onMute)
  }
}

function isNumber (x) {
  return typeof x === 'number'
}

function addFrames (options) {
  var defaultFontSize = 24
  var defaultFontWeight = 'bold'
  var defaultFontFamily = 'Arial'
  var defaultLineFactor = 1.25
  var defaultFontColor = '#333'
  var defaultBackgroundColor = '#CCC'
  var defaultPadding = 10
  var frames = options.frames
  var timeline = options.timeline
  var mc = options.mc
  /*
   * Set font & shape options
   */
  var fontSize = isNumber(options.fontSize) ? options.fontSize : defaultFontSize
  var fontWeight
  if (!options.fontWeight) {
    fontWeight = defaultFontWeight
  } else if (isNumber(options.fontWeight)) {
    if ([100, 200, 300, 400, 500, 600, 700, 800, 900].includes(options.fontWeight)) {
      fontWeight = options.fontWeight
    } else {
      console.error('Numeric allowable font weigths are 100, 200, ... 900')
      fontWeight = defaultFontWeight
    }
  } else if (['normal', 'bold', 'bolder', 'lighter'].includes(options.fontWeight)) {
    fontWeight = options.fontWeight
  } else {
    console.error('Named allowable font weights are normal, bold, bolder, or lighter')
    fontWeight = defaultFontWeight
  }
  var fontFamily = defaultFontFamily
  if (options.fontFamily) {
    var fonts = [
      'Arial',
      'Arial Black',
      'Helvetica',
      'Times New Roman',
      'Times',
      'Courier New',
      'Courier',
      'Verdana',
      'Georgia',
      'Palatino',
      'Garamond',
      'Bookman',
      'Comic Sans MS',
      'Trebuchet MS',
      'Impact'
    ]
    if (!fonts.includes(options.fontFamily)) {
      console.error('Font Family must be one of ' + fonts.join(', '))
    } else {
      fontFamily = options.fontFamily
    }
  }
  var fontStyle = 'normal'
  var fontStyles = ['normal', 'italic', 'oblique']
  if (options.fontStyle) {
    if (fontStyles.includes(options.fontStyle)) {
      fontStyle = options.fontStyle
    } else {
      console.error('font style must be one of ' + fontStyles.join(', '))
    }
  }
  var lineHeight = isNumber(options.lineFactor)
    ? options.lineFactor * fontSize : defaultLineFactor * fontSize
  var fontColor = options.fontColor || defaultFontColor
  var fontAlpha = isNumber(options.fontAlpha) ? options.fontAlpha : 0.8
  var backgroundColor = options.backgroundColor || defaultBackgroundColor
  var backgroundAlpha = isNumber(options.backgroundAlpha) ? options.backgroundAlpha : 0.3
  var padding = isNumber(options.padding) ? options.padding : defaultPadding
  var font = fontStyle + ' ' + fontWeight + ' ' + fontSize + 'px ' + fontFamily

  frames.forEach(function (frame) {
    if (typeof frame !== 'object') {
      console.error('each frame must be an object')
    } else {
      frame.text = typeof frame.text === 'string' ? frame.text : ''
      var textObj = new createjs.Text(frame.text, font, fontColor)
      textObj.textAlign = 'center'
      textObj.alpha = 0
      textObj.lineHeight = lineHeight
      var textBounds = textObj.getBounds()
      var shapeBounds = {
        x: textBounds.x - padding,
        y: -padding,
        width: textBounds.width + padding * 2,
        height: textBounds.height + padding * 2
      }
      var shape = new createjs.Shape()
      shape
        .graphics
        .beginFill(backgroundColor)
        .drawRect(shapeBounds.x, shapeBounds.y, shapeBounds.width, shapeBounds.height)
      shape.alpha = 0
      frame.items = [
        { object: textObj, alpha: fontAlpha },
        { object: shape, alpha: backgroundAlpha }
      ]
      applyTween({
        timeline: timeline,
        duration: options.duration,
        frame: frame
      })
      mc.addChild(shape)
      mc.addChild(textObj)
    }
  })
}

function applyTween (options) {
  var timeline = options.timeline
  var frame = options.frame
  var start = frame.start * 1000
  /*
   * Prevent tween at time 0 from displaying at end of video
   */
  start = Math.max(start, 100)
  var end = frame.end * 1000
  end = Math.min(end, options.duration * 1000)
  if (start >= end) return
  var delta = end - start
  var remainder = options.duration * 1000 - end
  frame.items.forEach(function (item) {
    var tween =
      createjs.Tween.get(item.object, { override: true, ignoreGlobalPause: true, loop: true })
        .to({ alpha: 0 })
        .wait(start)
        .to({ alpha: item.alpha })
        .wait(delta)
        .to({ alpha: 0 })
        .wait(remainder)
    timeline.addTween(tween)
  })
  console.log('start, end', start, end)
}

function loadVtt (options) {
  var url = window.$b.baseUrl + 'sounds/' + options.vtFileName
  return window.$.ajax({
    url: url,
    dataType: 'text'
  })
}

function parseVtt (options) {
  var text = options.text.split('\n')
  if (!/WEBVTT/i.test(text[0])) {
    console.error('captions file must have WEBVTT in first line')
    return
  }
  text.shift()
  text = removeCues(text)
  text = removeNotes(text)
  var frames = []
  while (getNextFrame(text, frames)) {}
  return frames

  function removeCues (text) {
    var textWithoutCues = []
    for (var i = 0; i < text.length - 1; i++) {
      /*
       * A cue is a non-blank line preceding a time period
       */
      if (!(text[i] && (text[i + 1].indexOf('-->') > -1))) {
        textWithoutCues.push(text[i])
      }
    }
    textWithoutCues.push(text[text.length - 1])
    return textWithoutCues
  }

  /**
   * A Note must be:
   * 1. preceded by a blank line
   * 2. start with NOTE in caps
   * 3. includes all text until a blank line followed by a time period
   * @param {Array} text
   * @returns {Array}
   */
  function removeNotes (text) {
    var textWithoutNotes = []
    var i = 0
    var isNote
    while (i < text.length - 1) {
      isNote = false
      if (!text[i] && (text[i + 1].indexOf('NOTE') === 0)) {
        isNote = true
        /*
         * A NOTE can be more than one line long
         */
        while ((i < text.length - 1) && (text[i + 1].indexOf('-->') < 0)) {
          i += 1
        }
      }
      /*
       * Check if NOTE is in the last block of the file
       */
      if (!(isNote && (i === text.length - 1))) {
        textWithoutNotes.push(text[i])
      }
      i += 1
    }
    /*
     * Check for a one-line NOTE
     */
    if ((i < text.length) && (text[i].indexOf('NOTE') !== 0)) {
      textWithoutNotes.push(text[i])
    }
    return textWithoutNotes
  }

  function getNextFrame (text, frames) {
    var frame = {}
    var found
    do {
      found = getTimes(text, frame)
    } while (!found)
    if (!text.length) return false
    getText(text, frame)
    frames.push(frame)
    return true
  }

  function getTimes (text, frame) {
    if (!text.length) return true
    var line = text.shift()
    var words = line.split(/\s*-->\s*/)
    if (words.length < 2) return false
    var start = parseTime(words[0])
    if (start === false) return false
    var end = parseTime(words[1])
    if (end === false) return false
    frame.start = start
    frame.end = end
    return true
  }

  function parseTime (word) {
    word = word.replace(/(\d)\s.*$/, '$1')
    var parts = word.split(':')
    var time = 0
    var seconds = parseFloat(parts.pop())
    if (isNaN(seconds)) return false
    time = seconds
    if (!parts.length) return time
    var minutes = parseInt(parts.pop())
    if (isNaN(minutes)) return time
    time += minutes * 60
    if (!parts.length) return time
    var hours = parseInt(parts.pop())
    if (isNaN(hours)) return time
    time += 3600 * hours
    return time
  }

  function getText (text, frame) {
    frame.text = ''
    while (text.length && text[0].indexOf('-->') < 0) {
      if (frame.text && (text.length > 1) && (text[1].indexOf('-->') < 0)) {
        frame.text += '\n'
      }
      /*
       * Remove HTML tags
       */
      frame.text += (text.shift())
        .replace(/<.*>/g, '')
        .replace(/&lt/g, '<')
        .replace(/&gt/g, '>')
        .replace(/&amp/g, '&')
    }
  }
}

window.$b = window.$b || {}
window.$b.snippets = window.$b.snippets || {}
var snippets = window.$b.snippets
snippets.addSubtitlesToVideo = addSubtitlesToVideo

if (window.module) {
  window.module.exports = addSubtitlesToVideo
}