• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2  AfterViewInit,
3  Component,
4  ElementRef,
5  Input,
6  OnChanges,
7  SimpleChanges,
8  ViewChild,
9} from '@angular/core';
10import { MotionGolden } from '../model/golden';
11import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
12import { NgIf } from '@angular/common';
13import { PreviewService } from '../service/preview.service';
14import { FormsModule } from '@angular/forms';
15import { MatIconModule } from '@angular/material/icon';
16import { MatButtonModule } from '@angular/material/button';
17
18@Component({
19  selector: 'app-preview',
20  imports: [
21    NgIf,
22    FormsModule,
23    MatIconModule,
24    MatButtonModule
25  ],
26  templateUrl: './preview.component.html',
27  styleUrl: './preview.component.css',
28})
29export class PreviewComponent implements OnChanges, AfterViewInit {
30  constructor(
31    private sanitizer: DomSanitizer,
32    private previewService: PreviewService
33  ) {
34    this.previewService.frameCount$.subscribe(
35      (frames) => (this.frames = frames)
36    );
37  }
38
39  @Input() selectedGolden: MotionGolden | null = null;
40  videoUrl: SafeResourceUrl | null = null;
41  frames: Array<string | number> | null = null;
42  currentFrame: number | null = null;
43  animationFrameId: number | null = null;
44  playbackSpeed = 0.25;
45  isPlaying: boolean = false
46
47  @ViewChild('videoPlayer')
48  videoPlayer!: ElementRef<HTMLVideoElement>;
49
50  ngAfterViewInit(): void {
51    if (this.videoPlayer && this.videoPlayer.nativeElement) {
52      this.videoPlayer.nativeElement.addEventListener('play', () => {
53        this.updateFrame();
54        this.isPlaying = true;
55      });
56
57      this.videoPlayer.nativeElement.addEventListener('pause', () => {
58        if (this.animationFrameId) {
59          cancelAnimationFrame(this.animationFrameId);
60          this.animationFrameId = null;
61          this.isPlaying = false;
62        }
63      });
64    }
65  }
66
67  ngOnChanges(changes: SimpleChanges): void {
68    if (changes['selectedGolden']) {
69      const currentGolden: MotionGolden | null =
70        changes['selectedGolden'].currentValue;
71
72      if (currentGolden?.videoUrl) {
73        this.videoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
74          currentGolden.videoUrl
75        );
76        this.ngAfterViewInit()
77      } else {
78        this.videoUrl = null;
79      }
80    }
81  }
82
83  updateFrame() {
84    this.calculateCurrentFrame();
85    this.animationFrameId = requestAnimationFrame(() => this.updateFrame());
86  }
87
88  calculateCurrentFrame() {
89    if (this.videoPlayer && this.videoPlayer.nativeElement && this.frames) {
90      const currentTime = this.videoPlayer.nativeElement.currentTime;
91      const totalDuration = this.videoPlayer.nativeElement.duration;
92      const frameCount = this.frames.length - 2;
93
94      if (currentTime && totalDuration) {
95        this.currentFrame = Math.round(
96          (currentTime / totalDuration) * frameCount
97        );
98        const nextFrame = this.frames[this.currentFrame];
99        if (
100          this.currentFrame < this.frames.length - 1 &&
101          typeof nextFrame === 'number'
102        ) {
103          this.previewService.setCurrentFrameFromView(nextFrame);
104        } else {
105          this.previewService.setCurrentFrameFromView(0);
106        }
107      } else {
108        this.currentFrame = null;
109        this.previewService.setCurrentFrameFromView(0);
110      }
111    } else {
112      this.currentFrame = null;
113      this.previewService.setCurrentFrameFromView(0);
114    }
115  }
116
117  goToStart() {
118    if (this.videoPlayer && this.videoPlayer.nativeElement) {
119      this.videoPlayer.nativeElement.currentTime = 0;
120      this.previewService.setCurrentFrameFromView(0)
121    }
122  }
123
124  togglePlayPause() {
125    if (this.videoPlayer && this.videoPlayer.nativeElement) {
126      if (this.videoPlayer.nativeElement.paused) {
127        this.isPlaying = true
128        this.videoPlayer.nativeElement.playbackRate = this.playbackSpeed;
129        this.videoPlayer.nativeElement.play();
130        this.updateFrame();
131      } else {
132        this.videoPlayer.nativeElement.pause();
133        if (this.animationFrameId) {
134          this.isPlaying = false
135          cancelAnimationFrame(this.animationFrameId);
136          this.animationFrameId = null;
137        }
138      }
139    }
140  }
141
142  stepBackward() {
143    if (this.videoPlayer && this.videoPlayer.nativeElement && this.frames && this.currentFrame !== null) {
144      if(this.currentFrame > 0) {
145        const frameCount = this.frames.length - 2;
146        const totalDuration = this.videoPlayer.nativeElement.duration;
147        const targetTime = (this.currentFrame - 1) / frameCount * totalDuration
148        this.videoPlayer.nativeElement.currentTime = targetTime;
149      }
150    }
151  }
152
153  setPlaybackSpeed() {
154    if (this.videoPlayer && this.videoPlayer.nativeElement) {
155      this.videoPlayer.nativeElement.playbackRate = this.playbackSpeed;
156    }
157  }
158
159  stepForward() {
160    if (this.videoPlayer && this.videoPlayer.nativeElement && this.frames && this.currentFrame !== null) {
161        const frameCount = this.frames.length - 2;
162        const totalDuration = this.videoPlayer.nativeElement.duration;
163      if(this.currentFrame < frameCount) {
164        const targetTime = (this.currentFrame + 1) / frameCount * totalDuration
165        this.videoPlayer.nativeElement.currentTime = targetTime;
166      }
167
168    }
169  }
170
171  goToEnd() {
172    if (this.videoPlayer && this.videoPlayer.nativeElement) {
173      this.videoPlayer.nativeElement.currentTime = this.videoPlayer.nativeElement.duration;
174      this.updateFrame()
175    }
176  }
177}
178