blob: 927adcbfcd75c3bb638d3b83dbfac982bcae764e [file] [log] [blame]
// 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() {
FFmpegDemuxer::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