Add annotation player
This addresses issue #110 from master. Yay for adding annotations back!
This commit is contained in:
		
							
								
								
									
										81
									
								
								assets/css/videojs-youtube-annotations.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								assets/css/videojs-youtube-annotations.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| .__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; | ||||
| } | ||||
							
								
								
									
										975
									
								
								assets/js/videojs-youtube-annotations.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										975
									
								
								assets/js/videojs-youtube-annotations.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,975 @@ | ||||
| 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); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 glmdgrielson
					glmdgrielson