Import Cobalt 23.master.0.308707
diff --git a/cobalt/bindings/contexts.py b/cobalt/bindings/contexts.py
index 9285877..fb03640 100644
--- a/cobalt/bindings/contexts.py
+++ b/cobalt/bindings/contexts.py
@@ -244,7 +244,8 @@
             not idl_type.is_callback_interface), 'Callback types not supported.'
     element_cobalt_type = self.idl_type_to_cobalt_type(
         self.resolve_typedef(result_idl_type))
-    result = '::cobalt::script::Promise< %s >' % element_cobalt_type
+    result = 'std::unique_ptr<::cobalt::script::Promise< %s* > >' % (
+        element_cobalt_type)
     return result
 
   def idl_union_type_to_cobalt(self, idl_type):
diff --git a/cobalt/bindings/v8c/templates/interface.cc.template b/cobalt/bindings/v8c/templates/interface.cc.template
index a11e229..65f703c 100644
--- a/cobalt/bindings/v8c/templates/interface.cc.template
+++ b/cobalt/bindings/v8c/templates/interface.cc.template
@@ -40,6 +40,7 @@
 #include "cobalt/script/v8c/entry_scope.h"
 #include "cobalt/script/v8c/helpers.h"
 #include "cobalt/script/v8c/native_promise.h"
+#include "cobalt/script/v8c/script_promise.h"
 #include "cobalt/script/v8c/type_traits.h"
 #include "cobalt/script/v8c/v8c_typed_arrays.h"
 #include "cobalt/script/v8c/v8c_data_view.h"
diff --git a/cobalt/black_box_tests/testdata/service_worker_test.js b/cobalt/black_box_tests/testdata/service_worker_test.js
index 44ffc55..5b9e117 100644
--- a/cobalt/black_box_tests/testdata/service_worker_test.js
+++ b/cobalt/black_box_tests/testdata/service_worker_test.js
@@ -53,10 +53,10 @@
     self.clients.matchAll(options).then(function (clients) {
       console.log('(Expected) self.clients.matchAll():', clients.length, clients);
       for (var i = 0; i < clients.length; i++) {
-        console.log('Client with url', clients[i].url);
-        console.log('Client with frameType', clients[i].frameType);
-        console.log('Client with id', clients[i].id);
-        console.log('Client with type', clients[i].type);
+        console.log('Client with url', clients[i].url,
+          'frameType', clients[i].frameType,
+          'id', clients[i].id,
+          'type', clients[i].type);
       }
     }, function (error) {
       console.log(`(Unexpected) self.clients.matchAll(): ${error}`, error);
@@ -106,10 +106,10 @@
   console.log('(Expected) self.clients.matchAll():', clients.length, clients);
   // Note: This will return 0 clients if none are controlled so far.
   for (var i = 0; i < clients.length; i++) {
-    console.log('Client with url', clients[i].url);
-    console.log('Client with frameType', clients[i].frameType);
-    console.log('Client with id', clients[i].id);
-    console.log('Client with type', clients[i].type);
+    console.log('Client with url', clients[i].url,
+      'frameType', clients[i].frameType,
+      'id', clients[i].id,
+      'type', clients[i].type);
   }
 }, function (error) {
   console.log(`(Unexpected) self.clients.matchAll(): ${error}`, error);
@@ -123,18 +123,18 @@
 self.clients.matchAll(options).then(function (clients) {
   console.log('(Expected) self.clients.matchAll():', clients.length, clients);
   for (var i = 0; i < clients.length; i++) {
-    console.log('Client with url', clients[i].url);
-    console.log('Client with frameType', clients[i].frameType);
-    console.log('Client with id', clients[i].id);
-    console.log('Client with type', clients[i].type);
+    console.log('Client with url', clients[i].url,
+      'frameType', clients[i].frameType,
+      'id', clients[i].id,
+      'type', clients[i].type);
 
     console.log('self.clients.get()');
     self.clients.get(clients[i].id).then(function (client) {
       console.log('(Expected) self.clients.get():', client);
-      console.log('Client with url', client.url);
-      console.log('Client with frameType', client.frameType);
-      console.log('Client with id', client.id);
-      console.log('Client with type', client.type);
+      console.log('Client with url', client.url,
+        'frameType', client.frameType,
+        'id', client.id,
+        'type', client.type);
     }, function (error) {
       console.log(`(Unexpected) self.clients.get(): ${error}`, error);
     });
diff --git a/cobalt/black_box_tests/testdata/service_worker_test_claimable.js b/cobalt/black_box_tests/testdata/service_worker_test_claimable.js
index cc0fcde..21cb0d7 100644
--- a/cobalt/black_box_tests/testdata/service_worker_test_claimable.js
+++ b/cobalt/black_box_tests/testdata/service_worker_test_claimable.js
@@ -27,37 +27,47 @@
   });
 }
 
+function delay_promise(delay) {
+  return new Promise(function (resolve) {
+    setTimeout(resolve.bind(null), delay)
+  });
+}
+
 self.oninstall = function (e) {
   console.log('oninstall event received', e);
+  e.waitUntil(delay_promise(500).then(() => console.log('Promised delay.'), () => console.log('\nPromised rejected.\n')));
 }
+
 self.onactivate = function (e) {
   console.log('onactivate event received', e);
 
   // Claim should pass here, since the state is activating.
   console.log('self.clients.claim()');
-  self.clients.claim().then(function (result) {
+  e.waitUntil(self.clients.claim().then(function (result) {
     console.log('(Expected) self.clients.claim():', result);
 
     var options = {
       includeUncontrolled: false, type: 'window'
     };
-    console.log('self.clients.matchAll(options)');
-    self.clients.matchAll(options).then(function (clients) {
-      console.log('(Expected) self.clients.matchAll():', clients.length, clients);
-      for (var i = 0; i < clients.length; i++) {
-        console.log('Client with url', clients[i].url);
-        console.log('Client with frameType', clients[i].frameType);
-        console.log('Client with id', clients[i].id);
-        console.log('Client with type', clients[i].type);
-        clients[i].postMessage(`You have been claimed, client with id ${clients[i].id}`);
-      }
-    }, function (error) {
-      console.log(`(Unexpected) self.clients.matchAll(): ${error}`, error);
-    });
+    e.waitUntil(delay_promise(1000).then(function () {
+      console.log('self.clients.matchAll(options)');
+      e.waitUntil(self.clients.matchAll(options).then(function (clients) {
+        console.log('(Expected) self.clients.matchAll():', clients.length, clients);
+        for (var i = 0; i < clients.length; i++) {
+          console.log('Client with url', clients[i].url,
+            'frameType', clients[i].frameType,
+            'id', clients[i].id,
+            'type', clients[i].type);
+          clients[i].postMessage(`You have been claimed, client with id ${clients[i].id}`);
+        }
+      }, function (error) {
+        console.log(`(Unexpected) self.clients.matchAll(): ${error}`, error);
+      }));
+    }));
 
   }, function (error) {
     console.log(`(Unexpected) self.clients.claim(): ${error}`, error);
-  });
+  }));
 
 }
 console.log('self.registration', self.registration);
@@ -73,10 +83,10 @@
 self.clients.matchAll(options).then(function (clients) {
   console.log('(Expected) self.clients.matchAll():', clients.length, clients);
   for (var i = 0; i < clients.length; i++) {
-    console.log('Client with url', clients[i].url);
-    console.log('Client with frameType', clients[i].frameType);
-    console.log('Client with id', clients[i].id);
-    console.log('Client with type', clients[i].type);
+    console.log('Client with url', clients[i].url,
+      'frameType', clients[i].frameType,
+      'id', clients[i].id,
+      'type', clients[i].type);
   }
 }, function (error) {
   console.log(`(Unexpected) self.clients.matchAll(): ${error}`, error);
diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn
index 529dd2f..4c30f02 100644
--- a/cobalt/browser/BUILD.gn
+++ b/cobalt/browser/BUILD.gn
@@ -155,6 +155,7 @@
     "//cobalt/browser/memory_settings:browser_memory_settings",
     "//cobalt/browser/memory_tracker:memory_tracker_tool",
     "//cobalt/build:cobalt_build_id",
+    "//cobalt/cache",
     "//cobalt/configuration",
     "//cobalt/css_parser",
     "//cobalt/cssom",
diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc
index d710cae..5fa7318 100644
--- a/cobalt/browser/application.cc
+++ b/cobalt/browser/application.cc
@@ -61,6 +61,7 @@
 #include "cobalt/browser/switches.h"
 #include "cobalt/browser/user_agent_platform_info.h"
 #include "cobalt/browser/user_agent_string.h"
+#include "cobalt/cache/cache.h"
 #include "cobalt/configuration/configuration.h"
 #include "cobalt/extension/crash_handler.h"
 #include "cobalt/extension/installation_manager.h"
@@ -648,6 +649,9 @@
       watchdog::Watchdog::CreateInstance(persistent_settings_.get());
   DCHECK(watchdog);
 
+  cobalt::cache::Cache::GetInstance()->set_persistent_settings(
+      persistent_settings_.get());
+
   base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
   base::Optional<cssom::ViewportSize> requested_viewport_size =
       GetRequestedViewportSize(command_line);
@@ -661,6 +665,7 @@
   // Create the main components of our browser.
   BrowserModule::Options options(web_options);
   network_module_options.preferred_language = language;
+  network_module_options.persistent_settings = persistent_settings_.get();
   options.persistent_settings = persistent_settings_.get();
   options.command_line_auto_mem_settings =
       memory_settings::GetSettings(*command_line);
@@ -860,7 +865,7 @@
 #if SB_IS(EVERGREEN)
       updater_module_.get(),
 #endif
-      options, persistent_settings_.get()));
+      options));
 
   UpdateUserAgent();
 
diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc
index 8b7b1d4..79dd071 100644
--- a/cobalt/browser/browser_module.cc
+++ b/cobalt/browser/browser_module.cc
@@ -219,16 +219,15 @@
 
 }  // namespace
 
-BrowserModule::BrowserModule(
-    const GURL& url, base::ApplicationState initial_application_state,
-    base::EventDispatcher* event_dispatcher,
-    account::AccountManager* account_manager,
-    network::NetworkModule* network_module,
+BrowserModule::BrowserModule(const GURL& url,
+                             base::ApplicationState initial_application_state,
+                             base::EventDispatcher* event_dispatcher,
+                             account::AccountManager* account_manager,
+                             network::NetworkModule* network_module,
 #if SB_IS(EVERGREEN)
-    updater::UpdaterModule* updater_module,
+                             updater::UpdaterModule* updater_module,
 #endif
-    const Options& options,
-    persistent_storage::PersistentSettings* persistent_settings)
+                             const Options& options)
     : ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)),
       ALLOW_THIS_IN_INITIALIZER_LIST(
           weak_this_(weak_ptr_factory_.GetWeakPtr())),
@@ -298,8 +297,7 @@
       next_timeline_id_(1),
       current_splash_screen_timeline_id_(-1),
       current_main_web_module_timeline_id_(-1),
-      service_worker_registry_(network_module),
-      persistent_settings_(persistent_settings) {
+      service_worker_registry_(network_module) {
   TRACE_EVENT0("cobalt::browser", "BrowserModule::BrowserModule()");
 
   // Apply platform memory setting adjustments and defaults.
@@ -2075,7 +2073,7 @@
       dom_settings->window()->navigator()->user_agent_data();
   h5vcc_settings.global_environment =
       dom_settings->context()->global_environment();
-  h5vcc_settings.persistent_settings = persistent_settings_;
+  h5vcc_settings.persistent_settings = options_.persistent_settings;
 
   auto* h5vcc_object = new h5vcc::H5vcc(h5vcc_settings);
   if (!web_module_created_callback_.is_null()) {
diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h
index 0706039..a12551e 100644
--- a/cobalt/browser/browser_module.h
+++ b/cobalt/browser/browser_module.h
@@ -131,8 +131,7 @@
 #if SB_IS(EVERGREEN)
                 updater::UpdaterModule* updater_module,
 #endif
-                const Options& options,
-                persistent_storage::PersistentSettings* persistent_settings);
+                const Options& options);
   ~BrowserModule();
 
   std::string GetUserAgent() { return network_module_->GetUserAgent(); }
@@ -724,8 +723,6 @@
 
   // Manages the Service Workers.
   ServiceWorkerRegistry service_worker_registry_;
-
-  persistent_storage::PersistentSettings* persistent_settings_;
 };
 
 }  // namespace browser
diff --git a/cobalt/build/ninja/README b/cobalt/build/ninja/README
deleted file mode 100644
index fe0f765..0000000
--- a/cobalt/build/ninja/README
+++ /dev/null
@@ -1,7 +0,0 @@
-ninja-win.exe built with MSVC 2012
-from
-https://github.com/REDACTED/ninja/commit/1287257fe343c4b6b0f760308a7bec52d86b0edf
-
-ninja-linux built with gcc
-from
-https://github.com/REDACTED/ninja/commit/613d89893e56acc8c3670a66a33fcd2caf7d9050
diff --git a/cobalt/build/ninja/ninja-linux b/cobalt/build/ninja/ninja-linux
deleted file mode 100755
index 1f30022..0000000
--- a/cobalt/build/ninja/ninja-linux
+++ /dev/null
Binary files differ
diff --git a/cobalt/build/ninja/ninja-linux32.armv7l b/cobalt/build/ninja/ninja-linux32.armv7l
deleted file mode 100644
index cb4c3ec..0000000
--- a/cobalt/build/ninja/ninja-linux32.armv7l
+++ /dev/null
Binary files differ
diff --git a/cobalt/build/ninja/ninja-win.exe b/cobalt/build/ninja/ninja-win.exe
deleted file mode 100755
index 4a68328..0000000
--- a/cobalt/build/ninja/ninja-win.exe
+++ /dev/null
Binary files differ
diff --git a/cobalt/cache/BUILD.gn b/cobalt/cache/BUILD.gn
index 7fe8ddd..edc1daf 100644
--- a/cobalt/cache/BUILD.gn
+++ b/cobalt/cache/BUILD.gn
@@ -22,6 +22,7 @@
     "//base",
     "//cobalt/base",
     "//cobalt/configuration",
+    "//cobalt/persistent_storage:persistent_settings",
     "//net",
     "//starboard:starboard_headers_only",
   ]
diff --git a/cobalt/cache/cache.cc b/cobalt/cache/cache.cc
index 0600895..e2c568d 100644
--- a/cobalt/cache/cache.cc
+++ b/cobalt/cache/cache.cc
@@ -24,6 +24,8 @@
 #include "base/optional.h"
 #include "cobalt/configuration/configuration.h"
 #include "cobalt/extension/javascript_cache.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
+#include "net/disk_cache/cobalt/cobalt_backend_impl.h"
 #include "starboard/configuration_constants.h"
 #include "starboard/system.h"
 
@@ -86,14 +88,6 @@
   return nullptr;
 }
 
-bool CanCache(disk_cache::ResourceType resource_type, uint32_t data_size) {
-  return cobalt::configuration::Configuration::GetInstance()
-             ->CobaltCanStoreCompiledJavascript() &&
-         data_size > 0u &&
-         data_size >= GetMinSizeToCacheInBytes(resource_type) &&
-         data_size <= GetMaxCacheStorageInBytes(resource_type);
-}
-
 }  // namespace
 
 namespace cobalt {
@@ -158,6 +152,19 @@
   return data;
 }
 
+void Cache::set_enabled(bool enabled) { enabled_ = enabled; }
+
+void Cache::set_persistent_settings(
+    persistent_storage::PersistentSettings* persistent_settings) {
+  persistent_settings_ = persistent_settings;
+
+  // Guaranteed to be called before any calls to Retrieve()
+  // since set_persistent_settings() is called from the Application()
+  // constructor before the NetworkModule is initialized.
+  set_enabled(persistent_settings_->GetPersistentSettingAsBool(
+      disk_cache::kCacheEnabledPersistentSettingsKey, true));
+}
+
 MemoryCappedDirectory* Cache::GetMemoryCappedDirectory(
     disk_cache::ResourceType resource_type) {
   base::AutoLock auto_lock(lock_);
@@ -221,5 +228,15 @@
   }
 }
 
+bool Cache::CanCache(disk_cache::ResourceType resource_type,
+                     uint32_t data_size) {
+  return enabled_ &&
+         cobalt::configuration::Configuration::GetInstance()
+             ->CobaltCanStoreCompiledJavascript() &&
+         data_size > 0u &&
+         data_size >= GetMinSizeToCacheInBytes(resource_type) &&
+         data_size <= GetMaxCacheStorageInBytes(resource_type);
+}
+
 }  // namespace cache
 }  // namespace cobalt
diff --git a/cobalt/cache/cache.h b/cobalt/cache/cache.h
index 790da73..4f9e4fe 100644
--- a/cobalt/cache/cache.h
+++ b/cobalt/cache/cache.h
@@ -27,6 +27,7 @@
 #include "base/synchronization/lock.h"
 #include "base/synchronization/waitable_event.h"
 #include "cobalt/cache/memory_capped_directory.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/disk_cache/cobalt/resource_type.h"
 
 namespace base {
@@ -45,6 +46,11 @@
       disk_cache::ResourceType resource_type, uint32_t key,
       std::function<std::unique_ptr<std::vector<uint8_t>>()> generate);
 
+  void set_enabled(bool enabled);
+
+  void set_persistent_settings(
+      persistent_storage::PersistentSettings* persistent_settings);
+
  private:
   friend struct base::DefaultSingletonTraits<Cache>;
   Cache() {}
@@ -56,6 +62,7 @@
   void Notify(disk_cache::ResourceType resource_type, uint32_t key);
   void TryStore(disk_cache::ResourceType resource_type, uint32_t key,
                 const std::vector<uint8_t>& data);
+  bool CanCache(disk_cache::ResourceType resource_type, uint32_t data_size);
 
   mutable base::Lock lock_;
   // The following map is only used when the JavaScript cache extension is
@@ -65,6 +72,9 @@
   std::map<disk_cache::ResourceType,
            std::map<uint32_t, std::vector<base::WaitableEvent*>>>
       pending_;
+  bool enabled_;
+
+  persistent_storage::PersistentSettings* persistent_settings_;
 
   DISALLOW_COPY_AND_ASSIGN(Cache);
 };  // class Cache
diff --git a/cobalt/evergreen_tests/evergreen_tests.py b/cobalt/evergreen_tests/evergreen_tests.py
index 74eafb0..00d23ae 100644
--- a/cobalt/evergreen_tests/evergreen_tests.py
+++ b/cobalt/evergreen_tests/evergreen_tests.py
@@ -31,6 +31,7 @@
 
 
 def _Exec(cmd, env=None):
+  """Executes a command in a subprocess and returns the result."""
   try:
     msg = 'Executing:\n    ' + ' '.join(cmd)
     logging.info(msg)
@@ -46,31 +47,8 @@
     return 1
 
 
-def main():
-  arg_parser = argparse.ArgumentParser()
-  arg_parser.add_argument(
-      '--no-can_mount_tmpfs',
-      dest='can_mount_tmpfs',
-      action='store_false',
-      help='A temporary filesystem cannot be mounted on the target device.')
-  arg_parser.add_argument(
-      '--platform_under_test',
-      default=_DEFAULT_PLATFORM_UNDER_TEST,
-      help='The platform to run the tests on (e.g., linux or raspi).')
-  authentication_method = arg_parser.add_mutually_exclusive_group()
-  authentication_method.add_argument(
-      '--public-key-auth',
-      help='Public key authentication should be used with the remote device.',
-      action='store_true')
-  authentication_method.add_argument(
-      '--password-auth',
-      help='Password authentication should be used with the remote device.',
-      action='store_true')
-  command_line.AddLauncherArguments(arg_parser)
-  args = arg_parser.parse_args()
-
-  log_level.InitializeLogging(args)
-
+def _RunTests(arg_parser, args, use_compressed_system_image):
+  """Runs an instance of the Evergreen tests for the provided configuration."""
   launcher_params = command_line.CreateLauncherParams(arg_parser)
 
   # Creating an instance of the Evergreen abstract launcher implementation
@@ -86,7 +64,8 @@
       loader_platform=launcher_params.loader_platform,
       loader_config=launcher_params.loader_config,
       loader_target='loader_app',
-      loader_out_directory=launcher_params.loader_out_directory)
+      loader_out_directory=launcher_params.loader_out_directory,
+      use_compressed_library=use_compressed_system_image)
 
   # The automated tests use the |OUT| environment variable as the path to a
   # known directory structure containing the desired binaries. This path is
@@ -118,10 +97,52 @@
     command.append('-a')
     command.append('password')
 
+  if use_compressed_system_image:
+    command.append('-c')
+
   command.append(args.platform_under_test)
 
   return _Exec(command, env)
 
 
+def main():
+  arg_parser = argparse.ArgumentParser()
+  arg_parser.add_argument(
+      '--no-can_mount_tmpfs',
+      dest='can_mount_tmpfs',
+      action='store_false',
+      help='A temporary filesystem cannot be mounted on the target device.')
+  arg_parser.add_argument(
+      '--platform_under_test',
+      default=_DEFAULT_PLATFORM_UNDER_TEST,
+      help='The platform to run the tests on (e.g., linux or raspi).')
+  authentication_method = arg_parser.add_mutually_exclusive_group()
+  authentication_method.add_argument(
+      '--public-key-auth',
+      help='Public key authentication should be used with the remote device.',
+      action='store_true')
+  authentication_method.add_argument(
+      '--password-auth',
+      help='Password authentication should be used with the remote device.',
+      action='store_true')
+  arg_parser.add_argument(
+      '--no-rerun_using_compressed_system_image',
+      dest='rerun_using_compressed_system_image',
+      action='store_false',
+      help='Do not run a second instance of the tests with a compressed system '
+      'image.')
+  command_line.AddLauncherArguments(arg_parser)
+  args = arg_parser.parse_args()
+
+  log_level.InitializeLogging(args)
+
+  uncompressed_system_image_result = _RunTests(arg_parser, args, False)
+  if args.rerun_using_compressed_system_image:
+    compressed_system_image_result = _RunTests(arg_parser, args, True)
+    return uncompressed_system_image_result or compressed_system_image_result
+
+  return uncompressed_system_image_result
+
+
 if __name__ == '__main__':
   sys.exit(main())
diff --git a/cobalt/h5vcc/BUILD.gn b/cobalt/h5vcc/BUILD.gn
index 246c396..9c5a145 100644
--- a/cobalt/h5vcc/BUILD.gn
+++ b/cobalt/h5vcc/BUILD.gn
@@ -76,6 +76,7 @@
     "//cobalt/base",
     "//cobalt/browser:browser_switches",
     "//cobalt/build:cobalt_build_id",
+    "//cobalt/cache",
     "//cobalt/configuration",
     "//cobalt/dom",
     "//cobalt/media",
diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc
index e9a9229..dbb6b3b 100644
--- a/cobalt/h5vcc/h5vcc_storage.cc
+++ b/cobalt/h5vcc/h5vcc_storage.cc
@@ -19,10 +19,14 @@
 
 #include "base/files/file_util.h"
 #include "base/values.h"
+#include "cobalt/cache/cache.h"
 #include "cobalt/h5vcc/h5vcc_storage.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "cobalt/storage/storage_manager.h"
+#include "net/disk_cache/cobalt/cobalt_backend_impl.h"
 #include "net/disk_cache/cobalt/resource_type.h"
+#include "net/http/http_cache.h"
+#include "net/http/http_transaction_factory.h"
 
 #include "starboard/common/file.h"
 #include "starboard/common/string.h"
@@ -31,6 +35,7 @@
 namespace h5vcc {
 
 namespace {
+
 const char kTestFileName[] = "cache_test_file.json";
 
 const uint32 kWriteBufferSize = 1024 * 1024;
@@ -69,7 +74,16 @@
     network::NetworkModule* network_module,
     persistent_storage::PersistentSettings* persistent_settings)
     : network_module_(network_module),
-      persistent_settings_(persistent_settings) {}
+      persistent_settings_(persistent_settings) {
+  http_cache_ = nullptr;
+  if (network_module == nullptr) {
+    return;
+  }
+  auto url_request_context = network_module_->url_request_context();
+  if (url_request_context->using_http_cache()) {
+    http_cache_ = url_request_context->http_transaction_factory()->GetCache();
+  }
+}
 
 void H5vccStorage::ClearCookies() {
   net::CookieStore* cookie_store =
@@ -334,5 +348,29 @@
   return quota;
 }
 
+void H5vccStorage::EnableCache() {
+  persistent_settings_->SetPersistentSetting(
+      disk_cache::kCacheEnabledPersistentSettingsKey,
+      std::make_unique<base::Value>(true));
+
+  cobalt::cache::Cache::GetInstance()->set_enabled(true);
+
+  if (http_cache_) {
+    http_cache_->set_mode(net::HttpCache::Mode::NORMAL);
+  }
+}
+
+void H5vccStorage::DisableCache() {
+  persistent_settings_->SetPersistentSetting(
+      disk_cache::kCacheEnabledPersistentSettingsKey,
+      std::make_unique<base::Value>(false));
+
+  cobalt::cache::Cache::GetInstance()->set_enabled(false);
+
+  if (http_cache_) {
+    http_cache_->set_mode(net::HttpCache::Mode::DISABLE);
+  }
+}
+
 }  // namespace h5vcc
 }  // namespace cobalt
diff --git a/cobalt/h5vcc/h5vcc_storage.h b/cobalt/h5vcc/h5vcc_storage.h
index 7f6368b..ff96351 100644
--- a/cobalt/h5vcc/h5vcc_storage.h
+++ b/cobalt/h5vcc/h5vcc_storage.h
@@ -26,6 +26,7 @@
 #include "cobalt/network/network_module.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "cobalt/script/wrappable.h"
+#include "net/http/http_cache.h"
 
 namespace cobalt {
 namespace h5vcc {
@@ -56,6 +57,10 @@
   H5vccStorageSetQuotaResponse SetQuota(
       H5vccStorageResourceTypeQuotaBytesDictionary quota);
 
+  void EnableCache();
+
+  void DisableCache();
+
   DEFINE_WRAPPABLE_TYPE(H5vccStorage);
 
  private:
@@ -63,6 +68,8 @@
 
   persistent_storage::PersistentSettings* persistent_settings_;
 
+  net::HttpCache* http_cache_;
+
   DISALLOW_COPY_AND_ASSIGN(H5vccStorage);
 };
 
diff --git a/cobalt/h5vcc/h5vcc_storage.idl b/cobalt/h5vcc/h5vcc_storage.idl
index 3a45910..93a6d37 100644
--- a/cobalt/h5vcc/h5vcc_storage.idl
+++ b/cobalt/h5vcc/h5vcc_storage.idl
@@ -24,4 +24,7 @@
 
   H5vccStorageResourceTypeQuotaBytesDictionary getQuota();
   H5vccStorageSetQuotaResponse setQuota(H5vccStorageResourceTypeQuotaBytesDictionary quota);
+
+  void enableCache();
+  void disableCache();
 };
diff --git a/cobalt/network/BUILD.gn b/cobalt/network/BUILD.gn
index a68d213..53821e9 100644
--- a/cobalt/network/BUILD.gn
+++ b/cobalt/network/BUILD.gn
@@ -51,6 +51,7 @@
     "//cobalt/build:cobalt_build_id",
     "//cobalt/configuration",
     "//cobalt/network_bridge",
+    "//cobalt/persistent_storage:persistent_settings",
     "//cobalt/storage",
     "//starboard/common",
     "//third_party/protobuf:protobuf_lite",
diff --git a/cobalt/network/network_module.cc b/cobalt/network/network_module.cc
index 4eae9a8..f192961 100644
--- a/cobalt/network/network_module.cc
+++ b/cobalt/network/network_module.cc
@@ -178,7 +178,8 @@
 #endif
   url_request_context_.reset(
       new URLRequestContext(storage_manager_, options_.custom_proxy, net_log,
-                            options_.ignore_certificate_errors, task_runner()));
+                            options_.ignore_certificate_errors, task_runner(),
+                            options_.persistent_settings));
   network_delegate_.reset(
       new NetworkDelegate(options_.cookie_policy, options_.https_requirement));
   url_request_context_->set_http_user_agent_settings(
diff --git a/cobalt/network/network_module.h b/cobalt/network/network_module.h
index be655f9..ee04620 100644
--- a/cobalt/network/network_module.h
+++ b/cobalt/network/network_module.h
@@ -28,6 +28,7 @@
 #include "cobalt/network/network_delegate.h"
 #include "cobalt/network/url_request_context.h"
 #include "cobalt/network/url_request_context_getter.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/base/static_cookie_policy.h"
 #include "url/gurl.h"
 #if defined(DIAL_SERVER)
@@ -60,13 +61,15 @@
           ignore_certificate_errors(false),
           https_requirement(network::kHTTPSRequired),
           preferred_language("en-US"),
-          max_network_delay(0) {}
+          max_network_delay(0),
+          persistent_settings(nullptr) {}
     net::StaticCookiePolicy::Type cookie_policy;
     bool ignore_certificate_errors;
     HTTPSRequirement https_requirement;
     std::string preferred_language;
     std::string custom_proxy;
     SbTime max_network_delay;
+    persistent_storage::PersistentSettings* persistent_settings;
   };
 
   // Simple constructor intended to be used only by tests.
diff --git a/cobalt/network/url_request_context.cc b/cobalt/network/url_request_context.cc
index 020410c..34cc482 100644
--- a/cobalt/network/url_request_context.cc
+++ b/cobalt/network/url_request_context.cc
@@ -26,12 +26,14 @@
 #include "cobalt/network/persistent_cookie_store.h"
 #include "cobalt/network/proxy_config_service.h"
 #include "cobalt/network/switches.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/cert/cert_net_fetcher.h"
 #include "net/cert/cert_verifier.h"
 #include "net/cert/cert_verify_proc.h"
 #include "net/cert/ct_policy_enforcer.h"
 #include "net/cert/do_nothing_ct_verifier.h"
 #include "net/cert_net/cert_net_fetcher_impl.h"
+#include "net/disk_cache/cobalt/cobalt_backend_impl.h"
 #include "net/dns/host_cache.h"
 #include "net/http/http_auth_handler_factory.h"
 #include "net/http/http_cache.h"
@@ -68,7 +70,8 @@
 URLRequestContext::URLRequestContext(
     storage::StorageManager* storage_manager, const std::string& custom_proxy,
     net::NetLog* net_log, bool ignore_certificate_errors,
-    scoped_refptr<base::SingleThreadTaskRunner> network_task_runner)
+    scoped_refptr<base::SingleThreadTaskRunner> network_task_runner,
+    persistent_storage::PersistentSettings* persistent_settings)
     : ALLOW_THIS_IN_INITIALIZER_LIST(storage_(this))
 #if defined(ENABLE_DEBUGGER)
       ,
@@ -176,6 +179,8 @@
         std::unique_ptr<net::HttpNetworkLayer>(
             new net::HttpNetworkLayer(storage_.http_network_session())));
   } else {
+    using_http_cache_ = true;
+
     // TODO: Set max size of cache in Starboard.
     const int cache_size_mb = 24;
     auto http_cache = std::make_unique<net::HttpCache>(
@@ -185,6 +190,15 @@
             base::FilePath(std::string(path.data())),
             /* max_bytes */ 1024 * 1024 * cache_size_mb),
         true);
+    if (persistent_settings != nullptr) {
+      auto cache_enabled = persistent_settings->GetPersistentSettingAsBool(
+          disk_cache::kCacheEnabledPersistentSettingsKey, true);
+
+      if (!cache_enabled) {
+        http_cache->set_mode(net::HttpCache::Mode::DISABLE);
+      }
+    }
+
     storage_.set_http_transaction_factory(std::move(http_cache));
   }
 
@@ -214,6 +228,8 @@
   storage_.http_network_session()->SetEnableQuic(enable_quic);
 }
 
+bool URLRequestContext::using_http_cache() { return using_http_cache_; }
+
 #if defined(ENABLE_DEBUGGER)
 void URLRequestContext::OnQuicToggle(const std::string& message) {
   DCHECK(storage_.http_network_session());
diff --git a/cobalt/network/url_request_context.h b/cobalt/network/url_request_context.h
index 074fcdb..8f673c3 100644
--- a/cobalt/network/url_request_context.h
+++ b/cobalt/network/url_request_context.h
@@ -20,6 +20,7 @@
 #include "base/basictypes.h"
 #include "base/macros.h"
 #include "base/threading/thread_checker.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/cookies/cookie_monster.h"
 #include "net/log/net_log.h"
 #include "net/url_request/url_request_context.h"
@@ -42,19 +43,24 @@
   URLRequestContext(
       storage::StorageManager* storage_manager, const std::string& custom_proxy,
       net::NetLog* net_log, bool ignore_certificate_errors,
-      scoped_refptr<base::SingleThreadTaskRunner> network_task_runner);
+      scoped_refptr<base::SingleThreadTaskRunner> network_task_runner,
+      persistent_storage::PersistentSettings* persistent_settings);
   ~URLRequestContext() override;
 
   void SetProxy(const std::string& custom_proxy_rules);
 
   void SetEnableQuic(bool enable_quic);
 
+  bool using_http_cache();
+
  private:
   THREAD_CHECKER(thread_checker_);
   net::URLRequestContextStorage storage_;
   scoped_refptr<net::CookieMonster::PersistentCookieStore>
       persistent_cookie_store_;
 
+  bool using_http_cache_;
+
 #if defined(ENABLE_DEBUGGER)
   // Command handler object for toggling the input fuzzer on/off.
   debug::console::ConsoleCommandManager::CommandHandler
diff --git a/cobalt/persistent_storage/persistent_settings.cc b/cobalt/persistent_storage/persistent_settings.cc
index d420c0e..ffebc2a 100644
--- a/cobalt/persistent_storage/persistent_settings.cc
+++ b/cobalt/persistent_storage/persistent_settings.cc
@@ -12,11 +12,12 @@
 // 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>
 
 #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"
diff --git a/cobalt/script/promise.h b/cobalt/script/promise.h
index 6fa4b54..d9e1c17 100644
--- a/cobalt/script/promise.h
+++ b/cobalt/script/promise.h
@@ -15,6 +15,9 @@
 #ifndef COBALT_SCRIPT_PROMISE_H_
 #define COBALT_SCRIPT_PROMISE_H_
 
+#include <memory>
+
+#include "base/callback.h"
 #include "base/memory/ref_counted.h"
 #include "cobalt/script/exception_message.h"
 #include "cobalt/script/script_exception.h"
@@ -42,21 +45,19 @@
  public:
   // Call the |resolve| function that was passed as an argument to the Promise's
   // executor function supplying |result| as its argument.
-  virtual void Resolve(const T& result) const { NOTREACHED(); }
+  virtual void Resolve(const T& result) const = 0;
 
   // Call the |reject| function passed as an argument to the Promise's executor
   // function.
-  virtual void Reject() const { NOTREACHED(); }
-  virtual void Reject(SimpleExceptionType exception) const { NOTREACHED(); }
-  virtual void Reject(const scoped_refptr<ScriptException>& result) const {
-    NOTREACHED();
-  }
+  virtual void Reject() const = 0;
+  virtual void Reject(SimpleExceptionType exception) const = 0;
+  virtual void Reject(const scoped_refptr<ScriptException>& result) const = 0;
 
   // Returns the value of the [[PromiseState]] field.
-  virtual PromiseState State() const {
-    NOTREACHED();
-    return PromiseState::kRejected;
-  }
+  virtual PromiseState State() const = 0;
+
+  virtual void AddStateChangeCallback(
+      std::unique_ptr<base::OnceCallback<void()>> callback) = 0;
 
   virtual ~Promise() {}
 };
diff --git a/cobalt/script/v8c/conversion_helpers.h b/cobalt/script/v8c/conversion_helpers.h
index 85aa597..81bf303 100644
--- a/cobalt/script/v8c/conversion_helpers.h
+++ b/cobalt/script/v8c/conversion_helpers.h
@@ -16,7 +16,6 @@
 #define COBALT_SCRIPT_V8C_CONVERSION_HELPERS_H_
 
 #include <cmath>
-
 #include <limits>
 #include <string>
 #include <utility>
@@ -29,6 +28,7 @@
 #include "cobalt/base/compiler.h"
 #include "cobalt/base/enable_if.h"
 #include "cobalt/base/token.h"
+#include "cobalt/script/promise.h"
 #include "cobalt/script/sequence.h"
 #include "cobalt/script/v8c/algorithm_helpers.h"
 #include "cobalt/script/v8c/helpers.h"
@@ -713,11 +713,7 @@
 template <typename T>
 void FromJSValue(v8::Isolate* isolate, v8::Local<v8::Value> value,
                  int conversion_flags, ExceptionState* exception_state,
-                 script::Promise<T>* out_promise) {
-  // TODO(b/228976500): Implement conversion from JS to native for Promise<T>.
-  // https://webidl.spec.whatwg.org/#es-promise
-  NOTIMPLEMENTED();
-}
+                 script::Promise<T>* out_promise);
 
 // script::Handle<T> -> JSValue
 template <typename T>
diff --git a/cobalt/script/v8c/native_promise.h b/cobalt/script/v8c/native_promise.h
index 85069c7..ab8b323 100644
--- a/cobalt/script/v8c/native_promise.h
+++ b/cobalt/script/v8c/native_promise.h
@@ -15,12 +15,15 @@
 #ifndef COBALT_SCRIPT_V8C_NATIVE_PROMISE_H_
 #define COBALT_SCRIPT_V8C_NATIVE_PROMISE_H_
 
+#include <memory>
+
 #include "base/logging.h"
 #include "base/threading/thread_checker.h"
 #include "cobalt/script/promise.h"
 #include "cobalt/script/v8c/conversion_helpers.h"
 #include "cobalt/script/v8c/entry_scope.h"
 #include "cobalt/script/v8c/scoped_persistent.h"
+#include "cobalt/script/v8c/script_promise.h"
 #include "cobalt/script/v8c/type_traits.h"
 #include "cobalt/script/v8c/v8c_exception_state.h"
 #include "cobalt/script/v8c/v8c_user_object_holder.h"
@@ -40,10 +43,9 @@
   *out_value = v8::Undefined(isolate);
 }
 
-// Shared functionality for NativePromise<T>. Does not implement the Resolve
-// function, since that needs to be specialized for Promise<T>.
+// Shared functionality for NativePromise<T>.
 template <typename T>
-class NativePromise : public ScopedPersistent<v8::Value>, public Promise<T> {
+class NativePromise : public ScriptPromise<T> {
  public:
   // ScriptValue boilerplate.
   typedef Promise<T> BaseType;
@@ -63,20 +65,20 @@
                                 PromiseResultUndefined, T>::type;
 
   NativePromise(v8::Isolate* isolate, v8::Local<v8::Value> resolver)
-      : isolate_(isolate), ScopedPersistent(isolate, resolver) {
+      : ScriptPromise<T>(isolate, resolver) {
     DCHECK(resolver->IsPromise());
   }
 
   void Resolve(const ResolveType& value) const override {
     DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
     DCHECK(!this->IsEmpty());
-    DCHECK(State() == PromiseState::kPending);
-    EntryScope entry_scope(isolate_);
-    v8::Local<v8::Context> context = isolate_->GetCurrentContext();
+    DCHECK(this->State() == PromiseState::kPending);
+    EntryScope entry_scope(this->isolate());
+    v8::Local<v8::Context> context = this->isolate()->GetCurrentContext();
 
     v8::Local<v8::Promise::Resolver> promise_resolver = this->resolver();
     v8::Local<v8::Value> converted_value;
-    ToJSValue(isolate_, value, &converted_value);
+    ToJSValue(this->isolate(), value, &converted_value);
     v8::Maybe<bool> reject_result =
         promise_resolver->Resolve(context, converted_value);
     DCHECK(reject_result.FromJust());
@@ -85,25 +87,26 @@
   void Reject() const override {
     DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
     DCHECK(!this->IsEmpty());
-    DCHECK(State() == PromiseState::kPending);
-    EntryScope entry_scope(isolate_);
-    v8::Local<v8::Context> context = isolate_->GetCurrentContext();
+    DCHECK(this->State() == PromiseState::kPending);
+    EntryScope entry_scope(this->isolate());
+    v8::Local<v8::Context> context = this->isolate()->GetCurrentContext();
 
     v8::Local<v8::Promise::Resolver> promise_resolver = this->resolver();
     v8::Maybe<bool> reject_result =
-        promise_resolver->Reject(context, v8::Undefined(isolate_));
+        promise_resolver->Reject(context, v8::Undefined(this->isolate()));
     DCHECK(reject_result.FromJust());
   }
 
   void Reject(SimpleExceptionType exception) const override {
     DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
     DCHECK(!this->IsEmpty());
-    DCHECK(State() == PromiseState::kPending);
-    EntryScope entry_scope(isolate_);
-    v8::Local<v8::Context> context = isolate_->GetCurrentContext();
+    DCHECK(this->State() == PromiseState::kPending);
+    EntryScope entry_scope(this->isolate());
+    v8::Local<v8::Context> context = this->isolate()->GetCurrentContext();
 
     v8::Local<v8::Promise::Resolver> promise_resolver = this->resolver();
-    v8::Local<v8::Value> error_result = CreateErrorObject(isolate_, exception);
+    v8::Local<v8::Value> error_result =
+        CreateErrorObject(this->isolate(), exception);
     v8::Maybe<bool> reject_result =
         promise_resolver->Reject(context, error_result);
     DCHECK(reject_result.FromJust());
@@ -112,51 +115,22 @@
   void Reject(const scoped_refptr<ScriptException>& result) const override {
     DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
     DCHECK(!this->IsEmpty());
-    DCHECK(State() == PromiseState::kPending);
-    EntryScope entry_scope(isolate_);
-    v8::Local<v8::Context> context = isolate_->GetCurrentContext();
+    DCHECK(this->State() == PromiseState::kPending);
+    EntryScope entry_scope(this->isolate());
+    v8::Local<v8::Context> context = this->isolate()->GetCurrentContext();
 
     v8::Local<v8::Promise::Resolver> promise_resolver = this->resolver();
     v8::Local<v8::Value> converted_result;
-    ToJSValue(isolate_, result, &converted_result);
+    ToJSValue(this->isolate(), result, &converted_result);
     v8::Maybe<bool> reject_result =
         promise_resolver->Reject(context, converted_result);
     DCHECK(reject_result.FromJust());
   }
 
-  PromiseState State() const override {
-    DCHECK(!this->IsEmpty());
-    EntryScope entry_scope(isolate_);
-
-    v8::Promise::PromiseState v8_promise_state = this->promise()->State();
-    switch (v8_promise_state) {
-      case v8::Promise::kPending:
-        return PromiseState::kPending;
-      case v8::Promise::kFulfilled:
-        return PromiseState::kFulfilled;
-      case v8::Promise::kRejected:
-        return PromiseState::kRejected;
-    }
-    NOTREACHED();
-    return PromiseState::kRejected;
-  }
-
-  v8::Local<v8::Promise> promise() const {
-    DCHECK(!this->IsEmpty());
-    return resolver()->GetPromise();
-  }
-
  private:
-  v8::Isolate* isolate_;
-
   // Thread checker ensures all calls to the Promise are made from the same
   // thread that it is created in.
   THREAD_CHECKER(thread_checker_);
-
-  v8::Local<v8::Promise::Resolver> resolver() const {
-    DCHECK(!this->IsEmpty());
-    return this->Get().Get(isolate_).template As<v8::Promise::Resolver>();
-  }
 };
 
 template <typename T>
diff --git a/cobalt/script/v8c/script_promise.h b/cobalt/script/v8c/script_promise.h
new file mode 100644
index 0000000..93c4561
--- /dev/null
+++ b/cobalt/script/v8c/script_promise.h
@@ -0,0 +1,148 @@
+// 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.
+
+#ifndef COBALT_SCRIPT_V8C_SCRIPT_PROMISE_H_
+#define COBALT_SCRIPT_V8C_SCRIPT_PROMISE_H_
+
+#include <memory>
+#include <utility>
+
+#include "base/logging.h"
+#include "base/threading/thread_checker.h"
+#include "cobalt/script/promise.h"
+#include "cobalt/script/v8c/conversion_helpers.h"
+#include "cobalt/script/v8c/entry_scope.h"
+#include "cobalt/script/v8c/scoped_persistent.h"
+#include "cobalt/script/v8c/type_traits.h"
+#include "cobalt/script/v8c/v8c_exception_state.h"
+#include "cobalt/script/v8c/v8c_user_object_holder.h"
+#include "v8/include/v8.h"
+
+namespace cobalt {
+namespace script {
+namespace v8c {
+
+// Shared functionality for ScriptPromise<T>. Does not implement the Resolve
+// function, since that needs to be specialized for Promise<T>.
+template <typename T>
+class ScriptPromise : public ScopedPersistent<v8::Value>, public Promise<T> {
+ public:
+  // ScriptValue boilerplate.
+  typedef Promise<T> BaseType;
+
+  // Handle special case T=void, by swapping the input parameter |T| for
+  // |PromiseResultUndefined|. Combined with how |Promise| handles this
+  // special case, we're left with something like:
+  //
+  //   NativePromise<T>    ->            Promise<T>
+  //                                         ^
+  //                                         | (T=PromiseResultUndefined)
+  //                                        /
+  //   NativePromise<void> -> Promise<void>
+  //
+  using ResolveType =
+      typename std::conditional<std::is_same<T, void>::value,
+                                PromiseResultUndefined, T>::type;
+
+  ScriptPromise(v8::Isolate* isolate, v8::Local<v8::Value> resolver)
+      : isolate_(isolate), ScopedPersistent(isolate, resolver) {
+    DCHECK(resolver->IsPromise());
+  }
+
+  void Resolve(const ResolveType& value) const override { NOTREACHED(); }
+
+  void Reject() const override { NOTREACHED(); }
+
+  void Reject(SimpleExceptionType exception) const override { NOTREACHED(); }
+
+  void Reject(const scoped_refptr<ScriptException>& result) const override {
+    NOTREACHED();
+  }
+
+  PromiseState State() const override {
+    DCHECK(!this->IsEmpty());
+    EntryScope entry_scope(isolate_);
+
+    v8::Promise::PromiseState v8_promise_state = this->promise()->State();
+    switch (v8_promise_state) {
+      case v8::Promise::kPending:
+        return PromiseState::kPending;
+      case v8::Promise::kFulfilled:
+        return PromiseState::kFulfilled;
+      case v8::Promise::kRejected:
+        return PromiseState::kRejected;
+    }
+    NOTREACHED();
+    return PromiseState::kRejected;
+  }
+
+  void AddStateChangeCallback(
+      std::unique_ptr<base::OnceCallback<void()>> callback) override {
+    v8::Local<v8::Context> context = context_.NewLocal(isolate_);
+
+    auto callback_lambda = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+      auto* callback = static_cast<base::OnceCallback<void()>*>(
+          info.Data().As<v8::External>()->Value());
+      DCHECK(callback);
+      std::move(*callback).Run();
+      delete callback;
+    };
+    v8::Local<v8::Function> function =
+        v8::Function::New(isolate_->GetCurrentContext(), callback_lambda,
+                          v8::External::New(isolate_, callback.release()))
+            .ToLocalChecked();
+    v8::Local<v8::Promise> result_promise;
+    if (!promise()
+             ->Then(context, function, function)
+             .ToLocal(&result_promise)) {
+      DLOG(ERROR) << "Unable to add promise state change callback.";
+      NOTREACHED();
+    }
+  }
+
+  v8::Local<v8::Promise> promise() const {
+    DCHECK(!this->IsEmpty());
+    return resolver()->GetPromise();
+  }
+
+ protected:
+  v8::Local<v8::Promise::Resolver> resolver() const {
+    DCHECK(!this->IsEmpty());
+    return this->Get().Get(isolate_).template As<v8::Promise::Resolver>();
+  }
+
+  v8::Isolate* isolate() const { return isolate_; }
+
+ private:
+  v8::Isolate* isolate_;
+  ScopedPersistent<v8::Context> context_;
+};
+
+// JSValue -> Promise
+template <typename T>
+void FromJSValue(v8::Isolate* isolate, v8::Local<v8::Value> value,
+                 int conversion_flags, ExceptionState* exception_state,
+                 std::unique_ptr<script::Promise<T*>>* out_promise) {
+  if (!value->IsPromise()) {
+    exception_state->SetSimpleException(kNotSupportedType);
+    return;
+  }
+  out_promise->reset(new ScriptPromise<T*>(isolate, value));
+}
+
+}  // namespace v8c
+}  // namespace script
+}  // namespace cobalt
+
+#endif  // COBALT_SCRIPT_V8C_SCRIPT_PROMISE_H_
diff --git a/cobalt/worker/extendable_event.h b/cobalt/worker/extendable_event.h
index 4e5d4ff..1765ad2 100644
--- a/cobalt/worker/extendable_event.h
+++ b/cobalt/worker/extendable_event.h
@@ -15,15 +15,26 @@
 #ifndef COBALT_WORKER_EXTENDABLE_EVENT_H_
 #define COBALT_WORKER_EXTENDABLE_EVENT_H_
 
+#include <memory>
 #include <string>
+#include <utility>
 
+#include "base/bind.h"
 #include "cobalt/base/token.h"
 #include "cobalt/script/promise.h"
 #include "cobalt/script/v8c/native_promise.h"
 #include "cobalt/script/value_handle.h"
 #include "cobalt/script/wrappable.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/dom_exception.h"
+#include "cobalt/web/environment_settings.h"
 #include "cobalt/web/event.h"
+#include "cobalt/web/window_or_worker_global_scope.h"
 #include "cobalt/worker/extendable_event_init.h"
+#include "cobalt/worker/service_worker_global_scope.h"
+#include "cobalt/worker/service_worker_jobs.h"
+#include "cobalt/worker/service_worker_object.h"
+#include "cobalt/worker/service_worker_registration_object.h"
 
 namespace cobalt {
 namespace worker {
@@ -35,16 +46,87 @@
   ExtendableEvent(const std::string& type, const ExtendableEventInit& init_dict)
       : Event(type, init_dict) {}
 
-  void WaitUntil(script::EnvironmentSettings* settings,
-                 const script::Promise<script::ValueHandle>& promise) {
-    // TODO(b/228976500): Implement WaitUntil().
-    NOTIMPLEMENTED();
+  void WaitUntil(
+      script::EnvironmentSettings* settings,
+      std::unique_ptr<script::Promise<script::ValueHandle*>>& promise,
+      script::ExceptionState* exception_state) {
+    // Algorithm for waitUntil(), to add lifetime promise to event.
+    //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+
+    // 1. If event’s isTrusted attribute is false, throw an "InvalidStateError"
+    //    DOMException.
+    // 2. If event is not active, throw an "InvalidStateError" DOMException.
+    if (!IsActive()) {
+      web::DOMException::Raise(web::DOMException::kInvalidStateErr,
+                               exception_state);
+      return;
+    }
+    // 3. Add promise to event’s extend lifetime promises.
+    // 4. Increment event’s pending promises count by one.
+    ++pending_promise_count_;
+    // 5. Upon fulfillment or rejection of promise, queue a microtask to run
+    //    these substeps:
+    std::unique_ptr<base::OnceCallback<void()>> callback(
+        new base::OnceCallback<void()>(std::move(
+            base::BindOnce(&ExtendableEvent::StateChange,
+                           base::Unretained(this), settings, promise.get()))));
+    promise->AddStateChangeCallback(std::move(callback));
+    promise.release();
+  }
+
+  void StateChange(script::EnvironmentSettings* settings,
+                   const script::Promise<script::ValueHandle*>* promise) {
+    // Implement the microtask called upon fulfillment or rejection of a
+    // promise, as part of the algorithm for waitUntil().
+    //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+    DCHECK(promise);
+    has_rejected_promise_ |=
+        promise->State() == script::PromiseState::kRejected;
+    // 5.1. Decrement event’s pending promises count by one.
+    --pending_promise_count_;
+    // 5.2. If event’s pending promises count is 0, then:
+    if (0 == pending_promise_count_) {
+      web::Context* context =
+          base::polymorphic_downcast<web::EnvironmentSettings*>(settings)
+              ->context();
+      ServiceWorkerJobs* jobs = context->service_worker_jobs();
+      DCHECK(jobs);
+      // 5.2.1. Let registration be the current global object's associated
+      //        service worker's containing service worker registration.
+      jobs->message_loop()->task_runner()->PostTask(
+          FROM_HERE,
+          base::BindOnce(&ServiceWorkerJobs::WaitUntilSubSteps,
+                         base::Unretained(jobs),
+                         base::Unretained(
+                             context->GetWindowOrWorkerGlobalScope()
+                                 ->AsServiceWorker()
+                                 ->service_worker_object()
+                                 ->containing_service_worker_registration())));
+    }
+    delete promise;
+  }
+
+  bool IsActive() {
+    // An ExtendableEvent object is said to be active when its timed out flag
+    // is unset and either its pending promises count is greater than zero or
+    // its dispatch flag is set.
+    //   https://w3c.github.io/ServiceWorker/#extendableevent-active
+    return !timed_out_flag_ &&
+           ((pending_promise_count_ > 0) || IsBeingDispatched());
   }
 
   DEFINE_WRAPPABLE_TYPE(ExtendableEvent);
 
  protected:
   ~ExtendableEvent() override {}
+
+ private:
+  // https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises
+  // std::list<script::Promise<script::ValueHandle*>> extend_lifetime_promises_;
+  int pending_promise_count_ = 0;
+  bool has_rejected_promise_ = false;
+  // https://w3c.github.io/ServiceWorker/#extendableevent-timed-out-flag
+  bool timed_out_flag_ = false;
 };
 
 }  // namespace worker
diff --git a/cobalt/worker/extendable_event.idl b/cobalt/worker/extendable_event.idl
index 2297cbc..8a12344 100644
--- a/cobalt/worker/extendable_event.idl
+++ b/cobalt/worker/extendable_event.idl
@@ -18,5 +18,5 @@
   Exposed = ServiceWorker,
   Constructor(DOMString type, optional ExtendableEventInit eventInitDict)
 ] interface ExtendableEvent : Event {
-  [CallWith = EnvironmentSettings] void waitUntil(Promise<any> f);
+  [RaisesException, CallWith = EnvironmentSettings] void waitUntil(Promise<any> f);
 };
diff --git a/cobalt/worker/service_worker_jobs.cc b/cobalt/worker/service_worker_jobs.cc
index f901854..9ba0101 100644
--- a/cobalt/worker/service_worker_jobs.cc
+++ b/cobalt/worker/service_worker_jobs.cc
@@ -44,6 +44,7 @@
 #include "cobalt/worker/client.h"
 #include "cobalt/worker/client_query_options.h"
 #include "cobalt/worker/client_type.h"
+#include "cobalt/worker/extendable_event.h"
 #include "cobalt/worker/frame_type.h"
 #include "cobalt/worker/service_worker.h"
 #include "cobalt/worker/service_worker_container.h"
@@ -891,7 +892,7 @@
                     // 11.3.1.2. Initialize e’s type attribute to install.
                     // 11.3.1.3. Dispatch e at installingWorker’s global object.
                     installing_worker->worker_global_scope()->DispatchEvent(
-                        new web::Event(base::Tokens::install()));
+                        new ExtendableEvent(base::Tokens::install()));
                     // 11.3.1.4. WaitForAsynchronousExtensions: Run the
                     //           following substeps in parallel:
                     // 11.3.1.4.1. Wait until e is not active.
@@ -1137,7 +1138,7 @@
                     // 11.1.1.2. Initialize e’s type attribute to activate.
                     // 11.1.1.3. Dispatch e at activeWorker’s global object.
                     active_worker->worker_global_scope()->DispatchEvent(
-                        new web::Event(base::Tokens::activate()));
+                        new ExtendableEvent(base::Tokens::activate()));
                     // 11.1.1.4. WaitForAsynchronousExtensions: Wait, in
                     //           parallel, until e is not active.
                   },
@@ -1837,6 +1838,23 @@
           std::move(promise_reference)));
 }
 
