dev@132: /** dev@132: * Created by lucas on 17/03/2017. dev@132: */ dev@145: import {Injectable, Inject, NgZone} from "@angular/core"; dev@132: import {Subject, Observable} from "rxjs"; 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@132: export type AudioInputProvider = () => PromiseLike; 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@132: 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: pause(): void; dev@132: requestData(): void; dev@132: resume(): void; dev@132: start(timeslice?: number): void; dev@132: stop(): void; 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: 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@132: 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: constructor(stream: MediaStream, dev@132: options?: MediaRecorderOptions) { dev@132: throw "MediaRecorder not available in this browser." dev@132: } dev@132: dev@132: static isTypeSupported(mimeType: string): boolean { dev@132: return false; dev@132: } dev@132: 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@132: private recorder: MediaRecorder; dev@132: private recordingStateChange: Subject; dev@132: recordingStateChange$: Observable; dev@132: private newRecording: Subject; dev@132: newRecording$: Observable; dev@132: private isRecordingAble: boolean; 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.isRecordingAble = false; dev@132: this.isRecording = false; dev@132: this.chunks = []; dev@132: this.hasRecordingCapabilities(); dev@132: } dev@132: dev@132: private hasRecordingCapabilities(): void { dev@132: this.requestProvider().then(stream => { dev@132: try { dev@132: this.recorder = new this.recorderImpl(stream); dev@132: dev@132: this.recorder.ondataavailable = e => this.chunks.push(e.data); dev@132: this.recorder.onstop = () => { dev@132: const blob = new Blob(this.chunks, { 'type': this.recorder.mimeType }); dev@132: this.chunks.length = 0; dev@145: this.ngZone.run(() => { dev@145: this.newRecording.next( dev@145: blob dev@145: ); dev@145: }); dev@132: }; dev@132: this.isRecordingAble = true; dev@132: this.recordingStateChange.next("enabled"); dev@132: } catch (e) { dev@132: this.isRecordingAble = false; dev@132: this.recordingStateChange.next("disabled"); // don't really need to do this dev@132: console.warn(e); // TODO emit an error message for display? dev@132: } dev@132: }, rejectMessage => { dev@132: this.isRecordingAble = false; dev@132: this.recordingStateChange.next("disabled"); // again, probably not needed dev@132: console.warn(rejectMessage); // TODO something better dev@132: }); dev@132: } dev@132: dev@132: toggleRecording(): void { dev@132: if (!this.isRecordingAble) return; dev@132: dev@132: if (this.isRecording) { dev@132: this.endRecording(); dev@132: } else { dev@132: this.startRecording(); dev@132: } dev@132: } dev@132: dev@132: private startRecording(): void { dev@132: if (this.recorder) { dev@132: this.isRecording = true; dev@132: this.recorder.start(); dev@132: this.recordingStateChange.next("recording"); dev@132: } dev@132: } dev@132: dev@132: private endRecording(): void { dev@132: if (this.recorder) { dev@132: this.isRecording = false; dev@132: this.recorder.stop(); dev@132: this.chunks.length = 0; // empty the array dev@132: this.recordingStateChange.next("enabled"); dev@132: } dev@132: } dev@132: }