dev@132: /** dev@132: * Created by lucas on 17/03/2017. dev@132: */ dev@236: import {Injectable, Inject, NgZone} from '@angular/core'; dev@236: import {Observable} from 'rxjs/Observable'; dev@236: import {Subject} from 'rxjs/Subject'; dev@132: dev@132: dev@132: // seems the TypeScript definitions are not up to date, dev@132: // introduce own types for now dev@132: dev@293: export type AudioInputProvider = () => Promise; dev@132: dev@132: export interface MediaRecorderOptions { dev@132: mimeType?: string; dev@132: audioBitsPerSecond?: number; dev@132: videoBitsPerSecond?: number; dev@132: bitsPerSecond?: number; dev@132: } dev@132: dev@236: export type RecordingState = 'inactive' | 'recording' | 'paused'; dev@132: dev@132: export interface BlobEvent extends Event { dev@132: readonly data: Blob; dev@132: readonly timecode: number; dev@132: } dev@132: dev@132: export interface MediaRecorderErrorEvent extends Event { dev@132: readonly error: DOMException; dev@132: } dev@132: dev@132: export interface MediaRecorder { dev@132: readonly mimeType: string; dev@132: readonly state: RecordingState; dev@132: readonly stream: MediaStream; dev@132: ignoreMutedMedia: boolean; dev@132: readonly videoBitsPerSecond: number; dev@132: readonly audioBitsPerSecond: number; dev@132: // isTypeSupported(mimeType: string): boolean; dev@132: onstart: (evt: Event) => void; dev@132: onstop: (evt: Event) => void; dev@132: ondataavailable: (evt: BlobEvent) => void; dev@132: onpause: (evt: Event) => void; dev@132: onresume: (evt: Event) => void; dev@132: onerror: (evt: MediaRecorderErrorEvent) => void; dev@236: pause(): void; dev@236: requestData(): void; dev@236: resume(): void; dev@236: start(timeslice?: number): void; dev@236: stop(): void; dev@132: } dev@132: dev@132: export interface MediaRecorderConstructor { dev@132: new(stream: MediaStream, dev@132: options?: MediaRecorderOptions): MediaRecorder; dev@132: isTypeSupported(mimeType: string): boolean; dev@132: } dev@132: dev@236: export type RecorderServiceStatus = 'disabled' | 'enabled' | 'recording'; dev@132: dev@132: export class ThrowingMediaRecorder implements MediaRecorder { dev@132: mimeType: string; dev@132: state: RecordingState; dev@132: stream: MediaStream; dev@132: ignoreMutedMedia: boolean; dev@132: videoBitsPerSecond: number; dev@132: audioBitsPerSecond: number; dev@132: onstart: (evt: Event) => void; dev@132: onstop: (evt: Event) => void; dev@132: ondataavailable: (evt: BlobEvent) => void; dev@132: onpause: (evt: Event) => void; dev@132: onresume: (evt: Event) => void; dev@132: onerror: (evt: MediaRecorderErrorEvent) => void; dev@132: dev@132: static isTypeSupported(mimeType: string): boolean { dev@132: return false; dev@132: } dev@132: dev@236: constructor(stream: MediaStream, dev@236: options?: MediaRecorderOptions) { dev@236: throw new Error('MediaRecorder not available in this browser.'); dev@236: } dev@236: dev@236: dev@132: pause(): void { dev@132: } dev@132: dev@132: requestData(): void { dev@132: } dev@132: dev@132: resume(): void { dev@132: } dev@132: dev@132: start(timeslice: number): void { dev@132: } dev@132: dev@132: stop(): void { dev@132: } dev@132: } dev@132: dev@132: @Injectable() dev@132: export class AudioRecorderService { dev@132: private requestProvider: AudioInputProvider; dev@132: private recorderImpl: MediaRecorderConstructor; dev@293: private currentRecorder: MediaRecorder; dev@132: private recordingStateChange: Subject; dev@132: recordingStateChange$: Observable; dev@132: private newRecording: Subject; dev@132: newRecording$: Observable; dev@132: private isRecording: boolean; dev@132: private chunks: Blob[]; dev@132: dev@132: constructor(@Inject('AudioInputProvider') requestProvider: AudioInputProvider, dev@132: @Inject( dev@132: 'MediaRecorderFactory' dev@145: ) recorderImpl: MediaRecorderConstructor, dev@145: private ngZone: NgZone) { dev@132: this.requestProvider = requestProvider; dev@132: this.recorderImpl = recorderImpl; dev@132: this.recordingStateChange = new Subject(); dev@132: this.recordingStateChange$ = this.recordingStateChange.asObservable(); dev@132: this.newRecording = new Subject(); dev@132: this.newRecording$ = this.newRecording.asObservable(); dev@132: this.isRecording = false; dev@132: this.chunks = []; dev@132: } dev@132: dev@293: private getRecorderInstance(): Promise { dev@293: return this.requestProvider().then(stream => { dev@293: const recorder = new this.recorderImpl(stream); dev@293: recorder.ondataavailable = e => this.chunks.push(e.data); dev@293: recorder.onstop = () => { dev@293: const blob = new Blob(this.chunks, {'type': recorder.mimeType}); dev@293: this.chunks.length = 0; dev@293: this.ngZone.run(() => { dev@293: this.newRecording.next( dev@293: blob dev@293: ); dev@293: }); dev@293: }; dev@293: return recorder; dev@132: }); dev@132: } dev@132: dev@132: toggleRecording(): void { dev@132: if (this.isRecording) { dev@132: this.endRecording(); dev@132: } else { dev@293: this.getRecorderInstance() dev@293: .then(recorder => this.startRecording(recorder)) dev@293: .catch(e => { dev@293: this.recordingStateChange.next('disabled'); // don't really need to do this dev@293: console.warn(e); // TODO emit an error message for display? dev@293: }); dev@132: } dev@132: } dev@132: dev@293: private startRecording(recorder: MediaRecorder): void { dev@293: this.currentRecorder = recorder; dev@293: this.isRecording = true; dev@293: recorder.start(); dev@293: this.recordingStateChange.next('recording'); dev@132: } dev@132: dev@132: private endRecording(): void { dev@293: if (this.currentRecorder) { dev@132: this.isRecording = false; dev@293: this.currentRecorder.stop(); dev@293: for (const track of this.currentRecorder.stream.getAudioTracks()) { dev@293: track.stop(); dev@293: } dev@132: this.chunks.length = 0; // empty the array dev@236: this.recordingStateChange.next('enabled'); dev@293: this.currentRecorder = null; dev@132: } dev@132: } dev@132: }