1/*
2 * Copyright 2024 Google LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import { Component, Input, OnDestroy } from '@angular/core';
18import { MatButtonModule } from '@angular/material/button';
19import { MatIconModule } from '@angular/material/icon';
20import { MatMenuModule } from '@angular/material/menu';
21import { Disposer } from '../../utils/disposer';
22import { checkNotNull } from '../../utils/preconditions';
23import { Preferences } from '../../utils/preferences';
24import { VideoSource } from '../video-source';
25import {
26  KeyboardShortcutsModule,
27  ShortcutEventOutput,
28  ShortcutInput,
29} from 'ng-keyboard-shortcuts';
30
31@Component({
32  selector: 'app-video-controls',
33  standalone: true,
34  imports: [
35    MatIconModule,
36    MatButtonModule,
37    MatMenuModule,
38    KeyboardShortcutsModule,
39  ],
40
41  templateUrl: './video-controls.component.html',
42  styleUrls: ['./video-controls.component.scss'],
43})
44export class VideoControlsComponent implements OnDestroy {
45  private _disposer = new Disposer();
46
47  private _source?: VideoSource;
48  private _sourceDisposer = new Disposer(true);
49
50  constructor(private _preferences: Preferences) {}
51
52  @Input()
53  set videoSource(newSource: VideoSource | undefined) {
54    if (this._source === newSource) return;
55
56    this._sourceDisposer.dispose();
57    this._source = newSource;
58    if (this._source) {
59      this._source.loop = this._preferences.loopVideo;
60      this._source.playbackRate = this._preferences.playbackRate;
61    }
62  }
63
64  ngOnDestroy(): void {
65    this._disposer.dispose();
66  }
67
68  get playStateIcon() {
69    return this._source?.state == 'play' ? 'pause' : 'play_arrow';
70  }
71
72  moveFrame(framesOffset: number) {
73    const videoSource = checkNotNull(this._source);
74    const timeline = videoSource.timeline;
75    videoSource.stop();
76
77    const currentTime = videoSource.currentTime;
78    const thisFrame = timeline.timeToFrame(currentTime);
79
80    const nextFrame = Math.min(
81      Math.max(thisFrame + framesOffset, 0),
82      timeline.frameCount - 1,
83    );
84
85    const targetTime = timeline.frameToTime(nextFrame) + 0.008;
86
87    return videoSource.seek(targetTime);
88  }
89
90  togglePlay() {
91    const videoSource = checkNotNull(this._source);
92    if (videoSource.state == 'play') {
93      videoSource.stop();
94    } else {
95      videoSource.play();
96    }
97  }
98
99  get repeatStateIcon() {
100    return this._preferences.loopVideo ? 'repeat_on' : 'repeat';
101  }
102
103  toggleRepeat() {
104    const loopVideo = !this._preferences.loopVideo;
105    this._preferences.loopVideo = loopVideo;
106    checkNotNull(this._source).loop = loopVideo;
107  }
108
109  playbackRateOptions: number[] = [1, 0.5, 0.25, 0.125];
110
111  get playbackRate() {
112    return this._preferences.playbackRate;
113  }
114
115  formatPlaybackRateLabel(rate: number): string {
116    return `${rate}x`;
117  }
118
119  setPlaybackRate(rate: number) {
120    this._preferences.playbackRate = rate;
121    checkNotNull(this._source).playbackRate = rate;
122  }
123  shortcuts: ShortcutInput[] = [
124    {
125      key: ['left'],
126      label: 'Previous frame',
127      command: (output: ShortcutEventOutput) => this.moveFrame(-1),
128    },
129
130    {
131      key: ['right'],
132      label: 'Next frame',
133      command: (output: ShortcutEventOutput) => this.moveFrame(+1),
134    },
135
136    {
137      key: ['space'],
138      label: 'play/pause',
139      command: (output: ShortcutEventOutput) => this.togglePlay(),
140    },
141  ];
142}
143