864 lines
26 KiB
C++
864 lines
26 KiB
C++
/*
|
|
* Oak Video Editor - ffmpeg_bridge C API Tests
|
|
* Copyright (C) 2025 Olive CE Team
|
|
*
|
|
* Direct tests of the pure C ffmpeg_bridge API. These tests exercise the
|
|
* bridge in isolation from the editor: handles are created, used and freed
|
|
* entirely through the fb_* functions, mirroring how the C++ adapter layer
|
|
* drives the library at runtime.
|
|
*/
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
|
|
#include <ffmpeg_bridge/ffmpeg_bridge.h>
|
|
|
|
#include "olive/core/render/channellayout.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
QString DemoPath()
|
|
{
|
|
return QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
|
|
.filePath(QStringLiteral("tests/demo.mp4"));
|
|
}
|
|
|
|
QString TempFilePath(const QString &name)
|
|
{
|
|
return QDir::temp().filePath(name);
|
|
}
|
|
|
|
constexpr double kPi = 3.14159265358979323846;
|
|
|
|
// Finds the first stream of `type`; returns its index or -1.
|
|
int FindStream(FBProbe *probe, int type)
|
|
{
|
|
const int count = fb_probe_get_stream_count(probe);
|
|
for (int i = 0; i < count; ++i) {
|
|
FBStreamInfo info;
|
|
if (fb_probe_get_stream_info(probe, i, &info) == 0 &&
|
|
info.codec_type == type) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ============================================================================
|
|
// Constants
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeConstants, ChannelLayoutsMatchCore)
|
|
{
|
|
// The core library and the bridge must agree on layout masks or all
|
|
// audio plumbing between them breaks.
|
|
EXPECT_EQ(FB_CH_LAYOUT_MONO, olive::core::kChannelLayoutMono);
|
|
EXPECT_EQ(FB_CH_LAYOUT_STEREO, olive::core::kChannelLayoutStereo);
|
|
EXPECT_EQ(FB_CH_LAYOUT_2_1, olive::core::kChannelLayout2_1);
|
|
EXPECT_EQ(FB_CH_LAYOUT_5POINT1, olive::core::kChannelLayout5Point1);
|
|
EXPECT_EQ(FB_CH_LAYOUT_7POINT1, olive::core::kChannelLayout7Point1);
|
|
}
|
|
|
|
TEST(FFmpegBridgeConstants, SpecialValues)
|
|
{
|
|
EXPECT_EQ(FB_NOPTS_VALUE, INT64_MIN);
|
|
EXPECT_EQ(FB_TIME_BASE, 1000000);
|
|
EXPECT_LT(FB_ERROR_EOF, 0);
|
|
EXPECT_EQ(FB_PIX_FMT_NONE, -1);
|
|
EXPECT_EQ(FB_SAMPLE_FMT_NONE, -1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Error strings / version
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeError, ErrorString)
|
|
{
|
|
char buffer[256];
|
|
fb_error_string(FB_ERROR_EOF, buffer, sizeof(buffer));
|
|
EXPECT_GT(std::strlen(buffer), 0u);
|
|
|
|
fb_error_string(0, buffer, sizeof(buffer));
|
|
EXPECT_GT(std::strlen(buffer), 0u);
|
|
}
|
|
|
|
TEST(FFmpegBridgeError, VersionString)
|
|
{
|
|
const char *version = fb_version_string();
|
|
ASSERT_NE(version, nullptr);
|
|
EXPECT_GT(std::strlen(version), 0u);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pixel/sample format utilities
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgePixFmt, NameRoundtrip)
|
|
{
|
|
const char *name = fb_pix_fmt_name(FB_PIX_FMT_YUV420P);
|
|
ASSERT_NE(name, nullptr);
|
|
EXPECT_STREQ(name, "yuv420p");
|
|
EXPECT_EQ(fb_pix_fmt_from_name(name), FB_PIX_FMT_YUV420P);
|
|
|
|
EXPECT_STREQ(fb_pix_fmt_name(FB_PIX_FMT_RGBA), "rgba");
|
|
EXPECT_EQ(fb_pix_fmt_from_name("rgba"), FB_PIX_FMT_RGBA);
|
|
}
|
|
|
|
TEST(FFmpegBridgePixFmt, Properties)
|
|
{
|
|
EXPECT_EQ(fb_pix_fmt_bits_per_pixel(FB_PIX_FMT_RGBA), 32);
|
|
EXPECT_EQ(fb_pix_fmt_bits_per_pixel(FB_PIX_FMT_YUV420P), 12);
|
|
EXPECT_EQ(fb_pix_fmt_bits_per_pixel(FB_PIX_FMT_RGBA64LE), 64);
|
|
|
|
EXPECT_EQ(fb_pix_fmt_has_alpha(FB_PIX_FMT_RGBA), 1);
|
|
EXPECT_EQ(fb_pix_fmt_has_alpha(FB_PIX_FMT_YUV420P), 0);
|
|
|
|
EXPECT_EQ(fb_pix_fmt_is_planar(FB_PIX_FMT_YUV420P), 1);
|
|
EXPECT_EQ(fb_pix_fmt_is_planar(FB_PIX_FMT_RGBA), 0);
|
|
|
|
EXPECT_EQ(fb_pix_fmt_component_size(FB_PIX_FMT_RGBA), 1);
|
|
EXPECT_EQ(fb_pix_fmt_component_size(FB_PIX_FMT_RGBA64LE), 2);
|
|
}
|
|
|
|
TEST(FFmpegBridgePixFmt, FindBestOfList)
|
|
{
|
|
const int list[] = { FB_PIX_FMT_YUV420P, FB_PIX_FMT_YUV444P,
|
|
FB_PIX_FMT_NONE };
|
|
EXPECT_EQ(fb_find_best_pix_fmt_of_list(list, FB_PIX_FMT_YUV420P),
|
|
FB_PIX_FMT_YUV420P);
|
|
}
|
|
|
|
TEST(FFmpegBridgeChannelLayout, ChannelCounts)
|
|
{
|
|
EXPECT_EQ(fb_channel_layout_get_channels(FB_CH_LAYOUT_MONO), 1);
|
|
EXPECT_EQ(fb_channel_layout_get_channels(FB_CH_LAYOUT_STEREO), 2);
|
|
EXPECT_EQ(fb_channel_layout_get_channels(FB_CH_LAYOUT_5POINT1), 6);
|
|
EXPECT_EQ(fb_channel_layout_get_channels(FB_CH_LAYOUT_7POINT1), 8);
|
|
}
|
|
|
|
TEST(FFmpegBridgeChannelLayout, Defaults)
|
|
{
|
|
EXPECT_EQ(fb_channel_layout_default(1), FB_CH_LAYOUT_MONO);
|
|
EXPECT_EQ(fb_channel_layout_default(2), FB_CH_LAYOUT_STEREO);
|
|
// av_channel_layout_default picks a valid layout with the right channel
|
|
// count, but not necessarily the same variant as the named constants
|
|
EXPECT_EQ(fb_channel_layout_get_channels(fb_channel_layout_default(6)), 6);
|
|
EXPECT_NE(fb_channel_layout_default(3), (uint64_t)0);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Frame
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeFrame, AllocAndFields)
|
|
{
|
|
FBFrame *frame = fb_frame_alloc();
|
|
ASSERT_NE(frame, nullptr);
|
|
|
|
EXPECT_EQ(fb_frame_get_width(frame), 0);
|
|
EXPECT_EQ(fb_frame_get_format(frame), FB_PIX_FMT_NONE);
|
|
|
|
fb_frame_set_width(frame, 320);
|
|
fb_frame_set_height(frame, 240);
|
|
fb_frame_set_format(frame, FB_PIX_FMT_RGBA);
|
|
fb_frame_set_pts(frame, 12345);
|
|
|
|
EXPECT_EQ(fb_frame_get_width(frame), 320);
|
|
EXPECT_EQ(fb_frame_get_height(frame), 240);
|
|
EXPECT_EQ(fb_frame_get_format(frame), FB_PIX_FMT_RGBA);
|
|
EXPECT_EQ(fb_frame_get_pts(frame), 12345);
|
|
|
|
fb_frame_free(&frame);
|
|
EXPECT_EQ(frame, nullptr);
|
|
}
|
|
|
|
TEST(FFmpegBridgeFrame, BufferAllocAndAccess)
|
|
{
|
|
FBFrame *frame = fb_frame_alloc();
|
|
ASSERT_NE(frame, nullptr);
|
|
|
|
fb_frame_set_width(frame, 64);
|
|
fb_frame_set_height(frame, 48);
|
|
fb_frame_set_format(frame, FB_PIX_FMT_RGBA);
|
|
ASSERT_EQ(fb_frame_get_buffer(frame, 0), 0);
|
|
ASSERT_EQ(fb_frame_make_writable(frame), 0);
|
|
|
|
EXPECT_NE(fb_frame_get_data(frame, 0), nullptr);
|
|
EXPECT_GE(fb_frame_get_linesize(frame, 0), 64 * 4);
|
|
EXPECT_EQ(fb_frame_is_hw(frame), 0);
|
|
|
|
// Write a pattern through the API and read it back
|
|
const int linesize = fb_frame_get_linesize(frame, 0);
|
|
for (int y = 0; y < 48; ++y) {
|
|
std::memset(fb_frame_get_data(frame, 0) + y * linesize, y & 0xFF,
|
|
linesize);
|
|
}
|
|
const uint8_t *data = fb_frame_get_data_const(frame, 0);
|
|
ASSERT_NE(data, nullptr);
|
|
EXPECT_EQ(data[0], 0);
|
|
EXPECT_EQ(data[10 * linesize], 10);
|
|
|
|
// Out-of-range plane access is safe
|
|
EXPECT_EQ(fb_frame_get_data(frame, -1), nullptr);
|
|
EXPECT_EQ(fb_frame_get_data(frame, 8), nullptr);
|
|
|
|
fb_frame_unref(frame);
|
|
EXPECT_EQ(fb_frame_get_data(frame, 0), nullptr);
|
|
|
|
fb_frame_free(&frame);
|
|
}
|
|
|
|
TEST(FFmpegBridgeFrame, AudioFields)
|
|
{
|
|
FBFrame *frame = fb_frame_alloc();
|
|
ASSERT_NE(frame, nullptr);
|
|
|
|
fb_frame_set_nb_samples(frame, 1024);
|
|
fb_frame_set_sample_rate(frame, 44100);
|
|
fb_frame_set_format(frame, FB_SAMPLE_FMT_FLTP);
|
|
fb_frame_set_channel_layout_mask(frame, FB_CH_LAYOUT_STEREO);
|
|
ASSERT_EQ(fb_frame_get_buffer(frame, 0), 0);
|
|
|
|
EXPECT_EQ(fb_frame_get_nb_samples(frame), 1024);
|
|
EXPECT_EQ(fb_frame_get_sample_rate(frame), 44100);
|
|
EXPECT_EQ(fb_frame_get_channel_layout_mask(frame), FB_CH_LAYOUT_STEREO);
|
|
// Planar stereo: two data planes
|
|
EXPECT_NE(fb_frame_get_data(frame, 0), nullptr);
|
|
EXPECT_NE(fb_frame_get_data(frame, 1), nullptr);
|
|
|
|
fb_frame_free(&frame);
|
|
}
|
|
|
|
TEST(FFmpegBridgeFrame, CopyProps)
|
|
{
|
|
FBFrame *src = fb_frame_alloc();
|
|
FBFrame *dst = fb_frame_alloc();
|
|
ASSERT_NE(src, nullptr);
|
|
ASSERT_NE(dst, nullptr);
|
|
|
|
// av_frame_copy_props copies metadata properties, not dimensions/format
|
|
fb_frame_set_pts(src, 777);
|
|
fb_frame_set_color_range(src, FB_COLOR_RANGE_JPEG);
|
|
fb_frame_set_colorspace(src, FB_COL_SPC_BT709);
|
|
|
|
ASSERT_EQ(fb_frame_copy_props(dst, src), 0);
|
|
EXPECT_EQ(fb_frame_get_pts(dst), 777);
|
|
EXPECT_EQ(fb_frame_get_color_range(dst), FB_COLOR_RANGE_JPEG);
|
|
EXPECT_EQ(fb_frame_get_colorspace(dst), FB_COL_SPC_BT709);
|
|
|
|
fb_frame_free(&src);
|
|
fb_frame_free(&dst);
|
|
}
|
|
|
|
TEST(FFmpegBridgeFrame, NullSafety)
|
|
{
|
|
EXPECT_LT(fb_frame_get_buffer(nullptr, 0), 0);
|
|
EXPECT_LT(fb_frame_make_writable(nullptr), 0);
|
|
EXPECT_EQ(fb_frame_get_width(nullptr), 0);
|
|
EXPECT_EQ(fb_frame_get_data(nullptr, 0), nullptr);
|
|
fb_frame_free(nullptr); // must not crash
|
|
}
|
|
|
|
// ============================================================================
|
|
// Packet
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgePacket, AllocAndDefaults)
|
|
{
|
|
FBPacket *packet = fb_packet_alloc();
|
|
ASSERT_NE(packet, nullptr);
|
|
|
|
EXPECT_EQ(fb_packet_get_pts(packet), FB_NOPTS_VALUE);
|
|
EXPECT_EQ(fb_packet_get_size(packet), 0);
|
|
EXPECT_EQ(fb_packet_get_data(packet), nullptr);
|
|
|
|
fb_packet_unref(packet);
|
|
fb_packet_free(&packet);
|
|
EXPECT_EQ(packet, nullptr);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Scaler
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeScaler, RgbaToYuv420P)
|
|
{
|
|
const int width = 64;
|
|
const int height = 64;
|
|
|
|
FBFrame *src = fb_frame_alloc();
|
|
fb_frame_set_width(src, width);
|
|
fb_frame_set_height(src, height);
|
|
fb_frame_set_format(src, FB_PIX_FMT_RGBA);
|
|
ASSERT_EQ(fb_frame_get_buffer(src, 0), 0);
|
|
|
|
// Solid mid-grey image
|
|
const int src_linesize = fb_frame_get_linesize(src, 0);
|
|
for (int y = 0; y < height; ++y) {
|
|
std::memset(fb_frame_get_data(src, 0) + y * src_linesize, 128,
|
|
width * 4);
|
|
}
|
|
|
|
FBFrame *dst = fb_frame_alloc();
|
|
fb_frame_set_width(dst, width);
|
|
fb_frame_set_height(dst, height);
|
|
fb_frame_set_format(dst, FB_PIX_FMT_YUV420P);
|
|
ASSERT_EQ(fb_frame_get_buffer(dst, 0), 0);
|
|
|
|
FBScaler *scaler = fb_scaler_create(width, height, FB_PIX_FMT_RGBA, width,
|
|
height, FB_PIX_FMT_YUV420P,
|
|
FB_SCALER_POINT);
|
|
ASSERT_NE(scaler, nullptr);
|
|
// sws_scale returns the output slice height on success
|
|
ASSERT_GE(fb_scaler_scale_frame(scaler, dst, src), 0);
|
|
fb_scaler_free(&scaler);
|
|
EXPECT_EQ(scaler, nullptr);
|
|
|
|
// Y plane of a mid-grey RGB source should sit near 128
|
|
const uint8_t *y_plane = fb_frame_get_data_const(dst, 0);
|
|
ASSERT_NE(y_plane, nullptr);
|
|
EXPECT_NEAR(y_plane[0], 128, 8);
|
|
|
|
// U and V planes of a grey image should sit near 128 (neutral chroma)
|
|
const uint8_t *u_plane = fb_frame_get_data_const(dst, 1);
|
|
const uint8_t *v_plane = fb_frame_get_data_const(dst, 2);
|
|
ASSERT_NE(u_plane, nullptr);
|
|
ASSERT_NE(v_plane, nullptr);
|
|
EXPECT_NEAR(u_plane[0], 128, 8);
|
|
EXPECT_NEAR(v_plane[0], 128, 8);
|
|
|
|
fb_frame_free(&src);
|
|
fb_frame_free(&dst);
|
|
}
|
|
|
|
TEST(FFmpegBridgeScaler, SetColorspace)
|
|
{
|
|
FBScaler *scaler = fb_scaler_create(64, 64, FB_PIX_FMT_YUV420P, 64, 64,
|
|
FB_PIX_FMT_RGBA, FB_SCALER_POINT);
|
|
ASSERT_NE(scaler, nullptr);
|
|
EXPECT_GE(fb_scaler_set_colorspace(scaler, FB_COL_SPC_BT709, 0), 0);
|
|
fb_scaler_free(&scaler);
|
|
}
|
|
|
|
TEST(FFmpegBridgeScaler, YuvCoefficients)
|
|
{
|
|
// Values come from swscale's coefficient tables (sws_getCoefficients /
|
|
// 65536), which include the studio-range scaling factor.
|
|
double coeffs[4] = { 0, 0, 0, 0 };
|
|
fb_get_yuv_coefficients(FB_COL_SPC_BT709, coeffs);
|
|
EXPECT_NEAR(coeffs[0], 117489 / 65536.0, 1e-6); // crv
|
|
EXPECT_NEAR(coeffs[1], 138438 / 65536.0, 1e-6); // cbu
|
|
EXPECT_GT(coeffs[2], 0.0);
|
|
EXPECT_GT(coeffs[3], 0.0);
|
|
|
|
fb_get_yuv_coefficients(FB_COL_SPC_SMPTE170M, coeffs);
|
|
EXPECT_NEAR(coeffs[0], 104597 / 65536.0, 1e-6); // crv
|
|
}
|
|
|
|
// ============================================================================
|
|
// Resampler
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeResampler, ConvertFltpToS16p)
|
|
{
|
|
const int in_samples = 1024;
|
|
const double freq = 440.0;
|
|
const double rate = 44100.0;
|
|
|
|
FBResampler *resampler =
|
|
fb_resampler_create(FB_CH_LAYOUT_STEREO, FB_SAMPLE_FMT_S16P, 44100,
|
|
FB_CH_LAYOUT_STEREO, FB_SAMPLE_FMT_FLTP, 44100);
|
|
ASSERT_NE(resampler, nullptr);
|
|
|
|
const int out_capacity = fb_resampler_get_out_samples(resampler, in_samples);
|
|
ASSERT_GT(out_capacity, 0);
|
|
|
|
std::vector<float> in_left(in_samples), in_right(in_samples);
|
|
for (int i = 0; i < in_samples; ++i) {
|
|
in_left[i] = 0.5f * std::sin(2.0 * kPi * freq * i / rate);
|
|
in_right[i] = 0.5f * std::sin(2.0 * kPi * freq * i / rate);
|
|
}
|
|
const uint8_t *in_planes[2] = {
|
|
reinterpret_cast<const uint8_t *>(in_left.data()),
|
|
reinterpret_cast<const uint8_t *>(in_right.data())
|
|
};
|
|
|
|
std::vector<int16_t> out_left(out_capacity), out_right(out_capacity);
|
|
uint8_t *out_planes[2] = {
|
|
reinterpret_cast<uint8_t *>(out_left.data()),
|
|
reinterpret_cast<uint8_t *>(out_right.data())
|
|
};
|
|
|
|
const int converted = fb_resampler_convert(
|
|
resampler, out_planes, out_capacity, in_planes, in_samples);
|
|
ASSERT_GT(converted, 0);
|
|
|
|
// Output must be non-silent and bounded to full scale
|
|
bool non_silent = false;
|
|
for (int i = 0; i < converted; ++i) {
|
|
if (out_left[i] != 0) {
|
|
non_silent = true;
|
|
}
|
|
EXPECT_LE(std::abs(out_left[i]), 32767);
|
|
}
|
|
EXPECT_TRUE(non_silent);
|
|
|
|
fb_resampler_free(&resampler);
|
|
EXPECT_EQ(resampler, nullptr);
|
|
}
|
|
|
|
TEST(FFmpegBridgeResampler, ConvertFrameInput)
|
|
{
|
|
const int in_samples = 512;
|
|
|
|
FBResampler *resampler =
|
|
fb_resampler_create(FB_CH_LAYOUT_STEREO, FB_SAMPLE_FMT_S16P, 44100,
|
|
FB_CH_LAYOUT_STEREO, FB_SAMPLE_FMT_FLTP, 44100);
|
|
ASSERT_NE(resampler, nullptr);
|
|
|
|
FBFrame *frame = fb_frame_alloc();
|
|
fb_frame_set_nb_samples(frame, in_samples);
|
|
fb_frame_set_sample_rate(frame, 44100);
|
|
fb_frame_set_format(frame, FB_SAMPLE_FMT_FLTP);
|
|
fb_frame_set_channel_layout_mask(frame, FB_CH_LAYOUT_STEREO);
|
|
ASSERT_EQ(fb_frame_get_buffer(frame, 0), 0);
|
|
|
|
float *left = reinterpret_cast<float *>(fb_frame_get_data(frame, 0));
|
|
float *right = reinterpret_cast<float *>(fb_frame_get_data(frame, 1));
|
|
for (int i = 0; i < in_samples; ++i) {
|
|
left[i] = 0.25f;
|
|
right[i] = -0.25f;
|
|
}
|
|
|
|
const int out_capacity = fb_resampler_get_out_samples(resampler, in_samples);
|
|
std::vector<int16_t> out_left(out_capacity), out_right(out_capacity);
|
|
uint8_t *out_planes[2] = {
|
|
reinterpret_cast<uint8_t *>(out_left.data()),
|
|
reinterpret_cast<uint8_t *>(out_right.data())
|
|
};
|
|
|
|
const int converted =
|
|
fb_resampler_convert_frame(resampler, out_planes, out_capacity, frame);
|
|
ASSERT_GT(converted, 0);
|
|
|
|
// Constant 0.25 input should land near 0.25 * 32767
|
|
EXPECT_NEAR(out_left[converted / 2], 8192, 256);
|
|
EXPECT_NEAR(out_right[converted / 2], -8192, 256);
|
|
|
|
fb_frame_free(&frame);
|
|
fb_resampler_free(&resampler);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Probe / Decoder on tests/demo.mp4
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeProbe, DemoMp4Streams)
|
|
{
|
|
const QString path = DemoPath();
|
|
ASSERT_TRUE(QFileInfo::exists(path));
|
|
|
|
FBProbe *probe = fb_probe_create();
|
|
ASSERT_NE(probe, nullptr);
|
|
ASSERT_EQ(fb_probe_open(probe, path.toUtf8().constData()), 0);
|
|
|
|
EXPECT_GE(fb_probe_get_stream_count(probe), 1);
|
|
EXPECT_GT(fb_probe_get_duration(probe), 0);
|
|
|
|
const int video = FindStream(probe, FB_MEDIA_TYPE_VIDEO);
|
|
ASSERT_GE(video, 0);
|
|
|
|
FBStreamInfo info;
|
|
ASSERT_EQ(fb_probe_get_stream_info(probe, video, &info), 0);
|
|
EXPECT_EQ(info.width, 1920);
|
|
EXPECT_EQ(info.height, 1080);
|
|
EXPECT_NE(info.pixel_format, FB_PIX_FMT_NONE);
|
|
EXPECT_EQ(info.has_decoder, 1);
|
|
EXPECT_GT(info.time_base_den, 0);
|
|
|
|
// Metadata lookups must be safe whether or not the key exists
|
|
char buffer[256];
|
|
int found = fb_probe_get_metadata(probe, -1, "title", buffer, sizeof(buffer));
|
|
EXPECT_TRUE(found == 0 || found == 1);
|
|
|
|
fb_probe_close(probe);
|
|
fb_probe_free(&probe);
|
|
EXPECT_EQ(probe, nullptr);
|
|
}
|
|
|
|
TEST(FFmpegBridgeProbe, VideoStreamDetails)
|
|
{
|
|
const QString path = DemoPath();
|
|
ASSERT_TRUE(QFileInfo::exists(path));
|
|
|
|
FBVideoStreamDetails details;
|
|
ASSERT_EQ(fb_probe_video_stream_details(path.toUtf8().constData(), 0,
|
|
&details, 0, nullptr, nullptr),
|
|
0);
|
|
EXPECT_TRUE(details.field_order == FB_FIELD_ORDER_PROGRESSIVE ||
|
|
details.field_order == FB_FIELD_ORDER_UNKNOWN);
|
|
EXPECT_GT(details.frame_rate_num, 0);
|
|
EXPECT_GT(details.frame_rate_den, 0);
|
|
EXPECT_EQ(details.pixel_aspect_num, 1);
|
|
EXPECT_EQ(details.pixel_aspect_den, 1);
|
|
}
|
|
|
|
TEST(FFmpegBridgeProbe, ReadSubtitleStream)
|
|
{
|
|
// Write a small SRT file and read it back through the bridge
|
|
const QString path = TempFilePath(QStringLiteral("fb_bridge_test.srt"));
|
|
{
|
|
QFile file(path);
|
|
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
|
|
file.write("1\n00:00:01,000 --> 00:00:02,000\nHello bridge\n\n"
|
|
"2\n00:00:03,000 --> 00:00:04,500\nSecond line\n\n");
|
|
file.close();
|
|
}
|
|
|
|
struct Context {
|
|
int count = 0;
|
|
std::string first_text;
|
|
int64_t first_pts = FB_NOPTS_VALUE;
|
|
} ctx;
|
|
|
|
int result = fb_probe_read_subtitle_stream(
|
|
path.toUtf8().constData(), 0,
|
|
[](int64_t pts, int64_t, const char *text, int, void *userdata) {
|
|
auto *c = static_cast<Context *>(userdata);
|
|
if (c->count == 0) {
|
|
c->first_pts = pts;
|
|
c->first_text = text ? text : "";
|
|
}
|
|
c->count++;
|
|
},
|
|
&ctx);
|
|
|
|
EXPECT_EQ(result, 0);
|
|
EXPECT_EQ(ctx.count, 2);
|
|
EXPECT_NE(ctx.first_text.find("Hello bridge"), std::string::npos);
|
|
EXPECT_GE(ctx.first_pts, 0);
|
|
|
|
QFile::remove(path);
|
|
}
|
|
|
|
TEST(FFmpegBridgeDecoder, DecodeFirstFrame)
|
|
{
|
|
const QString path = DemoPath();
|
|
ASSERT_TRUE(QFileInfo::exists(path));
|
|
|
|
FBDecoder *decoder = fb_decoder_create();
|
|
ASSERT_NE(decoder, nullptr);
|
|
ASSERT_EQ(fb_decoder_open(decoder, path.toUtf8().constData(), 0), 0);
|
|
|
|
FBStreamInfo info;
|
|
ASSERT_EQ(fb_decoder_get_stream_info(decoder, &info), 0);
|
|
EXPECT_EQ(info.width, 1920);
|
|
EXPECT_EQ(info.height, 1080);
|
|
EXPECT_EQ(info.codec_type, FB_MEDIA_TYPE_VIDEO);
|
|
EXPECT_GT(fb_decoder_get_format_duration(decoder), 0);
|
|
|
|
FBPacket *packet = fb_packet_alloc();
|
|
FBFrame *frame = fb_frame_alloc();
|
|
ASSERT_NE(packet, nullptr);
|
|
ASSERT_NE(frame, nullptr);
|
|
|
|
int result = -1;
|
|
for (int attempt = 0; attempt < 200 && result != 0; ++attempt) {
|
|
result = fb_decoder_get_frame(decoder, packet, frame);
|
|
}
|
|
ASSERT_EQ(result, 0);
|
|
|
|
EXPECT_EQ(fb_frame_get_width(frame), 1920);
|
|
EXPECT_EQ(fb_frame_get_height(frame), 1080);
|
|
EXPECT_NE(fb_frame_get_format(frame), FB_PIX_FMT_NONE);
|
|
// Software frames must have CPU-accessible data. Hardware frames live in
|
|
// device memory, so their data is checked after the transfer below.
|
|
if (!fb_frame_is_hw(frame)) {
|
|
EXPECT_NE(fb_frame_get_data(frame, 0), nullptr);
|
|
}
|
|
|
|
// Hardware frames live in device memory: transfer to a software frame
|
|
// before reading pixels
|
|
FBFrame *sw_frame = nullptr;
|
|
const FBFrame *read_frame = frame;
|
|
if (fb_frame_is_hw(frame)) {
|
|
sw_frame = fb_frame_alloc();
|
|
ASSERT_NE(sw_frame, nullptr);
|
|
ASSERT_EQ(fb_frame_hw_transfer_data(sw_frame, frame), 0);
|
|
read_frame = sw_frame;
|
|
}
|
|
|
|
// The decoded image should not be a black frame
|
|
const uint8_t *data = fb_frame_get_data_const(read_frame, 0);
|
|
ASSERT_NE(data, nullptr);
|
|
bool has_nonzero = false;
|
|
const int linesize = fb_frame_get_linesize(read_frame, 0);
|
|
for (int y = 0; y < 64 && !has_nonzero; ++y) {
|
|
for (int x = 0; x < 64; ++x) {
|
|
if (data[y * linesize + x] != 0) {
|
|
has_nonzero = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
EXPECT_TRUE(has_nonzero);
|
|
|
|
fb_frame_free(&sw_frame);
|
|
|
|
// Seek back to the start and decode again
|
|
fb_decoder_seek(decoder, 0);
|
|
result = -1;
|
|
for (int attempt = 0; attempt < 200 && result != 0; ++attempt) {
|
|
result = fb_decoder_get_frame(decoder, packet, frame);
|
|
}
|
|
EXPECT_EQ(result, 0);
|
|
|
|
fb_frame_free(&frame);
|
|
fb_packet_free(&packet);
|
|
fb_decoder_close(decoder);
|
|
fb_decoder_free(&decoder);
|
|
}
|
|
|
|
TEST(FFmpegBridgeDecoder, OpenFailure)
|
|
{
|
|
FBDecoder *decoder = fb_decoder_create();
|
|
ASSERT_NE(decoder, nullptr);
|
|
EXPECT_LT(fb_decoder_open(decoder, "/nonexistent/file.mp4", 0), 0);
|
|
fb_decoder_free(&decoder);
|
|
}
|
|
|
|
TEST(FFmpegBridgeDecoder, GuessRates)
|
|
{
|
|
const QString path = DemoPath();
|
|
ASSERT_TRUE(QFileInfo::exists(path));
|
|
|
|
FBDecoder *decoder = fb_decoder_create();
|
|
ASSERT_EQ(fb_decoder_open(decoder, path.toUtf8().constData(), 0), 0);
|
|
|
|
int num = 0, den = 0;
|
|
ASSERT_EQ(fb_decoder_guess_frame_rate(decoder, nullptr, &num, &den), 0);
|
|
EXPECT_GT(num, 0);
|
|
EXPECT_GT(den, 0);
|
|
|
|
ASSERT_EQ(fb_decoder_guess_sample_aspect_ratio(decoder, nullptr, &num, &den),
|
|
0);
|
|
EXPECT_EQ(num, 1);
|
|
EXPECT_EQ(den, 1);
|
|
|
|
fb_decoder_free(&decoder);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Audio graph
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeAudioGraph, TempoProcessing)
|
|
{
|
|
FBAudioGraphConfig config = {};
|
|
config.in_sample_rate = 44100;
|
|
config.in_channel_layout_mask = FB_CH_LAYOUT_STEREO;
|
|
config.in_sample_format = FB_SAMPLE_FMT_FLTP;
|
|
config.in_channels = 2;
|
|
config.out_sample_rate = 44100;
|
|
config.out_channel_layout_mask = FB_CH_LAYOUT_STEREO;
|
|
config.out_sample_format = FB_SAMPLE_FMT_FLTP;
|
|
config.out_channels = 2;
|
|
config.out_is_planar = 1;
|
|
config.tempo = 2.0;
|
|
|
|
FBAudioGraph *graph = fb_audio_graph_create(&config);
|
|
ASSERT_NE(graph, nullptr);
|
|
|
|
const int in_samples = 4410; // 0.1 seconds
|
|
std::vector<float> left(in_samples, 0.5f), right(in_samples, 0.5f);
|
|
const uint8_t *planes[2] = {
|
|
reinterpret_cast<const uint8_t *>(left.data()),
|
|
reinterpret_cast<const uint8_t *>(right.data())
|
|
};
|
|
|
|
ASSERT_EQ(fb_audio_graph_push(graph, planes, in_samples), 0);
|
|
// Flush
|
|
ASSERT_EQ(fb_audio_graph_push(graph, nullptr, 0), 0);
|
|
|
|
FBFrame *out = fb_frame_alloc();
|
|
int total_samples = 0;
|
|
int pull_result;
|
|
while ((pull_result = fb_audio_graph_pull(graph, out)) == 1) {
|
|
total_samples += fb_frame_get_nb_samples(out);
|
|
fb_frame_unref(out);
|
|
}
|
|
// 0 = needs more input, FB_ERROR_EOF = flushed graph is drained
|
|
EXPECT_TRUE(pull_result == 0 || pull_result == FB_ERROR_EOF);
|
|
|
|
// tempo 2.0 halves the duration; atempo has some internal padding,
|
|
// so allow generous bounds around the ideal 2205 samples
|
|
EXPECT_GT(total_samples, 1500);
|
|
EXPECT_LT(total_samples, 3000);
|
|
|
|
fb_frame_free(&out);
|
|
fb_audio_graph_free(&graph);
|
|
EXPECT_EQ(graph, nullptr);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Encoder (end-to-end: encode, then probe the result back)
|
|
// ============================================================================
|
|
|
|
TEST(FFmpegBridgeEncoder, CodecFormatLists)
|
|
{
|
|
// PNG is a native FFmpeg encoder and always available
|
|
const char *names[16];
|
|
const int pix_count =
|
|
fb_encoder_codec_get_pixel_formats(FB_CODEC_PNG, names, 16);
|
|
EXPECT_GT(pix_count, 0);
|
|
|
|
int fmts[16];
|
|
const int sample_count =
|
|
fb_encoder_codec_get_sample_formats(FB_CODEC_AAC, fmts, 16);
|
|
EXPECT_GT(sample_count, 0);
|
|
}
|
|
|
|
TEST(FFmpegBridgeEncoder, WritePngVideoAndProbeBack)
|
|
{
|
|
const QString path = TempFilePath(QStringLiteral("fb_bridge_test.mkv"));
|
|
QFile::remove(path);
|
|
|
|
const int width = 64;
|
|
const int height = 64;
|
|
const int frame_count = 5;
|
|
|
|
const QByteArray filename = path.toUtf8();
|
|
|
|
FBEncoderConfig config = {};
|
|
config.filename = filename.constData();
|
|
config.video_enabled = 1;
|
|
config.video_codec = FB_CODEC_PNG;
|
|
config.video_width = width;
|
|
config.video_height = height;
|
|
config.video_pixel_aspect_num = 1;
|
|
config.video_pixel_aspect_den = 1;
|
|
config.video_time_base_num = 1;
|
|
config.video_time_base_den = 30;
|
|
config.video_frame_rate_num = 30;
|
|
config.video_frame_rate_den = 1;
|
|
config.video_pix_fmt = "rgba";
|
|
config.video_src_pix_fmt = FB_PIX_FMT_RGBA;
|
|
config.video_color_range = FB_COLOR_RANGE_UNSPEC;
|
|
config.video_field_order = FB_FIELD_ORDER_PROGRESSIVE;
|
|
config.video_threads = 1;
|
|
|
|
FBEncoder *encoder = fb_encoder_create(&config);
|
|
ASSERT_NE(encoder, nullptr);
|
|
ASSERT_EQ(fb_encoder_open(encoder), 0)
|
|
<< "encoder error: " << fb_encoder_get_error(encoder);
|
|
|
|
std::vector<uint8_t> pixels(width * height * 4);
|
|
for (int f = 0; f < frame_count; ++f) {
|
|
for (int i = 0; i < width * height; ++i) {
|
|
pixels[i * 4 + 0] = static_cast<uint8_t>(f * 40); // R
|
|
pixels[i * 4 + 1] = 128; // G
|
|
pixels[i * 4 + 2] = 64; // B
|
|
pixels[i * 4 + 3] = 255; // A
|
|
}
|
|
ASSERT_EQ(fb_encoder_write_video_frame(encoder, width, height,
|
|
FB_PIX_FMT_RGBA, pixels.data(),
|
|
width * 4, f / 30.0),
|
|
0)
|
|
<< "encoder error: " << fb_encoder_get_error(encoder);
|
|
}
|
|
|
|
fb_encoder_close(encoder);
|
|
fb_encoder_free(&encoder);
|
|
|
|
// The file must exist and probe back as a 64x64 video
|
|
ASSERT_TRUE(QFileInfo::exists(path));
|
|
|
|
FBProbe *probe = fb_probe_create();
|
|
ASSERT_EQ(fb_probe_open(probe, path.toUtf8().constData()), 0);
|
|
const int video = FindStream(probe, FB_MEDIA_TYPE_VIDEO);
|
|
ASSERT_GE(video, 0);
|
|
|
|
FBStreamInfo info;
|
|
ASSERT_EQ(fb_probe_get_stream_info(probe, video, &info), 0);
|
|
EXPECT_EQ(info.width, width);
|
|
EXPECT_EQ(info.height, height);
|
|
|
|
fb_probe_close(probe);
|
|
fb_probe_free(&probe);
|
|
QFile::remove(path);
|
|
}
|
|
|
|
TEST(FFmpegBridgeEncoder, WritePcmAudioAndProbeBack)
|
|
{
|
|
const QString path = TempFilePath(QStringLiteral("fb_bridge_test.wav"));
|
|
QFile::remove(path);
|
|
|
|
const QByteArray filename = path.toUtf8();
|
|
|
|
FBEncoderConfig config = {};
|
|
config.filename = filename.constData();
|
|
config.audio_enabled = 1;
|
|
config.audio_codec = FB_CODEC_PCM;
|
|
config.audio_sample_rate = 44100;
|
|
config.audio_channel_layout_mask = FB_CH_LAYOUT_STEREO;
|
|
config.audio_sample_format = FB_SAMPLE_FMT_S16;
|
|
|
|
FBEncoder *encoder = fb_encoder_create(&config);
|
|
ASSERT_NE(encoder, nullptr);
|
|
ASSERT_EQ(fb_encoder_open(encoder), 0)
|
|
<< "encoder error: " << fb_encoder_get_error(encoder);
|
|
|
|
const int sample_count = 4410;
|
|
std::vector<int16_t> left(sample_count), right(sample_count);
|
|
for (int i = 0; i < sample_count; ++i) {
|
|
left[i] = static_cast<int16_t>(
|
|
10000 * std::sin(2.0 * kPi * 440.0 * i / 44100.0));
|
|
right[i] = left[i];
|
|
}
|
|
const uint8_t *planes[2] = {
|
|
reinterpret_cast<const uint8_t *>(left.data()),
|
|
reinterpret_cast<const uint8_t *>(right.data())
|
|
};
|
|
|
|
ASSERT_EQ(fb_encoder_write_audio(encoder, planes, 2, FB_SAMPLE_FMT_S16P,
|
|
44100, FB_CH_LAYOUT_STEREO, sample_count),
|
|
0)
|
|
<< "encoder error: " << fb_encoder_get_error(encoder);
|
|
|
|
// Flush
|
|
EXPECT_EQ(fb_encoder_write_audio(encoder, nullptr, 2, FB_SAMPLE_FMT_S16P,
|
|
44100, FB_CH_LAYOUT_STEREO, 0),
|
|
0);
|
|
|
|
fb_encoder_close(encoder);
|
|
fb_encoder_free(&encoder);
|
|
|
|
ASSERT_TRUE(QFileInfo::exists(path));
|
|
|
|
FBProbe *probe = fb_probe_create();
|
|
ASSERT_EQ(fb_probe_open(probe, path.toUtf8().constData()), 0);
|
|
const int audio = FindStream(probe, FB_MEDIA_TYPE_AUDIO);
|
|
ASSERT_GE(audio, 0);
|
|
|
|
FBStreamInfo info;
|
|
ASSERT_EQ(fb_probe_get_stream_info(probe, audio, &info), 0);
|
|
EXPECT_EQ(info.sample_rate, 44100);
|
|
EXPECT_EQ(fb_channel_layout_get_channels(info.channel_layout_mask), 2);
|
|
|
|
fb_probe_close(probe);
|
|
fb_probe_free(&probe);
|
|
QFile::remove(path);
|
|
}
|