Bug 865407 - Part 8: Update vtt.js to latest version. r=rillian

This completes the initial implementation of the processing algorithm.
The algorithm doesn't support vertical text yet, but that's not a
big issue since Firefox doesn't support it.
This commit is contained in:
Rick Eyre 2014-02-21 22:11:06 -05:00
parent 2b1ee432d6
commit 0610cbd281
2 changed files with 368 additions and 180 deletions

View File

@ -52,7 +52,7 @@ WebVTTParserWrapper.prototype =
processCues: function(window, cues, overlay)
{
WebVTTParser.processCues(window, cues, null, overlay);
WebVTTParser.processCues(window, cues, overlay);
},
classDescription: "Wrapper for the JS WebVTTParser (vtt.js)",

View File

@ -8,7 +8,7 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
* Code below is vtt.js the JS WebVTTParser.
* Current source code can be found at http://github.com/andreasgal/vtt.js
*
* Code taken from commit d819872e198d051dfcebcfb7ecf260462c9a9c6f
* Code taken from commit b812cd783d4284de1bc6b0349b7bda151052a1df
*/
/**
* Copyright 2013 vtt.js Contributors
@ -101,20 +101,18 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
// Accept a setting if its a valid (signed) integer.
integer: function(k, v) {
if (/^-?\d+$/.test(v)) { // integer
this.set(k, parseInt(v, 10));
// Only take values in the range of -1000 ~ 1000
this.set(k, Math.min(Math.max(parseInt(v, 10), -1000), 1000));
}
},
// Accept a setting if its a valid percentage.
percent: function(k, v, frac) {
percent: function(k, v) {
var m;
if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) {
v = v.replace("%", "");
if (!m[2] || (m[2] && frac)) {
v = parseFloat(v);
if (v >= 0 && v <= 100) {
this.set(k, v);
return true;
}
v = parseFloat(v);
if (v >= 0 && v <= 100) {
this.set(k, v);
return true;
}
}
return false;
@ -664,25 +662,59 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
return ++count * -1;
}
function BoundingBox() {
function StyleBox() {
}
BoundingBox.prototype.applyStyles = function(styles) {
var div = this.div;
// Apply styles to a div. If there is no div passed then it defaults to the
// div on 'this'.
StyleBox.prototype.applyStyles = function(styles, div) {
div = div || this.div;
Object.keys(styles).forEach(function(style) {
div.style[style] = styles[style];
});
};
BoundingBox.prototype.formatStyle = function(val, unit) {
StyleBox.prototype.formatStyle = function(val, unit) {
return val === 0 ? 0 : val + unit;
};
function BasicBoundingBox(window, cue) {
BoundingBox.call(this);
// Constructs the computed display state of the cue (a div). Places the div
// into the overlay which should be a block level element (usually a div).
function CueStyleBox(window, cue, styleOptions) {
StyleBox.call(this);
this.cue = cue;
// Parse our cue's text into a DOM tree rooted at 'div'.
this.div = parseContent(window, cue.text);
// Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
// have inline positioning and will function as the cue background box.
this.cueDiv = parseContent(window, cue.text);
this.applyStyles({
color: "rgba(255, 255, 255, 1)",
backgroundColor: "rgba(0, 0, 0, 0.8)",
position: "relative",
left: 0,
right: 0,
top: 0,
bottom: 0,
display: "inline"
}, this.cueDiv);
// Create an absolutely positioned div that will be used to position the cue
// div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
// mirrors of them except "middle" which is "center" in CSS.
this.div = window.document.createElement("div");
this.applyStyles({
textAlign: cue.align === "middle" ? "center" : cue.align,
direction: determineBidi(this.cueDiv),
writingMode: cue.vertical === "" ? "horizontal-tb"
: cue.vertical === "lr" ? "vertical-lr"
: "vertical-rl",
unicodeBidi: "plaintext",
font: styleOptions.font,
whiteSpace: "pre-line",
position: "absolute"
});
this.div.appendChild(this.cueDiv);
// Calculate the distance from the reference edge of the viewport to the text
// position of the cue box. The reference edge will be resolved later when
@ -718,149 +750,277 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
});
}
// All WebVTT cue-setting alignments are equivalent to the CSS mirrors of
// them except "middle" which is "center" in CSS.
this.applyStyles({
"textAlign": cue.align === "middle" ? "center" : cue.align
});
}
BasicBoundingBox.prototype = Object.create(BoundingBox.prototype);
BasicBoundingBox.prototype.constructor = BasicBoundingBox;
const CUE_FONT_SIZE = 2.5;
const SCROLL_DURATION = 0.433;
const LINE_HEIGHT = 0.0533;
const REGION_FONT_SIZE = 1.3;
// Constructs the computed display state of the cue (a div). Places the div
// into the overlay which should be a block level element (usually a div).
function CueBoundingBox(window, cue, overlay) {
BasicBoundingBox.call(this, window, cue);
this.applyStyles({
direction: determineBidi(this.div),
writingMode: cue.vertical === "" ? "horizontal-tb"
: cue.vertical === "lr" ? "vertical-lr"
: "vertical-rl",
position: "absolute",
unicodeBidi: "plaintext",
fontSize: CUE_FONT_SIZE + "vh",
fontFamily: "sans-serif",
color: "rgba(255, 255, 255, 1)",
backgroundColor: "rgba(0, 0, 0, 0.8)",
whiteSpace: "pre-line"
});
// Append the div to the overlay so we can get the computed styles of the
// element in order to position for overlap avoidance.
overlay.appendChild(this.div);
// Calculate the distance from the reference edge of the viewport to the line
// position of the cue box. The reference edge will be resolved later when
// the box orientation styles are applied. Default if snapToLines is not set
// is 85.
var linePos = 85;
if (!cue.snapToLines) {
var computedLinePos = computeLinePos(cue),
boxComputedStyle = window.getComputedStyle(this.div),
size = cue.vertical === "" ? boxComputedStyle.getPropertyValue("height") :
boxComputedStyle.getPropertyValue("width"),
// Get the percentage value of the computed height as getPropertyValue
// returns pixels.
overlayHeight = window.getComputedStyle(overlay).getPropertyValue("height"),
calculatedPercentage = (size.replace("px", "") / overlayHeight.replace("px", "")) * 100;
switch (cue.lineAlign) {
case "start":
linePos = computedLinePos;
break;
case "middle":
linePos = computedLinePos - (calculatedPercentage / 2);
break;
case "end":
linePos = computedLinePos - calculatedPercentage;
break;
}
}
switch (cue.vertical) {
case "":
this.move = function(box) {
this.applyStyles({
top: this.formatStyle(linePos, "%")
top: this.formatStyle(box.top, "px"),
bottom: this.formatStyle(box.bottom, "px"),
left: this.formatStyle(box.left, "px"),
right: this.formatStyle(box.right, "px"),
height: this.formatStyle(box.height, "px"),
width: this.formatStyle(box.width, "px"),
});
break;
case "rl":
this.applyStyles({
left: this.formatStyle(linePos, "%")
});
break;
case "lr":
this.applyStyles({
right: this.formatStyle(linePos, "%")
});
break;
}
cue.displayState = this.div;
}
CueBoundingBox.prototype = Object.create(BasicBoundingBox.prototype);
CueBoundingBox.prototype.constuctor = CueBoundingBox;
function RegionBoundingBox(window, region) {
BoundingBox.call(this);
this.div = window.document.createElement("div");
var left = region.viewportAnchorX -
region.regionAnchorX * region.width / 100,
top = region.viewportAnchorY -
region.regionAnchorY * region.lines * LINE_HEIGHT / 100;
this.applyStyles({
position: "absolute",
writingMode: "horizontal-tb",
backgroundColor: "rgba(0, 0, 0, 0.8)",
wordWrap: "break-word",
overflowWrap: "break-word",
font: REGION_FONT_SIZE + "vh/" + LINE_HEIGHT + "vh sans-serif",
lineHeight: LINE_HEIGHT + "vh",
color: "rgba(255, 255, 255, 1)",
overflow: "hidden",
width: this.formatStyle(region.width, "%"),
minHeight: "0",
// TODO: This value is undefined in the spec, but I am assuming that they
// refer to lines * line height to get the max height See issue #107.
maxHeight: this.formatStyle(region.lines * LINE_HEIGHT, "px"),
left: this.formatStyle(left, "%"),
top: this.formatStyle(top, "%"),
display: "inline-flex",
flexFlow: "column",
justifyContent: "flex-end"
});
this.maybeAddCue = function(cue) {
if (region.id !== cue.regionId) {
return false;
}
var basicBox = new BasicBoundingBox(window, cue);
basicBox.applyStyles({
position: "relative",
unicodeBidi: "plaintext",
width: "auto"
});
if (this.div.childNodes.length === 1 && region.scroll === "up") {
this.applyStyles({
transitionProperty: "top",
transitionDuration: SCROLL_DURATION + "s"
});
}
this.div.appendChild(basicBox.div);
return true;
};
}
RegionBoundingBox.prototype = Object.create(BoundingBox.prototype);
RegionBoundingBox.prototype.constructor = RegionBoundingBox;
CueStyleBox.prototype = Object.create(StyleBox.prototype);
CueStyleBox.prototype.constructor = CueStyleBox;
// Represents the co-ordinates of an Element in a way that we can easily
// compute things with such as if it overlaps or intersects with another Element.
// Can initialize it with either a StyleBox or another BoxPosition.
function BoxPosition(obj) {
var self = this;
// Either a BoxPosition was passed in and we need to copy it, or a StyleBox
// was passed in and we need to copy the results of 'getBoundingClientRect'
// as the object returned is readonly. All co-ordinate values are in reference
// to the viewport origin (top left).
var lh;
if (obj.div) {
var rects = (rects = obj.div.childNodes) && (rects = rects[0]) &&
rects.getClientRects && rects.getClientRects();
obj = obj.div.getBoundingClientRect();
// In certain cases the outter div will be slightly larger then the sum of
// the inner div's lines. This could be due to bold text, etc, on some platforms.
// In this case we should get the average line height and use that. This will
// result in the desired behaviour.
lh = rects ? Math.max((rects[0] && rects[0].height) || 0, obj.height / rects.length)
: 0;
}
this.left = obj.left;
this.right = obj.right;
this.top = obj.top;
this.height = obj.height;
this.bottom = obj.bottom;
this.width = obj.width;
this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
}
// Move the box along a particular axis. If no amount to move is passed, via
// the val parameter, then the default amount is the line height of the box.
BoxPosition.prototype.move = function(axis, val) {
val = val !== undefined ? val : this.lineHeight;
switch (axis) {
case "+x":
this.left += val;
this.right += val;
break;
case "-x":
this.left -= val;
this.right -= val;
break;
case "+y":
this.top += val;
this.bottom += val;
break;
case "-y":
this.top -= val;
this.bottom -= val;
break;
}
};
// Check if this box overlaps another box, b2.
BoxPosition.prototype.overlaps = function(b2) {
return this.left < b2.right &&
this.right > b2.left &&
this.top < b2.bottom &&
this.bottom > b2.top;
};
// Check if this box overlaps any other boxes in boxes.
BoxPosition.prototype.overlapsAny = function(boxes) {
for (var i = 0; i < boxes.length; i++) {
if (this.overlaps(boxes[i])) {
return true;
}
}
return false;
};
// Check if this box is within another box.
BoxPosition.prototype.within = function(container) {
return this.top >= container.top &&
this.bottom <= container.bottom &&
this.left >= container.left &&
this.right <= container.right;
};
// Check if this box is entirely within the container or it is overlapping
// on the edge opposite of the axis direction passed. For example, if "+x" is
// passed and the box is overlapping on the left edge of the container, then
// return true.
BoxPosition.prototype.overlapsOppositeAxis = function(container, axis) {
switch (axis) {
case "+x":
return this.left < container.left;
case "-x":
return this.right > container.right;
case "+y":
return this.top < container.top;
case "-y":
return this.bottom > container.bottom;
}
};
// Find the percentage of the area that this box is overlapping with another
// box.
BoxPosition.prototype.intersectPercentage = function(b2) {
var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
intersectArea = x * y;
return intersectArea / (this.height * this.width);
};
// Convert the positions from this box to CSS compatible positions using
// the reference container's positions. This has to be done because this
// box's positions are in reference to the viewport origin, whereas, CSS
// values are in referecne to their respective edges.
BoxPosition.prototype.toCSSCompatValues = function(reference) {
return {
top: this.top - reference.top,
bottom: reference.bottom - this.bottom,
left: this.left - reference.left,
right: reference.right - this.right,
height: this.height,
width: this.width
};
};
// Get an object that represents the box's position without anything extra.
// Can pass a StyleBox, HTMLElement, or another BoxPositon.
BoxPosition.getSimpleBoxPosition = function(obj) {
obj = obj.div ? obj.div.getBoundingClientRect() :
obj.tagName ? obj.getBoundingClientRect() : obj;
return {
left: obj.left,
right: obj.right,
top: obj.top,
height: obj.height,
bottom: obj.bottom,
width: obj.width
};
};
// Move a StyleBox to its specified, or next best, position. The containerBox
// is the box that contains the StyleBox, such as a div. boxPositions are
// a list of other boxes that the styleBox can't overlap with.
function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
// Find the best position for a cue box, b, on the video. The axis parameter
// is a list of axis, the order of which, it will move the box along. For example:
// Passing ["+x", "-x"] will move the box first along the x axis in the positive
// direction. If it doesn't find a good position for it there it will then move
// it along the x axis in the negative direction.
function findBestPosition(b, axis) {
var bestPosition,
specifiedPosition = new BoxPosition(b),
percentage = 1; // Highest possible so the first thing we get is better.
for (var i = 0; i < axis.length; i++) {
while (b.overlapsOppositeAxis(containerBox, axis[i]) ||
(b.within(containerBox) && b.overlapsAny(boxPositions))) {
b.move(axis[i]);
}
// We found a spot where we aren't overlapping anything. This is our
// best position.
if (b.within(containerBox)) {
return b;
}
var p = b.intersectPercentage(containerBox);
// If we're outside the container box less then we were on our last try
// then remember this position as the best position.
if (percentage > p) {
bestPosition = new BoxPosition(b);
percentage = p;
}
// Reset the box position to the specified position.
b = new BoxPosition(specifiedPosition);
}
return bestPosition || specifiedPosition;
}
function reverseAxis(axis) {
return axis.map(function(a) {
return a.indexOf("+") !== -1 ? a.replace("+", "-") : a.replace("-", "+");
});
}
var boxPosition = new BoxPosition(styleBox),
cue = styleBox.cue,
linePos = computeLinePos(cue),
axis = [];
// If we have a line number to align the cue to.
if (cue.snapToLines) {
switch (cue.vertical) {
case "":
axis = [ "+y", "-y" ];
break;
case "rl":
axis = [ "+x", "-x" ];
break;
case "lr":
axis = [ "-x", "+x" ];
break;
}
// If computed line position returns negative then line numbers are
// relative to the bottom of the video instead of the top. Therefore, we
// need to increase our initial position by the length or width of the
// video, depending on the writing direction, and reverse our axis directions.
var initialPosition = boxPosition.lineHeight * Math.floor(linePos + 0.5),
initialAxis = axis[0];
if (linePos < 0) {
initialPosition += cue.vertical === "" ? containerBox.height : containerBox.width;
axis = reverseAxis(axis);
}
// Move the box to the specified position. This may not be its best
// position.
boxPosition.move(initialAxis, initialPosition);
} else {
// If we have a percentage line value for the cue.
var calculatedPercentage = (boxPosition.lineHeight / containerBox.height) * 100;
switch (cue.lineAlign) {
case "middle":
linePos -= (calculatedPercentage / 2);
break;
case "end":
linePos -= calculatedPercentage;
break;
}
// Apply initial line position to the cue box.
switch (cue.vertical) {
case "":
styleBox.applyStyles({
top: styleBox.formatStyle(linePos, "%")
});
break;
case "rl":
styleBox.applyStyles({
left: styleBox.formatStyle(linePos, "%")
});
break;
case "lr":
styleBox.applyStyles({
right: styleBox.formatStyle(linePos, "%")
});
break;
}
axis = [ "+y", "-x", "+x", "-y" ];
// Get the box position again after we've applied the specified positioning
// to it.
boxPosition = new BoxPosition(styleBox);
}
var bestPosition = findBestPosition(boxPosition, axis);
styleBox.move(bestPosition.toCSSCompatValues(containerBox));
}
function WebVTTParser(window, decoder) {
this.window = window;
@ -891,10 +1051,14 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
return parseContent(window, cuetext);
};
const FONT_SIZE_PERCENT = 0.05;
const FONT_STYLE = "sans-serif";
const CUE_BACKGROUND_PADDING = "1.5%";
// Runs the processing model over the cues and regions passed to it.
// @param overlay A block level element (usually a div) that the computed cues
// and regions will be placed into.
WebVTTParser.processCues = function(window, cues, regions, overlay) {
WebVTTParser.processCues = function(window, cues, overlay) {
if (!window || !cues || !overlay) {
return null;
}
@ -904,32 +1068,56 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
overlay.removeChild(overlay.firstChild);
}
var regionBoxes = regions ? regions.map(function(region) {
return new RegionBoundingBox(window, region);
}) : null;
var paddedOverlay = window.document.createElement("div");
paddedOverlay.style.position = "absolute";
paddedOverlay.style.left = "0";
paddedOverlay.style.right = "0";
paddedOverlay.style.top = "0";
paddedOverlay.style.bottom = "0";
paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
overlay.appendChild(paddedOverlay);
function mapCueToRegion(cue) {
for (var i = 0; i < regionBoxes.length; i++) {
if (regionBoxes[i].maybeAddCue(cue)) {
// Determine if we need to compute the display states of the cues. This could
// be the case if a cue's state has been changed since the last computation or
// if it has not been computed yet.
function shouldCompute(cues) {
for (var i = 0; i < cues.length; i++) {
if (cues[i].hasBeenReset || !cues[i].displayState) {
return true;
}
}
return false;
}
for (var i = 0; i < cues.length; i++) {
// Check to see if this cue is contained within a VTTRegion.
if (regionBoxes && mapCueToRegion(cues[i])) {
continue;
}
// Check to see if we can just reuse the last computed styles of the cue.
if (cues[i].hasBeenReset !== true && cues[i].displayState) {
overlay.appendChild(cues[i].displayState);
continue;
}
// Compute the position of the cue box on the cue overlay.
var cueBox = new CueBoundingBox(window, cues[i], overlay);
// We don't need to recompute the cues' display states. Just reuse them.
if (!shouldCompute(cues)) {
cues.forEach(function(cue) {
paddedOverlay.appendChild(cue.displayState);
});
return;
}
var boxPositions = [],
containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
var styleOptions = {
font: fontSize + "px " + FONT_STYLE
};
cues.forEach(function(cue) {
// Compute the intial position and styles of the cue div.
var styleBox = new CueStyleBox(window, cue, styleOptions);
paddedOverlay.appendChild(styleBox.div);
// Move the cue div to it's correct line position.
moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
// Remember the computed div so that we don't have to recompute it later
// if we don't have too.
cue.displayState = styleBox.div;
boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
});
};
WebVTTParser.prototype = {
@ -972,7 +1160,7 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
settings.set(k, v);
break;
case "width":
settings.percent(k, v, true);
settings.percent(k, v);
break;
case "lines":
settings.integer(k, v);
@ -986,8 +1174,8 @@ this.EXPORTED_SYMBOLS = ["WebVTTParser"];
// We have to make sure both x and y parse, so use a temporary
// settings object here.
var anchor = new Settings();
anchor.percent("x", xy[0], true);
anchor.percent("y", xy[1], true);
anchor.percent("x", xy[0]);
anchor.percent("y", xy[1]);
if (!anchor.has("x") || !anchor.has("y")) {
break;
}