Import Cobalt 23.lts.1.308479
diff --git a/cobalt/content/licenses/platform/android/licenses_cobalt.txt b/cobalt/content/licenses/platform/android/licenses_cobalt.txt
index 022c76b..c92392b 100644
--- a/cobalt/content/licenses/platform/android/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/android/licenses_cobalt.txt
@@ -2467,6 +2467,36 @@
 
 
 
+  zlib
+
+  /* zlib.h -- interface of the 'zlib' general purpose compression library
+    version 1.2.4, March 14th, 2010
+
+    Copyright (C) 1995-2010 Jean-loup Gailly and Mark Adler
+
+    This software is provided 'as-is', without any express or implied
+    warranty.  In no event will the authors be held liable for any damages
+    arising from the use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+    1. The origin of this software must not be misrepresented; you must not
+       claim that you wrote the original software. If you use this software
+       in a product, an acknowledgment in the product documentation would be
+       appreciated but is not required.
+    2. Altered source versions must be plainly marked as such, and must not be
+       misrepresented as being the original software.
+    3. This notice may not be removed or altered from any source distribution.
+
+    Jean-loup Gailly
+    Mark Adler
+
+  */
+
+
+
   woff2
 
   Copyright (c) 2013-2017 by the WOFF2 Authors.
diff --git a/cobalt/content/licenses/platform/default/licenses_cobalt.txt b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
index 7ab54cf..7dddfd2 100644
--- a/cobalt/content/licenses/platform/default/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
@@ -2755,6 +2755,36 @@
 
 
 
+  zlib
+
+  /* zlib.h -- interface of the 'zlib' general purpose compression library
+    version 1.2.4, March 14th, 2010
+
+    Copyright (C) 1995-2010 Jean-loup Gailly and Mark Adler
+
+    This software is provided 'as-is', without any express or implied
+    warranty.  In no event will the authors be held liable for any damages
+    arising from the use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+    1. The origin of this software must not be misrepresented; you must not
+       claim that you wrote the original software. If you use this software
+       in a product, an acknowledgment in the product documentation would be
+       appreciated but is not required.
+    2. Altered source versions must be plainly marked as such, and must not be
+       misrepresented as being the original software.
+    3. This notice may not be removed or altered from any source distribution.
+
+    Jean-loup Gailly
+    Mark Adler
+
+  */
+
+
+
   woff2
 
   Copyright (c) 2013-2017 by the WOFF2 Authors.
diff --git a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
index a85d482..2f63709 100644
--- a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
@@ -2647,6 +2647,36 @@
 
 
 
+  zlib
+
+  /* zlib.h -- interface of the 'zlib' general purpose compression library
+    version 1.2.4, March 14th, 2010
+
+    Copyright (C) 1995-2010 Jean-loup Gailly and Mark Adler
+
+    This software is provided 'as-is', without any express or implied
+    warranty.  In no event will the authors be held liable for any damages
+    arising from the use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+    1. The origin of this software must not be misrepresented; you must not
+       claim that you wrote the original software. If you use this software
+       in a product, an acknowledgment in the product documentation would be
+       appreciated but is not required.
+    2. Altered source versions must be plainly marked as such, and must not be
+       misrepresented as being the original software.
+    3. This notice may not be removed or altered from any source distribution.
+
+    Jean-loup Gailly
+    Mark Adler
+
+  */
+
+
+
   woff2
 
   Copyright (c) 2013-2017 by the WOFF2 Authors.
diff --git a/cobalt/extension/demuxer.h b/cobalt/extension/demuxer.h
new file mode 100644
index 0000000..59a3055
--- /dev/null
+++ b/cobalt/extension/demuxer.h
@@ -0,0 +1,408 @@
+// 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.
+//
+// Contains extension code allowing partners to provide their own demuxer.
+// CobaltExtensionDemuxerApi is the main API.
+
+#ifndef COBALT_EXTENSION_DEMUXER_H_
+#define COBALT_EXTENSION_DEMUXER_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "starboard/time.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define kCobaltExtensionDemuxerApi "dev.cobalt.extension.Demuxer"
+
+// This must stay in sync with ::media::PipelineStatus. Missing values are
+// either irrelevant to the demuxer or are deprecated values of PipelineStatus.
+typedef enum CobaltExtensionDemuxerStatus {
+  kCobaltExtensionDemuxerOk = 0,
+  kCobaltExtensionDemuxerErrorNetwork = 2,
+  kCobaltExtensionDemuxerErrorAbort = 5,
+  kCobaltExtensionDemuxerErrorInitializationFailed = 6,
+  kCobaltExtensionDemuxerErrorRead = 9,
+  kCobaltExtensionDemuxerErrorInvalidState = 11,
+  kCobaltExtensionDemuxerErrorCouldNotOpen = 12,
+  kCobaltExtensionDemuxerErrorCouldNotParse = 13,
+  kCobaltExtensionDemuxerErrorNoSupportedStreams = 14
+} CobaltExtensionDemuxerStatus;
+
+// Type of side data associated with a buffer.
+typedef enum CobaltExtensionDemuxerSideDataType {
+  kCobaltExtensionDemuxerUnknownSideDataType = 0,
+  kCobaltExtensionDemuxerMatroskaBlockAdditional = 1,
+} CobaltExtensionDemuxerSideDataType;
+
+// This must stay in sync with ::media::AudioCodec.
+typedef enum CobaltExtensionDemuxerAudioCodec {
+  kCobaltExtensionDemuxerCodecUnknownAudio = 0,
+  kCobaltExtensionDemuxerCodecAAC = 1,
+  kCobaltExtensionDemuxerCodecMP3 = 2,
+  kCobaltExtensionDemuxerCodecPCM = 3,
+  kCobaltExtensionDemuxerCodecVorbis = 4,
+  kCobaltExtensionDemuxerCodecFLAC = 5,
+  kCobaltExtensionDemuxerCodecAMR_NB = 6,
+  kCobaltExtensionDemuxerCodecAMR_WB = 7,
+  kCobaltExtensionDemuxerCodecPCM_MULAW = 8,
+  kCobaltExtensionDemuxerCodecGSM_MS = 9,
+  kCobaltExtensionDemuxerCodecPCM_S16BE = 10,
+  kCobaltExtensionDemuxerCodecPCM_S24BE = 11,
+  kCobaltExtensionDemuxerCodecOpus = 12,
+  kCobaltExtensionDemuxerCodecEAC3 = 13,
+  kCobaltExtensionDemuxerCodecPCM_ALAW = 14,
+  kCobaltExtensionDemuxerCodecALAC = 15,
+  kCobaltExtensionDemuxerCodecAC3 = 16
+} CobaltExtensionDemuxerAudioCodec;
+
+// This must stay in sync with ::media::VideoCodec.
+typedef enum CobaltExtensionDemuxerVideoCodec {
+  kCobaltExtensionDemuxerCodecUnknownVideo = 0,
+  kCobaltExtensionDemuxerCodecH264,
+  kCobaltExtensionDemuxerCodecVC1,
+  kCobaltExtensionDemuxerCodecMPEG2,
+  kCobaltExtensionDemuxerCodecMPEG4,
+  kCobaltExtensionDemuxerCodecTheora,
+  kCobaltExtensionDemuxerCodecVP8,
+  kCobaltExtensionDemuxerCodecVP9,
+  kCobaltExtensionDemuxerCodecHEVC,
+  kCobaltExtensionDemuxerCodecDolbyVision,
+  kCobaltExtensionDemuxerCodecAV1,
+} CobaltExtensionDemuxerVideoCodec;
+
+// This must stay in sync with ::media::SampleFormat.
+typedef enum CobaltExtensionDemuxerSampleFormat {
+  kCobaltExtensionDemuxerSampleFormatUnknown = 0,
+  kCobaltExtensionDemuxerSampleFormatU8,   // Unsigned 8-bit w/ bias of 128.
+  kCobaltExtensionDemuxerSampleFormatS16,  // Signed 16-bit.
+  kCobaltExtensionDemuxerSampleFormatS32,  // Signed 32-bit.
+  kCobaltExtensionDemuxerSampleFormatF32,  // Float 32-bit.
+  kCobaltExtensionDemuxerSampleFormatPlanarS16,  // Signed 16-bit planar.
+  kCobaltExtensionDemuxerSampleFormatPlanarF32,  // Float 32-bit planar.
+  kCobaltExtensionDemuxerSampleFormatPlanarS32,  // Signed 32-bit planar.
+  kCobaltExtensionDemuxerSampleFormatS24,        // Signed 24-bit.
+} CobaltExtensionDemuxerSampleFormat;
+
+// This must stay in sync with ::media::ChannelLayout.
+typedef enum CobaltExtensionDemuxerChannelLayout {
+  kCobaltExtensionDemuxerChannelLayoutNone = 0,
+  kCobaltExtensionDemuxerChannelLayoutUnsupported = 1,
+  kCobaltExtensionDemuxerChannelLayoutMono = 2,
+  kCobaltExtensionDemuxerChannelLayoutStereo = 3,
+  kCobaltExtensionDemuxerChannelLayout2_1 = 4,
+  kCobaltExtensionDemuxerChannelLayoutSurround = 5,
+  kCobaltExtensionDemuxerChannelLayout4_0 = 6,
+  kCobaltExtensionDemuxerChannelLayout2_2 = 7,
+  kCobaltExtensionDemuxerChannelLayoutQuad = 8,
+  kCobaltExtensionDemuxerChannelLayout5_0 = 9,
+  kCobaltExtensionDemuxerChannelLayout5_1 = 10,
+  kCobaltExtensionDemuxerChannelLayout5_0Back = 11,
+  kCobaltExtensionDemuxerChannelLayout5_1Back = 12,
+  kCobaltExtensionDemuxerChannelLayout7_0 = 13,
+  kCobaltExtensionDemuxerChannelLayout7_1 = 14,
+  kCobaltExtensionDemuxerChannelLayout7_1Wide = 15,
+  kCobaltExtensionDemuxerChannelLayoutStereoDownmix = 16,
+  kCobaltExtensionDemuxerChannelLayout2point1 = 17,
+  kCobaltExtensionDemuxerChannelLayout3_1 = 18,
+  kCobaltExtensionDemuxerChannelLayout4_1 = 19,
+  kCobaltExtensionDemuxerChannelLayout6_0 = 20,
+  kCobaltExtensionDemuxerChannelLayout6_0Front = 21,
+  kCobaltExtensionDemuxerChannelLayoutHexagonal = 22,
+  kCobaltExtensionDemuxerChannelLayout6_1 = 23,
+  kCobaltExtensionDemuxerChannelLayout6_1Back = 24,
+  kCobaltExtensionDemuxerChannelLayout6_1Front = 25,
+  kCobaltExtensionDemuxerChannelLayout7_0Front = 26,
+  kCobaltExtensionDemuxerChannelLayout7_1WideBack = 27,
+  kCobaltExtensionDemuxerChannelLayoutOctagonal = 28,
+  kCobaltExtensionDemuxerChannelLayoutDiscrete = 29,
+  kCobaltExtensionDemuxerChannelLayoutStereoAndKeyboardMic = 30,
+  kCobaltExtensionDemuxerChannelLayout4_1QuadSide = 31,
+  kCobaltExtensionDemuxerChannelLayoutBitstream = 32
+} CobaltExtensionDemuxerChannelLayout;
+
+// This must stay in sync with ::media::VideoCodecProfile.
+typedef enum CobaltExtensionDemuxerVideoCodecProfile {
+  kCobaltExtensionDemuxerVideoCodecProfileUnknown = -1,
+  kCobaltExtensionDemuxerH264ProfileMin = 0,
+  kCobaltExtensionDemuxerH264ProfileBaseline =
+      kCobaltExtensionDemuxerH264ProfileMin,
+  kCobaltExtensionDemuxerH264ProfileMain = 1,
+  kCobaltExtensionDemuxerH264ProfileExtended = 2,
+  kCobaltExtensionDemuxerH264ProfileHigh = 3,
+  kCobaltExtensionDemuxerH264ProfileHigh10Profile = 4,
+  kCobaltExtensionDemuxerH264ProfileHigh422Profile = 5,
+  kCobaltExtensionDemuxerH264ProfileHigh444PredictiveProfile = 6,
+  kCobaltExtensionDemuxerH264ProfileScalableBaseline = 7,
+  kCobaltExtensionDemuxerH264ProfileScalableHigh = 8,
+  kCobaltExtensionDemuxerH264ProfileStereoHigh = 9,
+  kCobaltExtensionDemuxerH264ProfileMultiviewHigh = 10,
+  kCobaltExtensionDemuxerH264ProfileMax =
+      kCobaltExtensionDemuxerH264ProfileMultiviewHigh,
+  kCobaltExtensionDemuxerVp8ProfileMin = 11,
+  kCobaltExtensionDemuxerVp8ProfileAny = kCobaltExtensionDemuxerVp8ProfileMin,
+  kCobaltExtensionDemuxerVp8ProfileMax = kCobaltExtensionDemuxerVp8ProfileAny,
+  kCobaltExtensionDemuxerVp9ProfileMin = 12,
+  kCobaltExtensionDemuxerVp9ProfileProfile0 =
+      kCobaltExtensionDemuxerVp9ProfileMin,
+  kCobaltExtensionDemuxerVp9ProfileProfile1 = 13,
+  kCobaltExtensionDemuxerVp9ProfileProfile2 = 14,
+  kCobaltExtensionDemuxerVp9ProfileProfile3 = 15,
+  kCobaltExtensionDemuxerVp9ProfileMax =
+      kCobaltExtensionDemuxerVp9ProfileProfile3,
+  kCobaltExtensionDemuxerHevcProfileMin = 16,
+  kCobaltExtensionDemuxerHevcProfileMain =
+      kCobaltExtensionDemuxerHevcProfileMin,
+  kCobaltExtensionDemuxerHevcProfileMain10 = 17,
+  kCobaltExtensionDemuxerHevcProfileMainStillPicture = 18,
+  kCobaltExtensionDemuxerHevcProfileMax =
+      kCobaltExtensionDemuxerHevcProfileMainStillPicture,
+  kCobaltExtensionDemuxerDolbyVisionProfile0 = 19,
+  kCobaltExtensionDemuxerDolbyVisionProfile4 = 20,
+  kCobaltExtensionDemuxerDolbyVisionProfile5 = 21,
+  kCobaltExtensionDemuxerDolbyVisionProfile7 = 22,
+  kCobaltExtensionDemuxerTheoraProfileMin = 23,
+  kCobaltExtensionDemuxerTheoraProfileAny =
+      kCobaltExtensionDemuxerTheoraProfileMin,
+  kCobaltExtensionDemuxerTheoraProfileMax =
+      kCobaltExtensionDemuxerTheoraProfileAny,
+  kCobaltExtensionDemuxerAv1ProfileMin = 24,
+  kCobaltExtensionDemuxerAv1ProfileProfileMain =
+      kCobaltExtensionDemuxerAv1ProfileMin,
+  kCobaltExtensionDemuxerAv1ProfileProfileHigh = 25,
+  kCobaltExtensionDemuxerAv1ProfileProfilePro = 26,
+  kCobaltExtensionDemuxerAv1ProfileMax =
+      kCobaltExtensionDemuxerAv1ProfileProfilePro,
+  kCobaltExtensionDemuxerDolbyVisionProfile8 = 27,
+  kCobaltExtensionDemuxerDolbyVisionProfile9 = 28,
+} CobaltExtensionDemuxerVideoCodecProfile;
+
+// This must be kept in sync with gfx::ColorSpace::RangeID.
+typedef enum CobaltExtensionDemuxerColorSpaceRangeId {
+  kCobaltExtensionDemuxerColorSpaceRangeIdInvalid = 0,
+  kCobaltExtensionDemuxerColorSpaceRangeIdLimited = 1,
+  kCobaltExtensionDemuxerColorSpaceRangeIdFull = 2,
+  kCobaltExtensionDemuxerColorSpaceRangeIdDerived = 3
+} CobaltExtensionDemuxerColorSpaceRangeId;
+
+// This must be kept in sync with media::VideoDecoderConfig::AlphaMode.
+typedef enum CobaltExtensionDemuxerAlphaMode {
+  kCobaltExtensionDemuxerHasAlpha,
+  kCobaltExtensionDemuxerIsOpaque
+} CobaltExtensionDemuxerAlphaMode;
+
+// This must be kept in sync with ::media::DemuxerStream::Type.
+typedef enum CobaltExtensionDemuxerStreamType {
+  kCobaltExtensionDemuxerStreamTypeUnknown,
+  kCobaltExtensionDemuxerStreamTypeAudio,
+  kCobaltExtensionDemuxerStreamTypeVideo,
+  kCobaltExtensionDemuxerStreamTypeText
+} CobaltExtensionDemuxerStreamType;
+
+// This must be kept in sync with media::EncryptionScheme.
+typedef enum CobaltExtensionDemuxerEncryptionScheme {
+  kCobaltExtensionDemuxerEncryptionSchemeUnencrypted,
+  kCobaltExtensionDemuxerEncryptionSchemeCenc,
+  kCobaltExtensionDemuxerEncryptionSchemeCbcs,
+} CobaltExtensionDemuxerEncryptionScheme;
+
+typedef struct CobaltExtensionDemuxerAudioDecoderConfig {
+  CobaltExtensionDemuxerAudioCodec codec;
+  CobaltExtensionDemuxerSampleFormat sample_format;
+  CobaltExtensionDemuxerChannelLayout channel_layout;
+  CobaltExtensionDemuxerEncryptionScheme encryption_scheme;
+  int samples_per_second;
+
+  uint8_t* extra_data;  // Not owned by this struct.
+  int64_t extra_data_size;
+} CobaltExtensionDemuxerAudioDecoderConfig;
+
+typedef struct CobaltExtensionDemuxerVideoDecoderConfig {
+  CobaltExtensionDemuxerVideoCodec codec;
+  CobaltExtensionDemuxerVideoCodecProfile profile;
+
+  // These fields represent the color space.
+  int color_space_primaries;
+  int color_space_transfer;
+  int color_space_matrix;
+  CobaltExtensionDemuxerColorSpaceRangeId color_space_range_id;
+
+  CobaltExtensionDemuxerAlphaMode alpha_mode;
+
+  // These fields represent the coded size.
+  int coded_width;
+  int coded_height;
+
+  // These fields represent the visible rectangle.
+  int visible_rect_x;
+  int visible_rect_y;
+  int visible_rect_width;
+  int visible_rect_height;
+
+  // These fields represent the natural size.
+  int natural_width;
+  int natural_height;
+
+  CobaltExtensionDemuxerEncryptionScheme encryption_scheme;
+
+  uint8_t* extra_data;  // Not owned by this struct.
+  int64_t extra_data_size;
+} CobaltExtensionDemuxerVideoDecoderConfig;
+
+typedef struct CobaltExtensionDemuxerSideData {
+  uint8_t* data;  // Not owned by this struct.
+  // Number of bytes in |data|.
+  int64_t data_size;
+  // Specifies the format of |data|.
+  CobaltExtensionDemuxerSideDataType type;
+} CobaltExtensionDemuxerSideData;
+
+typedef struct CobaltExtensionDemuxerBuffer {
+  // The media data for this buffer. Ownership is not transferred via this
+  // struct.
+  uint8_t* data;
+  // Number of bytes in |data|.
+  int64_t data_size;
+  // An array of side data elements containing any side data for this buffer.
+  // Ownership is not transferred via this struct.
+  CobaltExtensionDemuxerSideData* side_data;
+  // Number of elements in |side_data|.
+  int64_t side_data_elements;
+  // Playback time in microseconds.
+  SbTime pts;
+  // Duration of this buffer in microseconds.
+  SbTime duration;
+  // True if this buffer contains a keyframe.
+  bool is_keyframe;
+  // Signifies the end of the stream. If this is true, the other fields will be
+  // ignored.
+  bool end_of_stream;
+} CobaltExtensionDemuxerBuffer;
+
+// Note: |buffer| is the input to this function, not the output. Cobalt
+// implements this function to read media data provided by the implementer of
+// CobaltExtensionDemuxer.
+typedef void (*CobaltExtensionDemuxerReadCB)(
+    CobaltExtensionDemuxerBuffer* buffer, void* user_data);
+
+// A fully synchronous demuxer API. Threading concerns are handled by the code
+// that uses this API.
+// When calling the defined functions, the |user_data| argument must be the
+// void* user_data field stored in this struct.
+typedef struct CobaltExtensionDemuxer {
+  // Initialize must only be called once for a demuxer; subsequent calls can
+  // fail.
+  CobaltExtensionDemuxerStatus (*Initialize)(void* user_data);
+
+  CobaltExtensionDemuxerStatus (*Seek)(SbTime seek_time, void* user_data);
+
+  // Returns the starting time for the media file; it is always positive.
+  SbTime (*GetStartTime)(void* user_data);
+
+  // Returns the time -- in microseconds since Windows epoch -- represented by
+  // presentation timestamp 0. If the timestamps are not associated with a time,
+  // returns 0.
+  SbTime (*GetTimelineOffset)(void* user_data);
+
+  // Calls |read_cb| with a buffer of type |type| and the user data provided by
+  // |read_cb_user_data|. |read_cb| is a synchronous function, so the data
+  // passed to it can safely be freed after |read_cb| returns. |read_cb| must be
+  // called exactly once, and it must be called before Read returns.
+  //
+  // An error can be handled in one of two ways:
+  // 1. Pass a null buffer to read_cb. This will cause the pipeline to handle
+  //    the situation as an error. Alternatively,
+  // 2. Pass an "end of stream" buffer to read_cb. This will cause the relevant
+  //    stream to end normally.
+  void (*Read)(CobaltExtensionDemuxerStreamType type,
+               CobaltExtensionDemuxerReadCB read_cb, void* read_cb_user_data,
+               void* user_data);
+
+  // Returns true and populates |audio_config| if an audio stream is present;
+  // returns false otherwise. |config| must not be null.
+  bool (*GetAudioConfig)(CobaltExtensionDemuxerAudioDecoderConfig* config,
+                         void* user_data);
+
+  // Returns true and populates |video_config| if a video stream is present;
+  // returns false otherwise. |config| must not be null.
+  bool (*GetVideoConfig)(CobaltExtensionDemuxerVideoDecoderConfig* config,
+                         void* user_data);
+
+  // Returns the duration, in microseconds.
+  SbTime (*GetDuration)(void* user_data);
+
+  // Will be passed to all functions.
+  void* user_data;
+} CobaltExtensionDemuxer;
+
+typedef struct CobaltExtensionDemuxerDataSource {
+  // Reads up to |bytes_requested|, writing the data into |data| and returning
+  // the number of bytes read. |data| must be able to store at least
+  // |bytes_requested| bytes. Calling BlockingRead advances the read position.
+  int (*BlockingRead)(uint8_t* data, int bytes_requested, void* user_data);
+
+  // Seeks to |position| (specified in bytes) in the data source.
+  void (*SeekTo)(int position, void* user_data);
+
+  // Returns the offset into the data source, in bytes.
+  int64_t (*GetPosition)(void* user_data);
+
+  // Returns the size of the data source, in bytes.
+  int64_t (*GetSize)(void* user_data);
+
+  // Whether this represents a streaming data source.
+  bool is_streaming;
+
+  // Will be passed to all functions.
+  void* user_data;
+} CobaltExtensionDemuxerDataSource;
+
+typedef struct CobaltExtensionDemuxerApi {
+  // Name should be the string |kCobaltExtensionDemuxerApi|.
+  // 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.
+
+  // Creates a demuxer for the content provided by |data_source|. Ownership of
+  // |data_source| is not transferred to this function.
+  //
+  // Ownership of the returned demuxer is transferred to the caller, but it must
+  // be deleted via DestroyDemuxer (below). The caller must not manually delete
+  // the demuxer.
+  CobaltExtensionDemuxer* (*CreateDemuxer)(
+      CobaltExtensionDemuxerDataSource* data_source,
+      CobaltExtensionDemuxerAudioCodec* supported_audio_codecs,
+      int64_t supported_audio_codecs_size,
+      CobaltExtensionDemuxerVideoCodec* supported_video_codecs,
+      int64_t supported_video_codecs_size);
+
+  // Destroys |demuxer|. After calling this, |demuxer| must not be dereferenced
+  // or deleted by the caller.
+  void (*DestroyDemuxer)(CobaltExtensionDemuxer* demuxer);
+} CobaltExtensionDemuxerApi;
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif
+
+
+#endif  // COBALT_EXTENSION_DEMUXER_H_
diff --git a/cobalt/media/BUILD.gn b/cobalt/media/BUILD.gn
index 50a4629..e7e4b0c 100644
--- a/cobalt/media/BUILD.gn
+++ b/cobalt/media/BUILD.gn
@@ -64,6 +64,8 @@
     "progressive/avc_parser.h",
     "progressive/data_source_reader.cc",
     "progressive/data_source_reader.h",
+    "progressive/demuxer_extension_wrapper.cc",
+    "progressive/demuxer_extension_wrapper.h",
     "progressive/mp4_map.cc",
     "progressive/mp4_map.h",
     "progressive/mp4_parser.cc",
@@ -106,6 +108,7 @@
   testonly = true
 
   sources = [
+    "progressive/demuxer_extension_wrapper_test.cc",
     "progressive/mock_data_source_reader.h",
     "progressive/mp4_map_unittest.cc",
     "progressive/rbsp_stream_unittest.cc",
@@ -120,5 +123,6 @@
     "//cobalt/test:run_all_unittests",
     "//testing/gmock",
     "//testing/gtest",
+    "//third_party/chromium/media:media",
   ]
 }
diff --git a/cobalt/media/player/web_media_player_impl.cc b/cobalt/media/player/web_media_player_impl.cc
index 19aec68..197a9a1 100644
--- a/cobalt/media/player/web_media_player_impl.cc
+++ b/cobalt/media/player/web_media_player_impl.cc
@@ -4,6 +4,7 @@
 #include "cobalt/media/player/web_media_player_impl.h"
 
 #include <cmath>
+#include <cstring>
 #include <limits>
 #include <memory>
 #include <string>
@@ -21,7 +22,10 @@
 #include "cobalt/base/instance_counter.h"
 #include "cobalt/media/base/drm_system.h"
 #include "cobalt/media/player/web_media_player_proxy.h"
+#include "cobalt/media/progressive/data_source_reader.h"
+#include "cobalt/media/progressive/demuxer_extension_wrapper.h"
 #include "cobalt/media/progressive/progressive_demuxer.h"
+#include "starboard/system.h"
 #include "starboard/types.h"
 #include "third_party/chromium/media/base/bind_to_current_loop.h"
 #include "third_party/chromium/media/base/limits.h"
@@ -275,8 +279,19 @@
 
   is_local_source_ = !url.SchemeIs("http") && !url.SchemeIs("https");
 
-  progressive_demuxer_.reset(new ProgressiveDemuxer(
-      pipeline_thread_.task_runner(), proxy_->data_source(), media_log_));
+  // Attempt to use the demuxer provided via Cobalt Extension, if available.
+  progressive_demuxer_ = DemuxerExtensionWrapper::Create(
+      proxy_->data_source(), pipeline_thread_.task_runner());
+
+  if (progressive_demuxer_) {
+    LOG(INFO) << "Using DemuxerExtensionWrapper.";
+  } else {
+    // Either the demuxer Cobalt extension was not provided, or it failed to
+    // create a demuxer; fall back to the ProgressiveDemuxer.
+    LOG(INFO) << "Using ProgressiveDemuxer.";
+    progressive_demuxer_.reset(new ProgressiveDemuxer(
+        pipeline_thread_.task_runner(), proxy_->data_source(), media_log_));
+  }
 
   state_.is_progressive = true;
   StartPipeline(progressive_demuxer_.get());
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper.cc b/cobalt/media/progressive/demuxer_extension_wrapper.cc
new file mode 100644
index 0000000..f4c8139
--- /dev/null
+++ b/cobalt/media/progressive/demuxer_extension_wrapper.cc
@@ -0,0 +1,1121 @@
+// 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 "cobalt/media/progressive/demuxer_extension_wrapper.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/task/post_task.h"
+#include "base/task_runner_util.h"
+#include "cobalt/extension/demuxer.h"
+#include "starboard/system.h"
+#include "third_party/chromium/media/base/audio_codecs.h"
+#include "third_party/chromium/media/base/bind_to_current_loop.h"
+#include "third_party/chromium/media/base/encryption_scheme.h"
+#include "third_party/chromium/media/base/sample_format.h"
+#include "third_party/chromium/media/base/starboard_utils.h"
+#include "third_party/chromium/media/base/video_types.h"
+#include "third_party/chromium/media/cobalt/ui/gfx/color_space.h"
+#include "third_party/chromium/media/cobalt/ui/gfx/geometry/rect.h"
+#include "third_party/chromium/media/cobalt/ui/gfx/geometry/size.h"
+#include "third_party/chromium/media/filters/h264_to_annex_b_bitstream_converter.h"
+#include "third_party/chromium/media/formats/mp4/box_definitions.h"
+
+namespace cobalt {
+namespace media {
+
+using ::media::AudioCodec;
+using ::media::AudioDecoderConfig;
+using ::media::ChannelLayout;
+using ::media::DecoderBuffer;
+using ::media::DemuxerHost;
+using ::media::DemuxerStream;
+using ::media::EncryptionScheme;
+using ::media::H264ToAnnexBBitstreamConverter;
+using ::media::MediaTrack;
+using ::media::PipelineStatus;
+using ::media::PipelineStatusCallback;
+using ::media::PipelineStatusCB;
+using ::media::Ranges;
+using ::media::SampleFormat;
+using ::media::VideoCodec;
+using ::media::VideoCodecProfile;
+using ::media::VideoColorSpace;
+using ::media::VideoDecoderConfig;
+using ::media::VideoPixelFormat;
+using ::media::VideoTransformation;
+using ::media::mp4::AVCDecoderConfigurationRecord;
+
+// Used to convert a lambda to a pure C function.
+// |user_data| is a callback of type T, which takes a U*.
+template <typename T, typename U>
+static void CallCB(U* u, void* user_data) {
+  (*static_cast<T*>(user_data))(u);
+}
+
+// Converts AVCC h.264 frames to Annex B. This is necessary because the decoder
+// expects packets in Annex B format.
+class DemuxerExtensionWrapper::H264AnnexBConverter {
+ public:
+  // Creates an H264AnnexBConverter from the MP4 file's header data.
+  static std::unique_ptr<H264AnnexBConverter> Create(const uint8_t* extra_data,
+                                                     size_t extra_data_size) {
+    if (!extra_data || extra_data_size == 0) {
+      LOG(ERROR) << "Invalid inputs to H264AnnexBConverter::Create.";
+      return nullptr;
+    }
+    AVCDecoderConfigurationRecord config;
+    std::unique_ptr<H264ToAnnexBBitstreamConverter> converter(
+        new H264ToAnnexBBitstreamConverter);
+    if (!converter->ParseConfiguration(
+            extra_data, static_cast<int>(extra_data_size), &config)) {
+      LOG(ERROR) << "Could not parse AVCC config.";
+      return nullptr;
+    }
+    return std::unique_ptr<H264AnnexBConverter>(
+        new H264AnnexBConverter(std::move(config), std::move(converter)));
+  }
+
+  // Disallow copy and assign.
+  H264AnnexBConverter(const H264AnnexBConverter&) = delete;
+  H264AnnexBConverter& operator=(const H264AnnexBConverter&) = delete;
+
+  ~H264AnnexBConverter() = default;
+
+  // Attempts to convert the data in |data| from AVCC to AnnexB format,
+  // returning the data as a DecoderBuffer. Upon failure, the data will be
+  // returned unmodified in the DecoderBuffer.
+  scoped_refptr<DecoderBuffer> Convert(const uint8_t* data, size_t data_size) {
+    const auto* const config = config_.has_value() ? &*config_ : nullptr;
+
+    std::vector<uint8_t> rewritten(
+        converter_->CalculateNeededOutputBufferSize(data, data_size, config));
+
+    uint32_t rewritten_size = rewritten.size();
+    if (rewritten.empty() ||
+        !converter_->ConvertNalUnitStreamToByteStream(
+            data, data_size, config, rewritten.data(), &rewritten_size)) {
+      // TODO(b/231994311): Add the buffer's side_data here, for HDR10+ support.
+      return DecoderBuffer::CopyFrom(data, data_size);
+    } else {
+      // The data was successfully rewritten.
+
+      // The SPS and PPS NALUs -- generated from the config -- should only be
+      // sent with the first real NALU.
+      config_ = base::nullopt;
+
+      // TODO(b/231994311): Add the buffer's side_data here, for HDR10+ support.
+      return DecoderBuffer::CopyFrom(rewritten.data(), rewritten.size());
+    }
+  }
+
+ private:
+  explicit H264AnnexBConverter(
+      AVCDecoderConfigurationRecord config,
+      std::unique_ptr<H264ToAnnexBBitstreamConverter> converter)
+      : config_(std::move(config)), converter_(std::move(converter)) {}
+
+  // This config data is only sent with the first NALU (as SPS and PPS NALUs).
+  base::Optional<AVCDecoderConfigurationRecord> config_;
+  std::unique_ptr<H264ToAnnexBBitstreamConverter> converter_;
+};
+
+DemuxerExtensionStream::DemuxerExtensionStream(
+    CobaltExtensionDemuxer* demuxer,
+    scoped_refptr<base::SequencedTaskRunner> message_loop,
+    CobaltExtensionDemuxerVideoDecoderConfig config)
+    : demuxer_(demuxer), message_loop_(std::move(message_loop)) {
+  CHECK(demuxer_);
+  CHECK(message_loop_);
+  std::vector<uint8_t> extra_data;
+  if (config.extra_data_size > 0 && config.extra_data != nullptr) {
+    extra_data.assign(config.extra_data,
+                      config.extra_data + config.extra_data_size);
+  }
+
+  video_config_.emplace(
+      static_cast<VideoCodec>(config.codec),
+      static_cast<VideoCodecProfile>(config.profile),
+      static_cast<VideoDecoderConfig::AlphaMode>(config.alpha_mode),
+      VideoColorSpace(
+          config.color_space_primaries, config.color_space_transfer,
+          config.color_space_matrix,
+          static_cast<gfx::ColorSpace::RangeID>(config.color_space_range_id)),
+      VideoTransformation(), gfx::Size(config.coded_width, config.coded_height),
+      gfx::Rect(config.visible_rect_x, config.visible_rect_y,
+                config.visible_rect_width, config.visible_rect_height),
+      gfx::Size(config.natural_width, config.natural_height), extra_data,
+      static_cast<EncryptionScheme>(config.encryption_scheme));
+
+  LOG_IF(ERROR, !video_config_->IsValidConfig())
+      << "Video config is not valid!";
+}
+
+DemuxerExtensionStream::DemuxerExtensionStream(
+    CobaltExtensionDemuxer* demuxer,
+    scoped_refptr<base::SequencedTaskRunner> message_loop,
+    CobaltExtensionDemuxerAudioDecoderConfig config)
+    : demuxer_(demuxer), message_loop_(std::move(message_loop)) {
+  CHECK(demuxer_);
+  CHECK(message_loop_);
+  std::vector<uint8_t> extra_data;
+  if (config.extra_data_size > 0 && config.extra_data != nullptr) {
+    extra_data.assign(config.extra_data,
+                      config.extra_data + config.extra_data_size);
+  }
+
+  audio_config_.emplace(
+      static_cast<AudioCodec>(config.codec),
+      static_cast<SampleFormat>(config.sample_format),
+      static_cast<ChannelLayout>(config.channel_layout),
+      config.samples_per_second, extra_data,
+      static_cast<EncryptionScheme>(config.encryption_scheme));
+
+  LOG_IF(ERROR, !audio_config_->IsValidConfig())
+      << "Audio config is not valid!";
+}
+
+void DemuxerExtensionStream::Read(ReadCB read_cb) {
+  DCHECK(!read_cb.is_null());
+  base::AutoLock auto_lock(lock_);
+  if (stopped_) {
+    LOG(INFO) << "Already stopped.";
+    std::move(read_cb).Run(
+        DemuxerStream::kOk,
+        scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+    return;
+  }
+
+  // Buffers are only queued when there are no pending reads.
+  CHECK(buffer_queue_.empty() || read_queue_.empty());
+
+  if (buffer_queue_.empty()) {
+    read_queue_.push_back(std::move(read_cb));
+    return;
+  }
+
+  // We already have a buffer queued. Send the oldest buffer back.
+  scoped_refptr<DecoderBuffer> buffer = buffer_queue_.front();
+  if (!buffer->end_of_stream()) {
+    // Do not pop EOS buffers, so that subsequent read requests also get EOS.
+    total_buffer_size_ -= buffer->data_size();
+    buffer_queue_.pop_front();
+  }
+
+  std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+}
+
+AudioDecoderConfig DemuxerExtensionStream::audio_decoder_config() {
+  DCHECK(audio_config_.has_value());
+  return *audio_config_;
+}
+
+VideoDecoderConfig DemuxerExtensionStream::video_decoder_config() {
+  DCHECK(video_config_.has_value());
+  return *video_config_;
+}
+
+DemuxerStream::Type DemuxerExtensionStream::type() const {
+  const uint8_t is_audio = static_cast<int>(audio_config_.has_value());
+  const uint8_t is_video = static_cast<int>(video_config_.has_value());
+  DCHECK((is_audio ^ is_video) == 1);
+  return is_audio ? Type::AUDIO : Type::VIDEO;
+}
+
+Ranges<base::TimeDelta> DemuxerExtensionStream::GetBufferedRanges() {
+  return buffered_ranges_;
+}
+
+void DemuxerExtensionStream::EnqueueBuffer(
+    scoped_refptr<DecoderBuffer> buffer) {
+  base::AutoLock auto_lock(lock_);
+
+  if (stopped_) {
+    // It is possible due to pipelining -- both downstream and within the
+    // demuxer -- that several pipelined reads will be enqueuing packets on a
+    // stopped stream. These will be dropped.
+    LOG(WARNING) << "attempted to enqueue packet on stopped stream";
+    return;
+  }
+
+  if (buffer->end_of_stream()) {
+    LOG(INFO) << "Received EOS";
+  } else if (buffer->timestamp() != ::media::kNoTimestamp) {
+    if (last_buffer_timestamp_ != ::media::kNoTimestamp &&
+        last_buffer_timestamp_ < buffer->timestamp()) {
+      buffered_ranges_.Add(last_buffer_timestamp_, buffer->timestamp());
+    }
+    last_buffer_timestamp_ = buffer->timestamp();
+  } else {
+    LOG(WARNING) << "Bad timestamp info on enqueued buffer.";
+  }
+
+  if (read_queue_.empty()) {
+    buffer_queue_.push_back(buffer);
+    if (!buffer->end_of_stream()) {
+      total_buffer_size_ += buffer->data_size();
+    }
+    return;
+  }
+
+  // A pending read implies that the buffer queue was empty; otherwise it should
+  // never have been added to the read queue in the first place.
+  CHECK_EQ(buffer_queue_.size(), 0);
+  ReadCB read_cb(std::move(read_queue_.front()));
+  read_queue_.pop_front();
+  std::move(read_cb).Run(DemuxerStream::kOk, std::move(buffer));
+}
+
+void DemuxerExtensionStream::FlushBuffers() {
+  base::AutoLock auto_lock(lock_);
+  buffer_queue_.clear();
+  total_buffer_size_ = 0;
+  last_buffer_timestamp_ = ::media::kNoTimestamp;
+}
+
+void DemuxerExtensionStream::Stop() {
+  DCHECK(message_loop_->RunsTasksInCurrentSequence());
+
+  base::AutoLock auto_lock(lock_);
+  buffer_queue_.clear();
+  total_buffer_size_ = 0;
+  last_buffer_timestamp_ = ::media::kNoTimestamp;
+  // Fulfill any pending callbacks with EOS buffers set to end timestamp.
+  for (auto& read_cb : read_queue_) {
+    std::move(read_cb).Run(
+        DemuxerStream::kOk,
+        scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+  }
+  read_queue_.clear();
+  stopped_ = true;
+}
+
+base::TimeDelta DemuxerExtensionStream::GetLastBufferTimestamp() const {
+  base::AutoLock auto_lock(lock_);
+  return last_buffer_timestamp_;
+}
+
+size_t DemuxerExtensionStream::GetTotalBufferSize() const {
+  base::AutoLock auto_lock(lock_);
+  return total_buffer_size_;
+}
+
+PositionalDataSource::PositionalDataSource(
+    scoped_refptr<DataSourceReader> reader)
+    : reader_(std::move(reader)), position_(0) {
+  CHECK(reader_);
+}
+
+PositionalDataSource::~PositionalDataSource() = default;
+
+void PositionalDataSource::Stop() { reader_->Stop(); }
+
+int PositionalDataSource::BlockingRead(uint8_t* data, int bytes_requested) {
+  const int bytes_read =
+      reader_->BlockingRead(position_, bytes_requested, data);
+  if (bytes_read != DataSourceReader::kReadError) {
+    position_ += bytes_read;
+  }
+  return bytes_read;
+}
+
+void PositionalDataSource::SeekTo(int position) { position_ = position; }
+
+int64_t PositionalDataSource::GetPosition() const { return position_; }
+
+int64_t PositionalDataSource::GetSize() { return reader_->FileSize(); }
+
+// Functions for converting a PositionalDataSource to
+// CobaltExtensionDemuxerDataSource.
+static int CobaltExtensionDemuxerDataSource_BlockingReadRead(
+    uint8_t* data, int bytes_requested, void* user_data) {
+  return static_cast<PositionalDataSource*>(user_data)->BlockingRead(
+      data, bytes_requested);
+}
+
+static void CobaltExtensionDemuxerDataSource_SeekTo(int position,
+                                                    void* user_data) {
+  static_cast<PositionalDataSource*>(user_data)->SeekTo(position);
+}
+
+static int64_t CobaltExtensionDemuxerDataSource_GetPosition(void* user_data) {
+  return static_cast<PositionalDataSource*>(user_data)->GetPosition();
+}
+
+static int64_t CobaltExtensionDemuxerDataSource_GetSize(void* user_data) {
+  return static_cast<PositionalDataSource*>(user_data)->GetSize();
+}
+
+std::unique_ptr<DemuxerExtensionWrapper> DemuxerExtensionWrapper::Create(
+    DataSource* data_source,
+    scoped_refptr<base::SequencedTaskRunner> message_loop,
+    const CobaltExtensionDemuxerApi* demuxer_api) {
+  if (demuxer_api == nullptr) {
+    // Attempt to use the Cobalt extension.
+    demuxer_api = static_cast<const CobaltExtensionDemuxerApi*>(
+        SbSystemGetExtension(kCobaltExtensionDemuxerApi));
+    if (!demuxer_api ||
+        strcmp(demuxer_api->name, kCobaltExtensionDemuxerApi) != 0) {
+      return nullptr;
+    }
+  }
+
+  DCHECK(demuxer_api);
+  if (demuxer_api->version < 1) {
+    LOG(ERROR) << "Demuxer API version is too low: " << demuxer_api->version;
+    return nullptr;
+  }
+
+  if (!data_source || !message_loop) {
+    LOG(ERROR) << "data_source and message_loop cannot be null.";
+    return nullptr;
+  }
+
+  scoped_refptr<DataSourceReader> reader = new DataSourceReader;
+  reader->SetDataSource(data_source);
+
+  std::unique_ptr<PositionalDataSource> positional_data_source(
+      new PositionalDataSource(std::move(reader)));
+
+  std::unique_ptr<CobaltExtensionDemuxerDataSource> c_data_source(
+      new CobaltExtensionDemuxerDataSource{
+          /*BlockingRead=*/&CobaltExtensionDemuxerDataSource_BlockingReadRead,
+          /*SeekTo=*/&CobaltExtensionDemuxerDataSource_SeekTo,
+          /*GetPosition=*/&CobaltExtensionDemuxerDataSource_GetPosition,
+          /*GetSize=*/&CobaltExtensionDemuxerDataSource_GetSize,
+          /*is_streaming=*/false,
+          /*user_data=*/positional_data_source.get()});
+
+  // TODO(b/231632632): Populate these vectors.
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = demuxer_api->CreateDemuxer(
+      c_data_source.get(), supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  if (!demuxer) {
+    LOG(ERROR) << "Failed to create a CobaltExtensionDemuxer.";
+    return nullptr;
+  }
+
+  return std::unique_ptr<DemuxerExtensionWrapper>(new DemuxerExtensionWrapper(
+      demuxer_api, demuxer, std::move(positional_data_source),
+      std::move(c_data_source), std::move(message_loop)));
+}
+
+DemuxerExtensionWrapper::DemuxerExtensionWrapper(
+    const CobaltExtensionDemuxerApi* demuxer_api,
+    CobaltExtensionDemuxer* demuxer,
+    std::unique_ptr<PositionalDataSource> data_source,
+    std::unique_ptr<CobaltExtensionDemuxerDataSource> c_data_source,
+    scoped_refptr<base::SequencedTaskRunner> message_loop)
+    : demuxer_api_(demuxer_api),
+      impl_(demuxer),
+      data_source_(std::move(data_source)),
+      c_data_source_(std::move(c_data_source)),
+      blocking_thread_("DemuxerExtensionWrapperBlockingThread"),
+      message_loop_(std::move(message_loop)) {
+  CHECK(demuxer_api_);
+  CHECK(impl_);
+  CHECK(data_source_);
+  CHECK(c_data_source_);
+  CHECK(message_loop_);
+}
+
+DemuxerExtensionWrapper::~DemuxerExtensionWrapper() {
+  if (impl_) {
+    demuxer_api_->DestroyDemuxer(impl_);
+  }
+  // Explicitly stop |blocking_thread_| to ensure that it stops before the
+  // destruction of any other members.
+  blocking_thread_.Stop();
+}
+
+std::vector<DemuxerStream*> DemuxerExtensionWrapper::GetAllStreams() {
+  std::vector<DemuxerStream*> streams;
+  if (audio_stream_.has_value()) {
+    streams.push_back(&*audio_stream_);
+  }
+  if (video_stream_.has_value()) {
+    streams.push_back(&*video_stream_);
+  }
+  return streams;
+}
+
+std::string DemuxerExtensionWrapper::GetDisplayName() const {
+  return "DemuxerExtensionWrapper";
+}
+void DemuxerExtensionWrapper::Initialize(DemuxerHost* host,
+                                         PipelineStatusCallback status_cb) {
+  DCHECK(message_loop_->RunsTasksInCurrentSequence());
+  host_ = host;
+
+  // Start the blocking thread and have it download and parse the media config.
+  if (!blocking_thread_.Start()) {
+    LOG(ERROR) << "Unable to start blocking thread";
+    std::move(status_cb).Run(::media::DEMUXER_ERROR_COULD_NOT_PARSE);
+    return;
+  }
+
+  // |status_cb| cannot be called until this function returns, so we post a task
+  // here.
+  base::PostTaskAndReplyWithResult(
+      blocking_thread_.message_loop()->task_runner().get(), FROM_HERE,
+      base::BindOnce(impl_->Initialize, impl_->user_data),
+      base::BindOnce(&DemuxerExtensionWrapper::OnInitializeDone,
+                     base::Unretained(this), std::move(status_cb)));
+}
+
+void DemuxerExtensionWrapper::OnInitializeDone(
+    PipelineStatusCallback status_cb, CobaltExtensionDemuxerStatus status) {
+  if (status == kCobaltExtensionDemuxerOk) {
+    // Set up the stream(s) on this end.
+    CobaltExtensionDemuxerAudioDecoderConfig audio_config = {};
+    if (impl_->GetAudioConfig(&audio_config, impl_->user_data)) {
+      if (audio_config.encryption_scheme !=
+          kCobaltExtensionDemuxerEncryptionSchemeUnencrypted) {
+        // TODO(b/232957482): Determine whether we need to handle this case.
+        LOG(ERROR)
+            << "Encrypted audio is not supported for progressive playback.";
+        std::move(status_cb).Run(::media::DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
+        return;
+      }
+      audio_stream_.emplace(impl_, message_loop_, std::move(audio_config));
+    }
+    CobaltExtensionDemuxerVideoDecoderConfig video_config = {};
+    if (impl_->GetVideoConfig(&video_config, impl_->user_data)) {
+      if (video_config.encryption_scheme !=
+          kCobaltExtensionDemuxerEncryptionSchemeUnencrypted) {
+        // TODO(b/232957482): Determine whether we need to handle this case.
+        LOG(ERROR)
+            << "Encrypted video is not supported for progressive playback.";
+        std::move(status_cb).Run(::media::DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
+        return;
+      }
+      if (video_config.extra_data && video_config.extra_data_size > 0 &&
+          video_config.codec == kCobaltExtensionDemuxerCodecH264) {
+        // This is probably an AVCC stream. We'll need to convert each packet
+        // from AVCC to AnnexB, so we create the converter based on the "extra
+        // data". This extra data will be passed in the form of SPS and PPS NALU
+        // packets in the AnnexB stream.
+        h264_converter_ = H264AnnexBConverter::Create(
+            video_config.extra_data, video_config.extra_data_size);
+        video_config.extra_data = nullptr;
+        video_config.extra_data_size = 0;
+      }
+      video_stream_.emplace(impl_, message_loop_, std::move(video_config));
+    }
+
+    if (!audio_stream_.has_value() && !video_stream_.has_value()) {
+      // Even though initialization seems to have succeeded, something is wrong
+      // if there are no streams.
+      LOG(ERROR) << "No streams are present";
+      std::move(status_cb).Run(::media::DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
+      return;
+    }
+
+    host_->SetDuration(base::TimeDelta::FromMicroseconds(
+        impl_->GetDuration(impl_->user_data)));
+
+    // Begin downloading data.
+    Request(audio_stream_.has_value() ? DemuxerStream::AUDIO
+                                      : DemuxerStream::VIDEO);
+  } else {
+    LOG(ERROR) << "Initialization failed with status " << status;
+  }
+  std::move(status_cb).Run(static_cast<PipelineStatus>(status));
+}
+
+void DemuxerExtensionWrapper::AbortPendingReads() {}
+
+void DemuxerExtensionWrapper::StartWaitingForSeek(base::TimeDelta seek_time) {}
+
+void DemuxerExtensionWrapper::CancelPendingSeek(base::TimeDelta seek_time) {}
+
+void DemuxerExtensionWrapper::Seek(base::TimeDelta time,
+                                   PipelineStatusCallback status_cb) {
+  // It's safe to use base::Unretained here because blocking_thread_ will be
+  // stopped in this class's destructor.
+  blocking_thread_.message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&DemuxerExtensionWrapper::SeekTask, base::Unretained(this),
+                     time, BindToCurrentLoop(std::move(status_cb))));
+}
+
+// TODO(b/232984963): Determine whether it's OK to have reads and seeks on the
+// same thread.
+void DemuxerExtensionWrapper::SeekTask(base::TimeDelta time,
+                                       PipelineStatusCallback status_cb) {
+  CHECK(blocking_thread_.message_loop()
+            ->task_runner()
+            ->RunsTasksInCurrentSequence());
+
+  // clear any enqueued buffers on demuxer streams
+  if (video_stream_.has_value()) video_stream_->FlushBuffers();
+  if (audio_stream_.has_value()) audio_stream_->FlushBuffers();
+
+  const CobaltExtensionDemuxerStatus status =
+      impl_->Seek(time.InMicroseconds(), impl_->user_data);
+
+  if (status != kCobaltExtensionDemuxerOk) {
+    LOG(ERROR) << "Seek failed with status " << status;
+    std::move(status_cb).Run(::media::PIPELINE_ERROR_READ);
+    return;
+  }
+
+  // If all streams had finished downloading, we need to restart the request.
+  const bool issue_new_request =
+      (!video_stream_.has_value() || video_reached_eos_) &&
+      (!audio_stream_.has_value() || audio_reached_eos_);
+  audio_reached_eos_ = false;
+  video_reached_eos_ = false;
+  flushing_ = true;
+  std::move(status_cb).Run(::media::PIPELINE_OK);
+
+  if (issue_new_request) {
+    IssueNextRequest();
+  }
+}
+
+Ranges<base::TimeDelta> DemuxerExtensionWrapper::GetBufferedRanges() {
+  DCHECK(audio_stream_.has_value() || video_stream_.has_value());
+
+  if (!audio_stream_.has_value()) {
+    return video_stream_->GetBufferedRanges();
+  }
+  if (!video_stream_.has_value()) {
+    return audio_stream_->GetBufferedRanges();
+  }
+  return video_stream_->GetBufferedRanges().IntersectionWith(
+      audio_stream_->GetBufferedRanges());
+}
+
+void DemuxerExtensionWrapper::Stop() {
+  DCHECK(message_loop_->RunsTasksInCurrentSequence());
+  {
+    base::AutoLock lock(lock_for_stopped_);
+    stopped_ = true;
+  }
+  data_source_->Stop();
+}
+
+base::TimeDelta DemuxerExtensionWrapper::GetStartTime() const {
+  return base::TimeDelta::FromMicroseconds(
+      impl_->GetStartTime(impl_->user_data));
+}
+
+base::Time DemuxerExtensionWrapper::GetTimelineOffset() const {
+  const SbTime reported_time = impl_->GetTimelineOffset(impl_->user_data);
+  return reported_time == 0
+             ? base::Time()
+             : base::Time::FromDeltaSinceWindowsEpoch(
+                   base::TimeDelta::FromMicroseconds(reported_time));
+}
+
+int64_t DemuxerExtensionWrapper::GetMemoryUsage() const {
+  NOTREACHED();
+  return 0;
+}
+
+void DemuxerExtensionWrapper::OnEnabledAudioTracksChanged(
+    const std::vector<MediaTrack::Id>& track_ids, base::TimeDelta curr_time,
+    TrackChangeCB change_completed_cb) {
+  NOTREACHED();
+}
+
+void DemuxerExtensionWrapper::OnSelectedVideoTrackChanged(
+    const std::vector<MediaTrack::Id>& track_ids, base::TimeDelta curr_time,
+    TrackChangeCB change_completed_cb) {
+  NOTREACHED();
+}
+
+void DemuxerExtensionWrapper::Request(DemuxerStream::Type type) {
+  static const auto kRequestDelay = base::TimeDelta::FromMilliseconds(100);
+
+  if (type == DemuxerStream::AUDIO) {
+    DCHECK(audio_stream_.has_value());
+  } else {
+    DCHECK(video_stream_.has_value());
+  }
+
+  if (!blocking_thread_.task_runner()->BelongsToCurrentThread()) {
+    blocking_thread_.task_runner()->PostTask(
+        FROM_HERE, base::Bind(&DemuxerExtensionWrapper::Request,
+                              base::Unretained(this), type));
+    return;
+  }
+
+  if (HasStopped()) {
+    return;
+  }
+
+  const size_t total_buffer_size =
+      (audio_stream_.has_value() ? audio_stream_->GetTotalBufferSize() : 0) +
+      (video_stream_.has_value() ? video_stream_->GetTotalBufferSize() : 0);
+
+  int progressive_budget = 0;
+  if (video_stream_.has_value()) {
+    const VideoDecoderConfig video_config =
+        video_stream_->video_decoder_config();
+    // Only sdr video is supported in progressive mode.
+    // TODO(b/231994311): Figure out how to set this value properly.
+    constexpr int kBitDepth = 8;
+    progressive_budget = SbMediaGetProgressiveBufferBudget(
+        MediaVideoCodecToSbMediaVideoCodec(video_config.codec()),
+        video_config.visible_rect().size().width(),
+        video_config.visible_rect().size().height(), kBitDepth);
+  } else {
+    progressive_budget = SbMediaGetAudioBufferBudget();
+  }
+
+  if (total_buffer_size >= progressive_budget) {
+    // Retry after a delay.
+    blocking_thread_.message_loop()->task_runner()->PostDelayedTask(
+        FROM_HERE,
+        base::Bind(&DemuxerExtensionWrapper::Request, base::Unretained(this),
+                   type),
+        kRequestDelay);
+    return;
+  }
+
+  scoped_refptr<DecoderBuffer> decoder_buffer;
+  bool called_cb = false;
+  auto read_cb = [this, type, &decoder_buffer,
+                  &called_cb](CobaltExtensionDemuxerBuffer* buffer) {
+    called_cb = true;
+    if (!buffer) {
+      return;
+    }
+
+    if (buffer->end_of_stream) {
+      decoder_buffer = DecoderBuffer::CreateEOSBuffer();
+      return;
+    }
+
+    if (h264_converter_ && type == DemuxerExtensionStream::VIDEO) {
+      // This converts from AVCC to AnnexB format for h.264 video.
+      decoder_buffer =
+          h264_converter_->Convert(buffer->data, buffer->data_size);
+    } else {
+      // TODO(b/231994311): Add the buffer's side_data here, for HDR10+ support.
+      decoder_buffer = DecoderBuffer::CopyFrom(buffer->data, buffer->data_size);
+    }
+
+    decoder_buffer->set_timestamp(
+        base::TimeDelta::FromMicroseconds(buffer->pts));
+    decoder_buffer->set_duration(
+        base::TimeDelta::FromMicroseconds(buffer->duration));
+    decoder_buffer->set_is_key_frame(buffer->is_keyframe);
+  };
+  impl_->Read(static_cast<CobaltExtensionDemuxerStreamType>(type),
+              &CallCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
+              &read_cb, impl_->user_data);
+
+  if (!called_cb) {
+    LOG(ERROR)
+        << "Demuxer extension implementation did not call the read callback.";
+    host_->OnDemuxerError(::media::PIPELINE_ERROR_READ);
+    return;
+  }
+  if (!decoder_buffer) {
+    LOG(ERROR) << "Received a null buffer from the demuxer.";
+    host_->OnDemuxerError(::media::PIPELINE_ERROR_READ);
+    return;
+  }
+
+  auto& stream =
+      (type == DemuxerStream::AUDIO) ? *audio_stream_ : *video_stream_;
+  bool& eos_status =
+      (type == DemuxerStream::AUDIO) ? audio_reached_eos_ : video_reached_eos_;
+
+  eos_status = decoder_buffer->end_of_stream();
+  stream.EnqueueBuffer(std::move(decoder_buffer));
+  if (!eos_status) {
+    host_->OnBufferedTimeRangesChanged(GetBufferedRanges());
+  }
+
+  // If we reach this point, enqueueing the buffer was successful.
+  IssueNextRequest();
+  return;
+}
+
+void DemuxerExtensionWrapper::IssueNextRequest() {
+  {
+    base::AutoLock lock(lock_for_stopped_);
+    if (stopped_) {
+      LOG(INFO) << "Already stopped; request loop is stopping.";
+      return;
+    }
+  }
+
+  DemuxerStream::Type type = DemuxerStream::UNKNOWN;
+  if (audio_reached_eos_ || video_reached_eos_) {
+    // If we have eos in one or both buffers, the decision is easy.
+    if ((audio_reached_eos_ && video_reached_eos_) ||
+        (audio_reached_eos_ && !video_stream_.has_value()) ||
+        (video_reached_eos_ && !audio_stream_.has_value())) {
+      LOG(INFO) << "All streams at EOS, request loop is stopping.";
+      return;
+    }
+    // Only one of two streams is at eos; download data for the stream NOT at
+    // eos.
+    type = audio_reached_eos_ ? DemuxerStream::VIDEO : DemuxerStream::AUDIO;
+  } else if (!audio_stream_.has_value() || !video_stream_.has_value()) {
+    // If only one stream is present and not at eos, just download that data.
+    type =
+        audio_stream_.has_value() ? DemuxerStream::AUDIO : DemuxerStream::VIDEO;
+  } else {
+    // Both streams are present, and neither is at eos. Priority order for
+    // figuring out what to download next.
+    const base::TimeDelta audio_stamp = audio_stream_->GetLastBufferTimestamp();
+    const base::TimeDelta video_stamp = video_stream_->GetLastBufferTimestamp();
+    // If the audio demuxer stream is empty, always fill it first.
+    if (audio_stamp == ::media::kNoTimestamp) {
+      type = DemuxerStream::AUDIO;
+    } else if (video_stamp == ::media::kNoTimestamp) {
+      // The video demuxer stream is empty; we need data for it.
+      type = DemuxerStream::VIDEO;
+    } else if (video_stamp < audio_stamp) {
+      // Video is earlier; fill it first.
+      type = DemuxerStream::VIDEO;
+    } else {
+      type = DemuxerStream::AUDIO;
+    }
+  }
+
+  DCHECK_NE(type, DemuxerStream::UNKNOWN);
+  // We cannot call Request() directly even if this function is also run on
+  // |blocking_thread_| as otherwise it is possible that this function is
+  // running in a tight loop and seek/stop requests would have no chance to kick
+  // in.
+  blocking_thread_.task_runner()->PostTask(
+      FROM_HERE, base::Bind(&DemuxerExtensionWrapper::Request,
+                            base::Unretained(this), type));
+}
+
+bool DemuxerExtensionWrapper::HasStopped() {
+  base::AutoLock lock(lock_for_stopped_);
+  return stopped_;
+}
+
+namespace {
+
+// Ensure that the demuxer extension's enums match up with the internal enums.
+// This doesn't affect any code, but prevents compilation if there's a mismatch
+// somewhere.
+#define DEMUXER_EXTENSION_ENUM_EQ(a, b) \
+  COMPILE_ASSERT(static_cast<int>(a) == static_cast<int>(b), mismatching_enums)
+
+// Pipeline status.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerOk, ::media::PIPELINE_OK);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorNetwork,
+                          ::media::PIPELINE_ERROR_NETWORK);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorAbort,
+                          ::media::PIPELINE_ERROR_ABORT);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorInitializationFailed,
+                          ::media::PIPELINE_ERROR_INITIALIZATION_FAILED);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorRead,
+                          ::media::PIPELINE_ERROR_READ);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorInvalidState,
+                          ::media::PIPELINE_ERROR_INVALID_STATE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorCouldNotOpen,
+                          ::media::DEMUXER_ERROR_COULD_NOT_OPEN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorCouldNotParse,
+                          ::media::DEMUXER_ERROR_COULD_NOT_PARSE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerErrorNoSupportedStreams,
+                          ::media::DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
+
+// Audio codecs.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecUnknownAudio,
+                          ::media::AudioCodec::kUnknown);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecAAC,
+                          ::media::AudioCodec::kAAC);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecMP3,
+                          ::media::AudioCodec::kMP3);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecPCM,
+                          ::media::AudioCodec::kPCM);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecVorbis,
+                          ::media::AudioCodec::kVorbis);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecFLAC,
+                          ::media::AudioCodec::kFLAC);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecAMR_NB,
+                          ::media::AudioCodec::kAMR_NB);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecAMR_WB,
+                          ::media::AudioCodec::kAMR_WB);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecPCM_MULAW,
+                          ::media::AudioCodec::kPCM_MULAW);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecGSM_MS,
+                          ::media::AudioCodec::kGSM_MS);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecPCM_S16BE,
+                          ::media::AudioCodec::kPCM_S16BE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecPCM_S24BE,
+                          ::media::AudioCodec::kPCM_S24BE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecOpus,
+                          ::media::AudioCodec::kOpus);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecEAC3,
+                          ::media::AudioCodec::kEAC3);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecPCM_ALAW,
+                          ::media::AudioCodec::kPCM_ALAW);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecALAC,
+                          ::media::AudioCodec::kALAC);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecAC3,
+                          ::media::AudioCodec::kAC3);
+
+
+// Video codecs.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecUnknownVideo,
+                          ::media::VideoCodec::kUnknown);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecH264,
+                          ::media::VideoCodec::kH264);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecVC1,
+                          ::media::VideoCodec::kVC1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecMPEG2,
+                          ::media::VideoCodec::kMPEG2);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecMPEG4,
+                          ::media::VideoCodec::kMPEG4);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecTheora,
+                          ::media::VideoCodec::kTheora);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecVP8,
+                          ::media::VideoCodec::kVP8);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecVP9,
+                          ::media::VideoCodec::kVP9);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecHEVC,
+                          ::media::VideoCodec::kHEVC);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecDolbyVision,
+                          ::media::VideoCodec::kDolbyVision);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerCodecAV1,
+                          ::media::VideoCodec::kAV1);
+
+// Sample formats.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatUnknown,
+                          ::media::kUnknownSampleFormat);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatU8,
+                          ::media::kSampleFormatU8);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatS16,
+                          ::media::kSampleFormatS16);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatS32,
+                          ::media::kSampleFormatS32);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatF32,
+                          ::media::kSampleFormatF32);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatPlanarS16,
+                          ::media::kSampleFormatPlanarS16);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatPlanarF32,
+                          ::media::kSampleFormatPlanarF32);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatPlanarS32,
+                          ::media::kSampleFormatPlanarS32);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerSampleFormatS24,
+                          ::media::kSampleFormatS24);
+
+
+// Channel layouts.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutNone,
+                          ::media::CHANNEL_LAYOUT_NONE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutUnsupported,
+                          ::media::CHANNEL_LAYOUT_UNSUPPORTED);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutMono,
+                          ::media::CHANNEL_LAYOUT_MONO);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutStereo,
+                          ::media::CHANNEL_LAYOUT_STEREO);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout2_1,
+                          ::media::CHANNEL_LAYOUT_2_1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutSurround,
+                          ::media::CHANNEL_LAYOUT_SURROUND);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout4_0,
+                          ::media::CHANNEL_LAYOUT_4_0);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout2_2,
+                          ::media::CHANNEL_LAYOUT_2_2);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutQuad,
+                          ::media::CHANNEL_LAYOUT_QUAD);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout5_0,
+                          ::media::CHANNEL_LAYOUT_5_0);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout5_1,
+                          ::media::CHANNEL_LAYOUT_5_1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout5_0Back,
+                          ::media::CHANNEL_LAYOUT_5_0_BACK);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout5_1Back,
+                          ::media::CHANNEL_LAYOUT_5_1_BACK);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout7_0,
+                          ::media::CHANNEL_LAYOUT_7_0);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout7_1,
+                          ::media::CHANNEL_LAYOUT_7_1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout7_1Wide,
+                          ::media::CHANNEL_LAYOUT_7_1_WIDE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutStereoDownmix,
+                          ::media::CHANNEL_LAYOUT_STEREO_DOWNMIX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout2point1,
+                          ::media::CHANNEL_LAYOUT_2POINT1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout3_1,
+                          ::media::CHANNEL_LAYOUT_3_1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout4_1,
+                          ::media::CHANNEL_LAYOUT_4_1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout6_0,
+                          ::media::CHANNEL_LAYOUT_6_0);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout6_0Front,
+                          ::media::CHANNEL_LAYOUT_6_0_FRONT);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutHexagonal,
+                          ::media::CHANNEL_LAYOUT_HEXAGONAL);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout6_1,
+                          ::media::CHANNEL_LAYOUT_6_1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout6_1Back,
+                          ::media::CHANNEL_LAYOUT_6_1_BACK);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout6_1Front,
+                          ::media::CHANNEL_LAYOUT_6_1_FRONT);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout7_0Front,
+                          ::media::CHANNEL_LAYOUT_7_0_FRONT);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout7_1WideBack,
+                          ::media::CHANNEL_LAYOUT_7_1_WIDE_BACK);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutOctagonal,
+                          ::media::CHANNEL_LAYOUT_OCTAGONAL);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutDiscrete,
+                          ::media::CHANNEL_LAYOUT_DISCRETE);
+DEMUXER_EXTENSION_ENUM_EQ(
+    kCobaltExtensionDemuxerChannelLayoutStereoAndKeyboardMic,
+    ::media::CHANNEL_LAYOUT_STEREO_AND_KEYBOARD_MIC);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayout4_1QuadSide,
+                          ::media::CHANNEL_LAYOUT_4_1_QUAD_SIDE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerChannelLayoutBitstream,
+                          ::media::CHANNEL_LAYOUT_BITSTREAM);
+
+// Video codec profiles.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVideoCodecProfileUnknown,
+                          ::media::VIDEO_CODEC_PROFILE_UNKNOWN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileMin,
+                          ::media::H264PROFILE_MIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileBaseline,
+                          ::media::H264PROFILE_BASELINE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileMain,
+                          ::media::H264PROFILE_MAIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileExtended,
+                          ::media::H264PROFILE_EXTENDED);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileHigh,
+                          ::media::H264PROFILE_HIGH);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileHigh10Profile,
+                          ::media::H264PROFILE_HIGH10PROFILE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileHigh422Profile,
+                          ::media::H264PROFILE_HIGH422PROFILE);
+DEMUXER_EXTENSION_ENUM_EQ(
+    kCobaltExtensionDemuxerH264ProfileHigh444PredictiveProfile,
+    ::media::H264PROFILE_HIGH444PREDICTIVEPROFILE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileScalableBaseline,
+                          ::media::H264PROFILE_SCALABLEBASELINE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileScalableHigh,
+                          ::media::H264PROFILE_SCALABLEHIGH);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileStereoHigh,
+                          ::media::H264PROFILE_STEREOHIGH);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileMultiviewHigh,
+                          ::media::H264PROFILE_MULTIVIEWHIGH);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerH264ProfileMax,
+                          ::media::H264PROFILE_MAX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp8ProfileMin,
+                          ::media::VP8PROFILE_MIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp8ProfileAny,
+                          ::media::VP8PROFILE_ANY);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp8ProfileMax,
+                          ::media::VP8PROFILE_MAX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp9ProfileMin,
+                          ::media::VP9PROFILE_MIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp9ProfileProfile0,
+                          ::media::VP9PROFILE_PROFILE0);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp9ProfileProfile1,
+                          ::media::VP9PROFILE_PROFILE1);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp9ProfileProfile2,
+                          ::media::VP9PROFILE_PROFILE2);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp9ProfileProfile3,
+                          ::media::VP9PROFILE_PROFILE3);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerVp9ProfileMax,
+                          ::media::VP9PROFILE_MAX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerHevcProfileMin,
+                          ::media::HEVCPROFILE_MIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerHevcProfileMain,
+                          ::media::HEVCPROFILE_MAIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerHevcProfileMain10,
+                          ::media::HEVCPROFILE_MAIN10);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerHevcProfileMainStillPicture,
+                          ::media::HEVCPROFILE_MAIN_STILL_PICTURE);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerHevcProfileMax,
+                          ::media::HEVCPROFILE_MAX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerDolbyVisionProfile0,
+                          ::media::DOLBYVISION_PROFILE0);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerDolbyVisionProfile4,
+                          ::media::DOLBYVISION_PROFILE4);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerDolbyVisionProfile5,
+                          ::media::DOLBYVISION_PROFILE5);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerDolbyVisionProfile7,
+                          ::media::DOLBYVISION_PROFILE7);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerTheoraProfileMin,
+                          ::media::THEORAPROFILE_MIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerTheoraProfileAny,
+                          ::media::THEORAPROFILE_ANY);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerTheoraProfileMax,
+                          ::media::THEORAPROFILE_MAX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerAv1ProfileMin,
+                          ::media::AV1PROFILE_MIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerAv1ProfileProfileMain,
+                          ::media::AV1PROFILE_PROFILE_MAIN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerAv1ProfileProfileHigh,
+                          ::media::AV1PROFILE_PROFILE_HIGH);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerAv1ProfileProfilePro,
+                          ::media::AV1PROFILE_PROFILE_PRO);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerAv1ProfileMax,
+                          ::media::AV1PROFILE_MAX);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerDolbyVisionProfile8,
+                          ::media::DOLBYVISION_PROFILE8);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerDolbyVisionProfile9,
+                          ::media::DOLBYVISION_PROFILE9);
+
+// Color range IDs.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerColorSpaceRangeIdInvalid,
+                          gfx::ColorSpace::RangeID::INVALID);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerColorSpaceRangeIdLimited,
+                          gfx::ColorSpace::RangeID::LIMITED);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerColorSpaceRangeIdFull,
+                          gfx::ColorSpace::RangeID::FULL);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerColorSpaceRangeIdDerived,
+                          gfx::ColorSpace::RangeID::DERIVED);
+
+// Alpha modes.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerHasAlpha,
+                          ::media::VideoDecoderConfig::AlphaMode::kHasAlpha);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerIsOpaque,
+                          ::media::VideoDecoderConfig::AlphaMode::kIsOpaque);
+
+// Demuxer stream types.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerStreamTypeUnknown,
+                          ::media::DemuxerStream::Type::UNKNOWN);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerStreamTypeAudio,
+                          ::media::DemuxerStream::Type::AUDIO);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerStreamTypeVideo,
+                          ::media::DemuxerStream::Type::VIDEO);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerStreamTypeText,
+                          ::media::DemuxerStream::Type::TEXT);
+
+// Encryption schemes.
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerEncryptionSchemeUnencrypted,
+                          ::media::EncryptionScheme::kUnencrypted);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerEncryptionSchemeCenc,
+                          ::media::EncryptionScheme::kCenc);
+DEMUXER_EXTENSION_ENUM_EQ(kCobaltExtensionDemuxerEncryptionSchemeCbcs,
+                          ::media::EncryptionScheme::kCbcs);
+
+#undef DEMUXER_EXTENSION_ENUM_EQ
+
+}  // namespace
+
+}  // namespace media
+}  // namespace cobalt
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper.h b/cobalt/media/progressive/demuxer_extension_wrapper.h
new file mode 100644
index 0000000..138b922
--- /dev/null
+++ b/cobalt/media/progressive/demuxer_extension_wrapper.h
@@ -0,0 +1,248 @@
+// 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.
+//
+// Contains classes that wrap the Demuxer Cobalt Extension, providing an
+// implementation of a Cobalt demuxer. The main API is DemuxerExtensionWrapper.
+
+#ifndef COBALT_MEDIA_PROGRESSIVE_DEMUXER_EXTENSION_WRAPPER_H_
+#define COBALT_MEDIA_PROGRESSIVE_DEMUXER_EXTENSION_WRAPPER_H_
+
+#include <deque>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/memory/scoped_refptr.h"
+#include "base/optional.h"
+#include "base/sequence_checker.h"
+#include "base/threading/thread.h"
+#include "cobalt/extension/demuxer.h"
+#include "cobalt/media/progressive/data_source_reader.h"
+#include "third_party/chromium/media/base/audio_decoder_config.h"
+#include "third_party/chromium/media/base/decoder_buffer.h"
+#include "third_party/chromium/media/base/demuxer.h"
+#include "third_party/chromium/media/base/pipeline_status.h"
+#include "third_party/chromium/media/base/ranges.h"
+#include "third_party/chromium/media/base/video_decoder_config.h"
+
+namespace cobalt {
+namespace media {
+
+// Represents an audio or video stream. Reads data via the demuxer Cobalt
+// Extension.
+class DemuxerExtensionStream : public ::media::DemuxerStream {
+ public:
+  // Represents a video stream.
+  explicit DemuxerExtensionStream(
+      CobaltExtensionDemuxer* demuxer,
+      scoped_refptr<base::SequencedTaskRunner> message_loop,
+      CobaltExtensionDemuxerVideoDecoderConfig config);
+  // Represents an audio stream.
+  explicit DemuxerExtensionStream(
+      CobaltExtensionDemuxer* demuxer,
+      scoped_refptr<base::SequencedTaskRunner> message_loop,
+      CobaltExtensionDemuxerAudioDecoderConfig config);
+
+  // Disallow copy and assign.
+  DemuxerExtensionStream(const DemuxerExtensionStream&) = delete;
+  DemuxerExtensionStream& operator=(const DemuxerExtensionStream&) = delete;
+
+  ~DemuxerExtensionStream() = default;
+
+  // Functions used by DemuxerExtensionWrapper.
+  ::media::Ranges<base::TimeDelta> GetBufferedRanges();
+  void EnqueueBuffer(scoped_refptr<::media::DecoderBuffer> buffer);
+  void FlushBuffers();
+  void Stop();
+  base::TimeDelta GetLastBufferTimestamp() const;
+  size_t GetTotalBufferSize() const;
+
+  // DemuxerStream implementation:
+  void Read(ReadCB read_cb) override;
+  ::media::AudioDecoderConfig audio_decoder_config() override;
+  ::media::VideoDecoderConfig video_decoder_config() override;
+  Type type() const override;
+
+  void EnableBitstreamConverter() override { NOTIMPLEMENTED(); }
+
+  bool SupportsConfigChanges() override { return false; }
+
+ private:
+  typedef std::deque<scoped_refptr<::media::DecoderBuffer>> BufferQueue;
+  typedef std::deque<ReadCB> ReadQueue;
+
+  CobaltExtensionDemuxer* demuxer_ = nullptr;  // Not owned.
+  base::Optional<::media::VideoDecoderConfig> video_config_;
+  base::Optional<::media::AudioDecoderConfig> audio_config_;
+
+  // Protects everything below.
+  mutable base::Lock lock_;
+  // Keeps track of all time ranges this object has seen since creation.
+  // The demuxer uses these ranges to update the pipeline about what data
+  // it has demuxed.
+  ::media::Ranges<base::TimeDelta> buffered_ranges_;
+  // The last timestamp of buffer enqueued. This is used in two places:
+  //   1. Used with the timestamp of the current frame to calculate the
+  //      buffer range.
+  //   2. Used by the demuxer to deteminate what type of frame to get next.
+  base::TimeDelta last_buffer_timestamp_ = ::media::kNoTimestamp;
+  bool stopped_ = false;
+
+  BufferQueue buffer_queue_;
+  ReadQueue read_queue_;
+
+  scoped_refptr<base::SequencedTaskRunner> message_loop_;
+
+  size_t total_buffer_size_ = 0;
+};
+
+// Wraps a DataSourceReader in an even simpler API, where each read increments
+// the read location. This better matches the C data source API.
+class PositionalDataSource {
+ public:
+  explicit PositionalDataSource(scoped_refptr<DataSourceReader> reader);
+
+  // Disallow copy and assign.
+  PositionalDataSource(const PositionalDataSource&) = delete;
+  PositionalDataSource& operator=(const PositionalDataSource&) = delete;
+
+  ~PositionalDataSource();
+
+  void Stop();
+
+  // Reads up to |bytes_requested|, writing the data into |data|.
+  int BlockingRead(uint8_t* data, int bytes_requested);
+
+  // Seeks to |position|.
+  void SeekTo(int position);
+
+  // Returns the current read position.
+  int64_t GetPosition() const;
+
+  // Returns the size of the file.
+  //
+  // TODO(b/231744342): investigate whether we need to fix
+  // DataSourceReader::FileSize(). In testing, it sometimes returned inaccurate
+  // results before a file was fully downloaded. That behavior affects what this
+  // function returns.
+  int64_t GetSize();
+
+ private:
+  scoped_refptr<DataSourceReader> reader_;
+  int64_t position_ = 0;
+};
+
+// Wraps the demuxer Cobalt Extension in the internal media::Demuxer API.
+// Instances should be created via the Create method.
+class DemuxerExtensionWrapper : public ::media::Demuxer {
+ public:
+  // Constructs a new DemuxerExtensionWrapper, returning null on failure. If
+  // |data_source| or |message_loop| is null, or if a demuxer cannot be created,
+  // this will return null. If |demuxer_api| is null, we will attempt to use the
+  // corresponding Cobalt extension.
+  static std::unique_ptr<DemuxerExtensionWrapper> Create(
+      DataSource* data_source,
+      scoped_refptr<base::SequencedTaskRunner> message_loop,
+      const CobaltExtensionDemuxerApi* demuxer_api = nullptr);
+
+  // Disallow copy and assign.
+  DemuxerExtensionWrapper(const DemuxerExtensionWrapper&) = delete;
+  DemuxerExtensionWrapper& operator=(const DemuxerExtensionWrapper&) = delete;
+
+  ~DemuxerExtensionWrapper() override;
+
+  // Demuxer implementation:
+  std::vector<::media::DemuxerStream*> GetAllStreams() override;
+  std::string GetDisplayName() const override;
+  void Initialize(::media::DemuxerHost* host,
+                  ::media::PipelineStatusCallback status_cb) override;
+  void AbortPendingReads() override;
+  void StartWaitingForSeek(base::TimeDelta seek_time) override;
+  void CancelPendingSeek(base::TimeDelta seek_time) override;
+  void Seek(base::TimeDelta time,
+            ::media::PipelineStatusCallback status_cb) override;
+  void Stop() override;
+  base::TimeDelta GetStartTime() const override;
+  base::Time GetTimelineOffset() const override;
+  int64_t GetMemoryUsage() const override;
+  void OnEnabledAudioTracksChanged(
+      const std::vector<::media::MediaTrack::Id>& track_ids,
+      base::TimeDelta curr_time, TrackChangeCB change_completed_cb) override;
+  void OnSelectedVideoTrackChanged(
+      const std::vector<::media::MediaTrack::Id>& track_ids,
+      base::TimeDelta curr_time, TrackChangeCB change_completed_cb) override;
+
+  absl::optional<::media::container_names::MediaContainerName>
+  GetContainerForMetrics() const override {
+    NOTREACHED();
+    return absl::nullopt;
+  }
+
+ private:
+  // Only a forward declaration here, since the specifics of this class are an
+  // implementation detail.
+  class H264AnnexBConverter;
+
+  // Arguments must not be null.
+  explicit DemuxerExtensionWrapper(
+      const CobaltExtensionDemuxerApi* demuxer_api,
+      CobaltExtensionDemuxer* demuxer,
+      std::unique_ptr<PositionalDataSource> data_source,
+      std::unique_ptr<CobaltExtensionDemuxerDataSource> c_data_source,
+      scoped_refptr<base::SequencedTaskRunner> message_loop);
+
+  void OnInitializeDone(::media::PipelineStatusCallback status_cb,
+                        CobaltExtensionDemuxerStatus status);
+  void Request(::media::DemuxerStream::Type type);
+  bool HasStopped();
+  void IssueNextRequest();
+  void SeekTask(base::TimeDelta time,
+                ::media::PipelineStatusCallback status_cb);
+
+  // Returns the range of buffered data. If both audio and video streams are
+  // present, this is the intersection of their buffered ranges; otherwise, it
+  // is whatever range of data is buffered.
+  ::media::Ranges<base::TimeDelta> GetBufferedRanges();
+
+  const CobaltExtensionDemuxerApi* demuxer_api_ = nullptr;  // Not owned.
+  // Owned by this class. Construction/destruction is done via demuxer_api_.
+  CobaltExtensionDemuxer* impl_ = nullptr;
+  std::unique_ptr<PositionalDataSource> data_source_;
+  std::unique_ptr<CobaltExtensionDemuxerDataSource> c_data_source_;
+  ::media::DemuxerHost* host_ = nullptr;
+  mutable base::Lock lock_for_stopped_;
+  // Indicates whether Stop has been called.
+  bool stopped_ = false;
+  bool video_reached_eos_ = false;
+  bool audio_reached_eos_ = false;
+  bool flushing_ = false;
+
+  base::Optional<DemuxerExtensionStream> video_stream_;
+  base::Optional<DemuxerExtensionStream> audio_stream_;
+
+  std::unique_ptr<H264AnnexBConverter> h264_converter_;
+
+  // Thread for blocking I/O operations.
+  base::Thread blocking_thread_;
+
+  scoped_refptr<base::SequencedTaskRunner> message_loop_;
+
+  SEQUENCE_CHECKER(sequence_checker_);
+  base::WeakPtrFactory<DemuxerExtensionWrapper> weak_factory_{this};
+};
+
+}  // namespace media
+}  // namespace cobalt
+
+#endif  // COBALT_MEDIA_PROGRESSIVE_DEMUXER_EXTENSION_WRAPPER_H_
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper_test.cc b/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
new file mode 100644
index 0000000..3aae16a
--- /dev/null
+++ b/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
@@ -0,0 +1,701 @@
+// 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 "cobalt/media/progressive/demuxer_extension_wrapper.h"
+
+#include <cstdint>
+#include <memory>
+#include <tuple>
+#include <vector>
+
+#include "base/synchronization/waitable_event.h"
+#include "base/test/mock_callback.h"
+#include "base/test/scoped_task_environment.h"
+#include "base/threading/platform_thread.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+#include "base/time/time.h"
+#include "cobalt/extension/demuxer.h"
+#include "cobalt/media/decoder_buffer_allocator.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/chromium/media/base/demuxer.h"
+
+namespace cobalt {
+namespace media {
+namespace {
+
+using ::testing::_;
+using ::testing::AtMost;
+using ::testing::ElementsAreArray;
+using ::testing::ExplainMatchResult;
+using ::testing::Invoke;
+using ::testing::InvokeWithoutArgs;
+using ::testing::NiceMock;
+using ::testing::NotNull;
+using ::testing::Pointee;
+using ::testing::Return;
+using ::testing::UnorderedElementsAre;
+
+// Matches a DemuxerStream and verifies that the type is |type|.
+MATCHER_P(TypeIs, type, "") { return arg.type() == type; }
+
+// Matches a DecoderBuffer and verifies that the data in the buffer is |data|.
+MATCHER_P(BufferHasData, data, "") {
+  return ExplainMatchResult(
+      ElementsAreArray(data),
+      std::tuple<const uint8_t*, size_t>{arg.data(), arg.data_size()},
+      result_listener);
+}
+
+class MockDemuxerHost : public ::media::DemuxerHost {
+ public:
+  MockDemuxerHost() = default;
+
+  MockDemuxerHost(const MockDemuxerHost&) = delete;
+  MockDemuxerHost& operator=(const MockDemuxerHost&) = delete;
+
+  ~MockDemuxerHost() override = default;
+
+  MOCK_METHOD1(OnBufferedTimeRangesChanged,
+               void(const ::media::Ranges<base::TimeDelta>&));
+  MOCK_METHOD1(SetDuration, void(base::TimeDelta duration));
+  MOCK_METHOD1(OnDemuxerError, void(::media::PipelineStatus error));
+};
+
+class MockDataSource : public DataSource {
+ public:
+  MockDataSource() {
+    // Set reasonable default behavior for functions that are expected to
+    // interact with callbacks and/or output parameters.
+    ON_CALL(*this, Read(_, _, _, _))
+        .WillByDefault(Invoke(+[](int64_t position, int size, uint8_t* data,
+                                  const DataSource::ReadCB& read_cb) {
+          memset(data, 0, size);
+          read_cb.Run(size);
+        }));
+    ON_CALL(*this, GetSize(_)).WillByDefault(Invoke(+[](int64_t* size_out) {
+      *size_out = 0;
+      return true;
+    }));
+  }
+
+  ~MockDataSource() override = default;
+
+  MOCK_METHOD4(Read, void(int64_t position, int size, uint8_t* data,
+                          const DataSource::ReadCB& read_cb));
+  MOCK_METHOD0(Stop, void());
+  MOCK_METHOD0(Abort, void());
+  MOCK_METHOD1(GetSize, bool(int64_t* size_out));
+  MOCK_METHOD0(IsStreaming, bool());
+  MOCK_METHOD1(SetBitrate, void(int bitrate));
+};
+
+// Mock class for receiving calls to the Cobalt Extension demuxer. Based on the
+// CobaltExtensionDemuxer struct.
+class MockCobaltExtensionDemuxer {
+ public:
+  MOCK_METHOD0(Initialize, CobaltExtensionDemuxerStatus());
+  MOCK_METHOD1(Seek, CobaltExtensionDemuxerStatus(int64_t seek_time_us));
+  MOCK_METHOD0(GetStartTime, SbTime());
+  MOCK_METHOD0(GetTimelineOffset, SbTime());
+  MOCK_METHOD3(Read, void(CobaltExtensionDemuxerStreamType type,
+                          CobaltExtensionDemuxerReadCB read_cb,
+                          void* read_cb_user_data));
+  MOCK_METHOD1(GetAudioConfig,
+               bool(CobaltExtensionDemuxerAudioDecoderConfig* config));
+  MOCK_METHOD1(GetVideoConfig,
+               bool(CobaltExtensionDemuxerVideoDecoderConfig* config));
+  MOCK_METHOD0(GetDuration, SbTime());
+
+  // Pure C functions to be used in CobaltExtensionDemuxer. These expect
+  // |user_data| to be a pointer to a MockCobaltExtensionDemuxer.
+  static CobaltExtensionDemuxerStatus InitializeImpl(void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)->Initialize();
+  }
+
+  static CobaltExtensionDemuxerStatus SeekImpl(int64_t seek_time_us,
+                                               void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)->Seek(
+        seek_time_us);
+  }
+
+  static SbTime GetStartTimeImpl(void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)->GetStartTime();
+  }
+
+  static SbTime GetTimelineOffsetImpl(void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)
+        ->GetTimelineOffset();
+  }
+  static void ReadImpl(CobaltExtensionDemuxerStreamType type,
+                       CobaltExtensionDemuxerReadCB read_cb,
+                       void* read_cb_user_data, void* user_data) {
+    static_cast<MockCobaltExtensionDemuxer*>(user_data)->Read(
+        type, read_cb, read_cb_user_data);
+  }
+
+  static bool GetAudioConfigImpl(
+      CobaltExtensionDemuxerAudioDecoderConfig* config, void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)->GetAudioConfig(
+        config);
+  }
+
+  static bool GetVideoConfigImpl(
+      CobaltExtensionDemuxerVideoDecoderConfig* config, void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)->GetVideoConfig(
+        config);
+  }
+
+  static SbTime GetDurationImpl(void* user_data) {
+    return static_cast<MockCobaltExtensionDemuxer*>(user_data)->GetDuration();
+  }
+};
+
+// Forward declaration for the purpose of defining GetMockDemuxerApi. Defined
+// below.
+class MockDemuxerApi;
+
+// Returns a pointer to a MockDemuxerApi with static storage duration. The
+// returned pointer should not be deleted. Defined below.
+MockDemuxerApi* GetMockDemuxerApi();
+
+// Mock class for receiving calls to the Cobalt Extension demuxer API. Based on
+// the CobaltExtensionDemuxerApi struct.
+class MockDemuxerApi {
+ public:
+  MOCK_METHOD5(CreateDemuxer,
+               CobaltExtensionDemuxer*(
+                   CobaltExtensionDemuxerDataSource* data_source,
+                   CobaltExtensionDemuxerAudioCodec* supported_audio_codecs,
+                   int64_t supported_audio_codecs_size,
+                   CobaltExtensionDemuxerVideoCodec* supported_video_codecs,
+                   int64_t supported_video_codecs_size));
+  MOCK_METHOD1(DestroyDemuxer, void(CobaltExtensionDemuxer* demuxer));
+
+  // Pure C functions to be used in CobaltExtensionDemuxer. These expect
+  // |user_data| to be a pointer to a MockDemuxerApi.
+  static CobaltExtensionDemuxer* CreateDemuxerImpl(
+      CobaltExtensionDemuxerDataSource* data_source,
+      CobaltExtensionDemuxerAudioCodec* supported_audio_codecs,
+      int64_t supported_audio_codecs_size,
+      CobaltExtensionDemuxerVideoCodec* supported_video_codecs,
+      int64_t supported_video_codecs_size) {
+    return GetMockDemuxerApi()->CreateDemuxer(
+        data_source, supported_audio_codecs, supported_audio_codecs_size,
+        supported_video_codecs, supported_video_codecs_size);
+  }
+
+  static void DestroyDemuxerImpl(CobaltExtensionDemuxer* demuxer) {
+    GetMockDemuxerApi()->DestroyDemuxer(demuxer);
+  }
+};
+
+MockDemuxerApi* GetMockDemuxerApi() {
+  static auto* const demuxer_api = []() {
+    auto* inner_demuxer_api = new MockDemuxerApi;
+    // This mock won't be destructed.
+    testing::Mock::AllowLeak(inner_demuxer_api);
+    return inner_demuxer_api;
+  }();
+
+  return demuxer_api;
+}
+
+CobaltExtensionDemuxer* CreateCDemuxer(
+    MockCobaltExtensionDemuxer* mock_demuxer) {
+  CHECK(mock_demuxer);
+  return new CobaltExtensionDemuxer{
+      &MockCobaltExtensionDemuxer::InitializeImpl,
+      &MockCobaltExtensionDemuxer::SeekImpl,
+      &MockCobaltExtensionDemuxer::GetStartTimeImpl,
+      &MockCobaltExtensionDemuxer::GetTimelineOffsetImpl,
+      &MockCobaltExtensionDemuxer::ReadImpl,
+      &MockCobaltExtensionDemuxer::GetAudioConfigImpl,
+      &MockCobaltExtensionDemuxer::GetVideoConfigImpl,
+      &MockCobaltExtensionDemuxer::GetDurationImpl,
+      mock_demuxer};
+}
+
+// A test fixture is used to verify and clear the global mock demuxer, and to
+// manage the lifetime of the ScopedTaskEnvironment.
+class DemuxerExtensionWrapperTest : public ::testing::Test {
+ protected:
+  DemuxerExtensionWrapperTest() = default;
+
+  ~DemuxerExtensionWrapperTest() override {
+    testing::Mock::VerifyAndClearExpectations(GetMockDemuxerApi());
+  }
+
+  // Waits |time_limit| for |done| to occur. Returns true if the event occurred,
+  // false otherwise. While waiting, allows other threads to run and runs the
+  // task runner until idle.
+  bool WaitForEvent(base::WaitableEvent& done,
+                    base::TimeDelta time_limit = base::Seconds(1)) {
+    const base::Time deadline = base::Time::Now() + base::Seconds(1);
+    while (base::Time::Now() < deadline) {
+      task_environment_.RunUntilIdle();
+      base::PlatformThread::YieldCurrentThread();
+      if (done.IsSignaled()) {
+        break;
+      }
+    }
+    return done.IsSignaled();
+  }
+
+  // This must be deleted last.
+  base::test::ScopedTaskEnvironment task_environment_{
+      base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME};
+  // This is necessary in order to allocate DecoderBuffers. This is done
+  // internally in DemuxerExtensionWrapper.
+  DecoderBufferAllocator allocator_;
+};
+
+TEST_F(DemuxerExtensionWrapperTest, SuccessfullyInitializes) {
+  // This must outlive the DemuxerExtensionWrapper.
+  NiceMock<MockDemuxerHost> mock_host;
+  MockDataSource data_source;
+  MockDemuxerApi* mock_demuxer_api = GetMockDemuxerApi();  // Not owned.
+  NiceMock<MockCobaltExtensionDemuxer> mock_demuxer;
+
+  // In this test we don't care about what data is read.
+  ON_CALL(mock_demuxer, Read(_, _, _))
+      .WillByDefault(Invoke([](CobaltExtensionDemuxerStreamType type,
+                               CobaltExtensionDemuxerReadCB read_cb,
+                               void* read_cb_user_data) {
+        // Simulate EOS.
+        CobaltExtensionDemuxerBuffer eos_buffer = {};
+        eos_buffer.end_of_stream = true;
+        read_cb(&eos_buffer, read_cb_user_data);
+      }));
+
+  const CobaltExtensionDemuxerApi api = {
+      /*name=*/kCobaltExtensionDemuxerApi,
+      /*version=*/1,
+      /*CreateDemuxer=*/&MockDemuxerApi::CreateDemuxerImpl,
+      /*DestroyDemuxer=*/&MockDemuxerApi::DestroyDemuxerImpl,
+  };
+
+  auto c_demuxer =
+      std::unique_ptr<CobaltExtensionDemuxer>(CreateCDemuxer(&mock_demuxer));
+  EXPECT_CALL(*mock_demuxer_api, CreateDemuxer(_, _, _, _, _))
+      .WillOnce(Return(c_demuxer.get()));
+  EXPECT_CALL(*mock_demuxer_api, DestroyDemuxer(c_demuxer.get())).Times(1);
+
+  std::unique_ptr<DemuxerExtensionWrapper> demuxer_wrapper =
+      DemuxerExtensionWrapper::Create(
+          &data_source, base::SequencedTaskRunnerHandle::Get(), &api);
+
+  ASSERT_THAT(demuxer_wrapper, NotNull());
+
+  base::WaitableEvent init_done;
+  base::MockCallback<base::OnceCallback<void(::media::PipelineStatus)>>
+      initialize_cb;
+  EXPECT_CALL(mock_demuxer, Initialize())
+      .WillOnce(Return(kCobaltExtensionDemuxerOk));
+  // Simulate an audio file.
+  EXPECT_CALL(mock_demuxer, GetAudioConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerAudioDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecAAC;
+        config->sample_format = kCobaltExtensionDemuxerSampleFormatF32;
+        config->channel_layout = kCobaltExtensionDemuxerChannelLayoutStereo;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->samples_per_second = 44100;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(mock_demuxer, GetVideoConfig(NotNull())).WillOnce(Return(false));
+  EXPECT_CALL(initialize_cb, Run(::media::PIPELINE_OK))
+      .WillOnce(InvokeWithoutArgs([&init_done]() { init_done.Signal(); }));
+
+  demuxer_wrapper->Initialize(&mock_host, initialize_cb.Get());
+
+  EXPECT_TRUE(WaitForEvent(init_done));
+}
+
+TEST_F(DemuxerExtensionWrapperTest, ProvidesAudioAndVideoStreams) {
+  // This must outlive the DemuxerExtensionWrapper.
+  NiceMock<MockDemuxerHost> mock_host;
+  MockDataSource data_source;
+  MockDemuxerApi* mock_demuxer_api = GetMockDemuxerApi();  // Not owned.
+  NiceMock<MockCobaltExtensionDemuxer> mock_demuxer;
+
+  // In this test we don't care about what data is read.
+  ON_CALL(mock_demuxer, Read(_, _, _))
+      .WillByDefault(Invoke([](CobaltExtensionDemuxerStreamType type,
+                               CobaltExtensionDemuxerReadCB read_cb,
+                               void* read_cb_user_data) {
+        // Simulate EOS.
+        CobaltExtensionDemuxerBuffer eos_buffer = {};
+        eos_buffer.end_of_stream = true;
+        read_cb(&eos_buffer, read_cb_user_data);
+      }));
+
+  const CobaltExtensionDemuxerApi api = {
+      /*name=*/kCobaltExtensionDemuxerApi,
+      /*version=*/1,
+      /*CreateDemuxer=*/&MockDemuxerApi::CreateDemuxerImpl,
+      /*DestroyDemuxer=*/&MockDemuxerApi::DestroyDemuxerImpl,
+  };
+
+  auto c_demuxer =
+      std::unique_ptr<CobaltExtensionDemuxer>(CreateCDemuxer(&mock_demuxer));
+  EXPECT_CALL(*mock_demuxer_api, CreateDemuxer(_, _, _, _, _))
+      .WillOnce(Return(c_demuxer.get()));
+  EXPECT_CALL(*mock_demuxer_api, DestroyDemuxer(c_demuxer.get())).Times(1);
+
+  std::unique_ptr<DemuxerExtensionWrapper> demuxer_wrapper =
+      DemuxerExtensionWrapper::Create(
+          &data_source, base::SequencedTaskRunnerHandle::Get(), &api);
+
+  ASSERT_THAT(demuxer_wrapper, NotNull());
+
+  base::WaitableEvent init_done;
+  base::MockCallback<base::OnceCallback<void(::media::PipelineStatus)>>
+      initialize_cb;
+  EXPECT_CALL(mock_demuxer, Initialize())
+      .WillOnce(Return(kCobaltExtensionDemuxerOk));
+  // Simulate an audio+video file.
+  EXPECT_CALL(mock_demuxer, GetAudioConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerAudioDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecAAC;
+        config->sample_format = kCobaltExtensionDemuxerSampleFormatF32;
+        config->channel_layout = kCobaltExtensionDemuxerChannelLayoutStereo;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->samples_per_second = 44100;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(mock_demuxer, GetVideoConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerVideoDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecH264;
+        config->profile = kCobaltExtensionDemuxerH264ProfileMain;
+        config->color_space_primaries = 1;
+        config->color_space_transfer = 1;
+        config->color_space_matrix = 1;
+        config->color_space_range_id =
+            kCobaltExtensionDemuxerColorSpaceRangeIdFull;
+        config->alpha_mode = kCobaltExtensionDemuxerHasAlpha;
+        config->coded_width = 1920;
+        config->coded_height = 1080;
+        config->visible_rect_x = 0;
+        config->visible_rect_y = 0;
+        config->visible_rect_width = 1920;
+        config->visible_rect_height = 1080;
+        config->natural_width = 1920;
+        config->natural_height = 1080;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(initialize_cb, Run(::media::PIPELINE_OK))
+      .WillOnce(InvokeWithoutArgs([&init_done]() { init_done.Signal(); }));
+
+  demuxer_wrapper->Initialize(&mock_host, initialize_cb.Get());
+
+  EXPECT_TRUE(WaitForEvent(init_done));
+
+  std::vector<::media::DemuxerStream*> streams =
+      demuxer_wrapper->GetAllStreams();
+  EXPECT_THAT(streams,
+              UnorderedElementsAre(
+                  Pointee(TypeIs(::media::DemuxerStream::Type::AUDIO)),
+                  Pointee(TypeIs(::media::DemuxerStream::Type::VIDEO))));
+}
+
+TEST_F(DemuxerExtensionWrapperTest, ReadsAudioData) {
+  // This must outlive the DemuxerExtensionWrapper.
+  NiceMock<MockDemuxerHost> mock_host;
+  MockDataSource data_source;
+  MockDemuxerApi* mock_demuxer_api = GetMockDemuxerApi();  // Not owned.
+  NiceMock<MockCobaltExtensionDemuxer> mock_demuxer;
+
+  const CobaltExtensionDemuxerApi api = {
+      /*name=*/kCobaltExtensionDemuxerApi,
+      /*version=*/1,
+      /*CreateDemuxer=*/&MockDemuxerApi::CreateDemuxerImpl,
+      /*DestroyDemuxer=*/&MockDemuxerApi::DestroyDemuxerImpl,
+  };
+
+  auto c_demuxer =
+      std::unique_ptr<CobaltExtensionDemuxer>(CreateCDemuxer(&mock_demuxer));
+  EXPECT_CALL(*mock_demuxer_api, CreateDemuxer(_, _, _, _, _))
+      .WillOnce(Return(c_demuxer.get()));
+  EXPECT_CALL(*mock_demuxer_api, DestroyDemuxer(c_demuxer.get())).Times(1);
+
+  std::unique_ptr<DemuxerExtensionWrapper> demuxer_wrapper =
+      DemuxerExtensionWrapper::Create(
+          &data_source, base::SequencedTaskRunnerHandle::Get(), &api);
+
+  ASSERT_THAT(demuxer_wrapper, NotNull());
+
+  base::WaitableEvent init_done;
+  base::MockCallback<base::OnceCallback<void(::media::PipelineStatus)>>
+      initialize_cb;
+  EXPECT_CALL(mock_demuxer, Initialize())
+      .WillOnce(Return(kCobaltExtensionDemuxerOk));
+  // Simulate an audio+video file.
+  EXPECT_CALL(mock_demuxer, GetAudioConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerAudioDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecAAC;
+        config->sample_format = kCobaltExtensionDemuxerSampleFormatF32;
+        config->channel_layout = kCobaltExtensionDemuxerChannelLayoutStereo;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->samples_per_second = 44100;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(mock_demuxer, GetVideoConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerVideoDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecH264;
+        config->profile = kCobaltExtensionDemuxerH264ProfileMain;
+        config->color_space_primaries = 1;
+        config->color_space_transfer = 1;
+        config->color_space_matrix = 1;
+        config->color_space_range_id =
+            kCobaltExtensionDemuxerColorSpaceRangeIdFull;
+        config->alpha_mode = kCobaltExtensionDemuxerHasAlpha;
+        config->coded_width = 1920;
+        config->coded_height = 1080;
+        config->visible_rect_x = 0;
+        config->visible_rect_y = 0;
+        config->visible_rect_width = 1920;
+        config->visible_rect_height = 1080;
+        config->natural_width = 1920;
+        config->natural_height = 1080;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(initialize_cb, Run(::media::PIPELINE_OK))
+      .WillOnce(InvokeWithoutArgs([&init_done]() { init_done.Signal(); }));
+
+  std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
+  EXPECT_CALL(mock_demuxer, Read(kCobaltExtensionDemuxerStreamTypeAudio, _, _))
+      .WillOnce(Invoke([&buffer_data](CobaltExtensionDemuxerStreamType type,
+                                      CobaltExtensionDemuxerReadCB read_cb,
+                                      void* read_cb_user_data) {
+        // Send one "real" buffer.
+        CobaltExtensionDemuxerBuffer buffer = {};
+        buffer.data = buffer_data.data();
+        buffer.data_size = buffer_data.size();
+        buffer.side_data = nullptr;
+        buffer.side_data_elements = 0;
+        buffer.pts = 0;
+        buffer.duration = 1000;
+        buffer.is_keyframe = true;
+        buffer.end_of_stream = false;
+        read_cb(&buffer, read_cb_user_data);
+      }))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerStreamType type,
+                          CobaltExtensionDemuxerReadCB read_cb,
+                          void* read_cb_user_data) {
+        // Simulate the audio stream being done.
+        CobaltExtensionDemuxerBuffer eos_buffer = {};
+        eos_buffer.end_of_stream = true;
+        read_cb(&eos_buffer, read_cb_user_data);
+      }));
+
+  // The impl may or may not try reading video data. If it does, return EOS.
+  EXPECT_CALL(mock_demuxer, Read(kCobaltExtensionDemuxerStreamTypeVideo, _, _))
+      .Times(AtMost(1))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerStreamType type,
+                          CobaltExtensionDemuxerReadCB read_cb,
+                          void* read_cb_user_data) {
+        // Simulate the video stream being done.
+        CobaltExtensionDemuxerBuffer eos_buffer = {};
+        eos_buffer.end_of_stream = true;
+        read_cb(&eos_buffer, read_cb_user_data);
+      }));
+
+  demuxer_wrapper->Initialize(&mock_host, initialize_cb.Get());
+
+  EXPECT_TRUE(WaitForEvent(init_done));
+
+  std::vector<::media::DemuxerStream*> streams =
+      demuxer_wrapper->GetAllStreams();
+  ASSERT_THAT(streams,
+              UnorderedElementsAre(
+                  Pointee(TypeIs(::media::DemuxerStream::Type::AUDIO)),
+                  Pointee(TypeIs(::media::DemuxerStream::Type::VIDEO))));
+  ::media::DemuxerStream* audio_stream =
+      streams[0]->type() == ::media::DemuxerStream::Type::AUDIO ? streams[0]
+                                                                : streams[1];
+
+  base::MockCallback<base::OnceCallback<void(
+      ::media::DemuxerStream::Status, scoped_refptr<::media::DecoderBuffer>)>>
+      read_cb;
+  base::WaitableEvent read_done;
+  EXPECT_CALL(read_cb, Run(::media::DemuxerStream::kOk,
+                           Pointee(BufferHasData(buffer_data))))
+      .WillOnce(InvokeWithoutArgs([&read_done]() { read_done.Signal(); }));
+
+  audio_stream->Read(read_cb.Get());
+  EXPECT_TRUE(WaitForEvent(read_done));
+}
+
+TEST_F(DemuxerExtensionWrapperTest, ReadsVideoData) {
+  // This must outlive the DemuxerExtensionWrapper.
+  NiceMock<MockDemuxerHost> mock_host;
+  MockDataSource data_source;
+  MockDemuxerApi* mock_demuxer_api = GetMockDemuxerApi();  // Not owned.
+  NiceMock<MockCobaltExtensionDemuxer> mock_demuxer;
+
+  const CobaltExtensionDemuxerApi api = {
+      /*name=*/kCobaltExtensionDemuxerApi,
+      /*version=*/1,
+      /*CreateDemuxer=*/&MockDemuxerApi::CreateDemuxerImpl,
+      /*DestroyDemuxer=*/&MockDemuxerApi::DestroyDemuxerImpl,
+  };
+
+  auto c_demuxer =
+      std::unique_ptr<CobaltExtensionDemuxer>(CreateCDemuxer(&mock_demuxer));
+  EXPECT_CALL(*mock_demuxer_api, CreateDemuxer(_, _, _, _, _))
+      .WillOnce(Return(c_demuxer.get()));
+  EXPECT_CALL(*mock_demuxer_api, DestroyDemuxer(c_demuxer.get())).Times(1);
+
+  std::unique_ptr<DemuxerExtensionWrapper> demuxer_wrapper =
+      DemuxerExtensionWrapper::Create(
+          &data_source, base::SequencedTaskRunnerHandle::Get(), &api);
+
+  ASSERT_THAT(demuxer_wrapper, NotNull());
+
+  base::WaitableEvent init_done;
+  base::MockCallback<base::OnceCallback<void(::media::PipelineStatus)>>
+      initialize_cb;
+  EXPECT_CALL(mock_demuxer, Initialize())
+      .WillOnce(Return(kCobaltExtensionDemuxerOk));
+  // Simulate an audio+video file.
+  EXPECT_CALL(mock_demuxer, GetAudioConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerAudioDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecAAC;
+        config->sample_format = kCobaltExtensionDemuxerSampleFormatF32;
+        config->channel_layout = kCobaltExtensionDemuxerChannelLayoutStereo;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->samples_per_second = 44100;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(mock_demuxer, GetVideoConfig(NotNull()))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerVideoDecoderConfig* config) {
+        config->codec = kCobaltExtensionDemuxerCodecH264;
+        config->profile = kCobaltExtensionDemuxerH264ProfileMain;
+        config->color_space_primaries = 1;
+        config->color_space_transfer = 1;
+        config->color_space_matrix = 1;
+        config->color_space_range_id =
+            kCobaltExtensionDemuxerColorSpaceRangeIdFull;
+        config->alpha_mode = kCobaltExtensionDemuxerHasAlpha;
+        config->coded_width = 1920;
+        config->coded_height = 1080;
+        config->visible_rect_x = 0;
+        config->visible_rect_y = 0;
+        config->visible_rect_width = 1920;
+        config->visible_rect_height = 1080;
+        config->natural_width = 1920;
+        config->natural_height = 1080;
+        config->encryption_scheme =
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted;
+        config->extra_data = nullptr;
+        config->extra_data_size = 0;
+
+        return true;
+      }));
+  EXPECT_CALL(initialize_cb, Run(::media::PIPELINE_OK))
+      .WillOnce(InvokeWithoutArgs([&init_done]() { init_done.Signal(); }));
+
+  std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
+  EXPECT_CALL(mock_demuxer, Read(kCobaltExtensionDemuxerStreamTypeVideo, _, _))
+      .WillOnce(Invoke([&buffer_data](CobaltExtensionDemuxerStreamType type,
+                                      CobaltExtensionDemuxerReadCB read_cb,
+                                      void* read_cb_user_data) {
+        // Send one "real" buffer.
+        CobaltExtensionDemuxerBuffer buffer = {};
+        buffer.data = buffer_data.data();
+        buffer.data_size = buffer_data.size();
+        buffer.side_data = nullptr;
+        buffer.side_data_elements = 0;
+        buffer.pts = 0;
+        buffer.duration = 1000;
+        buffer.is_keyframe = true;
+        buffer.end_of_stream = false;
+        read_cb(&buffer, read_cb_user_data);
+      }))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerStreamType type,
+                          CobaltExtensionDemuxerReadCB read_cb,
+                          void* read_cb_user_data) {
+        // Simulate the video stream being done.
+        CobaltExtensionDemuxerBuffer eos_buffer = {};
+        eos_buffer.end_of_stream = true;
+        read_cb(&eos_buffer, read_cb_user_data);
+      }));
+
+  // The impl may or may not try reading audio data. If it does, return EOS.
+  EXPECT_CALL(mock_demuxer, Read(kCobaltExtensionDemuxerStreamTypeAudio, _, _))
+      .Times(AtMost(1))
+      .WillOnce(Invoke([](CobaltExtensionDemuxerStreamType type,
+                          CobaltExtensionDemuxerReadCB read_cb,
+                          void* read_cb_user_data) {
+        // Simulate the audio stream being done.
+        CobaltExtensionDemuxerBuffer eos_buffer = {};
+        eos_buffer.end_of_stream = true;
+        read_cb(&eos_buffer, read_cb_user_data);
+      }));
+
+  demuxer_wrapper->Initialize(&mock_host, initialize_cb.Get());
+
+  EXPECT_TRUE(WaitForEvent(init_done));
+
+  std::vector<::media::DemuxerStream*> streams =
+      demuxer_wrapper->GetAllStreams();
+  ASSERT_THAT(streams,
+              UnorderedElementsAre(
+                  Pointee(TypeIs(::media::DemuxerStream::Type::AUDIO)),
+                  Pointee(TypeIs(::media::DemuxerStream::Type::VIDEO))));
+  ::media::DemuxerStream* video_stream =
+      streams[0]->type() == ::media::DemuxerStream::Type::VIDEO ? streams[0]
+                                                                : streams[1];
+
+  base::MockCallback<base::OnceCallback<void(
+      ::media::DemuxerStream::Status, scoped_refptr<::media::DecoderBuffer>)>>
+      read_cb;
+  base::WaitableEvent read_done;
+  EXPECT_CALL(read_cb, Run(::media::DemuxerStream::kOk,
+                           Pointee(BufferHasData(buffer_data))))
+      .WillOnce(InvokeWithoutArgs([&read_done]() { read_done.Signal(); }));
+
+  video_stream->Read(read_cb.Get());
+  EXPECT_TRUE(WaitForEvent(read_done));
+}
+
+}  // namespace
+}  // namespace media
+}  // namespace cobalt
diff --git a/cobalt/media_integration_tests/functionality/general_playback.py b/cobalt/media_integration_tests/functionality/general_playback.py
index 8b3ea3a..95b1e1a 100644
--- a/cobalt/media_integration_tests/functionality/general_playback.py
+++ b/cobalt/media_integration_tests/functionality/general_playback.py
@@ -16,7 +16,7 @@
 import logging
 
 from cobalt.media_integration_tests.test_case import TestCase
