/** * 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 */ /* 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. */ type VideoTagsArray = Array<{ node: HTMLVideoElement; loadedmetadata: boolean; timeupdate: boolean; duration: number; previousDurations: number; currentTime: number; active: boolean; index: number }>; // Create a class for the element class multi_video extends HTMLElement { /** * shadow dom container * @private */ private readonly _shadow = null; /** * wrapper container holds video tags * @private */ private readonly _wrapper : HTMLDivElement; /** * Styling contianer * @private */ private readonly _style : HTMLStyleElement; /** * contains video objects of type VideoTagsArray * @private */ private _videos : VideoTagsArray = []; /** * keeps duration time internally * @private */ private _duration : number = 0; /** * keeps currentTime internally * @private */ private _currentTime : number = 0; /** * Keeps video playing state internally * @private */ private __playing: boolean = 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; } } /** * Calls load method for all its sub videos * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/load */ load() { this._videos.forEach(_item =>{_item.node.load()}); } /** * init/update video tags * @param _value * @private */ private __buildVideoTags (_value) { let value = _value.split(','); let video = null; let duration = 0; for (let i=0;i { duration += _item.duration; }); return duration; } /** * Get current active video * @return {*[]} returns an array of object consist of current video displayed node * @private */ private __getActiveVideo() { return this._videos.filter(_item=>{ return (_item.active); }); } /** * check if all meta data from videos are ready then pushes the event * @private */ private __check_loadedmetadata () { let allReady = true; this._videos.forEach((_item) => { allReady = allReady && _item.loadedmetadata; }); if (allReady) { this._videos.forEach(_item => { _item.duration = _item.duration ? _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 */ private __pushEvent(_name: string) { 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(','); 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 : number) { 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 : number) { this._duration = _value; } /** * get video duration time */ get duration() { return this._duration; } /** * get paused attribute */ get paused() { return this.__getActiveVideo()[0]?.node?.paused; } /** * set muted attribute * @param _value */ set muted(_value: boolean) { this._videos.forEach(_item => { _item.node.muted = _value; }); } /** * get muted attribute */ get muted() { return this.__getActiveVideo()[0]?.node?.muted; } /** * get video ended attribute */ get ended() { return this._videos[this._videos.length-1]?.node?.ended; } /** * set playbackRate * @param _value */ set playbackRate(_value: number) { this._videos.forEach(_item => { _item.node.playbackRate = _value; }); } /** * get playbackRate */ get playbackRate() { return this.__getActiveVideo()[0]?.node?.playbackRate; } /** * set volume */ set volume(_value: number) { this._videos.forEach(_item => { _item.node.volume = _value; }); } /** * get volume */ get volume() { return this.__getActiveVideo()[0]?.node?.volume; } /************************************************* METHODES ******************************************/ /** * Play video */ play() { this.__playing = true; return this.__getActiveVideo()[0]?.node?.play(); } /** * pause video */ pause() { this.__playing = false; this.__getActiveVideo()[0]?.node?.pause(); } } // Define the new multi-video element customElements.define('multi-video', multi_video);