diff --git a/src/avcpp/formatcontext.cpp b/src/avcpp/formatcontext.cpp index b2241ab9..097eaf9f 100644 --- a/src/avcpp/formatcontext.cpp +++ b/src/avcpp/formatcontext.cpp @@ -69,7 +69,7 @@ void set_uri(AVFormatContext *ctx, string_view uri) #if AVCPP_API_AVFORMAT_URL if (ctx->url) av_free(ctx->url); - ctx->url = av_strdup(uri.data()); + ctx->url = av_strndup(uri.data(), uri.size()); #else av_strlcpy(ctx->filename, uri.data(), std::min(sizeof(ctx->filename), uri.size() + 1)); ctx->filename[uri.size()] = '\0'; @@ -195,10 +195,10 @@ void FormatContext::close() m_headerWriten = false; // To prevent free not out custom IO, e.g. setted via raw pointer access - if (m_customIO) { + if (m_customIO && avio) { // Close custom IO av_freep(&avio->buffer); - av_freep(&avio); + avio_context_free(&avio); m_customIO = false; } } @@ -606,22 +606,16 @@ Packet FormatContext::readPacket(OptionalErrorCode ec) void FormatContext::openOutput(const string &uri, OptionalErrorCode ec) { - return openOutput(uri, OutputFormat(), nullptr, ec); + openOutput(uri, OutputFormat(), nullptr, ec); } void FormatContext::openOutput(const string &uri, Dictionary &options, OptionalErrorCode ec) { auto ptr = options.release(); - try - { - openOutput(uri, OutputFormat(), &ptr, ec); - options.assign(ptr); - } - catch (const Exception&) - { + ScopeOutAction onScopeExit{[ptr, &options] { options.assign(ptr); - throw; - } + }}; + openOutput(uri, OutputFormat(), &ptr, ec); } void FormatContext::openOutput(const string &uri, Dictionary &&options, OptionalErrorCode ec) @@ -629,6 +623,25 @@ void FormatContext::openOutput(const string &uri, Dictionary &&options, Optional return openOutput(uri, options, ec); } +void FormatContext::openOutput(const std::string &uri, OutputFormat format, OptionalErrorCode ec) +{ + openOutput(uri, format, nullptr, ec); +} + +void FormatContext::openOutput(const std::string &uri, Dictionary &options, OutputFormat format, OptionalErrorCode ec) +{ + auto ptr = options.release(); + ScopeOutAction onScopeExit{[ptr, &options] { + options.assign(ptr); + }}; + openOutput(uri, format, &ptr, ec); +} + +void FormatContext::openOutput(const std::string &uri, Dictionary &&options, OutputFormat format, OptionalErrorCode ec) +{ + openOutput(uri, options, format, ec); +} + void FormatContext::openOutput(const string &uri, OutputFormat format, AVDictionary **options, OptionalErrorCode ec) { clear_if(ec); @@ -693,6 +706,57 @@ void FormatContext::openOutput(const string &uri, OutputFormat format, AVDiction m_isOpened = true; } +bool FormatContext::initOutput(Dictionary &options, bool closeOnError, OptionalErrorCode ec) +{ + auto dict = options.release(); + ScopeOutAction onScopeExit([this, &dict, &options, ec, closeOnError](){ + options.assign(dict); + //fflog(AV_LOG_ERROR, "init output.... done with %s\n", (is_error(ec) || std::uncaught_exceptions() > 0) ? "error" : "no error"); + if (closeOnError && (is_error(ec) || std::uncaught_exceptions() > 0)) { + close(); + } + }); + return initOutput(&dict, ec); +} + +bool FormatContext::initOutput(AVDictionary **options, OptionalErrorCode ec) +{ + clear_if(ec); + + if (!isOpened()) { + throws_if(ec, Errors::FormatNotOpened); + return false; + } + + if (!isOutput()) { + throws_if(ec, Errors::FormatInvalidDirection); + return false; + } + + // just silent it??? + if (m_headerWriten) { + return true; + } + + resetSocketAccess(); + int ret = avformat_init_output(m_raw, options); + ret = checkPbError(ret); + if (ret < 0) { + throws_if(ec, ret, ffmpeg_category()); + return false; + } + + fflog(AV_LOG_ERROR, "avformat_init_output: ret = %d\n", ret); + + switch (ret) { + case AVSTREAM_INIT_IN_INIT_OUTPUT: + return true; + case AVSTREAM_INIT_IN_WRITE_HEADER: + default: + return false; + } +} + void FormatContext::openOutput(CustomIO *io, OptionalErrorCode ec, size_t internalBufferSize) { openCustomIOOutput(io, internalBufferSize, ec); @@ -702,6 +766,66 @@ void FormatContext::openOutput(CustomIO *io, OptionalErrorCode ec, size_t intern } } +bool FormatContext::openOutput(CustomIO *io, Dictionary &options, OptionalErrorCode ec, size_t internalBufferSize) +{ + openOutput(io, ec, internalBufferSize); + if (!is_error(ec)) { + return initOutput(options, true, ec); + } + return false; +} + +bool FormatContext::openOutput(CustomIO *io, Dictionary &&formatOptions, OptionalErrorCode ec, size_t internalBufferSize) +{ + openOutput(io, ec, internalBufferSize); + if (!is_error(ec)) { + return initOutput(formatOptions, true, ec); + } + return false; +} + +void FormatContext::openOutput(CustomIO *io, OutputFormat format, OptionalErrorCode ec, size_t internalBufferSize) +{ + if (format.isNull()) + format = outputFormat(); + else + setFormat(format); + openOutput(io, ec, internalBufferSize); +} + +bool FormatContext::openOutput(CustomIO *io, Dictionary &formatOptions, OutputFormat format, OptionalErrorCode ec, size_t internalBufferSize) +{ + openOutput(io, format, ec, internalBufferSize); + if (!is_error(ec)) { + return initOutput(formatOptions, true, ec); + } + return false; +} + +bool FormatContext::openOutput(CustomIO *io, Dictionary &&formatOptions, OutputFormat format, OptionalErrorCode ec, size_t internalBufferSize) +{ + openOutput(io, format, ec, internalBufferSize); + if (!is_error(ec)) { + return initOutput(formatOptions, true, ec); + } + return false; +} + +bool FormatContext::initOutput(OptionalErrorCode ec) +{ + return initOutput(nullptr, ec); +} + +bool FormatContext::initOutput(Dictionary &options, OptionalErrorCode ec) +{ + return initOutput(options, false, ec); +} + +bool FormatContext::initOutput(Dictionary &&options, OptionalErrorCode ec) +{ + return initOutput(options, false, ec); +} + void FormatContext::writeHeader(OptionalErrorCode ec) { writeHeader(nullptr, ec); @@ -729,6 +853,11 @@ void FormatContext::writeHeader(AVDictionary **options, OptionalErrorCode ec) { clear_if(ec); + if (m_headerWriten) { + // TBD: just silent it? + return; + } + if (!isOpened()) { throws_if(ec, Errors::FormatNotOpened); @@ -1051,6 +1180,12 @@ void FormatContext::openCustomIO(CustomIO *io, size_t internalBufferSize, bool i { clear_if(ec); + if (!io) { + fflog(AV_LOG_ERROR, "Open CustomIO with null io context"); + throws_if(ec, Errors::InvalidArgument); + return; + } + if (!m_raw) { throws_if(ec, Errors::Unallocated); @@ -1067,26 +1202,24 @@ void FormatContext::openCustomIO(CustomIO *io, size_t internalBufferSize, bool i AVIOContext *ctx = nullptr; // Note: buffer must be allocated only with av_malloc() and friends - uint8_t *internalBuffer = (uint8_t*)av_mallocz(internalBufferSize); - if (!internalBuffer) - { + auto internalBuffer = av::mallocz(internalBufferSize); + if (!internalBuffer) { throws_if(ec, ENOMEM, std::system_category()); return; } - ctx = avio_alloc_context(internalBuffer, internalBufferSize, isWritable, (void*)(io), custom_io_read, custom_io_write, custom_io_seek); - if (ctx) - { + ctx = avio_alloc_context(internalBuffer.get(), internalBufferSize, isWritable, (void*)(io), custom_io_read, custom_io_write, custom_io_seek); + if (ctx) { ctx->seekable = io->seekable(); m_raw->flags |= AVFMT_FLAG_CUSTOM_IO; m_customIO = true; - } - else - { + } else { throws_if(ec, ENOMEM, std::system_category()); return; } + internalBuffer.release(); // drop owning + m_raw->pb = ctx; } diff --git a/src/avcpp/formatcontext.h b/src/avcpp/formatcontext.h index 2682417b..be511015 100644 --- a/src/avcpp/formatcontext.h +++ b/src/avcpp/formatcontext.h @@ -24,30 +24,103 @@ namespace av { using AvioInterruptCb = std::function; +/** + * Interface class to customize IO operations with FormatContext + * + * It can be used for: + * - From memory I/O + * - mmapped files I/O + * - Specific containers reading (tar, zip files, for example) + * - Device I/O + * - Network I/O + * - Pattern generation + * - Image output to the display (for the appropriate formats, like rawvideo) + * + * CustomIO object is not owned by the av::FormatContext and must be alived across av::FormatContext life. + * + */ struct CustomIO { virtual ~CustomIO() {} + + /** + * Write part of data to the destination. + * + * Note, FFmpeg does not consider return value ​​>0 as the number of bytes actually written and assumes that + * all data has been written. Be careful. If data can't be fit into destination better return appropriate + * AVERROR code. + * + * @see avio_write() + * + * @param data block of data to write + * @param size size of the data block + * + * @return >=0 on success. AVERROR(xxx) for the system errors (errno wrap) or AVERROR_xxx codes. + */ virtual int write(const uint8_t *data, size_t size) { static_cast(data); static_cast(size); return -1; } + + /** + * Read part of data from the data source + * + * Note, if requested more data that exists to read, you should fill buffer as much as possible and return + * actual count of the readed data. 0 readed bytes is a valid case, but return it only if there is temporary issues + * with upstream that can be solved quickly. Otherwise return AVERROR_EOF of AVERROR(EBUSY)/AVERROR(EAGAIN). + * + * @see avio_read() + * + * @param data buffer to store data + * @param size size of the buffer + * + * @return count of the actually readed data. AVERROR(xxx) for the system errors (errno wrap) or AVERROR_xxx for the + * FFmpeg errors. AVERROR_EOF should be returns when end of file reached. + */ virtual int read(uint8_t *data, size_t size) { static_cast(data); static_cast(size); return -1; } - /// whence is a one of SEEK_* from stdio.h + + /** + * Seek in stream. + * + * @a whence may support special FFmpeg-ralated values: + * - AVSEEK_SIZE - return size without actual seeking. If unsupported, seek() may return <0 + * - AVSEEK_FORCE - read official FFmpeg documentation. Just ignore it. + * + * @see avio_seek() + * + * @param offset offset, may be less zero, equal zero or greater zero. + * @param whence is a one of SEEK_* from stdio.h + * + * @return new position or AVERROR + */ virtual int64_t seek(int64_t offset, int whence) { static_cast(offset); static_cast(whence); return -1; } - /// Return combination of AVIO_SEEKABLE_* flags or zero + + /** + * Defines supported types of buffer seeking + * + * Used to fill AVIOContext::seekable. In any case, seek() work on the byte level. + * + * @return combination of AVIO_SEEKABLE_* flags or zero + */ virtual int seekable() const { return 0; } + + /** + * Name of the I/O. Didn't used for now. + * + * @return null-terminated name of the Custom I/O implementation + */ virtual const char* name() const { return ""; } }; @@ -190,12 +263,48 @@ class FormatContext : public FFWrapperPtr, public noncopyable void openOutput(const std::string& uri, Dictionary &options, OptionalErrorCode ec = throws()); void openOutput(const std::string& uri, Dictionary &&options, OptionalErrorCode ec = throws()); - // TBD - //void openOutput(const std::string& uri, OutputFormat format, OptionalErrorCode ec = throws()); - //void openOutput(const std::string& uri, Dictionary &options, OutputFormat format, OptionalErrorCode ec = throws()); - //void openOutput(const std::string& uri, Dictionary &&options, OutputFormat format, OptionalErrorCode ec = throws()); + void openOutput(const std::string& uri, OutputFormat format, OptionalErrorCode ec = throws()); + void openOutput(const std::string& uri, Dictionary &options, OutputFormat format, OptionalErrorCode ec = throws()); + void openOutput(const std::string& uri, Dictionary &&options, OutputFormat format, OptionalErrorCode ec = throws()); + /// @{ + /** + * Open Output with Custom IO + * + * Variants with @ref formatOptions internally calls @ref initOutput() with same return value meaning. + * + * @param io + * @param formatOptions + * @param format + * @param ec + * @param internalBufferSize + */ void openOutput(CustomIO *io, OptionalErrorCode ec = throws(), size_t internalBufferSize = CUSTOM_IO_DEFAULT_BUFFER_SIZE); + bool openOutput(CustomIO *io, Dictionary &formatOptions, OptionalErrorCode ec = throws(), size_t internalBufferSize = CUSTOM_IO_DEFAULT_BUFFER_SIZE); + bool openOutput(CustomIO *io, Dictionary &&formatOptions, OptionalErrorCode ec = throws(), size_t internalBufferSize = CUSTOM_IO_DEFAULT_BUFFER_SIZE); + + void openOutput(CustomIO *io, OutputFormat format, OptionalErrorCode ec = throws(), size_t internalBufferSize = CUSTOM_IO_DEFAULT_BUFFER_SIZE); + bool openOutput(CustomIO *io, Dictionary &formatOptions, OutputFormat format, OptionalErrorCode ec = throws(), size_t internalBufferSize = CUSTOM_IO_DEFAULT_BUFFER_SIZE); + bool openOutput(CustomIO *io, Dictionary &&formatOptions, OutputFormat format, OptionalErrorCode ec = throws(), size_t internalBufferSize = CUSTOM_IO_DEFAULT_BUFFER_SIZE); + /// @} + + /// @{ + /** + * Init output without header writing + * + * If dictionary passed, writeHeader() should not be called with the same dictioary. + * + * @param options format options in the dictionary form + * @param ec holder for the error code. If not-null and error code set return value has not matter + * + * @retval true format inited in avformat_init_output, @see AVSTREAM_INIT_IN_INIT_OUTPUT + * @retval false format inited (will be) in avformat_write_header(), @see AVSTREAM_INIT_IN_WRITE_HEADER + * + */ + bool initOutput(OptionalErrorCode ec = throws()); + bool initOutput(Dictionary &options, OptionalErrorCode ec = throws()); + bool initOutput(Dictionary &&options, OptionalErrorCode ec = throws()); + /// @} void writeHeader(OptionalErrorCode ec = throws()); void writeHeader(Dictionary &options, OptionalErrorCode ec = throws()); @@ -219,7 +328,9 @@ class FormatContext : public FFWrapperPtr, public noncopyable private: void openInput(const std::string& uri, InputFormat format, AVDictionary **options, OptionalErrorCode ec); void openOutput(const std::string& uri, OutputFormat format, AVDictionary **options, OptionalErrorCode ec); - void writeHeader(AVDictionary **options, OptionalErrorCode ec = throws()); + bool initOutput(Dictionary &options, bool closeOnError, OptionalErrorCode ec); + bool initOutput(AVDictionary **options, OptionalErrorCode ec); + void writeHeader(AVDictionary **options, OptionalErrorCode ec); void writePacket(const Packet &pkt, OptionalErrorCode ec, int(*write_proc)(AVFormatContext *, AVPacket *)); void writeFrame(AVFrame *frame, int streamIndex, OptionalErrorCode ec, int(*write_proc)(AVFormatContext*,int,AVFrame*)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 22e1b1dc..9fcdd356 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,7 +14,8 @@ add_executable(test_executor Codec.cpp PixelSampleFormat.cpp Common.cpp - Buffer.cpp) + Buffer.cpp + FormatCustomIO_test.cpp) target_link_libraries(test_executor PUBLIC Catch2::Catch2WithMain avcpp::avcpp) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../catch2/contrib") diff --git a/tests/FormatCustomIO_test.cpp b/tests/FormatCustomIO_test.cpp new file mode 100644 index 00000000..0a6fec98 --- /dev/null +++ b/tests/FormatCustomIO_test.cpp @@ -0,0 +1,336 @@ +#include +#include "catch2/catch_message.hpp" + +#include "avcpp/format.h" +#include "avcpp/formatcontext.h" + +#if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20) && defined(__cpp_lib_format) + +#include + +static constexpr std::size_t ImageW = 640; +static constexpr std::size_t ImageH = 480; +static constexpr std::size_t ImageCount = 11; +static constexpr av::PixelFormat ImagePixFmt = AV_PIX_FMT_GRAY8; + +static const std::size_t FrameSizeBytes = ImageW * ImageH * ImagePixFmt.bitsPerPixel() / 8; + +struct TestBufferIo : public av::CustomIO +{ +public: + std::vector _buffer{}; + std::vector::iterator _pos{}; + + TestBufferIo() + { + _buffer.resize(FrameSizeBytes * ImageCount); + _pos = _buffer.begin(); + } + + std::size_t remain() const noexcept + { + return std::ranges::distance(_pos, _buffer.end()); + } + + bool eof() const noexcept + { + return _pos == _buffer.end(); + } + + void fillPattern(uint8_t patternOffset) + { + std::span out = _buffer; + for (auto i = 0u; i < ImageCount; ++i) { + auto buf = out.subspan(i * FrameSizeBytes, FrameSizeBytes); + std::ranges::fill(buf, uint8_t(i + patternOffset)); + } + } + + void fill(uint8_t val = 0xFF) + { + std::ranges::fill(_buffer, val); + } + + // + // Iface + // + + int write(const uint8_t *data, size_t size) final + { + UNSCOPED_INFO("IO::write: size=" << size); + // ENOSPC + // EWOULDBLOCK + // EMSGSIZE + // Do not write + if (size > remain()) { + UNSCOPED_INFO("IO::write too big: " << size); + return AVERROR(EMSGSIZE); + } + + std::ranges::copy_n(data, size, _pos); + std::advance(_pos, size); + + return 0; + } + + int read(uint8_t *data, size_t size) final + { + UNSCOPED_INFO("IO::read: size=" << size); + if (eof()) { + INFO("IO::read: EOF reached"); + return AVERROR_EOF; + } + + auto readed = std::min(size, remain()); + std::ranges::copy_n(_pos, readed, data); + std::advance(_pos, readed); + return readed; + } + + int64_t seek(int64_t offset, int whence) final + { + UNSCOPED_INFO("IO::seek: offset=" << offset << ", whence=0x" << std::hex << whence << std::dec); + + if (whence & AVSEEK_SIZE) { + return _buffer.size(); + } + + ssize_t cur = -1; + + if (whence == SEEK_CUR) { + cur = std::distance(_buffer.begin(), _pos); + } else if (whence == SEEK_SET) { + cur = 0; + } else if (whence == SEEK_END) { + cur = _buffer.size() - 1; + } else { + AVERROR(EINVAL); + } + + assert(cur >= 0 && cur <= _buffer.size() - 1); + + auto next = cur + offset; + if (next >= _buffer.size() || next < 0) + return AVERROR(EINVAL); + std::advance(_pos, offset); + return next; + } + + int seekable() const final + { + return AVIO_SEEKABLE_NORMAL; + } + + const char *name() const final + { + return "TestBufferIo"; + } +}; + + +TEST_CASE("Format Custom IO checks", "[FormatCustomIo]") +{ + SECTION("CustomIo :: Basic") + { + TestBufferIo customIoIn; + TestBufferIo customIoOut; + static auto const VideoSize = std::format("{}x{}", ImageW, ImageH); + static auto const FrameSizeBytes = ImageW * ImageH * ImagePixFmt.bitsPerPixel() / 8; + static auto const PatternOffset = 100u; + + // Fill In buffer with pattern + customIoIn.fillPattern(PatternOffset); + + // Fill Out buffer with FF + customIoOut.fill(); + + { + av::FormatContext ictx; + ictx.openInput(&customIoIn, + av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + }, + av::InputFormat("rawvideo")); + ictx.findStreamInfo(); + std::size_t count = 0; + while (auto pkt = ictx.readPacket()) { + INFO("Pkt counter: " << count << ", pattern value " << (count + PatternOffset)); + REQUIRE(std::ranges::find_if_not(pkt.span(), [&](auto val) { return val == count + PatternOffset; }) == pkt.span().end()); + ++count; + } + REQUIRE(ictx.streamsCount() == 1); + REQUIRE(ImageCount == count); + } + + { + av::FormatContext octx; + octx.setFormat(av::OutputFormat("rawvideo")); + octx.addStream(); + octx.openOutput(&customIoOut); + octx.writeHeader(av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + }); + + std::vector packetData(FrameSizeBytes); + + for (auto i = 0u; i < ImageCount; ++i) { + std::ranges::fill(packetData, uint8_t(i + PatternOffset)); + av::Packet pkt{packetData, av::Packet::wrap_data_static{}}; + pkt.setPts(av::Timestamp{std::chrono::seconds(i)}); + pkt.setStreamIndex(0); + octx.writePacket(pkt); + } + octx.flush(); + + // Check Output data + std::span in = customIoOut._buffer; + for (auto i = 0u; i < ImageCount; ++i) { + auto buf = in.subspan(i * FrameSizeBytes, FrameSizeBytes); + INFO("Pkt counter: " << i << ", pattern value " << (i + PatternOffset)); + REQUIRE(std::ranges::find_if_not(buf, [&](auto val) { return val == i + PatternOffset; }) == buf.end()); + } + } + } + + SECTION("Output Open") + { + TestBufferIo customIo; + static auto const VideoSize = std::format("{}x{}", ImageW, ImageH); + static auto const FrameSizeBytes = ImageW * ImageH * ImagePixFmt.bitsPerPixel() / 8; + static auto const PatternOffset = 100u; + + { + av::FormatContext ctx; + CHECK_THROWS(ctx.openOutput(&customIo, + av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + })); + REQUIRE(ctx.isOpened() == false); + } + + { + av::FormatContext ctx; + ctx.setFormat(av::OutputFormat{"rawvideo"}); + + // Yep, format pointed, but there is no any streams for muxing and initOuput called. + REQUIRE_THROWS(ctx.openOutput(&customIo, + av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + })); + + REQUIRE(ctx.isOpened() == false); + + // set format again + ctx.setFormat(av::OutputFormat{"rawvideo"}); + // add stream for muxing, it must be valid now + REQUIRE_NOTHROW(ctx.addStream()); + REQUIRE_NOTHROW(ctx.openOutput(&customIo, + av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + })); + } + + { + av::FormatContext ctx; + // no streams, should throws + CHECK_THROWS(ctx.openOutput(&customIo, + av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + }, + av::OutputFormat{"rawvideo"})); + REQUIRE(ctx.isOpened() == false); + + ctx.addStream(); + + CHECK_NOTHROW(ctx.openOutput(&customIo, + av::Dictionary { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()}, + }, + av::OutputFormat{"rawvideo"})); + REQUIRE(ctx.isOpened() == true); + } + + // non-move stor for options + { + av::FormatContext ctx; + + av::Dictionary options { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()} + }; + + // no streams, should throws + CHECK_THROWS(ctx.openOutput(&customIo, + options, + av::OutputFormat{"rawvideo"})); + REQUIRE(ctx.isOpened() == false); + + // Options should kept: + REQUIRE(options.count() == 2); + + ctx.addStream(); + CHECK_NOTHROW(ctx.openOutput(&customIo, + options, + av::OutputFormat{"rawvideo"})); + REQUIRE(ctx.isOpened() == true); + + // Options should be empty + // REQUIRE(options.count() == 0); + } + + // Check write + { + av::FormatContext ctx; + + av::Dictionary options { + {"pixel_format", ImagePixFmt.name()}, + {"video_size", VideoSize.c_str()} + }; + + // Options should kept: + REQUIRE(options.count() == 2); + + ctx.addStream(); + bool openOutputFlag{}; + CHECK_NOTHROW(openOutputFlag = ctx.openOutput(&customIo, + options, + av::OutputFormat{"rawvideo"})); + REQUIRE(ctx.isOpened() == true); + // for the rawvideo openOutputFlag should be false + REQUIRE(openOutputFlag == false); + + ctx.writeHeader(); + + // fill with FF + customIo.fill(); + + std::vector packetData(FrameSizeBytes); + + for (auto i = 0u; i < ImageCount; ++i) { + std::ranges::fill(packetData, uint8_t(i + PatternOffset)); + av::Packet pkt{packetData, av::Packet::wrap_data_static{}}; + pkt.setPts(av::Timestamp{std::chrono::seconds(i)}); + pkt.setStreamIndex(0); + ctx.writePacket(pkt); + } + ctx.flush(); + + // Check Output data + std::span in = customIo._buffer; + for (auto i = 0u; i < ImageCount; ++i) { + auto buf = in.subspan(i * FrameSizeBytes, FrameSizeBytes); + INFO("Pkt counter: " << i << ", pattern value " << (i + PatternOffset)); + REQUIRE(std::ranges::find_if_not(buf, [&](auto val) { return val == i + PatternOffset; }) == buf.end()); + } + } + } +} + +#endif \ No newline at end of file