view src/app/services/audio-player/audio-player.service.ts @ 192:e4f38975c2bc

Introduce interface for reading a file, once again avoiding specific browser implementation.
author Lucas Thompson <dev@lucas.im>
date Thu, 23 Mar 2017 15:42:34 +0000
parents ea735ebeed0e
children ac57ddba8ba9
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>;
@Injectable()
export class AudioPlayerService {

  private currentObjectUrl: string;
  private playingStateChange: Subject<boolean>;
  playingStateChange$: Observable<boolean>;
  private seeked: Subject<number>;
  seeked$: Observable<number>;

  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);
    });
  }

  getCurrentTime(): number {
    return this.audioElement.currentTime;
  }

  isPlaying(): boolean {
    return !this.audioElement.paused;
  }

  decodeAudioData(buffer: ArrayBuffer): Promise<AudioBuffer> {
    return new Promise((res, rej) => this.audioContext.decodeAudioData(buffer, res, rej));
  }

  loadAudioFromUrl(url: string): void {
    if (this.currentObjectUrl)
      this.resourceManager.revokeUrlToResource(this.currentObjectUrl);
    this.currentObjectUrl = url;
    this.audioElement.pause();
    this.audioElement.src = url;
    this.audioElement.load();
  }

  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;
  }
}