#// Copyright (C) 2023  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.

#ifdef DLIB_USE_FFMPEG

#include <fstream>
#include <vector>
#include <chrono>
#include <dlib/dir_nav.h>
#include <dlib/config_reader.h>
#include <dlib/media.h>
#include <dlib/array2d.h>
#include <dlib/matrix.h>
#include <dlib/rand.h>
#include <dlib/image_transforms.h>
#include <dlib/image_io.h>
#include "tester.h"

#ifndef DLIB_FFMPEG_DATA
static_assert(false, "Build is faulty. DLIB_VIDEOS_FILEPATH should be defined by cmake");
#endif

namespace  
{
    using namespace std;
    using namespace std::chrono;
    using namespace test;
    using namespace dlib;
    using namespace dlib::ffmpeg;
    
    logger dlog("test.video");

//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// UTILS
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
    
    dlib::rand rng(10000);

    template <class pixel>
    matrix<pixel> get_random_image()
    {
        matrix<pixel> img;

        img.set_size(rng.get_integer_in_range(1, 128),
                     rng.get_integer_in_range(1, 128));

        for (long i = 0 ; i < img.nr() ; ++i)
            for (long j = 0 ; j < img.nc() ; ++j)
                assign_pixel(img(i,j), rng.get_random_8bit_number());

        return img;
    }

    template <class pixel>
    void check_image (
        const frame& f,
        const matrix<pixel>& img
    )
    {
        DLIB_TEST(!f.is_empty());
        DLIB_TEST(f.is_image());
        DLIB_TEST(!f.is_audio());
        DLIB_TEST(f.samplefmt()    == AV_SAMPLE_FMT_NONE);
        DLIB_TEST(f.sample_rate()  == 0);
        DLIB_TEST(f.nsamples()     == 0);
        DLIB_TEST(f.layout()       == 0);
        DLIB_TEST(f.nchannels()    == 0);
        DLIB_TEST(f.pixfmt()       == pix_traits<pixel>::fmt);
        DLIB_TEST(f.height()       == img.nr());
        DLIB_TEST(f.width()        == img.nc());

        matrix<pixel> dummy;
        convert(f, dummy);
        DLIB_TEST(dummy.nc() == img.nc());
        DLIB_TEST(dummy.nr() == img.nr());
        DLIB_TEST(img == dummy);
    }

    template <class pixel>
    void test_frame()
    {
        const matrix<pixel> img1 = get_random_image<pixel>();
        const matrix<pixel> img2 = get_random_image<pixel>();

        // Create a frame
        frame f1(img1.nr(), img1.nc(), pix_traits<pixel>::fmt, system_clock::time_point{});
        convert(img1, f1);

        // Check frame is correct
        check_image(f1, img1);

        // Copy
        frame f2(f1);

        // Check it's a deepcopy, i.e. pointers are different but values are the same
        check_image(f2, img1);

        // Check pointers are different
        DLIB_TEST(f1.get_frame().data[0] != f2.get_frame().data[0]);

        // Set f2 and check this doesn't affect f1
        f2 = frame(img2.nr(), img2.nc(), pix_traits<pixel>::fmt, system_clock::time_point{});
        convert(img2, f2);

        check_image(f2, img2);
        check_image(f1, img1);
        DLIB_TEST(f1.get_frame().data[0] != f2.get_frame().data[0]);

        // Move
        f2 = std::move(f1);
        check_image(f2, img1);
        DLIB_TEST(f1.is_empty());

        print_spinner();
    }

    template <typename pixel_type>
    static double psnr(const matrix<pixel_type>& img1, const matrix<pixel_type>& img2)
    {
        DLIB_TEST(have_same_dimensions(img1, img2));
        const long nk           = width_step(img1) / img1.nc();
        const long data_size    = img1.size() * nk;
        auto* data1             = reinterpret_cast<const uint8_t*>(image_data(img1));
        auto* data2             = reinterpret_cast<const uint8_t*>(image_data(img2));

        double mse = 0;
        for (long i = 0; i < data_size; i += nk)
        {
            for (long k = 0; k < nk; ++k)
                mse += std::pow(static_cast<double>(data1[i + k]) - static_cast<double>(data2[i + k]), 2);
        }
        mse /= data_size;
        return 20 * std::log10(pixel_traits<pixel_type>::max()) - 10 * std::log10(mse);
    }

