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 }
|