annotate src/app/services/audio-player/audio-player.service.ts @ 346:f87a96ab1e3f

Back the playing state observable with a ReplaySubject so that animating a play head can be picked up on render
author Lucas Thompson <dev@lucas.im>
date Thu, 25 May 2017 17:52:46 +0100
parents 53ea6406d601
children f93582c38b70
rev   line source
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 }