diff --git a/.github/scripts/install-ndi-linux.sh b/.github/scripts/install-ndi-linux.sh new file mode 100755 index 0000000..a99b015 --- /dev/null +++ b/.github/scripts/install-ndi-linux.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -z "${NDI_SDK_URL_LINUX:-}" ]]; then + echo "NDI_SDK_URL_LINUX is not set" >&2 + exit 1 +fi + +archive="$RUNNER_TEMP/ndi-sdk-linux" +extract_dir="$RUNNER_TEMP/ndi-sdk-linux-extract" +mkdir -p "$extract_dir" + +curl -L --retry 3 --retry-delay 5 "$NDI_SDK_URL_LINUX" -o "$archive" + +if file "$archive" | grep -qi 'gzip compressed'; then + tar -xzf "$archive" -C "$extract_dir" +elif file "$archive" | grep -qi 'bzip2 compressed'; then + tar -xjf "$archive" -C "$extract_dir" +elif file "$archive" | grep -qi 'Zip archive'; then + unzip -q "$archive" -d "$extract_dir" +else + chmod +x "$archive" + (cd "$extract_dir" && printf 'y\ny\n' | "$archive") +fi + +# NDI's Linux download is a .tar.gz containing a self-extracting shell installer. +# Run it non-interactively after unpacking the outer archive. +installer="$(find "$extract_dir" -maxdepth 2 -type f -name 'Install_NDI_SDK*_Linux*.sh' -print -quit || true)" +existing_header="$(find "$extract_dir" -type f -path '*/include/Processing.NDI.Lib.h' -print -quit || true)" +if [[ -n "$installer" && -z "$existing_header" ]]; then + chmod +x "$installer" + (cd "$(dirname "$installer")" && printf 'y\ny\n' | PAGER=cat ./"$(basename "$installer")") +fi + +sdk_root="" +while IFS= read -r header; do + candidate="$(dirname "$(dirname "$header")")" + first_lib="$(find "$candidate" -type f -name 'libndi.so*' -print -quit || true)" + if [[ -n "$first_lib" ]]; then + sdk_root="$candidate" + break + fi +done < <(find "$extract_dir" /usr/local /opt -type f -path '*/include/Processing.NDI.Lib.h' 2>/dev/null || true) + +if [[ -z "$sdk_root" ]]; then + echo "Could not locate Processing.NDI.Lib.h and libndi.so after extracting Linux NDI SDK" >&2 + find "$extract_dir" -maxdepth 4 -type f | sort >&2 || true + exit 1 +fi + +# Normalize into the project-local layout CMake searches first. Preserve symlinks +# so versioned libndi.so chains remain intact. +case "$(uname -m)" in + x86_64|amd64) ndi_arch_dir="x86_64-linux-gnu" ;; + aarch64|arm64) ndi_arch_dir="aarch64-rpi4-linux-gnueabi" ;; + armv7l) ndi_arch_dir="arm-rpi4-linux-gnueabihf" ;; + *) ndi_arch_dir="" ;; +esac + +lib_source_dir="" +if [[ -n "$ndi_arch_dir" && -d "$sdk_root/lib/$ndi_arch_dir" ]]; then + lib_source_dir="$sdk_root/lib/$ndi_arch_dir" +elif [[ -d "$sdk_root/lib" ]]; then + lib_source_dir="$sdk_root/lib" +fi + +rm -rf ndi +mkdir -p ndi/include ndi/lib +cp -a "$sdk_root/include"/. ndi/include/ +if [[ -z "$lib_source_dir" ]]; then + echo "Could not locate an NDI library directory for $(uname -m) under $sdk_root/lib" >&2 + exit 1 +fi +cp -a "$lib_source_dir"/libndi.so* ndi/lib/ + +if [[ ! -e ndi/lib/libndi.so ]]; then + first_lib="$(find ndi/lib -type f -name 'libndi.so*' | sort | head -n1)" + [[ -n "$first_lib" ]] || { echo "No libndi.so found in normalized SDK" >&2; exit 1; } + ln -s "$(basename "$first_lib")" ndi/lib/libndi.so +fi + +echo "NDI SDK normalized to $PWD/ndi" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1468b2e..7709d08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,8 @@ on: push: branches: ["**"] tags: ["v*"] + pull_request: + branches: ["**"] workflow_dispatch: inputs: tag: @@ -265,11 +267,79 @@ jobs: path: OnPoint-*-Setup.exe if-no-files-found: error + # ───────────────────────────────────────────────────────────────────────────── + build-linux: + name: Linux x64 + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + curl \ + file \ + unzip \ + libgl1-mesa-dev \ + libxkbcommon-dev \ + libxcb-cursor0 \ + libxcb-cursor-dev \ + libopencv-dev \ + qt6-base-dev \ + qt6-base-dev-tools \ + qt6-multimedia-dev \ + libqt6svg6-dev \ + libavahi-compat-libdnssd-dev + + - name: Install NDI SDK + env: + NDI_SDK_URL_LINUX: ${{ secrets.NDI_SDK_URL_LINUX }} + run: bash .github/scripts/install-ndi-linux.sh + + - name: Configure + run: | + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$PWD/package/OnPoint-Linux-x86_64" + + - name: Build + run: cmake --build build --parallel $(nproc) + + - name: Package + run: | + mkdir -p package/OnPoint-Linux-x86_64/bin package/OnPoint-Linux-x86_64/lib package/OnPoint-Linux-x86_64/share/doc/onpoint + cp build/onpoint package/OnPoint-Linux-x86_64/bin/ + cp -a ndi/lib/libndi.so* package/OnPoint-Linux-x86_64/lib/ + cp README.md package/OnPoint-Linux-x86_64/share/doc/onpoint/ 2>/dev/null || true + { + echo '#!/usr/bin/env bash' + echo 'set -euo pipefail' + echo 'APPDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' + echo 'export LD_LIBRARY_PATH="$APPDIR/lib:${LD_LIBRARY_PATH:-}"' + echo 'exec "$APPDIR/bin/onpoint" "$@"' + } > package/OnPoint-Linux-x86_64/run-onpoint.sh + chmod +x package/OnPoint-Linux-x86_64/run-onpoint.sh + tar -C package -czf OnPoint-Linux-x86_64.tar.gz OnPoint-Linux-x86_64 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: OnPoint-Linux-x86_64 + path: OnPoint-Linux-x86_64.tar.gz + if-no-files-found: error + # ───────────────────────────────────────────────────────────────────────────── release: name: GitHub Release if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' - needs: [build-macos, build-windows] + needs: [build-macos, build-windows, build-linux] runs-on: ubuntu-latest env: TAG_NAME: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} @@ -287,3 +357,4 @@ jobs: files: | OnPoint.dmg OnPoint-*-Setup.exe + OnPoint-Linux-x86_64.tar.gz diff --git a/CMakeLists.txt b/CMakeLists.txt index e55e5b7..36ed545 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,11 +109,20 @@ if(WIN32) "C:/Program Files (x86)/NDI/NDI Advanced SDK 6" "$ENV{NDI_SDK_DIR}" ) -else() +elseif(APPLE) set(NDI_SEARCH_PATHS "${CMAKE_SOURCE_DIR}/ndi" "/Library/NDI SDK for Apple" "/usr/local" + "$ENV{NDI_SDK_DIR}" + ) +else() + set(NDI_SEARCH_PATHS + "${CMAKE_SOURCE_DIR}/ndi" + "/opt/ndi" + "/usr/local" + "/usr" + "$ENV{NDI_SDK_DIR}" ) endif() @@ -131,12 +140,23 @@ foreach(NDI_BASE ${NDI_SEARCH_PATHS}) break() endif() endforeach() - else() + elseif(APPLE) if(EXISTS "${NDI_BASE}/lib/macOS/libndi.dylib") set(NDI_LIB "${NDI_BASE}/lib/macOS/libndi.dylib") elseif(EXISTS "${NDI_BASE}/lib/libndi.dylib") set(NDI_LIB "${NDI_BASE}/lib/libndi.dylib") endif() + else() + foreach(_lib_path + "${NDI_BASE}/lib/x86_64-linux-gnu/libndi.so" + "${NDI_BASE}/lib/x64/libndi.so" + "${NDI_BASE}/lib/libndi.so" + "${NDI_BASE}/lib64/libndi.so") + if(EXISTS "${_lib_path}") + set(NDI_LIB "${_lib_path}") + break() + endif() + endforeach() endif() if(NDI_LIB) set(NDI_FOUND TRUE) @@ -198,11 +218,16 @@ else() "Install the NDI 6 SDK for Windows from https://ndi.video/download-ndi-sdk/ " "or place the SDK directory under ${CMAKE_SOURCE_DIR}/ndi/.\n" "You can also set the NDI_SDK_DIR environment variable to the SDK root.") - else() + elseif(APPLE) message(FATAL_ERROR "NDI SDK not found. " "Install 'NDI SDK for Apple' (6.x) so CMake can find " "Processing.NDI.Lib.h and libndi.dylib.") + else() + message(FATAL_ERROR + "NDI SDK not found. Install the NDI SDK for Linux so CMake can find " + "Processing.NDI.Lib.h and libndi.so, place it under ${CMAKE_SOURCE_DIR}/ndi/, " + "or set NDI_SDK_DIR to the SDK root.") endif() endif() @@ -268,6 +293,25 @@ if(APPLE) endif() +# ── Linux-specific ──────────────────────────────────────────────────────────── +if(UNIX AND NOT APPLE) + find_path(DNSSD_INCLUDE_DIR dns_sd.h) + find_library(DNSSD_LIB dns_sd) + if(NOT DNSSD_INCLUDE_DIR OR NOT DNSSD_LIB) + message(FATAL_ERROR + "DNS-SD compatibility library not found. Install libavahi-compat-libdnssd-dev " + "(Debian/Ubuntu) or the equivalent package for your distribution.") + endif() + + target_sources(onpoint PRIVATE src/DnsSdBridge_win.cpp) + target_include_directories(onpoint PRIVATE "${DNSSD_INCLUDE_DIR}") + target_link_libraries(onpoint PRIVATE "${DNSSD_LIB}") + target_compile_definitions(onpoint PRIVATE DNSSD_AVAILABLE=1) + message(STATUS "Found DNS-SD compatibility library — session discovery enabled") + + install(TARGETS onpoint RUNTIME DESTINATION bin) +endif() + # ── Windows-specific ────────────────────────────────────────────────────────── if(WIN32) # Use the Windows GUI subsystem (no console window) diff --git a/src/DnsSdBridge_win.cpp b/src/DnsSdBridge_win.cpp index 53ececb..a9e418e 100644 --- a/src/DnsSdBridge_win.cpp +++ b/src/DnsSdBridge_win.cpp @@ -3,7 +3,11 @@ #if defined(DNSSD_AVAILABLE) && DNSSD_AVAILABLE -#include +#ifdef _WIN32 +# include +#else +# include +#endif #include #include #include @@ -201,8 +205,8 @@ DnsSdBridge::~DnsSdBridge() {} void DnsSdBridge::advertise(const QString&, quint16) { QTimer::singleShot(0, this, [this]() { emit advertiseError( - QStringLiteral("Session discovery unavailable: " - "install Apple Bonjour for Windows")); + QStringLiteral("Session discovery unavailable: install a DNS-SD compatibility library " + "(Bonjour on Windows or Avahi libdns_sd on Linux)")); }); } void DnsSdBridge::stopAdvertising() {} diff --git a/src/ui/StreamSourcePanel.cpp b/src/ui/StreamSourcePanel.cpp index 288127d..0bf14ea 100644 --- a/src/ui/StreamSourcePanel.cpp +++ b/src/ui/StreamSourcePanel.cpp @@ -511,7 +511,6 @@ class DecklinkSourceTab : public VideoSourceTab { bool settingSource_ = false; #else void refreshSources() override {} - QString selectedSource() const override { return {}; } void setCurrentSource(const QString&) override {} #endif };