{"name":"Video JS Audio Transcript","key":"videojsaudiotranscript","version":"1.0.1","instructions":"This is a version of VideoJS audio template that shows just an audio player and an interactive transcript. You will need a subtitle file of the same filename but with a .vtt extension in the same directory as the video. This is a wee bit beta ....\n","showatto":"0","showplayers":"1","requirecss":"//vjs.zencdn.net/5.8.8/video-js.css","requirejs":"//vjs.zencdn.net/5.8.8/video.js","shim":"videojs","defaults":"WIDTH=400,HEIGHT=30","amd":"1","body":"<audio id=\"@@AUTOID@@\" class=\"video-js vjs-default-skin nomediaplugin\" controls preload=\"auto\" width=\"@@WIDTH@@\" height=\"@@HEIGHT@@\" data-setup='{\n  \"controlBar\": {\"fullscreenToggle\": false}}'>\n<source src=\"@@VIDEOURL@@\" type=\"@@AUTOMIME@@\" />\n<track kind=\"captions\" src=\"@@URLSTUB@@.vtt\" srclang=\"en\" label=\"English\" default />\n</audio>\n<div id='@@AUTOID@@_transcript' class='videojs_transcript'></div>","bodyend":"","script":"// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel\n// MIT license\n// https://gist.github.com/paulirish/1579671\n(function() {\n  var lastTime = 0;\n  var vendors = ['ms', 'moz', 'webkit', 'o'];\n  for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {\n    window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];\n    window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']\n    || window[vendors[x]+'CancelRequestAnimationFrame'];\n  }\n  if (!window.requestAnimationFrame)\n    window.requestAnimationFrame = function(callback, element) {\n      var currTime = new Date().getTime();\n      var timeToCall = Math.max(0, 16 - (currTime - lastTime));\n      var id = window.setTimeout(function() { callback(currTime + timeToCall); },\n      timeToCall);\n      lastTime = currTime + timeToCall;\n      return id;\n    };\n  if (!window.cancelAnimationFrame)\n    window.cancelAnimationFrame = function(id) {\n      clearTimeout(id);\n    };\n}());\n\n// Object.create() polyfill\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Polyfill\nif (typeof Object.create != 'function') {\n  Object.create = (function() {\n    var Object = function() {};\n    return function (prototype) {\n      if (arguments.length > 1) {\n        throw Error('Second argument not supported');\n      }\n      if (typeof prototype != 'object') {\n        throw TypeError('Argument must be an object');\n      }\n      Object.prototype = prototype;\n      var result = new Object();\n      Object.prototype = null;\n      return result;\n    };\n  })();\n}\n\n// forEach polyfill\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill\nif (!Array.prototype.forEach) {\n  Array.prototype.forEach = function(callback, thisArg) {\n    var T, k;\n    if (this == null) {\n      throw new TypeError(' this is null or not defined');\n    }\n    var O = Object(this);\n    var len = O.length >>> 0;\n    if (typeof callback != \"function\") {\n      throw new TypeError(callback + ' is not a function');\n    }\n    if (arguments.length > 1) {\n      T = thisArg;\n    }\n    k = 0;\n    while (k < len) {\n      var kValue;\n      if (k in O) {\n        kValue = O[k];\n        callback.call(T, kValue, k, O);\n      }\n      k++;\n    }\n  };\n}\n\n// classList polyfill\n/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/\n;if(\"document\" in self&&!(\"classList\" in document.createElement(\"_\"))){(function(j){\"use strict\";if(!(\"Element\" in j)){return}var a=\"classList\",f=\"prototype\",m=j.Element[f],b=Object,k=String[f].trim||function(){return this.replace(/^\\s+|\\s+$/g,\"\")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p<o;p++){if(p in this&&this[p]===q){return p}}return -1},n=function(o,p){this.name=o;this.code=DOMException[o];this.message=p},g=function(p,o){if(o===\"\"){throw new n(\"SYNTAX_ERR\",\"An invalid or illegal string was specified\")}if(/\\s/.test(o)){throw new n(\"INVALID_CHARACTER_ERR\",\"String contains an invalid character\")}return c.call(p,o)},d=function(s){var r=k.call(s.getAttribute(\"class\")||\"\"),q=r?r.split(/\\s+/):[],p=0,o=q.length;for(;p<o;p++){this.push(q[p])}this._updateClassName=function(){s.setAttribute(\"class\",this.toString())}},e=d[f]=[],i=function(){return new d(this)};n[f]=Error[f];e.item=function(o){return this[o]||null};e.contains=function(o){o+=\"\";return g(this,o)!==-1};e.add=function(){var s=arguments,r=0,p=s.length,q,o=false;do{q=s[r]+\"\";if(g(this,q)===-1){this.push(q);o=true}}while(++r<p);if(o){this._updateClassName()}};e.remove=function(){var t=arguments,s=0,p=t.length,r,o=false;do{r=t[s]+\"\";var q=g(this,r);if(q!==-1){this.splice(q,1);o=true}}while(++s<p);if(o){this._updateClassName()}};e.toggle=function(p,q){p+=\"\";var o=this.contains(p),r=o?q!==true&&\"remove\":q!==false&&\"add\";if(r){this[r](p)}return !o};e.toString=function(){return this.join(\" \")};if(b.defineProperty){var l={get:i,enumerable:true,configurable:true};try{b.defineProperty(m,a,l)}catch(h){if(h.number===-2146823252){l.enumerable=false;b.defineProperty(m,a,l)}}}else{if(b[f].__defineGetter__){m.__defineGetter__(a,i)}}}(self))};\n\n\n\n// Global settings\nvar my = {};\nmy.settings = {};\nmy.prefix = 'transcript';\nmy.player = this;\n\n// Defaults\nvar defaults = {\n  autoscroll: true,\n  clickArea: 'text',\n  showTitle: true,\n  showTrackSelector: true,\n  followPlayerTrack: true,\n  stopScrollWhenInUse: true,\n};\n\n/*global my*/\nvar utils = (function (plugin) {\n  return {\n    secondsToTime: function (timeInSeconds) {\n      var hour = Math.floor(timeInSeconds / 3600);\n      var min = Math.floor(timeInSeconds % 3600 / 60);\n      var sec = Math.floor(timeInSeconds % 60);\n      sec = (sec < 10) ? '0' + sec : sec;\n      min = (hour > 0 && min < 10) ? '0' + min : min;\n      if (hour > 0) {\n        return hour + ':' + min + ':' + sec;\n      }\n      return min + ':' + sec;\n    },\n    localize: function (string) {\n      return string; // TODO: do something here;\n    },\n    createEl: function (elementName, classSuffix) {\n      classSuffix = classSuffix || '';\n      var el = document.createElement(elementName);\n      el.className = plugin.prefix + classSuffix;\n      return el;\n    },\n    extend: function(obj) {\n      var type = typeof obj;\n      if (!(type === 'function' || type === 'object' && !!obj)) {\n        return obj;\n      }\n      var source, prop;\n      for (var i = 1, length = arguments.length; i < length; i++) {\n        source = arguments[i];\n        for (prop in source) {\n          obj[prop] = source[prop];\n        }\n      }\n      return obj;\n    }\n  };\n}(my));\n\nvar eventEmitter = {\n  handlers_: [],\n  on: function on (object, eventtype, callback) {\n    if (typeof callback === 'function') {\n      this.handlers_.push([object, eventtype, callback]);\n    } else {\n      throw new TypeError('Callback is not a function.');\n    }\n  },\n  trigger: function trigger (object, eventtype) {\n    this.handlers_.forEach( function(h) {\n      if (h[0] === object &&\n          h[1] === eventtype) {\n            h[2].apply();\n      }\n    });\n  }\n};\n\nvar scrollerProto = function(plugin) {\n\n  var initHandlers = function (el) {\n    var self = this;\n    // The scroll event. We want to keep track of when the user is scrolling the transcript.\n    el.addEventListener('scroll', function () {\n      if (self.isAutoScrolling) {\n\n        // If isAutoScrolling was set to true, we can set it to false and then ignore this event.\n        // It wasn't the user.\n        self.isAutoScrolling = false; // event handled\n      } else {\n\n        // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class.\n        self.userIsScrolling = true;\n        el.classList.add('is-inuse');\n      }\n    });\n\n    // The mouseover event.\n    el.addEventListener('mouseenter', function () {\n      self.mouseIsOverTranscript = true;\n    });\n    el.addEventListener('mouseleave', function () {\n      self.mouseIsOverTranscript = false;\n\n      // Have a small delay before deciding user as done interacting.\n      setTimeout(function () {\n\n        // Make sure the user didn't move the pointer back in.\n        if (!self.mouseIsOverTranscript) {\n          self.userIsScrolling = false;\n          el.classList.remove('is-inuse');\n        }\n      }, 1000);\n    });\n  };\n\n  // Init instance variables\n  var init = function (element, plugin) {\n    this.element = element;\n    this.userIsScrolling = false;\n\n    //default to true in case user isn't using a mouse;\n    this.mouseIsOverTranscript = true;\n    this.isAutoScrolling = true;\n    initHandlers.call(this, this.element);\n    return this;\n  };\n\n  // Easing function for smoothness.\n  var easeOut = function (time, start, change, duration) {\n    return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2));\n  };\n\n  // Animate the scrolling.\n  var scrollTo = function (element, newPos, duration) {\n    var startTime = Date.now();\n    var startPos = element.scrollTop;\n    var self = this;\n\n    // Don't try to scroll beyond the limits. You won't get there and this will loop forever.\n    newPos = Math.max(0, newPos);\n    newPos = Math.min(element.scrollHeight - element.clientHeight, newPos);\n    var change = newPos - startPos;\n\n    // This inner function is called until the elements scrollTop reaches newPos.\n    var updateScroll = function () {\n      var now = Date.now();\n      var time = now - startTime;\n      self.isAutoScrolling = true;\n      element.scrollTop = easeOut(time, startPos, change, duration);\n      if (element.scrollTop !== newPos) {\n        requestAnimationFrame(updateScroll, element);\n      }\n    };\n    requestAnimationFrame(updateScroll, element);\n  };\n\n  // Scroll an element's parent so the element is brought into view.\n  var scrollToElement = function (element) {\n    if (this.canScroll()) {\n      var parent = element.parentElement;\n      var parentOffsetBottom = parent.offsetTop + parent.clientHeight;\n      var elementOffsetBottom = element.offsetTop + element.clientHeight;\n      var relTop = element.offsetTop - parent.offsetTop;\n      var relBottom = (element.offsetTop + element.clientHeight) - parent.offsetTop;\n      var newPos;\n\n      // If the top of the line is above the top of the parent view, were scrolling up,\n      // so we want to move the top of the element downwards to match the top of the parent.\n      if (relTop < parent.scrollTop) {\n        newPos = element.offsetTop - parent.offsetTop;\n\n      // If the bottom of the line is below the parent view, we're scrolling down, so we want the\n      // bottom edge of the line to move up to meet the bottom edge of the parent.\n      } else if (relBottom > (parent.scrollTop + parent.clientHeight)) {\n        newPos = elementOffsetBottom - parentOffsetBottom;\n      }\n\n      // Don't try to scroll if we haven't set a new position.  If we didn't\n      // set a new position the line is already in view (i.e. It's not above\n      // or below the view)\n      // And don't try to scroll when the element is already in position.\n      if (newPos !== undefined && parent.scrollTop !== newPos) {\n        scrollTo.call(this, parent, newPos, 400);\n      }\n    }\n  };\n\n  // Return whether the element is scrollable.\n  var canScroll = function () {\n    var el = this.element;\n    return el.scrollHeight > el.offsetHeight;\n  };\n\n  // Return whether the user is interacting with the transcript.\n  var inUse = function () {\n    return this.userIsScrolling;\n  };\n\n  return {\n    init: init,\n    to : scrollToElement,\n    canScroll : canScroll,\n    inUse : inUse\n  }\n}(my);\n\nvar scroller = function(element) {\n  return Object.create(scrollerProto).init(element);\n};\n\n\n/*global my*/\nvar trackList = function (plugin) {\n  var activeTrack;\n  return {\n    get: function () {\n      var validTracks = [];\n      var i, track;\n      my.tracks = my.player.textTracks();\n      for (i = 0; i < my.tracks.length; i++) {\n        track = my.tracks[i];\n        if (track.kind === 'captions' || track.kind === 'subtitles') {\n          validTracks.push(track);\n        }\n      }\n      return validTracks;\n    },\n    active: function (tracks) {\n      var i, track;\n      for (i = 0; i < my.tracks.length; i++) {\n        track = my.tracks[i];\n        if (track.mode === 'showing') {\n          activeTrack = track;\n          return track;\n        }\n      }\n      // fallback to first track\n      return activeTrack || tracks[0];\n    },\n  };\n}(my);\n\n/*globals utils, eventEmitter, my, scrollable*/\n\nvar widget = function (plugin) {\n  var my = {};\n  my.element = {};\n  my.body = {};\n  var on = function (event, callback) {\n    eventEmitter.on(this, event, callback);\n  };\n  var trigger = function (event) {\n    eventEmitter.trigger(this, event);\n  };\n  var createTitle = function () {\n    var header = utils.createEl('header', '-header');\n    header.textContent = utils.localize('Transcript');\n    return header;\n  };\n  var createSelector = function (){\n    var selector = utils.createEl('select', '-selector');\n      plugin.validTracks.forEach(function (track, i) {\n      var option = document.createElement('option');\n      option.value = i;\n      option.textContent = track.label + ' (' + track.language + ')';\n      selector.appendChild(option);\n    });\n    selector.addEventListener('change', function (e) {\n      setTrack(document.querySelector('#' + plugin.prefix + '-' + plugin.player.id() + ' option:checked').value);\n      trigger('trackchanged');\n    });\n    return selector;\n  };\n  var clickToSeekHandler = function (event) {\n    var clickedClasses = event.target.classList;\n    var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin');\n    if (clickedTime !== undefined && clickedTime !== null) { // can be zero\n      if ((plugin.settings.clickArea === 'line') || // clickArea: 'line' activates on all elements\n        (plugin.settings.clickArea === 'timestamp' && clickedClasses.contains(plugin.prefix + '-timestamp')) ||\n        (plugin.settings.clickArea === 'text' && clickedClasses.contains(plugin.prefix + '-text'))) {\n        plugin.player.currentTime(clickedTime);\n      }\n    }\n  };\n  var createLine = function (cue) {\n    var line = utils.createEl('div', '-line');\n    var timestamp = utils.createEl('span', '-timestamp');\n    var text = utils.createEl('span', '-text');\n    line.setAttribute('data-begin', cue.startTime);\n    timestamp.textContent = utils.secondsToTime(cue.startTime);\n    text.innerHTML = cue.text;\n    line.appendChild(timestamp);\n    line.appendChild(text);\n    return line;\n  };\n  var createTranscriptBody = function (track) {\n    if (typeof track !== 'object') {\n      track = plugin.player.textTracks()[track];\n    }\n    var body = utils.createEl('div', '-body');\n    var line, i;\n    var fragment = document.createDocumentFragment();\n    // activeCues returns null when the track isn't loaded (for now?)\n    if (!track.activeCues) {\n      // If cues aren't loaded, set mode to hidden, wait, and try again.\n      // But don't hide an active track. In that case, just wait and try again.\n      if (track.mode !== 'showing') {\n        track.mode = 'hidden';\n      }\n      window.setTimeout(function() {\n        createTranscriptBody(track);\n      }, 100);\n    } else {\n      var cues = track.cues;\n      for (i = 0; i < cues.length; i++) {\n        line = createLine(cues[i]);\n        fragment.appendChild(line);\n      }\n      body.innerHTML = '';\n      body.appendChild(fragment);\n      body.setAttribute('lang', track.language);\n      body.scroll = scroller(body);\n      body.addEventListener('click', clickToSeekHandler);\n      my.element.replaceChild(body, my.body);\n      my.body = body;\n    }\n\n  };\n  var create = function () {\n    var el = document.createElement('div');\n    my.element = el;\n    el.setAttribute('id', plugin.prefix + '-' + plugin.player.id());\n    if (plugin.settings.showTitle) {\n      var title = createTitle();\n      el.appendChild(title);\n    }\n    if (plugin.settings.showTrackSelector) {\n      var selector = createSelector();\n      el.appendChild(selector);\n    }\n    my.body = utils.createEl('div', '-body');\n    el.appendChild(my.body);\n    setTrack(plugin.currentTrack);\n    return this;\n  };\n  var setTrack = function (track, trackCreated) {\n    createTranscriptBody(track, trackCreated);\n  };\n  var setCue = function (time) {\n    var active, i, line, begin, end;\n    var lines = my.body.children;\n    for (i = 0; i < lines.length; i++) {\n      line = lines[i];\n      begin = line.getAttribute('data-begin');\n      if (i < lines.length - 1) {\n        end = lines[i + 1].getAttribute('data-begin');\n      } else {\n        end = plugin.player.duration() || Infinity;\n      }\n      if (time > begin && time < end) {\n        if (!line.classList.contains('is-active')) { // don't update if it hasn't changed\n          line.classList.add('is-active');\n          if (plugin.settings.autoscroll && !(plugin.settings.stopScrollWhenInUse && my.body.scroll.inUse())) {\n              my.body.scroll.to(line);\n          }\n        }\n      } else {\n        line.classList.remove('is-active');\n      }\n    }\n  };\n  var el = function () {\n    return my.element;\n  };\n  return {\n    create: create,\n    setTrack: setTrack,\n    setCue: setCue,\n    el : el,\n    on: on,\n    trigger: trigger,\n  };\n\n}(my);\n\nvar transcript = function (options) {\n  my.player = this;\n  my.validTracks = trackList.get();\n  my.currentTrack = trackList.active(my.validTracks);\n  my.settings = videojs.mergeOptions(defaults, options);\n  my.widget = widget.create();\n  var timeUpdate = function () {\n    my.widget.setCue(my.player.currentTime());\n  };\n  var updateTrack = function () {\n    my.currentTrack = trackList.active(my.validTracks);\n    my.widget.setTrack(my.currentTrack);\n  };\n  if (my.validTracks.length > 0) {\n    updateTrack();\n    my.player.on('timeupdate', timeUpdate);\n    if (my.settings.followPlayerTrack) {\n      my.player.on('captionstrackchange', updateTrack);\n      my.player.on('subtitlestrackchange', updateTrack);\n    }\n  } else {\n    throw new Error('videojs-transcript: No tracks found!');\n  }\n  return {\n    el: function () {\n      return my.widget.el();\n    },\n    setTrack: my.widget.setTrack\n  };\n};\nvideojs.plugin('transcript', transcript);\n\n//-----------------------\nvar video = videojs(@@AUTOID@@ ).ready(function(){\n      // Set up any options.\n      var options = {\n        showTitle: false,\n        showTrackSelector: false,\n      };\n\n      // Initialize the plugin.\n      var transcript = this.transcript(options);\n\n      // Then attach the widget to the page.\n      var transcriptContainer = document.querySelector('#' + @@AUTOID@@ + '_transcript');\n      transcriptContainer.appendChild(transcript.el()); \n    }); ","style":"/* Audio: Remove big play button (leave only the button in controls). */\n.video-js.vjs-audio .vjs-big-play-button {\n  display: none;\n}\n/* Audio: Make the controlbar visible by default */\n.video-js.vjs-audio .vjs-control-bar {\n  display: -webkit-box;\n  display: -webkit-flex;\n  display: -ms-flexbox;\n  display: flex;\n}\n/* Make player height minimum to the controls height so when we hide video/poster area the controls are displayed correctly. */\n.video-js.vjs-audio {\n  min-height: 3em;\n}\n\n.videojs_transcript {\n  width: 600px;\n  font-family: Arial, sans-serif;\n  overflow-x: scroll;\n  border: 1px solid #111;\n height: 265px;\n}\n.transcript-header {\n  height: 19px;\n  padding: 2px;\n  font-weight: bold;\n}\n.transcript-selector {\n  height: 25px;\n}\n.transcript-body {\n  width: 600px;\n  overflow-y: scroll;\n  background-color: #e7e7e7;\n  height: 250px;\n}\n\n.transcript-line {\n  position: relative;\n  padding: 5px;\n  cursor: pointer;\n  line-height: 1.3;\n}\n\n.transcript-line:nth-child(odd) {\n  background-color: #f5f5f5;\n}\n\n\n.transcript-timestamp {\n  position: absolute;\n  display: inline-block;\n  color: #333;\n  width: 50px;\n}\n\n.transcript-text {\n  display: block;\n  margin-left: 50px;\n}\n\n.transcript-line:hover,\n.transcript-line:hover .transcript-timestamp,\n.transcript-line:hover .transcript-text {\n  background-color: #777;\n  color: #e7e7e7;\n}\n\n.transcript-line.is-active,\n.transcript-line.is-active .transcript-timestamp,\n.transcript-line.is-active .transcript-text {\n  background-color: #555;\n  color: #e7e7e7;\n}\n","dataset":"","datasetvars":"","alternate":"<audio id=\"@@AUTOID@@\"  style=\"background-color: #CCC;\" class=\"nomediaplugin\" controls preload=\"auto\" width=\"320\" height=\"30\" >\n<source src=\"@@VIDEOURL@@\" type=\"@@AUTOMIME@@\" />\n</audio>","alternateend":""}