extendable-media-recorder
Version:
An extendable drop-in replacement for the native MediaRecorder.
131 lines • 7.52 kB
JavaScript
import { encode, instantiate } from 'media-encoder-host';
import { MultiBufferDataView } from 'multi-buffer-data-view';
import { on } from 'subscribable-things';
export const createWebmPcmMediaRecorderFactory = (createBlobEvent, decodeWebMChunk, readVariableSizeInteger) => {
return (eventTarget, nativeMediaRecorderConstructor, mediaStream, mimeType) => {
const bufferedArrayBuffers = [];
const nativeMediaRecorder = new nativeMediaRecorderConstructor(mediaStream, { mimeType: 'audio/webm;codecs=pcm' });
let promisedPartialRecording = null;
let stopRecording = () => { }; // tslint:disable-line:no-empty
const dispatchDataAvailableEvent = (arrayBuffers) => {
eventTarget.dispatchEvent(createBlobEvent('dataavailable', { data: new Blob(arrayBuffers, { type: mimeType }) }));
};
const requestNextPartialRecording = async (encoderInstanceId, timeslice) => {
const arrayBuffers = await encode(encoderInstanceId, timeslice);
if (nativeMediaRecorder.state === 'inactive') {
bufferedArrayBuffers.push(...arrayBuffers);
}
else {
dispatchDataAvailableEvent(arrayBuffers);
promisedPartialRecording = requestNextPartialRecording(encoderInstanceId, timeslice);
}
};
const stop = () => {
if (nativeMediaRecorder.state === 'inactive') {
return;
}
if (promisedPartialRecording !== null) {
promisedPartialRecording.catch(() => {
/* @todo Only catch the errors caused by a duplicate call to encode. */
});
promisedPartialRecording = null;
}
stopRecording();
stopRecording = () => { }; // tslint:disable-line:no-empty
nativeMediaRecorder.stop();
};
nativeMediaRecorder.addEventListener('error', (event) => {
stop();
eventTarget.dispatchEvent(new ErrorEvent('error', {
error: event.error
}));
});
nativeMediaRecorder.addEventListener('pause', () => eventTarget.dispatchEvent(new Event('pause')));
nativeMediaRecorder.addEventListener('resume', () => eventTarget.dispatchEvent(new Event('resume')));
nativeMediaRecorder.addEventListener('start', () => eventTarget.dispatchEvent(new Event('start')));
return {
get mimeType() {
return mimeType;
},
get state() {
return nativeMediaRecorder.state;
},
pause() {
return nativeMediaRecorder.pause();
},
resume() {
return nativeMediaRecorder.resume();
},
start(timeslice) {
const [audioTrack] = mediaStream.getAudioTracks();
if (audioTrack !== undefined && nativeMediaRecorder.state === 'inactive') {
// Bug #19: Chrome does not expose the correct channelCount property right away.
const { channelCount, sampleRate } = audioTrack.getSettings();
if (channelCount === undefined) {
throw new Error('The channelCount is not defined.');
}
if (sampleRate === undefined) {
throw new Error('The sampleRate is not defined.');
}
let isRecording = false;
let isStopped = false;
// Bug #9: Chrome sometimes fires more than one dataavailable event while being inactive.
let pendingInvocations = 0;
let promisedDataViewElementTypeEncoderInstanceIdAndPort = instantiate(mimeType, sampleRate);
stopRecording = () => {
isStopped = true;
};
const removeEventListener = on(nativeMediaRecorder, 'dataavailable')(({ data }) => {
pendingInvocations += 1;
const promisedArrayBuffer = data.arrayBuffer();
promisedDataViewElementTypeEncoderInstanceIdAndPort = promisedDataViewElementTypeEncoderInstanceIdAndPort.then(async ({ dataView = null, elementType = null, encoderInstanceId, port }) => {
const arrayBuffer = await promisedArrayBuffer;
pendingInvocations -= 1;
const currentDataView = dataView === null
? new MultiBufferDataView([arrayBuffer])
: new MultiBufferDataView([...dataView.buffers, arrayBuffer], dataView.byteOffset);
if (!isRecording && nativeMediaRecorder.state === 'recording' && !isStopped) {
const lengthAndValue = readVariableSizeInteger(currentDataView, 0);
if (lengthAndValue === null) {
return { dataView: currentDataView, elementType, encoderInstanceId, port };
}
const { value } = lengthAndValue;
if (value !== 172351395) {
return { dataView, elementType, encoderInstanceId, port };
}
isRecording = true;
}
const { currentElementType, offset, contents } = decodeWebMChunk(currentDataView, elementType, channelCount);
const remainingDataView = offset < currentDataView.byteLength
? new MultiBufferDataView(currentDataView.buffers, currentDataView.byteOffset + offset)
: null;
contents.forEach((content) => port.postMessage(content, content.map(({ buffer }) => buffer)));
if (pendingInvocations === 0 && (nativeMediaRecorder.state === 'inactive' || isStopped)) {
encode(encoderInstanceId, null).then((arrayBuffers) => {
dispatchDataAvailableEvent([...bufferedArrayBuffers, ...arrayBuffers]);
bufferedArrayBuffers.length = 0;
eventTarget.dispatchEvent(new Event('stop'));
});
port.postMessage([]);
port.close();
removeEventListener();
}
return { dataView: remainingDataView, elementType: currentElementType, encoderInstanceId, port };
});
});
if (timeslice !== undefined) {
promisedDataViewElementTypeEncoderInstanceIdAndPort.then(({ encoderInstanceId }) => {
if (isStopped) {
return;
}
promisedPartialRecording = requestNextPartialRecording(encoderInstanceId, timeslice);
});
}
}
nativeMediaRecorder.start(100);
},
stop
};
};
};
//# sourceMappingURL=webm-pcm-media-recorder.js.map