view src/app/services/audio-recorder/audio-recorder.service.ts @ 509:041468f553e1 tip master

Merge pull request #57 from LucasThompson/fix/session-stack-max-call-stack Fix accidental recursion in PersistentStack
author Lucas Thompson <LucasThompson@users.noreply.github.com>
date Mon, 27 Nov 2017 11:04:30 +0000
parents f52eb1b422f5
children
line wrap: on
line source
/**
 * Created by lucas on 17/03/2017.
 */
import {Injectable, Inject, NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {NotificationService} from '../notifications/notifications.service';


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

export type AudioInputProvider = () => Promise<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;
  onstart: (evt: Event) => void;
  onstop: (evt: Event) => void;
  ondataavailable: (evt: BlobEvent) => void;
  onpause: (evt: Event) => void;
  onresume: (evt: Event) => void;
  onerror: (evt: MediaRecorderErrorEvent) => void;
  pause(): void;
  requestData(): void;
  resume(): void;
  start(timeslice?: number): void;
  stop(): 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;

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

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


  pause(): void {
  }

  requestData(): void {
  }

  resume(): void {
  }

  start(timeslice: number): void {
  }

  stop(): void {
  }
}

@Injectable()
export class AudioRecorderService {
  private requestProvider: AudioInputProvider;
  private recorderImpl: MediaRecorderConstructor;
  private currentRecorder: MediaRecorder;
  private recordingStateChange: Subject<RecorderServiceStatus>;
  recordingStateChange$: Observable<RecorderServiceStatus>;
  private newRecording: Subject<Blob>;
  newRecording$: Observable<Blob>;
  private isRecording: boolean;
  private chunks: Blob[];
  private knownTypes = [
    {mimeType: 'audio/ogg', extension: 'ogg'},
    {mimeType: 'audio/webm', extension: 'webm'},
    {mimeType: 'audio/wav', extension: 'wav'}
  ];

  constructor(@Inject('AudioInputProvider') requestProvider: AudioInputProvider,
              @Inject(
                'MediaRecorderFactory'
              ) recorderImpl: MediaRecorderConstructor,
              private ngZone: NgZone,
              private notifier: NotificationService) {
    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.isRecording = false;
    this.chunks = [];
  }

  private getRecorderInstance(): Promise<MediaRecorder> {
    return this.requestProvider().then(stream => {
      const supported = this.knownTypes.find(
        ({mimeType, extension}) => this.recorderImpl.isTypeSupported(mimeType)
      );
      const recorder = new this.recorderImpl(stream, supported ? {
        mimeType: supported.mimeType
      } : {});

      recorder.ondataavailable = e => this.chunks.push(e.data);
      recorder.onstop = () => {
        const blob = new Blob(this.chunks, {
          'type': recorder.mimeType || supported.mimeType
        });
        this.chunks.length = 0;
        this.ngZone.run(() => {
          this.newRecording.next(
            blob
          );
        });
      };
      return recorder;
    });
  }

  toggleRecording(): void {
    if (this.isRecording) {
      this.endRecording();
    } else {
      this.getRecorderInstance()
        .then(recorder => this.startRecording(recorder))
        .catch(e => {
          this.recordingStateChange.next('disabled'); // don't really need to do this
          this.notifier.displayError(e);
        });
    }
  }

  private startRecording(recorder: MediaRecorder): void {
    this.currentRecorder = recorder;
    this.isRecording = true;
    recorder.start();
    this.recordingStateChange.next('recording');
  }

  private endRecording(): void {
    if (this.currentRecorder) {
      this.isRecording = false;
      this.currentRecorder.stop();
      for (const track of this.currentRecorder.stream.getAudioTracks()) {
        track.stop();
      }
      this.chunks.length = 0; // empty the array
      this.recordingStateChange.next('enabled');
      this.currentRecorder = null;
    }
  }
}