Import Cobalt 21.lts.4.301329
diff --git a/src/cobalt/CHANGELOG.md b/src/cobalt/CHANGELOG.md
index b201080..dc2d88d 100644
--- a/src/cobalt/CHANGELOG.md
+++ b/src/cobalt/CHANGELOG.md
@@ -137,6 +137,11 @@
    rendering; the web app does not know about the custom transform so may not
    layout elements appropriately.
 
+ - **Added support for javascript code caching.**
+
+   Platforms can provide javascript code caching by implementing
+   CobaltExtensionJavaScriptCacheApi.
+
 ## Version 20
 
  - **Support for QUIC and SPDY is now enabled.**
diff --git a/src/cobalt/audio/audio_destination_node.cc b/src/cobalt/audio/audio_destination_node.cc
index e35f1f9..6f82ba8 100644
--- a/src/cobalt/audio/audio_destination_node.cc
+++ b/src/cobalt/audio/audio_destination_node.cc
@@ -59,6 +59,7 @@
   if (!audio_device_) {
     audio_device_.reset(
         new AudioDevice(static_cast<int>(channel_count(NULL)), this));
+    SB_LOG(INFO) << "Created audio device " << audio_device_.get() << '.';
     context()->PreventGarbageCollection();
   }
   audio_device_to_delete_ = NULL;
@@ -73,7 +74,10 @@
   DCHECK_EQ(number_of_inputs(), 1u);
   bool all_finished = true;
   Input(0)->FillAudioBus(audio_bus, silence, &all_finished);
-  if (all_consumed && all_finished) {
+  if (all_consumed && all_finished &&
+      audio_device_to_delete_ != audio_device_.get()) {
+    SB_LOG(INFO) << "Schedule to destroy audio device " << audio_device_.get()
+                 << '.';
     audio_device_to_delete_ = audio_device_.get();
     message_loop_->task_runner()->PostTask(
         FROM_HERE, base::Bind(&AudioDestinationNode::DestroyAudioDevice,
@@ -82,7 +86,12 @@
 }
 
 void AudioDestinationNode::DestroyAudioDevice() {
+  AudioLock::AutoLock lock(audio_lock());
+  if (!audio_device_.get()) {
+    return;
+  }
   if (audio_device_.get() == audio_device_to_delete_) {
+    SB_LOG(INFO) << "Destroying audio device " << audio_device_.get() << '.';
     audio_device_.reset();
     context()->AllowGarbageCollection();
   }
diff --git a/src/cobalt/base/message_queue.h b/src/cobalt/base/message_queue.h
index 60e72ea..be36eea 100644
--- a/src/cobalt/base/message_queue.h
+++ b/src/cobalt/base/message_queue.h
@@ -58,6 +58,14 @@
     }
   }
 
+  // Clear all the messages in the queue.
+  void ClearAll() {
+    TRACE_EVENT0("cobalt::base", "MessageQueue::ClearAll()");
+    base::AutoLock lock(mutex_);
+    std::queue<base::Closure> empty_queue;
+    empty_queue.swap(queue_);
+  }
+
  private:
   base::Lock mutex_;
   std::queue<base::Closure> queue_;
diff --git a/src/cobalt/browser/browser_module.cc b/src/cobalt/browser/browser_module.cc
index f2ba949..27fab77 100644
--- a/src/cobalt/browser/browser_module.cc
+++ b/src/cobalt/browser/browser_module.cc
@@ -765,7 +765,13 @@
   TRACE_EVENT0("cobalt::browser",
                "BrowserModule::ProcessRenderTreeSubmissionQueue()");
   DCHECK_EQ(base::MessageLoop::current(), self_message_loop_);
-  render_tree_submission_queue_.ProcessAll();
+  // If the app is preloaded, clear the render tree queue to avoid unnecessary
+  // rendering overhead.
+  if (application_state_ == base::kApplicationStatePreloading) {
+    render_tree_submission_queue_.ClearAll();
+  } else {
+    render_tree_submission_queue_.ProcessAll();
+  }
 }
 
 void BrowserModule::QueueOnRenderTreeProduced(
diff --git a/src/cobalt/build/build.id b/src/cobalt/build/build.id
index 4021ee4..189d70d 100644
--- a/src/cobalt/build/build.id
+++ b/src/cobalt/build/build.id
@@ -1 +1 @@
-300899
\ No newline at end of file
+301329
\ No newline at end of file
diff --git a/src/cobalt/cssom/viewport_size.h b/src/cobalt/cssom/viewport_size.h
index 6efe128..040d414 100644
--- a/src/cobalt/cssom/viewport_size.h
+++ b/src/cobalt/cssom/viewport_size.h
@@ -53,7 +53,7 @@
 
   // Ratio of CSS pixels per device pixel, matching the devicePixelRatio
   // attribute.
-  //   https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-window-devicepixelratio
+  //   https://www.w3.org/TR/cssom-view-1/#dom-window-devicepixelratio
   float device_pixel_ratio_ = 1.0f;
 };
 
diff --git a/src/cobalt/extension/extension_test.cc b/src/cobalt/extension/extension_test.cc
index 1bbc6c8..989ccac 100644
--- a/src/cobalt/extension/extension_test.cc
+++ b/src/cobalt/extension/extension_test.cc
@@ -19,6 +19,7 @@
 #include "cobalt/extension/font.h"
 #include "cobalt/extension/graphics.h"
 #include "cobalt/extension/installation_manager.h"
+#include "cobalt/extension/javascript_cache.h"
 #include "cobalt/extension/platform_service.h"
 #include "starboard/system.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -219,6 +220,29 @@
   EXPECT_EQ(second_extension_api, extension_api)
       << "Extension struct should be a singleton";
 }
+
+TEST(ExtensionTest, JavaScriptCache) {
+  typedef CobaltExtensionJavaScriptCacheApi ExtensionApi;
+  const char* kExtensionName = kCobaltExtensionJavaScriptCacheName;
+
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  if (!extension_api) {
+    return;
+  }
+
+  EXPECT_STREQ(extension_api->name, kExtensionName);
+  EXPECT_EQ(extension_api->version, 1u);
+  EXPECT_NE(extension_api->GetCachedScript, nullptr);
+  EXPECT_NE(extension_api->ReleaseCachedScriptData, nullptr);
+  EXPECT_NE(extension_api->StoreCachedScript, nullptr);
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
+  EXPECT_EQ(second_extension_api, extension_api)
+      << "Extension struct should be a singleton";
+}
+
 }  // namespace extension
 }  // namespace cobalt
 #endif  // SB_API_VERSION >= 11
diff --git a/src/cobalt/extension/javascript_cache.h b/src/cobalt/extension/javascript_cache.h
new file mode 100644
index 0000000..660c578
--- /dev/null
+++ b/src/cobalt/extension/javascript_cache.h
@@ -0,0 +1,62 @@
+// Copyright 2021 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_EXTENSION_JAVASCRIPT_CACHE_H_
+#define COBALT_EXTENSION_JAVASCRIPT_CACHE_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionJavaScriptCacheName \
+  "dev.cobalt.extension.JavaScriptCache"
+
+// The implementation must be thread-safe as the extension would
+// be called from different threads. Also all storage management
+// is delegated to the platform.
+typedef struct CobaltExtensionJavaScriptCacheApi {
+  // Name should be the string |kCobaltExtensionJavaScriptCacheName|.
+  // This helps to validate that the extension API is correct.
+  const char* name;
+
+  // This specifies the version of the API that is implemented.
+  uint32_t version;
+
+  // The fields below this point were added in version 1 or later.
+
+  // Retrieves the cached data for a script using |key|. The |source_length|
+  // provides the actual size of the script source in bytes.  The cached script
+  // bytes will be returned in |cache_data_out|. After the |cache_data| is
+  // processed the memory should be released by calling
+  // |ReleaseScriptCacheData|.
+  bool (*GetCachedScript)(uint32_t key, int source_length,
+                          const uint8_t** cache_data_out,
+                          int* cache_data_length);
+
+  // Releases the memory allocated for the |cache_data| by |GetCachedScript|.
+  void (*ReleaseCachedScriptData)(const uint8_t* cache_data);
+
+  // Stores the cached data for |key|.
+  bool (*StoreCachedScript)(uint32_t key, int source_length,
+                            const uint8_t* cache_data, int cache_data_length);
+} CobaltExtensionJavaScriptCacheApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+#endif  // COBALT_EXTENSION_JAVASCRIPT_CACHE_H_
diff --git a/src/cobalt/renderer/glimp_shaders/glsl/fragment_textured_vbo_uyvy_1plane.glsl b/src/cobalt/renderer/glimp_shaders/glsl/fragment_textured_vbo_uyvy_1plane.glsl
index 1a36b31..9a77757 100644
--- a/src/cobalt/renderer/glimp_shaders/glsl/fragment_textured_vbo_uyvy_1plane.glsl
+++ b/src/cobalt/renderer/glimp_shaders/glsl/fragment_textured_vbo_uyvy_1plane.glsl
@@ -30,6 +30,5 @@
   }

 

   vec4 untransformed_color = vec4(y_value, uv_value.r, uv_value.g, 1.0);

-  vec4 color = untransformed_color * to_rgb_color_matrix;

-  gl_FragColor = color;

+  gl_FragColor = untransformed_color * to_rgb_color_matrix;

 }

diff --git a/src/cobalt/script/v8c/v8c_global_environment.cc b/src/cobalt/script/v8c/v8c_global_environment.cc
index b19174c..044ea64 100644
--- a/src/cobalt/script/v8c/v8c_global_environment.cc
+++ b/src/cobalt/script/v8c/v8c_global_environment.cc
@@ -25,6 +25,7 @@
 #include "base/strings/stringprintf.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/polymorphic_downcast.h"
+#include "cobalt/script/javascript_engine.h"
 #include "cobalt/script/v8c/embedded_resources.h"
 #include "cobalt/script/v8c/entry_scope.h"
 #include "cobalt/script/v8c/v8c_script_value_factory.h"
@@ -32,6 +33,8 @@
 #include "cobalt/script/v8c/v8c_user_object_holder.h"
 #include "cobalt/script/v8c/v8c_value_handle.h"
 #include "nb/memory_scope.h"
+#include "starboard/common/murmurhash2.h"
+
 
 namespace cobalt {
 namespace script {
@@ -71,6 +74,18 @@
   return *v8::String::Utf8Value(isolate, value.As<v8::String>());
 }
 
+uint32_t CreateJavaScriptCacheKey(const std::string& javascript_engine_version,
+                                  uint32_t cached_data_version_tag,
+                                  const std::string& source,
+                                  const std::string& origin) {
+  uint32_t res = starboard::MurmurHash2_32(javascript_engine_version.c_str(),
+                                           javascript_engine_version.size(),
+                                           cached_data_version_tag);
+  res = starboard::MurmurHash2_32(source.c_str(), source.size(), res);
+  res = starboard::MurmurHash2_32(origin.c_str(), origin.size(), res);
+  return res;
+}
+
 }  // namespace
 
 V8cGlobalEnvironment::V8cGlobalEnvironment(v8::Isolate* isolate)
@@ -229,7 +244,11 @@
 }
 
 void V8cGlobalEnvironment::RemoveRoot(Traceable* traceable) {
-  V8cEngine::GetFromIsolate(isolate_)->heap_tracer()->RemoveRoot(traceable);
+  CHECK(isolate_);
+  V8cEngine* v8c_engine = V8cEngine::GetFromIsolate(isolate_);
+  CHECK(v8c_engine);
+  CHECK(v8c_engine->heap_tracer());
+  v8c_engine->heap_tracer()->RemoveRoot(traceable);
 }
 
 void V8cGlobalEnvironment::PreventGarbageCollection(
@@ -415,7 +434,29 @@
 
   v8::Local<v8::Context> context = isolate_->GetCurrentContext();
   v8::Local<v8::Script> script;
-  {
+
+  bool used_cache = false;
+#if SB_API_VERSION >= 11
+  const CobaltExtensionJavaScriptCacheApi* javascript_cache_extension =
+      static_cast<const CobaltExtensionJavaScriptCacheApi*>(
+          SbSystemGetExtension(kCobaltExtensionJavaScriptCacheName));
+  if (javascript_cache_extension &&
+      SbStringCompareAll(javascript_cache_extension->name,
+                         kCobaltExtensionJavaScriptCacheName) == 0 &&
+      javascript_cache_extension->version >= 1) {
+    TRACE_EVENT0("cobalt::script",
+                 "V8cGlobalEnvironment::CompileWithCaching()");
+    if (CompileWithCaching(javascript_cache_extension, context, v8c_source_code,
+                           source, &script_origin)
+            .ToLocal(&script)) {
+      used_cache = true;
+    } else {
+      LOG(WARNING) << "Failed to compile script.";
+      return {};
+    }
+  }
+#endif
+  if (!used_cache) {
     TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
     if (!v8::Script::Compile(context, source, &script_origin)
              .ToLocal(&script)) {
@@ -436,6 +477,78 @@
   return result;
 }
 
+v8::MaybeLocal<v8::Script> V8cGlobalEnvironment::CompileWithCaching(
+    const CobaltExtensionJavaScriptCacheApi* javascript_cache_extension,
+    v8::Local<v8::Context> context, V8cSourceCode* v8c_source_code,
+    v8::Local<v8::String> source, v8::ScriptOrigin* script_origin) {
+  const base::SourceLocation& source_location = v8c_source_code->location();
+
+  DLOG(INFO) << "CompileWithCaching: " << source_location.file_path.c_str()
+             << " " << v8c_source_code->location().file_path;
+
+  bool successful_cache = false;
+
+  std::string javascript_engine_version =
+      script::GetJavaScriptEngineNameAndVersion();
+  uint32_t javascript_cache_key = CreateJavaScriptCacheKey(
+      javascript_engine_version, v8::ScriptCompiler::CachedDataVersionTag(),
+      v8c_source_code->source_utf8(), source_location.file_path);
+  const uint8_t* cache_data_buf = nullptr;
+  int cache_data_size = -1;
+  v8::Local<v8::Script> script;
+  if (javascript_cache_extension->GetCachedScript(
+          javascript_cache_key, v8c_source_code->source_utf8().size(),
+          &cache_data_buf, &cache_data_size)) {
+    DLOG(INFO) << "CompileWithCaching:  Using cached resource";
+    v8::ScriptCompiler::CachedData* cached_code =
+        new v8::ScriptCompiler::CachedData(
+            cache_data_buf, cache_data_size,
+            v8::ScriptCompiler::CachedData::BufferNotOwned);
+    // The script_source owns the cached_code object.
+    v8::ScriptCompiler::Source script_source(source, *script_origin,
+                                             cached_code);
+
+    {
+      TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
+      successful_cache =
+          v8::ScriptCompiler::Compile(context, &script_source,
+                                      v8::ScriptCompiler::kConsumeCodeCache)
+              .ToLocal(&script) &&
+          !cached_code->rejected;
+      if (!successful_cache) {
+        LOG(WARNING)
+            << "CompileWithCaching: Failed to reuse the cached script rejected="
+            << cached_code->rejected;
+      }
+    }
+  }
+
+  if (cache_data_buf != nullptr) {
+    javascript_cache_extension->ReleaseCachedScriptData(cache_data_buf);
+    cache_data_buf = nullptr;
+  }
+
+  if (!successful_cache) {
+    DLOG(INFO) << "CompileWithCaching: compile";
+    {
+      TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
+      if (!v8::Script::Compile(context, source, script_origin)
+               .ToLocal(&script)) {
+        LOG(WARNING) << "CompileWithCaching: Failed to compile script.";
+        return {};
+      }
+    }
+    std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data(
+        v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
+    if (!javascript_cache_extension->StoreCachedScript(
+            javascript_cache_key, v8c_source_code->source_utf8().size(),
+            cached_data->data, cached_data->length)) {
+      LOG(WARNING) << "CompileWithCaching: Failed to store cached script";
+    }
+  }
+  return script;
+}
+
 void V8cGlobalEnvironment::EvaluateEmbeddedScript(const unsigned char* data,
                                                   size_t size,
                                                   const char* filename) {
diff --git a/src/cobalt/script/v8c/v8c_global_environment.h b/src/cobalt/script/v8c/v8c_global_environment.h
index c131a8d..6d6c9c5 100644
--- a/src/cobalt/script/v8c/v8c_global_environment.h
+++ b/src/cobalt/script/v8c/v8c_global_environment.h
@@ -26,9 +26,11 @@
 #include "base/optional.h"
 #include "base/stl_util.h"
 #include "base/threading/thread_checker.h"
+#include "cobalt/extension/javascript_cache.h"
 #include "cobalt/script/global_environment.h"
 #include "cobalt/script/javascript_engine.h"
 #include "cobalt/script/v8c/v8c_heap_tracer.h"
+#include "cobalt/script/v8c/v8c_source_code.h"
 #include "cobalt/script/v8c/wrapper_factory.h"
 #include "v8/include/libplatform/libplatform.h"
 #include "v8/include/v8.h"
@@ -145,6 +147,11 @@
   v8::MaybeLocal<v8::Value> EvaluateScriptInternal(
       const scoped_refptr<SourceCode>& source_code);
 
+  v8::MaybeLocal<v8::Script> CompileWithCaching(
+      const CobaltExtensionJavaScriptCacheApi* javascript_cache_extension,
+      v8::Local<v8::Context> context, V8cSourceCode* v8c_source_code,
+      v8::Local<v8::String> source, v8::ScriptOrigin* script_origin);
+
   void EvaluateEmbeddedScript(const unsigned char* data, size_t size,
                               const char* filename);
 
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
index 048ecfa..ac387f5 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -74,6 +74,9 @@
       int preferredBufferSizeInBytes,
       boolean enableAudioRouting,
       int tunnelModeAudioSessionId) {
+    // TODO: Re-enable audio routing when all related bugs are fixed.
+    enableAudioRouting = false;
+
     tunnelModeEnabled = tunnelModeAudioSessionId != -1;
     int channelConfig;
     switch (channelCount) {
@@ -166,6 +169,7 @@
             preferredBufferSizeInBytes,
             AudioTrack.getMinBufferSize(sampleRate, channelConfig, sampleType)));
     if (audioTrack != null && enableAudioRouting && Build.VERSION.SDK_INT >= 24) {
+      Log.i(TAG, "Audio routing enabled.");
       currentRoutedDevice = audioTrack.getRoutedDevice();
       onRoutingChangedListener =
           new AudioRouting.OnRoutingChangedListener() {
@@ -186,6 +190,8 @@
             }
           };
       audioTrack.addOnRoutingChangedListener(onRoutingChangedListener, null);
+    } else {
+      Log.i(TAG, "Audio routing disabled.");
     }
   }
 
@@ -195,7 +201,7 @@
 
   public void release() {
     if (audioTrack != null) {
-      if (Build.VERSION.SDK_INT >= 24) {
+      if (Build.VERSION.SDK_INT >= 24 && onRoutingChangedListener != null) {
         audioTrack.removeOnRoutingChangedListener(onRoutingChangedListener);
       }
       audioTrack.release();
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
index 5318110..cdbbf9a 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
@@ -120,6 +120,7 @@
     // On the emulator it fails with the log: "storeMetaDataInBuffers failed w/ err -1010"
     codecBlackList.add("OMX.google.vp9.decoder");
 
+    vp9WhiteList.put("Amazon", new HashSet<String>());
     vp9WhiteList.put("Amlogic", new HashSet<String>());
     vp9WhiteList.put("Arcadyan", new HashSet<String>());
     vp9WhiteList.put("arcelik", new HashSet<String>());
@@ -159,6 +160,7 @@
     vp9WhiteList.put("Xiaomi", new HashSet<String>());
     vp9WhiteList.put("ZTE TV", new HashSet<String>());
 
+    vp9WhiteList.get("Amazon").add("AFTS");
     vp9WhiteList.get("Amlogic").add("p212");
     vp9WhiteList.get("Arcadyan").add("Bouygtel4K");
     vp9WhiteList.get("Arcadyan").add("HMB2213PW22TS");
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
index c49cacb..9ec58e5 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
@@ -15,7 +15,6 @@
 package dev.cobalt.util;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.util.DisplayMetrics;
 import android.util.Size;
 import android.util.SizeF;
@@ -134,7 +133,6 @@
   private static DisplayMetrics cachedDisplayMetrics = null;
 
   private static DisplayMetrics getDisplayMetrics() {
-    Resources.getSystem().getDisplayMetrics();
     if (cachedDisplayMetrics == null) {
       cachedDisplayMetrics = new DisplayMetrics();
       Display display = getDefaultDisplay();
diff --git a/src/starboard/android/apk/build.id b/src/starboard/android/apk/build.id
new file mode 100644
index 0000000..13984bc
--- /dev/null
+++ b/src/starboard/android/apk/build.id
@@ -0,0 +1 @@
+301279
\ No newline at end of file
diff --git a/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc b/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc
index e28eabe..1c7d413 100644
--- a/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc
+++ b/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc
@@ -14,6 +14,7 @@
 
 #include "starboard/android/shared/audio_sink_min_required_frames_tester.h"
 
+#include <string>
 #include <vector>
 
 #include "starboard/android/shared/audio_track_audio_sink_type.h"
@@ -177,7 +178,9 @@
 
 // static
 void MinRequiredFramesTester::ErrorFunc(bool capability_changed,
+                                        const std::string& error_message,
                                         void* context) {
+  SB_LOG(ERROR) << "Error occurred while writing frames: " << error_message;
   // TODO: Handle errors during minimum frames test, maybe by terminating the
   //       test earlier.
   SB_NOTREACHED();
diff --git a/src/starboard/android/shared/audio_sink_min_required_frames_tester.h b/src/starboard/android/shared/audio_sink_min_required_frames_tester.h
index e689e96..669217a 100644
--- a/src/starboard/android/shared/audio_sink_min_required_frames_tester.h
+++ b/src/starboard/android/shared/audio_sink_min_required_frames_tester.h
@@ -17,6 +17,7 @@
 
 #include <atomic>
 #include <functional>
+#include <string>
 #include <vector>
 
 #include "starboard/common/condition_variable.h"
@@ -85,7 +86,9 @@
   static void ConsumeFramesFunc(int frames_consumed,
                                 SbTime frames_consumed_at,
                                 void* context);
-  static void ErrorFunc(bool capability_changed, void* context);
+  static void ErrorFunc(bool capability_changed,
+                        const std::string& error_message,
+                        void* context);
   void UpdateSourceStatus(int* frames_in_buffer,
                           int* offset_in_frames,
                           bool* is_playing,
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.cc b/src/starboard/android/shared/audio_track_audio_sink_type.cc
index 3f5b5b8..4c24e5b 100644
--- a/src/starboard/android/shared/audio_track_audio_sink_type.cc
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.cc
@@ -15,8 +15,10 @@
 #include "starboard/android/shared/audio_track_audio_sink_type.h"
 
 #include <algorithm>
+#include <string>
 #include <vector>
 
+#include "starboard/common/string.h"
 #include "starboard/shared/starboard/player/filter/common.h"
 
 namespace {
@@ -246,7 +248,8 @@
         j_audio_track_bridge_, "getAndResetHasNewAudioDeviceAdded", "()Z");
     if (new_audio_device_added) {
       SB_LOG(INFO) << "New audio device added.";
-      error_func_(kSbPlayerErrorCapabilityChanged, context_);
+      error_func_(kSbPlayerErrorCapabilityChanged, "New audio device added.",
+                  context_);
       break;
     }
 
@@ -395,10 +398,12 @@
       // Take all |frames_in_audio_track| as consumed since audio track could be
       // dead.
       consume_frames_func_(frames_in_audio_track, now, context_);
-      error_func_(written_frames == kAudioTrackErrorDeadObject
-                      ? kSbPlayerErrorCapabilityChanged
-                      : kSbPlayerErrorDecode,
-                  context_);
+
+      bool capabilities_changed = written_frames == kAudioTrackErrorDeadObject;
+      error_func_(
+          capabilities_changed,
+          FormatString("Error while writing frames: %d", written_frames),
+          context_);
       break;
     } else if (written_frames > 0) {
       last_written_succeeded_at = now;
diff --git a/src/starboard/android/shared/player_components_factory.h b/src/starboard/android/shared/player_components_factory.h
index 5e1b078..7caf57b 100644
--- a/src/starboard/android/shared/player_components_factory.h
+++ b/src/starboard/android/shared/player_components_factory.h
@@ -49,6 +49,11 @@
 
 using starboard::shared::starboard::media::MimeType;
 
+// Tunnel mode has to be enabled explicitly by the web app via mime attributes
+// "tunnelmode", set the following variable to true to force enabling tunnel
+// mode on all playbacks.
+constexpr bool kForceTunnelMode = false;
+
 // On some platforms tunnel mode is only supported in the secure pipeline.  Set
 // the following variable to true to force creating a secure pipeline in tunnel
 // mode, even for clear content.
@@ -113,7 +118,8 @@
     SB_DCHECK(frames_consumed == 0);
   }
 
-  void OnError(bool capability_changed) override {
+  void OnError(bool capability_changed,
+               const std::string& error_message) override {
     error_occurred_.store(true);
   }
 
@@ -196,6 +202,12 @@
                    << ". Tunnel mode is disabled.";
     }
 
+    if (kForceTunnelMode && !enable_tunnel_mode) {
+      SB_LOG(INFO) << "`kForceTunnelMode` is set to true, force enabling tunnel"
+                   << " mode.";
+      enable_tunnel_mode = true;
+    }
+
     bool force_secure_pipeline_under_tunnel_mode = false;
     if (enable_tunnel_mode &&
         IsTunnelModeSupported(creation_parameters,
diff --git a/src/starboard/android/shared/starboard_platform.gypi b/src/starboard/android/shared/starboard_platform.gypi
index 60e8129..97371fe 100644
--- a/src/starboard/android/shared/starboard_platform.gypi
+++ b/src/starboard/android/shared/starboard_platform.gypi
@@ -164,6 +164,8 @@
         'trace_util.h',
         'video_decoder.cc',
         'video_decoder.h',
+        'video_frame_tracker.cc',
+        'video_frame_tracker.h',
         'video_render_algorithm.cc',
         'video_render_algorithm.h',
         'video_window.cc',
diff --git a/src/starboard/android/shared/starboard_platform_tests.gypi b/src/starboard/android/shared/starboard_platform_tests.gypi
index 5b40e63..5b8fe50 100644
--- a/src/starboard/android/shared/starboard_platform_tests.gypi
+++ b/src/starboard/android/shared/starboard_platform_tests.gypi
@@ -23,6 +23,7 @@
         '<(DEPTH)/starboard/common/test_main.cc',
         '<@(media_tests_sources)',
         'jni_env_ext_test.cc',
+        'video_frame_tracker_test.cc',
       ],
       'defines': [
         # This allows the tests to include internal only header files.
diff --git a/src/starboard/android/shared/video_decoder.cc b/src/starboard/android/shared/video_decoder.cc
index ff53ef1..267285d 100644
--- a/src/starboard/android/shared/video_decoder.cc
+++ b/src/starboard/android/shared/video_decoder.cc
@@ -20,7 +20,6 @@
 #include <cmath>
 #include <functional>
 #include <list>
-#include <vector>
 
 #include "starboard/android/shared/application_android.h"
 #include "starboard/android/shared/decode_target_create.h"
@@ -36,7 +35,6 @@
 #include "starboard/drm.h"
 #include "starboard/memory.h"
 #include "starboard/shared/starboard/player/filter/video_frame_internal.h"
-#include "starboard/shared/starboard/thread_checker.h"
 #include "starboard/string.h"
 #include "starboard/thread.h"
 
@@ -154,125 +152,7 @@
 
 }  // namespace
 
-// TODO: Move into a separate file and write unit tests for it.
-class VideoFrameTracker {
- public:
-  SbTime seek_to_time() const { return seek_to_time_; }
-
-  void OnInputBuffer(SbTime timestamp) {
-    SB_DCHECK(thread_checker_.CalledOnValidThread());
-
-    if (frames_to_be_rendered_.empty()) {
-      frames_to_be_rendered_.push_back(timestamp);
-      return;
-    }
-
-    if (frames_to_be_rendered_.size() > kMaxPendingWorkSize * 2) {
-      // OnFrameRendered() is only available after API level 23.  Cap the size
-      // of |frames_to_be_rendered_| in case OnFrameRendered() is not available.
-      frames_to_be_rendered_.pop_front();
-    }
-
-    // Sort by |timestamp|, because |timestamp| won't be monotonic if there are
-    // B frames.
-    for (auto it = frames_to_be_rendered_.end();
-         it != frames_to_be_rendered_.begin();) {
-      it--;
-      if (*it < timestamp) {
-        frames_to_be_rendered_.emplace(++it, timestamp);
-        return;
-      } else if (*it == timestamp) {
-        SB_LOG(WARNING) << "feed video AU with same time stamp " << timestamp;
-        return;
-      }
-    }
-
-    frames_to_be_rendered_.emplace_front(timestamp);
-  }
-
-  void OnFrameRendered(int64_t frame_timestamp) {
-    ScopedLock lock(rendered_frames_mutex_);
-    rendered_frames_on_decoder_thread_.push_back(frame_timestamp);
-  }
-
-  void Seek(SbTime seek_to_time) {
-    SB_DCHECK(thread_checker_.CalledOnValidThread());
-
-    // Ensure that all dropped frames before seeking are captured.
-    UpdateDroppedFrames();
-
-    frames_to_be_rendered_.clear();
-    seek_to_time_ = seek_to_time;
-  }
-
-  int UpdateAndGetDroppedFrames() {
-    SB_DCHECK(thread_checker_.CalledOnValidThread());
-    UpdateDroppedFrames();
-    return dropped_frames_;
-  }
-
- private:
-  // TODO:
-  // * It is possible that the initial frame rendered time is before the seek to
-  //   time, when the platform decides to render a frame earlier than the seek
-  //   to time during preroll. This shouldn't be an issue after we align seek
-  //   time to the next video key frame.
-  // * The reported frame rendering time is the real time the frame is rendered.
-  //   It can be slightly different than the timestamp associated with the
-  //   InputBuffer.  For example, the frame with timestamp 120000 may be
-  //   rendered at 120020.  We have to account for this difference, as otherwise
-  //   lots of frames will be reported as being dropped.
-  void UpdateDroppedFrames() {
-    SB_DCHECK(thread_checker_.CalledOnValidThread());
-
-    {
-      ScopedLock lock(rendered_frames_mutex_);
-      rendered_frames_on_tracker_thread_.swap(
-          rendered_frames_on_decoder_thread_);
-    }
-
-    // TODO: Refine the algorithm, and consider using std::set<> for
-    //       |frames_to_be_rendered_|.
-    for (auto timestamp : rendered_frames_on_tracker_thread_) {
-      auto it = frames_to_be_rendered_.begin();
-      while (it != frames_to_be_rendered_.end()) {
-        if (*it > timestamp) {
-          break;
-        }
-
-        if (*it < seek_to_time_) {
-          it = frames_to_be_rendered_.erase(it);
-        } else if (*it < timestamp) {
-          SB_LOG(WARNING) << "Video frame dropped:" << *it
-                          << ", current frame timestamp:" << timestamp
-                          << ", frames in the backlog:"
-                          << frames_to_be_rendered_.size();
-          ++dropped_frames_;
-          it = frames_to_be_rendered_.erase(it);
-        } else if (*it == timestamp) {
-          it = frames_to_be_rendered_.erase(it);
-        } else {
-          ++it;
-        }
-      }
-    }
-
-    rendered_frames_on_tracker_thread_.clear();
-  }
-
-  ::starboard::shared::starboard::ThreadChecker thread_checker_;
-
-  std::list<SbTime> frames_to_be_rendered_;
-
-  int dropped_frames_ = 0;
-  SbTime seek_to_time_ = 0;
-
-  std::vector<SbTime> rendered_frames_on_tracker_thread_;
-  Mutex rendered_frames_mutex_;
-  std::vector<SbTime> rendered_frames_on_decoder_thread_;
-};
-
-// TODO: Merge this with VideoFrameTracker
+// TODO: Merge this with VideoFrameTracker, maybe?
 class VideoRenderAlgorithmTunneled : public VideoRenderAlgorithmBase {
  public:
   explicit VideoRenderAlgorithmTunneled(VideoFrameTracker* frame_tracker)
@@ -352,7 +232,7 @@
   SB_DCHECK(error_message);
 
   if (tunnel_mode_audio_session_id != -1) {
-    video_frame_tracker_.reset(new VideoFrameTracker);
+    video_frame_tracker_.reset(new VideoFrameTracker(kMaxPendingWorkSize * 2));
   }
   if (force_secure_pipeline_under_tunnel_mode) {
     SB_DCHECK(tunnel_mode_audio_session_id != -1);
diff --git a/src/starboard/android/shared/video_decoder.h b/src/starboard/android/shared/video_decoder.h
index 42d6901..5297bb5 100644
--- a/src/starboard/android/shared/video_decoder.h
+++ b/src/starboard/android/shared/video_decoder.h
@@ -22,6 +22,7 @@
 #include "starboard/android/shared/drm_system.h"
 #include "starboard/android/shared/media_codec_bridge.h"
 #include "starboard/android/shared/media_decoder.h"
+#include "starboard/android/shared/video_frame_tracker.h"
 #include "starboard/android/shared/video_window.h"
 #include "starboard/atomic.h"
 #include "starboard/common/condition_variable.h"
@@ -41,8 +42,6 @@
 namespace android {
 namespace shared {
 
-class VideoFrameTracker;
-
 class VideoDecoder
     : public ::starboard::shared::starboard::player::filter::VideoDecoder,
       private MediaDecoder::Host,
diff --git a/src/starboard/android/shared/video_frame_tracker.cc b/src/starboard/android/shared/video_frame_tracker.cc
new file mode 100644
index 0000000..3e3349f
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_tracker.cc
@@ -0,0 +1,144 @@
+// Copyright 2021 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/android/shared/video_frame_tracker.h"
+
+#include <cmath>
+#include <cstdint>
+#include <cstdlib>
+#include <vector>
+
+#include "starboard/common/log.h"
+#include "starboard/common/mutex.h"
+#include "starboard/time.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+const SbTime kMaxAllowedSkew = 5000;
+
+}  // namespace
+
+SbTime VideoFrameTracker::seek_to_time() const {
+  return seek_to_time_;
+}
+
+void VideoFrameTracker::OnInputBuffer(SbTime timestamp) {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  if (frames_to_be_rendered_.empty()) {
+    frames_to_be_rendered_.push_back(timestamp);
+    return;
+  }
+
+  if (frames_to_be_rendered_.size() > max_pending_frames_size_) {
+    // OnFrameRendered() is only available after API level 23.  Cap the size
+    // of |frames_to_be_rendered_| in case OnFrameRendered() is not available.
+    frames_to_be_rendered_.pop_front();
+  }
+
+  // Sort by |timestamp|, because |timestamp| won't be monotonic if there are
+  // B frames.
+  for (auto it = frames_to_be_rendered_.end();
+       it != frames_to_be_rendered_.begin();) {
+    it--;
+    if (*it < timestamp) {
+      frames_to_be_rendered_.emplace(++it, timestamp);
+      return;
+    } else if (*it == timestamp) {
+      SB_LOG(WARNING) << "feed video AU with same time stamp " << timestamp;
+      return;
+    }
+  }
+
+  frames_to_be_rendered_.emplace_front(timestamp);
+}
+
+void VideoFrameTracker::OnFrameRendered(int64_t frame_timestamp) {
+  ScopedLock lock(rendered_frames_mutex_);
+  rendered_frames_on_decoder_thread_.push_back(frame_timestamp);
+}
+
+void VideoFrameTracker::Seek(SbTime seek_to_time) {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  // Ensure that all dropped frames before seeking are captured.
+  UpdateDroppedFrames();
+
+  frames_to_be_rendered_.clear();
+  seek_to_time_ = seek_to_time;
+}
+
+int VideoFrameTracker::UpdateAndGetDroppedFrames() {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+  UpdateDroppedFrames();
+  return dropped_frames_;
+}
+
+void VideoFrameTracker::UpdateDroppedFrames() {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  {
+    ScopedLock lock(rendered_frames_mutex_);
+    rendered_frames_on_tracker_thread_.swap(rendered_frames_on_decoder_thread_);
+  }
+
+  while (frames_to_be_rendered_.front() < seek_to_time_) {
+    // It is possible that the initial frame rendered time is before the
+    // seek to time, when the platform decides to render a frame earlier
+    // than the seek to time during preroll. This shouldn't be an issue
+    // after we align seek time to the next video key frame.
+    frames_to_be_rendered_.pop_front();
+  }
+
+  // Loop over all timestamps from OnFrameRendered and compare against ones from
+  // OnInputBuffer.
+  for (auto rendered_timestamp : rendered_frames_on_tracker_thread_) {
+    auto to_render_timestamp = frames_to_be_rendered_.begin();
+    // Loop over all frames to render until we've caught up to the timestamp of
+    // the last rendered frame.
+    while (to_render_timestamp != frames_to_be_rendered_.end() &&
+           !(*to_render_timestamp - rendered_timestamp > kMaxAllowedSkew)) {
+      if (std::abs(*to_render_timestamp - rendered_timestamp) <=
+          kMaxAllowedSkew) {
+        // This frame was rendered, remove it from frames_to_be_rendered_.
+        to_render_timestamp = frames_to_be_rendered_.erase(to_render_timestamp);
+      } else if (rendered_timestamp - *to_render_timestamp > kMaxAllowedSkew) {
+        // The rendered frame is too far ahead. The to_render_timestamp frame
+        // was dropped.
+        SB_LOG(WARNING) << "Video frame dropped:" << *to_render_timestamp
+                        << ", current frame timestamp:" << rendered_timestamp
+                        << ", frames in the backlog:"
+                        << frames_to_be_rendered_.size();
+        ++dropped_frames_;
+        to_render_timestamp = frames_to_be_rendered_.erase(to_render_timestamp);
+      } else {
+        // The rendered frame is too early to match the next frame to render.
+        // This could happen if a frame is reported to be rendered twice or if
+        // it is rendered more than kMaxAllowedSkew early. In the latter
+        // scenario the frame will be reported dropped in the next iteration of
+        // the outer loop.
+        ++to_render_timestamp;
+      }
+    }
+  }
+
+  rendered_frames_on_tracker_thread_.clear();
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/video_frame_tracker.h b/src/starboard/android/shared/video_frame_tracker.h
new file mode 100644
index 0000000..8b88f59
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_tracker.h
@@ -0,0 +1,64 @@
+// Copyright 2021 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_ANDROID_SHARED_VIDEO_FRAME_TRACKER_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_FRAME_TRACKER_H_
+
+#include <list>
+#include <vector>
+
+#include "starboard/common/mutex.h"
+#include "starboard/shared/starboard/thread_checker.h"
+#include "starboard/time.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoFrameTracker {
+ public:
+  explicit VideoFrameTracker(int max_pending_frames_size)
+      : max_pending_frames_size_(max_pending_frames_size) {}
+
+  SbTime seek_to_time() const;
+
+  void OnInputBuffer(SbTime timestamp);
+
+  void OnFrameRendered(int64_t frame_timestamp);
+
+  void Seek(SbTime seek_to_time);
+
+  int UpdateAndGetDroppedFrames();
+
+ private:
+  void UpdateDroppedFrames();
+
+  ::starboard::shared::starboard::ThreadChecker thread_checker_;
+
+  std::list<SbTime> frames_to_be_rendered_;
+
+  const int max_pending_frames_size_;
+  int dropped_frames_ = 0;
+  SbTime seek_to_time_ = 0;
+
+  Mutex rendered_frames_mutex_;
+  std::vector<SbTime> rendered_frames_on_tracker_thread_;
+  std::vector<SbTime> rendered_frames_on_decoder_thread_;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_VIDEO_FRAME_TRACKER_H_
diff --git a/src/starboard/android/shared/video_frame_tracker_test.cc b/src/starboard/android/shared/video_frame_tracker_test.cc
new file mode 100644
index 0000000..5e61b00
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_tracker_test.cc
@@ -0,0 +1,167 @@
+// Copyright 2021 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 <string>
+
+#include "starboard/android/shared/video_frame_tracker.h"
+#include "starboard/time.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+TEST(VideoFrameTrackerTest, DroppedFrameCountIsCumulative) {
+  VideoFrameTracker video_frame_tracker(100);
+
+  video_frame_tracker.OnInputBuffer(10000);
+  video_frame_tracker.OnInputBuffer(20000);
+  video_frame_tracker.OnInputBuffer(30000);
+  video_frame_tracker.OnInputBuffer(40000);
+  video_frame_tracker.OnInputBuffer(50000);
+  video_frame_tracker.OnInputBuffer(60000);
+  video_frame_tracker.OnInputBuffer(70000);
+  video_frame_tracker.OnInputBuffer(80000);
+
+  video_frame_tracker.OnFrameRendered(10000);
+  video_frame_tracker.OnFrameRendered(20000);
+  // Frame with timestamp 30000 was dropped
+  video_frame_tracker.OnFrameRendered(40000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 1);
+  video_frame_tracker.OnFrameRendered(50000);
+  // Frame with timestamp 60000 was dropped
+  video_frame_tracker.OnFrameRendered(70000);
+  // Frame with timestamp 80000 is not rendered and not dropped.
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 2);
+}
+
+TEST(VideoFrameTrackerTest, OnlySkewOverMaxAllowedShouldResultInDroppedFrame) {
+  VideoFrameTracker video_frame_tracker(100);
+
+  video_frame_tracker.OnInputBuffer(100000);
+  video_frame_tracker.OnInputBuffer(200000);
+  video_frame_tracker.OnInputBuffer(300000);
+  video_frame_tracker.OnInputBuffer(400000);
+  video_frame_tracker.OnInputBuffer(500000);
+  video_frame_tracker.OnInputBuffer(600000);
+
+  video_frame_tracker.OnFrameRendered(104999);  // 4999us late -> not dropped
+  video_frame_tracker.OnFrameRendered(195001);  // 4999us early -> not dropped
+  video_frame_tracker.OnFrameRendered(300000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 0);
+
+  video_frame_tracker.OnFrameRendered(394999);  // 5001us early -> dropped
+  video_frame_tracker.OnFrameRendered(505001);  // 5001us late -> dropped
+  video_frame_tracker.OnFrameRendered(600000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 2);
+}
+
+TEST(VideoFrameTrackerTest, MultipleFramesDroppedAreReportedCorrectly) {
+  VideoFrameTracker video_frame_tracker(100);
+
+  video_frame_tracker.OnInputBuffer(10000);
+  video_frame_tracker.OnInputBuffer(20000);
+  video_frame_tracker.OnInputBuffer(30000);
+  video_frame_tracker.OnInputBuffer(40000);
+  video_frame_tracker.OnInputBuffer(50000);
+  video_frame_tracker.OnInputBuffer(60000);
+  video_frame_tracker.OnInputBuffer(70000);
+  video_frame_tracker.OnInputBuffer(80000);
+  video_frame_tracker.OnInputBuffer(90000);
+
+  video_frame_tracker.OnFrameRendered(10000);
+  video_frame_tracker.OnFrameRendered(20000);
+  // Frame with timestamp 30000 was dropped.
+  video_frame_tracker.OnFrameRendered(40000);
+  // Frames with timestamp 50000, 60000, 70000, 80000 were dropped.
+  video_frame_tracker.OnFrameRendered(90000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 5);
+}
+
+TEST(VideoFrameTrackerTest, RenderQueueIsClearedOnSeek) {
+  VideoFrameTracker video_frame_tracker(100);
+
+  video_frame_tracker.OnInputBuffer(10000);
+  video_frame_tracker.OnInputBuffer(20000);
+  video_frame_tracker.OnInputBuffer(30000);
+
+  // These frames are not rendered but due to the seek below should not count as
+  // dropped.
+  video_frame_tracker.OnInputBuffer(40000);
+  video_frame_tracker.OnInputBuffer(50000);
+  video_frame_tracker.OnInputBuffer(60000);
+
+  video_frame_tracker.OnFrameRendered(10000);
+  // Frame with timestamp 20000 was dropped and should be registered.
+  video_frame_tracker.OnFrameRendered(30000);
+  // Frames 40000, 50000, 60000 were never rendered.
+
+  const SbTime kSeekToTime = 90000;
+  video_frame_tracker.Seek(kSeekToTime);
+  ASSERT_EQ(kSeekToTime, video_frame_tracker.seek_to_time());
+
+  video_frame_tracker.OnInputBuffer(90000);
+  video_frame_tracker.OnInputBuffer(100000);
+  video_frame_tracker.OnInputBuffer(110000);
+  video_frame_tracker.OnInputBuffer(120000);
+
+  video_frame_tracker.OnFrameRendered(90000);
+  video_frame_tracker.OnFrameRendered(100000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 1);
+}
+
+TEST(VideoFrameTrackerTest, UnorderedInputFramesAreHandled) {
+  VideoFrameTracker video_frame_tracker(100);
+
+  // Order of input frames should not matter as long as they are before the
+  // render timestamp.
+  video_frame_tracker.OnInputBuffer(20000);
+  video_frame_tracker.OnInputBuffer(30000);
+  video_frame_tracker.OnInputBuffer(10000);
+  video_frame_tracker.OnInputBuffer(40000);
+  video_frame_tracker.OnInputBuffer(60000);
+  video_frame_tracker.OnInputBuffer(80000);
+
+  video_frame_tracker.OnFrameRendered(10000);
+  video_frame_tracker.OnFrameRendered(20000);
+  video_frame_tracker.OnFrameRendered(30000);
+  video_frame_tracker.OnFrameRendered(40000);
+
+  // Add more input frames after the first frames are rendered.
+  video_frame_tracker.OnInputBuffer(50000);
+  video_frame_tracker.OnInputBuffer(70000);
+  video_frame_tracker.OnInputBuffer(90000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 0);
+
+  video_frame_tracker.OnFrameRendered(50000);
+  video_frame_tracker.OnFrameRendered(60000);
+  video_frame_tracker.OnFrameRendered(70000);
+  video_frame_tracker.OnFrameRendered(80000);
+
+  EXPECT_EQ(video_frame_tracker.UpdateAndGetDroppedFrames(), 0);
+}
+
+}  // namespace
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/evergreen/testing/linux/start_cobalt.sh b/src/starboard/evergreen/testing/linux/start_cobalt.sh
index e4d3823..b060bbc 100755
--- a/src/starboard/evergreen/testing/linux/start_cobalt.sh
+++ b/src/starboard/evergreen/testing/linux/start_cobalt.sh
@@ -37,7 +37,6 @@
 
   log "info" " Starting Cobalt with:"
   log "info" "  --url=${URL}"
-  log "info" "  --content=${CONTENT}"
 
   for arg in $ARGS; do
     log "info" "  ${arg}"
@@ -45,7 +44,7 @@
 
   log "info" " Logs will be output to '${LOG_PATH}/${LOG}'"
 
-  eval "${OUT}/loader_app --url=\"\"${URL}\"\" --content=${CONTENT} ${ARGS} 2>&1 | tee \"${LOG_PATH}/${LOG}\"" &
+  eval "${OUT}/loader_app --url=\"\"${URL}\"\" ${ARGS} 2>&1 | tee \"${LOG_PATH}/${LOG}\"" &
 
   eval $__LOADER=$!
 
diff --git a/src/starboard/evergreen/testing/raspi/start_cobalt.sh b/src/starboard/evergreen/testing/raspi/start_cobalt.sh
index ae370d8..df4efb4 100755
--- a/src/starboard/evergreen/testing/raspi/start_cobalt.sh
+++ b/src/starboard/evergreen/testing/raspi/start_cobalt.sh
@@ -37,7 +37,6 @@
 
   log "info" " Starting Cobalt with:"
   log "info" "  --url=${URL}"
-  log "info" "  --content=${CONTENT}"
 
   for arg in $ARGS; do
     log "info" "  ${arg}"
@@ -45,7 +44,7 @@
 
   log "info" " Logs will be output to '${LOG_PATH}/${LOG}'"
 
-  eval "${SSH}\"/home/pi/coeg/loader_app --url=\"\"${URL}\"\" --content=${CONTENT} ${ARGS} \" 2>&1 | tee \"${LOG_PATH}/${LOG}\"" &
+  eval "${SSH}\"/home/pi/coeg/loader_app --url=\"\"${URL}\"\" ${ARGS} \" 2>&1 | tee \"${LOG_PATH}/${LOG}\"" &
 
   eval $__LOADER=$!
 
diff --git a/src/starboard/evergreen/testing/tests/alternative_content_test.sh b/src/starboard/evergreen/testing/tests/alternative_content_test.sh
index 84601d9..f3f6bb8 100755
--- a/src/starboard/evergreen/testing/tests/alternative_content_test.sh
+++ b/src/starboard/evergreen/testing/tests/alternative_content_test.sh
@@ -32,7 +32,7 @@
     return 1
   fi
 
-  cycle_cobalt "file:///tests/${TEST_FILE}?channel=test" "${TEST_NAME}.1.log" "App is up to date" "--content=${CONTENT}"
+  cycle_cobalt "file:///tests/${TEST_FILE}?channel=test" "${TEST_NAME}.1.log" "App is up to date" "--content=${STORAGE_DIR}/installation_1/content"
 
   if [[ $? -ne 0 ]]; then
     log "error" "Failed to find 'App is up to date' indicating failure"
diff --git a/src/starboard/loader_app/app_key_internal.cc b/src/starboard/loader_app/app_key_internal.cc
index 4fcd641..12e2f62 100644
--- a/src/starboard/loader_app/app_key_internal.cc
+++ b/src/starboard/loader_app/app_key_internal.cc
@@ -115,6 +115,11 @@
 
   output.resize(output_size);
 
+  // Replace the '+' and '/' characters with '-' and '_' so that they are safe
+  // to use in a URL and with the filesystem.
+  std::replace(output.begin(), output.end(), '+', '-');
+  std::replace(output.begin(), output.end(), '/', '_');
+
   return output;
 }
 
diff --git a/src/starboard/loader_app/app_key_test.cc b/src/starboard/loader_app/app_key_test.cc
index 8207ee4..af3d5e1 100644
--- a/src/starboard/loader_app/app_key_test.cc
+++ b/src/starboard/loader_app/app_key_test.cc
@@ -673,5 +673,18 @@
   }
 }
 
+TEST(AppKeyTest, SunnyDayExtractAppKeySanitizesResult) {
+  // This magic is 11111011 11110000 in binary and thus begins with '+' and '/'
+  // when base64 encoded.
+  uint8_t magic[3] = {0xFB, 0xF0, 0x00};
+
+  const std::string actual =
+      ExtractAppKey(reinterpret_cast<const char*>(magic));
+
+  // Check that the '+' and '/' characters were replaced with '-' and '_'.
+  EXPECT_EQ("\xFB\xF0", actual);
+  EXPECT_EQ("-_A=", EncodeAppKey(actual));
+}
+
 }  // namespace loader_app
 }  // namespace starboard
diff --git a/src/starboard/sabi/README.md b/src/starboard/sabi/README.md
new file mode 100644
index 0000000..7becc8b
--- /dev/null
+++ b/src/starboard/sabi/README.md
@@ -0,0 +1,4 @@
+**IMPORTANT:** None of the files in this directory or any subdirectory should be
+modified. These files are used to define the Starboard ABI for a particular
+platform, and any changes can cause the binaries generated using those changes
+to no longer be compatible with any Evergreen binaries.
diff --git a/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h b/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h
index c1cf847..fce1821 100644
--- a/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h
+++ b/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h
@@ -18,6 +18,7 @@
 #include "starboard/audio_sink.h"
 
 #include <functional>
+#include <string>
 
 #include "starboard/configuration.h"
 #include "starboard/shared/internal_only.h"
@@ -27,8 +28,9 @@
   // When |capability_changed| is true, it hints that the error is caused by a
   // a transisent capability on the platform.  The app should retry playback to
   // recover from the error.
-  // TODO: Allow to pass an error message.
-  typedef void (*ErrorFunc)(bool capability_changed, void* context);
+  typedef void (*ErrorFunc)(bool capability_changed,
+                            const std::string& error_message,
+                            void* context);
 #endif  // SB_API_VERSION >= 12
 
   typedef std::function<
diff --git a/src/starboard/shared/starboard/media/avc_util.cc b/src/starboard/shared/starboard/media/avc_util.cc
index d5b9620..15b5ba9 100644
--- a/src/starboard/shared/starboard/media/avc_util.cc
+++ b/src/starboard/shared/starboard/media/avc_util.cc
@@ -117,10 +117,12 @@
       }
       parameter_sets_.push_back(nalu);
       combined_size_in_bytes_ += nalu.size();
-    } else {
+    } else if (nalu[kAnnexBHeaderSizeInBytes] == kIdrStartCode) {
       break;
     }
   }
+  SB_LOG_IF(ERROR, first_sps_index_ == -1 || first_pps_index_ == -1)
+      << "AVC parameter set NALUs not found.";
 }
 
 AvcParameterSets AvcParameterSets::ConvertTo(Format new_format) const {
diff --git a/src/starboard/shared/starboard/media/avc_util.h b/src/starboard/shared/starboard/media/avc_util.h
index d603aac..c9d7931 100644
--- a/src/starboard/shared/starboard/media/avc_util.h
+++ b/src/starboard/shared/starboard/media/avc_util.h
@@ -45,6 +45,7 @@
   };
 
   static const size_t kAnnexBHeaderSizeInBytes = 4;
+  static const uint8_t kIdrStartCode = 0x65;
   static const uint8_t kSpsStartCode = 0x67;
   static const uint8_t kPpsStartCode = 0x68;
 
diff --git a/src/starboard/shared/starboard/media/avc_util_test.cc b/src/starboard/shared/starboard/media/avc_util_test.cc
index 09345cb..a6b954e 100644
--- a/src/starboard/shared/starboard/media/avc_util_test.cc
+++ b/src/starboard/shared/starboard/media/avc_util_test.cc
@@ -33,7 +33,7 @@
 const auto kAnnexBHeaderSizeInBytes =
     AvcParameterSets::kAnnexBHeaderSizeInBytes;
 const uint8_t kSliceStartCode = 0x61;
-const uint8_t kIdrStartCode = 0x65;
+const uint8_t kIdrStartCode = AvcParameterSets::kIdrStartCode;
 const uint8_t kSpsStartCode = AvcParameterSets::kSpsStartCode;
 const uint8_t kPpsStartCode = AvcParameterSets::kPpsStartCode;
 
@@ -323,7 +323,7 @@
   }
 }
 
-TEST(AvcParameterSetsTest, SpsAfterPayload) {
+TEST(AvcParameterSetsTest, SpsAfterIdr) {
   auto parameter_sets_in_annex_b = kSpsInAnnexB + kPpsInAnnexB;
   auto nalus_in_annex_b =
       parameter_sets_in_annex_b + kIdrInAnnexB + kSpsInAnnexB;
@@ -334,7 +334,7 @@
       HasEqualParameterSets(parameter_sets_in_annex_b, nalus_in_annex_b));
 }
 
-TEST(AvcParameterSetsTest, PpsAfterPayload) {
+TEST(AvcParameterSetsTest, PpsAfterIdr) {
   auto parameter_sets_in_annex_b = kSpsInAnnexB + kPpsInAnnexB;
   auto nalus_in_annex_b =
       parameter_sets_in_annex_b + kIdrInAnnexB + kPpsInAnnexB;
@@ -345,7 +345,7 @@
       HasEqualParameterSets(parameter_sets_in_annex_b, nalus_in_annex_b));
 }
 
-TEST(AvcParameterSetsTest, SpsAndPpsAfterPayloadWithoutSpsAndPps) {
+TEST(AvcParameterSetsTest, SpsAndPpsAfterIdrWithoutSpsAndPps) {
   auto parameter_sets_in_annex_b = kSpsInAnnexB + kPpsInAnnexB;
   auto nalus_in_annex_b =
       kIdrInAnnexB + kPpsInAnnexB + parameter_sets_in_annex_b;
diff --git a/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.cc b/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.cc
index 1e3bb83..12ce929 100644
--- a/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.cc
+++ b/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.cc
@@ -15,6 +15,7 @@
 #include "starboard/shared/starboard/player/filter/audio_renderer_internal_impl.h"
 
 #include <algorithm>
+#include <string>
 
 #include "starboard/memory.h"
 #include "starboard/shared/starboard/media/media_util.h"
@@ -449,15 +450,16 @@
   }
 }
 
-void AudioRendererImpl::OnError(bool capability_changed) {
+void AudioRendererImpl::OnError(bool capability_changed,
+                                const std::string& error_message) {
   SB_DCHECK(error_cb_);
   if (capability_changed) {
-    error_cb_(kSbPlayerErrorCapabilityChanged, "failed to start audio sink");
+    error_cb_(kSbPlayerErrorCapabilityChanged, error_message);
   } else {
     // Send |kSbPlayerErrorDecode| on fatal audio sink error.  The error code
     // will be mapped into MediaError eventually, and there is no corresponding
     // error code in MediaError for audio sink error anyway.
-    error_cb_(kSbPlayerErrorDecode, "failed to start audio sink");
+    error_cb_(kSbPlayerErrorDecode, error_message);
   }
 }
 
diff --git a/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.h b/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.h
index 90e9a1b..1129b9d 100644
--- a/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.h
+++ b/src/starboard/shared/starboard/player/filter/audio_renderer_internal_impl.h
@@ -16,6 +16,7 @@
 #define STARBOARD_SHARED_STARBOARD_PLAYER_FILTER_AUDIO_RENDERER_INTERNAL_IMPL_H_
 
 #include <functional>
+#include <string>
 #include <vector>
 
 #include "starboard/atomic.h"
@@ -138,7 +139,8 @@
                        bool* is_playing,
                        bool* is_eos_reached) override;
   void ConsumeFrames(int frames_consumed, SbTime frames_consumed_at) override;
-  void OnError(bool capability_changed) override;
+  void OnError(bool capability_changed,
+               const std::string& error_message) override;
 
   void UpdateVariablesOnSinkThread_Locked(SbTime system_time_on_consume_frames);
 
diff --git a/src/starboard/shared/starboard/player/filter/audio_renderer_sink.h b/src/starboard/shared/starboard/player/filter/audio_renderer_sink.h
index 7142d0a..1859e7c 100644
--- a/src/starboard/shared/starboard/player/filter/audio_renderer_sink.h
+++ b/src/starboard/shared/starboard/player/filter/audio_renderer_sink.h
@@ -15,6 +15,8 @@
 #ifndef STARBOARD_SHARED_STARBOARD_PLAYER_FILTER_AUDIO_RENDERER_SINK_H_
 #define STARBOARD_SHARED_STARBOARD_PLAYER_FILTER_AUDIO_RENDERER_SINK_H_
 
+#include <string>
+
 #include "starboard/audio_sink.h"
 #include "starboard/shared/internal_only.h"
 #include "starboard/time.h"
@@ -40,7 +42,8 @@
     // When |capability_changed| is true, it hints that the error is caused by a
     // a transisent capability on the platform.  The app should retry playback
     // to recover from the error.
-    virtual void OnError(bool capability_changed) = 0;
+    virtual void OnError(bool capability_changed,
+                         const std::string& error_message) = 0;
 
    protected:
     ~RenderCallback() {}
diff --git a/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.cc b/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.cc
index 61a48db..4970a5b 100644
--- a/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.cc
+++ b/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.cc
@@ -14,6 +14,8 @@
 
 #include "starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h"
 
+#include <string>
+
 #include "starboard/common/log.h"
 #include "starboard/configuration_constants.h"
 #include "starboard/shared/starboard/thread_checker.h"
@@ -187,13 +189,16 @@
 }
 
 // static
-void AudioRendererSinkImpl::ErrorFunc(bool capability_changed, void* context) {
+void AudioRendererSinkImpl::ErrorFunc(bool capability_changed,
+                                      const std::string& error_message,
+                                      void* context) {
   AudioRendererSinkImpl* audio_renderer_sink =
       static_cast<AudioRendererSinkImpl*>(context);
   SB_DCHECK(audio_renderer_sink);
   SB_DCHECK(audio_renderer_sink->render_callback_);
 
-  audio_renderer_sink->render_callback_->OnError(capability_changed);
+  audio_renderer_sink->render_callback_->OnError(capability_changed,
+                                                 error_message);
 }
 
 }  // namespace filter
diff --git a/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h b/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h
index 239180f..319cb32 100644
--- a/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h
+++ b/src/starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h
@@ -16,6 +16,7 @@
 #define STARBOARD_SHARED_STARBOARD_PLAYER_FILTER_AUDIO_RENDERER_SINK_IMPL_H_
 
 #include <functional>
+#include <string>
 
 #include "starboard/audio_sink.h"
 #include "starboard/shared/internal_only.h"
@@ -57,8 +58,8 @@
       SbMediaAudioSampleType audio_sample_type) const override;
   bool IsAudioFrameStorageTypeSupported(
       SbMediaAudioFrameStorageType audio_frame_storage_type) const override;
-  int GetNearestSupportedSampleFrequency(int sampling_frequency_hz) const
-      override;
+  int GetNearestSupportedSampleFrequency(
+      int sampling_frequency_hz) const override;
 
   bool HasStarted() const override;
   void Start(SbTime media_start_time,
@@ -83,7 +84,9 @@
   static void ConsumeFramesFunc(int frames_consumed,
                                 SbTime frames_consumed_at,
                                 void* context);
-  static void ErrorFunc(bool capability_changed, void* context);
+  static void ErrorFunc(bool capability_changed,
+                        const std::string& error_message,
+                        void* context);
 
   ThreadChecker thread_checker_;
   const CreateAudioSinkFunc create_audio_sink_func_;
diff --git a/src/starboard/shared/starboard/player/player_create.cc b/src/starboard/shared/starboard/player/player_create.cc
index 5e7bb68..9a55ac8 100644
--- a/src/starboard/shared/starboard/player/player_create.cc
+++ b/src/starboard/shared/starboard/player/player_create.cc
@@ -88,7 +88,7 @@
       &creation_param->audio_sample_info;
   const auto output_mode = creation_param->output_mode;
 
-#else  // SB_HAS(PLAYER_CREATION_AND_OUTPUT_MODE_QUERY_IMPROVEMENT)
+#else   // SB_HAS(PLAYER_CREATION_AND_OUTPUT_MODE_QUERY_IMPROVEMENT)
 
 SbPlayer SbPlayerCreate(SbWindow window,
                         SbMediaVideoCodec video_codec,
@@ -169,8 +169,11 @@
 
   if (audio_sample_info &&
       audio_sample_info->number_of_channels > SbAudioSinkGetMaxChannels()) {
-    SB_LOG(ERROR) << "audio_sample_info->number_of_channels exceeds the maximum"
-                  << " number of audio channels supported by this platform.";
+    SB_LOG(ERROR) << "audio_sample_info->number_of_channels ("
+                  << audio_sample_info->number_of_channels
+                  << ") exceeds the maximum"
+                  << " number of audio channels supported by this platform ("
+                  << SbAudioSinkGetMaxChannels() << ").";
     return kSbPlayerInvalid;
   }
 
diff --git a/src/starboard/stub/javascript_cache.cc b/src/starboard/stub/javascript_cache.cc
new file mode 100644
index 0000000..82e8bdd
--- /dev/null
+++ b/src/starboard/stub/javascript_cache.cc
@@ -0,0 +1,60 @@
+// Copyright 2021 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/stub/javascript_cache.h"
+
+#include <functional>
+#include <string>
+
+#include "cobalt/extension/javascript_cache.h"
+#include "starboard/common/log.h"
+#include "starboard/file.h"
+
+namespace starboard {
+namespace stub {
+
+namespace {
+
+bool GetCachedScript(uint32_t key,
+                     int source_length,
+                     const uint8_t** cache_data_out,
+                     int* cache_data_length) {
+  return false;
+}
+
+void ReleaseCachedScriptData(const uint8_t* cache_data) {}
+
+bool StoreCachedScript(uint32_t key,
+                       int source_length,
+                       const uint8_t* cache_data,
+                       int cache_data_length) {
+  return false;
+}
+
+const CobaltExtensionJavaScriptCacheApi kJavaScriptCacheApi = {
+    kCobaltExtensionJavaScriptCacheName,
+    1,  // API version that's implemented.
+    &GetCachedScript,
+    &ReleaseCachedScriptData,
+    &StoreCachedScript,
+};
+
+}  // namespace
+
+const void* GetJavaScriptCacheApi() {
+  return &kJavaScriptCacheApi;
+}
+
+}  // namespace stub
+}  // namespace starboard
diff --git a/src/starboard/stub/javascript_cache.h b/src/starboard/stub/javascript_cache.h
new file mode 100644
index 0000000..e0e04d6
--- /dev/null
+++ b/src/starboard/stub/javascript_cache.h
@@ -0,0 +1,26 @@
+// Copyright 2021 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_STUB_JAVASCRIPT_CACHE_H_
+#define STARBOARD_STUB_JAVASCRIPT_CACHE_H_
+
+namespace starboard {
+namespace stub {
+
+const void* GetJavaScriptCacheApi();
+
+}  // namespace stub
+}  // namespace starboard
+
+#endif  // STARBOARD_STUB_JAVASCRIPT_CACHE_H_
diff --git a/src/starboard/stub/starboard_platform.gyp b/src/starboard/stub/starboard_platform.gyp
index 717849d..62cceef 100644
--- a/src/starboard/stub/starboard_platform.gyp
+++ b/src/starboard/stub/starboard_platform.gyp
@@ -29,6 +29,8 @@
         'configuration_constants.cc',
         'font.cc',
         'font.h',
+        'javascript_cache.h',
+        'javascript_cache.cc',
         'main.cc',
         'system_get_extensions.cc',
         'thread_types_public.h',
diff --git a/src/starboard/stub/system_get_extensions.cc b/src/starboard/stub/system_get_extensions.cc
index 22e8777..f3f9783 100644
--- a/src/starboard/stub/system_get_extensions.cc
+++ b/src/starboard/stub/system_get_extensions.cc
@@ -16,9 +16,11 @@
 
 #include "cobalt/extension/configuration.h"
 #include "cobalt/extension/font.h"
+#include "cobalt/extension/javascript_cache.h"
 #include "starboard/common/string.h"
 #include "starboard/stub/configuration.h"
 #include "starboard/stub/font.h"
+#include "starboard/stub/javascript_cache.h"
 
 const void* SbSystemGetExtension(const char* name) {
   if (SbStringCompareAll(name, kCobaltExtensionConfigurationName) == 0) {
@@ -27,5 +29,8 @@
   if (SbStringCompareAll(name, kCobaltExtensionFontName) == 0) {
     return starboard::stub::GetFontApi();
   }
+  if (SbStringCompareAll(name, kCobaltExtensionJavaScriptCacheName) == 0) {
+    return starboard::stub::GetJavaScriptCacheApi();
+  }
   return NULL;
 }
diff --git a/src/third_party/mozjs-45/js/src/gc/StoreBuffer.h b/src/third_party/mozjs-45/js/src/gc/StoreBuffer.h
index c343d6b..8ff3a80 100644
--- a/src/third_party/mozjs-45/js/src/gc/StoreBuffer.h
+++ b/src/third_party/mozjs-45/js/src/gc/StoreBuffer.h
@@ -296,7 +296,7 @@
 
         explicit operator bool() const { return objectAndKind_ != 0; }
 
-        typedef struct {
+        typedef struct Hasher {
             typedef SlotsEdge Lookup;
             static HashNumber hash(const Lookup& l) { return l.objectAndKind_ ^ l.start_ ^ l.count_; }
             static bool match(const SlotsEdge& k, const Lookup& l) { return k == l; }
diff --git a/src/tools/gyp/pylib/gyp/win_tool.py b/src/tools/gyp/pylib/gyp/win_tool.py
index 6b950b7..c79ffd7 100755
--- a/src/tools/gyp/pylib/gyp/win_tool.py
+++ b/src/tools/gyp/pylib/gyp/win_tool.py
@@ -69,6 +69,14 @@
       shutil.copytree(source, dest, ignore=shutil.ignore_patterns(r'.git'))
     else:
       shutil.copy2(source, dest)
+      # Hack to make target look 2 seconds newer, to prevent ninja erroneously re-trigger
+      # copy rules on nanosecond differences. Risk of getting stale content files due to this
+      # is almost non-existent
+      OFFSET_SECONDS = 2
+      stat = os.stat(source)
+      mtime = stat.st_mtime + OFFSET_SECONDS
+      atime = stat.st_atime + OFFSET_SECONDS
+      os.utime(dest, (atime, mtime))
 
   if platform.system() == 'Windows':
     def ExecLinkWrapper(self, arch, *args):