/* * This file is part of the ZoneMinder Project. See AUTHORS file for contributors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "zm_catch2.h" #include "zm_config.h" #include "zm_image.h" #include "zm_packet.h" #include "zm_packetqueue.h" #include #include namespace { std::shared_ptr video_packet(int keyframe) { auto packet = std::make_shared(); packet->packet->stream_index = 0; packet->keyframe = keyframe; packet->codec_type = AVMEDIA_TYPE_VIDEO; packet->timestamp = std::chrono::system_clock::now(); return packet; } // A video packet holding a decoded image, so a test can see releaseDecoded() // free it. Image::Initialise() needs config.font_file_location, which is null // without a zm.conf. std::shared_ptr decoded_video_packet(int keyframe) { if (!config.font_file_location) config.font_file_location = ""; auto packet = video_packet(keyframe); packet->image = new Image(16, 16, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE); return packet; } } // namespace // Monitor::openEvent() takes an iterator from get_event_start_packet_it() and // normally hands it to the Event, which frees it in ~Event. The error path that // fails to lock the starting packet used to return without freeing it. This // covers what that leak does to the queue: a registered iterator sitting on the // front packet stops clearPackets() removing anything, permanently, because // deletePacket() drags registered iterators onto each new front packet. TEST_CASE("PacketQueue: an abandoned iterator blocks trimming until it is freed") { PacketQueue queue; queue.addStream(); queue.setKeepKeyframes(false); queue.setMaxVideoPackets(10); queue.setPreEventVideoPackets(2); // queuePacket() drops everything when no iterator is registered, so stand in // for the analysis thread and keep one registered for the whole test. packetqueue_iterator *analysis_it = queue.get_video_it(false); REQUIRE(analysis_it != nullptr); for (int i = 0; i < 6; i++) { REQUIRE(queue.queuePacket(video_packet(i == 0 ? 1 : 0))); } REQUIRE(queue.size() == 6); SECTION("the queue trims once nothing points at the front") { // Analysis has consumed the queue, so its iterator sits at the tail. while (queue.increment_it(analysis_it, false)) {} auto trigger = video_packet(1); REQUIRE(queue.queuePacket(trigger)); REQUIRE(queue.clearPackets(trigger)); REQUIRE(queue.size() < 7); } SECTION("an abandoned iterator on the front packet stops every removal") { // What openEvent() used to leave behind: a registered iterator that nothing // owns any more, pointing at the front of the queue. packetqueue_iterator *leaked_it = queue.get_video_it(false); REQUIRE(leaked_it != nullptr); while (queue.increment_it(analysis_it, false)) {} auto trigger = video_packet(1); REQUIRE(queue.queuePacket(trigger)); queue.clearPackets(trigger); REQUIRE(queue.size() == 7); // Freeing it is all the fix does, and the queue drains on the next attempt. queue.free_it(leaked_it); auto next = video_packet(1); REQUIRE(queue.queuePacket(next)); REQUIRE(queue.clearPackets(next)); REQUIRE(queue.size() < 8); } queue.stop(); queue.clear(); } // The pair of calls the fix in Monitor::openEvent() now makes on its error path: // an iterator from get_event_start_packet_it() is registered with the queue, and // free_it() is what takes it back out again. TEST_CASE("PacketQueue: free_it unregisters an event start iterator") { PacketQueue queue; queue.addStream(); queue.setKeepKeyframes(false); queue.setMaxVideoPackets(0); queue.setPreEventVideoPackets(2); packetqueue_iterator *analysis_it = queue.get_video_it(false); REQUIRE(analysis_it != nullptr); for (int i = 0; i < 6; i++) { REQUIRE(queue.queuePacket(video_packet(i == 0 ? 1 : 0))); } std::shared_ptr front = *queue.begin(); while (queue.increment_it(analysis_it, false)) {} REQUIRE_FALSE(queue.is_there_an_iterator_pointing_to_packet(front)); packetqueue_iterator *start_it = queue.get_event_start_packet_it(*analysis_it, 10); REQUIRE(start_it != nullptr); REQUIRE(queue.is_there_an_iterator_pointing_to_packet(front)); queue.free_it(start_it); REQUIRE_FALSE(queue.is_there_an_iterator_pointing_to_packet(front)); queue.stop(); queue.clear(); } // releaseDecoded() frees the decoded data of packets before the earliest point // an event could start, computed with the current pre-event window. A RELOAD // (zmu --reload) can widen that window without clearing the queue; the next // event must then start at the oldest packet that still has its data, not on // released ones, which would have no image (no capture JPEGs) and, for // encoding, nothing to encode. refs #4860 TEST_CASE("PacketQueue: an event never starts on a released packet") { PacketQueue queue; queue.addStream(); queue.setMaxVideoPackets(0); queue.setPreEventVideoPackets(5); // queuePacket() drops everything when no iterator is registered. packetqueue_iterator *holder_it = queue.get_video_it(false); REQUIRE(holder_it != nullptr); packetqueue_iterator *analysis_it = nullptr; // Keyframes every 10 packets; analysis on the last one, which is also the // event's snapshot packet. auto fill = [&](int count) { std::vector> packets; for (int i = 0; i < count; i++) { packets.push_back(decoded_video_packet(i % 10 == 0)); REQUIRE(queue.queuePacket(packets.back())); } // The holder was moved onto the first packet queued; take it past the // last one so only analysis_it bounds what may be released. while (queue.increment_it(holder_it, false)) {} analysis_it = queue.get_video_it(false); for (int i = 1; i < count; i++) REQUIRE(queue.increment_it(analysis_it, false)); REQUIRE(*(*analysis_it) == packets.back()); return packets; }; SECTION("passthrough: a widened window stops at the release boundary") { queue.setKeepKeyframes(true); auto packets = fill(30); // 5 back from packet 29 is 24, then back to the keyframe at 20. queue.releaseDecoded(); REQUIRE(packets[19]->image == nullptr); REQUIRE(packets[20]->image != nullptr); // Same window: the start is the keyframe at 20, as before the change. packetqueue_iterator *start = queue.get_event_start_packet_it(*analysis_it, 5); REQUIRE(*(*start) == packets[20]); queue.free_it(start); // Widened to 25 by a reload: unchanged code walked back to the keyframe at // 0, onto 20 packets without images. queue.setPreEventVideoPackets(25); start = queue.get_event_start_packet_it(*analysis_it, 25); REQUIRE(*(*start) == packets[20]); REQUIRE((*(*start))->image != nullptr); queue.free_it(start); } SECTION("encode: a widened window stops at the release boundary") { queue.setKeepKeyframes(false); auto packets = fill(30); // No keyframe needed: 5 back from packet 29 is 24. queue.releaseDecoded(); REQUIRE(packets[23]->image == nullptr); REQUIRE(packets[24]->image != nullptr); queue.setPreEventVideoPackets(25); packetqueue_iterator *start = queue.get_event_start_packet_it(*analysis_it, 25); REQUIRE(*(*start) == packets[24]); queue.free_it(start); } SECTION("encode reloaded to passthrough: forward to the next keyframe") { queue.setKeepKeyframes(false); auto packets = fill(35); // Released before 29, which is no keyframe. queue.releaseDecoded(); REQUIRE(packets[28]->image == nullptr); REQUIRE(packets[29]->image != nullptr); queue.setKeepKeyframes(true); queue.setPreEventVideoPackets(25); packetqueue_iterator *start = queue.get_event_start_packet_it(*analysis_it, 25); REQUIRE(*(*start) == packets[30]); REQUIRE((*(*start))->keyframe); queue.free_it(start); } queue.free_it(analysis_it); queue.free_it(holder_it); queue.stop(); queue.clear(); }