diff --git a/.github/workflows/release.yml b/.github/workflows/build.yml similarity index 67% rename from .github/workflows/release.yml rename to .github/workflows/build.yml index 4c45bc1..665697a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,16 @@ -name: Release +name: Build and Release on: push: branches: - - main + - '**' + tags: + - 'v*' + pull_request: jobs: build-linux: - name: Build Linux (.deb) + name: Build Linux runs-on: ubuntu-latest steps: - name: Checkout code @@ -31,10 +34,16 @@ jobs: run: | mkdir build cd build - cmake .. -DCMAKE_BUILD_TYPE=Release + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + BUILD_TYPE="Release" + else + BUILD_TYPE="Debug" + fi + cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE make -j$(nproc) - name: Package with CPack + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | cd build cpack -G DEB @@ -44,9 +53,10 @@ jobs: with: name: linux-deb path: build/*.deb + if-no-files-found: ignore build-macos: - name: Build macOS (.dmg) + name: Build macOS runs-on: macos-latest steps: - name: Checkout code @@ -54,22 +64,28 @@ jobs: - name: Install dependencies run: | - brew install sdl2 sdl2_ttf dylibbundler create-dmg + brew install sdl2 sdl2_ttf pkg-config dylibbundler create-dmg - name: Configure and Build run: | mkdir build cd build - # We prefer static SDL2 and SDL2_ttf if possible, as per CMakeLists.txt - cmake .. -DCMAKE_BUILD_TYPE=Release + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + BUILD_TYPE="Release" + else + BUILD_TYPE="Debug" + fi + cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE make -j$(sysctl -n hw.ncpu) - name: Create App Bundle + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | chmod +x bundle_macos.sh BUILD_DIR="build" ./bundle_macos.sh - name: Create DMG + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | mkdir -p dist create-dmg \ @@ -88,27 +104,48 @@ jobs: with: name: macos-dmg path: dist/*.dmg + if-no-files-found: ignore build-windows: - name: Build Windows (.zip) + name: Build Windows runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v4 + - name: Initialize vcpkg + shell: pwsh + run: | + cd $env:VCPKG_INSTALLATION_ROOT + git fetch --unshallow --tags + git checkout master + git pull + - name: Configure shell: pwsh run: | + if ("${{ github.ref }}" -eq "refs/heads/main") { + $build_type = "Release" + } else { + $build_type = "Debug" + } cmake -S . -B build ` - -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_BUILD_TYPE=$build_type ` -DUSE_FFMPEG=OFF ` -DVCPKG_TARGET_TRIPLET=x64-windows ` -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" - name: Build - run: cmake --build build --config Release --parallel + run: | + if ("${{ github.ref }}" -eq "refs/heads/main") { + $config = "Release" + } else { + $config = "Debug" + } + cmake --build build --config $config --parallel - name: Package + if: github.event_name == 'push' && github.ref == 'refs/heads/main' shell: pwsh run: | $dist = "dist/P2ProViewer-Windows" @@ -122,10 +159,12 @@ jobs: with: name: windows-zip path: dist/P2ProViewer-windows.zip + if-no-files-found: ignore release: name: Release needs: [build-linux, build-macos, build-windows] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index d4828ce..04a65d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ initial_tests __pycache__ cmake-build-debug build +.idea \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b350233 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +P2ProViewer \ No newline at end of file diff --git a/.idea/P2Pro-Viewer.iml b/.idea/P2Pro-Viewer.iml new file mode 100644 index 0000000..4c94235 --- /dev/null +++ b/.idea/P2Pro-Viewer.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9a1c684 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8de0f79..e98b947 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,17 +35,16 @@ find_package(SDL2_ttf CONFIG REQUIRED) set(SDL2_LIBRARIES SDL2::SDL2 SDL2::SDL2main) set(SDL2_TTF_LIBRARIES SDL2_ttf::SDL2_ttf) -# Try pkg-config for libusb and ffmpeg -find_package(PkgConfig QUIET) -if(PKG_CONFIG_FOUND) - if(NOT APPLE AND NOT WIN32) - pkg_check_modules(LIBUSB libusb-1.0) - if(USE_FFMPEG) - pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libswscale libavutil) + # Try pkg-config for libusb and ffmpeg + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + if(NOT APPLE AND NOT WIN32) + pkg_check_modules(LIBUSB libusb-1.0) + if(USE_FFMPEG) + pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libswscale libavutil) + endif() endif() endif() - # pkg_check_modules(SDL2_TTF SDL2_ttf) # Commented out to avoid polluting SDL2_TTF variables -endif() if(WIN32) # On Windows with vcpkg, libusb-1.0 and ffmpeg are found via find_package or find_library @@ -144,18 +143,21 @@ else () ) endif () -if (APPLE) +if(APPLE) # Prefer static linking for SDL2 and SDL2_ttf on macOS to make it more portable # We force the search for static libraries for these specific components set(ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) # Use pkg-config to find static versions and their dependencies if possible - pkg_check_modules(SDL2_STATIC_PKG QUIET SDL2 STATIC IMPORTED_TARGET) - pkg_check_modules(SDL2_TTF_STATIC_PKG QUIET SDL2_ttf STATIC IMPORTED_TARGET) + # Note: We don't use IMPORTED_TARGET here to avoid issues with missing dependencies in the project environment + pkg_check_modules(SDL2_STATIC_PKG QUIET SDL2 STATIC) + pkg_check_modules(SDL2_TTF_STATIC_PKG QUIET SDL2_ttf STATIC) - if(TARGET PkgConfig::SDL2_TTF_STATIC_PKG) - target_link_libraries(P2ProViewer PkgConfig::SDL2_TTF_STATIC_PKG) + if(SDL2_TTF_STATIC_PKG_FOUND) + target_link_libraries(P2ProViewer ${SDL2_TTF_STATIC_PKG_LIBRARIES}) + target_link_directories(P2ProViewer PRIVATE ${SDL2_TTF_STATIC_PKG_LIBRARY_DIRS}) + target_include_directories(P2ProViewer PRIVATE ${SDL2_TTF_STATIC_PKG_INCLUDE_DIRS}) elseif(TARGET SDL2_ttf::SDL2_ttf-static) target_link_libraries(P2ProViewer SDL2_ttf::SDL2_ttf-static) else() @@ -167,9 +169,11 @@ if (APPLE) endif() endif() - if(NOT TARGET PkgConfig::SDL2_TTF_STATIC_PKG) - if(TARGET PkgConfig::SDL2_STATIC_PKG) - target_link_libraries(P2ProViewer PkgConfig::SDL2_STATIC_PKG) + if(NOT SDL2_TTF_STATIC_PKG_FOUND) + if(SDL2_STATIC_PKG_FOUND) + target_link_libraries(P2ProViewer ${SDL2_STATIC_PKG_LIBRARIES}) + target_link_directories(P2ProViewer PRIVATE ${SDL2_STATIC_PKG_LIBRARY_DIRS}) + target_include_directories(P2ProViewer PRIVATE ${SDL2_STATIC_PKG_INCLUDE_DIRS}) elseif(TARGET SDL2::SDL2-static) target_link_libraries(P2ProViewer SDL2::SDL2-static) else() @@ -191,6 +195,7 @@ if (APPLE) find_library(AUDIOUNIT_FRAMEWORK AudioUnit) find_library(FORCEFEEDBACK_FRAMEWORK ForceFeedback) find_library(GAMECONTROLLER_FRAMEWORK GameController) + find_library(AUDIOTOOLBOX_FRAMEWORK AudioToolbox) target_link_libraries(P2ProViewer ${METAL_FRAMEWORK} ${QUARTZCORE_FRAMEWORK} @@ -200,30 +205,36 @@ if (APPLE) ${AUDIOUNIT_FRAMEWORK} ${FORCEFEEDBACK_FRAMEWORK} ${GAMECONTROLLER_FRAMEWORK} - ) - - # Link against required frameworks for SDL2 static - find_library(METAL_FRAMEWORK Metal) - find_library(QUARTZCORE_FRAMEWORK QuartzCore) - find_library(COCOA_FRAMEWORK Cocoa) - find_library(CARBON_FRAMEWORK Carbon) - find_library(COREAUDIO_FRAMEWORK CoreAudio) - find_library(AUDIOUNIT_FRAMEWORK AudioUnit) - find_library(FORCEFEEDBACK_FRAMEWORK ForceFeedback) - find_library(GAMECONTROLLER_FRAMEWORK GameController) - target_link_libraries(P2ProViewer - ${METAL_FRAMEWORK} - ${QUARTZCORE_FRAMEWORK} - ${COCOA_FRAMEWORK} - ${CARBON_FRAMEWORK} - ${COREAUDIO_FRAMEWORK} - ${AUDIOUNIT_FRAMEWORK} - ${FORCEFEEDBACK_FRAMEWORK} - ${GAMECONTROLLER_FRAMEWORK} + ${AUDIOTOOLBOX_FRAMEWORK} + "-framework CoreHaptics" + "-lobjc" ) # Restore original suffixes set(CMAKE_FIND_LIBRARY_SUFFIXES ${ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) + + # Add common library search paths for macOS (Homebrew) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/opt/homebrew/lib -L/usr/local/lib") + include_directories(/opt/homebrew/include /usr/local/include) + # SDL2_ttf headers are sometimes in SDL2/SDL_ttf.h under the include dir + include_directories(/opt/homebrew/include/SDL2 /usr/local/include/SDL2) + + # Find and link freetype and harfbuzz if we're linking SDL2_ttf statically + # These are needed because static libraries don't include their dependencies + find_library(FREETYPE_LIB NAMES libfreetype.dylib freetype HINTS /opt/homebrew/lib /usr/local/lib) + find_library(HARFBUZZ_LIB NAMES libharfbuzz.dylib harfbuzz HINTS /opt/homebrew/lib /usr/local/lib) + + if(FREETYPE_LIB) + target_link_libraries(P2ProViewer "${FREETYPE_LIB}") + else() + target_link_libraries(P2ProViewer freetype) + endif() + + if(HARFBUZZ_LIB) + target_link_libraries(P2ProViewer "${HARFBUZZ_LIB}") + else() + target_link_libraries(P2ProViewer harfbuzz) + endif() else() target_link_libraries(P2ProViewer ${SDL2_LIBRARIES} ${SDL2_TTF_LIBRARIES} ${LIBUSB_LIBRARIES} ${FFMPEG_LIBRARIES}) endif() diff --git a/MaterialIcons-Regular.ttf b/MaterialIcons-Regular.ttf new file mode 100644 index 0000000..9d09b0f Binary files /dev/null and b/MaterialIcons-Regular.ttf differ diff --git a/src/AVFoundationVideoSource.mm b/src/AVFoundationVideoSource.mm index f71b213..4b00d32 100644 --- a/src/AVFoundationVideoSource.mm +++ b/src/AVFoundationVideoSource.mm @@ -5,10 +5,12 @@ #include #include #include +#include @interface P2ProCaptureDelegate : NSObject { std::vector latestFrame; std::mutex frameMutex; + std::condition_variable frameCond; BOOL hasNewFrame; } @property (nonatomic, strong) AVCaptureSession *session; @@ -45,25 +47,30 @@ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleB // We expect YUYV (kCVPixelFormatType_422YpCbCr8) which is 2 bytes per pixel size_t expectedBytesPerRow = width * 2; - std::lock_guard lock(frameMutex); - latestFrame.resize(width * height * 2); - - if (bytesPerRow == expectedBytesPerRow) { - memcpy(latestFrame.data(), baseAddress, width * height * 2); - } else { - for (size_t y = 0; y < height; ++y) { - memcpy(latestFrame.data() + y * expectedBytesPerRow, (uint8_t*)baseAddress + y * bytesPerRow, expectedBytesPerRow); + { + std::lock_guard lock(frameMutex); + latestFrame.resize(width * height * 2); + + if (bytesPerRow == expectedBytesPerRow) { + memcpy(latestFrame.data(), baseAddress, width * height * 2); + } else { + for (size_t y = 0; y < height; ++y) { + memcpy(latestFrame.data() + y * expectedBytesPerRow, (uint8_t*)baseAddress + y * bytesPerRow, expectedBytesPerRow); + } } + + hasNewFrame = YES; } - - hasNewFrame = YES; + frameCond.notify_all(); CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); } - (bool)getLatestFrame:(std::vector&)frameData { - std::lock_guard lock(frameMutex); - if (!hasNewFrame) return false; + std::unique_lock lock(frameMutex); + if (!frameCond.wait_for(lock, std::chrono::milliseconds(500), [self] { return hasNewFrame == YES; })) { + return false; + } frameData = latestFrame; hasNewFrame = NO; return true; diff --git a/src/MacOSAdapter.cpp b/src/MacOSAdapter.cpp index a6332b3..6075fcd 100644 --- a/src/MacOSAdapter.cpp +++ b/src/MacOSAdapter.cpp @@ -34,7 +34,7 @@ bool MacOSAdapter::connect(uint16_t vid, uint16_t pid) { io_iterator_t iter; // kIOMasterPortDefault is deprecated but widely used. NULL is also acceptable. - kern_return_t kr = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &iter); + kern_return_t kr = IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iter); if (kr != KERN_SUCCESS) { dprintf("MacOSAdapter::connect() - Failed to get matching services.\n"); return false; diff --git a/src/main.cpp b/src/main.cpp index af58b09..d50b53f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -259,8 +259,10 @@ int main(int argc, char *argv[]) { } P2ProFrame frame; + static int readFailCount = 0; if (cameraConnected) { if (camera.get_frame(frame)) { + readFailCount = 0; hs = detectHotSpot(frame, hs.found); tracker.update(hs, frame); @@ -306,14 +308,18 @@ int main(int argc, char *argv[]) { recorder.writeFrame(recordingBuffer.data()); } } else { - dprintf("Camera disconnected!\n"); - cameraConnected = false; - camera.disconnect(); - if (recorder.isRecording()) { - dprintf("Stopping recording due to disconnection.\n"); - recorder.stop(); + readFailCount++; + if (readFailCount > 50) { // 50 attempts @ ~10ms = ~500ms + dprintf("Camera disconnected!\n"); + cameraConnected = false; + camera.disconnect(); + if (recorder.isRecording()) { + dprintf("Stopping recording due to disconnection.\n"); + recorder.stop(); + } + hs.found = false; + readFailCount = 0; } - hs.found = false; } }