blob: 392e3057dece9466f7e9bb374ac0a4ad72b34f2a [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 <sys/stat.h>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
#include "starboard/configuration_constants.h"
#include "starboard/log.h"
#include "starboard/system.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::AtLeast;
using ::testing::ElementsAreArray;
using ::testing::ExplainMatchResult;
using ::testing::Ge;
using ::testing::Invoke;
using ::testing::IsNull;
using ::testing::MockFunction;
using ::testing::NotNull;
using ::testing::SizeIs;
// Streaming is not supported.
constexpr bool kIsStreaming = false;
constexpr char kInputWebm[] = "bear-320x240.webm";
std::string ResolveTestFilename(const char* filename) {
std::vector<char> content_path(kSbFileMaxPath);
SB_CHECK(SbSystemGetPath(kSbSystemPathContentDirectory, content_path.data(),
kSbFileMaxPath));
const std::string directory_path =
std::string(content_path.data()) + kSbFileSepChar + "third_party" +
kSbFileSepChar + "chromium" + kSbFileSepChar + "media" + kSbFileSepChar +
"test" + kSbFileSepChar + "data";
struct stat info;
SB_CHECK(stat(directory_path.c_str(), &info) == 0 && S_ISDIR(info.st_mode))
<< "Cannot open directory " << directory_path;
return directory_path + kSbFileSepChar + filename;
}
// 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);
}
// These functions forward calls to a FstreamDataSource.
int FstreamBlockingRead(uint8_t* data, int bytes_requested, void* user_data) {
auto* input = static_cast<std::ifstream*>(user_data);
input->read(reinterpret_cast<char*>(data), bytes_requested);
return input->gcount();
}
void FstreamSeekTo(int position, void* user_data) {
auto* input = static_cast<std::ifstream*>(user_data);
input->seekg(position, input->beg);
}
int64_t FstreamGetPosition(void* user_data) {
return static_cast<std::ifstream*>(user_data)->tellg();
}
int64_t FstreamGetSize(void* user_data) {
// Seek to the end of the file to get the size, then seek back to the current
// position.
auto* input = static_cast<std::ifstream*>(user_data);
const auto current_pos = input->tellg();
input->seekg(0, input->end);
const int64_t filesize = input->tellg();
input->seekg(current_pos);
return filesize;
}
TEST(FFmpegDemuxerTest, InitializeSucceeds) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, ReadsAudioData) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
const std::vector<uint8_t> expected_audio = {
0xbc, 0xd6, 0xf2, 0xda, 0x7b, 0xad, 0xe5, 0xb5,
0x87, 0x29, 0x05, 0x50, 0x6b, 0xfc, 0x6f, 0xd6,
};
MockFunction<void(CobaltExtensionDemuxerBuffer*)> read_cb;
std::vector<uint8_t> actual_audio;
bool read_eos = false;
EXPECT_CALL(read_cb, Call(_))
.Times(AtLeast(1))
.WillRepeatedly(Invoke([&](CobaltExtensionDemuxerBuffer* buffer) {
SB_CHECK(buffer);
if (buffer->end_of_stream || buffer->data_size <= 0) {
read_eos = buffer->end_of_stream;
return;
}
actual_audio.insert(actual_audio.end(), buffer->data,
buffer->data + buffer->data_size);
}));
// Calling Read may return less -- or more -- data than we need. Keep reading
// until we have at least as much data as the amount we're checking.
while (!read_eos && actual_audio.size() < expected_audio.size()) {
demuxer->Read(kCobaltExtensionDemuxerStreamTypeAudio,
&CallMockCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
&read_cb, demuxer->user_data);
}
ASSERT_THAT(actual_audio, SizeIs(Ge(expected_audio.size())));
actual_audio.resize(expected_audio.size());
EXPECT_THAT(actual_audio, ElementsAreArray(expected_audio));
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, ReadsVideoData) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
const std::vector<uint8_t> expected_video = {
0x50, 0x04, 0x01, 0x9d, 0x01, 0x2a, 0x40, 0x01,
0xf0, 0x00, 0x00, 0x47, 0x08, 0x85, 0x85, 0x88,
};
MockFunction<void(CobaltExtensionDemuxerBuffer*)> read_cb;
std::vector<uint8_t> actual_video;
bool read_eos = false;
EXPECT_CALL(read_cb, Call(_))
.Times(AtLeast(1))
.WillRepeatedly(Invoke([&](CobaltExtensionDemuxerBuffer* buffer) {
SB_CHECK(buffer);
if (buffer->end_of_stream || buffer->data_size <= 0) {
read_eos = buffer->end_of_stream;
return;
}
actual_video.insert(actual_video.end(), buffer->data,
buffer->data + buffer->data_size);
}));
// Calling Read may return less -- or more -- data than we need. Keep reading
// until we have at least as much data as the amount we're checking.
while (!read_eos && actual_video.size() < expected_video.size()) {
demuxer->Read(kCobaltExtensionDemuxerStreamTypeVideo,
&CallMockCB<decltype(read_cb), CobaltExtensionDemuxerBuffer>,
&read_cb, demuxer->user_data);
}
ASSERT_THAT(actual_video, SizeIs(Ge(expected_video.size())));
actual_video.resize(expected_video.size());
EXPECT_THAT(actual_video, ElementsAreArray(expected_video));
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, PopulatesAudioConfig) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
CobaltExtensionDemuxerAudioDecoderConfig actual_config = {};
demuxer->GetAudioConfig(&actual_config, demuxer->user_data);
EXPECT_EQ(actual_config.codec, kCobaltExtensionDemuxerCodecVorbis);
EXPECT_EQ(actual_config.sample_format,
kCobaltExtensionDemuxerSampleFormatPlanarF32);
EXPECT_EQ(actual_config.channel_layout,
kCobaltExtensionDemuxerChannelLayoutStereo);
EXPECT_EQ(actual_config.encryption_scheme,
kCobaltExtensionDemuxerEncryptionSchemeUnencrypted);
EXPECT_EQ(actual_config.samples_per_second, 44100);
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, PopulatesVideoConfig) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
CobaltExtensionDemuxerVideoDecoderConfig actual_config = {};
demuxer->GetVideoConfig(&actual_config, demuxer->user_data);
EXPECT_EQ(actual_config.codec, kCobaltExtensionDemuxerCodecVP8);
EXPECT_EQ(actual_config.profile, kCobaltExtensionDemuxerVp8ProfileMin);
EXPECT_EQ(actual_config.color_space_primaries, 2);
EXPECT_EQ(actual_config.color_space_transfer, 2);
EXPECT_EQ(actual_config.color_space_matrix, 2);
EXPECT_EQ(actual_config.color_space_range_id,
kCobaltExtensionDemuxerColorSpaceRangeIdLimited);
EXPECT_EQ(actual_config.alpha_mode, kCobaltExtensionDemuxerHasAlpha);
EXPECT_EQ(actual_config.coded_width, 320);
EXPECT_EQ(actual_config.coded_height, 240);
EXPECT_EQ(actual_config.visible_rect_x, 0);
EXPECT_EQ(actual_config.visible_rect_y, 0);
EXPECT_EQ(actual_config.visible_rect_width, 320);
EXPECT_EQ(actual_config.visible_rect_height, 240);
EXPECT_EQ(actual_config.natural_width, 320);
EXPECT_EQ(actual_config.natural_height, 240);
EXPECT_EQ(actual_config.encryption_scheme,
kCobaltExtensionDemuxerEncryptionSchemeUnencrypted);
EXPECT_THAT(actual_config.extra_data, IsNull());
EXPECT_EQ(actual_config.extra_data_size, 0);
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, ReturnsVideoDuration) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
EXPECT_EQ(demuxer->GetDuration(demuxer->user_data), 2744000);
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, TimelineOffsetReturnsZero) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
EXPECT_EQ(demuxer->GetTimelineOffset(demuxer->user_data), 0);
api->DestroyDemuxer(demuxer);
}
TEST(FFmpegDemuxerTest, StartTimeReturnsZero) {
const std::string filename = ResolveTestFilename(kInputWebm);
std::ifstream input(filename, std::ios_base::binary);
SB_CHECK(input.good()) << "Unable to load file " << filename;
const CobaltExtensionDemuxerApi* api = GetFFmpegDemuxerApi();
ASSERT_THAT(api, NotNull());
CobaltExtensionDemuxerDataSource c_data_source{
&FstreamBlockingRead, &FstreamSeekTo, &FstreamGetPosition,
&FstreamGetSize, kIsStreaming, &input};
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(demuxer, NotNull());
EXPECT_EQ(demuxer->Initialize(demuxer->user_data), kCobaltExtensionDemuxerOk);
EXPECT_EQ(demuxer->GetStartTime(demuxer->user_data), 0);
api->DestroyDemuxer(demuxer);
}
} // namespace
} // namespace ffmpeg
} // namespace shared
} // namespace starboard