1+ #include < catch2/catch_test_macros.hpp>
2+
3+ #include " avcpp/format.h"
4+ #include " avcpp/codec.h"
5+ #include " avcpp/ffmpeg.h"
6+ #include " avcpp/formatcontext.h"
7+ #include " catch2/catch_message.hpp"
8+
9+ #include < print>
10+
11+ #if AVCPP_HAS_AVFORMAT && (AVCPP_CXX_STANDARD >= 20)
12+
13+ static constexpr std::size_t ImageW = 640 ;
14+ static constexpr std::size_t ImageH = 480 ;
15+ static constexpr std::size_t ImageCount = 11 ;
16+ static constexpr av::PixelFormat ImagePixFmt = AV_PIX_FMT_GRAY8 ;
17+
18+ static const std::size_t FrameSizeBytes = ImageW * ImageH * ImagePixFmt.bitsPerPixel() / 8 ;
19+
20+ struct TestBufferIo : public av ::CustomIO
21+ {
22+ public:
23+ std::vector<uint8_t > _buffer{};
24+ std::vector<uint8_t >::iterator _pos{};
25+
26+ TestBufferIo ()
27+ {
28+ _buffer.resize (FrameSizeBytes * ImageCount);
29+ _pos = _buffer.begin ();
30+ }
31+
32+ std::size_t remain () const noexcept
33+ {
34+ return std::ranges::distance (_pos, _buffer.end ());
35+ }
36+
37+ bool eof () const noexcept
38+ {
39+ return _pos == _buffer.end ();
40+ }
41+
42+ void fillPattern (uint8_t patternOffset)
43+ {
44+ std::span out = _buffer;
45+ for (auto i = 0u ; i < ImageCount; ++i) {
46+ auto buf = out.subspan (i * FrameSizeBytes, FrameSizeBytes);
47+ std::ranges::fill (buf, uint8_t (i + patternOffset));
48+ }
49+ }
50+
51+ void fill (uint8_t val = 0xFF )
52+ {
53+ std::ranges::fill (_buffer, val);
54+ }
55+
56+ //
57+ // Iface
58+ //
59+
60+ int write (const uint8_t *data, size_t size) final
61+ {
62+ UNSCOPED_INFO (" IO::write: size=" << size);
63+ // ENOSPC
64+ // EWOULDBLOCK
65+ // EMSGSIZE
66+ // Do not write
67+ if (size > remain ()) {
68+ UNSCOPED_INFO (" IO::write too big: " << size);
69+ return AVERROR (EMSGSIZE );
70+ }
71+
72+ std::ranges::copy_n (data, size, _pos);
73+ std::advance (_pos, size);
74+
75+ return 0 ;
76+ }
77+
78+ int read (uint8_t *data, size_t size) final
79+ {
80+ UNSCOPED_INFO (" IO::read: size=" << size);
81+ if (eof ()) {
82+ INFO (" IO::read: EOF reached" );
83+ return AVERROR_EOF ;
84+ }
85+
86+ auto readed = std::min (size, remain ());
87+ std::ranges::copy_n (_pos, readed, data);
88+ std::advance (_pos, readed);
89+ return readed;
90+ }
91+
92+ int64_t seek (int64_t offset, int whence) final
93+ {
94+ UNSCOPED_INFO (" IO::seek: offset=" << offset << " , whence=0x" << std::hex << whence << std::dec);
95+
96+ if (whence & AVSEEK_SIZE ) {
97+ return _buffer.size ();
98+ }
99+
100+ ssize_t cur = -1 ;
101+
102+ if (whence == SEEK_CUR ) {
103+ cur = std::distance (_buffer.begin (), _pos);
104+ } else if (whence == SEEK_SET ) {
105+ cur = 0 ;
106+ } else if (whence == SEEK_END ) {
107+ cur = _buffer.size () - 1 ;
108+ } else {
109+ AVERROR (EINVAL );
110+ }
111+
112+ assert (cur >= 0 && cur <= _buffer.size () - 1 );
113+
114+ auto next = cur + offset;
115+ if (next >= _buffer.size () || next < 0 )
116+ return AVERROR (EINVAL );
117+ std::advance (_pos, offset);
118+ return next;
119+ }
120+
121+ int seekable () const final
122+ {
123+ return AVIO_SEEKABLE_NORMAL ;
124+ }
125+
126+ const char *name () const final
127+ {
128+ return " TestBufferIo" ;
129+ }
130+ };
131+
132+
133+ TEST_CASE (" Format Custom IO checks" , " [FormatCustomIo]" )
134+ {
135+ SECTION (" CustomIo :: Basic" )
136+ {
137+ TestBufferIo customIoIn;
138+ TestBufferIo customIoOut;
139+ static auto const VideoSize = std::format (" {}x{}" , ImageW, ImageH);
140+ static auto const FrameSizeBytes = ImageW * ImageH * ImagePixFmt.bitsPerPixel () / 8 ;
141+ static auto const PatternOffset = 100u ;
142+
143+ // Fill In buffer with pattern
144+ customIoIn.fillPattern (PatternOffset);
145+
146+ // Fill Out buffer with FF
147+ customIoOut.fill ();
148+
149+ {
150+ av::FormatContext ictx;
151+ ictx.openInput (&customIoIn,
152+ av::Dictionary {
153+ {" pixel_format" , ImagePixFmt.name ()},
154+ {" video_size" , VideoSize.c_str ()},
155+ },
156+ av::InputFormat (" rawvideo" ));
157+ ictx.findStreamInfo ();
158+ std::size_t count = 0 ;
159+ while (auto pkt = ictx.readPacket ()) {
160+ INFO (" Pkt counter: " << count << " , pattern value " << (count + PatternOffset));
161+ REQUIRE (std::ranges::find_if_not (pkt.span (), [&](auto val) { return val == count + PatternOffset; }) == pkt.span ().end ());
162+ ++count;
163+ }
164+ REQUIRE (ictx.streamsCount () == 1 );
165+ REQUIRE (ImageCount == count);
166+ }
167+
168+ {
169+ av::FormatContext octx;
170+ octx.setFormat (av::OutputFormat (" rawvideo" ));
171+ octx.addStream ();
172+ octx.openOutput (&customIoOut);
173+ octx.writeHeader (av::Dictionary {
174+ {" pixel_format" , ImagePixFmt.name ()},
175+ {" video_size" , VideoSize.c_str ()},
176+ });
177+
178+ std::vector<uint8_t > packetData (FrameSizeBytes);
179+
180+ for (auto i = 0u ; i < ImageCount; ++i) {
181+ std::ranges::fill (packetData, uint8_t (i + PatternOffset));
182+ av::Packet pkt{packetData, av::Packet::wrap_data_static{}};
183+ pkt.setPts (av::Timestamp{std::chrono::seconds (i)});
184+ pkt.setStreamIndex (0 );
185+ octx.writePacket (pkt);
186+ }
187+ octx.flush ();
188+
189+ // Check Output data
190+ std::span in = customIoOut._buffer ;
191+ for (auto i = 0u ; i < ImageCount; ++i) {
192+ auto buf = in.subspan (i * FrameSizeBytes, FrameSizeBytes);
193+ INFO (" Pkt counter: " << i << " , pattern value " << (i + PatternOffset));
194+ REQUIRE (std::ranges::find_if_not (buf, [&](auto val) { return val == i + PatternOffset; }) == buf.end ());
195+ }
196+ }
197+ }
198+
199+ SECTION (" Output Open" )
200+ {
201+ TestBufferIo customIo;
202+ static auto const VideoSize = std::format (" {}x{}" , ImageW, ImageH);
203+ static auto const FrameSizeBytes = ImageW * ImageH * ImagePixFmt.bitsPerPixel () / 8 ;
204+ static auto const PatternOffset = 100u ;
205+
206+ {
207+ av::FormatContext ctx;
208+ CHECK_THROWS (ctx.openOutput (&customIo,
209+ av::Dictionary {
210+ {" pixel_format" , ImagePixFmt.name ()},
211+ {" video_size" , VideoSize.c_str ()},
212+ }));
213+ REQUIRE (ctx.isOpened () == false );
214+ }
215+
216+ {
217+ av::FormatContext ctx;
218+ ctx.setFormat (av::OutputFormat{" rawvideo" });
219+
220+ // Yep, format pointed, but there is no any streams for muxing and initOuput called.
221+ REQUIRE_THROWS (ctx.openOutput (&customIo,
222+ av::Dictionary {
223+ {" pixel_format" , ImagePixFmt.name ()},
224+ {" video_size" , VideoSize.c_str ()},
225+ }));
226+
227+ REQUIRE (ctx.isOpened () == false );
228+
229+ // set format again
230+ ctx.setFormat (av::OutputFormat{" rawvideo" });
231+ // add stream for muxing, it must be valid now
232+ REQUIRE_NOTHROW (ctx.addStream ());
233+ REQUIRE_NOTHROW (ctx.openOutput (&customIo,
234+ av::Dictionary {
235+ {" pixel_format" , ImagePixFmt.name ()},
236+ {" video_size" , VideoSize.c_str ()},
237+ }));
238+ }
239+
240+ {
241+ av::FormatContext ctx;
242+ // no streams, should throws
243+ CHECK_THROWS (ctx.openOutput (&customIo,
244+ av::Dictionary {
245+ {" pixel_format" , ImagePixFmt.name ()},
246+ {" video_size" , VideoSize.c_str ()},
247+ },
248+ av::OutputFormat{" rawvideo" }));
249+ REQUIRE (ctx.isOpened () == false );
250+
251+ ctx.addStream ();
252+
253+ CHECK_NOTHROW (ctx.openOutput (&customIo,
254+ av::Dictionary {
255+ {" pixel_format" , ImagePixFmt.name ()},
256+ {" video_size" , VideoSize.c_str ()},
257+ },
258+ av::OutputFormat{" rawvideo" }));
259+ REQUIRE (ctx.isOpened () == true );
260+ }
261+
262+ // non-move stor for options
263+ {
264+ av::FormatContext ctx;
265+
266+ av::Dictionary options {
267+ {" pixel_format" , ImagePixFmt.name ()},
268+ {" video_size" , VideoSize.c_str ()}
269+ };
270+
271+ // no streams, should throws
272+ CHECK_THROWS (ctx.openOutput (&customIo,
273+ options,
274+ av::OutputFormat{" rawvideo" }));
275+ REQUIRE (ctx.isOpened () == false );
276+
277+ // Options should kept:
278+ REQUIRE (options.count () == 2 );
279+
280+ ctx.addStream ();
281+ CHECK_NOTHROW (ctx.openOutput (&customIo,
282+ options,
283+ av::OutputFormat{" rawvideo" }));
284+ REQUIRE (ctx.isOpened () == true );
285+
286+ // Options should be empty
287+ // REQUIRE(options.count() == 0);
288+ }
289+
290+ // Check write
291+ {
292+ av::FormatContext ctx;
293+
294+ av::Dictionary options {
295+ {" pixel_format" , ImagePixFmt.name ()},
296+ {" video_size" , VideoSize.c_str ()}
297+ };
298+
299+ // Options should kept:
300+ REQUIRE (options.count () == 2 );
301+
302+ ctx.addStream ();
303+ bool openOutputFlag{};
304+ CHECK_NOTHROW (openOutputFlag = ctx.openOutput (&customIo,
305+ options,
306+ av::OutputFormat{" rawvideo" }));
307+ REQUIRE (ctx.isOpened () == true );
308+ // for the rawvideo openOutputFlag should be false
309+ REQUIRE (openOutputFlag == false );
310+
311+ ctx.writeHeader ();
312+
313+ // fill with FF
314+ customIo.fill ();
315+
316+ std::vector<uint8_t > packetData (FrameSizeBytes);
317+
318+ for (auto i = 0u ; i < ImageCount; ++i) {
319+ std::ranges::fill (packetData, uint8_t (i + PatternOffset));
320+ av::Packet pkt{packetData, av::Packet::wrap_data_static{}};
321+ pkt.setPts (av::Timestamp{std::chrono::seconds (i)});
322+ pkt.setStreamIndex (0 );
323+ ctx.writePacket (pkt);
324+ }
325+ ctx.flush ();
326+
327+ // Check Output data
328+ std::span in = customIo._buffer ;
329+ for (auto i = 0u ; i < ImageCount; ++i) {
330+ auto buf = in.subspan (i * FrameSizeBytes, FrameSizeBytes);
331+ INFO (" Pkt counter: " << i << " , pattern value " << (i + PatternOffset));
332+ REQUIRE (std::ranges::find_if_not (buf, [&](auto val) { return val == i + PatternOffset; }) == buf.end ());
333+ }
334+ }
335+ }
336+ }
337+
338+ #endif
0 commit comments