changeset 132:36f57a21637c

Add a service for audio recording which uses relies on being provided implementations of a provider to a MediaStream and a MediaRecorder. Include a stub for use when recording isn't supported.
author Lucas Thompson <dev@lucas.im>
date Mon, 20 Mar 2017 13:24:36 +0000
parents 4eb3cc32f6c0
children 4452f4b9f9a8
files src/app/services/audio-recorder/audio-recorder.service.ts
diffstat 1 files changed, 181 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/services/audio-recorder/audio-recorder.service.ts	Mon Mar 20 13:24:36 2017 +0000
@@ -0,0 +1,181 @@
+/**
+ * Created by lucas on 17/03/2017.
+ */
+import {Injectable, Inject} 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) {
+    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.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");
+    }
+  }
+}