dev@37
|
1 import {Injectable, Inject} from '@angular/core';
|
dev@52
|
2 import {Subject} from "rxjs/Subject";
|
dev@52
|
3 import {Observable} from "rxjs";
|
dev@37
|
4
|
dev@191
|
5 export interface UrlResourceLifetimeManager {
|
dev@191
|
6 createUrlToResource(resource: File | Blob): string;
|
dev@191
|
7 revokeUrlToResource(url: string): void;
|
dev@191
|
8 }
|
dev@191
|
9
|
dev@192
|
10 export type ResourceReader = (resource: File | Blob) => Promise<ArrayBuffer>;
|
dev@193
|
11
|
dev@193
|
12 export interface AudioResource {
|
dev@193
|
13 samples: AudioBuffer;
|
dev@193
|
14 url: string;
|
dev@193
|
15 mimeType: string;
|
dev@193
|
16 }
|
dev@193
|
17
|
dev@193
|
18 export interface AudioResourceError {
|
dev@193
|
19 message: string;
|
dev@193
|
20 }
|
dev@193
|
21
|
dev@193
|
22 export type AudioLoadResponse = AudioResource | AudioResourceError;
|
dev@193
|
23
|
dev@37
|
24 @Injectable()
|
dev@37
|
25 export class AudioPlayerService {
|
dev@37
|
26
|
dev@52
|
27 private currentObjectUrl: string;
|
dev@52
|
28 private playingStateChange: Subject<boolean>;
|
dev@52
|
29 playingStateChange$: Observable<boolean>;
|
dev@52
|
30 private seeked: Subject<number>;
|
dev@52
|
31 seeked$: Observable<number>;
|
dev@193
|
32 private audioLoaded: Subject<AudioLoadResponse>;
|
dev@193
|
33 audioLoaded$: Observable<AudioLoadResponse>;
|
dev@52
|
34
|
dev@37
|
35 constructor(@Inject(HTMLAudioElement) private audioElement: HTMLAudioElement /* TODO probably shouldn't play audio this way */,
|
dev@191
|
36 @Inject('AudioContext') private audioContext: AudioContext,
|
dev@192
|
37 @Inject('ResourceReader') private readResource: ResourceReader,
|
dev@191
|
38 @Inject(
|
dev@191
|
39 'UrlResourceLifetimeManager'
|
dev@191
|
40 ) private resourceManager: UrlResourceLifetimeManager) {
|
dev@52
|
41 this.currentObjectUrl = '';
|
dev@52
|
42 this.playingStateChange = new Subject<boolean>();
|
dev@52
|
43 this.playingStateChange$ = this.playingStateChange.asObservable();
|
dev@52
|
44 this.seeked = new Subject<number>();
|
dev@52
|
45 this.seeked$ = this.seeked.asObservable();
|
dev@52
|
46 this.audioElement.addEventListener('ended', () => {
|
dev@52
|
47 this.playingStateChange.next(this.isPlaying());
|
dev@52
|
48 });
|
dev@52
|
49 this.audioElement.addEventListener('seeked', () => {
|
dev@52
|
50 this.seeked.next(this.audioElement.currentTime);
|
dev@52
|
51 });
|
dev@193
|
52 this.audioLoaded = new Subject<AudioLoadResponse>();
|
dev@193
|
53 this.audioLoaded$ = this.audioLoaded.asObservable();
|
dev@37
|
54 }
|
dev@37
|
55
|
dev@37
|
56 getCurrentTime(): number {
|
dev@37
|
57 return this.audioElement.currentTime;
|
dev@37
|
58 }
|
dev@37
|
59
|
dev@37
|
60 isPlaying(): boolean {
|
dev@37
|
61 return !this.audioElement.paused;
|
dev@37
|
62 }
|
dev@37
|
63
|
dev@37
|
64
|
dev@199
|
65 loadAudio(resource: File | Blob): string {
|
dev@52
|
66 if (this.currentObjectUrl)
|
dev@191
|
67 this.resourceManager.revokeUrlToResource(this.currentObjectUrl);
|
dev@193
|
68 const url: string = this.resourceManager.createUrlToResource(resource);
|
dev@52
|
69 this.currentObjectUrl = url;
|
dev@37
|
70 this.audioElement.pause();
|
dev@37
|
71 this.audioElement.src = url;
|
dev@82
|
72 this.audioElement.load();
|
dev@193
|
73
|
dev@193
|
74 const decode: (buffer: ArrayBuffer) => Promise<AudioBuffer> = buffer => {
|
dev@193
|
75 return new Promise(
|
dev@193
|
76 (res, rej) => this.audioContext.decodeAudioData(buffer, res, rej)
|
dev@193
|
77 );
|
dev@193
|
78 };
|
dev@193
|
79
|
dev@193
|
80 this.readResource(resource)
|
dev@193
|
81 .then(decode)
|
dev@193
|
82 .then(val => {
|
dev@193
|
83 this.audioLoaded.next({
|
dev@193
|
84 samples: val,
|
dev@193
|
85 url: url,
|
dev@193
|
86 mimeType: resource.type
|
dev@193
|
87 });
|
dev@193
|
88 })
|
dev@193
|
89 .catch(err => {
|
dev@193
|
90 this.audioLoaded.next({
|
dev@193
|
91 message: err.message
|
dev@193
|
92 });
|
dev@193
|
93 });
|
dev@199
|
94 return url;
|
dev@37
|
95 }
|
dev@37
|
96
|
dev@37
|
97 togglePlaying(): void {
|
dev@57
|
98 if (this.audioElement.readyState >= 2) {
|
dev@57
|
99 this.isPlaying() ? this.audioElement.pause() : this.audioElement.play();
|
dev@57
|
100 this.playingStateChange.next(this.isPlaying());
|
dev@57
|
101 }
|
dev@37
|
102 }
|
dev@37
|
103
|
dev@37
|
104 setVolume(value: number): void {
|
dev@37
|
105 this.audioElement.volume = value; // TODO check bounds?
|
dev@37
|
106 }
|
dev@37
|
107
|
dev@82
|
108 seekTo(seconds: number): void {
|
dev@82
|
109 if (seconds < 0) {
|
dev@82
|
110 this.audioElement.currentTime = 0;
|
dev@82
|
111 } else if (seconds < this.getDuration()) {
|
dev@82
|
112 this.audioElement.currentTime = seconds;
|
dev@82
|
113 } else {
|
dev@82
|
114 this.audioElement.currentTime = this.getDuration();
|
dev@82
|
115 }
|
dev@82
|
116 }
|
dev@82
|
117
|
dev@37
|
118 seekBy(seconds: number): void {
|
dev@37
|
119 // TODO some kind of error handling?
|
dev@37
|
120 this.audioElement.currentTime += seconds;
|
dev@37
|
121 }
|
dev@37
|
122
|
dev@37
|
123 seekToStart(): void {
|
dev@37
|
124 this.audioElement.currentTime = 0;
|
dev@37
|
125 }
|
dev@37
|
126
|
dev@37
|
127 seekToEnd(): void {
|
dev@37
|
128 this.audioElement.currentTime = this.getDuration();
|
dev@37
|
129 }
|
dev@37
|
130
|
dev@37
|
131 getDuration(): number {
|
dev@153
|
132 return this.audioElement.duration || 0;
|
dev@37
|
133 }
|
dev@37
|
134 }
|