feat(radio): add audio player for radio playback

This commit is contained in:
4gray committed 2023-08-20 20:59:40 +02:00
1 parent 7791686093
commit 5e9e53a02a
3 files changed
+248

No files matched your search

@@ -0,0 +1,112 @@
:host {
display: flex;
justify-content: center;
margin: 20px 0;
}
#audio-player {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.radio-logo {
border-radius: 14px;
height: 300px;
width: 300px;
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 300px;
max-height: 300px;
}
}
.volume-panel {
display: flex;
align-self: center;
align-items: center;
input {
flex: 1;
}
}
.controls {
height: 100px;
align-items: center;
display: flex;
gap: 8px;
justify-content: center;
}
.playing {
width: 36px;
height: 36px;
border-radius: .3rem;
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: .5rem;
box-sizing: border-box;
}
.playing__bar {
display: inline-block;
background: white;
width: 30%;
height: 100%;
animation: up-and-down 1.3s ease infinite alternate;
}
.playing__bar__stopped {
display: inline-block;
background: white;
width: 30%;
height: 100%;
}
.playing__bar1 {
height: 60%;
}
.playing__bar2 {
height: 30%;
animation-delay: -2.2s;
}
.playing__bar3 {
height: 75%;
animation-delay: -3.7s;
}
@keyframes up-and-down {
10% {
height: 30%;
}
30% {
height: 100%;
}
60% {
height: 50%;
}
80% {
height: 75%;
}
100% {
height: 60%;
}
}
.icon-button-large {
transform: scale(2);
margin: 0 20px;
}
@@ -0,0 +1,134 @@
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
import {
Component,
ElementRef,
Input,
OnChanges,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSliderModule } from '@angular/material/slider';
import { Store } from '@ngrx/store';
import { setAdjacentChannelAsActive } from '../../../state/actions';
@Component({
selector: 'app-audio-player',
standalone: true,
template: `
<div id="audio-player">
<div class="radio-logo">
<img [src]="icon" alt="radio icon" />
</div>
<audio preload="metadata" autoplay #audio>
<source [src]="url" />
</audio>
<div class="controls">
<button mat-icon-button (click)="switchChannel('previous')">
<mat-icon>skip_previous</mat-icon>
</button>
<button
class="icon-button-large"
mat-icon-button
(click)="play()"
*ngIf="playState === 'paused'"
>
<mat-icon>play_arrow</mat-icon>
</button>
<button
class="icon-button-large"
mat-icon-button
(click)="stop()"
*ngIf="playState === 'play'"
>
<mat-icon>pause</mat-icon>
</button>
<button mat-icon-button (click)="switchChannel('next')">
<mat-icon>skip_next</mat-icon>
</button>
</div>
<div class="volume-panel">
<div class="playing" *ngIf="playState === 'play'">
<span class="playing__bar playing__bar1"></span>
<span class="playing__bar playing__bar2"></span>
<span class="playing__bar playing__bar3"></span>
</div>
<div class="playing" *ngIf="playState === 'paused'">
<span class="playing__bar__stopped playing__bar1"></span>
<span class="playing__bar__stopped playing__bar2"></span>
<span class="playing__bar__stopped playing__bar3"></span>
</div>
<mat-slider min="0" max="1" step="0.1" color="accent">
<input matSliderThumb [(ngModel)]="audio.volume" />
</mat-slider>
<button mat-icon-button (click)="mute()">
<mat-icon *ngIf="audio.volume > 0 && !audio.muted"
>volume_up</mat-icon
>
<mat-icon *ngIf="audio.volume === 0 || audio.muted"
>volume_off</mat-icon
>
</button>
</div>
</div>
`,
styleUrls: ['./audio-player.component.scss'],
imports: [
MatSliderModule,
MatIconModule,
MatButtonModule,
NgIf,
NgClass,
FormsModule,
NgOptimizedImage,
],
})
export class AudioPlayerComponent implements OnChanges {
@Input() icon: string;
@Input({ required: true }) url: string;
playState: 'play' | 'paused' = 'paused';
fallbackVolume: number;
@ViewChild('audio', { static: true }) audio!: ElementRef<HTMLAudioElement>;
constructor(private store: Store) {}
ngOnChanges(changes: SimpleChanges): void {
this.audio.nativeElement.src = changes.url.currentValue;
this.audio.nativeElement.load();
this.play();
}
play() {
const playPromise = this.audio.nativeElement.play();
if (playPromise !== undefined) {
playPromise.catch((error) => {
console.log(error);
});
}
this.playState = 'play';
}
stop() {
this.audio.nativeElement.pause();
this.playState = 'paused';
}
mute() {
this.audio.nativeElement.muted = !this.audio.nativeElement.muted;
if (this.audio.nativeElement.muted) {
this.fallbackVolume = this.audio.nativeElement.volume;
this.audio.nativeElement.volume = 0;
} else this.audio.nativeElement.volume = this.fallbackVolume;
}
switchChannel(direction: 'next' | 'previous') {
console.log(direction);
this.store.dispatch(setAdjacentChannelAsActive({ direction }));
}
}
+2
View File
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { AudioPlayerComponent } from './components/audio-player/audio-player.component';
import { ChannelListContainerComponent } from './components/channel-list-container/channel-list-container.component';
import { ChannelListItemComponent } from './components/channel-list-container/channel-list-item/channel-list-item.component';
import { EpgItemDescriptionComponent } from './components/epg-list/epg-item-description/epg-item-description.component';
@@ -20,6 +21,7 @@ const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
@NgModule({
imports: [
AudioPlayerComponent,
CommonModule,
HtmlVideoPlayerComponent,
OverlayModule,