Import Cobalt 23.lts.6.1031897
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 860c540..1155938 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -149,14 +149,6 @@
         stages: [push]
         always_run: true
         pass_filenames: false
-    -   id: run-py2-tests
-        name: Run Python 2 Tests
-        description: Run Python 2 unittests
-        entry: python precommit_hooks/run_python2_unittests.py
-        language: python
-        language_version: python2.7
-        additional_dependencies: ['mock']
-        types: [python]
     -   id: osslint
         name: osslint
         entry: python precommit_hooks/osslint_wrapper.py
diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn
index 07a6de0..7dccc07 100644
--- a/cobalt/browser/BUILD.gn
+++ b/cobalt/browser/BUILD.gn
@@ -158,6 +158,7 @@
     "//cobalt/browser/memory_settings:browser_memory_settings",
     "//cobalt/browser/memory_tracker:memory_tracker_tool",
     "//cobalt/build:cobalt_build_id",
+    "//cobalt/build:cobalt_build_info",
     "//cobalt/cache",
     "//cobalt/configuration",
     "//cobalt/css_parser",
diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc
index fe4c098..7e541f0 100644
--- a/cobalt/browser/application.cc
+++ b/cobalt/browser/application.cc
@@ -65,6 +65,7 @@
 #include "cobalt/configuration/configuration.h"
 #include "cobalt/extension/crash_handler.h"
 #include "cobalt/extension/installation_manager.h"
+#include "cobalt/h5vcc/h5vcc_crash_log.h"
 #include "cobalt/loader/image/image_decoder.h"
 #include "cobalt/math/size.h"
 #include "cobalt/script/javascript_engine.h"
@@ -589,6 +590,26 @@
   }
 }
 
+void AddCrashLogApplicationState(base::ApplicationState state) {
+  std::string application_state = std::string(GetApplicationStateString(state));
+  application_state.push_back('\0');
+
+  auto crash_handler_extension =
+      static_cast<const CobaltExtensionCrashHandlerApi*>(
+          SbSystemGetExtension(kCobaltExtensionCrashHandlerName));
+  if (crash_handler_extension && crash_handler_extension->version >= 2) {
+    if (!crash_handler_extension->SetString("application_state",
+                                            application_state.c_str())) {
+      LOG(ERROR) << "Could not send application state to crash handler.";
+    }
+    return;
+  }
+
+  // Crash handler is not supported, fallback to crash log dictionary.
+  h5vcc::CrashLogDictionary::GetInstance()->SetString("application_state",
+                                                      application_state);
+}
+
 }  // namespace
 
 // Helper stub to disable histogram tracking in StatisticsRecorder
@@ -1198,6 +1219,7 @@
     case kSbEventTypeStop:
       LOG(INFO) << "Got quit event.";
       if (watchdog) watchdog->UpdateState(base::kApplicationStateStopped);
+      AddCrashLogApplicationState(base::kApplicationStateStopped);
       Quit();
       LOG(INFO) << "Finished quitting.";
       break;
@@ -1316,6 +1338,7 @@
       return;
   }
   if (watchdog) watchdog->UpdateState(browser_module_->GetApplicationState());
+  AddCrashLogApplicationState(browser_module_->GetApplicationState());
 }
 
 void Application::OnWindowSizeChangedEvent(const base::Event* event) {
diff --git a/cobalt/browser/web_module.cc b/cobalt/browser/web_module.cc
index 3531cbf..f1158dd 100644
--- a/cobalt/browser/web_module.cc
+++ b/cobalt/browser/web_module.cc
@@ -141,7 +141,21 @@
   ~Impl();
 
 #if defined(ENABLE_DEBUGGER)
+  void EnsureDebugModule(
+      debug::backend::DebuggerState* debugger_state = nullptr) {
+    if (!debug_overlay_) {
+      debug_overlay_.reset(
+          new debug::backend::RenderOverlay(render_tree_produced_callback_));
+    }
+    if (!debug_module_) {
+      debug_module_.reset(new debug::backend::DebugModule(
+          &debugger_hooks_, web_context_->global_environment(),
+          debug_overlay_.get(), resource_provider_, window_, debugger_state));
+    }
+  }
+
   debug::backend::DebugDispatcher* debug_dispatcher() {
+    EnsureDebugModule();
     DCHECK(debug_module_);
     return debug_module_->debug_dispatcher();
   }
@@ -229,7 +243,8 @@
 
   void FreezeDebugger(
       std::unique_ptr<debug::backend::DebuggerState>* debugger_state) {
-    if (debugger_state) *debugger_state = debug_module_->Freeze();
+    if (debugger_state && debug_module_)
+      *debugger_state = debug_module_->Freeze();
   }
 #endif  // defined(ENABLE_DEBUGGER)
 
@@ -549,6 +564,7 @@
       data.options.loader_thread_priority));
 
   animated_image_tracker_.reset(new loader::image::AnimatedImageTracker(
+      web_context_->name().c_str(),
       data.options.animated_image_decode_thread_priority));
 
   DCHECK_LE(0, data.options.image_cache_capacity);
@@ -744,14 +760,9 @@
   }
 
 #if defined(ENABLE_DEBUGGER)
-  debug_overlay_.reset(
-      new debug::backend::RenderOverlay(render_tree_produced_callback_));
-
-  debug_module_.reset(new debug::backend::DebugModule(
-      &debugger_hooks_, web_context_->global_environment(),
-      debug_overlay_.get(), resource_provider_, window_,
-      data.options.debugger_state));
-#endif  // ENABLE_DEBUGGER
+  if (data.options.debugger_state)
+    EnsureDebugModule(data.options.debugger_state);
+#endif
 
   report_unload_timing_info_callback_ =
       data.options.collect_unload_event_time_callback;
@@ -982,7 +993,11 @@
                  last_render_tree_produced_time_));
 
 #if defined(ENABLE_DEBUGGER)
-  debug_overlay_->OnRenderTreeProduced(layout_results_with_callback);
+  if (debug_overlay_) {
+    debug_overlay_->OnRenderTreeProduced(layout_results_with_callback);
+  } else {
+    render_tree_produced_callback_.Run(layout_results_with_callback);
+  }
 #else   // ENABLE_DEBUGGER
   render_tree_produced_callback_.Run(layout_results_with_callback);
 #endif  // ENABLE_DEBUGGER
@@ -1003,6 +1018,7 @@
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   web_module_stat_tracker_->OnRenderTreeRasterized(produced_time,
                                                    rasterized_time);
+  animated_image_tracker_->OnRenderTreeRasterized();
   if (produced_time >= last_render_tree_produced_time_) {
     is_render_tree_rasterization_pending_ = false;
   }
@@ -1075,7 +1091,7 @@
                   "\n Waiting for web debugger to connect "
                   "\n-------------------------------------";
   // This blocks until the web debugger connects.
-  debug_module_->debug_dispatcher()->SetPaused(true);
+  debug_dispatcher()->SetPaused(true);
   waiting_for_web_debugger_->store(false);
 }
 #endif  // defined(ENABLE_DEBUGGER)
@@ -1166,7 +1182,9 @@
 
 #if defined(ENABLE_DEBUGGER)
   // The debug overlay may be holding onto a render tree, clear that out.
-  debug_overlay_->ClearInput();
+  if (debug_overlay_) {
+    debug_overlay_->ClearInput();
+  }
 #endif
 
   // Force garbage collection in |javascript_engine|.
diff --git a/cobalt/build/BUILD.gn b/cobalt/build/BUILD.gn
index 6c6e1e8..7b6a002 100644
--- a/cobalt/build/BUILD.gn
+++ b/cobalt/build/BUILD.gn
@@ -23,3 +23,9 @@
     cobalt_version,
   ]
 }
+
+action("cobalt_build_info") {
+  script = "build_info.py"
+  outputs = [ "$root_gen_dir/build_info.json" ]
+  args = [ rebase_path(outputs[0], root_build_dir) ]
+}
diff --git a/cobalt/build/build.id b/cobalt/build/build.id
index 3d15dd7..20a1fde 100644
--- a/cobalt/build/build.id
+++ b/cobalt/build/build.id
@@ -1 +1 @@
-1031871
\ No newline at end of file
+1031897
\ No newline at end of file
diff --git a/cobalt/build/build_info.py b/cobalt/build/build_info.py
new file mode 100755
index 0000000..52f4494
--- /dev/null
+++ b/cobalt/build/build_info.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Generates a Cobalt Build Info json."""
+
+import datetime
+import json
+import os
+import re
+import subprocess
+import sys
+
+FILE_DIR = os.path.dirname(__file__)
+COMMIT_COUNT_BUILD_ID_OFFSET = 1000000
+
+_BUILD_ID_PATTERN = '^BUILD_NUMBER=([1-9][0-9]{6,})$'
+_GIT_REV_PATTERN = '^GitOrigin-RevId: ([0-9a-f]{40})$'
+_COBALT_VERSION_PATTERN = '^#define COBALT_VERSION "(.*)"$'
+
+
+def get_build_id_and_git_rev_from_commits(cwd):
+  # Build id and git rev must come from the same commit.
+  output = subprocess.check_output(
+      ['git', 'log', '--grep', _BUILD_ID_PATTERN, '-1', '-E', '--pretty=%b'],
+      cwd=cwd).decode()
+
+  # Gets build id.
+  compiled_build_id_pattern = re.compile(_BUILD_ID_PATTERN, flags=re.MULTILINE)
+  match_build_id = compiled_build_id_pattern.search(output)
+  if not match_build_id:
+    return None, None
+  build_id = match_build_id.group(1)
+
+  # Gets git rev.
+  compiled_git_rev_pattern = re.compile(_GIT_REV_PATTERN, flags=re.MULTILINE)
+  match_git_rev = compiled_git_rev_pattern.search(output)
+  if not match_git_rev:
+    git_rev = ''
+  else:
+    git_rev = match_git_rev.group(1)
+
+  return build_id, git_rev
+
+
+def get_build_id_from_commit_count(cwd):
+  output = subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'],
+                                   cwd=cwd)
+  build_id = int(output.strip().decode()) + COMMIT_COUNT_BUILD_ID_OFFSET
+  return str(build_id)
+
+
+def _get_last_commit_with_format(placeholder, cwd):
+  output = subprocess.check_output(
+      ['git', 'log', '-1', f'--pretty=format:{placeholder}'], cwd=cwd)
+  return output.strip().decode()
+
+
+def _get_hash_from_last_commit(cwd):
+  return _get_last_commit_with_format(r'%H', cwd=cwd)
+
+
+def _get_author_from_last_commit(cwd):
+  return _get_last_commit_with_format(r'%an', cwd=cwd)
+
+
+def _get_subject_from_last_commit(cwd):
+  return _get_last_commit_with_format(r'%s', cwd=cwd)
+
+
+def _get_cobalt_version():
+  version_header_path = os.path.join(os.path.dirname(FILE_DIR), 'version.h')
+  contents = ''
+  with open(version_header_path, 'r', encoding='utf-8') as f:
+    contents = f.read()
+  compiled_cobalt_version_pattern = re.compile(
+      _COBALT_VERSION_PATTERN, flags=re.MULTILINE)
+  return compiled_cobalt_version_pattern.search(contents).group(1)
+
+
+def main(output_path, cwd=FILE_DIR):
+  """Writes a Cobalt build_info json file."""
+  build_rev = _get_hash_from_last_commit(cwd=cwd)
+  build_id, git_rev = get_build_id_and_git_rev_from_commits(cwd=cwd)
+  if build_id is None:
+    build_id = get_build_id_from_commit_count(cwd=cwd)
+    git_rev = build_rev
+  cobalt_version = _get_cobalt_version()
+  build_time = datetime.datetime.now().ctime()
+  author = _get_author_from_last_commit(cwd=cwd)
+  commit = _get_subject_from_last_commit(cwd=cwd)
+
+  info_json = {
+      'build_id': build_id,
+      'build_rev': build_rev,
+      'git_rev': git_rev,
+      'cobalt_version': cobalt_version,
+      'build_time': build_time,
+      'author': author,
+      'commit': commit
+  }
+
+  with open(output_path, 'w', encoding='utf-8') as f:
+    f.write(json.dumps(info_json, indent=4))
+
+  # Supports legacy build_info.txt.
+  output_text_path = os.path.join(
+      os.path.dirname(output_path), 'build_info.txt')
+  with open(output_text_path, 'w', encoding='utf-8') as f:
+    f.write(f'''\
+Build ID: {build_id}
+Build Rev: {build_rev}
+Git Rev: {git_rev}
+Cobalt Version: {cobalt_version}
+Build Time: {build_time}
+Author: {author}
+Commit: {commit}
+''')
+
+
+if __name__ == '__main__':
+  main(sys.argv[1])
diff --git a/cobalt/build/get_build_id.py b/cobalt/build/get_build_id.py
index 9c19e4c..ba86bb3 100755
--- a/cobalt/build/get_build_id.py
+++ b/cobalt/build/get_build_id.py
@@ -15,35 +15,10 @@
 """Prints out the Cobalt Build ID."""
 
 import os
-import re
-import subprocess
 
+from cobalt.build import build_info
 from cobalt.build.build_number import GetOrGenerateNewBuildNumber
 
-_FILE_DIR = os.path.dirname(__file__)
-COMMIT_COUNT_BUILD_NUMBER_OFFSET = 1000000
-
-# Matches numbers > 1000000. The pattern is basic so git log --grep is able to
-# interpret it.
-GIT_BUILD_NUMBER_PATTERN = r'[1-9]' + r'[0-9]' * 6 + r'[0-9]*'
-BUILD_NUMBER_TAG_PATTERN = r'^BUILD_NUMBER={}$'
-
-# git log --grep can't handle capture groups.
-BUILD_NUBER_PATTERN_WITH_CAPTURE = f'({GIT_BUILD_NUMBER_PATTERN})'
-
-
-def get_build_number_from_commits():
-  full_pattern = BUILD_NUMBER_TAG_PATTERN.format(GIT_BUILD_NUMBER_PATTERN)
-  output = subprocess.check_output(
-      ['git', 'log', '--grep', full_pattern, '-1', '--pretty=%b'],
-      cwd=_FILE_DIR).decode()
-
-  full_pattern_with_capture = re.compile(
-      BUILD_NUMBER_TAG_PATTERN.format(BUILD_NUBER_PATTERN_WITH_CAPTURE),
-      flags=re.MULTILINE)
-  match = full_pattern_with_capture.search(output)
-  return match.group(1) if match else None
-
 
 def get_build_number_from_server():
   # Note $BUILD_ID_SERVER_URL will always be set in CI.
@@ -57,24 +32,14 @@
   return build_num
 
 
-def get_build_number_from_commit_count():
-  output = subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'],
-                                   cwd=_FILE_DIR)
-  build_number = int(output.strip().decode('utf-8'))
-  return build_number + COMMIT_COUNT_BUILD_NUMBER_OFFSET
-
-
-def main():
-  build_number = get_build_number_from_commits()
-
+def main(cwd=build_info.FILE_DIR):
+  build_number, _ = build_info.get_build_id_and_git_rev_from_commits(cwd=cwd)
   if not build_number:
     build_number = get_build_number_from_server()
-
   if not build_number:
-    build_number = get_build_number_from_commit_count()
-
-  print(build_number)
+    build_number = build_info.get_build_id_from_commit_count(cwd=cwd)
+  return build_number
 
 
 if __name__ == '__main__':
-  main()
+  print(main())
diff --git a/cobalt/build/get_build_id_test.py b/cobalt/build/get_build_id_test.py
new file mode 100644
index 0000000..26e7b2b
--- /dev/null
+++ b/cobalt/build/get_build_id_test.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Tests the get_build_id module."""
+
+import os
+import subprocess
+import tempfile
+import unittest
+
+from cobalt.build import build_info
+from cobalt.build import get_build_id
+
+_TEST_BUILD_NUMBER = 1234 + build_info.COMMIT_COUNT_BUILD_ID_OFFSET
+
+
+# TODO(b/282040638): fix and re-enabled this
+@unittest.skipIf(os.name == 'nt', 'Broken on Windows')
+class GetBuildIdTest(unittest.TestCase):
+
+  def setUp(self):
+    self.test_dir = tempfile.TemporaryDirectory()  # pylint: disable=consider-using-with
+    self.original_cwd = os.getcwd()
+    os.chdir(self.test_dir.name)
+    subprocess.check_call(['git', 'init'])
+    subprocess.check_call(['git', 'config', 'user.name', 'pytest'])
+    subprocess.check_call(['git', 'config', 'user.email', 'pytest@pytest.com'])
+
+  def tearDown(self):
+    os.chdir(self.original_cwd)
+    self.test_dir.cleanup()
+
+  def make_commit(self, message='Temporary commit'):
+    with tempfile.NamedTemporaryFile('w', dir=self.test_dir.name) as temp_file:
+      subprocess.check_call(['git', 'add', temp_file.name])
+    subprocess.check_call(['git', 'commit', '-m', message])
+
+  def make_commit_with_build_number(self, build_number=_TEST_BUILD_NUMBER):
+    message = f'Subject line\n\nBUILD_NUMBER={build_number}'
+    self.make_commit(message)
+
+  def testSanity(self):
+    self.make_commit()
+    head_rev = subprocess.check_output(['git', 'rev-parse', 'HEAD'])
+    self.assertNotEqual(head_rev.strip().decode('utf-8'), '')
+
+  def testGetBuildNumberFromCommitsSunnyDay(self):
+    self.make_commit_with_build_number()
+    build_number, _ = build_info.get_build_id_and_git_rev_from_commits(
+        cwd=self.test_dir.name)
+    self.assertEqual(int(build_number), _TEST_BUILD_NUMBER)
+
+  def testGetBuildNumberFromCommitsSunnyDayGetMostRecent(self):
+    num_commits = 5
+    for i in range(num_commits):
+      self.make_commit_with_build_number(
+          build_info.COMMIT_COUNT_BUILD_ID_OFFSET + i)
+    build_number, _ = build_info.get_build_id_and_git_rev_from_commits(
+        cwd=self.test_dir.name)
+    self.assertEqual(
+        int(build_number),
+        num_commits + build_info.COMMIT_COUNT_BUILD_ID_OFFSET - 1)
+
+  def testGetBuildNumberFromCommitsRainyDayInvalidBuildNumber(self):
+    self.make_commit()
+    self.make_commit(f'BUILD_NUMBER={_TEST_BUILD_NUMBER}')
+    build_number, _ = build_info.get_build_id_and_git_rev_from_commits(
+        cwd=self.test_dir.name)
+    self.assertIsNone(build_number)
+
+  def testGetBuildNumberFromCommitCountSunnyDay(self):
+    num_commits = 5
+    for _ in range(num_commits):
+      self.make_commit()
+    build_number = build_info.get_build_id_from_commit_count(
+        cwd=self.test_dir.name)
+    self.assertEqual(
+        int(build_number),
+        num_commits + build_info.COMMIT_COUNT_BUILD_ID_OFFSET)
+
+  def testCommitsOutrankCommitCount(self):
+    self.make_commit()
+    self.make_commit_with_build_number()
+    self.make_commit()
+    build_number = get_build_id.main(cwd=self.test_dir.name)
+    self.assertEqual(int(build_number), _TEST_BUILD_NUMBER)
+
+  def testFallbackToCommitCount(self):
+    num_commits = 5
+    for _ in range(num_commits):
+      self.make_commit()
+    build_number = get_build_id.main(cwd=self.test_dir.name)
+    self.assertEqual(
+        int(build_number),
+        num_commits + build_info.COMMIT_COUNT_BUILD_ID_OFFSET)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cobalt/debug/BUILD.gn b/cobalt/debug/BUILD.gn
index 91c5586..0bdf9b5 100644
--- a/cobalt/debug/BUILD.gn
+++ b/cobalt/debug/BUILD.gn
@@ -17,6 +17,8 @@
   sources = [
     "backend/agent_base.cc",
     "backend/agent_base.h",
+    "backend/cobalt_agent.cc",
+    "backend/cobalt_agent.h",
     "backend/command_map.h",
     "backend/css_agent.cc",
     "backend/css_agent.h",
@@ -90,6 +92,9 @@
       "console/command_manager.cc",
       "console/command_manager.h",
     ]
