import { WebVTTThumbnail } from "./WebVTTThumbnail.js";
import { WebVTTThumbnailUtils } from "./WebVTTThumbnailUtils.js";
import { IntervalTimeout, TimeConversion } from '../utils/helperTools.js'
export { createCoverOverlay, TextOverlay, ThumbnailPositionOnElementHover, ThumbnailLoopOnImageHover, ThumbnailPositionOnElementHoverViaTimestamp, ThumbnailPositionOnImageHover, ThumbnailPositionOnSeekbarHover };
/**
* Create an overlay to a cover image providing media related information like duration and resolution.
* @param {Object} target - Target element to attach overlay as child element.
* @param {Object} dataSourceElement - Element that provides the information via data attributes.
*/
function createCoverOverlay(target, dataSourceElement) {
const div = document.createElement('div');
div.className = "video-cover-text-container";
div.innerHTML = '<i class="overlay-item fas fa-film"></i>';
new TextOverlay(div, dataSourceElement, 'data-text-duration');
new TextOverlay(div, dataSourceElement, 'data-text-resolution');
target.append(div);
}
/**
* Create a text overlay.
* @author ckraemme <ckraemme@awi.de>
* @private
*/
class TextOverlay {
/**
* Append data attributes as text overlay to a target element.
* @param {Object} target - Target element to append text overlay as child.
* @param {Object} dataSourceElement - Element that contains data attributes.
* @param {RegExp} dataSourceSelector - Regex expression to select the data attribute.
*/
constructor(target, dataSourceElement, dataSourceSelector) {
this.target = target;
this.dataSourceElement = dataSourceElement;
this.dataSourceSelector = dataSourceSelector;
this.addOverlayTextElements_();
}
/**
* Get specific data attributes in an HTML element.
* @param {Object} target - Target element with data attributes.
* @param {RegExp} regexSelector - Regex expression to select the data attribute.
* @returns {Array} Contains the filtered attributes.
*/
static filterAttributesByName(target, regexSelector) {
const re = new RegExp(regexSelector);
let collectAttributes = target.attributes;
const filteredAttributes = [...collectAttributes].filter(item => re.test(item.name));
return filteredAttributes
}
/**
* For each filtered data attribute, create a overlay text.
* @private
*/
addOverlayTextElements_() {
const textDataAttributes = TextOverlay.filterAttributesByName(this.dataSourceElement, this.dataSourceSelector);
textDataAttributes.forEach((item) => {
this.addOverlayText_(item);
});
}
/**
* Create the overlay text element and append it as child to the target element.
* @param {String} item - Specific data attribute.
* @private
*/
addOverlayText_(item) {
const re = new RegExp(this.dataSourceSelector);
const attributeName = item.name;
const attributeValue = item.value;
if (attributeValue) {
const textElement = document.createElement('span');
textElement.className = "overlay-item " + attributeName.replace(re, 'overlay-text-');
textElement.innerText = attributeValue;
textElement.style.cursor = 'default';
this.target.appendChild(textElement);
}
}
}
/**
* Show thumbnails of a webVTT file relative to the position of an element that is hovered.
* @author ckraemme <ckraemme@awi.de>
* @private
*/
class ThumbnailPositionOnElementHover {
/**
* Provide a mouse controlled thumbnail playback. Construct the object and attach all required event listeners to the element.
* @param {Object} hoverElement - Hover element where event listeners should be attached.
* @param {Object} targetElement - Target element that should contain timestamp and canvas element.
* @param {Number} canvasWidth - Canvas width in pixel.
* @param {Number} canvasHeight - Canvas height in pixel.
* @param {String} vttUrl - URL to the webVTT file.
* @param {Object} timestampTextElement - Text element to show timestamps as inner text.
* @param {String} timestampTextOnPointerLeave - Timestamp text that is shown when the pointer leaves the element.
* @param {Number} showNextThumbnailEveryNthPixel - Skip thumbnails that are to close in order to avoid a too sensitive mouse pointer.
*/
constructor(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement = null, timestampTextOnPointerLeave, showNextThumbnailEveryNthPixel = 10) {
this.manifestData, this.canvasElement;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.vttUrl = vttUrl;
this.targetElement = targetElement;
this.showNextThumbnailEveryNthPixel = showNextThumbnailEveryNthPixel;
this.stateToPreventDuplicateBuilds = 0;
this.hoverElement = hoverElement;
this.timestampTextElement = timestampTextElement;
this.timestampTextOnPointerLeave = timestampTextOnPointerLeave;
this.hoverElement.addEventListener('pointerenter', this.coverPointerEnter_.bind(this));
this.hoverElement.addEventListener('pointerleave', this.coverPointerLeave_.bind(this));
}
/**
* Read the manifest file and attach an event lister when the element is hovered.
* @param {Event} event - Pointerenter event.
* @private
*/
coverPointerEnter_(event) {
if (this.stateToPreventDuplicateBuilds === 0) {
this.stateToPreventDuplicateBuilds = 1;
let webVTT = new WebVTTThumbnail(this.vttUrl);
webVTT.init(-1, false).then(() => {
this.manifestData = webVTT.manifestData;
this.coverPointerHover_(event);
this.pointerMoveListener = this.coverPointerHover_.bind(this);
this.hoverElement.addEventListener('pointermove', this.pointerMoveListener);
})
}
}
/**
* Create or update the thumbnail frame in the canvas and timestamp.
* @param {Event} event - Pointermove event.
* @private
*/
coverPointerHover_(event) {
const reducedManifestObjectNumber = Math.round(event.target.clientWidth / this.showNextThumbnailEveryNthPixel);
const manifestDataReduced = WebVTTThumbnailUtils.reduceManifestEvenlyDistributed(this.manifestData, reducedManifestObjectNumber);
const [manifestDataAtTimestamp,] = WebVTTThumbnailUtils.getManifestObjectOnHover(event, manifestDataReduced);
this.generateThumbnail_(manifestDataAtTimestamp, manifestDataAtTimestamp['time_start']);
}
generateThumbnail_(manifestDataAtTimestamp, timestampTextElementText) {
if (typeof this.canvasElement == 'undefined')
this.canvasElement = ThumbnailCanvas.createThumbnailCanvas(this.targetElement);
if (this.timestampTextElement != null)
this.timestampTextElement.innerText = timestampTextElementText;
ThumbnailCanvas.paintThumbnail(manifestDataAtTimestamp, this.canvasElement, this.canvasWidth, this.canvasHeight);
}
/**
* Remove all attached elements and events.
* @param {Event} event - Pointerleave event.
* @private
*/
coverPointerLeave_(event) {
if (this.stateToPreventDuplicateBuilds === 1) {
if (ThumbnailCanvas.removeThumbnailCanvas(this.targetElement)) {
this.canvasElement = undefined;
this.hoverElement.removeEventListener('pointermove', this.pointerMoveListener);
if (this.timestampTextElement != null && this.timestampTextOnPointerLeave != null)
this.timestampTextElement.innerText = this.timestampTextOnPointerLeave;
this.stateToPreventDuplicateBuilds = 0;
}
else
setTimeout(() => { this.coverPointerLeave_(event) }, 100);
}
}
}
/**
* Overwrite the default ThumbnailPositionOnElementHover class and replace the referenced element.
* Here, the seek bar is used to show thumbnails referenced in a webVTT file. It is based on the position of a pointer that hovers the seek bar.
* @extends ThumbnailPositionOnElementHover
* @author ckraemme <ckraemme@awi.de>
*/
class ThumbnailPositionOnSeekbarHover extends ThumbnailPositionOnElementHover {
/**
* Note: This class is based on class inheritance.
* Only new initialized or removed parameters are described here.
* Observe: showNextThumbnailEveryNthPixel can be omitted, as it is not used in the coverPointerHover_() method defined here.
*/
constructor(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement = null, timestampTextOnPointerLeave) {
super(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement, timestampTextOnPointerLeave);
}
/**
* Overwrites ThumbnailPositionOnElementHover.coverPointerHover_() due to the required seek bar specificity.
* @param {Event} event - Pointermove event.
* @private
*/
coverPointerHover_(event) {
const isSeekbar = true;
const [manifestDataAtTimestamp, currentTimeHHMMSSms] = WebVTTThumbnailUtils.getManifestObjectOnHover(event, this.manifestData, isSeekbar);
this.generateThumbnail_(manifestDataAtTimestamp, currentTimeHHMMSSms.split('.')[0]);
}
}
/**
* Overwrite the default ThumbnailPositionOnElementHover class and replace the referenced element.
* Here, a time element is used to show thumbnails referenced in a webVTT file. It is based on the inner text of an html element that indicates the time when hovering the seek bar.
* @extends ThumbnailPositionOnElementHover
* @author ckraemme <ckraemme@awi.de>
*/
class ThumbnailPositionOnElementHoverViaTimestamp extends ThumbnailPositionOnElementHover {
/**
* Note: This class is based on class inheritance.
* Only new initialized or removed parameters are described here.
* Observe: showNextThumbnailEveryNthPixel can be omitted, as it is not used in the coverPointerHover_() method defined here.
* @param {Object} timeElementSelector - Selector of time element that contains the timestamp as innerText.
*/
constructor(hoverElement, targetElement, timeElementSelector, canvasWidth, canvasHeight, vttUrl, timestampTextElement = null, timestampTextOnPointerLeave) {
// observe: timeElementSelector as additional input argument
super(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement, timestampTextOnPointerLeave);
this.timeElementSelector = timeElementSelector;
}
/**
* Overwrites ThumbnailPositionOnElementHover.coverPointerHover_() due to the required seek bar specificity.
* @param {Event} event - Pointermove event.
* @private
*/
coverPointerHover_(event) {
const delayReadingOfTimeElement_ms = 20; // wait until the time element changed - could be replaced by a mutation observer in the future
setTimeout(() => {
const currentTimeHHMMSS = document.querySelector(this.timeElementSelector)?.innerText;
const currentTimeHHMMSSms = TimeConversion.time2IsoDayTime(currentTimeHHMMSS)
const manifestDataAtTimestamp = WebVTTThumbnailUtils.getManifestObjectViaTimestamp(currentTimeHHMMSSms, this.manifestData);
this.generateThumbnail_(manifestDataAtTimestamp, currentTimeHHMMSSms.split('.')[0]);
}, delayReadingOfTimeElement_ms);
}
}
/**
* Show thumbnails referenced in a webVTT file relative to the hover position of an image element.
* @extends ThumbnailPositionOnElementHover
* @author ckraemme <ckraemme@awi.de>
*/
class ThumbnailPositionOnImageHover extends ThumbnailPositionOnElementHover {
/**
* Provide a mouse controlled thumbnail playback. Construct the object and attach all required event listeners to the image element.
* @param {Object} imageElement - Image element where event listeners should be attached.
* @param {String} imageElementDataAttributeWebVTTUrl - Name of the data attribute in the image element.
* @param {Object} timestampTextElement - Text element to show timestamps as inner text.
* @param {String} timestampTextOnPointerLeave - Timestamp text that is shown when the pointer leaves the element.
*/
constructor(imageElement, imageElementDataAttributeWebVTTUrl = "data-src-webvtt", timestampTextElement = null, timestampTextOnPointerLeave) {
const vttUrl = imageElement.getAttribute(imageElementDataAttributeWebVTTUrl);
const imageContainer = imageElement.parentElement;
const coverContainer = document.createElement('div');
coverContainer.className = 'cover-container';
imageContainer.appendChild(coverContainer);
const canvasWidth = imageElement.width;
const canvasHeight = imageElement.height;
const hoverElement = coverContainer;
super(hoverElement, coverContainer, canvasWidth, canvasHeight, vttUrl, timestampTextElement, timestampTextOnPointerLeave);
}
}
/**
* Play thumbnails referenced in a webVTT file in a loop on top of an image element that is hovered.
* @author ckraemme <ckraemme@awi.de>
*/
class ThumbnailLoopOnImageHover {
/**
* Create a canvas element on top of an image element where thumbnails are shown when the image is hovered.
* @param {Object} imageElement - Image element that provides the url to the webVTT file as data attribute.
* @param {string} imageElementDataAttributeWebVTTUrl - Name of the data attribute in the image element.
* @param {Number} timeIntervalMilliseconds - Time interval between thumbnail images. Allows playback speed control.
*/
constructor(imageElement, imageElementDataAttributeWebVTTUrl = "data-src-webvtt", timeIntervalMilliseconds = 100) {
this.playback;
this.timeIntervalMilliseconds = timeIntervalMilliseconds;
this.stateToPreventDuplicateBuilds = 0;
this.imageElement = imageElement;
this.vttUrl = imageElement.getAttribute(imageElementDataAttributeWebVTTUrl);
const coverContainer = imageElement.parentElement;
coverContainer.addEventListener('mouseenter', this.coverMouseEnter_.bind(this));
coverContainer.addEventListener('mouseleave', this.coverMouseLeave_.bind(this));
}
/**
* Play a loop of thumbnail images provided via a webVTT file.
* @param {Object} imageElement - Image element where thumbnail loop is attached as sibling.
* @param {String} vttUrl - URL to the webVTT file.
* @param {Number} currentFrame - Current thumbnail frame. Starts with zero.
* @param {Number} timeIntervalMilliseconds - Time interval between thumbnail images. Allows playback speed control.
* @param {Boolean} reversePlayback - If true play thumbnails in reverse order.
* @param {Object} timestampTextElement - Text element to show timestamps as inner text.
* @param {String} timestampTextOnRemove - Timestamp text that is shown when the thumbnail loop is removed.
*/
static playThumbnailLoop(imageElement, vttUrl, currentFrame = 0, timeIntervalMilliseconds = 100, reversePlayback = false, timestampTextElement = null, timestampTextOnRemove) {
let webVTT = new WebVTTThumbnail(vttUrl);
return webVTT.init(-1, false).then(() => {
return new ThumbnailLoop(imageElement.parentElement, webVTT.manifestData, imageElement.width, imageElement.height, currentFrame, timeIntervalMilliseconds, reversePlayback, timestampTextElement, timestampTextOnRemove);
});
}
/**
* Start thumbnail playback loop.
* @param {Event} event - Pointerenter event.
* @private
*/
coverMouseEnter_(event) {
if (this.stateToPreventDuplicateBuilds === 0) {
this.stateToPreventDuplicateBuilds = 1;
const currentFrame = 0;
ThumbnailLoopOnImageHover.playThumbnailLoop(this.imageElement, this.vttUrl, currentFrame, this.timeIntervalMilliseconds).then(playback => this.playback = playback);
}
}
/**
* Stop and destroy thumbnail playback loop.
* @param {Event} event - Pointerleave event.
* @private
*/
coverMouseLeave_(event) {
if (this.stateToPreventDuplicateBuilds === 1) {
if (typeof this.playback !== 'undefined') {
this.playback.remove();
this.stateToPreventDuplicateBuilds = 0;
}
else
setTimeout(() => { this.coverMouseLeave_(event) }, 1000);
}
}
}
/**
* Create an thumbnail loop and provide methods for controlling the playback.
* @author ckraemme <ckraemme@awi.de>
* @private
*/
class ThumbnailLoop {
/**
* Create a thumbnail loop playback.
* @param {Object} targetElement - Target element.
* @param {Object} manifestData - Manifest data object providing all thumbnail related information.
* @param {Number} containerWidth - Container width in pixel.
* @param {Number} containerHeight - Container height in pixel.
* @param {Number} currentFrame - Current thumbnail frame. Starts with zero.
* @param {Number} timeIntervalMilliseconds - Time interval between thumbnail images. Allows playback speed control.
* @param {Boolean} reversePlayback - If true play thumbnails in reverse order.
* @param {Object} timestampTextElement - Text element to show timestamps as inner text.
* @param {String} timestampTextOnRemove - Timestamp text that is shown when the thumbnail loop is removed.
*/
constructor(targetElement, manifestData, containerWidth, containerHeight, currentFrame = 0, timeIntervalMilliseconds = 100, reversePlayback = false, timestampTextElement = null, timestampTextOnRemove) {
this.targetElement = targetElement;
this.manifestData = manifestData;
this.containerWidth = containerWidth;
this.containerHeight = containerHeight;
this.currentFrame = currentFrame;
this.timeIntervalMilliseconds = timeIntervalMilliseconds;
this.reversePlayback = reversePlayback;
this.timestampTextElement = timestampTextElement;
this.timestampTextOnRemove = timestampTextOnRemove;
this.canvasElement = ThumbnailCanvas.createThumbnailCanvas(targetElement);
this.intervalTimer = new IntervalTimeout(this.looping_.bind(this), timeIntervalMilliseconds);
this.intervalTimer.setInterval();
}
/**
* Create the thumbnail playback loop effect by controlling and painting the frames.
* @private
*/
looping_() {
this.intervalTimer.delayMS = this.timeIntervalMilliseconds;
const boundary = false;
if (this.reversePlayback === false)
this.skipFrame(1, boundary);
else
this.skipFrame(-1, boundary);
this.paintCurrentFrame();
}
/**
* Paint current thumbnail frame to the canvas.
*/
paintCurrentFrame() {
ThumbnailCanvas.paintThumbnail(this.manifestData[this.currentFrame], this.canvasElement, this.containerWidth, this.containerHeight);
if (this.timestampTextElement != null)
this.timestampTextElement.innerText = this.manifestData[this.currentFrame]['time_start'];
}
/**
* Pause thumbnail loop playback.
*/
pause() {
this.intervalTimer.clearInterval();
this.intervalTimer.timeoutID = null;
}
/**
* Pause thumbnail loop playback.
* @param {Number} numberOfFrames - Amount of frames that should be skipped starting from the current playback position (can be positive and negative).
* @param {Boolean} boundary - If true, limit the amount of skipped frames to the number of available frames. Else, start at the other end like in a loop.
*/
skipFrame(numberOfFrames, boundary = true) {
var newPosition;
if (boundary) {
const firstBoundaryValue = Math.max(0, this.currentFrame + numberOfFrames);
newPosition = Math.min(firstBoundaryValue, this.manifestData.length - 1)
}
else {
newPosition = (this.currentFrame + numberOfFrames) % this.manifestData.length
if (newPosition < 0)
newPosition = this.manifestData.length + newPosition;
}
this.currentFrame = newPosition;
}
/**
* Resume thumbnail loop playback.
*/
resume() {
if (this.intervalTimer.timeoutID == null)
this.intervalTimer.setInterval();
}
/**
* Remove thumbnail loop object containing all child elements.
* @param {Boolean} removeTargetCompletely - Indicates to remove target element too.
*/
remove(removeTargetCompletely = false) {
if (ThumbnailCanvas.removeThumbnailCanvas(this.targetElement)) {
if (removeTargetCompletely)
this.targetElement.remove();
this.intervalTimer.clearInterval();
if (this.timestampTextElement != null && this.timestampTextOnRemove != null)
this.timestampTextElement.innerText = this.timestampTextOnRemove;
}
else
setTimeout(() => { this.remove(removeTargetCompletely) }, 100);
}
}
/**
* Class that provides tools to create, paint images on and remove canvas in the context of the webVTT thumbnails.
* @author ckraemme <ckraemme@awi.de>
*/
class ThumbnailCanvas {
/**
* Create canvas element, set default canvas settings and append it to a target element.
* @param {Object} targetElement - Target element to append canvas as child.
* @returns {Object} Canvas element.
*/
static createThumbnailCanvas(targetElement) {
const canvasElement = document.createElement('canvas');
const context = canvasElement.getContext('2d');
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "high";
targetElement.append(canvasElement);
return canvasElement
}
/**
* Remove the first canvas element found in a target element.
* @param {Object} targetElement - Target element that contain a canvas.
* @returns {Boolean} Return true when canvas could be removed, else false.
*/
static removeThumbnailCanvas(targetElement) {
const canvas = targetElement.querySelector('canvas');
if (canvas)
canvas.remove();
else
return false
return true
}
/**
* Paint the image from a thumbnail object to a canvas element.
* @param {Object} thumbnailObject - Thumbnail object, i.e., Object { time_start: "00:00:00.000", time_end: "00:00:00.280", data: "thumb.jpeg#xywh=0,0,250,141", url: "https://localhost/Desktop/marine-data/thumbnail-hover/thumb.jpeg", image: img }.
* @param {Object} canvasElement - Target canvas element.
* @param {Object} canvasWidth - Canvas width in pixel.
* @param {Object} canvasHeight - Canvas height in pixel.
*/
static paintThumbnail(thumbnailObject, canvasElement, canvasWidth, canvasHeight) {
const imageSprite = thumbnailObject.data.includes('#xywh')
if (imageSprite)
var [x, y, frameWidth, frameHeight] = thumbnailObject.data.split('#xywh=')[1].split(',');
else
var [x, y, frameWidth, frameHeight] = [0, 0, thumbnailObject.image.width, thumbnailObject.image.height];
if (canvasHeight === undefined)
canvasHeight = canvasWidth * frameHeight / frameWidth;
if (canvasWidth === undefined)
canvasWidth = canvasHeight * frameWidth / frameHeight;
canvasElement.width = canvasWidth;
canvasElement.height = canvasHeight;
const context = canvasElement.getContext('2d');
context.clearRect(0, 0, canvasElement.width, canvasElement.height);
context.drawImage(thumbnailObject.image, x, y, frameWidth, frameHeight, 0, 0, canvasWidth, canvasHeight);
}
}