Skip to content

Commit e9984e9

Browse files
committed
fix: address review feedback for video writer and CLI parsing
1 parent 3a116a1 commit e9984e9

10 files changed

Lines changed: 207 additions & 83 deletions

cli/args_parser.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,18 @@ CLIOptions parseArgs(int argc, char* argv[]) {
404404
opts.videoFilePath = argv[++i];
405405
}
406406
} else if (arg == "--record") {
407-
if (i + 1 < argc) {
408-
opts.recordVideoPath = argv[++i];
407+
#ifdef CCAP_ENABLE_VIDEO_WRITER
408+
if (i + 1 >= argc || argv[i + 1][0] == '-') {
409+
std::cerr << "Error: --record requires an output file path.\n\n";
410+
printUsage(argv[0]);
411+
std::exit(1);
409412
}
413+
opts.recordVideoPath = argv[++i];
414+
#else
415+
std::cerr << "Error: --record is not supported in this build. Rebuild with CCAP_ENABLE_VIDEO_WRITER=ON.\n\n";
416+
printUsage(argv[0]);
417+
std::exit(1);
418+
#endif
410419
} else if (arg == "-w" || arg == "--width") {
411420
if (i + 1 < argc) {
412421
opts.width = std::atoi(argv[++i]);

examples/desktop/6-record_video.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ int main(int argc, char** argv) {
5050
outputPath = commandLine.argv[1];
5151
} else {
5252
std::string exeDir = commandLine.argv[0];
53-
if (auto pos = exeDir.find_last_of("/\\"); pos != std::string::npos && exeDir[0] != '.') {
53+
if (auto pos = exeDir.find_last_of("/\\"); pos != std::string::npos && !exeDir.empty() && exeDir[0] != '.') {
5454
exeDir = exeDir.substr(0, pos);
5555
} else {
5656
exeDir = std::filesystem::current_path().string();

include/ccap_writer.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class CCAP_EXPORT VideoWriter {
7070
* @brief Open writer to a file path.
7171
* @param filePath Output file path (e.g., "output.mp4")
7272
* @param config Writer configuration (width, height, codec, etc.)
73+
* @note Call `close()` before reopening an existing writer instance.
7374
* @return true on success, false on failure.
7475
*/
7576
bool open(std::string_view filePath, const WriterConfig& config);
@@ -87,6 +88,7 @@ class CCAP_EXPORT VideoWriter {
8788
bool writeFrame(const VideoFrame& frame, uint64_t timestampNs = 0);
8889

8990
/// Query the actual codec being used (may differ from config due to fallback).
91+
/// Only meaningful after `open()` succeeds.
9092
VideoCodec actualCodec() const;
9193

9294
uint32_t width() const;

include/ccap_writer_c.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,26 @@ typedef enum {
3939

4040
/* ========== Data Structures ========== */
4141

42-
/** @brief Video writer configuration */
42+
/**
43+
* @brief Video writer configuration.
44+
* @note Use `CCAP_WRITER_CONFIG_INIT` for codec/container/frameRate/bitRate defaults.
45+
* `width` and `height` must still be set before opening a writer.
46+
*/
4347
typedef struct {
4448
CcapVideoCodec codec; ///< Preferred codec
4549
CcapVideoFormat container; ///< Container format
4650
uint32_t width; ///< Frame width
4751
uint32_t height; ///< Frame height
48-
double frameRate; ///< Target frame rate (default 30fps)
52+
double frameRate; ///< Target frame rate; 0 lets open() normalize to 30fps
4953
uint64_t bitRate; ///< Target bit rate in bits/s (0 = auto)
5054
} CcapWriterConfig;
5155

56+
/**
57+
* @brief Default initializer for `CcapWriterConfig`.
58+
* @note `width` and `height` remain 0 and must be assigned by the caller.
59+
*/
60+
#define CCAP_WRITER_CONFIG_INIT { CCAP_VIDEO_CODEC_HEVC, CCAP_VIDEO_FORMAT_MP4, 0u, 0u, 30.0, 5000000ULL }
61+
5262
/* ========== Writer Lifecycle ========== */
5363

5464
/**
@@ -100,7 +110,8 @@ CCAP_EXPORT bool ccap_video_writer_write_frame(CcapVideoWriter* writer,
100110
/**
101111
* @brief Get the actual codec being used (may differ from config due to fallback)
102112
* @param writer Pointer to CcapVideoWriter instance
103-
* @return Actual codec enum value
113+
* @return Actual codec enum value. Only meaningful after `ccap_video_writer_open()` succeeds.
114+
* Unopened or null writers return `CCAP_VIDEO_CODEC_H264` for ABI compatibility.
104115
*/
105116
CCAP_EXPORT CcapVideoCodec ccap_video_writer_actual_codec(const CcapVideoWriter* writer);
106117

src/ccap_writer.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ namespace ccap {
1616
static VideoWriter::Impl* impl(void* p) { return reinterpret_cast<VideoWriter::Impl*>(p); }
1717
static const VideoWriter::Impl* impl(const void* p) { return reinterpret_cast<const VideoWriter::Impl*>(p); }
1818

19-
VideoWriter::VideoWriter() : m_impl(createVideoWriterImpl()) {}
19+
VideoWriter::VideoWriter() :
20+
m_impl(createVideoWriterImpl()) {}
2021

2122
VideoWriter::~VideoWriter() {
2223
delete impl(m_impl);
2324
}
2425

25-
VideoWriter::VideoWriter(VideoWriter&& other) noexcept : m_impl(other.m_impl) {
26+
VideoWriter::VideoWriter(VideoWriter&& other) noexcept :
27+
m_impl(other.m_impl) {
2628
other.m_impl = nullptr;
2729
}
2830

@@ -40,6 +42,10 @@ bool VideoWriter::open(std::string_view filePath, const WriterConfig& config) {
4042
reportError(ErrorCode::WriterNotOpened, "VideoWriter not available on this platform");
4143
return false;
4244
}
45+
if (impl(m_impl)->isOpened()) {
46+
reportError(ErrorCode::WriterOpenFailed, "VideoWriter is already opened. Call close() before reopening.");
47+
return false;
48+
}
4349
return impl(m_impl)->open(filePath, config);
4450
}
4551

@@ -81,7 +87,8 @@ double VideoWriter::frameRate() const {
8187

8288
namespace ccap {
8389

84-
VideoWriter::VideoWriter() : m_impl(nullptr) {}
90+
VideoWriter::VideoWriter() :
91+
m_impl(nullptr) {}
8592
VideoWriter::~VideoWriter() = default;
8693
VideoWriter::VideoWriter(VideoWriter&&) noexcept = default;
8794
VideoWriter& VideoWriter::operator=(VideoWriter&&) noexcept = default;

src/ccap_writer_apple.mm

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,22 +160,24 @@ void close() override {
160160
if (!m_isOpened) return;
161161
m_isOpened = false;
162162

163+
AVAssetWriter* assetWriter = m_assetWriter;
164+
AVAssetWriterInput* writerInput = m_writerInput;
165+
163166
@try {
164-
if (m_writerInput) {
165-
[m_writerInput markAsFinished];
167+
if (writerInput) {
168+
[writerInput markAsFinished];
166169
}
167-
if (m_assetWriter) {
168-
dispatch_queue_t queue = dispatch_queue_create("com.ccap.writer.close", DISPATCH_QUEUE_SERIAL);
170+
if (assetWriter) {
169171
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
170-
dispatch_async(queue, ^{
171-
[m_assetWriter finishWritingWithCompletionHandler:^{
172-
dispatch_semaphore_signal(sem);
173-
}];
174-
});
175-
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC));
176-
177-
if (m_assetWriter.error) {
178-
reportError(ErrorCode::WriterCloseFailed, "finishWriting failed: " + std::string(m_assetWriter.error.localizedDescription.UTF8String));
172+
[assetWriter finishWritingWithCompletionHandler:^{
173+
dispatch_semaphore_signal(sem);
174+
}];
175+
176+
const long waitResult = dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC));
177+
if (waitResult != 0) {
178+
reportError(ErrorCode::WriterCloseFailed, "finishWriting timed out after 10 seconds");
179+
} else if (assetWriter.error) {
180+
reportError(ErrorCode::WriterCloseFailed, "finishWriting failed: " + std::string(assetWriter.error.localizedDescription.UTF8String));
179181
}
180182
}
181183
}
@@ -186,6 +188,8 @@ void close() override {
186188
m_pixelBufferAdaptor = nil;
187189
m_writerInput = nil;
188190
m_assetWriter = nil;
191+
m_sessionStarted = NO;
192+
m_frameCount = 0;
189193
std::memset(&m_config, 0, sizeof(m_config));
190194
}
191195

src/ccap_writer_imp.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ namespace ccap {
2020
void reportError(ErrorCode errorCode, std::string_view description);
2121

2222
struct VideoWriter::Impl {
23-
Impl() : m_actualCodec(VideoCodec::H264) {}
23+
Impl() :
24+
m_actualCodec(VideoCodec::H264) {}
2425
virtual ~Impl() = default;
2526

2627
virtual bool open(std::string_view filePath, const WriterConfig& config) = 0;
@@ -76,6 +77,9 @@ inline bool convertFrameToNv12(const VideoFrame& frame,
7677
uint32_t& yStride, uint32_t& uvStride) {
7778
const int w = static_cast<int>(frame.width);
7879
const int h = static_cast<int>(frame.height);
80+
if (w <= 0 || h <= 0 || (w % 2) != 0 || (h % 2) != 0) {
81+
return false;
82+
}
7983
const int w2 = w / 2;
8084
const int h2 = h / 2;
8185
const FrameOrientation orientation = frame.orientation;

src/ccap_writer_windows.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
#define NOMINMAX
1515
#endif
1616
#include <atomic>
17+
#include <iomanip>
1718
#include <mfapi.h>
1819
#include <mferror.h>
1920
#include <mfidl.h>
2021
#include <mfreadwrite.h>
2122
#include <mutex>
23+
#include <sstream>
2224
#include <vector>
2325
#include <windows.h>
2426

@@ -31,9 +33,22 @@
3133

3234
namespace ccap {
3335

36+
namespace {
37+
38+
std::string formatHRESULT(HRESULT hr) {
39+
std::ostringstream stream;
40+
stream << "0x"
41+
<< std::uppercase << std::hex << std::setw(8) << std::setfill('0')
42+
<< static_cast<unsigned int>(hr);
43+
return stream.str();
44+
}
45+
46+
} // namespace
47+
3448
class WriterWindows : public VideoWriter::Impl {
3549
public:
36-
WriterWindows() : m_sinkWriter(nullptr), m_streamIndex(0), m_mfInitialized(false) {
50+
WriterWindows() :
51+
m_sinkWriter(nullptr), m_streamIndex(0), m_mfInitialized(false) {
3752
HRESULT hr = MFStartup(MF_VERSION, MFSTARTUP_FULL);
3853
m_mfInitialized = SUCCEEDED(hr);
3954
if (!m_mfInitialized) {
@@ -103,7 +118,7 @@ class WriterWindows : public VideoWriter::Impl {
103118
if (m_sinkWriter) {
104119
HRESULT hr = m_sinkWriter->Finalize();
105120
if (FAILED(hr)) {
106-
reportError(ErrorCode::WriterCloseFailed, "IMFSinkWriter::Finalize failed: 0x" + std::to_string(hr));
121+
reportError(ErrorCode::WriterCloseFailed, "IMFSinkWriter::Finalize failed: " + formatHRESULT(hr));
107122
}
108123
m_sinkWriter->Release();
109124
m_sinkWriter = nullptr;

tests/test_cli_args_parser.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,50 @@ TEST(CLIArgsParserTest, RejectsMissingSchemaVersionValue) {
103103
"--schema-version requires a value");
104104
}
105105

106+
#ifdef CCAP_ENABLE_VIDEO_WRITER
107+
TEST(CLIArgsParserTest, ParsesRecordOutputPath) {
108+
char arg0[] = "ccap";
109+
char arg1[] = "--record";
110+
char arg2[] = "capture.mp4";
111+
char* argv[] = { arg0, arg1, arg2, nullptr };
112+
113+
const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(3, argv);
114+
115+
EXPECT_EQ(opts.recordVideoPath, "capture.mp4");
116+
}
117+
118+
TEST(CLIArgsParserTest, RejectsMissingRecordValue) {
119+
char arg0[] = "ccap";
120+
char arg1[] = "--record";
121+
char arg2[] = "--timeout";
122+
char arg3[] = "5";
123+
char* argv[] = { arg0, arg1, arg2, arg3, nullptr };
124+
125+
EXPECT_EXIT(
126+
{
127+
(void)ccap_cli::parseArgs(4, argv);
128+
std::exit(0);
129+
},
130+
::testing::ExitedWithCode(1),
131+
"--record requires an output file path");
132+
}
133+
#else
134+
TEST(CLIArgsParserTest, RejectsRecordWhenWriterUnsupported) {
135+
char arg0[] = "ccap";
136+
char arg1[] = "--record";
137+
char arg2[] = "capture.mp4";
138+
char* argv[] = { arg0, arg1, arg2, nullptr };
139+
140+
EXPECT_EXIT(
141+
{
142+
(void)ccap_cli::parseArgs(3, argv);
143+
std::exit(0);
144+
},
145+
::testing::ExitedWithCode(1),
146+
"--record is not supported in this build");
147+
}
148+
#endif
149+
106150
#if defined(_WIN32) || defined(_WIN64)
107151
TEST(CLIArgsParserTest, ParsesWindowsCameraBackendOption) {
108152
char arg0[] = "ccap";

0 commit comments

Comments
 (0)