view src/app/services/audio-recorder/audio-recorder.service.ts @ 145:fd3f25a41ecf

Notify subscribers of new recordings within the Angular Zone.
author Lucas Thompson <dev@lucas.im>
date Tue, 21 Mar 2017 13:00:59 +0000
parents 36f57a21637c
children 53ea6406d601
line wrap: on
line source
/**
 * Created by lucas on 17/03/2017.
 */
import {Injectable, Inject, NgZone} from "@angular/core";
import {Subject, Observable} from "rxjs";


// seems the TypeScript definitions are not up to date,
// introduce own types for now

export type AudioInputProvider = () => PromiseLike<MediaStream>;

export interface MediaRecorderOptions {
  mimeType?: string;
  audioBitsPerSecond?: number;
  videoBitsPerSecond?: number;
  bitsPerSecond?: number;
}

export type RecordingState = "inactive" | "recording" | "paused";

export interface BlobEvent extends Event {
  readonly data: Blob;
  readonly timecode: number;
}

export interface MediaRecorderErrorEvent extends Event {
  readonly error: DOMException;
}

export interface MediaRecorder {
  readonly mimeType: string;
  readonly state: RecordingState;
  readonly stream: MediaStream;
  ignoreMutedMedia: boolean;
  readonly videoBitsPerSecond: number;
  readonly audioBitsPerSecond: number;
  // isTypeSupported(mimeType: string): boolean;
  pause(): void;
  requestData(): void;
  resume(): void;
  start(timeslice?: number): void;
  stop(): void;
  onstart: (evt: Event) => void;
  onstop: (evt: Event) => void;
  ondataavailable: (evt: BlobEvent) => void;
  onpause: (evt: Event) => void;
  onresume: (evt: Event) => void;
  onerror: (evt: MediaRecorderErrorEvent) => void;
}

export interface MediaRecorderConstructor {
  new(stream: MediaStream,
      options?: MediaRecorderOptions): MediaRecorder;
  isTypeSupported(mimeType: string): boolean;
}

export type RecorderServiceStatus = "disabled" | "enabled" | "recording";

export class ThrowingMediaRecorder implements MediaRecorder {
  mimeType: string;
  state: RecordingState;
  stream: MediaStream;
  ignoreMutedMedia: boolean;
  videoBitsPerSecond: number;
  audioBitsPerSecond: number;
  onstart: (evt: Event) => void;
  onstop: (evt: Event) => void;
  ondataavailable: (evt: BlobEvent) => void;
  onpause: (evt: Event) => void;
  onresume: (evt: Event) => void;
  onerror: (evt: MediaRecorderErrorEvent) => void;

  constructor(stream: MediaStream,
              options?: MediaRecorderOptions) {
    throw "MediaRecorder not available in this browser."
  }

  static isTypeSupported(mimeType: string): boolean {
    return false;
  }

  pause(): void {
  }

  requestData(): void {
  }

  resume(): void {
  }

  start(timeslice: number): void {
  }

  stop(): void {
  }
}

@Injectable()
export class AudioRecorderService {
  private requestProvider: AudioInputProvider;
  private recorderImpl: MediaRecorderConstructor;
  private recorder: MediaRecorder;
  private recordingStateChange: Subject<RecorderServiceStatus>;
  recordingStateChange$: Observable<RecorderServiceStatus>;
  private newRecording: Subject<Blob>;
  newRecording$: Observable<Blob>;
  private isRecordingAble: boolean;
  private isRecording: boolean;
  private chunks: Blob[];

  constructor(@Inject('AudioInputProvider') requestProvider: AudioInputProvider,
              @Inject(
                'MediaRecorderFactory'
              ) recorderImpl: MediaRecorderConstructor,
              private ngZone: NgZone) {
    this.requestProvider = requestProvider;
    this.recorderImpl = recorderImpl;
    this.recordingStateChange = new Subject<RecorderServiceStatus>();
    this.recordingStateChange$ = this.recordingStateChange.asObservable();
    this.newRecording = new Subject<Blob>();
    this.newRecording$ = this.newRecording.asObservable();
    this.isRecordingAble = false;
    this.isRecording = false;
    this.chunks = [];
    this.hasRecordingCapabilities();
  }

  private hasRecordingCapabilities(): void {
    this.requestProvider().then(stream => {
      try {
        this.recorder = new this.recorderImpl(stream);

        this.recorder.ondataavailable = e => this.chunks.push(e.data);
        this.recorder.onstop = () => {
          const blob = new Blob(this.chunks, { 'type': this.recorder.mimeType });
          this.chunks.length = 0;
          this.ngZone.run(() => {
            this.newRecording.next(
              blob
            );
          });
        };
        this.isRecordingAble = true;
        this.recordingStateChange.next("enabled");
      } catch (e) {
        this.isRecordingAble = false;
        this.recordingStateChange.next("disabled"); // don't really need to do this
        console.warn(e); // TODO emit an error message for display?
      }
    }, rejectMessage => {
      this.isRecordingAble = false;
      this.recordingStateChange.next("disabled"); // again, probably not needed
      console.warn(rejectMessage); // TODO something better
    });
  }

  toggleRecording(): void {
    if (!this.isRecordingAble) return;

    if (this.isRecording) {
      this.endRecording();
    } else {
      this.startRecording();
    }
  }

  private startRecording(): void {
    if (this.recorder) {
      this.isRecording = true;
      this.recorder.start();
      this.recordingStateChange.next("recording");
    }
  }

  private endRecording(): void {
    if (this.recorder) {
      this.isRecording = false;
      this.recorder.stop();
      this.chunks.length = 0; // empty the array
      this.recordingStateChange.next("enabled");
    }
  }
}