Mercurial > hg > ugly-duckling
view src/app/services/audio-player/audio-player.service.ts @ 193:ac57ddba8ba9
Provide an observable in the audio service for when new audio has been loaded. The handling of errors is currently undesirable, using optional fields on the returned object. I couldn't figure out the proper Observable error flow without closing the stream.
author | Lucas Thompson <dev@lucas.im> |
---|---|
date | Thu, 23 Mar 2017 15:44:32 +0000 |
parents | e4f38975c2bc |
children | 3ef1aaa2ebed |
line wrap: on
line source
import {Injectable, Inject} from '@angular/core'; import {Subject} from "rxjs/Subject"; import {Observable} from "rxjs"; export interface UrlResourceLifetimeManager { createUrlToResource(resource: File | Blob): string; revokeUrlToResource(url: string): void; } export type ResourceReader = (resource: File | Blob) => Promise<ArrayBuffer>; export interface AudioResource { samples: AudioBuffer; url: string; mimeType: string; } export interface AudioResourceError { message: string; } export type AudioLoadResponse = AudioResource | AudioResourceError; @Injectable() export class AudioPlayerService { private currentObjectUrl: string; private playingStateChange: Subject<boolean>; playingStateChange$: Observable<boolean>; private seeked: Subject<number>; seeked$: Observable<number>; private audioLoaded: Subject<AudioLoadResponse>; audioLoaded$: Observable<AudioLoadResponse>; constructor(@Inject(HTMLAudioElement) private audioElement: HTMLAudioElement /* TODO probably shouldn't play audio this way */, @Inject('AudioContext') private audioContext: AudioContext, @Inject('ResourceReader') private readResource: ResourceReader, @Inject( 'UrlResourceLifetimeManager' ) private resourceManager: UrlResourceLifetimeManager) { this.currentObjectUrl = ''; this.playingStateChange = new Subject<boolean>(); this.playingStateChange$ = this.playingStateChange.asObservable(); this.seeked = new Subject<number>(); this.seeked$ = this.seeked.asObservable(); this.audioElement.addEventListener('ended', () => { this.playingStateChange.next(this.isPlaying()); }); this.audioElement.addEventListener('seeked', () => { this.seeked.next(this.audioElement.currentTime); }); this.audioLoaded = new Subject<AudioLoadResponse>(); this.audioLoaded$ = this.audioLoaded.asObservable(); } getCurrentTime(): number { return this.audioElement.currentTime; } isPlaying(): boolean { return !this.audioElement.paused; } loadAudio(resource: File | Blob): void { if (this.currentObjectUrl) this.resourceManager.revokeUrlToResource(this.currentObjectUrl); const url: string = this.resourceManager.createUrlToResource(resource); this.currentObjectUrl = url; this.audioElement.pause(); this.audioElement.src = url; this.audioElement.load(); const decode: (buffer: ArrayBuffer) => Promise<AudioBuffer> = buffer => { return new Promise( (res, rej) => this.audioContext.decodeAudioData(buffer, res, rej) ); }; this.readResource(resource) .then(decode) .then(val => { this.audioLoaded.next({ samples: val, url: url, mimeType: resource.type }); }) .catch(err => { this.audioLoaded.next({ message: err.message }); }); } togglePlaying(): void { if (this.audioElement.readyState >= 2) { this.isPlaying() ? this.audioElement.pause() : this.audioElement.play(); this.playingStateChange.next(this.isPlaying()); } } setVolume(value: number): void { this.audioElement.volume = value; // TODO check bounds? } seekTo(seconds: number): void { if (seconds < 0) { this.audioElement.currentTime = 0; } else if (seconds < this.getDuration()) { this.audioElement.currentTime = seconds; } else { this.audioElement.currentTime = this.getDuration(); } } seekBy(seconds: number): void { // TODO some kind of error handling? this.audioElement.currentTime += seconds; } seekToStart(): void { this.audioElement.currentTime = 0; } seekToEnd(): void { this.audioElement.currentTime = this.getDuration(); } getDuration(): number { return this.audioElement.duration || 0; } }