    void test_load_frame(const std::string& filename)
    {
        matrix<rgb_pixel> img1, img2;
        load_image(img1, filename);
        load_frame(img2, filename);
        DLIB_TEST(img1.nr() == img2.nr());
        DLIB_TEST(img1.nc() == img2.nc());
        const double similarity = psnr(img1, img2);
        DLIB_TEST_MSG(similarity > 25.0, "psnr " << similarity);
    }

    template<class pixel_type>
    void test_load_save_frame(const std::string& filename)
    {
        matrix<pixel_type> img1, img2;
        img1 = get_random_image<pixel_type>();

        save_frame(img1, filename, {{"qmin", "1"}, {"qmax", "1"}});
        load_frame(img2, filename);

        DLIB_TEST(img1.nr() == img2.nr());
        DLIB_TEST(img1.nc() == img2.nc());
        const double similarity = psnr(img1, img2);
        DLIB_TEST_MSG(similarity > 20.0, "psnr " << similarity);
    }

//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// DECODER
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////

    template <
      class image_type
    >
    void test_decoder_images_only(
        const std::string& filepath,
        const std::string& codec,
        const int nframes,
        const int height,
        const int width
    )
    {
        decoder dec([&] {
            decoder::args args;
            args.codec_name = codec;
            return args;
        }());

        DLIB_TEST(dec.is_open());
        DLIB_TEST(dec.get_codec_name() == codec);
        DLIB_TEST(dec.is_image_decoder());
        // You can't test for dec.height() or dec.width() yet because no data has been pushed to the decoder.

        image_type      img;
        int             counter{0};

        ifstream fin{filepath, std::ios::binary};
        std::vector<char> buf(1024);

        const auto callback = [&](image_type& img) mutable
        {
            DLIB_TEST(img.nr()      == height);
            DLIB_TEST(img.nc()      == width);
            DLIB_TEST(dec.height()  == height);
            DLIB_TEST(dec.width()   == width);
            ++counter;
            
            if (counter % 10 == 0)
                print_spinner();
        };

        while (fin)
        {
            fin.read(buf.data(), buf.size());
            size_t ret = fin.gcount();
            DLIB_TEST(dec.push((const uint8_t*)buf.data(), ret, wrap(callback)));
        }

        dec.flush(wrap(callback));
        DLIB_TEST(counter == nframes);
        DLIB_TEST(!dec.is_open());
    }

    template <
      class image_type
    >
    void test_decoder_full (
        const std::string&  filepath,
        const std::string&  codec,
        const int           nframes,
        const int           height,
        const int           width,
        const int           sample_rate,
        bool                has_image,
        bool                has_audio
    )
    {
        decoder dec([&] {
            decoder::args args;
            args.codec_name = codec;
            return args;
        }());

        DLIB_TEST(dec.is_open());
        DLIB_TEST(dec.get_codec_name() == codec);
        DLIB_TEST(dec.is_audio_decoder() == has_audio);
        DLIB_TEST(dec.is_image_decoder() == has_image);

        image_type                  img;
        audio<int16_t, 2>           audio_buf;
        frame                       frame_copy;
        int                         count{0};
        int                         nsamples{0};
        int                         iteration{0};

        ifstream fin{filepath, std::ios::binary};
        std::vector<char> buf(1024);

        const resizing_args args_image {
            0,
            0,
            pix_traits<pixel_type_t<image_type>>::fmt
        };

        const resampling_args args_audio {
            sample_rate,
            dlib::ffmpeg::details::get_layout_from_channels(decltype(audio_buf)::nchannels),
            sample_traits<decltype(audio_buf)::format>::fmt
        };

        const auto callback = [&](frame& f)
        {
            if (has_audio)
            {
                convert(f, audio_buf);
                convert(audio_buf, frame_copy);
                DLIB_TEST(f.is_audio());
                DLIB_TEST(f.sample_rate() == sample_rate);
                DLIB_TEST(f.samplefmt() == args_audio.fmt);
                DLIB_TEST(frame_copy.is_audio());
                DLIB_TEST(frame_copy.sample_rate() == sample_rate);
                DLIB_TEST(frame_copy.samplefmt() == args_audio.fmt);

                nsamples += f.nsamples();

                DLIB_TEST(dec.sample_rate() == sample_rate);
                DLIB_TEST(dec.sample_fmt() == args_audio.fmt);
            }
            else
            {
                convert(f, img);
                convert(img, frame_copy);
                DLIB_TEST(f.is_image());
                DLIB_TEST(f.height() == height);
                DLIB_TEST(f.width()  == width);
                DLIB_TEST(f.pixfmt() == args_image.fmt);
                DLIB_TEST(frame_copy.is_image());
                DLIB_TEST(frame_copy.height() == height);
                DLIB_TEST(frame_copy.width()  == width);
                DLIB_TEST(frame_copy.pixfmt() == args_image.fmt);

                DLIB_TEST(img.nr() == height);
                DLIB_TEST(img.nc() == width);
                ++count;

                DLIB_TEST(dec.height() == height);
                DLIB_TEST(dec.width() == width);
            }

            ++iteration;
            
            if (iteration % 10 == 0)
                print_spinner();
        };

        while (fin)
        {
            fin.read(buf.data(), buf.size());
            size_t ret = fin.gcount();
            DLIB_TEST(dec.push((const uint8_t*)buf.data(), ret, wrap(callback, args_image, args_audio)));
        }

        dec.flush(wrap(callback, args_image, args_audio));
        DLIB_TEST(count == nframes);
        DLIB_TEST(!dec.is_open());
    }

//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// DEMUXER
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////

