cmake_minimum_required(VERSION 3.16) project(fpv_relay_cpp LANGUAGES C CXX) # devourer(用户态 USB 驱动) 需要 C++20;本项目其余代码向后兼容,统一用 C++20。 set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD 11) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) endif() # ---------------- 选项 ---------------- option(FPC_BUILD_WFB_CAPTURE "编译 wfb-ng pcap 采集模块(仅 Linux)" ON) if(WIN32) set(_fpc_usb_default ON) else() set(_fpc_usb_default OFF) endif() option(FPC_BUILD_WFB_USB "编译 devourer(USB) 无线采集/上行" ${_fpc_usb_default}) option(FPC_BUILD_GUI "编译 Qt 图形界面" OFF) # Qt 安装前缀:由 CMakePresets.json 的 FPC_QT_PREFIX 指定(也可用 -D 覆盖)。 set(FPC_QT_PREFIX "" CACHE PATH "Qt 安装前缀") set(FPC_DEVOURER_GIT_TAG "30d248e021f35be0a8fe85c84939a57a45756901" CACHE STRING "devourer 固定 commit") # 本地 devourer 源码目录(可选):设了就用本地,否则联网 FetchContent 拉取。 # 也支持把源码放到 3rd/devourer(存在即自动使用,免联网)。 set(FPC_DEVOURER_SRC "" CACHE PATH "本地 devourer 源码目录(可选)") # 编译产物输出目录:所有可执行文件/运行库/示例配置都汇总到这里 set(FPC_DIST_DIR "${CMAKE_BINARY_DIR}/dist" CACHE PATH "编译产物输出目录") # Windows 运行期依赖(libusb/libsodium 的 bin/include/lib)与 ffmpeg.exe(预览解码)。 # 默认使用仓库内 vendored 的 3rd/win-deps 与 3rd/ffmpeg;也可用环境变量 # FPV_DEPS_PREFIX / FFMPEG_EXE 或 -D 覆盖。 set(FPC_DEPS_PREFIX "$ENV{FPV_DEPS_PREFIX}" CACHE PATH "libusb/libsodium 前缀(Windows)") if(NOT FPC_DEPS_PREFIX AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rd/win-deps/include/libusb-1.0/libusb.h") set(FPC_DEPS_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/3rd/win-deps" CACHE PATH "libusb/libsodium 前缀(Windows)" FORCE) endif() set(FPC_FFMPEG_EXE "$ENV{FFMPEG_EXE}" CACHE FILEPATH "ffmpeg.exe 路径(视频预览解码)") if(NOT FPC_FFMPEG_EXE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rd/ffmpeg/ffmpeg.exe") set(FPC_FFMPEG_EXE "${CMAKE_CURRENT_SOURCE_DIR}/3rd/ffmpeg/ffmpeg.exe" CACHE FILEPATH "ffmpeg.exe 路径(视频预览解码)" FORCE) endif() # ---------------- 第三方库 ---------------- set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE) set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE) set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE) add_subdirectory(3rd/yaml-cpp EXCLUDE_FROM_ALL) set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_BENCH OFF CACHE BOOL "" FORCE) add_subdirectory(3rd/spdlog EXCLUDE_FROM_ALL) add_library(cli11 INTERFACE) target_include_directories(cli11 INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/3rd/CLI11/include) # ---------------- devourer(用户态 USB 无线驱动,仅 Jaguar1=RTL8812AU) ---------------- set(FPC_HAVE_WFB_USB OFF) if(FPC_BUILD_WFB_USB) # 定位 libusb / libsodium。 # Windows 上不依赖 pkg-config:直接从 FPC_DEPS_PREFIX 手动查找, # 并提供一个 PkgConfig::libusb 导入目标供 devourer 的 CMake 复用。 if(WIN32 AND FPC_DEPS_PREFIX) find_path(FPC_LIBUSB_INCLUDE_DIR libusb-1.0/libusb.h HINTS "${FPC_DEPS_PREFIX}/include") find_library(FPC_LIBUSB_LIBRARY NAMES usb-1.0 libusb-1.0 HINTS "${FPC_DEPS_PREFIX}/lib") find_path(FPC_SODIUM_INCLUDE_DIR sodium.h HINTS "${FPC_DEPS_PREFIX}/include") find_library(FPC_SODIUM_LIBRARY NAMES sodium libsodium HINTS "${FPC_DEPS_PREFIX}/lib") if(NOT FPC_LIBUSB_LIBRARY OR NOT FPC_SODIUM_LIBRARY) message(FATAL_ERROR "未在 FPC_DEPS_PREFIX=${FPC_DEPS_PREFIX} 找到 libusb/libsodium。" "请设置环境变量 FPV_DEPS_PREFIX 指向含 include/lib/bin 的前缀目录。") endif() add_library(PkgConfig::libusb INTERFACE IMPORTED) target_include_directories(PkgConfig::libusb INTERFACE "${FPC_LIBUSB_INCLUDE_DIR}") target_link_libraries(PkgConfig::libusb INTERFACE "${FPC_LIBUSB_LIBRARY}") set(SODIUM_INCLUDE_DIRS "${FPC_SODIUM_INCLUDE_DIR}") set(SODIUM_LIBRARIES "${FPC_SODIUM_LIBRARY}") set(SODIUM_LIBRARY_DIRS "") else() find_package(PkgConfig REQUIRED) pkg_check_modules(LIBUSB REQUIRED IMPORTED_TARGET libusb-1.0) pkg_check_modules(SODIUM REQUIRED libsodium) endif() # 只编 RTL8812AU/8811AU/8821AU(Jaguar1),其余后端关闭以缩小体积与依赖。 set(DEVOURER_JAGUAR1 ON CACHE BOOL "" FORCE) set(DEVOURER_8814 OFF CACHE BOOL "" FORCE) set(DEVOURER_JAGUAR2_8822B OFF CACHE BOOL "" FORCE) set(DEVOURER_JAGUAR2_8821C OFF CACHE BOOL "" FORCE) set(DEVOURER_JAGUAR3_8822C OFF CACHE BOOL "" FORCE) set(DEVOURER_JAGUAR3_8822E OFF CACHE BOOL "" FORCE) set(DEVOURER_8733B OFF CACHE BOOL "" FORCE) set(DEVOURER_KESTREL_8852B OFF CACHE BOOL "" FORCE) set(DEVOURER_KESTREL_8852C OFF CACHE BOOL "" FORCE) set(DEVOURER_MT7612U OFF CACHE BOOL "" FORCE) set(DEVOURER_PCIE OFF CACHE BOOL "" FORCE) include(FetchContent) # CMake 4.x:允许 FetchContent_Populate(不把 devourer 的示例/测试加进默认构建) if(POLICY CMP0169) cmake_policy(SET CMP0169 OLD) endif() # 1) 本地源码优先:FPC_DEVOURER_SRC,或仓库内 3rd/devourer if(NOT FPC_DEVOURER_SRC AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rd/devourer/CMakeLists.txt") set(FPC_DEVOURER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/3rd/devourer") endif() # 2) 清掉失效的 FETCHCONTENT_SOURCE_DIR_DEVOURER(指向已删除的目录会让 configure 报错) if(DEFINED FETCHCONTENT_SOURCE_DIR_DEVOURER AND NOT EXISTS "${FETCHCONTENT_SOURCE_DIR_DEVOURER}") unset(FETCHCONTENT_SOURCE_DIR_DEVOURER CACHE) endif() if(FPC_DEVOURER_SRC AND EXISTS "${FPC_DEVOURER_SRC}/CMakeLists.txt") message(STATUS "使用本地 devourer: ${FPC_DEVOURER_SRC}") add_subdirectory("${FPC_DEVOURER_SRC}" "${CMAKE_BINARY_DIR}/devourer-build" EXCLUDE_FROM_ALL) else() FetchContent_Declare(devourer GIT_REPOSITORY https://github.com/openipc/devourer.git GIT_TAG ${FPC_DEVOURER_GIT_TAG} GIT_SHALLOW TRUE) FetchContent_GetProperties(devourer) if(NOT devourer_POPULATED) FetchContent_Populate(devourer) endif() add_subdirectory(${devourer_SOURCE_DIR} ${devourer_BINARY_DIR} EXCLUDE_FROM_ALL) endif() set(FPC_HAVE_WFB_USB ON) endif() # ---------------- wfb-ng 静态库(FEC/解密/加密) ---------------- # Linux pcap 采集版:含 tx.cpp(AF_PACKET 注入)。 if(FPC_BUILD_WFB_CAPTURE AND UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) pkg_check_modules(SODIUM REQUIRED libsodium) pkg_check_modules(PCAP REQUIRED libpcap) add_library(wfbng STATIC 3rd/wfb-ng/src/rx.cpp 3rd/wfb-ng/src/tx.cpp 3rd/wfb-ng/src/wifibroadcast.cpp 3rd/wfb-ng/src/radiotap.c 3rd/wfb-ng/src/zfex.c ) target_compile_definitions(wfbng PRIVATE __WFB_RX_SHARED_LIBRARY__ FPC_WFB_TX_NO_MAIN) target_include_directories(wfbng PUBLIC 3rd/wfb-ng/src ${SODIUM_INCLUDE_DIRS} ${PCAP_INCLUDE_DIRS} ) target_link_libraries(wfbng PUBLIC ${SODIUM_LIBRARIES} ${PCAP_LIBRARIES}) # wfb-ng 对报文做了大量指针类型转换,必须关闭严格别名优化,否则解密会失败 target_compile_options(wfbng PRIVATE -w -fno-strict-aliasing) set(FPC_HAVE_WFB_CAPTURE ON) endif() # Windows(或仅 USB)版:只编 Aggregator,不含 pcap Receiver / AF_PACKET tx。 if(FPC_HAVE_WFB_USB AND NOT TARGET wfbng) add_library(wfbng STATIC 3rd/wfb-ng/src/rx.cpp 3rd/wfb-ng/src/wifibroadcast.cpp 3rd/wfb-ng/src/radiotap.c 3rd/wfb-ng/src/zfex.c ) target_compile_definitions(wfbng PRIVATE __WFB_RX_SHARED_LIBRARY__) target_include_directories(wfbng PUBLIC 3rd/wfb-ng/src ${SODIUM_INCLUDE_DIRS}) target_link_directories(wfbng PUBLIC ${SODIUM_LIBRARY_DIRS}) target_link_libraries(wfbng PUBLIC ${SODIUM_LIBRARIES}) if(NOT MSVC) target_compile_options(wfbng PRIVATE -w -fno-strict-aliasing) endif() endif() # ---------------- 核心库(采集/转发/录制/RTSP/状态) ---------------- set(FPC_CORE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/capture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/radio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/udp_capture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/wfb_uplink.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/config/config.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/relay/dvr_sink.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/relay/h265.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/relay/stream.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/relay/udp_sink.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/rtsp/rtsp_server.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/status/http_server.cpp ) # Linux pcap 采集(wfb_capture.cpp 依赖 pcap/radiotap)。 if(FPC_HAVE_WFB_CAPTURE AND NOT WIN32) list(APPEND FPC_CORE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/wfb_capture.cpp) endif() # USB(devourer) 采集 + 上行。 if(FPC_HAVE_WFB_USB) list(APPEND FPC_CORE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/wfb_usb_device.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/wfb_usb_capture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/capture/wfb_usb_tx.cpp ) endif() add_library(fpv_core STATIC ${FPC_CORE_SRC}) target_include_directories(fpv_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) if(TARGET yaml-cpp::yaml-cpp) set(FPC_YAML_TARGET yaml-cpp::yaml-cpp) else() set(FPC_YAML_TARGET yaml-cpp) endif() find_package(Threads REQUIRED) target_link_libraries(fpv_core PUBLIC ${FPC_YAML_TARGET} spdlog::spdlog Threads::Threads) if(FPC_HAVE_WFB_CAPTURE) target_compile_definitions(fpv_core PUBLIC FPC_HAVE_WFB_CAPTURE=1) target_link_libraries(fpv_core PUBLIC wfbng) endif() if(FPC_HAVE_WFB_USB) target_compile_definitions(fpv_core PUBLIC FPC_HAVE_WFB_USB=1) target_link_libraries(fpv_core PUBLIC wfbng devourer) endif() # Windows 平台:统一编译宏 + 链接 Winsock。 if(WIN32) target_compile_definitions(fpv_core PUBLIC WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0601 _CRT_SECURE_NO_WARNINGS ) target_link_libraries(fpv_core PUBLIC ws2_32) # 强制 winsock2.h 先于 windows.h(spdlog/Qt/devourer 会间接包含 windows.h)。 if(MSVC) target_compile_options(fpv_core PUBLIC /FIwinsock2.h) else() target_compile_options(fpv_core PUBLIC -include winsock2.h) endif() endif() # ============================================================================ # 编译产物汇总到 FPC_DIST_DIR:可执行文件 + 运行期依赖 + 示例配置 + 密钥 # ============================================================================ set(FPC_STAGE_CMDS COMMAND ${CMAKE_COMMAND} -E make_directory "${FPC_DIST_DIR}" ) # 示例配置与密钥 foreach(_f config.example.yaml config.windows.example.yaml) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${_f}") list(APPEND FPC_STAGE_CMDS COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${_f}" "${FPC_DIST_DIR}/") endif() endforeach() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ass/gs.key") list(APPEND FPC_STAGE_CMDS COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/ass/gs.key" "${FPC_DIST_DIR}/gs.key") endif() # 本地 config.yaml(若存在,未被 git 跟踪)也一并带到 dist,方便直接用 if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/config.yaml") list(APPEND FPC_STAGE_CMDS COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/config.yaml" "${FPC_DIST_DIR}/config.yaml") endif() # Windows 运行期依赖 DLL(libusb/libsodium/ffmpeg) if(WIN32) set(_fpc_dep_dlls) if(FPC_DEPS_PREFIX) file(GLOB _fpc_dep_dlls "${FPC_DEPS_PREFIX}/bin/libusb*.dll" "${FPC_DEPS_PREFIX}/bin/libsodium*.dll") endif() if(FPC_FFMPEG_EXE AND EXISTS "${FPC_FFMPEG_EXE}") list(APPEND _fpc_dep_dlls "${FPC_FFMPEG_EXE}") endif() if(_fpc_dep_dlls) list(APPEND FPC_STAGE_CMDS COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_fpc_dep_dlls} "${FPC_DIST_DIR}/") endif() endif() # ---------------- 命令行程序 ---------------- add_executable(fpv-relay src/main.cpp) target_link_libraries(fpv-relay PRIVATE fpv_core cli11) # 可执行文件直接输出到 dist,和运行期依赖放在一起(IDE 直接运行也能找到 DLL) set_target_properties(fpv-relay PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${FPC_DIST_DIR}") add_custom_command(TARGET fpv-relay POST_BUILD ${FPC_STAGE_CMDS}) # ---------------- Qt 图形界面(可选) ---------------- if(FPC_BUILD_GUI) list(APPEND CMAKE_PREFIX_PATH ${FPC_QT_PREFIX}) find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2 Svg Widgets Network PrintSupport) qt_standard_project_setup() set(FLUENTUI_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(FLUENTUI_BUILD_STATIC_LIB ON CACHE BOOL "" FORCE) add_subdirectory(3rd/FluentUI EXCLUDE_FROM_ALL) # 视频预览不再依赖 GStreamer:运行时调用随程序分发的 ffmpeg.exe 解码 # (ffmpeg 能解 H264/H265,单文件自包含)。见 src/gui/video_item.cpp。 qt_add_executable(fpv-relay-gui src/gui/main_gui.cpp src/gui/backend.cpp src/gui/video_item.cpp ) # 可执行文件直接输出到 dist,和 Qt/ffmpeg/libusb 等运行库放在一起 set_target_properties(fpv-relay-gui PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${FPC_DIST_DIR}") target_link_libraries(fpv-relay-gui PRIVATE fpv_core fluentuiplugin Qt6::Quick Qt6::QuickControls2 Qt6::Svg Qt6::Widgets Qt6::Network Qt6::PrintSupport ) target_compile_definitions(fpv-relay-gui PRIVATE FLUENTUI_BUILD_STATIC_LIB) qt_add_qml_module(fpv-relay-gui URI FpvRelay VERSION 1.0 QML_FILES src/gui/qml/App.qml src/gui/qml/DroneCard.qml src/gui/qml/Settings.qml ) # GUI 产物汇总(exe 与 FpvRelay QML 模块已随 RUNTIME_OUTPUT_DIRECTORY 落到 dist) set(FPC_GUI_STAGE) if(WIN32) find_program(FPC_WINDEPLOYQT windeployqt HINTS "${FPC_QT_PREFIX}/bin") if(FPC_WINDEPLOYQT) list(APPEND FPC_GUI_STAGE COMMAND "${FPC_WINDEPLOYQT}" --release --qmldir "${CMAKE_CURRENT_SOURCE_DIR}/src/gui" --dir "${FPC_DIST_DIR}" "${FPC_DIST_DIR}/fpv-relay-gui.exe") endif() # FluentUI 依赖的 Qt5Compat.GraphicalEffects + Qt6ShaderTools if(EXISTS "${FPC_QT_PREFIX}/qml/Qt5Compat") list(APPEND FPC_GUI_STAGE COMMAND ${CMAKE_COMMAND} -E make_directory "${FPC_DIST_DIR}/qml" COMMAND ${CMAKE_COMMAND} -E copy_directory "${FPC_QT_PREFIX}/qml/Qt5Compat" "${FPC_DIST_DIR}/qml/Qt5Compat") endif() if(EXISTS "${FPC_QT_PREFIX}/bin/Qt6ShaderTools.dll") list(APPEND FPC_GUI_STAGE COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FPC_QT_PREFIX}/bin/Qt6ShaderTools.dll" "${FPC_DIST_DIR}/") endif() # FpvRelay QML 模块目录(生成在构建目录,拷贝到 dist) # qt.conf:让 Qt 从程序目录找 QML file(WRITE "${CMAKE_BINARY_DIR}/fpc_qt.conf" "[Paths]\nPrefix=.\nQmlImportsPath=qml\n") list(APPEND FPC_GUI_STAGE COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/fpc_qt.conf" "${FPC_DIST_DIR}/qt.conf") # windeployqt 拷的是 Qt 自带的 MinGW 运行库;若编译器版本不同,用编译器自带的覆盖。 if(MINGW) get_filename_component(_fpc_mingw_bin "${CMAKE_CXX_COMPILER}" DIRECTORY) foreach(_rt libstdc++-6.dll libgcc_s_seh-1.dll libwinpthread-1.dll) if(EXISTS "${_fpc_mingw_bin}/${_rt}") list(APPEND FPC_GUI_STAGE COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_fpc_mingw_bin}/${_rt}" "${FPC_DIST_DIR}/") endif() endforeach() endif() endif() # 所有平台都把 FpvRelay QML 模块目录拷到 dist,保证从 dist 直接运行能找到模块 list(APPEND FPC_GUI_STAGE COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_BINARY_DIR}/FpvRelay" "${FPC_DIST_DIR}/FpvRelay") # 依赖 DLL / 配置 / 密钥的汇总也挂到 GUI 目标,避免只构建 GUI 时缺失 add_custom_command(TARGET fpv-relay-gui POST_BUILD ${FPC_STAGE_CMDS} ${FPC_GUI_STAGE}) endif()