ng-plyr
Version:
An HTML5 media player for developers, built using Angular, having interface similar to Youtube player.
1 lines • 96.5 kB
Source Map (JSON)
{"version":3,"file":"ng-plyr.mjs","sources":["../../../projects/ng-plyr/src/lib/models/error.model.ts","../../../projects/ng-plyr/src/lib/models/playlist.model.ts","../../../projects/ng-plyr/src/lib/services/ng-plyr.service.ts","../../../projects/ng-plyr/src/lib/services/cast.service.ts","../../../projects/ng-plyr/src/lib/models/media.model.ts","../../../projects/ng-plyr/src/lib/directive/stop-click-propagation.directive.ts","../../../projects/ng-plyr/src/lib/ng-plyr.component.ts","../../../projects/ng-plyr/src/lib/ng-plyr.component.html","../../../projects/ng-plyr/src/lib/ng-plyr.module.ts","../../../projects/ng-plyr/src/public-api.ts","../../../projects/ng-plyr/src/ng-plyr.ts"],"sourcesContent":["export class Error {\r\n message?:string;\r\n code?:string|number;\r\n\r\n constructor(message?:string, code?:string|number) {\r\n this.message = message;\r\n this.code = code;\r\n }\r\n}\r\n\r\nexport enum ErrorCodes {\r\n 'player/not-initialized' = 'Player not yet initialized',\r\n 'video/element-not-initialized' = 'Video element is not yet initialized'\r\n}\r\n","import { Media } from \"./media.model\";\r\n\r\nexport class PlaylistItem {\r\n media:Media;\r\n next?:PlaylistItem;\r\n prev?:PlaylistItem;\r\n \r\n constructor(media:Media) {\r\n this.media = media;\r\n }\r\n\r\n static hasSameMediaSrc(p1:PlaylistItem, p2:PlaylistItem) {\r\n return p1.media.src === p2.media.src;\r\n }\r\n \r\n // Inquiry functions\r\n hasNext() { return this.next ? true : false; }\r\n\r\n hasPrev() { return this.prev ? true : false; }\r\n}\r\n\r\nexport class Playlist {\r\n id?:string;\r\n // Current will store currently playing item, on stopping it will be null\r\n current?:PlaylistItem;\r\n head?:PlaylistItem;\r\n tail?:PlaylistItem;\r\n itemCount:number = 0;\r\n\r\n constructor(item?:PlaylistItem) {\r\n if (item) {\r\n this.head = item;\r\n this.updateTail(item);\r\n }\r\n }\r\n\r\n static getLastItemFrom(item:PlaylistItem):PlaylistItem {\r\n while (item.hasNext()) {\r\n item = item.next!;\r\n }\r\n return item;\r\n }\r\n\r\n static getItemsCount(item:PlaylistItem):number {\r\n let count = 1;\r\n while (item.hasNext()) {\r\n item = item.next!;\r\n count++;\r\n }\r\n return count;\r\n }\r\n\r\n static mediaArrToPaylistItems(playlist:Media[]):PlaylistItem {\r\n let pl = new PlaylistItem(playlist[playlist.length-1]);\r\n\t\tfor (let i = playlist.length-2; i>=0; i--) {\r\n\t\t\tlet tempPl = new PlaylistItem(playlist[i]);\r\n\t\t\ttempPl.next = pl;\r\n\t\t\tpl.prev = tempPl;\r\n\t\t\tpl = tempPl;\r\n\t\t}\r\n return pl;\r\n }\r\n\r\n\r\n // Reset this playlist\r\n resetPlaylist() {\r\n this.head = undefined;\r\n this.tail = undefined;\r\n this.current = undefined;\r\n this.itemCount = 0;\r\n }\r\n\r\n // Initialize playlist with mediaItems\r\n initializePlaylist(mediaItems:Media[]) {\r\n // Reseting\r\n this.resetPlaylist();\r\n // Adding new items to playlist\r\n let playlistItems = Playlist.mediaArrToPaylistItems(mediaItems);\r\n\t\tthis.appendPlaylist(playlistItems);\r\n this.current = this.head;\r\n }\r\n\r\n // Update head to add items at beginning\r\n updateHead(item:PlaylistItem) {\r\n let oldHead = this.head;\r\n this.head = item;\r\n this.itemCount++;\r\n while (item.hasNext()) {\r\n item = item.next!;\r\n this.itemCount++;\r\n }\r\n if (oldHead) {\r\n oldHead.prev = item;\r\n item.next = oldHead;\r\n } else { this.tail = item; }\r\n }\r\n\r\n // Update tail after item is added at end\r\n updateTail(item:PlaylistItem) {\r\n while (item.hasNext()) {\r\n item = item.next!;\r\n this.itemCount++;\r\n }\r\n this.tail = item;\r\n this.itemCount++;\r\n }\r\n\r\n // Add items at end\r\n appendPlaylist(item:PlaylistItem, atHead?:boolean) {\r\n if (atHead) return this.updateHead(item);\r\n \r\n if (this.isEmpty()) {\r\n this.head = item;\r\n } else if (this.tail) {\r\n item.prev = this.tail;\r\n this.tail.next = item;\r\n }\r\n this.updateTail(item);\r\n }\r\n\r\n // Append item after current, update tail if req, update itemCount\r\n addNext(item:PlaylistItem) {\r\n if (this.current) {\r\n this.itemCount += Playlist.getItemsCount(item);\r\n let tempNext = this.current.next;\r\n item.prev = this.current;\r\n this.current.next = item;\r\n // Update links at end of item\r\n let lastItem = Playlist.getLastItemFrom(item);\r\n if (tempNext) {\r\n tempNext.prev = lastItem;\r\n lastItem.next = tempNext;\r\n } else {\r\n this.tail = lastItem;\r\n }\r\n } else {\r\n this.appendPlaylist(item, true);\r\n this.current = item;\r\n }\r\n }\r\n\r\n // Remove an existing PlaylistItem from Playlist\r\n remove(item:PlaylistItem) {\r\n if (!this.contains(item)) return;\r\n \r\n let prev = item.prev;\r\n let next = item.next;\r\n if (next) { next.prev = prev; }\r\n if (prev) { prev.next = next; }\r\n this.itemCount--;\r\n return item.media;\r\n }\r\n \r\n // Inquiry functions\r\n // Check if Playlist is empty\r\n isEmpty():boolean { return this.head ? false : true; }\r\n\r\n // Check whether this playlist contains the item\r\n contains(item:PlaylistItem):boolean {\r\n let start = this.head;\r\n while(start) {\r\n if (item === start) return true;\r\n start = start.next;\r\n }\r\n return false;\r\n }\r\n\r\n // Get PlaylistItem at index\r\n getPlaylistItemAt(index:number):PlaylistItem | undefined {\r\n let item = this.head;\r\n while (index > 0 && item?.hasNext()) {\r\n item = item.next;\r\n index--;\r\n }\r\n return item;\r\n }\r\n\r\n // Count number of items in this playlist\r\n countPlaylistItems():number {\r\n let item = this.head;\r\n let count = 0;\r\n while(item) {\r\n count++;\r\n item = item.next;\r\n }\r\n this.itemCount = count;\r\n return count;\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { ErrorCodes } from '../models/error.model';\r\nimport { Media } from '../models/media.model';\r\nimport { Playlist } from '../models/playlist.model';\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class PlayerService {\r\n private _plyr:any;\r\n\r\n constructor() { }\r\n\r\n private _checkForErrors(elements?:Array<any>) {\r\n let check = new Set(elements);\r\n \r\n if (!this._plyr) throw new Error(ErrorCodes['player/not-initialized']);\r\n if (check?.has('video') && !this._plyr.video) throw new Error(ErrorCodes['video/element-not-initialized']);\r\n }\r\n\r\n // Accessing `this` from player component\r\n addComponentRef(plyrComponent:any) {\r\n this._plyr = plyrComponent;\r\n }\r\n removeComponentRef() {\r\n this._plyr = undefined;\r\n }\r\n \r\n // Public functions\r\n\t// Actions\r\n\tplay() {\r\n this._checkForErrors(['video']);\r\n this._plyr.video?.nativeElement.play();\r\n\t}\r\n\tpause() {\r\n this._checkForErrors(['video']);\r\n\t\tthis._plyr.video?.nativeElement.pause();\r\n\t}\r\n\r\n\tnext() {\r\n this._checkForErrors();\r\n this._plyr.playNextMedia();\r\n }\r\n\tprev() {\r\n this._checkForErrors();\r\n this._plyr.playPrevMedia();\r\n }\r\n\r\n\tenableMediaLooping(loop:boolean = true) {\r\n this._checkForErrors();\r\n this._plyr.isLoopingEnabled = loop;\r\n }\r\n\tenablePlaylistLooping(loop:boolean = true) {\r\n this._checkForErrors();\r\n this._plyr.loopPlaylist = loop;\r\n }\r\n\r\n changeVolume(level:number) {\r\n this._checkForErrors(['video']);\r\n this._plyr.changeVolume(level);\r\n }\r\n seekTo(atSecond:number) {\r\n this._checkForErrors(['video']);\r\n this._plyr.seekTo(atSecond);\r\n }\r\n\r\n setPlaybackSpeed(rate:number) {\r\n this._checkForErrors();\r\n this._plyr.setPlaybackSpeed(rate);\r\n }\r\n\r\n\t// Getter functions\r\n\tgetCurrentlyPlaying() {\r\n this._checkForErrors();\r\n\t\treturn this._plyr.media;\r\n\t}\r\n\tgetNextMedia() {\r\n this._checkForErrors();\r\n\t\treturn this._plyr.nextMedia;\r\n\t}\r\n\tgetNumOfMediaInPlaylist() {\r\n this._checkForErrors();\r\n\t\treturn this._plyr.playlist.itemCount;\r\n\t}\r\n\r\n\t// Modify playlist\r\n addToPlaylist(mediaItems:Media[], atStart?:boolean) {\r\n this._checkForErrors();\r\n let items = Playlist.mediaArrToPaylistItems(mediaItems);\r\n this._plyr.playlist.appendPlaylist(items, atStart);\r\n }\r\n playNext(media:Media[]) {\r\n this._checkForErrors();\r\n let items = Playlist.mediaArrToPaylistItems([...media]);\r\n this._plyr.playlist.addNext(items);\r\n }\r\n\r\n}\r\n","import { Injectable } from '@angular/core';\r\n\r\ndeclare const cast: any;\r\ndeclare const chrome: any;\r\n\r\n@Injectable({\r\n providedIn: 'root'\r\n})\r\nexport class CastService {\r\n private isInitialized = false;\r\n private castContext: any;\r\n private currentSession: any;\r\n player:any;\r\n playerController:any;\r\n\r\n constructor() {}\r\n\r\n \r\n isSdkAvailable(): boolean {\r\n return !!cast?.framework;\r\n }\r\n\r\n initializeCastApi() {\r\n // Check if the Cast SDK is already initialized\r\n if (this.isInitialized) return;\r\n\r\n console.log('Initializing Cast API...');\r\n\r\n // Initialize the Cast SDK\r\n cast.framework.CastContext.getInstance().setOptions({\r\n receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,\r\n autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED\r\n });\r\n\r\n // Store the CastContext object\r\n this.castContext = cast.framework.CastContext.getInstance();\r\n \r\n // Initializing Remote Player\r\n this.player = new cast.framework.RemotePlayer();\r\n this.playerController = new cast.framework.RemotePlayerController(this.player);\r\n \r\n this.isInitialized = true;\r\n }\r\n \r\n // Cast Media to receiver\r\n loadMedia(mediaURL?:string, contentType:string = 'video/mp4') {\r\n if (!mediaURL) {\r\n this.currentSession.endSession(false);\r\n }\r\n\r\n let mediaInfo = new chrome.cast.media.MediaInfo(mediaURL, contentType);\r\n let request = new chrome.cast.media.LoadRequest(mediaInfo);\r\n\r\n this.currentSession = this.castContext.getCurrentSession();\r\n \r\n if (this.currentSession) {\r\n this.currentSession.loadMedia(request).then(\r\n ()=> {\r\n console.log('Media load succeed');\r\n },\r\n ).catch((error: string)=> {\r\n console.error('Error in casting media: ' + error);\r\n });\r\n } else {\r\n console.log('Session ended or not present.');\r\n }\r\n }\r\n\r\n getCastContext(): any {\r\n return this.castContext;\r\n }\r\n\r\n // getCurrentSession(): any {\r\n // return this.currentSession;\r\n // }\r\n\r\n isCasting(): boolean {\r\n return !!this.currentSession;\r\n }\r\n\r\n // Stop casting or just stop session\r\n endCurrentSession(stopCasting: boolean) {\r\n this.currentSession?.endSession(stopCasting);\r\n this.stopListeningRemotePlayerEvents();\r\n }\r\n\r\n\t// Remote Player controls\r\n\tprivate playOrPauseCasting() {\r\n\t\tif (this.currentSession && !this.player.isPlayingBreak) {\r\n\t\t\tthis.playerController.playOrPause();\r\n\t\t}\r\n\t}\r\n\r\n play() {\r\n if (this.player?.isPaused) {\r\n this.playOrPauseCasting();\r\n }\r\n }\r\n\r\n pause() {\r\n if (!this.player?.isPaused) {\r\n this.playOrPauseCasting();\r\n }\r\n }\r\n\t\r\n\tstopCasting() {\r\n if (this.currentSession) {\r\n this.playerController.stop();\r\n }\r\n\t}\r\n\r\n // Seeks the media item to player currentTime value\r\n\tseekTo(toSec: number) {\r\n if (this.currentSession && this.player.canSeek) {\r\n this.player.currentTime = toSec;\r\n this.playerController.seek();\r\n }\r\n }\r\n \r\n private muteOrUnmute() {\r\n if (this.currentSession) {\r\n this.playerController.muteOrUnmute();\r\n }\r\n }\r\n \r\n mute() {\r\n if (!this.player?.isMuted) {\r\n this.muteOrUnmute();\r\n }\r\n }\r\n \r\n unmute() {\r\n if (this.player?.isMuted) {\r\n this.muteOrUnmute();\r\n }\r\n }\r\n \r\n setVolumeLevel(volume:number) {\r\n if (this.currentSession) {\r\n // Sets the volume level of the connected device to the player volumeLevel value.\r\n this.player.volumeLevel = volume;\r\n this.playerController.setVolumeLevel();\r\n }\r\n }\r\n \r\n // Skip the ad currently playing on the receiver\r\n skipAd() {\r\n if (this.currentSession) {\r\n this.playerController.skipAd();\r\n }\r\n }\r\n\r\n // Create RemotePlayer events\r\n onCastEvent(eventName:string, cb:Function) {\r\n return new Promise((resolve, reject)=> {\r\n this.playerController.addEventListener(cast.framework.RemotePlayerEventType[eventName], ()=> {\r\n try {\r\n cb();\r\n } catch (error) {\r\n reject(error);\r\n }\r\n });\r\n resolve('Added event listener on ' + eventName);\r\n });\r\n }\r\n\r\n // Stop all event listeners on playerController\r\n stopListeningRemotePlayerEvents() {\r\n console.info('Stopped listening to cast events');\r\n this.playerController.removeEventListener(cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, ()=> {});\r\n this.playerController.removeEventListener(cast.framework.RemotePlayerEventType.IS_PLAYING_BREAK_CHANGED, ()=> {});\r\n this.playerController.removeEventListener(cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, ()=> {});\r\n this.playerController.removeEventListener(cast.framework.RemotePlayerEventType.VOLUME_LEVEL_CHANGED, ()=> {});\r\n this.playerController.removeEventListener(cast.framework.RemotePlayerEventType.IS_MUTED_CHANGED, ()=> {});\r\n }\r\n}\r\n","export class Media {\r\n id?:string;\r\n src: string;\r\n sources?: Array<string>;\r\n type: MediaType;\r\n title?:string;\r\n description?:string;\r\n thumb?:string;\r\n poster?:string;\r\n playFrom?: number = 0;\r\n duration?: number;\r\n captions?: Array<{path:string, lang:string}>;\r\n bookmarks?: Array<number>;\r\n paused?: boolean = true;\r\n\r\n constructor(src:string, type?:MediaType) {\r\n this.src = src;\r\n this.type = type || MediaType.VIDEO;\r\n }\r\n}\r\n\r\nexport enum MediaType {\r\n 'VIDEO', 'AUDIO'\r\n}\r\n","import { Directive, HostListener } from '@angular/core';\r\n\r\n@Directive({\r\n selector: '[stopClickPropagation]'\r\n})\r\nexport class StopClickPropagationDirective {\r\n\r\n constructor() { }\r\n\r\n @HostListener('click', [\"$event\"])\r\n public onClick(event:InputEvent) {\r\n event?.stopPropagation();\r\n }\r\n\r\n}\r\n","import { DOCUMENT } from '@angular/common';\r\nimport { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, Input, OnChanges, OnInit, OnDestroy, Output, SimpleChanges, ViewChild, ChangeDetectorRef } from '@angular/core';\r\nimport { Media, MediaType } from './models/media.model';\r\nimport { Playlist, PlaylistItem } from './models/playlist.model';\r\nimport { PlayerService } from './services/ng-plyr.service';\r\nimport { CastService } from './services/cast.service';\r\n\r\n@Component({\r\n\tselector: 'ng-plyr',\r\n\ttemplateUrl: './ng-plyr.component.html',\r\n\tstyleUrls: [ './ng-plyr.component.scss' ]\r\n})\r\nexport class NgPlyrComponent implements AfterViewInit, OnChanges, OnInit, OnDestroy {\r\n\t// Flags\r\n\tisPlaying = false;\r\n\tisMuted = false;\r\n\tisFullscreen = false;\r\n\tisPIP = false;\r\n\tisCasting = false;\r\n\t// isAutoplayEnabled = false;\r\n\tisLoopingEnabled = false;\r\n\tisControlSettingsOpen = false;\r\n\tisMenuSettingsOpen = false;\r\n\r\n\t// Variables\r\n\tseekStepInSec = 5;\r\n\tplayerVolume = 1;\r\n\tcurrentTime = '0:00';\r\n\ttotalTime = '0:00';\r\n\tprogressPercent = 0;\r\n\tmediaBuffers: Array<{ start: number; end: number }> = [];\r\n\tisMediaLoading = true;\r\n\tmedia!:Media;\r\n\tprevMedia?:Media;\r\n\tnextMedia?:Media;\r\n\tplaylist:Playlist = new Playlist();\r\n\tplayingTrack:number = 0;\r\n\t// TODO:\r\n\tmediaMarkers: [] = [];\r\n\r\n\t// Keeping input changes to trigger required actions on other events (like, after VideoInfoLoaded)\r\n\tinpChanges?: SimpleChanges;\r\n\t// Inputs\r\n\t@Input('src') mediaURL: string = '';\r\n\t@Input('type') mediaType?:MediaType;\r\n\t@Input('preload') preload:string = 'none';\r\n\t@Input('playFrom') playFrom?: number;\r\n\t@Input('loadingImgSrc') loadingImgSrc?: string;\r\n\t@Input('captions') captions?: Array<{ path: string, lang: string }>;\r\n\t@Input('bookmarks') bookmarks?: Array<number>;\r\n\t@Input('volume') volume?: number;\r\n\t@Input('loop') loopMedia?: boolean;\r\n\t@Input('autoplay') isAutoplayEnabled?: boolean;\r\n\t@Input('nextMedia') nextMediaToAdd?: Media;\r\n\t@Input('playlist') mediaItems?: Media[];\r\n\t@Input('loopPlaylist') loopPlaylist?: boolean;\r\n\t@Input('poster') posterUrl?: string;\r\n\t@Input('controls') enableControls: boolean = true;\r\n\r\n\t// Output events\r\n\t@Output() playing = new EventEmitter<boolean>();\r\n\t@Output() paused = new EventEmitter<boolean>();\r\n\t@Output() ended = new EventEmitter<boolean>();\r\n\t@Output() onprev = new EventEmitter<Media>();\r\n\t@Output() onnext = new EventEmitter<Media>();\r\n\t@Output() fullscreen = new EventEmitter<boolean>();\r\n\t@Output() volumechange = new EventEmitter<object>();\r\n\t// @Output() error = new EventEmitter<object>();\r\n\t// @Output() pip = new EventEmitter<boolean>();\r\n\r\n\t// HTML element references\r\n\t@ViewChild('videoContainer') videoContainer!: ElementRef;\r\n\t@ViewChild('video') video!: ElementRef;\r\n\r\n\tconstructor(@Inject(DOCUMENT) private document: any,\r\n\t\t\t\tprivate _plyrService:PlayerService,\r\n\t\t\t\tprivate _castService:CastService,\r\n\t\t\t\tprivate _cd: ChangeDetectorRef) { }\r\n\r\n\t// Lifecycle hooks\r\n\t// OnChanges LC hook\r\n\tngOnChanges(changes: SimpleChanges) {\r\n\t\tthis.inpChanges = changes;\r\n\r\n\t\t// When both mediaURL & mediaItems are present, media will be set from mediaItems\r\n\t\tif (changes['mediaURL'] && !changes['mediaItems']) { this.changeMedia(); }\r\n\t\tif (changes['mediaItems']) { this.createPlaylist(this.mediaItems!); }\r\n\r\n\t\t// nextMediaToAdd after playlist is initialized, or not present in same changes\r\n\t\tif (changes['nextMediaToAdd'] && !changes['mediaItems']) { this.onNextMediaInput(); }\r\n\t\t\r\n\t\tthis.mediaMarkers = changes['bookmarks']?.currentValue;\r\n\t\tthis.isLoopingEnabled = changes['loopMedia']?.currentValue;\r\n\t}\r\n\t\r\n\tngOnInit() {\r\n\t\tthis._plyrService.addComponentRef(this);\r\n\t}\r\n\r\n\tngAfterViewInit() {\r\n\t\tthis.isPlaying = !this.video.nativeElement.paused;\r\n\t\tthis.isMuted = this.video.nativeElement.muted;\r\n\t\tthis.isFullscreen = this.document.fullscreenElement ? true : false;\r\n\r\n\t\tthis.document.addEventListener('fullscreenchange', () => {\r\n\t\t\tif (!this.document.fullscreenElement) this.isFullscreen = false;\r\n\t\t});\r\n\t}\r\n\r\n\tngOnDestroy() {\r\n\t\tthis._plyrService.removeComponentRef();\r\n\t}\r\n\t\r\n\t\r\n\tresetPlayer() {\r\n\t\tthis.currentTime = '0:00';\r\n\t\tthis.totalTime = '0:00';\r\n\t\tthis.progressPercent = 0;\r\n\t\tthis.mediaBuffers = [];\r\n\t\tthis.isMediaLoading = true;\r\n\t\tthis.mediaMarkers = [];\r\n\t}\r\n\r\n\tchangeMedia(media?:Media) {\r\n\t\tthis.resetPlayer();\r\n\t\tthis.media = media ? media : new Media(this.mediaURL, this.mediaType);\r\n\t\tif(this.isCasting) this._castService.loadMedia(this.media.src);\r\n\t}\r\n\r\n\t// Playlist functions\r\n\t// Create new playlist / reinitialize playlist\r\n\tcreatePlaylist(mediaItems:Media[]) {\r\n\t\tif (mediaItems) {\r\n\t\t\tthis.playlist.initializePlaylist(mediaItems);\r\n\t\t\tthis.media = this.playlist.current?.media!;\r\n\t\t\tif (this.nextMediaToAdd) {\r\n\t\t\t\tthis.playlist.addNext(new PlaylistItem(this.nextMediaToAdd));\r\n\t\t\t}\r\n\t\t\tthis.nextMedia = this.playlist.current?.next?.media;\r\n\t\t}\r\n\t}\r\n\r\n\t// nextMedia will be used to play next inboth src and playlist cases\r\n\tonNextMediaInput() {\r\n\t\tif (this.playlist.current && this.nextMediaToAdd) {\r\n\t\t\tthis.playlist.addNext(new PlaylistItem(this.nextMediaToAdd));\r\n\t\t}\r\n\t\tthis.nextMedia = this.nextMediaToAdd;\r\n\t}\r\n\r\n\t// Output events for media\r\n\tonPlay(event:Event) {\r\n\t\tthis.playing.emit(true);\r\n\t\tthis.paused.emit(false);\r\n\t\tthis.isPlaying = true;\r\n\t\tthis.media.paused = false;\r\n\t\tif(this.isCasting) this.video.nativeElement.pause();\r\n\t}\r\n\r\n\tonPause(event:Event) {\r\n\t\tthis.playing.emit(false);\r\n\t\tthis.paused.emit(true);\r\n\t\tthis.media.paused = true;\r\n\t\tif(!this.isCasting) this.isPlaying = false;\r\n\t}\r\n\r\n\tonEnd(event?:Event) {\r\n\t\tthis.ended.emit(true);\r\n\t\tif (this.isAutoplayEnabled) {\r\n\t\t\tthis.playNextMedia();\r\n\t\t}\r\n\t}\r\n\r\n\tonVolumeChange(event:Event) {\r\n\t\tthis.volumechange.emit({\r\n\t\t\tlevel: this.video.nativeElement.volume,\r\n\t\t\tmuted: this.video.nativeElement.muted\r\n\t\t});\r\n\t}\r\n\r\n\t// TODO: Emit custom error\r\n\tonError(event:Event) {\r\n\t\tconsole.error(event);\r\n\t}\r\n\r\n\t// Shortcut keys\r\n\t@HostListener('window:keydown', ['$event'])\r\n\tdoShortcutKeyAction(event: KeyboardEvent) {\r\n\t\tif (this.enableControls === false) return;\r\n\r\n\t\tconst tagName = this.document.activeElement.tagName.toLowerCase();\r\n\t\tif (tagName === 'input') return;\r\n\r\n\t\tconst key = event.key.toLowerCase();\r\n\t\tif ((key === ' ' && tagName != 'button') || key === 'k') {\r\n\t\t\tevent.preventDefault();\r\n\t\t\tthis.togglePlay();\r\n\t\t} else if (key === 'm') {\r\n\t\t\tthis.toggleMute();\r\n\t\t} else if (key === 'f') {\r\n\t\t\tthis.toggleFullscreen();\r\n\t\t} else if (key === 'i') {\r\n\t\t\tthis.togglePIP();\r\n\t\t} else if (key === 'arrowleft') {\r\n\t\t\tthis.seekTo(Math.max(0, this.video.nativeElement.currentTime - 5));\r\n\t\t} else if (key === 'arrowright') {\r\n\t\t\tthis.seekTo(Math.min(this.video.nativeElement.duration, this.video.nativeElement.currentTime + 5));\r\n\t\t} else if (key === 'arrowup') {\r\n\t\t\tthis.changeVolume((this.playerVolume * 100 + 5) / 100);\r\n\t\t} else if (key === 'arrowdown') {\r\n\t\t\tconst vol = (this.playerVolume * 100 - 5) / 100;\r\n\t\t\tthis.changeVolume(vol < 0 ? 0 : vol);\r\n\t\t} else if (key === '0' || key === 'home') {\r\n\t\t\tthis.seekTo(0);\r\n\t\t} else if (Number(key) > 0 && Number(key) < 10 && this.media?.duration) {\r\n\t\t\tthis.seekTo(this.media.duration * Number(key)/10);\r\n\t\t} else if (key === 'end') {\r\n\t\t\tthis.stopVideo();\r\n\t\t} else if (event.key === 'N') {\r\n\t\t\tthis.playNextMedia();\r\n\t\t} else if (event.key === 'P') {\r\n\t\t\tthis.playPrevMedia();\r\n\t\t} else if (event.key === '<') {\r\n\t\t\tthis.setPlaybackSpeed(this.video.nativeElement.playbackRate - .25);\r\n\t\t} else if (event.key === '>') {\r\n\t\t\tthis.setPlaybackSpeed(this.video.nativeElement.playbackRate + .25);\r\n\t\t}\r\n\t}\r\n\r\n\t// Volume control\r\n\tchangeVolume(value: any) {\r\n\t\tvalue = Number(value).toPrecision(2);\r\n\t\tif (!this.video || value > 1 || value < 0) return;\r\n\r\n\t\tif (this.isCasting) {\r\n\t\t\t// Not yet working\r\n\t\t\tthis._castService.setVolumeLevel(value);\r\n\t\t} else {\r\n\t\t\tthis.video.nativeElement.volume = value;\r\n\t\t\tthis.playerVolume = value;\r\n\t\t\tif (this.video.nativeElement.muted && value > 0) {\r\n\t\t\t\tthis.toggleMute();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Update video metadata in player\r\n\tupdateAfterVideoInfoLoaded() {\r\n\t\tif (this.inpChanges && this.inpChanges[\"mediaURL\"]) {\r\n\t\t\tthis.video.nativeElement.currentTime = 0;\r\n\t\t\tthis.media.playFrom = this.playFrom;\r\n\t\t\tthis.media.duration = this.video.nativeElement.duration ? this.video.nativeElement.duration : this.video.nativeElement.currentTime;\r\n\t\t\tthis.media.captions = this.captions;\r\n\t\t}\r\n\t\tif (this.inpChanges && this.inpChanges['volume']?.currentValue) {\r\n\t\t\tthis.changeVolume(this.inpChanges['volume'].currentValue);\r\n\t\t}\r\n\t\tif (this.inpChanges && this.inpChanges['playFrom']?.currentValue) {\r\n\t\t\tthis.seekTo(this.inpChanges['playFrom'].currentValue);\r\n\t\t}\r\n\t\tif (this.isAutoplayEnabled) this.video.nativeElement.play();\r\n\t\t\r\n\t\tthis.isPlaying = !this.video.nativeElement.paused;\r\n\t\tthis.currentTime = this.formatDuration(this.media.playFrom);\r\n\t\tthis.totalTime = this.formatDuration(this.media.duration);\r\n\t\tthis.showBuffers();\r\n\t}\r\n\r\n\t// Display video buffers on timeline\r\n\tshowBuffers() {\r\n\t\tconst buf = this.video.nativeElement.buffered;\r\n\t\tif (buf.length === 1 && buf.end(0) - buf.start(0) === this.video.nativeElement.duration) {\r\n\t\t\tthis.isMediaLoading = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tthis.isMediaLoading = true;\r\n\r\n\t\tthis.mediaBuffers = [];\r\n\t\tfor (let i = 0; i < buf.length; i++) {\r\n\t\t\tlet start = Number((buf.start(i) / this.video.nativeElement.duration).toPrecision(3)) * 100;\r\n\t\t\tlet end = Number((buf.end(i) / this.video.nativeElement.duration).toPrecision(3)) * 100;\r\n\t\t\tthis.mediaBuffers.push({ start, end });\r\n\t\t\tif (this.video.nativeElement.currentTime >= buf.start(i) && this.video.nativeElement.currentTime <= buf.end(i)) {\r\n\t\t\t\tthis.isMediaLoading = false;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Seek to specific time\r\n\tseekTo(atSecond: number | string) {\r\n\t\tif (!this.video) return;\r\n\t\tif (atSecond > this.video.nativeElement.duration) {\r\n\t\t\tthis.stopVideo();\r\n\t\t\treturn;\r\n\t\t} else if (Number(atSecond) < 0) {\r\n\t\t\tthis.video.nativeElement.currentTime = 0;\r\n\t\t\tthis._castService.seekTo(0);\r\n\t\t} else {\r\n\t\t\tthis.video.nativeElement.currentTime = Number(atSecond).toPrecision(3);\r\n\t\t\tthis._castService.seekTo(Number(atSecond));\r\n\t\t}\r\n\t\tthis.progressPercent =\r\n\t\t\tNumber((this.video.nativeElement.currentTime / this.video.nativeElement.duration).toPrecision(3)) * 100;\r\n\t}\r\n\r\n\t// Call seekTo fn in specific direction and set count to 0\r\n\tseekAfterTimeout(direction:string) {\r\n\t\tif (direction === 'fwd') {\r\n\t\t\tthis.seekTo(this.video.nativeElement.currentTime + this.seekStepInSec * this.fwdClickCount);\r\n\t\t\tthis.fwdClickCount = 0;\r\n\t\t} else if (direction === 'bwd') {\r\n\t\t\tthis.seekTo(this.video.nativeElement.currentTime - this.seekStepInSec * this.bwdClickCount);\r\n\t\t\tthis.bwdClickCount = 0;\r\n\t\t}\r\n\t}\r\n\r\n\t// Seek backward on mouseclick\r\n\tbwdClickCount = 0;\r\n\tbwdClickTimeout = setTimeout(() => this.bwdClickCount = 0, 500);\r\n\tseekBwd(e: MouseEvent | PointerEvent) {\r\n\t\tif (e.type === 'dblclick') {\r\n\t\t\tthis.bwdClickCount = 1;\r\n\t\t\tclearTimeout(this.bwdClickTimeout);\r\n\t\t\tthis.bwdClickTimeout = setTimeout(() => this.seekAfterTimeout('bwd'), 500);\r\n\t\t} else if (e.type === 'click' && this.bwdClickCount > 0) {\r\n\t\t\tthis.bwdClickCount++;\r\n\t\t\tclearTimeout(this.bwdClickTimeout);\r\n\t\t\tthis.bwdClickTimeout = setTimeout(() => this.seekAfterTimeout('bwd'), 500);\r\n\t\t}\r\n\t}\r\n\r\n\t// Seek forward on mouseclick\r\n\tfwdClickCount = 0;\r\n\tfwdClickTimeout = setTimeout(() => this.fwdClickCount = 0, 500);\r\n\tseekFwd(e: MouseEvent | PointerEvent) {\r\n\t\tif (e.type === 'dblclick') {\r\n\t\t\tthis.fwdClickCount = 1;\r\n\t\t\tclearTimeout(this.fwdClickTimeout);\r\n\t\t\tthis.fwdClickTimeout = setTimeout(() => this.seekAfterTimeout('fwd'), 500);\r\n\t\t} else if (e.type === 'click' && this.fwdClickCount > 0) {\r\n\t\t\tthis.fwdClickCount++;\r\n\t\t\tclearTimeout(this.fwdClickTimeout);\r\n\t\t\tthis.fwdClickTimeout = setTimeout(() => this.seekAfterTimeout('fwd'), 500);\r\n\t\t}\r\n\t}\r\n\r\n\tupdateCurrentTime() {\r\n\t\tthis.currentTime = this.formatDuration(this.video.nativeElement.currentTime);\r\n\t\tthis.progressPercent =\r\n\t\t\tNumber((this.video.nativeElement.currentTime / this.video.nativeElement.duration).toPrecision(3)) * 100;\r\n\t\tthis.showBuffers();\r\n\t}\r\n\r\n\t// Play previous media from Q\r\n\tplayPrevMedia() {\r\n\t\tif (this.isLoopingEnabled) return this.onprev.emit(this.media);\r\n\r\n\t\tlet mediaToPlay: Media | undefined;\r\n\t\tif (this.playlist.current?.hasPrev()) {\r\n\t\t\tthis.playlist.current = this.playlist.current.prev;\r\n\t\t\tmediaToPlay = this.playlist.current?.media;\r\n\t\t\t// Update prevMedia\r\n\t\t\tif (this.playlist.current?.hasPrev()) {\r\n\t\t\t\tthis.prevMedia = this.playlist.current.prev!.media;\r\n\t\t\t} else if (this.loopPlaylist) {\r\n\t\t\t\tthis.prevMedia = this.playlist.tail?.media;\r\n\t\t\t}\r\n\t\t} else if (this.loopPlaylist) {\r\n\t\t\tthis.playlist.current = this.playlist.tail;\r\n\t\t\tmediaToPlay = this.playlist.current?.media;\r\n\t\t\tthis.prevMedia = this.playlist.current?.prev?.media;\r\n\t\t} else if (this.prevMedia) {\r\n\t\t\tmediaToPlay = this.prevMedia;\r\n\t\t\tthis.prevMedia = undefined;\r\n\t\t}\r\n\r\n\t\tthis.nextMedia = this.media;\r\n\t\tthis.changeMedia(mediaToPlay);\r\n\t\tthis.onprev.emit(mediaToPlay);\r\n\t}\r\n\t\r\n\t// Play next media from Q\r\n\tplayNextMedia() {\r\n\t\tif (this.isLoopingEnabled) return this.onnext.emit(this.media);\r\n\r\n\t\tlet mediaToPlay: Media | undefined;\r\n\t\tif (this.playlist.current?.hasNext()) {\r\n\t\t\tthis.playlist.current = this.playlist.current.next;\r\n\t\t\tmediaToPlay = this.playlist.current?.media;\r\n\t\t\t// Updating nextMedia\r\n\t\t\tif (this.playlist.current?.hasNext()) {\r\n\t\t\t\tthis.nextMedia = this.playlist.current.next!.media;\r\n\t\t\t} else if (this.loopPlaylist) {\r\n\t\t\t\tthis.nextMedia = this.playlist.head?.media;\r\n\t\t\t}\r\n\t\t} else if (this.loopPlaylist) {\r\n\t\t\tthis.playlist.current = this.playlist.head;\r\n\t\t\tmediaToPlay = this.playlist.current?.media;\r\n\t\t\tthis.nextMedia = this.playlist.current?.next?.media;\r\n\t\t} else if (this.nextMedia) {\r\n\t\t\tmediaToPlay = this.nextMedia;\r\n\t\t\tthis.nextMedia = undefined;\r\n\t\t}\r\n\t\t\r\n\t\tthis.prevMedia = this.media;\r\n\t\tthis.changeMedia(mediaToPlay);\r\n\t\tthis.onnext.emit(mediaToPlay);\r\n\t}\r\n\r\n\t// End/Stop current video\r\n\tstopVideo() {\r\n\t\tthis.video.nativeElement.pause();\r\n\t\tthis.seekTo(this.video.nativeElement.duration);\r\n\t\tthis._castService.stopCasting();\r\n\t}\r\n\r\n\tsetPlaybackSpeed(rate: number) {\r\n\t\tif (rate < 0.25 || rate > 2) return;\r\n\t\tthis.video.nativeElement.playbackRate = rate;\r\n\t}\r\n\r\n\t// Initialize Cast and requestSession\r\n\tcastToChromecast() {\r\n\t\t// Check if the Cast SDK is available and initialized\r\n\t\tif (this._castService.isSdkAvailable()) {\r\n\t\t\t// Initialize Cast API\r\n\t\t\tthis._castService.initializeCastApi();\r\n\t\r\n\t\t\t// Get the CastContext and show the Cast dialog to the user\r\n\t\t\tlet castContext = this._castService.getCastContext();\r\n\t\t\t// Starts media casting after session starts\r\n\t\t\tcastContext.requestSession().then((session: any)=> {\r\n\t\t\t\t// Add event listeners\r\n\t\t\t\tthis.listenToPlayerEvents();\r\n\t\t\t\tthis.enableCastingMode();\r\n\t\t\t\t// load media in receiver\r\n\t\t\t\tthis._castService.loadMedia(this.media.src, 'video/mp4');\r\n\t\t\t}).catch((err: any)=> {\r\n\t\t\t\tconsole.error(err);\r\n\t\t\t});\r\n\t\t} else {\r\n\t\t\tconsole.warn('Cast SDK is not available or not initialized.');\r\n\t\t}\r\n\t}\r\n\r\n\t// After successful connection, use this method to change mode to casting\r\n\tenableCastingMode() {\r\n\t\tthis.isCasting = true;\r\n\t\t// Stop playing on local device\r\n\t\tthis.video.nativeElement.pause();\r\n\t\tthis.isMuted = this._castService.player.isMuted;\r\n\t\tthis.isPlaying = !this._castService.player.isPaused;\r\n\t\tthis.playerVolume = this._castService.player.volumeLevel.toPrecision(2);\r\n\t}\r\n\r\n\t// Stop casting or just stop session\r\n\tendCurrentSession(stopCasting: boolean) {\r\n\t\tthis._castService.endCurrentSession(stopCasting);\r\n\t}\r\n\r\n\t// Listening to remote events\r\n\tlistenToPlayerEvents() {\r\n\t\t// Storing local player states in local variables to restore after receiver disconnects\r\n\t\tlet isMutedLocal = this.isMuted;\r\n\t\tlet playerVolumeLocal = this.playerVolume;\r\n\r\n\t\t// Cast Connected/disconnected\r\n\t\tthis._castService.onCastEvent('IS_CONNECTED_CHANGED', () => {\r\n\t\t\tif (this._castService.player.isConnected) {\r\n\t\t\t\tconsole.log('Cast Player connected');\r\n\t\t\t\t// Stop playing on local device\r\n\t\t\t\tthis.enableCastingMode();\r\n\t\t\t} else {\r\n\t\t\t\tconsole.log('Cast Player disconnected');\r\n\t\t\t\tthis.isCasting = false;\r\n\t\t\t\tthis._castService.stopListeningRemotePlayerEvents();\r\n\t\t\t\t// Start playing on local device\r\n\t\t\t\tthis.seekTo(this._castService.player.currentTime);\r\n\t\t\t\tthis.isPlaying ? this.video.nativeElement.play() : this.video.nativeElement.pause();\r\n\t\t\t\t// Setting local UI to original state\r\n\t\t\t\tthis.isMuted = isMutedLocal;\r\n\t\t\t\tthis.playerVolume = playerVolumeLocal;\r\n\t\t\t}\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\r\n\t\t// Pause/Play\r\n\t\tthis._castService.onCastEvent('IS_PAUSED_CHANGED', () => {\r\n\t\t\tif (this._castService.player.isPaused) {\r\n\t\t\t\tconsole.log('Receiver paused');\r\n\t\t\t\tthis.isPlaying = false;\r\n\t\t\t} else {\r\n\t\t\t\tconsole.log('Receiver playing');\r\n\t\t\t\tthis.isPlaying = true;\r\n\t\t\t}\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\r\n\t\t// MediaInfo changes\r\n\t\tthis._castService.onCastEvent('PLAYER_STATE_CHANGED', () => {\r\n\t\t\t// console.log('PlayerState:', this._castService.player.playerState);\r\n\t\t\tif (this._castService.player.playerState === 'IDLE') {\r\n\t\t\t\tthis.onEnd();\r\n\t\t\t\tthis._castService.play();\r\n\t\t\t\tthis.isPlaying = true;\r\n\t\t\t}\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\r\n\t\t// Paying break\r\n\t\tthis._castService.onCastEvent('IS_PLAYING_BREAK_CHANGED', () => {\r\n\t\t\tif (this._castService.player.isPlayingBreak) {\r\n\t\t\t\tconsole.log('Player is playing break');\r\n\t\t\t\t// Change on local device\r\n\t\t\t\tthis.video.nativeElement.pause();\r\n\t\t\t\t// Show Playing break, in UI\r\n\t\t\t} else {\r\n\t\t\t\tconsole.log('Player break ended');\r\n\t\t\t\t// Change on local device\r\n\t\t\t}\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\r\n\t\t// currentTime changed\r\n\t\tthis._castService.onCastEvent('CURRENT_TIME_CHANGED', () => {\r\n\t\t\t// Change current time on local device\r\n\t\t\tlet currentTime = this._castService.player.currentTime;\r\n\t\t\tthis.progressPercent = Number((currentTime / this.video.nativeElement.duration).toPrecision(3)) * 100;\r\n\t\t\tthis.currentTime = this.formatDuration(currentTime);\r\n\t\t\t// Update in UI on currentTime changes\r\n\t\t\tthis._cd.detectChanges();\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\r\n\t\t// volumeLevel changed\r\n\t\tthis._castService.onCastEvent('VOLUME_LEVEL_CHANGED', () => {\r\n\t\t\t// Change volume level in UI\r\n\t\t\tthis.playerVolume = this._castService.player.volumeLevel.toPrecision(2);\r\n\t\t\t// Update in UI on volume changes\r\n\t\t\tthis._cd.detectChanges();\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\r\n\t\t// Muted/unmuted\r\n\t\tthis._castService.onCastEvent('IS_MUTED_CHANGED', () => {\r\n\t\t\t// Change mute icon in UI\r\n\t\t\tthis.isMuted = this._castService.player.isMuted;\r\n\t\t}).then((res: any) => console.log(res))\r\n\t\t.catch((err: any) => console.error(err));\r\n\t}\r\n\r\n\t// Utility functions\r\n\tformatDuration(time: any): string {\r\n\t\tconst sec = Math.floor(time % 60);\r\n\t\tconst min = Math.floor(time / 60) % 60;\r\n\t\tconst hr = Math.floor(time / 3600);\r\n\r\n\t\tif (hr > 0) {\r\n\t\t\treturn `${hr}:${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;\r\n\t\t} else {\r\n\t\t\treturn `${min}:${sec < 10 ? '0' + sec : sec}`;\r\n\t\t}\r\n\t}\r\n\r\n\t// Toggles\r\n\ttoggleLoop() {\r\n\t\tthis.isLoopingEnabled = !this.isLoopingEnabled;\r\n\t}\r\n\r\n\ttogglePlay() {\r\n\t\tif (this.enableControls === false) return;\r\n\t\tif (!this.isCasting) {\r\n\t\t\tif (this.video.nativeElement.currentTime === this.video.nativeElement.duration) {\r\n\t\t\t\tthis.video.nativeElement.currentTime = 0;\r\n\t\t\t}\r\n\t\t\tthis.video.nativeElement.paused ? this.video.nativeElement.play() : this.video.nativeElement.pause();\r\n\t\t} else {\r\n\t\t\t// Play/pause receiver\r\n\t\t\tthis.isPlaying ? this._castService.pause() : this._castService.play();\r\n\t\t}\r\n\t}\r\n\r\n\ttoggleMute() {\r\n\t\tif (!this.isCasting) {\r\n\t\t\tthis.video.nativeElement.muted = !this.video.nativeElement.muted;\r\n\t\t\tthis.isMuted = this.video.nativeElement.muted;\r\n\t\t} else {\r\n\t\t\t// mute/unmute receiver\r\n\t\t\tthis.isMuted ? this._castService.unmute() : this._castService.mute();\r\n\t\t}\r\n\t}\r\n\r\n\ttogglePIP() {\r\n\t\tif (this.isPIP) {\r\n\t\t\tthis.document.exitPictureInPicture();\r\n\t\t\tthis.isPIP = false;\r\n\t\t} else {\r\n\t\t\tthis.video.nativeElement.requestPictureInPicture();\r\n\t\t\tthis.isPIP = true;\r\n\t\t}\r\n\t}\r\n\r\n\ttoggleFullscreen() {\r\n\t\tif (this.document.fullscreenElement) {\r\n\t\t\tthis.document.exitFullscreen();\r\n\t\t\tthis.isFullscreen = false;\r\n\t\t\tthis.fullscreen.emit(false);\r\n\t\t} else {\r\n\t\t\tthis.videoContainer.nativeElement.requestFullscreen();\r\n\t\t\tthis.isFullscreen = true;\r\n\t\t\tthis.fullscreen.emit(true);\r\n\t\t}\r\n\t}\r\n}\r\n","<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