// Phase 3 smoke test: capture a few real desktop frames through the portal, // encode them to H.264, and write an Annex-B elementary stream to disk. // // This tool is intentionally manual: it needs a running desktop session and // the user must confirm the source picker dialog, so it is not registered // with `meson test`. Validation steps are documented in docs/RUNBOOK.md. #include "screencast/capture/capture.h" #include "screencast/codec/encoder.h" #include #include #include #include #include #include #include #include #include namespace { int positive_int_arg(std::string_view text) { int value = 0; const auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value); if (ec != std::errc{} || ptr != text.data() + text.size() || value <= 0) { return -1; } return value; } std::string_view pixel_format_name(sc::PixelFormat format) { switch (format) { case sc::PixelFormat::Rgba: return "rgba"; case sc::PixelFormat::Bgrx: return "bgrx"; case sc::PixelFormat::Yuv420p: return "yuv420p"; } return "unknown"; } bool write_bytes(std::ofstream& output, const std::vector& bytes) { output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); return output.good(); } } // namespace int main(int argc, char** argv) { const int max_frames = argc > 1 ? positive_int_arg(argv[1]) : 10; if (max_frames < 0) { std::cerr << "usage: capture_smoke [frames] [output.h264]\n"; return 1; } const std::string output_path = argc > 2 ? argv[2] : "screencast_smoke.h264"; std::cout << "creating capture session (choose a source in the portal dialog)...\n"; auto capture_result = sc::CaptureFactory::create(sc::CaptureTargetWholeScreen{}); if (sc::is_capture_error(capture_result)) { std::cerr << std::format("capture failed: {}\n", sc::capture_error(capture_result).message); return 1; } auto capture = std::move(sc::capture_value(capture_result)); std::ofstream output(output_path, std::ios::binary | std::ios::trunc); if (!output.is_open()) { std::cerr << std::format("failed to open {} for writing\n", output_path); return 1; } std::unique_ptr encoder; std::uint64_t total_bytes = 0; std::uint64_t encoded_frames = 0; std::uint64_t keyframes = 0; for (int captured = 0; captured < max_frames; ++captured) { auto frame = capture->next_frame(); if (!frame.has_value()) { std::cout << std::format("capture session ended after {} frames\n", captured); break; } if (encoder == nullptr) { sc::EncoderConfig config; config.width = frame->width; config.height = frame->height; config.frame_rate_num = 25; config.frame_rate_den = 1; config.bitrate_kbps = 8000; auto encoder_result = sc::EncoderFactory::create(config); if (sc::is_codec_error(encoder_result)) { std::cerr << std::format("encoder creation failed: {}\n", sc::codec_error(encoder_result).message); return 1; } encoder = std::move(sc::codec_value(encoder_result)); // The encoder is configured with AV_CODEC_FLAG_GLOBAL_HEADER, so // parameter sets live in extradata; prepend them so the output // file is a self-contained Annex-B stream. const auto extradata = encoder->get_extradata(); if (!extradata.empty()) { if (!write_bytes(output, extradata)) { std::cerr << std::format("failed to write parameter sets to {}\n", output_path); return 1; } total_bytes += extradata.size(); } std::cout << std::format("capturing {}x{} ({}, stride {})\n", frame->width, frame->height, pixel_format_name(frame->pixel_format), frame->stride); } auto encoded_result = encoder->encode(*frame); if (sc::is_codec_error(encoded_result)) { std::cerr << std::format("encode failed: {}\n", sc::codec_error(encoded_result).message); return 1; } for (const auto& encoded : sc::codec_value(encoded_result)) { if (!write_bytes(output, encoded.data)) { std::cerr << std::format("failed to write encoded data to {}\n", output_path); return 1; } total_bytes += encoded.data.size(); ++encoded_frames; keyframes += encoded.is_keyframe ? 1 : 0; } } if (encoder != nullptr) { auto flushed = encoder->flush(); if (sc::is_codec_error(flushed)) { std::cerr << std::format("flush failed: {}\n", sc::codec_error(flushed).message); return 1; } for (const auto& encoded : sc::codec_value(flushed)) { if (!write_bytes(output, encoded.data)) { std::cerr << std::format("failed to write flushed data to {}\n", output_path); return 1; } total_bytes += encoded.data.size(); ++encoded_frames; } } capture->stop(); output.close(); if (total_bytes == 0 || encoded_frames == 0) { std::cerr << "no frames were captured\n"; return 1; } std::cout << std::format("wrote {} bytes to {} ({} encoded frames, {} keyframes)\n", total_bytes, output_path, encoded_frames, keyframes); std::cout << std::format("validate with: ffprobe -v error -show_entries stream=codec_name,width,height {}\n", output_path); return 0; }