Skip to content

Instantly share code, notes, and snippets.

@Jorenm
Created February 25, 2020 18:50
Show Gist options
  • Save Jorenm/b70cfa53eee9391cd3f0b8810f621931 to your computer and use it in GitHub Desktop.
Save Jorenm/b70cfa53eee9391cd3f0b8810f621931 to your computer and use it in GitHub Desktop.
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['video.js'], factory);
} else if (typeof exports !== "undefined") {
factory(require('video.js'));
} else {
var mod = {
exports: {}
};
factory(global.videojs);
global.videojsMarkers = mod.exports;
}
})(this, function (_video) {
/*! videojs-markers - v1.0.1 - 2018-10-31
* Copyright (c) 2018 ; Licensed */
'use strict';
var _video2 = _interopRequireDefault(_video);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
// default setting
var defaultSetting = {
markerStyle: {
'width': '7px',
'border-radius': '30%',
'background-color': 'red'
},
markerTip: {
display: true,
text: function text(marker) {
return marker.text;
},
time: function time(marker) {
return marker.time;
}
},
breakOverlay: {
display: false,
displayTime: 3,
text: function text(marker) {
return "Break overlay: " + marker.overlayText;
},
style: {
'width': '100%',
'height': '20%',
'background-color': 'rgba(0,0,0,0.7)',
'color': 'white',
'font-size': '17px'
}
},
onMarkerClick: function onMarkerClick(marker) {},
onMarkerReached: function onMarkerReached(marker, index) {},
markers: []
};
// create a non-colliding random number
function generateUUID() {
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : r & 0x3 | 0x8).toString(16);
});
return uuid;
};
/**
* Returns the size of an element and its position
* a default Object with 0 on each of its properties
* its return in case there's an error
* @param {Element} element el to get the size and position
* @return {DOMRect|Object} size and position of an element
*/
function getElementBounding(element) {
var elementBounding;
var defaultBoundingRect = {
top: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
right: 0
};
try {
elementBounding = element.getBoundingClientRect();
} catch (e) {
elementBounding = defaultBoundingRect;
}
return elementBounding;
}
var NULL_INDEX = -1;
function registerVideoJsMarkersPlugin(options) {
// copied from video.js/src/js/utils/merge-options.js since
// videojs 4 doens't support it by defualt.
if (!_video2.default.mergeOptions) {
var isPlain = function isPlain(value) {
return !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && toString.call(value) === '[object Object]' && value.constructor === Object;
};
var mergeOptions = function mergeOptions(source1, source2) {
var result = {};
var sources = [source1, source2];
sources.forEach(function (source) {
if (!source) {
return;
}
Object.keys(source).forEach(function (key) {
var value = source[key];
if (!isPlain(value)) {
result[key] = value;
return;
}
if (!isPlain(result[key])) {
result[key] = {};
}
result[key] = mergeOptions(result[key], value);
});
});
return result;
};
_video2.default.mergeOptions = mergeOptions;
}
if (!_video2.default.dom.createEl) {
_video2.default.dom.createEl = function (tagName, props, attrs) {
var el = _video2.default.Player.prototype.dom.createEl(tagName, props);
if (!!attrs) {
Object.keys(attrs).forEach(function (key) {
el.setAttribute(key, attrs[key]);
});
}
return el;
};
}
/**
* register the markers plugin (dependent on jquery)
*/
var setting = _video2.default.mergeOptions(defaultSetting, options),
markersMap = {},
markersList = [],
// list of markers sorted by time
currentMarkerIndex = NULL_INDEX,
player = this,
markerTip = null,
breakOverlay = null,
overlayIndex = NULL_INDEX;
function sortMarkersList() {
// sort the list by time in asc order
markersList.sort(function (a, b) {
return setting.markerTip.time(a) - setting.markerTip.time(b);
});
}
function addMarkers(newMarkers) {
newMarkers.forEach(function (marker) {
marker.key = generateUUID();
player.el().querySelector('.vjs-progress-holder').appendChild(createMarkerDiv(marker));
// store marker in an internal hash map
markersMap[marker.key] = marker;
markersList.push(marker);
});
sortMarkersList();
}
function getPosition(marker) {
return setting.markerTip.time(marker) / player.duration() * 100;
}
function setMarkderDivStyle(marker, markerDiv) {
markerDiv.className = 'vjs-marker ' + (marker.class || "");
Object.keys(setting.markerStyle).forEach(function (key) {
markerDiv.style[key] = setting.markerStyle[key];
});
// hide out-of-bound markers
var ratio = marker.time / player.duration();
if (ratio < 0 || ratio > 1) {
markerDiv.style.display = 'none';
}
// set position
markerDiv.style.left = getPosition(marker) + '%';
if (marker.duration) {
markerDiv.style.width = marker.duration / player.duration() * 100 + '%';
markerDiv.style.marginLeft = '0px';
} else {
var markerDivBounding = getElementBounding(markerDiv);
markerDiv.style.marginLeft = markerDivBounding.width / 2 + 'px';
}
}
function createMarkerDiv(marker) {
var markerDiv = _video2.default.dom.createEl('div', {}, {
'data-marker-key': marker.key,
'data-marker-time': setting.markerTip.time(marker)
});
setMarkderDivStyle(marker, markerDiv);
// bind click event to seek to marker time
markerDiv.addEventListener('click', function (e) {
var preventDefault = false;
if (typeof setting.onMarkerClick === "function") {
// if return false, prevent default behavior
preventDefault = setting.onMarkerClick(marker) === false;
}
if (!preventDefault) {
var key = this.getAttribute('data-marker-key');
player.currentTime(setting.markerTip.time(markersMap[key]));
}
});
if (setting.markerTip.display) {
registerMarkerTipHandler(markerDiv);
}
return markerDiv;
}
function updateMarkers(force) {
// update UI for markers whose time changed
markersList.forEach(function (marker) {
var markerDiv = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']");
var markerTime = setting.markerTip.time(marker);
if (force || markerDiv.getAttribute('data-marker-time') !== markerTime) {
setMarkderDivStyle(marker, markerDiv);
markerDiv.setAttribute('data-marker-time', markerTime);
}
});
sortMarkersList();
}
function removeMarkers(indexArray) {
// reset overlay
if (!!breakOverlay) {
overlayIndex = NULL_INDEX;
breakOverlay.style.visibility = "hidden";
}
currentMarkerIndex = NULL_INDEX;
var deleteIndexList = [];
indexArray.forEach(function (index) {
var marker = markersList[index];
if (marker) {
// delete from memory
delete markersMap[marker.key];
deleteIndexList.push(index);
// delete from dom
var el = player.el().querySelector(".vjs-marker[data-marker-key='" + marker.key + "']");
el && el.parentNode.removeChild(el);
}
});
// clean up markers array
deleteIndexList.reverse();
deleteIndexList.forEach(function (deleteIndex) {
markersList.splice(deleteIndex, 1);
});
// sort again
sortMarkersList();
}
// attach hover event handler
function registerMarkerTipHandler(markerDiv) {
markerDiv.addEventListener('mouseover', function () {
var marker = markersMap[markerDiv.getAttribute('data-marker-key')];
if (!!markerTip) {
if (setting.markerTip.html) {
markerTip.querySelector('.vjs-tip-inner').innerHTML = setting.markerTip.html(marker);
} else {
markerTip.querySelector('.vjs-tip-inner').innerText = setting.markerTip.text(marker);
}
// margin-left needs to minus the padding length to align correctly with the marker
markerTip.style.left = getPosition(marker) + '%';
var markerTipBounding = getElementBounding(markerTip);
var markerDivBounding = getElementBounding(markerDiv);
markerTip.style.marginLeft = -parseFloat(markerTipBounding.width / 2) + parseFloat(markerDivBounding.width / 4) + 'px';
markerTip.style.visibility = 'visible';
}
});
markerDiv.addEventListener('mouseout', function () {
if (!!markerTip) {
markerTip.style.visibility = "hidden";
}
});
}
function initializeMarkerTip() {
markerTip = _video2.default.dom.createEl('div', {
className: 'vjs-tip',
innerHTML: "<div class='vjs-tip-arrow'></div><div class='vjs-tip-inner'></div>"
});
player.el().querySelector('.vjs-progress-holder').appendChild(markerTip);
}
// show or hide break overlays
function updateBreakOverlay() {
if (!setting.breakOverlay.display || currentMarkerIndex < 0) {
return;
}
var currentTime = player.currentTime();
var marker = markersList[currentMarkerIndex];
var markerTime = setting.markerTip.time(marker);
if (currentTime >= markerTime && currentTime <= markerTime + setting.breakOverlay.displayTime) {
if (overlayIndex !== currentMarkerIndex) {
overlayIndex = currentMarkerIndex;
if (breakOverlay) {
breakOverlay.querySelector('.vjs-break-overlay-text').innerHTML = setting.breakOverlay.text(marker);
}
}
if (breakOverlay) {
breakOverlay.style.visibility = "visible";
}
} else {
overlayIndex = NULL_INDEX;
if (breakOverlay) {
breakOverlay.style.visibility = "hidden";
}
}
}
// problem when the next marker is within the overlay display time from the previous marker
function initializeOverlay() {
breakOverlay = _video2.default.dom.createEl('div', {
className: 'vjs-break-overlay',
innerHTML: "<div class='vjs-break-overlay-text'></div>"
});
Object.keys(setting.breakOverlay.style).forEach(function (key) {
if (breakOverlay) {
breakOverlay.style[key] = setting.breakOverlay.style[key];
}
});
player.el().appendChild(breakOverlay);
overlayIndex = NULL_INDEX;
}
function onTimeUpdate() {
onUpdateMarker();
updateBreakOverlay();
options.onTimeUpdateAfterMarkerUpdate && options.onTimeUpdateAfterMarkerUpdate();
}
function onUpdateMarker() {
/*
check marker reached in between markers
the logic here is that it triggers a new marker reached event only if the player
enters a new marker range (e.g. from marker 1 to marker 2). Thus, if player is on marker 1 and user clicked on marker 1 again, no new reached event is triggered)
*/
if (!markersList.length) {
return;
}
var getNextMarkerTime = function getNextMarkerTime(index) {
if (index < markersList.length - 1) {
return setting.markerTip.time(markersList[index + 1]);
}
// next marker time of last marker would be end of video time
return player.duration();
};
var currentTime = player.currentTime();
var newMarkerIndex = NULL_INDEX;
if (currentMarkerIndex !== NULL_INDEX) {
// check if staying at same marker
var nextMarkerTime = getNextMarkerTime(currentMarkerIndex);
if (currentTime >= setting.markerTip.time(markersList[currentMarkerIndex]) && currentTime < nextMarkerTime) {
return;
}
// check for ending (at the end current time equals player duration)
if (currentMarkerIndex === markersList.length - 1 && currentTime === player.duration()) {
return;
}
}
// check first marker, no marker is selected
if (currentTime < setting.markerTip.time(markersList[0])) {
newMarkerIndex = NULL_INDEX;
} else {
// look for new index
for (var i = 0; i < markersList.length; i++) {
nextMarkerTime = getNextMarkerTime(i);
if (currentTime >= setting.markerTip.time(markersList[i]) && currentTime < nextMarkerTime) {
newMarkerIndex = i;
break;
}
}
}
// set new marker index
if (newMarkerIndex !== currentMarkerIndex) {
// trigger event if index is not null
if (newMarkerIndex !== NULL_INDEX && options.onMarkerReached) {
options.onMarkerReached(markersList[newMarkerIndex], newMarkerIndex);
}
currentMarkerIndex = newMarkerIndex;
}
}
// setup the whole thing
function initialize() {
if (setting.markerTip.display) {
initializeMarkerTip();
}
// remove existing markers if already initialized
player.markers.removeAll();
addMarkers(setting.markers);
if (setting.breakOverlay.display) {
initializeOverlay();
}
onTimeUpdate();
player.on("timeupdate", onTimeUpdate);
player.off("loadedmetadata");
}
// setup the plugin after we loaded video's meta data
player.on("loadedmetadata", function () {
initialize();
});
// exposed plugin API
player.markers = {
getMarkers: function getMarkers() {
return markersList;
},
next: function next() {
// go to the next marker from current timestamp
var currentTime = player.currentTime();
for (var i = 0; i < markersList.length; i++) {
var markerTime = setting.markerTip.time(markersList[i]);
if (markerTime > currentTime) {
player.currentTime(markerTime);
break;
}
}
},
prev: function prev() {
// go to previous marker
var currentTime = player.currentTime();
for (var i = markersList.length - 1; i >= 0; i--) {
var markerTime = setting.markerTip.time(markersList[i]);
// add a threshold
if (markerTime + 0.5 < currentTime) {
player.currentTime(markerTime);
return;
}
}
},
add: function add(newMarkers) {
// add new markers given an array of index
addMarkers(newMarkers);
},
remove: function remove(indexArray) {
// remove markers given an array of index
removeMarkers(indexArray);
},
removeAll: function removeAll() {
var indexArray = [];
for (var i = 0; i < markersList.length; i++) {
indexArray.push(i);
}
removeMarkers(indexArray);
},
// force - force all markers to be updated, regardless of if they have changed or not.
updateTime: function updateTime(force) {
// notify the plugin to update the UI for changes in marker times
updateMarkers(force);
},
reset: function reset(newMarkers) {
// remove all the existing markers and add new ones
player.markers.removeAll();
addMarkers(newMarkers);
},
destroy: function destroy() {
// unregister the plugins and clean up even handlers
player.markers.removeAll();
breakOverlay && breakOverlay.remove();
markerTip && markerTip.remove();
player.off("timeupdate", updateBreakOverlay);
delete player.markers;
}
};
}
window.initVideojsMarkers = function(player) {
var plugin = videojs.registerPlugin('markers', registerVideoJsMarkersPlugin.bind(player));
plugin({});
}
});
//# sourceMappingURL=videojs-markers.js.map
@Jorenm
Copy link
Author

Jorenm commented Feb 25, 2020

Markers plugin modified to work with newer versions of videojs.

Init plugin with:
window.initVideojsMarkers(videojs(yourplayerid))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment