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@497
|
5 import {UrlResourceLifetimeManager} from '../File';
|
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@458
|
63 loadAudioFromUri(uri: string): void {
|
dev@458
|
64 this.currentObjectUrl = uri;
|
dev@458
|
65 this.audioElement.pause();
|
dev@458
|
66 this.audioElement.src = uri;
|
dev@458
|
67 this.audioElement.load();
|
dev@458
|
68 }
|
dev@37
|
69
|
dev@199
|
70 loadAudio(resource: File | Blob): string {
|
dev@193
|
71 const url: string = this.resourceManager.createUrlToResource(resource);
|
dev@458
|
72 this.loadAudioFromUri(url);
|
dev@193
|
73
|
dev@193
|
74 const decode: (buffer: ArrayBuffer) => Promise<AudioBuffer> = buffer => {
|
dev@207
|
75 try {
|
dev@207
|
76 return this.audioContext.decodeAudioData(buffer) as Promise<AudioBuffer>;
|
dev@207
|
77 } catch (e) {
|
dev@207
|
78 console.warn('Falling back to callback style decodeAudioData');
|
dev@207
|
79 return new Promise(
|
dev@207
|
80 (res, rej) => this.audioContext.decodeAudioData(buffer, res, rej)
|
dev@207
|
81 );
|
dev@207
|
82 }
|
dev@193
|
83 };
|
dev@193
|
84
|
dev@193
|
85 this.readResource(resource)
|
dev@193
|
86 .then(decode)
|
dev@193
|
87 .then(val => {
|
dev@193
|
88 this.audioLoaded.next({
|
dev@193
|
89 samples: val,
|
dev@193
|
90 url: url,
|
dev@193
|
91 mimeType: resource.type
|
dev@193
|
92 });
|
dev@193
|
93 })
|
dev@193
|
94 .catch(err => {
|
dev@236
|
95 const message = err && err.message ? err.message : 'Read error';
|
dev@193
|
96 this.audioLoaded.next({
|
dev@207
|
97 message: message
|
dev@193
|
98 });
|
dev@193
|
99 });
|
dev@199
|
100 return url;
|
dev@37
|
101 }
|
dev@37
|
102
|
dev@457
|
103 unload(): void {
|
dev@458
|
104 this.loadAudioFromUri('');
|
dev@457
|
105 }
|
dev@457
|
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 }
|