    template <
      class image_type
    >
    void test_demuxer_images_only (
        const std::string& filepath,
        const int nframes,
        const int height,
        const int width
    )
    {
        demuxer cap({filepath, video_enabled, audio_enabled});
        DLIB_TEST(cap.is_open());
        DLIB_TEST(cap.video_enabled());
        DLIB_TEST(cap.height()  == height);
        DLIB_TEST(cap.width()   == width);
        // DLIB_TEST(cap.estimated_nframes() == nframes); // This won't always work with ffmpeg v3. v4 onwards is fine

        image_type img;
        int counter{0};

        while (cap.read(img))
        {
            DLIB_TEST(img.nr() == height);
            DLIB_TEST(img.nc() == width);
            ++counter;

            if (counter % 10 == 0)
                print_spinner();
        }

        DLIB_TEST(counter == nframes);
        DLIB_TEST(!cap.is_open());
    }

    void test_demuxer_full (
        const std::string& filepath,
        const int nframes,
        const int height,
        const int width,
        const int sample_rate,
        bool has_video,
        bool has_audio
    )
    {
        demuxer cap(filepath);
        DLIB_TEST(cap.video_enabled()       == has_video);
        DLIB_TEST(cap.audio_enabled()       == has_audio);
        DLIB_TEST(cap.height()              == height);
        DLIB_TEST(cap.width()               == width);
        DLIB_TEST(cap.sample_rate()         == sample_rate);
        // DLIB_TEST(cap.estimated_nframes()   == nframes); // This won't always work with ffmpeg v3. v4 onwards is fine
        const int estimated_samples_min = cap.estimated_total_samples() - cap.sample_rate(); // - 1s
        const int estimated_samples_max = cap.estimated_total_samples() + cap.sample_rate(); // + 1s

        dlib::ffmpeg::frame frame;
        int                 counter_images{0};
        int                 counter_samples{0};
        int                 iteration{0};

        resizing_args args_image;
        args_image.fmt = AV_PIX_FMT_RGB24;

        resampling_args args_audio;
        args_audio.sample_rate      = sample_rate;
        args_audio.fmt              = AV_SAMPLE_FMT_S16;
        args_audio.channel_layout   = AV_CH_LAYOUT_STEREO;

        while (cap.read(frame, args_image, args_audio))
        {
            if (frame.is_image())
            {
                DLIB_TEST(frame.height() == height);
                DLIB_TEST(frame.width()  == width);
                DLIB_TEST(frame.pixfmt() == args_image.fmt);
                
                ++counter_images;
            }

            if (frame.is_audio())
            {
                DLIB_TEST(frame.sample_rate()   == sample_rate);
                DLIB_TEST(frame.layout()        == args_audio.channel_layout);
                DLIB_TEST(frame.samplefmt()     == args_audio.fmt);
                counter_samples += frame.nsamples();
            }

            ++iteration;
            if (iteration % 10 == 0)
                print_spinner();
        }

        DLIB_TEST(counter_images == nframes);
        DLIB_TEST(counter_samples >= estimated_samples_min); //within 1 second
        DLIB_TEST(counter_samples <= estimated_samples_max); //within 1 second
        DLIB_TEST(!cap.is_open());
    }
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// ENCODER
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////

