@twilio/rtc-diagnostics
Version:
Various diagnostics functions to help analyze connections to Twilio
125 lines (112 loc) • 3.5 kB
text/typescript
import { DiagnosticError } from '../errors';
import { MediaStreamRecorder } from '../types';
import { Encoder } from './encoder';
/**
* The [[AudioRecorder]] allows cross browser recording of audio from an input MediaStream.
* It uses the native MediaStream Recording APIs if available, else, it process raw audio data
* and converts it to a blob.
* @internalapi
*/
export class AudioRecorder {
/**
* The raw audio data captured during the test
*/
private _audioData: MediaStreamRecorder.AudioData[] = [];
/**
* The MediaRecorder instance to be used for capturing audio
*/
private _mediaRecorder: MediaStreamRecorder.MediaRecorder;
/**
* Promise handle after calling .stop()
*/
private _stopPromise: Promise<DiagnosticError> | null = null;
/**
* The source input stream
*/
private _stream: MediaStream;
/**
* The resuling object url that can be used for audio playback
*/
private _url: string = '';
/**
* Construct an [[AudioRecorder]] instance and will start the recording immediately.
* @constructor
* @param options
*/
constructor(options: AudioRecorder.ExtendedOptions) {
const factory = options.MediaRecorderFactory ?? (window as any).MediaRecorder ?? Encoder;
this._stream = options.stream.clone();
this._mediaRecorder = new factory(this._stream, options.audioContext);
this._mediaRecorder.ondataavailable = (e: MediaStreamRecorder.DataEvent) => this._audioData.push(e.data);
this._mediaRecorder.start();
}
/**
* Stops the recording process.
* If successful, the `.url` property will be populated.
*/
stop(): Promise<DiagnosticError | null> {
if (this._stopPromise) {
return Promise.reject(new DiagnosticError(undefined, 'MediaRecorder has already stopped'));
}
this._stopPromise = new Promise((resolve, reject) => {
this._mediaRecorder.onstop = () => {
try {
this._stream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
this._generateObjectUrl();
} catch (ex) {
reject(new DiagnosticError(ex, 'Unable to generate Object URL'));
return;
}
resolve();
};
this._mediaRecorder.stop();
});
return this._stopPromise;
}
/**
* Generates the object url that can be used for audio playback from raw audio data
*/
private _generateObjectUrl(): void {
// Select default browser mime type if it exists.
// Otherwise, use wav for faster and simple encoding.
const type: string = this._mediaRecorder && this._mediaRecorder.mimeType ? this._mediaRecorder.mimeType : 'audio/wav';
const blob = new Blob(this._audioData, { type });
this._url = URL.createObjectURL(blob);
this._audioData = [];
}
/**
* The resuling object url that can be used for audio playback
*/
get url(): string {
return this._url;
}
}
/**
* @internalapi
*/
export namespace AudioRecorder {
/**
* Options that may be passed to [[AudioRecorder]] constructor for internal testing.
* @internalapi
*/
export interface ExtendedOptions extends Options {
/**
* The MediaRecorder class to use for testing
*/
MediaRecorderFactory?: any;
}
/**
* Options passed to [[AudioRecorder]] constructor.
* @internalapi
*/
export interface Options {
/**
* The AudioContext instance to use
*/
audioContext: AudioContext;
/**
* The source input stream
*/
stream: MediaStream;
}
}