blob: f3723e06fdb8e90bcdebe4bedd8bfb00dc1f0c1c [file] [edit]
/* JavaScript Renderer for WebVTT tracks
Generates GPAC Scene Graph (SVG, BIFS) to display WebVTT cues */
alert("WebVTT Rendered Loaded");
var DEBUG = false;
var rect = document.documentElement.getRectPresentationTrait("viewBox");
var width = rect.width;
var height = rect.height;
//filter-assignable variables - if changing names, also do it in filters/dec_webvtt.c
var xOffset = 0;
var yOffset = 0; //positive towards top
var fontSize = 20;
var textColor = "white";
var fontFamily = "SANS";
var lineSpaceFactor = 1.0;
//end of filter-assignable variables
var nbCues = 0;
var cueArea = document.createElement("g");
document.documentElement.appendChild(cueArea);
function reportMessage(msg) {
if (DEBUG) {
alert(msg);
}
}
function createTextArea(settingsObj) {
var t = document.createElement("textArea");
let x = xOffset + settingsObj.xPosition;
let y = -yOffset - settingsObj.linePosition;
t.setAttribute("x", x);
t.setAttribute("y", y);
var w, h;
t.setAttribute("display-align", "after");
h=height;
w = settingsObj.size;
t.setAttribute("height", h);
t.setAttribute("width", w);
reportMessage("Creating textArea size " + x + 'x' + y + '@' + w + "x" + h);
t.setAttribute("fill", textColor);
t.setAttribute("font-size", fontSize);
t.setAttribute("font-family", fontFamily);
t.setAttribute("text-align", settingsObj.align);
t.setAttribute("line-increment", lineSpaceFactor*fontSize);
/* if (w < width) {
var r = document.createElement("rect");
r.setAttribute("height", h);
r.setAttribute("width", w);
r.setAttribute("x", xOffset+settingsObj.xPosition);
r.setAttribute("y", yOffset-5);
r.setAttribute("fill", "cyan");
cueArea.appendChild(r);
}
*/
cueArea.appendChild(t);
reportMessage("textArea created: "+t);
return t;
}
function printObj(o) {
for (var p in o) {
if (o.hasOwnProperty(p)) {
alert(""+p+": "+o[p]);
}
}
}
function addSpan(parent, cuePart) {
reportMessage("adding tspan to "+parent+" with cuePart: "+ cuePart);
var i;
var span = document.createElement('tspan');
//alert('adding span for '+parent+': '+cuePart+' token:'+cuePart.token);
if (cuePart.token === undefined) {
span.textContent = cuePart;
} else {
if (cuePart.token === 'i') {
span.setAttribute("font-style", "italic");
} else if (cuePart.token === 'u') {
span.setAttribute("text-decoration", "underline");
} else if (cuePart.token === 'b') {
span.setAttribute("font-weight", "bold");
}
for (i in cuePart.children) {
addSpan(span, cuePart.children[i]);
}
}
parent.appendChild(span);
reportMessage("tspan created "+span);
return span;
}
function parseCueSettings(cueSettings){
reportMessage("Parsing cue settings: "+cueSettings);
var obj = {};
var settingsArray = cueSettings.split(/\s/g);
//.filter(function(set) { return set && !!set.length; });
// Convert the Array into an object
var compositeCueSettings = {};
for (var settingsIndex in settingsArray) {
var nvArray = settingsArray[settingsIndex].split(":");
compositeCueSettings[nvArray[0]] = nvArray[1];
}
// Compute real values
if (typeof compositeCueSettings.line != 'undefined') {
if (compositeCueSettings.line.match(/\%/)) {
obj.linePosition = parseFloat(compositeCueSettings.line.replace(/\%/ig,""));
if (isNaN(obj.linePosition)) {
obj.linePosition = (nbCues + 0.5) * lineSpaceFactor*fontSize;
} else {
if (obj.linePosition<0) obj.linePosition=100;
else if (obj.linePosition>100) obj.linePosition=100;
//convert to vertical translation
let h = height - 3*lineSpaceFactor*fontSize/2;
obj.linePosition = h - obj.linePosition*h/100 + lineSpaceFactor*fontSize/2;
}
} else {
obj.linePosition = parseFloat(compositeCueSettings.line);
if (isNaN(obj.linePosition)) {
obj.linePosition = (nbCues + 0.5) * lineSpaceFactor*fontSize;
} else {
let h = (height - 2*fontSize) / lineSpaceFactor*fontSize;
obj.linePosition *= lineSpaceFactor*fontSize;
if (obj.linePosition >= 0) {
obj.linePosition = height - lineSpaceFactor*fontSize - obj.linePosition;
} else {
obj.linePosition = -obj.linePosition - lineSpaceFactor*fontSize/2;
}
}
}
} else {
obj.linePosition = (nbCues + 0.5) * lineSpaceFactor*fontSize;
}
let position_auto=true;
if (typeof compositeCueSettings.position != 'undefined') {
obj.xPosition = parseFloat(compositeCueSettings.position.replace(/\%/ig,""));
if (isNaN(obj.xPosition)) {
obj.xPosition = 0;
} else {
obj.xPosition *= width/100;
position_auto = false;
}
} else {
obj.xPosition = 0;
}
if (typeof compositeCueSettings.size != 'undefined') {
obj.size = parseFloat(compositeCueSettings.size.replace(/\%/ig,""));
if (isNaN(obj.size)) {
obj.size = 100;
}
} else {
obj.size = 100;
}
if (obj.size==100) position_auto = false;
obj.size *= width/100;
//default to center
if (position_auto) {
obj.xPosition = width/2 - obj.size/2;
}
if (typeof compositeCueSettings.align != 'undefined') {
if (compositeCueSettings.align === "left") {
obj.align = "start";
if (position_auto) {
obj.xPosition = 0;
}
} else if (compositeCueSettings.align === "start") {
obj.align = "start";
} else if (compositeCueSettings.align === "right") {
obj.align = "end";
if (position_auto) {
obj.xPosition = width - obj.size;
}
} else if (compositeCueSettings.align === "end") {
obj.align = "end";
/*} else if (compositeCueSettings.align === "middle") {
obj.align = "center";
*/
} else {
obj.align = "center";
}
} else {
obj.align = "center";
}
reportMessage("cue settings parsed: " + JSON.stringify(obj));
return obj;
}
function removeCues() {
cueArea.textContent = '';
nbCues = 0;
}
function addCue(id, start, end, settings, payload) {
reportMessage("adding cue: "+id+","+start+","+end+","+settings+","+payload);
var lines = payload.split('\n');
var settingObj = parseCueSettings(settings);
var tA = createTextArea(settingObj);
for (var lineIndex in lines) {
var parsedCue = parseCue(lines[lineIndex]);
for (var cuePartIndex in parsedCue) {
addSpan(tA, parsedCue[cuePartIndex]);
}
tA.appendChild(document.createElement('tbreak'));
}
nbCues++;
reportMessage("cue added: "+tA);
}
/* Code adapted from https://raw.github.com/cgiffard/Captionator/master/js/captionator.js */
var SRTChunkTimestampParser = /(\d{2})?:?(\d{2}):(\d{2})[\.\,](\d+)/;
function parseCue(cueText) {
reportMessage("Parsing cue text: "+cueText);
var cueStructure = new Array,
cueSplit = [],
splitIndex,
currentToken,
currentContext,
stack = [],
stackIndex = 0,
chunkTimestamp,
timeData,
lastCueTime;
cueSplit = cueText.split(/(<\/?[^>]+>)/ig);
currentContext = cueStructure;
for (splitIndex in cueSplit) {
if (cueSplit.hasOwnProperty(splitIndex)) {
currentToken = cueSplit[splitIndex];
if (currentToken.substr(0,1) === "<") {
if (currentToken.substr(1,1) === "/") {
// Closing tag
var TagName = currentToken.substr(2).split(/[\s>]+/g)[0];
if (stack.length > 0) {
// Scan backwards through the stack to determine whether we've got an open tag somewhere to close.
var stackScanDepth = 0;
for (stackIndex = stack.length-1; stackIndex >= 0; stackIndex --) {
var parentContext = stack[stackIndex][stack[stackIndex].length-1];
stackScanDepth = stackIndex;
if (parentContext.token === TagName) { break; }
}
currentContext = stack[stackScanDepth];
stack = stack.slice(0,stackScanDepth);
} else {
// Tag mismatch!
alert("Tag mismatch when parsing WebVTT cue: " + cueText + "\n : tag name was " + TagName);
}
} else {
// Opening Tag
// Check whether the tag is valid according to the WebVTT specification
// If not, don't allow it (unless the sanitiseCueHTML option is explicitly set to false)
if (( currentToken.substr(1).match(SRTChunkTimestampParser) ||
currentToken.match(/^<(v|lang)\s+[^>]+>/i) ||
currentToken.match(/^<c[a-zA-Z0-9\-\_\.#\(\)\[\]]+>/) ||
currentToken.match(/^<(b|i|u|ruby|ruby|rt)>/))
) {
var tmpObject = {
"token": currentToken.replace(/[<\/>]+/ig,"").split(/[\s\.]+/)[0],
"rawToken": currentToken,
"children": []
};
if (tmpObject.token === "v") {
tmpObject.voice = currentToken.match(/^<v\s*([^>]+)>/i)[1];
} else if (tmpObject.token === "c") {
var hasRealTextContent = function(textInput) {
return !!textInput.replace(/[^a-z0-9]+/ig,"").length;
};
tmpObject.classes = currentToken
.replace(/[<\/>\s]+/ig,"")
.split(/[\.]+/ig)
.slice(1)
.filter(hasRealTextContent);
} else if (!!(chunkTimestamp = tmpObject.rawToken.match(SRTChunkTimestampParser))) {
cueStructure.isTimeDependent = true;
timeData = chunkTimestamp.slice(1);
tmpObject.timeIn = parseInt((timeData[0]||0) * 60 * 60,10) + // Hours
parseInt((timeData[1]||0) * 60,10) + // Minutes
parseInt((timeData[2]||0),10) + // Seconds
parseFloat("0." + (timeData[3]||0)); // MS
}
currentContext.push(tmpObject);
stack.push(currentContext);
currentContext = tmpObject.children;
}
}
} else {
// Text string
currentToken = currentToken
.replace(/</g,"&lt;")
.replace(/>/g,"&gt;")
.replace(/\&/g,"&amp;");
currentToken = currentToken.replace(/\n+/g,"<br />");
currentContext.push(currentToken);
}
}
}
reportMessage("cue text parsed: "+cueStructure);
return cueStructure;
}