-    deps = [ "//cobalt/base" ]
+    deps = [
+      "//cobalt/base",
+      "//starboard:starboard_headers_only",
+    ]
   }
 }
diff --git a/cobalt/debug/backend/cobalt_agent.cc b/cobalt/debug/backend/cobalt_agent.cc
new file mode 100644
index 0000000..63901c2
--- /dev/null
+++ b/cobalt/debug/backend/cobalt_agent.cc
@@ -0,0 +1,85 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/debug/backend/cobalt_agent.h"
+
+#include <set>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/values.h"
+#include "cobalt/debug/console/command_manager.h"
+#include "cobalt/debug/json_object.h"
+
+namespace cobalt {
+namespace debug {
+namespace backend {
+
+CobaltAgent::CobaltAgent(DebugDispatcher* dispatcher)
+    : AgentBase("Cobalt", dispatcher) {
+  commands_["getConsoleCommands"] =
+      base::Bind(&CobaltAgent::GetConsoleCommands, base::Unretained(this));
+  commands_["sendConsoleCommand"] =
+      base::Bind(&CobaltAgent::SendConsoleCommand, base::Unretained(this));
+  // dispatcher_->AddDomain(domain_, commands_.Bind());
+}
+
+void CobaltAgent::GetConsoleCommands(Command command) {
+  JSONObject response(new base::DictionaryValue());
+  JSONList list(new base::ListValue());
+
+  console::ConsoleCommandManager* command_manager =
+      console::ConsoleCommandManager::GetInstance();
+  DCHECK(command_manager);
+  if (command_manager) {
+    std::set<std::string> commands = command_manager->GetRegisteredCommands();
+    for (auto& command_name : commands) {
+      JSONObject console_command(new base::DictionaryValue());
+      console_command->SetString("command", command_name);
+      console_command->SetString("shortHelp",
+                                 command_manager->GetShortHelp(command_name));
+      console_command->SetString("longHelp",
+                                 command_manager->GetLongHelp(command_name));
+      list->Append(std::move(console_command));
+    }
+  }
+
+  JSONObject commands(new base::DictionaryValue());
+  commands->Set("commands", std::move(list));
+  response->Set("result", std::move(commands));
+  command.SendResponse(response);
+}
+
+void CobaltAgent::SendConsoleCommand(Command command) {
+  JSONObject params = JSONParse(command.GetParams());
+  if (params) {
+    std::string console_command;
+    if (params->GetString("command", &console_command)) {
+      std::string message;
+      params->GetString("message", &message);
+      console::ConsoleCommandManager* console_command_manager =
+          console::ConsoleCommandManager::GetInstance();
+      DCHECK(console_command_manager);
+      console_command_manager->HandleCommand(console_command, message);
+      command.SendResponse();
+      return;
+    }
+  }
+  command.SendErrorResponse(Command::kInvalidParams, "Missing command.");
+}
+
+}  // namespace backend
+}  // namespace debug
+}  // namespace cobalt
diff --git a/cobalt/debug/backend/cobalt_agent.h b/cobalt/debug/backend/cobalt_agent.h
new file mode 100644
index 0000000..27f8613
--- /dev/null
+++ b/cobalt/debug/backend/cobalt_agent.h
@@ -0,0 +1,47 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_DEBUG_BACKEND_COBALT_AGENT_H_
+#define COBALT_DEBUG_BACKEND_COBALT_AGENT_H_
+
+#include "cobalt/debug/backend/agent_base.h"
+#include "cobalt/debug/backend/debug_dispatcher.h"
+#include "cobalt/debug/command.h"
+#include "cobalt/debug/json_object.h"
+#include "cobalt/dom/window.h"
+
+namespace cobalt {
+namespace debug {
+namespace backend {
+
+// Implements a small part of the the "Runtime" inspector protocol domain with
+// just enough to support console input. When using the V8 JavaScript engine,
+// this class is not needed since the V8 inspector implements the Runtime domain
+// for us.
+//
+// https://chromedevtools.github.io/devtools-protocol/tot/Runtime
+class CobaltAgent : public AgentBase {
+ public:
+  explicit CobaltAgent(DebugDispatcher* dispatcher);
+
+ private:
+  void GetConsoleCommands(Command command);
+  void SendConsoleCommand(Command command);
+};
+
+}  // namespace backend
+}  // namespace debug
+}  // namespace cobalt
+
+#endif  // COBALT_DEBUG_BACKEND_COBALT_AGENT_H_
diff --git a/cobalt/debug/backend/debug_module.cc b/cobalt/debug/backend/debug_module.cc
index 56e8b26..ea767af 100644
--- a/cobalt/debug/backend/debug_module.cc
+++ b/cobalt/debug/backend/debug_module.cc
@@ -23,6 +23,7 @@
 namespace backend {
 
 namespace {
+constexpr char kCobaltAgent[] = "CobaltAgent";
 constexpr char kScriptDebuggerAgent[] = "ScriptDebuggerAgent";
 constexpr char kLogAgent[] = "LogAgent";
 constexpr char kDomAgent[] = "DomAgent";
@@ -140,6 +141,7 @@
   // directly handle one or more protocol domains.
   script_debugger_agent_.reset(
       new ScriptDebuggerAgent(debug_dispatcher_.get(), script_debugger_.get()));
+  cobalt_agent_.reset(new CobaltAgent(debug_dispatcher_.get()));
   log_agent_.reset(new LogAgent(debug_dispatcher_.get()));
   dom_agent_.reset(new DOMAgent(debug_dispatcher_.get()));
   css_agent_ = WrapRefCounted(new CSSAgent(debug_dispatcher_.get()));
@@ -178,6 +180,7 @@
   base::DictionaryValue* agents_state =
       data.debugger_state == nullptr ? nullptr
                                      : data.debugger_state->agents_state.get();
+  cobalt_agent_->Thaw(RemoveAgentState(kCobaltAgent, agents_state));
   script_debugger_agent_->Thaw(
       RemoveAgentState(kScriptDebuggerAgent, agents_state));
   log_agent_->Thaw(RemoveAgentState(kLogAgent, agents_state));
@@ -200,6 +203,7 @@
 
   debugger_state->agents_state.reset(new base::DictionaryValue());
   base::DictionaryValue* agents_state = debugger_state->agents_state.get();
+  StoreAgentState(agents_state, kCobaltAgent, cobalt_agent_->Freeze());
   StoreAgentState(agents_state, kScriptDebuggerAgent,
                   script_debugger_agent_->Freeze());
   StoreAgentState(agents_state, kLogAgent, log_agent_->Freeze());
diff --git a/cobalt/debug/backend/debug_module.h b/cobalt/debug/backend/debug_module.h
index da1778a..4736968 100644
--- a/cobalt/debug/backend/debug_module.h
+++ b/cobalt/debug/backend/debug_module.h
@@ -20,6 +20,7 @@
 
 #include "base/message_loop/message_loop.h"
 #include "cobalt/base/debugger_hooks.h"
+#include "cobalt/debug/backend/cobalt_agent.h"
 #include "cobalt/debug/backend/css_agent.h"
 #include "cobalt/debug/backend/debug_backend.h"
 #include "cobalt/debug/backend/debug_dispatcher.h"
@@ -134,6 +135,7 @@
 
   // Wrappable object providing native helpers for backend JavaScript.
   scoped_refptr<DebugBackend> debug_backend_;
+  std::unique_ptr<CobaltAgent> cobalt_agent_;
   std::unique_ptr<LogAgent> log_agent_;
   std::unique_ptr<DOMAgent> dom_agent_;
   scoped_refptr<CSSAgent> css_agent_;
diff --git a/cobalt/debug/backend/log_agent.cc b/cobalt/debug/backend/log_agent.cc
index 9fc974a..593fb0a 100644
--- a/cobalt/debug/backend/log_agent.cc
+++ b/cobalt/debug/backend/log_agent.cc
@@ -14,13 +14,22 @@
 
 #include "cobalt/debug/backend/log_agent.h"
 
+#include "base/bind.h"
 #include "base/logging.h"
+#include "cobalt/debug/console/command_manager.h"
 
 namespace cobalt {
 namespace debug {
 namespace backend {
 
 namespace {
+const char kDebugLogCommand[] = "debug_log";
+const char kDebugLogCommandShortHelp[] =
+    "Turns browser debug logging on or off.";
+const char kDebugLogCommandLongHelp[] =
+    "When turned on, browser logs are sent in such a way that they are visible "
+    "in devtools.";
+
 // Error levels:
 constexpr char kInfoLevel[] = "info";
 constexpr char kWarningLevel[] = "warning";
@@ -42,7 +51,21 @@
 }
 }  // namespace
 
-LogAgent::LogAgent(DebugDispatcher* dispatcher) : AgentBase("Log", dispatcher) {
+void LogAgent::OnDebugLog(const std::string& message) {
+  SetDebugLog(console::ConsoleCommandManager::CommandHandler::IsOnEnableOrTrue(
+      message));
+}
+void LogAgent::SetDebugLog(bool enable) {
+  event_method_ = domain_ + (enable ? ".entryAdded" : ".browserEntryAdded");
+}
+
+LogAgent::LogAgent(DebugDispatcher* dispatcher)
+    : AgentBase("Log", dispatcher),
+      debug_log_command_handler_(
+          kDebugLogCommand,
+          base::Bind(&LogAgent::OnDebugLog, base::Unretained(this)),
+          kDebugLogCommandShortHelp, kDebugLogCommandLongHelp) {
+  SetDebugLog(false);
   // Get log output while still making it available elsewhere.
   log_message_handler_callback_id_ =
       base::LogMessageHandler::GetInstance()->AddCallback(
@@ -63,9 +86,10 @@
     // except it only shows up in the debug console and not in remote devtools.
     // TODO: Flesh out the rest of LogEntry properties (source, timestamp)
     JSONObject params(new base::DictionaryValue());
+    params->SetString("entry.source", "other");
     params->SetString("entry.text", str);
     params->SetString("entry.level", GetLogLevelFromSeverity(severity));
-    dispatcher_->SendEvent(domain_ + ".browserEntryAdded", params);
+    dispatcher_->SendEvent(event_method_, params);
   }
 
   // Don't suppress the log message.
diff --git a/cobalt/debug/backend/log_agent.h b/cobalt/debug/backend/log_agent.h
index 97e6fe0..5a6fb50 100644
--- a/cobalt/debug/backend/log_agent.h
+++ b/cobalt/debug/backend/log_agent.h
@@ -19,6 +19,7 @@
 #include "cobalt/base/log_message_handler.h"
 #include "cobalt/debug/backend/agent_base.h"
 #include "cobalt/debug/backend/debug_dispatcher.h"
+#include "cobalt/debug/console/command_manager.h"
 
 namespace cobalt {
 namespace debug {
@@ -33,6 +34,9 @@
   explicit LogAgent(DebugDispatcher* dispatcher);
   ~LogAgent();
 
+  void OnDebugLog(const std::string& message);
+  void SetDebugLog(bool enable);
+
  private:
   // Called by LogMessageHandler for each log message.
   // May be called from any thread.
@@ -46,6 +50,11 @@
 
   // The callback id of our recipient of log messages so we can unregister it.
   base::LogMessageHandler::CallbackId log_message_handler_callback_id_;
+
+  std::string event_method_;
+
+  debug::console::ConsoleCommandManager::CommandHandler
+      debug_log_command_handler_;
 };
 
 }  // namespace backend
diff --git a/cobalt/debug/console/command_manager.cc b/cobalt/debug/console/command_manager.cc
index a25879a..3c30ae3 100644
--- a/cobalt/debug/console/command_manager.cc
+++ b/cobalt/debug/console/command_manager.cc
@@ -15,6 +15,7 @@
 #include "cobalt/debug/console/command_manager.h"
 
 #include "base/logging.h"
+#include "starboard/string.h"
 
 namespace cobalt {
 namespace debug {
@@ -45,6 +46,16 @@
   manager->UnregisterCommandHandler(this);
 }
 
+// Returns true if the message is 'on', 'enable', or 'true'.
+// static
+bool ConsoleCommandManager::CommandHandler::IsOnEnableOrTrue(
+    const std::string& message) {
+  return (SbStringCompareNoCase("on", message.c_str()) == 0) ||
+         (SbStringCompareNoCase("enable", message.c_str()) == 0) ||
+         (SbStringCompareNoCase("true", message.c_str()) == 0);
+}
+
+
 void ConsoleCommandManager::HandleCommand(const std::string& command,
                                           const std::string& message) const {
   DCHECK_GT(command.length(), size_t(0));
diff --git a/cobalt/debug/console/command_manager.h b/cobalt/debug/console/command_manager.h
index 66a5601..1ace46c 100644
--- a/cobalt/debug/console/command_manager.h
+++ b/cobalt/debug/console/command_manager.h
@@ -61,6 +61,9 @@
     const std::string& short_help() const { return short_help_; }
     const std::string& long_help() const { return long_help_; }
 
+    // Returns true if the message is 'on', 'enable', or 'true'.
+    static bool IsOnEnableOrTrue(const std::string& message);
+
    private:
     std::string command_;
     CommandCallback callback_;
diff --git a/cobalt/demos/content/media-element-demo/src/components/player.ts b/cobalt/demos/content/media-element-demo/src/components/player.ts
index 7bd9c17..60e3a5c 100644
--- a/cobalt/demos/content/media-element-demo/src/components/player.ts
+++ b/cobalt/demos/content/media-element-demo/src/components/player.ts
@@ -25,6 +25,18 @@
   /** The <video> element. */
   private videoEl!: HTMLVideoElement;
 
+  /** The video SourceBuffer */
+  private videoSourceBuffer!: SourceBuffer;
+
+  /** The audio SourceBuffer */
+  private audioSourceBuffer!: SourceBuffer;
+
+  /** max(videoSourceBuffer.writeHead - videoEl.currentTime) */
+  private maxVideoWriteHeadDistance!: number;
+
+  /** max(audioSourceBuffer.writeHead - videoEl.currentTime) */
+  private maxAudioWriteHeadDistance!: number;
+
   /** The element displaying video download buffer info. */
   private videoDownloadBufferInfo!: Element;
 
@@ -53,6 +65,8 @@
     super(props);
     this.videos = convertToMediaArray(props.video);
     this.audios = convertToMediaArray(props.audio);
+    this.maxVideoWriteHeadDistance = 0;
+    this.maxAudioWriteHeadDistance = 0;
   }
 
   /** @override */
@@ -93,12 +107,37 @@
   }
 
   private renderVideoInfo() {
+    var h5vccAudioConnectors = '';
+    try {
+      h5vccAudioConnectors = this.videoEl.h5vccAudioConnectors;
+    } catch (error) {}
     renderComponent(
         VideoInfo, {
           duration: this.videoEl.duration,
           currentTime: this.videoEl.currentTime,
+          audioConnectors: h5vccAudioConnectors,
         },
         this.videoInfo);
+    if (this.videoSourceBuffer) {
+      this.maxVideoWriteHeadDistance =
+          Math.max(this.maxVideoWriteHeadDistance,
+                   this.videoSourceBuffer.writeHead - this.videoEl.currentTime);
+      renderComponent(
+        SourceBufferInfo,
+          {name: 'Video', sourceBuffer: this.videoSourceBuffer,
+           maxWriteHeadDistance: this.maxVideoWriteHeadDistance},
+         this.videoSourceBufferInfo);
+      }
+    if (this.audioSourceBuffer) {
+      this.maxAudioWriteHeadDistance =
+          Math.max(this.maxAudioWriteHeadDistance,
+                   this.audioSourceBuffer.writeHead - this.videoEl.currentTime);
+      renderComponent(
+        SourceBufferInfo,
+        {name: 'Audio', sourceBuffer: this.audioSourceBuffer,
+         maxWriteHeadDistance: this.maxAudioWriteHeadDistance},
+        this.audioSourceBufferInfo);
+    }
   }
 
   private async play() {
@@ -149,7 +188,7 @@
 
   /**
    * Plays all videos as adaptive videos.
-   * TODO: dynmaically calculate the source buffer MIME.
+   * TODO: dynamically calculate the source buffer MIME.
    */
   private playAdaptiveVideo() {
     const ms = new MediaSource();
@@ -158,12 +197,7 @@
       if (this.videos.length > 0) {
         const videoSourceBuffer =
             ms.addSourceBuffer('video/mp4; codecs="avc1.640028"');
-        videoSourceBuffer.addEventListener('updateend', () => {
-          renderComponent(
-              SourceBufferInfo,
-              {name: 'Video', sourceBuffer: videoSourceBuffer},
-              this.videoSourceBufferInfo);
-        });
+        this.videoSourceBuffer = videoSourceBuffer;
         const downloadBuffer = new DownloadBuffer(this.videos);
         downloadBuffer.register((reportMap) => {
           renderComponent(
@@ -176,12 +210,7 @@
       if (this.audios.length > 0) {
         const audioSourceBuffer =
             ms.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
-        audioSourceBuffer.addEventListener('updateend', () => {
-          renderComponent(
-              SourceBufferInfo,
-              {name: 'Audio', sourceBuffer: audioSourceBuffer},
-              this.audioSourceBufferInfo);
-        });
+        this.audioSourceBuffer = audioSourceBuffer;
         const downloadBuffer = new DownloadBuffer(this.audios);
         downloadBuffer.register(
             (reportMap) => {renderComponent(
diff --git a/cobalt/demos/content/media-element-demo/src/components/source_buffer_info.ts b/cobalt/demos/content/media-element-demo/src/components/source_buffer_info.ts
index 92343f3..6747f66 100644
--- a/cobalt/demos/content/media-element-demo/src/components/source_buffer_info.ts
+++ b/cobalt/demos/content/media-element-demo/src/components/source_buffer_info.ts
@@ -1,9 +1,12 @@
 interface Props {
   sourceBuffer: SourceBuffer;
   name: string;
+  maxWriteHeadDistance: number;
 }
 
 /** A component that displays the source buffer info. */
-export function SourceBufferInfo({sourceBuffer, name}: Props) {
-  return `<div>${name} buffered: ${sourceBuffer.buffered.end(0)} sec</div>`;
+export function SourceBufferInfo({sourceBuffer, name, maxWriteHeadDistance}: Props) {
+  return `<div>${name} buffered: ${sourceBuffer.buffered.end(0)} sec` +
+      `, writeHead: ${sourceBuffer.writeHead} sec` +
+      `, maxWriteHeadDistance: ${maxWriteHeadDistance} sec</div>`;
 }
diff --git a/cobalt/demos/content/media-element-demo/src/components/video_info.ts b/cobalt/demos/content/media-element-demo/src/components/video_info.ts
index 3f8306f..68012c8 100644
--- a/cobalt/demos/content/media-element-demo/src/components/video_info.ts
+++ b/cobalt/demos/content/media-element-demo/src/components/video_info.ts
@@ -1,9 +1,13 @@
 interface Props {
   duration: number;
   currentTime: number;
+  audioConnectors: string;
 }
 
 /** A component that displays video info. */
-export function VideoInfo({duration, currentTime}: Props) {
+export function VideoInfo({duration, currentTime, audioConnectors}: Props) {
+  if (audioConnectors) {
+    return `<div>${currentTime} / ${duration} / audioConnectors: ${audioConnectors}</div>`;
+  }
   return `<div>${currentTime} / ${duration}</div>`;
 }
diff --git a/cobalt/dom/html_media_element.cc b/cobalt/dom/html_media_element.cc
index d516ac8..054209f 100644
--- a/cobalt/dom/html_media_element.cc
+++ b/cobalt/dom/html_media_element.cc
@@ -19,6 +19,7 @@
 #include <limits>
 #include <memory>
 #include <utility>
+#include <vector>
 
 #include "base/bind.h"
 #include "base/compiler_specific.h"
@@ -26,6 +27,7 @@
 #include "base/lazy_instance.h"
 #include "base/logging.h"
 #include "base/message_loop/message_loop.h"
+#include "base/strings/string_util.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/instance_counter.h"
 #include "cobalt/base/tokens.h"
@@ -36,6 +38,7 @@
 #include "cobalt/dom/media_settings.h"
 #include "cobalt/dom/media_source.h"
 #include "cobalt/dom/media_source_ready_state.h"
+#include "cobalt/extension/audio_write_ahead.h"
 #include "cobalt/loader/fetcher_factory.h"
 #include "cobalt/media/url_fetcher_data_source.h"
 #include "cobalt/media/web_media_player_factory.h"
@@ -45,6 +48,7 @@
 #include "cobalt/web/dom_exception.h"
 #include "cobalt/web/event.h"
 #include "cobalt/web/web_settings.h"
+#include "starboard/system.h"
 
 #include "cobalt/dom/eme/media_encrypted_event.h"
 #include "cobalt/dom/eme/media_encrypted_event_init.h"
@@ -641,6 +645,32 @@
   event_queue_.Enqueue(event);
 }
 
+std::string HTMLMediaElement::h5vcc_audio_connectors(
+    script::ExceptionState* exception_state) const {
+  static const CobaltExtensionConfigurableAudioWriteAheadApi* extension_api =
+      static_cast<const CobaltExtensionConfigurableAudioWriteAheadApi*>(
+          SbSystemGetExtension(
+              kCobaltExtensionConfigurableAudioWriteAheadName));
+  if (extension_api) {
+    if (!player_) {
+      web::DOMException::Raise(web::DOMException::kInvalidStateErr,
+                               exception_state);
+      return std::string();
+    }
+
+    DCHECK_EQ(extension_api->name,
+              std::string(kCobaltExtensionConfigurableAudioWriteAheadName));
+    DCHECK_EQ(extension_api->version, 1u);
+
+    std::vector<std::string> configs = player_->GetAudioConnectors();
+    return base::JoinString(configs, ";");
+  }
+
+  web::DOMException::Raise(web::DOMException::kNotSupportedErr,
+                           exception_state);
+  return std::string();
+}
+
 void HTMLMediaElement::CreateMediaPlayer() {
   TRACE_EVENT0("cobalt::dom", "HTMLMediaElement::CreateMediaPlayer()");
   LOG(INFO) << "Create media player.";
diff --git a/cobalt/dom/html_media_element.h b/cobalt/dom/html_media_element.h
index 4276fd8..612e21c 100644
--- a/cobalt/dom/html_media_element.h
+++ b/cobalt/dom/html_media_element.h
@@ -146,6 +146,12 @@
   // function won't modify the target of the |event| passed in.
   void ScheduleEvent(const scoped_refptr<web::Event>& event);
 
+  // Returns semicolon separated names of audio connectors, like
+  // "hdmi;bluetooth".
+  // TODO(b/267678497): The current interface is tentative, to be refined.
+  std::string h5vcc_audio_connectors(
+      script::ExceptionState* exception_state) const;
+
   // Set max video capabilities.
   void SetMaxVideoCapabilities(const std::string& max_video_capabilities,
                                script::ExceptionState* exception_state);
diff --git a/cobalt/dom/html_media_element.idl b/cobalt/dom/html_media_element.idl
index 4a1a509..401060a 100644
--- a/cobalt/dom/html_media_element.idl
+++ b/cobalt/dom/html_media_element.idl
@@ -61,4 +61,10 @@
   attribute boolean controls;
   [RaisesException] attribute double volume;
   attribute boolean muted;
+
+  // non standard, semicolon separated names of audio connectors, like
+  // "hdmi;bluetooth".  It raises `NotSupportedError` on apps doesn't support
+  // this feature, or `InvalidStateError` if there isn't an active playback.
+  // TODO(b/267678497): The current interface is tentative, to be refined.
+  [RaisesException] readonly attribute DOMString h5vccAudioConnectors;
 };
diff --git a/cobalt/dom/source_buffer.cc b/cobalt/dom/source_buffer.cc
index 267677b..039303f 100644
--- a/cobalt/dom/source_buffer.cc
+++ b/cobalt/dom/source_buffer.cc
@@ -453,6 +453,17 @@
   track_defaults_ = track_defaults;
 }
 
+double SourceBuffer::write_head(script::ExceptionState* exception_state) const {
+  if (media_source_ == NULL) {
+    web::DOMException::Raise(web::DOMException::kInvalidStateErr,
+                             exception_state);
+    return 0.0;
+  }
+
+  DCHECK(chunk_demuxer_);
+  return chunk_demuxer_->GetWriteHead(id_).InSecondsF();
+}
+
 void SourceBuffer::OnRemovedFromMediaSource() {
   if (media_source_ == NULL) {
     return;
diff --git a/cobalt/dom/source_buffer.h b/cobalt/dom/source_buffer.h
index c611177..76d0aa7 100644
--- a/cobalt/dom/source_buffer.h
+++ b/cobalt/dom/source_buffer.h
@@ -134,6 +134,9 @@
 
   // Custom, not in any spec.
   //
+  // Return the highest presentation timestamp written to SbPlayer.
+  double write_head(script::ExceptionState* exception_state) const;
+
   void OnRemovedFromMediaSource();
   double GetHighestPresentationTimestamp() const;
 
diff --git a/cobalt/dom/source_buffer.idl b/cobalt/dom/source_buffer.idl
index 1fd8053..bb755e2 100644
--- a/cobalt/dom/source_buffer.idl
+++ b/cobalt/dom/source_buffer.idl
@@ -38,4 +38,10 @@
   // [RaisesException] void abort();
   [RaisesException] void remove(double start, unrestricted double end);
   [RaisesException] attribute TrackDefaultList trackDefaults;
+
+  // Non standard interface (b/267678497).
+  // Returns the highest presentation timestamp written to SbPlayer, raises
+  // `InvalidStateError` if the SourceBuffer object has been removed from the
+  // MediaSource object.
+  [RaisesException] readonly attribute double writeHead;
 };
diff --git a/cobalt/extension/audio_write_ahead.h b/cobalt/extension/audio_write_ahead.h
new file mode 100644
index 0000000..985b6d3
--- /dev/null
+++ b/cobalt/extension/audio_write_ahead.h
@@ -0,0 +1,75 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_EXTENSION_AUDIO_WRITE_AHEAD_H_
+#define COBALT_EXTENSION_AUDIO_WRITE_AHEAD_H_
+
+#include "starboard/media.h"
+#include "starboard/player.h"
+#include "starboard/time.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionConfigurableAudioWriteAheadName \
+  "dev.cobalt.extension.ConfigurableAudioWriteAhead"
+
+#define kCobaltExtensionPlayerWriteDurationLocal (kSbTimeSecond / 2)
+#define kCobaltExtensionPlayerWriteDurationRemote (kSbTimeSecond * 10)
+
+typedef enum CobaltExtensionMediaAudioConnector {
+  kCobaltExtensionMediaAudioConnectorUnknown,
+
+  kCobaltExtensionMediaAudioConnectorAnalog,
+  kCobaltExtensionMediaAudioConnectorBluetooth,
+  kCobaltExtensionMediaAudioConnectorBuiltIn,
+  kCobaltExtensionMediaAudioConnectorHdmi,
+  kCobaltExtensionMediaAudioConnectorRemoteWired,
+  kCobaltExtensionMediaAudioConnectorRemoteWireless,
+  kCobaltExtensionMediaAudioConnectorRemoteOther,
+  kCobaltExtensionMediaAudioConnectorSpdif,
+  kCobaltExtensionMediaAudioConnectorUsb,
+} CobaltExtensionMediaAudioConnector;
+
+typedef struct CobaltExtensionMediaAudioConfiguration {
+  int index;
+  CobaltExtensionMediaAudioConnector connector;
+  SbTime latency;
+  SbMediaAudioCodingType coding_type;
+  int number_of_channels;
+} CobaltExtensionMediaAudioConfiguration;
+
+typedef struct CobaltExtensionConfigurableAudioWriteAheadApi {
+  // Name should be the string
+  // |kCobaltExtensionConfigurableAudioWriteAheadName|. This helps to validate
+  // that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  bool (*PlayerGetAudioConfiguration)(
+      SbPlayer player, int index,
+      CobaltExtensionMediaAudioConfiguration* out_audio_configuration);
+
+} CobaltExtensionConfigurableAudioWriteAheadApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // COBALT_EXTENSION_AUDIO_WRITE_AHEAD_H_
diff --git a/cobalt/extension/extension_test.cc b/cobalt/extension/extension_test.cc
index b6cf3bc..2dfdae9 100644
--- a/cobalt/extension/extension_test.cc
+++ b/cobalt/extension/extension_test.cc
@@ -20,6 +20,7 @@
 #include "cobalt/extension/font.h"
 #include "cobalt/extension/free_space.h"
 #include "cobalt/extension/graphics.h"
+#include "cobalt/extension/ifa.h"
 #include "cobalt/extension/installation_manager.h"
 #include "cobalt/extension/javascript_cache.h"
 #include "cobalt/extension/media_session.h"
@@ -382,5 +383,27 @@
   EXPECT_EQ(second_extension_api, extension_api)
       << "Extension struct should be a singleton";
 }
+
+TEST(ExtensionTest, Ifa) {
+  typedef StarboardExtensionIfaApi ExtensionApi;
+  const char* kExtensionName = kStarboardExtensionIfaName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->GetAdvertisingId, nullptr);
+  EXPECT_NE(extension_api->GetLimitAdTracking, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
 }  // namespace extension
 }  // namespace cobalt
diff --git a/cobalt/extension/ifa.h b/cobalt/extension/ifa.h
new file mode 100644
index 0000000..a207049
--- /dev/null
+++ b/cobalt/extension/ifa.h
@@ -0,0 +1,58 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_EXTENSION_IFA_H_
+#define STARBOARD_EXTENSION_IFA_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kStarboardExtensionIfaName "dev.cobalt.extension.Ifa"
+
+typedef struct StarboardExtensionIfaApi {
+  // Name should be the string |kCobaltExtensionIfaName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Advertising ID or IFA, typically a 128-bit UUID
+  // Please see https://iabtechlab.com/OTT-IFA for details.
+  // Corresponds to 'ifa' field. Note: `ifa_type` field is not provided.
+  // In Starboard 14 this the value is retrieved through the system
+  // property `kSbSystemPropertyAdvertisingId` defined in
+  // `starboard/system.h`.
+  bool (*GetAdvertisingId)(char* out_value, int value_length);
+
+  // Limit advertising tracking, treated as boolean. Set to nonzero to indicate
+  // a true value. Corresponds to 'lmt' field.
+  // In Starboard 14 this the value is retrieved through the system
+  // property `kSbSystemPropertyLimitAdTracking` defined in
+  // `starboard/system.h`.
+
+  bool (*GetLimitAdTracking)(char* out_value, int value_length);
+
+} CobaltExtensionIfaApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // STARBOARD_EXTENSION_IFA_H_
diff --git a/cobalt/h5vcc/h5vcc_crash_log.cc b/cobalt/h5vcc/h5vcc_crash_log.cc
index a240d92..4088a84 100644
--- a/cobalt/h5vcc/h5vcc_crash_log.cc
+++ b/cobalt/h5vcc/h5vcc_crash_log.cc
@@ -31,70 +31,49 @@
 namespace cobalt {
 namespace h5vcc {
 
-// We keep a global mapping of all registered logs.  When a crash occurs, we
-// will iterate through the mapping in this dictionary to extract all
-// logged entries and write them to the system's crash logger via Starboard.
-class CrashLogDictionary {
- public:
-  static CrashLogDictionary* GetInstance() {
-    return base::Singleton<CrashLogDictionary, base::DefaultSingletonTraits<
-                                                   CrashLogDictionary> >::get();
-  }
+CrashLogDictionary* CrashLogDictionary::GetInstance() {
+  return base::Singleton<CrashLogDictionary, base::DefaultSingletonTraits<
+                                                 CrashLogDictionary>>::get();
+}
 
-  void SetString(const std::string& key, const std::string& value) {
-    base::AutoLock lock(mutex_);
-    // While the lock prevents contention between other calls to SetString(),
-    // the atomics guard against OnCrash(), which doesn't acquire |mutex_|, from
-    // accessing the data at the same time.  In the case that OnCrash() is
-    // being called, we give up and skip adding the data.
-    if (base::subtle::Acquire_CompareAndSwap(&accessing_log_data_, 0, 1) == 0) {
-      string_log_map_[key] = value;
-      base::subtle::Release_Store(&accessing_log_data_, 0);
-    }
+void CrashLogDictionary::SetString(const std::string& key,
+                                   const std::string& value) {
+  base::AutoLock lock(mutex_);
+  // While the lock prevents contention between other calls to SetString(),
+  // the atomics guard against OnCrash(), which doesn't acquire |mutex_|, from
+  // accessing the data at the same time.  In the case that OnCrash() is
+  // being called, we give up and skip adding the data.
+  if (base::subtle::Acquire_CompareAndSwap(&accessing_log_data_, 0, 1) == 0) {
+    string_log_map_[key] = value;
+    base::subtle::Release_Store(&accessing_log_data_, 0);
   }
+}
 
- private:
-  CrashLogDictionary() : accessing_log_data_(0) {
+CrashLogDictionary::CrashLogDictionary() : accessing_log_data_(0) {
 #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT)
-    SbCoreDumpRegisterHandler(&CoreDumpHandler, this);
+  SbCoreDumpRegisterHandler(&CoreDumpHandler, this);
 #endif
-  }
+}
 
-  static void CoreDumpHandler(void* context) {
-    CrashLogDictionary* crash_log_dictionary =
-        static_cast<CrashLogDictionary*>(context);
-    crash_log_dictionary->OnCrash();
-  }
+void CrashLogDictionary::CoreDumpHandler(void* context) {
+  CrashLogDictionary* crash_log_dictionary =
+      static_cast<CrashLogDictionary*>(context);
+  crash_log_dictionary->OnCrash();
+}
 
-  void OnCrash() {
+void CrashLogDictionary::OnCrash() {
 #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT)
-    // Check that we're not already updating log data.  If we are, we just
-    // give up and skip recording any crash data, but hopefully this is rare.
-    if (base::subtle::Acquire_CompareAndSwap(&accessing_log_data_, 0, 1) == 0) {
-      for (StringMap::const_iterator iter = string_log_map_.begin();
-           iter != string_log_map_.end(); ++iter) {
-        SbCoreDumpLogString(iter->first.c_str(), iter->second.c_str());
-      }
-      base::subtle::Release_Store(&accessing_log_data_, 0);
+  // Check that we're not already updating log data.  If we are, we just
+  // give up and skip recording any crash data, but hopefully this is rare.
+  if (base::subtle::Acquire_CompareAndSwap(&accessing_log_data_, 0, 1) == 0) {
+    for (StringMap::const_iterator iter = string_log_map_.begin();
+         iter != string_log_map_.end(); ++iter) {
+      SbCoreDumpLogString(iter->first.c_str(), iter->second.c_str());
     }
-#endif
+    base::subtle::Release_Store(&accessing_log_data_, 0);
   }
-
-  friend struct base::DefaultSingletonTraits<CrashLogDictionary>;
-
-  base::subtle::Atomic32 accessing_log_data_;
-
-  // It is possible for multiple threads to call the H5VCC interface at the
-  // same time, and they will all forward to this global singleton, so we
-  // use this mutex to protect against concurrent access.
-  base::Lock mutex_;
-
-  typedef std::map<std::string, std::string> StringMap;
-  // Keeps track of all string values to be logged when a crash occurs.
-  StringMap string_log_map_;
-
-  DISALLOW_COPY_AND_ASSIGN(CrashLogDictionary);
-};
+#endif
+}
 
 bool H5vccCrashLog::SetString(const std::string& key,
                               const std::string& value) {
diff --git a/cobalt/h5vcc/h5vcc_crash_log.h b/cobalt/h5vcc/h5vcc_crash_log.h
index b85c0c5..bb20a48 100644
--- a/cobalt/h5vcc/h5vcc_crash_log.h
+++ b/cobalt/h5vcc/h5vcc_crash_log.h
@@ -15,8 +15,10 @@
 #ifndef COBALT_H5VCC_H5VCC_CRASH_LOG_H_
 #define COBALT_H5VCC_H5VCC_CRASH_LOG_H_
 
+#include <map>
 #include <string>
 
+#include "base/memory/singleton.h"
 #include "cobalt/h5vcc/h5vcc_crash_type.h"
 #include "cobalt/h5vcc/watchdog_replace.h"
 #include "cobalt/h5vcc/watchdog_state.h"
@@ -26,6 +28,38 @@
 namespace cobalt {
 namespace h5vcc {
 
+// We keep a global mapping of all registered logs.  When a crash occurs, we
+// will iterate through the mapping in this dictionary to extract all
+// logged entries and write them to the system's crash logger via Starboard.
+class CrashLogDictionary {
+ public:
+  static CrashLogDictionary* GetInstance();
+
+  void SetString(const std::string& key, const std::string& value);
+
+ private:
+  CrashLogDictionary();
+
+  static void CoreDumpHandler(void* context);
+
+  void OnCrash();
+
+  friend struct base::DefaultSingletonTraits<CrashLogDictionary>;
+
+  base::subtle::Atomic32 accessing_log_data_;
+
+  // It is possible for multiple threads to call the H5VCC interface at the
+  // same time, and they will all forward to this global singleton, so we
+  // use this mutex to protect against concurrent access.
+  base::Lock mutex_;
+
+  typedef std::map<std::string, std::string> StringMap;
+  // Keeps track of all string values to be logged when a crash occurs.
+  StringMap string_log_map_;
+
+  DISALLOW_COPY_AND_ASSIGN(CrashLogDictionary);
+};
+
 class H5vccCrashLog : public script::Wrappable {
  public:
   H5vccCrashLog() {}
diff --git a/cobalt/h5vcc/h5vcc_system.cc b/cobalt/h5vcc/h5vcc_system.cc
index b28e941..ad4de95 100644
--- a/cobalt/h5vcc/h5vcc_system.cc
+++ b/cobalt/h5vcc/h5vcc_system.cc
@@ -20,6 +20,10 @@
 #include "cobalt_build_id.h"  // NOLINT(build/include_subdir)
 #include "starboard/system.h"
 
+#if SB_API_VERSION < 14
+#include "cobalt/extension/ifa.h"
+#endif  // SB_API_VERSION < 14
+
 namespace cobalt {
 namespace h5vcc {
 
@@ -54,29 +58,58 @@
 
 std::string H5vccSystem::advertising_id() const {
   std::string result;
-#if SB_API_VERSION >= 14
   const size_t kSystemPropertyMaxLength = 1024;
   char property[kSystemPropertyMaxLength] = {0};
+#if SB_API_VERSION >= 14
   if (!SbSystemGetProperty(kSbSystemPropertyAdvertisingId, property,
                            SB_ARRAY_SIZE_INT(property))) {
     DLOG(FATAL) << "Failed to get kSbSystemPropertyAdvertisingId.";
   } else {
     result = property;
   }
+#else
+  static auto const* ifa_extension =
+      static_cast<const StarboardExtensionIfaApi*>(
+          SbSystemGetExtension(kStarboardExtensionIfaName));
+  if (ifa_extension &&
+      strcmp(ifa_extension->name, kStarboardExtensionIfaName) == 0 &&
+      ifa_extension->version >= 1) {
+    if (!ifa_extension->GetAdvertisingId(property,
+                                         SB_ARRAY_SIZE_INT(property))) {
+      DLOG(FATAL) << "Failed to get AdvertisingId from IFA extension.";
+    } else {
+      result = property;
+    }
+  }
 #endif
   return result;
 }
 bool H5vccSystem::limit_ad_tracking() const {
   bool result = false;
-#if SB_API_VERSION >= 14
   const size_t kSystemPropertyMaxLength = 1024;
   char property[kSystemPropertyMaxLength] = {0};
+#if SB_API_VERSION >= 14
   if (!SbSystemGetProperty(kSbSystemPropertyLimitAdTracking, property,
                            SB_ARRAY_SIZE_INT(property))) {
     DLOG(FATAL) << "Failed to get kSbSystemPropertyAdvertisingId.";
   } else {
     result = std::atoi(property);
   }
+#else
+  static auto const* ifa_extension =
+      static_cast<const StarboardExtensionIfaApi*>(
+          SbSystemGetExtension(kStarboardExtensionIfaName));
+
+  if (ifa_extension &&
+      strcmp(ifa_extension->name, kStarboardExtensionIfaName) == 0 &&
+      ifa_extension->version >= 1) {
+    if (!ifa_extension->GetLimitAdTracking(property,
+                                           SB_ARRAY_SIZE_INT(property))) {
+      DLOG(FATAL) << "Failed to get LimitAdTracking from IFA extension.";
+    } else {
+      result = std::atoi(property);
+    }
+  }
 #endif
   return result;
 }
diff --git a/cobalt/loader/image/animated_image_tracker.cc b/cobalt/loader/image/animated_image_tracker.cc
index 98dadca..c7a3e73 100644
--- a/cobalt/loader/image/animated_image_tracker.cc
+++ b/cobalt/loader/image/animated_image_tracker.cc
@@ -15,7 +15,9 @@
 #include "cobalt/loader/image/animated_image_tracker.h"
 
 #include <algorithm>
+#include <utility>
 
+#include "base/strings/stringprintf.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/polymorphic_downcast.h"
 
@@ -24,8 +26,22 @@
 namespace image {
 
 AnimatedImageTracker::AnimatedImageTracker(
+    const char* name,
     base::ThreadPriority animated_image_decode_thread_priority)
-    : animated_image_decode_thread_("AnimatedImage") {
+    : animated_image_decode_thread_("AnimatedImage"),
+      name_(name),
+      count_animated_images_active(
+          base::StringPrintf("Count.%s.AnimatedImage.Active", name), 0,
+          "Total number of active animated image decoders."),
+      count_animated_frames_decoded(
+          base::StringPrintf("Count.%s.AnimatedImage.DecodedFrames", name), 0,
+          "Total number of decoded animated image frames."),
+      count_animated_frames_decoding_underrun(
+          base::StringPrintf("Count.%s.AnimatedImage.DecodingUnderruns", name),
+          0, "Number of underruns from decoding animated images"),
+      count_animated_frames_decoding_overrun(
+          base::StringPrintf("Count.%s.AnimatedImage.DecodingOverruns", name),
+          0, "Number of overruns from decoding animated images") {
   TRACE_EVENT0("cobalt::loader::image", "AnimatedImageTracker::RecordImage()");
   base::Thread::Options options(base::MessageLoop::TYPE_DEFAULT,
                                 0 /* default stack size */);
@@ -123,6 +139,17 @@
   playing_urls_.clear();
 }
 
+void AnimatedImageTracker::OnRenderTreeRasterized() {
+  count_animated_images_active = playing_urls_.size();
+  for (const auto& playing_url : playing_urls_) {
+    auto image = image_map_[playing_url.first].get();
+    auto stats = image->GetFrameDeltaStats();
+    count_animated_frames_decoded += stats.frames_decoded;
+    count_animated_frames_decoding_underrun += stats.frames_underrun;
+    count_animated_frames_decoding_overrun += stats.frames_overrun;
+  }
+}
+
 }  // namespace image
 }  // namespace loader
 }  // namespace cobalt
diff --git a/cobalt/loader/image/animated_image_tracker.h b/cobalt/loader/image/animated_image_tracker.h
index f6f38e4..6df920b 100644
--- a/cobalt/loader/image/animated_image_tracker.h
+++ b/cobalt/loader/image/animated_image_tracker.h
@@ -20,6 +20,7 @@
 
 #include "base/containers/small_map.h"
 #include "base/threading/thread.h"
+#include "cobalt/base/c_val.h"
 #include "cobalt/base/unused.h"
 #include "cobalt/loader/image/image.h"
 #include "url/gurl.h"
@@ -33,7 +34,8 @@
 // playing status is updated hence decoding is turned on / off for it.
 class AnimatedImageTracker {
  public:
-  explicit AnimatedImageTracker(
+  AnimatedImageTracker(
+      const char* name,
       base::ThreadPriority animated_image_decode_thread_priority);
   ~AnimatedImageTracker();
 
@@ -54,6 +56,9 @@
   // animations.
   void Reset();
 
+  // Called from WebModule to compute image animation related stats.
+  void OnRenderTreeRasterized();
+
  private:
   void OnDisplayStart(loader::image::AnimatedImage* image);
   void OnDisplayEnd(loader::image::AnimatedImage* image);
@@ -72,6 +77,15 @@
   URLToIntMap current_url_counts_;
   URLSet playing_urls_;
 
+  // The name of the WebModule this AnimatedImage tracker belongs to, for CVals.
+  const std::string name_;
+
+  // Animated image counters
+  base::CVal<int, base::CValPublic> count_animated_images_active;
+  base::CVal<int, base::CValPublic> count_animated_frames_decoded;
+  base::CVal<int, base::CValPublic> count_animated_frames_decoding_underrun;
+  base::CVal<int, base::CValPublic> count_animated_frames_decoding_overrun;
+
   // Used to ensure that all AnimatedImageTracker methods are called on the
   // same thread (*not* |animated_image_decode_thread_|), the thread that we
   // were constructed on.
diff --git a/cobalt/loader/image/animated_webp_image.cc b/cobalt/loader/image/animated_webp_image.cc
index ed0b959..8689168 100644
--- a/cobalt/loader/image/animated_webp_image.cc
+++ b/cobalt/loader/image/animated_webp_image.cc
@@ -148,6 +148,8 @@
     return;
   }
   is_playing_ = true;
+  current_stats.frames_underrun = 0;
+  current_stats.frames_overrun = 0;
 
   if (received_first_frame_) {
     StartDecoding();
@@ -171,7 +173,8 @@
 void AnimatedWebPImage::StartDecoding() {
   TRACE_EVENT0("cobalt::loader::image", "AnimatedWebPImage::StartDecoding()");
   lock_.AssertAcquired();
-  current_frame_time_ = base::TimeTicks::Now();
+  decoding_start_time_ = current_frame_time_ = base::TimeTicks::Now();
+  current_stats.frames_decoded = 0;
   if (task_runner_->BelongsToCurrentThread()) {
     DecodeFrames();
   } else {
@@ -269,6 +272,7 @@
       LOG(ERROR) << "Failed to decode WebP image frame.";
       return false;
     }
+    current_stats.frames_decoded++;
   }
 
   // Alpha blend the current frame on top of the buffer.
@@ -354,6 +358,7 @@
   // Always wait for a consumer to consume the previous frame before moving
   // forward with decoding the next frame.
   if (!frame_provider_->FrameConsumed()) {
+    current_stats.frames_overrun++;
     return false;
   }
 
@@ -386,6 +391,7 @@
   if (next_frame_time_ < current_time) {
     // Don't let the animation fall back for more than a frame.
     next_frame_time_ = current_time;
+    current_stats.frames_underrun++;
   }
 
   return true;
@@ -431,6 +437,25 @@
   return target_canvas;
 }
 
+AnimatedImage::AnimatedImageDecodingStats
+AnimatedWebPImage::GetFrameDeltaStats() {
+  AnimatedImageDecodingStats result;
+  if (current_stats.frames_decoded >= last_stats.frames_decoded) {
+    result.frames_decoded =
+        current_stats.frames_decoded - last_stats.frames_decoded;
+    result.frames_underrun =
+        current_stats.frames_underrun - last_stats.frames_underrun;
+    result.frames_overrun =
+        current_stats.frames_overrun - last_stats.frames_overrun;
+  } else {
+    // There was a reset somewhere
+    // Simply return total, this discards any overflow data we might have had.
+    result = current_stats;
+  }
+  last_stats = current_stats;
+  return result;
+}
+
 }  // namespace image
 }  // namespace loader
 }  // namespace cobalt
diff --git a/cobalt/loader/image/animated_webp_image.h b/cobalt/loader/image/animated_webp_image.h
index 135bf6a..4d1be04 100644
--- a/cobalt/loader/image/animated_webp_image.h
+++ b/cobalt/loader/image/animated_webp_image.h
@@ -66,6 +66,8 @@
   // Returns the render image of the frame for debugging
   scoped_refptr<render_tree::Image> GetFrameForDebugging(int target_frame);
 
+  AnimatedImageDecodingStats GetFrameDeltaStats() override;
+
  private:
   ~AnimatedWebPImage() override;
 
@@ -119,12 +121,17 @@
   base::CancelableClosure decode_closure_;
   base::TimeTicks current_frame_time_;
   base::Optional<base::TimeTicks> next_frame_time_;
+
   // The original encoded data.
   std::vector<uint8> data_buffer_;
   scoped_refptr<render_tree::Image> current_canvas_;
   scoped_refptr<FrameProvider> frame_provider_;
   base::Lock lock_;
 
+  base::TimeTicks decoding_start_time_;
+  AnimatedImageDecodingStats current_stats;
+  AnimatedImageDecodingStats last_stats;
+
   // Makes sure that the thread that sets the task_runner is always consistent.
   // This is the thread sending Play()/Stop() calls, and is not necessarily
   // the same thread that the task_runner itself is running on.
diff --git a/cobalt/loader/image/image.h b/cobalt/loader/image/image.h
index af99363..bac9849 100644
--- a/cobalt/loader/image/image.h
+++ b/cobalt/loader/image/image.h
@@ -145,6 +145,16 @@
     image_node_builder->destination_rect = destination_rect;
     image_node_builder->local_transform = local_transform;
   }
+
+  // Frame counters for decoding.
+  struct AnimatedImageDecodingStats {
+    unsigned int frames_decoded = 0;
+    unsigned int frames_underrun = 0;
+    unsigned int frames_overrun = 0;
+  };
+
+  // Returns decoded frame stats since the last call, as a delta.
+  virtual AnimatedImageDecodingStats GetFrameDeltaStats() = 0;
 };
 
 }  // namespace image
diff --git a/cobalt/media/base/pipeline.h b/cobalt/media/base/pipeline.h
index ccb3a1b..d2d8296 100644
--- a/cobalt/media/base/pipeline.h
+++ b/cobalt/media/base/pipeline.h
@@ -98,6 +98,7 @@
       const GetDecodeTargetGraphicsContextProviderFunc&
           get_decode_target_graphics_context_provider_func,
       bool allow_resume_after_suspend, bool allow_batched_sample_write,
+      SbTime audio_write_duration_local, SbTime audio_write_duration_remote,
       MediaLog* media_log, DecodeTargetProvider* decode_target_provider);
 
   virtual ~Pipeline() {}
@@ -217,6 +218,9 @@
   // be 0.
   virtual void GetNaturalVideoSize(gfx::Size* out_size) const = 0;
 
+  // Gets the names of audio connectors used by the audio output.
+  virtual std::vector<std::string> GetAudioConnectors() const = 0;
+
   // Return true if loading progress has been made since the last time this
   // method was called.
   virtual bool DidLoadingProgress() const = 0;
diff --git a/cobalt/media/base/sbplayer_bridge.cc b/cobalt/media/base/sbplayer_bridge.cc
index 10674e3..7d45642 100644
--- a/cobalt/media/base/sbplayer_bridge.cc
+++ b/cobalt/media/base/sbplayer_bridge.cc
@@ -370,6 +370,36 @@
   GetInfo_Locked(video_frames_decoded, video_frames_dropped, media_time);
 }
 
+std::vector<CobaltExtensionMediaAudioConfiguration>
+SbPlayerBridge::GetAudioConfigurations() {
+  DCHECK(sbplayer_interface_->IsAudioWriteAheadExtensionEnabled());
+  base::AutoLock auto_lock(lock_);
+
+  if (!SbPlayerIsValid(player_)) {
+    return std::vector<CobaltExtensionMediaAudioConfiguration>();
+  }
+
+  std::vector<CobaltExtensionMediaAudioConfiguration> configurations;
+
+  // Set a limit to avoid infinite loop.
+  constexpr int kMaxAudioConfigurations = 32;
+
+  for (int i = 0; i < kMaxAudioConfigurations; ++i) {
+    CobaltExtensionMediaAudioConfiguration configuration;
+    if (!sbplayer_interface_->GetAudioConfiguration(player_, i,
+                                                    &configuration)) {
+      break;
+    }
+
+    configurations.push_back(configuration);
+  }
+
+  LOG_IF(WARNING, configurations.empty())
+      << "Failed to find any audio configurations.";
+
+  return configurations;
+}
+
 #if SB_HAS(PLAYER_WITH_URL)
 void SbPlayerBridge::GetUrlPlayerBufferedTimeRanges(
     base::TimeDelta* buffer_start_time, base::TimeDelta* buffer_length_time) {
diff --git a/cobalt/media/base/sbplayer_bridge.h b/cobalt/media/base/sbplayer_bridge.h
index efb6ea8..80f1215 100644
--- a/cobalt/media/base/sbplayer_bridge.h
+++ b/cobalt/media/base/sbplayer_bridge.h
@@ -24,6 +24,7 @@
 #include "base/message_loop/message_loop.h"
 #include "base/synchronization/lock.h"
 #include "base/time/time.h"
+#include "cobalt/extension/audio_write_ahead.h"
 #include "cobalt/media/base/cval_stats.h"
 #include "cobalt/media/base/decode_target_provider.h"
 #include "cobalt/media/base/decoder_buffer_cache.h"
@@ -115,6 +116,7 @@
   void SetPlaybackRate(double playback_rate);
   void GetInfo(uint32* video_frames_decoded, uint32* video_frames_dropped,
                base::TimeDelta* media_time);
+  std::vector<CobaltExtensionMediaAudioConfiguration> GetAudioConfigurations();
 
 #if SB_HAS(PLAYER_WITH_URL)
   void GetUrlPlayerBufferedTimeRanges(base::TimeDelta* buffer_start_time,
diff --git a/cobalt/media/base/sbplayer_interface.cc b/cobalt/media/base/sbplayer_interface.cc
index 7c44dae..968d526 100644
--- a/cobalt/media/base/sbplayer_interface.cc
+++ b/cobalt/media/base/sbplayer_interface.cc
@@ -14,9 +14,29 @@
 
 #include "cobalt/media/base/sbplayer_interface.h"
 
+#include <string>
+
 namespace cobalt {
 namespace media {
 
+DefaultSbPlayerInterface::DefaultSbPlayerInterface() {
+  const CobaltExtensionConfigurableAudioWriteAheadApi* extension_api =
+      static_cast<const CobaltExtensionConfigurableAudioWriteAheadApi*>(
+          SbSystemGetExtension(
+              kCobaltExtensionConfigurableAudioWriteAheadName));
+  if (!extension_api) {
+    return;
+  }
+
+  DCHECK_EQ(extension_api->name,
+            std::string(kCobaltExtensionConfigurableAudioWriteAheadName));
+  DCHECK_EQ(extension_api->version, 1u);
+  DCHECK_NE(extension_api->PlayerGetAudioConfiguration, nullptr);
+
+  audio_write_duration_player_get_audio_configuration_ =
+      extension_api->PlayerGetAudioConfiguration;
+}
+
 SbPlayer DefaultSbPlayerInterface::Create(
     SbWindow window, const SbPlayerCreationParam* creation_param,
     SbPlayerDeallocateSampleFunc sample_deallocate_func,
@@ -109,5 +129,17 @@
 }
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
+bool DefaultSbPlayerInterface::IsAudioWriteAheadExtensionEnabled() const {
+  return audio_write_duration_player_get_audio_configuration_ != nullptr;
+}
+
+bool DefaultSbPlayerInterface::GetAudioConfiguration(
+    SbPlayer player, int index,
+    CobaltExtensionMediaAudioConfiguration* out_audio_configuration) {
+  DCHECK(IsAudioWriteAheadExtensionEnabled());
+  return audio_write_duration_player_get_audio_configuration_(
+      player, index, out_audio_configuration);
+}
+
 }  // namespace media
 }  // namespace cobalt
diff --git a/cobalt/media/base/sbplayer_interface.h b/cobalt/media/base/sbplayer_interface.h
index e79c1e6..751ff11 100644
--- a/cobalt/media/base/sbplayer_interface.h
+++ b/cobalt/media/base/sbplayer_interface.h
@@ -15,6 +15,7 @@
 #ifndef COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
 #define COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
 
+#include "cobalt/extension/audio_write_ahead.h"
 #include "cobalt/media/base/cval_stats.h"
 #include "starboard/player.h"
 
@@ -68,6 +69,11 @@
       SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) = 0;
 #endif  // SB_HAS(PLAYER_WITH_URL)
 
+  virtual bool IsAudioWriteAheadExtensionEnabled() const = 0;
+  virtual bool GetAudioConfiguration(
+      SbPlayer player, int index,
+      CobaltExtensionMediaAudioConfiguration* out_audio_configuration) = 0;
+
   // disabled by default, but can be enabled via h5vcc setting.
   void EnableCValStats(bool should_enable) {
     cval_stats_.Enable(should_enable);
@@ -77,6 +83,8 @@
 
 class DefaultSbPlayerInterface final : public SbPlayerInterface {
  public:
+  DefaultSbPlayerInterface();
+
   SbPlayer Create(
       SbWindow window, const SbPlayerCreationParam* creation_param,
       SbPlayerDeallocateSampleFunc sample_deallocate_func,
@@ -113,6 +121,17 @@
   void GetUrlPlayerExtraInfo(
       SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) override;
 #endif  // SB_HAS(PLAYER_WITH_URL)
+
+  bool IsAudioWriteAheadExtensionEnabled() const override;
+  bool GetAudioConfiguration(
+      SbPlayer player, int index,
+      CobaltExtensionMediaAudioConfiguration* out_audio_configuration) override;
+
+ private:
+  bool (*audio_write_duration_player_get_audio_configuration_)(
+      SbPlayer player, int index,
+      CobaltExtensionMediaAudioConfiguration* out_audio_configuration) =
+      nullptr;
 };
 
 
diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc
index 261af02..e14d0d0 100644
--- a/cobalt/media/base/sbplayer_pipeline.cc
+++ b/cobalt/media/base/sbplayer_pipeline.cc
@@ -30,12 +30,14 @@
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/c_val.h"
 #include "cobalt/base/startup_timer.h"
+#include "cobalt/extension/audio_write_ahead.h"
 #include "cobalt/math/size.h"
 #include "cobalt/media/base/media_export.h"
 #include "cobalt/media/base/pipeline.h"
 #include "cobalt/media/base/playback_statistics.h"
 #include "cobalt/media/base/sbplayer_bridge.h"
 #include "cobalt/media/base/sbplayer_set_bounds_helper.h"
+#include "starboard/common/media.h"
 #include "starboard/common/string.h"
 #include "starboard/configuration_constants.h"
 #include "starboard/time.h"
@@ -66,6 +68,7 @@
 using ::media::PipelineStatistics;
 using ::media::PipelineStatusCallback;
 using ::media::VideoDecoderConfig;
+using ::starboard::GetCobaltExtensionMediaAudioConnectorName;
 
 static const int kRetryDelayAtSuspendInMilliseconds = 100;
 
@@ -90,6 +93,35 @@
 #endif  // SB_HAS(PLAYER_WITH_URL)
 };
 
+bool HasRemoteAudioOutputs(
+    const std::vector<CobaltExtensionMediaAudioConfiguration>& configurations) {
+  for (auto&& configuration : configurations) {
+    const auto connector = configuration.connector;
+    switch (connector) {
+      case kCobaltExtensionMediaAudioConnectorUnknown:
+      case kCobaltExtensionMediaAudioConnectorAnalog:
+      case kCobaltExtensionMediaAudioConnectorBuiltIn:
+      case kCobaltExtensionMediaAudioConnectorHdmi:
+      case kCobaltExtensionMediaAudioConnectorSpdif:
+      case kCobaltExtensionMediaAudioConnectorUsb:
+        LOG(INFO) << "Encountered local audio connector: "
+                  << GetCobaltExtensionMediaAudioConnectorName(connector);
+        break;
+      case kCobaltExtensionMediaAudioConnectorBluetooth:
+      case kCobaltExtensionMediaAudioConnectorRemoteWired:
+      case kCobaltExtensionMediaAudioConnectorRemoteWireless:
+      case kCobaltExtensionMediaAudioConnectorRemoteOther:
+        LOG(INFO) << "Encountered remote audio connector: "
+                  << GetCobaltExtensionMediaAudioConnectorName(connector);
+        return true;
+    }
+  }
+
+  LOG(INFO) << "No remote audio outputs found.";
+
+  return false;
+}
+
 // SbPlayerPipeline is a PipelineBase implementation that uses the SbPlayer
 // interface internally.
 class MEDIA_EXPORT SbPlayerPipeline : public Pipeline,
@@ -103,6 +135,7 @@
       const GetDecodeTargetGraphicsContextProviderFunc&
           get_decode_target_graphics_context_provider_func,
       bool allow_resume_after_suspend, bool allow_batched_sample_write,
+      SbTime audio_write_duration_local, SbTime audio_write_duration_remote,
       MediaLog* media_log, DecodeTargetProvider* decode_target_provider);
   ~SbPlayerPipeline() override;
 
@@ -148,6 +181,7 @@
   TimeDelta GetMediaStartDate() const override;
 #endif  // SB_HAS(PLAYER_WITH_URL)
   void GetNaturalVideoSize(gfx::Size* out_size) const override;
+  std::vector<std::string> GetAudioConnectors() const override;
 
   bool DidLoadingProgress() const override;
   PipelineStatistics GetStatistics() const override;
@@ -322,19 +356,30 @@
 
   DecodeTargetProvider* decode_target_provider_;
 
+  // The following two variables are used only when the Configurable Audio Write
+  // Ahead extension is enabled.
+  SbTime audio_write_duration_local_;
+  SbTime audio_write_duration_remote_;
+
+  // When the Configurable Audio Write Ahead extension is enabled,
+  // the two variables below should always contain the same value.
+  //
   // Read audio from the stream if |timestamp_of_last_written_audio_| is less
-  // than |seek_time_| + |kAudioPrerollLimit|, this effectively allows 10
-  // seconds of audio to be written to the SbPlayer after playback startup or
-  // seek.
-  static const SbTime kAudioPrerollLimit = 10 * kSbTimeSecond;
-  // Don't read audio from the stream more than |kAudioLimit| ahead of the
-  // current media time during playing.
-  static const SbTime kAudioLimit = kSbTimeSecond;
+  // than |seek_time_| + |audio_write_duration_for_preroll_|. When the
+  // Configurable Audio Write Ahead extension is disabled, this effectively
+  // allows 10 seconds of audio to be written to the SbPlayer after playback
+  // startup or seek.
+  SbTime audio_write_duration_ = kSbTimeSecond;
+  // Don't read audio from the stream more than |audio_write_duration_| ahead of
+  // the current media time during playing.
+  SbTime audio_write_duration_for_preroll_ = 10 * kSbTimeSecond;
+
   // Only call GetMediaTime() from OnNeedData if it has been
   // |kMediaTimeCheckInterval| since the last call to GetMediaTime().
   static const SbTime kMediaTimeCheckInterval = 0.1 * kSbTimeSecond;
   // Timestamp for the last written audio.
   SbTime timestamp_of_last_written_audio_ = 0;
+
   // Last media time reported by GetMediaTime().
   base::CVal<SbTime> last_media_time_;
   // Time when we last checked the media time.
@@ -357,6 +402,7 @@
     const GetDecodeTargetGraphicsContextProviderFunc&
         get_decode_target_graphics_context_provider_func,
     bool allow_resume_after_suspend, bool allow_batched_sample_write,
+    SbTime audio_write_duration_local, SbTime audio_write_duration_remote,
     MediaLog* media_log, DecodeTargetProvider* decode_target_provider)
     : pipeline_identifier_(
           base::StringPrintf("%X", g_pipeline_identifier_counter++)),
@@ -406,7 +452,16 @@
                              pipeline_identifier_.c_str()),
           "", "The max video capabilities required for the media pipeline."),
       playback_statistics_(pipeline_identifier_) {
-  SbMediaSetAudioWriteDuration(kAudioLimit);
+  SbMediaSetAudioWriteDuration(audio_write_duration_);
+  DCHECK(sbplayer_interface_);
+  if (sbplayer_interface_->IsAudioWriteAheadExtensionEnabled()) {
+    audio_write_duration_local_ = audio_write_duration_local;
+    audio_write_duration_remote_ = audio_write_duration_remote;
+  } else {
+    LOG(INFO) << "Setting audio write duration to " << audio_write_duration_
+              << ", the duration during preroll is "
+              << audio_write_duration_for_preroll_;
+  }
 }
 
 SbPlayerPipeline::~SbPlayerPipeline() { DCHECK(!player_bridge_); }
@@ -571,6 +626,8 @@
   if (demuxer_) {
     stop_cb_ = stop_cb;
     demuxer_->Stop();
+    video_stream_ = nullptr;
+    audio_stream_ = nullptr;
     OnDemuxerStopped();
   } else {
     stop_cb.Run();
@@ -780,6 +837,27 @@
   *out_size = natural_size_;
 }
 
+std::vector<std::string> SbPlayerPipeline::GetAudioConnectors() const {
+  if (sbplayer_interface_->IsAudioWriteAheadExtensionEnabled()) {
+    base::AutoLock auto_lock(lock_);
+    if (!player_bridge_) {
+      return std::vector<std::string>();
+    }
+
+    std::vector<std::string> connectors;
+
+    auto configurations = player_bridge_->GetAudioConfigurations();
+    for (auto&& configuration : configurations) {
+      connectors.push_back(
+          GetCobaltExtensionMediaAudioConnectorName(configuration.connector));
+    }
+
+    return connectors;
+  }
+
+  return std::vector<std::string>();
+}
+
 bool SbPlayerPipeline::DidLoadingProgress() const {
   base::AutoLock auto_lock(lock_);
   bool ret = did_loading_progress_;
@@ -1035,6 +1113,18 @@
         *decode_to_texture_output_mode_, decode_target_provider_,
         max_video_capabilities_, pipeline_identifier_));
     if (player_bridge_->IsValid()) {
+      if (sbplayer_interface_->IsAudioWriteAheadExtensionEnabled()) {
+        // TODO(b/267678497): When `player_bridge_->GetAudioConfigurations()`
+        // returns no audio configurations, update the write durations again
+        // before the SbPlayer reaches `kSbPlayerStatePresenting`.
+        audio_write_duration_for_preroll_ = audio_write_duration_ =
+            HasRemoteAudioOutputs(player_bridge_->GetAudioConfigurations())
+                ? audio_write_duration_remote_
+                : audio_write_duration_local_;
+        LOG(INFO) << "SbPlayerBridge created, with audio write duration at "
+                  << audio_write_duration_for_preroll_;
+      }
+
       SetPlaybackRateTask(playback_rate_);
       SetVolumeTask(volume_);
     } else {
@@ -1268,17 +1358,18 @@
       GetMediaTime();
     }
 
-    // Delay reading audio more than |kAudioLimit| ahead of playback after the
-    // player has received enough audio for preroll, taking into account that
-    // our estimate of playback time might be behind by
+    // Delay reading audio more than |audio_write_duration_| ahead of playback
+    // after the player has received enough audio for preroll, taking into
+    // account that our estimate of playback time might be behind by
     // |kMediaTimeCheckInterval|.
     if (timestamp_of_last_written_audio_ - seek_time_.ToSbTime() >
-        kAudioPrerollLimit) {
+        audio_write_duration_for_preroll_) {
       // The estimated time ahead of playback may be negative if no audio has
       // been written.
       SbTime time_ahead_of_playback =
           timestamp_of_last_written_audio_ - last_media_time_;
-      if (time_ahead_of_playback > (kAudioLimit + kMediaTimeCheckInterval)) {
+      if (time_ahead_of_playback >
+          (audio_write_duration_ + kMediaTimeCheckInterval)) {
         task_runner_->PostDelayedTask(
             FROM_HERE,
             base::Bind(&SbPlayerPipeline::DelayedNeedData, this, max_buffers),
@@ -1348,6 +1439,15 @@
         playback_statistics_.OnPresenting(
             video_stream_->video_decoder_config());
       }
+      if (sbplayer_interface_->IsAudioWriteAheadExtensionEnabled()) {
+        audio_write_duration_for_preroll_ = audio_write_duration_ =
+            HasRemoteAudioOutputs(player_bridge_->GetAudioConfigurations())
+                ? audio_write_duration_remote_
+                : audio_write_duration_local_;
+        LOG(INFO)
+            << "SbPlayerBridge reaches kSbPlayerStatePresenting, with audio"
+            << " write duration at " << audio_write_duration_;
+      }
       break;
     }
     case kSbPlayerStateEndOfStream:
@@ -1535,6 +1635,8 @@
 
 std::string SbPlayerPipeline::AppendStatisticsString(
     const std::string& message) const {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
   if (nullptr == video_stream_) {
     return message + ", playback statistics: n/a.";
   } else {
@@ -1602,12 +1704,14 @@
     const GetDecodeTargetGraphicsContextProviderFunc&
         get_decode_target_graphics_context_provider_func,
     bool allow_resume_after_suspend, bool allow_batched_sample_write,
+    SbTime audio_write_duration_local, SbTime audio_write_duration_remote,
     MediaLog* media_log, DecodeTargetProvider* decode_target_provider) {
-  return new SbPlayerPipeline(interface, window, task_runner,
-                              get_decode_target_graphics_context_provider_func,
-                              allow_resume_after_suspend,
-                              allow_batched_sample_write, media_log,
-                              decode_target_provider);
+  return new SbPlayerPipeline(
+      interface, window, task_runner,
+      get_decode_target_graphics_context_provider_func,
+      allow_resume_after_suspend, allow_batched_sample_write,
+      audio_write_duration_local, audio_write_duration_remote, media_log,
+      decode_target_provider);
 }
 
 }  // namespace media
diff --git a/cobalt/media/media_module.cc b/cobalt/media/media_module.cc
index 95965a5..aedc5c6 100644
--- a/cobalt/media/media_module.cc
+++ b/cobalt/media/media_module.cc
@@ -194,6 +194,18 @@
     LOG(INFO) << (value ? "Enabling" : "Disabling")
               << " media metrics collection.";
     return true;
+  } else if (sbplayer_interface_->IsAudioWriteAheadExtensionEnabled()) {
+    if (name == "AudioWriteDurationLocal" && value > 0) {
+      audio_write_duration_local_ = value;
+      LOG(INFO) << "Set AudioWriteDurationLocal to "
+                << audio_write_duration_local_;
+      return true;
+    } else if (name == "AudioWriteDurationRemote" && value > 0) {
+      audio_write_duration_remote_ = value;
+      LOG(INFO) << "Set AudioWriteDurationRemote to "
+                << audio_write_duration_remote_;
+      return true;
+    }
   }
   return false;
 }
@@ -206,12 +218,21 @@
     window = system_window_->GetSbWindow();
   }
 
+  SbTime audio_write_duration_local = -1;
+  SbTime audio_write_duration_remote = -1;
+
+  if (sbplayer_interface_->IsAudioWriteAheadExtensionEnabled()) {
+    audio_write_duration_local = audio_write_duration_local_;
+    audio_write_duration_remote = audio_write_duration_remote_;
+  }
+
   return std::unique_ptr<WebMediaPlayer>(new media::WebMediaPlayerImpl(
       sbplayer_interface_.get(), window,
       base::Bind(&MediaModule::GetSbDecodeTargetGraphicsContextProvider,
                  base::Unretained(this)),
       client, this, options_.allow_resume_after_suspend,
-      allow_batched_sample_write_, &media_log_));
+      allow_batched_sample_write_, audio_write_duration_local,
+      audio_write_duration_remote, &media_log_));
 }
 
 void MediaModule::Suspend() {
diff --git a/cobalt/media/media_module.h b/cobalt/media/media_module.h
index 6ca47b8..f4c2594 100644
--- a/cobalt/media/media_module.h
+++ b/cobalt/media/media_module.h
@@ -24,6 +24,7 @@
 #include "base/logging.h"
 #include "base/memory/ref_counted.h"
 #include "base/optional.h"
+#include "cobalt/extension/audio_write_ahead.h"
 #include "cobalt/math/size.h"
 #include "cobalt/media/base/sbplayer_interface.h"
 #include "cobalt/media/can_play_type_handler.h"
@@ -123,6 +124,12 @@
 
   bool allow_batched_sample_write_ = false;
 
+  // The following two variables are used only when the Configurable Audio Write
+  // Ahead extension is enabled.
+  SbTime audio_write_duration_local_ = kCobaltExtensionPlayerWriteDurationLocal;
+  SbTime audio_write_duration_remote_ =
+      kCobaltExtensionPlayerWriteDurationRemote;
+
   DecoderBufferAllocator decoder_buffer_allocator_;
 };
 
diff --git a/cobalt/media/player/web_media_player.h b/cobalt/media/player/web_media_player.h
index 1322473..459ec6e 100644
--- a/cobalt/media/player/web_media_player.h
+++ b/cobalt/media/player/web_media_player.h
@@ -130,6 +130,9 @@
   virtual int GetNaturalWidth() const = 0;
   virtual int GetNaturalHeight() const = 0;
 
+  // Names of audio connectors used by the playback.
+  virtual std::vector<std::string> GetAudioConnectors() const = 0;
+
   // Getters of playback state.
   virtual bool IsPaused() const = 0;
   virtual bool IsSeeking() const = 0;
diff --git a/cobalt/media/player/web_media_player_impl.cc b/cobalt/media/player/web_media_player_impl.cc
index 8073366..8f9ba9a 100644
--- a/cobalt/media/player/web_media_player_impl.cc
+++ b/cobalt/media/player/web_media_player_impl.cc
@@ -118,6 +118,7 @@
         get_decode_target_graphics_context_provider_func,
     WebMediaPlayerClient* client, WebMediaPlayerDelegate* delegate,
     bool allow_resume_after_suspend, bool allow_batched_sample_write,
+    SbTime audio_write_duration_local, SbTime audio_write_duration_remote,
     ::media::MediaLog* const media_log)
     : pipeline_thread_("media_pipeline"),
       network_state_(WebMediaPlayer::kNetworkStateEmpty),
@@ -146,6 +147,7 @@
       Pipeline::Create(interface, window, pipeline_thread_.task_runner(),
                        get_decode_target_graphics_context_provider_func,
                        allow_resume_after_suspend_, allow_batched_sample_write_,
+                       audio_write_duration_local, audio_write_duration_remote,
                        media_log_, decode_target_provider_.get());
 
   // Also we want to be notified of |main_loop_| destruction.
@@ -428,6 +430,11 @@
   return size.height();
 }
 
+std::vector<std::string> WebMediaPlayerImpl::GetAudioConnectors() const {
+  DCHECK_EQ(main_loop_, base::MessageLoop::current());
+  return pipeline_->GetAudioConnectors();
+}
+
 bool WebMediaPlayerImpl::IsPaused() const {
   DCHECK_EQ(main_loop_, base::MessageLoop::current());
 
diff --git a/cobalt/media/player/web_media_player_impl.h b/cobalt/media/player/web_media_player_impl.h
index b64f318..9139ce8 100644
--- a/cobalt/media/player/web_media_player_impl.h
+++ b/cobalt/media/player/web_media_player_impl.h
@@ -110,6 +110,8 @@
                      WebMediaPlayerDelegate* delegate,
                      bool allow_resume_after_suspend,
                      bool allow_batched_sample_write,
+                     SbTime audio_write_duration_local,
+                     SbTime audio_write_duration_remote,
                      ::media::MediaLog* const media_log);
   ~WebMediaPlayerImpl() override;
 
@@ -146,6 +148,9 @@
   int GetNaturalWidth() const override;
   int GetNaturalHeight() const override;
 
+  // Names of audio connectors used by the playback.
+  std::vector<std::string> GetAudioConnectors() const override;
+
   // Getters of playback state.
   bool IsPaused() const override;
   bool IsSeeking() const override;
diff --git a/cobalt/persistent_storage/persistent_settings.cc b/cobalt/persistent_storage/persistent_settings.cc
index c4db368..c93ff54 100644
--- a/cobalt/persistent_storage/persistent_settings.cc
+++ b/cobalt/persistent_storage/persistent_settings.cc
@@ -201,7 +201,7 @@
 
 void PersistentSettings::SetPersistentSetting(
     const std::string& key, std::unique_ptr<base::Value> value,
-    base::OnceClosure closure) {
+    base::OnceClosure closure, bool blocking) {
   if (key == kValidated) {
     LOG(ERROR) << "Cannot set protected persistent setting: " << key;
     return;
@@ -209,12 +209,12 @@
   message_loop()->task_runner()->PostTask(
       FROM_HERE, base::BindOnce(&PersistentSettings::SetPersistentSettingHelper,
                                 base::Unretained(this), key, std::move(value),
-                                std::move(closure)));
+                                std::move(closure), blocking));
 }
 
 void PersistentSettings::SetPersistentSettingHelper(
     const std::string& key, std::unique_ptr<base::Value> value,
-    base::OnceClosure closure) {
+    base::OnceClosure closure, bool blocking) {
   DCHECK_EQ(base::MessageLoop::current(), message_loop());
   if (validated_initial_settings_) {
     base::AutoLock auto_lock(pref_store_lock_);
@@ -223,7 +223,7 @@
         WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
     writeable_pref_store()->SetValue(
         key, std::move(value), WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    writeable_pref_store()->CommitPendingWrite();
+    commit_pending_write(blocking);
   } else {
     LOG(ERROR) << "Cannot set persistent setting while unvalidated: " << key;
   }
@@ -231,15 +231,17 @@
 }
 
 void PersistentSettings::RemovePersistentSetting(const std::string& key,
-                                                 base::OnceClosure closure) {
+                                                 base::OnceClosure closure,
+                                                 bool blocking) {
   message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(&PersistentSettings::RemovePersistentSettingHelper,
-                     base::Unretained(this), key, std::move(closure)));
+                     base::Unretained(this), key, std::move(closure),
+                     blocking));
 }
 
 void PersistentSettings::RemovePersistentSettingHelper(
-    const std::string& key, base::OnceClosure closure) {
+    const std::string& key, base::OnceClosure closure, bool blocking) {
   DCHECK_EQ(base::MessageLoop::current(), message_loop());
   if (validated_initial_settings_) {
     base::AutoLock auto_lock(pref_store_lock_);
@@ -248,7 +250,7 @@
         WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
     writeable_pref_store()->RemoveValue(
         key, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    writeable_pref_store()->CommitPendingWrite();
+    commit_pending_write(blocking);
   } else {
     LOG(ERROR) << "Cannot remove persistent setting while unvalidated: " << key;
   }
diff --git a/cobalt/persistent_storage/persistent_settings.h b/cobalt/persistent_storage/persistent_settings.h
index 0cf789f..ff3ea58 100644
--- a/cobalt/persistent_storage/persistent_settings.h
+++ b/cobalt/persistent_storage/persistent_settings.h
@@ -71,11 +71,13 @@
 
   void SetPersistentSetting(
       const std::string& key, std::unique_ptr<base::Value> value,
-      base::OnceClosure closure = std::move(base::DoNothing()));
+      base::OnceClosure closure = std::move(base::DoNothing()),
+      bool blocking = false);
 
   void RemovePersistentSetting(
       const std::string& key,
-      base::OnceClosure closure = std::move(base::DoNothing()));
+      base::OnceClosure closure = std::move(base::DoNothing()),
+      bool blocking = false);
 
   void DeletePersistentSettings(
       base::OnceClosure closure = std::move(base::DoNothing()));
@@ -89,10 +91,10 @@
 
   void SetPersistentSettingHelper(const std::string& key,
                                   std::unique_ptr<base::Value> value,
-                                  base::OnceClosure closure);
+                                  base::OnceClosure closure, bool blocking);
 
   void RemovePersistentSettingHelper(const std::string& key,
-                                     base::OnceClosure closure);
+                                     base::OnceClosure closure, bool blocking);
 
   void DeletePersistentSettingsHelper(base::OnceClosure closure);
 
@@ -101,6 +103,18 @@
     return pref_store_;
   }
 
+  void commit_pending_write(bool blocking) {
+    if (blocking) {
+      base::WaitableEvent written;
+      writeable_pref_store()->CommitPendingWrite(
+          base::OnceClosure(),
+          base::BindOnce(&base::WaitableEvent::Signal, Unretained(&written)));
+      written.Wait();
+    } else {
+      writeable_pref_store()->CommitPendingWrite();
+    }
+  }
+
   // Persistent settings file path.
   std::string persistent_settings_file_;
 
diff --git a/cobalt/persistent_storage/persistent_settings_test.cc b/cobalt/persistent_storage/persistent_settings_test.cc
index 3f88397..91b2bce 100644
--- a/cobalt/persistent_storage/persistent_settings_test.cc
+++ b/cobalt/persistent_storage/persistent_settings_test.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "cobalt/persistent_storage/persistent_settings.h"
+
 #include <utility>
 #include <vector>
 
@@ -20,9 +22,7 @@
 #include "base/callback_forward.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/test/scoped_task_environment.h"
-#include "base/threading/platform_thread.h"
 #include "base/values.h"
-#include "cobalt/persistent_storage/persistent_settings.h"
 #include "starboard/common/file.h"
 #include "starboard/common/log.h"
 #include "starboard/configuration_constants.h"
@@ -81,13 +81,13 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsInt("key", true));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(4.2), std::move(closure));
+      "key", std::make_unique<base::Value>(4.2), std::move(closure), true);
   test_done_.Wait();
 }
 
