1. Introduction
This section is non-normative.
MediaStream objects act as opaque handles to a stream of audio and video data. These can be consumed in a variety of ways by various platform APIs, as discussed in [GETUSERMEDIA]. This specification defines a way of consuming them by creating a readable stream, whose chunks are Blobs of encoded audio/video data recorded from the stream in a standard container format.
The resulting readable stream, known as a MediaStream recorder and embodied by the MediaStreamRecorder interface, can then be read from directly by author code which wishes to manipulate these blobs. Alternately, it may be piped to another destination, or consumed by other code that takes a readable stream.
2. Example Usage
This section is non-normative.
To read six seconds of audio-video input from a user’s webcam as a single Blob, the following code could be used:
function getSixSecondsOfVideo() {
navigator.mediaDevices.getUserMedia({ video: true }).then(mediaStream => {
const recorder = new MediaStreamRecorder(mediaStream, { timeSlice: 6 * 1000 });
const reader = recorder.getReader();
return reader.read().then(({ value }) => {
reader.cancel();
return value;
});
});
}
getSixSecondsOfVideo().then(blob => ...);
This uses the timeSlice option to ensure that each chunk read from the MediaStreamRecorder is at least six seconds long. Upon receiving the first chunk, it immediately cancels the readable stream, as no more recording is necessary.
If the ultimate destination for the streaming audio-video input were somewhere else, say an [INDEXEDDB] database, then it would be more prudent to let the user agent choose the time slice, and to store the chunks as they are available:
navigator.mediaDevices.getUserMedia({ video: true }).then(mediaStream => {
const recorder = new MediaStreamRecorder(mediaStream);
writeReadableStreamToIndexedDBForSixSeconds(recorder);
});
let startTime;
function writeReadableStreamToIndexedDBForSixSeconds(rs) {
const reader = rs.getReader();
startTime = Date.now();
return pump();
function pump() {
return reader.read().then(({ value }) => {
writeBlobToIndexedDB(value); // gory details omitted
if (Date.now() - startTime > 6 * 1000) {
reader.cancel();
} else {
return pump();
}
});
}
}
If you were writing to a destination which had a proper writable stream representing it, this would of course become much easier:
navigator.mediaDevices.getUserMedia({ video: true }).then(mediaStream => {
startTime = Date.now();
const recorder = new MediaStreamRecorder(mediaStream);
const dest = getIndexedDBWritableStream(); // using hypothetical future capabilities
const piping = recorder.pipeTo(dest);
setTimeout(() => piping.cancel(), 6 * 1000); // XXX depends on cancelable promises
});
Alternately, your destination may accept readable streams, as is planned for [FETCH]. This example will continually stream video from the user’s video camera directly to a server endpoint, using standard [STREAMS] and [FETCH] idioms that work with any readable stream:
navigator.mediaDevices.getUserMedia({ video: true }).then(mediaStream => {
const recorder = new MediaStreamRecorder(mediaStream, { type: "video/mp4" });
return fetch("/storage/user-video.mp4", {
body: recorder,
headers: {
"Content-Type": "video/mp4"
}
});
});
Instead of a new class subclassing ReadableStream, with a bunch of useless-seeming getters, we could just have mediaStream.recordAsReadableStream(options) returning a vanilla ReadableStream. You'd presumably add MediaStream.canRecord(...) as well.
This design seems much simpler, if there is no use case for reading the options after creation. Which I can't imagine there are...
<https://github.com/domenic/streaming-mediastreams/issues/6>
3. The MediaStreamRecorder API
[Constructor(MediaStream stream, optional MediaStreamRecorderOptions options)]
interface MediaStreamRecorder : ReadableStream {
readonly attribute MediaStream mediaStream;
readonly attribute DOMString type;
readonly attribute boolean ignoreMutedMedia;
readonly attribute unsigned long long timeSlice;
readonly attribute unsigned long long bitRate;
static CanPlayTypeResult canRecordType(DOMString type);
};
dictionary MediaStreamRecorderOptions {
DOMString type;
boolean ignoreMutedMedia = false;
[EnforceRange] unsigned long long timeSlice = 0;
[EnforceRange] unsigned long long bitRate;
};
All MediaStreamRecorder instances have [[mediaStream]], [[type]], [[ignoreMutedMedia]], [[timeSlice]], and [[bitRate]] internal slots.
3.1. new MediaStreamRecorder(stream, options)
-
If
options.typeis present but is not a supported MIME type for media stream recording, throw aNotSupportedErrorDOMException. -
If
options.typeis present, let type beoptions.type. Otherwise, let type be a user-agent chosen default recording MIME type. -
If
options.bitRateis present, let bitRate beoptions.bitRate, clamped within a range deemed acceptable by the user agent. Otherwise, let bitRate be a default bit rate, perhaps dependent on type or timeSlice. -
Let timeSlice be the greater of
options.timeSliceand some minimum recording time slice imposed by the user agent. -
Call the superconstructor with appropriate underlying source and queuing strategy arguments so as to record
mediaStreamaccording to the following requirements:-
All data from
mediaStreammust be recorded asBlobchunks that are enqueued into this readable stream.The choice ofBlobinstead of, e.g.,ArrayBuffer, is to allow the data to be kept in a place that is not immediately accessible to the main thread. For example, Firefox separates its media subsystem from the main thread via asynchronous dispatch. See #5 for more discussion. -
All such chunks must represent at least timeSlice milliseconds of data, except potentially the last one if the
MediaStreamends before that much data can be recorded. Any excess length beyond timeSlice milliseconds for each chunk should be minimized. -
The resulting chunks must be created such that the original tracks of the
MediaStreamcan be retrieved at playback time by standard software meant for replaying the container format specified by type. When multipleBlobchunks are enqueued, the individualBlobs need not be playable, but the concatenation of all theBlobs from a completed recording must be playable. -
The resulting chunks must be encoded using bitRate as the bit rate for encoding.
-
If any track within the
MediaStreamis muted at any time, then either:-
If
options.ignoreMutedMediais true, nothing must be recorded for those tracks. -
Otherwise, the chunks enqueued to represent those tracks must be recorded as black frames or silence (as appropriate) while the track remains muted.
-
-
If at any point
mediaStream’s isolation properties change so that access to it is no longer allowed, this readable stream must be errored with aSecurityErrorDOMException. -
If recording cannot be started or at any point cannot continue (for reasons other than a security violation),
-
A chunk containing any currently-recorded but not-yet-enqueued data must be enqueued into this readable stream.
-
This readable stream must be errored with a
TypeError.
-
-
If
mediaStreamends, then this readable stream must be closed.
-
-
Set this@[[mediaStream]] to
mediaStream, this@[[type]] to type, this@[[ignoreMutedMedia]] tooptions.ignoreMutedMedia, this@[[timeSlice]] to timeSlice, and this@[[bitRate]] to bitRate.
<https://github.com/domenic/streaming-mediastreams/issues/1>
<https://github.com/domenic/streaming-mediastreams/issues/4>
3.2. get MediaStreamRecorder.prototype.mediaStream
-
Return this@[[mediaStream]].
3.3. get MediaStreamRecorder.prototype.type
-
Return this@[[type]].
<https://github.com/domenic/streaming-mediastreams/issues/2>
3.4. get MediaStreamRecorder.prototype.ignoreMutedMedia
-
Return this@[[ignoreMutedMedia]].
3.5. get MediaStreamRecorder.prototype.timeSlice
-
Return this@[[timeSlice]].
3.6. get MediaStreamRecorder.prototype.bitRate
-
Return this@[[bitRate]].
3.7. MediaStreamRecorder.canRecordType(type)
-
If the user agent knows that it cannot record
type, return"". -
If the user agent is confident that it can record
type, return"probably". -
Return
"maybe".
"maybe" unless the type can be confidently established as being
supported or not.
Acknowledgments
The editor would like to thank Jim Barnett and Travis Leithead for ther original [MEDIASTREAM-RECORDING] specification. This document is largely a reframing of their work on top of [STREAMS].
This specification is written by Domenic Denicola (Google, d@domenic.me).
Per CC0, to the extent possible under law, the editor has waived all copyright and related or neighboring rights to this work.
