From 3c7a5a201a119258324e98a2032155d71d27e09b Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Fri, 28 Jan 2022 13:49:17 +0100 Subject: [PATCH] Implement custom html multi video element (makes it possible to merge and control multiple videos as one tag) --- .../CustomHtmlElements/multi-video.ts | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 api/js/etemplate/CustomHtmlElements/multi-video.ts diff --git a/api/js/etemplate/CustomHtmlElements/multi-video.ts b/api/js/etemplate/CustomHtmlElements/multi-video.ts new file mode 100644 index 0000000000..cf5d4b03da --- /dev/null +++ b/api/js/etemplate/CustomHtmlElements/multi-video.ts @@ -0,0 +1,397 @@ +/** + * 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; + } + } + + /** + * init/update video tags + * @param _value + * @private + */ + private __buildVideoTags (_value) + { + let value = _value.split(','); + let video = null; + 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.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); \ No newline at end of file