diff --git a/configure.ac b/configure.ac index 0db2434381c6d06475bc663bd8ea69ff1216d7c8..2e99babc184686ed68c6a9819322a0ce1e2f90e4 100644 --- a/configure.ac +++ b/configure.ac @@ -449,6 +449,8 @@ PKG_CHECK_MODULES(LIBAVFORMAT, libavformat >= 56.40.101,, AC_MSG_ERROR([Missing PKG_CHECK_MODULES(LIBAVDEVICE, libavdevice >= 56.4.100,, AC_MSG_ERROR([Missing libavdevice development files])) +PKG_CHECK_MODULES(LIBAVFILTER, libavfilter >= 5.40.101,, AC_MSG_ERROR([Missing libavfilter development files])) + PKG_CHECK_MODULES(LIBSWSCALE, libswscale >= 3.1.101,, AC_MSG_ERROR([Missing libswscale development files])) dnl Video is default-enabled diff --git a/contrib/src/ffmpeg/rules.mak b/contrib/src/ffmpeg/rules.mak index a4674de9145ada423ec8c8d16b1af5c7dd7b9fd2..f01801cb84d9b885c9853961ee1347deb376aa52 100644 --- a/contrib/src/ffmpeg/rules.mak +++ b/contrib/src/ffmpeg/rules.mak @@ -3,7 +3,7 @@ FFMPEG_URL := https://git.ffmpeg.org/gitweb/ffmpeg.git/snapshot/$(FFMPEG_HASH).t PKGS+=ffmpeg -ifeq ($(call need_pkg,"libavutil >= 55.75.100 libavcodec >= 57.106.101 libavformat >= 57.82.100 libavdevice >= 57.8.101 libswscale >= 4.7.103"),) +ifeq ($(call need_pkg,"libavutil >= 55.75.100 libavcodec >= 57.106.101 libavformat >= 57.82.100 libavdevice >= 57.8.101 libavfilter >= 6.105.100 libswscale >= 4.7.103"),) PKGS_FOUND += ffmpeg endif @@ -21,6 +21,7 @@ FFMPEGCONF += \ --enable-swscale \ --enable-protocols \ --enable-bsfs \ + --enable-filters \ --disable-programs #enable muxers/demuxers diff --git a/src/media/Makefile.am b/src/media/Makefile.am index 50944f3e09f0dda748ce50d67778c00730441aa5..a4f3461cc2ce19dbce9926709c9840385b917200 100644 --- a/src/media/Makefile.am +++ b/src/media/Makefile.am @@ -18,7 +18,8 @@ libmedia_la_SOURCES = \ media_codec.cpp \ system_codec_container.cpp \ srtp.c \ - recordable.cpp + recordable.cpp \ + media_filter.cpp noinst_HEADERS = \ rtp_session.h \ @@ -34,7 +35,9 @@ noinst_HEADERS = \ system_codec_container.h \ srtp.h \ recordable.h \ - decoder_finder.h + decoder_finder.h \ + media_filter.h \ + media_stream.h libmedia_la_LIBADD = \ ./audio/libaudio.la @@ -44,12 +47,12 @@ libmedia_la_libADD = \ ./video/libvideo.la endif -libmedia_la_LDFLAGS = @LIBAVCODEC_LIBS@ @LIBAVFORMAT_LIBS@ @LIBAVDEVICE_LIBS@ @LIBSWSCALE_LIBS@ @LIBAVUTIL_LIBS@ +libmedia_la_LDFLAGS = @LIBAVCODEC_LIBS@ @LIBAVFORMAT_LIBS@ @LIBAVDEVICE_LIBS@ @LIBAVFILTER_LIBS@ @LIBSWSCALE_LIBS@ @LIBAVUTIL_LIBS@ if HAVE_WIN32 libmedia_la_LDFLAGS += -lws2_32 -lwsock32 -lshlwapi endif -AM_CFLAGS=@LIBAVCODEC_CFLAGS@ @LIBAVFORMAT_CFLAGS@ @LIBAVDEVICE_CFLAGS@ @LIBSWSCALE_CFLAGS@ +AM_CFLAGS=@LIBAVCODEC_CFLAGS@ @LIBAVFORMAT_CFLAGS@ @LIBAVDEVICE_CFLAGS@ @LIBAVFILTER_CFLAGS@ @LIBSWSCALE_CFLAGS@ -AM_CXXFLAGS=@LIBAVCODEC_CFLAGS@ @LIBAVFORMAT_CFLAGS@ @LIBAVDEVICE_CFLAGS@ @LIBSWSCALE_CFLAGS@ +AM_CXXFLAGS=@LIBAVCODEC_CFLAGS@ @LIBAVFORMAT_CFLAGS@ @LIBAVDEVICE_CFLAGS@ @LIBAVFILTER_CFLAGS@ @LIBSWSCALE_CFLAGS@ diff --git a/src/media/libav_deps.h b/src/media/libav_deps.h index 4cb170c9c747a73019a63d1b490888ddfb992b10..a835996d0179b28e487f55b0785467307f8f7028 100644 --- a/src/media/libav_deps.h +++ b/src/media/libav_deps.h @@ -26,6 +26,7 @@ extern "C" { #include <libavcodec/avcodec.h> +#include <libavfilter/avfilter.h> #include <libavformat/avformat.h> #include <libavdevice/avdevice.h> #include <libswscale/swscale.h> diff --git a/src/media/libav_utils.cpp b/src/media/libav_utils.cpp index 05ea52cf43a3e0b6750013d143202870493bc2ac..d87875ca6266a117d85748f3ead4608ceb7bebe1 100644 --- a/src/media/libav_utils.cpp +++ b/src/media/libav_utils.cpp @@ -141,6 +141,9 @@ init_once() #endif avdevice_register_all(); avformat_network_init(); +#if LIBAVFILTER_VERSION_INT < AV_VERSION_INT(7, 13, 100) + avfilter_register_all(); +#endif #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100) av_lockmgr_register(avcodecManageMutex); diff --git a/src/media/media_filter.cpp b/src/media/media_filter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9d49018a9436c7478f7526271e8dfd3f6ae704a0 --- /dev/null +++ b/src/media/media_filter.cpp @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * + * Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "libav_deps.h" // MUST BE INCLUDED FIRST +#include "logger.h" +#include "media_filter.h" + +extern "C" { +#include <libavfilter/buffersink.h> +#include <libavfilter/buffersrc.h> +} + +#include <algorithm> +#include <functional> +#include <memory> +#include <sstream> + +namespace ring { + +MediaFilter::MediaFilter() +{} + +MediaFilter::~MediaFilter() +{ + clean(); +} + +std::string +MediaFilter::getFilterDesc() const +{ + return desc_; +} + +int +MediaFilter::initialize(const std::string& filterDesc, MediaStream msp) +{ + std::vector<MediaStream> msps; + msps.push_back(msp); + desc_ = filterDesc; + return initialize(desc_, msps); +} + +int +MediaFilter::initialize(const std::string& filterDesc, std::vector<MediaStream> msps) +{ + int ret = 0; + desc_ = filterDesc; + graph_ = avfilter_graph_alloc(); + + if (!graph_) + return fail("Failed to allocate filter graph", AVERROR(ENOMEM)); + + AVFilterInOut* in; + AVFilterInOut* out; + if ((ret = avfilter_graph_parse2(graph_, desc_.c_str(), &in, &out)) < 0) + return fail("Failed to parse filter graph", ret); + + using AVFilterInOutPtr = std::unique_ptr<AVFilterInOut, std::function<void(AVFilterInOut*)>>; + AVFilterInOutPtr outputs(out, [](AVFilterInOut* f){ avfilter_inout_free(&f); }); + AVFilterInOutPtr inputs(in, [](AVFilterInOut* f){ avfilter_inout_free(&f); }); + + if (outputs && outputs->next) + return fail("Filters with multiple outputs are not supported", AVERROR(ENOTSUP)); + + if ((ret = initOutputFilter(outputs.get())) < 0) + return fail("Failed to create output for filter graph", ret); + + // make sure inputs linked list is the same size as msps + size_t count = 0; + AVFilterInOut* dummyInput = inputs.get(); + while (dummyInput && ++count) // increment count before evaluating its value + dummyInput = dummyInput->next; + if (count != msps.size()) + return fail("Size mismatch between number of inputs in filter graph and input parameter array", + AVERROR(EINVAL)); + + if (count > 1) { + /* Complex filter */ + for (AVFilterInOut* current = inputs.get(); current; current = current->next) { + if (!current->name) + return fail("Complex filters' inputs require names", AVERROR(EINVAL)); + std::string name = current->name; + const auto& it = std::find_if(msps.begin(), msps.end(), [name](const MediaStream& msp) + { return msp.name == name; }); + if (it != msps.end()) { + if ((ret = initInputFilter(current, *it, false)) < 0) { + std::string msg = "Failed to find matching parameters for: " + name; + return fail(msg, ret); + } + } + } + } else { + /* Simple filter */ + if ((ret = initInputFilter(inputs.get(), msps[0], true)) < 0) { + return fail("Failed to create input for filter graph", ret); + } + } + + if ((ret = avfilter_graph_config(graph_, nullptr)) < 0) + return fail("Failed to configure filter graph", ret); + + RING_DBG() << "Filter graph initialized with: " << desc_; + initialized_ = true; + return 0; +} + +int +MediaFilter::feedInput(AVFrame* frame) +{ + return feedInput(frame, "default"); +} + +int +MediaFilter::feedInput(AVFrame* frame, std::string inputName) +{ + int ret = 0; + if (!initialized_) + return fail("Filter not initialized", -1); + + for (size_t i = 0; i < inputs_.size(); ++i) { + auto filterCtx = inputs_[i]; + if (inputNames_[i] != inputName) + continue; + + int flags = AV_BUFFERSRC_FLAG_PUSH | AV_BUFFERSRC_FLAG_KEEP_REF; + if ((ret = av_buffersrc_add_frame_flags(filterCtx, frame, flags)) < 0) + return fail("Could not pass frame to filters", ret); + else + return 0; + } + + std::stringstream ss; + ss << "Specified filter (" << inputName << ") not found"; + return fail(ss.str(), AVERROR(EINVAL)); +} + +AVFrame* +MediaFilter::readOutput() +{ + if (!initialized_) { + fail("Not properly initialized", -1); + return nullptr; + } + + int ret = 0; + AVFrame* frame = av_frame_alloc(); + ret = av_buffersink_get_frame_flags(output_, frame, 0); + if (ret >= 0) { + return frame; + } else if (ret == AVERROR(EAGAIN)) { + // return nullptr + } else if (ret == AVERROR_EOF) { + RING_WARN() << "Filters have reached EOF, no more frames will be output"; + } else { + fail("Error occurred while pulling from filter graph", ret); + } + av_frame_free(&frame); + return nullptr; +} + +int +MediaFilter::initOutputFilter(AVFilterInOut* out) +{ + int ret = 0; + const AVFilter* buffersink; + AVFilterContext* buffersinkCtx = nullptr; + AVMediaType mediaType = avfilter_pad_get_type(out->filter_ctx->input_pads, out->pad_idx); + + if (mediaType == AVMEDIA_TYPE_VIDEO) + buffersink = avfilter_get_by_name("buffersink"); + else + buffersink = avfilter_get_by_name("abuffersink"); + + if ((ret = avfilter_graph_create_filter(&buffersinkCtx, buffersink, "out", + nullptr, nullptr, graph_)) < 0) { + avfilter_free(buffersinkCtx); + return fail("Failed to create buffer sink", ret); + } + + if ((ret = avfilter_link(out->filter_ctx, out->pad_idx, buffersinkCtx, 0)) < 0) { + avfilter_free(buffersinkCtx); + return fail("Could not link buffer sink to graph", ret); + } + + output_ = buffersinkCtx; + return ret; +} + +int +MediaFilter::initInputFilter(AVFilterInOut* in, MediaStream msp, bool simple) +{ + int ret = 0; + AVBufferSrcParameters* params = av_buffersrc_parameters_alloc(); + if (!params) + return -1; + + const AVFilter* buffersrc; + AVMediaType mediaType = avfilter_pad_get_type(in->filter_ctx->input_pads, in->pad_idx); + params->format = msp.format; + params->time_base = msp.timeBase; + if (mediaType == AVMEDIA_TYPE_VIDEO) { + params->width = msp.width; + params->height = msp.height; + params->sample_aspect_ratio = msp.aspectRatio; + params->frame_rate = msp.frameRate; + buffersrc = avfilter_get_by_name("buffer"); + } else { + params->sample_rate = msp.sampleRate; + params->channel_layout = av_get_default_channel_layout(msp.nbChannels); + buffersrc = avfilter_get_by_name("abuffer"); + } + + AVFilterContext* buffersrcCtx = nullptr; + if (buffersrc) { + char name[128]; + if (simple) + snprintf(name, sizeof(name), "buffersrc"); + else + snprintf(name, sizeof(name), "buffersrc_%s_%d", in->name, in->pad_idx); + buffersrcCtx = avfilter_graph_alloc_filter(graph_, buffersrc, name); + } + if (!buffersrcCtx) { + av_free(params); + return fail("Failed to allocate filter graph input", AVERROR(ENOMEM)); + } + ret = av_buffersrc_parameters_set(buffersrcCtx, params); + av_free(params); + if (ret < 0) + return fail("Failed to set filter graph input parameters", ret); + + if ((ret = avfilter_init_str(buffersrcCtx, nullptr)) < 0) + return fail("Failed to initialize buffer source", ret); + + if ((ret = avfilter_link(buffersrcCtx, 0, in->filter_ctx, in->pad_idx)) < 0) + return fail("Failed to link buffer source to graph", ret); + + inputs_.push_back(buffersrcCtx); + if (simple) + inputNames_.push_back("default"); + else + inputNames_.push_back(in->name); + return ret; +} + +int +MediaFilter::fail(std::string msg, int err) +{ + if (!msg.empty()) + RING_ERR() << msg << ": " << libav_utils::getError(err); + return err; +} + +void +MediaFilter::clean() +{ + avfilter_graph_free(&graph_); + initialized_ = false; +} + +} // namespace ring diff --git a/src/media/media_filter.h b/src/media/media_filter.h new file mode 100644 index 0000000000000000000000000000000000000000..aa51a43fed5a5d8e58e517e5b74f1c2079566b04 --- /dev/null +++ b/src/media/media_filter.h @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * + * Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "config.h" +#include "media_stream.h" +#include "noncopyable.h" + +#include <map> +#include <string> +#include <vector> + +class AVFilterContext; +class AVFilterGraph; +class AVFilterInOut; + +namespace ring { + +/** + * Provides access to libavfilter. + * + * Can be used for simple filters (1 input, 1 output), or complex filters (multiple inputs, 1 output). + * Multiple outputs are not supported. They add complexity for little gain. + * + * For information on how to write a filter graph description, see: + * https://ffmpeg.org/ffmpeg-filters.html + * http://trac.ffmpeg.org/wiki/FilteringGuide + * + * For complex filters, it is required to name each filter graph input. These names are used to feed the correct input. + * It is the same name that will be passed as second argument to feedInput(AVFrame*, std::string). This is not required + * for simple filters, as there is only one input. + * + * Simple filter: "scale=320:240" + * Scales the input to 320x240. No need to specify input names. + * + * Complex filter: "[in1] scale=iw/4:ih/4 [mid]; [in2] [mid] overlay=main_w-overlay_w-10:main_h-overlay_h-10" + * in1 will be scaled to 1/16th its size and placed over in2 in the bottom right corner. When feeding frames to + * the filter, you need to specify whether the frame is destined for in1 or in2. + */ +class MediaFilter { + public: + MediaFilter(); + ~MediaFilter(); + + /** + * Returns the current filter graph string. + */ + std::string getFilterDesc() const; + + /** + * Initializes the filter graph with 1 input. + * + * NOTE This method will fail if @filterDesc has more than 1 input. + * NOTE Wraps @msp in a vector and calls initialize. + */ + int initialize(const std::string& filterDesc, MediaStream msp); + + /** + * Initializes the filter graph with one or more inputs and one output. Returns a negative code on error. + */ + int initialize(const std::string& filterDesc, std::vector<MediaStream> msps); + + /** + * Give the filter graph an input frame. Caller is responsible for freeing the frame. + * + * NOTE This is a wrapper for feedInput(AVFrame*, std::string) + * NOTE This is for filters with 1 input. + */ + int feedInput(AVFrame* frame); + + /** + * Give the specified source filter an input frame. Caller is responsible for freeing the frame. + * + * NOTE Will fail if @inputName is not found in the graph. + */ + int feedInput(AVFrame* frame, std::string inputName); + + /** + * Pull a frame from the filter graph. Caller owns the frame reference. + * + * Returns AVERROR(EAGAIN) if filter graph requires more input. + */ + AVFrame* readOutput(); // frame reference belongs to caller + + private: + NON_COPYABLE(MediaFilter); + + /** + * Initializes output of filter graph. + */ + int initOutputFilter(AVFilterInOut* out); + + /** + * Initializes an input of filter graph. + */ + int initInputFilter(AVFilterInOut* in, MediaStream msp, bool simple); + + /** + * Convenience method that prints @msg and returns err. + * + * NOTE @msg should not be null. + */ + int fail(std::string msg, int err); + + /** + * Frees resources used by MediaFilter. + */ + void clean(); + + /** + * Filter graph pointer. + */ + AVFilterGraph* graph_ = nullptr; + + /** + * Filter graph output. Corresponds to a buffersink/abuffersink filter. + */ + AVFilterContext* output_ = nullptr; + + /** + * List of filter graph inputs. Each corresponds to a buffer/abuffer filter. + */ + std::vector<AVFilterContext*> inputs_; + + /** + * List of filter graph input names. Same order as @inputs_. + */ + std::vector<std::string> inputNames_; + + /** + * Filter graph string. + */ + std::string desc_ {}; + + /** + * Flag to know whether or not the filter graph is initialized. + */ + bool initialized_ {false}; +}; + +}; // namespace ring diff --git a/src/media/media_stream.h b/src/media/media_stream.h new file mode 100644 index 0000000000000000000000000000000000000000..6f75dd78d76617c68e935ccbed6da4a661f63c22 --- /dev/null +++ b/src/media/media_stream.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * + * Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "config.h" +#include "rational.h" + +#include <string> + +namespace ring { + +struct MediaStream { + std::string name {}; + int format {-1}; + bool isVideo {false}; + rational<int> timeBase; + int width {0}; + int height {0}; + rational<int> aspectRatio; + rational<int> frameRate; + int sampleRate {0}; + int nbChannels {0}; + + MediaStream() + {} + + MediaStream(std::string name, int fmt, rational<int> tb, int w, int h, + rational<int> sar, rational<int> fr) + : name(name) + , format(fmt) + , isVideo(true) + , timeBase(tb) + , width(w) + , height(h) + , aspectRatio(sar) + , frameRate(fr) + {} + + MediaStream(std::string name, int fmt, rational<int> tb, int sr, int channels) + : name(name) + , format(fmt) + , isVideo(false) + , timeBase(tb) + , sampleRate(sr) + , nbChannels(channels) + {} +}; + +}; // namespace ring diff --git a/src/rational.h b/src/rational.h index 2f09e99e640e73849de805e0929daa770dcb5583..364c75f9f02b923b6413c8ca4fe839d31a8168a0 100644 --- a/src/rational.h +++ b/src/rational.h @@ -24,6 +24,10 @@ #include <cstdlib> // std::abs #include <iostream> +extern "C" { +#include <libavutil/rational.h> // specify conversions for AVRational +} + namespace ring { /** @@ -39,6 +43,10 @@ public: rational(I n) : num_(n) {}; // Equal to n/1 rational(I n, I d) : num_(n), den_(d) {}; // General case (n/d) + // Define conversions to and from AVRational (equivalent) + rational(AVRational r) : num_(r.num), den_(r.den) {}; + operator AVRational() { return AVRational{num_, den_}; } + // Normal copy constructors and assignment operators // Assignment from I diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index 7e71fa76781e871a1f8fb4db3b5a79fa32478f93..21cbe74b31e313bec933447267398f00d4613a7a 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -67,4 +67,9 @@ ut_video_input_SOURCES = media/video/testVideo_input.cpp check_PROGRAMS += ut_media_encoder ut_media_encoder_SOURCES = media/test_media_encoder.cpp +# media_filter +# +check_PROGRAMS += ut_media_filter +ut_media_filter_SOURCES = media/test_media_filter.cpp + TESTS = $(check_PROGRAMS) diff --git a/test/unitTest/media/test_media_filter.cpp b/test/unitTest/media/test_media_filter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..82d5f87f1942e85a16bc18d6618b456e54d4c43d --- /dev/null +++ b/test/unitTest/media/test_media_filter.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * + * Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include "dring.h" +#include "libav_deps.h" +#include "media_filter.h" + +#include "../../test_runner.h" + +namespace ring { namespace test { + +class MediaFilterTest : public CppUnit::TestFixture { +public: + static std::string name() { return "media_filter"; } + + void setUp(); + void tearDown(); + +private: + void testSimpleVideoFilter(); + void testSimpleAudioFilter(); + void testComplexVideoFilter(); + + CPPUNIT_TEST_SUITE(MediaFilterTest); + CPPUNIT_TEST(testSimpleVideoFilter); + CPPUNIT_TEST(testSimpleAudioFilter); + CPPUNIT_TEST(testComplexVideoFilter); + CPPUNIT_TEST_SUITE_END(); + + std::unique_ptr<MediaFilter> filter_; + AVFrame* frame_ = nullptr; + AVFrame* extra_ = nullptr; // used for filters with multiple inputs +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MediaFilterTest, MediaFilterTest::name()); + +void +MediaFilterTest::setUp() +{ + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + libav_utils::ring_avcodec_init(); + filter_.reset(new MediaFilter); +} + +void +MediaFilterTest::tearDown() +{ + av_frame_free(&frame_); + av_frame_free(&extra_); + DRing::fini(); +} + +static void +fill_yuv_image(uint8_t *data[4], int linesize[4], int width, int height, int frame_index) +{ + int x, y; + + /* Y */ + for (y = 0; y < height; y++) + for (x = 0; x < width; x++) + data[0][y * linesize[0] + x] = x + y + frame_index * 3; + + /* Cb and Cr */ + for (y = 0; y < height / 2; y++) { + for (x = 0; x < width / 2; x++) { + data[1][y * linesize[1] + x] = 128 + y + frame_index * 2; + data[2][y * linesize[2] + x] = 64 + x + frame_index * 5; + } + } +} + +static void +fill_samples(uint16_t* samples, int sampleRate, int nbSamples, int nbChannels, float tone) +{ + const constexpr float pi = 3.14159265358979323846264338327950288; // M_PI + const float tincr = 2 * pi * tone / sampleRate; + float t = 0; + + for (int i = 0; i < 200; ++i) { + for (int j = 0; j < nbSamples; ++j) { + samples[2 * j] = static_cast<int>(sin(t) * 10000); + for (int k = 1; k < nbChannels; ++k) { + samples[2 * j + k] = samples[2 * j]; + } + t += tincr; + } + } +} + +void +MediaFilterTest::testSimpleVideoFilter() +{ + std::string filterSpec = "scale=200x100"; + + // constants + const constexpr int width = 320; + const constexpr int height = 240; + const constexpr AVPixelFormat format = AV_PIX_FMT_YUV420P; + + // prepare video frame + frame_ = av_frame_alloc(); + frame_->format = format; + frame_->width = width; + frame_->height = height; + + // construct the filter parameters + rational<int> one = rational<int>(1); + auto params = MediaStream("vf", format, one, width, height, one, one); + + // allocate and fill frame buffers + CPPUNIT_ASSERT(av_frame_get_buffer(frame_, 32) >= 0); + fill_yuv_image(frame_->data, frame_->linesize, frame_->width, frame_->height, 0); + + // prepare filter + CPPUNIT_ASSERT(filter_->initialize(filterSpec, params) >= 0); + + // apply filter + CPPUNIT_ASSERT(filter_->feedInput(frame_) >= 0); + frame_ = filter_->readOutput(); + CPPUNIT_ASSERT(frame_); + + // check if the filter worked + CPPUNIT_ASSERT(frame_->width == 200 && frame_->height == 100); +} + +void +MediaFilterTest::testSimpleAudioFilter() +{ + std::string filterSpec = "aformat=sample_fmts=u8"; + + // constants + const constexpr int nbSamples = 100; + const constexpr int64_t channelLayout = AV_CH_LAYOUT_STEREO; + const constexpr int sampleRate = 44100; + const constexpr enum AVSampleFormat format = AV_SAMPLE_FMT_S16; + + // prepare audio frame + frame_ = av_frame_alloc(); + frame_->format = format; + frame_->channel_layout = channelLayout; + frame_->nb_samples = nbSamples; + frame_->sample_rate = sampleRate; + frame_->channels = av_get_channel_layout_nb_channels(channelLayout); + + // construct the filter parameters + auto params = MediaStream("af", format, rational<int>(1, 1), sampleRate, frame_->channels); + + // allocate and fill frame buffers + CPPUNIT_ASSERT(av_frame_get_buffer(frame_, 0) >= 0); + fill_samples(reinterpret_cast<uint16_t*>(frame_->data[0]), sampleRate, nbSamples, frame_->channels, 440.0); + + // prepare filter + CPPUNIT_ASSERT(filter_->initialize(filterSpec, params) >= 0); + + // apply filter + CPPUNIT_ASSERT(filter_->feedInput(frame_) >= 0); + frame_ = filter_->readOutput(); + CPPUNIT_ASSERT(frame_); + + // check if the filter worked + CPPUNIT_ASSERT(frame_->format == AV_SAMPLE_FMT_U8); +} + +void +MediaFilterTest::testComplexVideoFilter() +{ + std::string filterSpec = "[main] [top] overlay=main_w-overlay_w-10:main_h-overlay_h-10"; + std::string main = "main"; + std::string top = "top"; + + // constants + const constexpr int width1 = 320; + const constexpr int height1 = 240; + const constexpr int width2 = 30; + const constexpr int height2 = 30; + const constexpr AVPixelFormat format = AV_PIX_FMT_YUV420P; + + // prepare video frame + frame_ = av_frame_alloc(); + frame_->format = format; + frame_->width = width1; + frame_->height = height1; + extra_ = av_frame_alloc(); + extra_->format = format; + extra_->width = width2; + extra_->height = height2; + + // construct the filter parameters + rational<int> one = rational<int>(1); + auto params1 = MediaStream("main", format, one, width1, height1, one, one); + auto params2 = MediaStream("top", format, one, width2, height2, one, one); + + // allocate and fill frame buffers + CPPUNIT_ASSERT(av_frame_get_buffer(frame_, 32) >= 0); + fill_yuv_image(frame_->data, frame_->linesize, frame_->width, frame_->height, 0); + CPPUNIT_ASSERT(av_frame_get_buffer(extra_, 32) >= 0); + fill_yuv_image(extra_->data, extra_->linesize, extra_->width, extra_->height, 0); + + // prepare filter + auto vec = std::vector<MediaStream>(); + vec.push_back(params2); // order does not matter, as long as names match + vec.push_back(params1); + CPPUNIT_ASSERT(filter_->initialize(filterSpec, vec) >= 0); + + // apply filter + CPPUNIT_ASSERT(filter_->feedInput(frame_, main) >= 0); + CPPUNIT_ASSERT(filter_->feedInput(extra_, top) >= 0); + av_frame_free(&frame_); + av_frame_free(&extra_); + frame_ = filter_->readOutput(); + CPPUNIT_ASSERT(frame_); + + // check if the filter worked + CPPUNIT_ASSERT(frame_->width == width1 && frame_->height == height1); +} + +}} // namespace ring::test + +RING_TEST_RUNNER(ring::test::MediaFilterTest::name());