blob: 49e7e034c0b067a832efc6ed3fe1a2f422d4547c [file] [edit]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/mediarecorder/media_recorder.h"
#include <algorithm>
#include <limits>
#include "base/time/time.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/dictionary.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_error_event_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_recording_state.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/events/error_event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/modules/event_target_modules.h"
#include "third_party/blink/renderer/modules/mediarecorder/blob_event.h"
#include "third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h"
#include "third_party/blink/renderer/modules/mediarecorder/video_track_recorder.h"
#include "third_party/blink/renderer/platform/blob/blob_data.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_descriptor.h"
#include "third_party/blink/renderer/platform/network/mime/content_type.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
namespace {
struct MediaRecorderBitrates {
const std::optional<uint32_t> audio_bps;
const std::optional<uint32_t> video_bps;
const std::optional<uint32_t> overall_bps;
};
// Boundaries of Opus SILK bitrate from https://www.opus-codec.org/.
const int kSmallestPossibleOpusBitRate = 5000;
const int kLargestPossibleOpusBitRate = 510000;
// Smallest Vpx bitrate that can be requested.
// 75kbps is the min bitrate recommended by VP9 VOD settings for 320x240 videos.
const int kSmallestPossibleVpxBitRate = 75000;
// Both values come from YouTube recommended upload encoding settings and are
// used by other browser vendors. See
// https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
const int kDefaultVideoBitRate = 2500e3; // 2.5Mbps
const int kDefaultAudioBitRate = 128e3; // 128kbps
V8RecordingState::Enum StateToV8Enum(MediaRecorder::State state) {
switch (state) {
case MediaRecorder::State::kInactive:
return V8RecordingState::Enum::kInactive;
case MediaRecorder::State::kRecording:
return V8RecordingState::Enum::kRecording;
case MediaRecorder::State::kPaused:
return V8RecordingState::Enum::kPaused;
}
NOTREACHED();
}
V8BitrateMode::Enum BitrateModeToV8Enum(
AudioTrackRecorder::BitrateMode bitrateMode) {
switch (bitrateMode) {
case AudioTrackRecorder::BitrateMode::kConstant:
return V8BitrateMode::Enum::kConstant;
case AudioTrackRecorder::BitrateMode::kVariable:
return V8BitrateMode::Enum::kVariable;
}
NOTREACHED();
}
AudioTrackRecorder::BitrateMode GetBitrateModeFromOptions(
const MediaRecorderOptions* const options) {
if (options->hasAudioBitrateMode()) {
if (options->audioBitrateMode() == V8BitrateMode::Enum::kConstant) {
return AudioTrackRecorder::BitrateMode::kConstant;
}
}
return AudioTrackRecorder::BitrateMode::kVariable;
}
void LogConsoleMessage(ExecutionContext* context, const String& message) {
context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kWarning, message));
}
uint32_t ClampAudioBitRate(ExecutionContext* context, uint32_t audio_bps) {
if (audio_bps > kLargestPossibleOpusBitRate) {
LogConsoleMessage(
context,
String::Format(
"Clamping calculated audio bitrate (%dbps) to the maximum (%dbps)",
audio_bps, kLargestPossibleOpusBitRate));
return kLargestPossibleOpusBitRate;
}
if (audio_bps < kSmallestPossibleOpusBitRate) {
LogConsoleMessage(
context,
String::Format(
"Clamping calculated audio bitrate (%dbps) to the minimum (%dbps)",
audio_bps, kSmallestPossibleOpusBitRate));
return kSmallestPossibleOpusBitRate;
}
return audio_bps;
}
uint32_t ClampVideoBitRate(ExecutionContext* context, uint32_t video_bps) {
if (video_bps < kSmallestPossibleVpxBitRate) {
LogConsoleMessage(
context,
String::Format(
"Clamping calculated video bitrate (%dbps) to the minimum (%dbps)",
video_bps, kSmallestPossibleVpxBitRate));
return kSmallestPossibleVpxBitRate;
}
return video_bps;
}
// Allocates the requested bit rates from |options| into the respective
// |{audio,video}_bps| (where a value of zero indicates Platform to use
// whatever it sees fit). If |options.bitsPerSecond()| is specified, it
// overrides any specific bitrate, and the UA is free to allocate as desired:
// here a 90%/10% video/audio is used. In all cases where a value is explicited
// or calculated, values are clamped in sane ranges.
// This method throws NotSupportedError.
MediaRecorderBitrates GetBitratesFromOptions(
ExceptionState& exception_state,
ExecutionContext* context,
const MediaRecorderOptions* options) {
// Clamp incoming values into a signed integer's range.
// TODO(mcasas): This section would no be needed if the bit rates are signed
// or double, see https://github.com/w3c/mediacapture-record/issues/48.
constexpr uint32_t kMaxIntAsUnsigned = std::numeric_limits<int>::max();
std::optional<uint32_t> audio_bps;
if (options->hasAudioBitsPerSecond()) {
audio_bps = std::min(options->audioBitsPerSecond(), kMaxIntAsUnsigned);
}
std::optional<uint32_t> video_bps;
if (options->hasVideoBitsPerSecond()) {
video_bps = std::min(options->videoBitsPerSecond(), kMaxIntAsUnsigned);
}
std::optional<uint32_t> overall_bps;
if (options->hasBitsPerSecond()) {
overall_bps = std::min(options->bitsPerSecond(), kMaxIntAsUnsigned);
audio_bps = ClampAudioBitRate(context, overall_bps.value() / 10);
video_bps = overall_bps.value() >= audio_bps.value()
? overall_bps.value() - audio_bps.value()
: 0u;
video_bps = ClampVideoBitRate(context, video_bps.value());
}
return {audio_bps, video_bps, overall_bps};
}
} // namespace
MediaRecorder* MediaRecorder::Create(ExecutionContext* context,
MediaStream* stream,
ExceptionState& exception_state) {
return MakeGarbageCollected<MediaRecorder>(
context, stream, MediaRecorderOptions::Create(), exception_state);
}
MediaRecorder* MediaRecorder::Create(ExecutionContext* context,
MediaStream* stream,
const MediaRecorderOptions* options,
ExceptionState& exception_state) {
return MakeGarbageCollected<MediaRecorder>(context, stream, options,
exception_state);
}
MediaRecorder::MediaRecorder(ExecutionContext* context,
MediaStream* stream,
const MediaRecorderOptions* options,
ExceptionState& exception_state)
: ActiveScriptWrappable<MediaRecorder>({}),
ExecutionContextLifecycleObserver(context),
stream_(stream),
mime_type_(options->mimeType()) {
if (context->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return;
}
if (options->hasVideoKeyFrameIntervalDuration() &&
options->hasVideoKeyFrameIntervalCount()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Both videoKeyFrameIntervalDuration and videoKeyFrameIntervalCount "
"can't be specified.");
return;
}
KeyFrameRequestProcessor::Configuration key_frame_config;
if (options->hasVideoKeyFrameIntervalDuration()) {
key_frame_config =
base::Milliseconds(options->videoKeyFrameIntervalDuration());
} else if (options->hasVideoKeyFrameIntervalCount()) {
key_frame_config = options->videoKeyFrameIntervalCount();
}
recorder_handler_ = MakeGarbageCollected<MediaRecorderHandler>(
context->GetTaskRunner(TaskType::kInternalMediaRealTime),
key_frame_config);
if (!recorder_handler_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"No MediaRecorder handler can be created.");
return;
}
const MediaRecorderBitrates bitrates =
GetBitratesFromOptions(exception_state, context, options);
const ContentType content_type(mime_type_);
if (!recorder_handler_->Initialize(this, stream->Descriptor(),
content_type.GetType(),
content_type.Parameter("codecs"),
GetBitrateModeFromOptions(options))) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
StrCat({"Failed to initialize native MediaRecorder the type provided (",
mime_type_, ") is not supported."}));
}
audio_bits_per_second_ = bitrates.audio_bps.value_or(kDefaultAudioBitRate);
video_bits_per_second_ = bitrates.video_bps.value_or(kDefaultVideoBitRate);
overall_bits_per_second_ = bitrates.overall_bps;
}
MediaRecorder::~MediaRecorder() = default;
V8RecordingState MediaRecorder::state() const {
return V8RecordingState(StateToV8Enum(state_));
}
V8BitrateMode MediaRecorder::audioBitrateMode() const {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
// Return a valid enum value; variable is the default.
return V8BitrateMode(V8BitrateMode::Enum::kVariable);
}
DCHECK(recorder_handler_);
return V8BitrateMode(
BitrateModeToV8Enum(recorder_handler_->AudioBitrateMode()));
}
void MediaRecorder::start(ExceptionState& exception_state) {
start(std::numeric_limits<int>::max() /* timeSlice */, exception_state);
}
void MediaRecorder::start(int time_slice, ExceptionState& exception_state) {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return;
}
if (state_ != State::kInactive) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
StrCat({"The MediaRecorder's state is '",
state().AsStringView(), "'."}));
return;
}
if (stream_->getTracks().size() == 0) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"The MediaRecorder cannot start because"
"there are no audio or video tracks "
"available.");
return;
}
state_ = State::kRecording;
if (stream_->getAudioTracks().size() == 0) {
audio_bits_per_second_ = 0;
if (overall_bits_per_second_.has_value()) {
video_bits_per_second_ = ClampVideoBitRate(
GetExecutionContext(), overall_bits_per_second_.value());
}
}
if (stream_->getVideoTracks().size() == 0) {
video_bits_per_second_ = 0;
if (overall_bits_per_second_.has_value()) {
audio_bits_per_second_ = ClampAudioBitRate(
GetExecutionContext(), overall_bits_per_second_.value());
}
}
const ContentType content_type(mime_type_);
if (!recorder_handler_->Start(time_slice, content_type.GetType(),
audio_bits_per_second_,
video_bits_per_second_)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"There was an error starting the MediaRecorder.");
}
}
void MediaRecorder::stop(ExceptionState& exception_state) {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return;
}
if (state_ == State::kInactive) {
return;
}
StopRecording(/*error_event=*/nullptr);
}
void MediaRecorder::pause(ExceptionState& exception_state) {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return;
}
if (state_ == State::kInactive) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The MediaRecorder's state is 'inactive'.");
return;
}
if (state_ == State::kPaused)
return;
state_ = State::kPaused;
recorder_handler_->Pause();
ScheduleDispatchEvent(Event::Create(event_type_names::kPause));
}
void MediaRecorder::resume(ExceptionState& exception_state) {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return;
}
if (state_ == State::kInactive) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The MediaRecorder's state is 'inactive'.");
return;
}
if (state_ == State::kRecording)
return;
state_ = State::kRecording;
recorder_handler_->Resume();
ScheduleDispatchEvent(Event::Create(event_type_names::kResume));
}
void MediaRecorder::requestData(ExceptionState& exception_state) {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed()) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Execution context is detached.");
return;
}
if (state_ == State::kInactive) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The MediaRecorder's state is 'inactive'.");
return;
}
if (recorder_handler_) {
recorder_handler_->MaybeFlush();
}
WriteData(/*data=*/{}, /*last_in_slice=*/true, /*error_event=*/nullptr);
}
bool MediaRecorder::isTypeSupported(ExecutionContext* context,
const String& type) {
MediaRecorderHandler* handler = MakeGarbageCollected<MediaRecorderHandler>(
context->GetTaskRunner(TaskType::kInternalMediaRealTime),
KeyFrameRequestProcessor::Configuration());
if (!handler)
return false;
// If true is returned from this method, it only indicates that the
// MediaRecorder implementation is capable of recording Blob objects for the
// specified MIME type. Recording may still fail if sufficient resources are
// not available to support the concrete media encoding.
// https://w3c.github.io/mediacapture-record/#dom-mediarecorder-istypesupported
ContentType content_type(type);
bool result = handler->CanSupportMimeType(
content_type.GetType(), content_type.Parameter("codecs"),
MediaRecorderHandler::CanSupportMimeTypeCaller::kIsTypeSupported);
return result;
}
const AtomicString& MediaRecorder::InterfaceName() const {
return event_target_names::kMediaRecorder;
}
ExecutionContext* MediaRecorder::GetExecutionContext() const {
return ExecutionContextLifecycleObserver::GetExecutionContext();
}
void MediaRecorder::ContextDestroyed() {
if (blob_data_) {
// Cache |blob_data_->length()| because of std::move in argument list.
const uint64_t blob_data_length = blob_data_->length();
CreateBlobEvent(MakeGarbageCollected<Blob>(
BlobDataHandle::Create(std::move(blob_data_), blob_data_length)));
}
state_ = State::kInactive;
stream_.Clear();
if (recorder_handler_) {
recorder_handler_->Stop();
recorder_handler_ = nullptr;
}
}
void MediaRecorder::WriteData(base::span<const uint8_t> data,
bool last_in_slice,
ErrorEvent* error_event) {
MaybeEmitStartEvent();
if (error_event) {
ScheduleDispatchEvent(error_event);
}
if (!blob_data_) {
blob_data_ = std::make_unique<BlobData>();
blob_data_->SetContentType(mime_type_);
}
if (!data.empty()) {
blob_data_->AppendBytes(data);
}
if (!last_in_slice)
return;
// Cache |blob_data_->length()| because of std::move in argument list.
const uint64_t blob_data_length = blob_data_->length();
CreateBlobEvent(MakeGarbageCollected<Blob>(
BlobDataHandle::Create(std::move(blob_data_), blob_data_length)));
}
void MediaRecorder::OnError(DOMExceptionCode code, const String& message) {
DVLOG(1) << __func__ << " message=" << message.Ascii();
ScriptState* script_state =
ToScriptStateForMainWorld(DomWindow()->GetFrame());
ScriptState::Scope scope(script_state);
ScriptValue error_value = ScriptValue::From(
script_state, MakeGarbageCollected<DOMException>(code, message));
ErrorEventInit* event_init = ErrorEventInit::Create();
event_init->setError(error_value);
StopRecording(
ErrorEvent::Create(script_state, event_type_names::kError, event_init));
}
void MediaRecorder::MaybeEmitStartEvent() {
if (emitted_start_event_) {
return;
}
mime_type_ = recorder_handler_->ActualMimeType();
ScheduleDispatchEvent(Event::Create(event_type_names::kStart));
emitted_start_event_ = true;
}
void MediaRecorder::OnAllTracksEnded() {
DVLOG(1) << __func__;
StopRecording(/*error_event=*/nullptr);
}
void MediaRecorder::OnStreamChanged(const String& message) {
DVLOG(1) << __func__ << " message=" << message.Ascii()
<< " state_=" << static_cast<int>(state_);
if (state_ != State::kInactive) {
OnError(DOMExceptionCode::kInvalidModificationError, message);
}
}
void MediaRecorder::CreateBlobEvent(Blob* blob) {
const base::TimeTicks now = base::TimeTicks::Now();
double timecode = 0;
if (!blob_event_first_chunk_timecode_.has_value()) {
blob_event_first_chunk_timecode_ = now;
} else {
timecode =
(now - blob_event_first_chunk_timecode_.value()).InMillisecondsF();
}
ScheduleDispatchEvent(MakeGarbageCollected<BlobEvent>(
event_type_names::kDataavailable, blob, timecode));
}
void MediaRecorder::StopRecording(ErrorEvent* error_event) {
if (state_ == State::kInactive) {
// This may happen if all tracks have ended and recording has stopped or
// never started.
return;
}
if (!recorder_handler_) {
// This may happen when ContextDestroyed has executed, but the
// MediaRecorderHandler still exists and all tracks
// have ended leading to a call to OnAllTracksEnded.
return;
}
// Make sure that starting the recorder again yields an onstart event.
state_ = State::kInactive;
recorder_handler_->Stop();
WriteData(/*data=*/{}, /*last_in_slice=*/true, error_event);
ScheduleDispatchEvent(Event::Create(event_type_names::kStop));
emitted_start_event_ = false;
}
void MediaRecorder::ScheduleDispatchEvent(Event* event) {
scheduled_events_.push_back(event);
// Only schedule a post if we are placing the first item in the queue.
if (scheduled_events_.size() == 1) {
if (auto* context = GetExecutionContext()) {
// MediaStream recording should use DOM manipulation task source.
// https://www.w3.org/TR/mediastream-recording/
context->GetTaskRunner(TaskType::kDOMManipulation)
->PostTask(FROM_HERE, BindOnce(&MediaRecorder::DispatchScheduledEvent,
WrapPersistent(this)));
}
}
}
void MediaRecorder::DispatchScheduledEvent() {
HeapVector<Member<Event>> events;
events.swap(scheduled_events_);
for (const auto& event : events)
DispatchEvent(*event);
}
void MediaRecorder::Trace(Visitor* visitor) const {
visitor->Trace(stream_);
visitor->Trace(recorder_handler_);
visitor->Trace(scheduled_events_);
EventTarget::Trace(visitor);
ExecutionContextLifecycleObserver::Trace(visitor);
}
void MediaRecorder::UpdateAudioBitrate(uint32_t bits_per_second) {
audio_bits_per_second_ = bits_per_second;
}
} // namespace blink