    void test_encoder (
        const std::string& filepath,
        AVCodecID image_codec,
        AVCodecID audio_codec
    )
    {
        // Load a video/audio as a source of frames
        demuxer cap(filepath);
        DLIB_TEST(cap.is_open());
        const bool has_video = cap.video_enabled();
        const bool has_audio = cap.audio_enabled();
        const int  height    = cap.height();
        const int  width     = cap.width();
        const auto pixfmt    = cap.pixel_fmt();
        const auto fps       = cap.fps();
        const int  rate      = cap.sample_rate();
        const auto samplefmt = cap.sample_fmt();
        const auto layout    = cap.channel_layout();

        std::vector<frame> frames;
        frame f;
        int nimages{0};
        int nsamples{0};

        while (cap.read(f))
        {
            if (f.is_image())
                ++nimages;
            if (f.is_audio())
                nsamples += f.nsamples();
            frames.push_back(std::move(f));
        }
            
        print_spinner();

        // Encoder configured using same parameters as video
        encoder enc_image, enc_audio;
        std::vector<uint8_t> buf_image, buf_audio;

        if (has_video)
        {
            enc_image = encoder([&] {
                encoder::args args;
                args.args_codec.codec       = image_codec;
                args.args_image.h           = height;
                args.args_image.w           = width;
                args.args_image.framerate   = fps;
                args.args_image.fmt         = AV_PIX_FMT_YUV420P;
                return args;
            }());

            DLIB_TEST(enc_image.is_open());
            DLIB_TEST(enc_image.is_image_encoder());
            DLIB_TEST(enc_image.get_codec_id()  == image_codec);
            DLIB_TEST(enc_image.height()        == height);
            DLIB_TEST(enc_image.width()         == width);
            // Can't check for framerate, as this might have been changed from requested value, due to codec availability.
            print_spinner();
        }

        if (has_audio)
        {
            enc_audio = encoder([&] {
                encoder::args args;
                args.args_codec.codec           = audio_codec;
                args.args_audio.sample_rate     = rate;
                args.args_audio.channel_layout  = layout;
                args.args_audio.fmt             = samplefmt;
                return args;
            }());

            DLIB_TEST(enc_audio.is_open());
            DLIB_TEST(enc_audio.is_audio_encoder());
            DLIB_TEST(enc_audio.get_codec_id() == audio_codec);
            print_spinner();
        }

        int iteration{0};

        for (auto& f : frames)
        {
            if (f.is_image())
                DLIB_TEST(enc_image.push(std::move(f), sink(buf_image)));
            
            if (f.is_audio())
                DLIB_TEST(enc_audio.push(std::move(f), sink(buf_audio)));

            if ((iteration++ % 10) == 0)
                print_spinner();
        }

        enc_image.flush(sink(buf_image));
        enc_audio.flush(sink(buf_audio));
        print_spinner();

        // Decode everything back
        decoder dec_image, dec_audio;
        std::queue<frame> queue;

        if (has_video)
        {
            dec_image = decoder([&] {
                decoder::args args;
                args.codec = image_codec;
                return args;
            }());

            DLIB_TEST(dec_image.is_open());
            DLIB_TEST(dec_image.is_image_decoder());
            DLIB_TEST(dec_image.get_codec_id() == image_codec);
            DLIB_TEST(dec_image.push(buf_image.data(), buf_image.size(), wrap(queue)));
            dec_image.flush(wrap(queue));

            int images = 0;

            while (!queue.empty())
            {
                auto f = std::move(queue.front());
                queue.pop();

                ++images;
                DLIB_TEST(f.height()            == height);
                DLIB_TEST(f.width()             == width);
                DLIB_TEST(dec_image.height()    == height);
                DLIB_TEST(dec_image.width()     == width);
                print_spinner();
            }

            DLIB_TEST(images == nimages);
        }

        if (has_audio)
        {
            dec_audio = decoder([&] {
                decoder::args args;
                args.codec = audio_codec;
                return args;
            }());

            DLIB_TEST(dec_audio.is_open());
            DLIB_TEST(dec_audio.is_audio_decoder());
            DLIB_TEST(dec_audio.get_codec_id()  == audio_codec);
            DLIB_TEST(dec_audio.push(buf_audio.data(), buf_audio.size(), wrap(queue, {}, {rate})));
            dec_audio.flush(wrap(queue, {}, {rate}));

            int samples = 0;

            while (!queue.empty())
            {
                auto f = std::move(queue.front());
                queue.pop();

                samples += f.nsamples();
                DLIB_TEST(f.sample_rate() == rate);
                print_spinner();
            }

            DLIB_TEST(samples > (nsamples - rate));
            DLIB_TEST(samples < (nsamples + rate));
        }
    }

//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// MUXER
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////

    template<class image_type>
    void test_muxer1 (
        const std::string& filepath,
        AVCodecID image_codec
    )
    {
        const std::string tmpfile = "dummy.avi";

        // Load a video/audio as a source of frames
        demuxer cap({filepath, video_enabled, audio_disabled});
        DLIB_TEST(cap.is_open());
        DLIB_TEST(cap.video_enabled());
        DLIB_TEST(!cap.audio_enabled());
        const int height = cap.height();
        const int width  = cap.width();

        // Open muxer
        muxer writer([&] {
            muxer::args args;
            args.filepath = tmpfile;
            args.enable_audio = false;
            args.args_image.codec        = image_codec;
            args.args_image.h            = cap.height();
            args.args_image.w            = cap.width();
            args.args_image.framerate    = cap.fps();
            args.args_image.fmt          = AV_PIX_FMT_YUV420P;
            return args;
        }());

        DLIB_TEST(writer.is_open());
        DLIB_TEST(!writer.audio_enabled());
        DLIB_TEST(writer.video_enabled());

        DLIB_TEST(writer.get_video_codec_id()   == image_codec);
        DLIB_TEST(writer.height()               == cap.height());
        DLIB_TEST(writer.width()                == cap.width());

        // Demux then remux
        int nimages_demuxed{0};
        image_type img;

        while (cap.read(img))
        {
            ++nimages_demuxed;
            DLIB_TEST(img.nr() == height);
            DLIB_TEST(img.nc() == width);
            DLIB_TEST(writer.push(img));

            if (nimages_demuxed % 10 == 0)
                print_spinner();
        }

        writer.flush();

        // Demux everything back
        demuxer cap2(tmpfile);
        DLIB_TEST(cap2.is_open());
        DLIB_TEST(cap2.video_enabled());
        DLIB_TEST(!cap2.audio_enabled());
        DLIB_TEST(cap2.get_video_codec_id() == image_codec);
        DLIB_TEST(cap2.height() == height);
        DLIB_TEST(cap2.width()  == width);

        int nimages_muxed{0};

        while (cap2.read(img))
        {
            ++nimages_muxed;
            if (nimages_muxed % 10 == 0)
                print_spinner();
        }

        DLIB_TEST(nimages_muxed == nimages_demuxed);
    }