+void ServiceWorkerJobs::WaitUntilSubSteps(
+    ServiceWorkerRegistrationObject* registration) {
+  TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::WaitUntilSubSteps()");
+  DCHECK_EQ(message_loop_, base::MessageLoop::current());
+  // Sub steps for WaitUntil.
+  //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+  // 5.2.2. If registration is unregistered, invoke Try Clear Registration
+  //        with registration.
+  if (scope_to_registration_map_.IsUnregistered(registration)) {
+    TryClearRegistration(registration);
+  }
+  // 5.2.3. If registration is not null, invoke Try Activate with
+  //        registration.
+  if (registration) {
+    TryActivate(registration);
+  }
+}
 void ServiceWorkerJobs::ClientsGetSubSteps(
     web::EnvironmentSettings* settings,
     ServiceWorkerObject* associated_service_worker,
diff --git a/cobalt/worker/service_worker_jobs.h b/cobalt/worker/service_worker_jobs.h
index 89dd24a..8e09831 100644
--- a/cobalt/worker/service_worker_jobs.h
+++ b/cobalt/worker/service_worker_jobs.h
@@ -204,6 +204,10 @@
       const base::WeakPtr<ServiceWorkerObject>& service_worker,
       std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
 
+  // Sub steps for WaitUntil.
+  //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+  void WaitUntilSubSteps(ServiceWorkerRegistrationObject* registration);
+
   // Parallel sub steps (2) for algorithm for Clients.get(id):
   //   https://w3c.github.io/ServiceWorker/#clients-get
   void ClientsGetSubSteps(
diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.h b/net/disk_cache/cobalt/cobalt_backend_impl.h
index c2b82f7..5a4443b 100644
--- a/net/disk_cache/cobalt/cobalt_backend_impl.h
+++ b/net/disk_cache/cobalt/cobalt_backend_impl.h
@@ -31,6 +31,8 @@
 
 namespace disk_cache {
 
+const char kCacheEnabledPersistentSettingsKey[] = "cacheEnabled";
+
 // This class implements the Backend interface. An object of this class handles
 // the operations of the cache without writing to disk.
 class NET_EXPORT_PRIVATE CobaltBackendImpl final : public Backend {
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
index a858ea3..4b05f41 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
@@ -29,6 +29,7 @@
 import android.view.ViewGroup.LayoutParams;
 import android.view.ViewParent;
 import android.widget.FrameLayout;
+import dev.cobalt.media.AudioOutputManager;
 import dev.cobalt.media.MediaCodecUtil;
 import dev.cobalt.media.VideoSurfaceView;
 import dev.cobalt.util.DisplayUtil;
@@ -37,6 +38,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 
 /** Native activity that has the required JNI methods called by the Starboard implementation. */
 public abstract class CobaltActivity extends NativeActivity {
@@ -130,6 +132,8 @@
     }
 
     DisplayUtil.cacheDefaultDisplay(this);
+    DisplayUtil.addDisplayListener(this);
+    AudioOutputManager.addAudioDeviceListener(this);
 
     getStarboardBridge().onActivityStart(this, keyboardEditor);
     super.onStart();
@@ -248,11 +252,12 @@
         return;
       }
 
-      String customProxy = String.format("--proxy=\"http=http://%s:%d\"", config.first, port);
+      String customProxy =
+          String.format(Locale.US, "--proxy=\"http=http://%s:%d\"", config.first, port);
       Log.i(TAG, "addCustomProxyArgs: " + customProxy);
       args.add(customProxy);
     } catch (NumberFormatException e) {
-      Log.w(TAG, String.format("http.proxyPort: %s is not valid number", config.second), e);
+      Log.w(TAG, "http.proxyPort: %s is not valid number", config.second, e);
     }
   }
 
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
index f131da7..758030d 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
@@ -29,6 +29,7 @@
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
+import java.util.Locale;
 
 /** Loads MediaImage artwork, and caches one image. */
 public class ArtworkLoader {
@@ -97,7 +98,7 @@
   private Size parseImageSize(MediaImage image) {
     try {
       String sizeStr = image.sizes.split("\\s+", -1)[0];
-      return Size.parseSize(sizeStr.toLowerCase());
+      return Size.parseSize(sizeStr.toLowerCase(Locale.US));
     } catch (NumberFormatException | NullPointerException e) {
       return new Size(0, 0);
     }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
index 5a13b01..ebd8967 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
@@ -30,6 +30,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Creates and destroys AudioTrackBridge and handles the volume change. */
@@ -103,9 +104,9 @@
                 // AudioFormat.
                 Log.v(
                     TAG,
-                    String.format(
-                        "Setting |hasAudioDeviceChanged| to true for audio device %s, %s.",
-                        info.getProductName(), getDeviceTypeNameV23(info.getType())));
+                    "Setting |hasAudioDeviceChanged| to true for audio device %s, %s.",
+                    info.getProductName(),
+                    getDeviceTypeNameV23(info.getType()));
                 hasAudioDeviceChanged.set(true);
                 break;
               }
@@ -116,9 +117,8 @@
           public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
             Log.v(
                 TAG,
-                String.format(
-                    "onAudioDevicesAdded() called, |initialDevicesAdded| is: %b.",
-                    initialDevicesAdded));
+                "onAudioDevicesAdded() called, |initialDevicesAdded| is: %b.",
+                initialDevicesAdded);
             if (initialDevicesAdded) {
               handleConnectedDeviceChange(addedDevices);
               return;
@@ -236,7 +236,7 @@
         return "TYPE_WIRED_HEADSET";
       default:
         // This may include constants introduced after API 23.
-        return String.format("TYPE_UNKNOWN (%d)", device_type);
+        return String.format(Locale.US, "TYPE_UNKNOWN (%d)", device_type);
     }
   }
 
@@ -278,7 +278,7 @@
           break;
         default:
           // This may include constants introduced after API 23.
-          encodings_in_string.append(String.format("UNKNOWN (%d)", encodings[i]));
+          encodings_in_string.append(String.format(Locale.US, "UNKNOWN (%d)", encodings[i]));
           break;
       }
       if (i != encodings.length - 1) {
@@ -304,12 +304,11 @@
     for (AudioDeviceInfo info : deviceInfos) {
       Log.i(
           TAG,
-          String.format(
-              "  Audio Device: %s, channels: %s, sample rates: %s, encodings: %s",
-              getDeviceTypeNameV23(info.getType()),
-              Arrays.toString(info.getChannelCounts()),
-              Arrays.toString(info.getSampleRates()),
-              getEncodingNames(info.getEncodings())));
+          "  Audio Device: %s, channels: %s, sample rates: %s, encodings: %s",
+          getDeviceTypeNameV23(info.getType()),
+          Arrays.toString(info.getChannelCounts()),
+          Arrays.toString(info.getSampleRates()),
+          getEncodingNames(info.getEncodings()));
     }
   }
 
@@ -348,10 +347,11 @@
       if (AudioTrackBridge.AV_SYNC_HEADER_V1_SIZE % frameSizeInBytes != 0) {
         Log.w(
             TAG,
-            String.format(
-                "Disable tunnel mode due to sampleSizeInBytes (%d) * numberOfChannels (%d) isn't"
-                    + " aligned to AV_SYNC_HEADER_V1_SIZE (%d).",
-                sampleSizeInBytes, numberOfChannels, AudioTrackBridge.AV_SYNC_HEADER_V1_SIZE));
+            "Disable tunnel mode due to sampleSizeInBytes (%d) * numberOfChannels (%d) isn't"
+                + " aligned to AV_SYNC_HEADER_V1_SIZE (%d).",
+            sampleSizeInBytes,
+            numberOfChannels,
+            AudioTrackBridge.AV_SYNC_HEADER_V1_SIZE);
         return -1;
       }
     }
@@ -366,10 +366,10 @@
     if (Build.VERSION.SDK_INT < 23) {
       Log.i(
           TAG,
-          String.format(
-              "Passthrough on encoding %d is rejected on api %d, as passthrough is only"
-                  + " supported on api 23 or later.",
-              encoding, Build.VERSION.SDK_INT));
+          "Passthrough on encoding %d is rejected on api %d, as passthrough is only"
+              + " supported on api 23 or later.",
+          encoding,
+          Build.VERSION.SDK_INT);
       return false;
     }
 
@@ -382,10 +382,9 @@
       if (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
         Log.i(
             TAG,
-            String.format(
-                "Passthrough on encoding %d is disabled because Bluetooth output device is"
-                    + " connected.",
-                encoding));
+            "Passthrough on encoding %d is disabled because Bluetooth output device is"
+                + " connected.",
+            encoding);
         return false;
       }
     }
@@ -396,34 +395,31 @@
     if (hasPassthroughSupportForV23(deviceInfos, encoding)) {
       Log.i(
           TAG,
-          String.format(
-              "Passthrough on encoding %d is supported, as hasPassthroughSupportForV23() returns"
-                  + " true.",
-              encoding));
+          "Passthrough on encoding %d is supported, as hasPassthroughSupportForV23() returns"
+              + " true.",
+          encoding);
     } else {
       if (Build.VERSION.SDK_INT < 29) {
         Log.i(
             TAG,
-            String.format(
-                "Passthrough on encoding %d is rejected, as"
-                    + " hasDirectSurroundPlaybackSupportForV29() is not called for api %d.",
-                encoding, Build.VERSION.SDK_INT));
+            "Passthrough on encoding %d is rejected, as"
+                + " hasDirectSurroundPlaybackSupportForV29() is not called for api %d.",
+            encoding,
+            Build.VERSION.SDK_INT);
         return false;
       }
       if (hasDirectSurroundPlaybackSupportForV29(encoding, DEFAULT_SURROUND_SAMPLE_RATE)) {
         Log.i(
             TAG,
-            String.format(
-                "Passthrough on encoding %d is supported, as"
-                    + " hasDirectSurroundPlaybackSupportForV29() returns true.",
-                encoding));
+            "Passthrough on encoding %d is supported, as"
+                + " hasDirectSurroundPlaybackSupportForV29() returns true.",
+            encoding);
       } else {
         Log.i(
             TAG,
-            String.format(
-                "Passthrough on encoding %d is not supported, as"
-                    + " hasDirectSurroundPlaybackSupportForV29() returns false.",
-                encoding));
+            "Passthrough on encoding %d is not supported, as"
+                + " hasDirectSurroundPlaybackSupportForV29() returns false.",
+            encoding);
         return false;
       }
     }
@@ -445,10 +441,9 @@
       // HDMI and SPDIF are connected, where the output should fallback to AC3.
       Log.w(
           TAG,
-          String.format(
-              "Passthrough on encoding %d is disabled because creating AudioTrack raises"
-                  + " exception: ",
-              encoding),
+          "Passthrough on encoding %d is disabled because creating AudioTrack raises"
+              + " exception: ",
+          encoding,
           e);
       return false;
     }
@@ -475,31 +470,29 @@
         // an empty array indicates that the device supports arbitrary encodings.
         Log.i(
             TAG,
-            String.format(
-                "Passthrough on encoding %d is supported on %s, because getEncodings() returns"
-                    + " an empty array.",
-                encoding, getDeviceTypeNameV23(type)));
+            "Passthrough on encoding %d is supported on %s, because getEncodings() returns"
+                + " an empty array.",
+            encoding,
+            getDeviceTypeNameV23(type));
         return true;
       }
       for (int i = 0; i < encodings.length; ++i) {
         if (encodings[i] == encoding) {
           Log.i(
               TAG,
-              String.format(
-                  "Passthrough on encoding %d is supported on %s.",
-                  encoding, getDeviceTypeNameV23(type)));
+              "Passthrough on encoding %d is supported on %s.",
+              encoding,
+              getDeviceTypeNameV23(type));
           return true;
         }
       }
       Log.i(
           TAG,
-          String.format(
-              "Passthrough on encoding %d is not supported on %s.",
-              encoding, getDeviceTypeNameV23(type)));
+          "Passthrough on encoding %d is not supported on %s.",
+          encoding,
+          getDeviceTypeNameV23(type));
     }
-    Log.i(
-        TAG,
-        String.format("Passthrough on encoding %d is not supported on any devices.", encoding));
+    Log.i(TAG, "Passthrough on encoding %d is not supported on any devices.", encoding);
     return false;
   }
 
@@ -511,19 +504,15 @@
         && encoding != AudioFormat.ENCODING_E_AC3_JOC) {
       Log.w(
           TAG,
-          String.format(
-              "hasDirectSurroundPlaybackSupportForV29() encountered unsupported encoding %d.",
-              encoding));
+          "hasDirectSurroundPlaybackSupportForV29() encountered unsupported encoding %d.",
+          encoding);
       return false;
     }
 
     boolean supported =
         AudioTrack.isDirectPlaybackSupported(
             getPassthroughAudioFormatFor(encoding, sampleRate), getDefaultAudioAttributes());
-    Log.i(
-        TAG,
-        String.format(
-            "isDirectPlaybackSupported() for encoding %d returned %b.", encoding, supported));
+    Log.i(TAG, "isDirectPlaybackSupported() for encoding %d returned %b.", encoding, supported);
     return supported;
   }
 
@@ -550,4 +539,31 @@
   private boolean getAndResetHasAudioDeviceChanged() {
     return hasAudioDeviceChanged.getAndSet(false);
   }
+
+  private static AudioDeviceCallback audioDeviceCallback =
+      new AudioDeviceCallback() {
+        @Override
+        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+          nativeOnAudioDeviceChanged();
+        }
+
+        @Override
+        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+          nativeOnAudioDeviceChanged();
+        }
+      };
+
+  private static boolean audioDeviceListenerAdded = false;
+
+  public static void addAudioDeviceListener(Context context) {
+    if (audioDeviceListenerAdded) {
+      return;
+    }
+
+    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
+    audioDeviceListenerAdded = true;
+  }
+
+  private static native void nativeOnAudioDeviceChanged();
 }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
index 4c11399..5050078 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -27,6 +27,7 @@
 import dev.cobalt.util.UsedByNative;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Locale;
 
 /**
  * A wrapper of the android AudioTrack class. Android AudioTrack would not start playing until the
@@ -96,9 +97,12 @@
           audioTrack = null;
           String errorMessage =
               String.format(
+                  Locale.US,
                   "Enable tunnel mode when frame size is unaligned, "
                       + "sampleType: %d, channel: %d, sync header size: %d.",
-                  sampleType, channelCount, AV_SYNC_HEADER_V1_SIZE);
+                  sampleType,
+                  channelCount,
+                  AV_SYNC_HEADER_V1_SIZE);
           Log.e(TAG, errorMessage);
           throw new RuntimeException(errorMessage);
         }
@@ -160,12 +164,11 @@
     }
     Log.i(
         TAG,
-        String.format(
-            "AudioTrack created with buffer size %d (preferred: %d).  The minimum buffer size is"
-                + " %d.",
-            audioTrackBufferSize,
-            preferredBufferSizeInBytes,
-            AudioTrack.getMinBufferSize(sampleRate, channelConfig, sampleType)));
+        "AudioTrack created with buffer size %d (preferred: %d).  The minimum buffer size is"
+            + " %d.",
+        audioTrackBufferSize,
+        preferredBufferSizeInBytes,
+        AudioTrack.getMinBufferSize(sampleRate, channelConfig, sampleType));
   }
 
   public Boolean isAudioTrackValid() {
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
index a310b15..f36dad3 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
@@ -515,9 +515,11 @@
 
     Log.i(
         TAG,
-        String.format(
-            "MediaSession state: %s, position: %d ms, speed: %f x, duration: %d ms",
-            stateName, positionMs, speed, duration));
+        "MediaSession state: %s, position: %d ms, speed: %f x, duration: %d ms",
+        stateName,
+        positionMs,
+        speed,
+        duration);
 
     playbackStateBuilder =
         new PlaybackStateCompat.Builder()
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 c5c66cd..eeed7a7 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
@@ -37,6 +37,7 @@
 import dev.cobalt.util.UsedByNative;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Locale;
 
 /** A wrapper of the MediaCodec class. */
 @SuppressWarnings("unused")
@@ -141,7 +142,7 @@
         return;
       }
       if (presentationTimeUs <= mLastFrameTimestampUs) {
-        Log.v(TAG, String.format("Invalid output presentation timestamp."));
+        Log.v(TAG, "Invalid output presentation timestamp.");
         return;
       }
 
@@ -537,13 +538,10 @@
     }
     MediaCodec mediaCodec = null;
     try {
-      Log.i(TAG, String.format("Creating \"%s\" decoder.", decoderName));
+      Log.i(TAG, "Creating \"%s\" decoder.", decoderName);
       mediaCodec = MediaCodec.createByCodecName(decoderName);
     } catch (Exception e) {
-      Log.e(
-          TAG,
-          String.format("Failed to create MediaCodec: %s, DecoderName: %s", mime, decoderName),
-          e);
+      Log.e(TAG, "Failed to create MediaCodec: %s, DecoderName: %s", mime, decoderName, e);
       return null;
     }
     if (mediaCodec == null) {
@@ -609,13 +607,16 @@
     }
 
     try {
-      Log.i(TAG, String.format("Creating \"%s\" decoder.", decoderName));
+      Log.i(TAG, "Creating \"%s\" decoder.", decoderName);
       mediaCodec = MediaCodec.createByCodecName(decoderName);
     } catch (Exception e) {
       String message =
           String.format(
+              Locale.US,
               "Failed to create MediaCodec: %s, mustSupportSecure: %s," + " DecoderName: %s",
-              mime, crypto != null, decoderName);
+              mime,
+              crypto != null,
+              decoderName);
       Log.e(TAG, message, e);
       outCreateMediaCodecBridgeResult.mErrorMessage = message;
       return;
@@ -1189,8 +1190,10 @@
               + (configurationData == null
                   ? "|configurationData| is null."
                   : String.format(
+                      Locale.US,
                       "Configuration data size (%d) is less than the required size (%d).",
-                      configurationData.length, MIN_OPUS_INITIALIZATION_DATA_BUFFER_SIZE)));
+                      configurationData.length,
+                      MIN_OPUS_INITIALIZATION_DATA_BUFFER_SIZE)));
       return false;
     }
     // Both the number of samples to skip from the beginning of the stream and the amount of time
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
index 2d023be..e2e9b43 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
@@ -436,7 +436,7 @@
       // Filter blacklisted video decoders.
       String name = codecInfo.getName();
       if (!isVp9AllowListed && videoCodecDenyList.contains(name)) {
-        Log.v(TAG, String.format("Rejecting %s, reason: codec is on deny list", name));
+        Log.v(TAG, "Rejecting %s, reason: codec is on deny list", name);
         continue;
       }
       if (name.endsWith(SECURE_DECODER_SUFFIX)) {
@@ -446,7 +446,7 @@
             name.substring(0, name.length() - SECURE_DECODER_SUFFIX.length());
         if (!isVp9AllowListed && videoCodecDenyList.contains(nameWithoutSecureSuffix)) {
           String format = "Rejecting %s, reason: offpsec denylisted secure decoder";
-          Log.v(TAG, String.format(format, name));
+          Log.v(TAG, format, name);
           continue;
         }
       }
@@ -537,6 +537,7 @@
       int fps) {
     String decoderInfo =
         String.format(
+            Locale.US,
             "Searching for video decoder with parameters mimeType: %s, secure: %b, frameWidth:"
                 + " %d, frameHeight: %d, bitrate: %d, fps: %d, mustSupportHdr: %b,"
                 + " mustSupportSoftwareCodec: %b, mustSupportTunnelMode: %b,"
@@ -554,6 +555,7 @@
     Log.v(TAG, decoderInfo);
     String deviceInfo =
         String.format(
+            Locale.US,
             "brand: %s, model: %s, version: %s, API level: %d, isVp9AllowListed: %b",
             Build.BRAND,
             Build.MODEL,
@@ -606,9 +608,13 @@
           || !mustSupportSecure && requiresSecurePlayback) {
         String message =
             String.format(
+                Locale.US,
                 "Rejecting %s, reason: secure decoder requested: %b, "
                     + "codec FEATURE_SecurePlayback supported: %b, required: %b",
-                name, mustSupportSecure, supportsSecurePlayback, requiresSecurePlayback);
+                name,
+                mustSupportSecure,
+                supportsSecurePlayback,
+                requiresSecurePlayback);
         Log.v(TAG, message);
         continue;
       }
@@ -623,9 +629,13 @@
           || !mustSupportTunnelMode && requiresTunneledPlayback) {
         String message =
             String.format(
+                Locale.US,
                 "Rejecting %s, reason: tunneled playback requested: %b, "
                     + "codec FEATURE_TunneledPlayback supported: %b, required: %b",
-                name, mustSupportTunnelMode, supportsTunneledPlayback, requiresTunneledPlayback);
+                name,
+                mustSupportTunnelMode,
+                supportsTunneledPlayback,
+                requiresTunneledPlayback);
         Log.v(TAG, message);
         continue;
       }
@@ -652,31 +662,31 @@
         if (frameWidth != 0 && frameHeight != 0) {
           if (!videoCapabilities.isSizeSupported(frameWidth, frameHeight)) {
             String format = "Rejecting %s, reason: width %s is not compatible with height %d";
-            Log.v(TAG, String.format(format, name, frameWidth, frameHeight));
+            Log.v(TAG, format, name, frameWidth, frameHeight);
             continue;
           }
         } else if (frameWidth != 0) {
           if (!supportedWidths.contains(frameWidth)) {
             String format = "Rejecting %s, reason: supported widths %s does not contain %d";
-            Log.v(TAG, String.format(format, name, supportedWidths.toString(), frameWidth));
+            Log.v(TAG, format, name, supportedWidths.toString(), frameWidth);
             continue;
           }
         } else if (frameHeight != 0) {
           if (!supportedHeights.contains(frameHeight)) {
             String format = "Rejecting %s, reason: supported heights %s does not contain %d";
-            Log.v(TAG, String.format(format, name, supportedHeights.toString(), frameHeight));
+            Log.v(TAG, format, name, supportedHeights.toString(), frameHeight);
             continue;
           }
         }
       } else {
         if (frameWidth != 0 && !supportedWidths.contains(frameWidth)) {
           String format = "Rejecting %s, reason: supported widths %s does not contain %d";
-          Log.v(TAG, String.format(format, name, supportedWidths.toString(), frameWidth));
+          Log.v(TAG, format, name, supportedWidths.toString(), frameWidth);
           continue;
         }
         if (frameHeight != 0 && !supportedHeights.contains(frameHeight)) {
           String format = "Rejecting %s, reason: supported heights %s does not contain %d";
-          Log.v(TAG, String.format(format, name, supportedHeights.toString(), frameHeight));
+          Log.v(TAG, format, name, supportedHeights.toString(), frameHeight);
           continue;
         }
       }
@@ -684,7 +694,7 @@
       Range<Integer> bitrates = videoCapabilities.getBitrateRange();
       if (bitrate != 0 && !bitrates.contains(bitrate)) {
         String format = "Rejecting %s, reason: bitrate range %s does not contain %d";
-        Log.v(TAG, String.format(format, name, bitrates.toString(), bitrate));
+        Log.v(TAG, format, name, bitrates.toString(), bitrate);
         continue;
       }
 
@@ -694,14 +704,14 @@
           if (frameHeight != 0 && frameWidth != 0) {
             if (!videoCapabilities.areSizeAndRateSupported(frameWidth, frameHeight, fps)) {
               String format = "Rejecting %s, reason: supported frame rates %s does not contain %d";
-              Log.v(TAG, String.format(format, name, supportedFrameRates.toString(), fps));
+              Log.v(TAG, format, name, supportedFrameRates.toString(), fps);
               continue;
             }
           } else {
             // At least one of frameHeight or frameWidth is 0
             if (!supportedFrameRates.contains(fps)) {
               String format = "Rejecting %s, reason: supported frame rates %s does not contain %d";
-              Log.v(TAG, String.format(format, name, supportedFrameRates.toString(), fps));
+              Log.v(TAG, format, name, supportedFrameRates.toString(), fps);
               continue;
             }
           }
@@ -709,7 +719,7 @@
       } else {
         if (fps != 0 && !supportedFrameRates.contains(fps)) {
           String format = "Rejecting %s, reason: supported frame rates %s does not contain %d";
-          Log.v(TAG, String.format(format, name, supportedFrameRates.toString(), fps));
+          Log.v(TAG, format, name, supportedFrameRates.toString(), fps);
           continue;
         }
       }
@@ -855,6 +865,7 @@
     for (ResolutionAndFrameRate resolutionAndFrameRate : supported) {
       frameRateAndResolutionString +=
           String.format(
+              Locale.US,
               "[%d x %d, %.3f fps], ",
               resolutionAndFrameRate.width,
               resolutionAndFrameRate.height,
@@ -878,6 +889,7 @@
         String name = info.getName();
         decoderDumpString +=
             String.format(
+                Locale.US,
                 "name: %s (%s, %s): ",
                 name,
                 supportedType,
@@ -897,6 +909,7 @@
               getSupportedResolutionsAndFrameRates(videoCapabilities, isHdrCapable);
           decoderDumpString +=
               String.format(
+                  Locale.US,
                   "\n\t\t"
                       + "widths: %s, "
                       + "heights: %s, "
@@ -923,6 +936,7 @@
             || isTunneledPlaybackSupported) {
           decoderDumpString +=
               String.format(
+                  Locale.US,
                   "(%s%s%s",
                   isAdaptivePlaybackSupported ? "AdaptivePlayback, " : "",
                   isSecurePlaybackSupported ? "SecurePlayback, " : "",
@@ -938,13 +952,12 @@
     }
     Log.v(
         TAG,
-        String.format(
-            " \n"
-                + "==================================================\n"
-                + "Full list of decoder features: [AdaptivePlayback, SecurePlayback,"
-                + " TunneledPlayback]\n"
-                + "Unsupported features for each codec are not listed\n"
-                + decoderDumpString
-                + "=================================================="));
+        " \n"
+            + "==================================================\n"
+            + "Full list of decoder features: [AdaptivePlayback, SecurePlayback,"
+            + " TunneledPlayback]\n"
+            + "Unsupported features for each codec are not listed\n"
+            + decoderDumpString
+            + "==================================================");
   }
 }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java b/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
index 29a2f80..b96bb87 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
@@ -16,6 +16,8 @@
 
 import android.app.Activity;
 import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
 import android.util.DisplayMetrics;
 import android.util.Size;
 import android.util.SizeF;
@@ -139,4 +141,40 @@
   private static DisplayMetrics getDisplayMetrics() {
     return cachedDisplayMetrics;
   }
+
+  private static DisplayListener displayerListener =
+      new DisplayListener() {
+        @Override
+        public void onDisplayAdded(int displayId) {
+          nativeOnDisplayChanged();
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+          nativeOnDisplayChanged();
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+          nativeOnDisplayChanged();
+        }
+      };
+
+  private static boolean displayerListenerAdded = false;
+
+  public static void addDisplayListener(Context context) {
+    if (displayerListenerAdded) {
+      return;
+    }
+
+    DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+    displayManager.registerDisplayListener(displayerListener, null);
+    displayerListenerAdded = true;
+
+    // Call nativeOnDisplayChanged() to reload supported hdr types here after a default
+    // Display created.
+    nativeOnDisplayChanged();
+  }
+
+  private static native void nativeOnDisplayChanged();
 }
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java b/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
index 0a75c79..cca7c02 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
@@ -15,6 +15,7 @@
 package dev.cobalt.util;
 
 import java.lang.reflect.Method;
