From fefc76b1cdd43e3658e4557b625a3af9a916f2fc Mon Sep 17 00:00:00 2001 From: Fancy code <258828110.@qq.com> Date: Thu, 25 Jul 2024 02:04:40 +0800 Subject: [PATCH] Backup:use hardware jpeg encode. --- application/HuntingCamera/src/MainThread.cpp | 8 - .../MissionManager/src/MediaTaskHandle.cpp | 2 + hal/abstract/IHalCpp.cpp | 7 +- hal/include/IHalCpp.h | 4 +- hal/src/CameraHal.cpp | 15 +- hal/src/CameraHal.h | 4 + middleware/IpcConfig/include/IIpcConfig.h | 1 + middleware/McuManager/src/McuManagerImpl.cpp | 4 - middleware/MediaManager/src/MediaHandle.cpp | 73 ++- middleware/MediaManager/src/MediaHandle.h | 3 + middleware/MediaManager/src/TakePicture.cpp | 14 +- middleware/MediaManager/src/TakePicture.h | 1 + middleware/MediaManager/src/VStreamHandle.cpp | 6 +- middleware/MediaManager/src/VStreamHandle.h | 1 + test/application/HuntingCamera/CMakeLists.txt | 2 + .../src_mock/HuntingCameraTest.cpp | 1 + .../src_mock/MediaManager_Mock_Test.cpp | 3 + test/hal/tool/src/CameraHalMock.cpp | 52 ++- test/hal/tool/src/CameraHalMock.h | 3 + test/support_test/rv1106_jpeg_encoder.jpg | Bin 0 -> 96617 bytes utils/MediaBase/include/MediaBase.h | 3 +- utils/MediaBase/src/FfmpegDecoderV2.cpp | 294 ++++++++++++ utils/MediaBase/src/FfmpegDecoderV2.h | 74 ++++ utils/MediaBase/src/FfmpegEncoderV2.cpp | 418 ++++++++++++++++++ utils/MediaBase/src/FfmpegEncoderV2.h | 79 ++++ utils/MediaBase/src/FfmpegReadFile.cpp | 2 + utils/MediaBase/src/FfmpegReadFile.h | 3 + utils/MediaBase/src/FfmpegTakePicture.cpp | 50 ++- utils/MediaBase/src/FfmpegTakePicture.h | 4 +- utils/MediaBase/src/FfmpegThumbnailV2.cpp | 215 +++++++++ utils/MediaBase/src/FfmpegThumbnailV2.h | 84 ++++ utils/MediaBase/src/IMediaBase.cpp | 2 +- utils/MediaBase/src/IMediaBase.h | 2 +- utils/MediaBase/src/MediaBase.cpp | 4 +- 34 files changed, 1391 insertions(+), 47 deletions(-) create mode 100755 test/support_test/rv1106_jpeg_encoder.jpg create mode 100644 utils/MediaBase/src/FfmpegDecoderV2.cpp create mode 100644 utils/MediaBase/src/FfmpegDecoderV2.h create mode 100644 utils/MediaBase/src/FfmpegEncoderV2.cpp create mode 100644 utils/MediaBase/src/FfmpegEncoderV2.h create mode 100644 utils/MediaBase/src/FfmpegThumbnailV2.cpp create mode 100644 utils/MediaBase/src/FfmpegThumbnailV2.h diff --git a/application/HuntingCamera/src/MainThread.cpp b/application/HuntingCamera/src/MainThread.cpp index aad4b62..b145651 100644 --- a/application/HuntingCamera/src/MainThread.cpp +++ b/application/HuntingCamera/src/MainThread.cpp @@ -76,21 +76,13 @@ StatusCode MainThread::Init(void) } StatusCode MainThread::UnInit(void) { - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.1\n"); IMissionManager::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.2\n"); IMediaManager::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.3\n"); IIpcConfig::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.4\n"); IStorageManager::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.5\n"); IMcuManager::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.6\n"); IDeviceManager::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.7\n"); IHalCpp::GetInstance()->UnInit(); - LogInfo("uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.8\n"); DestoryAllModules(); ILogUnInit(); return CreateStatusCode(STATUS_CODE_OK); diff --git a/application/MissionManager/src/MediaTaskHandle.cpp b/application/MissionManager/src/MediaTaskHandle.cpp index 90c888e..3794a15 100644 --- a/application/MissionManager/src/MediaTaskHandle.cpp +++ b/application/MissionManager/src/MediaTaskHandle.cpp @@ -67,6 +67,8 @@ MediaTaskType MediaTaskHandle::WorkModeConvert(const WorkMode &mode) switch (mode) { case WorkMode::MODE_PIC: return MediaTaskType::TAKE_PICTURE; + case WorkMode::MODE_VIDEO: + return MediaTaskType::TAKE_VIDEO; case WorkMode::MODE_PIC_VIDEO: return MediaTaskType::TAKE_PICTURE_AND_VIDEO; diff --git a/hal/abstract/IHalCpp.cpp b/hal/abstract/IHalCpp.cpp index 9c77f1d..f3344f1 100644 --- a/hal/abstract/IHalCpp.cpp +++ b/hal/abstract/IHalCpp.cpp @@ -72,7 +72,7 @@ StatusCode VCameraTaskContext::TaskFinished(void) LogWarning("STATUS_CODE_VIRTUAL_FUNCTION.\n"); return CreateStatusCode(STATUS_CODE_VIRTUAL_FUNCTION); } -camera_task_param::camera_task_param(const CameraTaskType &cameraTask) : mCameraTask(cameraTask) +camera_task_param::camera_task_param(const CameraTaskType &cameraTask) : mTaskType(cameraTask) { mVideoRecordingTimeMs = DEFAULT_VIDEO_RECORDING_TIME_MS; } @@ -100,6 +100,11 @@ StatusCode VCameraHal::SetVideoStreamCallback(VideoStreamCallback callback) LogWarning("STATUS_CODE_VIRTUAL_FUNCTION.\n"); return CreateStatusCode(STATUS_CODE_VIRTUAL_FUNCTION); } +StatusCode VCameraHal::SetJpegEncodeCallback(JpegEncodeCallback callback) +{ + LogWarning("STATUS_CODE_VIRTUAL_FUNCTION.\n"); + return CreateStatusCode(STATUS_CODE_VIRTUAL_FUNCTION); +} void VSdCardHalMonitor::ReportEvent(const SdCardHalStatus &status) { LogWarning("STATUS_CODE_VIRTUAL_FUNCTION.\n"); diff --git a/hal/include/IHalCpp.h b/hal/include/IHalCpp.h index 115c519..d34f520 100644 --- a/hal/include/IHalCpp.h +++ b/hal/include/IHalCpp.h @@ -118,12 +118,13 @@ constexpr int DEFAULT_VIDEO_RECORDING_TIME_MS = 10 * 1000; typedef struct camera_task_param { camera_task_param(const CameraTaskType &cameraTask); - const CameraTaskType mCameraTask; + const CameraTaskType mTaskType; unsigned int mVideoRecordingTimeMs; // TODO: delete? std::shared_ptr mCtx; } CameraTaskParam; using AudioStreamCallback = std::function; using VideoStreamCallback = std::function; +using JpegEncodeCallback = std::function; class VCameraHal { public: @@ -134,6 +135,7 @@ public: virtual StatusCode StopTask(void); virtual StatusCode SetAudioStreamCallback(AudioStreamCallback callback); virtual StatusCode SetVideoStreamCallback(VideoStreamCallback callback); + virtual StatusCode SetJpegEncodeCallback(JpegEncodeCallback callback); }; class VSdCardHalMonitor { diff --git a/hal/src/CameraHal.cpp b/hal/src/CameraHal.cpp index ea6257e..20580d0 100644 --- a/hal/src/CameraHal.cpp +++ b/hal/src/CameraHal.cpp @@ -20,7 +20,7 @@ #include CameraHal::CameraHal() : mTaskRuning(false), mAudioStreamCallback(nullptr), mVideoStreamCallback(nullptr), mVideoFile(nullptr), - mAudioFile(nullptr) + mAudioFile(nullptr), mTaskType(CameraTaskType::END) { } void CameraHal::Init(void) @@ -32,6 +32,7 @@ void CameraHal::UnInit(void) StatusCode CameraHal::StartSingleTask(const CameraTaskParam ¶m) { LogInfo("StartSingleTask.\n"); + // mTaskType = param.mTaskType; mTaskRuning = true; fx_system_v2("rm -f " SD_CARD_MOUNT_PATH "/chip.g711a"); fx_system_v2("rm -f " SD_CARD_MOUNT_PATH "/chip.h264"); @@ -50,6 +51,7 @@ StatusCode CameraHal::StopTask(void) fclose(mVideoFile); mVideoFile = nullptr; } + // mTaskType = CameraTaskType::END; fx_system_v2("sync"); return CreateStatusCode(STATUS_CODE_OK); } @@ -63,6 +65,11 @@ StatusCode CameraHal::SetVideoStreamCallback(VideoStreamCallback callback) mVideoStreamCallback = callback; return CreateStatusCode(STATUS_CODE_OK); } +StatusCode CameraHal::SetJpegEncodeCallback(JpegEncodeCallback callback) +{ + mJpegEncodeCallback = callback; + return CreateStatusCode(STATUS_CODE_OK); +} void CameraHal::GetAudioStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) { if (mTaskRuning && nullptr != mAudioStreamCallback) { @@ -77,6 +84,12 @@ void CameraHal::GetVideoStream(const void *stream, const unsigned int &length, c mVideoStreamCallback(stream, length, timeStamp); } } +void CameraHal::GetJpegData(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) +{ + if (mTaskRuning && nullptr != mJpegEncodeCallback) { + mJpegEncodeCallback(stream, length, timeStamp); + } +} void CameraHal::SaveChipStream(const ChipStreamType &streamType, const void *stream, const unsigned int &length) { FILE *file = nullptr; diff --git a/hal/src/CameraHal.h b/hal/src/CameraHal.h index bea9561..fb4d9e5 100644 --- a/hal/src/CameraHal.h +++ b/hal/src/CameraHal.h @@ -37,10 +37,12 @@ protected: StatusCode StopTask(void) override; StatusCode SetAudioStreamCallback(AudioStreamCallback callback) override; StatusCode SetVideoStreamCallback(VideoStreamCallback callback) override; + StatusCode SetJpegEncodeCallback(JpegEncodeCallback callback) override; protected: void GetAudioStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp); void GetVideoStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp); + void GetJpegData(const void *stream, const unsigned int &length, const unsigned long long &timeStamp); void SaveChipStream(const ChipStreamType &streamType, const void *stream, const unsigned int &length); private: @@ -48,10 +50,12 @@ private: bool mTaskRuning; AudioStreamCallback mAudioStreamCallback; VideoStreamCallback mVideoStreamCallback; + JpegEncodeCallback mJpegEncodeCallback; /** * @brief Each time a media task is executed, the original data stream of the chip is saved to the SD card. */ FILE *mVideoFile; ///< The original video stream data is saved. FILE *mAudioFile; ///< The original audio stream data is saved. + CameraTaskType mTaskType; }; #endif \ No newline at end of file diff --git a/middleware/IpcConfig/include/IIpcConfig.h b/middleware/IpcConfig/include/IIpcConfig.h index 57b545e..5525719 100644 --- a/middleware/IpcConfig/include/IIpcConfig.h +++ b/middleware/IpcConfig/include/IIpcConfig.h @@ -46,6 +46,7 @@ enum class IpcConfigKey enum class WorkMode { MODE_PIC = 0, + MODE_VIDEO, MODE_PIC_VIDEO, END, }; diff --git a/middleware/McuManager/src/McuManagerImpl.cpp b/middleware/McuManager/src/McuManagerImpl.cpp index 2f0774e..16a5357 100644 --- a/middleware/McuManager/src/McuManagerImpl.cpp +++ b/middleware/McuManager/src/McuManagerImpl.cpp @@ -72,18 +72,14 @@ const StatusCode McuManagerImpl::Init(void) } const StatusCode McuManagerImpl::UnInit(void) { - LogInfo("=============================== 00\n"); McuDevice::UnInit(); - LogInfo("=============================== 11\n"); McuProtocol::UnInit(); - LogInfo("=============================== 22\n"); mMcuAskList.clear(); mMonitor.reset(); mMutex.lock(); mWatchDogRuning = false; mCv.notify_one(); mMutex.unlock(); - LogInfo("=============================== 33\n"); if (mWatchDogThread.joinable()) { mWatchDogThread.join(); } diff --git a/middleware/MediaManager/src/MediaHandle.cpp b/middleware/MediaManager/src/MediaHandle.cpp index 7d97757..4793914 100644 --- a/middleware/MediaManager/src/MediaHandle.cpp +++ b/middleware/MediaManager/src/MediaHandle.cpp @@ -40,14 +40,14 @@ MediaHandle::MediaHandle(const MediaChannel &mediaChannel, const std::shared_ptr } void MediaHandle::Init(void) { - if (mCameraHal == nullptr) { - LogError("CameraHal is null.\n"); - return; - } - auto audioFunc = std::bind(&MediaHandle::GetAudioStreamCallback, this, _1, _2, _3); - mCameraHal->SetAudioStreamCallback(audioFunc); - auto videoFunc = std::bind(&MediaHandle::GetVideoStreamCallback, this, _1, _2, _3); - mCameraHal->SetVideoStreamCallback(videoFunc); + // if (mCameraHal == nullptr) { + // LogError("CameraHal is null.\n"); + // return; + // } + // auto audioFunc = std::bind(&MediaHandle::GetAudioStreamCallback, this, _1, _2, _3); + // mCameraHal->SetAudioStreamCallback(audioFunc); + // auto videoFunc = std::bind(&MediaHandle::GetVideoStreamCallback, this, _1, _2, _3); + // mCameraHal->SetVideoStreamCallback(videoFunc); } void MediaHandle::UnInit(void) { @@ -67,6 +67,7 @@ void MediaHandle::UnInit(void) */ mCameraHal->SetAudioStreamCallback(nullptr); mCameraHal->SetVideoStreamCallback(nullptr); + mCameraHal->SetJpegEncodeCallback(nullptr); } } StatusCode MediaHandle::ExecuteTask(std::shared_ptr &task) @@ -94,7 +95,7 @@ StatusCode MediaHandle::ExecuteTask(std::shared_ptr &task) } CameraTaskType taskType = TaskTypeConvert(task->GetTaskType()); CameraTaskParam data(taskType); - auto code = mCameraHal->StartSingleTask(data); + auto code = StartMediaTask(data); if (IsCodeOK(code)) { mCurrentTask = task; StartTaskTimer(); @@ -212,6 +213,9 @@ void inline MediaHandle::HandleListFrame(void) else if (FrameType::AUDIO == frontFrame.mType) { mStreamHandle->GetAudioStream(frontFrame.mData, frontFrame.mLength, frontFrame.mTimeStamp_us); } + else if (FrameType::JPEG == frontFrame.mType) { + mStreamHandle->GetJpegData(frontFrame.mData, frontFrame.mLength, frontFrame.mTimeStamp_us); + } free(frontFrame.mData); frontFrame.mData = nullptr; // mFrameList.pop_front(); @@ -230,7 +234,18 @@ void inline MediaHandle::HandleListFrame(void) } CameraTaskType MediaHandle::TaskTypeConvert(const MediaTaskType &type) { - return CameraTaskType::END; + switch (type) { + case MediaTaskType::TAKE_PICTURE: + return CameraTaskType::PICTURE; + case MediaTaskType::TAKE_VIDEO: + return CameraTaskType::VIDEO; + case MediaTaskType::TAKE_PICTURE_AND_VIDEO: + return CameraTaskType::PICTURE_AND_VIDEO; + + default: + LogError("TaskTypeConvert error.\n"); + return CameraTaskType::END; + } } void MediaHandle::GetVideoStreamCallback(const void *stream, const int &length, const unsigned long long &timeStamp) { @@ -240,6 +255,16 @@ void MediaHandle::GetAudioStreamCallback(const void *stream, const int &length, { GetAVStream(FrameType::AUDIO, stream, length, timeStamp); } +void MediaHandle::GetJpegDataCallback(const void *stream, const int &length, const unsigned long long &timeStamp) +{ + /** + * @brief If it is a screenshot, each task only needs to process one piece of image data. + * + */ + if (MEDIA_TASK_NOT_START == mFirstFrameTimeStamp_us) { + GetAVStream(FrameType::JPEG, stream, length, timeStamp); + } +} void MediaHandle::GetAVStream(const FrameType &type, const void *stream, const int &length, const unsigned long long &timeStamp_us) { @@ -283,4 +308,32 @@ void MediaHandle::DeleteFrame(const OneFrameStream &frame) return KEEP_THE_FRAME; }; mFrameList.remove_if(searchMcuAsk); +} +StatusCode MediaHandle::StartMediaTask(const CameraTaskParam ¶m) +{ + if (mCameraHal == nullptr) { + LogError("CameraHal is null.\n"); + return CreateStatusCode(STATUS_CODE_NOT_OK); + } + mCameraHal->SetAudioStreamCallback(nullptr); + mCameraHal->SetVideoStreamCallback(nullptr); + mCameraHal->SetJpegEncodeCallback(nullptr); + auto audioFunc = std::bind(&MediaHandle::GetAudioStreamCallback, this, _1, _2, _3); + auto videoFunc = std::bind(&MediaHandle::GetVideoStreamCallback, this, _1, _2, _3); + auto jpegFunc = std::bind(&MediaHandle::GetJpegDataCallback, this, _1, _2, _3); + switch (param.mTaskType) { + case CameraTaskType::PICTURE: + mCameraHal->SetJpegEncodeCallback(jpegFunc); + break; + case CameraTaskType::VIDEO: + mCameraHal->SetAudioStreamCallback(audioFunc); + mCameraHal->SetVideoStreamCallback(videoFunc); + break; + case CameraTaskType::PICTURE_AND_VIDEO: + break; + + default: + break; + } + return mCameraHal->StartSingleTask(param); } \ No newline at end of file diff --git a/middleware/MediaManager/src/MediaHandle.h b/middleware/MediaManager/src/MediaHandle.h index 55fc763..40be51b 100644 --- a/middleware/MediaManager/src/MediaHandle.h +++ b/middleware/MediaManager/src/MediaHandle.h @@ -28,6 +28,7 @@ enum class FrameType { VIDEO, AUDIO, + JPEG, END }; typedef struct one_frame_stream @@ -61,6 +62,7 @@ private: CameraTaskType TaskTypeConvert(const MediaTaskType &type); void GetVideoStreamCallback(const void *stream, const int &length, const unsigned long long &timeStamp); void GetAudioStreamCallback(const void *stream, const int &length, const unsigned long long &timeStamp); + void GetJpegDataCallback(const void *stream, const int &length, const unsigned long long &timeStamp); /** * @brief * @@ -73,6 +75,7 @@ private: const unsigned long long &timeStamp_us); void ClearFrameList(void); void DeleteFrame(const OneFrameStream &frame); + StatusCode StartMediaTask(const CameraTaskParam ¶m); private: std::mutex mMutex; diff --git a/middleware/MediaManager/src/TakePicture.cpp b/middleware/MediaManager/src/TakePicture.cpp index 9eb874f..014061c 100644 --- a/middleware/MediaManager/src/TakePicture.cpp +++ b/middleware/MediaManager/src/TakePicture.cpp @@ -73,7 +73,8 @@ void TakePicture::StopHandleStream(void) std::lock_guard locker(mMutex); if (mTakePictureObject) { // OutputFileInfo finalFile = IGetOutputFileInfo(mTakePictureObject); - MediaTaskResponse response("finalFile.mFileName", 0); + std::string picturePath = mPictureTask->GetTargetNameForSaving(); + MediaTaskResponse response(picturePath, 0); mTaskResponse.push_back(response); ICloseJpegFile(mTakePictureObject); IMediaBaseFree(mTakePictureObject); @@ -85,7 +86,16 @@ void TakePicture::GetVideoStream(const void *stream, const unsigned int &length, { std::lock_guard locker(mMutex); if (mTakePictureObject) { - IWriteJpegData(mTakePictureObject, stream, length); + StreamInfo info = {.mType = STREAM_TYPE_VIDEO_H264, .mTimeStamp_us = timeStamp}; + IWriteJpegData(mTakePictureObject, stream, length, info); + } +} +void TakePicture::GetJpegData(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) +{ + std::lock_guard locker(mMutex); + if (mTakePictureObject) { + StreamInfo info = {.mType = STREAM_TYPE_JPEG, .mTimeStamp_us = timeStamp}; + IWriteJpegData(mTakePictureObject, stream, length, info); } } StatusCode TakePicture::GetAllFiles(std::vector &files) diff --git a/middleware/MediaManager/src/TakePicture.h b/middleware/MediaManager/src/TakePicture.h index 8579112..27fcf2e 100644 --- a/middleware/MediaManager/src/TakePicture.h +++ b/middleware/MediaManager/src/TakePicture.h @@ -28,6 +28,7 @@ public: StatusCode UnInit(void) override; void StopHandleStream(void) override; void GetVideoStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) override; + void GetJpegData(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) override; StatusCode GetAllFiles(std::vector &files) override; bool HandleFinished(void) override; diff --git a/middleware/MediaManager/src/VStreamHandle.cpp b/middleware/MediaManager/src/VStreamHandle.cpp index 8079f7c..c44c566 100644 --- a/middleware/MediaManager/src/VStreamHandle.cpp +++ b/middleware/MediaManager/src/VStreamHandle.cpp @@ -33,12 +33,16 @@ void VStreamHandle::StopHandleStream(void) } void VStreamHandle::GetVideoStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) { - LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); + // LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); } void VStreamHandle::GetAudioStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) { // LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); } +void VStreamHandle::GetJpegData(const void *stream, const unsigned int &length, const unsigned long long &timeStamp) +{ + // LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); +} StatusCode VStreamHandle::GetAllFiles(std::vector &files) { LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); diff --git a/middleware/MediaManager/src/VStreamHandle.h b/middleware/MediaManager/src/VStreamHandle.h index 92e3390..3308260 100644 --- a/middleware/MediaManager/src/VStreamHandle.h +++ b/middleware/MediaManager/src/VStreamHandle.h @@ -27,6 +27,7 @@ public: virtual void StopHandleStream(void); virtual void GetVideoStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp); virtual void GetAudioStream(const void *stream, const unsigned int &length, const unsigned long long &timeStamp); + virtual void GetJpegData(const void *stream, const unsigned int &length, const unsigned long long &timeStamp); virtual StatusCode GetAllFiles(std::vector &files); virtual bool HandleFinished(void); }; diff --git a/test/application/HuntingCamera/CMakeLists.txt b/test/application/HuntingCamera/CMakeLists.txt index 9fa6e13..1a4224a 100644 --- a/test/application/HuntingCamera/CMakeLists.txt +++ b/test/application/HuntingCamera/CMakeLists.txt @@ -13,6 +13,7 @@ include_directories( ${MIDDLEWARE_SOURCE_PATH}/McuManager/src ${MIDDLEWARE_SOURCE_PATH}/DeviceManager/include ${MIDDLEWARE_SOURCE_PATH}/DeviceManager/src + ${MIDDLEWARE_SOURCE_PATH}/IpcConfig/include ${UTILS_SOURCE_PATH}/McuProtocol/include ${UTILS_SOURCE_PATH}/UartDevice/include ${UTILS_SOURCE_PATH}/LedControl/include @@ -46,6 +47,7 @@ endif() set(TARGET_NAME HuntingCameraTest) add_executable(${TARGET_NAME} ${SRC_FILES_MAIN} ${SRC_FILES}) target_link_libraries(${TARGET_NAME}# -Wl,--start-group + IpcConfig HuntingMainLib MissionManagerTestTool McuManagerTestTool McuAskBaseTestTool AppManagerTestTool HalTestTool DeviceManagerTestTool IpcConfigTestTool TestManager # -Wl,--end-group diff --git a/test/application/HuntingCamera/src_mock/HuntingCameraTest.cpp b/test/application/HuntingCamera/src_mock/HuntingCameraTest.cpp index 245196a..ab9af31 100644 --- a/test/application/HuntingCamera/src_mock/HuntingCameraTest.cpp +++ b/test/application/HuntingCamera/src_mock/HuntingCameraTest.cpp @@ -71,6 +71,7 @@ void HuntingCameraTest::TearDown() std::shared_ptr test = std::make_shared(); LinuxApiMock::GetInstance(&test); McuManagerTestTool::UnInit(); + IpcConfigTestTool::UnInit(); DeviceManagerTestTool::UnInit(); DestroyAllCamerasMock(); DestroyAllKeysMock(); diff --git a/test/application/HuntingCamera/src_mock/MediaManager_Mock_Test.cpp b/test/application/HuntingCamera/src_mock/MediaManager_Mock_Test.cpp index 0df3419..ee17ceb 100644 --- a/test/application/HuntingCamera/src_mock/MediaManager_Mock_Test.cpp +++ b/test/application/HuntingCamera/src_mock/MediaManager_Mock_Test.cpp @@ -16,6 +16,7 @@ #include "GtestUsing.h" #include "HalTestTool.h" #include "HuntingCameraTest.h" +#include "IIpcConfig.h" #include "ILog.h" #include "MainThread.h" #include "McuManagerTestTool.h" @@ -61,6 +62,7 @@ TEST_F(HuntingCameraTest, INTEGRATION_HunttingCamera_EXAMPLE_MediaTask) std::this_thread::sleep_for(std::chrono::milliseconds(100)); MainThread::GetInstance()->Init(); TestManager::ResetTimeOut(1000 * 15); + IIpcConfig::GetInstance()->SetWorkMode(WorkMode::MODE_VIDEO); std::this_thread::sleep_for(std::chrono::milliseconds(100)); HalTestTool::MockKeyClick("reset", 200); // Simulate pressing a button. MainThread::GetInstance()->Runing(); @@ -74,6 +76,7 @@ TEST_F(HuntingCameraTest, INTEGRATION_HunttingCamera_EXAMPLE_MediaTask_Twice) std::this_thread::sleep_for(std::chrono::milliseconds(100)); MainThread::GetInstance()->Init(); TestManager::ResetTimeOut(1000 * 25); + IIpcConfig::GetInstance()->SetWorkMode(WorkMode::MODE_VIDEO); std::this_thread::sleep_for(std::chrono::milliseconds(100)); HalTestTool::MockKeyClick("reset", 200); // Simulate pressing a button. std::this_thread::sleep_for(std::chrono::milliseconds(1000 * 15)); diff --git a/test/hal/tool/src/CameraHalMock.cpp b/test/hal/tool/src/CameraHalMock.cpp index 8ea5f95..4f7c1fc 100644 --- a/test/hal/tool/src/CameraHalMock.cpp +++ b/test/hal/tool/src/CameraHalMock.cpp @@ -19,10 +19,11 @@ #include "StatusCode.h" #include #include +#include #include CameraHalTest::CameraHalTest(const CameraType &cameraType) : mCameraType(cameraType), mReadH264File(nullptr), mReadG711aFile(nullptr), mTaskRuning(false), - mVideoTimeStamp_us(0), mAudioTimeStamp_us(0) + mVideoTimeStamp_us(0), mAudioTimeStamp_us(0), mJpegData(nullptr), mJpegSize(0) { } void CameraHalTest::Init(void) @@ -39,6 +40,10 @@ void CameraHalTest::UnInit(void) mTaskTimerThread.join(); } ReadFileUnInit(); + if (mJpegData) { + free(mJpegData); + mJpegData = nullptr; + } } void CameraHalTest::SetCameraMonitor(std::shared_ptr &monitor) { @@ -98,17 +103,29 @@ void CameraHalTest::MockReportMediaStream(void) if (nullptr != mReadG711aFile) { IStartReadFile(mReadG711aFile, TEST_SOURCE_PATH "/support_test/chip.g711a"); } + ReadJpegFileForMockData(); + constexpr int TIME_OUT_MS = 100; while (mTaskRuning) { + if (mJpegData) { + GetJpegData(mJpegData, mJpegSize, 1000); + } std::unique_lock lock(mMutex); - mCv.wait(lock, [&] { + // mCv.wait(lock, [&] { + // return !mTaskRuning; + // }); + mCv.wait_for(lock, std::chrono::milliseconds(TIME_OUT_MS), [&] { return !mTaskRuning; }); /** * @brief If the recording time is over, you need to stop the recording timer here. */ - mTaskRuning = false; + // mTaskRuning = false; } ReadFileUnInit(); + if (mJpegData) { + free(mJpegData); + mJpegData = nullptr; + } } void CameraHalTest::ReadDataFromH264File(const void *stream, const unsigned int length, const unsigned long long duration_us) @@ -168,6 +185,35 @@ void CameraHalTest::ReadFileUnInit(void) mReadG711aFile = nullptr; } } +int CameraHalTest::ReadJpegFileForMockData(void) +{ + const char *filename = TEST_SOURCE_PATH "/support_test/rv1106_jpeg_encoder.jpg"; + + FILE *file = fopen(filename, "rb"); + if (!file) { + LogError("Error opening file: %s\n", filename); + return 1; + } + + fseek(file, 0, SEEK_END); + mJpegSize = ftell(file); + fseek(file, 0, SEEK_SET); + + mJpegData = (unsigned char *)malloc(mJpegSize); + if (!mJpegData) { + LogError("Error allocating memory\n"); + fclose(file); + return 1; + } + size_t read_size = fread(mJpegData, 1, mJpegSize, file); + if (read_size != mJpegSize) { + LogError("Error reading file\n"); + fclose(file); + return 1; + } + fclose(file); + return 0; +} CameraHalMock::CameraHalMock(const CameraType &cameraType) : CameraHalTest(cameraType) { } diff --git a/test/hal/tool/src/CameraHalMock.h b/test/hal/tool/src/CameraHalMock.h index 391e03b..566d41f 100644 --- a/test/hal/tool/src/CameraHalMock.h +++ b/test/hal/tool/src/CameraHalMock.h @@ -41,6 +41,7 @@ private: void ReadDataFromG711aFile(const void *stream, const unsigned int length, const unsigned long long duration_us); void ReadFileInit(void); void ReadFileUnInit(void); + int ReadJpegFileForMockData(void); protected: const CameraType mCameraType; @@ -54,6 +55,8 @@ protected: std::thread mTaskTimerThread; unsigned long long mVideoTimeStamp_us; unsigned long long mAudioTimeStamp_us; + unsigned char *mJpegData; + unsigned int mJpegSize; }; class CameraHalMock : public CameraHalTest { diff --git a/test/support_test/rv1106_jpeg_encoder.jpg b/test/support_test/rv1106_jpeg_encoder.jpg new file mode 100755 index 0000000000000000000000000000000000000000..672da4424eaa525ce5faac7a417ab4c20d0b0787 GIT binary patch literal 96617 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 RpO~jpAYj5P9Qpb8{|Cy&%bx%M literal 0 HcmV?d00001 diff --git a/utils/MediaBase/include/MediaBase.h b/utils/MediaBase/include/MediaBase.h index 5b38ef9..d825386 100644 --- a/utils/MediaBase/include/MediaBase.h +++ b/utils/MediaBase/include/MediaBase.h @@ -31,6 +31,7 @@ enum StreamType { STREAM_TYPE_VIDEO_H264 = 0, STREAM_TYPE_AUDIO_G711A, + STREAM_TYPE_JPEG, STREAM_TYPE_END }; /** @@ -82,7 +83,7 @@ OutputFileInfo IGetOutputFileInfo(void *object); StatusCode IOpenJpegFile(void *object, const OutputFileInfo *info); StatusCode ICloseJpegFile(void *object); -StatusCode IWriteJpegData(void *object, const void *data, const size_t size); +StatusCode IWriteJpegData(void *object, const void *data, const size_t size, const StreamInfo streamInfo); void IMediaBaseFree(void *object); #ifdef __cplusplus diff --git a/utils/MediaBase/src/FfmpegDecoderV2.cpp b/utils/MediaBase/src/FfmpegDecoderV2.cpp new file mode 100644 index 0000000..ed67d59 --- /dev/null +++ b/utils/MediaBase/src/FfmpegDecoderV2.cpp @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2023 Fancy Code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "FfmpegDecoderV2.h" +#include "ILog.h" +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +} +#endif +#include +#include +#include +#include +#include +FfmpegDecoderV2::FfmpegDecoderV2(const enum AVCodecID &codecId, const AVPixelFormat &decodePixelFormat, + const int &width, const int &height) + : mCodecId(codecId), mCodec(nullptr), mCodecCtx(nullptr), mFrame(nullptr), mPacket(nullptr), mParser(nullptr), + mVideoWidth(width), mVideoHeight(height), mDecodePixelFormat(decodePixelFormat) +{ +} +bool FfmpegDecoderV2::Init(void) +{ + int ret = 0; + LogInfo("find decoder : %s\n", avcodec_get_name(mCodecId)); + mCodec = (AVCodec *)avcodec_find_decoder(mCodecId); + // mCodec = (AVCodec *)avcodec_find_decoder_by_name("libfdk_aac"); + if (!(mCodec)) { + LogError("decoder not found:%s\n", avcodec_get_name(mCodecId)); + return false; + } + mCodecCtx = avcodec_alloc_context3((const AVCodec *)(mCodec)); + if (!(mCodecCtx)) { + LogError("Could not allocate codec context\n"); + return false; + } + if (AVMEDIA_TYPE_AUDIO == mCodec->type) { + LogInfo("Audio decoder.\n"); + /* put sample parameters */ + mCodecCtx->bit_rate = 64000; + // mCodecCtx->bit_rate = 352800; + // mCodecCtx->sample_rate = 8000; + + /* check that the encoder supports s16 pcm input */ + mCodecCtx->sample_fmt = AV_SAMPLE_FMT_S16; + if (!check_sample_fmt(mCodec, mCodecCtx->sample_fmt)) { + LogError("decoder does not support sample format %s", av_get_sample_fmt_name(mCodecCtx->sample_fmt)); + return false; + } + + /* select other audio parameters supported by the encoder */ + mCodecCtx->sample_rate = select_sample_rate(mCodec); + LogInfo("decoder sample_rate:%d\n", mCodecCtx->sample_rate); + // const AVChannelLayout src = (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO; + // av_channel_layout_copy(&mCodecCtx->ch_layout, &src); + ret = select_channel_layout(mCodec, &(mCodecCtx->ch_layout)); + if (ret < 0) { + LogError("Could not set channel layout\n"); + return false; + } + } + else { + mCodecCtx->pix_fmt = mDecodePixelFormat; + mCodecCtx->width = mVideoWidth; + mCodecCtx->height = mVideoHeight; + } + if ((ret = avcodec_open2(mCodecCtx, mCodec, nullptr)) < 0) { + char error_str[AV_ERROR_MAX_STRING_SIZE] = {0}; + LogError("Could not open codec:%s\n", av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret)); + return false; + } + mFrame = av_frame_alloc(); + if (!mFrame) { + LogError("Could not allocate video frame\n"); + return false; + } + mPacket = av_packet_alloc(); + if (!mPacket) { + LogError("Could not allocate video frame\n"); + return false; + } + // mParser = av_parser_init(mCodec->id); + // if (!mParser) { + // LogError("mParser not found : %s\n", avcodec_get_name(mCodec->id)); + // return false; + // } + if (AVMEDIA_TYPE_AUDIO == mCodec->type) { + // mFrame->nb_samples = mCodecCtx->frame_size; + // mFrame->format = mCodecCtx->sample_fmt; + ret = av_channel_layout_copy(&(mFrame->ch_layout), &(mCodecCtx->ch_layout)); + if (ret < 0) { + LogError("Could not copy channel layout\n"); + return false; + } + } + LogInfo("init success pix_fmt = %d\n", mCodecCtx->pix_fmt); + return true; +} +bool FfmpegDecoderV2::UnInit(void) +{ + LogInfo("uninit %s\n", avcodec_get_name(mCodecId)); + av_packet_free(&mPacket); + mPacket = nullptr; + if (mParser) { + av_parser_close(mParser); + mParser = nullptr; + } + avcodec_free_context(&mCodecCtx); + mCodecCtx = nullptr; + if (mFrame) { + av_frame_free(&mFrame); + mFrame = nullptr; + } + return true; +} +void FfmpegDecoderV2::DecodeData(const void *data, const size_t &size, const unsigned long long &pts, + std::function callback) +{ + if (nullptr == mParser) { + mPacket->data = (uint8_t *)data; + mPacket->size = size; + mPacket->pts = pts; + mPacket->dts = mPacket->pts; + // LogInfo("source data mPacket->pts:%d\n", mPacket->pts); + AVDecodeData(mPacket, callback); + return; + } + AVParseData(data, size, callback); +} +void inline FfmpegDecoderV2::AVParseData(const void *data, const size_t &size, + std::function callback) +{ + if (nullptr == data) { + LogError("data is null\n"); + return; + } + uint8_t *frameData = (uint8_t *)data; + size_t data_size = size; + while (data_size > 0) { + int ret = av_parser_parse2(mParser, + mCodecCtx, + &mPacket->data, + &mPacket->size, + frameData, + data_size, + AV_NOPTS_VALUE, + AV_NOPTS_VALUE, + 0); + if (ret < 0) { + LogError("av_parse_frame failed\n"); + break; + } + frameData += ret; + data_size -= ret; + if (mPacket->size) { + AVDecodeData(mPacket, callback); + } + } +} +// static void save_code_stream_file(const void *data, const size_t &size) +// { +// char OutPath[128] = {0}; +// const void *pData = data; +// FILE *file = NULL; +// LogInfo("save_code_stream_file size = %d\n", size); +// sprintf(OutPath, "./test.yuv"); +// file = fopen(OutPath, "a+"); + +// if (file) { // TODO: Don't open very time. +// fwrite(pData, 1, size, file); +// fflush(file); +// } + +// if (file) +// fclose(file); +// } +// static void pgm_save(unsigned char *buf, int wrap, int xsize, int ysize, char *filename) +// { +// FILE *f; +// int i; + +// f = fopen(filename, "wb"); +// fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255); +// for (i = 0; i < ysize; i++) +// fwrite(buf + i * wrap, 1, xsize, f); +// fclose(f); +// } +void inline FfmpegDecoderV2::AVDecodeData(AVPacket *pkt, std::function callback) +{ + int ret = avcodec_send_packet(mCodecCtx, pkt); + if (ret < 0) { + LogError("Error sending a packet for decoding\n"); + av_packet_unref(pkt); + return; + } + while (ret >= 0) { + ret = avcodec_receive_frame(mCodecCtx, mFrame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } + if (ret < 0) { + LogError("Error during decoding\n"); + break; + } + if (callback) { + // int i, ch, data_size; + // data_size = av_get_bytes_per_sample(mCodecCtx->sample_fmt); + // for (i = 0; i < mFrame->nb_samples; i++) + // for (ch = 0; ch < mCodecCtx->ch_layout.nb_channels; ch++) + // // fwrite(frame->data[ch] + data_size * i, 1, data_size, outfile); + // save_code_stream_file(mFrame->data[ch] + data_size * i, data_size); + // save_code_stream_file(mFrame->data[0], mFrame->linesize[0]); + // if (mCodecId == AV_CODEC_ID_H264) { + // pgm_save(mFrame->data[0], mFrame->linesize[0], mFrame->width, mFrame->height, "./test.yuv"); + // } + // LogInfo("decode frame pts = %llu, nb_samples = %d\n", mFrame->pts, mFrame->nb_samples); + callback(mFrame); + } + break; + } + av_packet_unref(pkt); +} +/* just pick the highest supported samplerate */ +int FfmpegDecoderV2::select_sample_rate(const AVCodec *codec) +{ + const int *p; + int best_samplerate = 0; + + if (!codec->supported_samplerates) + return 44100; + + p = codec->supported_samplerates; + while (*p) { + if (!best_samplerate || abs(44100 - *p) < abs(44100 - best_samplerate)) + best_samplerate = *p; + p++; + } + return best_samplerate; +} +/* select layout with the highest channel count */ +int FfmpegDecoderV2::select_channel_layout(const AVCodec *codec, AVChannelLayout *dst) +{ + const AVChannelLayout *p, *best_ch_layout = nullptr; + int best_nb_channels = 0; + AVChannelLayout channelLayout = AV_CHANNEL_LAYOUT_STEREO; + if (!codec->ch_layouts) + return av_channel_layout_copy(dst, &channelLayout); + + p = codec->ch_layouts; + while (p->nb_channels) { + int nb_channels = p->nb_channels; + + if (nb_channels > best_nb_channels) { + best_ch_layout = p; + best_nb_channels = nb_channels; + } + p++; + } + return av_channel_layout_copy(dst, best_ch_layout); +} +/* check that a given sample format is supported by the encoder */ +int FfmpegDecoderV2::check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt) +{ + const enum AVSampleFormat *p = codec->sample_fmts; + + while (*p != AV_SAMPLE_FMT_NONE) { + if (*p == sample_fmt) + return 1; + p++; + } + return 0; +} \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegDecoderV2.h b/utils/MediaBase/src/FfmpegDecoderV2.h new file mode 100644 index 0000000..a3173b6 --- /dev/null +++ b/utils/MediaBase/src/FfmpegDecoderV2.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Fancy Code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef FFMPEG_DECODER_V2_H +#define FFMPEG_DECODER_V2_H +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +} +#endif +#include +// constexpr int DECODER_UNSUPORTED = 0; +class FfmpegDecoderV2 +{ +public: + /** + * @brief When decoding a video stream, you need to use this constructor to provide the required parameters. + * @param codecId Video stream format + * @param width Video height + * @param height Video width + */ + FfmpegDecoderV2(const enum AVCodecID &codecId, const AVPixelFormat &decodePixelFormat, const int &width, + const int &height); + virtual ~FfmpegDecoderV2() = default; + bool Init(void); + bool UnInit(void); + void DecodeData(const void *data, const size_t &size, const unsigned long long &pts, + std::function callback); + +private: + void AVParseData(const void *data, const size_t &size, std::function callback); + void AVDecodeData(AVPacket *pkt, std::function callback); + +private: + static int select_sample_rate(const AVCodec *codec); + static int select_channel_layout(const AVCodec *codec, AVChannelLayout *dst); + static int check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt); + +private: + const enum AVCodecID mCodecId; + AVCodec *mCodec; + AVCodecContext *mCodecCtx; + AVFrame *mFrame; + AVPacket *mPacket; + AVCodecParserContext *mParser; + const int mVideoWidth; + const int mVideoHeight; + const AVPixelFormat mDecodePixelFormat; +}; +#endif \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegEncoderV2.cpp b/utils/MediaBase/src/FfmpegEncoderV2.cpp new file mode 100644 index 0000000..bf95be9 --- /dev/null +++ b/utils/MediaBase/src/FfmpegEncoderV2.cpp @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2023 Fancy Code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "FfmpegEncoderV2.h" +#include "ILog.h" +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#include +#include +// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +} +#endif +#include +#include +#include +constexpr long SOURCE_AUDIO_SAMPEL_RATE = 8000; +#define STREAM_DURATION 10.0 +#define STREAM_FRAME_RATE 25 /* 25 images/s */ +#define STREAM_PIX_FMT AV_PIX_FMT_YUV420P /* default pix_fmt */ +FfmpegEncoderV2::FfmpegEncoderV2(const enum AVCodecID &codecId, const AVPixelFormat &encodePixelFormat, + const int &width, const int &height) + : mCodecId(codecId), mCodecCtx(nullptr), mCodec(nullptr), mFrame(nullptr), mTmpFrame(nullptr), mTmpPkt(nullptr), + mSamplesCount(0), mSwrCtx(nullptr), next_pts(0), mVideoWidth(width), mVideoHeight(height), + mEncodePixelFormat(encodePixelFormat) +{ +} +bool FfmpegEncoderV2::Init(const int &outputFlags) +{ + mTmpPkt = av_packet_alloc(); + if (!mTmpPkt) { + LogError("Could not allocate AVPacket\n"); + return false; + } + LogInfo("find encoder : %s\n", avcodec_get_name(mCodecId)); + int i = 0; + /* find the encoder */ + mCodec = (AVCodec *)avcodec_find_encoder(mCodecId); + if (!mCodec) { + LogError("Could not find encoder for '%s'\n", avcodec_get_name(mCodecId)); + return false; + } + mCodecCtx = avcodec_alloc_context3(mCodec); + if (!mCodecCtx) { + LogError("Could not alloc an encoding context\n"); + return false; + } + const AVChannelLayout src = (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO; + switch (mCodec->type) { + case AVMEDIA_TYPE_AUDIO: + mCodecCtx->sample_fmt = mCodec->sample_fmts ? mCodec->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; + mCodecCtx->bit_rate = 64000; + // mCodecCtx->bit_rate = 24000; + mCodecCtx->sample_rate = 44100; + if (mCodec->supported_samplerates) { + mCodecCtx->sample_rate = mCodec->supported_samplerates[0]; + for (i = 0; mCodec->supported_samplerates[i]; i++) { + if (mCodec->supported_samplerates[i] == 44100) + mCodecCtx->sample_rate = 44100; + } + } + mCodecCtx->sample_rate = 16000; + // mCodecCtx->time_base = (AVRational){1, mCodecCtx->sample_rate}; + // mCodecCtx->ch_layout.nb_channels = 1; + // av_channel_layout_default(&mCodecCtx->ch_layout, 1); + av_channel_layout_copy(&mCodecCtx->ch_layout, &src); + break; + + case AVMEDIA_TYPE_VIDEO: + mCodecCtx->codec_id = mCodecId; + + mCodecCtx->bit_rate = 300000; + /* Resolution must be a multiple of two. */ + mCodecCtx->width = mVideoWidth; + mCodecCtx->height = mVideoHeight; + /* timebase: This is the fundamental unit of time (in seconds) in terms + * of which frame timestamps are represented. For fixed-fps content, + * timebase should be 1/framerate and timestamp increments should be + * identical to 1. */ + mCodecCtx->time_base = (AVRational){1, STREAM_FRAME_RATE}; + + mCodecCtx->gop_size = 12; /* emit one intra frame every twelve frames at most */ + mCodecCtx->pix_fmt = mEncodePixelFormat; + if (mCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO) { + /* just for testing, we also add B-frames */ + mCodecCtx->max_b_frames = 2; + } + if (mCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO) { + /* Needed to avoid using macroblocks in which some coeffs overflow. + * This does not happen with normal video, it just happens here as + * the motion of the chroma plane does not match the luma plane. */ + mCodecCtx->mb_decision = 2; + } + break; + + default: + break; + } + /* Some formats want stream headers to be separate. */ + if (outputFlags & AVFMT_GLOBALHEADER) { + mCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + return true; +} +void FfmpegEncoderV2::UnInit(void) +{ + if (mFrame) { + av_frame_free(&mFrame); + mFrame = nullptr; + } + if (mTmpFrame) { + av_frame_free(&mTmpFrame); + mTmpFrame = nullptr; + } + if (mCodecCtx) { + avcodec_free_context(&mCodecCtx); + mCodecCtx = nullptr; + } + av_packet_free(&mTmpPkt); + swr_free(&mSwrCtx); +} +AVRational FfmpegEncoderV2::GetTimeBase(void) +{ + switch (mCodec->type) { + case AVMEDIA_TYPE_AUDIO: + return (AVRational){1, mCodecCtx->sample_rate}; + + case AVMEDIA_TYPE_VIDEO: + return mCodecCtx->time_base; + + default: + LogError("Unsupported media type.\n"); + return (AVRational){0, -1}; + } +} +bool FfmpegEncoderV2::OpenEncoder(AVDictionary *optArg, AVStream *stream) +{ + switch (mCodec->type) { + case AVMEDIA_TYPE_AUDIO: + return OpenAudio(optArg, stream); + + case AVMEDIA_TYPE_VIDEO: + return OpenVideo(optArg, stream); + + default: + LogError("Unsupported media type.\n"); + return false; + } +} +// static void save_code_stream_file(const void *data, const size_t &size) +// { +// char OutPath[16]; +// const void *pData = data; +// FILE *file = NULL; +// LogInfo("save_code_stream_file: %d\n", size); +// sprintf(OutPath, "./test.jpg"); +// file = fopen(OutPath, "a+"); + +// if (file) { // TODO: Don't open very time. +// fwrite(pData, 1, size, file); +// fflush(file); +// } + +// if (file) +// fclose(file); +// } +int FfmpegEncoderV2::EncodeData(AVFrame *frame, AVStream *stream, std::function callback) +{ + int ret = 0; + AVFrame *tmpFrame = frame; + if (AVMEDIA_TYPE_AUDIO == mCodec->type) { + tmpFrame = ConvertAudioFrame(frame, mSwrCtx); + } + if (!tmpFrame) { + LogError("Could not convert audio frame.\n"); + return AVERROR_EXIT; + } + // send the frame to the encoder + ret = avcodec_send_frame(mCodecCtx, tmpFrame); + if (ret < 0) { + char error_str[AV_ERROR_MAX_STRING_SIZE] = {0}; + LogInfo("Error sending a frame to the encoder: %s\n", + av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret)); + return AVERROR_EXIT; + } + + while (ret >= 0) { + ret = avcodec_receive_packet(mCodecCtx, mTmpPkt); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } + if (ret < 0) { + char error_str[AV_ERROR_MAX_STRING_SIZE] = {0}; + LogInfo("Error encoding a frame: %s\n", av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret)); + return AVERROR_EXIT; + } + + /* rescale output packet timestamp values from codec to stream timebase */ + av_packet_rescale_ts(mTmpPkt, mCodecCtx->time_base, stream->time_base); + // LogInfo("Write mCodecCtx->time_base.num: %d\n", mCodecCtx->time_base.num); + // LogInfo("Write mCodecCtx->time_base.den: %d\n", mCodecCtx->time_base.den); + // LogInfo("Write stream->time_base.num: %d\n", stream->time_base.num); + // LogInfo("Write stream->time_base.den: %d\n", stream->time_base.den); + mTmpPkt->stream_index = stream->index; + // LogInfo(" Write frame mTmpPkt->pts: %llu\n", mTmpPkt->pts); + + if (callback) { + // if (mCodecId == AV_CODEC_ID_MJPEG) { + // save_code_stream_file(mTmpPkt->data, mTmpPkt->size); + // } + callback(mTmpPkt); + } + } + + return ret == AVERROR_EOF ? 1 : 0; +} +bool FfmpegEncoderV2::OpenVideo(AVDictionary *optArg, AVStream *stream) +{ + int ret = 0; + AVDictionary *opt = nullptr; + av_dict_copy(&opt, optArg, 0); + // av_dict_set(&opt, "strict_std_compliance", "experimental", 0); + av_opt_set(mCodecCtx, "strict", "unofficial", 0); // Add for jpeg + /* open the codec */ + ret = avcodec_open2(mCodecCtx, mCodec, &opt); + av_dict_free(&opt); + if (ret < 0) { + char error_str[AV_ERROR_MAX_STRING_SIZE] = {0}; + LogError("Could not open video codec: %s\n", av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret)); + return false; + } + /* allocate and init a re-usable frame */ + mFrame = alloc_frame(mCodecCtx->pix_fmt, mCodecCtx->width, mCodecCtx->height); + if (!mFrame) { + LogError("Could not allocate video frame\n"); + return false; + } + if (mCodecCtx->pix_fmt != AV_PIX_FMT_YUV420P) { + mTmpFrame = alloc_frame(AV_PIX_FMT_YUV420P, mCodecCtx->width, mCodecCtx->height); + if (!mTmpFrame) { + LogError("Could not allocate temporary video frame\n"); + return false; + } + } + /* copy the stream parameters to the muxer */ + ret = avcodec_parameters_from_context(stream->codecpar, mCodecCtx); + if (ret < 0) { + LogError("Could not copy the stream parameters\n"); + return false; + } + LogInfo(" Open video success, mCodecCtx->pix_fmt = %d\n", mCodecCtx->pix_fmt); + return true; +} +bool FfmpegEncoderV2::OpenAudio(AVDictionary *optArg, AVStream *stream) +{ + int nb_samples = 0; + int ret = 0; + AVDictionary *opt = nullptr; + av_dict_copy(&opt, optArg, 0); + /* open it */ + ret = avcodec_open2(mCodecCtx, mCodec, &opt); + av_dict_free(&opt); + if (ret < 0) { + char error_str[AV_ERROR_MAX_STRING_SIZE] = {0}; + LogError("Could not open audio codec: %s\n", av_make_error_string(error_str, AV_ERROR_MAX_STRING_SIZE, ret)); + return false; + } + if (mCodecCtx->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) + nb_samples = 10000; + else + nb_samples = mCodecCtx->frame_size; + mFrame = alloc_audio_frame(mCodecCtx->sample_fmt, &mCodecCtx->ch_layout, mCodecCtx->sample_rate, nb_samples); + // mTmpFrame = alloc_audio_frame(AV_SAMPLE_FMT_S16, &mCodecCtx->ch_layout, mCodecCtx->sample_rate, nb_samples); + /* copy the stream parameters to the muxer */ + ret = avcodec_parameters_from_context(stream->codecpar, mCodecCtx); + if (ret < 0) { + LogError("Could not copy the stream parameters\n"); + return false; + } + /* create resampler context */ + mSwrCtx = swr_alloc(); + if (!mSwrCtx) { + LogError("Could not allocate resampler context\n"); + return false; + } + const AVChannelLayout src = (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO; + AVChannelLayout ch_layout; + av_channel_layout_copy(&ch_layout, &src); + /* set options */ + av_opt_set_chlayout(mSwrCtx, "in_chlayout", &ch_layout, 0); + // av_opt_set_chlayout(mSwrCtx, "in_chlayout", &mCodecCtx->ch_layout, 0); + av_opt_set_int(mSwrCtx, "in_sample_rate", SOURCE_AUDIO_SAMPEL_RATE, 0); + av_opt_set_sample_fmt(mSwrCtx, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0); + av_opt_set_chlayout(mSwrCtx, "out_chlayout", &mCodecCtx->ch_layout, 0); + av_opt_set_int(mSwrCtx, "out_sample_rate", mCodecCtx->sample_rate, 0); + av_opt_set_sample_fmt(mSwrCtx, "out_sample_fmt", mCodecCtx->sample_fmt, 0); + /* initialize the resampling context */ + if ((ret = swr_init(mSwrCtx)) < 0) { + LogError("Failed to initialize the resampling context\n"); + return false; + } + return true; +} +AVFrame *FfmpegEncoderV2::ConvertAudioFrame(AVFrame *decodeFrame, struct SwrContext *swr_ctx) +{ + if (nullptr == decodeFrame) { + LogError("decodeFrame is null\n"); + return nullptr; + } + // LogInfo("decodeFrame->pts = %d\n", decodeFrame->pts); + // decodeFrame->pts = next_pts; + // next_pts += decodeFrame->nb_samples; + int ret = 0; + int dst_nb_samples = 0; + /* convert samples from native format to destination codec format, using the resampler */ + /* compute destination number of samples */ + dst_nb_samples = av_rescale_rnd(swr_get_delay(swr_ctx, mCodecCtx->sample_rate) + decodeFrame->nb_samples, + mCodecCtx->sample_rate, + SOURCE_AUDIO_SAMPEL_RATE, + AV_ROUND_UP); + // av_assert0(dst_nb_samples == decodeFrame->nb_samples); + + /* when we pass a frame to the encoder, it may keep a reference to it + * internally; + * make sure we do not overwrite it here + */ + ret = av_frame_make_writable(mFrame); + if (ret < 0) { + LogError("av_frame_make_writable failed\n"); + return nullptr; + } + + /* convert to destination format */ + ret = swr_convert( + swr_ctx, mFrame->data, dst_nb_samples, (const uint8_t **)decodeFrame->data, decodeFrame->nb_samples); + if (ret < 0) { + LogError("Error while converting\n"); + return nullptr; + } + // LogInfo("mCodecCtx->time_base.num = %d, mCodecCtx->time_base.den=%d\n", + // mCodecCtx->time_base.num, + // mCodecCtx->time_base.den); + mFrame->pts = av_rescale_q(decodeFrame->pts, (AVRational){1, 1000000}, mCodecCtx->time_base); + // LogInfo("decodeFrame->pts = %d\n", decodeFrame->pts); + // LogInfo("mFrame->pts = %d\n", mFrame->pts); + mSamplesCount += dst_nb_samples; + return mFrame; +} +AVFrame *FfmpegEncoderV2::alloc_frame(enum AVPixelFormat pix_fmt, int width, int height) +{ + AVFrame *frame; + int ret; + + frame = av_frame_alloc(); + if (!frame) + return nullptr; + + frame->format = pix_fmt; + frame->width = width; + frame->height = height; + + /* allocate the buffers for the frame data */ + ret = av_frame_get_buffer(frame, 0); + if (ret < 0) { + LogInfo("Could not allocate frame data.\n"); + return nullptr; + } + + return frame; +} +AVFrame *FfmpegEncoderV2::alloc_audio_frame(enum AVSampleFormat sample_fmt, const AVChannelLayout *channel_layout, + int sample_rate, int nb_samples) +{ + AVFrame *frame = av_frame_alloc(); + if (!frame) { + LogError("Error allocating an audio frame\n"); + return nullptr; + } + + frame->format = sample_fmt; + av_channel_layout_copy(&frame->ch_layout, channel_layout); + frame->sample_rate = sample_rate; + frame->nb_samples = nb_samples; + + if (nb_samples) { + if (av_frame_get_buffer(frame, 0) < 0) { + LogError("Error allocating an audio buffer\n"); + return nullptr; + } + } + + return frame; +} \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegEncoderV2.h b/utils/MediaBase/src/FfmpegEncoderV2.h new file mode 100644 index 0000000..69d2923 --- /dev/null +++ b/utils/MediaBase/src/FfmpegEncoderV2.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 Fancy Code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef FFMPEG_ENCODER_V2_H +#define FFMPEG_ENCODER_V2_H +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +} +#endif +#include +// constexpr int ENCODER_UNSUPORTED = 0; +class FfmpegEncoderV2 +{ +public: + /** + * @brief When encoding a video stream, you need to use this constructor to provide the required parameters. + * @param codecId Video stream format. + * @param width Video width. + * @param height Video height. + */ + FfmpegEncoderV2(const enum AVCodecID &codecId, const AVPixelFormat &encodePixelFormat, const int &width, + const int &height); + virtual ~FfmpegEncoderV2() = default; + bool Init(const int &outputFlags); + void UnInit(void); + AVRational GetTimeBase(void); + bool OpenEncoder(AVDictionary *optArg, AVStream *stream); + int EncodeData(AVFrame *frame, AVStream *stream, std::function callback); + +private: + bool OpenVideo(AVDictionary *optArg, AVStream *stream); + bool OpenAudio(AVDictionary *optArg, AVStream *stream); + AVFrame *ConvertAudioFrame(AVFrame *decodeFrame, struct SwrContext *swr_ctx); + +private: + static AVFrame *alloc_frame(enum AVPixelFormat pix_fmt, int width, int height); + static AVFrame *alloc_audio_frame(enum AVSampleFormat sample_fmt, const AVChannelLayout *channel_layout, + int sample_rate, int nb_samples); + +private: + const enum AVCodecID mCodecId; + AVCodecContext *mCodecCtx; + AVCodec *mCodec; + AVFrame *mFrame; + AVFrame *mTmpFrame; + AVPacket *mTmpPkt; + int mSamplesCount; + struct SwrContext *mSwrCtx; + int64_t next_pts; + const int mVideoWidth; + const int mVideoHeight; + const AVPixelFormat mEncodePixelFormat; +}; +#endif \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegReadFile.cpp b/utils/MediaBase/src/FfmpegReadFile.cpp index 6698962..f4f0185 100644 --- a/utils/MediaBase/src/FfmpegReadFile.cpp +++ b/utils/MediaBase/src/FfmpegReadFile.cpp @@ -42,6 +42,7 @@ FfmpegReadFile::FfmpegReadFile() } StatusCode FfmpegReadFile::StartReadFile(const std::string &path) { + mFilePath = path; InitFfmpeg(); int result = 0; const AVInputFormat *iformat = av_find_input_format(FfmpegBase::InputFormat(mType)); @@ -81,6 +82,7 @@ StatusCode FfmpegReadFile::StartReadFile(const std::string &path) } StatusCode FfmpegReadFile::StopReadFile(void) { + std::lock_guard locker(mMutex); mTaskRuning = false; if (mTaskTimerThread.joinable()) { mTaskTimerThread.join(); diff --git a/utils/MediaBase/src/FfmpegReadFile.h b/utils/MediaBase/src/FfmpegReadFile.h index 35df727..50d61fd 100644 --- a/utils/MediaBase/src/FfmpegReadFile.h +++ b/utils/MediaBase/src/FfmpegReadFile.h @@ -16,6 +16,7 @@ #define FFMPEG_READ_FILE_H #include "FfmpegBase.h" #include "MediaBase.h" +#include class FfmpegReadFile : virtual public FfmpegBase { public: @@ -33,9 +34,11 @@ private: void ReadFrame(AVPacket *packet, const unsigned int duration_us); private: + std::mutex mMutex; ReadVideoFileCallback mReadVideoCallback; void *mReadVideoCallbackContext; ReadVideoFileCallback mReadAudioCallback; void *mReadAudioCallbackContext; + std::string mFilePath; }; #endif \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegTakePicture.cpp b/utils/MediaBase/src/FfmpegTakePicture.cpp index 2bacf39..2d9857b 100644 --- a/utils/MediaBase/src/FfmpegTakePicture.cpp +++ b/utils/MediaBase/src/FfmpegTakePicture.cpp @@ -15,6 +15,7 @@ #include "FfmpegTakePicture.h" #include "FfmpegBase.h" #include "FfmpegOriginalPicture.h" +#include "FfmpegThumbnailV2.h" #include "ILog.h" #include "MediaBase.h" #include "StatusCode.h" @@ -51,24 +52,33 @@ StatusCode FfmpegTakePicture::CloseJpegFile(void) } return CreateStatusCode(STATUS_CODE_OK); } -StatusCode FfmpegTakePicture::WriteJpegData(const void *data, const size_t &size) +StatusCode FfmpegTakePicture::WriteJpegData(const void *data, const size_t &size, const StreamInfo &streamInfo) { if (!mOutputFileInfo) { LogError("mOutputFileInfoCit is null\n"); return CreateStatusCode(STATUS_CODE_NOT_OK); } - char *pData = (char *)data; - for (size_t i = 0; i < size; i++) { - if ((0x00 == pData[i]) && (0x00 == pData[i + 1]) && (0x00 == pData[i + 2]) && (0x01 == pData[i + 3]) && - (0x5 == (pData[i + 4] & 0x1F))) { - LogInfo("Found extradata\n"); - CreateJpegFile(data, size); - if (mOutputFileInfo->mFinished) { - *(mOutputFileInfo->mFinished) = static_cast(OUTPUT_FILE_STATUS_FINISHED); + if (STREAM_TYPE_VIDEO_H264 == streamInfo.mType) { + char *pData = (char *)data; + for (size_t i = 0; i < size; i++) { + if ((0x00 == pData[i]) && (0x00 == pData[i + 1]) && (0x00 == pData[i + 2]) && (0x01 == pData[i + 3]) && + (0x5 == (pData[i + 4] & 0x1F))) { + LogInfo("Found extradata\n"); + CreateJpegFile(data, size); + if (mOutputFileInfo->mFinished) { + *(mOutputFileInfo->mFinished) = static_cast(OUTPUT_FILE_STATUS_FINISHED); + } + return CreateStatusCode(STATUS_CODE_OK); } - return CreateStatusCode(STATUS_CODE_OK); } } + if (STREAM_TYPE_JPEG == streamInfo.mType) { + CreateJpegFile2(data, size); + if (mOutputFileInfo->mFinished) { + *(mOutputFileInfo->mFinished) = static_cast(OUTPUT_FILE_STATUS_FINISHED); + } + return CreateStatusCode(STATUS_CODE_OK); + } return CreateStatusCode(STATUS_CODE_NOT_OK); } void inline FfmpegTakePicture::CreateJpegFile(const void *data, const size_t &size) @@ -94,4 +104,24 @@ void FfmpegTakePicture::CreateJpegFileThread(const void *data, const size_t &siz picture.Init(info); picture.CreateOriginalPicture(mOutputFileInfo->mFileName, data, size); picture.UnInit(); +} +void inline FfmpegTakePicture::CreateJpegFile2(const void *data, const size_t &size) +{ + mFrameData = (char *)malloc(size); + if (!mFrameData) { + LogError("malloc failed\n"); + return; + } + memcpy(mFrameData, data, size); + auto codecThread = [](std::shared_ptr output, const void *frameData, const size_t dataSize) { + LogInfo("CreateJpegFile start.\n"); + output->CreateJpegFileThread2(frameData, dataSize); + }; + std::shared_ptr impl = + std::dynamic_pointer_cast(FfmpegBase::shared_from_this()); + mCodecThread = std::thread(codecThread, impl, mFrameData, size); +} +void FfmpegTakePicture::CreateJpegFileThread2(const void *data, const size_t &size) +{ + FfmpegThumbnailV2::SavePicture(mOutputFileInfo->mFileName, data, size); } \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegTakePicture.h b/utils/MediaBase/src/FfmpegTakePicture.h index cdb1925..7265de7 100644 --- a/utils/MediaBase/src/FfmpegTakePicture.h +++ b/utils/MediaBase/src/FfmpegTakePicture.h @@ -50,11 +50,13 @@ public: * @param size * @return StatusCode */ - StatusCode WriteJpegData(const void *data, const size_t &size) override; + StatusCode WriteJpegData(const void *data, const size_t &size, const StreamInfo &streamInfo) override; private: void CreateJpegFile(const void *data, const size_t &size); void CreateJpegFileThread(const void *data, const size_t &size); + void CreateJpegFile2(const void *data, const size_t &size); + void CreateJpegFileThread2(const void *data, const size_t &size); private: std::shared_ptr mOutputFileInfo; diff --git a/utils/MediaBase/src/FfmpegThumbnailV2.cpp b/utils/MediaBase/src/FfmpegThumbnailV2.cpp new file mode 100644 index 0000000..d930e37 --- /dev/null +++ b/utils/MediaBase/src/FfmpegThumbnailV2.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2023 Fancy Code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "FfmpegThumbnailV2.h" +#include "FfmpegDecoderV2.h" +#include "FfmpegEncoderV2.h" +#include "ILog.h" +#include "LinuxApi.h" +#include +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +} +#endif +#include +#include +#include +#include +#include +thumbnail_info_v2::thumbnail_info_v2(const int &sourceWidth, const int &sourceHeight, const int &targetWidth, + const int &targetHeight) + : mSourceWidth(sourceWidth), mSourceHeight(sourceHeight), mTargetWidth(targetWidth), mTargetHeight(targetHeight) +{ +} +FfmpegThumbnailV2::FfmpegThumbnailV2(const AVCodecID &encodecId, const AVCodecID &decodecId) + : mOutputFormat(nullptr), mStream(nullptr), mSwsCtx(nullptr), mEncodecId(encodecId), mDecodecId(decodecId), + mDecodePixelFormat(AV_PIX_FMT_RGB4) +{ +} +void FfmpegThumbnailV2::Init(const ThumbnailInfoV2 &thumbnailInfo) +{ + LogInfo("FfmpegThumbnailV2 Init\n"); + mSourceWidth = thumbnailInfo.mSourceWidth; + mSourceHeight = thumbnailInfo.mSourceHeight; + mTargetWidth = thumbnailInfo.mTargetWidth; + mTargetHeight = thumbnailInfo.mTargetHeight; + mDecoder = std::make_shared(mDecodecId, mDecodePixelFormat, mSourceWidth, mSourceHeight); + if (!mDecoder) { + LogError("mDecoder = nullptr.\n"); + } + mEncoder = std::make_shared(mEncodecId, mDecodePixelFormat, mTargetWidth, mTargetHeight); + if (!mEncoder) { + LogError("mEncoder = nullptr.\n"); + } +} +void FfmpegThumbnailV2::UnInit(void) +{ + if (mOutputFormat && mOutputFormat->pb) { + av_write_trailer(mOutputFormat); + } + if (mEncoder) { + mEncoder->UnInit(); + mEncoder.reset(); + } + if (mDecoder) { + mDecoder->UnInit(); + mDecoder.reset(); + } + if (nullptr == mOutputFormat) { + return; + } + if (!(mOutputFormat->oformat->flags & AVFMT_NOFILE)) { + /* Close the output file. */ + avio_closep(&mOutputFormat->pb); + } + avformat_free_context(mOutputFormat); + fx_system_v2("sync"); + if (mSwsCtx) { + sws_freeContext(mSwsCtx); + mSwsCtx = nullptr; + } +} +bool FfmpegThumbnailV2::CreateThumbnail(const std::string &outputFile, const void *data, const size_t &size) +{ + if (!mDecoder) { + LogError("CreateThumbnail mDecoder && mDecodeCallback\n"); + return true; + } + mDecodeCallback = std::bind(&FfmpegThumbnailV2::GetDecodeDataCallback, this, std::placeholders::_1); + mEncodeCallback = std::bind(&FfmpegThumbnailV2::GetEncodeDataCallback, this, std::placeholders::_1, outputFile); + /* allocate the output media context */ + avformat_alloc_output_context2(&mOutputFormat, nullptr, "image2", outputFile.c_str()); + if (!mOutputFormat) { + LogError("Could not deduce output format from file.\n"); + return false; + } + /* Add the audio and video streams using the default format codecs + * and initialize the codecs. */ + if (mOutputFormat->oformat->video_codec != AV_CODEC_ID_NONE) { + /** + * @brief Maybe there is no need to use avformat_alloc_output_context2 function to create ffmpeg container. + * TODO: if mOutputFormat can be deleted here? + */ + mStream = avformat_new_stream(mOutputFormat, nullptr); + if (!mStream) { + LogError("Could not allocate stream\n"); + return false; + } + mStream->id = mOutputFormat->nb_streams - 1; + LogInfo("Create video stream\n"); + } + mDecoder->Init(); + constexpr int NO_FLAGS = 0; + mEncoder->Init(NO_FLAGS); + mStream->time_base = mEncoder->GetTimeBase(); + mEncoder->OpenEncoder(nullptr, mStream); + LogInfo("Start to decode data\n"); + mDecoder->DecodeData(data, size, AV_NOPTS_VALUE, mDecodeCallback); + LogInfo("Decode data end\n"); + return false; +} +void FfmpegThumbnailV2::GetDecodeDataCallback(AVFrame *frame) +{ + EncodeDataToPicture(frame); +} +void FfmpegThumbnailV2::GetEncodeDataCallback(AVPacket *pkt, const std::string &fileName) +{ + SavePicture(fileName, pkt->data, pkt->size); +} +void FfmpegThumbnailV2::EncodeDataToPicture(AVFrame *frame) +{ + LogInfo("Decode frame->width = %d, frame->height=%d\n", frame->width, frame->height); + AVFrame *thumbnailFrame = av_frame_alloc(); + if (!thumbnailFrame) { + LogError("thumbnailFrame = nullptr.\n"); + return; + } + thumbnailFrame->format = mDecodePixelFormat; + thumbnailFrame->width = mTargetWidth; + thumbnailFrame->height = mTargetHeight; + + int jpegBufSize = av_image_get_buffer_size(mDecodePixelFormat, mSourceWidth, mSourceHeight, 1); + LogInfo("jpegBufSize: %d\n", jpegBufSize); + uint8_t *jpegBuf = (uint8_t *)av_malloc(jpegBufSize); + if (!jpegBuf) { + LogError("jpegBuf = nullptr.\n"); + goto END; + } + av_image_fill_arrays( + thumbnailFrame->data, thumbnailFrame->linesize, jpegBuf, mDecodePixelFormat, frame->width, frame->height, 1); + + mSwsCtx = sws_getContext(mSourceWidth, + mSourceHeight, + static_cast(frame->format), + thumbnailFrame->width, + thumbnailFrame->height, + mDecodePixelFormat, + SWS_BILINEAR, + nullptr, + nullptr, + nullptr); + if (!mSwsCtx) { + LogError("mSwsCtx = nullptr.\n"); + goto END; + } + fx_system_v2("echo 1 > /proc/sys/vm/drop_caches"); + /** + * @brief Perform pixel format conversion. + * NOTE: This function will crash when the chip does not have enough memory. + */ + sws_scale(mSwsCtx, frame->data, frame->linesize, 0, frame->height, thumbnailFrame->data, thumbnailFrame->linesize); + + if (mEncoder) { + mEncoder->EncodeData(thumbnailFrame, mStream, mEncodeCallback); + } +END: + if (thumbnailFrame) { + av_frame_free(&thumbnailFrame); + } + if (jpegBuf) { + av_free(jpegBuf); + } + return; +} +bool FfmpegThumbnailV2::SavePicture(const std::string &fileName, const void *data, const size_t &size) +{ + FILE *file = nullptr; + if (!data) { + LogError("SavePicture:%s failed, data is nullptr.\n", fileName.c_str()); + return false; + } + LogInfo("SavePicture:%s, size = %u\n", fileName.c_str(), size); + file = fopen(fileName.c_str(), "a+"); + if (!file) { + LogError("fopen failed.\n"); + return false; + } + fwrite(data, 1, size, file); + fflush(file); + fclose(file); + // system("sync"); + return true; +} \ No newline at end of file diff --git a/utils/MediaBase/src/FfmpegThumbnailV2.h b/utils/MediaBase/src/FfmpegThumbnailV2.h new file mode 100644 index 0000000..e5f6dfc --- /dev/null +++ b/utils/MediaBase/src/FfmpegThumbnailV2.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Fancy Code. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef FFMPEG_THUMBNAIL_V2_H +#define FFMPEG_THUMBNAIL_V2_H +#include "FfmpegDecoderV2.h" +#include "FfmpegEncoderV2.h" +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +} +#endif +#include +#include +typedef struct thumbnail_info_v2 +{ + thumbnail_info_v2(const int &sourceWidth, const int &sourceHeight, const int &targetWidth, const int &targetHeight); + const int mSourceWidth; + const int mSourceHeight; + const int mTargetWidth; + const int mTargetHeight; +} ThumbnailInfoV2; +class FfmpegThumbnailV2 +{ +public: + FfmpegThumbnailV2(const AVCodecID &encodecId, const AVCodecID &decodecId); + virtual ~FfmpegThumbnailV2() = default; + void Init(const ThumbnailInfoV2 &thumbnailInfo); + void UnInit(void); + bool CreateThumbnail(const std::string &outputFile, const void *data, const size_t &size); + +private: + void GetDecodeDataCallback(AVFrame *frame); + void GetEncodeDataCallback(AVPacket *pkt, const std::string &fileName); + +protected: + virtual void EncodeDataToPicture(AVFrame *frame); + +public: + static bool SavePicture(const std::string &fileName, const void *data, const size_t &size); + +private: + std::shared_ptr mEncoder; + std::shared_ptr mDecoder; + std::function mDecodeCallback; + std::function mEncodeCallback; + +protected: + AVFormatContext *mOutputFormat; + AVStream *mStream; + struct SwsContext *mSwsCtx; + int mSourceWidth; + int mSourceHeight; + int mTargetWidth; + int mTargetHeight; + const AVCodecID mEncodecId; + const AVCodecID mDecodecId; + AVPixelFormat mDecodePixelFormat; +}; +#endif \ No newline at end of file diff --git a/utils/MediaBase/src/IMediaBase.cpp b/utils/MediaBase/src/IMediaBase.cpp index 0418f0a..50f9e42 100644 --- a/utils/MediaBase/src/IMediaBase.cpp +++ b/utils/MediaBase/src/IMediaBase.cpp @@ -70,7 +70,7 @@ StatusCode IMediaBase::CloseJpegFile(void) LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); return CreateStatusCode(STATUS_CODE_VIRTUAL_FUNCTION); } -StatusCode IMediaBase::WriteJpegData(const void *data, const size_t &size) +StatusCode IMediaBase::WriteJpegData(const void *data, const size_t &size, const StreamInfo &streamInfo) { LogWarning("STATUS_CODE_VIRTUAL_FUNCTION\n"); return CreateStatusCode(STATUS_CODE_VIRTUAL_FUNCTION); diff --git a/utils/MediaBase/src/IMediaBase.h b/utils/MediaBase/src/IMediaBase.h index 50808a8..efe5a2c 100644 --- a/utils/MediaBase/src/IMediaBase.h +++ b/utils/MediaBase/src/IMediaBase.h @@ -37,7 +37,7 @@ public: // About combine file. public: // About take picture. virtual StatusCode OpenJpegFile(const OutputFileInfo &fileInfo); virtual StatusCode CloseJpegFile(void); - virtual StatusCode WriteJpegData(const void *data, const size_t &size); + virtual StatusCode WriteJpegData(const void *data, const size_t &size, const StreamInfo &streamInfo); }; typedef struct media_base_header { diff --git a/utils/MediaBase/src/MediaBase.cpp b/utils/MediaBase/src/MediaBase.cpp index 8b6e112..5a7ef77 100644 --- a/utils/MediaBase/src/MediaBase.cpp +++ b/utils/MediaBase/src/MediaBase.cpp @@ -106,10 +106,10 @@ StatusCode ICloseJpegFile(void *object) } return CreateStatusCode(STATUS_CODE_NOT_OK); } -StatusCode IWriteJpegData(void *object, const void *data, const size_t size) +StatusCode IWriteJpegData(void *object, const void *data, const size_t size, const StreamInfo streamInfo) { if (ObjectCheck(object) == true) { - return (*(std::shared_ptr *)object)->WriteJpegData(data, size); + return (*(std::shared_ptr *)object)->WriteJpegData(data, size, streamInfo); } return CreateStatusCode(STATUS_CODE_NOT_OK); }