    void test_muxer2 (
        const std::string& filepath,
        AVCodecID image_codec,
        AVCodecID audio_codec
    )
    {
        const std::string tmpfile = "dummy.avi";

        // Load a video/audio as a source of frames
        demuxer cap(filepath);
        DLIB_TEST(cap.is_open());
        const bool has_video = cap.video_enabled();
        const bool has_audio = cap.audio_enabled();
        const int  height    = cap.height();
        const int  width     = cap.width();
        const auto pixfmt    = cap.pixel_fmt();
        const auto fps       = cap.fps();
        const int  rate      = cap.sample_rate();
        const auto samplefmt = cap.sample_fmt();
        const auto layout    = cap.channel_layout();

        std::vector<frame> frames;
        frame f;
        int nimages{0};
        int nsamples{0};

        while (cap.read(f))
        {
            if (f.is_image())
                ++nimages;
            if (f.is_audio())
                nsamples += f.nsamples();
            frames.push_back(std::move(f));
        }
            
        print_spinner();

        // Muxer configured using parameters in video
        {
            muxer writer([&] {
                muxer::args args;
                args.filepath = tmpfile;
                args.enable_image = has_video;
                args.enable_audio = has_audio;

                if (has_video)
                {
                    args.args_image.codec        = image_codec;
                    args.args_image.h            = height;
                    args.args_image.w            = width;
                    args.args_image.framerate    = fps;
                    args.args_image.fmt          = AV_PIX_FMT_YUV420P;
                }

                if (has_audio)
                {
                    args.args_audio.codec            = audio_codec;
                    args.args_audio.sample_rate      = rate;
                    args.args_audio.channel_layout   = layout;
                    args.args_audio.fmt              = samplefmt;
                }

                return args;
            }());

            DLIB_TEST(writer.is_open());
            DLIB_TEST(writer.audio_enabled() == has_audio);
            DLIB_TEST(writer.video_enabled() == has_video);

            if (has_video)
            {
                DLIB_TEST(writer.get_video_codec_id()   == image_codec);
                DLIB_TEST(writer.height()               == height);
                DLIB_TEST(writer.width()                == width);
            }

            if (has_audio)
            {
                DLIB_TEST(writer.get_audio_codec_id() == audio_codec);
                //You can't guarantee that the requested sample rate or sample format are supported.
                //In which case, the object changes them to values that ARE supported. So we can't add
                //tests that check the sample rate is set to what we asked for.
            } 

            for (auto& f : frames)
                writer.push(std::move(f));

            // muxer.flush(); // You don't need to call this since muxer's destructor already does.
        }

        // Demux everything back
        demuxer cap2(tmpfile);
        DLIB_TEST(cap2.is_open());
        DLIB_TEST(cap2.video_enabled()       == has_video);
        DLIB_TEST(cap2.audio_enabled()       == has_audio);
        if (has_video)
            DLIB_TEST(cap2.get_video_codec_id() == image_codec);
        if (has_audio)
            DLIB_TEST(cap2.get_audio_codec_id() == audio_codec);
        DLIB_TEST(cap2.height() == height);
        DLIB_TEST(cap2.width()  == width);
        // Can't test for sample_rate since muxer may have changed it due to codec availability.

        int images{0};
        int samples{0};
        int iteration{0};

        while (cap2.read(f, {}, {rate}))
        {
            if (f.is_image())
                ++images;

            if (f.is_audio())
                samples += f.nsamples();

            ++iteration;
            if (iteration % 10 == 0)
                print_spinner();
        }

        DLIB_TEST(images == nimages);
        DLIB_TEST(samples >= (nsamples - rate));
        DLIB_TEST(samples <= (nsamples + rate));
    }

    const auto codec_supported = [](const AVCodecID id)
    {
        return std::find_if(begin(list_codecs()), end(list_codecs()), [=](const auto& supported) {
            return supported.codec_id == id && supported.supports_encoding;
        }) != end(list_codecs());
    };

    class video_tester : public tester
    {
    public:
        video_tester (
        ) :
            tester ("test_ffmpeg",
                    "Runs tests on video IO.")
        {}