+import java.util.Locale;
 
 /**
  * Logging wrapper to allow for better control of Proguard log stripping. Many dependent
@@ -59,9 +60,27 @@
     }
   }
 
-  private static int logWithMethod(Method logMethod, String tag, String msg, Throwable tr) {
+  private static Throwable getThrowableToLog(Object[] args) {
+    if (args == null || args.length == 0) return null;
+    Object lastArg = args[args.length - 1];
+    if (!(lastArg instanceof Throwable)) return null;
+    return (Throwable) lastArg;
+  }
+
+  /** Returns a formatted log message, using the supplied format and arguments. */
+  private static String formatLog(String messageTemplate, Throwable tr, Object... params) {
+    if ((params != null) && ((tr == null && params.length > 0) || params.length > 1)) {
+      messageTemplate = String.format(Locale.US, messageTemplate, params);
+    }
+    return messageTemplate;
+  }
+
+  private static int logWithMethod(
+      Method logMethod, String tag, String messageTemplate, Object... args) {
     try {
       if (logMethod != null) {
+        Throwable tr = getThrowableToLog(args);
+        String msg = formatLog(messageTemplate, tr, args);
         return (int) logMethod.invoke(null, tag, msg, tr);
       }
     } catch (Throwable e) {
@@ -70,47 +89,27 @@
     return 0;
   }
 
-  public static int v(String tag, String msg) {
-    return logWithMethod(logV, tag, msg, null);
+  public static int v(String tag, String messageTemplate, Object... args) {
+    return logWithMethod(logV, tag, messageTemplate, args);
   }
 
-  public static int v(String tag, String msg, Throwable tr) {
-    return logWithMethod(logV, tag, msg, tr);
+  public static int d(String tag, String messageTemplate, Object... args) {
+    return logWithMethod(logD, tag, messageTemplate, args);
   }
 
-  public static int d(String tag, String msg) {
-    return logWithMethod(logD, tag, msg, null);
+  public static int i(String tag, String messageTemplate, Object... args) {
+    return logWithMethod(logI, tag, messageTemplate, args);
   }
 
-  public static int d(String tag, String msg, Throwable tr) {
-    return logWithMethod(logD, tag, msg, tr);
-  }
-
-  public static int i(String tag, String msg) {
-    return logWithMethod(logI, tag, msg, null);
-  }
-
-  public static int i(String tag, String msg, Throwable tr) {
-    return logWithMethod(logI, tag, msg, tr);
-  }
-
-  public static int w(String tag, String msg) {
-    return logWithMethod(logW, tag, msg, null);
-  }
-
-  public static int w(String tag, String msg, Throwable tr) {
-    return logWithMethod(logW, tag, msg, tr);
+  public static int w(String tag, String messageTemplate, Object... args) {
+    return logWithMethod(logW, tag, messageTemplate, args);
   }
 
   public static int w(String tag, Throwable tr) {
     return logWithMethod(logW, tag, "", tr);
   }
 
-  public static int e(String tag, String msg) {
-    return logWithMethod(logE, tag, msg, null);
-  }
-
-  public static int e(String tag, String msg, Throwable tr) {
-    return logWithMethod(logE, tag, msg, tr);
+  public static int e(String tag, String messageTemplate, Object... args) {
+    return logWithMethod(logE, tag, messageTemplate, args);
   }
 }
diff --git a/starboard/android/shared/file_open.cc b/starboard/android/shared/file_open.cc
index 83d924a..0f398a4 100644
--- a/starboard/android/shared/file_open.cc
+++ b/starboard/android/shared/file_open.cc
@@ -30,7 +30,12 @@
 // font file of the same name.
 const std::string kFontsXml("fonts.xml");
 const std::string kSystemFontsDir("/system/fonts/");
+
+#if SB_IS(EVERGREEN_COMPATIBLE)
+const std::string kCobaltFontsDir("/cobalt/assets/app/cobalt/content/fonts/");
+#else
 const std::string kCobaltFontsDir("/cobalt/assets/fonts/");
+#endif
 
 // Returns the fallback for the given asset path, or an empty string if none.
 // NOTE: While Cobalt now provides a mechanism for loading system fonts through
@@ -67,8 +72,8 @@
                   bool* out_created,
                   SbFileError* out_error) {
   if (!IsAndroidAssetPath(path)) {
-    return ::starboard::shared::posix::impl::FileOpen(
-        path, flags, out_created, out_error);
+    return ::starboard::shared::posix::impl::FileOpen(path, flags, out_created,
+                                                      out_error);
   }
 
   // Assets are never created and are always read-only, whether it's actually an
diff --git a/starboard/android/shared/media_capabilities_cache.cc b/starboard/android/shared/media_capabilities_cache.cc
index 53360ea..e8090b5 100644
--- a/starboard/android/shared/media_capabilities_cache.cc
+++ b/starboard/android/shared/media_capabilities_cache.cc
@@ -20,12 +20,17 @@
 #include "starboard/android/shared/media_common.h"
 #include "starboard/common/log.h"
 #include "starboard/once.h"
+#include "starboard/shared/starboard/media/key_system_supportability_cache.h"
+#include "starboard/shared/starboard/media/mime_supportability_cache.h"
 
 namespace starboard {
 namespace android {
 namespace shared {
 namespace {
 
+using ::starboard::shared::starboard::media::KeySystemSupportabilityCache;
+using ::starboard::shared::starboard::media::MimeSupportabilityCache;
+
 // https://developer.android.com/reference/android/view/Display.HdrCapabilities.html#HDR_TYPE_HDR10
 const jint HDR_TYPE_DOLBY_VISION = 1;
 const jint HDR_TYPE_HDR10 = 2;
@@ -79,6 +84,13 @@
   JniEnvExt* env = JniEnvExt::Get();
   jintArray j_supported_hdr_types = static_cast<jintArray>(
       env->CallStarboardObjectMethodOrAbort("getSupportedHdrTypes", "()[I"));
+
+  if (!j_supported_hdr_types) {
+    // Failed to get supported hdr types.
+    SB_LOG(ERROR) << "Failed to load supported hdr types.";
+    return std::set<SbMediaTransferId>();
+  }
+
   jsize length = env->GetArrayLength(j_supported_hdr_types);
   jint* numbers = env->GetIntArrayElements(j_supported_hdr_types, 0);
   for (int i = 0; i < length; i++) {
@@ -461,6 +473,31 @@
   max_audio_output_channels_ = -1;
 }
 
+void MediaCapabilitiesCache::ReloadSupportedHdrTypes() {
+  ScopedLock scoped_lock(mutex_);
+  if (!is_initialized_) {
+    LazyInitialize_Locked();
+    return;
+  }
+  supported_transfer_ids_ = GetSupportedHdrTypes();
+}
+
+void MediaCapabilitiesCache::ReloadAudioOutputChannels() {
+  ScopedLock scoped_lock(mutex_);
+  if (!is_initialized_) {
+    LazyInitialize_Locked();
+    return;
+  }
+  max_audio_output_channels_ =
+      ::starboard::android::shared::GetMaxAudioOutputChannels();
+}
+
+MediaCapabilitiesCache::MediaCapabilitiesCache() {
+  // Enable mime and key system caches.
+  MimeSupportabilityCache::GetInstance()->SetCacheEnabled(true);
+  KeySystemSupportabilityCache::GetInstance()->SetCacheEnabled(true);
+}
+
 void MediaCapabilitiesCache::LazyInitialize_Locked() {
   mutex_.DCheckAcquired();
 
@@ -526,6 +563,20 @@
   }
 }
 
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_util_DisplayUtil_nativeOnDisplayChanged() {
+  SB_DLOG(INFO) << "Display device has changed.";
+  MediaCapabilitiesCache::GetInstance()->ReloadSupportedHdrTypes();
+  MimeSupportabilityCache::GetInstance()->ClearCachedMimeSupportabilities();
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_AudioOutputManager_nativeOnAudioDeviceChanged() {
+  SB_DLOG(INFO) << "Audio device has changed.";
+  MediaCapabilitiesCache::GetInstance()->ReloadAudioOutputChannels();
+  MimeSupportabilityCache::GetInstance()->ClearCachedMimeSupportabilities();
+}
+
 }  // namespace shared
 }  // namespace android
 }  // namespace starboard
diff --git a/starboard/android/shared/media_capabilities_cache.h b/starboard/android/shared/media_capabilities_cache.h
index 46da91a..21d735b 100644
--- a/starboard/android/shared/media_capabilities_cache.h
+++ b/starboard/android/shared/media_capabilities_cache.h
@@ -160,8 +160,11 @@
   void SetCacheEnabled(bool enabled) { is_enabled_ = enabled; }
   void ClearCache();
 
+  void ReloadSupportedHdrTypes();
+  void ReloadAudioOutputChannels();
+
  private:
-  MediaCapabilitiesCache() {}
+  MediaCapabilitiesCache();
   ~MediaCapabilitiesCache() {}
 
   MediaCapabilitiesCache(const MediaCapabilitiesCache&) = delete;
@@ -183,7 +186,7 @@
   std::map<std::string, AudioCodecCapabilities> audio_codec_capabilities_map_;
   std::map<std::string, VideoCodecCapabilities> video_codec_capabilities_map_;
 
-  std::atomic_bool is_enabled_{false};
+  std::atomic_bool is_enabled_{true};
   bool is_initialized_ = false;
   bool is_widevine_supported_ = false;
   bool is_cbcs_supported_ = false;
diff --git a/starboard/android/shared/media_is_video_supported.cc b/starboard/android/shared/media_is_video_supported.cc
index 1f5c851..9b8fcf5 100644
--- a/starboard/android/shared/media_is_video_supported.cc
+++ b/starboard/android/shared/media_is_video_supported.cc
@@ -80,6 +80,14 @@
 
     decoder_cache_ttl_ms =
         mime_type->GetParamIntValue("decoder_cache_ttl_ms", -1);
+
+    // Disable MediaCapabilitiesCache if "disablecache" option presented.
+    if (!mime_type->ValidateBoolParameter("disablecache")) {
+      return false;
+    }
+    if (mime_type->GetParamBoolValue("disablecache", false)) {
+      MediaCapabilitiesCache::GetInstance()->SetCacheEnabled(false);
+    }
   }
 
   if (must_support_tunnel_mode && decode_to_texture_required) {
diff --git a/starboard/android/shared/system_get_path.cc b/starboard/android/shared/system_get_path.cc
index 2ca0815..5e4610a 100644
--- a/starboard/android/shared/system_get_path.cc
+++ b/starboard/android/shared/system_get_path.cc
@@ -25,10 +25,35 @@
 #include "starboard/common/string.h"
 #include "starboard/directory.h"
 
+#if SB_IS(EVERGREEN_COMPATIBLE)
+#include "starboard/elf_loader/evergreen_config.h"  // nogncheck
+#endif
+
 using ::starboard::android::shared::g_app_assets_dir;
 using ::starboard::android::shared::g_app_cache_dir;
+using ::starboard::android::shared::g_app_files_dir;
 using ::starboard::android::shared::g_app_lib_dir;
 
+#if SB_IS(EVERGREEN_COMPATIBLE)
+bool GetEvergreenContentPathOverride(char* out_path, int path_size) {
+  const starboard::elf_loader::EvergreenConfig* evergreen_config =
+      starboard::elf_loader::EvergreenConfig::GetInstance();
+  if (!evergreen_config) {
+    return true;
+  }
+  if (evergreen_config->content_path_.empty()) {
+    return true;
+  }
+
+  if (starboard::strlcpy(out_path, evergreen_config->content_path_.c_str(),
+                         path_size) >= path_size) {
+    return false;
+  }
+
+  return true;
+}
+#endif
+
 bool SbSystemGetPath(SbSystemPathId path_id, char* out_path, int path_size) {
   if (!out_path || !path_size) {
     return false;
@@ -43,9 +68,25 @@
       if (starboard::strlcat(path, g_app_assets_dir, kPathSize) >= kPathSize) {
         return false;
       }
+
+#if SB_IS(EVERGREEN_COMPATIBLE)
+      if (!GetEvergreenContentPathOverride(path, kPathSize)) {
+        return false;
+      }
+#endif
       break;
     }
 
+    case kSbSystemPathStorageDirectory: {
+      if (starboard::strlcpy(path, g_app_files_dir, kPathSize) >= kPathSize) {
+        return false;
+      }
+      if (starboard::strlcat(path, "/storage", kPathSize) >= kPathSize) {
+        return false;
+      }
+      SbDirectoryCreate(path);
+      break;
+    }
     case kSbSystemPathCacheDirectory: {
       if (!SbSystemGetPath(kSbSystemPathTempDirectory, path, kPathSize)) {
         return false;
diff --git a/starboard/evergreen/shared/launcher.py b/starboard/evergreen/shared/launcher.py
index c8bf2d5..e6f331d 100644
--- a/starboard/evergreen/shared/launcher.py
+++ b/starboard/evergreen/shared/launcher.py
@@ -75,6 +75,8 @@
       self.loader_out_directory = paths.BuildOutputDirectory(
           self.loader_platform, self.loader_config)
 
+    self.use_compressed_library = kwargs.get('use_compressed_library')
+
     # The relationship of loader platforms and configurations to evergreen
     # platforms and configurations is many-to-many. We need a separate directory
     # for each of them, i.e. linux-x64x11_debug__evergreen-x64_gold.
@@ -92,9 +94,18 @@
 
     # Ensure the path, relative to the content of the ELF Loader, to the
     # Evergreen target and its content are passed as command line switches.
+    library_path_param = '--evergreen_library=app/{}/lib/lib{}'.format(
+        self.target_name, self.target_name)
+    if self.use_compressed_library:
+      if self.target_name != 'cobalt':
+        raise ValueError(
+            '|use_compressed_library| only expected with |target_name| cobalt')
+      library_path_param += '.lz4'
+    else:
+      library_path_param += '.so'
+
     target_command_line_params = [
-        '--evergreen_library=app/{}/lib/lib{}.so'.format(
-            self.target_name, self.target_name),
+        library_path_param,
         '--evergreen_content=app/{}/content'.format(self.target_name)
     ]
 
@@ -171,6 +182,7 @@
       self._StageTargetsAndContentsGyp()
 
   def _StageTargetsAndContentsGnLinux(self):
+    """Stage targets and their contents for GN builds for Linux platforms."""
     content_subdir = os.path.join('usr', 'share', 'cobalt')
 
     # Copy loader content and binaries
@@ -199,13 +211,15 @@
     target_content_dst = os.path.join(target_staging_dir, 'content')
     shutil.copytree(target_content_src, target_content_dst)
 
-    shlib_name = 'lib{}.so'.format(self.target_name)
+    shlib_name = 'lib{}'.format(self.target_name)
+    shlib_name += '.lz4' if self.use_compressed_library else '.so'
     target_binary_src = os.path.join(target_install_path, 'lib', shlib_name)
     target_binary_dst = os.path.join(target_staging_dir, 'lib', shlib_name)
     os.makedirs(os.path.join(target_staging_dir, 'lib'))
     shutil.copy(target_binary_src, target_binary_dst)
 
   def _StageTargetsAndContentsGnRaspi(self):
+    """Stage targets and their contents for GN builds for Raspi platforms."""
     # TODO(b/218889313): `content` is hardcoded on raspi and must be in the same
     # directory as the binaries.
     if 'raspi' in self.loader_platform:
@@ -251,13 +265,15 @@
     target_content_dst = os.path.join(target_staging_dir, 'content')
     shutil.copytree(target_content_src, target_content_dst)
 
-    shlib_name = 'lib{}.so'.format(self.target_name)
+    shlib_name = 'lib{}'.format(self.target_name)
+    shlib_name += '.lz4' if self.use_compressed_library else '.so'
     target_binary_src = os.path.join(target_install_path, 'lib', shlib_name)
     target_binary_dst = os.path.join(target_staging_dir, 'lib', shlib_name)
     os.makedirs(os.path.join(target_staging_dir, 'lib'))
     shutil.copy(target_binary_src, target_binary_dst)
 
   def _StageTargetsAndContentsGyp(self):
+    """Stage targets and their contents for GYP builds."""
     # <outpath>/deploy/elf_loader_sandbox
     staging_directory_loader = os.path.join(self.staging_directory, 'deploy',
                                             self.loader_target)
diff --git a/starboard/evergreen/testing/README.md b/starboard/evergreen/testing/README.md
index bbd33fe..e401325 100644
--- a/starboard/evergreen/testing/README.md
+++ b/starboard/evergreen/testing/README.md
@@ -121,9 +121,9 @@
              +-- content    <-- loader content
                   +-- app
                        +-- cobalt
-                            +-- content           <-- cobalt content
+                            +-- content                 <-- cobalt content
                             +-- lib
-                                 +-- libcobalt.so <-- cobalt binary
+                                 +-- libcobalt.{so,lz4} <-- cobalt binary
 ```
 
 Note: This directory structure is the same as what would be generated by
diff --git a/starboard/evergreen/testing/linux/deploy_cobalt.sh b/starboard/evergreen/testing/linux/deploy_cobalt.sh
index 6dc6e59..a461fae 100755
--- a/starboard/evergreen/testing/linux/deploy_cobalt.sh
+++ b/starboard/evergreen/testing/linux/deploy_cobalt.sh
@@ -31,8 +31,8 @@
 
   echo " Checking '${staging_dir}'"
 
-  PATHS=("${staging_dir}/loader_app"                          \
-         "${staging_dir}/content/app/cobalt/lib/libcobalt.so" \
+  PATHS=("${staging_dir}/loader_app"                                                \
+         "${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION}" \
          "${staging_dir}/content/app/cobalt/content/")
 
   for file in "${PATHS[@]}"; do
diff --git a/starboard/evergreen/testing/raspi/deploy_cobalt.sh b/starboard/evergreen/testing/raspi/deploy_cobalt.sh
index 8fdbc22..9012f3c 100755
--- a/starboard/evergreen/testing/raspi/deploy_cobalt.sh
+++ b/starboard/evergreen/testing/raspi/deploy_cobalt.sh
@@ -31,8 +31,8 @@
 
   echo " Checking '${staging_dir}'"
 
-  PATHS=("${staging_dir}/loader_app"                          \
-         "${staging_dir}/content/app/cobalt/lib/libcobalt.so" \
+  PATHS=("${staging_dir}/loader_app"                                                \
+         "${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION}" \
          "${staging_dir}/content/app/cobalt/content/")
 
   for file in "${PATHS[@]}"; do
@@ -60,7 +60,7 @@
   eval "${SSH} \"mkdir -p /home/pi/coeg/content/app/cobalt/lib\""
 
   echo " Copying cobalt to system image directory"
-  eval "${SCP} \"${staging_dir}/content/app/cobalt/lib/libcobalt.so pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/lib/\""
+  eval "${SCP} \"${staging_dir}/content/app/cobalt/lib/libcobalt${SYSTEM_IMAGE_EXTENSION} pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/lib/\""
 
   echo " Copying content to system image directory"
   eval "${SCP} \"-r ${staging_dir}/content/app/cobalt/content/ pi@${RASPI_ADDR}:/home/pi/coeg/content/app/cobalt/\""
diff --git a/starboard/evergreen/testing/run_all_tests.sh b/starboard/evergreen/testing/run_all_tests.sh
index efb9d0b..91c53fb 100755
--- a/starboard/evergreen/testing/run_all_tests.sh
+++ b/starboard/evergreen/testing/run_all_tests.sh
@@ -21,7 +21,9 @@
 DIR="$(dirname "${0}")"
 
 AUTH_METHOD="public-key"
-while getopts "d:a:" o; do
+USE_COMPRESSED_SYSTEM_IMAGE="false"
+SYSTEM_IMAGE_EXTENSION=".so"
+while getopts "d:a:c" o; do
     case "${o}" in
         d)
             DEVICE_ID=${OPTARG}
@@ -29,6 +31,10 @@
         a)
             AUTH_METHOD=${OPTARG}
             ;;
+        c)
+            USE_COMPRESSED_SYSTEM_IMAGE="true"
+            SYSTEM_IMAGE_EXTENSION=".lz4"
+            ;;
     esac
 done
 shift $((OPTIND-1))
@@ -40,8 +46,14 @@
 
 source $DIR/setup.sh
 
-# Find all of the test files within the 'test' subdirectory.
-TESTS=($(eval "find ${DIR}/tests -maxdepth 1 -name '*_test.sh'"))
+if [[ "${USE_COMPRESSED_SYSTEM_IMAGE}" == "true" ]]; then
+  # It would be valid to run all test cases using a compressed system image but
+  # is probably excessive. Instead, just the Evergreen Lite case is run to test
+  # that the compressed system image can be successfully loaded.
+  TESTS=($(eval "find ${DIR}/tests -maxdepth 1 -name 'evergreen_lite_test.sh'"))
+else
+  TESTS=($(eval "find ${DIR}/tests -maxdepth 1 -name '*_test.sh'"))
+fi
 
 COUNT=0
 RETRIED=()
@@ -145,7 +157,7 @@
 
 clean_up
 
-log "info" " [==========] Finished."
+log "info" " [==========] Finished testing with USE_COMPRESSED_SYSTEM_IMAGE=${USE_COMPRESSED_SYSTEM_IMAGE}."
 
 if [[ "${#FAILED[@]}" -eq 0 ]]; then
   exit 0
diff --git a/starboard/evergreen/testing/setup.sh b/starboard/evergreen/testing/setup.sh
index 57bdbc4..393ea6d 100755
--- a/starboard/evergreen/testing/setup.sh
+++ b/starboard/evergreen/testing/setup.sh
@@ -24,7 +24,7 @@
 
 source $DIR/pprint.sh
 
-log "info" " [==========] Preparing Cobalt."
+log "info" " [==========] Preparing to test with USE_COMPRESSED_SYSTEM_IMAGE=${USE_COMPRESSED_SYSTEM_IMAGE}."
 
 if [[ -z ${1} ]]; then
   log "error" "A platform must be provided"
diff --git a/starboard/shared/starboard/media/BUILD.gn b/starboard/shared/starboard/media/BUILD.gn
index 28ba606..7baaf99 100644
--- a/starboard/shared/starboard/media/BUILD.gn
+++ b/starboard/shared/starboard/media/BUILD.gn
@@ -17,8 +17,6 @@
   sources = [
     "//starboard/shared/starboard/media/avc_util.cc",
     "//starboard/shared/starboard/media/avc_util.h",
-    "//starboard/shared/starboard/media/bitrate_supportability_cache.cc",
-    "//starboard/shared/starboard/media/bitrate_supportability_cache.h",
     "//starboard/shared/starboard/media/codec_util.cc",
     "//starboard/shared/starboard/media/codec_util.h",
     "//starboard/shared/starboard/media/key_system_supportability_cache.cc",
diff --git a/starboard/shared/starboard/media/bitrate_supportability_cache.cc b/starboard/shared/starboard/media/bitrate_supportability_cache.cc
deleted file mode 100644
index 01c2917..0000000
--- a/starboard/shared/starboard/media/bitrate_supportability_cache.cc
+++ /dev/null
@@ -1,150 +0,0 @@
-// 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.
-
-#include "starboard/shared/starboard/media/bitrate_supportability_cache.h"
-
-#include <map>
-
-#include "starboard/common/log.h"
-#include "starboard/common/mutex.h"
-#include "starboard/log.h"
-#include "starboard/media.h"
-#include "starboard/once.h"
-
-namespace starboard {
-namespace shared {
-namespace starboard {
-namespace media {
-
-namespace {
-
-template <typename T>
-class BitrateSupportabilityContainer {
- public:
-  Supportability GetSupportability(T codec, int bitrate) {
-    // Reject invalid parameters.
-    if (bitrate < 0) {
-      return kSupportabilityNotSupported;
-    }
-    // Bitrate 0 is always supported.
-    if (bitrate == 0) {
-      return kSupportabilitySupported;
-    }
-
-    ScopedLock scoped_lock(mutex_);
-    auto iter = supported_bitrate_ranges_.find(codec);
-    if (iter == supported_bitrate_ranges_.end()) {
-      return kSupportabilityUnknown;
-    }
-    const Range& range = iter->second;
-    if (bitrate < range.minimum || bitrate > range.maximum) {
-      return kSupportabilityNotSupported;
-    }
-    return kSupportabilitySupported;
-  }
-
-  void SetSupportedBitrate(T codec, int min, int max) {
-    SB_DCHECK(min >= 0 && max >= min);
-
-    ScopedLock scoped_lock(mutex_);
-    supported_bitrate_ranges_[codec] = Range(min, max);
-  }
-  void ClearContainer() {
-    ScopedLock scoped_lock(mutex_);
-    supported_bitrate_ranges_.clear();
-  }
-
- private:
-  struct Range {
-    Range() : minimum(0), maximum(0) {}
-    Range(int min, int max) : minimum(min), maximum(max) {}
-    int minimum;
-    int maximum;
-  };
-
-  Mutex mutex_;
-  std::map<T, Range> supported_bitrate_ranges_;
-};
-
-template <typename T>
-SB_ONCE_INITIALIZE_FUNCTION(BitrateSupportabilityContainer<T>, GetContainer);
-
-}  // namespace
-
-// static
-SB_ONCE_INITIALIZE_FUNCTION(BitrateSupportabilityCache,
-                            BitrateSupportabilityCache::GetInstance);
-
-Supportability BitrateSupportabilityCache::GetBitrateSupportability(
-    const ParsedMimeInfo& mime_info) {
-  SB_DCHECK(mime_info.is_valid());
-
-  if (!is_enabled_) {
-    return kSupportabilityUnknown;
-  }
-
-  Supportability audio_supportability = kSupportabilitySupported;
-  if (mime_info.has_audio_info()) {
-    audio_supportability = GetContainer<SbMediaAudioCodec>()->GetSupportability(
-        mime_info.audio_info().codec, mime_info.audio_info().bitrate);
-  }
-
-  Supportability video_supportability = kSupportabilitySupported;
-  if (mime_info.has_video_info()) {
-    video_supportability = GetContainer<SbMediaVideoCodec>()->GetSupportability(
-        mime_info.video_info().codec, mime_info.video_info().bitrate);
-  }
-
-  if (audio_supportability == kSupportabilityNotSupported ||
-      video_supportability == kSupportabilityNotSupported) {
-    return kSupportabilityNotSupported;
-  }
-  if (audio_supportability == kSupportabilityUnknown ||
-      video_supportability == kSupportabilityUnknown) {
-    return kSupportabilityUnknown;
-  }
-  return kSupportabilitySupported;
-}
-
-void BitrateSupportabilityCache::SetSupportedBitrate(SbMediaAudioCodec codec,
-                                                     int min,
-                                                     int max) {
-  SB_DCHECK(min >= 0 && min <= max) << "Invalid bitrate range.";
-
-  if (!is_enabled_) {
-    return;
-  }
-  GetContainer<SbMediaAudioCodec>()->SetSupportedBitrate(codec, min, max);
-}
-
-void BitrateSupportabilityCache::SetSupportedBitrate(SbMediaVideoCodec codec,
-                                                     int min,
-                                                     int max) {
-  SB_DCHECK(min >= 0 && min <= max) << "Invalid bitrate range.";
-
-  if (!is_enabled_) {
-    return;
-  }
-  GetContainer<SbMediaVideoCodec>()->SetSupportedBitrate(codec, min, max);
-}
-
-void BitrateSupportabilityCache::ClearCache() {
-  GetContainer<SbMediaAudioCodec>()->ClearContainer();
-  GetContainer<SbMediaVideoCodec>()->ClearContainer();
-}
-
-}  // namespace media
-}  // namespace starboard
-}  // namespace shared
-}  // namespace starboard
diff --git a/starboard/shared/starboard/media/bitrate_supportability_cache.h b/starboard/shared/starboard/media/bitrate_supportability_cache.h
deleted file mode 100644
index fc1f553..0000000
--- a/starboard/shared/starboard/media/bitrate_supportability_cache.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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.
-
-#ifndef STARBOARD_SHARED_STARBOARD_MEDIA_BITRATE_SUPPORTABILITY_CACHE_H_
-#define STARBOARD_SHARED_STARBOARD_MEDIA_BITRATE_SUPPORTABILITY_CACHE_H_
-
-#include "starboard/media.h"
-#include "starboard/shared/internal_only.h"
-#include "starboard/shared/starboard/media/mime_supportability_cache.h"
-#include "starboard/shared/starboard/media/parsed_mime_info.h"
-
-namespace starboard {
-namespace shared {
-namespace starboard {
-namespace media {
-
-// TODO: add unit tests for BitrateSupportabilityCache
-class BitrateSupportabilityCache {
- public:
-  static BitrateSupportabilityCache* GetInstance();
-
-  // When cache is not enabled, GetBitrateSupportability() will always return
-  // kSupportabilityUnknown, and SetSupportedBitrate() will do nothing.
-  bool IsEnabled() const { return is_enabled_; }
-  void SetCacheEnabled(bool enabled) { is_enabled_ = enabled; }
-
-  // Get bitrate supportability.
-  Supportability GetBitrateSupportability(const ParsedMimeInfo& mime_info);
-  // Set supported bitrate range for the |codec|. Note that if supported bitrate
-  // range is not set, MimeSupportabilityCache::GetMimeSupportability() will
-  // always return kSupportabilityUnknown.
-  void SetSupportedBitrate(SbMediaAudioCodec codec, int min, int max);
-  void SetSupportedBitrate(SbMediaVideoCodec codec, int min, int max);
-
-  // Clear all cached supported bitrate ranges.
-  void ClearCache();
-
- private:
-  // Class can only be instanced via the singleton
-  BitrateSupportabilityCache() {}
-  ~BitrateSupportabilityCache() {}
-
-  BitrateSupportabilityCache(const BitrateSupportabilityCache&) = delete;
-  BitrateSupportabilityCache& operator=(const BitrateSupportabilityCache&) =
-      delete;
-
-  std::atomic_bool is_enabled_{false};
-};
-
-}  // namespace media
-}  // namespace starboard
-}  // namespace shared
-}  // namespace starboard
-
-#endif  // STARBOARD_SHARED_STARBOARD_MEDIA_BITRATE_SUPPORTABILITY_CACHE_H_
diff --git a/starboard/shared/starboard/media/key_system_supportability_cache.h b/starboard/shared/starboard/media/key_system_supportability_cache.h
index a1f2f68..e9ad962 100644
--- a/starboard/shared/starboard/media/key_system_supportability_cache.h
+++ b/starboard/shared/starboard/media/key_system_supportability_cache.h
@@ -25,7 +25,14 @@
 namespace starboard {
 namespace media {
 
-// TODO: add unit tests for KeySystemSupportabilityCache
+// KeySystemSupportabilityCache caches the supportabilities of the combinations
+// of codec and key system.
+//
+// Note: anytime the platform key system capabilities have changed, please
+// call KeySystemSupportabilityCache::ClearCache() to clear the outdated
+// results.
+//
+// TODO: add unit tests for KeySystemSupportabilityCache.
 class KeySystemSupportabilityCache {
  public:
   static KeySystemSupportabilityCache* GetInstance();
diff --git a/starboard/shared/starboard/media/mime_supportability_cache.cc b/starboard/shared/starboard/media/mime_supportability_cache.cc
index 34024f3..d52761c 100644
--- a/starboard/shared/starboard/media/mime_supportability_cache.cc
+++ b/starboard/shared/starboard/media/mime_supportability_cache.cc
@@ -14,6 +14,7 @@
 
 #include "starboard/shared/starboard/media/mime_supportability_cache.h"
 
+#include <cstring>
 #include <queue>
 #include <sstream>
 #include <string>
@@ -34,147 +35,116 @@
 
 namespace {
 
-const size_t kDefaultCacheMaxSize = 2000;
-
-class MimeSupportabilityContainer {
- public:
-  struct Entry {
-    ParsedMimeInfo mime_info;
-    Supportability supportability = kSupportabilityUnknown;
-
-    explicit Entry(const std::string& mime_string) : mime_info(mime_string) {}
-  };
-
-  // GetParsedMimeAndSupportability() will first try to find a cached Entry for
-  // the mime string. If no cached entry, a new Entry will be created with
-  // parsed mime information and supportability kSupportabilityUnknown.
-  // Ideally, we should decouple mime parsing and cache functionality, but
-  // considering that the cache is only for internal use, to avoid repeated
-  // lookups, we do parsing in this function.
-  const Entry& GetParsedMimeAndSupportability(const std::string& mime_string) {
-    ScopedLock scoped_lock(mutex_);
-    auto entry_iter = entries_.find(mime_string);
-    if (entry_iter != entries_.end()) {
-      return entry_iter->second;
-    }
-
-    // We can't find anything from the cache. Parse mime string and cache
-    // parsed MimeType and ParsedMimeInfo.
-    auto insert_result = entries_.insert({mime_string, Entry(mime_string)});
-
-    fifo_queue_.push(insert_result.first);
-    while (fifo_queue_.size() > max_size_) {
-      entries_.erase(fifo_queue_.front());
-      fifo_queue_.pop();
-    }
-    SB_DCHECK(entries_.size() == fifo_queue_.size());
-
-    return insert_result.first->second;
+// RemoveAttributeFromMime() will return a new mime string with the specified
+// attribute removed. If |attribute_string| is not null, the removed attribute
+// string will be returned via |attribute_string|. Following are some examples:
+//   mime: "video/webm; codecs=\"vp9\"; bitrate=300000"
+//   attribute_name: "bitrate"
+//   return: "video/webm; codecs=\"vp9\""
+//   attribute_string: "bitrate=300000"
+//
+//   mime: "video/webm; codecs=\"vp9\"; bitrate=300000; eotf=bt709"
+//   attribute_name: "bitrate"
+//   return: "video/webm; codecs=\"vp9\"; eotf=bt709"
+//   attribute_string: "bitrate=300000"
+//
+//   mime: "bitrate=300000"
+//   attribute_name: "bitrate"
+//   return: ""
+//   attribute_string: "bitrate=300000"
+std::string RemoveAttributeFromMime(const char* mime,
+                                    const char* attribute_name,
+                                    std::string* attribute_string) {
+  size_t name_length = strlen(attribute_name);
+  if (name_length == 0) {
+    return mime;
   }
 
-  // CacheSupportability() will find the target entry and update the
-  // supportability. If there's no existing entry, it will parse the mime
-  // string and create one.
-  void CacheSupportability(const std::string& mime_string,
-                           Supportability supportability) {
-    SB_DCHECK(!mime_string.empty());
-    SB_DCHECK(supportability != kSupportabilityUnknown);
+  std::string mime_without_attribute;
+  const char* start_pos = strstr(mime, attribute_name);
+  while (start_pos) {
+    if ((start_pos == mime || start_pos[-1] == ';' || isspace(start_pos[-1])) &&
+        (start_pos[name_length] &&
+         (start_pos[name_length] == '=' || isspace(start_pos[name_length])))) {
+      break;
+    }
+    start_pos += name_length;
+    start_pos = strstr(start_pos, attribute_name);
+  }
 
-    {
-      ScopedLock scoped_lock(mutex_);
-      auto entry_iter = entries_.find(mime_string);
-      if (entry_iter != entries_.end()) {
-        entry_iter->second.supportability = supportability;
-        return;
+  if (!start_pos) {
+    // Target attribute is not found.
+    return std::string(mime);
+  }
+  const char* end_pos = strstr(start_pos, ";");
+  if (end_pos) {
+    // There may be other attribute after target attribute.
+    if (attribute_string) {
+      // Returned |attribute_string| will not have a trailing ';'.
+      attribute_string->assign(start_pos, end_pos - start_pos);
+    }
+
+    end_pos++;
+    // Remove leading spaces.
+    while (*end_pos && isspace(*end_pos)) {
+      end_pos++;
+    }
+    if (*end_pos) {
+      // Append the string after target attribute.
+      mime_without_attribute = std::string(mime, start_pos - mime);
+      mime_without_attribute.append(end_pos);
+    } else {
+      // Target attribute is the last one. Remove trailing spaces.
+      size_t mime_length = start_pos - mime;
+      while (mime_length > 0 && (isspace(mime[mime_length - 1]))) {
+        mime_length--;
       }
+      mime_without_attribute = std::string(mime, mime_length);
     }
-
-    // Parse the mime string and create an entry.
-    GetParsedMimeAndSupportability(mime_string);
-    // Update the supportability again.
-    CacheSupportability(mime_string, supportability);
-  }
-
-  // ClearCachedSupportabilities() will reset all cached |supportability|, but
-  // will not remove parsed mime infos.
-  void ClearCachedSupportabilities() {
-    ScopedLock scoped_lock(mutex_);
-    for (auto& iter : entries_) {
-      iter.second.supportability = kSupportabilityUnknown;
+  } else {
+    // It can't find a trailing ';'. The target attribute must be the last one.
+    size_t mime_length = start_pos - mime;
+    // Remove trailing spaces.
+    while (mime_length > 0 && (isspace(mime[mime_length - 1]))) {
+      mime_length--;
+    }
+    // Remove the trailing ';'.
+    if (mime_length > 0 && mime[mime_length - 1] == ';') {
+      mime_length--;
+    }
+    mime_without_attribute = std::string(mime, mime_length);
+    if (attribute_string) {
+      *attribute_string = std::string(start_pos);
     }
   }
+  return mime_without_attribute;
+}
 
-  void SetCacheMaxSize(int size) { max_size_ = size; }
+// Note that if bitrate parsing failed, |bitrate| will be set to -1.
+void StripAndParseBitrate(const char* mime,
+                          std::string* mime_without_bitrate,
+                          int* bitrate) {
+  SB_DCHECK(mime_without_bitrate);
+  SB_DCHECK(bitrate);
 
-  void DumpCache() {
-    ScopedLock scoped_lock(mutex_);
-    std::stringstream ss;
-    ss << "\n========Dumping MimeInfoCache========";
-    for (const auto& entry_iter : entries_) {
-      const ParsedMimeInfo& mime_info = entry_iter.second.mime_info;
-      ss << "\nMime: " << entry_iter.first;
-      ss << "\n  ParsedMimeInfo:";
-      ss << "\n    MimeType : " << mime_info.mime_type().ToString();
-      if (mime_info.is_valid()) {
-        if (mime_info.has_audio_info()) {
-          const ParsedMimeInfo::AudioCodecInfo& audio_info =
-              mime_info.audio_info();
-          ss << "\n    Audio Codec : "
-             << GetMediaAudioCodecName(audio_info.codec);
-          ss << "\n    Channels : " << audio_info.channels;
-        }
-        if (mime_info.has_video_info()) {
-          const ParsedMimeInfo::VideoCodecInfo& video_info =
-              mime_info.video_info();
-          ss << "\n    Video Codec : "
-             << GetMediaVideoCodecName(video_info.codec);
-          ss << "\n    Profile : " << video_info.profile;
-          ss << "\n    Level : " << video_info.level;
-          ss << "\n    BitDepth : " << video_info.bit_depth;
-          ss << "\n    PrimaryId : "
-             << GetMediaPrimaryIdName(video_info.primary_id);
-          ss << "\n    TransferId : "
-             << GetMediaTransferIdName(video_info.transfer_id);
-          ss << "\n    MatrixId : "
-             << GetMediaMatrixIdName(video_info.matrix_id);
-          ss << "\n    Width : " << video_info.frame_width;
-          ss << "\n    Height : " << video_info.frame_height;
-          ss << "\n    Fps : " << video_info.fps;
-          ss << "\n    DecodeToTexture : "
-             << (video_info.decode_to_texture_required ? "true" : "false");
-        }
-      } else {
-        ss << "\n    Mime info is not valid";
-      }
+  std::string bitrate_string;
+  *mime_without_bitrate =
+      RemoveAttributeFromMime(mime, "bitrate", &bitrate_string);
 
-      ss << "\n  Supportability: ";
-      switch (entry_iter.second.supportability) {
-        case kSupportabilityUnknown:
-          ss << "Unknown";
-          break;
-        case kSupportabilitySupported:
-          ss << "Supported";
-          break;
-        case kSupportabilityNotSupported:
-          ss << "NotSupported";
-          break;
-      }
-    }
-    ss << "\n========End of Dumping========";
-
-    SB_DLOG(INFO) << ss.str();
+  if (bitrate_string.empty()) {
+    *bitrate = 0;
+    return;
   }
 
- private:
-  typedef std::unordered_map<std::string, Entry> Entries;
-
-  Mutex mutex_;
-  Entries entries_;
-  std::queue<Entries::iterator> fifo_queue_;
-  std::atomic_int max_size_{kDefaultCacheMaxSize};
-};
-
-SB_ONCE_INITIALIZE_FUNCTION(MimeSupportabilityContainer, GetContainer);
+  MimeType::Param param;
+  if (!MimeType::ParseParamString(bitrate_string, &param) ||
+      param.type != MimeType::kParamTypeInteger) {
+    *bitrate = -1;
+    return;
+  }
+  SB_DCHECK(param.name == "bitrate");
+  *bitrate = param.int_value;
+}
 
 }  // namespace
 
@@ -182,43 +152,182 @@
 SB_ONCE_INITIALIZE_FUNCTION(MimeSupportabilityCache,
                             MimeSupportabilityCache::GetInstance);
 
-void MimeSupportabilityCache::SetCacheMaxSize(size_t size) {
-  GetContainer()->SetCacheMaxSize(size);
-}
-
 Supportability MimeSupportabilityCache::GetMimeSupportability(
-    const std::string& mime,
+    const char* mime,
     ParsedMimeInfo* mime_info) {
-  // Get cached parsed mime infos and supportability. If no cache is found,
-  // MimeSupportabilityContainer will parse the mime string, and return a parsed
-  // MimeType and its parsed audio/video information.
-  const MimeSupportabilityContainer::Entry& entry =
-      GetContainer()->GetParsedMimeAndSupportability(mime);
+  SB_DCHECK(mime);
+  SB_DCHECK(mime_info);
 
-  if (mime_info) {
-    // Return cached ParsedMimeInfo.
-    *mime_info = entry.mime_info;
+  // Strip the bitrate from mime string and check it separately.
+  std::string mime_without_bitrate;
+  int bitrate;
+  StripAndParseBitrate(mime, &mime_without_bitrate, &bitrate);
+
+  if (bitrate < 0) {
+    // The mime string contains an invalid bitrate attribute. In that case, we
+    // return an invalid ParsedMimeInfo with kSbMediaSupportTypeNotSupported.
+    *mime_info = ParsedMimeInfo(mime);
+    return kSupportabilityNotSupported;
   }
 
-  return is_enabled_ ? entry.supportability : kSupportabilityUnknown;
+  ScopedLock scoped_lock(mutex_);
+  Entry& entry = GetEntry_Locked(mime_without_bitrate);
+
+  // Return cached ParsedMimeInfo with real bitrate.
+  *mime_info = entry.mime_info;
+  mime_info->SetBitrate(bitrate);
+
+  if (!mime_info->is_valid()) {
+    // Return kSupportabilityNotSupported if we can't get a valid
+    // ParsedMimeInfo.
+    return kSupportabilityNotSupported;
+  }
+
+  return is_enabled_ ? IsBitrateSupported_Locked(entry, bitrate)
+                     : kSupportabilityUnknown;
 }
 
 void MimeSupportabilityCache::CacheMimeSupportability(
-    const std::string& mime,
+    const char* mime,
     Supportability supportability) {
+  SB_DCHECK(mime);
+  SB_DCHECK(supportability != kSupportabilityUnknown);
+
   if (!is_enabled_) {
     return;
   }
-  if (supportability == kSupportabilityUnknown) {
-    SB_LOG(WARNING) << "Rejected unknown supportability.";
+
+  // Strip bitrate as what we do in GetMimeSupportability().
+  std::string mime_without_bitrate;
+  int bitrate;
+  StripAndParseBitrate(mime, &mime_without_bitrate, &bitrate);
+
+  if (bitrate < 0) {
+    // The mime string contains an invalid bitrate attribute.
     return;
   }
 
-  GetContainer()->CacheSupportability(mime, supportability);
+  ScopedLock scoped_lock(mutex_);
+  Entry& entry = GetEntry_Locked(mime_without_bitrate);
+
+  if (entry.mime_info.is_valid()) {
+    UpdateBitrateSupportability_Locked(&entry, bitrate, supportability);
+  }
 }
 
 void MimeSupportabilityCache::ClearCachedMimeSupportabilities() {
-  GetContainer()->ClearCachedSupportabilities();
+  ScopedLock scoped_lock(mutex_);
+  for (auto& iter : entries_) {
+    iter.second.max_supported_bitrate = -1;
+    iter.second.min_unsupported_bitrate = INT_MAX;
+  }
+}
+
+void MimeSupportabilityCache::DumpCache() {
+  ScopedLock scoped_lock(mutex_);
+  std::stringstream ss;
+  ss << "\n========Dumping MimeSupportabilityCache========";
+  for (const auto& entry_iter : entries_) {
+    const ParsedMimeInfo& mime_info = entry_iter.second.mime_info;
+    ss << "\nMime: " << entry_iter.first;
+    ss << "\n  ParsedMimeInfo:";
+    ss << "\n    MimeType : " << mime_info.mime_type().ToString();
+    if (mime_info.is_valid()) {
+      if (mime_info.has_audio_info()) {
+        const ParsedMimeInfo::AudioCodecInfo& audio_info =
+            mime_info.audio_info();
+        ss << "\n    Audio Codec : "
+           << GetMediaAudioCodecName(audio_info.codec);
+        ss << "\n    Channels : " << audio_info.channels;
+      }
+      if (mime_info.has_video_info()) {
+        const ParsedMimeInfo::VideoCodecInfo& video_info =
+            mime_info.video_info();
+        ss << "\n    Video Codec : "
+           << GetMediaVideoCodecName(video_info.codec);
+        ss << "\n    Profile : " << video_info.profile;
+        ss << "\n    Level : " << video_info.level;
+        ss << "\n    BitDepth : " << video_info.bit_depth;
+        ss << "\n    PrimaryId : "
+           << GetMediaPrimaryIdName(video_info.primary_id);
+        ss << "\n    TransferId : "
+           << GetMediaTransferIdName(video_info.transfer_id);
+        ss << "\n    MatrixId : " << GetMediaMatrixIdName(video_info.matrix_id);
+        ss << "\n    Width : " << video_info.frame_width;
+        ss << "\n    Height : " << video_info.frame_height;
+        ss << "\n    Fps : " << video_info.fps;
+        ss << "\n    DecodeToTexture : "
+           << (video_info.decode_to_texture_required ? "true" : "false");
+      }
+    } else {
+      ss << "\n    Mime info is not valid";
+    }
+
+    ss << "\n  MaxSupportedBitrate: "
+       << entry_iter.second.max_supported_bitrate;
+    ss << "\n  MinUnsupportedBitrate: "
+       << entry_iter.second.min_unsupported_bitrate;
+  }
+  ss << "\n========End of Dumping========";
+
+  SB_DLOG(INFO) << ss.str();
+}
+
+MimeSupportabilityCache::Entry& MimeSupportabilityCache::GetEntry_Locked(
+    const std::string& mime_string) {
+  auto entry_iter = entries_.find(mime_string);
+  if (entry_iter != entries_.end()) {
+    return entry_iter->second;
+  }
+
+  // We can't find anything from the cache. Parse mime string and cache
+  // parsed ParsedMimeInfo.
+  auto insert_result = entries_.insert({mime_string, Entry(mime_string)});
+
+  // Keep cached items not exceeding max size.
+  fifo_queue_.push(insert_result.first);
+  while (fifo_queue_.size() > max_size_) {
+    entries_.erase(fifo_queue_.front());
+    fifo_queue_.pop();
+  }
+  SB_DCHECK(entries_.size() == fifo_queue_.size());
+
+  return insert_result.first->second;
+}
+
+Supportability MimeSupportabilityCache::IsBitrateSupported_Locked(
+    const Entry& entry,
+    int bitrate) const {
+  SB_DCHECK(bitrate >= 0);
+
+  if (bitrate <= entry.max_supported_bitrate) {
+    return kSupportabilitySupported;
+  }
+  if (bitrate >= entry.min_unsupported_bitrate) {
+    return kSupportabilityNotSupported;
+  }
+  return kSupportabilityUnknown;
+}
+
+void MimeSupportabilityCache::UpdateBitrateSupportability_Locked(
+    Entry* entry,
+    int bitrate,
+    Supportability supportability) {
+  SB_DCHECK(entry);
+  SB_DCHECK(bitrate >= 0);
+  SB_DCHECK(supportability != kSupportabilityUnknown);
+
+  if (supportability == kSupportabilitySupported) {
+    SB_DCHECK(bitrate < entry->min_unsupported_bitrate);
+    if (bitrate > entry->max_supported_bitrate) {
+      entry->max_supported_bitrate = bitrate;
+    }
+  } else if (supportability == kSupportabilityNotSupported) {
+    SB_DCHECK(bitrate > entry->max_supported_bitrate);
+    if (bitrate < entry->min_unsupported_bitrate) {
+      entry->min_unsupported_bitrate = bitrate;
+    }
+  }
 }
 
 }  // namespace media
diff --git a/starboard/shared/starboard/media/mime_supportability_cache.h b/starboard/shared/starboard/media/mime_supportability_cache.h
index 7eb3d4f..c1e560b 100644
--- a/starboard/shared/starboard/media/mime_supportability_cache.h
+++ b/starboard/shared/starboard/media/mime_supportability_cache.h
@@ -16,8 +16,11 @@
 #define STARBOARD_SHARED_STARBOARD_MEDIA_MIME_SUPPORTABILITY_CACHE_H_
 
 #include <atomic>
+#include <queue>
 #include <string>
+#include <unordered_map>
 
+#include "starboard/common/mutex.h"
 #include "starboard/shared/internal_only.h"
 #include "starboard/shared/starboard/media/parsed_mime_info.h"
 
@@ -32,34 +35,68 @@
   kSupportabilityNotSupported,
 } Supportability;
 
-// TODO: add unit tests for MimeSupportabilityCache
+// MimeSupportabilityCache caches the supportabilities of raw mime strings.
+// To increase cache hit rate, it strips bitrate from the raw mime string, and
+// stores a supported bitrate range for mime strings with same other attributes.
+//
+// Note: MimeSupportabilityCache leverage the assumption that if the
+// platform can support a codec with bitrate of n, the codec should also support
+// any bitrate less than n. If that assumption is not true, please do NOT enable
+// MimeSupportabilityCache.
+//
+// Note: anytime the platform codec capabilities have changed, please call
+// MimeSupportabilityCache::ClearCachedMimeSupportabilities() to clear the
+// outdated results.
+//
+// TODO: add unit tests for MimeSupportabilityCache.
 class MimeSupportabilityCache {
  public:
   static MimeSupportabilityCache* GetInstance();
 
-  // When cache is not enabled, GetMimeSupportability() will always return
-  // kSupportabilityUnknown, and CacheMimeSupportability() will do nothing,
-  // but GetMimeSupportability() will still return parsed ParsedMimeInfo.
+  // When cache is not enabled, GetMimeSupportability() will return cached
+  // ParsedMimeInfo with kSupportabilityUnknown, and CacheMimeSupportability()
+  // will do nothing.
   bool IsEnabled() const { return is_enabled_; }
   void SetCacheEnabled(bool enabled) { is_enabled_ = enabled; }
 
-  void SetCacheMaxSize(size_t size);
+  // Set the max number of the cached ParsedMimeInfos and its supportabilities.
+  void SetCacheMaxSize(size_t size) { max_size_ = size; }
 
-  // Get cached mime supportability. The parsed mime information would be
-  // returned via |mime_info| if it is not NULL.
-  Supportability GetMimeSupportability(const std::string& mime,
+  // Get cached ParsedMimeInfo and mime supportability. If there's no cached
+  // ParsedMimeInfo, it will parse the mime string and cache the result.
+  // If we cannot get a valid ParsedMimeInfo from |mime|,
+  // GetMimeSupportability() will return kSupportabilityNotSupported with an
+  // invalid ParsedMimeInfo. Ideally, we should decouple mime parsing and
+  // supportability cache, but considering that the cache is only for internal
+  // use, to avoid repeated lookups, we do parsing in this function for now.
+  // Note that |mime| and |mime_info| cannot be null.
+  Supportability GetMimeSupportability(const char* mime,
                                        ParsedMimeInfo* mime_info);
-  // Cache mime supportability. If there's no cached parsed mime info and
-  // supportability for the mime, the function will parse the mime first and
-  // then update its supportability.
-  void CacheMimeSupportability(const std::string& mime,
-                               Supportability supportability);
 
-  // Clear all cached supportabilities. Note that it will not remove cached
-  // parsed mime infos.
+  // Update cached supportability of the mime string.
+  // Note that if |supportability| is kSupportabilityUnknown or we cannot
+  // get a valid ParsedMimeInfo from |mime|, CacheMimeSupportability()
+  // will not cache the supportability.
+  void CacheMimeSupportability(const char* mime, Supportability supportability);
+
+  // Clear all cached supportabilities. But it will not remove cached
+  // ParsedMimeInfos, as for the same mime string, the parsed results should be
+  // always the same.
   void ClearCachedMimeSupportabilities();
 
+  void DumpCache();
+
  private:
+  const int kDefaultCacheMaxSize = 2000;
+
+  struct Entry {
+    ParsedMimeInfo mime_info;
+    int max_supported_bitrate = -1;
+    int min_unsupported_bitrate = INT_MAX;
+
+    explicit Entry(const std::string& mime) : mime_info(mime) {}
+  };
+
   // Class can only be instanced via the singleton
   MimeSupportabilityCache() {}
   ~MimeSupportabilityCache() {}
@@ -67,6 +104,19 @@
   MimeSupportabilityCache(const MimeSupportabilityCache&) = delete;
   MimeSupportabilityCache& operator=(const MimeSupportabilityCache&) = delete;
 
+  Entry& GetEntry_Locked(const std::string& mime_string);
+  Supportability IsBitrateSupported_Locked(const Entry& entry,
+                                           int bitrate) const;
+  void UpdateBitrateSupportability_Locked(Entry* entry,
+                                          int bitrate,
+                                          Supportability supportability);
+
+  typedef std::unordered_map<std::string, Entry> Entries;
+
+  Mutex mutex_;
+  Entries entries_;
+  std::queue<Entries::iterator> fifo_queue_;
+  std::atomic_int max_size_{kDefaultCacheMaxSize};
   std::atomic_bool is_enabled_{false};
 };
 
diff --git a/starboard/shared/starboard/media/mime_util.cc b/starboard/shared/starboard/media/mime_util.cc
index 518338f..6c53aa7 100644
--- a/starboard/shared/starboard/media/mime_util.cc
+++ b/starboard/shared/starboard/media/mime_util.cc
@@ -21,7 +21,6 @@
 #include "starboard/common/log.h"
 #include "starboard/common/media.h"
 #include "starboard/log.h"
-#include "starboard/shared/starboard/media/bitrate_supportability_cache.h"
 #include "starboard/shared/starboard/media/key_system_supportability_cache.h"
 #include "starboard/shared/starboard/media/media_support_internal.h"
 #include "starboard/shared/starboard/media/mime_supportability_cache.h"
@@ -35,91 +34,6 @@
 
 namespace {
 
-// RemoveAttributeFromMime() will return a new mime string with the specified
-// attribute removed. If |attribute_string| is not null, the removed attribute
-// string will be returned via |attribute_string|. Following are some examples:
-//   mime: "video/webm; codecs=\"vp9\"; bitrate=300000"
-//   attribute_name: "bitrate"
-//   return: "video/webm; codecs=\"vp9\""
-//   attribute_string: "bitrate=300000"
-//
-//   mime: "video/webm; codecs=\"vp9\"; bitrate=300000; eotf=bt709"
-//   attribute_name: "bitrate"
-//   return: "video/webm; codecs=\"vp9\"; eotf=bt709"
-//   attribute_string: "bitrate=300000"
-//
-//   mime: "bitrate=300000"
-//   attribute_name: "bitrate"
-//   return: ""
-//   attribute_string: "bitrate=300000"
-std::string RemoveAttributeFromMime(const char* mime,
-                                    const char* attribute_name,
-                                    std::string* attribute_string) {
-  size_t name_length = strlen(attribute_name);
-  if (name_length == 0) {
-    return mime;
-  }
-
-  std::string mime_without_attribute;
-  const char* start_pos = strstr(mime, attribute_name);
-  while (start_pos) {
-    if ((start_pos == mime || start_pos[-1] == ';' || isspace(start_pos[-1])) &&
-        (start_pos[name_length] &&
-         (start_pos[name_length] == '=' || isspace(start_pos[name_length])))) {
-      break;
-    }
-    start_pos += name_length;
-    start_pos = strstr(start_pos, attribute_name);
-  }
-
-  if (!start_pos) {
-    // Target attribute is not found.
-    return std::string(mime);
-  }
-  const char* end_pos = strstr(start_pos, ";");
-  if (end_pos) {
-    // There may be other attribute after target attribute.
-    if (attribute_string) {
-      // Returned |attribute_string| will not have a trailing ';'.
-      attribute_string->assign(start_pos, end_pos - start_pos);
-    }
-
-    end_pos++;
-    // Remove leading spaces.
-    while (*end_pos && isspace(*end_pos)) {
-      end_pos++;
-    }
-    if (*end_pos) {
-      // Append the string after target attribute.
-      mime_without_attribute = std::string(mime, start_pos - mime);
-      mime_without_attribute.append(end_pos);
-    } else {
-      // Target attribute is the last one. Remove trailing spaces.
-      size_t mime_length = start_pos - mime;
-      while (mime_length > 0 && (isspace(mime[mime_length - 1]))) {
-        mime_length--;
-      }
-      mime_without_attribute = std::string(mime, mime_length);
-    }
-  } else {
-    // It can't find a trailing ';'. The target attribute must be the last one.
-    size_t mime_length = start_pos - mime;
-    // Remove trailing spaces.
-    while (mime_length > 0 && (isspace(mime[mime_length - 1]))) {
-      mime_length--;
-    }
-    // Remove the trailing ';'.
-    if (mime_length > 0 && mime[mime_length - 1] == ';') {
-      mime_length--;
-    }
-    mime_without_attribute = std::string(mime, mime_length);
-    if (attribute_string) {
-      *attribute_string = std::string(start_pos);
-    }
-  }
-  return mime_without_attribute;
-}
-
 // Use SbMediaGetAudioConfiguration() to check if the platform can support
 // |channels|.
 bool IsAudioOutputSupported(SbMediaAudioCodingType coding_type, int channels) {
@@ -255,22 +169,6 @@
       video_info.decode_to_texture_required);
 }
 
-bool ValidateAndParseBitrate(const std::string& bitrate_string, int* bitrate) {
-  SB_DCHECK(!bitrate_string.empty());
-
-  MimeType::Param param;
-  if (!MimeType::ParseParamString(bitrate_string, &param)) {
-    return false;
-  }
-  if (param.type != MimeType::kParamTypeInteger) {
-    return false;
-  }
-  if (bitrate) {
-    *bitrate = param.int_value;
-  }
-  return true;
-}
-
 }  // namespace
 
 SbMediaSupportType CanPlayMimeAndKeySystem(const char* mime,
@@ -278,38 +176,19 @@
   SB_DCHECK(mime);
   SB_DCHECK(key_system);
 
-  // Remove bitrate from mime string and read bitrate if presents.
-  std::string bitrate_string;
-  std::string mime_without_bitrate =
-      RemoveAttributeFromMime(mime, "bitrate", &bitrate_string);
-  int bitrate = 0;
-  if (!bitrate_string.empty()) {
-    if (!ValidateAndParseBitrate(bitrate_string, &bitrate)) {
-      return kSbMediaSupportTypeNotSupported;
-    }
-  }
-
-  if (bitrate < 0) {
-    // Reject invalid bitrate.
-    return kSbMediaSupportTypeNotSupported;
-  }
-
-  // Get cached parsed mime infos and supportability. If it is not found in the
-  // cache, MimeSupportabilityCache would parse the mime string and return a
-  // ParsedMimeInfo.
+  // Get cached ParsedMimeInfo with its supportability. If it is not found in
+  // the cache, MimeSupportabilityCache would parse the mime string and return
+  // the ParsedMimeInfo with kSupportabilityUnknown.
   ParsedMimeInfo mime_info;
   Supportability mime_supportability =
-      MimeSupportabilityCache::GetInstance()->GetMimeSupportability(
-          mime_without_bitrate, &mime_info);
-  // Overwrite the bitrate.
-  mime_info.SetBitrate(bitrate);
+      MimeSupportabilityCache::GetInstance()->GetMimeSupportability(mime,
+                                                                    &mime_info);
 
   if (mime_info.disable_cache()) {
     // Disable all caches if required.
     mime_supportability = kSupportabilityUnknown;
     MimeSupportabilityCache::GetInstance()->SetCacheEnabled(false);
     KeySystemSupportabilityCache::GetInstance()->SetCacheEnabled(false);
-    BitrateSupportabilityCache::GetInstance()->SetCacheEnabled(false);
   }
 
   // Reject mime if cached result is not supported.
@@ -317,10 +196,10 @@
     return kSbMediaSupportTypeNotSupported;
   }
 
-  // Reject mime if parsed mime info is invalid.
-  if (!mime_info.is_valid()) {
-    return kSbMediaSupportTypeNotSupported;
-  }
+  // MimeSupportabilityCache::GetMimeSupportability() returns
+  // kSupportabilityNotSupported if ParsedMimeInfo is not valid, so |mime_info|
+  // must be valid here.
+  SB_DCHECK(mime_info.is_valid());
 
   const MimeType& mime_type = mime_info.mime_type();
   const std::vector<std::string>& codecs = mime_type.GetCodecs();
@@ -379,26 +258,14 @@
     }
   }
 
-  // Get cached bitrate supportability.
-  Supportability bitrate_supportability =
-      BitrateSupportabilityCache::GetInstance()->GetBitrateSupportability(
-          mime_info);
-
-  // Reject mime if bitrate is not supported.
-  if (bitrate_supportability == kSupportabilityNotSupported) {
-    return kSbMediaSupportTypeNotSupported;
-  }
-
-  // Return supported if mime and bitrate are all supported.
-  if (mime_supportability == kSupportabilitySupported &&
-      bitrate_supportability == kSupportabilitySupported) {
+  // At this point, |key_system| is supported. Return supported here if
+  // mime is also supported. Otherwise, |mime_supportability| must be unknown.
+  if (mime_supportability == kSupportabilitySupported) {
     return kSbMediaSupportTypeProbably;
   }
+  SB_DCHECK(mime_supportability == kSupportabilityUnknown);
 
-  // At this point, either mime or bitrate supportability must be unknown.
-  // Call platform functions to check if they are supported.
-  SB_DCHECK(mime_supportability == kSupportabilityUnknown ||
-            bitrate_supportability == kSupportabilityUnknown);
+  // Call platform functions to check if it's supported.
   if (mime_info.has_audio_info() && !IsSupportedAudioCodec(mime_info)) {
     mime_supportability = kSupportabilityNotSupported;
   } else if (mime_info.has_video_info() && !IsSupportedVideoCodec(mime_info)) {
@@ -407,13 +274,10 @@
     mime_supportability = kSupportabilitySupported;
   }
 
-  // Cache mime supportability when bitrate supportability is known.
-  if (bitrate_supportability == kSupportabilitySupported) {
-    MimeSupportabilityCache::GetInstance()->CacheMimeSupportability(
-        mime_without_bitrate, mime_supportability);
-  }
+  // Cache mime supportability.
+  MimeSupportabilityCache::GetInstance()->CacheMimeSupportability(
+      mime, mime_supportability);
 
-  SB_DCHECK(mime_supportability != kSupportabilityUnknown);
   return mime_supportability == kSupportabilitySupported
              ? kSbMediaSupportTypeProbably
              : kSbMediaSupportTypeNotSupported;
diff --git a/starboard/shared/starboard/player/player_create.cc b/starboard/shared/starboard/player/player_create.cc
index 16bc57f..7ffb717 100644
--- a/starboard/shared/starboard/player/player_create.cc
+++ b/starboard/shared/starboard/player/player_create.cc
@@ -138,8 +138,9 @@
   const int64_t kDefaultBitRate = 0;
   if (audio_codec != kSbMediaAudioCodecNone) {
     const MimeType audio_mime_type(audio_mime);
-    if (!SbMediaIsAudioSupported(audio_codec, &audio_mime_type,
-                                 kDefaultBitRate)) {
+    if (!SbMediaIsAudioSupported(
+            audio_codec, strlen(audio_mime) > 0 ? &audio_mime_type : nullptr,
+            kDefaultBitRate)) {
       SB_LOG(ERROR) << "Unsupported audio codec "
                     << starboard::GetMediaAudioCodecName(audio_codec) << ".";
       player_error_func(kSbPlayerInvalid, context, kSbPlayerErrorDecode,
@@ -160,11 +161,11 @@
   if (video_codec != kSbMediaVideoCodecNone) {
     const MimeType video_mime_type(video_mime);
     if (!SbMediaIsVideoSupported(
-            video_codec, &video_mime_type, kDefaultProfile, kDefaultLevel,
-            kDefaultColorDepth, kSbMediaPrimaryIdUnspecified,
-            kSbMediaTransferIdUnspecified, kSbMediaMatrixIdUnspecified,
-            kDefaultFrameWidth, kDefaultFrameHeight, kDefaultBitRate,
-            kDefaultFrameRate,
+            video_codec, strlen(video_mime) > 0 ? &video_mime_type : nullptr,
+            kDefaultProfile, kDefaultLevel, kDefaultColorDepth,
+            kSbMediaPrimaryIdUnspecified, kSbMediaTransferIdUnspecified,
+            kSbMediaMatrixIdUnspecified, kDefaultFrameWidth,
+            kDefaultFrameHeight, kDefaultBitRate, kDefaultFrameRate,
             output_mode == kSbPlayerOutputModeDecodeToTexture)) {
       SB_LOG(ERROR) << "Unsupported video codec "
                     << starboard::GetMediaVideoCodecName(video_codec) << ".";