Запись медиа элемента
В статье Использование интерфейса MediaStream Recording API демонстрируется использование объекта типа MediaRecorder
для захвата потока, представляющего объект типа MediaStream
, сгенерированного аппаратными средствами устройства и возвращаемого методом navigator.mediaDevices.getUserMedia()
, но можно также использовать HTML медиа элемент (а именно <audio>
или <video>
) в качестве источника потока MediaStream
для его записи. В этой статье рассматривается пример выполняющий это.
Пример записи с помощью медиа элемента
HTML
Рассмотрим ключевые моменты кода HTML. Это только небольшой отрывок, относящийся к информационной части приложения.
<div class="left">
<div id="startButton" class="button">Start</div>
<h2>Preview</h2>
<video id="preview" width="160" height="120" autoplay muted></video>
</div>
Основной интерфейс представляется в двух колонках. В левой находиться кнопка старта и элемент <video>
, который отображает предварительный просмотр видео. Это видео, воспроизводится камерой устройства. Заметьте, что используется атрибут autoplay
, что бы поток начал воспроизводиться немедленно, снимаясь прямо с камеры. Атрибут muted
гарантирует отключение звука с микрофона, для предотвращения цикличного эхо эффекта.
<div class="right">
<div id="stopButton" class="button">Stop</div>
<h2>Recording</h2>
<video id="recording" width="160" height="120" controls></video>
<a id="downloadButton" class="button"> Download </a>
</div>
Справа мы видим кнопку остановки и элемент <video>,
который будет использоваться для воспроизведения записанного видео. Обратите внимание, что на панели воспроизведения не установлен режим автозапуска (поэтому воспроизведение не начинается сразу после поступления мультимедиа), а также установлен атрибут controls
, что говорит о необходимости показывать пользовательские элементы управления для воспроизведения, паузы и т. д.
Под элементом воспроизведения находится кнопка для загрузки записанного видео.
JavaScript
Теперь давайте посмотрим на код JavaScript
Установка глобальных переменных
Мы начнём с установления некоторых глобальных переменных, которые нам понадобятся.
let preview = document.getElementById("preview");
let recording = document.getElementById("recording");
let startButton = document.getElementById("startButton");
let stopButton = document.getElementById("stopButton");
let downloadButton = document.getElementById("downloadButton");
let logElement = document.getElementById("log");
let recordingTimeMS = 5000;
Большинство из них являются ссылками на элементы, с которыми нам нужно работать. Последняя, recordingTimeMS
, установлена на 5000 миллисекунд (5 секунд);.
Используемые функции
Далее мы создадим несколько служебных функций, которые будут использованы позже.
function log(msg) {
logElement.innerHTML += msg + "\n";
}
Функция log ()
используется для вывода текстовых строк в <div>
, чтобы мы могли делиться информацией с пользователем.
function wait(delayInMS) {
return new Promise((resolve) => setTimeout(resolve, delayInMS));
}
The wait()
function returns a new Promise
which resolves once the specified number of milliseconds have elapsed. It works by using an arrow function which calls window.setTimeout()
, specifying the promise's resolution handler as the timeout handler function. That lets us use promise syntax when using timeouts, which can be very handy when chaining promises, as we'll see later.
Starting media recording
The startRecording()
function handles starting the recording process:
function startRecording(stream, lengthInMS) {
let recorder = new MediaRecorder(stream);
let data = [];
recorder.ondataavailable = (event) => data.push(event.data);
recorder.start();
log(recorder.state + " for " + lengthInMS / 1000 + " seconds...");
let stopped = new Promise((resolve, reject) => {
recorder.onstop = resolve;
recorder.onerror = (event) => reject(event.name);
});
let recorded = wait(lengthInMS).then(
() => recorder.state == "recording" && recorder.stop(),
);
return Promise.all([stopped, recorded]).then(() => data);
}
startRecording()
takes two input parameters: a MediaStream
to record from and the length in milliseconds of the recording to make. We always record no more than the specified number of milliseconds of media, although if the media stops before that time is reached, MediaRecorder
automatically stops recording as well.
- Line 2
-
Creates the
MediaRecorder
that will handle recording the inputstream
. - Line 3
-
Creates an empty array,
data
, which will be used to hold theBlob
s of media data provided to ourondataavailable
event handler. - Line 5
-
Sets up the handler for the
dataavailable
event. The received event'sdata
property is aBlob
that contains the media data. The event handler simply pushes theBlob
onto thedata
array. - Lines 6-7
-
Starts the recording process by calling
recorder.start()
, and outputs a message to the log with the updated state of the recorder and the number of seconds it will be recording. - Lines 9-12
-
Creates a new
Promise
, namedstopped
, which is resolved when theMediaRecorder
'sonstop
event handler is called, and is rejected if itsonerror
event handler is called. The rejection handler receives as input the name of the error that occurred. - Lines 14-16
-
Creates a new
Promise
, namedrecorded
, which is resolved when the specified number of milliseconds have elapsed. Upon resolution, it stops theMediaRecorder
if it's recording. - Lines 18-22
-
These lines create a new
Promise
which is fulfilled when both of the twoPromise
s (stopped
andrecorded
) have resolved. Once that resolves, the array data is returned bystartRecording()
to its caller.
Stopping the input stream
The stop()
function simply stops the input media:
function stop(stream) {
stream.getTracks().forEach((track) => track.stop());
}
This works by calling MediaStream.getTracks()
, using forEach()
to call MediaStreamTrack.stop()
on each track in the stream.
Getting an input stream and setting up the recorder
Now let's look at the most intricate piece of code in this example: our event handler for clicks on the start button:
startButton.addEventListener(
"click",
function () {
navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then((stream) => {
preview.srcObject = stream;
downloadButton.href = stream;
preview.captureStream =
preview.captureStream || preview.mozCaptureStream;
return new Promise((resolve) => (preview.onplaying = resolve));
})
.then(() => startRecording(preview.captureStream(), recordingTimeMS))
.then((recordedChunks) => {
let recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
recording.src = URL.createObjectURL(recordedBlob);
downloadButton.href = recording.src;
downloadButton.download = "RecordedVideo.webm";
log(
"Successfully recorded " +
recordedBlob.size +
" bytes of " +
recordedBlob.type +
" media.",
);
})
.catch(log);
},
false,
);
When a click
event occurs, here's what happens:
- Lines 2-4
-
navigator.mediaDevices.getUserMedia()
is called to request a newMediaStream
that has both video and audio tracks. This is the stream we'll record. - Lines 5-9
-
When the Promise returned by
getUserMedia()
is resolved, the preview<video>
element'ssrcObject
property is set to be the input stream, which causes the video being captured by the user's camera to be displayed in the preview box. Since the<video>
element is muted, the audio won't play. The "Download" button's link is then set to refer to the stream as well. Then, in line 8, we arrange forpreview.captureStream()
to callpreview.mozCaptureStream()
so that our code will work on Firefox, on which theMediaRecorder.captureStream()
method is prefixed. Then a newPromise
which resolves when the preview video starts to play is created and returned. - Line 10
-
When the preview video begins to play, we know there's media to record, so we respond by calling the
startRecording()
function we created earlier, passing in the preview video stream (as the source media to be recorded) andrecordingTimeMS
as the number of milliseconds of media to record. As mentioned before,startRecording()
returns aPromise
whose resolution handler is called (receiving as input an array ofBlob
objects containing the chunks of recorded media data) once recording has completed. - Lines 11-15
-
The recording process's resolution handler receives as input an array of media data
Blob
s locally known asrecordedChunks
. The first thing we do is merge the chunks into a singleBlob
whose MIME type is"video/webm"
by taking advantage of the fact that theBlob()
constructor concatenates arrays of objects into one object. ThenURL.createObjectURL()
is used to create an URL that references the blob; this is then made the value of the recorded video playback element'ssrc
attribute (so that you can play the video from the blob) as well as the target of the download button's link.Then the download button's
download
attribute is set. While thedownload
attribute can be a Boolean, you can also set it to a string to use as the name for the downloaded file. So by setting the download link'sdownload
attribute to "RecordedVideo.webm", we tell the browser that clicking the button should download a file named"RecordedVideo.webm"
whose contents are the recorded video. - Lines 17-18
-
The size and type of the recorded media are output to the log area below the two videos and the download button.
- Line 20
-
The
catch()
for all thePromise
s outputs the error to the logging area by calling ourlog()
function.
Handling the stop button
The last bit of code adds a handler for the click
event on the stop button using addEventListener()
:
stopButton.addEventListener(
"click",
function () {
stop(preview.srcObject);
},
false,
);
This simply calls the stop()
function we covered earlier.
Result
When put all together with the rest of the HTML and the CSS not shown above, it looks and works like this:
You can take a look at all the code, including the parts hidden above because they aren't critical to the explanation of how the APIs are being used.