This change to the ownership model for the delegate passed into `SetEventDelegate` requires us to separate the controller from the delegate, and have them share a weak relationship. Chromium changes: https://chromium.googlesource.com/chromium/src/+/50d43c7bcf04f5fac7cdc7ad26732f3d94b5bd43 commit 50d43c7bcf04f5fac7cdc7ad26732f3d94b5bd43 Author: Di Wu <diwux@google.com> Date: Sun Jul 27 20:29:49 2025 -0700 Refactor TtsUtterance to always own its event delegate This change refactors the ownership model for UtteranceEventDelegate to establish a single, clear model where TtsUtterance always takes ownership of its delegate via std::unique_ptr. This simplifies the code, addresses code review feedback, and fixes underlying memory management issues that caused test failures. Problem: The previous memory management for UtteranceEventDelegate was fragile. Some delegates used a "delete this" pattern, which is error-prone and led to memory leaks in browser tests. An initial refactoring moved to a std::unique_ptr model but introduced a NonOwnedUtteranceEventDelegate wrapper to handle cases where the delegate's lifetime was managed externally (e.g., TtsSpeakFunction, SettingsWithTtsPreviewHandler). Code reviewers pointed out that this wrapper complicated the ownership model and that a single, consistent ownership pattern would be preferable. Solution: This commit fully adopts the single-ownership model and removes the NonOwnedUtteranceEventDelegate wrapper. 1. TtsUtterance always owns its delegate: TtsUtterance::SetEventDelegate now exclusively takes a std::unique_ptr<UtteranceEventDelegate>, ensuring the delegate is automatically destroyed with the utterance. 2. Refactored externally-managed delegates: The two cases that previously required a non-owned wrapper have been refactored to use dedicated, owned delegate classes: 2.1 TtsSpeakFunction: Now uses a new, private TtsExtensionEventHandler class that implements UtteranceEventDelegate. This handler is owned by the TtsUtterance and holds the `extension_id` to dispatch events. This removes the need for a reference back to the `TtsSpeakFunction`, simplifying lifetime management. 2.2 SettingsWithTtsPreviewHandler: Now uses a new TtsPreviewEventDelegate class. This handler is owned by the TtsUtterance and holds a base::WeakPtr to the SettingsWithTtsPreviewHandler to safely make callbacks. 3. Simplified delegate cleanup: With the removal of the non-owned wrapper, the GetType() virtual method on UtteranceEventDelegate and the corresponding logic in TtsControllerImpl are no longer needed and have been removed. This refactoring makes the TTS delegate ownership model clear, consistent, and safe, resolving memory leaks and directly addressing code review feedback for a simpler design. Bug: b:281717553, b:251732518, 431531726, 432151731 Test: Run and symbolize the affected two tests with asan and lsan enabled. Then run with MiraclePtr check on the SelectToSpeakTest.FullscreenMagnifierFollowsTextBoundsWhenPrefOn test. Change-Id: Ia7524c0a859ee2f7c1f208412215c6e6b1b4e512 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6733896 Reviewed-by: Xiyuan Xia <xiyuan@chromium.org> Commit-Queue: Di Wu <diwux@google.com> Reviewed-by: Avi Drissman <avi@chromium.org> Reviewed-by: Mitsuru Oshima <oshima@chromium.org> Cr-Commit-Position: refs/heads/main@{#1492580}
341 lines
9.7 KiB
C++
341 lines
9.7 KiB
C++
// Copyright (c) 2023 The Brave Authors. All rights reserved.
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
#include "brave/components/speedreader/tts_player.h"
|
|
|
|
#include <algorithm>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
#include "base/check.h"
|
|
#include "base/strings/utf_string_conversions.h"
|
|
#include "content/public/browser/navigation_handle.h"
|
|
#include "content/public/browser/tts_controller.h"
|
|
#include "content/public/browser/tts_utterance.h"
|
|
#include "content/public/browser/web_contents.h"
|
|
|
|
namespace {
|
|
constexpr const char kParagraphsKey[] = "paragraphs";
|
|
}
|
|
|
|
namespace speedreader {
|
|
|
|
class TtsPlayer::Controller::Delegate : public content::UtteranceEventDelegate {
|
|
public:
|
|
explicit Delegate(base::WeakPtr<TtsPlayer::Controller> handler)
|
|
: handler_(handler) {}
|
|
~Delegate() override = default;
|
|
|
|
using PassKey = base::PassKey<Delegate>;
|
|
|
|
// content::UtteranceEventDelegate:
|
|
void OnTtsEvent(content::TtsUtterance* utterance,
|
|
content::TtsEventType event_type,
|
|
int char_index,
|
|
int length,
|
|
const std::string& error_message) override {
|
|
if (!handler_) {
|
|
return;
|
|
}
|
|
|
|
handler_->OnTtsEvent(PassKey(), utterance, event_type, char_index, length,
|
|
error_message);
|
|
}
|
|
|
|
private:
|
|
base::WeakPtr<TtsPlayer::Controller> handler_;
|
|
};
|
|
|
|
TtsPlayer::TtsPlayer() = default;
|
|
|
|
TtsPlayer::~TtsPlayer() = default;
|
|
|
|
// static
|
|
TtsPlayer* TtsPlayer::GetInstance() {
|
|
return base::Singleton<TtsPlayer>::get();
|
|
}
|
|
|
|
TtsPlayer::Controller& TtsPlayer::GetControllerFor(
|
|
content::WebContents* web_contents) {
|
|
controller_.SetRequestWebContents(web_contents);
|
|
return controller_;
|
|
}
|
|
|
|
void TtsPlayer::AddObserver(Observer* observer) {
|
|
observers_.AddObserver(observer);
|
|
}
|
|
|
|
void TtsPlayer::RemoveObserver(Observer* observer) {
|
|
observers_.RemoveObserver(observer);
|
|
}
|
|
|
|
double TtsPlayer::GetSpeed() const {
|
|
return controller_.current_speed_;
|
|
}
|
|
|
|
const std::string& TtsPlayer::GetVoice() const {
|
|
return controller_.current_voice_;
|
|
}
|
|
|
|
void TtsPlayer::SetSpeed(double speed) {
|
|
if (std::abs(speed - controller_.current_speed_) > 0.05) {
|
|
controller_.current_speed_ = speed;
|
|
if (controller_.IsPlaying()) {
|
|
controller_.Resume(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TtsPlayer::SetVoice(const std::string& voice) {
|
|
if (controller_.current_voice_ != voice) {
|
|
controller_.current_voice_ = voice;
|
|
if (controller_.IsPlaying()) {
|
|
controller_.Resume(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
TtsPlayer::Controller::Controller(TtsPlayer* owner) : owner_(owner) {}
|
|
TtsPlayer::Controller::~Controller() = default;
|
|
|
|
void TtsPlayer::Controller::SetRequestWebContents(
|
|
content::WebContents* web_contents) {
|
|
request_web_contents_ = web_contents;
|
|
}
|
|
|
|
bool TtsPlayer::Controller::IsPlaying() const {
|
|
auto* tts = content::TtsController::GetInstance();
|
|
return tts->IsSpeaking();
|
|
}
|
|
|
|
bool TtsPlayer::Controller::IsPlayingRequestedWebContents(
|
|
std::optional<int> paragraph_index) const {
|
|
if (paragraph_index.has_value() && paragraph_index != paragraph_index_) {
|
|
return false;
|
|
}
|
|
return playing_web_contents_ == request_web_contents_;
|
|
}
|
|
|
|
void TtsPlayer::Controller::Play(std::optional<int> paragraph_index) {
|
|
DCHECK(request_web_contents_);
|
|
if (IsPlayingRequestedWebContents()) {
|
|
Observe(playing_web_contents_);
|
|
if (paragraph_index.has_value() && paragraph_index != paragraph_index_) {
|
|
paragraph_index_ = paragraph_index.value();
|
|
reading_start_position_ = 0;
|
|
reading_position_ = 0;
|
|
}
|
|
Resume(true);
|
|
} else {
|
|
Stop();
|
|
TtsPlayer::GetInstance()->delegate_->RequestReadingContent(
|
|
request_web_contents_,
|
|
base::BindOnce(&Controller::OnContentReady, base::Unretained(this),
|
|
request_web_contents_, std::move(paragraph_index)));
|
|
}
|
|
}
|
|
|
|
void TtsPlayer::Controller::Pause() {
|
|
if (IsPlayingRequestedWebContents()) {
|
|
auto* tts = content::TtsController::GetInstance();
|
|
reading_start_position_ =
|
|
std::min(static_cast<int>(GetParagraphToRead().size()),
|
|
reading_start_position_ + reading_position_);
|
|
reading_position_ = 0;
|
|
tts->Stop();
|
|
} else {
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
void TtsPlayer::Controller::Resume() {
|
|
const bool start_new = !IsPlayingRequestedWebContents();
|
|
Resume(start_new);
|
|
}
|
|
|
|
void TtsPlayer::Controller::Stop() {
|
|
auto* tts = content::TtsController::GetInstance();
|
|
tts->Stop();
|
|
|
|
paragraph_index_ = -1;
|
|
reading_position_ = 0;
|
|
reading_start_position_ = 0;
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingProgress(playing_web_contents_, paragraph_index_, 0, 0);
|
|
}
|
|
playing_web_contents_ = nullptr;
|
|
|
|
Observe(nullptr);
|
|
}
|
|
|
|
void TtsPlayer::Controller::Forward() {
|
|
if (!HasNextParagraph()) {
|
|
return;
|
|
}
|
|
++paragraph_index_;
|
|
reading_start_position_ = 0;
|
|
reading_position_ = 0;
|
|
if (IsPlaying()) {
|
|
Resume(true);
|
|
} else {
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingProgress(request_web_contents_, paragraph_index_, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TtsPlayer::Controller::Rewind() {
|
|
if (paragraph_index_ > 0) {
|
|
--paragraph_index_;
|
|
}
|
|
reading_start_position_ = 0;
|
|
reading_position_ = 0;
|
|
if (IsPlaying()) {
|
|
Resume(true);
|
|
} else {
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingProgress(request_web_contents_, paragraph_index_, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TtsPlayer::Controller::Resume(bool recreate_utterance) {
|
|
auto* tts = content::TtsController::GetInstance();
|
|
if (!recreate_utterance) {
|
|
tts->Resume();
|
|
} else {
|
|
auto utterance = content::TtsUtterance::Create();
|
|
|
|
reading_start_position_ += reading_position_;
|
|
reading_position_ = 0;
|
|
|
|
utterance->SetText(base::UTF16ToUTF8(
|
|
GetParagraphToRead().substr(reading_start_position_)));
|
|
utterance->SetShouldClearQueue(true);
|
|
utterance->SetEventDelegate(
|
|
std::make_unique<TtsPlayer::Controller::Delegate>(
|
|
weak_factory_.GetWeakPtr()));
|
|
utterance->SetVoiceName(current_voice_);
|
|
const auto& params = utterance->GetContinuousParameters();
|
|
utterance->SetContinuousParameters(current_speed_, params.pitch,
|
|
params.volume);
|
|
tts->SpeakOrEnqueue(std::move(utterance));
|
|
}
|
|
}
|
|
|
|
bool TtsPlayer::Controller::HasNextParagraph() {
|
|
if (!reading_content_.is_dict()) {
|
|
return false;
|
|
}
|
|
const auto* content = reading_content_.GetDict().FindList(kParagraphsKey);
|
|
if (!content) {
|
|
return false;
|
|
}
|
|
return paragraph_index_ + 1 < static_cast<int>(content->size());
|
|
}
|
|
|
|
std::u16string TtsPlayer::Controller::GetParagraphToRead() {
|
|
if (!reading_content_.is_dict()) {
|
|
return {};
|
|
}
|
|
const auto* content = reading_content_.GetDict().FindList(kParagraphsKey);
|
|
if (!content) {
|
|
return {};
|
|
}
|
|
if (0 <= paragraph_index_ &&
|
|
paragraph_index_ < static_cast<int>(content->size())) {
|
|
return base::UTF8ToUTF16((*content)[paragraph_index_].GetString());
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void TtsPlayer::Controller::DidStartNavigation(
|
|
content::NavigationHandle* handle) {
|
|
if (!handle->IsInPrimaryMainFrame() || handle->IsSameDocument()) {
|
|
return;
|
|
}
|
|
Stop();
|
|
}
|
|
|
|
void TtsPlayer::Controller::WebContentsDestroyed() {
|
|
Stop();
|
|
}
|
|
|
|
void TtsPlayer::Controller::OnTtsEvent(base::PassKey<Delegate>,
|
|
content::TtsUtterance* utterance,
|
|
content::TtsEventType event_type,
|
|
int char_index,
|
|
int length,
|
|
const std::string& error_message) {
|
|
switch (event_type) {
|
|
case content::TtsEventType::TTS_EVENT_WORD:
|
|
reading_position_ = char_index;
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingProgress(playing_web_contents_, paragraph_index_,
|
|
reading_start_position_ + char_index, length);
|
|
}
|
|
break;
|
|
case content::TtsEventType::TTS_EVENT_ERROR:
|
|
case content::TtsEventType::TTS_EVENT_INTERRUPTED:
|
|
case content::TtsEventType::TTS_EVENT_CANCELLED:
|
|
case content::TtsEventType::TTS_EVENT_PAUSE:
|
|
if (!continue_next_paragraph_) {
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingStop(playing_web_contents_);
|
|
}
|
|
}
|
|
break;
|
|
case content::TtsEventType::TTS_EVENT_END:
|
|
reading_position_ = 0;
|
|
reading_start_position_ = 0;
|
|
|
|
if (HasNextParagraph()) {
|
|
++paragraph_index_;
|
|
continue_next_paragraph_ = true;
|
|
Resume(true);
|
|
} else {
|
|
paragraph_index_ = -1;
|
|
continue_next_paragraph_ = false;
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingProgress(playing_web_contents_, paragraph_index_,
|
|
char_index, length);
|
|
o.OnReadingStop(playing_web_contents_);
|
|
}
|
|
}
|
|
break;
|
|
case content::TtsEventType::TTS_EVENT_RESUME:
|
|
case content::TtsEventType::TTS_EVENT_START:
|
|
if (!continue_next_paragraph_) {
|
|
for (auto& o : owner_->observers_) {
|
|
o.OnReadingStart(playing_web_contents_);
|
|
}
|
|
}
|
|
continue_next_paragraph_ = false;
|
|
break;
|
|
case content::TTS_EVENT_SENTENCE:
|
|
case content::TTS_EVENT_MARKER:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TtsPlayer::Controller::OnContentReady(content::WebContents* web_contents,
|
|
std::optional<int> paragraph_index,
|
|
base::Value content) {
|
|
if (!content.is_dict() || web_contents != request_web_contents_) {
|
|
return;
|
|
}
|
|
playing_web_contents_ = web_contents;
|
|
|
|
Observe(playing_web_contents_);
|
|
|
|
paragraph_index_ = paragraph_index.value_or(0);
|
|
reading_content_ = std::move(content);
|
|
reading_position_ = 0;
|
|
reading_start_position_ = 0;
|
|
Resume(true);
|
|
}
|
|
|
|
} // namespace speedreader
|