Update source and licenses
This commit is contained in:
parent
ba02be08bb
commit
6fb44083ec
7 changed files with 27 additions and 1061 deletions
|
@ -1,81 +0,0 @@
|
||||||
.__cxt-ar-annotations-container__ {
|
|
||||||
--annotation-close-size: 20px;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.__cxt-ar-annotation__ {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.__cxt-ar-annotation__ {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.__cxt-ar-annotation__ span {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
padding: 2%;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none; /* Chrome all / Safari all */
|
|
||||||
-moz-user-select: none; /* Firefox all */
|
|
||||||
-ms-user-select: none; /* IE 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
.__cxt-ar-annotation-close__ {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
width: var(--annotation-close-size);
|
|
||||||
height: var(--annotation-close-size);
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
right: calc(var(--annotation-close-size) / -1.8);
|
|
||||||
top: calc(var(--annotation-close-size) / -1.8);
|
|
||||||
/* place the close button above the svg */
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.__cxt-ar-annotation__:hover:not([hidden]):not([data-ar-closed]) .__cxt-ar-annotation-close__ {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.__cxt-ar-annotation__[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.__cxt-ar-annotation__[data-ar-type="highlight"] {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.__cxt-ar-annotation__[data-ar-type="highlight"]:hover {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.50);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.__cxt-ar-annotation__ svg {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
1
assets/css/videojs-youtube-annotations.min.css
vendored
Normal file
1
assets/css/videojs-youtube-annotations.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.__cxt-ar-annotations-container__{--annotation-close-size: 20px;position:absolute;width:100%;height:100%;top:0;left:0;pointer-events:none;overflow:hidden}.__cxt-ar-annotation__{position:absolute;box-sizing:border-box;font-family:Arial,sans-serif;color:#fff;z-index:20;pointer-events:auto}.__cxt-ar-annotation__ span{position:absolute;left:0;top:0;overflow:hidden;word-wrap:break-word;white-space:pre-wrap;pointer-events:none;box-sizing:border-box;padding:2%;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.__cxt-ar-annotation-close__{display:none;position:absolute;width:var(--annotation-close-size);height:var(--annotation-close-size);cursor:pointer;right:calc(var(--annotation-close-size)/-1.8);top:calc(var(--annotation-close-size)/-1.8);z-index:1}.__cxt-ar-annotation__:hover:not([hidden]):not([data-ar-closed]) .__cxt-ar-annotation-close__{display:block}.__cxt-ar-annotation__[hidden]{display:none!important}.__cxt-ar-annotation__[data-ar-type=highlight]{border:1px solid rgba(255,255,255,.1);background-color:transparent}.__cxt-ar-annotation__[data-ar-type=highlight]:hover{border:1px solid rgba(255,255,255,.5);background-color:transparent}.__cxt-ar-annotation__ svg{pointer-events:all}
|
|
@ -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);
|
|
||||||
}
|
|
1
assets/js/videojs-youtube-annotations.min.js
vendored
Normal file
1
assets/js/videojs-youtube-annotations.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -347,7 +347,6 @@ get "/watch" do |env|
|
||||||
|
|
||||||
params = process_video_params(env.params.query, preferences)
|
params = process_video_params(env.params.query, preferences)
|
||||||
env.params.query.delete_all("listen")
|
env.params.query.delete_all("listen")
|
||||||
env.params.query.delete_all("iv_load_policy")
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(id, PG_DB, proxies, region: params.region)
|
video = get_video(id, PG_DB, proxies, region: params.region)
|
||||||
|
@ -359,9 +358,12 @@ get "/watch" do |env|
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
if preferences.annotations_subscribed && subscriptions.includes? video.ucid
|
if preferences.annotations_subscribed &&
|
||||||
|
subscriptions.includes?(video.ucid) &&
|
||||||
|
(env.params.query["iv_load_policy"]? || "1") == "1"
|
||||||
params.annotations = true
|
params.annotations = true
|
||||||
end
|
end
|
||||||
|
env.params.query.delete_all("iv_load_policy")
|
||||||
|
|
||||||
if watched && !watched.includes? id
|
if watched && !watched.includes? id
|
||||||
PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.as(User).email)
|
PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.as(User).email)
|
||||||
|
@ -554,7 +556,9 @@ get "/embed/:id" do |env|
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
if preferences.annotations_subscribed && subscriptions.includes? video.ucid
|
if preferences.annotations_subscribed &&
|
||||||
|
subscriptions.includes?(video.ucid) &&
|
||||||
|
(env.params.query["iv_load_policy"]? || "1") == "1"
|
||||||
params.annotations = true
|
params.annotations = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
<link rel="stylesheet" href="/css/videojs-http-source-selector.css">
|
<link rel="stylesheet" href="/css/videojs-http-source-selector.css">
|
||||||
<link rel="stylesheet" href="/css/videojs.markers.min.css">
|
<link rel="stylesheet" href="/css/videojs.markers.min.css">
|
||||||
<link rel="stylesheet" href="/css/videojs-share.css">
|
<link rel="stylesheet" href="/css/videojs-share.css">
|
||||||
<link rel="stylesheet" href="/css/videojs-youtube-annotations.css">
|
|
||||||
<script src="/js/video.min.js"></script>
|
<script src="/js/video.min.js"></script>
|
||||||
<script src="/js/videojs-contrib-quality-levels.min.js"></script>
|
<script src="/js/videojs-contrib-quality-levels.min.js"></script>
|
||||||
<script src="/js/videojs-http-source-selector.min.js"></script>
|
<script src="/js/videojs-http-source-selector.min.js"></script>
|
||||||
<script src="/js/videojs.hotkeys.min.js"></script>
|
<script src="/js/videojs.hotkeys.min.js"></script>
|
||||||
<script src="/js/videojs-markers.min.js"></script>
|
<script src="/js/videojs-markers.min.js"></script>
|
||||||
<script src="/js/videojs-share.min.js"></script>
|
<script src="/js/videojs-share.min.js"></script>
|
||||||
<script src="/js/videojs-youtube-annotations.js"></script>
|
<% if params.annotations %>
|
||||||
|
<link rel="stylesheet" href="/css/videojs-youtube-annotations.min.css">
|
||||||
|
<script src="/js/videojs-youtube-annotations.min.js"></script>
|
||||||
|
<% end %>
|
||||||
<% if params.listen || params.quality != "dash" %>
|
<% if params.listen || params.quality != "dash" %>
|
||||||
<link rel="stylesheet" href="/css/quality-selector.css">
|
<link rel="stylesheet" href="/css/quality-selector.css">
|
||||||
<script src="/js/silvermine-videojs-quality-selector.min.js"></script>
|
<script src="/js/silvermine-videojs-quality-selector.min.js"></script>
|
||||||
|
|
|
@ -93,6 +93,20 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/js/videojs-youtube-annotations.min.js">videojs-youtube-annotations.min.js</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a href="https://www.gnu.org/licenses/gpl-3.0.html">GPL-3.0</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= translate(locale, "source") %></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="/js/video.min.js">video.min.js</a>
|
<a href="/js/video.min.js">video.min.js</a>
|
||||||
|
|
Loading…
Reference in a new issue