summaryrefslogtreecommitdiffstats
path: root/assets/js/videojs-youtube-annotations.js
diff options
context:
space:
mode:
authorOmar Roth <omarroth@protonmail.com>2019-05-01 07:38:42 -0500
committerOmar Roth <omarroth@protonmail.com>2019-05-01 07:40:18 -0500
commit6fb44083eca81499ad3c53381eaff85cdc3e1fd9 (patch)
tree44da86584311790df4312bf875049e47cb0ee723 /assets/js/videojs-youtube-annotations.js
parentba02be08bb9c19e6c41c6c7764bd8377ee7f4435 (diff)
downloadinvidious-6fb44083eca81499ad3c53381eaff85cdc3e1fd9.tar.gz
invidious-6fb44083eca81499ad3c53381eaff85cdc3e1fd9.tar.bz2
invidious-6fb44083eca81499ad3c53381eaff85cdc3e1fd9.zip
Update source and licenses
Diffstat (limited to 'assets/js/videojs-youtube-annotations.js')
-rw-r--r--assets/js/videojs-youtube-annotations.js975
1 files changed, 0 insertions, 975 deletions
diff --git a/assets/js/videojs-youtube-annotations.js b/assets/js/videojs-youtube-annotations.js
deleted file mode 100644
index b6055054..00000000
--- a/assets/js/videojs-youtube-annotations.js
+++ /dev/null
@@ -1,975 +0,0 @@
-class AnnotationParser {
- static get defaultAppearanceAttributes() {
- return {
- bgColor: 0xFFFFFF,
- bgOpacity: 0.80,
- fgColor: 0,
- textSize: 3.15
- };
- }
-
- static get attributeMap() {
- return {
- type: "tp",
- style: "s",
- x: "x",
- y: "y",
- width: "w",
- height: "h",
-
- sx: "sx",
- sy: "sy",
-
- timeStart: "ts",
- timeEnd: "te",
- text: "t",
-
- actionType: "at",
- actionUrl: "au",
- actionUrlTarget: "aut",
- actionSeconds: "as",
-
- bgOpacity: "bgo",
- bgColor: "bgc",
- fgColor: "fgc",
- textSize: "txsz"
- };
- }
-
- /* AR ANNOTATION FORMAT */
- deserializeAnnotation(serializedAnnotation) {
- const map = this.constructor.attributeMap;
- const attributes = serializedAnnotation.split(",");
- const annotation = {};
- for (const attribute of attributes) {
- const [ key, value ] = attribute.split("=");
- const mappedKey = this.getKeyByValue(map, key);
-
- let finalValue = "";
-
- if (["text", "actionType", "actionUrl", "actionUrlTarget", "type", "style"].indexOf(mappedKey) > -1) {
- finalValue = decodeURIComponent(value);
- }
- else {
- finalValue = parseFloat(value, 10);
- }
- annotation[mappedKey] = finalValue;
- }
- return annotation;
- }
- serializeAnnotation(annotation) {
- const map = this.constructor.attributeMap;
- let serialized = "";
- for (const key in annotation) {
- const mappedKey = map[key];
- if ((["text", "actionType", "actionUrl", "actionUrlTarget"].indexOf(key) > -1) && mappedKey && annotation.hasOwnProperty(key)) {
- let text = encodeURIComponent(annotation[key]);
- serialized += `${mappedKey}=${text},`;
- }
- else if ((["text", "actionType", "actionUrl", "actionUrlTarget"].indexOf("key") === -1) && mappedKey && annotation.hasOwnProperty(key)) {
- serialized += `${mappedKey}=${annotation[key]},`;
- }
- }
- // remove trailing comma
- return serialized.substring(0, serialized.length - 1);
- }
-
- deserializeAnnotationList(serializedAnnotationString) {
- const serializedAnnotations = serializedAnnotationString.split(";");
- serializedAnnotations.length = serializedAnnotations.length - 1;
- const annotations = [];
- for (const annotation of serializedAnnotations) {
- annotations.push(this.deserializeAnnotation(annotation));
- }
- return annotations;
- }
- serializeAnnotationList(annotations) {
- let serialized = "";
- for (const annotation of annotations) {
- serialized += this.serializeAnnotation(annotation) + ";";
- }
- return serialized;
- }
-
- /* PARSING YOUTUBE'S ANNOTATION FORMAT */
- xmlToDom(xml) {
- const parser = new DOMParser();
- const dom = parser.parseFromString(xml, "application/xml");
- return dom;
- }
- getAnnotationsFromXml(xml) {
- const dom = this.xmlToDom(xml);
- return dom.getElementsByTagName("annotation");
- }
- parseYoutubeAnnotationList(annotationElements) {
- const annotations = [];
- for (const el of annotationElements) {
- const parsedAnnotation = this.parseYoutubeAnnotation(el);
- if (parsedAnnotation) annotations.push(parsedAnnotation);
- }
- return annotations;
- }
- parseYoutubeAnnotation(annotationElement) {
- const base = annotationElement;
- const attributes = this.getAttributesFromBase(base);
- if (!attributes.type || attributes.type === "pause") return null;
-
- const text = this.getTextFromBase(base);
- const action = this.getActionFromBase(base);
-
- const backgroundShape = this.getBackgroundShapeFromBase(base);
- if (!backgroundShape) return null;
- const timeStart = backgroundShape.timeRange.start;
- const timeEnd = backgroundShape.timeRange.end;
-
- if (isNaN(timeStart) || isNaN(timeEnd) || timeStart === null || timeEnd === null) {
- return null;
- }
-
- const appearance = this.getAppearanceFromBase(base);
-
- // properties the renderer needs
- let annotation = {
- // possible values: text, highlight, pause, branding
- type: attributes.type,
- // x, y, width, and height as percent of video size
- x: backgroundShape.x,
- y: backgroundShape.y,
- width: backgroundShape.width,
- height: backgroundShape.height,
- // what time the annotation is shown in seconds
- timeStart,
- timeEnd
- };
- // properties the renderer can work without
- if (attributes.style) annotation.style = attributes.style;
- if (text) annotation.text = text;
- if (action) annotation = Object.assign(action, annotation);
- if (appearance) annotation = Object.assign(appearance, annotation);
-
- if (backgroundShape.hasOwnProperty("sx")) annotation.sx = backgroundShape.sx;
- if (backgroundShape.hasOwnProperty("sy")) annotation.sy = backgroundShape.sy;
-
- return annotation;
- }
- getBackgroundShapeFromBase(base) {
- const movingRegion = base.getElementsByTagName("movingRegion")[0];
- if (!movingRegion) return null;
- const regionType = movingRegion.getAttribute("type");
-
- const regions = movingRegion.getElementsByTagName(`${regionType}Region`);
- const timeRange = this.extractRegionTime(regions);
-
- const shape = {
- type: regionType,
- x: parseFloat(regions[0].getAttribute("x"), 10),
- y: parseFloat(regions[0].getAttribute("y"), 10),
- width: parseFloat(regions[0].getAttribute("w"), 10),
- height: parseFloat(regions[0].getAttribute("h"), 10),
- timeRange
- }
-
- const sx = regions[0].getAttribute("sx");
- const sy = regions[0].getAttribute("sy");
-
- if (sx) shape.sx = parseFloat(sx, 10);
- if (sy) shape.sy = parseFloat(sy, 10);
-
- return shape;
- }
- getAttributesFromBase(base) {
- const attributes = {};
- attributes.type = base.getAttribute("type");
- attributes.style = base.getAttribute("style");
- return attributes;
- }
- getTextFromBase(base) {
- const textElement = base.getElementsByTagName("TEXT")[0];
- if (textElement) return textElement.textContent;
- }
- getActionFromBase(base) {
- const actionElement = base.getElementsByTagName("action")[0];
- if (!actionElement) return null;
- const typeAttr = actionElement.getAttribute("type");
-
- const urlElement = actionElement.getElementsByTagName("url")[0];
- if (!urlElement) return null;
- const actionUrlTarget = urlElement.getAttribute("target");
- const href = urlElement.getAttribute("value");
- // only allow links to youtube
- // can be changed in the future
- if (href.startsWith("https://www.youtube.com/")) {
- const url = new URL(href);
- const srcVid = url.searchParams.get("src_vid");
- const toVid = url.searchParams.get("v");
-
- return this.linkOrTimestamp(url, srcVid, toVid, actionUrlTarget);
- }
- }
- linkOrTimestamp(url, srcVid, toVid, actionUrlTarget) {
- // check if it's a link to a new video
- // or just a timestamp
- if (srcVid && toVid && srcVid === toVid) {
- let seconds = 0;
- const hash = url.hash;
- if (hash && hash.startsWith("#t=")) {
- const timeString = url.hash.split("#t=")[1];
- seconds = this.timeStringToSeconds(timeString);
- }
- return {actionType: "time", actionSeconds: seconds}
- }
- else {
- return {actionType: "url", actionUrl: url.href, actionUrlTarget};
- }
- }
- getAppearanceFromBase(base) {
- const appearanceElement = base.getElementsByTagName("appearance")[0];
- const styles = this.constructor.defaultAppearanceAttributes;
-
- if (appearanceElement) {
- const bgOpacity = appearanceElement.getAttribute("bgAlpha");
- const bgColor = appearanceElement.getAttribute("bgColor");
- const fgColor = appearanceElement.getAttribute("fgColor");
- const textSize = appearanceElement.getAttribute("textSize");
- // not yet sure what to do with effects
- // const effects = appearanceElement.getAttribute("effects");
-
- // 0.00 to 1.00
- if (bgOpacity) styles.bgOpacity = parseFloat(bgOpacity, 10);
- // 0 to 256 ** 3
- if (bgColor) styles.bgColor = parseInt(bgColor, 10);
- if (fgColor) styles.fgColor = parseInt(fgColor, 10);
- // 0.00 to 100.00?
- if (textSize) styles.textSize = parseFloat(textSize, 10);
- }
-
- return styles;
- }
-
- /* helper functions */
- extractRegionTime(regions) {
- let timeStart = regions[0].getAttribute("t");
- timeStart = this.hmsToSeconds(timeStart);
-
- let timeEnd = regions[regions.length - 1].getAttribute("t");
- timeEnd = this.hmsToSeconds(timeEnd);
-
- return {start: timeStart, end: timeEnd}
- }
- // https://stackoverflow.com/a/9640417/10817894
- hmsToSeconds(hms) {
- let p = hms.split(":");
- let s = 0;
- let m = 1;
-
- while (p.length > 0) {
- s += m * parseFloat(p.pop(), 10);
- m *= 60;
- }
- return s;
- }
- timeStringToSeconds(time) {
- let seconds = 0;
-
- const h = time.split("h");
- const m = (h[1] || time).split("m");
- const s = (m[1] || time).split("s");
-
- if (h[0] && h.length === 2) seconds += parseInt(h[0], 10) * 60 * 60;
- if (m[0] && m.length === 2) seconds += parseInt(m[0], 10) * 60;
- if (s[0] && s.length === 2) seconds += parseInt(s[0], 10);
-
- return seconds;
- }
- getKeyByValue(obj, value) {
- for (const key in obj) {
- if (obj.hasOwnProperty(key)) {
- if (obj[key] === value) {
- return key;
- }
- }
- }
- }
-}
-class AnnotationRenderer {
- constructor(annotations, container, playerOptions, updateInterval = 1000) {
- if (!annotations) throw new Error("Annotation objects must be provided");
- if (!container) throw new Error("An element to contain the annotations must be provided");
-
- if (playerOptions && playerOptions.getVideoTime && playerOptions.seekTo) {
- this.playerOptions = playerOptions;
- }
- else {
- console.info("AnnotationRenderer is running without a player. The update method will need to be called manually.");
- }
-
- this.annotations = annotations;
- this.container = container;
-
- this.annotationsContainer = document.createElement("div");
- this.annotationsContainer.classList.add("__cxt-ar-annotations-container__");
- this.annotationsContainer.setAttribute("data-layer", "4");
- this.annotationsContainer.addEventListener("click", e => {
- this.annotationClickHandler(e);
- });
- this.container.prepend(this.annotationsContainer);
-
- this.createAnnotationElements();
-
- // in case the dom already loaded
- this.updateAllAnnotationSizes();
- window.addEventListener("DOMContentLoaded", e => {
- this.updateAllAnnotationSizes();
- });
-
- this.updateInterval = updateInterval;
- this.updateIntervalId = null;
- }
- changeAnnotationData(annotations) {
- this.stop();
- this.removeAnnotationElements();
- this.annotations = annotations;
- this.createAnnotationElements();
- this.start();
- }
- createAnnotationElements() {
- for (const annotation of this.annotations) {
- const el = document.createElement("div");
- el.classList.add("__cxt-ar-annotation__");
-
- annotation.__element = el;
- el.__annotation = annotation;
-
- // close button
- const closeButton = this.createCloseElement();
- closeButton.addEventListener("click", e => {
- el.setAttribute("hidden", "");
- el.setAttribute("data-ar-closed", "");
- if (el.__annotation.__speechBubble) {
- const speechBubble = el.__annotation.__speechBubble;
- speechBubble.style.display = "none";
- }
- });
- el.append(closeButton);
-
- if (annotation.text) {
- const textNode = document.createElement("span");
- textNode.textContent = annotation.text;
- el.append(textNode);
- el.setAttribute("data-ar-has-text", "");
- }
-
- if (annotation.style === "speech") {
- const containerDimensions = this.container.getBoundingClientRect();
- const speechX = this.percentToPixels(containerDimensions.width, annotation.x);
- const speechY = this.percentToPixels(containerDimensions.height, annotation.y);
-
- const speechWidth = this.percentToPixels(containerDimensions.width, annotation.width);
- const speechHeight = this.percentToPixels(containerDimensions.height, annotation.height);
-
- const speechPointX = this.percentToPixels(containerDimensions.width, annotation.sx);
- const speechPointY = this.percentToPixels(containerDimensions.height, annotation.sy);
-
- const bubbleColor = this.getFinalAnnotationColor(annotation, false);
- const bubble = this.createSvgSpeechBubble(speechX, speechY, speechWidth, speechHeight, speechPointX, speechPointY, bubbleColor, annotation.__element);
- bubble.style.display = "none";
- bubble.style.overflow = "visible";
- el.style.pointerEvents = "none";
- bubble.__annotationEl = el;
- annotation.__speechBubble = bubble;
-
- const path = bubble.getElementsByTagName("path")[0];
- path.addEventListener("mouseover", () => {
- closeButton.style.display = "block";
- // path.style.cursor = "pointer";
- closeButton.style.cursor = "pointer";
- path.setAttribute("fill", this.getFinalAnnotationColor(annotation, true));
- });
- path.addEventListener("mouseout", e => {
- if (!e.relatedTarget.classList.contains("__cxt-ar-annotation-close__")) {
- closeButton.style.display ="none";
- // path.style.cursor = "default";
- closeButton.style.cursor = "default";
- path.setAttribute("fill", this.getFinalAnnotationColor(annotation, false));
- }
- });
-
- closeButton.addEventListener("mouseleave", () => {
- closeButton.style.display = "none";
- path.style.cursor = "default";
- closeButton.style.cursor = "default";
- path.setAttribute("fill", this.getFinalAnnotationColor(annotation, false));
- });
-
- el.prepend(bubble);
- }
- else if (annotation.type === "highlight") {
- el.style.backgroundColor = "";
- el.style.border = `2.5px solid ${this.getFinalAnnotationColor(annotation, false)}`;
- if (annotation.actionType === "url")
- el.style.cursor = "pointer";
- }
- else if (annotation.style !== "title") {
- el.style.backgroundColor = this.getFinalAnnotationColor(annotation);
- el.addEventListener("mouseenter", () => {
- el.style.backgroundColor = this.getFinalAnnotationColor(annotation, true);
- });
- el.addEventListener("mouseleave", () => {
- el.style.backgroundColor = this.getFinalAnnotationColor(annotation, false);
- });
- if (annotation.actionType === "url")
- el.style.cursor = "pointer";
- }
-
- el.style.color = `#${this.decimalToHex(annotation.fgColor)}`;
-
- el.setAttribute("data-ar-type", annotation.type);
- el.setAttribute("hidden", "");
- this.annotationsContainer.append(el);
- }
- }
- createCloseElement() {
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("viewBox", "0 0 100 100")
- svg.classList.add("__cxt-ar-annotation-close__");
-
- const path = document.createElementNS(svg.namespaceURI, "path");
- path.setAttribute("d", "M25 25 L 75 75 M 75 25 L 25 75");
- path.setAttribute("stroke", "#bbb");
- path.setAttribute("stroke-width", 10)
- path.setAttribute("x", 5);
- path.setAttribute("y", 5);
-
- const circle = document.createElementNS(svg.namespaceURI, "circle");
- circle.setAttribute("cx", 50);
- circle.setAttribute("cy", 50);
- circle.setAttribute("r", 50);
-
- svg.append(circle, path);
- return svg;
- }
- createSvgSpeechBubble(x, y, width, height, pointX, pointY, color = "white", element, svg) {
-
- const horizontalBaseStartMultiplier = 0.17379070765180116;
- const horizontalBaseEndMultiplier = 0.14896346370154384;
-
- const verticalBaseStartMultiplier = 0.12;
- const verticalBaseEndMultiplier = 0.3;
-
- let path;
-
- if (!svg) {
- svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.classList.add("__cxt-ar-annotation-speech-bubble__");
-
- path = document.createElementNS("http://www.w3.org/2000/svg", "path");
- path.setAttribute("fill", color);
- svg.append(path);
- }
- else {
- path = svg.children[0];
- }
-
- svg.style.position = "absolute";
- svg.setAttribute("width", "100%");
- svg.setAttribute("height", "100%");
- svg.style.left = "0";
- svg.style.top = "0";
-
- let positionStart;
-
- let baseStartX = 0;
- let baseStartY = 0;
-
- let baseEndX = 0;
- let baseEndY = 0;
-
- let pointFinalX = pointX;
- let pointFinalY = pointY;
-
- let commentRectPath;
- const pospad = 20;
-
- let textWidth = 0;
- let textHeight = 0;
- let textX = 0;
- let textY = 0;
-
- let textElement;
- let closeElement;
-
- if (element) {
- textElement = element.getElementsByTagName("span")[0];
- closeElement = element.getElementsByClassName("__cxt-ar-annotation-close__")[0];
- }
-
- if (pointX > ((x + width) - (width / 2)) && pointY > y + height) {
- positionStart = "br";
- baseStartX = width - ((width * horizontalBaseStartMultiplier) * 2);
- baseEndX = baseStartX + (width * horizontalBaseEndMultiplier);
- baseStartY = height;
- baseEndY = height;
-
- pointFinalX = pointX - x;
- pointFinalY = pointY - y;
- element.style.height = pointY - y;
- commentRectPath = `L${width} ${height} L${width} 0 L0 0 L0 ${baseStartY} L${baseStartX} ${baseStartY}`;
- if (textElement) {
- textWidth = width;
- textHeight = height;
- textX = 0;
- textY = 0;
- }
- }
- else if (pointX < ((x + width) - (width / 2)) && pointY > y + height) {
- positionStart = "bl";
- baseStartX = width * horizontalBaseStartMultiplier;
- baseEndX = baseStartX + (width * horizontalBaseEndMultiplier);
- baseStartY = height;
- baseEndY = height;
-
- pointFinalX = pointX - x;
- pointFinalY = pointY - y;
- element.style.height = `${pointY - y}px`;
- commentRectPath = `L${width} ${height} L${width} 0 L0 0 L0 ${baseStartY} L${baseStartX} ${baseStartY}`;
- if (textElement) {
- textWidth = width;
- textHeight = height;
- textX = 0;
- textY = 0;
- }
- }
- else if (pointX > ((x + width) - (width / 2)) && pointY < (y - pospad)) {
- positionStart = "tr";
- baseStartX = width - ((width * horizontalBaseStartMultiplier) * 2);
- baseEndX = baseStartX + (width * horizontalBaseEndMultiplier);
-
- const yOffset = y - pointY;
- baseStartY = yOffset;
- baseEndY = yOffset;
- element.style.top = y - yOffset + "px";
- element.style.height = height + yOffset + "px";
-
- pointFinalX = pointX - x;
- pointFinalY = 0;
- commentRectPath = `L${width} ${yOffset} L${width} ${height + yOffset} L0 ${height + yOffset} L0 ${yOffset} L${baseStartX} ${baseStartY}`;
- if (textElement) {
- textWidth = width;
- textHeight = height;
- textX = 0;
- textY = yOffset;
- }
- }
- else if (pointX < ((x + width) - (width / 2)) && pointY < y) {
- positionStart = "tl";
- baseStartX = width * horizontalBaseStartMultiplier;
- baseEndX = baseStartX + (width * horizontalBaseEndMultiplier);
-
- const yOffset = y - pointY;
- baseStartY = yOffset;
- baseEndY = yOffset;
- element.style.top = y - yOffset + "px";
- element.style.height = height + yOffset + "px";
-
- pointFinalX = pointX - x;
- pointFinalY = 0;
- commentRectPath = `L${width} ${yOffset} L${width} ${height + yOffset} L0 ${height + yOffset} L0 ${yOffset} L${baseStartX} ${baseStartY}`;
-
- if (textElement) {
- textWidth = width;
- textHeight = height;
- textX = 0;
- textY = yOffset;
- }
- }
- else if (pointX > (x + width) && pointY > (y - pospad) && pointY < ((y + height) - pospad)) {
- positionStart = "r";
-
- const xOffset = pointX - (x + width);
-
- baseStartX = width;
- baseEndX = width;
-
- element.style.width = width + xOffset + "px";
-
- baseStartY = height * verticalBaseStartMultiplier;
- baseEndY = baseStartY + (height * verticalBaseEndMultiplier);
-
- pointFinalX = width + xOffset;
- pointFinalY = pointY - y;
- commentRectPath = `L${baseStartX} ${height} L0 ${height} L0 0 L${baseStartX} 0 L${baseStartX} ${baseStartY}`;
- if (textElement) {
- textWidth = width;
- textHeight = height;
- textX = 0;
- textY = 0;
- }
- }
- else if (pointX < x && pointY > y && pointY < (y + height)) {
- positionStart = "l";
-
- const xOffset = x - pointX;
-
- baseStartX = xOffset;
- baseEndX = xOffset;
-
- element.style.left = x - xOffset + "px";
- element.style.width = width + xOffset + "px";
-
- baseStartY = height * verticalBaseStartMultiplier;
- baseEndY = baseStartY + (height * verticalBaseEndMultiplier);
-
- pointFinalX = 0;
- pointFinalY = pointY - y;
- commentRectPath = `L${baseStartX} ${height} L${width + baseStartX} ${height} L${width + baseStartX} 0 L${baseStartX} 0 L${baseStartX} ${baseStartY}`;
- if (textElement) {
- textWidth = width;
- textHeight = height;
- textX = xOffset;
- textY = 0;
- }
- }
- else {
- return svg;
- }
-
- if (textElement) {
- textElement.style.left = textX + "px";
- textElement.style.top = textY + "px";
- textElement.style.width = textWidth + "px";
- textElement.style.height = textHeight + "px";
- }
- if (closeElement) {
- const closeSize = parseFloat(this.annotationsContainer.style.getPropertyValue("--annotation-close-size"), 10);
- if (closeSize) {
- closeElement.style.left = ((textX + textWidth) + (closeSize / -1.8)) + "px";
- closeElement.style.top = (textY + (closeSize / -1.8)) + "px";
- }
- }
-
- const pathData = `M${baseStartX} ${baseStartY} L${pointFinalX} ${pointFinalY} L${baseEndX} ${baseEndY} ${commentRectPath}`;
- path.setAttribute("d", pathData);
-
- return svg;
- }
- getFinalAnnotationColor(annotation, hover = false) {
- const alphaHex = hover ? (0xE6).toString(16) : Math.floor((annotation.bgOpacity * 255)).toString(16);
- if (!isNaN(annotation.bgColor)) {
- const bgColorHex = this.decimalToHex(annotation.bgColor);
-
- const backgroundColor = `#${bgColorHex}${alphaHex}`;
- return backgroundColor;
- }
- }
- removeAnnotationElements() {
- for (const annotation of this.annotations) {
- annotation.__element.remove();
- }
- }
- update(videoTime) {
- for (const annotation of this.annotations) {
- const el = annotation.__element;
- if (el.hasAttribute("data-ar-closed")) continue;
- const start = annotation.timeStart;
- const end = annotation.timeEnd;
-
- if (el.hasAttribute("hidden") && (videoTime >= start && videoTime < end)) {
- el.removeAttribute("hidden");
- if (annotation.style === "speech" && annotation.__speechBubble) {
- annotation.__speechBubble.style.display = "block";
- }
- }
- else if (!el.hasAttribute("hidden") && (videoTime < start || videoTime > end)) {
- el.setAttribute("hidden", "");
- if (annotation.style === "speech" && annotation.__speechBubble) {
- annotation.__speechBubble.style.display = "none";
- }
- }
- }
- }
- start() {
- if (!this.playerOptions) throw new Error("playerOptions must be provided to use the start method");
-
- const videoTime = this.playerOptions.getVideoTime();
- if (!this.updateIntervalId) {
- this.update(videoTime);
- this.updateIntervalId = setInterval(() => {
- const videoTime = this.playerOptions.getVideoTime();
- this.update(videoTime);
- window.dispatchEvent(new CustomEvent("__ar_renderer_start"));
- }, this.updateInterval);
- }
- }
- stop() {
- if (!this.playerOptions) throw new Error("playerOptions must be provided to use the stop method");
-
- const videoTime = this.playerOptions.getVideoTime();
- if (this.updateIntervalId) {
- this.update(videoTime);
- clearInterval(this.updateIntervalId);
- this.updateIntervalId = null;
- window.dispatchEvent(new CustomEvent("__ar_renderer_stop"));
- }
- }
-
- updateAnnotationTextSize(annotation, containerHeight) {
- if (annotation.textSize) {
- const textSize = (annotation.textSize / 100) * containerHeight;
- annotation.__element.style.fontSize = `${textSize}px`;
- }
- }
- updateTextSize() {
- const containerHeight = this.container.getBoundingClientRect().height;
- // should be run when the video resizes
- for (const annotation of this.annotations) {
- this.updateAnnotationTextSize(annotation, containerHeight);
- }
- }
- updateCloseSize(containerHeight) {
- if (!containerHeight) containerHeight = this.container.getBoundingClientRect().height;
- const multiplier = 0.0423;
- this.annotationsContainer.style.setProperty("--annotation-close-size", `${containerHeight * multiplier}px`);
- }
- updateAnnotationDimensions(annotations, videoWidth, videoHeight) {
- const playerWidth = this.container.getBoundingClientRect().width;
- const playerHeight = this.container.getBoundingClientRect().height;
-
- const widthDivider = playerWidth / videoWidth;
- const heightDivider = playerHeight / videoHeight;
-
- let scaledVideoWidth = playerWidth;
- let scaledVideoHeight = playerHeight;
-
- if (widthDivider % 1 !== 0 || heightDivider % 1 !== 0) {
- // vertical bars
- if (widthDivider > heightDivider) {
- scaledVideoWidth = (playerHeight / videoHeight) * videoWidth;
- scaledVideoHeight = playerHeight;
- }
- // horizontal bars
- else if (heightDivider > widthDivider) {
- scaledVideoWidth = playerWidth;
- scaledVideoHeight = (playerWidth / videoWidth) * videoHeight;
- }
- }
-
- const verticalBlackBarWidth = (playerWidth - scaledVideoWidth) / 2;
- const horizontalBlackBarHeight = (playerHeight - scaledVideoHeight) / 2;
-
- const widthOffsetPercent = (verticalBlackBarWidth / playerWidth * 100);
- const heightOffsetPercent = (horizontalBlackBarHeight / playerHeight * 100);
-
- const widthMultiplier = (scaledVideoWidth / playerWidth);
- const heightMultiplier = (scaledVideoHeight / playerHeight);
-
- for (const annotation of annotations) {
- const el = annotation.__element;
-
- let ax = widthOffsetPercent + (annotation.x * widthMultiplier);
- let ay = heightOffsetPercent + (annotation.y * heightMultiplier);
- let aw = annotation.width * widthMultiplier;
- let ah = annotation.height * heightMultiplier;
-
- el.style.left = `${ax}%`;
- el.style.top = `${ay}%`;
-
- el.style.width = `${aw}%`;
- el.style.height = `${ah}%`;
-
- let horizontalPadding = scaledVideoWidth * 0.008;
- let verticalPadding = scaledVideoHeight * 0.008;
-
- if (annotation.style === "speech" && annotation.text) {
- const pel = annotation.__element.getElementsByTagName("span")[0];
- horizontalPadding *= 2;
- verticalPadding *= 2;
-
- pel.style.paddingLeft = horizontalPadding + "px";
- pel.style.paddingRight = horizontalPadding + "px";
- pel.style.paddingBottom = verticalPadding + "px";
- pel.style.paddingTop = verticalPadding + "px";
- }
- else if (annotation.style !== "speech") {
- el.style.paddingLeft = horizontalPadding + "px";
- el.style.paddingRight = horizontalPadding + "px";
- el.style.paddingBottom = verticalPadding + "px";
- el.style.paddingTop = verticalPadding + "px";
- }
-
- if (annotation.__speechBubble) {
- const asx = this.percentToPixels(playerWidth, ax);
- const asy = this.percentToPixels(playerHeight, ay);
- const asw = this.percentToPixels(playerWidth, aw);
- const ash = this.percentToPixels(playerHeight, ah);
-
- let sx = widthOffsetPercent + (annotation.sx * widthMultiplier);
- let sy = heightOffsetPercent + (annotation.sy * heightMultiplier);
- sx = this.percentToPixels(playerWidth, sx);
- sy = this.percentToPixels(playerHeight, sy);
-
- this.createSvgSpeechBubble(asx, asy, asw, ash, sx, sy, null, annotation.__element, annotation.__speechBubble);
- }
-
- this.updateAnnotationTextSize(annotation, scaledVideoHeight);
- this.updateCloseSize(scaledVideoHeight);
- }
- }
-
- updateAllAnnotationSizes() {
- if (this.playerOptions && this.playerOptions.getOriginalVideoWidth && this.playerOptions.getOriginalVideoHeight) {
- const videoWidth = this.playerOptions.getOriginalVideoWidth();
- const videoHeight = this.playerOptions.getOriginalVideoHeight();
- this.updateAnnotationDimensions(this.annotations, videoWidth, videoHeight);
- }
- else {
- const playerWidth = this.container.getBoundingClientRect().width;
- const playerHeight = this.container.getBoundingClientRect().height;
- this.updateAnnotationDimensions(this.annotations, playerWidth, playerHeight);
- }
- }
-
- hideAll() {
- for (const annotation of this.annotations) {
- annotation.__element.setAttribute("hidden", "");
- }
- }
- annotationClickHandler(e) {
- let annotationElement = e.target;
- // if we click on annotation text instead of the actual annotation element
- if (!annotationElement.matches(".__cxt-ar-annotation__") && !annotationElement.closest(".__cxt-ar-annotation-close__")) {
- annotationElement = annotationElement.closest(".__cxt-ar-annotation__");
- if (!annotationElement) return null;
- }
- let annotationData = annotationElement.__annotation;
-
- if (!annotationElement || !annotationData) return;
-
- if (annotationData.actionType === "time") {
- const seconds = annotationData.actionSeconds;
- if (this.playerOptions) {
- this.playerOptions.seekTo(seconds);
- const videoTime = this.playerOptions.getVideoTime();
- this.update(videoTime);
- }
- window.dispatchEvent(new CustomEvent("__ar_seek_to", {detail: {seconds}}));
- }
- else if (annotationData.actionType === "url") {
- const data = {url: annotationData.actionUrl, target: annotationData.actionUrlTarget || "current"};
-
- const timeHash = this.extractTimeHash(new URL(data.url));
- if (timeHash && timeHash.hasOwnProperty("seconds")) {
- data.seconds = timeHash.seconds;
- }
- window.dispatchEvent(new CustomEvent("__ar_annotation_click", {detail: data}));
- }
- }
-
- setUpdateInterval(ms) {
- this.updateInterval = ms;
- this.stop();
- this.start();
- }
- // https://stackoverflow.com/a/3689638/10817894
- decimalToHex(dec) {
- let hex = dec.toString(16);
- hex = "000000".substr(0, 6 - hex.length) + hex;
- return hex;
- }
- extractTimeHash(url) {
- if (!url) throw new Error("A URL must be provided");
- const hash = url.hash;
-
- if (hash && hash.startsWith("#t=")) {
- const timeString = url.hash.split("#t=")[1];
- const seconds = this.timeStringToSeconds(timeString);
- return {seconds};
- }
- else {
- return false;
- }
- }
- timeStringToSeconds(time) {
- let seconds = 0;
-
- const h = time.split("h");
- const m = (h[1] || time).split("m");
- const s = (m[1] || time).split("s");
-
- if (h[0] && h.length === 2) seconds += parseInt(h[0], 10) * 60 * 60;
- if (m[0] && m.length === 2) seconds += parseInt(m[0], 10) * 60;
- if (s[0] && s.length === 2) seconds += parseInt(s[0], 10);
-
- return seconds;
- }
- percentToPixels(a, b) {
- return a * b / 100;
- }
-}
-function youtubeAnnotationsPlugin(options) {
- if (!options.annotationXml) throw new Error("Annotation data must be provided");
- if (!options.videoContainer) throw new Error("A video container to overlay the data on must be provided");
-
- const player = this;
-
- const xml = options.annotationXml;
- const parser = new AnnotationParser();
- const annotationElements = parser.getAnnotationsFromXml(xml);
- const annotations = parser.parseYoutubeAnnotationList(annotationElements);
-
- const videoContainer = options.videoContainer;
-
- const playerOptions = {
- getVideoTime() {
- return player.currentTime();
- },
- seekTo(seconds) {
- player.currentTime(seconds);
- },
- getOriginalVideoWidth() {
- return player.videoWidth();
- },
- getOriginalVideoHeight() {
- return player.videoHeight();
- }
- };
-
- raiseControls();
- const renderer = new AnnotationRenderer(annotations, videoContainer, playerOptions, options.updateInterval);
- setupEventListeners(player, renderer);
- renderer.start();
-}
-
-function setupEventListeners(player, renderer) {
- if (!player) throw new Error("A video player must be provided");
- // should be throttled for performance
- player.on("playerresize", e => {
- renderer.updateAllAnnotationSizes(renderer.annotations);
- });
- // Trigger resize since the video can have different dimensions than player
- player.one("loadedmetadata", e => {
- renderer.updateAllAnnotationSizes(renderer.annotations);
- });
-
- player.on("pause", e => {
- renderer.stop();
- });
- player.on("play", e => {
- renderer.start();
- });
- player.on("seeking", e => {
- renderer.update();
- });
- player.on("seeked", e => {
- renderer.update();
- });
-}
-
-function raiseControls() {
- const styles = document.createElement("style");
- styles.textContent = `
- .vjs-control-bar {
- z-index: 21;
- }
- `;
- document.body.append(styles);
-}