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@494: import {NotificationService} from '../notifications/notifications.service'; 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@444: 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@452: private knownTypes = [ dev@452: {mimeType: 'audio/ogg', extension: 'ogg'}, dev@452: {mimeType: 'audio/webm', extension: 'webm'}, dev@452: {mimeType: 'audio/wav', extension: 'wav'} dev@452: ]; dev@132: dev@132: constructor(@Inject('AudioInputProvider') requestProvider: AudioInputProvider, dev@132: @Inject( dev@132: 'MediaRecorderFactory' dev@145: ) recorderImpl: MediaRecorderConstructor, dev@494: private ngZone: NgZone, dev@494: private notifier: NotificationService) { 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@452: const supported = this.knownTypes.find( dev@452: ({mimeType, extension}) => this.recorderImpl.isTypeSupported(mimeType) dev@452: ); dev@452: const recorder = new this.recorderImpl(stream, supported ? { dev@452: mimeType: supported.mimeType dev@452: } : {}); dev@452: dev@293: recorder.ondataavailable = e => this.chunks.push(e.data); dev@293: recorder.onstop = () => { dev@452: const blob = new Blob(this.chunks, { dev@452: 'type': recorder.mimeType || supported.mimeType dev@452: }); 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@494: this.notifier.displayError(e); 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: }