dev@37: import {Injectable, Inject} from '@angular/core'; dev@236: import {Subject} from 'rxjs/Subject'; dev@236: import {Observable} from 'rxjs/Observable'; dev@346: import {ReplaySubject} from 'rxjs/ReplaySubject'; dev@497: import {UrlResourceLifetimeManager} from '../File'; dev@191: dev@192: export type ResourceReader = (resource: File | Blob) => Promise; dev@193: dev@193: export interface AudioResource { dev@193: samples: AudioBuffer; dev@193: url: string; dev@193: mimeType: string; dev@193: } dev@193: dev@193: export interface AudioResourceError { dev@193: message: string; dev@193: } dev@193: dev@193: export type AudioLoadResponse = AudioResource | AudioResourceError; dev@193: dev@37: @Injectable() dev@37: export class AudioPlayerService { dev@37: dev@52: private currentObjectUrl: string; dev@52: private playingStateChange: Subject; dev@52: playingStateChange$: Observable; dev@52: private seeked: Subject; dev@52: seeked$: Observable; dev@193: private audioLoaded: Subject; dev@193: audioLoaded$: Observable; dev@52: dev@37: constructor(@Inject(HTMLAudioElement) private audioElement: HTMLAudioElement /* TODO probably shouldn't play audio this way */, dev@191: @Inject('AudioContext') private audioContext: AudioContext, dev@192: @Inject('ResourceReader') private readResource: ResourceReader, dev@191: @Inject( dev@191: 'UrlResourceLifetimeManager' dev@191: ) private resourceManager: UrlResourceLifetimeManager) { dev@52: this.currentObjectUrl = ''; dev@346: this.playingStateChange = new ReplaySubject(1); dev@346: this.playingStateChange$ = this.playingStateChange dev@346: .asObservable(); dev@346: dev@52: this.seeked = new Subject(); dev@52: this.seeked$ = this.seeked.asObservable(); dev@52: this.audioElement.addEventListener('ended', () => { dev@52: this.playingStateChange.next(this.isPlaying()); dev@52: }); dev@52: this.audioElement.addEventListener('seeked', () => { dev@52: this.seeked.next(this.audioElement.currentTime); dev@52: }); dev@193: this.audioLoaded = new Subject(); dev@193: this.audioLoaded$ = this.audioLoaded.asObservable(); dev@37: } dev@37: dev@37: getCurrentTime(): number { dev@37: return this.audioElement.currentTime; dev@37: } dev@37: dev@37: isPlaying(): boolean { dev@37: return !this.audioElement.paused; dev@37: } dev@37: dev@458: loadAudioFromUri(uri: string): void { dev@458: this.currentObjectUrl = uri; dev@458: this.audioElement.pause(); dev@458: this.audioElement.src = uri; dev@458: this.audioElement.load(); dev@458: } dev@37: dev@199: loadAudio(resource: File | Blob): string { dev@193: const url: string = this.resourceManager.createUrlToResource(resource); dev@458: this.loadAudioFromUri(url); dev@193: dev@193: const decode: (buffer: ArrayBuffer) => Promise = buffer => { dev@207: try { dev@207: return this.audioContext.decodeAudioData(buffer) as Promise; dev@207: } catch (e) { dev@207: console.warn('Falling back to callback style decodeAudioData'); dev@207: return new Promise( dev@207: (res, rej) => this.audioContext.decodeAudioData(buffer, res, rej) dev@207: ); dev@207: } dev@193: }; dev@193: dev@193: this.readResource(resource) dev@193: .then(decode) dev@193: .then(val => { dev@193: this.audioLoaded.next({ dev@193: samples: val, dev@193: url: url, dev@193: mimeType: resource.type dev@193: }); dev@193: }) dev@193: .catch(err => { dev@236: const message = err && err.message ? err.message : 'Read error'; dev@193: this.audioLoaded.next({ dev@207: message: message dev@193: }); dev@193: }); dev@199: return url; dev@37: } dev@37: dev@457: unload(): void { dev@458: this.loadAudioFromUri(''); dev@457: } dev@457: dev@37: togglePlaying(): void { dev@57: if (this.audioElement.readyState >= 2) { dev@57: this.isPlaying() ? this.audioElement.pause() : this.audioElement.play(); dev@57: this.playingStateChange.next(this.isPlaying()); dev@57: } dev@37: } dev@37: dev@37: setVolume(value: number): void { dev@37: this.audioElement.volume = value; // TODO check bounds? dev@37: } dev@37: dev@82: seekTo(seconds: number): void { dev@82: if (seconds < 0) { dev@82: this.audioElement.currentTime = 0; dev@82: } else if (seconds < this.getDuration()) { dev@82: this.audioElement.currentTime = seconds; dev@82: } else { dev@82: this.audioElement.currentTime = this.getDuration(); dev@82: } dev@82: } dev@82: dev@37: seekBy(seconds: number): void { dev@37: // TODO some kind of error handling? dev@37: this.audioElement.currentTime += seconds; dev@37: } dev@37: dev@37: seekToStart(): void { dev@37: this.audioElement.currentTime = 0; dev@37: } dev@37: dev@37: seekToEnd(): void { dev@37: this.audioElement.currentTime = this.getDuration(); dev@37: } dev@37: dev@37: getDuration(): number { dev@153: return this.audioElement.duration || 0; dev@37: } dev@37: }