diff --git a/examples/video_encode_demo/main/src/main.cpp b/examples/video_encode_demo/main/src/main.cpp index faace240..4d80f4ef 100644 --- a/examples/video_encode_demo/main/src/main.cpp +++ b/examples/video_encode_demo/main/src/main.cpp @@ -42,6 +42,18 @@ static double timebase_to_ms(std::vector timebase, uint64_t value) { return value * 1000 / ((double)timebase[1] / timebase[0]); } +static int64_t high_fps_available_memory() { + // memory_info's total - used is Linux MemAvailable, not hardware/CMM RAM. + const auto info = sys::memory_info(); + const auto total = info.find("total"); + const auto used = info.find("used"); + if (total == info.end() || used == info.end() || total->second <= 0 || + used->second < 0 || used->second > total->second) { + return -1; + } + return total->second - used->second; +} + static int remux_high_fps_stream(const std::string &input_path, const std::string &output_path, int framerate) { AVFormatContext *input = nullptr; @@ -497,7 +509,7 @@ static void helper(void) "0 : encode without bind\r\n" "1 : encode with bind\r\n" "2 : time-lapse record\r\n" - "4 : high-fps encode with an in-memory queue\r\n" + "4 : high-fps encode with 15-fps preview; stop and drain when queue is full\r\n" "5 : encode image to jpeg\r\n" "note:\r\n" "format=%d, NV21\r\n" @@ -677,13 +689,11 @@ int _main(int argc, char* argv[]) if (argc > 11) capture = atoi(argv[11]) == 0 ? false : true; if (argc > 12) block = atoi(argv[12]) == 0 ? false : true; if (argc > 13) queue_depth = atoi(argv[13]); + err::check_bool_raise(framerate > 0, "fps must be greater than zero"); if (queue_depth < 0) queue_depth = framerate; err::check_bool_raise(queue_depth > 0, "queue_depth must be greater than zero"); - const double queue_mib = static_cast(width) * height * 3 / 2 * queue_depth / - (1024.0 * 1024.0); log::info("path:%s width:%d height:%d format:%d type:%d fps:%d gop:%d bitrate:%d time_base:%d capture:%d queue_depth:%d\r\n", path.c_str(), width, height, format, type, framerate, gop, bitrate, time_base, capture, queue_depth); - log::info("high-fps queue capacity: %.1f MiB; oldest frames are dropped if it becomes full", queue_mib); constexpr int venc_max_fps = 180; const int venc_fps = std::min(framerate, venc_max_fps); const bool needs_remux = framerate > venc_max_fps; @@ -704,7 +714,30 @@ int _main(int argc, char* argv[]) log::info("sensor fps:%d, VENC RC fps:%d; recording elementary stream for %d-fps remux", framerate, venc_fps, framerate); } - + display::Display disp = display::Display(); + const int preview_fps = std::min(framerate, 15); + // Budget after camera, display and VENC initialization. Image copies also + // allocate alignment padding; leave room for allocator/page overhead. + constexpr int64_t mib = 1024 * 1024; + const int64_t frame_memory = static_cast(width) * height * 3 / 2 + + 8192 + sizeof(image::Image); + const int64_t available_memory = high_fps_available_memory(); + err::check_bool_raise(available_memory >= 0, "cannot read available memory for recording"); + const int64_t memory_reserve = std::max(64 * mib, available_memory / 4); + // Include the capture and encode frames outside the queue in the budget. + const int64_t memory_queue_depth = (available_memory - memory_reserve) / frame_memory - 2; + err::check_bool_raise(memory_queue_depth > 0, "not enough available memory for recording"); + if (memory_queue_depth < queue_depth) { + log::warn("reducing queue_depth from %d to %lld to preserve recording memory", + queue_depth, static_cast(memory_queue_depth)); + queue_depth = static_cast(memory_queue_depth); + } + const int64_t memory_check_frames = std::min(16, memory_queue_depth); + const int64_t memory_stop_threshold = memory_reserve + frame_memory * memory_check_frames; + log::info("high-fps queue capacity: %d frames, approximately %.1f MiB; available: %.1f MiB, reserve: %.1f MiB", + queue_depth, static_cast(frame_memory * queue_depth) / mib, + static_cast(available_memory) / mib, static_cast(memory_reserve) / mib); + log::info("capture stops and queued frames are saved when the queue is full or memory is low"); // Keep camera acquisition independent from VENC/file I/O. The image returned // by Camera::read() retains a scarce VIN pool buffer, so copy its pixels into // normal heap memory before enqueueing and release the VIN frame immediately. @@ -716,11 +749,23 @@ int _main(int argc, char* argv[]) std::atomic worker_failed(false); std::atomic captured_count(0); std::atomic encoded_count(0); - std::atomic dropped_count(0); std::thread capture_thread([&]() { try { + uint64_t preview_accumulator = 0; while (!app::need_exit() && !stop_requested.load()) { + // Check before allocating another batch, with enough headroom + // for the frames captured until the next memory check. + if (captured_count.load() % memory_check_frames == 0) { + const int64_t available = high_fps_available_memory(); + if (available < memory_stop_threshold) { + log::warn("stopping capture and saving queued frames: available memory %.1f MiB, threshold %.1f MiB", + available < 0 ? -1.0 : static_cast(available) / mib, + static_cast(memory_stop_threshold) / mib); + stop_requested.store(true); + break; + } + } std::unique_ptr camera_img(cam.read()); if (!camera_img) { continue; @@ -731,18 +776,36 @@ int _main(int argc, char* argv[]) camera_img.reset(); ++captured_count; + // // Sample at 15 fps relative to the configured capture rate, + // // releasing the VIN buffer before potentially blocking display I/O. + // preview_accumulator += preview_fps; + // if (preview_accumulator >= static_cast(framerate)) { + // preview_accumulator -= framerate; + // disp.show(*img); + // } + + bool queue_full = false; { std::lock_guard lock(queue_mutex); - // Do not let a slow encoder throttle sensor acquisition. Retain - // the newest frames, and release a discarded frame immediately. - if (image_queue.size() >= static_cast(queue_depth)) { - image_queue.pop_front(); - ++dropped_count; - } image_queue.emplace_back(std::move(img)); + // Retain the frame that fills the queue, then stop acquisition. + // The encoder exits only after capture_done and an empty queue. + queue_full = image_queue.size() >= static_cast(queue_depth); + if (queue_full) { + stop_requested.store(true); + } } queue_not_empty.notify_one(); + if (queue_full) { + log::info("high-fps queue full: stopping capture and saving queued frames"); + break; + } } + } catch (const std::bad_alloc &) { + // Allocation can still fail between memory checks. Preserve the + // queued frames and allow normal encoder/remux finalization. + log::warn("capture allocation failed: stopping capture and saving queued frames"); + stop_requested.store(true); } catch (const std::exception &e) { log::error("camera capture thread stopped: %s", e.what()); worker_failed.store(true); @@ -774,8 +837,9 @@ int _main(int argc, char* argv[]) img = std::move(image_queue.front()); image_queue.pop_front(); } - + uint64_t t = time::ticks_us(); std::unique_ptr frame(encoder->encode(img.get())); + log::info("video encode use %lld us", time::ticks_us() - t); ++encoded_count; } } catch (const std::exception &e) { @@ -798,10 +862,10 @@ int _main(int argc, char* argv[]) err::check_bool_raise(remux_ret >= 0, "high-fps stream remux failed"); std::remove(encoder_path.c_str()); } - log::info("high-fps record stopped: captured:%llu encoded:%llu dropped:%llu queue_depth:%d", + log::info("high-fps record stopped: captured:%llu encoded:%llu queue_depth:%d", static_cast(captured_count.load()), static_cast(encoded_count.load()), - static_cast(dropped_count.load()), queue_depth); + queue_depth); break; } case 5: