UNPKG

ng-plyr

Version:

An HTML5 media player for developers, built using Angular, having interface similar to Youtube player.

582 lines 172 kB
import { DOCUMENT } from '@angular/common'; import { Component, EventEmitter, HostListener, Inject, Input, Output, ViewChild } from '@angular/core'; import { Media } from './models/media.model'; import { Playlist, PlaylistItem } from './models/playlist.model'; import * as i0 from "@angular/core"; import * as i1 from "./services/ng-plyr.service"; import * as i2 from "./services/cast.service"; import * as i3 from "@angular/common"; import * as i4 from "./directive/stop-click-propagation.directive"; export class NgPlyrComponent { constructor(document, _plyrService, _castService, _cd) { this.document = document; this._plyrService = _plyrService; this._castService = _castService; this._cd = _cd; // Flags this.isPlaying = false; this.isMuted = false; this.isFullscreen = false; this.isPIP = false; this.isCasting = false; // isAutoplayEnabled = false; this.isLoopingEnabled = false; this.isControlSettingsOpen = false; this.isMenuSettingsOpen = false; // Variables this.seekStepInSec = 5; this.playerVolume = 1; this.currentTime = '0:00'; this.totalTime = '0:00'; this.progressPercent = 0; this.mediaBuffers = []; this.isMediaLoading = true; this.playlist = new Playlist(); this.playingTrack = 0; // TODO: this.mediaMarkers = []; // Inputs this.mediaURL = ''; this.preload = 'none'; this.enableControls = true; // Output events this.playing = new EventEmitter(); this.paused = new EventEmitter(); this.ended = new EventEmitter(); this.onprev = new EventEmitter(); this.onnext = new EventEmitter(); this.fullscreen = new EventEmitter(); this.volumechange = new EventEmitter(); // Seek backward on mouseclick this.bwdClickCount = 0; this.bwdClickTimeout = setTimeout(() => this.bwdClickCount = 0, 500); // Seek forward on mouseclick this.fwdClickCount = 0; this.fwdClickTimeout = setTimeout(() => this.fwdClickCount = 0, 500); } // Lifecycle hooks // OnChanges LC hook ngOnChanges(changes) { this.inpChanges = changes; // When both mediaURL & mediaItems are present, media will be set from mediaItems if (changes['mediaURL'] && !changes['mediaItems']) { this.changeMedia(); } if (changes['mediaItems']) { this.createPlaylist(this.mediaItems); } // nextMediaToAdd after playlist is initialized, or not present in same changes if (changes['nextMediaToAdd'] && !changes['mediaItems']) { this.onNextMediaInput(); } this.mediaMarkers = changes['bookmarks']?.currentValue; this.isLoopingEnabled = changes['loopMedia']?.currentValue; } ngOnInit() { this._plyrService.addComponentRef(this); } ngAfterViewInit() { this.isPlaying = !this.video.nativeElement.paused; this.isMuted = this.video.nativeElement.muted; this.isFullscreen = this.document.fullscreenElement ? true : false; this.document.addEventListener('fullscreenchange', () => { if (!this.document.fullscreenElement) this.isFullscreen = false; }); } ngOnDestroy() { this._plyrService.removeComponentRef(); } resetPlayer() { this.currentTime = '0:00'; this.totalTime = '0:00'; this.progressPercent = 0; this.mediaBuffers = []; this.isMediaLoading = true; this.mediaMarkers = []; } changeMedia(media) { this.resetPlayer(); this.media = media ? media : new Media(this.mediaURL, this.mediaType); if (this.isCasting) this._castService.loadMedia(this.media.src); } // Playlist functions // Create new playlist / reinitialize playlist createPlaylist(mediaItems) { if (mediaItems) { this.playlist.initializePlaylist(mediaItems); this.media = this.playlist.current?.media; if (this.nextMediaToAdd) { this.playlist.addNext(new PlaylistItem(this.nextMediaToAdd)); } this.nextMedia = this.playlist.current?.next?.media; } } // nextMedia will be used to play next inboth src and playlist cases onNextMediaInput() { if (this.playlist.current && this.nextMediaToAdd) { this.playlist.addNext(new PlaylistItem(this.nextMediaToAdd)); } this.nextMedia = this.nextMediaToAdd; } // Output events for media onPlay(event) { this.playing.emit(true); this.paused.emit(false); this.isPlaying = true; this.media.paused = false; if (this.isCasting) this.video.nativeElement.pause(); } onPause(event) { this.playing.emit(false); this.paused.emit(true); this.media.paused = true; if (!this.isCasting) this.isPlaying = false; } onEnd(event) { this.ended.emit(true); if (this.isAutoplayEnabled) { this.playNextMedia(); } } onVolumeChange(event) { this.volumechange.emit({ level: this.video.nativeElement.volume, muted: this.video.nativeElement.muted }); } // TODO: Emit custom error onError(event) { console.error(event); } // Shortcut keys doShortcutKeyAction(event) { if (this.enableControls === false) return; const tagName = this.document.activeElement.tagName.toLowerCase(); if (tagName === 'input') return; const key = event.key.toLowerCase(); if ((key === ' ' && tagName != 'button') || key === 'k') { event.preventDefault(); this.togglePlay(); } else if (key === 'm') { this.toggleMute(); } else if (key === 'f') { this.toggleFullscreen(); } else if (key === 'i') { this.togglePIP(); } else if (key === 'arrowleft') { this.seekTo(Math.max(0, this.video.nativeElement.currentTime - 5)); } else if (key === 'arrowright') { this.seekTo(Math.min(this.video.nativeElement.duration, this.video.nativeElement.currentTime + 5)); } else if (key === 'arrowup') { this.changeVolume((this.playerVolume * 100 + 5) / 100); } else if (key === 'arrowdown') { const vol = (this.playerVolume * 100 - 5) / 100; this.changeVolume(vol < 0 ? 0 : vol); } else if (key === '0' || key === 'home') { this.seekTo(0); } else if (Number(key) > 0 && Number(key) < 10 && this.media?.duration) { this.seekTo(this.media.duration * Number(key) / 10); } else if (key === 'end') { this.stopVideo(); } else if (event.key === 'N') { this.playNextMedia(); } else if (event.key === 'P') { this.playPrevMedia(); } else if (event.key === '<') { this.setPlaybackSpeed(this.video.nativeElement.playbackRate - .25); } else if (event.key === '>') { this.setPlaybackSpeed(this.video.nativeElement.playbackRate + .25); } } // Volume control changeVolume(value) { value = Number(value).toPrecision(2); if (!this.video || value > 1 || value < 0) return; if (this.isCasting) { // Not yet working this._castService.setVolumeLevel(value); } else { this.video.nativeElement.volume = value; this.playerVolume = value; if (this.video.nativeElement.muted && value > 0) { this.toggleMute(); } } } // Update video metadata in player updateAfterVideoInfoLoaded() { if (this.inpChanges && this.inpChanges["mediaURL"]) { this.video.nativeElement.currentTime = 0; this.media.playFrom = this.playFrom; this.media.duration = this.video.nativeElement.duration ? this.video.nativeElement.duration : this.video.nativeElement.currentTime; this.media.captions = this.captions; } if (this.inpChanges && this.inpChanges['volume']?.currentValue) { this.changeVolume(this.inpChanges['volume'].currentValue); } if (this.inpChanges && this.inpChanges['playFrom']?.currentValue) { this.seekTo(this.inpChanges['playFrom'].currentValue); } if (this.isAutoplayEnabled) this.video.nativeElement.play(); this.isPlaying = !this.video.nativeElement.paused; this.currentTime = this.formatDuration(this.media.playFrom); this.totalTime = this.formatDuration(this.media.duration); this.showBuffers(); } // Display video buffers on timeline showBuffers() { const buf = this.video.nativeElement.buffered; if (buf.length === 1 && buf.end(0) - buf.start(0) === this.video.nativeElement.duration) { this.isMediaLoading = false; return; } this.isMediaLoading = true; this.mediaBuffers = []; for (let i = 0; i < buf.length; i++) { let start = Number((buf.start(i) / this.video.nativeElement.duration).toPrecision(3)) * 100; let end = Number((buf.end(i) / this.video.nativeElement.duration).toPrecision(3)) * 100; this.mediaBuffers.push({ start, end }); if (this.video.nativeElement.currentTime >= buf.start(i) && this.video.nativeElement.currentTime <= buf.end(i)) { this.isMediaLoading = false; } } } // Seek to specific time seekTo(atSecond) { if (!this.video) return; if (atSecond > this.video.nativeElement.duration) { this.stopVideo(); return; } else if (Number(atSecond) < 0) { this.video.nativeElement.currentTime = 0; this._castService.seekTo(0); } else { this.video.nativeElement.currentTime = Number(atSecond).toPrecision(3); this._castService.seekTo(Number(atSecond)); } this.progressPercent = Number((this.video.nativeElement.currentTime / this.video.nativeElement.duration).toPrecision(3)) * 100; } // Call seekTo fn in specific direction and set count to 0 seekAfterTimeout(direction) { if (direction === 'fwd') { this.seekTo(this.video.nativeElement.currentTime + this.seekStepInSec * this.fwdClickCount); this.fwdClickCount = 0; } else if (direction === 'bwd') { this.seekTo(this.video.nativeElement.currentTime - this.seekStepInSec * this.bwdClickCount); this.bwdClickCount = 0; } } seekBwd(e) { if (e.type === 'dblclick') { this.bwdClickCount = 1; clearTimeout(this.bwdClickTimeout); this.bwdClickTimeout = setTimeout(() => this.seekAfterTimeout('bwd'), 500); } else if (e.type === 'click' && this.bwdClickCount > 0) { this.bwdClickCount++; clearTimeout(this.bwdClickTimeout); this.bwdClickTimeout = setTimeout(() => this.seekAfterTimeout('bwd'), 500); } } seekFwd(e) { if (e.type === 'dblclick') { this.fwdClickCount = 1; clearTimeout(this.fwdClickTimeout); this.fwdClickTimeout = setTimeout(() => this.seekAfterTimeout('fwd'), 500); } else if (e.type === 'click' && this.fwdClickCount > 0) { this.fwdClickCount++; clearTimeout(this.fwdClickTimeout); this.fwdClickTimeout = setTimeout(() => this.seekAfterTimeout('fwd'), 500); } } updateCurrentTime() { this.currentTime = this.formatDuration(this.video.nativeElement.currentTime); this.progressPercent = Number((this.video.nativeElement.currentTime / this.video.nativeElement.duration).toPrecision(3)) * 100; this.showBuffers(); } // Play previous media from Q playPrevMedia() { if (this.isLoopingEnabled) return this.onprev.emit(this.media); let mediaToPlay; if (this.playlist.current?.hasPrev()) { this.playlist.current = this.playlist.current.prev; mediaToPlay = this.playlist.current?.media; // Update prevMedia if (this.playlist.current?.hasPrev()) { this.prevMedia = this.playlist.current.prev.media; } else if (this.loopPlaylist) { this.prevMedia = this.playlist.tail?.media; } } else if (this.loopPlaylist) { this.playlist.current = this.playlist.tail; mediaToPlay = this.playlist.current?.media; this.prevMedia = this.playlist.current?.prev?.media; } else if (this.prevMedia) { mediaToPlay = this.prevMedia; this.prevMedia = undefined; } this.nextMedia = this.media; this.changeMedia(mediaToPlay); this.onprev.emit(mediaToPlay); } // Play next media from Q playNextMedia() { if (this.isLoopingEnabled) return this.onnext.emit(this.media); let mediaToPlay; if (this.playlist.current?.hasNext()) { this.playlist.current = this.playlist.current.next; mediaToPlay = this.playlist.current?.media; // Updating nextMedia if (this.playlist.current?.hasNext()) { this.nextMedia = this.playlist.current.next.media; } else if (this.loopPlaylist) { this.nextMedia = this.playlist.head?.media; } } else if (this.loopPlaylist) { this.playlist.current = this.playlist.head; mediaToPlay = this.playlist.current?.media; this.nextMedia = this.playlist.current?.next?.media; } else if (this.nextMedia) { mediaToPlay = this.nextMedia; this.nextMedia = undefined; } this.prevMedia = this.media; this.changeMedia(mediaToPlay); this.onnext.emit(mediaToPlay); } // End/Stop current video stopVideo() { this.video.nativeElement.pause(); this.seekTo(this.video.nativeElement.duration); this._castService.stopCasting(); } setPlaybackSpeed(rate) { if (rate < 0.25 || rate > 2) return; this.video.nativeElement.playbackRate = rate; } // Initialize Cast and requestSession castToChromecast() { // Check if the Cast SDK is available and initialized if (this._castService.isSdkAvailable()) { // Initialize Cast API this._castService.initializeCastApi(); // Get the CastContext and show the Cast dialog to the user let castContext = this._castService.getCastContext(); // Starts media casting after session starts castContext.requestSession().then((session) => { // Add event listeners this.listenToPlayerEvents(); this.enableCastingMode(); // load media in receiver this._castService.loadMedia(this.media.src, 'video/mp4'); }).catch((err) => { console.error(err); }); } else { console.warn('Cast SDK is not available or not initialized.'); } } // After successful connection, use this method to change mode to casting enableCastingMode() { this.isCasting = true; // Stop playing on local device this.video.nativeElement.pause(); this.isMuted = this._castService.player.isMuted; this.isPlaying = !this._castService.player.isPaused; this.playerVolume = this._castService.player.volumeLevel.toPrecision(2); } // Stop casting or just stop session endCurrentSession(stopCasting) { this._castService.endCurrentSession(stopCasting); } // Listening to remote events listenToPlayerEvents() { // Storing local player states in local variables to restore after receiver disconnects let isMutedLocal = this.isMuted; let playerVolumeLocal = this.playerVolume; // Cast Connected/disconnected this._castService.onCastEvent('IS_CONNECTED_CHANGED', () => { if (this._castService.player.isConnected) { console.log('Cast Player connected'); // Stop playing on local device this.enableCastingMode(); } else { console.log('Cast Player disconnected'); this.isCasting = false; this._castService.stopListeningRemotePlayerEvents(); // Start playing on local device this.seekTo(this._castService.player.currentTime); this.isPlaying ? this.video.nativeElement.play() : this.video.nativeElement.pause(); // Setting local UI to original state this.isMuted = isMutedLocal; this.playerVolume = playerVolumeLocal; } }).then((res) => console.log(res)) .catch((err) => console.error(err)); // Pause/Play this._castService.onCastEvent('IS_PAUSED_CHANGED', () => { if (this._castService.player.isPaused) { console.log('Receiver paused'); this.isPlaying = false; } else { console.log('Receiver playing'); this.isPlaying = true; } }).then((res) => console.log(res)) .catch((err) => console.error(err)); // MediaInfo changes this._castService.onCastEvent('PLAYER_STATE_CHANGED', () => { // console.log('PlayerState:', this._castService.player.playerState); if (this._castService.player.playerState === 'IDLE') { this.onEnd(); this._castService.play(); this.isPlaying = true; } }).then((res) => console.log(res)) .catch((err) => console.error(err)); // Paying break this._castService.onCastEvent('IS_PLAYING_BREAK_CHANGED', () => { if (this._castService.player.isPlayingBreak) { console.log('Player is playing break'); // Change on local device this.video.nativeElement.pause(); // Show Playing break, in UI } else { console.log('Player break ended'); // Change on local device } }).then((res) => console.log(res)) .catch((err) => console.error(err)); // currentTime changed this._castService.onCastEvent('CURRENT_TIME_CHANGED', () => { // Change current time on local device let currentTime = this._castService.player.currentTime; this.progressPercent = Number((currentTime / this.video.nativeElement.duration).toPrecision(3)) * 100; this.currentTime = this.formatDuration(currentTime); // Update in UI on currentTime changes this._cd.detectChanges(); }).then((res) => console.log(res)) .catch((err) => console.error(err)); // volumeLevel changed this._castService.onCastEvent('VOLUME_LEVEL_CHANGED', () => { // Change volume level in UI this.playerVolume = this._castService.player.volumeLevel.toPrecision(2); // Update in UI on volume changes this._cd.detectChanges(); }).then((res) => console.log(res)) .catch((err) => console.error(err)); // Muted/unmuted this._castService.onCastEvent('IS_MUTED_CHANGED', () => { // Change mute icon in UI this.isMuted = this._castService.player.isMuted; }).then((res) => console.log(res)) .catch((err) => console.error(err)); } // Utility functions formatDuration(time) { const sec = Math.floor(time % 60); const min = Math.floor(time / 60) % 60; const hr = Math.floor(time / 3600); if (hr > 0) { return `${hr}:${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`; } else { return `${min}:${sec < 10 ? '0' + sec : sec}`; } } // Toggles toggleLoop() { this.isLoopingEnabled = !this.isLoopingEnabled; } togglePlay() { if (this.enableControls === false) return; if (!this.isCasting) { if (this.video.nativeElement.currentTime === this.video.nativeElement.duration) { this.video.nativeElement.currentTime = 0; } this.video.nativeElement.paused ? this.video.nativeElement.play() : this.video.nativeElement.pause(); } else { // Play/pause receiver this.isPlaying ? this._castService.pause() : this._castService.play(); } } toggleMute() { if (!this.isCasting) { this.video.nativeElement.muted = !this.video.nativeElement.muted; this.isMuted = this.video.nativeElement.muted; } else { // mute/unmute receiver this.isMuted ? this._castService.unmute() : this._castService.mute(); } } togglePIP() { if (this.isPIP) { this.document.exitPictureInPicture(); this.isPIP = false; } else { this.video.nativeElement.requestPictureInPicture(); this.isPIP = true; } } toggleFullscreen() { if (this.document.fullscreenElement) { this.document.exitFullscreen(); this.isFullscreen = false; this.fullscreen.emit(false); } else { this.videoContainer.nativeElement.requestFullscreen(); this.isFullscreen = true; this.fullscreen.emit(true); } } } NgPlyrComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.0", ngImport: i0, type: NgPlyrComponent, deps: [{ token: DOCUMENT }, { token: i1.PlayerService }, { token: i2.CastService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); NgPlyrComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.2.0", type: NgPlyrComponent, selector: "ng-plyr", inputs: { mediaURL: ["src", "mediaURL"], mediaType: ["type", "mediaType"], preload: "preload", playFrom: "playFrom", loadingImgSrc: "loadingImgSrc", captions: "captions", bookmarks: "bookmarks", volume: "volume", loopMedia: ["loop", "loopMedia"], isAutoplayEnabled: ["autoplay", "isAutoplayEnabled"], nextMediaToAdd: ["nextMedia", "nextMediaToAdd"], mediaItems: ["playlist", "mediaItems"], loopPlaylist: "loopPlaylist", posterUrl: ["poster", "posterUrl"], enableControls: ["controls", "enableControls"] }, outputs: { playing: "playing", paused: "paused", ended: "ended", onprev: "onprev", onnext: "onnext", fullscreen: "fullscreen", volumechange: "volumechange" }, host: { listeners: { "window:keydown": "doShortcutKeyAction($event)" } }, viewQueries: [{ propertyName: "videoContainer", first: true, predicate: ["videoContainer"], descendants: true }, { propertyName: "video", first: true, predicate: ["video"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div #videoContainer class=\"video-container\" [ngClass]=\"isPlaying ? '' : 'paused'\">\r\n <div class=\"video-header-container\" *ngIf=\"enableControls\">\r\n <button class=\"menu-icon-btn\" title=\"Menu\" (click)=\"isMenuSettingsOpen = !isMenuSettingsOpen\">\r\n <svg class=\"menu-icon\" viewBox=\"0 -960 960 960\" *ngIf=\"!isPlaying\">\r\n <path fill=\"currentColor\" d=\"M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z\"/>\r\n </svg>\r\n </button>\r\n <span class=\"title\">\r\n {{media.title}}\r\n </span>\r\n <div class=\"menu-box\" [ngClass]=\"isMenuSettingsOpen ? '': 'hidden'\">\r\n <!-- <input type=\"file\" name=\"Select file to play\" (change)=\"onLocalFileSelected($event)\"> -->\r\n <button (click)=\"stopVideo()\">Stop Playing</button>\r\n </div>\r\n </div>\r\n <div class=\"video-overlay\" (click)=\"togglePlay()\" *ngIf=\"enableControls\">\r\n <div stopClickPropagation class=\"loading\"\r\n [ngStyle]=\"{'background-image': loadingImgSrc ? 'url(' + loadingImgSrc + ')':'../assets/images/Spinner-multicolor.svg'}\"\r\n *ngIf=\"isMediaLoading\">\r\n <!-- <img [src]=\"loadingImgSrc\"> -->\r\n </div>\r\n <div class=\"video-actions\" *ngIf=\"!isMediaLoading && media.paused\">\r\n <button stopClickPropagation class=\"play-prev-btn\" title=\"Previous\" (click)=\"playPrevMedia()\" [disabled]=\"!prevMedia\">\r\n <svg class=\"play-prev-icon\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\r\n <path d=\"M6 6h2v12H6zm3.5 6l8.5 6V6z\" />\r\n </svg>\r\n </button>\r\n <button stopClickPropagation class=\"play-btn\" (click)=\"togglePlay()\">\r\n <svg class=\"play-icon\" viewBox=\"0 0 24 24\" *ngIf=\"!isCasting\">\r\n <path fill=\"currentColor\" d=\"M8,5.14V19.14L19,12.14L8,5.14Z\" />\r\n </svg>\r\n <span *ngIf=\"isCasting\">\r\n <svg viewBox=\"0 -960 960 960\" title=\"Playing on TV\" *ngIf=\"isPlaying\">\r\n <path fill=\"currentColor\" d=\"M200-320h80q0-33-23.5-56.5T200-400v80Zm142 0h58q0-83-58.5-141.5T200-520v58q59 0 100.5 41.5T342-320Zm120 0h58q0-66-25-124.5t-68.5-102Q383-590 324.5-615T200-640v58q109 0 185.5 76.5T462-320ZM320-120v-80H160q-33 0-56.5-23.5T80-280v-480q0-33 23.5-56.5T160-840h640q33 0 56.5 23.5T880-760v480q0 33-23.5 56.5T800-200H640v80H320ZM160-280h640v-480H160v480Zm0 0v-480 480Z\"/>\r\n </svg>\r\n <svg class=\"cast-pause-icon\" viewBox=\"0 -960 960 960\" title=\"Casting paused\" *ngIf=\"!isPlaying\">\r\n <path fill=\"currentColor\" d=\"M750-640h40v-160h-40v160Zm-100 0h40v-160h-40v160ZM480-480ZM80-280q50 0 85 35t35 85H80v-120Zm0-160q117 0 198.5 81.5T360-160h-80q0-83-58.5-141.5T80-360v-80Zm0-160q91 0 171 34.5T391-471q60 60 94.5 140T520-160h-80q0-75-28.5-140.5t-77-114q-48.5-48.5-114-77T80-520v-80Zm720 440H600q0-20-1.5-40t-4.5-40h206v-212q22-7 42-16.5t38-22.5v251q0 33-23.5 56.5T800-160ZM80-680v-40q0-33 23.5-56.5T160-800h292q-6 19-9 39t-3 41H160v46q-20-3-40-4.5T80-680Zm640 160q-83 0-141.5-58.5T520-720q0-83 58.5-141.5T720-920q83 0 141.5 58.5T920-720q0 83-58.5 141.5T720-520Z\"/>\r\n </svg>\r\n </span>\r\n </button>\r\n <button stopClickPropagation class=\"play-next-btn\" title=\"Next\" (click)=\"playNextMedia()\" [disabled]=\"!nextMedia\">\r\n <svg class=\"play-next-icon\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\r\n <path d=\"M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z\" />\r\n </svg>\r\n </button>\r\n </div>\r\n <div class=\"seek\" *ngIf=\"!isMediaLoading && !media.paused\">\r\n <button stopClickPropagation class=\"seek-back-btn\" (click)=\"seekBwd($event)\" (dblclick)=\"seekBwd($event)\">\r\n <div [ngStyle]=\"{'display': bwdClickCount>0 ? 'block' : 'none'}\">\r\n <div>{{bwdClickCount * seekStepInSec}} seconds</div>\r\n <svg class=\"seek-back-icon\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\r\n <path d=\"M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z\" />\r\n </svg>\r\n </div>\r\n </button>\r\n <button stopClickPropagation class=\"seek-ahead-btn\" (click)=\"seekFwd($event)\" (dblclick)=\"seekFwd($event)\">\r\n <div [ngStyle]=\"{'display': fwdClickCount>0 ? 'block' : 'none'}\">\r\n <div>{{fwdClickCount * seekStepInSec}} seconds</div>\r\n <svg class=\"seek-ahead-icon\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\r\n <path d=\"M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z\" />\r\n </svg>\r\n </div> \r\n </button>\r\n </div>\r\n </div>\r\n <div class=\"video-controls-container\" *ngIf=\"enableControls\">\r\n <div class=\"timeline-container\">\r\n <img src=\"\" class=\"preview-img\">\r\n <input #progressBar type=\"range\" class=\"timeline-progress\" (input)=\"seekTo(progressBar.value)\"\r\n [value]=\"media.playFrom\" [max]=\"media.duration\">\r\n <div class=\"track\">\r\n <div class=\"track-progress\" [ngStyle]=\"{'width':progressPercent + '%'}\"></div>\r\n <div class=\"track-buffers\">\r\n <span class=\"buffer\" *ngFor=\"let buffer of mediaBuffers\"\r\n [ngStyle]=\"{'left':buffer.start + '%', 'width':buffer.end - buffer.start + '%'}\"></span>\r\n </div>\r\n <div class=\"track-markers\">\r\n <span class=\"marker\" *ngFor=\"let marker of mediaMarkers\" [ngStyle]=\"{'left':marker + '%'}\"></span>\r\n </div>\r\n </div>\r\n <div class=\"thumb\" [ngStyle]=\"{'left':progressPercent + '%', 'transform': 'translate(-'+ progressPercent + '%,-50%)'}\"></div>\r\n </div>\r\n <div class=\"controls\">\r\n <button class=\"play-pause-btn\" [title]=\"isPlaying ? 'Pause (k)' : 'Play (k)'\" (click)=\"togglePlay()\">\r\n <svg class=\"play-icon\" viewBox=\"0 0 24 24\" *ngIf=\"!isPlaying\">\r\n <path fill=\"currentColor\" d=\"M8,5.14V19.14L19,12.14L8,5.14Z\" />\r\n </svg>\r\n <svg class=\"pause-icon\" viewBox=\"0 0 24 24\" *ngIf=\"isPlaying\">\r\n <path fill=\"currentColor\" d=\"M14,19H18V5H14M6,19H10V5H6V19Z\" />\r\n </svg>\r\n </button>\r\n <button class=\"play-next-btn\" title=\"Next\" (click)=\"playNextMedia()\" [disabled]=\"!nextMedia\">\r\n <svg class=\"play-next-icon\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\r\n <path d=\"M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z\" />\r\n </svg>\r\n </button>\r\n <div class=\"volume-container\">\r\n <button class=\"volume-btn\" [title]=\"isMuted ? 'Unmute (m)' : 'Mute (m)'\" (click)=\"toggleMute()\">\r\n <svg class=\"volume-high-icon\" viewBox=\"0 0 24 24\"\r\n *ngIf=\"!isMuted && playerVolume>.5 && playerVolume<=1\">\r\n <path fill=\"currentColor\"\r\n d=\"M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z\" />\r\n </svg>\r\n <svg class=\"volume-low-icon\" viewBox=\"0 0 24 24\"\r\n *ngIf=\"!isMuted && playerVolume>0 && playerVolume<=.5\">\r\n <path fill=\"currentColor\"\r\n d=\"M5,9V15H9L14,20V4L9,9M18.5,12C18.5,10.23 17.5,8.71 16,7.97V16C17.5,15.29 18.5,13.76 18.5,12Z\" />\r\n </svg>\r\n <svg class=\"volume-muted-icon\" viewBox=\"0 0 24 24\" *ngIf=\"isMuted || playerVolume==0\">\r\n <path fill=\"currentColor\"\r\n d=\"M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,10.23 15.5,8.71 14,7.97V10.18L16.45,12.63C16.5,12.43 16.5,12.21 16.5,12Z\" />\r\n </svg>\r\n </button>\r\n <input #volumeInput class=\"volume-slider\" type=\"range\" (input)=\"changeVolume(volumeInput.value)\"\r\n [value]=\"playerVolume\" min=\"0\" max=\"1\" step=\"any\">\r\n </div>\r\n <div class=\"duration-container\">\r\n <span class=\"current-time\">{{currentTime}}</span>\r\n /\r\n <span class=\"total-time\">{{totalTime}}</span>\r\n </div>\r\n <span class=\"right-side-btns\">\r\n <button class=\"loop-btn\" (click)=\"toggleLoop()\"\r\n [title]=\"isLoopingEnabled ? 'Looping one': loopPlaylist ? 'Looping all' : 'Looping off'\">\r\n <svg title=\"Loop off\" viewBox=\"0 0 24 24\" *ngIf=\"!loopPlaylist && !isLoopingEnabled\">\r\n <path fill=\"currentColor\" d=\"M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z\" />\r\n </svg>\r\n <svg title=\"Loop one\" viewBox=\"0 0 48 48\" *ngIf=\"loopPlaylist && !isLoopingEnabled\">\r\n <path fill=\"currentColor\" d=\"m14 44-8-8 8-8 2.1 2.2-4.3 4.3H35v-8h3v11H11.8l4.3 4.3Zm-4-22.5v-11h26.2l-4.3-4.3L34 4l8 8-8 8-2.1-2.2 4.3-4.3H13v8Z\"/>\r\n </svg>\r\n <svg title=\"Loop all\" viewBox=\"0 0 48 48\" *ngIf=\"isLoopingEnabled\">\r\n <path fill=\"currentColor\" d=\"m14 44-8-8 8-8 2.1 2.2-4.3 4.3H35v-8h3v11H11.8l4.3 4.3Zm9.3-14.1v-9.45h-2.8V18h5.25v11.9ZM10 21.5v-11h26.2l-4.3-4.3L34 4l8 8-8 8-2.1-2.2 4.3-4.3H13v8Z\"/>\r\n </svg>\r\n </button>\r\n <button class=\"settings-btn\" title=\"Settings\" (click)=\"isControlSettingsOpen = !isControlSettingsOpen\">\r\n <svg class=\"settings-icon\" viewBox=\"0 -960 960 960\">\r\n <path fill=\"currentColor\" d=\"m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm112-260q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Z\"/>\r\n </svg>\r\n <div class=\"menu-box\" [ngClass]=\"isControlSettingsOpen ? '': 'hidden'\">\r\n <button stopClickPropagation class=\"loop-btn\" (click)=\"toggleLoop()\"\r\n [title]=\"isLoopingEnabled ? 'Looping one': loopPlaylist ? 'Looping all' : 'Looping off'\">\r\n <svg title=\"Loop off\" viewBox=\"0 0 24 24\" *ngIf=\"!loopPlaylist && !isLoopingEnabled\">\r\n <path fill=\"currentColor\" d=\"M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z\" />\r\n </svg>\r\n <svg title=\"Loop one\" viewBox=\"0 0 48 48\" *ngIf=\"loopPlaylist && !isLoopingEnabled\">\r\n <path fill=\"currentColor\" d=\"m14 44-8-8 8-8 2.1 2.2-4.3 4.3H35v-8h3v11H11.8l4.3 4.3Zm-4-22.5v-11h26.2l-4.3-4.3L34 4l8 8-8 8-2.1-2.2 4.3-4.3H13v8Z\"/>\r\n </svg>\r\n <svg title=\"Loop all\" viewBox=\"0 0 48 48\" *ngIf=\"isLoopingEnabled\">\r\n <path fill=\"currentColor\" d=\"m14 44-8-8 8-8 2.1 2.2-4.3 4.3H35v-8h3v11H11.8l4.3 4.3Zm9.3-14.1v-9.45h-2.8V18h5.25v11.9ZM10 21.5v-11h26.2l-4.3-4.3L34 4l8 8-8 8-2.1-2.2 4.3-4.3H13v8Z\"/>\r\n </svg>\r\n <span>Loop {{isLoopingEnabled ? 'all': loopPlaylist ? 'one' : 'off'}}</span>\r\n </button>\r\n <button stopClickPropagation title=\"Change playback speed\" (click)=\"setPlaybackSpeed(video.playbackRate % 2 + .25)\"\r\n [disabled]=\"isCasting\">\r\n <span class=\"icon\">{{video.playbackRate}}x </span>\r\n <span>Speed</span>\r\n </button>\r\n </div>\r\n </button>\r\n <button class=\"mini-player-btn\" title=\"Miniplayer (i)\" (click)=\"togglePIP()\"\r\n [disabled]=\"media.duration === undefined || isCasting\">\r\n <svg viewBox=\"0 0 24 24\">\r\n <path fill=\"currentColor\" d=\"M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zm-10-7h9v6h-9z\" />\r\n </svg>\r\n </button>\r\n <button class=\"cast-screen-btn\" [title]=\"isCasting ? 'Stop casting' : 'Cast to screen'\"\r\n (click)=\"isCasting ? endCurrentSession(true) : castToChromecast()\">\r\n <svg class=\"\" viewBox=\"0 -960 960 960\" *ngIf=\"!isCasting\">\r\n <path fill=\"currentColor\" d=\"M480-480Zm320 320H600q0-20-1.5-40t-4.5-40h206v-480H160v46q-20-3-40-4.5T80-680v-40q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160Zm-720 0v-120q50 0 85 35t35 85H80Zm200 0q0-83-58.5-141.5T80-360v-80q117 0 198.5 81.5T360-160h-80Zm160 0q0-75-28.5-140.5t-77-114q-48.5-48.5-114-77T80-520v-80q91 0 171 34.5T391-471q60 60 94.5 140T520-160h-80Z\"/>\r\n </svg>\r\n <svg class=\"\" viewBox=\"0 -960 960 960\" *ngIf=\"isCasting\">\r\n <path fill=\"currentColor\" d=\"M720-320H575q-35-109-111.5-192T281-640h439v320ZM80-160v-120q50 0 85 35t35 85H80Zm200 0q0-83-58.5-141.5T80-360v-80q117 0 198.5 81.5T360-160h-80Zm160 0q0-75-28.5-140.5t-77-114q-48.5-48.5-114-77T80-520v-80q91 0 171 34.5T391-471q60 60 94.5 140T520-160h-80Zm360 0H600q0-20-1.5-40t-4.5-40h206v-480H160v46q-20-3-40-4.5T80-680v-40q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160Z\"/>\r\n </svg>\r\n </button>\r\n <button class=\"full-screen-btn\" [title]=\"isFullscreen ? 'Exit full screen (f)' : 'Full screen (f)'\"\r\n (click)=\"toggleFullscreen()\">\r\n <svg class=\"open\" viewBox=\"0 0 24 24\" *ngIf=\"!isFullscreen\">\r\n <path fill=\"currentColor\" d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\" />\r\n </svg>\r\n <svg class=\"close\" viewBox=\"0 0 24 24\" *ngIf=\"isFullscreen\">\r\n <path fill=\"currentColor\" d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\" />\r\n </svg>\r\n </button>\r\n </span>\r\n </div>\r\n </div>\r\n <video #video [src]=\"media.src\" [loop]=\"isLoopingEnabled\" [poster]=\"media.poster\" [preload]=\"preload\" (onprogress)=\"isMediaLoading=true\"\r\n (canplay)=\"isMediaLoading=false\" (loadeddata)=\"updateAfterVideoInfoLoaded()\" (timeupdate)=\"updateCurrentTime()\"\r\n (click)=\"togglePlay()\" (playing)=\"onPlay($event)\" (pause)=\"onPause($event)\" (ended)=\"onEnd($event)\" (volumechange)=\"onVolumeChange($event)\"\r\n (error)=\"onError($event)\">\r\n <track kind=\"captions\" [srclang]=\"cc.lang\" [src]=\"cc.path\" *ngFor=\"let cc of captions\">\r\n </video>\r\n</div>", styles: [".video-container{font-family:var(--ng-plyr-font-family, \"Trebuchet MS\", \"Lucida Sans Unicode\", \"Lucida Grande\", \"Lucida Sans\", Arial, sans-serif);position:relative;background:var(--ng-plyr-video-bg, black);display:flex;justify-content:center;margin-inline:auto;max-width:100vw;max-height:100vh;min-height:200px;-webkit-tap-highlight-color:transparent}.video-container.full-screen{width:100%;max-width:initial;max-height:100vh}.video-container:hover .video-controls-container,.video-container:focus-within .video-controls-container,.video-container.paused .video-controls-container,.video-container:hover .video-header-container,.video-container:focus-within .video-header-container,.video-container.paused .video-header-container{opacity:1}.video-container .video-header-container{position:absolute;top:0;left:0;right:0;z-index:101;color:var(--ng-plyr-font-color, white);opacity:0;transition:opacity .2s ease-in-out;display:flex;align-items:center;padding:.5rem}.video-container .video-header-container:before{content:\"\";position:absolute;top:0;left:0;background:var(--ng-plyr-header-bg, linear-gradient(to bottom, rgba(0, 0, 0, .75), transparent));width:100%;aspect-ratio:8/1;z-index:-1;pointer-events:none}.video-container .video-header-container .menu-icon-btn{width:30px;height:30px}.video-container .video-header-container .menu-box{top:3rem}.video-container .video-header-container .title{display:inline-block;opacity:inherit;font-size:1.3rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;inline-size:90%;padding:.5rem}.video-container .video-overlay{position:absolute;inset:0;z-index:100;cursor:pointer}.video-container .video-overlay .loading,.video-container .video-overlay .video-actions,.video-container .video-overlay .seek{display:flex;width:100%;height:100%;align-items:center;justify-content:center}.video-container .video-overlay .loading button,.video-container .video-overlay .video-actions button,.video-container .video-overlay .seek button{margin:0 auto}.video-container .video-overlay .loading{background:var(--ng-plyr-buffering-bg, rgba(0, 0, 0, .6) no-repeat center 45%);background-image:var(--ng-plyr-buffering-img, url(\"data:image/svg+xml,%3C%3Fxml version%3D%221.0%22 encoding%3D%22utf-8%22%3F%3E%0D%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22 style%3D%22margin%3A auto%3B background%3A transparent%3B display%3A block%3B shape-rendering%3A auto%3B%22 width%3D%22100px%22 height%3D%22100px%22 viewBox%3D%220 0 100 100%22 preserveAspectRatio%3D%22xMidYMid%22%3E%0D%3Cg transform%3D%22rotate(0 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23eb7f19%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.9166666666666666s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(30 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23dfa950%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.8333333333333334s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(60 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%2394733c%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.75s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(90 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23f4edd8%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.6666666666666666s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(120 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23fae127%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.5833333333333334s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(150 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23189ad2%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.5s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(180 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23a5b5bc%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.4166666666666667s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(210 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%2353697e%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.3333333333333333s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(240 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%232d2a2e%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.25s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(270 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23eb7f19%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.16666666666666666s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(300 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%23dfa950%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%22-0.08333333333333333s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%3Cg transform%3D%22rotate(330 50 50)%22%3E%0D %3Crect x%3D%2246%22 y%3D%2222%22 rx%3D%222%22 ry%3D%222%22 width%3D%228%22 height%3D%228%22 fill%3D%22%2394733c%22%3E%0D %3Canimate attributeName%3D%22opacity%22 values%3D%221%3B0%22 keyTimes%3D%220%3B1%22 dur%3D%221s%22 begin%3D%220s%22 repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%0D %3C%2Frect%3E%0D%3C%2Fg%3E%0D%3C!-- %5Bldio%5D generated by https%3A%2F%2Floading.io%2F --%3E%3C%2Fsvg%3E\"));background-size:var(--ng-plyr-buffering-bg-size, 200px)}.video-container .video-overlay .video-actions{background:var(--ng-plyr-overlay-bg, rgba(0, 0, 0, .6))}.video-container .video-overlay .video-actions .play-btn{width:80px;height:80px}.video-container .video-overlay .seek{justify-content:space-between}.video-container .video-overlay .seek button{margin:0;width:30%;height:100%}.video-container .video-overlay .seek button svg{margin:auto;width:40px;height:40px}.video-container .video-controls-container{position:absolute;left:0;right:0;bottom:0;z-index:100;color:var(--ng-plyr-font-color, white);opacity:0;transition:opacity .2s ease-in-out}.video-container .video-controls-container:before{content:\"\";position:absolute;bottom:0;background:var(--ng-plyr-controls-bg, linear-gradient(to top, rgba(0, 0, 0, .75), transparent));width:100%;aspect-ratio:6/1;z-index:-1;pointer-events:none}.video-container .video-controls-container .timeline-container{width:calc(100% - 1rem);margin:0 .5rem;position:relative}.video-container .video-controls-container .timeline-container .timeline-progress{width:100%;opacity:0;cursor:pointer;position:absolute;top:50%;transform:translateY(-50%)}.video-container .video-controls-container .timeline-container .timeline-progress::-ms-tooltip{display:none}.video-container .video-controls-container .timeline-container .track{width:100%;height:var(--ng-plyr-progress-bar-track-height, 3px);background:rgba(150,150,150,.75);pointer-events:none;position:absolute;top:50%;transform:trans