diff --git a/docs/release-notes-player.txt b/docs/release-notes-player.txt index 2136adb9a39424edfc86f5257ea5f3f2f8c9e9af..bf81c925a299ea01d7e6b951cf3a905190194a0f 100644 --- a/docs/release-notes-player.txt +++ b/docs/release-notes-player.txt @@ -1,6 +1,7 @@ Player ====== next: +- Add custom control bar for audio and video elements - Changes background color of virtual keyboard keys - Changes font size of virtual keyboard keys - [bug] Solve problem with deleting markers in Firefox diff --git a/projects/common/element-components/audio.component.ts b/projects/common/element-components/audio.component.ts index dda243d1b6e2e5f78a41b2afad3640071918822e..3fd6e1e1cac80f51df0518ca2f2662e1f7f0a60b 100644 --- a/projects/common/element-components/audio.component.ts +++ b/projects/common/element-components/audio.component.ts @@ -5,9 +5,15 @@ import { AudioElement } from '../models/audio-element'; @Component({ selector: 'app-audio', template: ` - <audio controls [src]="elementModel.src | safeResourceUrl" - [style.width.%]="100"> - </audio> + <div [style.width.%]="100" + [style.height.%]="100"> + <audio #player + [src]="elementModel.src | safeResourceUrl" + [style.width.%]="100"> + </audio> + <app-control-bar [player]="player"> + </app-control-bar> + </div> ` }) export class AudioComponent extends ElementComponent { diff --git a/projects/common/element-components/control-bar/control-bar.component.css b/projects/common/element-components/control-bar/control-bar.component.css new file mode 100644 index 0000000000000000000000000000000000000000..8a71ed8c5e8e7a9bfd8e613fecb7babf19f9a104 --- /dev/null +++ b/projects/common/element-components/control-bar/control-bar.component.css @@ -0,0 +1,45 @@ +.control-bar { + background-color: #f1f1f1; + display: flex; + flex-direction: row; + padding-right: 10px; + overflow: hidden; + margin-top: -4px +} + +.control-button { + min-width: 20px; + padding: 0 5px; +} + +.control-button:hover { + color: #006064; +} + +.time { + white-space: nowrap; + padding: 15px 5px 0 0; +} + +.active-control{ + color: #006064; +} + +.duration{ + flex-grow: 4; + min-width: 20%; +} + +.volume{ + min-width: 10%; +} + +::ng-deep .mat-accent .mat-slider-thumb { + background-color: #006064; +} +::ng-deep .mat-accent .mat-slider-thumb-label { + background-color: #006064; +} +::ng-deep .mat-accent .mat-slider-track-fill { + background-color: #006064; +} diff --git a/projects/common/element-components/control-bar/control-bar.component.html b/projects/common/element-components/control-bar/control-bar.component.html new file mode 100644 index 0000000000000000000000000000000000000000..12bc5f018abee7aaf5c126c4fc5c0390b1a4f01d --- /dev/null +++ b/projects/common/element-components/control-bar/control-bar.component.html @@ -0,0 +1,36 @@ +<div class="control-bar"> + <button mat-button + class="control-button" + [class.active-control]="playing" + (click)="play()"> + <mat-icon>play_arrow</mat-icon> + </button> + <button mat-button + class="control-button" + [class.active-control]="pausing" + (click)="pause()"> + <mat-icon>pause</mat-icon> + </button> + <mat-slider class="duration" + min="0" + [max]="player.duration" + step="1" + (input)="onTimeChange($event)" + [value]="player.currentTime"> + </mat-slider> + <div class="time mat-typography">{{currentTime | playerTimeFormat}} / {{duration | playerTimeFormat }}</div> + <button mat-button + class="control-button" + (click)="toggleVolume()"> + <mat-icon *ngIf="!player.muted">volume_up</mat-icon> + <mat-icon *ngIf="player.muted">volume_off</mat-icon> + </button> + <mat-slider class="volume" + min="0" + [max]="1" + step="0.01" + (input)="onVolumeChange($event)" + [value]="player.volume"> + </mat-slider> +</div> + diff --git a/projects/common/element-components/control-bar/control-bar.component.ts b/projects/common/element-components/control-bar/control-bar.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..df24fabd493b8a05667a01a76a17eb806158efd7 --- /dev/null +++ b/projects/common/element-components/control-bar/control-bar.component.ts @@ -0,0 +1,74 @@ +import { + Component, Input, OnInit +} from '@angular/core'; +import { MatSliderChange } from '@angular/material/slider'; + +@Component({ + selector: 'app-control-bar', + templateUrl: './control-bar.component.html', + styleUrls: ['./control-bar.component.css'] +}) +export class ControlBarComponent implements OnInit { + @Input() player!: HTMLVideoElement | HTMLAudioElement; + duration!: number; + currentTime!: number; + playing!: boolean; + pausing!: boolean; + runCounter!: number; + lastVolume!: number; + + ngOnInit(): void { + // Firefox has problems to get the duration + this.player.ondurationchange = () => this.getDuration(); + this.player.onloadedmetadata = () => this.getDuration(); + this.player.onloadeddata = () => this.getDuration(); + this.player.onprogress = () => this.getDuration(); + this.player.oncanplay = () => this.getDuration(); + this.player.oncanplaythrough = () => this.getDuration(); + + this.player.ontimeupdate = () => { this.currentTime = this.player.currentTime / 60; }; + this.player.onpause = () => { this.playing = false; this.pausing = true; }; + this.player.onended = () => { this.runCounter += 1; }; // playing, pausing? + this.player.onvolumechange = () => { this.player.muted = !this.player.volume; }; + this.lastVolume = this.player.volume; + } + + play(): void { + // eslint-disable-next-line no-console + this.player.play().then(() => { this.playing = true; this.pausing = false; }, () => console.error('error')); + } + + pause(): void { + this.player.pause(); + } + + stop(): void { + this.player.pause(); + this.player.currentTime = 0; + } + + onTimeChange(event: MatSliderChange): void { + this.player.currentTime = event.value ? event.value : 0; + } + + onVolumeChange(event: MatSliderChange): void { + this.player.volume = event.value ? event.value : 0; + } + + toggleVolume(): void { + if (this.player.volume) { + this.lastVolume = this.player.volume; + this.player.volume = 0; + } else { + this.player.volume = this.lastVolume; + } + } + + private getDuration(): void { + if (this.player.duration !== Infinity && this.player.duration && !this.duration) { + this.duration = this.player.duration / 60; + } else { + this.duration = 0; + } + } +} diff --git a/projects/common/element-components/control-bar/player-time-format.pipe.ts b/projects/common/element-components/control-bar/player-time-format.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c1282e9e74268114f175030d86730eccedfa636 --- /dev/null +++ b/projects/common/element-components/control-bar/player-time-format.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'playerTimeFormat' +}) +export class PlayerTimeFormatPipe implements PipeTransform { + // eslint-disable-next-line class-methods-use-this + transform(value: number = 0): string { + const minutes: number = Math.floor(value); + const seconds: number = Math.floor((value - minutes) * 60); + return `${minutes.toString(10).padStart(2, '0')}:${seconds.toString(10).padStart(2, '0')}`; + } +} diff --git a/projects/common/element-components/video.component.ts b/projects/common/element-components/video.component.ts index fa70b500c2994888f58547482cde47ac1bf06bb4..945a8549fe871e2e3d4e2d1215c235528b8ce733 100644 --- a/projects/common/element-components/video.component.ts +++ b/projects/common/element-components/video.component.ts @@ -5,13 +5,14 @@ import { VideoElement } from '../models/video-element'; @Component({ selector: 'app-video', template: ` - <div [style.height.%]="100" + <div [style.object-fit]="'contain'" + [style.height.%]="100" [style.width.%]="100"> - <video controls [src]="elementModel.src | safeResourceUrl" - [style.object-fit]="'contain'" - [style.height.%]="100" + <video #player [src]="elementModel.src | safeResourceUrl" [style.width.%]="100"> </video> + <app-control-bar [player]="player"> + </app-control-bar> </div> ` }) diff --git a/projects/common/interfaces/UIElementInterfaces.ts b/projects/common/interfaces/UIElementInterfaces.ts index 40c4102c1616f4e70811dfedc28da6aa22bb54e7..99be1826dfa7e2f88843c776256dd5621cab9912 100644 --- a/projects/common/interfaces/UIElementInterfaces.ts +++ b/projects/common/interfaces/UIElementInterfaces.ts @@ -11,7 +11,7 @@ export interface SurfaceUIElement { backgroundColor: string; } -export interface MediaElement { +export interface PlayerElement { autostart: boolean; // default: false autostartDelay: number; // default: 0 (milliseconds) loop: boolean; // false diff --git a/projects/common/models/audio-element.ts b/projects/common/models/audio-element.ts index 3db8a7c9ff35e2ea9e81dce27b3be00c7a800341..2b82a3c7b50f83308edca6b7f5634c120776772c 100644 --- a/projects/common/models/audio-element.ts +++ b/projects/common/models/audio-element.ts @@ -5,5 +5,6 @@ export class AudioElement extends UIElement { constructor(serializedElement: UIElement, coordinates?: { x: number; y: number }) { super(serializedElement, coordinates); Object.assign(this, serializedElement); + this.width = serializedElement.width || 280; } } diff --git a/projects/common/models/video-element.ts b/projects/common/models/video-element.ts index 5549d3c4f235e97e690ce989b0300fdce1592d3c..7d7af9cf04686587b85f52b1207a2935d43fd0b6 100644 --- a/projects/common/models/video-element.ts +++ b/projects/common/models/video-element.ts @@ -6,7 +6,7 @@ export class VideoElement extends UIElement { constructor(serializedElement: UIElement, coordinates?: { x: number; y: number }) { super(serializedElement, coordinates); Object.assign(this, serializedElement); - - this.height = serializedElement.height || 100; + this.height = serializedElement.height || 200; + this.width = serializedElement.width || 280; } } diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts index 2bc500e13ae77812cd2cca5acc7a712fcd411d34..b09fc3676867f01099bbd6699b7c0c184d5bdeeb 100644 --- a/projects/common/shared.module.ts +++ b/projects/common/shared.module.ts @@ -21,6 +21,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { MatDialogModule } from '@angular/material/dialog'; +import { MatSliderModule } from '@angular/material/slider'; import { TextComponent } from './element-components/text.component'; import { ButtonComponent } from './element-components/button.component'; import { TextFieldComponent } from './element-components/text-field.component'; @@ -35,6 +36,8 @@ import { SafeResourceUrlPipe } from './element-components/pipes/safe-resource-ur import { InputBackgroundColorDirective } from './element-components/directives/input-background-color.directive'; import { ErrorTransformPipe } from './element-components/pipes/error-transform.pipe'; import { SafeResourceHTMLPipe } from './element-components/pipes/safe-resource-html.pipe'; +import { ControlBarComponent } from './element-components/control-bar/control-bar.component'; +import { PlayerTimeFormatPipe } from './element-components/control-bar/player-time-format.pipe'; @NgModule({ imports: [ @@ -51,7 +54,8 @@ import { SafeResourceHTMLPipe } from './element-components/pipes/safe-resource-h MatInputModule, MatDialogModule, MatButtonModule, - TranslateModule + TranslateModule, + MatSliderModule ], declarations: [ ButtonComponent, @@ -67,7 +71,9 @@ import { SafeResourceHTMLPipe } from './element-components/pipes/safe-resource-h SafeResourceUrlPipe, InputBackgroundColorDirective, ErrorTransformPipe, - SafeResourceHTMLPipe + SafeResourceHTMLPipe, + ControlBarComponent, + PlayerTimeFormatPipe ], exports: [ CommonModule,