@@ -99,32 +99,32 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true), std::move(closure));
+      "key", std::make_unique<base::Value>(true), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_FALSE(
+        EXPECT_FALSE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_FALSE(
+        EXPECT_FALSE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(false), std::move(closure));
+      "key", std::make_unique<base::Value>(false), std::move(closure), true);
 
   test_done_.Wait();
 }
@@ -143,13 +143,13 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ(8, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        EXPECT_EQ(8, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(4.2), std::move(closure));
+      "key", std::make_unique<base::Value>(4.2), std::move(closure), true);
   test_done_.Wait();
 }
 
@@ -161,36 +161,36 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ(-1, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        EXPECT_EQ(-1, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(-1), std::move(closure));
+      "key", std::make_unique<base::Value>(-1), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ(0, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        EXPECT_EQ(0, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(0), std::move(closure));
+      "key", std::make_unique<base::Value>(0), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        EXPECT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 8));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(42), std::move(closure));
+      "key", std::make_unique<base::Value>(42), std::move(closure), true);
   test_done_.Wait();
 }
 
@@ -214,13 +214,13 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ("hello", persistent_settings->GetPersistentSettingAsString(
+        EXPECT_EQ("hello", persistent_settings->GetPersistentSettingAsString(
                                "key", "hello"));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(4.2), std::move(closure));
+      "key", std::make_unique<base::Value>(4.2), std::move(closure), true);
   test_done_.Wait();
 }
 
