annotate 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
rev   line source
dev@37 1 import {Injectable, Inject} from '@angular/core';
dev@52 2 import {Subject} from "rxjs/Subject";
dev@52 3 import {Observable} from "rxjs";
dev@37 4
dev@191 5 export interface UrlResourceLifetimeManager {
dev@191 6 createUrlToResource(resource: File | Blob): string;
dev@191 7 revokeUrlToResource(url: string): void;
dev@191 8 }
dev@191 9
dev@192 10 export type ResourceReader = (resource: File | Blob) => Promise<ArrayBuffer>;
dev@193 11
dev@193 12 export interface AudioResource {
dev@193 13 samples: AudioBuffer;
dev@193 14 url: string;
dev@193 15 mimeType: string;
dev@193 16 }
dev@193 17
dev@193 18 export interface AudioResourceError {
dev@193 19 message: string;
dev@193 20 }
dev@193 21
dev@193 22 export type AudioLoadResponse = AudioResource | AudioResourceError;
dev@193 23
dev@37 24 @Injectable()
dev@37 25 export class AudioPlayerService {
dev@37 26
dev@52 27 private currentObjectUrl: string;
dev@52 28 private playingStateChange: Subject<boolean>;
dev@52 29 playingStateChange$: Observable<boolean>;
dev@52 30 private seeked: Subject<number>;
dev@52 31 seeked$: Observable<number>;
dev@193 32 private audioLoaded: Subject<AudioLoadResponse>;
dev@193 33 audioLoaded$: Observable<AudioLoadResponse>;
dev@52 34
dev@37 35 constructor(@Inject(HTMLAudioElement) private audioElement: HTMLAudioElement /* TODO probably shouldn't play audio this way */,
dev@191 36 @Inject('AudioContext') private audioContext: AudioContext,
dev@192 37 @Inject('ResourceReader') private readResource: ResourceReader,
dev@191 38 @Inject(
dev@191 39 'UrlResourceLifetimeManager'
dev@191 40 ) private resourceManager: UrlResourceLifetimeManager) {
dev@52 41 this.currentObjectUrl = '';
dev@52 42 this.playingStateChange = new Subject<boolean>();
dev@52 43 this.playingStateChange$ = this.playingStateChange.asObservable();
dev@52 44 this.seeked = new Subject<number>();
dev@52 45 this.seeked$ = this.seeked.asObservable();
dev@52 46 this.audioElement.addEventListener('ended', () => {
dev@52 47 this.playingStateChange.next(this.isPlaying());
dev@52 48 });
dev@52 49 this.audioElement.addEventListener('seeked', () => {
dev@52 50 this.seeked.next(this.audioElement.currentTime);
dev@52 51 });
dev@193 52 this.audioLoaded = new Subject<AudioLoadResponse>();
dev@193 53 this.audioLoaded$ = this.audioLoaded.asObservable();
dev@37 54 }
dev@37 55
dev@37 56 getCurrentTime(): number {
dev@37 57 return this.audioElement.currentTime;
dev@37 58 }
dev@37 59
dev@37 60 isPlaying(): boolean {
dev@37 61 return !this.audioElement.paused;
dev@37 62 }
dev@37 63
dev@37 64
dev@193 65 loadAudio(resource: File | Blob): void {
dev@52 66 if (this.currentObjectUrl)
dev@191 67 this.resourceManager.revokeUrlToResource(this.currentObjectUrl);
dev@193 68 const url: string = this.resourceManager.createUrlToResource(resource);
dev@52 69 this.currentObjectUrl = url;
dev@37 70 this.audioElement.pause();
dev@37 71 this.audioElement.src = url;
dev@82 72 this.audioElement.load();
dev@193 73
dev@193 74 const decode: (buffer: ArrayBuffer) => Promise<AudioBuffer> = buffer => {
dev@193 75 return new Promise(
dev@193 76 (res, rej) => this.audioContext.decodeAudioData(buffer, res, rej)
dev@193 77 );
dev@193 78 };
dev@193 79
dev@193 80 this.readResource(resource)
dev@193 81 .then(decode)
dev@193 82 .then(val => {
dev@193 83 this.audioLoaded.next({
dev@193 84 samples: val,
dev@193 85 url: url,
dev@193 86 mimeType: resource.type
dev@193 87 });
dev@193 88 })
dev@193 89 .catch(err => {
dev@193 90 this.audioLoaded.next({
dev@193 91 message: err.message
dev@193 92 });
dev@193 93 });
dev@37 94 }
dev@37 95
dev@37 96 togglePlaying(): void {
dev@57 97 if (this.audioElement.readyState >= 2) {
dev@57 98 this.isPlaying() ? this.audioElement.pause() : this.audioElement.play();
dev@57 99 this.playingStateChange.next(this.isPlaying());
dev@57 100 }
dev@37 101 }
dev@37 102
dev@37 103 setVolume(value: number): void {
dev@37 104 this.audioElement.volume = value; // TODO check bounds?
dev@37 105 }
dev@37 106
dev@82 107 seekTo(seconds: number): void {
dev@82 108 if (seconds < 0) {
dev@82 109 this.audioElement.currentTime = 0;
dev@82 110 } else if (seconds < this.getDuration()) {
dev@82 111 this.audioElement.currentTime = seconds;
dev@82 112 } else {
dev@82 113 this.audioElement.currentTime = this.getDuration();
dev@82 114 }
dev@82 115 }
dev@82 116
dev@37 117 seekBy(seconds: number): void {
dev@37 118 // TODO some kind of error handling?
dev@37 119 this.audioElement.currentTime += seconds;
dev@37 120 }
dev@37 121
dev@37 122 seekToStart(): void {
dev@37 123 this.audioElement.currentTime = 0;
dev@37 124 }
dev@37 125
dev@37 126 seekToEnd(): void {
dev@37 127 this.audioElement.currentTime = this.getDuration();
dev@37 128 }
dev@37 129
dev@37 130 getDuration(): number {
dev@153 131 return this.audioElement.duration || 0;
dev@37 132 }
dev@37 133 }