From 7b259d35a54610f192687288f4ec9d69cdfb9187 Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 11 Jun 2024 16:03:11 -0700 Subject: [PATCH] Unit test to read Insta360 information using telemetry reader instead of file creation time --- CMakeLists.txt | 2 +- Insta360/Insta360.cpp | 122 ++++++++++++++++ Insta360/Insta360.h | 18 +++ adobe_premiere/ClipMarker.h | 4 + .../insta360/MarkerReaderInsta360.cpp | 60 +++++--- .../insta360/MarkerReaderInsta360.h | 11 +- cameras/CameraBase.cpp | 130 ++++++++++++++++++ cameras/CameraBase.h | 62 +++++++++ ffmpeg/FFMpeg.cpp | 13 +- ffmpeg/test_cmd.sh | 8 ++ gui/InputDataDialog.qml | 39 ++++++ gui/Project.cpp | 10 ++ gui/Project.h | 22 +++ gui/RaceTreeModel.cpp | 10 +- gui/RaceTreeModel.h | 22 ++- gui/Worker.cpp | 62 +++++++-- gui/Worker.h | 2 +- movie/MovieProducer.cpp | 18 ++- movie/OverlayMaker.cpp | 2 +- n2k/YdvrReader.cpp | 11 +- test/CMakeLists.txt | 2 + test/test_adobe_markers.cpp | 75 +++++++--- 22 files changed, 638 insertions(+), 67 deletions(-) create mode 100644 Insta360/Insta360.cpp create mode 100644 Insta360/Insta360.h create mode 100644 cameras/CameraBase.cpp create mode 100644 cameras/CameraBase.h create mode 100755 ffmpeg/test_cmd.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 0209653..042471a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,7 +163,7 @@ set (NAV_SRC magnetic/wmm.c magnetic/GeomagnetismLibrary.c magnetic/mem_file.c - adobe_premiere/insta360/MarkerReaderInsta360.cpp adobe_premiere/insta360/MarkerReaderInsta360.h adobe_premiere/ClipMarker.cpp adobe_premiere/ClipMarker.h) + adobe_premiere/insta360/MarkerReaderInsta360.cpp adobe_premiere/insta360/MarkerReaderInsta360.h adobe_premiere/ClipMarker.cpp adobe_premiere/ClipMarker.h Insta360/Insta360.cpp Insta360/Insta360.h cameras/CameraBase.cpp cameras/CameraBase.h) qt_add_executable(sailvue WIN32 MACOSX_BUNDLE main.cpp diff --git a/Insta360/Insta360.cpp b/Insta360/Insta360.cpp new file mode 100644 index 0000000..122a1d7 --- /dev/null +++ b/Insta360/Insta360.cpp @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "Insta360.h" + +Insta360::Insta360(InstrDataReader &rInstrDataReader, IProgressListener &rProgressListener): + CameraBase("insta360", ".insv", rInstrDataReader, rProgressListener) +{ +} + + +#define READ 0 +#define WRITE 1 + +static pid_t +popen2(const char *command, int *infp, int *outfp) +{ + int p_stdin[2], p_stdout[2]; + pid_t pid; + + if (pipe(p_stdin) != 0 || pipe(p_stdout) != 0) + return -1; + + pid = fork(); + + if (pid < 0) + return pid; + else if (pid == 0) + { + close(p_stdin[WRITE]); + dup2(p_stdin[READ], READ); + close(p_stdout[READ]); + dup2(p_stdout[WRITE], WRITE); + + execl("/bin/sh", "sh", "-c", command, NULL); + perror("execl"); + exit(1); + } + + if (infp == nullptr) + close(p_stdin[WRITE]); + else + *infp = p_stdin[WRITE]; + + if (outfp == nullptr) + close(p_stdout[READ]); + else + *outfp = p_stdout[READ]; + + return pid; +} + +std::tuple Insta360::readClipFile(const std::string &clipFileName, std::list &listInputs) { + std::string telemetryParserArgs("/Users/sergei/github/telemetry-parser/bin/gyro2bb/target/release/gyro2bb -d "); + telemetryParserArgs += clipFileName; + + + int outfp; + int pid = popen2((const char *)telemetryParserArgs.c_str(), nullptr, &outfp); + if (pid == -1) { + throw std::runtime_error("popen2() failed!"); + } + std::cout << "gyro2bb started with PID " << pid << std::endl; + + FILE *outStream = fdopen(outfp, "r"); + + std::string out; + try { + std::array buffer{}; + + int lineCount = 0; + while ( fgets(buffer.data(), sizeof(buffer), outStream) != nullptr ) { + out = std::string(buffer.data()); + if ( lineCount == 1 ){ + kill(pid, SIGABRT); + int status; + waitpid(pid, &status, WNOHANG); + if (WIFEXITED(status)){ + std::cout << "gyro2bb with PID " << pid << " stopped." << std::endl; + } + break; + } + lineCount ++; + } + } catch (...) { + fclose(outStream); + throw; + } + + int64_t createTimeUtcMs = -1; + int totalTimeSec = -1; + int width=-1; + int height=-1; + if ( out.find("Metadata") !=std::string::npos ){ + std::size_t jsonStart = out.find('{'); + std::string jsonString = out.substr(jsonStart); + std::cout << "gyro2bbPID " << pid << std::endl; + QJsonDocument loadDoc(QJsonDocument::fromJson(QByteArray::fromStdString(jsonString))); + QJsonObject json = loadDoc.object(); + if( QJsonValue v = json["first_gps_timestamp"]; v.isDouble()){ + createTimeUtcMs = int64_t(v.toDouble()); + } + if( QJsonValue v = json["total_time"]; v.isDouble()){ + totalTimeSec = v.toInt(); + } + + + auto dimension = json.value("dimension"); + width = dimension["x"].toInt(); + height = dimension["y"].toInt(); + + m_ulClipStartUtcMs = createTimeUtcMs; + m_ulClipEndUtcMs = m_ulClipStartUtcMs + totalTimeSec * 1000; + } + + return {width,height}; +} diff --git a/Insta360/Insta360.h b/Insta360/Insta360.h new file mode 100644 index 0000000..070ab38 --- /dev/null +++ b/Insta360/Insta360.h @@ -0,0 +1,18 @@ +#ifndef SAILVUE_INSTA360_H +#define SAILVUE_INSTA360_H + +#include +#include "../InstrDataReader.h" +#include "navcomputer/IProgressListener.h" +#include "cameras/CameraBase.h" + + +class Insta360 : public CameraBase{ +public: + Insta360(InstrDataReader& rInstrDataReader, IProgressListener& rProgressListener); +private: + std::tuple readClipFile(const std::string &clipFileName, std::list &listInputs) override; +}; + + +#endif //SAILVUE_INSTA360_H diff --git a/adobe_premiere/ClipMarker.h b/adobe_premiere/ClipMarker.h index 1bdeabe..391d1e0 100644 --- a/adobe_premiere/ClipMarker.h +++ b/adobe_premiere/ClipMarker.h @@ -2,6 +2,7 @@ #define SAILVUE_CLIPMARKER_H #include +#include "cameras/CameraBase.h" class ClipMarker { @@ -18,6 +19,8 @@ class ClipMarker { void setName(const std::string &makerName) { m_name = makerName; } void setInTimeSec(u_int64_t mInTimeSec) { m_inTimeSec = mInTimeSec; } void setOutTimeSec(u_int64_t mOutTimeSec) { m_outTimeSec = mOutTimeSec; } + CameraClipInfo *getClipInfo() const { return m_pClipInfo; } + void setClipInfo(CameraClipInfo *mPClipInfo) { m_pClipInfo = mPClipInfo; } private: std::string m_name="Untitled Marker"; @@ -25,6 +28,7 @@ class ClipMarker { u_int64_t m_outTimeSec=0; u_int64_t m_UtcMsIn=0; u_int64_t m_UtcMsOut=0; + CameraClipInfo *m_pClipInfo = nullptr; }; diff --git a/adobe_premiere/insta360/MarkerReaderInsta360.cpp b/adobe_premiere/insta360/MarkerReaderInsta360.cpp index 9ddb193..8ee25cf 100644 --- a/adobe_premiere/insta360/MarkerReaderInsta360.cpp +++ b/adobe_premiere/insta360/MarkerReaderInsta360.cpp @@ -1,29 +1,39 @@ #include -#include #include #include #include +#include + #include "MarkerReaderInsta360.h" -void MarkerReaderInsta360::read(std::filesystem::path &markersDir, std::filesystem::path &insta360Dir) { +void MarkerReaderInsta360::read(const std::filesystem::path &markersDir, const std::list &cameraClips) { auto markerFiles = std::filesystem::recursive_directory_iterator(markersDir); for( const auto& markerFile : markerFiles ) { + std::cout << "Reading marker file " << markerFile.path() << std::endl; + if (markerFile.path().extension() == ".csv") { - // Find corresponding .insv file - std::filesystem::path insvBaseName = markerFile.path().filename().stem(); - std::filesystem::path insvPath = insta360Dir / insvBaseName; + std::filesystem::path clipBaseName = markerFile.path().filename().stem().string(); + + // Find corresponding clip - std::cout << insvPath << std::endl; + u_int64_t createTimeUtcMs = 0; + CameraClipInfo *pClipInfo; - const char *fname = insvPath.c_str(); + for( auto clip : cameraClips){ + if( clip->getFileName().find(clipBaseName) != std::string::npos){ + std::cout << " Marker for clip " << clip->getFileName() << std::endl; + createTimeUtcMs = clip->getClipStartUtcMs(); + pClipInfo = clip; + break; + } + } - // Get file creation time - struct stat t_stat{}; - stat(fname, &t_stat); - u_int64_t createTimeUtcMs = t_stat.st_birthtimespec.tv_sec * 1000 + - t_stat.st_birthtimespec.tv_nsec / 1000 / 1000 ; + if ( createTimeUtcMs == 0 ){ + std::cerr << "Failed to find clip for marker file " << markerFile.path() << std::endl; + continue; + } // Read marker file line by line // Marker file is encoded as UTF-16LE @@ -59,24 +69,32 @@ void MarkerReaderInsta360::read(std::filesystem::path &markersDir, std::filesyst int outSec = timeCodeToSec(item); marker->setOutTimeSec(outSec); - marker->setClipStartUtc(createTimeUtcMs); + marker->setClipStartUtc(createTimeUtcMs + m_timeAdjustmentMs); + + marker->setClipInfo(pClipInfo); + m_markersList.push_back(*marker); - } + std::cout << " Added marker " << marker->getName() << std::endl; + } } } + + // Sort Markers by start UTC time + m_markersList.sort( [] (const ClipMarker &c1, const ClipMarker &c2) {return c1.getUtcMsIn() < c2.getUtcMsIn();}); + } + int MarkerReaderInsta360::timeCodeToSec(const std::string &item) { return stoi(item.substr(0, 2)) * 3600 + stoi(item.substr(3, 2)) * 60 + stoi(item.substr(6, 2)); } -void MarkerReaderInsta360::makeChapters(std::vector &instrDataVector, std::list &chapters) { +void MarkerReaderInsta360::makeChapters(std::list &chapters, std::vector &instrDataVector) { - for( auto marker: m_markersList){ + for( const auto& marker: m_markersList){ // Find index of the clip in - auto inIter = std::lower_bound(instrDataVector.begin(), instrDataVector.end(), marker.getUtcMsIn(), [](const InstrumentInput& ii, u_int64_t utcMs) { return ii.utc.getUnixTimeMs() <= utcMs;} @@ -88,18 +106,18 @@ void MarkerReaderInsta360::makeChapters(std::vector &instrDataV ); if ( inIter != instrDataVector.end() && outIter != instrDataVector.end()){ - u_int64_t startIdx = inIter - instrDataVector.begin(); - u_int64_t endIdx = outIter - instrDataVector.begin(); + u_int64_t startIdx = std::distance(instrDataVector.begin(), inIter); + u_int64_t endIdx = std::distance(instrDataVector.begin(), outIter); auto chapter = new Chapter(startIdx, endIdx); chapter->SetName(marker.getName()); - chapters.push_back(*chapter); + chapters.push_back(chapter); }else{ std::cerr << "Could not timestamp marker " << marker.getName() << std::endl; } } // Sort chapters by index - chapters.sort( [] (const Chapter &c1, const Chapter &c2) {return c1.getStartIdx() < c2.getStartIdx();}); + chapters.sort( [] (const Chapter *c1, const Chapter *c2) {return c1->getStartIdx() < c2->getStartIdx();}); } diff --git a/adobe_premiere/insta360/MarkerReaderInsta360.h b/adobe_premiere/insta360/MarkerReaderInsta360.h index 8d37259..cbf1ab4 100644 --- a/adobe_premiere/insta360/MarkerReaderInsta360.h +++ b/adobe_premiere/insta360/MarkerReaderInsta360.h @@ -1,20 +1,25 @@ #ifndef SAILVUE_MARKERREADERINSTA360_H #define SAILVUE_MARKERREADERINSTA360_H - #include #include #include "adobe_premiere/ClipMarker.h" #include "navcomputer/Chapter.h" +#include "cameras/CameraBase.h" class MarkerReaderInsta360 { public: - void read(std::filesystem::path &markersDir, std::filesystem::path &insta360Dir); - void makeChapters(std::vector &instrDataVector, std::list &chapters); + void setTimeAdjustmentMs(int64_t timeAdjustmentMs){m_timeAdjustmentMs = timeAdjustmentMs;}; + void read(const std::filesystem::path &markersDir, const std::list &cameraClips); + void makeChapters(std::list &chapters, std::vector &instrDataVector); + const std::list &getMarkersList() const { return m_markersList; } private: std::list m_markersList; +private: + int64_t m_timeAdjustmentMs = 0; + static int timeCodeToSec(const std::string &item) ; }; diff --git a/cameras/CameraBase.cpp b/cameras/CameraBase.cpp new file mode 100644 index 0000000..b7181b5 --- /dev/null +++ b/cameras/CameraBase.cpp @@ -0,0 +1,130 @@ +#include "CameraBase.h" + +#include + +CameraBase::CameraBase(const std::string& cameraName, const std::string& cameraClipExtension, + InstrDataReader &rInstrDataReader, + IProgressListener &rProgressListener) +:m_cameraName(std::move(cameraName)), m_cameraClipExtension(std::move(cameraClipExtension)), + m_rInstrDataReader(rInstrDataReader), m_rProgressListener(rProgressListener) +{ +} + +void CameraBase::processClipsDir(const std::string &stClipsDir, const std::string &stWorkDir) { + std::filesystem::path stCacheDir; + stCacheDir = std::filesystem::path(stWorkDir) / m_cameraName / "cache"; + std::filesystem::create_directories(stCacheDir); + std::filesystem::path stSummaryDir = std::filesystem::path(stWorkDir) / m_cameraName / "summary" ; + std::filesystem::create_directories(stSummaryDir); + + auto files = std::filesystem::recursive_directory_iterator(stClipsDir); + float filesCount = 0; + for( const auto& file : files ) + if (file.path().extension() == m_cameraClipExtension) + filesCount++; + files = std::filesystem::recursive_directory_iterator(stClipsDir); + float fileNo = 0; + for( const auto& file : files ) { + if (file.path().extension() == m_cameraClipExtension) { + processClipFile(file.path().string(), stCacheDir, stSummaryDir); + int progress = (int)round(fileNo / filesCount * 100.f); + m_rProgressListener.progress(file.path().filename(), progress); + if( m_rProgressListener.stopRequested() ) { + m_rProgressListener.progress("Terminated", 100); + break; + } + fileNo++; + } + } + + // Sort the list by start time + m_clipInfoList.sort([](const CameraClipInfo *a, const CameraClipInfo *b) { + return a->getClipStartUtcMs() < b->getClipStartUtcMs(); + }); +} + +void CameraBase::processClipFile(const std::string &mp4FileName, const std::filesystem::path &cacheDir, + const std::filesystem::path &summaryDir) { + + + std::cout << mp4FileName << std::endl; + + std::string csvFileName = std::filesystem::path(mp4FileName).filename().string() + ".csv"; + std::filesystem::path stCacheFile = std::filesystem::path(cacheDir) / csvFileName; + std::filesystem::path stSummaryFile = std::filesystem::path(summaryDir) / csvFileName; + + auto haveCache = std::filesystem::exists(stCacheFile) && std::filesystem::is_regular_file(stCacheFile) ; + + auto haveSummary = std::filesystem::exists(stSummaryFile) && std::filesystem::is_regular_file(stSummaryFile) && + std::filesystem::file_size(stSummaryFile) > 0; + + int width, height; + if( haveCache && haveSummary ) { + std::cout << "have cache and summary" << std::endl; + auto *pInstrData = new std::list; + ReadCacheAndSummary(stCacheFile, stSummaryFile, *pInstrData,width, height); + auto goProClipInfo = new CameraClipInfo (mp4FileName, m_ulClipStartUtcMs, m_ulClipEndUtcMs, + pInstrData, width, height); + m_clipInfoList.push_back(goProClipInfo); + }else{ + auto *cameraInstrData = new std::list(); + std::tie(width, height) = readClipFile(mp4FileName, *cameraInstrData); + + if ( width != -1 ){ + std::ofstream ofsCache(stCacheFile); + std::ofstream ofsSummary(stSummaryFile); + ofsSummary << "StartGpsTimeMs,EndGpsTimeMs,width,height" << std::endl; + ofsSummary << m_ulClipStartUtcMs << "," << m_ulClipEndUtcMs << "," << width << "," << height << std::endl; + } + + // Now try to get the instrument data from instruments themselves + auto *instrData = new std::list(); + m_rInstrDataReader.read(m_ulClipStartUtcMs, m_ulClipEndUtcMs, *instrData); + if ( instrData->empty()) { + // Use the data from camera + storeCacheFile(stCacheFile, *cameraInstrData); + auto clipInfo = new CameraClipInfo (mp4FileName, m_ulClipStartUtcMs, m_ulClipEndUtcMs, + cameraInstrData, width, height); + m_clipInfoList.push_back(clipInfo); + }else{ + // Use the data from instruments + auto clipInfo = new CameraClipInfo(mp4FileName, m_ulClipStartUtcMs, m_ulClipEndUtcMs, + instrData, width, height); + storeCacheFile(stCacheFile, *instrData); + m_clipInfoList.push_back(clipInfo); + } + } +} + +void CameraBase::storeCacheFile(const std::filesystem::path &cacheFile, const std::list& instrDataList) { + std::ofstream cache (cacheFile, std::ios::out); + for ( const InstrumentInput &ii : instrDataList ) { + cache << static_cast(ii) << std::endl; + } + +} + +void CameraBase::ReadCacheAndSummary(const std::filesystem::path &pathCacheFile, const std::filesystem::path &pathSummaryFile, + std::list &instrData, int &width, int &height) { + // Read summary file + std::ifstream summary(pathSummaryFile); + std::string line; + std::getline(summary, line); // Skip header + std::getline(summary, line); + std::stringstream ss(line); + std::string item; + std::getline(ss, item, ','); + m_ulClipStartUtcMs = std::stoull(item); + std::getline(ss, item, ','); + m_ulClipEndUtcMs = std::stoull(item); + std::getline(ss, item, ','); + width = std::stoi(item); + std::getline(ss, item, ','); + height = std::stoi(item); + + // Read cache file + std::ifstream cache(pathCacheFile); + while (std::getline(cache, line)) { + instrData.push_back(InstrumentInput::fromString(line)); + } +} diff --git a/cameras/CameraBase.h b/cameras/CameraBase.h new file mode 100644 index 0000000..a4bd05d --- /dev/null +++ b/cameras/CameraBase.h @@ -0,0 +1,62 @@ +#ifndef SAILVUE_CAMERABASE_H +#define SAILVUE_CAMERABASE_H + +#include +#include "../InstrDataReader.h" +#include "navcomputer/IProgressListener.h" + +class CameraClipInfo { +public: + CameraClipInfo(std::string clipFileName, uint64_t ulClipStartUtcMs, uint64_t ulClipEndUtcMs, + std::list *pInstrDataList, int w, int h) + : m_clipFileName(std::move(clipFileName)), + m_ulClipStartUtcMs(ulClipStartUtcMs), m_ulClipEndUtcMs(ulClipEndUtcMs), m_pInstrDataList(pInstrDataList), + m_width(w), m_height(h){ + + } + [[nodiscard]] uint64_t getClipStartUtcMs() const { return m_ulClipStartUtcMs; } + [[nodiscard]] uint64_t getClipEndUtcMs() const { return m_ulClipEndUtcMs; } + [[nodiscard]] const std::string &getFileName() const {return m_clipFileName;} + [[nodiscard]] int getWidth() const { return m_width; } + [[nodiscard]] int getHeight() const { return m_height; } + std::list *getInstrData() {return m_pInstrDataList;}; + +protected: + const std::string m_clipFileName; + const int m_width; + const int m_height; + uint64_t m_ulClipStartUtcMs = 0; + uint64_t m_ulClipEndUtcMs = 0; + std::list *m_pInstrDataList = nullptr; +}; + +class CameraBase { +public: + CameraBase(const std::string& cameraName, const std::string& cameraClipExtension, + InstrDataReader& rInstrDataReader, IProgressListener& rProgressListener); + + void processClipsDir(const std::string &stClipsDir, const std::string &stWorkDir); + std::list &getClipList() {return m_clipInfoList; }; + +protected: + static void storeCacheFile(const std::filesystem::path &cacheFile, const std::list& instrDataList); + void ReadCacheAndSummary(const std::filesystem::path &pathCacheFile, const std::filesystem::path &pathSummaryFile, + std::list &instrData, int &width, int &height); + void processClipFile(const std::string &mp4FileName, const std::filesystem::path &cacheDir, + const std::filesystem::path &summaryDir); + // Returns width, height + virtual std::tuple readClipFile(const std::string &clipFileName, std::list &listInputs) = 0; + + +protected: + InstrDataReader& m_rInstrDataReader; + IProgressListener& m_rProgressListener; + std::list m_clipInfoList; + uint64_t m_ulClipStartUtcMs = 0; + uint64_t m_ulClipEndUtcMs = 0; + std::string m_cameraName; + std::string m_cameraClipExtension; +}; + + +#endif //SAILVUE_CAMERABASE_H diff --git a/ffmpeg/FFMpeg.cpp b/ffmpeg/FFMpeg.cpp index 18f8762..3e63f81 100644 --- a/ffmpeg/FFMpeg.cpp +++ b/ffmpeg/FFMpeg.cpp @@ -190,7 +190,7 @@ std::string FFMpeg::makeClipFfmpegArgs(const std::string &clipPath) { // List of overlay files for(const auto& overlay: m_pOverlays){ - ffmpegArgs += " -framerate " + std::to_string(overlay.fps) + " -i " + overlay.path.string() + "/" + overlay.filePattern; + ffmpegArgs += " -framerate " + std::to_string(overlay.fps) + " -i \"" + overlay.path.string() + "/" + overlay.filePattern + "\""; ffmpegArgs += " \\\n"; clipIdx++; } @@ -255,10 +255,19 @@ std::string FFMpeg::makeClipFfmpegArgs(const std::string &clipPath) { ffmpegArgs += " \\\n"; ffmpegArgs += " -map " + merged; - if ( !m_changeDuration ){ // If duration is changed, don't copy the audio + if ( !m_changeDuration && !m_pClipFragments->empty() ){ // If duration is changed or no GOPRO clips presents, don't copy the audio ffmpegArgs += " -map " + audio; } ffmpegArgs += " \\\n"; + + if( m_pClipFragments->empty() ){ + ffmpegArgs += "-vcodec png "; + ffmpegArgs += " \\\n"; + ffmpegArgs += "-pix_fmt yuva420p "; + ffmpegArgs += " \\\n"; + } + + ffmpegArgs += " \"" + clipPath + "\""; return ffmpegArgs; } diff --git a/ffmpeg/test_cmd.sh b/ffmpeg/test_cmd.sh new file mode 100755 index 0000000..aa9b369 --- /dev/null +++ b/ffmpeg/test_cmd.sh @@ -0,0 +1,8 @@ +/Users/sergei/github/sailvue/cmake-build-debug/sailvue.app/Contents/MacOS/ffmpeg -progress - -nostats -y \ + -framerate 9.822618 -i /private/tmp/race0/chapter_000_Start/overlay_%05d.png \ + -filter_complex \ +"[0:v][0] overlay=0:0 [out0]; " \ + -map [out0] \ + -vcodec png \ +-pix_fmt yuva420p \ + "/private/tmp/race0/chapter_000_Start/clip.mp4" \ No newline at end of file diff --git a/gui/InputDataDialog.qml b/gui/InputDataDialog.qml index d084da8..cdbec17 100644 --- a/gui/InputDataDialog.qml +++ b/gui/InputDataDialog.qml @@ -27,6 +27,7 @@ Dialog { columns: 1 visible: true + // GOPRO Label { id: goproFolderLabel text: raceTreeModel.goproPath @@ -36,6 +37,24 @@ Dialog { onClicked: goProFolderDialog.open() } + // Insta 360 + Label { + text: raceTreeModel.insta360Path + } + Button { + text: "Select Insta360 folder" + onClicked: insta360FolderDialog.open() + } + + // Adobe markers + Label { + text: raceTreeModel.adobeMarkersPath + } + Button { + text: "Select Adobe markers folder" + onClicked: adobeMarkersFolderDialog.open() + } + Label { id: nmeaFolderLabel text: raceTreeModel.nmeaPath @@ -81,6 +100,26 @@ Dialog { } } + FolderDialog { + id: insta360FolderDialog + visible: false + title: "Select Insta360 folder" + currentFolder: raceTreeModel.insta360Path + onAccepted: { + raceTreeModel.insta360Path = selectedFolder + } + } + + FolderDialog { + id: adobeMarkersFolderDialog + visible: false + title: "Select Adobe markers folder" + currentFolder: raceTreeModel.adobeMarkersPath + onAccepted: { + raceTreeModel.adobeMarkersPath = selectedFolder + } + } + FolderDialog { id: nmeaFolderDialog visible: false diff --git a/gui/Project.cpp b/gui/Project.cpp index d231753..4ca721c 100644 --- a/gui/Project.cpp +++ b/gui/Project.cpp @@ -30,6 +30,14 @@ void Project::fromJson(const QJsonObject &json){ setGoProPath(v.toString()); } + if( QJsonValue v = json["insta360Path"]; v.isString()){ + setInsta360Path(v.toString()); + } + + if( QJsonValue v = json["adobeMarkersPath"]; v.isString()){ + setAdobeMarkersPath(v.toString()); + } + if( QJsonValue v = json["nmeaPath"]; v.isString()){ setNmeaPath(v.toString()); } @@ -76,6 +84,8 @@ void Project::fromJson(const QJsonObject &json){ QJsonObject Project::toJson() const { QJsonObject json; json["goproPath"] = goproPath(); + json["insta360Path"] = insta360Path(); + json["adobeMarkersPath"] = adobeMarkersPath(); json["nmeaPath"] = nmeaPath(); json["logsType"] = logsType(); diff --git a/gui/Project.h b/gui/Project.h index a81a273..39a4e6b 100644 --- a/gui/Project.h +++ b/gui/Project.h @@ -23,6 +23,18 @@ class Project { std::cout << "set gopro path to " << path.toStdString() << std::endl; } + void setInsta360Path(const QString &path){ + m_insta360Path = path; + m_isDirty = true; + std::cout << "set insta 360 path to " << path.toStdString() << std::endl; + } + + void setAdobeMarkersPath(const QString &path){ + m_adobeMarkersPath = path; + m_isDirty = true; + std::cout << "set adobe markers path to " << path.toStdString() << std::endl; + } + void setNmeaPath(const QString &path){ m_nmeaPath = path; m_isDirty = true; @@ -49,6 +61,14 @@ class Project { return m_goproPath; } + [[nodiscard]] QString insta360Path() const{ + return m_insta360Path; + } + + [[nodiscard]] QString adobeMarkersPath() const{ + return m_adobeMarkersPath; + } + [[nodiscard]] QString nmeaPath() const{ return m_nmeaPath; } @@ -80,6 +100,8 @@ class Project { std::list &m_RaceDataList; QString m_projectName = "Untitled"; QString m_goproPath = ""; + QString m_insta360Path = ""; + QString m_adobeMarkersPath = ""; QString m_nmeaPath = ""; QString m_logsType = ""; QString m_polarPath = ""; diff --git a/gui/RaceTreeModel.cpp b/gui/RaceTreeModel.cpp index 7f6fee8..aa1f402 100644 --- a/gui/RaceTreeModel.cpp +++ b/gui/RaceTreeModel.cpp @@ -351,14 +351,20 @@ void RaceTreeModel::load(const QString &path) { emit polarPathChanged(); emit projectNameChanged(); emit isDirtyChanged(); - emit readData(m_project.goproPath(), m_project.logsType(), m_project.nmeaPath(), m_project.polarPath(), false); + emit readData(m_project.goproPath(), + m_project.insta360Path(), + m_project.adobeMarkersPath(), + m_project.logsType(), m_project.nmeaPath(), m_project.polarPath(), false); } } void RaceTreeModel::read(bool ignoreCache) { emit loadStarted(); deleteAllRaces(); - emit readData(m_project.goproPath(), m_project.logsType(), m_project.nmeaPath(), m_project.polarPath(), ignoreCache); + emit readData(m_project.goproPath(), + m_project.insta360Path(), + m_project.adobeMarkersPath(), + m_project.logsType(), m_project.nmeaPath(), m_project.polarPath(), ignoreCache); } void RaceTreeModel::save() { diff --git a/gui/RaceTreeModel.h b/gui/RaceTreeModel.h index 6b3ebd2..2e7694a 100644 --- a/gui/RaceTreeModel.h +++ b/gui/RaceTreeModel.h @@ -54,7 +54,11 @@ class RaceTreeModel : public QAbstractItemModel { Q_PROPERTY(QString projectName READ projectName NOTIFY projectNameChanged) Q_PROPERTY(bool isDirty READ isDirty NOTIFY isDirtyChanged) + Q_PROPERTY(QString goproPath READ goproPath WRITE setGoProPath NOTIFY goProPathChanged) + Q_PROPERTY(QString insta360Path READ insta360Path WRITE setInsta360Path NOTIFY insta360PathChanged) + Q_PROPERTY(QString adobeMarkersPath READ adobeMarkersPath WRITE setAdobeMarkersPath NOTIFY adobeMarkersPathChanged) + Q_PROPERTY(QString nmeaPath READ nmeaPath WRITE setNmeaPath NOTIFY nmeaPathChanged) Q_PROPERTY(QString logsType READ logsType WRITE setLogsType NOTIFY logsTypeChanged) Q_PROPERTY(QString polarPath READ polarPath WRITE setPolarPath NOTIFY polarPathChanged) @@ -103,6 +107,18 @@ class RaceTreeModel : public QAbstractItemModel { emit isDirtyChanged(); } + void setInsta360Path(const QString &path){ + m_project.setInsta360Path(path); + emit insta360PathChanged(); + emit isDirtyChanged(); + } + + void setAdobeMarkersPath(const QString &path){ + m_project.setAdobeMarkersPath(path); + emit adobeMarkersPathChanged(); + emit isDirtyChanged(); + } + void setNmeaPath(const QString &path){ m_project.setNmeaPath(path); emit nmeaPathChanged(); @@ -128,6 +144,8 @@ class RaceTreeModel : public QAbstractItemModel { [[nodiscard]] QString projectName() const { return m_project.projectName(); } [[nodiscard]] bool isDirty() const { return m_project.isDirty(); } [[nodiscard]] QString goproPath() const{ return m_project.goproPath(); } + [[nodiscard]] QString insta360Path() const{ return m_project.insta360Path(); } + [[nodiscard]] QString adobeMarkersPath() const{ return m_project.adobeMarkersPath(); } [[nodiscard]] QString nmeaPath() const{ return m_project.nmeaPath(); } [[nodiscard]] QString logsType() const{ return m_project.logsType(); } [[nodiscard]] QString polarPath() const{ return m_project.polarPath(); } @@ -183,6 +201,8 @@ public slots: #pragma clang diagnostic push #pragma ide diagnostic ignored "NotImplementedFunctions" void goProPathChanged(); + void insta360PathChanged(); + void adobeMarkersPathChanged(); void isDirtyChanged(); void projectNameChanged(); void nmeaPathChanged(); @@ -190,7 +210,7 @@ public slots: void polarPathChanged(); void twaOffsetChanged(); - void readData(const QString &goproDir, const QString &logsType, const QString &nmeaDir, const QString &polarFile, bool ignoreCache); + void readData(const QString &goproDir, const QString &insta360Dir, const QString &adobeMarkersDir, const QString &logsType, const QString &nmeaDir, const QString &polarFile, bool ignoreCache); void stop(); void progressStatus(const QString &state, int progress); void fullPathReady(const QGeoPath fullPath); diff --git a/gui/Worker.cpp b/gui/Worker.cpp index 918464e..2979422 100644 --- a/gui/Worker.cpp +++ b/gui/Worker.cpp @@ -11,17 +11,22 @@ #include "Project.h" #include "navcomputer/Calibration.h" #include "n2k/ExpeditionReader.h" +#include "adobe_premiere/insta360/MarkerReaderInsta360.h" -void Worker::readData(const QString &goproDir, const QString &logsType, const QString &nmeaDir, const QString &polarFile, bool bIgnoreCache){ +void Worker::readData(const QString &goproDir, const QString &insta360Dir, const QString &adobeMarkersDir, const QString &logsType, const QString &nmeaDir, const QString &polarFile, bool bIgnoreCache){ std::cout << "goproDir " + goproDir.toStdString() << std::endl; std::cout << "logsType " + nmeaDir.toStdString() << std::endl; std::cout << "nmeaDir " + nmeaDir.toStdString() << std::endl; std::cout << "polarFile " + polarFile.toStdString() << std::endl; - /* ... here is the expensive or blocking operation ... */ std::string stYdvrDir = QUrl(nmeaDir).toLocalFile().toStdString(); std::string stGoProDir = QUrl(goproDir).toLocalFile().toStdString(); + + const std::string &stAdobeMarkersDir = QUrl(adobeMarkersDir).toLocalFile().toStdString(); + const std::string &stInsta360Dir = QUrl(insta360Dir).toLocalFile().toStdString(); + std::string stCacheDir = "/tmp/sailvue"; + bool bSummaryOnly = false; bool bMappingOnly = false; @@ -53,23 +58,58 @@ void Worker::readData(const QString &goproDir, const QString &logsType, const QS YdvrReader *ydvrReader = new YdvrReader(stYdvrDir, stCacheDir, stPgnSrcCsv, bSummaryOnly, bMappingOnly, *this); reader = ydvrReader; } - GoPro goPro(stGoProDir, stCacheDir, *reader, *this); - // Create path containing points from all gopro clips - int clipCount = 0; int pointsCount = 0; - m_rGoProClipInfoList.clear(); - m_rInstrDataVector.clear(); - for (auto& clip : goPro.getGoProClipList()) { - m_rGoProClipInfoList.push_back(clip); - clipCount++; - for( auto &ii : *clip.getInstrData()) { + int clipCount = 0; + if ( ! stGoProDir.empty() ){ + GoPro goPro(stGoProDir, stCacheDir, *reader, *this); + + // Create path containing points from all gopro clips + m_rGoProClipInfoList.clear(); + m_rInstrDataVector.clear(); + for (auto& clip : goPro.getGoProClipList()) { + m_rGoProClipInfoList.push_back(clip); + clipCount++; + for( auto &ii : *clip.getInstrData()) { + calibration.calibrate(ii); + m_rInstrDataVector.push_back(ii); + pointsCount++; + } + } + }else{ + std::list listInputs; + reader->read(0, 0xFFFFFFFFFFFFFFFFLL, listInputs); + m_rInstrDataVector.clear(); + for( auto &ii : listInputs){ calibration.calibrate(ii); m_rInstrDataVector.push_back(ii); pointsCount++; } + + if( !stAdobeMarkersDir.empty() && !stInsta360Dir.empty() ) { + QDateTime raceTime = QDateTime::fromMSecsSinceEpoch(qint64(m_rInstrDataVector[0].utc.getUnixTimeMs())); + std::string raceName = "Adobe Race " + raceTime.toString("yyyy-MM-dd hh:mm").toStdString(); + + + + MarkerReaderInsta360 markerReader; + markerReader.setTimeAdjustmentMs(5000); + markerReader.read(stAdobeMarkersDir, stInsta360Dir); + + std::list chapters; + markerReader.makeChapters(m_rInstrDataVector, chapters); + + auto *race = new RaceData(0, m_rInstrDataVector.size() - 1); + race->SetName(raceName); + for( auto chapter : chapters){ + race->insertChapter(chapter); + } + m_RaceDataList.push_back(race); + } + } + // Make performance vector the same size as the instr data vector m_rPerformanceMap.clear(); diff --git a/gui/Worker.h b/gui/Worker.h index ab12010..380c4ac 100644 --- a/gui/Worker.h +++ b/gui/Worker.h @@ -28,7 +28,7 @@ class Worker : public QObject, IProgressListener { bool stopRequested() override; public slots: - void readData(const QString &goproDir, const QString &logsType, const QString &nmeaDir, const QString &polarFile, bool bIgnoreCache); + void readData(const QString &goproDir, const QString &insta360Dir, const QString &adobeMarkersDir, const QString &logsType, const QString &nmeaDir, const QString &polarFile, bool bIgnoreCache); void stopWork() ; void produce(const QString &moviePathUrl, const QString &polarUrl); void exportStats(const QString &polarUrl, const QString &path); diff --git a/movie/MovieProducer.cpp b/movie/MovieProducer.cpp index 2d04c8b..d27737f 100644 --- a/movie/MovieProducer.cpp +++ b/movie/MovieProducer.cpp @@ -46,8 +46,13 @@ void MovieProducer::produce() { std::list chapterList = race->getChapters(); - int movieWidth = m_rGoProClipInfoList.front().getWidth(); - int movieHeight = m_rGoProClipInfoList.front().getHeight(); + int movieWidth = 1920; + int movieHeight = 1080; + + if ( !m_rGoProClipInfoList.empty() ){ + movieWidth = m_rGoProClipInfoList.front().getWidth(); + movieHeight = m_rGoProClipInfoList.front().getHeight(); + } int target_ovl_width = movieWidth; int target_ovl_height = 128; @@ -94,11 +99,11 @@ void MovieProducer::produce() { OverlayMaker overlayMaker(raceFolder, movieWidth, movieHeight); overlayMaker.addOverlayElement(instrOverlayMaker); - overlayMaker.addOverlayElement(targetsOverlayMaker); +// overlayMaker.addOverlayElement(targetsOverlayMaker); overlayMaker.addOverlayElement(polarOverlayMaker); overlayMaker.addOverlayElement(rudderOverlayMaker); overlayMaker.addOverlayElement(startTimerOverlayMaker); - overlayMaker.addOverlayElement(performanceOverlayMaker); +// overlayMaker.addOverlayElement(performanceOverlayMaker); int chapterCount = 0; int numChapters = chapterList.size(); @@ -218,7 +223,10 @@ std::string MovieProducer::produceChapter(OverlayMaker &overlayMaker, Chapter &c } std::filesystem::path chapterFolder = overlayMaker.setChapter(chapter, chapterEpochs); // This call creates new chapter name - std::filesystem::path clipFulPathName = chapterFolder / "clip.mp4"; + std::ostringstream oss; + oss << "CH_" << std::setw(2) << std::setfill('0') << chapterNum << "_" << chapter.getName() << ".mov"; + + std::filesystem::path clipFulPathName = chapterFolder.parent_path() / oss.str() ; // Check if the chapter already exists // Don't return until the m_totalRaceDuration is updated if ( std::filesystem::is_regular_file(clipFulPathName) ){ diff --git a/movie/OverlayMaker.cpp b/movie/OverlayMaker.cpp index fb8e5cb..b4e6707 100644 --- a/movie/OverlayMaker.cpp +++ b/movie/OverlayMaker.cpp @@ -11,7 +11,7 @@ OverlayMaker::OverlayMaker(const std::filesystem::path &folder, int width, int h std::filesystem::path & OverlayMaker::setChapter(Chapter &chapter, const std::list &chapterEpochs) { std::ostringstream oss; - oss << "chapter_" << std::setw(3) << std::setfill('0') << m_ChapterCount; + oss << "chapter_" << std::setw(3) << std::setfill('0') << m_ChapterCount << "_" << chapter.getName(); m_OverlayCount = 0; m_ChapterCount ++; m_ChapterFolder = m_workDir / std::filesystem::path(oss.str()); diff --git a/n2k/YdvrReader.cpp b/n2k/YdvrReader.cpp index 165fa25..9f15024 100644 --- a/n2k/YdvrReader.cpp +++ b/n2k/YdvrReader.cpp @@ -121,8 +121,12 @@ void YdvrReader::processDatFile(const std::string &ydvrFile, const std::string& std::cout << ydvrFile << std::endl; std::string csvFileName = std::filesystem::path(ydvrFile).filename().string() + ".csv"; - std::filesystem::path stCacheFile = bMappingOnly ? std::filesystem::path("/dev/null") : std::filesystem::path(stCacheDir) / csvFileName; - std::filesystem::path stSummaryFile = std::filesystem::path(stSummaryDir) / csvFileName; + + std::filesystem::path fileCacheDir = std::filesystem::path(stCacheDir) / std::filesystem::path(ydvrFile).parent_path().filename(); + std::filesystem::path fileSummaryDir = std::filesystem::path(stSummaryDir) / std::filesystem::path(ydvrFile).parent_path().filename(); + + std::filesystem::path stCacheFile = bMappingOnly ? std::filesystem::path("/dev/null") : fileCacheDir / csvFileName; + std::filesystem::path stSummaryFile = fileSummaryDir / csvFileName; auto haveCache = std::filesystem::exists(stCacheFile) && std::filesystem::is_regular_file(stCacheFile) ; @@ -145,6 +149,9 @@ void YdvrReader::processDatFile(const std::string &ydvrFile, const std::string& if( m_ulEpochCount > 0 ) m_listDatFiles.push_back(DatFileInfo{ydvrFile, stCacheFile, m_ulStartGpsTimeMs, m_ulEndGpsTimeMs, m_ulEpochCount}); } else { + std::filesystem::create_directories(fileCacheDir); + std::filesystem::create_directories(fileSummaryDir); + readDatFile(ydvrFile, stCacheFile, stSummaryFile); if( m_ulEpochCount > 0 ) m_listDatFiles.push_back(DatFileInfo{ydvrFile, stCacheFile, m_ulStartGpsTimeMs, m_ulEndGpsTimeMs, m_ulEpochCount}); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4cb91f6..242a959 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,6 +23,8 @@ add_executable (TestPolars ../movie/RudderOverlayMaker.cpp ../movie/TargetsOverlayMaker.cpp ../adobe_premiere/insta360/MarkerReaderInsta360.cpp + ../Insta360/Insta360.cpp + ../cameras/CameraBase.cpp test_polars.cpp test_median.cpp diff --git a/test/test_adobe_markers.cpp b/test/test_adobe_markers.cpp index 6b00909..d33fcfc 100644 --- a/test/test_adobe_markers.cpp +++ b/test/test_adobe_markers.cpp @@ -2,34 +2,75 @@ #include #include "navcomputer/InstrumentInput.h" #include "adobe_premiere/insta360/MarkerReaderInsta360.h" +#include "Insta360/Insta360.h" + + +class TestInstrDataReader : public InstrDataReader { +public: + explicit TestInstrDataReader(const std::string& iiFile){ + std::cout << "Reading data file: " << iiFile << std::endl; + std::ifstream cache (iiFile, std::ios::in); + std::string line; + + while (std::getline(cache, line)) { + std::stringstream ss(line); + std::string item; + std::getline(ss, item, ','); + InstrumentInput ii = InstrumentInput::fromString(line); + m_allInputs.push_back(ii); + } + } + + void read(uint64_t ulStartUtcMs, uint64_t ulEndUtcMs, std::list &listInputs) override{ + for( const auto& ii: m_allInputs){ + if ( ii.utc.getUnixTimeMs() >= ulStartUtcMs && ii.utc.getUnixTimeMs() < ulEndUtcMs){ + listInputs.push_back(ii); + } + } + }; +private: + std::list m_allInputs; +}; + +class TestProgressListener : public IProgressListener { +public: + void progress(const std::string& state, int progress) override {}; + bool stopRequested() override {return false; }; +}; TEST(AdobeMarkersTests, ReadTest) { std::filesystem::path markersDir("./data/02-MARKERS"); std::filesystem::path insta360Dir("/Volumes/SailingVideos2/2024-Coastal-Cup/01-FOOTAGE/01-INSTA-X4/Camera01/"); + + std::string iiFile = "./data/00010030.DAT.csv"; + TestInstrDataReader testInstrDataReader(iiFile); + TestProgressListener testProgressListener; + + auto camera = new Insta360(testInstrDataReader, testProgressListener); + camera->processClipsDir(insta360Dir, "/tmp"); + + std::list chapters; + MarkerReaderInsta360 markerReader; + markerReader.setTimeAdjustmentMs(5000); + markerReader.read(markersDir, camera->getClipList()); - markerReader.read(markersDir, insta360Dir); + const std::list markers = markerReader.getMarkersList(); + ASSERT_EQ(5, markers.size()); + ASSERT_EQ("Start", markers.begin()->getName()); + ASSERT_EQ(1716836639, markers.begin()->getUtcMsIn() / 1000); - std::string iiFile = "./data/00010030.DAT.csv"; - std::cout << "Reading data file: " << iiFile << std::endl; - std::ifstream cache (iiFile, std::ios::in); - std::string line; - std::vector iiVector; - - while (std::getline(cache, line)) { - std::stringstream ss(line); - std::string item; - std::getline(ss, item, ','); - InstrumentInput ii = InstrumentInput::fromString(line); - iiVector.push_back(ii); - } - ASSERT_FALSE(iiVector.empty()); + std::vector instrDataVector; - std::list chapters; - markerReader.makeChapters(iiVector, chapters); + for (auto clip : camera->getClipList()) { + for( auto &ii : *clip -> getInstrData()) { + instrDataVector.push_back(ii); + } + } + markerReader.makeChapters(chapters, instrDataVector); ASSERT_EQ(5, chapters.size()); }