annotate src/app/services/audio-player/audio-player.service.ts @ 457:906dd152e333

Unload current audio from the audio player if removed.
author Lucas Thompson <dev@lucas.im>
date Thu, 29 Jun 2017 20:24:31 +0100
parents f93582c38b70
children f8d37ce6f475
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@454 5 import {UrlResourceLifetimeManager} from '../../app.module';
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@37 63
dev@199 64 loadAudio(resource: File | Blob): string {
dev@193 65 const url: string = this.resourceManager.createUrlToResource(resource);
dev@52 66 this.currentObjectUrl = url;
dev@37 67 this.audioElement.pause();
dev@37 68 this.audioElement.src = url;
dev@82 69 this.audioElement.load();
dev@193 70
dev@193 71 const decode: (buffer: ArrayBuffer) => Promise<AudioBuffer> = buffer => {
dev@207 72 try {
dev@207 73 return this.audioContext.decodeAudioData(buffer) as Promise<AudioBuffer>;
dev@207 74 } catch (e) {
dev@207 75 console.warn('Falling back to callback style decodeAudioData');
dev@207 76 return new Promise(
dev@207 77 (res, rej) => this.audioContext.decodeAudioData(buffer, res, rej)
dev@207 78 );
dev@207 79 }
dev@193 80 };
dev@193 81
dev@193 82 this.readResource(resource)
dev@193 83 .then(decode)
dev@193 84 .then(val => {
dev@193 85 this.audioLoaded.next({
dev@193 86 samples: val,
dev@193 87 url: url,
dev@193 88 mimeType: resource.type
dev@193 89 });
dev@193 90 })
dev@193 91 .catch(err => {
dev@236 92 const message = err && err.message ? err.message : 'Read error';
dev@193 93 this.audioLoaded.next({
dev@207 94 message: message
dev@193 95 });
dev@193 96 });
dev@199 97 return url;
dev@37 98 }
dev@37 99
dev@457 100 unload(): void {
dev@457 101 this.audioElement.pause();
dev@457 102 this.audioElement.src = '';
dev@457 103 this.audioElement.load();
dev@457 104 if (this.currentObjectUrl) {
dev@457 105 this.resourceManager.revokeUrlToResource(this.currentObjectUrl);
dev@457 106 this.currentObjectUrl = '';
dev@457 107 }
dev@457 108 }
dev@457 109
dev@37 110 togglePlaying(): void {
dev@57 111 if (this.audioElement.readyState >= 2) {
dev@57 112 this.isPlaying() ? this.audioElement.pause() : this.audioElement.play();
dev@57 113 this.playingStateChange.next(this.isPlaying());
dev@57 114 }
dev@37 115 }
dev@37 116
dev@37 117 setVolume(value: number): void {
dev@37 118 this.audioElement.volume = value; // TODO check bounds?
dev@37 119 }
dev@37 120
dev@82 121 seekTo(seconds: number): void {
dev@82 122 if (seconds < 0) {
dev@82 123 this.audioElement.currentTime = 0;
dev@82 124 } else if (seconds < this.getDuration()) {
dev@82 125 this.audioElement.currentTime = seconds;
dev@82 126 } else {
dev@82 127 this.audioElement.currentTime = this.getDuration();
dev@82 128 }
dev@82 129 }
dev@82 130
dev@37 131 seekBy(seconds: number): void {
dev@37 132 // TODO some kind of error handling?
dev@37 133 this.audioElement.currentTime += seconds;
dev@37 134 }
dev@37 135
dev@37 136 seekToStart(): void {
dev@37 137 this.audioElement.currentTime = 0;
dev@37 138 }
dev@37 139
dev@37 140 seekToEnd(): void {
dev@37 141 this.audioElement.currentTime = this.getDuration();
dev@37 142 }
dev@37 143
dev@37 144 getDuration(): number {
dev@153 145 return this.audioElement.duration || 0;
dev@37 146 }
dev@37 147 }