@@ -232,21 +232,21 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ("", persistent_settings->GetPersistentSettingAsString(
+        EXPECT_EQ("", persistent_settings->GetPersistentSettingAsString(
                           "key", "hello"));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(""), std::move(closure));
+      "key", std::make_unique<base::Value>(""), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ(
+        EXPECT_EQ(
             "hello there",
             persistent_settings->GetPersistentSettingAsString("key", "hello"));
         test_done->Signal();
@@ -254,46 +254,47 @@
       persistent_settings.get(), &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("hello there"), std::move(closure));
+      "key", std::make_unique<base::Value>("hello there"), std::move(closure),
+      true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ("42", persistent_settings->GetPersistentSettingAsString(
+        EXPECT_EQ("42", persistent_settings->GetPersistentSettingAsString(
                             "key", "hello"));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("42"), std::move(closure));
+      "key", std::make_unique<base::Value>("42"), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ("\n", persistent_settings->GetPersistentSettingAsString(
+        EXPECT_EQ("\n", persistent_settings->GetPersistentSettingAsString(
                             "key", "hello"));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("\n"), std::move(closure));
+      "key", std::make_unique<base::Value>("\n"), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_EQ("\\n", persistent_settings->GetPersistentSettingAsString(
+        EXPECT_EQ("\\n", persistent_settings->GetPersistentSettingAsString(
                              "key", "hello"));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("\\n"), std::move(closure));
+      "key", std::make_unique<base::Value>("\\n"), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 }
@@ -307,10 +308,10 @@
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
         auto test_list = persistent_settings->GetPersistentSettingAsList("key");
-        ASSERT_FALSE(test_list.empty());
-        ASSERT_EQ(1, test_list.size());
-        ASSERT_TRUE(test_list[0].is_string());
-        ASSERT_EQ("hello", test_list[0].GetString());
+        EXPECT_FALSE(test_list.empty());
+        EXPECT_EQ(1, test_list.size());
+        EXPECT_TRUE(test_list[0].is_string());
+        EXPECT_EQ("hello", test_list[0].GetString());
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
@@ -318,7 +319,7 @@
   std::vector<base::Value> list;
   list.emplace_back("hello");
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(list), std::move(closure));
+      "key", std::make_unique<base::Value>(list), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
@@ -326,19 +327,19 @@
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
         auto test_list = persistent_settings->GetPersistentSettingAsList("key");
-        ASSERT_FALSE(test_list.empty());
-        ASSERT_EQ(2, test_list.size());
-        ASSERT_TRUE(test_list[0].is_string());
-        ASSERT_EQ("hello", test_list[0].GetString());
-        ASSERT_TRUE(test_list[1].is_string());
-        ASSERT_EQ("there", test_list[1].GetString());
+        EXPECT_FALSE(test_list.empty());
+        EXPECT_EQ(2, test_list.size());
+        EXPECT_TRUE(test_list[0].is_string());
+        EXPECT_EQ("hello", test_list[0].GetString());
+        EXPECT_TRUE(test_list[1].is_string());
+        EXPECT_EQ("there", test_list[1].GetString());
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   list.emplace_back("there");
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(list), std::move(closure));
+      "key", std::make_unique<base::Value>(list), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
@@ -346,21 +347,21 @@
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
         auto test_list = persistent_settings->GetPersistentSettingAsList("key");
-        ASSERT_FALSE(test_list.empty());
-        ASSERT_EQ(3, test_list.size());
-        ASSERT_TRUE(test_list[0].is_string());
-        ASSERT_EQ("hello", test_list[0].GetString());
-        ASSERT_TRUE(test_list[1].is_string());
-        ASSERT_EQ("there", test_list[1].GetString());
-        ASSERT_TRUE(test_list[2].is_int());
-        ASSERT_EQ(42, test_list[2].GetInt());
+        EXPECT_FALSE(test_list.empty());
+        EXPECT_EQ(3, test_list.size());
+        EXPECT_TRUE(test_list[0].is_string());
+        EXPECT_EQ("hello", test_list[0].GetString());
+        EXPECT_TRUE(test_list[1].is_string());
+        EXPECT_EQ("there", test_list[1].GetString());
+        EXPECT_TRUE(test_list[2].is_int());
+        EXPECT_EQ(42, test_list[2].GetInt());
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   list.emplace_back(42);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(list), std::move(closure));
+      "key", std::make_unique<base::Value>(list), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 }
@@ -375,10 +376,10 @@
          base::WaitableEvent* test_done) {
         auto test_dict =
             persistent_settings->GetPersistentSettingAsDictionary("key");
-        ASSERT_FALSE(test_dict.empty());
-        ASSERT_EQ(1, test_dict.size());
-        ASSERT_TRUE(test_dict["key_string"]->is_string());
-        ASSERT_EQ("hello", test_dict["key_string"]->GetString());
+        EXPECT_FALSE(test_dict.empty());
+        EXPECT_EQ(1, test_dict.size());
+        EXPECT_TRUE(test_dict["key_string"]->is_string());
+        EXPECT_EQ("hello", test_dict["key_string"]->GetString());
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
@@ -386,7 +387,7 @@
   base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
   dict.try_emplace("key_string", std::make_unique<base::Value>("hello"));
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(dict), std::move(closure));
+      "key", std::make_unique<base::Value>(dict), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
@@ -395,19 +396,19 @@
          base::WaitableEvent* test_done) {
         auto test_dict =
             persistent_settings->GetPersistentSettingAsDictionary("key");
-        ASSERT_FALSE(test_dict.empty());
-        ASSERT_EQ(2, test_dict.size());
-        ASSERT_TRUE(test_dict["key_string"]->is_string());
-        ASSERT_EQ("hello", test_dict["key_string"]->GetString());
-        ASSERT_TRUE(test_dict["key_int"]->is_int());
-        ASSERT_EQ(42, test_dict["key_int"]->GetInt());
+        EXPECT_FALSE(test_dict.empty());
+        EXPECT_EQ(2, test_dict.size());
+        EXPECT_TRUE(test_dict["key_string"]->is_string());
+        EXPECT_EQ("hello", test_dict["key_string"]->GetString());
+        EXPECT_TRUE(test_dict["key_int"]->is_int());
+        EXPECT_EQ(42, test_dict["key_int"]->GetInt());
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
 
   dict.try_emplace("key_int", std::make_unique<base::Value>(42));
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(dict), std::move(closure));
+      "key", std::make_unique<base::Value>(dict), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 }
@@ -425,10 +426,10 @@
          base::WaitableEvent* test_done) {
         auto test_dict = persistent_settings->GetPersistentSettingAsDictionary(
             "http://127.0.0.1:45019/");
-        ASSERT_FALSE(test_dict.empty());
-        ASSERT_EQ(1, test_dict.size());
-        ASSERT_TRUE(test_dict["http://127.0.0.1:45019/"]->is_string());
-        ASSERT_EQ("Dictionary URL Key Works!",
+        EXPECT_FALSE(test_dict.empty());
+        EXPECT_EQ(1, test_dict.size());
+        EXPECT_TRUE(test_dict["http://127.0.0.1:45019/"]->is_string());
+        EXPECT_EQ("Dictionary URL Key Works!",
                   test_dict["http://127.0.0.1:45019/"]->GetString());
         test_done->Signal();
       },
@@ -441,7 +442,7 @@
                    std::make_unique<base::Value>("Dictionary URL Key Works!"));
   persistent_settings->SetPersistentSetting("http://127.0.0.1:45019/",
                                             std::make_unique<base::Value>(dict),
-                                            std::move(closure));
+                                            std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
@@ -450,12 +451,12 @@
          base::WaitableEvent* test_done) {
         auto test_dict = persistent_settings->GetPersistentSettingAsDictionary(
             "http://127.0.0.1:45019/");
-        ASSERT_TRUE(test_dict.empty());
+        EXPECT_TRUE(test_dict.empty());
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->RemovePersistentSetting("http://127.0.0.1:45019/",
-                                               std::move(closure));
+                                               std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 }
@@ -471,29 +472,29 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true), std::move(closure));
+      "key", std::make_unique<base::Value>(true), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_FALSE(
+        EXPECT_FALSE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
-  persistent_settings->RemovePersistentSetting("key", std::move(closure));
+  persistent_settings->RemovePersistentSetting("key", std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 }
@@ -509,24 +510,24 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true), std::move(closure));
+      "key", std::make_unique<base::Value>(true), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_FALSE(
+        EXPECT_FALSE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
@@ -547,22 +548,18 @@
   base::OnceClosure closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true), std::move(closure));
+      "key", std::make_unique<base::Value>(true), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
-  // Sleep for one second to allow for the previous persistent_setting's
-  // JsonPrefStore instance time to write to disk before creating a new
-  // persistent_settings and JsonPrefStore instance.
-  base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
   persistent_settings =
       std::make_unique<PersistentSettings>(kPersistentSettingsJson);
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -571,15 +568,15 @@
   closure = base::BindOnce(
       [](PersistentSettings* persistent_settings,
          base::WaitableEvent* test_done) {
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", true));
-        ASSERT_TRUE(
+        EXPECT_TRUE(
             persistent_settings->GetPersistentSettingAsBool("key", false));
         test_done->Signal();
       },
       persistent_settings.get(), &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(false), std::move(closure));
+      "key", std::make_unique<base::Value>(false), std::move(closure), true);
   test_done_.Wait();
   test_done_.Reset();
 
diff --git a/cobalt/version.h b/cobalt/version.h
index 3182bc9..74f79af 100644
--- a/cobalt/version.h
+++ b/cobalt/version.h
@@ -35,6 +35,6 @@
 //                  release is cut.
 //.
 
-#define COBALT_VERSION "23.lts.5"
+#define COBALT_VERSION "23.lts.6"
 
 #endif  // COBALT_VERSION_H_
diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml
index e577ee4..c60c372 100644
--- a/docker-compose-windows.yml
+++ b/docker-compose-windows.yml
@@ -98,3 +98,14 @@
     depends_on:
       - cobalt-build-win32-base
     image: cobalt-build-win-win32
+
+  runner-win-win32:
+    <<: *common-definitions
+    build:
+      context: ./docker/windows/runner
+      dockerfile: ./Dockerfile
+      args:
+        - RUNNER_VERSION=2.294.0
+    depends_on:
+      - cobalt-build-win32-base
+    image: cobalt-runner-win-win32
diff --git a/docker/linux/base/build/Dockerfile b/docker/linux/base/build/Dockerfile
index 60533d6..26590dd 100644
--- a/docker/linux/base/build/Dockerfile
+++ b/docker/linux/base/build/Dockerfile
@@ -26,7 +26,6 @@
         ninja-build \
         pkgconf \
         unzip \
-        yasm \
     && /opt/clean-after-apt.sh
 
 # === Get Nodejs pinned LTS version via NVM
diff --git a/docker/windows/base/build/Dockerfile b/docker/windows/base/build/Dockerfile
index 19aa9b6..6e76594 100644
--- a/docker/windows/base/build/Dockerfile
+++ b/docker/windows/base/build/Dockerfile
@@ -29,10 +29,14 @@
 # of the execution, i.e. the full invocation string.
 COPY ./list_python_processes.py /list_python_processes.py
 
