blob: 8d587c2df7ee134f7ba3dceab1bc007ad28d53de [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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/audio_output_devices/html_media_element_audio_output_device.h"
#include <memory>
#include <utility>
#include "base/callback_helpers.h"
#include "base/macros.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_set_sink_id_callbacks.h"
#include "third_party/blink/public/web/web_local_frame_client.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
namespace {
DOMException* ToException(WebSetSinkIdError error) {
switch (error) {
case WebSetSinkIdError::kNotFound:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotFoundError, "Requested device not found");
case WebSetSinkIdError::kNotAuthorized:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"No permission to use requested device");
case WebSetSinkIdError::kAborted:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError,
"The operation could not be performed and was aborted");
case WebSetSinkIdError::kNotSupported:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError, "Operation not supported");
default:
NOTREACHED();
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError,
"Invalid error code");
}
}
class SetSinkIdResolver : public ScriptPromiseResolver {
public:
static SetSinkIdResolver* Create(ScriptState*,
HTMLMediaElement&,
const String& sink_id);
SetSinkIdResolver(ScriptState*, HTMLMediaElement&, const String& sink_id);
~SetSinkIdResolver() override = default;
void StartAsync();
void Trace(Visitor*) const override;
private:
void DoSetSinkId();
void OnSetSinkIdComplete(absl::optional<WebSetSinkIdError> error);
Member<HTMLMediaElement> element_;
String sink_id_;
DISALLOW_COPY_AND_ASSIGN(SetSinkIdResolver);
};
SetSinkIdResolver* SetSinkIdResolver::Create(ScriptState* script_state,
HTMLMediaElement& element,
const String& sink_id) {
SetSinkIdResolver* resolver =
MakeGarbageCollected<SetSinkIdResolver>(script_state, element, sink_id);
resolver->KeepAliveWhilePending();
return resolver;
}
SetSinkIdResolver::SetSinkIdResolver(ScriptState* script_state,
HTMLMediaElement& element,
const String& sink_id)
: ScriptPromiseResolver(script_state),
element_(element),
sink_id_(sink_id) {}
void SetSinkIdResolver::StartAsync() {
ExecutionContext* context = GetExecutionContext();
if (!context)
return;
context->GetTaskRunner(TaskType::kInternalMedia)
->PostTask(FROM_HERE, WTF::Bind(&SetSinkIdResolver::DoSetSinkId,
WrapWeakPersistent(this)));
}
void SetSinkIdResolver::DoSetSinkId() {
auto set_sink_id_completion_callback =
WTF::Bind(&SetSinkIdResolver::OnSetSinkIdComplete, WrapPersistent(this));
WebMediaPlayer* web_media_player = element_->GetWebMediaPlayer();
if (web_media_player) {
if (web_media_player->SetSinkId(
sink_id_, std::move(set_sink_id_completion_callback))) {
element_->DidAudioOutputSinkChanged(sink_id_);
}
return;
}
ExecutionContext* context = GetExecutionContext();
if (!context) {
// Detached contexts shouldn't be playing audio. Note that despite this
// explicit Reject(), any associated JS callbacks will never be called
// because the context is already detached...
Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"Impossible to authorize device for detached context"));
return;
}
// This is associated with an HTML element, so the context must be a window.
if (WebLocalFrameImpl* web_frame = WebLocalFrameImpl::FromFrame(
To<LocalDOMWindow>(context)->GetFrame())) {
web_frame->Client()->CheckIfAudioSinkExistsAndIsAuthorized(
sink_id_, std::move(set_sink_id_completion_callback));
} else {
Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"Impossible to authorize device if there is no frame"));
return;
}
}
void SetSinkIdResolver::OnSetSinkIdComplete(
absl::optional<WebSetSinkIdError> error) {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed())
return;
if (error) {
Reject(ToException(*error));
return;
}
HTMLMediaElementAudioOutputDevice& aod_element =
HTMLMediaElementAudioOutputDevice::From(*element_);
aod_element.setSinkId(sink_id_);
Resolve();
}
void SetSinkIdResolver::Trace(Visitor* visitor) const {
visitor->Trace(element_);
ScriptPromiseResolver::Trace(visitor);
}
} // namespace
HTMLMediaElementAudioOutputDevice::HTMLMediaElementAudioOutputDevice(
HTMLMediaElement& element)
: AudioOutputDeviceController(element) {}
// static
HTMLMediaElementAudioOutputDevice& HTMLMediaElementAudioOutputDevice::From(
HTMLMediaElement& element) {
HTMLMediaElementAudioOutputDevice* self =
static_cast<HTMLMediaElementAudioOutputDevice*>(
AudioOutputDeviceController::From(element));
if (!self) {
self = MakeGarbageCollected<HTMLMediaElementAudioOutputDevice>(element);
AudioOutputDeviceController::ProvideTo(element, self);
}
return *self;
}
String HTMLMediaElementAudioOutputDevice::sinkId(HTMLMediaElement& element) {
HTMLMediaElementAudioOutputDevice& aod_element =
HTMLMediaElementAudioOutputDevice::From(element);
return aod_element.sink_id_;
}
void HTMLMediaElementAudioOutputDevice::setSinkId(const String& sink_id) {
sink_id_ = sink_id;
}
ScriptPromise HTMLMediaElementAudioOutputDevice::setSinkId(
ScriptState* script_state,
HTMLMediaElement& element,
const String& sink_id) {
SetSinkIdResolver* resolver =
SetSinkIdResolver::Create(script_state, element, sink_id);
ScriptPromise promise = resolver->Promise();
if (sink_id == HTMLMediaElementAudioOutputDevice::sinkId(element))
resolver->Resolve();
else
resolver->StartAsync();
return promise;
}
void HTMLMediaElementAudioOutputDevice::SetSinkId(const String& sink_id) {
// No need to call WebFrameClient::CheckIfAudioSinkExistsAndIsAuthorized as
// this call is not coming from content and should already be allowed.
HTMLMediaElement* html_media_element = GetSupplementable();
WebMediaPlayer* web_media_player = html_media_element->GetWebMediaPlayer();
if (!web_media_player)
return;
sink_id_ = sink_id;
if (web_media_player->SetSinkId(sink_id_, base::DoNothing()))
html_media_element->DidAudioOutputSinkChanged(sink_id_);
}
void HTMLMediaElementAudioOutputDevice::Trace(Visitor* visitor) const {
AudioOutputDeviceController::Trace(visitor);
}
} // namespace blink