回顾
【一】从0定制浏览器:引言-CSDN博客
上一期博客中我们改造了 Chromium 的 webui_examples
示例程序,并修复了一些bug,让它可以正常打开网页以及devtools。不过我们遗留了一个问题没有解决:即不支持HTML5播放器的问题:
修复不支持 HTML5 播放器的问题
为了确定这个问题是否是我们改造的示例程序的问题,先用编译的 Chromium 浏览器打开B站视频试一下:
因此这应该是我们编译的 Chromium 的问题。通过查询资料我找到了这么一篇:
参考 How to add HTML5 support to Chromium? - Stack Overflow
可以在 out/Default/args.gn
中添加下面的参数:
ffmpeg_branding = "Chrome"
proprietary_codecs = true
添加了这个参数后,Chromium 就可以正常打开B站的视频了:
不过添加了这个编译参数后用 Aloha 打开打开B站视频会白屏,说明渲染前端资源的 Renderer 进程可能崩溃了,这需要我们分析一下崩溃原因。
解决Aloha打开视频白屏的问题
使用 visual studio 2022 调试后发现是 renderer 进程触发了一个 DCHECK 导致了崩溃:
// src\third_party\blink\renderer\core\css\css_default_style_sheets.cc
void CSSDefaultStyleSheets::VerifyUniversalRuleCount()
检查触发CHECK 的值是多少,通过调试可以确定是 NULL:
看起来更成员变量 default_media_controls_style_
有关,我们检查一下它的来源:
// In class CSSDefaultStyleSheets
Member<RuleSet> default_media_controls_style_;
// In class RuleSet
base::span<const RuleData> UniversalRules() const { return universal_rules_; }
HeapVector<RuleData> universal_rules_;
总结一下崩溃场景:default_media_controls_style_.universal_rules_ == nullptr
。
观察变量名称是和多媒体的默认样式有关的 CHECK,让 AI 帮忙实现一个 POC (Proof of Concept) 程序可以稳定复现这个问题:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Trigger Media Controls Style Check</title>
</head>
<body>
<video controls>
<source src="file:///C:/Users/XXX/Videos/Captures/poc.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<script>
// 这个脚本部分可以用来模拟一些操作,可能会影响 media controls 的样式
document.addEventListener('DOMContentLoaded', function() {
var video = document.querySelector('video');
// 模拟播放和暂停操作,可能会触发样式的应用
video.play().catch(e => {});
video.pause();
});
</script>
</body>
</html>
通过这个 poc 调试后,可以发现会执行到下面的函数中,特别注意一下最后一句 AddChildRules
:
void RuleSet::AddRulesFromSheet(StyleSheetContents* sheet,
const MediaQueryEvaluator& medium,
CascadeLayer* cascade_layer) {
TRACE_EVENT0("blink", "RuleSet::addRulesFromSheet");
DCHECK(sheet);
for (const auto& pre_import_layer : sheet->PreImportLayerStatementRules()) {
for (const auto& name : pre_import_layer->GetNames()) {
GetOrAddSubLayer(cascade_layer, name);
}
}
const HeapVector<Member<StyleRuleImport>>& import_rules =
sheet->ImportRules();
for (unsigned i = 0; i < import_rules.size(); ++i) {
StyleRuleImport* import_rule = import_rules[i].Get();
if (!import_rule->IsSupported()) {
continue;
}
if (!MatchMediaForAddRules(medium, import_rule->MediaQueries())) {
continue;
}
CascadeLayer* import_layer = cascade_layer;
if (import_rule->IsLayered()) {
import_layer =
GetOrAddSubLayer(cascade_layer, import_rule->GetLayerName());
}
if (import_rule->GetStyleSheet()) {
AddRulesFromSheet(import_rule->GetStyleSheet(), medium, import_layer);
}
}
AddChildRules(/*parent_rule=*/nullptr, sheet->ChildRules(), medium,
kRuleHasNoSpecialState, nullptr /* container_query */,
cascade_layer, nullptr, /*within_mixin=*/false);
}
对比 Chromium 和 Aloha 执行到这个位置时的变量变化
因此问题就在 sheet 变量。而sheet 的来源是 上一层 CSSDefaultStyleSheets::AddRulesToDefaultStyleSheets
,再上一层CSSDefaultStyleSheets::EnsureDefaultStyleSheetsForElement
就找到了目标 media_controls_style_sheet_
if (!media_controls_style_sheet_ && HasMediaControlsStyleSheetLoader() &&
(IsA<HTMLVideoElement>(element) || IsA<HTMLAudioElement>(element))) {
// FIXME: We should assert that this sheet only contains rules for <video>
// and <audio>.
media_controls_style_sheet_ =
ParseUASheet(media_controls_style_sheet_loader_->GetUAStyleSheet());
AddRulesToDefaultStyleSheets(media_controls_style_sheet_,
NamespaceType::kMediaControls);
changed_default_style = true;
}
我们可以看一下 media_controls_style_sheet_loader_
的成员函数 GetUAStyleSheet()
,里面有一句 UncompressResourceAsString(IDR_UASTYLE_MEDIA_INTERSTITIALS_CSS);
:
还记得上一期中我们为Aloha添加资源的操作吗,很明显 IDR_UASTYLE_MEDIA_INTERSTITIALS_CSS
,应该是某个资源的编号。我们让代码执行到 UncompressResourceAsString
内部,看看资源的加载情况如何:
发现得到的是一个空字符串。相应的我们再到 chromium 上调试一下这个poc看看对应的代码是什么情况:
看样子这应该是一个资源丢失的问题,资源编号 (48771 或 0xbe83)。那么我们需要找到这个资源把它添加到 aloha 的编译依赖中。还记得上一期中我们在 src\tools\gritsettings\resource_ids.spec
中为 aloha 的资源分配id范围吗,我们可以在这里找到定义这个资源的文件名称和路径:
"aloha/resources/aloha_resources.grd": {
"messages": [12000],
},
"<(SHARED_INTERMEDIATE_DIR)/aloha/resources/browser/resources.grd": {
"META": {"sizes": {"includes": [10]}},
"includes": [12500],
}
这个是一个虚拟的 id,实际上这个文件会通过Chromium 的构建重新分配id范围,生成的文件是 out\Default\gen\tools\gritsettings\default_resource_ids
,在这里我们可以找到 48771 所在的范围:
然后我们便可以在 third_party/blink/renderer/modules/media_controls/resources/media_controls_resources.grd
中找到上面那个资源的定义:
找到这个资源的打包产物名称,然后参考 aloha 的打包方式把它打包进我们的程序中:
third_party\blink\renderer\modules\media_controls\BUILD.gn
third_party\blink\public\BUILD.gn
修复效果
其他注意事项
有多个都是 blink 资源丢失的问题,我们这里还是提前把它们都加上,避免再出现资源丢失的问题。
repack("pak") {
testonly = true
sources = [
"$root_gen_dir/aloha/resources/browser/aloha_browser_resources.pak",
"$root_gen_dir/chrome/webui_gallery_resources.pak",
"$root_gen_dir/content/browser/devtools/devtools_resources.pak",
# fix CSSDefaultStyleSheet loss default_media_control_style($root_gen_dir/third_party/blink/renderer/modules/media_controls/resources/media_controls_resources_100_percent.pak)
"$root_gen_dir/third_party/blink/public/resources/blink_scaled_resources_100_percent.pak",
# blink 资源提前补充:
"$root_gen_dir/third_party/blink/public/resources/inspector_overlay_resources.pak",
# fix blink_strings loss
"$root_gen_dir/third_party/blink/public/strings/blink_strings_af.pak",
# aloha 自定义资源
"$target_gen_dir/aloha_resources.pak",
# webui_examples 和 views_examples_with_content 重复的
# "$root_gen_dir/mojo/public/js/mojo_bindings_resources.pak",
# "$root_gen_dir/third_party/blink/public/resources/blink_resources.pak",
# "$root_gen_dir/ui/resources/webui_resources.pak",
# "$root_gen_dir/ui/strings/app_locale_settings_en-US.pak",
# "$root_gen_dir/ui/strings/ui_strings_en-US.pak",
# 这里应该在 PreSandboxStartup 中加载,而不是静态打包:
# ~把 ui/resources 的资源打包进来, 否则 52613 资源找不到会触发 ImageSkia 的CHECK~
# "$root_gen_dir/ui/resources/ui_resources_100_percent.pak",
]
deps = [
":resources",
"resources/browser:resources",
"//chrome/browser/resources/webui_gallery:resources",
"//content/browser/devtools:devtools_resources",
"//mojo/public/js:resources",
"//third_party/blink/public:devtools_inspector_resources",
"//third_party/blink/public:image_resources",
"//third_party/blink/public:resources",
"//third_party/blink/public:scaled_resources_100_percent",
"//third_party/blink/public/strings:strings",
"//ui/resources",
"//ui/strings",
]
output = "$root_out_dir/aloha.pak"
}
相信经过这段分析练手,再遇到类似的资源丢失问题你应该不会无从下手了吧。博主留了一个类似的bug可以供大家练习一下,复现的办法如下:
为 html tag 添加一个样式 cursor: grab;
,把鼠标移动到这个tag上就会触发崩溃。
PS:除了在 BUILD.gn 中静态加载,你还可以在 aloha\app\main_delegate.cc
的 MainDelegate::PreSandboxStartup
中动态地加载资源
void MainDelegate::PreSandboxStartup() {
base::FilePath ui_test_pak_path;
CHECK(base::PathService::Get(ui::UI_TEST_PAK, &ui_test_pak_path));
ui::ResourceBundle::InitSharedInstanceWithPakPath(ui_test_pak_path);
base::FilePath content_resources_pak_file;
CHECK(base::PathService::Get(base::DIR_ASSETS, &content_resources_pak_file));
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
content_resources_pak_file.AppendASCII("content_resources.pak"),
ui::k100Percent);
base::FilePath aloha_pak_file;
CHECK(base::PathService::Get(base::DIR_ASSETS, &aloha_pak_file));
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
aloha_pak_file.AppendASCII("aloha.pak"), ui::k100Percent);
base::FilePath ui_resource_percent100;
CHECK(base::PathService::Get(base::DIR_ASSETS, &aloha_pak_file));
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
ui_resource_percent100.AppendASCII("ui_resources_100_percent.pak"),
ui::k100Percent);
}
经过这一阵折腾我们的 ‘浏览器’ 终于是可以播放视频了:
添加 Native Views
前面我们以及基本完成了基于 webui_examples
的改造,虽然已经可以正常打开和浏览网页了,但是如果打开的网页太多了就会显得太乱。我们不妨像 Chromium 浏览器一样为它添加一个标签栏吧。views_examples_with_content
示例提供的标签栏就不错:
不过应该怎么把 views_examples_with_content
的 native ui 迁移到 aloha 上呢?
我们可以对比一下 views_examples_with_content
和 webui_examples
的入口:
它们的初始化过程有不少的差异。看来直接合并可能会比较困难,不如通过命令行参数把两段加载逻辑分割开来,后续再慢慢把所有的功能进行合并。另外为了方便我们后续的更改,views_examples_with_content
的一些基础组件也可以拷贝到我们的 aloha
目录下。主要是 ui\views_content_client
目录下的内容,它负责实现了 Native Views 中 web 的主体功能,这些模块你都可以在 aloha\browser
目录下找到对应。如何拷贝过来以及把编译目标链接过去,这里就不展开说了,如果需要帮助可以加入我们的交流群(在文末):
我们可以通过命令行分割加载逻辑,下面是博主的实现办法(仅供参考,因为代码迭代了多次,和原始版本可能有不同,需要读者自行适配):
// aloha/app/main.cc
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "aloha/app/main_delegate.h"
#include "aloha/browser/ui/native/widget_delegate_view.h"
#include "aloha/views_content_client/views_content_client.h"
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/process/process_handle.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "content/public/app/content_main.h"
#include "content/public/browser/browser_context.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "url/url_util.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#include "content/public/app/sandbox_helper_win.h"
#include "sandbox/win/src/sandbox_types.h"
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_MAC)
#include "base/files/file_path.h"
#include "sandbox/mac/seatbelt_exec.h"
#endif // BUILDFLAG(IS_MAC)
namespace {
// Called after: ViewsContentMainDelegate::PreSandboxStartup()
void OnResourcesLoaded() {
base::FilePath aloha_pak_file;
CHECK(base::PathService::Get(base::DIR_ASSETS, &aloha_pak_file));
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
aloha_pak_file.AppendASCII("aloha.pak"), ui::k100Percent);
base::FilePath ui_resource_percent100;
CHECK(base::PathService::Get(base::DIR_ASSETS, &aloha_pak_file));
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
ui_resource_percent100.AppendASCII("ui_resources_100_percent.pak"),
ui::k100Percent);
}
// View / Widget 是自动管理内存的,不需要手动释放,否则会触发 HEAP_CORRUPTION
void CreateAndShowMainWindow(aloha::ViewsContentClient* views_content_client,
content::BrowserContext* browser_context,
gfx::NativeWindow window_context) {
// 窗口已存在
if (aloha::NativeWidgetDelegateView::instance()) {
aloha::NativeWidgetDelegateView::instance()->GetWidget()->Activate();
return;
}
// 创建窗口
views::Widget* aloha_main_widget = new views::Widget();
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW);
aloha::SetDefaultBrowserContext(browser_context);
params.delegate = new aloha::NativeWidgetDelegateView();
params.delegate->RegisterWindowClosingCallback(
std::move(views_content_client->quit_closure()));
params.context = window_context;
params.name =
base::UTF16ToUTF8(l10n_util::GetStringUTF16(IDS_ALOHA_WEBSHELL_TITLE));
// 移除系统的默认样式,以添加我们自己的窗口样式
// params.remove_standard_frame = true;
aloha_main_widget->Init(std::move(params));
aloha_main_widget->Show();
// These lines serve no purpose other than to introduce an explicit content
// dependency. If the main executable doesn't have this dependency, the linker
// has more flexibility to reorder library dependencies in a shared component
// build. On linux, this can cause libc to appear before libcontent in the
// dlsym search path, which breaks (usually valid) assumptions made in
// sandbox::InitLibcUrandomOverrides(). See http://crbug.com/374712.
if (!browser_context) {
browser_context->SaveSessionState();
NOTREACHED();
}
}
} // namespace
#if BUILDFLAG(IS_WIN)
int wWinMain(HINSTANCE instance, HINSTANCE, wchar_t*, int) {
base::CommandLine::Init(0, nullptr);
sandbox::SandboxInterfaceInfo sandbox_info{};
content::InitializeSandboxInfo(&sandbox_info);
// 转为基于 View 构建界面
if (base::CommandLine::ForCurrentProcess()->HasSwitch("use-webui")) {
// 纯 web方式参考 ui\webui\examples\app\main.cc
aloha::MainDelegate delegate;
content::ContentMainParams params(&delegate);
params.instance = instance;
params.sandbox_info = &sandbox_info;
return content::ContentMain(std::move(params));
} else {
// 实现方式参考 ui\views\examples\examples_with_content_main.cc
aloha::ViewsContentClient aloha_views_content_client(instance,
&sandbox_info);
// 加载 aloha资源
aloha_views_content_client.set_on_resources_loaded_callback(
base::BindOnce(&OnResourcesLoaded));
// 设置预启动回调
aloha_views_content_client.set_on_pre_main_message_loop_run_callback(
base::BindOnce(&CreateAndShowMainWindow,
base::Unretained(&aloha_views_content_client)));
// 启动消息循环
return aloha_views_content_client.RunMain();
}
}
#elif BUILDFLAG(IS_MAC)
int main(int argc, const char** argv) {
base::CommandLine::Init(argc, argv);
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
sandbox::SeatbeltExecServer::CreateFromArgumentsResult seatbelt =
sandbox::SeatbeltExecServer::CreateFromArguments(
command_line->GetProgram().value().c_str(), argc,
const_cast<char**>(argv));
if (seatbelt.sandbox_required) {
CHECK(seatbelt.server->InitializeSandbox());
}
aloha::MainDelegate delegate;
content::ContentMainParams params(&delegate);
return content::ContentMain(std::move(params));
}
#else
int main(int argc, const char** argv) {
base::CommandLine::Init(argc, argv);
aloha::MainDelegate delegate;
content::ContentMainParams params(&delegate);
return content::ContentMain(std::move(params));
}
#endif // BUILDFLAG(IS_WIN)
我们为基于 webui_examples
修改的 aloha
逻辑添加了 use-webui
参数,但是浏览器是多进程架构的,而我们修改的只是主进程的逻辑,子进程生成的时候并不会自动添加 use-webui
参数。
通过调研发现可以在 content::ContentBrowserClient
的派生实现中实现下面的接口(aloha\browser\content_browser_client.h
)
// class ContentBrowserClient : public content::ContentBrowserClient
// 需要添加 --use-pure-webview
void AppendExtraCommandLineSwitches(base::CommandLine* command_line,
int child_process_id) override;
void ContentBrowserClient::AppendExtraCommandLineSwitches(base::CommandLine* command_line,
int child_process_id) {
// 检查主进程是否包含 --use-webui 参数
if (base::CommandLine::ForCurrentProcess()->HasSwitch("use-webui")) {
// 将参数传递给子进程
command_line->AppendSwitch("use-webui");
}
LOG(INFO) << "ChildProcessId:" << child_process_id;
}
定义主窗口界面
通过派生 views::WidgetDelegateView
我们可以定义主窗口的样式,大家可以参考 ui\views\examples\examples_window.cc
的方式实现,受限于文章篇幅,这里就不展开说明了。下面是博主实现的界面:
总结
本期我们修复了无法使用HTML5播放器播放视频的问题,并引入了Native UI。从这期开始我们对 Chromium 原始代码的改动会变得越来越多,如果完全靠图文解说,受限于文章的篇幅,很多的细节可能没有办法完全解释清楚。因此下期我们将聊聊怎么开发一个脚本工具,把我们的更改单独提取出来,然后我会把这些更改推送到 Github 仓库中,方便大家交流学习。最后欢迎大家加入我们的群聊交流学习: