blob: e05013e64eb65da5405eef85639e58f123b510b3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/live_caption/live_caption_controller.h"
#include <memory>
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "build/build_config.h"
#include "components/live_caption/caption_bubble_context.h"
#include "components/live_caption/caption_bubble_controller.h"
#include "components/live_caption/caption_util.h"
#include "components/live_caption/pref_names.h"
#include "components/live_caption/views/caption_bubble.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/soda/constants.h"
#include "components/soda/soda_installer.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "media/base/media_switches.h"
#include "ui/native_theme/native_theme.h"
namespace {
const char* const kCaptionStylePrefsToObserve[] = {
prefs::kAccessibilityCaptionsTextSize,
prefs::kAccessibilityCaptionsTextFont,
prefs::kAccessibilityCaptionsTextColor,
prefs::kAccessibilityCaptionsTextOpacity,
prefs::kAccessibilityCaptionsBackgroundColor,
prefs::kAccessibilityCaptionsTextShadow,
prefs::kAccessibilityCaptionsBackgroundOpacity};
} // namespace
namespace captions {
LiveCaptionController::LiveCaptionController(
PrefService* profile_prefs,
PrefService* global_prefs,
const std::string& application_locale,
content::BrowserContext* browser_context)
: profile_prefs_(profile_prefs),
global_prefs_(global_prefs),
browser_context_(browser_context),
application_locale_(application_locale) {
base::UmaHistogramBoolean("Accessibility.LiveCaption.FeatureEnabled2",
IsLiveCaptionFeatureSupported());
// Hidden behind a feature flag.
if (!IsLiveCaptionFeatureSupported()) {
return;
}
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(profile_prefs_);
auto* command_line = base::CommandLine::ForCurrentProcess();
if (command_line &&
command_line->HasSwitch(switches::kEnableLiveCaptionPrefForTesting)) {
profile_prefs_->SetBoolean(prefs::kLiveCaptionEnabled, true);
}
pref_change_registrar_->Add(
prefs::kLiveCaptionEnabled,
base::BindRepeating(&LiveCaptionController::OnLiveCaptionEnabledChanged,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kLiveCaptionLanguageCode,
base::BindRepeating(&LiveCaptionController::OnLiveCaptionLanguageChanged,
base::Unretained(this)));
enabled_ = IsLiveCaptionEnabled();
base::UmaHistogramBoolean("Accessibility.LiveCaption2", enabled_);
if (enabled_) {
StartLiveCaption();
}
}
LiveCaptionController::~LiveCaptionController() {
if (enabled_) {
enabled_ = false;
StopLiveCaption();
}
}
// static
void LiveCaptionController::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(
prefs::kLiveCaptionBubbleExpanded, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
prefs::kLiveCaptionBubblePinned, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
prefs::kLiveCaptionEnabled, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
prefs::kLiveCaptionMaskOffensiveWords, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
// Initially default the language to en-US.
registry->RegisterStringPref(prefs::kLiveCaptionLanguageCode,
speech::kUsEnglishLocale,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterListPref(
prefs::kLiveCaptionMediaFoundationRendererErrorSilenced,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Flags for User Microphone Captioning are only available on ash.
registry->RegisterBooleanPref(prefs::kLiveCaptionUserMicrophoneEnabled,
false);
registry->RegisterStringPref(prefs::kUserMicrophoneCaptionLanguageCode,
speech::kUsEnglishLocale);
#endif
}
void LiveCaptionController::OnLiveCaptionEnabledChanged() {
bool enabled = IsLiveCaptionEnabled();
if (enabled == enabled_) {
return;
}
enabled_ = enabled;
if (enabled) {
StartLiveCaption();
} else {
StopLiveCaption();
speech::SodaInstaller::GetInstance()->SetUninstallTimer(profile_prefs_,
global_prefs_);
}
}
void LiveCaptionController::OnLiveCaptionLanguageChanged() {
if (enabled_) {
const auto language_code =
prefs::GetLiveCaptionLanguageCode(profile_prefs_);
auto* soda_installer = speech::SodaInstaller::GetInstance();
// Only trigger an install when the language is not already installed.
if (!soda_installer->IsSodaInstalled(
speech::GetLanguageCode(language_code))) {
soda_installer->InstallLanguage(language_code, global_prefs_);
}
}
}
bool LiveCaptionController::IsLiveCaptionEnabled() {
return profile_prefs_->GetBoolean(prefs::kLiveCaptionEnabled);
}
void LiveCaptionController::StartLiveCaption() {
DCHECK(enabled_);
// The SodaInstaller determines whether SODA is already on the device and
// whether or not to download. Once SODA is on the device and ready, the
// SODAInstaller calls OnSodaInstalled on its observers. The UI is created at
// that time.
if (speech::SodaInstaller::GetInstance()->IsSodaInstalled(
speech::GetLanguageCode(
prefs::GetLiveCaptionLanguageCode(profile_prefs_)))) {
CreateUI();
} else {
speech::SodaInstaller::GetInstance()->AddObserver(this);
speech::SodaInstaller::GetInstance()->Init(profile_prefs_, global_prefs_);
}
}
void LiveCaptionController::StopLiveCaption() {
DCHECK(!enabled_);
speech::SodaInstaller::GetInstance()->RemoveObserver(this);
DestroyUI();
}
void LiveCaptionController::OnSodaInstalled(
speech::LanguageCode language_code) {
if (!prefs::IsLanguageCodeForLiveCaption(language_code, profile_prefs_)) {
return;
}
// Live Caption should always be enabled when this is called. If Live Caption
// has been disabled, then this should not be observing the SodaInstaller
// anymore.
DCHECK(enabled_);
speech::SodaInstaller::GetInstance()->RemoveObserver(this);
CreateUI();
}
void LiveCaptionController::OnSodaInstallError(
speech::LanguageCode language_code,
speech::SodaInstaller::ErrorCode error_code) {
// Check that language code matches the selected language for Live Caption or
// is LanguageCode::kNone (signifying the SODA binary failed).
if (!prefs::IsLanguageCodeForLiveCaption(language_code, profile_prefs_) &&
language_code != speech::LanguageCode::kNone) {
return;
}
if (!base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage)) {
profile_prefs_->SetBoolean(prefs::kLiveCaptionEnabled, false);
}
}
void LiveCaptionController::CreateUI() {
if (is_ui_constructed_) {
return;
}
is_ui_constructed_ = true;
caption_bubble_controller_ =
CaptionBubbleController::Create(profile_prefs_, application_locale_);
caption_bubble_controller_->UpdateCaptionStyle(caption_style_);
// Observe native theme changes for caption style updates.
ui::NativeTheme::GetInstanceForWeb()->AddObserver(this);
// Observe caption style prefs.
for (const char* const pref_name : kCaptionStylePrefsToObserve) {
DCHECK(!pref_change_registrar_->IsObserved(pref_name));
pref_change_registrar_->Add(
pref_name,
base::BindRepeating(&LiveCaptionController::OnCaptionStyleUpdated,
base::Unretained(this)));
}
OnCaptionStyleUpdated();
}
void LiveCaptionController::DestroyUI() {
if (!is_ui_constructed_) {
return;
}
is_ui_constructed_ = false;
caption_bubble_controller_.reset(nullptr);
// Remove native theme observer.
ui::NativeTheme::GetInstanceForWeb()->RemoveObserver(this);
// Remove prefs to observe.
for (const char* const pref_name : kCaptionStylePrefsToObserve) {
DCHECK(pref_change_registrar_->IsObserved(pref_name));
pref_change_registrar_->Remove(pref_name);
}
}
bool LiveCaptionController::DispatchTranscription(
CaptionBubbleContext* caption_bubble_context,
const media::SpeechRecognitionResult& result) {
if (!caption_bubble_controller_) {
return false;
}
return caption_bubble_controller_->OnTranscription(caption_bubble_context,
result);
}
void LiveCaptionController::OnError(
CaptionBubbleContext* caption_bubble_context,
CaptionBubbleErrorType error_type,
OnErrorClickedCallback error_clicked_callback,
OnDoNotShowAgainClickedCallback error_silenced_callback) {
if (!caption_bubble_controller_) {
CreateUI();
}
caption_bubble_controller_->OnError(caption_bubble_context, error_type,
std::move(error_clicked_callback),
std::move(error_silenced_callback));
}
void LiveCaptionController::OnAudioStreamEnd(
CaptionBubbleContext* caption_bubble_context) {
if (!caption_bubble_controller_) {
return;
}
caption_bubble_controller_->OnAudioStreamEnd(caption_bubble_context);
}
void LiveCaptionController::OnLanguageIdentificationEvent(
CaptionBubbleContext* caption_bubble_context,
const media::mojom::LanguageIdentificationEventPtr& event) {
// TODO(crbug.com/40167928): Implement the UI for language identification.
if (caption_bubble_controller_) {
return caption_bubble_controller_->OnLanguageIdentificationEvent(
caption_bubble_context, event);
}
}
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
void LiveCaptionController::OnToggleFullscreen(
CaptionBubbleContext* caption_bubble_context) {
if (!enabled_) {
return;
}
// The easiest way to move the Live Caption UI to the right workspace is to
// simply destroy and recreate the UI. The UI will automatically be created
// in the workspace of the browser window that is transmitting captions.
DestroyUI();
CreateUI();
}
#endif
void LiveCaptionController::OnCaptionStyleUpdated() {
// Metrics are recorded when passing the caption prefs to the browser, so do
// not duplicate them here.
caption_style_ = GetCaptionStyleFromUserSettings(profile_prefs_,
false /* record_metrics */);
caption_bubble_controller_->UpdateCaptionStyle(caption_style_);
}
} // namespace captions