+# Pin Choco to 1.4.0 to avoid required reboot in 2.0.0
+ENV chocolateyVersion '1.4.0'
 # Install deps via chocolatey.
 RUN iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'));`
     mkdir C:\choco-cache;`
-    choco install -y -c C:\choco-cache python3 -ia '/quiet InstallAllUsers=1 PrependPath=1 TargetDir="C:\Python3"';`
+    # Note: We pinned python 3.11.0 version here because 3.11.2 has a regression
+    # where the install arguments like TargetDir were being ignored.
+    choco install -y -c C:\choco-cache python3 --version 3.11.0 -ia '/quiet InstallAllUsers=1 PrependPath=1 TargetDir="C:\Python3"';`
     choco install -y -c C:\choco-cache winflexbison3 --params '/InstallDir:C:\bison';`
     choco install -y -c C:\choco-cache ninja;`
     choco install -y -c C:\choco-cache nodejs-lts;`
diff --git a/docker/windows/runner/Dockerfile b/docker/windows/runner/Dockerfile
new file mode 100644
index 0000000..a494811
--- /dev/null
+++ b/docker/windows/runner/Dockerfile
@@ -0,0 +1,34 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+FROM cobalt-build-win32-base
+
+SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';$ProgressPreference='silentlyContinue';"]
+
+ARG RUNNER_VERSION
+
+RUN Invoke-WebRequest -Uri 'https://aka.ms/install-powershell.ps1' -OutFile install-powershell.ps1; \
+    powershell -ExecutionPolicy Unrestricted -File ./install-powershell.ps1 -AddToPath
+
+RUN Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v$env:RUNNER_VERSION/actions-runner-win-x64-$env:RUNNER_VERSION.zip -OutFile runner.zip; \
+    Expand-Archive -Path C:/runner.zip -DestinationPath C:/actions-runner; \
+    Remove-Item -Path C:\runner.zip; \
+    setx /M PATH $(${Env:PATH} + \";${Env:ProgramFiles}\Git\bin\")
+
+# Required for packaging artifacts.
+RUN choco install -f -y 7zip --version=19.0
+
+ENV TMP "C:\Users\ContainerAdministrator\AppData\Local\Temp2"
+
+ADD runner.ps1 C:/runner.ps1
+CMD ["pwsh", "-ExecutionPolicy", "Unrestricted", "-File", ".\\runner.ps1"]
diff --git a/docker/windows/runner/runner.ps1 b/docker/windows/runner/runner.ps1
new file mode 100644
index 0000000..9e96a5e
--- /dev/null
+++ b/docker/windows/runner/runner.ps1
@@ -0,0 +1,2 @@
+.\actions-runner\config.cmd --unattended --replace --url https://github.com/${env:RUNNER_REPO} --token $env:RUNNER_TOKEN --name $env:RUNNER_NAME --work $env:RUNNER_WORKDIR --labels $env:RUNNER_LABELS;
+.\actions-runner\run.cmd;
diff --git a/net/url_request/url_fetcher_core.cc b/net/url_request/url_fetcher_core.cc
index 0d77b78..6835662 100644
--- a/net/url_request/url_fetcher_core.cc
+++ b/net/url_request/url_fetcher_core.cc
@@ -175,8 +175,9 @@
 }
 
 void URLFetcherCore::Stop() {
-  if (delegate_task_runner_)  // May be NULL in tests.
+  if (delegate_task_runner_) {  // May be NULL in tests.
     DCHECK(delegate_task_runner_->RunsTasksInCurrentSequence());
+  }
 
   delegate_ = NULL;
   fetcher_ = NULL;
@@ -783,8 +784,11 @@
   if (!extra_request_headers_.IsEmpty())
     request_->SetExtraRequestHeaders(extra_request_headers_);
 
+#if defined(STARBOARD)
   request_->SetLoadTimingInfoCallback(base::Bind(&URLFetcherCore::GetLoadTimingInfo,
       base::Unretained(this)));
+#endif
+
   request_->Start();
 }
 
@@ -1133,6 +1137,14 @@
 #if defined(STARBOARD)
 void URLFetcherCore::GetLoadTimingInfo(
     const net::LoadTimingInfo& timing_info) {
+  delegate_task_runner_->PostTask(
+      FROM_HERE,
+      base::Bind(&URLFetcherCore::GetLoadTimingInfoInDelegateThread,
+                 this, timing_info));
+}
+
+void URLFetcherCore::GetLoadTimingInfoInDelegateThread(
+    const net::LoadTimingInfo& timing_info) {
   // Check if the URLFetcherCore has been stopped before.
   if (delegate_) {
     delegate_->ReportLoadTimingInfo(timing_info);
diff --git a/net/url_request/url_fetcher_core.h b/net/url_request/url_fetcher_core.h
index 02048d1..cfbd717 100644
--- a/net/url_request/url_fetcher_core.h
+++ b/net/url_request/url_fetcher_core.h
@@ -165,6 +165,7 @@
   static void SetIgnoreCertificateRequests(bool ignored);
 #if defined (STARBOARD)
   void GetLoadTimingInfo(const net::LoadTimingInfo& timing_info);
+  void GetLoadTimingInfoInDelegateThread(const net::LoadTimingInfo& timing_info);
 #endif  // defined(STARBOARD)
  private:
   friend class base::RefCountedThreadSafe<URLFetcherCore>;
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
index 1478b37..7efa465 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
@@ -498,6 +498,9 @@
                 return;
               }
               nativeOnMediaCodecOutputFormatChanged(mNativeMediaCodecBridge);
+              if (mFrameRateEstimator != null) {
+                mFrameRateEstimator.reset();
+              }
             }
           }
         };
diff --git a/starboard/common/media.cc b/starboard/common/media.cc
index eb74ac8..f9cdac0 100644
--- a/starboard/common/media.cc
+++ b/starboard/common/media.cc
@@ -639,6 +639,55 @@
   return "invalid";
 }
 
