/**
* EGroupware Custom Html Elements - Multi Video Web Components
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link https://www.egroupware.org
* @author Hadi Nategh
* @copyright EGroupware GmbH
*/
// Unfortunately compiled version of module:ES2015 TS would break webcomponent constructor.
/*
This web component allows to merge multiple videos and display them as one single widget/element
most of the html video attributes and methodes are supported. No controls attribute supported yet.
*/
// Create a class for the element
class multi_video extends HTMLElement {
/**
* shadow dom container
* @private
*/
_shadow = null;
/**
* wrapper container holds video tags
* @private
*/
/**
* contains video objects of type VideoTagsArray
* @private
*/
_videos = [];
/**
* keeps duration time internally
* @private
*/
_duration = 0;
/**
* keeps currentTime internally
* @private
*/
_currentTime = 0;
/**
* Keeps video playing state internally
* @private
*/
__playing = false;
constructor() {
super(); // Create a shadow root
this._shadow = this.attachShadow({
mode: 'open'
}); // Create videos wrapper
this._wrapper = document.createElement('div');
this._wrapper.setAttribute('class', 'wrapper'); // Create some CSS to apply to the shadow dom
this._style = document.createElement('style');
this._style.textContent = '.wrapper {' + 'width: 100%;' + 'height: auto;' + 'display: block;' + '}' + '.wrapper video {' + 'width: 100%;' + 'height: auto;' + '}'; // attach to the shadow dom
this._shadow.appendChild(this._style);
this._shadow.appendChild(this._wrapper);
}
/**
* set observable attributes
* @return {string[]}
*/
static get observedAttributes() {
return ['src', 'type'];
}
/**
* Gets called on observable attributes changes
* @param name attribute name
* @param _
* @param newVal new value
*/
attributeChangedCallback(name, _, newVal) {
switch (name) {
case 'src':
this.__buildVideoTags(newVal);
break;
case 'type':
this._videos.forEach(_item => {
_item.node.setAttribute('type', newVal);
});
break;
}
}
/**
* init/update video tags
* @param _value
* @private
*/
__buildVideoTags(_value) {
let value = _value.split(',');
let video = null;
for (let i = 0; i < value.length; i++) {
video = document.createElement('video');
video.src = value[i];
this._videos[i] = {
node: this._wrapper.appendChild(video),
loadedmetadata: false,
timeupdate: false,
duration: 0,
previousDurations: 0,
currentTime: 0,
active: false,
index: i
}; // loadmetadata event
this._videos[i]['node'].addEventListener("loadedmetadata", function (_i, _event) {
this._videos[_i]['loadedmetadata'] = true;
this.__check_loadedmetadata();
}.bind(this, i)); //timeupdate event
this._videos[i]['node'].addEventListener("timeupdate", function (_i, _event) {
this._currentTime = this._videos[i]['previousDurations'] + _event.target.currentTime; // push the next video to start otherwise the time update gets paused as it ends automatically
// with the current video being ended
if (this._videos[i].node.ended && !this.ended) this.currentTime = this._currentTime + 0.1;
this.__pushEvent('timeupdate');
}.bind(this, i));
}
}
/**
* calculates duration of videos
* @return {number} returns accumulated durations
* @private
*/
__duration() {
let duration = 0;
this._videos.forEach(_item => {
duration += _item.duration;
});
return duration;
}
/**
* Get current active video
* @return {*[]} returns an array of object consist of current video displayed node
* @private
*/
__getActiveVideo() {
return this._videos.filter(_item => {
return _item.active;
});
}
/**
* check if all meta data from videos are ready then pushes the event
* @private
*/
__check_loadedmetadata() {
let allReady = true;
this._videos.forEach(_item => {
allReady = allReady && _item.loadedmetadata;
});
if (allReady) {
this._videos.forEach(_item => {
_item.duration = _item.node.duration;
_item.previousDurations = _item.index > 0 ? this._videos[_item.index - 1]['duration'] + this._videos[_item.index - 1]['previousDurations'] : 0;
});
this.duration = this.__duration();
this.currentTime = 0;
this.__pushEvent('loadedmetadata');
}
}
/**
* Creates event and dispatches it
* @param _name
*/
__pushEvent(_name) {
let event = document.createEvent("Event");
event.initEvent(_name, true, true);
this.dispatchEvent(event);
}
/**************************** PUBLIC ATTRIBUTES & METHODES *************************************************/
/****************************** ATTRIBUTES **************************************/
/**
* set src
* @param _value
*/
set src(_value) {
let value = _value.split(',');
Array.from(this._wrapper.children).forEach(_ch => {
_ch.remove();
});
this.__buildVideoTags(value);
}
/**
* get src
* @return string returns comma separated sources
*/
get src() {
return this.src;
}
/**
* currentTime
* @param _time
*/
set currentTime(_time) {
let ctime = _time;
this._currentTime = _time;
this._videos.forEach(_item => {
if (ctime < _item.duration + _item.previousDurations && (ctime == 0 && _item.previousDurations == 0 || ctime > _item.previousDurations)) {
if (this.__playing && _item.node.paused) _item.node.play();
_item.currentTime = Math.abs(_item.previousDurations - ctime);
_item.node.currentTime = _item.currentTime;
_item.active = true;
} else {
_item.active = false;
_item.node.pause();
}
_item.node.hidden = !_item.active;
});
}
/**
* get currentTime
* @return {number}
*/
get currentTime() {
return this._currentTime;
}
/**
* set video duration time attribute
* @param _value
*/
set duration(_value) {
this._duration = _value;
}
/**
* get video duration time
*/
get duration() {
return this._duration;
}
/**
* get paused attribute
*/
get paused() {
return this.__getActiveVideo()[0] ? (this.__getActiveVideo()[0].node ? this.__getActiveVideo()[0].node.paused : undefined) : undefined;
}
/**
* set muted attribute
* @param _value
*/
set muted(_value) {
this._videos.forEach(_item => {
_item.node.muted = _value;
});
}
/**
* get muted attribute
*/
get muted() {
return this.__getActiveVideo()[0] ? (this.__getActiveVideo()[0].node ? this.__getActiveVideo()[0].node.muted : undefined) : undefined;;
}
/**
* get video ended attribute
*/
get ended() {
return this._videos[this._videos.length - 1] ? (this._videos[this._videos.length - 1].node ? this._videos[this._videos.length - 1].node.ended : undefined) : undefined;
}
/**
* set playbackRate
* @param _value
*/
set playbackRate(_value) {
this._videos.forEach(_item => {
_item.node.playbackRate = _value;
});
}
/**
* get playbackRate
*/
get playbackRate() {
return this.__getActiveVideo()[0] ? (this.__getActiveVideo()[0].node ? this.__getActiveVideo()[0].node.playbackRate : undefined) : undefined;
}
/**
* set volume
*/
set volume(_value) {
this._videos.forEach(_item => {
_item.node.volume = _value;
});
}
/**
* get volume
*/
get volume() {
return this.__getActiveVideo()[0] ? (this.__getActiveVideo()[0].node ? this.__getActiveVideo()[0].node.volume : undefined) : undefined;
}
/************************************************* METHODES ******************************************/
/**
* Play video
*/
play() {
this.__playing = true;
return this.__getActiveVideo()[0] ? (this.__getActiveVideo()[0].node ? this.__getActiveVideo()[0].node.play() : undefined) : undefined;
}
/**
* pause video
*/
pause() {
this.__playing = false;
this.__getActiveVideo()[0] ? (this.__getActiveVideo()[0].node ? this.__getActiveVideo()[0].node.pause() : undefined) : undefined;
}
} // Define the new multi-video element
customElements.define('multi-video', multi_video);