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