+const char* GetMediaAudioConnectorName(SbMediaAudioConnector connector) {
+  switch (connector) {
+    case kSbMediaAudioConnectorNone:
+      return "none";
+    case kSbMediaAudioConnectorAnalog:
+      return "analog";
+    case kSbMediaAudioConnectorBluetooth:
+      return "bluetooth";
+    case kSbMediaAudioConnectorHdmi:
+      return "hdmi";
+    case kSbMediaAudioConnectorNetwork:
+      return "network";
+    case kSbMediaAudioConnectorSpdif:
+      return "spdif";
+    case kSbMediaAudioConnectorUsb:
+      return "usb";
+  }
+  SB_NOTREACHED();
+  return "invalid";
+}
+
+const char* GetCobaltExtensionMediaAudioConnectorName(
+    CobaltExtensionMediaAudioConnector connector) {
+  switch (connector) {
+    case kCobaltExtensionMediaAudioConnectorUnknown:
+      return "unknown";
+    case kCobaltExtensionMediaAudioConnectorAnalog:
+      return "analog";
+    case kCobaltExtensionMediaAudioConnectorBluetooth:
+      return "bluetooth";
+    case kCobaltExtensionMediaAudioConnectorBuiltIn:
+      return "builtin";
+    case kCobaltExtensionMediaAudioConnectorHdmi:
+      return "hdmi";
+    case kCobaltExtensionMediaAudioConnectorRemoteWired:
+      return "remote-wired";
+    case kCobaltExtensionMediaAudioConnectorRemoteWireless:
+      return "remote-wireless";
+    case kCobaltExtensionMediaAudioConnectorRemoteOther:
+      return "remote-other";
+    case kCobaltExtensionMediaAudioConnectorSpdif:
+      return "spdif";
+    case kCobaltExtensionMediaAudioConnectorUsb:
+      return "usb";
+  }
+  SB_NOTREACHED();
+  return "invalid";
+}
+
 const char* GetMediaPrimaryIdName(SbMediaPrimaryId primary_id) {
   switch (primary_id) {
     case kSbMediaPrimaryIdReserved0:
diff --git a/starboard/common/media.h b/starboard/common/media.h
index a51641e..401551d 100644
--- a/starboard/common/media.h
+++ b/starboard/common/media.h
@@ -17,12 +17,16 @@
 
 #include <ostream>
 
+#include "cobalt/extension/audio_write_ahead.h"
 #include "starboard/media.h"
 
 namespace starboard {
 
 const char* GetMediaAudioCodecName(SbMediaAudioCodec codec);
 const char* GetMediaVideoCodecName(SbMediaVideoCodec codec);
+const char* GetMediaAudioConnectorName(SbMediaAudioConnector connector);
+const char* GetCobaltExtensionMediaAudioConnectorName(
+    CobaltExtensionMediaAudioConnector connector);
 const char* GetMediaPrimaryIdName(SbMediaPrimaryId primary_id);
 const char* GetMediaTransferIdName(SbMediaTransferId transfer_id);
 const char* GetMediaMatrixIdName(SbMediaMatrixId matrix_id);
diff --git a/starboard/doc/evergreen/cobalt_evergreen_overview.md b/starboard/doc/evergreen/cobalt_evergreen_overview.md
index cc21fa2..e3ce087 100644
--- a/starboard/doc/evergreen/cobalt_evergreen_overview.md
+++ b/starboard/doc/evergreen/cobalt_evergreen_overview.md
@@ -90,38 +90,43 @@
 
 ### Building Cobalt Evergreen Components
 
-Cobalt Evergreen requires that there are two separate build(`gyp`)
+Cobalt Evergreen requires that there are two separate build
 configurations used due to the separation of the Cobalt core(`libcobalt.so`) and
-the platform-specific Starboard layer(`loader_app`). As a result, you will have
-to initiate a separate gyp process for each. This is required since the Cobalt
-core binary is built with the Google toolchain settings and the
-platform-specific Starboard layer is built with partner toolchain
-configurations.
+the platform-specific layers. As a result, you will have to run `gen gn`
+separately for each. This is required since the Cobalt core binary is built
+with the Google toolchain settings and the components in the platform-specific
+Starboard layer are built with partner toolchain configurations.
 
-Cobalt Evergreen is built by a separate gyp platform using the Google toolchain:
+Cobalt Evergreen is built by a separate GN platform using the Google toolchain.
+For example:
 
 ```
-$ cobalt/build/gn.py -p evergreen-arm-softfp -c qa
-$ ninja -C out/evergreen-arm-softfp_qa cobalt
+$ gn gen out/evergreen-arm-softfp_qa --args="target_platform=\"evergreen-arm-softfp\" build_type=\"qa\" target_cpu=\"arm\" target_os=\"linux\" sb_api_version=14"
+$ ninja -C out/evergreen-arm-softfp_qa cobalt_install
 ```
 
 Which produces a shared library `libcobalt.so` targeted for specific
 architecture, ABI and Starboard version.
 
-The gyp variable `sb_evergreen` is set to 1 when building `libcobalt.so`.
+The GN variable `sb_is_evergreen` is set to `true` when building `libcobalt.so`.
+
+Finally, note that these instructions are for producing a local build of
+`libcobalt.so`. For submissions to YTS and deployments to users' devices, an
+official release build of `libcobalt.so` should be downloaded from GitHub.
 
 The partner port of Starboard is built with the partner’s toolchain and is
 linked into the `loader_app` which knows how to dynamically load
 `libcobalt.so`, and the `crashpad_handler` which handles crashes.
 
 ```
-$ cobalt/build/gn.py -p <partner_port_name> -c qa
+$ gn gen out/<partner_port_name>_qa --args='target_platform="<partner_port_name>" build_type="qa" sb_api_version=14'
 $ ninja -C out/<partner_port_name>_qa loader_app crashpad_handler
 ```
 
-Partners should set `sb_evergreen_compatible` to 1 in their gyp platform config.
-DO NOT set the `sb_evergreen` to 1 in your platform-specific configuration as it
-is used only by Cobalt when building with the Google toolchain.
+Partners should set `sb_is_evergreen_compatible` to `true` in their GN platform
+config. DO NOT set the `sb_is_evergreen` to `true` in your platform-specific
+configuration as it is used only by Cobalt when building with the Google
+toolchain.
 
 Additionally, partners should install crash handlers as instructed in the
 [Installing Crash Handlers for Cobalt guide](../crash_handlers.md).
diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn
index f097404..f313fde 100644
--- a/starboard/linux/shared/BUILD.gn
+++ b/starboard/linux/shared/BUILD.gn
@@ -56,6 +56,10 @@
   sources = [
     "//starboard/linux/shared/atomic_public.h",
     "//starboard/linux/shared/audio_sink_type_dispatcher.cc",
+    "//starboard/linux/shared/audio_write_ahead.cc",
+    "//starboard/linux/shared/audio_write_ahead.h",
+    "//starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.cc",
+    "//starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.h",
     "//starboard/linux/shared/configuration.cc",
     "//starboard/linux/shared/configuration.h",
     "//starboard/linux/shared/configuration_constants.cc",
@@ -64,6 +68,8 @@
     "//starboard/linux/shared/decode_target_internal.cc",
     "//starboard/linux/shared/decode_target_internal.h",
     "//starboard/linux/shared/decode_target_release.cc",
+    "//starboard/linux/shared/ifa.cc",
+    "//starboard/linux/shared/ifa.h",
     "//starboard/linux/shared/media_is_audio_supported.cc",
     "//starboard/linux/shared/media_is_video_supported.cc",
     "//starboard/linux/shared/netlink.cc",
diff --git a/starboard/linux/shared/audio_write_ahead.cc b/starboard/linux/shared/audio_write_ahead.cc
new file mode 100644
index 0000000..ffcf1f1
--- /dev/null
+++ b/starboard/linux/shared/audio_write_ahead.cc
@@ -0,0 +1,43 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/linux/shared/audio_write_ahead.h"
+
+#include "cobalt/extension/audio_write_ahead.h"
+#include "starboard/common/log.h"
+#include "starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.h"
+
+// Omit namespace linux due to symbol name conflict.
+
+namespace starboard {
+namespace shared {
+
+namespace {
+
+const CobaltExtensionConfigurableAudioWriteAheadApi
+    kConfigurableAudioWriteAheadApi = {
+        kCobaltExtensionConfigurableAudioWriteAheadName,
+        1,
+        &ConfigurableAudioWriteAheadPlayerGetAudioConfiguration,
+};
+
+}  // namespace
+
+const void* GetConfigurableAudioWriteAheadApi() {
+  SB_LOG(INFO) << "Audio write ahead extension enabled.";
+  return &kConfigurableAudioWriteAheadApi;
+}
+
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/linux/shared/audio_write_ahead.h b/starboard/linux/shared/audio_write_ahead.h
new file mode 100644
index 0000000..15d19e8
--- /dev/null
+++ b/starboard/linux/shared/audio_write_ahead.h
@@ -0,0 +1,28 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_AUDIO_WRITE_AHEAD_H_
+#define STARBOARD_SHARED_AUDIO_WRITE_AHEAD_H_
+
+// Omit namespace linux due to symbol name conflict.
+
+namespace starboard {
+namespace shared {
+
+const void* GetConfigurableAudioWriteAheadApi();
+
+}  // namespace shared
+}  // namespace starboard
+
+#endif  // STARBOARD_SHARED_AUDIO_WRITE_AHEAD_H_
diff --git a/starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.cc b/starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.cc
new file mode 100644
index 0000000..c2e5f00
--- /dev/null
+++ b/starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.cc
@@ -0,0 +1,56 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.h"
+
+#include <string>
+
+#include "starboard/audio_sink.h"
+#include "starboard/common/log.h"
+#include "starboard/system.h"
+
+// Omit namespace linux due to symbol name conflict.
+
+namespace starboard {
+namespace shared {
+
+bool ConfigurableAudioWriteAheadPlayerGetAudioConfiguration(
+    SbPlayer player,
+    int index,
+    CobaltExtensionMediaAudioConfiguration* out_audio_configuration) {
+  SB_DCHECK(SbPlayerIsValid(player));
+  SB_DCHECK(index >= 0);
+  SB_DCHECK(out_audio_configuration);
+
+  if (index > 0) {
+    // We assume that |player| only uses the primary (index 0) audio output.
+    // For playbacks using more than one audio outputs, or using audio outputs
+    // other than the primary one, the platform should have its own
+    // `SbPlayerGetAudioConfiguration()` implementation.
+    return false;
+  }
+
+  *out_audio_configuration = {};
+
+  out_audio_configuration->connector =
+      kCobaltExtensionMediaAudioConnectorUnknown;
+  out_audio_configuration->latency = 0;
+  out_audio_configuration->coding_type = kSbMediaAudioCodingTypePcm;
+  out_audio_configuration->number_of_channels = SbAudioSinkGetMaxChannels();
+
+  return true;
+}
+
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.h b/starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.h
new file mode 100644
index 0000000..08126df
--- /dev/null
+++ b/starboard/linux/shared/audio_write_ahead_player_get_audio_configuration.h
@@ -0,0 +1,35 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_AUDIO_WRITE_AHEAD_PLAYER_GET_AUDIO_CONFIGURATION_H_
+#define STARBOARD_SHARED_AUDIO_WRITE_AHEAD_PLAYER_GET_AUDIO_CONFIGURATION_H_
+
+#include "cobalt/extension/audio_write_ahead.h"
+
+#include "starboard/player.h"
+
+// Omit namespace linux due to symbol name conflict.
+
+namespace starboard {
+namespace shared {
+
+bool ConfigurableAudioWriteAheadPlayerGetAudioConfiguration(
+    SbPlayer player,
+    int index,
+    CobaltExtensionMediaAudioConfiguration* out_audio_configuration);
+
+}  // namespace shared
+}  // namespace starboard
+
+#endif  // STARBOARD_SHARED_AUDIO_WRITE_AHEAD_PLAYER_GET_AUDIO_CONFIGURATION_H_
diff --git a/starboard/linux/shared/ifa.cc b/starboard/linux/shared/ifa.cc
new file mode 100644
index 0000000..548f9bb
--- /dev/null
+++ b/starboard/linux/shared/ifa.cc
@@ -0,0 +1,65 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/linux/shared/ifa.h"
+
+#include "cobalt/extension/ifa.h"
+
+#include "starboard/common/string.h"
+#include "starboard/shared/environment.h"
+
+namespace starboard {
+namespace shared {
+
+namespace {
+
+bool CopyStringAndTestIfSuccess(char* out_value,
+                                int value_length,
+                                const char* from_value) {
+  if (strlen(from_value) + 1 > value_length)
+    return false;
+  starboard::strlcpy(out_value, from_value, value_length);
+  return true;
+}
+
+// Definitions of any functions included as components in the extension
+// are added here.
+
+bool GetAdvertisingId(char* out_value, int value_length) {
+  return CopyStringAndTestIfSuccess(
+      out_value, value_length,
+      starboard::GetEnvironment("COBALT_ADVERTISING_ID").c_str());
+}
+
+bool GetLimitAdTracking(char* out_value, int value_length) {
+  return CopyStringAndTestIfSuccess(
+      out_value, value_length,
+      GetEnvironment("COBALT_LIMIT_AD_TRACKING").c_str());
+}
+
+const StarboardExtensionIfaApi kIfaApi = {
+    kStarboardExtensionIfaName,
+    1,  // API version that's implemented.
+    &GetAdvertisingId,
+    &GetLimitAdTracking,
+};
+
+}  // namespace
+
+const void* GetIfaApi() {
+  return &kIfaApi;
+}
+
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/linux/shared/ifa.h b/starboard/linux/shared/ifa.h
new file mode 100644
index 0000000..fbe61ab
--- /dev/null
+++ b/starboard/linux/shared/ifa.h
@@ -0,0 +1,27 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_LINUX_SHARED_IFA_H_
+#define STARBOARD_LINUX_SHARED_IFA_H_
+
+// Omit namespace linux due to symbol name conflict.
+namespace starboard {
+namespace shared {
+
+const void* GetIfaApi();
+
+}  // namespace shared
+}  // namespace starboard
+
+#endif  // STARBOARD_LINUX_SHARED_IFA_H_
diff --git a/starboard/linux/shared/system_get_extensions.cc b/starboard/linux/shared/system_get_extensions.cc
index fba44bd..f6a7e8d 100644
--- a/starboard/linux/shared/system_get_extensions.cc
+++ b/starboard/linux/shared/system_get_extensions.cc
@@ -14,13 +14,16 @@
 
 #include "starboard/system.h"
 
+#include "cobalt/extension/audio_write_ahead.h"
 #include "cobalt/extension/configuration.h"
 #include "cobalt/extension/crash_handler.h"
 #include "cobalt/extension/demuxer.h"
 #include "cobalt/extension/free_space.h"
+#include "cobalt/extension/ifa.h"
 #include "cobalt/extension/memory_mapped_file.h"
 #include "cobalt/extension/platform_service.h"
 #include "starboard/common/string.h"
+#include "starboard/linux/shared/ifa.h"
 #include "starboard/linux/shared/soft_mic_platform_service.h"
 #include "starboard/shared/ffmpeg/ffmpeg_demuxer.h"
 #include "starboard/shared/posix/free_space.h"
@@ -30,6 +33,7 @@
 #if SB_IS(EVERGREEN_COMPATIBLE)
 #include "starboard/elf_loader/evergreen_config.h"
 #endif
+#include "starboard/linux/shared/audio_write_ahead.h"
 #include "starboard/linux/shared/configuration.h"
 
 const void* SbSystemGetExtension(const char* name) {
@@ -59,6 +63,9 @@
   if (strcmp(name, kCobaltExtensionFreeSpaceName) == 0) {
     return starboard::shared::posix::GetFreeSpaceApi();
   }
+  if (strcmp(name, kCobaltExtensionConfigurableAudioWriteAheadName) == 0) {
+    return starboard::shared::GetConfigurableAudioWriteAheadApi();
+  }
   if (strcmp(name, kCobaltExtensionDemuxerApi) == 0) {
     auto command_line =
         starboard::shared::starboard::Application::Get()->GetCommandLine();
@@ -67,5 +74,10 @@
     return use_ffmpeg_demuxer ? starboard::shared::ffmpeg::GetFFmpegDemuxerApi()
                               : NULL;
   }
+#if SB_API_VERSION < 14
+  if (strcmp(name, kStarboardExtensionIfaName) == 0) {
+    return starboard::shared::GetIfaApi();
+  }
+#endif  // SB_API_VERSION < 14
   return NULL;
 }
diff --git a/starboard/shared/win32/video_decoder.cc b/starboard/shared/win32/video_decoder.cc
index fe58e7b..dd588a3 100644
--- a/starboard/shared/win32/video_decoder.cc
+++ b/starboard/shared/win32/video_decoder.cc
@@ -468,8 +468,9 @@
 
   ComPtr<IMFAttributes> attributes = transform->GetAttributes();
   SB_DCHECK(attributes);
-  CheckResult(attributes->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT,
-                                    kMaxOutputSamples));
+  CheckResult(
+      attributes->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT,
+                            static_cast<UINT32>(GetMaxNumberOfCachedFrames())));
 
   UpdateVideoArea(transform->GetCurrentOutputType());
 
@@ -704,7 +705,7 @@
       // MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT.
       thread_lock_.Acquire();
       bool input_full = thread_events_.size() >= kMaxInputSamples;
-      bool output_full = thread_outputs_.size() >= kMaxOutputSamples;
+      bool output_full = thread_outputs_.size() >= GetMaxNumberOfCachedFrames();
       thread_lock_.Release();
 
       Status status = input_full ? kBufferFull : kNeedMoreInput;
diff --git a/third_party/angle/include/GLSLANG/ShaderVars.h b/third_party/angle/include/GLSLANG/ShaderVars.h
index 52f6ad0..5d2d146 100644
--- a/third_party/angle/include/GLSLANG/ShaderVars.h
+++ b/third_party/angle/include/GLSLANG/ShaderVars.h
@@ -12,6 +12,7 @@
 
 #include <algorithm>
 #include <array>
+#include <cstdint>
 #include <string>
 #include <vector>
 
diff --git a/third_party/angle/src/common/angleutils.h b/third_party/angle/src/common/angleutils.h
index 3a1391e..c8d36f5 100644
--- a/third_party/angle/src/common/angleutils.h
+++ b/third_party/angle/src/common/angleutils.h
@@ -14,6 +14,7 @@
 #include <climits>
 #include <cstdarg>
 #include <cstddef>
+#include <cstdint>
 #include <set>
 #include <sstream>
 #include <string>
diff --git a/third_party/chromium/media/filters/chunk_demuxer.cc b/third_party/chromium/media/filters/chunk_demuxer.cc
index 1d6e475..2b729a4 100644
--- a/third_party/chromium/media/filters/chunk_demuxer.cc
+++ b/third_party/chromium/media/filters/chunk_demuxer.cc
@@ -180,6 +180,9 @@
   DCHECK(state_ == UNINITIALIZED || state_ == RETURNING_ABORT_FOR_READS)
       << state_;
 
+#if defined(STARBOARD)
+  write_head_ = time;
+#endif // defined (STARBOARD)
   stream_->Seek(time);
 }
 
@@ -225,6 +228,15 @@
   return stream_->GarbageCollectIfNeeded(media_time, newDataSize);
 }
 
+#if defined(STARBOARD)
+
+base::TimeDelta ChunkDemuxerStream::GetWriteHead() const {
+  base::AutoLock auto_lock(lock_);
+  return write_head_;
+}
+
+#endif  // defined(STARBOARD)
+
 void ChunkDemuxerStream::OnMemoryPressure(
     base::TimeDelta media_time,
     base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level,
@@ -473,6 +485,7 @@
 
   DemuxerStream::Status status = DemuxerStream::kAborted;
   std::vector<scoped_refptr<DecoderBuffer>> buffers;
+  base::TimeDelta write_head = write_head_;
 
   if (pending_config_change_) {
     status = kConfigChanged;
@@ -491,6 +504,7 @@
                  << buffer->timestamp().InSecondsF() << ", dur "
                  << buffer->duration().InSecondsF() << ", key "
                  << buffer->is_key_frame();
+        write_head = std::max(write_head, buffer->timestamp());
         buffers.push_back(std::move(buffer));
         status = DemuxerStream::kOk;
       } else if (stream_status == SourceBufferStreamStatus::kEndOfStream) {
@@ -529,6 +543,8 @@
     return;
   }
 
+  DCHECK_LE(write_head_, write_head);
+  write_head_ = write_head;
   std::move(read_cb_).Run(status, buffers);
 }
 #else // defined(STARBOARD)
@@ -1077,6 +1093,25 @@
   return itr->second->EvictCodedFrames(currentMediaTime, newDataSize);
 }
 
+#if defined(STARBOARD)
+
+base::TimeDelta ChunkDemuxer::GetWriteHead(const std::string& id) const {
+  base::AutoLock auto_lock(lock_);
+  DCHECK(IsValidId(id));
+
+  auto iter = id_to_streams_map_.find(id);
+  if (iter == id_to_streams_map_.end() ||
+      iter->second.empty()) {
+    // Handled just in case.
+    SB_NOTREACHED();
+    return base::TimeDelta();
+  }
+
+  return iter->second[0]->GetWriteHead();
+}
+
+#endif  // defined(STARBOARD)
+
 bool ChunkDemuxer::AppendData(const std::string& id,
                               const uint8_t* data,
                               size_t length,
diff --git a/third_party/chromium/media/filters/chunk_demuxer.h b/third_party/chromium/media/filters/chunk_demuxer.h
index fd67e6f..2e71291 100644
--- a/third_party/chromium/media/filters/chunk_demuxer.h
+++ b/third_party/chromium/media/filters/chunk_demuxer.h
@@ -79,6 +79,10 @@
   // https://w3c.github.io/media-source/#sourcebuffer-coded-frame-eviction
   bool EvictCodedFrames(base::TimeDelta media_time, size_t newDataSize);
 
+#if defined(STARBOARD)
+  base::TimeDelta GetWriteHead() const;
+#endif  // defined(STARBOARD)
+
   void OnMemoryPressure(
       base::TimeDelta media_time,
       base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level,
@@ -207,6 +211,9 @@
   State state_ GUARDED_BY(lock_);
   ReadCB read_cb_ GUARDED_BY(lock_);
   bool is_enabled_ GUARDED_BY(lock_);
+#if defined(STARBOARD)
+  base::TimeDelta write_head_ GUARDED_BY(lock_);
+#endif  // defined(STARBOARD)
 
   DISALLOW_IMPLICIT_CONSTRUCTORS(ChunkDemuxerStream);
 };
@@ -383,6 +390,10 @@
                         base::TimeDelta currentMediaTime,
                         size_t newDataSize) WARN_UNUSED_RESULT;
 
+#if defined(STARBOARD)
+  base::TimeDelta GetWriteHead(const std::string& id) const;
+#endif  // defined(STARBOARD)
+
   void OnMemoryPressure(
       base::TimeDelta currentMediaTime,
       base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level,
diff --git a/third_party/crashpad/util/misc/reinterpret_bytes.cc b/third_party/crashpad/util/misc/reinterpret_bytes.cc
index 65ec33f..3fee722 100644
--- a/third_party/crashpad/util/misc/reinterpret_bytes.cc
+++ b/third_party/crashpad/util/misc/reinterpret_bytes.cc
@@ -17,6 +17,7 @@
 #include <string.h>
 
 #include <algorithm>
+#include <cstdint>
 
 #include "base/logging.h"
 
diff --git a/third_party/devtools/front_end/cobalt/cobalt.js b/third_party/devtools/front_end/cobalt/cobalt.js
index a085c62..c65ff59 100644
--- a/third_party/devtools/front_end/cobalt/cobalt.js
+++ b/third_party/devtools/front_end/cobalt/cobalt.js
@@ -14,6 +14,7 @@
 
         this._target = UI.context.flavor(SDK.Target);
         this._runtimeAgent = this._target.runtimeAgent();
+        this._cobaltAgent = this._target.cobaltAgent();
 
         this.element = this._shadowRoot.createChild('div');
         this.element.textContent = 'Cobalt Console';
@@ -32,7 +33,7 @@
             this.run(`(function() { window.h5vcc.traceEvent.stop();})()`);
             console.log("Stopped Trace");
         }));
-        trace_files.forEach( (file) => {
+        trace_files.forEach((file) => {
             traceContainer.appendChild(UI.createTextButton(Common.UIString('Download ' + file[0]), event => {
                 console.log("Download Trace");
                 const filename = file[1];
@@ -46,6 +47,39 @@
                 });
             }));
         });
+        const debugLogContainer = this.element.createChild('div', 'debug-log-container');
+        debugLogContainer.appendChild(UI.createTextButton(Common.UIString('DebugLog On'), event => {
+            this._cobaltAgent.invoke_sendConsoleCommand({
+                command: 'debug_log', message: 'on'
+            });
+        }));
+        debugLogContainer.appendChild(UI.createTextButton(Common.UIString('DebugLog Off'), event => {
+            this._cobaltAgent.invoke_sendConsoleCommand({
+                command: 'debug_log', message: 'off'
+            });
+        }));
+        const consoleContainer = this.element.createChild('div', 'console-container');
+        consoleContainer.appendChild(UI.createTextButton(Common.UIString('DebugCommand'), event => {
+            const outputElement = document.getElementsByClassName('console-output')[0];
+            const command = document.getElementsByClassName('debug-command')[0].value;
+            if (command.length == 0) {
+                const result = this._cobaltAgent.invoke_getConsoleCommands().then(result => {
+                    outputElement.innerHTML = JSON.stringify(result.commands, undefined, 2);
+                });
+            } else {
+                const result = this._cobaltAgent.invoke_sendConsoleCommand({
+                    command: command,
+                    message: document.getElementsByClassName('debug-message')[0].value
+                }).then(result => {
+                    outputElement.innerHTML = JSON.stringify(result, undefined, 2);
+                });
+            }
+        }));
+        consoleContainer.appendChild(UI.createLabel('Command:'));
+        consoleContainer.appendChild(UI.createInput('debug-command', 'text'));
+        consoleContainer.appendChild(UI.createLabel('Message:'));
+        consoleContainer.appendChild(UI.createInput('debug-message', 'text'));
+        consoleContainer.createChild('pre', 'console-output');
     }
 
     async run(expression) {
diff --git a/third_party/devtools/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/devtools/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index 0091c32..e98162f 100644
--- a/third_party/devtools/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/devtools/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -8,6 +8,27 @@
   major 1
   minor 3
 
+# The Cobalt domain defines custom methods and events for Cobalt
+experimental domain Cobalt
+  command disable
+
+  command enable
+
+  experimental type ConsoleCommand extends object
+    properties
+      string command
+      string shortHelp
+      string longHelp
+
+  experimental command getConsoleCommands
+    returns
+      array of ConsoleCommand commands
+
+  experimental command sendConsoleCommand
+    parameters
+      string command
+      optional string message
+
 experimental domain Accessibility
   depends on DOM
 
diff --git a/third_party/v8/src/base/logging.h b/third_party/v8/src/base/logging.h
index fe39f98..dbe1305 100644
--- a/third_party/v8/src/base/logging.h
+++ b/third_party/v8/src/base/logging.h
@@ -5,6 +5,7 @@
 #ifndef V8_BASE_LOGGING_H_
 #define V8_BASE_LOGGING_H_
 
+#include <cstdint>
 #include <cstring>
 #include <sstream>
 #include <string>
diff --git a/third_party/v8/src/base/macros.h b/third_party/v8/src/base/macros.h
index 515a9e3..a7cd7c0 100644
--- a/third_party/v8/src/base/macros.h
+++ b/third_party/v8/src/base/macros.h
@@ -5,6 +5,7 @@
 #ifndef V8_BASE_MACROS_H_
 #define V8_BASE_MACROS_H_
 
+#include <cstdint>
 #include <limits>
 #include <type_traits>
 
diff --git a/third_party/v8/src/inspector/v8-string-conversions.h b/third_party/v8/src/inspector/v8-string-conversions.h
index c1d69c1..eb33c68 100644
--- a/third_party/v8/src/inspector/v8-string-conversions.h
+++ b/third_party/v8/src/inspector/v8-string-conversions.h
@@ -5,6 +5,7 @@
 #ifndef V8_INSPECTOR_V8_STRING_CONVERSIONS_H_
 #define V8_INSPECTOR_V8_STRING_CONVERSIONS_H_
 
+#include <cstdint>
 #include <string>
 
 // Conversion routines between UT8 and UTF16, used by string-16.{h,cc}. You may
diff --git a/tools/copy_and_filter_out_dir.py b/tools/copy_and_filter_out_dir.py
index 8e01bf8..080a69f 100755
--- a/tools/copy_and_filter_out_dir.py
+++ b/tools/copy_and_filter_out_dir.py
@@ -102,6 +102,12 @@
     return 0
 
   _IterateAndFilter(source_out_dir, dest_out_dir, copies)
+
+  # Add build_info json to the out directory root.
+  source_build_info = os.path.join(source_out_dir, 'gen', 'build_info.json')
+  dest_build_info = os.path.join(dest_out_dir, 'build_info.json')
+  copies[source_build_info] = dest_build_info
+
   for source, dest in copies.items():
     dirname = os.path.dirname(dest)
     if not os.path.exists(dirname):
diff --git a/tools/lz4_compress/lz4_compress.cc b/tools/lz4_compress/lz4_compress.cc
index 26421dc..08a79f5 100644
--- a/tools/lz4_compress/lz4_compress.cc
+++ b/tools/lz4_compress/lz4_compress.cc
@@ -22,6 +22,7 @@
 
 #include <stdio.h>
 
+#include <cstdint>
 #include <iostream>
 #include <vector>