diff --git "a/.clang-format\r" "b/.clang-format\r" deleted file mode 100644 index 33bf2a3b9..000000000 --- "a/.clang-format\r" +++ /dev/null @@ -1,137 +0,0 @@ ---- -Language: Cpp -# BasedOnStyle: LLVM -AccessModifierOffset: -2 -AlignAfterOpenBracket: Align -AlignConsecutiveMacros: false -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AlignEscapedNewlines: Right -AlignOperands: true -AlignTrailingComments: true -AllowAllArgumentsOnNextLine: true -AllowAllConstructorInitializersOnNextLine: true -AllowAllParametersOfDeclarationOnNextLine: true -AllowShortBlocksOnASingleLine: Never -AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: All -AllowShortLambdasOnASingleLine: All -AllowShortIfStatementsOnASingleLine: Never -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: None -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: MultiLine -BinPackArguments: true -BinPackParameters: true -BraceWrapping: - AfterCaseLabel: false - AfterClass: false - AfterControlStatement: false - AfterEnum: false - AfterFunction: false - AfterNamespace: false - AfterObjCDeclaration: false - AfterStruct: false - AfterUnion: false - AfterExternBlock: false - BeforeCatch: false - BeforeElse: false - IndentBraces: false - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakBeforeBinaryOperators: None -BreakBeforeBraces: Attach -BreakBeforeInheritanceComma: false -BreakInheritanceList: BeforeColon -BreakBeforeTernaryOperators: true -BreakConstructorInitializersBeforeComma: false -BreakConstructorInitializers: BeforeColon -BreakAfterJavaFieldAnnotations: false -BreakStringLiterals: true -ColumnLimit: 80 -CommentPragmas: '^ IWYU pragma:' -CompactNamespaces: false -ConstructorInitializerAllOnOneLineOrOnePerLine: false -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 -Cpp11BracedListStyle: true -DeriveLineEnding: true -DerivePointerAlignment: false -DisableFormat: false -ExperimentalAutoDetectBinPacking: false -FixNamespaceComments: true -ForEachMacros: - - foreach - - Q_FOREACH - - BOOST_FOREACH -IncludeBlocks: Preserve -IncludeCategories: - - Regex: '^"(llvm|llvm-c|clang|clang-c)/' - Priority: 2 - SortPriority: 0 - - Regex: '^(<|"(gtest|gmock|isl|json)/)' - Priority: 3 - SortPriority: 0 - - Regex: '.*' - Priority: 1 - SortPriority: 0 -IncludeIsMainRegex: '(Test)?$' -IncludeIsMainSourceRegex: '' -IndentCaseLabels: false -IndentGotoLabels: true -IndentPPDirectives: None -IndentWidth: 2 -IndentWrappedFunctionNames: false -JavaScriptQuotes: Leave -JavaScriptWrapImports: true -KeepEmptyLinesAtTheStartOfBlocks: true -MacroBlockBegin: '' -MacroBlockEnd: '' -MaxEmptyLinesToKeep: 1 -NamespaceIndentation: None -ObjCBinPackProtocolList: Auto -ObjCBlockIndentWidth: 2 -ObjCSpaceAfterProperty: false -ObjCSpaceBeforeProtocolList: true -PenaltyBreakAssignment: 2 -PenaltyBreakBeforeFirstCallParameter: 19 -PenaltyBreakComment: 300 -PenaltyBreakFirstLessLess: 120 -PenaltyBreakString: 1000 -PenaltyBreakTemplateDeclaration: 10 -PenaltyExcessCharacter: 1000000 -PenaltyReturnTypeOnItsOwnLine: 60 -PointerAlignment: Right -ReflowComments: true -SortIncludes: true -SortUsingDeclarations: true -SpaceAfterCStyleCast: false -SpaceAfterLogicalNot: false -SpaceAfterTemplateKeyword: true -SpaceBeforeAssignmentOperators: true -SpaceBeforeCpp11BracedList: false -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyBlock: false -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 1 -SpacesInAngles: false -SpacesInConditionalStatement: false -SpacesInContainerLiterals: true -SpacesInCStyleCastParentheses: false -SpacesInParentheses: false -SpacesInSquareBrackets: false -SpaceBeforeSquareBrackets: false -Standard: Latest -StatementMacros: - - Q_UNUSED - - QT_REQUIRE_VERSION -TabWidth: 8 -UseCRLF: false -UseTab: Never -... - diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index f755bc395..9338885c1 100755 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -628,6 +628,7 @@ SET(UT_FILES test/overlaymodule_tests.cpp test/testSignalGeneratorSrc_tests.cpp test/audioToTextXform_tests.cpp + test/framefactory_memory_tests.cpp ${ARM64_UT_FILES} ${CUDA_UT_FILES} ) diff --git a/base/include/Command.h b/base/include/Command.h index f93a97294..5566176e5 100644 --- a/base/include/Command.h +++ b/base/include/Command.h @@ -24,7 +24,8 @@ class Command { SendMMQTimestamps, SendLastGTKGLRenderTS, DecoderPlaybackSpeed, - Mp4FileClose + Mp4FileClose, + Mp4ReaderPlaybackSpeed }; Command() { type = CommandType::None; } @@ -324,4 +325,39 @@ class DecoderPlaybackSpeed : public Command ar& playbackFps; ar& playbackSpeed; } +}; + +class Mp4ReaderPlaybackSpeedCommand : public Command +{ +public: + Mp4ReaderPlaybackSpeedCommand() : Command(CommandType::Mp4ReaderPlaybackSpeed) + { + playbackSpeed = 1.0f; + direction = true; + } + + Mp4ReaderPlaybackSpeedCommand(float _playbackSpeed, bool _direction = true) + : Command(CommandType::Mp4ReaderPlaybackSpeed) + { + playbackSpeed = _playbackSpeed; + direction = _direction; + } + + size_t getSerializeSize() + { + return Command::getSerializeSize() + sizeof(playbackSpeed) + sizeof(direction); + } + + float playbackSpeed; + bool direction; // fwd = true, bwd = false + +private: + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int /* file_version */) + { + ar& boost::serialization::base_object(*this); + ar& playbackSpeed; + ar& direction; + } }; \ No newline at end of file diff --git a/base/include/Mp4ReaderSource.h b/base/include/Mp4ReaderSource.h index 775f1e340..694e9c5c9 100644 --- a/base/include/Mp4ReaderSource.h +++ b/base/include/Mp4ReaderSource.h @@ -14,7 +14,7 @@ class Mp4ReaderSourceProps : public ModuleProps } - Mp4ReaderSourceProps(std::string _videoPath, bool _parseFS, uint16_t _reInitInterval, bool _direction, bool _readLoop, bool _giveLiveTS, int _parseFSTimeoutDuration = 15, bool _bFramesEnabled = false) : ModuleProps() + Mp4ReaderSourceProps(std::string _videoPath, bool _parseFS, uint16_t _reInitInterval, bool _direction, bool _readLoop, bool _giveLiveTS, int _parseFSTimeoutDuration = 15, bool _bFramesEnabled = false, float _playbackSpeed = 1.0f) : ModuleProps() { /* About props: - videoPath - Path of a video from where the reading will start. @@ -23,6 +23,7 @@ class Mp4ReaderSourceProps : public ModuleProps - parseFS - Read the NVR format till infinity, if true. Else we read only one file. - readLoop - Read a single video in loop. It can not be used in conjuction with live mode (reInitInterval > 0) or NVR mode (parseFS = true) mode. - giveLiveTS - If enabled, gives live timestamps instead of recorded timestamps in the video files. + - playbackSpeed - Initial playback speed (0.25x to 32x). Can be changed dynamically via changePlaybackSpeed(). */ if (reInitInterval < 0) @@ -37,12 +38,21 @@ class Mp4ReaderSourceProps : public ModuleProps "> reInitInterval <" + std::to_string(reInitInterval) + "> parseFS <" + std::to_string(_parseFS) + ">"; throw AIPException(AIP_FATAL, errMsg); } + + // Validate playback speed + if (_playbackSpeed <= 0.0f) + { + auto errMsg = "playbackSpeed must be greater than 0. Provided: <" + std::to_string(_playbackSpeed) + ">"; + throw AIPException(AIP_FATAL, errMsg); + } + auto canonicalVideoPath = boost::filesystem::canonical(_videoPath); videoPath = canonicalVideoPath.string(); parseFS = _parseFS; bFramesEnabled = _bFramesEnabled; direction = _direction; giveLiveTS = _giveLiveTS; + playbackSpeed = _playbackSpeed; if (_reInitInterval < 0) { throw AIPException(AIP_FATAL, "reInitInterval must be 0 or more seconds"); @@ -70,7 +80,7 @@ class Mp4ReaderSourceProps : public ModuleProps size_t getSerializeSize() { - return ModuleProps::getSerializeSize() + sizeof(videoPath) + sizeof(parseFS) + sizeof(skipDir) + sizeof(direction) + sizeof(parseFSTimeoutDuration) + sizeof(biggerFrameSize) + sizeof(biggerMetadataFrameSize) + sizeof(bFramesEnabled) + sizeof(forceFPS); + return ModuleProps::getSerializeSize() + sizeof(videoPath) + sizeof(parseFS) + sizeof(skipDir) + sizeof(direction) + sizeof(parseFSTimeoutDuration) + sizeof(biggerFrameSize) + sizeof(biggerMetadataFrameSize) + sizeof(bFramesEnabled) + sizeof(forceFPS) + sizeof(playbackSpeed); } std::string skipDir = "./data/Mp4_videos"; @@ -85,6 +95,7 @@ class Mp4ReaderSourceProps : public ModuleProps bool readLoop = false; bool giveLiveTS = false; bool forceFPS = false; + float playbackSpeed = 1.0f; private: friend class boost::serialization::access; @@ -103,6 +114,7 @@ class Mp4ReaderSourceProps : public ModuleProps ar& readLoop; ar& giveLiveTS; ar& forceFPS; + ar& playbackSpeed; } }; @@ -119,6 +131,7 @@ class Mp4ReaderSource : public Module void setImageMetadata(std::string& pinId, framemetadata_sp& metadata); std::string addOutPutPin(framemetadata_sp& metadata); bool changePlayback(float speed, bool direction); + bool changePlaybackSpeed(float speed, bool direction = true); bool getVideoRangeFromCache(std::string videoPath, uint64_t& start_ts, uint64_t& end_ts); bool randomSeek(uint64_t skipTS, bool forceReopen = false); bool refreshCache(); diff --git a/base/src/FrameFactory.cpp b/base/src/FrameFactory.cpp index fc56d2ee1..a423dbf0e 100755 --- a/base/src/FrameFactory.cpp +++ b/base/src/FrameFactory.cpp @@ -80,9 +80,29 @@ void FrameFactory::destroy(Frame *pointer) boost::mutex::scoped_lock lock(m_mutex); counter.fetch_sub(1, memory_order_seq_cst); + // Calculate the actual allocation size + // If the pointer has been offset (by skipBytes), we need to reconstruct + // the original allocation size to free the correct number of chunks + size_t actualSize = pointer->size(); + bool isOriginal = (pointer->myOrig == pointer->data()); + + if (!isOriginal) { + // The pointer has been moved forward by skipBytes + ptrdiff_t offset = static_cast(pointer->data()) - static_cast(pointer->myOrig); + // The original allocation was for (current size + offset) bytes + actualSize = pointer->size() + offset; + LOG_TRACE << "destroy frame with offset: current size=" << pointer->size() + << " offset=" << offset + << " original allocation size=" << actualSize + << " pointer=" << pointer->myOrig; + } + + // Calculate chunks based on the original allocation size + size_t n = getNumberOfChunks(actualSize); + + // Free the memory chunks from the original pointer if (pointer->myOrig != NULL) { - size_t n = getNumberOfChunks(pointer->size()); numberOfChunks.fetch_sub(n, memory_order_seq_cst); memory_allocator->freeChunks(pointer->myOrig, n); } diff --git a/base/src/Logger.cpp b/base/src/Logger.cpp index c23e3f207..979eabeef 100755 --- a/base/src/Logger.cpp +++ b/base/src/Logger.cpp @@ -20,8 +20,10 @@ Logger* Logger::getLogger() { return instance.get(); } - - initLogger(LoggerProps()); + LoggerProps props; + std::cout << "Logger initializing with default props. => console logs "<< (props.enableConsoleLog ? "enabled" : "disabled") + << ", file logs " << (props.enableFileLog ? "enabled" : "disabled") << std::endl; + initLogger(props); return instance.get(); } diff --git a/base/src/Module.cpp b/base/src/Module.cpp index 1bb9ce204..401ec1304 100644 --- a/base/src/Module.cpp +++ b/base/src/Module.cpp @@ -153,7 +153,7 @@ Module::Module(Kind nature, string name, ModuleProps _props) mQue.reset(new FrameContainerQueue(_props.qlen)); onStepFail = boost::bind(&Module::ignore, this, 0); - LOG_INFO << "Setting Module tolerance for step failure as: " << "<0>. Currently there is no way to change this."; + LOG_INFO << getId() << ": Setting Module tolerance for step failure as: " << "<0>. Currently there is no way to change this."; pacer = boost::shared_ptr(new PaceMaker(_props.fps)); auto tempId = getId(); @@ -268,14 +268,14 @@ bool Module::setNext(boost::shared_ptr next, vector &pinIdArr, { if (next->getNature() < this->getNature()) { - LOG_ERROR << "Can not connect these modules " << this->getId() << " -> " + LOG_ERROR << getId() << ": Can not connect these modules " << this->getId() << " -> " << next->getId(); return false; } if (pinIdArr.size() == 0) { - LOG_ERROR << "No Pins to connect. " << this->getId() << " -> " + LOG_ERROR << getId() << ": No Pins to connect. " << this->getId() << " -> " << next->getId(); return false; } @@ -283,8 +283,8 @@ bool Module::setNext(boost::shared_ptr next, vector &pinIdArr, auto nextModuleId = next->getId(); if (mModules.find(nextModuleId) != mModules.end()) { - LOG_ERROR << "<" << getId() << "> Connection for <" << nextModuleId - << " > already done."; + LOG_ERROR << getId() << ": Connection for <" << nextModuleId + << "> already done."; return false; } mModules[nextModuleId] = next; @@ -324,7 +324,7 @@ bool Module::setNext(boost::shared_ptr next, vector &pinIdArr, { mModules.erase(nextModuleId); mConnections.erase(nextModuleId); - LOG_FATAL << ""; + LOG_FATAL << getId() << ": addInputPin. PinId<" << pinId << ">. Unknown exception."; throw AIPException(AIP_FATAL, "<" + getId() + "> addInputPin. PinId<" + pinId + ">. Unknown exception."); } @@ -363,7 +363,7 @@ bool Module::setNext(boost::shared_ptr next, vector &pinIdArr, { mModules.erase(nextModuleId); mConnections.erase(nextModuleId); - LOG_FATAL << ""; + LOG_FATAL << getId() << ": addInputPin. PinId<" << pinId << ">. Unknown exception."; throw AIPException(AIP_FATAL, "<" + getId() + "> addInputPin. PinId<" + pinId + ">. Unknown exception."); @@ -391,7 +391,7 @@ bool Module::setNext(boost::shared_ptr next, vector &pinIdArr, { mModules.erase(nextModuleId); mConnections.erase(nextModuleId); - LOG_FATAL << ""; + LOG_FATAL << getId() << ": addInputPin. PinId<" << pinId << ">. Unknown exception."; throw AIPException(AIP_FATAL, "<" + getId() + "> addInputPin. PinId<" + pinId + ">. Unknown exception."); @@ -1118,7 +1118,7 @@ bool Module::queuePlayPauseCommand(PlayPauseCommand ppCmd, bool priority) { if (!Module::try_push(frames)) { - LOG_ERROR << "failed to push play command to the que"; + LOG_ERROR << getId() << ": failed to push play command to the que"; return false; } } @@ -1160,10 +1160,12 @@ bool Module::queueStep() bool Module::relay(boost::shared_ptr next, bool open, bool priority) { auto nextModuleId = next->getId(); + LOG_INFO << getId() << ": Setting relay to <" << nextModuleId << "> to " << (open ? "open" : "close"); + if (mModules.find(nextModuleId) == mModules.end()) { - LOG_ERROR << "<" << getId() << "> Connection for <" << nextModuleId - << " > doesn't exist."; + LOG_ERROR << getId() << ": Connection for <" << nextModuleId + << "> doesn't exist."; return false; } @@ -1244,7 +1246,7 @@ bool Module::processSourceQue() } else { - LOG_ERROR << frame->getMetadata()->getFrameType() << "<> not handled"; + LOG_ERROR << getId() << ": " << frame->getMetadata()->getFrameType() << "<> not handled"; } } } @@ -1271,23 +1273,23 @@ bool Module::step() bool ret = false; if (myNature == SOURCE) { - LOG_TRACE << "Step start"; + LOG_TRACE << getId() << ": Step start"; if (!processSourceQue()) { return true; } - LOG_TRACE << "Step source q processed"; + LOG_TRACE << getId() << ": Step source q processed"; bool forceStep = shouldForceStep(); pacer->start(); if (mPlay || forceStep) { - LOG_TRACE << "produce call immminent"; + LOG_TRACE << getId() << ": produce call immminent"; mProfiler->startPipelineLap(); ret = produce(); mProfiler->endLap(0); - LOG_TRACE << "produce call fin"; + LOG_TRACE << getId() << ": produce call fin"; } else { @@ -1553,11 +1555,11 @@ bool Module::stepNonSource(frame_container &frames) } catch (const std::exception &exception) { - LOG_FATAL << getId() << "<>" << exception.what(); + LOG_FATAL << getId() << ": " << exception.what(); } catch (...) { - LOG_FATAL << getId() << "<> Unknown exception. Catching throw"; + LOG_FATAL << getId() << ": Unknown exception. Catching throw"; } return ret; @@ -1647,7 +1649,7 @@ void Module::ignore(int times) observed++; if (observed >= times && times > 0) { - LOG_TRACE << "stopping due to step failure "; + LOG_TRACE << getId() << ": stopping due to step failure "; observed = 0; handleStop(); } @@ -1656,7 +1658,7 @@ void Module::ignore(int times) void Module::stop_onStepfail() { // ctrl - get and print the last command processed which might have caused the error - LOG_ERROR << "Stopping " << myId << " due to step failure "; + LOG_ERROR << getId() << ": Stopping due to step failure "; handleStop(); } @@ -1680,7 +1682,7 @@ void Module::emit_fatal(unsigned short eventID) { // we dont have a handler let's kill this thread std::string msg("Fatal error in module "); - LOG_FATAL << "FATAL error in module : " << myName; + LOG_FATAL << getId() << ": FATAL error in module"; msg += myName; msg += " Event ID "; msg += std::to_string(eventID); diff --git a/base/src/Mp4ReaderSource.cpp b/base/src/Mp4ReaderSource.cpp index 731c35dfe..d68e01994 100644 --- a/base/src/Mp4ReaderSource.cpp +++ b/base/src/Mp4ReaderSource.cpp @@ -233,11 +233,54 @@ class Mp4ReaderDetailAbs void setPlayback(float _speed, bool _direction) { - if (_speed != mState.speed) + // Update both speed variables for consistency + bool speedChanged = (_speed != playbackSpeed); + if (speedChanged) { + playbackSpeed = _speed; mState.speed = _speed; + + // Only update FPS if video is open and forceFPS is not enabled + if (mState.demux && !mProps.forceFPS) + { + // For speeds < 8x: Adjust FPS without frame dropping (slow-mo, 1x, 2x, 4x) + // All frames are read, but delivered at different rate + if (playbackSpeed < 8) + { + mProps.fps = mFPS * playbackSpeed; + LOG_INFO << "Playback speed changed to <" << playbackSpeed << "x>, FPS updated to <" << mProps.fps << ">"; + // Update Module's FPS directly without triggering full setProps + // This only updates FPS in the Module, avoiding video reinitialization + setMp4ReaderProps(mProps); + } + // For 8x, 16x, 32x: Use I-frame skipping mode + // GOP-based FPS adjustment + randomSeek in produceFrames + else if (playbackSpeed == 8 || playbackSpeed == 16 || playbackSpeed == 32) + { + auto gop = getGop(); + if (gop) + { + mProps.fps = (mFPS * playbackSpeed) / gop; + LOG_INFO << "Playback speed changed to <" << playbackSpeed << "x> with GOP <" << gop << ">, FPS updated to <" << mProps.fps << ">"; + } + else + { + mProps.fps = mFPS * playbackSpeed; + LOG_WARNING << "GOP is 0, using fallback FPS calculation: <" << mProps.fps << ">"; + } + setMp4ReaderProps(mProps); + } + else + { + // Unsupported speed + LOG_WARNING << "Playback speed <" << playbackSpeed << "x> not explicitly supported. Using direct FPS multiplication."; + mProps.fps = mFPS * playbackSpeed; + setMp4ReaderProps(mProps); + } + } } - // only if direction changes + + // Handle direction changes if (mState.direction != _direction) { mState.direction = _direction; @@ -1744,7 +1787,17 @@ bool Mp4ReaderSource::init() mDetail->h264ImagePinId = h264ImagePinId; mDetail->metadataFramePinId = metadataFramePinId; mDetail->controlModule = controlModule; - return mDetail->Init(); + + bool initResult = mDetail->Init(); + + // Apply initial playback speed from props if not default (1.0) + if (initResult && props.playbackSpeed != 1.0f) + { + LOG_INFO << "Applying initial playback speed from props: " << props.playbackSpeed << "x"; + mDetail->setPlayback(props.playbackSpeed, props.direction); + } + + return initResult; } void Mp4ReaderSource::setImageMetadata(std::string& pinId, framemetadata_sp& metadata) @@ -1893,6 +1946,12 @@ bool Mp4ReaderSource::changePlayback(float speed, bool direction) return queuePlayPauseCommand(ppc); } +bool Mp4ReaderSource::changePlaybackSpeed(float speed, bool direction) +{ + Mp4ReaderPlaybackSpeedCommand cmd(speed, direction); + return queueCommand(cmd, true); +} + bool Mp4ReaderSource::handleCommand(Command::CommandType type, frame_sp& frame) { if (type == Command::CommandType::Seek) @@ -1901,6 +1960,13 @@ bool Mp4ReaderSource::handleCommand(Command::CommandType type, frame_sp& frame) getCommand(seekCmd, frame); return mDetail->randomSeek(seekCmd.seekStartTS, seekCmd.forceReopen); } + else if (type == Command::CommandType::Mp4ReaderPlaybackSpeed) + { + Mp4ReaderPlaybackSpeedCommand speedCmd; + getCommand(speedCmd, frame); + mDetail->setPlayback(speedCmd.playbackSpeed, speedCmd.direction); + return true; + } else { return Module::handleCommand(type, frame); diff --git a/base/test/framefactory_memory_tests.cpp b/base/test/framefactory_memory_tests.cpp new file mode 100644 index 000000000..b403f7625 --- /dev/null +++ b/base/test/framefactory_memory_tests.cpp @@ -0,0 +1,512 @@ +#include +#include "FrameFactory.h" +#include "Frame.h" +#include "FrameMetadata.h" +#include "EncodedImageMetadata.h" +#include "RawImageMetadata.h" +#include "H264Metadata.h" +#include +#include +#include +#include + +BOOST_AUTO_TEST_SUITE(framefactory_memory_tests) + +// Helper class to track memory allocations +class MemoryTracker { +private: + std::atomic totalAllocated{0}; + std::atomic totalFreed{0}; + std::atomic currentInUse{0}; + std::atomic allocCount{0}; + std::atomic freeCount{0}; + +public: + void recordAllocation(size_t bytes) { + totalAllocated += bytes; + currentInUse += bytes; + allocCount++; + } + + void recordFree(size_t bytes) { + totalFreed += bytes; + currentInUse -= bytes; + freeCount++; + } + + void printStats(const std::string& testName) { + LOG_INFO << "=== Memory Stats for " << testName << " ==="; + LOG_INFO << "Total Allocated: " << totalAllocated << " bytes"; + LOG_INFO << "Total Freed: " << totalFreed << " bytes"; + LOG_INFO << "Currently In Use: " << currentInUse << " bytes"; + LOG_INFO << "Alloc Count: " << allocCount; + LOG_INFO << "Free Count: " << freeCount; + LOG_INFO << "Leaked: " << (totalAllocated - totalFreed) << " bytes"; + } + + bool hasLeaks() { + return totalAllocated != totalFreed; + } + + size_t getLeakedBytes() { + return totalAllocated > totalFreed ? totalAllocated - totalFreed : 0; + } +}; + +// Test 1: Basic FrameFactory allocation and deallocation +BOOST_AUTO_TEST_CASE(framefactory_basic_memory_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + LOG_INFO << "Starting framefactory_basic_memory_test"; + + auto metadata = framemetadata_sp(new RawImageMetadata(1920, 1080, ImageMetadata::ImageType::RGB, CV_8UC3, 0, CV_8U, FrameMetadata::HOST)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 10)); + + // Get initial pool health + auto initialHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Initial pool health: " << initialHealth; + + // Create and destroy frames + { + auto frame1 = factory->create(1024, factory); + BOOST_TEST(frame1.get() != nullptr); + BOOST_TEST(frame1->size() == 1024); + + auto frame2 = factory->create(2048, factory); + BOOST_TEST(frame2.get() != nullptr); + BOOST_TEST(frame2->size() == 2048); + } + + // Frames should be destroyed here + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final pool health: " << finalHealth; + + // Check that all memory is released + BOOST_TEST(finalHealth.find("Frames<0>") != std::string::npos); +} + +// Test 2: Simulate the skipBytes pointer increment issue +BOOST_AUTO_TEST_CASE(framefactory_pointer_increment_leak_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_pointer_increment_leak_test"; + + auto metadata = framemetadata_sp(new H264Metadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 100)); + + MemoryTracker tracker; + + // Simulate what happens in Mp4ReaderDetailH264::skipBytes + const int skipOffset = 512; + const int numFrames = 10; + + for (int i = 0; i < numFrames; i++) { + // Create a frame (simulating makeFrame in Mp4ReaderSource) + size_t frameSize = 4096; + auto frame = factory->create(frameSize, factory); + tracker.recordAllocation(frameSize); + + // Get the data pointer + uint8_t* dataPtr = static_cast(frame->data()); + + // SIMULATE THE BUG: Increment pointer (like skipBytes does) + // In the real code, this modified pointer is stored back + dataPtr += skipOffset; + + // Now when frame goes out of scope, it tries to free the WRONG address + // This is the memory leak! + + LOG_TRACE << "Frame " << i << ": Original ptr=" << (void*)frame->data() + << ", Modified ptr=" << (void*)dataPtr; + } + + // Check pool health after frames are destroyed + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final pool health after pointer increment test: " << finalHealth; + + tracker.printStats("pointer_increment_leak_test"); +} + +// Test 3: Test makeFrame with size trimming (as used in Mp4ReaderSource) +BOOST_AUTO_TEST_CASE(framefactory_trim_frame_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_trim_frame_test"; + + auto metadata = framemetadata_sp(new EncodedImageMetadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 10)); + + // Create a big frame + size_t bigSize = 8192; + auto bigFrame = factory->create(bigSize, factory); + BOOST_TEST(bigFrame->size() == bigSize); + + // Simulate makeFrameTrim operation + size_t trimmedSize = 4096; + auto trimmedFrame = factory->create(bigFrame, trimmedSize, factory); + BOOST_TEST(trimmedFrame->size() == trimmedSize); + + // Original big frame should release excess memory + auto health = factory->getPoolHealthRecord(); + LOG_INFO << "Pool health after trim: " << health; +} + +// Test 4: Stress test with rapid allocation/deallocation +BOOST_AUTO_TEST_CASE(framefactory_stress_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_stress_test"; + + auto metadata = framemetadata_sp(new RawImageMetadata(1920, 1080, ImageMetadata::ImageType::YUV420, CV_8UC1, 0, CV_8U, FrameMetadata::HOST)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 100)); + + const int numIterations = 100; + const int framesPerIteration = 10; + + auto initialHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Initial health: " << initialHealth; + + for (int iter = 0; iter < numIterations; iter++) { + std::vector frames; + + // Allocate frames + for (int i = 0; i < framesPerIteration; i++) { + // YUV420 at 1920x1080 requires 1920*1080*1.5 = 3,110,400 bytes + size_t size = 3110400 + (i * 1024); // Base YUV420 size with small variations + frames.push_back(factory->create(size, factory)); + } + + // Clear frames (should deallocate) + frames.clear(); + + if (iter % 10 == 0) { + auto health = factory->getPoolHealthRecord(); + LOG_TRACE << "Iteration " << iter << " health: " << health; + } + } + + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final health after stress test: " << finalHealth; + + // All frames should be released + BOOST_TEST(finalHealth.find("Frames<0>") != std::string::npos); +} + +// Test 5: Reproduce the exact Mp4ReaderSource memory leak pattern +BOOST_AUTO_TEST_CASE(mp4reader_memory_leak_simulation) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting mp4reader_memory_leak_simulation"; + + // Simulate Mp4ReaderDetailH264 behavior + auto h264Metadata = framemetadata_sp(new H264Metadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(h264Metadata, 1000)); + + const size_t biggerFrameSize = 300000; // From Mp4ReaderSourceProps + const int skipOffset = 512; // From Mp4ReaderDetailH264::skipOffset + const int numFramesToProcess = 50; + + MemoryTracker tracker; + size_t expectedLeakPerFrame = 0; + + for (int i = 0; i < numFramesToProcess; i++) { + // Step 1: Create frame (like makeFrame in produceFrames) + auto imgFrame = factory->create(biggerFrameSize, factory); + tracker.recordAllocation(biggerFrameSize); + + // Step 2: Get data pointer and simulate skipBytes + uint8_t* sampleFrame = static_cast(imgFrame->data()); + + // THE BUG: This modifies the pointer that Frame stores internally + // When frame is destroyed, it will try to free (sampleFrame + skipOffset) + // instead of the original allocation + sampleFrame += skipOffset; + + // Step 3: Simulate frame processing (readNextFrame would write to this) + // In real code, data is written starting at the incremented pointer + + // Step 4: Create trimmed frame (like makeFrameTrim) + size_t actualDataSize = 2048; // Simulated actual frame size + auto trimmedFrame = factory->create(imgFrame, actualDataSize + skipOffset, factory); + + // Step 5: Frame goes out of scope and attempts to free wrong address + // This leaks the first skipOffset bytes! + expectedLeakPerFrame = skipOffset; + + if (i % 10 == 0) { + LOG_TRACE << "Processed " << i << " frames"; + } + } + + // Calculate expected leak + size_t expectedTotalLeak = expectedLeakPerFrame * numFramesToProcess; + LOG_INFO << "Expected leak: " << expectedTotalLeak << " bytes"; + + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final pool health: " << finalHealth; + + tracker.printStats("mp4reader_leak_simulation"); + + // The test will show that memory is leaked + LOG_WARNING << "Memory leak detected: " << tracker.getLeakedBytes() << " bytes"; +} + +// Test 6: Test with concurrent access (thread safety) +BOOST_AUTO_TEST_CASE(framefactory_thread_safety_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_thread_safety_test"; + + auto metadata = framemetadata_sp(new RawImageMetadata(640, 480, ImageMetadata::ImageType::MONO, CV_8UC1, 0, CV_8U, FrameMetadata::HOST)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 100)); + + std::atomic totalFramesCreated{0}; + std::atomic totalFramesDestroyed{0}; + + const int numThreads = 4; + const int framesPerThread = 25; + + std::vector threads; + + // Create threads that allocate and deallocate frames + for (int t = 0; t < numThreads; t++) { + threads.emplace_back([&factory, &totalFramesCreated, &totalFramesDestroyed, framesPerThread]() { + boost::shared_ptr localFactory = factory; + for (int i = 0; i < framesPerThread; i++) { + auto frame = localFactory->create(1024 * (i % 10 + 1), localFactory); + totalFramesCreated++; + + // Simulate some work + std::this_thread::sleep_for(std::chrono::microseconds(100)); + + // Frame destroyed when going out of scope + totalFramesDestroyed++; + } + }); + } + + // Wait for all threads to complete + for (auto& t : threads) { + t.join(); + } + + LOG_INFO << "Total frames created: " << totalFramesCreated; + LOG_INFO << "Total frames destroyed: " << totalFramesDestroyed; + + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final health after thread test: " << finalHealth; + + // All frames should be released + BOOST_TEST(finalHealth.find("Frames<0>") != std::string::npos); +} + +// Test 7: Verify the fix for skipBytes issue +BOOST_AUTO_TEST_CASE(framefactory_skipbytes_fix_verification) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_skipbytes_fix_verification"; + + auto metadata = framemetadata_sp(new H264Metadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 10)); + + const int skipOffset = 512; + + // Correct way to handle skipBytes + auto frame = factory->create(4096, factory); + uint8_t* originalPtr = static_cast(frame->data()); + + // CORRECT: Use a local variable for the offset pointer + uint8_t* workingPtr = originalPtr + skipOffset; + + // Do work with workingPtr... + // But frame still has the original pointer for proper cleanup + + LOG_INFO << "Original ptr: " << (void*)originalPtr; + LOG_INFO << "Working ptr: " << (void*)workingPtr; + LOG_INFO << "Frame data ptr (unchanged): " << frame->data(); + + // Verify frame still has original pointer + BOOST_TEST(frame->data() == originalPtr); + + // Frame will correctly free originalPtr when destroyed + auto health = factory->getPoolHealthRecord(); + LOG_INFO << "Pool health before frame destruction: " << health; +} + +// Test 8: Verify that the FrameFactory::destroy fix handles offset pointers correctly +BOOST_AUTO_TEST_CASE(framefactory_offset_pointer_destroy_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::trace; + Logger::setLogLevel(boost::log::trivial::severity_level::trace); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_offset_pointer_destroy_test - Testing the fix for skipBytes memory leak"; + + auto metadata = framemetadata_sp(new H264Metadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 100)); + + const int skipOffset = 512; + const size_t originalSize = 4096; + const int numIterations = 100; + + // Get initial pool health + auto initialHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Initial pool health: " << initialHealth; + + for (int i = 0; i < numIterations; i++) { + // Step 1: Create a frame with original size + auto frame = factory->create(originalSize, factory); + BOOST_TEST(frame->size() == originalSize); + + // Step 2: Get the raw Frame pointer to manipulate it (simulating skipBytes) + Frame* rawFrame = frame.get(); + + // Store original data pointer for comparison + void* originalDataPtr = frame->data(); + + // Step 3: SIMULATE SKIPBYTES BUG + // We need to modify the mutable_buffer base class to simulate the pointer increment + // This is exactly what happens in Mp4ReaderDetailH264::skipBytes + boost::asio::mutable_buffer* bufferPtr = static_cast(rawFrame); + uint8_t* currentData = static_cast(bufferPtr->data()); + + // Modify the data pointer (simulating buffer += skipOffset in skipBytes) + *bufferPtr = boost::asio::mutable_buffer(currentData + skipOffset, frame->size() - skipOffset); + + // Verify the pointer has been offset + BOOST_TEST(frame->data() != originalDataPtr); + BOOST_TEST(static_cast(frame->data()) == static_cast(originalDataPtr) + skipOffset); + + // Step 4: Frame goes out of scope + // The destroy() method should handle the offset pointer correctly + // It should: + // 1. Detect that frame->data() != frame->myOrig + // 2. Calculate the original allocation size as (current size + offset) + // 3. Free the correct number of chunks from myOrig + } + + // Give time for frames to be destroyed + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Check final pool health - should show no leaks + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final pool health after " << numIterations << " iterations: " << finalHealth; + + // All frames should be properly freed + BOOST_TEST(finalHealth.find("Frames<0>") != std::string::npos); +} + +// Test 9: Verify edge cases for the offset pointer fix +BOOST_AUTO_TEST_CASE(framefactory_offset_pointer_edge_cases) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_offset_pointer_edge_cases"; + + auto metadata = framemetadata_sp(new H264Metadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 100)); + + // Test Case 1: Multiple different offset sizes + std::vector offsets = {0, 256, 512, 1024, 2048}; + std::vector frameSizes = {1024, 2048, 4096, 8192, 16384}; + + for (size_t frameSize : frameSizes) { + for (size_t offset : offsets) { + if (offset >= frameSize) continue; // Skip invalid offsets + + auto frame = factory->create(frameSize, factory); + Frame* rawFrame = frame.get(); + + // Apply offset if non-zero + if (offset > 0) { + boost::asio::mutable_buffer* bufferPtr = static_cast(rawFrame); + uint8_t* currentData = static_cast(bufferPtr->data()); + *bufferPtr = boost::asio::mutable_buffer(currentData + offset, frameSize - offset); + } + + LOG_TRACE << "Testing frame size=" << frameSize << " offset=" << offset; + } + } + + // Check for no memory leaks + auto health = factory->getPoolHealthRecord(); + LOG_INFO << "Final pool health for edge cases: " << health; + BOOST_TEST(health.find("Frames<0>") != std::string::npos); +} + +// Test 10: Performance test to ensure the fix doesn't significantly impact performance +BOOST_AUTO_TEST_CASE(framefactory_offset_pointer_performance_test) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::warning; + Logger::setLogLevel(boost::log::trivial::severity_level::warning); + Logger::initLogger(loggerProps); + + LOG_INFO << "Starting framefactory_offset_pointer_performance_test"; + + auto metadata = framemetadata_sp(new H264Metadata(1920, 1080)); + auto factory = boost::shared_ptr(new FrameFactory(metadata, 1000)); + + const int numFrames = 10000; + const size_t frameSize = 4096; + const int skipOffset = 512; + + // Time allocation and deallocation with offset pointers + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < numFrames; i++) { + auto frame = factory->create(frameSize, factory); + + // Apply offset to half the frames + if (i % 2 == 0) { + Frame* rawFrame = frame.get(); + boost::asio::mutable_buffer* bufferPtr = static_cast(rawFrame); + uint8_t* currentData = static_cast(bufferPtr->data()); + *bufferPtr = boost::asio::mutable_buffer(currentData + skipOffset, frameSize - skipOffset); + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + LOG_INFO << "Processed " << numFrames << " frames in " << duration.count() << "ms"; + LOG_INFO << "Average time per frame: " << (duration.count() / (double)numFrames) << "ms"; + + // Verify no memory leaks + auto finalHealth = factory->getPoolHealthRecord(); + LOG_INFO << "Final pool health after performance test: " << finalHealth; + BOOST_TEST(finalHealth.find("Frames<0>") != std::string::npos); +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/base/test/mp4readersource_tests.cpp b/base/test/mp4readersource_tests.cpp index 53c97198c..cd0857ec5 100644 --- a/base/test/mp4readersource_tests.cpp +++ b/base/test/mp4readersource_tests.cpp @@ -573,4 +573,35 @@ BOOST_AUTO_TEST_CASE(max_buffer_size_change_props) BOOST_TEST((frames.find(pinId) != frames.end())); } +BOOST_AUTO_TEST_CASE(mp4_reader_memory_leak) +{ + std::string videoPath = "./data/Mp4_videos/h264_video_metadata/20230514/0011/1686723796848.mp4"; + bool parseFS = false; + auto mp4ReaderProps = Mp4ReaderSourceProps(videoPath, parseFS, 0, true, false, false); + mp4ReaderProps.readLoop = true; + mp4ReaderProps.fps = 2000; + auto mp4Reader = boost::shared_ptr(new Mp4ReaderSource(mp4ReaderProps)); + + auto encodedImageMetadata = framemetadata_sp(new H264Metadata(0, 0)); + mp4Reader->addOutPutPin(encodedImageMetadata); + + auto mp4Metadata = framemetadata_sp(new Mp4VideoMetadata("v_1")); + mp4Reader->addOutPutPin(mp4Metadata); + + auto statSink = boost::shared_ptr(new StatSink()); + mp4Reader->setNext(statSink); + + auto p = boost::shared_ptr(new PipeLine("test")); + p->appendModule(mp4Reader); + p->init(); + + p->run_all_threaded(); + boost::this_thread::sleep_for(boost::chrono::seconds(1000)); + + LOG_INFO<<"Stopping pipeline"; + p->stop(); + p->term(); + p->wait_for_all(); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/thirdparty/custom-overlay/libmp4/portfile.cmake b/thirdparty/custom-overlay/libmp4/portfile.cmake index 192d535b0..5dd72702e 100644 --- a/thirdparty/custom-overlay/libmp4/portfile.cmake +++ b/thirdparty/custom-overlay/libmp4/portfile.cmake @@ -3,8 +3,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO Apra-Labs/libmp4 - REF 98f8ae9637093c822f344ec95c8cffbb814dd336 - SHA512 34c8ced415b5b1e03c0b04148ca5647109a70226af1fdc3c0739c8d88e68294ebe187a59d44008d5bea3fbab7e09b19e311a03712710f62b53444a92e924db4c + REF db1b88f707e40edf7a77a7dcff58ec023e801caf + SHA512 503320d12cc3da5bf5b611d4852ad7d241bcff001ddae6f26b19d17c89746b53863798e71ec80c43853dd4bf9666502a9506f2ae9c3b58fcbd53be2fb11667e2 HEAD_REF forApraPipes ) vcpkg_configure_cmake(