From a6219e9f15d93d723ae22bea18c916a8893018ff Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 14:55:07 +1000 Subject: [PATCH 1/7] Add Doxygen ducumantation for the CustomIO --- src/avcpp/formatcontext.h | 77 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/avcpp/formatcontext.h b/src/avcpp/formatcontext.h index 2682417b..635a1481 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 ""; } }; From 5df0c5e50ff3d370c61162b63bb769f8df38a518 Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 14:58:34 +1000 Subject: [PATCH 2/7] FormatContext::openCustomIO: fix potential memory leak Can occurs when avio_alloc_context() fails to allocate context. Also, using av_freep(&avio) instead of avio_context_free(&avio) also can cause memory leaks, at least black/white protocol lists. --- src/avcpp/formatcontext.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/avcpp/formatcontext.cpp b/src/avcpp/formatcontext.cpp index b2241ab9..c97d1897 100644 --- a/src/avcpp/formatcontext.cpp +++ b/src/avcpp/formatcontext.cpp @@ -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; } } @@ -1067,26 +1067,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; } From bea31e9147705a666df559071fc76978ff042c4b Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 17:58:00 +1000 Subject: [PATCH 3/7] FormatContext: add extra openOutput() methods for CustomIO ...and more: - CustomIO output open with Format Options, with/without Format Options and Output Format specification - URI based output open with Output Format specification - Add initOutput() method that wraps avformat_init_outpur() --- src/avcpp/formatcontext.cpp | 157 +++++++++++++++++++++++++++++++++--- src/avcpp/formatcontext.h | 48 +++++++++-- 2 files changed, 189 insertions(+), 16 deletions(-) diff --git a/src/avcpp/formatcontext.cpp b/src/avcpp/formatcontext.cpp index c97d1897..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'; @@ -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); diff --git a/src/avcpp/formatcontext.h b/src/avcpp/formatcontext.h index 635a1481..be511015 100644 --- a/src/avcpp/formatcontext.h +++ b/src/avcpp/formatcontext.h @@ -263,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()); @@ -292,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*)); From b87f2a7c290d4e7d0e79847dce4db8407bebe3e5 Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 17:58:33 +1000 Subject: [PATCH 4/7] Add basic tests for the FormatContext and CustomIO --- tests/CMakeLists.txt | 3 +- tests/FormatCustomIO_test.cpp | 338 ++++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 tests/FormatCustomIO_test.cpp 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..440ae140 --- /dev/null +++ b/tests/FormatCustomIO_test.cpp @@ -0,0 +1,338 @@ +#include + +#include "avcpp/format.h" +#include "avcpp/codec.h" +#include "avcpp/ffmpeg.h" +#include "avcpp/formatcontext.h" +#include "catch2/catch_message.hpp" + +#include + +#if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20) + +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 From 03e85fbbf30bfde604facd72c2f97191e5eacd05 Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 19:25:19 +1000 Subject: [PATCH 5/7] FormatCustomIO_test: remove header --- tests/FormatCustomIO_test.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/FormatCustomIO_test.cpp b/tests/FormatCustomIO_test.cpp index 440ae140..4c745b6f 100644 --- a/tests/FormatCustomIO_test.cpp +++ b/tests/FormatCustomIO_test.cpp @@ -1,12 +1,8 @@ #include +#include "catch2/catch_message.hpp" #include "avcpp/format.h" -#include "avcpp/codec.h" -#include "avcpp/ffmpeg.h" #include "avcpp/formatcontext.h" -#include "catch2/catch_message.hpp" - -#include #if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20) From 93876c20ca5940ebb3445a11b8f4025f21e54da1 Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 23:35:40 +1000 Subject: [PATCH 6/7] FormatCustomIO_test: add include --- tests/FormatCustomIO_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/FormatCustomIO_test.cpp b/tests/FormatCustomIO_test.cpp index 4c745b6f..2ec8939d 100644 --- a/tests/FormatCustomIO_test.cpp +++ b/tests/FormatCustomIO_test.cpp @@ -6,6 +6,8 @@ #if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20) +#include + static constexpr std::size_t ImageW = 640; static constexpr std::size_t ImageH = 480; static constexpr std::size_t ImageCount = 11; From b9f0699fbc680638650bdd3a59546945d60ba788 Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Fri, 27 Feb 2026 23:57:17 +1000 Subject: [PATCH 7/7] FormatCustomIO_test: depend test on __cpp_lib_format --- tests/FormatCustomIO_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FormatCustomIO_test.cpp b/tests/FormatCustomIO_test.cpp index 2ec8939d..0a6fec98 100644 --- a/tests/FormatCustomIO_test.cpp +++ b/tests/FormatCustomIO_test.cpp @@ -4,7 +4,7 @@ #include "avcpp/format.h" #include "avcpp/formatcontext.h" -#if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20) +#if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20) && defined(__cpp_lib_format) #include