annotate 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
rev   line source
dev@132 1 /**
dev@132 2 * Created by lucas on 17/03/2017.
dev@132 3 */
dev@145 4 import {Injectable, Inject, NgZone} from "@angular/core";
dev@132 5 import {Subject, Observable} from "rxjs";
dev@132 6
dev@132 7
dev@132 8 // seems the TypeScript definitions are not up to date,
dev@132 9 // introduce own types for now
dev@132 10
dev@132 11 export type AudioInputProvider = () => PromiseLike<MediaStream>;
dev@132 12
dev@132 13 export interface MediaRecorderOptions {
dev@132 14 mimeType?: string;
dev@132 15 audioBitsPerSecond?: number;
dev@132 16 videoBitsPerSecond?: number;
dev@132 17 bitsPerSecond?: number;
dev@132 18 }
dev@132 19
dev@132 20 export type RecordingState = "inactive" | "recording" | "paused";
dev@132 21
dev@132 22 export interface BlobEvent extends Event {
dev@132 23 readonly data: Blob;
dev@132 24 readonly timecode: number;
dev@132 25 }
dev@132 26
dev@132 27 export interface MediaRecorderErrorEvent extends Event {
dev@132 28 readonly error: DOMException;
dev@132 29 }
dev@132 30
dev@132 31 export interface MediaRecorder {
dev@132 32 readonly mimeType: string;
dev@132 33 readonly state: RecordingState;
dev@132 34 readonly stream: MediaStream;
dev@132 35 ignoreMutedMedia: boolean;
dev@132 36 readonly videoBitsPerSecond: number;
dev@132 37 readonly audioBitsPerSecond: number;
dev@132 38 // isTypeSupported(mimeType: string): boolean;
dev@132 39 pause(): void;
dev@132 40 requestData(): void;
dev@132 41 resume(): void;
dev@132 42 start(timeslice?: number): void;
dev@132 43 stop(): void;
dev@132 44 onstart: (evt: Event) => void;
dev@132 45 onstop: (evt: Event) => void;
dev@132 46 ondataavailable: (evt: BlobEvent) => void;
dev@132 47 onpause: (evt: Event) => void;
dev@132 48 onresume: (evt: Event) => void;
dev@132 49 onerror: (evt: MediaRecorderErrorEvent) => void;
dev@132 50 }
dev@132 51
dev@132 52 export interface MediaRecorderConstructor {
dev@132 53 new(stream: MediaStream,
dev@132 54 options?: MediaRecorderOptions): MediaRecorder;
dev@132 55 isTypeSupported(mimeType: string): boolean;
dev@132 56 }
dev@132 57
dev@132 58 export type RecorderServiceStatus = "disabled" | "enabled" | "recording";
dev@132 59
dev@132 60 export class ThrowingMediaRecorder implements MediaRecorder {
dev@132 61 mimeType: string;
dev@132 62 state: RecordingState;
dev@132 63 stream: MediaStream;
dev@132 64 ignoreMutedMedia: boolean;
dev@132 65 videoBitsPerSecond: number;
dev@132 66 audioBitsPerSecond: number;
dev@132 67 onstart: (evt: Event) => void;
dev@132 68 onstop: (evt: Event) => void;
dev@132 69 ondataavailable: (evt: BlobEvent) => void;
dev@132 70 onpause: (evt: Event) => void;
dev@132 71 onresume: (evt: Event) => void;
dev@132 72 onerror: (evt: MediaRecorderErrorEvent) => void;
dev@132 73
dev@132 74 constructor(stream: MediaStream,
dev@132 75 options?: MediaRecorderOptions) {
dev@132 76 throw "MediaRecorder not available in this browser."
dev@132 77 }
dev@132 78
dev@132 79 static isTypeSupported(mimeType: string): boolean {
dev@132 80 return false;
dev@132 81 }
dev@132 82
dev@132 83 pause(): void {
dev@132 84 }
dev@132 85
dev@132 86 requestData(): void {
dev@132 87 }
dev@132 88
dev@132 89 resume(): void {
dev@132 90 }
dev@132 91
dev@132 92 start(timeslice: number): void {
dev@132 93 }
dev@132 94
dev@132 95 stop(): void {
dev@132 96 }
dev@132 97 }
dev@132 98
dev@132 99 @Injectable()
dev@132 100 export class AudioRecorderService {
dev@132 101 private requestProvider: AudioInputProvider;
dev@132 102 private recorderImpl: MediaRecorderConstructor;
dev@132 103 private recorder: MediaRecorder;
dev@132 104 private recordingStateChange: Subject<RecorderServiceStatus>;
dev@132 105 recordingStateChange$: Observable<RecorderServiceStatus>;
dev@132 106 private newRecording: Subject<Blob>;
dev@132 107 newRecording$: Observable<Blob>;
dev@132 108 private isRecordingAble: boolean;
dev@132 109 private isRecording: boolean;
dev@132 110 private chunks: Blob[];
dev@132 111
dev@132 112 constructor(@Inject('AudioInputProvider') requestProvider: AudioInputProvider,
dev@132 113 @Inject(
dev@132 114 'MediaRecorderFactory'
dev@145 115 ) recorderImpl: MediaRecorderConstructor,
dev@145 116 private ngZone: NgZone) {
dev@132 117 this.requestProvider = requestProvider;
dev@132 118 this.recorderImpl = recorderImpl;
dev@132 119 this.recordingStateChange = new Subject<RecorderServiceStatus>();
dev@132 120 this.recordingStateChange$ = this.recordingStateChange.asObservable();
dev@132 121 this.newRecording = new Subject<Blob>();
dev@132 122 this.newRecording$ = this.newRecording.asObservable();
dev@132 123 this.isRecordingAble = false;
dev@132 124 this.isRecording = false;
dev@132 125 this.chunks = [];
dev@132 126 this.hasRecordingCapabilities();
dev@132 127 }
dev@132 128
dev@132 129 private hasRecordingCapabilities(): void {
dev@132 130 this.requestProvider().then(stream => {
dev@132 131 try {
dev@132 132 this.recorder = new this.recorderImpl(stream);
dev@132 133
dev@132 134 this.recorder.ondataavailable = e => this.chunks.push(e.data);
dev@132 135 this.recorder.onstop = () => {
dev@132 136 const blob = new Blob(this.chunks, { 'type': this.recorder.mimeType });
dev@132 137 this.chunks.length = 0;
dev@145 138 this.ngZone.run(() => {
dev@145 139 this.newRecording.next(
dev@145 140 blob
dev@145 141 );
dev@145 142 });
dev@132 143 };
dev@132 144 this.isRecordingAble = true;
dev@132 145 this.recordingStateChange.next("enabled");
dev@132 146 } catch (e) {
dev@132 147 this.isRecordingAble = false;
dev@132 148 this.recordingStateChange.next("disabled"); // don't really need to do this
dev@132 149 console.warn(e); // TODO emit an error message for display?
dev@132 150 }
dev@132 151 }, rejectMessage => {
dev@132 152 this.isRecordingAble = false;
dev@132 153 this.recordingStateChange.next("disabled"); // again, probably not needed
dev@132 154 console.warn(rejectMessage); // TODO something better
dev@132 155 });
dev@132 156 }
dev@132 157
dev@132 158 toggleRecording(): void {
dev@132 159 if (!this.isRecordingAble) return;
dev@132 160
dev@132 161 if (this.isRecording) {
dev@132 162 this.endRecording();
dev@132 163 } else {
dev@132 164 this.startRecording();
dev@132 165 }
dev@132 166 }
dev@132 167
dev@132 168 private startRecording(): void {
dev@132 169 if (this.recorder) {
dev@132 170 this.isRecording = true;
dev@132 171 this.recorder.start();
dev@132 172 this.recordingStateChange.next("recording");
dev@132 173 }
dev@132 174 }
dev@132 175
dev@132 176 private endRecording(): void {
dev@132 177 if (this.recorder) {
dev@132 178 this.isRecording = false;
dev@132 179 this.recorder.stop();
dev@132 180 this.chunks.length = 0; // empty the array
dev@132 181 this.recordingStateChange.next("enabled");
dev@132 182 }
dev@132 183 }
dev@132 184 }