/// /// Copyright 2015-2026 Micro Focus or one of its affiliates. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import { DecimalPipe } from '@angular/common'; import { AfterViewInit, Component, ElementRef, inject, OnDestroy, OnInit, ViewChild, } from '@angular/core'; import { fromEvent, Subject } from 'rxjs'; import { switchMap, takeUntil } from 'rxjs/operators'; import { DurationPipe } from '../../../../pipes/duration/duration.pipe'; import { TooltipDirective } from '../../../tooltip/tooltip.directive'; import { MediaPlayerService } from '../../media-player.service'; @Component({ selector: 'ux-media-player-timeline', templateUrl: './timeline.component.html', host: { '(document:mouseup)': 'mouseDown = false', '[class.quiet]': 'mediaPlayerService.quietMode || mediaPlayerService.fullscreen', }, imports: [TooltipDirective, DecimalPipe, DurationPipe], }) export class MediaPlayerTimelineExtensionComponent implements OnInit, AfterViewInit, OnDestroy { readonly mediaPlayerService = inject(MediaPlayerService); @ViewChild('progressThumb', { static: true }) thumb: ElementRef; @ViewChild('timeline', { static: true }) timelineRef: ElementRef; current: number = 0; position: number = 0; buffered: MediaPlayerBuffered[] = []; mouseDown: boolean = false; scrub = { visible: false, position: 0, time: 0 }; private readonly _onDestroy = new Subject(); ngOnInit(): void { // watch for changes to the current time this.mediaPlayerService.fullscreenEvent.pipe(takeUntil(this._onDestroy)).subscribe(() => { this.scrub.position = 0; }); this.mediaPlayerService.timeUpdateEvent.pipe(takeUntil(this._onDestroy)).subscribe(current => { this.current = current; this.position = (this.current / this.mediaPlayerService.duration) * 100; }); this.mediaPlayerService.progressEvent .pipe(takeUntil(this._onDestroy)) .subscribe((buffered: TimeRanges) => { this.buffered = []; for (let idx = 0; idx < buffered.length; idx++) { this.buffered.push({ start: (buffered.start(idx) / this.mediaPlayerService.duration) * 100, end: (buffered.end(idx) / this.mediaPlayerService.duration) * 100, }); } }); } ngAfterViewInit(): void { const mousedown$ = fromEvent(this.thumb.nativeElement, 'mousedown'); const mousemove$ = fromEvent(document, 'mousemove'); const mouseup$ = fromEvent(document, 'mouseup'); mousedown$ .pipe( switchMap(() => mousemove$.pipe(takeUntil(mouseup$))), takeUntil(this._onDestroy) ) .subscribe(() => (this.scrub.visible = false)); } ngOnDestroy(): void { this._onDestroy.next(); this._onDestroy.complete(); } updateScrub(event: MouseEvent): void { const target = event.target as HTMLElement; if (target.classList.contains('media-progress-bar-thumb')) { return; } const timeline = this.timelineRef.nativeElement as HTMLDivElement; const bounds = timeline.getBoundingClientRect(); this.scrub.position = event.offsetX; this.scrub.time = (event.offsetX / bounds.width) * this.mediaPlayerService.duration; if (this.mouseDown) { this.mediaPlayerService.pause(); this.mediaPlayerService.currentTime = this.scrub.time; } } /** Skip a number of seconds in any direction */ skip(seconds: number): void { let target = this.current + seconds; // ensure that the target position is within the bounds of the clip if (target < 0) { target = 0; } if (target > this.mediaPlayerService.duration) { target = this.mediaPlayerService.duration; } this.mediaPlayerService.currentTime = target; } } export interface MediaPlayerBuffered { start: number; end: number; }