        void perform_test (
        )
        {
            for (int i = 0 ; i < 10 ; ++i)
            {
                test_frame<rgb_pixel>();
                test_frame<bgr_pixel>();
                test_frame<rgb_alpha_pixel>();
                test_frame<bgr_alpha_pixel>();

                if (codec_supported(AV_CODEC_ID_PNG))
                {
                    test_load_save_frame<rgb_pixel>("dummy.png");
                    test_load_save_frame<bgr_pixel>("dummy.png");
                }

                if (codec_supported(AV_CODEC_ID_MJPEG))
                {
                    test_load_save_frame<rgb_pixel>("dummy.jpg");
                    test_load_save_frame<bgr_pixel>("dummy.jpg");
                }

                if (codec_supported(AV_CODEC_ID_BMP))
                {
                    test_load_save_frame<rgb_pixel>("dummy.bmp");
                    test_load_save_frame<bgr_pixel>("dummy.bmp");
                }

                if (codec_supported(AV_CODEC_ID_TIFF))
                {
                    test_load_save_frame<rgb_pixel>("dummy.tiff");
                    test_load_save_frame<bgr_pixel>("dummy.tiff");
                }
            }

            dlib::file f(DLIB_FFMPEG_DATA);
            dlib::config_reader cfg(f.full_name());

            {
                const auto& image_block = cfg.block("images");
                std::vector<string> blocks;
                image_block.get_blocks(blocks);

                for (const auto& block : blocks)
                {
                    const auto& sublock = image_block.block(block);
                    const std::string filepath = get_parent_directory(f).full_name() + "/" + sublock["file"];

                    test_load_frame(filepath);
                }
            }

            {
                const auto& video_raw_block = cfg.block("decoding");
                std::vector<string> blocks;
                video_raw_block.get_blocks(blocks);

                for (const auto& block : blocks)
                {
                    const auto& sublock = video_raw_block.block(block);
                    const std::string filepath  = get_parent_directory(f).full_name() + "/" + sublock["file"];
                    const std::string codec     = dlib::get_option(sublock, "codec", "");
                    const int nframes           = dlib::get_option(sublock, "nframes", 0);
                    const int height            = dlib::get_option(sublock, "height", 0);
                    const int width             = dlib::get_option(sublock, "width", 0);
                    const int sample_rate       = dlib::get_option(sublock, "sample_rate", 0);
                    const bool has_image        = height > 0 && width > 0;
                    const bool has_audio        = sample_rate > 0;   

                    if (has_image)
                    {
                        test_decoder_images_only<array2d<rgb_pixel>>(filepath, codec, nframes, height, width);
                        test_decoder_images_only<array2d<rgb_alpha_pixel>>(filepath, codec, nframes, height, width);
                        test_decoder_images_only<matrix<bgr_pixel>>(filepath, codec, nframes, height, width);
                        test_decoder_images_only<matrix<bgr_alpha_pixel>>(filepath, codec, nframes, height, width);
                    } 

                    test_decoder_full<array2d<rgb_pixel>>(filepath, codec, nframes, height, width, sample_rate, has_image, has_audio);
                    test_decoder_full<matrix<bgr_pixel>>(filepath, codec, nframes, height, width, sample_rate, has_image, has_audio);
                }
            }

            {
                const auto& video_file_block = cfg.block("demuxing");
                std::vector<string> blocks;
                video_file_block.get_blocks(blocks);

                for (const auto& block : blocks)
                {
                    const auto& sublock = video_file_block.block(block);
                    const std::string filepath = get_parent_directory(f).full_name() + "/" + sublock["file"];

                    const std::string tmpfile = "dummy.avi";
                    const int nframes       = dlib::get_option(sublock, "nframes", 0);
                    const int height        = dlib::get_option(sublock, "height", 0);
                    const int width         = dlib::get_option(sublock, "width", 0);
                    const int sample_rate   = dlib::get_option(sublock, "sample_rate", 0);
                    const bool has_video    = height > 0 && width > 0 && nframes > 0;
                    const bool has_audio    = sample_rate > 0;

                    if (has_video)
                    {
                        test_demuxer_images_only<array2d<rgb_pixel>>(filepath, nframes, height, width);
                        test_demuxer_images_only<matrix<bgr_pixel>>(filepath, nframes, height, width);
                    }

                    test_demuxer_full(filepath, nframes, height, width, sample_rate, has_video, has_audio);
                    test_encoder(filepath, AV_CODEC_ID_MPEG4, AV_CODEC_ID_AC3);
                    
                    if (has_video)
                    {
                        test_muxer1<array2d<rgb_pixel>>(filepath, AV_CODEC_ID_MPEG4);
                        test_muxer1<matrix<bgr_pixel>>(filepath, AV_CODEC_ID_MPEG4);
                    }
                    
                    test_muxer2(filepath, AV_CODEC_ID_MPEG4, AV_CODEC_ID_AC3);
                }
            }
        }
    } a;
}

#endif