Skip to content

Instantly share code, notes, and snippets.

@mindon
Last active July 25, 2017 05:06
Show Gist options
  • Select an option

  • Save mindon/4eec55058e191e5e2306053f2cd28328 to your computer and use it in GitHub Desktop.

Select an option

Save mindon/4eec55058e191e5e2306053f2cd28328 to your computer and use it in GitHub Desktop.
Audio/Video Track Feature
// track feature
// author: Mindon <mindon@gmail.com>
// updated: July 13, 2017. Shenzhen, Guangdong, China P.R.
// track.bind(audio-element, cues)
// track.bind(audio-element, cues, track-bubble-container-element)
// cues-array = track.get(cues-data or cues-dataUrl, cb) // cb = (cues)=>{}
// supports VTT and lyric format
var track = (function(){
// ------ track namespace begin
var nxp = /^\s$/;
var dur = /^((?:\d{2}:)?\d{2}:\d{2}(?:[,.]\d{1,3})?)\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}(?:[,.]\d{1,3})?)(.*)/;
var sets = /\w+:[^\s]+/g;
function t2ms(t) {
var v = t.split(":"), i = 0;
var h = v.length == 2 ? 0 : parseInt(v[i++], 10);
return h*3600*1000 + parseInt(v[i++], 10)*60*1000 + Math.round(parseFloat(v[i++].replace(",", ".").replace(/^0+/, "")||'0') * 1000);
}
var voiceTag = /<v\s+([^>]+)\s*>/gi;
var classTag = /<c\.([^>\s]+)\s*>/gi, classTagEnd = /<\/c>/gi;
var delayTag = /<((\d{2}:)?\d{2}:\d{2}[,.]\d{1,3})[^>]*>/g;
function sortLines(a, b) {return a[0] < b[0] ? -1: (a[0] > b[0] ? 1: 0)};
function sortCues(a, b) {return a.from < b.from ? -1: (a.from > b.from ? 1: 0)};
function cueSettings(cftext) {
var ms = cftext.match(sets);
if(!ms) return null;
var settings = {};
for(var j=0; j<ms.length; j++) {
var n = ms[j].indexOf(":");
settings[ms[j].substr(0,n)] = ms[j].substr(n+1);
}
return settings;
}
function cues(body, kind) {
var c = [], cid = 0; lines = body.split(/\r?\n/);
for(var i=0, imax=lines.length; i<imax; i++) {
var line = lines[i];
if(!line || nxp.test(line)) {
continue;
}
var mr = line.match(dur);
if(!mr) {
continue;
}
var cue = {id: ++cid};
var last = lines[i-1];
if(last && !nxp.test(last)) {
cue.id = last;
}
// cue.range = {from: mr[1], to: mr[2]};
cue.from = t2ms(mr[1]);
cue.to = t2ms(mr[2]);
var settings = cueSettings(mr[3]);
if(settings) {
cue.settings = settings;
}
var texts = [];
i++; line = lines[i];
while(line && !nxp.test(line)) {
texts.push(line);
i++; line = lines[i];
}
cue.text = texts.join("\n");
var dtm = cue.text.match(delayTag);
if(dtm) {
var k = 0, from = cue.from, waits = [], wet = [];
cue.text = cue.text.replace(delayTag, function(s){
var t = t2ms(s.substr(1, s.length-2)) - from;
waits.push(t);
wet.push('</span>');
return '<span id="_t__'+from+'_' +t+'" class="' +delayClass +'">';
}) + wet.join('');
cue.waits = waits;
}
c.push(cue);
}
c.sort(sortCues);
return c;
}
var lrc = /^((?:\[\d{2}:\d{2}(?:(?:\.\d+)?)\])+)(.*)\s*$/;
function lyric(body) {
var c = [], cid = 0; lines = body.split(/\r?\n/), darr = [];
for(var i=0, imax=lines.length; i<imax; i++) {
var line = lines[i];
if(!line || nxp.test(line)) {
continue;
}
var mr = line.match(lrc);
var tms = mr[1];
var text = mr[2] || '';
while(i<imax && !lrc.test(lines[i+1])) {
i++;
if(lines[i] && !nxp.test(lines[i])) {
text += "\n" + lines[i];
}
}
var tarr = tms.replace(/\s*\[|\]\s*$/g, '').split(']');
for(var j=0, jmax=tarr.length; j<jmax; j++) {
darr.push([tarr[j], text.trim()]);
}
}
darr.sort(sortLines);
for(var i=0, imax=darr.length; i<imax; i++) {
var d = darr[i];
var from = t2ms(d[0]);
var to = i<imax-1 ? t2ms(darr[i+1][0]) : from + 10000;
var cue = {id: ++cid, from: from, to: to, text: d[1]};
c.push(cue);
}
return c;
}
var bubbleClass='-aTrAcK-bubble', bubbleOnClass = '-aTrAcK-bubble-on';
var delayClass='-aTrAcK-delay', delayOnClass = '-aTrAcK-delay-on';
// /(A|align):(start|middle|end)/i
// /(S|size):(100|\d{1,2})%/i
// /(T|position):(100|\d{1,2})%/i
// /(D|vertical):(vertical-lr|vertical|lr|rl)/i
// /(L|line):(-?[0-9]{0,3})(%?)/i;
// /(bubble):([\w-\.]+)/i;
function bubble(prefix, cue) {
var tob = document.createElement("div");
tob.id = prefix + cue.id;
var classList = [bubbleClass]
var style = tob.style;
if(cue.settings) {
var s = cue.settings.L || cue.settings.line;
if(s) {
var ng = s.charAt(0)=='-';
if(ng) s = s.substr(1);
style[ng?'bottom':'top'] = s.indexOf('%') > 0 ? s : s +'em';
}
s = cue.settings.T || cue.settings.position;
var p = '';
if(s) {
var ng = s.charAt(0)=='-';
p = s.indexOf('%') > 0 ? s : s +'%';
if(ng) s = s.substr(1);
style[ng?'right':'left'] = ng ? p.substr(1): p;
if(ng) {
style.textAlign = 'right';
}
}
s = cue.settings.S || cue.settings.size;
if(s) {
var v = parseInt(s, 10);
if(p) {
if(p.charAt(0)=='-') {
style.left = (100 - v + parseInt(p, 10)) +'%';
} else {
style.right = (100 - v - parseInt(p, 10)) +'%';
}
} else {
style.left = (100 - v/2) +'%';
style.right = (100 - v/2) +'%';
}
}
s = cue.settings.A || cue.settings.align;
if(s) {
style.textAlign = /end/i.test(s)?'right':(/start/i.test(s)?'left':'center');
}
s = cue.settings.D || cue.settings.vertical;
if(s) {
var rl = /rl/i.test(s);
if(rl) {
style.unicodeBidi = 'bidi-override';
style.direction = 'rtl';
}
}
if(cue.settings.class) {
classList.push(cue.settings.class);
}
} else {
style.bottom = '.5em';
}
var text = cue.text;
if(cue.waits && cue.waits.length > 0) {
text = text.replace(/(<span id=")(_t__)/g, '$1'+prefix+'$2');
}
text = text.replace(voiceTag, '<span class="_track_voice">[$1]</span> ');
if(classTagEnd.test(text)) {
text = text.replace(classTag, stylize);
text = text.replace(classTagEnd, '</span>');
}
tob.className = classList.join(' ');
tob.innerHTML = text.replace("\n", "<br/>");
return tob;
}
function stylize(s, c) {
return '<span class="' +c.replace(/\./g, ' ') +'">';
}
var tsn = 1;
function _prefix(){return ('_aTrAcK_' +tsn++) +"_bubbles_"}
function prepare(cues, doc, prefix, options) {
var tp = [], tpis = {}, tpie = {};
if(!prefix) prefix = _prefix();
for(var i=0, imax=cues.length; i<imax; i++) {
var cue = cues[i];
var tpiss = tpis[cue.from];
if(!tpiss) {
tpiss = [i];
tpis[cue.from] = tpiss;
} else {
tpiss.push(i);
}
if(tp.indexOf(cue.from) < 0) {
tp.push(cue.from);
}
var tpies = tpie[cue.to];
if(!tpies) {
tpies = [i];
tpie[cue.to] = tpies;
} else {
tpies.push(i);
}
if(tp.indexOf(cue.to) < 0) {
tp.push(cue.to);
}
if(!options || !options.silence) {
var tob = bubble(prefix, cue);
tob.style.transitionDuration = (0 +(Math.floor((cue.to - cue.from)/1000)||1)) +'s';
if(options && options.style) {
tob.classList.add(options.style);
}
doc.appendChild(tob);
tob = null;
}
}
return {points: tp.sort((a,b)=>a>b?1:(a<b?-1:0)), starts: tpis, stops: tpie, prefix: prefix}
}
var lasts = {};
function clearDelay(tob) {
var tags = tob.getElementsByTagName('span');
for(var k=0; k<tags.length; k++) {
if(tags[k].classList.contains(delayOnClass)) {
tags[k].classList.remove(delayOnClass);
}
}
}
function hideCue(cue, stage, prefix, current, ani) {
var tob = document.getElementById(prefix + cue.id);
if(!tob) {
return
}
if(ani) ani(tob, cue, current, false);
stage.doc.appendChild(tob);
tob.classList.remove(bubbleOnClass);
clearDelay(tob);
}
function showCue(cue, stage, prefix, current, ani) {
var tob = stage.doc.getElementById(prefix + cue.id);
if(tob && !tob.classList.contains(bubbleOnClass)) {
tob.classList.add(bubbleOnClass);
// show(tob, cues[j]);
stage.view.appendChild(tob);
if(ani) {
tob.style.bottom = "auto";
tob.style.top = "1em";
setTimeout(()=>ani(tob, cue, current, true),50);
}
}
}
function display(prefix, idxes, cues, current, stage, direction, bubbleAni) {
var doc = stage.doc || document, view = stage.view;
var last = lasts[prefix]||[];
if(!idxes) idxes = [];
idxes = unique(idxes);
var tmp = idxes.slice(), keeps = [];
for(var i=0; i<last.length; i++) {
var j = last[i], n = idxes.indexOf(j);
if(n < 0) {
if((direction > 0 && cues[j].to < current) ||
(direction < 0 && cues[j].from > current)) {
hideCue(cues[j], stage, prefix, current, bubbleAni);
} else {
keeps.push(j);
}
} else {
keeps.push(idxes.splice(n, 1)[0]);
}
}
for(var i=0; i<idxes.length; i++) {
var j = idxes[i];
showCue(cues[j], stage, prefix, current, bubbleAni);
}
// sub delays tag
for(var i=0; i<keeps.length; i++) {
var j = keeps[i], waits = cues[j].waits;
if(tmp.indexOf(j)<0) {
tmp.push(j);
}
if(!waits) continue;
var from = cues[j].from, offset = current - from;
for(var k=0; k<waits.length; k++) {
var dt = document.getElementById(prefix +'_t__' + from +'_' +waits[k]);
if(!dt || !dt.className) continue;
dt.className = waits[k] > offset ? delayClass:delayClass +' ' + delayOnClass;
}
}
lasts[prefix] = tmp;
}
function unique(d) {
// ES6 return [...new Set(d)];
var r = [], j = 0;
for(var k=0, kmax=d.length; k<kmax; k++) {
if(r.indexOf(d[k]) < 0) {
r[j++]=d[k];
}
}
return r;
}
var _stageStyleAttr = 'stage-style';
var _dotBubbleOn = '.' +bubbleOnClass;
var _styleBody = `.-aTrAcK-stage {
position: relative; min-height: 180px;
overflow: hidden;
} ` +' .' +bubbleClass +` {
position: absolute; z-index: 999;
font-size: 12px; text-align: center;
color: #fff; display: none;
width: 100%;
transition: all 3s cubic-bezier(.97,.25,.61,.9);
transform: translate3d(0,0,0);
opacity: 1.0;
} ` +_dotBubbleOn +` {
display: block;
} ` +_dotBubbleOn +' span.' +delayClass+` {
visibility: hidden;
} ` +_dotBubbleOn +' span.' + delayOnClass +` {
visibility: visible;
} .-aTrAcK-fill{position: absolute; left: 0; right: 0; top: 0; bottom: 0;}`;
// http://cubic-bezier.com/#.97,.25,.61,.9
function _style() {
if(!_styleBody) return;
var el= document.createElement('style');
el.type= "text/css";
if(el.styleSheet) el.styleSheet.cssText= _styleBody;
else el.appendChild(document.createTextNode(_styleBody));
document.getElementsByTagName('head')[0].appendChild(el);
_styleBody = ''; // clear
_style = undefined;
}
var vttRxp = /\d{2}:[.:\d]+\s+-->\s+\d{2}:\d{2}/;
var lyricRxp = /\n\[\d{2}:\d{2}[:\.\d]*\]/;
var dataUrl = /^(((http[s]:)?\/\/)|[.]{0,2}\/)\w+.+/i;
function get(d, cb) {
var c = [];
if(d.indexOf('\n') > -1) {
if(vttRxp.test(d)) {
c = cues(d);
} else if(lyricRxp.test(d)) {
c = lyric(d);
} else {
throw new Error('Invalid cues data :' + d);
}
} else { // if(dataUrl.test(d)) {
var xhr = new XMLHttpRequest();
xhr.open('GET', d, true);
xhr.onload = function(){
var c = get(xhr.responseText, cb);
if(cb && c!==false) cb(c);
};
xhr.send(null);
return false;
}
return c;
}
function bind(aob, dCues, options) {
if(_style) _style();
if(!aob) { // || aob.tracked || (cob && cob.tracked)
return;
}
if(!options) options = {};
var cob = options.container;
if(typeof dCues == 'string') {
dCues = get(dCues, function(c){
bind(aob, c, options);
});
if(dCues === false) {
return; //
}
}
var cues = dCues;
var p = cob||aob.parentElement, className = '', playground;
if(cob) {
playground = cob.playground;
cob.tracked = true;
} else {
playground = aob.playground;
aob.tracked = true;
}
var prefix = _prefix();
if(playground) {
var series = playground.series;
var ps = prepare(cues, playground.stage.doc, prefix, options);
var tp = ps.points, imax = tp.length, tmax = tp[imax-1] +10000;
series.push({cues: cues, ps: ps, dur: tmax, ani: options.ani, style: options.style});
return;
}
var div = document.createElement("div"), style = '', height = 0, videoInside = false;
if(cob) {
if(cob && cob.style.positon == 'static') cob.style.positon = 'relative';
div.classList.add("-aTrAcK-fill");
className = cob.getAttribute(_stageStyleAttr);
} else {
if(aob.offsetWidth > 0) {
div.style.width = aob.offsetWidth +'px';
}
if(aob.tagName == 'VIDEO') {
height = parseInt(aob.offsetHeight, 10) +20;
div.style.minHeight = height +'px';
div.style.height = height+'px';
videoInside = true;
}
className = aob.getAttribute(_stageStyleAttr);
}
div.classList.add('-aTrAcK-stage');
if(className) {
div.classList.add(className);
}
if(cob) {
cob.appendChild(div);
} else {
p.insertBefore(div, aob);
}
if(videoInside) { // video
var subDiv = div.cloneNode();
subDiv.style.height = (height - 32) +'px';
subDiv.style.minHeight = (height - 32) +'px';
subDiv.style.zIndex = 1;
div.className += ' -aTrAcK-video';
aob.style.position = 'absolute';
aob.style.zIndex = 0;
aob.style.left = 0;
aob.style.top = 0;
div.appendChild(subDiv);
div.appendChild(aob);
div= subDiv;
}
var playground = {stage: {view: div}, series: []};
if(cob) {
cob.playground = playground;
} else {
aob.playground = playground;
}
var doc = document.createDocumentFragment();
playground.stage.doc = doc;
var ps = prepare(cues, doc, prefix, options);
var tp = ps.points, imax = tp.length, tmax = tp[imax-1] +10000, lastMoment = 0;
playground.series.push({cues: cues, ps: ps, dur: tmax, ani: options.ani, style: options.style});
playground.dur = tmax;
aob.addEventListener("durationchange", function(){
playground.dur = Math.round(this.duration * 1000);
window.dispatchEvent(new CustomEvent("audio-ready", {detail: {prefix: prefix, audio: aob, tracker: cob||aob}}));
});
aob.addEventListener("play", function(){
var series = playground.series, dur = playground.dur;
for(var s=0, smax=series.length; s<smax; s++) {
series[s].dur = dur;
}
});
aob.addEventListener("paused", function(){
window.dispatchEvent(new CustomEvent("audio-paused", {detail: {prefix: prefix, current: ms2t(playground.current), moment: playground.current}}));
});
aob.addEventListener("ended", function(){
var last = lasts[prefix]||[];
for(var i=0; i<last.length; i++) {
var j = last[i];
if(cues[j] && !cues[j].fin) hideCue(cues[j], playground.stage, prefix, playground.current, options.ani);
}
window.dispatchEvent(new CustomEvent("audio-end", {detail: {prefix: prefix, duration: playground.dur}}));
});
aob.addEventListener("timeupdate", function(event){
var t = Math.round(this.currentTime * 1000);
var direction = t - lastMoment;
playground.current = t;
playground.direction = direction;
lastMoment = t;
var series = playground.series;
for(var s=0, smax=series.length; s<smax; s++) {
var cps = series[s];
play(playground, cps, t, direction, cps.ani);
}
window.dispatchEvent(new CustomEvent("audio-playing", {detail: {prefix: prefix, current: ms2t(t), moment: t}}));
});
}
function play(playground, cps, t, direction, bubbleAni) {
var stage = playground.stage, cues = cps.cues, ps = cps.ps, prefix = ps.prefix;
var tp = ps.points, tb = ps.starts, te = ps.stops, imax = tp.length, tmax = cps.dur;
if(t <= tp[imax -1]) {
var ones = [], i = 0, t2i = Math.floor(imax/2), nmax = imax;
if(t >= tp[t2i]) {
i = t2i;
} else {
nmax = t2i +1;
}
var wq = [], xq = [];
for(; i<nmax; i++) {
var ti = tp[i];
if(t >=ti && t < tp[i+1] && tb[ti]) {
ones = ones.concat(tb[ti]);
} else if( ti >= t) {
if(tb[ti] && tb[ti].length > 0) wq = wq.concat(tb[ti]);
if(te[ti] && te[ti].length > 0) xq = xq.concat(te[ti]);
}
}
for(var k in xq) {
var ki = xq[k];
if(wq.indexOf(ki) > -1) continue;
if(ones.indexOf(ki) < 0) {
ones.push(ki);
}
}
display(prefix, ones, cues, t, stage, direction, bubbleAni);
} else if( t < tmax) {
ti = tp[imax -1];
display(prefix, [], cues, t, stage, direction, bubbleAni);
cps.dur = t;
}
}
function join(tracker, cue, dur, idx) {
var playground = tracker.playground;
var series = playground.series;
if(idx === undefined) idx = series.length -1;
var s = series[idx];
if(!s||!s.cues) return;
if(typeof cue == 'string') {
cue = {text: cue};
}
if(dur === undefined) {
dur = 3000; // default 3 seconds
}
cue.id = s.cues.length +1;
if(cue.from === undefined) {
cue.from = (playground.current||0) + 500;
} else if(typeof cue.from == 'string') {
cue.from = t2ms(cue.from);
}
cue.to = cue.from + dur;
s.cues.push(cue);
var prefix = s.ps.prefix, doc = playground.stage.doc;
var ps = prepare(s.cues, doc, prefix, {silence:true, style: s.style});
var tp = ps.points, imax = tp.length, tmax = tp[imax-1] +1000;
s.ps = ps;
if(s.dur < tmax) {
s.dur = tmax;
}
var tob = bubble(prefix, cue);
if(s.style) {
tob.classList.add(s.style);
}
tob.style.transitionDuration = (0 +(Math.floor((cue.to - cue.from)/1000)||1)) +'s';
// tob.style.left = Math.round(100 * ((playground.current/1000) %5)/7) +'%';
doc.appendChild(tob);
return tob;
}
function ms2t(ms) {
var m = ms%1000, s = Math.floor((ms-m)/1000), sec = s%60, minutes = Math.floor((s - sec)%3600/60), hours = Math.floor((s - sec - minutes*60)/3600);
return (hours>0?(hours<10?'0'+hours:hours)+':':'') + (minutes<10?'0'+minutes:minutes) +':' +
(sec<10?'0'+sec:sec) + (m>0?'.'+(m<100?'00':(m<10?'0':''))+m: '');
}
function raw(tracker, idx) {
var playground = tracker.playground;
var series = playground.series;
var s = series[idx];
if(!s||!s.cues) return false;
var d = ["WEBVTT"];
for(var i=0, cues = s.cues, imax=cues.length; i<imax; i++) {
var cue = cues[i], c = ms2t(cue.from) +' --> ' + ms2t(cue.to);
if(cue.settings) {
for(var k in cue.settings) {
c += ' ' +k +':'+ cue.settings[k];
}
}
c += '\n' +cue.text;
d.push(c);
}
return d.join('\n\n');
}
// export
return {get: get, bind: bind, join: join, raw: raw, conf: cueSettings};
// ------ track namespace end
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment