ng-plyr
Version:
An HTML5 media player for developers, built using Angular, having interface similar to Youtube player.
582 lines • 172 kB
JavaScript
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