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