回顾
【一】从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 仓库中,方便大家交流学习。最后欢迎大家加入我们的群聊交流学习: