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
}