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