-from cobalt.media_integration_tests.test_util import PlaybackUrls, MimeStrings
+from cobalt.media_integration_tests.test_util import PlaybackUrls
 
 
 class GeneralPlaybackTest(TestCase):
@@ -40,19 +40,19 @@
 
 TEST_PARAMETERS = [
     ('H264', PlaybackUrls.H264_ONLY, None),
-    # TODO(b/223856877) -- find out why Progressive still broken
+    # TODO(b/223856877) -- renable these when tests are stable.
     #('PROGRESSIVE', PlaybackUrls.PROGRESSIVE, None),
-    ('ENCRYPTED', PlaybackUrls.ENCRYPTED, None),
-    ('VR', PlaybackUrls.VR, None),
-    ('VP9', PlaybackUrls.VP9, MimeStrings.VP9),
-    ('VP9_HFR', PlaybackUrls.VP9_HFR, MimeStrings.VP9_HFR),
-    ('AV1', PlaybackUrls.AV1, MimeStrings.AV1),
-    ('AV1_HFR', PlaybackUrls.AV1_HFR, MimeStrings.AV1_HFR),
-    ('VERTICAL', PlaybackUrls.VERTICAL, None),
-    ('SHORT', PlaybackUrls.SHORT, None),
-    ('VP9_HDR_HLG', PlaybackUrls.VP9_HDR_HLG, MimeStrings.VP9_HDR_HLG),
-    ('VP9_HDR_PQ', PlaybackUrls.VP9_HDR_PQ, MimeStrings.VP9_HDR_PQ),
-    ('HDR_PQ_HFR', PlaybackUrls.HDR_PQ_HFR, MimeStrings.VP9_HDR_PQ_HFR),
+    #('ENCRYPTED', PlaybackUrls.ENCRYPTED, None),
+    #('VR', PlaybackUrls.VR, None),
+    #('VP9', PlaybackUrls.VP9, MimeStrings.VP9),
+    #('VP9_HFR', PlaybackUrls.VP9_HFR, MimeStrings.VP9_HFR),
+    #('AV1', PlaybackUrls.AV1, MimeStrings.AV1),
+    #('AV1_HFR', PlaybackUrls.AV1_HFR, MimeStrings.AV1_HFR),
+    #('VERTICAL', PlaybackUrls.VERTICAL, None),
+    #('SHORT', PlaybackUrls.SHORT, None),
+    #('VP9_HDR_HLG', PlaybackUrls.VP9_HDR_HLG, MimeStrings.VP9_HDR_HLG),
+    #('VP9_HDR_PQ', PlaybackUrls.VP9_HDR_PQ, MimeStrings.VP9_HDR_PQ),
+    #('HDR_PQ_HFR', PlaybackUrls.HDR_PQ_HFR, MimeStrings.VP9_HDR_PQ_HFR),
 ]
 
 for name, playback_url, mime_str in TEST_PARAMETERS:
diff --git a/cobalt/version.h b/cobalt/version.h
index 18d66ee..3f59478 100644
--- a/cobalt/version.h
+++ b/cobalt/version.h
@@ -35,6 +35,6 @@
 //                  release is cut.
 //.
 
-#define COBALT_VERSION "23.master.0"
+#define COBALT_VERSION "23.lts.1"
 
 #endif  // COBALT_VERSION_H_
diff --git a/starboard/android/shared/cobalt/configuration.py b/starboard/android/shared/cobalt/configuration.py
index df2e568..c606d82 100644
--- a/starboard/android/shared/cobalt/configuration.py
+++ b/starboard/android/shared/cobalt/configuration.py
@@ -64,7 +64,8 @@
       ],
       'crypto_unittests': ['P224.*'],
       'renderer_test': [
-          # TODO(b/236034292): These tests load the wrong fonts sometimes.
+          # TODO(b/215739322): Using the android fonts breaks these tests. They
+          # make use of fonts that are not bundled on Android.
           'PixelTest.SimpleTextInRed40PtChineseFont',
           'PixelTest.SimpleTextInRed40PtThaiFont',
 
diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn
index 8e4a107..74986a73 100644
--- a/starboard/linux/shared/BUILD.gn
+++ b/starboard/linux/shared/BUILD.gn
@@ -84,6 +84,8 @@
     "//starboard/shared/alsa/alsa_util.h",
     "//starboard/shared/deviceauth/deviceauth_internal.cc",
     "//starboard/shared/egl/system_egl.cc",
+    "//starboard/shared/ffmpeg/ffmpeg_demuxer.cc",
+    "//starboard/shared/ffmpeg/ffmpeg_demuxer.h",
     "//starboard/shared/gcc/atomic_gcc_public.h",
     "//starboard/shared/gles/system_gles2.cc",
     "//starboard/shared/iso/character_is_alphanumeric.cc",
diff --git a/starboard/linux/shared/system_get_extensions.cc b/starboard/linux/shared/system_get_extensions.cc
index 3c44ffd..fba44bd 100644
--- a/starboard/linux/shared/system_get_extensions.cc
+++ b/starboard/linux/shared/system_get_extensions.cc
@@ -16,13 +16,16 @@
 
 #include "cobalt/extension/configuration.h"
 #include "cobalt/extension/crash_handler.h"
+#include "cobalt/extension/demuxer.h"
 #include "cobalt/extension/free_space.h"
 #include "cobalt/extension/memory_mapped_file.h"
 #include "cobalt/extension/platform_service.h"
 #include "starboard/common/string.h"
 #include "starboard/linux/shared/soft_mic_platform_service.h"
+#include "starboard/shared/ffmpeg/ffmpeg_demuxer.h"
 #include "starboard/shared/posix/free_space.h"
 #include "starboard/shared/posix/memory_mapped_file.h"
+#include "starboard/shared/starboard/application.h"
 #include "starboard/shared/starboard/crash_handler.h"
 #if SB_IS(EVERGREEN_COMPATIBLE)
 #include "starboard/elf_loader/evergreen_config.h"
@@ -56,5 +59,13 @@
   if (strcmp(name, kCobaltExtensionFreeSpaceName) == 0) {
     return starboard::shared::posix::GetFreeSpaceApi();
   }
+  if (strcmp(name, kCobaltExtensionDemuxerApi) == 0) {
+    auto command_line =
+        starboard::shared::starboard::Application::Get()->GetCommandLine();
+    const bool use_ffmpeg_demuxer =
+        command_line->HasSwitch("enable_demuxer_extension");
+    return use_ffmpeg_demuxer ? starboard::shared::ffmpeg::GetFFmpegDemuxerApi()
+                              : NULL;
+  }
   return NULL;
 }
diff --git a/starboard/nplb/player_create_test.cc b/starboard/nplb/player_create_test.cc
index f3038b1..87c3e65 100644
--- a/starboard/nplb/player_create_test.cc
+++ b/starboard/nplb/player_create_test.cc
@@ -54,6 +54,26 @@
   SbPlayerOutputMode output_mode_;
 };
 
+void DummyDeallocateSampleFunc(SbPlayer player,
+                               void* context,
+                               const void* sample_buffer) {}
+
+void DummyDecoderStatusFunc(SbPlayer player,
+                            void* context,
+                            SbMediaType type,
+                            SbPlayerDecoderState state,
+                            int ticket) {}
+
+void DummyStatusFunc(SbPlayer player,
+                     void* context,
+                     SbPlayerState state,
+                     int ticket) {}
+
+void DummyErrorFunc(SbPlayer player,
+                    void* context,
+                    SbPlayerError error,
+                    const char* message) {}
+
 TEST_P(SbPlayerTest, SunnyDay) {
   SbMediaAudioSampleInfo audio_sample_info =
       CreateAudioSampleInfo(kSbMediaAudioCodecAac);
@@ -67,7 +87,7 @@
       fake_graphics_context_provider_.window(), kSbMediaVideoCodecH264,
       kSbMediaAudioCodecAac, kSbDrmSystemInvalid, &audio_sample_info,
       "" /* max_video_capabilities */, DummyDeallocateSampleFunc,
-      DummyDecoderStatusFunc, DummyPlayerStatusFunc, DummyErrorFunc,
+      DummyDecoderStatusFunc, DummyStatusFunc, DummyErrorFunc,
       NULL /* context */, output_mode_,
       fake_graphics_context_provider_.decoder_target_provider());
   EXPECT_TRUE(SbPlayerIsValid(player));
@@ -91,7 +111,7 @@
         fake_graphics_context_provider_.window(), kSbMediaVideoCodecH264,
         kSbMediaAudioCodecAac, kSbDrmSystemInvalid, &audio_sample_info,
         "" /* max_video_capabilities */, NULL /* deallocate_sample_func */,
-        DummyDecoderStatusFunc, DummyPlayerStatusFunc, DummyErrorFunc,
+        DummyDecoderStatusFunc, DummyStatusFunc, DummyErrorFunc,
         NULL /* context */, output_mode_,
         fake_graphics_context_provider_.decoder_target_provider());
     EXPECT_FALSE(SbPlayerIsValid(player));
@@ -104,7 +124,7 @@
         fake_graphics_context_provider_.window(), kSbMediaVideoCodecH264,
         kSbMediaAudioCodecAac, kSbDrmSystemInvalid, &audio_sample_info,
         "" /* max_video_capabilities */, DummyDeallocateSampleFunc,
-        NULL /* decoder_status_func */, DummyPlayerStatusFunc, DummyErrorFunc,
+        NULL /* decoder_status_func */, DummyStatusFunc, DummyErrorFunc,
         NULL /* context */, output_mode_,
         fake_graphics_context_provider_.decoder_target_provider());
     EXPECT_FALSE(SbPlayerIsValid(player));
@@ -129,9 +149,8 @@
     SbPlayer player = CallSbPlayerCreate(
         fake_graphics_context_provider_.window(), kSbMediaVideoCodecH264,
         kSbMediaAudioCodecAac, kSbDrmSystemInvalid, &audio_sample_info, "",
-        DummyDeallocateSampleFunc, DummyDecoderStatusFunc,
-        DummyPlayerStatusFunc, NULL /* error_func */, NULL /* context */,
-        output_mode_,
+        DummyDeallocateSampleFunc, DummyDecoderStatusFunc, DummyStatusFunc,
+        NULL /* error_func */, NULL /* context */, output_mode_,
         fake_graphics_context_provider_.decoder_target_provider());
     EXPECT_FALSE(SbPlayerIsValid(player));
 
@@ -149,7 +168,7 @@
       fake_graphics_context_provider_.window(), kSbMediaVideoCodecH264,
       kSbMediaAudioCodecNone, kSbDrmSystemInvalid, NULL /* audio_sample_info */,
       "" /* max_video_capabilities */, DummyDeallocateSampleFunc,
-      DummyDecoderStatusFunc, DummyPlayerStatusFunc, DummyErrorFunc,
+      DummyDecoderStatusFunc, DummyStatusFunc, DummyErrorFunc,
       NULL /* context */, output_mode_,
       fake_graphics_context_provider_.decoder_target_provider());
   EXPECT_TRUE(SbPlayerIsValid(player));
@@ -172,7 +191,7 @@
       fake_graphics_context_provider_.window(), kSbMediaVideoCodecNone,
       kSbMediaAudioCodecAac, kSbDrmSystemInvalid, &audio_sample_info,
       "" /* max_video_capabilities */, DummyDeallocateSampleFunc,
-      DummyDecoderStatusFunc, DummyPlayerStatusFunc, DummyErrorFunc,
+      DummyDecoderStatusFunc, DummyStatusFunc, DummyErrorFunc,
       NULL /* context */, output_mode_,
       fake_graphics_context_provider_.decoder_target_provider());
   EXPECT_TRUE(SbPlayerIsValid(player));
@@ -250,7 +269,7 @@
               fake_graphics_context_provider_.window(), kVideoCodecs[l],
               kAudioCodecs[k], kSbDrmSystemInvalid, &audio_sample_info,
               "" /* max_video_capabilities */, DummyDeallocateSampleFunc,
-              DummyDecoderStatusFunc, DummyPlayerStatusFunc, DummyErrorFunc,
+              DummyDecoderStatusFunc, DummyStatusFunc, DummyErrorFunc,
               NULL /* context */, kOutputModes[j],
               fake_graphics_context_provider_.decoder_target_provider()));
           if (!SbPlayerIsValid(created_players.back())) {
diff --git a/starboard/shared/ffmpeg/BUILD.gn b/starboard/shared/ffmpeg/BUILD.gn
index e6bc469..096087b 100644
--- a/starboard/shared/ffmpeg/BUILD.gn
+++ b/starboard/shared/ffmpeg/BUILD.gn
@@ -90,3 +90,20 @@
 
   public_configs = [ "//starboard/build/config:starboard_implementation" ]
 }
+
+target(gtest_target_type, "ffmpeg_demuxer_test") {
+  testonly = true
+  configs += [ "//starboard/build/config:starboard_implementation" ]
+  sources = ffmpeg_specialization_sources + [
+              "ffmpeg_demuxer.h",
+              "ffmpeg_demuxer.cc",
+              "ffmpeg_demuxer_test.cc",
+            ]
+  deps = [
+    "//cobalt/test:run_all_unittests",
+    "//starboard",
+    "//starboard/common",
+    "//testing/gmock",
+    "//testing/gtest",
+  ]
+}
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer.cc b/starboard/shared/ffmpeg/ffmpeg_demuxer.cc
new file mode 100644
index 0000000..38df873
--- /dev/null
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer.cc
@@ -0,0 +1,1054 @@
+// 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/ffmpeg/ffmpeg_demuxer.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <deque>
+#include <functional>
+#include <iostream>
+#include <limits>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "starboard/shared/ffmpeg/ffmpeg_common.h"
+#include "starboard/shared/ffmpeg/ffmpeg_dispatch.h"
+#include "starboard/time.h"
+
+namespace starboard {
+namespace shared {
+namespace ffmpeg {
+
+namespace {
+
+constexpr int64_t kNoFFmpegTimestamp = static_cast<int64_t>(AV_NOPTS_VALUE);
+constexpr int kAvioBufferSize = 4 * 1024;  // 4KB.
+
+// For testing, this can be overridden via TestOnlySetFFmpegDispatch below.
+// Aside from that function and GetDispatch, no code should access this global
+// directly. Instead, calls to the dispatch should go through GetDispatch().
+FFMPEGDispatch* g_test_dispatch = nullptr;
+
+const FFMPEGDispatch* GetDispatch() {
+  if (g_test_dispatch) {
+    return g_test_dispatch;
+  }
+
+  static const auto* const dispatch = FFMPEGDispatch::GetInstance();
+  return dispatch;
+}
+
+struct ScopedPtrAVFreeContext {
+  void operator()(void* ptr) const {
+    if (!ptr) {
+      return;
+    }
+    auto* codec_context = static_cast<AVCodecContext*>(ptr);
+    GetDispatch()->avcodec_free_context(&codec_context);
+  }
+};
+
+struct ScopedPtrAVFree {
+  void operator()(void* ptr) const {
+    if (!ptr) {
+      return;
+    }
+    GetDispatch()->av_free(ptr);
+  }
+};
+
+struct ScopedPtrAVFreePacket {
+  void operator()(void* ptr) const {
+    if (!ptr) {
+      return;
+    }
+    auto* packet = static_cast<AVPacket*>(ptr);
+    GetDispatch()->av_packet_free(&packet);
+  }
+};
+
+using ScopedAVPacket = std::unique_ptr<AVPacket, ScopedPtrAVFreePacket>;
+
+CobaltExtensionDemuxerAudioCodec AvCodecIdToAudioCodec(AVCodecID codec) {
+  switch (codec) {
+    case AV_CODEC_ID_AAC:
+      return kCobaltExtensionDemuxerCodecAAC;
+    case AV_CODEC_ID_MP3:
+      return kCobaltExtensionDemuxerCodecMP3;
+    case AV_CODEC_ID_PCM_U8:
+    case AV_CODEC_ID_PCM_S16LE:
+    case AV_CODEC_ID_PCM_S24LE:
+    case AV_CODEC_ID_PCM_S32LE:
+    case AV_CODEC_ID_PCM_F32LE:
+      return kCobaltExtensionDemuxerCodecPCM;
+    case AV_CODEC_ID_VORBIS:
+      return kCobaltExtensionDemuxerCodecVorbis;
+    case AV_CODEC_ID_FLAC:
+      return kCobaltExtensionDemuxerCodecFLAC;
+    case AV_CODEC_ID_AMR_NB:
+      return kCobaltExtensionDemuxerCodecAMR_NB;
+    case AV_CODEC_ID_AMR_WB:
+      return kCobaltExtensionDemuxerCodecAMR_WB;
+    case AV_CODEC_ID_PCM_MULAW:
+      return kCobaltExtensionDemuxerCodecPCM_MULAW;
+    case AV_CODEC_ID_PCM_S16BE:
+      return kCobaltExtensionDemuxerCodecPCM_S16BE;
+    case AV_CODEC_ID_PCM_S24BE:
+      return kCobaltExtensionDemuxerCodecPCM_S24BE;
+    case AV_CODEC_ID_OPUS:
+      return kCobaltExtensionDemuxerCodecOpus;
+    case AV_CODEC_ID_EAC3:
+      return kCobaltExtensionDemuxerCodecEAC3;
+    case AV_CODEC_ID_PCM_ALAW:
+      return kCobaltExtensionDemuxerCodecPCM_ALAW;
+    case AV_CODEC_ID_ALAC:
+      return kCobaltExtensionDemuxerCodecALAC;
+    case AV_CODEC_ID_AC3:
+      return kCobaltExtensionDemuxerCodecAC3;
+    default:
+      return kCobaltExtensionDemuxerCodecUnknownAudio;
+  }
+}
+
+CobaltExtensionDemuxerVideoCodec AvCodecIdToVideoCodec(AVCodecID codec) {
+  switch (codec) {
+    case AV_CODEC_ID_H264:
+      return kCobaltExtensionDemuxerCodecH264;
+    case AV_CODEC_ID_VC1:
+      return kCobaltExtensionDemuxerCodecVC1;
+    case AV_CODEC_ID_MPEG2VIDEO:
+      return kCobaltExtensionDemuxerCodecMPEG2;
+    case AV_CODEC_ID_MPEG4:
+      return kCobaltExtensionDemuxerCodecMPEG4;
+    case AV_CODEC_ID_THEORA:
+      return kCobaltExtensionDemuxerCodecTheora;
+    case AV_CODEC_ID_VP8:
+      return kCobaltExtensionDemuxerCodecVP8;
+    case AV_CODEC_ID_VP9:
+      return kCobaltExtensionDemuxerCodecVP9;
+    case AV_CODEC_ID_HEVC:
+      return kCobaltExtensionDemuxerCodecHEVC;
+    case AV_CODEC_ID_AV1:
+      return kCobaltExtensionDemuxerCodecAV1;
+    default:
+      return kCobaltExtensionDemuxerCodecUnknownVideo;
+  }
+}
+
+CobaltExtensionDemuxerSampleFormat AvSampleFormatToSampleFormat(
+    AVSampleFormat sample_format) {
+  switch (sample_format) {
+    case AV_SAMPLE_FMT_U8:
+      return kCobaltExtensionDemuxerSampleFormatU8;
+    case AV_SAMPLE_FMT_S16:
+      return kCobaltExtensionDemuxerSampleFormatS16;
+    case AV_SAMPLE_FMT_S32:
+      return kCobaltExtensionDemuxerSampleFormatS32;
+    case AV_SAMPLE_FMT_FLT:
+      return kCobaltExtensionDemuxerSampleFormatF32;
+    case AV_SAMPLE_FMT_S16P:
+      return kCobaltExtensionDemuxerSampleFormatPlanarS16;
+    case AV_SAMPLE_FMT_S32P:
+      return kCobaltExtensionDemuxerSampleFormatPlanarS32;
+    case AV_SAMPLE_FMT_FLTP:
+      return kCobaltExtensionDemuxerSampleFormatPlanarF32;
+    default:
+      return kCobaltExtensionDemuxerSampleFormatUnknown;
+  }
+}
+
+CobaltExtensionDemuxerChannelLayout GuessChannelLayout(int channels) {
+  switch (channels) {
+    case 1:
+      return kCobaltExtensionDemuxerChannelLayoutMono;
+    case 2:
+      return kCobaltExtensionDemuxerChannelLayoutStereo;
+    case 3:
+      return kCobaltExtensionDemuxerChannelLayoutSurround;
+    case 4:
+      return kCobaltExtensionDemuxerChannelLayoutQuad;
+    case 5:
+      return kCobaltExtensionDemuxerChannelLayout5_0;
+    case 6:
+      return kCobaltExtensionDemuxerChannelLayout5_1;
+    case 7:
+      return kCobaltExtensionDemuxerChannelLayout6_1;
+    case 8:
+      return kCobaltExtensionDemuxerChannelLayout7_1;
+    default:
+      std::cerr << "Unsupported channel count: " << channels << std::endl;
+  }
+  return kCobaltExtensionDemuxerChannelLayoutUnsupported;
+}
+
+CobaltExtensionDemuxerChannelLayout AvChannelLayoutToChannelLayout(
+    uint64_t channel_layout,
+    int num_channels) {
+  if (num_channels > 8) {
+    return kCobaltExtensionDemuxerChannelLayoutDiscrete;
+  }
+
+  switch (channel_layout) {
+    case AV_CH_LAYOUT_MONO:
+      return kCobaltExtensionDemuxerChannelLayoutMono;
+    case AV_CH_LAYOUT_STEREO:
+      return kCobaltExtensionDemuxerChannelLayoutStereo;
+    case AV_CH_LAYOUT_2_1:
+      return kCobaltExtensionDemuxerChannelLayout2_1;
+    case AV_CH_LAYOUT_SURROUND:
+      return kCobaltExtensionDemuxerChannelLayoutSurround;
+    case AV_CH_LAYOUT_4POINT0:
+      return kCobaltExtensionDemuxerChannelLayout4_0;
+    case AV_CH_LAYOUT_2_2:
+      return kCobaltExtensionDemuxerChannelLayout2_2;
+    case AV_CH_LAYOUT_QUAD:
+      return kCobaltExtensionDemuxerChannelLayoutQuad;
+    case AV_CH_LAYOUT_5POINT0:
+      return kCobaltExtensionDemuxerChannelLayout5_0;
+    case AV_CH_LAYOUT_5POINT1:
+      return kCobaltExtensionDemuxerChannelLayout5_1;
+    case AV_CH_LAYOUT_5POINT0_BACK:
+      return kCobaltExtensionDemuxerChannelLayout5_0Back;
+    case AV_CH_LAYOUT_5POINT1_BACK:
+      return kCobaltExtensionDemuxerChannelLayout5_1Back;
+    case AV_CH_LAYOUT_7POINT0:
+      return kCobaltExtensionDemuxerChannelLayout7_0;
+    case AV_CH_LAYOUT_7POINT1:
+      return kCobaltExtensionDemuxerChannelLayout7_1;
+    case AV_CH_LAYOUT_7POINT1_WIDE:
+      return kCobaltExtensionDemuxerChannelLayout7_1Wide;
+    case AV_CH_LAYOUT_STEREO_DOWNMIX:
+      return kCobaltExtensionDemuxerChannelLayoutStereoDownmix;
+    case AV_CH_LAYOUT_2POINT1:
+      return kCobaltExtensionDemuxerChannelLayout2point1;
+    case AV_CH_LAYOUT_3POINT1:
+      return kCobaltExtensionDemuxerChannelLayout3_1;
+    case AV_CH_LAYOUT_4POINT1:
+      return kCobaltExtensionDemuxerChannelLayout4_1;
+    case AV_CH_LAYOUT_6POINT0:
+      return kCobaltExtensionDemuxerChannelLayout6_0;
+    case AV_CH_LAYOUT_6POINT0_FRONT:
+      return kCobaltExtensionDemuxerChannelLayout6_0Front;
+    case AV_CH_LAYOUT_HEXAGONAL:
+      return kCobaltExtensionDemuxerChannelLayoutHexagonal;
+    case AV_CH_LAYOUT_6POINT1:
+      return kCobaltExtensionDemuxerChannelLayout6_1;
+    case AV_CH_LAYOUT_6POINT1_BACK:
+      return kCobaltExtensionDemuxerChannelLayout6_1Back;
+    case AV_CH_LAYOUT_6POINT1_FRONT:
+      return kCobaltExtensionDemuxerChannelLayout6_1Front;
+    case AV_CH_LAYOUT_7POINT0_FRONT:
+      return kCobaltExtensionDemuxerChannelLayout7_0Front;
+    case AV_CH_LAYOUT_7POINT1_WIDE_BACK:
+      return kCobaltExtensionDemuxerChannelLayout7_1WideBack;
+    case AV_CH_LAYOUT_OCTAGONAL:
+      return kCobaltExtensionDemuxerChannelLayoutOctagonal;
+    default:
+      return GuessChannelLayout(num_channels);
+  }
+}
+
+CobaltExtensionDemuxerVideoCodecProfile ProfileIDToVideoCodecProfile(
+    int profile) {
+  // Clear out the CONSTRAINED & INTRA flags which are strict subsets of the
+  // corresponding profiles with which they're used.
+  profile &= ~FF_PROFILE_H264_CONSTRAINED;
+  profile &= ~FF_PROFILE_H264_INTRA;
+  switch (profile) {
+    case FF_PROFILE_H264_BASELINE:
+      return kCobaltExtensionDemuxerH264ProfileBaseline;
+    case FF_PROFILE_H264_MAIN:
+      return kCobaltExtensionDemuxerH264ProfileMain;
+    case FF_PROFILE_H264_EXTENDED:
+      return kCobaltExtensionDemuxerH264ProfileExtended;
+    case FF_PROFILE_H264_HIGH:
+      return kCobaltExtensionDemuxerH264ProfileHigh;
+    case FF_PROFILE_H264_HIGH_10:
+      return kCobaltExtensionDemuxerH264ProfileHigh10Profile;
+    case FF_PROFILE_H264_HIGH_422:
+      return kCobaltExtensionDemuxerH264ProfileHigh422Profile;
+    case FF_PROFILE_H264_HIGH_444_PREDICTIVE:
+      return kCobaltExtensionDemuxerH264ProfileHigh444PredictiveProfile;
+    default:
+      std::cerr << "Unknown profile id: " << profile << std::endl;
+      return kCobaltExtensionDemuxerVideoCodecProfileUnknown;
+  }
+}
+
+int AVIOReadOperation(void* opaque, uint8_t* buf, int buf_size) {
+  auto* data_source = static_cast<CobaltExtensionDemuxerDataSource*>(opaque);
+  const int bytes_read =
+      data_source->BlockingRead(buf, buf_size, data_source->user_data);
+
+  if (bytes_read == 0) {
+    return AVERROR_EOF;
+  } else if (bytes_read < 0) {
+    return AVERROR(EIO);
+  } else {
+    return bytes_read;
+  }
+}
+
+int64_t AVIOSeekOperation(void* opaque, int64_t offset, int whence) {
+  auto* data_source = static_cast<CobaltExtensionDemuxerDataSource*>(opaque);
+  switch (whence) {
+    case SEEK_SET: {
+      data_source->SeekTo(offset, data_source->user_data);
+      break;
+    }
+    case SEEK_CUR: {
+      const int64_t current_position =
+          data_source->GetPosition(data_source->user_data);
+      data_source->SeekTo(current_position + offset, data_source->user_data);
+      break;
+    }
+    case SEEK_END: {
+      const int64_t size = data_source->GetSize(data_source->user_data);
+      data_source->SeekTo(size + offset, data_source->user_data);
+      break;
+    }
+    case AVSEEK_SIZE: {
+      return data_source->GetSize(data_source->user_data);
+    }
+    default: {
+      std::cerr << "Invalid whence: " << whence << std::endl;
+      return AVERROR(EIO);
+    }
+  }
+
+  // In the case where we did a real seek, return the new position.
+  return data_source->GetPosition(data_source->user_data);
+}
+
+int64_t ConvertFromTimeBaseToMicros(AVRational time_base, int64_t timestamp) {
+  return GetDispatch()->av_rescale_rnd(timestamp, time_base.num * kSbTimeSecond,
+                                       time_base.den,
+                                       static_cast<int>(AV_ROUND_NEAR_INF));
+}
+
+int64_t ConvertMicrosToTimeBase(AVRational time_base, int64_t timestamp_us) {
+  return GetDispatch()->av_rescale_rnd(timestamp_us, time_base.den,
+                                       time_base.num * kSbTimeSecond,
+                                       static_cast<int>(AV_ROUND_NEAR_INF));
+}
+
+CobaltExtensionDemuxerEncryptionScheme GetEncryptionScheme(
+    const AVStream& stream) {
+  return GetDispatch()->av_dict_get(stream.metadata, "enc_key_id", nullptr,
+                                    0) == nullptr
+             ? kCobaltExtensionDemuxerEncryptionSchemeUnencrypted
+             : kCobaltExtensionDemuxerEncryptionSchemeCenc;
+}
+
+int64_t ExtractStartTime(AVStream* stream) {
+  int64_t start_time = 0;
+  if (stream->start_time != kNoFFmpegTimestamp) {
+    start_time =
+        ConvertFromTimeBaseToMicros(stream->time_base, stream->start_time);
+  }
+
+  if (stream->first_dts != kNoFFmpegTimestamp &&
+      stream->codecpar->codec_id != AV_CODEC_ID_HEVC &&
+      stream->codecpar->codec_id != AV_CODEC_ID_H264 &&
+      stream->codecpar->codec_id != AV_CODEC_ID_MPEG4) {
+    const int64_t first_pts =
+        ConvertFromTimeBaseToMicros(stream->time_base, stream->first_dts);
+    start_time = std::min(first_pts, start_time);
+  }
+
+  return start_time;
+}
+
+// Recursively splits |s| around |delimiter| characters.
+std::vector<std::string> Split(const std::string& s, char delimiter) {
+  // Work from right to left, since it's faster to append to the end of a
+  // vector.
+  const size_t pos = s.rfind(delimiter);
+  if (pos == std::string::npos) {
+    // Base case.
+    return {s};
+  }
+
+  // Recursive case.
+  std::vector<std::string> previous_splits = Split(s.substr(0, pos), delimiter);
+  previous_splits.push_back(s.substr(pos + 1));
+  return previous_splits;
+}
+
+int64_t ExtractTimelineOffset(AVFormatContext* format_context) {
+  const std::vector<std::string> input_formats =
+      Split(format_context->iformat->name, ',');
+
+  // The name for ff_matroska_demuxer contains "webm" in its comma-separated
+  // list.
+  const bool is_webm = std::any_of(
+      input_formats.cbegin(), input_formats.cend(),
+      +[](const std::string& format) -> bool { return format == "webm"; });
+
+  if (is_webm) {
+    const AVDictionaryEntry* entry = GetDispatch()->av_dict_get(
+        format_context->metadata, "creation_time", nullptr, 0);
+
+    // TODO(b/231634260): properly implement this if necessary. We need to
+    // return microseconds since epoch for the given date string in UTC, which
+    // is harder than it sounds in pure C++.
+    return 0;
+  }
+  return 0;
+}
+
+// A demuxer implemented via FFmpeg. Calls to FFmpeg go through GetDispatch()
+// (defined above).
+// The API of this class mirrors that of CobaltExtensionDemuxer; those calls get
+// forwarded to an instance of this class.
+class FFmpegDemuxer {
+ public:
+  explicit FFmpegDemuxer(CobaltExtensionDemuxerDataSource* data_source)
+      : data_source_(data_source) {
+    assert(data_source_);
+  }
+
+  // Disallow copy and assign.
+  FFmpegDemuxer(const FFmpegDemuxer&) = delete;
+  FFmpegDemuxer& operator=(const FFmpegDemuxer&) = delete;
+
+  ~FFmpegDemuxer() {
+    if (format_context_) {
+      GetDispatch()->avformat_close_input(&format_context_);
+    }
+  }
+
+  CobaltExtensionDemuxerStatus Initialize() {
+    assert(format_context_ == nullptr);
+
+    if (initialized_) {
+      std::cerr
+          << "Multiple calls to FFmpegDemuxer::Initialize are not allowed."
+          << std::endl;
+      return kCobaltExtensionDemuxerErrorInitializationFailed;
+    }
+    initialized_ = true;
+
+    avio_context_.reset(GetDispatch()->avio_alloc_context(
+        static_cast<unsigned char*>(GetDispatch()->av_malloc(kAvioBufferSize)),
+        kAvioBufferSize, 0,
+        /*opaque=*/data_source_, &AVIOReadOperation, nullptr,
+        &AVIOSeekOperation));
+    avio_context_->seekable =
+        data_source_->is_streaming ? 0 : AVIO_SEEKABLE_NORMAL;
+    avio_context_->write_flag = 0;
+
+    format_context_ = GetDispatch()->avformat_alloc_context();
+    format_context_->flags |= AVFMT_FLAG_CUSTOM_IO;
+    format_context_->flags |= AVFMT_FLAG_FAST_SEEK;
+    format_context_->flags |= AVFMT_FLAG_KEEP_SIDE_DATA;
+    format_context_->error_recognition |= AV_EF_EXPLODE;
+    format_context_->pb = avio_context_.get();
+
+    if (GetDispatch()->avformat_open_input(&format_context_, nullptr, nullptr,
+                                           nullptr) < 0) {
+      std::cerr << "avformat_open_input failed." << std::endl;
+      return kCobaltExtensionDemuxerErrorCouldNotOpen;
+    }
+    if (GetDispatch()->avformat_find_stream_info(format_context_, nullptr) <
+        0) {
+      std::cerr << "avformat_find_stream_info failed." << std::endl;
+      return kCobaltExtensionDemuxerErrorCouldNotParse;
+    }
+
+    // Find the first audio stream and video stream, if present.
+    // TODO(b/231632632): pick a stream based on supported codecs, not the first
+    // stream present.
+    for (int i = 0; i < format_context_->nb_streams; ++i) {
+      AVStream* stream = format_context_->streams[i];
+      const AVCodecParameters* codec_parameters = stream->codecpar;
+      const AVMediaType codec_type = codec_parameters->codec_type;
+      const AVCodecID codec_id = codec_parameters->codec_id;
+      // Skip streams which are not properly detected.
+      if (codec_id == AV_CODEC_ID_NONE) {
+        stream->discard = AVDISCARD_ALL;
+        continue;
+      }
+
+      if (codec_type == AVMEDIA_TYPE_AUDIO) {
+        if (audio_stream_) {
+          continue;
+        }
+        audio_stream_ = stream;
+      } else if (codec_type == AVMEDIA_TYPE_VIDEO) {
+        if (video_stream_)
+          continue;
+        video_stream_ = stream;
+      }
+    }
+
+    if (!audio_stream_ && !video_stream_) {
+      std::cerr << "No audio or video stream was present." << std::endl;
+      return kCobaltExtensionDemuxerErrorNoSupportedStreams;
+    }
+
+    if (audio_stream_ && !ParseAudioConfig(audio_stream_, &audio_config_)) {
+      return kCobaltExtensionDemuxerErrorInitializationFailed;
+    }
+    if (video_stream_ && !ParseVideoConfig(video_stream_, &video_config_)) {
+      return kCobaltExtensionDemuxerErrorInitializationFailed;
+    }
+
+    if (format_context_->duration != kNoFFmpegTimestamp) {
+      duration_us_ = ConvertFromTimeBaseToMicros(
+          /*time_base=*/{1, AV_TIME_BASE}, format_context_->duration);
+    }
+
+    start_time_ = std::min(audio_stream_ ? ExtractStartTime(audio_stream_)
+                                         : std::numeric_limits<int64_t>::max(),
+                           video_stream_ ? ExtractStartTime(video_stream_)
+                                         : std::numeric_limits<int64_t>::max());
+
+    timeline_offset_us_ = ExtractTimelineOffset(format_context_);
+
+    return kCobaltExtensionDemuxerOk;
+  }
+
+  bool HasAudioStream() const { return audio_stream_ != nullptr; }
+
+  const CobaltExtensionDemuxerAudioDecoderConfig& GetAudioConfig() const {
+    return audio_config_;
+  }
+
+  bool HasVideoStream() const { return video_stream_ != nullptr; }
+
+  const CobaltExtensionDemuxerVideoDecoderConfig& GetVideoConfig() const {
+    return video_config_;
+  }
+
+  SbTime GetDuration() const { return duration_us_; }
+
+  SbTime GetStartTime() const { return start_time_; }
+
+  SbTime GetTimelineOffset() const { return timeline_offset_us_; }
+
+  void Read(CobaltExtensionDemuxerStreamType type,
+            CobaltExtensionDemuxerReadCB read_cb,
+            void* read_cb_user_data) {
+    assert(type == kCobaltExtensionDemuxerStreamTypeAudio ||
+           type == kCobaltExtensionDemuxerStreamTypeVideo);
+
+    if (type == kCobaltExtensionDemuxerStreamTypeAudio) {
+      assert(audio_stream_);
+    } else {
+      assert(video_stream_);
+    }
+
+    const AVRational time_base = type == kCobaltExtensionDemuxerStreamTypeAudio
+                                     ? audio_stream_->time_base
+                                     : video_stream_->time_base;
+
+    CobaltExtensionDemuxerBuffer buffer = {};
+    ScopedAVPacket packet = GetNextPacket(type);
+    if (!packet) {
+      // Either an error occurred or we reached EOS. Treat as EOS.
+      buffer.end_of_stream = true;
+      read_cb(&buffer, read_cb_user_data);
+      return;
+    }
+
+    // NOTE: subtracting start_time_ is necessary because the rest of the cobalt
+    // pipeline never calls the demuxer's GetStartTime() to handle the offset
+    // (it assumes 0 offset).
+    //
+    // TODO(b/231634475): don't subtract start_time_ here if the rest of the
+    // pipeline is updated to handle nonzero start times.
+    buffer.pts =
+        ConvertFromTimeBaseToMicros(time_base, packet->pts) - start_time_;
+    buffer.duration = ConvertFromTimeBaseToMicros(time_base, packet->duration);
+    buffer.is_keyframe = packet->flags & AV_PKT_FLAG_KEY;
+    buffer.end_of_stream = false;
+    buffer.data = packet->data;
+    buffer.data_size = packet->size;
+
+    std::vector<CobaltExtensionDemuxerSideData> side_data;
+    for (int i = 0; i < packet->side_data_elems; ++i) {
+      const AVPacketSideData& packet_side_data = packet->side_data[i];
+      if (packet_side_data.type == AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL) {
+        CobaltExtensionDemuxerSideData extension_side_data = {};
+        extension_side_data.data = packet_side_data.data;
+        extension_side_data.data_size = packet_side_data.size;
+        extension_side_data.type =
+            kCobaltExtensionDemuxerMatroskaBlockAdditional;
+        side_data.push_back(std::move(extension_side_data));
+      }
+      // TODO(b/231635220): support other types of side data, if necessary.
+    }
+
+    if (side_data.empty()) {
+      buffer.side_data = nullptr;
+      buffer.side_data_elements = 0;
+    } else {
+      buffer.side_data = side_data.data();
+      buffer.side_data_elements = side_data.size();
+    }
+
+    read_cb(&buffer, read_cb_user_data);
+  }
+
+  CobaltExtensionDemuxerStatus Seek(int64_t seek_time_us) {
+    // Clear any buffered packets and seek via FFmpeg.
+    video_packets_.clear();
+    audio_packets_.clear();
+
+    AVStream* const stream = video_stream_ ? video_stream_ : audio_stream_;
+    GetDispatch()->av_seek_frame(
+        format_context_, stream->index,
+        ConvertMicrosToTimeBase(stream->time_base, seek_time_us),
+        AVSEEK_FLAG_BACKWARD);
+
+    return kCobaltExtensionDemuxerOk;
+  }
+
+ private:
+  // Returns the next packet of type |type|, or nullptr if EoS has been reached
+  // or an error was encountered.
+  ScopedAVPacket GetNextPacket(CobaltExtensionDemuxerStreamType type) {
+    // Handle the simple case: if we already have a packet buffered, just return
+    // it.
+    ScopedAVPacket packet = GetBufferedPacket(type);
+    if (packet)
+      return packet;
+
+    // Read another packet from FFmpeg. We may have to discard a packet if it's
+    // not from the right stream. Additionally, if we hit end-of-file or an
+    // error, we need to return null.
+    packet.reset(GetDispatch()->av_packet_alloc());
+    while (true) {
+      int result = GetDispatch()->av_read_frame(format_context_, packet.get());
+      if (result < 0) {
+        // The packet will be unref-ed when ScopedAVPacket's destructor runs.
+        return nullptr;
+      }
+
+      // Determine whether to drop the packet. In that case, we need to manually
+      // unref the packet, since new data will be written to it.
+      if (video_stream_ && packet->stream_index == video_stream_->index) {
+        if (type == kCobaltExtensionDemuxerStreamTypeVideo) {
+          // We found the packet that the caller was looking for.
+          return packet;
+        }
+
+        // The caller doesn't need a video packet; just buffer it and allocate a
+        // new packet.
+        BufferPacket(std::move(packet), kCobaltExtensionDemuxerStreamTypeVideo);
+        packet.reset(GetDispatch()->av_packet_alloc());
+        continue;
+      } else if (audio_stream_ &&
+                 packet->stream_index == audio_stream_->index) {
+        if (type == kCobaltExtensionDemuxerStreamTypeAudio) {
+          // We found the packet that the caller was looking for.
+          return packet;
+        }
+
+        // The caller doesn't need an audio packet; just buffer it and allocate
+        // a new packet.
+        BufferPacket(std::move(packet), kCobaltExtensionDemuxerStreamTypeAudio);
+        packet.reset(GetDispatch()->av_packet_alloc());
+        continue;
+      }
+
+      // This is a packet for a stream we don't care about. Unref it and keep
+      // searching.
+      GetDispatch()->av_packet_unref(packet.get());
+    }
+
+    // We should never reach this point.
+    assert(false);
+    return nullptr;
+  }
+
+  // Returns a buffered packet of type |type|, or nullptr if no buffered packet
+  // is available.
+  ScopedAVPacket GetBufferedPacket(CobaltExtensionDemuxerStreamType type) {
+    if (type == kCobaltExtensionDemuxerStreamTypeVideo) {
+      if (video_packets_.empty()) {
+        return nullptr;
+      }
+      ScopedAVPacket packet = std::move(video_packets_.front());
+      video_packets_.pop_front();
+      return packet;
+    } else {
+      if (audio_packets_.empty()) {
+        return nullptr;
+      }
+      ScopedAVPacket packet = std::move(audio_packets_.front());
+      audio_packets_.pop_front();
+      return packet;
+    }
+  }
+
+  // Pushes |packet| into the queue specified by |type|.
+  void BufferPacket(ScopedAVPacket packet,
+                    CobaltExtensionDemuxerStreamType type) {
+    if (type == kCobaltExtensionDemuxerStreamTypeVideo) {
+      video_packets_.push_back(std::move(packet));
+    } else {
+      audio_packets_.push_back(std::move(packet));
+    }
+  }
+
+  bool ParseAudioConfig(AVStream* audio_stream,
+                        CobaltExtensionDemuxerAudioDecoderConfig* config) {
+    if (!config) {
+      return false;
+    }
+
+    config->encryption_scheme = GetEncryptionScheme(*audio_stream);
+
+    std::unique_ptr<AVCodecContext, ScopedPtrAVFreeContext> codec_context(
+        GetDispatch()->avcodec_alloc_context3(nullptr));
+    if (!codec_context) {
+      std::cerr << "Could not allocate codec context." << std::endl;
+      return false;
+    }
+    if (GetDispatch()->avcodec_parameters_to_context(
+            codec_context.get(), audio_stream->codecpar) < 0) {
+      return false;
+    }
+
+    config->codec = AvCodecIdToAudioCodec(codec_context->codec_id);
+    config->sample_format =
+        AvSampleFormatToSampleFormat(codec_context->sample_fmt);
+    config->channel_layout = AvChannelLayoutToChannelLayout(
+        codec_context->channel_layout, codec_context->channels);
+    config->samples_per_second = codec_context->sample_rate;
+
+    // Catch a potential FFmpeg bug. See http://crbug.com/517163 for more info.
+    if ((codec_context->extradata_size == 0) !=
+        (codec_context->extradata == nullptr)) {
+      std::cerr << (codec_context->extradata == nullptr ? " NULL" : " Non-NULL")
+                << " extra data cannot have size of "
+                << codec_context->extradata_size << "." << std::endl;
+      return false;
+    }
+
+    if (codec_context->extradata_size > 0) {
+      extra_audio_data_.assign(
+          codec_context->extradata,
+          codec_context->extradata + codec_context->extradata_size);
+      config->extra_data = extra_audio_data_.data();
+      config->extra_data_size = extra_audio_data_.size();
+    } else {
+      config->extra_data = nullptr;
+      config->extra_data_size = 0;
+    }
+
+    // The spec for AC3/EAC3 audio is ETSI TS 102 366. According to sections
+    // F.3.1 and F.5.1 in that spec, the sample format must be 16 bits.
+    if (config->codec == kCobaltExtensionDemuxerCodecAC3 ||
+        config->codec == kCobaltExtensionDemuxerCodecEAC3) {
+      config->sample_format = kCobaltExtensionDemuxerSampleFormatS16;
+    }
+
+    // TODO(b/231637692): If we need to support MPEG-H, the channel layout and
+    // sample format need to be set here.
+    return true;
+  }
+
+  bool ParseVideoConfig(AVStream* video_stream,
+                        CobaltExtensionDemuxerVideoDecoderConfig* config) {
+    std::unique_ptr<AVCodecContext, ScopedPtrAVFreeContext> codec_context(
+        GetDispatch()->avcodec_alloc_context3(nullptr));
+    if (!codec_context) {
+      std::cerr << "Could not allocate codec context." << std::endl;
+      return false;
+    }
+    if (GetDispatch()->avcodec_parameters_to_context(
+            codec_context.get(), video_stream->codecpar) < 0) {
+      return false;
+    }
+
+    config->visible_rect_x = 0;
+    config->visible_rect_y = 0;
+    config->visible_rect_width = codec_context->width;
+    config->visible_rect_height = codec_context->height;
+
+    config->coded_width = codec_context->width;
+    config->coded_height = codec_context->height;
+
+    auto get_aspect_ratio = +[](AVRational rational) -> double {
+      return rational.den == 0
+                 ? 0.0
+                 : static_cast<double>(rational.num) / rational.den;
+    };
+
+    const double aspect_ratio =
+        video_stream->sample_aspect_ratio.num
+            ? get_aspect_ratio(video_stream->sample_aspect_ratio)
+            : codec_context->sample_aspect_ratio.num
+                  ? get_aspect_ratio(codec_context->sample_aspect_ratio)
+                  : 0.0;
+    {
+      double width = config->visible_rect_width;
+      double height = config->visible_rect_height;
+      if (aspect_ratio >= 1) {
+        // Wide pixels; grow width.
+        width = width * aspect_ratio;
+      } else {
+        // Narrow pixels; grow height.
+        height = height / aspect_ratio;
+      }
+
+      width = std::round(width);
+      height = std::round(height);
+      if (width < 1.0 || width > std::numeric_limits<int>::max() ||
+          height < 1.0 || height > std::numeric_limits<int>::max()) {
+        // Invalid width and height. Just use the visible width and height.
+        config->natural_width = config->visible_rect_width;
+        config->natural_height = config->visible_rect_height;
+      } else {
+        config->natural_width = static_cast<int>(width);
+        config->natural_height = static_cast<int>(height);
+      }
+    }
+    config->codec = AvCodecIdToVideoCodec(codec_context->codec_id);
+
+    // Without the ffmpeg decoder configured, libavformat is unable to get the
+    // profile, format, or coded size. So choose sensible defaults and let
+    // decoders fail later if the configuration is actually unsupported.
+    config->profile = kCobaltExtensionDemuxerVideoCodecProfileUnknown;
+
+    switch (config->codec) {
+      case kCobaltExtensionDemuxerCodecH264: {
+        config->profile = ProfileIDToVideoCodecProfile(codec_context->profile);
+        if (config->profile ==
+                kCobaltExtensionDemuxerVideoCodecProfileUnknown &&
+            codec_context->extradata && codec_context->extradata_size) {
+          // TODO(b/231631898): handle the extra data here, if necessary.
+          std::cerr << "Extra data is not currently handled." << std::endl;
+        }
+        break;
+      }
+      case kCobaltExtensionDemuxerCodecHEVC: {
+        int hevc_profile = FF_PROFILE_UNKNOWN;
+        if ((codec_context->profile < FF_PROFILE_HEVC_MAIN ||
+             codec_context->profile > FF_PROFILE_HEVC_REXT) &&
+            codec_context->extradata && codec_context->extradata_size) {
+          // TODO(b/231631898): handle the extra data here, if necessary.
+          std::cerr << "Extra data is not currently handled." << std::endl;
+        } else {
+          hevc_profile = codec_context->profile;
+        }
+        switch (hevc_profile) {
+          case FF_PROFILE_HEVC_MAIN:
+            config->profile = kCobaltExtensionDemuxerHevcProfileMain;
+            break;
+          case FF_PROFILE_HEVC_MAIN_10:
+            config->profile = kCobaltExtensionDemuxerHevcProfileMain10;
+            break;
+          case FF_PROFILE_HEVC_MAIN_STILL_PICTURE:
+            config->profile =
+                kCobaltExtensionDemuxerHevcProfileMainStillPicture;
+            break;
+          default:
+            // Always assign a default if all heuristics fail.
+            config->profile = kCobaltExtensionDemuxerHevcProfileMain;
+            break;
+        }
+        break;
+      }
+      case kCobaltExtensionDemuxerCodecVP8:
+        config->profile = kCobaltExtensionDemuxerVp8ProfileAny;
+        break;
+      case kCobaltExtensionDemuxerCodecVP9:
+        switch (codec_context->profile) {
+          case FF_PROFILE_VP9_0:
+            config->profile = kCobaltExtensionDemuxerVp9ProfileProfile0;
+            break;
+          case FF_PROFILE_VP9_1:
+            config->profile = kCobaltExtensionDemuxerVp9ProfileProfile1;
+            break;
+          case FF_PROFILE_VP9_2:
+            config->profile = kCobaltExtensionDemuxerVp9ProfileProfile2;
+            break;
+          case FF_PROFILE_VP9_3:
+            config->profile = kCobaltExtensionDemuxerVp9ProfileProfile3;
+            break;
+          default:
+            config->profile = kCobaltExtensionDemuxerVp9ProfileMin;
+            break;
+        }
+        break;
+      case kCobaltExtensionDemuxerCodecAV1:
+        config->profile = kCobaltExtensionDemuxerAv1ProfileProfileMain;
+        break;
+      case kCobaltExtensionDemuxerCodecTheora:
+        config->profile = kCobaltExtensionDemuxerTheoraProfileAny;
+        break;
+      default:
+        config->profile = ProfileIDToVideoCodecProfile(codec_context->profile);
+    }
+
+    config->color_space_primaries = codec_context->color_primaries;
+    config->color_space_transfer = codec_context->color_trc;
+    config->color_space_matrix = codec_context->colorspace;
+    config->color_space_range_id =
+        codec_context->color_range == AVCOL_RANGE_JPEG
+            ? kCobaltExtensionDemuxerColorSpaceRangeIdFull
+            : kCobaltExtensionDemuxerColorSpaceRangeIdLimited;
+
+    // Catch a potential FFmpeg bug.
+    if ((codec_context->extradata_size == 0) !=
+        (codec_context->extradata == nullptr)) {
+      std::cerr << (codec_context->extradata == nullptr ? " NULL" : " Non-NULL")
+                << " extra data cannot have size of "
+                << codec_context->extradata_size << "." << std::endl;
+      return false;
+    }
+
+    if (codec_context->extradata_size > 0) {
+      extra_video_data_.assign(
+          codec_context->extradata,
+          codec_context->extradata + codec_context->extradata_size);
+      config->extra_data = extra_video_data_.data();
+      config->extra_data_size = extra_video_data_.size();
+    } else {
+      config->extra_data = nullptr;
+      config->extra_data_size = 0;
+    }
+
+    config->encryption_scheme = GetEncryptionScheme(*video_stream);
+
+    return true;
+  }
+
+  CobaltExtensionDemuxerDataSource* data_source_ = nullptr;
+  AVStream* video_stream_ = nullptr;
+  AVStream* audio_stream_ = nullptr;
+  std::deque<ScopedAVPacket> video_packets_;
+  std::deque<ScopedAVPacket> audio_packets_;
+
+  std::vector<uint8_t> extra_audio_data_;
+  std::vector<uint8_t> extra_video_data_;
+
+  // These will only be properly populated if the corresponding AVStream is not
+  // null.
+  CobaltExtensionDemuxerAudioDecoderConfig audio_config_ = {};
+  CobaltExtensionDemuxerVideoDecoderConfig video_config_ = {};
+
+  bool initialized_ = false;
+  int64_t start_time_ = 0L;
+  int64_t duration_us_ = 0L;
+  int64_t timeline_offset_us_ = 0L;
+
+  // FFmpeg-related structs.
+  std::unique_ptr<AVIOContext, ScopedPtrAVFree> avio_context_;
+  AVFormatContext* format_context_ = nullptr;
+};
+
+CobaltExtensionDemuxerStatus FFmpegDemuxer_Initialize(void* user_data) {
+  return static_cast<FFmpegDemuxer*>(user_data)->Initialize();
+}
+
+CobaltExtensionDemuxerStatus FFmpegDemuxer_Seek(int64_t seek_time_us,
+                                                void* user_data) {
+  return static_cast<FFmpegDemuxer*>(user_data)->Seek(seek_time_us);
+}
+
+SbTime FFmpegDemuxer_GetStartTime(void* user_data) {
+  return static_cast<FFmpegDemuxer*>(user_data)->GetStartTime();
+}
+
+SbTime FFmpegDemuxer_GetTimelineOffset(void* user_data) {
+  return static_cast<FFmpegDemuxer*>(user_data)->GetTimelineOffset();
+}
+
+void FFmpegDemuxer_Read(CobaltExtensionDemuxerStreamType type,
+                        CobaltExtensionDemuxerReadCB read_cb,
+                        void* read_cb_user_data,
+                        void* user_data) {
+  static_cast<FFmpegDemuxer*>(user_data)->Read(type, read_cb,
+                                               read_cb_user_data);
+}
+
+bool FFmpegDemuxer_GetAudioConfig(
+    CobaltExtensionDemuxerAudioDecoderConfig* config,
+    void* user_data) {
+  auto* ffmpeg_demuxer = static_cast<FFmpegDemuxer*>(user_data);
+  if (!ffmpeg_demuxer->HasAudioStream()) {
+    return false;
+  }
+  *config = ffmpeg_demuxer->GetAudioConfig();
+  return true;
+}
+
+bool FFmpegDemuxer_GetVideoConfig(
+    CobaltExtensionDemuxerVideoDecoderConfig* config,
+    void* user_data) {
+  auto* ffmpeg_demuxer = static_cast<FFmpegDemuxer*>(user_data);
+  if (!ffmpeg_demuxer->HasVideoStream()) {
+    return false;
+  }
+  *config = ffmpeg_demuxer->GetVideoConfig();
+  return true;
+}
+
+SbTime FFmpegDemuxer_GetDuration(void* user_data) {
+  return static_cast<FFmpegDemuxer*>(user_data)->GetDuration();
+}
+
+CobaltExtensionDemuxer* CreateFFmpegDemuxer(
+    CobaltExtensionDemuxerDataSource* data_source,
+    CobaltExtensionDemuxerAudioCodec* supported_audio_codecs,
+    int64_t supported_audio_codecs_size,
+    CobaltExtensionDemuxerVideoCodec* supported_video_codecs,
+    int64_t supported_video_codecs_size) {
+  // TODO(b/231632632): utilize supported_audio_codecs and
+  // supported_video_codecs. They should ultimately be passed to FFmpegDemuxer's
+  // ctor (as vectors), and the demuxer should fail fast for unsupported codecs.
+  return new CobaltExtensionDemuxer{
+      &FFmpegDemuxer_Initialize,     &FFmpegDemuxer_Seek,
+      &FFmpegDemuxer_GetStartTime,   &FFmpegDemuxer_GetTimelineOffset,
+      &FFmpegDemuxer_Read,           &FFmpegDemuxer_GetAudioConfig,
+      &FFmpegDemuxer_GetVideoConfig, &FFmpegDemuxer_GetDuration,
+      new FFmpegDemuxer(data_source)};
+}
+
+void DestroyFFmpegDemuxer(CobaltExtensionDemuxer* demuxer) {
+  auto* ffmpeg_demuxer = static_cast<FFmpegDemuxer*>(demuxer->user_data);
+  delete ffmpeg_demuxer;
+  delete demuxer;
+}
+
+const CobaltExtensionDemuxerApi kDemuxerApi = {
+    /*name=*/kCobaltExtensionDemuxerApi,
+    /*version=*/1,
+    /*CreateDemuxer=*/&CreateFFmpegDemuxer,
+    /*DestroyDemuxer=*/&DestroyFFmpegDemuxer};
+
+}  // namespace
+
+const CobaltExtensionDemuxerApi* GetFFmpegDemuxerApi() {
+  return &kDemuxerApi;
+}
+
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+void TestOnlySetFFmpegDispatch(FFMPEGDispatch* dispatch) {
+  g_test_dispatch = dispatch;
+}
+#endif
+
+}  // namespace ffmpeg
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer.h b/starboard/shared/ffmpeg/ffmpeg_demuxer.h
new file mode 100644
index 0000000..cc90fc6
--- /dev/null
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer.h
@@ -0,0 +1,39 @@
+// 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_FFMPEG_FFMPEG_DEMUXER_H_
+#define STARBOARD_SHARED_FFMPEG_FFMPEG_DEMUXER_H_
+
+#include "cobalt/extension/demuxer.h"
+
+namespace starboard {
+namespace shared {
+namespace ffmpeg {
+
+class FFMPEGDispatch;
+
+// Returns a demuxer implementation based on FFmpeg.
+const CobaltExtensionDemuxerApi* GetFFmpegDemuxerApi();
+
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+// For testing purposes, the FFMPEGDispatch -- through which all FFmpeg calls go
+// -- can be overridden. This should never be called in production code.
+void TestOnlySetFFmpegDispatch(FFMPEGDispatch* dispatch);
+#endif
+
+}  // namespace ffmpeg
+}  // namespace shared
+}  // namespace starboard
+
+#endif  // STARBOARD_SHARED_FFMPEG_FFMPEG_DEMUXER_H_
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc b/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc
new file mode 100644
index 0000000..ae66ec7
--- /dev/null
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_test.cc
@@ -0,0 +1,761 @@
+// 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/ffmpeg/ffmpeg_demuxer.h"
+
+#include <cstdint>
+#include <cstdlib>
+#include <memory>
+#include <tuple>
+#include <vector>
+
+#include "starboard/shared/ffmpeg/ffmpeg_common.h"
+#include "starboard/shared/ffmpeg/ffmpeg_dispatch.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace starboard {
+namespace shared {
+namespace ffmpeg {
+namespace {
+
+using ::testing::_;
+using ::testing::AllArgs;
+using ::testing::ElementsAreArray;
+using ::testing::Eq;
+using ::testing::ExplainMatchResult;
+using ::testing::Invoke;
+using ::testing::IsNull;
+using ::testing::MockFunction;
+using ::testing::NotNull;
+using ::testing::Pointee;
+using ::testing::Pointwise;
+using ::testing::Return;
+using ::testing::SaveArg;
+using ::testing::WithArg;
+
+// Compares two CobaltExtensionDemuxerSideData structs. Deep equality is
+// checked; in other words, the actual side data is inspected (not just the ptr
+// addresses).
+MATCHER_P(SideDataEq, expected, "") {
+  return arg.type == expected.type && arg.data_size == expected.data_size &&
+         ExplainMatchResult(
+             ElementsAreArray(expected.data,
+                              static_cast<size_t>(expected.data_size)),
+             std::tuple<uint8_t*, size_t>{arg.data, arg.data_size},
+             result_listener);
+}
+
+// Compares two CobaltExtensionDemuxerBuffers. Deep equality is checked; the
+// side data and data are checked element-by-element, rather than simply
+// checking ptr addresses.
+MATCHER_P(BufferMatches, expected_buffer, "") {
+  if (expected_buffer.end_of_stream) {
+    // For EoS buffers, we don't care about the other values.
+    return arg.end_of_stream;
+  }
+
+  if (arg.data_size != expected_buffer.data_size) {
+    return false;
+  }
+  if (!ExplainMatchResult(
+          ElementsAreArray(expected_buffer.data,
+                           static_cast<size_t>(expected_buffer.data_size)),
+          std::tuple<uint8_t*, size_t>{arg.data, arg.data_size},
+          result_listener)) {
+    return false;
+  }
+  if (arg.side_data_elements != expected_buffer.side_data_elements) {
+    return false;
+  }
+  // Note: ::testing::Pointwise doesn't support pointer/count as a
+  // representation of an array, so we manually check each side data element.
+  for (int i = 0; i < expected_buffer.side_data_elements; ++i) {
+    if (!ExplainMatchResult(SideDataEq(expected_buffer.side_data[i]),
+                            arg.side_data[i], result_listener)) {
+      return false;
+    }
+  }
+  return (arg.pts == expected_buffer.pts &&
+          arg.duration == expected_buffer.duration &&
+          arg.is_keyframe == expected_buffer.is_keyframe &&
+          arg.end_of_stream == expected_buffer.end_of_stream);
+}
+
+// Streaming is not supported.
+constexpr bool kIsStreaming = false;
+
+// Used to convert a MockFn to a pure C function.
+template <typename T, typename U>
+void CallMockCB(U* u, void* user_data) {
+  static_cast<T*>(user_data)->AsStdFunction()(u);
+}
+
+// A mock class for receiving FFmpeg calls. The API mimics the relevant parts of
+// the real FFmpeg API.
+class MockFFmpegImpl {
+ public:
+  MOCK_METHOD1(AVCodecFreeContext, void(AVCodecContext** avctx));
+  MOCK_METHOD1(AVFree, void(void* ptr));
+  MOCK_METHOD4(AVRescaleRnd, int64_t(int64_t a, int64_t b, int64_t c, int rnd));
+  MOCK_METHOD4(AVDictGet,
+               AVDictionaryEntry*(const AVDictionary* m,
+                                  const char* key,
+                                  const AVDictionaryEntry* prev,
+                                  int flags));
+  MOCK_METHOD4(AVFormatOpenInput,
+               int(AVFormatContext** ps,
+                   const char* filename,
+                   AVInputFormat* fmt,
+                   AVDictionary** options));
+  MOCK_METHOD1(AVFormatCloseInput, void(AVFormatContext** s));
+  MOCK_METHOD7(
+      AVIOAllocContext,
+      AVIOContext*(
+          unsigned char* buffer,
+          int buffer_size,
+          int write_flag,
+          void* opaque,
+          int (*read_packet)(void* opaque, uint8_t* buf, int buf_size),
+          int (*write_packet)(void* opaque, uint8_t* buf, int buf_size),
+          int64_t (*seek)(void* opaque, int64_t offset, int whence)));
+  MOCK_METHOD1(AVMalloc, void*(size_t size));
+  MOCK_METHOD0(AVFormatAllocContext, AVFormatContext*());
+  MOCK_METHOD2(AVFormatFindStreamInfo,
+               int(AVFormatContext* ic, AVDictionary** options));
+  MOCK_METHOD4(
+      AVSeekFrame,
+      int(AVFormatContext* s, int stream_index, int64_t timestamp, int flags));
+  MOCK_METHOD0(AVPacketAlloc, AVPacket*());
+  MOCK_METHOD1(AVPacketFree, void(AVPacket** pkt));
+  MOCK_METHOD1(AVPacketUnref, void(AVPacket* pkt));
+  MOCK_METHOD2(AVReadFrame, int(AVFormatContext* s, AVPacket* pkt));
+  MOCK_METHOD1(AVCodecAllocContext3, AVCodecContext*(const AVCodec* codec));
+  MOCK_METHOD2(AVCodecParametersToContext,
+               int(AVCodecContext* codec, const AVCodecParameters* par));
+};
+
+// Returns a MockFFmpegImpl instance. It should not be deleted by the caller.
+MockFFmpegImpl* GetMockFFmpegImpl() {
+  static auto* const ffmpeg_wrapper = []() {
+    auto* wrapper = new MockFFmpegImpl;
+    // This mock won't be destructed.
+    testing::Mock::AllowLeak(wrapper);
+    return wrapper;
+  }();
+  return ffmpeg_wrapper;
+}
+
+// Pure C functions that call the static mock.
+void mock_avcodec_free_context(AVCodecContext** avctx) {
+  GetMockFFmpegImpl()->AVCodecFreeContext(avctx);
+}
+
+void mock_av_free(void* ptr) {
+  GetMockFFmpegImpl()->AVFree(ptr);
+}
+
+int64_t mock_av_rescale_rnd(int64_t a, int64_t b, int64_t c, int rnd) {
+  return GetMockFFmpegImpl()->AVRescaleRnd(a, b, c, rnd);
+}
+
+AVDictionaryEntry* mock_av_dict_get(const AVDictionary* m,
+                                    const char* key,
+                                    const AVDictionaryEntry* prev,
+                                    int flags) {
+  return GetMockFFmpegImpl()->AVDictGet(m, key, prev, flags);
+}
+
+int mock_avformat_open_input(AVFormatContext** ps,
+                             const char* filename,
+                             AVInputFormat* fmt,
+                             AVDictionary** options) {
+  return GetMockFFmpegImpl()->AVFormatOpenInput(ps, filename, fmt, options);
+}
+
+void mock_avformat_close_input(AVFormatContext** s) {
+  GetMockFFmpegImpl()->AVFormatCloseInput(s);
+}
+
+AVIOContext* mock_avio_alloc_context(
+    unsigned char* buffer,
+    int buffer_size,
+    int write_flag,
+    void* opaque,
+    int (*read_packet)(void* opaque, uint8_t* buf, int buf_size),
+    int (*write_packet)(void* opaque, uint8_t* buf, int buf_size),
+    int64_t (*seek)(void* opaque, int64_t offset, int whence)) {
+  return GetMockFFmpegImpl()->AVIOAllocContext(
+      buffer, buffer_size, write_flag, opaque, read_packet, write_packet, seek);
+}
+
+void* mock_av_malloc(size_t size) {
+  return GetMockFFmpegImpl()->AVMalloc(size);
+}
+
+AVFormatContext* mock_avformat_alloc_context() {
+  return GetMockFFmpegImpl()->AVFormatAllocContext();
+}
+
+int mock_avformat_find_stream_info(AVFormatContext* ic,
+                                   AVDictionary** options) {
+  return GetMockFFmpegImpl()->AVFormatFindStreamInfo(ic, options);
+}
+
+int mock_av_seek_frame(AVFormatContext* s,
+                       int stream_index,
+                       int64_t timestamp,
+                       int flags) {
+  return GetMockFFmpegImpl()->AVSeekFrame(s, stream_index, timestamp, flags);
+}
+
+AVPacket* mock_av_packet_alloc() {
+  return GetMockFFmpegImpl()->AVPacketAlloc();
+}
+
+void mock_av_packet_free(AVPacket** pkt) {
+  GetMockFFmpegImpl()->AVPacketFree(pkt);
+}
+
+void mock_av_packet_unref(AVPacket* pkt) {
+  GetMockFFmpegImpl()->AVPacketUnref(pkt);
+}
+
+int mock_av_read_frame(AVFormatContext* s, AVPacket* pkt) {
+  return GetMockFFmpegImpl()->AVReadFrame(s, pkt);
+}
+
+AVCodecContext* mock_avcodec_alloc_context3(const AVCodec* codec) {
+  return GetMockFFmpegImpl()->AVCodecAllocContext3(codec);
+}
+
+int mock_avcodec_parameters_to_context(AVCodecContext* codec,
+                                       const AVCodecParameters* par) {
+  return GetMockFFmpegImpl()->AVCodecParametersToContext(codec, par);
+}
+
+// Returns an FFMPEGDispatch instance that forwards calls to the mock stored in
+// GetMockFFmpegImpl() above. The returned FFMPEGDispatch should not be
+// deleted; it has static storage duration.
+FFMPEGDispatch* GetFFMPEGDispatch() {
+  static auto* const ffmpeg_dispatch = []() -> FFMPEGDispatch* {
+    auto* dispatch = new FFMPEGDispatch;
+    dispatch->avcodec_free_context = &mock_avcodec_free_context;
+    dispatch->av_free = &mock_av_free;
+    dispatch->av_rescale_rnd = &mock_av_rescale_rnd;
+    dispatch->av_dict_get = &mock_av_dict_get;
+    dispatch->avformat_open_input = &mock_avformat_open_input;
+    dispatch->avformat_close_input = &mock_avformat_close_input;
+    dispatch->avio_alloc_context = &mock_avio_alloc_context;
+    dispatch->av_malloc = &mock_av_malloc;
+    dispatch->avformat_alloc_context = &mock_avformat_alloc_context;
+    dispatch->avformat_find_stream_info = &mock_avformat_find_stream_info;
+    dispatch->av_seek_frame = &mock_av_seek_frame;
+    dispatch->av_packet_alloc = &mock_av_packet_alloc;
+    dispatch->av_packet_free = &mock_av_packet_free;
+    dispatch->av_packet_unref = &mock_av_packet_unref;
+    dispatch->av_read_frame = &mock_av_read_frame;
+    dispatch->avcodec_alloc_context3 = &mock_avcodec_alloc_context3;
+    dispatch->avcodec_parameters_to_context =
+        &mock_avcodec_parameters_to_context;
+    return dispatch;
+  }();
+
+  return ffmpeg_dispatch;
+}
+
+// A mock class representing a data source passed to the cobalt extension
+// demuxer.
+class MockDataSource {
+ public:
+  MOCK_METHOD2(BlockingRead, int(uint8_t* data, int bytes_requested));
+  MOCK_METHOD1(SeekTo, void(int position));
+  MOCK_METHOD0(GetPosition, int64_t());
+  MOCK_METHOD0(GetSize, int64_t());
+};
+
+// These functions forward calls to a MockDataSource.
+int MockBlockingRead(uint8_t* data, int bytes_requested, void* user_data) {
+  return static_cast<MockDataSource*>(user_data)->BlockingRead(data,
+                                                               bytes_requested);
+}
+
+void MockSeekTo(int position, void* user_data) {
+  static_cast<MockDataSource*>(user_data)->SeekTo(position);
+}
+
+int64_t MockGetPosition(void* user_data) {
+  return static_cast<MockDataSource*>(user_data)->GetPosition();
+}
+
+int64_t MockGetSize(void* user_data) {
+  return static_cast<MockDataSource*>(user_data)->GetSize();
+}
+
+// A test fixture is used to ensure that the (static) mock is checked and reset
+// between tests.
+class FFmpegDemuxerTest : public ::testing::Test {
+ public:
+  FFmpegDemuxerTest() { TestOnlySetFFmpegDispatch(GetFFMPEGDispatch()); }
+
+  ~FFmpegDemuxerTest() override {
+    testing::Mock::VerifyAndClearExpectations(GetMockFFmpegImpl());
+  }
+};
+
+TEST_F(FFmpegDemuxerTest, InitializeAllocatesContextAndOpensInput) {
+  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
+
+  AVFormatContext format_context = {};
+  AVInputFormat iformat = {};
+  iformat.name = "mp4";
+  format_context.iformat = &iformat;
+
+  std::vector<AVStream> streams = {AVStream{}};
+  std::vector<AVStream*> stream_ptrs = {&streams[0]};
+  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{}};
+  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
+  stream_params[0].codec_id = AV_CODEC_ID_AAC;
+
+  AVIOContext avio_context = {};
+  AVCodecContext codec_context = {};
+
+  // Sanity checks; if any of these fail, the test has a bug.
+  SB_CHECK(streams.size() == stream_ptrs.size());
+  SB_CHECK(streams.size() == stream_params.size());
+
+  for (int i = 0; i < streams.size(); ++i) {
+    streams[i].codecpar = &stream_params[i];
+    streams[i].time_base.num = 1;
+    streams[i].time_base.den = 1000000;
+    streams[i].start_time = 0;
+  }
+
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
+      .WillOnce(Return(&format_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatCloseInput(Pointee(Eq(&format_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
+      .WillOnce(Return(&avio_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
+      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
+        context->nb_streams = stream_ptrs.size();
+        context->streams = stream_ptrs.data();
+        context->duration = 120 * AV_TIME_BASE;
+        return 0;
+      })));
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
+      .WillOnce(Return(&codec_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecFreeContext(Pointee(Eq(&codec_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecParametersToContext(&codec_context, streams[0].codecpar))
+      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
+        context->codec_id = AV_CODEC_ID_AAC;
+        context->sample_fmt = AV_SAMPLE_FMT_FLT;
+        context->channel_layout = AV_CH_LAYOUT_STEREO;
+        context->channels = 2;
+        context->sample_rate = 44100;
+        return 0;
+      })));
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
+      &MockGetSize,      kIsStreaming, &data_source};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(api, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST_F(FFmpegDemuxerTest, ReadsDataFromDataSource) {
+  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
+  constexpr size_t kReadSize = 5;
+
+  AVFormatContext format_context = {};
+  AVInputFormat iformat = {};
+  iformat.name = "mp4";
+  format_context.iformat = &iformat;
+
+  std::vector<AVStream> streams = {AVStream{}};
+  std::vector<AVStream*> stream_ptrs = {&streams[0]};
+  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{}};
+  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
+  stream_params[0].codec_id = AV_CODEC_ID_AAC;
+
+  AVIOContext avio_context = {};
+  AVCodecContext codec_context = {};
+
+  // Sanity checks; if any of these fail, the test has a bug.
+  SB_CHECK(streams.size() == stream_ptrs.size());
+  SB_CHECK(streams.size() == stream_params.size());
+
+  for (int i = 0; i < streams.size(); ++i) {
+    streams[i].codecpar = &stream_params[i];
+    streams[i].time_base.num = 1;
+    streams[i].time_base.den = 1000000;
+    streams[i].start_time = 0;
+    streams[i].index = i;
+  }
+
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
+      .WillOnce(Return(&format_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatCloseInput(Pointee(Eq(&format_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
+
+  // We will capture the AVIO read operation passed to FFmpeg, so that we can
+  // simulate FFmpeg reading data from the data source.
+  int (*read_packet)(void*, uint8_t*, int) = nullptr;
+  // Data blob that will be passed to read_packet.
+  void* opaque_read_packet = nullptr;
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
+      .WillOnce(DoAll(SaveArg<3>(&opaque_read_packet), SaveArg<4>(&read_packet),
+                      Return(&avio_context)));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
+      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
+        context->nb_streams = stream_ptrs.size();
+        context->streams = stream_ptrs.data();
+        context->duration = 120 * AV_TIME_BASE;
+        return 0;
+      })));
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
+      .WillOnce(Return(&codec_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecFreeContext(Pointee(Eq(&codec_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecParametersToContext(&codec_context, streams[0].codecpar))
+      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
+        context->codec_id = AV_CODEC_ID_AAC;
+        context->sample_fmt = AV_SAMPLE_FMT_FLT;
+        context->channel_layout = AV_CH_LAYOUT_STEREO;
+        context->channels = 2;
+        context->sample_rate = 44100;
+        return 0;
+      })));
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVReadFrame(&format_context, _))
+      .WillOnce(WithArg<1>(Invoke([&opaque_read_packet, &read_packet,
+                                   kReadSize](AVPacket* packet) {
+        SB_CHECK(read_packet != nullptr)
+            << "FFmpeg's read operation should be set via avio_alloc_context "
+               "before av_read_frame is called.";
+        // This will be freed when av_packet_free is called (which eventually
+        // calls AVPacketFree).
+        packet->data =
+            static_cast<uint8_t*>(malloc(kReadSize * sizeof(uint8_t)));
+        packet->size = kReadSize;
+        read_packet(opaque_read_packet, packet->data, kReadSize);
+        return 0;
+      })));
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  ASSERT_THAT(api, NotNull());
+
+  std::vector<uint8_t> expected_data = {0, 1, 2, 3, 4};
+  SB_CHECK(expected_data.size() == kReadSize);
+
+  MockDataSource data_source;
+  EXPECT_CALL(data_source, BlockingRead(_, kReadSize))
+      .WillOnce(WithArg<0>(Invoke([expected_data](uint8_t* buffer) {
+        for (int i = 0; i < expected_data.size(); ++i) {
+          buffer[i] = expected_data[i];
+        }
+        return kReadSize;
+      })));
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
+      &MockGetSize,      kIsStreaming, &data_source};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+
+  const CobaltExtensionDemuxerBuffer expected_buffer = {
+      expected_data.data(),
+      static_cast<int64_t>(expected_data.size()),
+      nullptr,
+      0,
+      0,
+      0,
+      false,
+      false};
+
+  MockFunction<void(CobaltExtensionDemuxerBuffer*)> read_cb;
+  AVPacket av_packet = {};
+
+  // This is the main check: we ensure that the expected buffer is passed to us
+  // via the read callback.
+  EXPECT_CALL(read_cb, Call(Pointee(BufferMatches(expected_buffer)))).Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVPacketAlloc())
+      .WillOnce(Return(&av_packet));
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVPacketFree(Pointee(Eq(&av_packet))))
+      .WillOnce(Invoke([](AVPacket** av_packet) { free((*av_packet)->data); }));
+
+  demuxer->Read(kCobaltExtensionDemuxerStreamTypeAudio,
+                &CallMockCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
+                &read_cb, demuxer->user_data);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST_F(FFmpegDemuxerTest, ReturnsAudioConfig) {
+  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
+
+  AVFormatContext format_context = {};
+  AVInputFormat iformat = {};
+  iformat.name = "mp4";
+  format_context.iformat = &iformat;
+
+  std::vector<AVStream> streams = {AVStream{}};
+  std::vector<AVStream*> stream_ptrs = {&streams[0]};
+  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{}};
+  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
+  stream_params[0].codec_id = AV_CODEC_ID_AAC;
+
+  AVIOContext avio_context = {};
+  AVCodecContext codec_context = {};
+
+  // Sanity checks; if any of these fail, the test has a bug.
+  SB_CHECK(streams.size() == stream_ptrs.size());
+  SB_CHECK(streams.size() == stream_params.size());
+
+  for (int i = 0; i < streams.size(); ++i) {
+    streams[i].codecpar = &stream_params[i];
+    streams[i].time_base.num = 1;
+    streams[i].time_base.den = 1000000;
+    streams[i].start_time = 0;
+  }
+
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
+      .WillOnce(Return(&format_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatCloseInput(Pointee(Eq(&format_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
+      .WillOnce(Return(&avio_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
+      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
+        context->nb_streams = stream_ptrs.size();
+        context->streams = stream_ptrs.data();
+        context->duration = 120 * AV_TIME_BASE;
+        return 0;
+      })));
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
+      .WillOnce(Return(&codec_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecFreeContext(Pointee(Eq(&codec_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecParametersToContext(&codec_context, streams[0].codecpar))
+      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
+        context->codec_id = AV_CODEC_ID_AAC;
+        context->sample_fmt = AV_SAMPLE_FMT_FLT;
+        context->channel_layout = AV_CH_LAYOUT_STEREO;
+        context->channels = 2;
+        context->sample_rate = 44100;
+        return 0;
+      })));
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
+      &MockGetSize,      kIsStreaming, &data_source};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(api, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+
+  CobaltExtensionDemuxerAudioDecoderConfig actual_audio_config = {};
+  demuxer->GetAudioConfig(&actual_audio_config, demuxer->user_data);
+
+  // These values are derived from those set via AVCodecParametersToContext.
+  EXPECT_EQ(actual_audio_config.codec, kCobaltExtensionDemuxerCodecAAC);
+  EXPECT_EQ(actual_audio_config.sample_format,
+            kCobaltExtensionDemuxerSampleFormatF32);
+  EXPECT_EQ(actual_audio_config.channel_layout,
+            kCobaltExtensionDemuxerChannelLayoutStereo);
+  EXPECT_EQ(actual_audio_config.encryption_scheme,
+            kCobaltExtensionDemuxerEncryptionSchemeUnencrypted);
+  EXPECT_EQ(actual_audio_config.samples_per_second, 44100);
+  EXPECT_THAT(actual_audio_config.extra_data, IsNull());
+  EXPECT_EQ(actual_audio_config.extra_data_size, 0);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+TEST_F(FFmpegDemuxerTest, ReturnsVideoConfig) {
+  auto* const mock_ffmpeg_wrapper = GetMockFFmpegImpl();
+
+  AVFormatContext format_context = {};
+  AVInputFormat iformat = {};
+  iformat.name = "mp4";
+  format_context.iformat = &iformat;
+
+  // In this test we simulate both an audio stream and a video stream being
+  // present.
+  std::vector<AVStream> streams = {AVStream{}, AVStream{}};
+  std::vector<AVStream*> stream_ptrs = {&streams[0], &streams[1]};
+  std::vector<AVCodecParameters> stream_params = {AVCodecParameters{},
+                                                  AVCodecParameters{}};
+  stream_params[0].codec_type = AVMEDIA_TYPE_AUDIO;
+  stream_params[0].codec_id = AV_CODEC_ID_AAC;
+  stream_params[1].codec_type = AVMEDIA_TYPE_VIDEO;
+  stream_params[1].codec_id = AV_CODEC_ID_H264;
+
+  AVIOContext avio_context = {};
+  AVCodecContext codec_context_1 = {};
+  AVCodecContext codec_context_2 = {};
+
+  // Sanity checks; if any of these fail, the test has a bug.
+  SB_CHECK(streams.size() == stream_ptrs.size());
+  SB_CHECK(streams.size() == stream_params.size());
+
+  for (int i = 0; i < streams.size(); ++i) {
+    streams[i].codecpar = &stream_params[i];
+    streams[i].time_base.num = 1;
+    streams[i].time_base.den = 1000000;
+    streams[i].start_time = 0;
+  }
+
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatAllocContext())
+      .WillOnce(Return(&format_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatCloseInput(Pointee(Eq(&format_context))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVMalloc(_)).Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVIOAllocContext(_, _, _, _, _, _, _))
+      .WillOnce(Return(&avio_context));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVFormatOpenInput(Pointee(Eq(&format_context)), _, _, _))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVFormatFindStreamInfo(&format_context, _))
+      .WillOnce(WithArg<0>(Invoke([&stream_ptrs](AVFormatContext* context) {
+        context->nb_streams = stream_ptrs.size();
+        context->streams = stream_ptrs.data();
+        context->duration = 120 * AV_TIME_BASE;
+        return 0;
+      })));
+  EXPECT_CALL(*mock_ffmpeg_wrapper, AVCodecAllocContext3(_))
+      .WillOnce(Return(&codec_context_1))
+      .WillOnce(Return(&codec_context_2));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecFreeContext(Pointee(Eq(&codec_context_1))))
+      .Times(1);
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecFreeContext(Pointee(Eq(&codec_context_2))))
+      .Times(1);
+
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecParametersToContext(_, streams[0].codecpar))
+      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
+        context->codec_id = AV_CODEC_ID_AAC;
+        context->sample_fmt = AV_SAMPLE_FMT_FLT;
+        context->channel_layout = AV_CH_LAYOUT_STEREO;
+        context->channels = 2;
+        context->sample_rate = 44100;
+        return 0;
+      })));
+  EXPECT_CALL(*mock_ffmpeg_wrapper,
+              AVCodecParametersToContext(_, streams[1].codecpar))
+      .WillOnce(WithArg<0>(Invoke([](AVCodecContext* context) {
+        context->codec_id = AV_CODEC_ID_H264;
+        context->width = 1920;
+        context->height = 1080;
+        context->sample_aspect_ratio.num = 1;
+        context->sample_aspect_ratio.den = 1;
+        context->profile = FF_PROFILE_H264_BASELINE;
+        context->pix_fmt = AV_PIX_FMT_YUVJ420P;
+        context->colorspace = AVCOL_SPC_BT709;
+        context->color_range = AVCOL_RANGE_MPEG;
+        return 0;
+      })));
+
+  const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
+  MockDataSource data_source;
+  CobaltExtensionDemuxerDataSource c_data_source{
+      &MockBlockingRead, &MockSeekTo,  &MockGetPosition,
+      &MockGetSize,      kIsStreaming, &data_source};
+  std::vector<CobaltExtensionDemuxerAudioCodec> supported_audio_codecs;
+  std::vector<CobaltExtensionDemuxerVideoCodec> supported_video_codecs;
+
+  CobaltExtensionDemuxer* demuxer = api->CreateDemuxer(
+      &c_data_source, supported_audio_codecs.data(),
+      supported_audio_codecs.size(), supported_video_codecs.data(),
+      supported_video_codecs.size());
+
+  ASSERT_THAT(api, NotNull());
+  EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
+
+  CobaltExtensionDemuxerVideoDecoderConfig actual_video_config = {};
+  demuxer->GetVideoConfig(&actual_video_config, demuxer->user_data);
+
+  // These values are derived from those set via AVCodecParametersToContext.
+  EXPECT_EQ(actual_video_config.codec, kCobaltExtensionDemuxerCodecH264);
+  EXPECT_EQ(actual_video_config.profile,
+            kCobaltExtensionDemuxerH264ProfileBaseline);
+  EXPECT_EQ(actual_video_config.coded_width, 1920);
+  EXPECT_EQ(actual_video_config.coded_height, 1080);
+  EXPECT_EQ(actual_video_config.visible_rect_x, 0);
+  EXPECT_EQ(actual_video_config.visible_rect_y, 0);
+  EXPECT_EQ(actual_video_config.visible_rect_width, 1920);
+  EXPECT_EQ(actual_video_config.visible_rect_height, 1080);
+  EXPECT_EQ(actual_video_config.natural_width, 1920);
+  EXPECT_EQ(actual_video_config.natural_height, 1080);
+  EXPECT_THAT(actual_video_config.extra_data, IsNull());
+  EXPECT_EQ(actual_video_config.extra_data_size, 0);
+
+  api->DestroyDemuxer(demuxer);
+}
+
+}  // namespace
+}  // namespace ffmpeg
+}  // namespace shared
+}  // namespace starboard
diff --git a/starboard/shared/ffmpeg/ffmpeg_dispatch.h b/starboard/shared/ffmpeg/ffmpeg_dispatch.h
index 4d70b66..1b46ff6 100644
--- a/starboard/shared/ffmpeg/ffmpeg_dispatch.h
+++ b/starboard/shared/ffmpeg/ffmpeg_dispatch.h
@@ -23,8 +23,13 @@
 
 struct AVCodec;
 struct AVCodecContext;
+struct AVCodecParameters;
 struct AVDictionary;
+struct AVDictionaryEntry;
+struct AVFormatContext;
 struct AVFrame;
+struct AVInputFormat;
+struct AVIOContext;
 struct AVPacket;
 
 namespace starboard {
@@ -99,12 +104,46 @@
   AVFrame* (*avcodec_alloc_frame)(void);
   void (*avcodec_get_frame_defaults)(AVFrame* frame);
   void (*avcodec_align_dimensions2)(AVCodecContext* avctx,
-                                    int* width, int* height,
+                                    int* width,
+                                    int* height,
                                     int linesize_align[]);
 
   unsigned (*avformat_version)(void);
   void (*av_register_all)(void);
 
+  void (*av_free)(void* ptr);
+  AVPacket* (*av_packet_alloc)(void);
+  void (*av_packet_free)(AVPacket** pkt);
+  AVDictionaryEntry* (*av_dict_get)(const AVDictionary* m,
+                                    const char* key,
+                                    const AVDictionaryEntry* prev,
+                                    int flags);
+  // Note: |rnd| represents type enum AVRounding.
+  int64_t (*av_rescale_rnd)(int64_t a, int64_t b, int64_t c, int rnd);
+  int (*av_seek_frame)(AVFormatContext* s,
+                       int stream_index,
+                       int64_t timestamp,
+                       int flags);
+  int (*av_read_frame)(AVFormatContext* s, AVPacket* pkt);
+  void (*av_packet_unref)(AVPacket* pkt);
+  int (*avformat_open_input)(AVFormatContext** ps,
+                             const char* filename,
+                             AVInputFormat* fmt,
+                             AVDictionary** options);
+  void (*avformat_close_input)(AVFormatContext** s);
+  AVFormatContext* (*avformat_alloc_context)(void);
+  int (*avformat_find_stream_info)(AVFormatContext* ic, AVDictionary** options);
+  AVIOContext* (*avio_alloc_context)(
+      unsigned char* buffer,
+      int buffer_size,
+      int write_flag,
+      void* opaque,
+      int (*read_packet)(void* opaque, uint8_t* buf, int buf_size),
+      int (*write_packet)(void* opaque, uint8_t* buf, int buf_size),
+      int64_t (*seek)(void* opaque, int64_t offset, int whence));
+  int (*avcodec_parameters_to_context)(AVCodecContext* codec,
+                                       const AVCodecParameters* par);
+
   int specialization_version() const;
 
   // In Ffmpeg, the calls to avcodec_open2() and avcodec_close() are not
diff --git a/starboard/shared/ffmpeg/ffmpeg_dynamic_load_dispatch_impl.cc b/starboard/shared/ffmpeg/ffmpeg_dynamic_load_dispatch_impl.cc
index 4f8aec4..748d897 100644
--- a/starboard/shared/ffmpeg/ffmpeg_dynamic_load_dispatch_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_dynamic_load_dispatch_impl.cc
@@ -261,6 +261,9 @@
   INITSYMBOL(avutil_, av_malloc);
   INITSYMBOL(avutil_, av_freep);
   INITSYMBOL(avutil_, av_frame_alloc);
+  INITSYMBOL(avutil_, av_free);
+  INITSYMBOL(avutil_, av_dict_get);
+  INITSYMBOL(avutil_, av_rescale_rnd);
 #if LIBAVUTIL_VERSION_INT >= LIBAVUTIL_VERSION_52_8
   INITSYMBOL(avutil_, av_frame_free);
 #endif  // LIBAVUTIL_VERSION_INT >= LIBAVUTIL_VERSION_52_8
@@ -287,12 +290,23 @@
   INITSYMBOL(avcodec_, avcodec_alloc_frame);
   INITSYMBOL(avcodec_, avcodec_get_frame_defaults);
   INITSYMBOL(avcodec_, avcodec_align_dimensions2);
+  INITSYMBOL(avcodec_, av_packet_alloc);
+  INITSYMBOL(avcodec_, av_packet_free);
+  INITSYMBOL(avcodec_, av_packet_unref);
+  INITSYMBOL(avcodec_, avcodec_parameters_to_context);
 
   // Load symbols from the avformat shared library.
   INITSYMBOL(avformat_, avformat_version);
   SB_DCHECK(ffmpeg_->avformat_version);
   INITSYMBOL(avformat_, av_register_all);
   SB_DCHECK(ffmpeg_->av_register_all);
+  INITSYMBOL(avformat_, av_read_frame);
+  INITSYMBOL(avformat_, av_seek_frame);
+  INITSYMBOL(avformat_, avformat_open_input);
+  INITSYMBOL(avformat_, avformat_close_input);
+  INITSYMBOL(avformat_, avformat_alloc_context);
+  INITSYMBOL(avformat_, avformat_find_stream_info);
+  INITSYMBOL(avformat_, avio_alloc_context);
 
 #undef INITSYMBOL
 }