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