Repository: barry-ran/QtScrcpy Branch: dev Commit: 7b8a9580a698 Files: 93 Total size: 388.8 KB Directory structure: gitextract_v8zjkv6h/ ├── .clang-format ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── macos.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── QtScrcpy/ │ ├── CMakeLists.txt │ ├── appversion │ ├── audio/ │ │ ├── audiooutput.cpp │ │ └── audiooutput.h │ ├── clang-format-all.sh │ ├── fontawesome/ │ │ ├── iconhelper.cpp │ │ └── iconhelper.h │ ├── groupcontroller/ │ │ ├── groupcontroller.cpp │ │ └── groupcontroller.h │ ├── main.cpp │ ├── render/ │ │ ├── qyuvopenglwidget.cpp │ │ └── qyuvopenglwidget.h │ ├── res/ │ │ ├── Info_Mac.plist.in │ │ ├── QtScrcpy.icns │ │ ├── QtScrcpy.rc │ │ ├── i18n/ │ │ │ ├── CMakeLists.txt │ │ │ ├── en_US.qm │ │ │ ├── en_US.ts │ │ │ ├── ja_JP.qm │ │ │ ├── ja_JP.ts │ │ │ ├── ko_KR.qm │ │ │ ├── ko_KR.ts │ │ │ ├── zh_CN.qm │ │ │ └── zh_CN.ts │ │ ├── qss/ │ │ │ └── psblack.css │ │ └── res.qrc │ ├── sndcpy/ │ │ ├── sndcpy.apk │ │ ├── sndcpy.bat │ │ └── sndcpy.sh │ ├── ui/ │ │ ├── dialog.cpp │ │ ├── dialog.h │ │ ├── dialog.ui │ │ ├── toolform.cpp │ │ ├── toolform.h │ │ ├── toolform.ui │ │ ├── videoform.cpp │ │ ├── videoform.h │ │ └── videoform.ui │ ├── uibase/ │ │ ├── keepratiowidget.cpp │ │ ├── keepratiowidget.h │ │ ├── magneticwidget.cpp │ │ └── magneticwidget.h │ └── util/ │ ├── config.cpp │ ├── config.h │ ├── mousetap/ │ │ ├── cocoamousetap.h │ │ ├── cocoamousetap.mm │ │ ├── mousetap.cpp │ │ ├── mousetap.h │ │ ├── winmousetap.cpp │ │ ├── winmousetap.h │ │ ├── xmousetap.cpp │ │ └── xmousetap.h │ ├── path.h │ ├── path.mm │ ├── winutils.cpp │ └── winutils.h ├── README.md ├── README_zh.md ├── backup/ │ └── myconfig.sh ├── ci/ │ ├── generate-version.py │ ├── linux/ │ │ ├── build_for_linux.sh │ │ ├── package_appimage.sh │ │ └── publish_for_ubuntu.sh.todo │ ├── lrelease.sh │ ├── lupdate.sh │ ├── mac/ │ │ ├── build_for_mac.sh │ │ ├── package/ │ │ │ ├── dmg-settings.json │ │ │ ├── package.py │ │ │ └── requirements.txt │ │ ├── package_for_mac.sh │ │ └── publish_for_mac.sh │ └── win/ │ ├── build_for_win.bat │ └── publish_for_win.bat ├── config/ │ └── config.ini ├── docs/ │ ├── DEVELOP.md │ ├── FAQ.md │ ├── KeyMapDes.md │ ├── KeyMapDes_zh.md │ └── TODO.md └── keymap/ ├── FRAG.json ├── gameforpeace.json ├── identityv.json ├── test.json └── tiktok.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- # 语言: None, Cpp, Java, JavaScript, ObjC, Proto, TableGen, TextProto Language: Cpp # BasedOnStyle: WebKit # 访问说明符(public、private等)的偏移 AccessModifierOffset: -4 # 开括号(开圆括号、开尖括号、开方括号)后的对齐: Align, DontAlign, AlwaysBreak(总是在开括号后换行) AlignAfterOpenBracket: AlwaysBreak # 连续赋值时,对齐所有等号 AlignConsecutiveAssignments: false # 连续声明时,对齐所有声明的变量名 AlignConsecutiveDeclarations: false # 左对齐逃脱换行(使用反斜杠换行)的反斜杠 AlignEscapedNewlines: Right # 水平对齐二元和三元表达式的操作数 AlignOperands: true # 对齐连续的尾随的注释 AlignTrailingComments: true # 允许函数声明的所有参数在放在下一行 AllowAllParametersOfDeclarationOnNextLine: false # 允许短的块放在同一行 AllowShortBlocksOnASingleLine: false # 允许短的case标签放在同一行 AllowShortCaseLabelsOnASingleLine: false # 允许短的函数放在同一行: None, InlineOnly(定义在类中), Empty(空函数), Inline(定义在类中,空函数), All AllowShortFunctionsOnASingleLine: Empty # 允许短的if语句保持在同一行 AllowShortIfStatementsOnASingleLine: false # 允许短的循环保持在同一行 AllowShortLoopsOnASingleLine: false # 总是在定义返回类型后换行(deprecated) AlwaysBreakAfterDefinitionReturnType: None # 总是在返回类型后换行: None, All, TopLevel(顶级函数,不包括在类中的函数), # AllDefinitions(所有的定义,不包括声明), TopLevelDefinitions(所有的顶级函数的定义) AlwaysBreakAfterReturnType: None # 总是在多行string字面量前换行 AlwaysBreakBeforeMultilineStrings: false # 总是在template声明后换行 AlwaysBreakTemplateDeclarations: true # false表示函数实参要么都在同一行,要么都各自一行 BinPackArguments: false # false表示所有形参要么都在同一行,要么都各自一行 BinPackParameters: false # 在大括号前换行: Attach(始终将大括号附加到周围的上下文), Linux(除函数、命名空间和类定义,与Attach类似), # Mozilla(除枚举、函数、记录定义,与Attach类似), Stroustrup(除函数定义、catch、else,与Attach类似), # Allman(总是在大括号前换行), GNU(总是在大括号前换行,并对于控制语句的大括号增加额外的缩进), WebKit(在函数前换行), Custom # 注:这里认为语句块也属于函数 BreakBeforeBraces: Custom # 大括号换行,只有当BreakBeforeBraces设置为Custom时才有效 BraceWrapping: # class定义后面 AfterClass: true # 控制语句后面 AfterControlStatement: false # enum定义后面 AfterEnum: true # 函数定义后面 AfterFunction: true # 命名空间定义后面 AfterNamespace: true # ObjC定义后面 AfterObjCDeclaration: false # struct定义后面 AfterStruct: true # union定义后面 AfterUnion: true # extern 定义后面 AfterExternBlock: true # catch之前 BeforeCatch: false # else 之前 BeforeElse: false # 缩进大括号 IndentBraces: false # 在二元运算符前换行: None(在操作符后换行), NonAssignment(在非赋值的操作符前换行), All(在操作符前换行) BreakBeforeBinaryOperators: All # 继承列表的逗号前换行 BreakBeforeInheritanceComma: true # 继承列表换行 #BreakInheritanceList: BeforeColon # 在三元运算符前换行 BreakBeforeTernaryOperators: true # 在构造函数的初始化列表的逗号前换行 BreakConstructorInitializersBeforeComma: true # 初始化列表前换行 BreakConstructorInitializers: BeforeComma # Java注解后换行 BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true # 每行字符的限制,0表示没有限制 ColumnLimit: 160 # 描述具有特殊意义的注释的正则表达式,它不应该被分割为多行或以其它方式改变 CommentPragmas: '^ IWYU pragma:' # 紧凑 命名空间 CompactNamespaces: false # 构造函数的初始化列表要么都在同一行,要么都各自一行 ConstructorInitializerAllOnOneLineOrOnePerLine: true # 构造函数的初始化列表的缩进宽度 ConstructorInitializerIndentWidth: 4 # 延续的行的缩进宽度 ContinuationIndentWidth: 4 # 去除C++11的列表初始化的大括号{后和}前的空格 Cpp11BracedListStyle: false # 继承最常用的指针和引用的对齐方式 DerivePointerAlignment: false # 关闭格式化 DisableFormat: false # 自动检测函数的调用和定义是否被格式为每行一个参数(Experimental) ExperimentalAutoDetectBinPacking: false # 固定命名空间注释 FixNamespaceComments: true # 需要被解读为foreach循环而不是函数调用的宏 ForEachMacros: - foreach - Q_FOREACH - BOOST_FOREACH IncludeBlocks: Preserve # 对#include进行排序,匹配了某正则表达式的#include拥有对应的优先级,匹配不到的则默认优先级为INT_MAX(优先级越小排序越靠前), # 可以定义负数优先级从而保证某些#include永远在最前面 IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 - Regex: '^(<|"(gtest|gmock|isl|json)/)' Priority: 3 - Regex: 'stdafx\.' Priority: 1 - Regex: '.*' Priority: 1 IncludeIsMainRegex: '(Test)?$' # 缩进case标签 IndentCaseLabels: false IndentPPDirectives: None # 缩进宽度 IndentWidth: 4 # 函数返回类型换行时,缩进函数声明或函数定义的函数名 IndentWrappedFunctionNames: true JavaScriptQuotes: Leave JavaScriptWrapImports: true # 保留在块开始处的空行 KeepEmptyLinesAtTheStartOfBlocks: true # 开始一个块的宏的正则表达式 MacroBlockBegin: '' # 结束一个块的宏的正则表达式 MacroBlockEnd: '' # 连续空行的最大数量 MaxEmptyLinesToKeep: 1 # 命名空间的缩进: None, Inner(缩进嵌套的命名空间中的内容), All NamespaceIndentation: All ObjCBinPackProtocolList: Auto # 使用ObjC块时缩进宽度 ObjCBlockIndentWidth: 4 # 在ObjC的@property后添加一个空格 ObjCSpaceAfterProperty: true # 在ObjC的protocol列表前添加一个空格 ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 19 # 在一个注释中引入换行的penalty PenaltyBreakComment: 300 # 第一次在<<前换行的penalty PenaltyBreakFirstLessLess: 120 # 在一个字符串字面量中引入换行的penalty PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 # 对于每个在行字符数限制之外的字符的penalty PenaltyExcessCharacter: 1000000 # 将函数的返回类型放到它自己的行的penalty PenaltyReturnTypeOnItsOwnLine: 60 # 指针和引用的对齐: Left, Right, Middle PointerAlignment: Right #RawStringFormats: # - Delimiter: pb # Language: TextProto # BasedOnStyle: google # 允许重新排版注释 ReflowComments: false # 允许排序#include SortIncludes: true SortUsingDeclarations: true # 在C风格类型转换后添加空格 SpaceAfterCStyleCast: false # 模板关键字后面添加空格 SpaceAfterTemplateKeyword: true # 在赋值运算符之前添加空格 SpaceBeforeAssignmentOperators: true # 开圆括号之前添加一个空格: Never, ControlStatements, Always SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true # 在空的圆括号中添加空格 SpaceInEmptyParentheses: false # 在尾随的评论前添加的空格数(只适用于//) SpacesBeforeTrailingComments: 1 # 在尖括号的<后和>前添加空格 SpacesInAngles: false # 在容器(ObjC和JavaScript的数组和字典等)字面量中添加空格 SpacesInContainerLiterals: true # 在C风格类型转换的括号中添加空格 SpacesInCStyleCastParentheses: false # 在圆括号的(后和)前添加空格 SpacesInParentheses: false # 在方括号的[后和]前添加空格,lamda表达式和未指明大小的数组的声明不受影响 SpacesInSquareBrackets: false # 标准: Cpp03, Cpp11, Auto Standard: Cpp11 # tab宽度 TabWidth: 4 # 使用tab字符: Never, ForIndentation, ForContinuationAndIndentation, Always UseTab: Never ... ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: barry-ran patreon: # Replace with a single Patreon username open_collective: QtScrcpy ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ["https://paypal.me/QtScrcpy", "https://gitee.com/Barryda/MyPictureBed/blob/master/QtScrcpy/payme.md"] ================================================ FILE: .github/workflows/macos.yml ================================================ name: MacOS on: push: paths: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/macos.yml' pull_request: paths: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/macos.yml' jobs: build: name: Build # install-qt-action在arm上执行macdeployqt会报parse otool错误,所以在intel mac上执行: # 用qt6时在arm mac上编译arm和intel都没有问题 # qt5+intel mac编译intel没问题 # qt5+arm mac编译intel会报错 # https://github.com/actions/runner-images?tab=readme-ov-file#available-images runs-on: macos-13 strategy: matrix: qt-ver: [5.15.2, 6.5.3] # 配置qt-ver的额外设置qt-arch-install,build-arch include: - qt-ver: 5.15.2 qt-arch-install: clang_64 build-arch: x64 - qt-ver: 6.5.3 qt-arch-install: arm64 build-arch: arm64 env: target-name: QtScrcpy qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }} plantform-des: mac steps: - name: Cache Qt id: cache-qt uses: actions/cache@v4 with: path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }} key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }} - name: Install Qt5 if: startsWith(matrix.qt-ver, '5.') uses: jurplel/install-qt-action@v4.1.1 with: version: ${{ matrix.qt-ver }} cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Install Qt6 if: startsWith(matrix.qt-ver, '6.') uses: jurplel/install-qt-action@v4.1.1 with: version: ${{ matrix.qt-ver }} modules: qtmultimedia cached: ${{ steps.cache-qt.outputs.cache-hit }} - uses: actions/checkout@v2 with: fetch-depth: 0 submodules: 'true' ssh-key: ${{ secrets.BOT_SSH_KEY }} # 编译 - name: Build MacOS env: ENV_QT_PATH: ${{ env.qt-install-path }} run: | python ci/generate-version.py ci/mac/build_for_mac.sh RelWithDebInfo ${{ matrix.build-arch }} # 获取ref最后一个/后的内容 - name: Get the version shell: bash id: get-version # ${ GITHUB_REF/refs\/tags\// }是linux shell ${}的变量替换语法 run: echo ::set-output name=version::${GITHUB_REF##*/} # 打包 - name: Package id: package env: ENV_QT_PATH: ${{ env.qt-install-path }} publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.build-arch }}-Qt${{matrix.qt-ver}}-${{ steps.get-version.outputs.version }} run: | ci/mac/publish_for_mac.sh ../build ${{ matrix.build-arch }} ci/mac/package_for_mac.sh mv ci/build/QtScrcpy.app ci/build/${{ env.publish_name }}.app mv ci/build/QtScrcpy.dmg ci/build/${{ env.publish_name }}.dmg echo "::set-output name=package-name::${{ env.publish_name }}" - uses: actions/upload-artifact@v4 with: name: ${{ steps.package.outputs.package-name }}.zip path: ci/build/${{ steps.package.outputs.package-name }}.dmg # Upload to release - name: Upload Release if: startsWith(github.ref, 'refs/tags/') uses: svenstaro/upload-release-action@v1-release with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ci/build/${{ steps.package.outputs.package-name }}.dmg asset_name: ${{ steps.package.outputs.package-name }}.dmg tag: ${{ github.ref }} overwrite: true ================================================ FILE: .github/workflows/ubuntu.yml ================================================ name: Ubuntu on: push: paths: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/ubuntu.yml' - 'ci/linux/**' pull_request: paths: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/ubuntu.yml' - 'ci/linux/**' jobs: build: name: Build runs-on: ubuntu-22.04 container: image: ubuntu:20.04 options: --privileged strategy: matrix: qt-ver: [5.15.2] qt-arch-install: [gcc_64] gcc-arch: [x64] env: target-name: QtScrcpy qt-install-path: ${{ github.workspace }}/Qt/${{ matrix.qt-ver }} plantform-des: ubuntu DEBIAN_FRONTEND: noninteractive steps: - name: Install Git and basic dependencies run: | apt-get update apt-get install -y git ca-certificates sudo - uses: actions/checkout@v2 with: fetch-depth: 0 submodules: 'true' ssh-key: ${{ secrets.BOT_SSH_KEY }} - name: Install system dependencies run: | apt-get update apt-get install -y \ build-essential \ cmake \ libglew-dev \ libglfw3-dev \ imagemagick \ wget \ patchelf \ zip \ libxcb1-dev \ libxkbcommon-dev \ libxkbcommon-x11-dev \ libx11-dev \ libx11-xcb-dev \ libfontconfig1-dev \ libfreetype6-dev \ libxrender-dev \ libxext-dev \ gnupg \ lsb-release \ python3 \ python3-pip \ fuse \ libasound2-dev - name: Setup FUSE run: | apt-get install -y fuse if [ ! -e /dev/fuse ]; then mknod /dev/fuse c 10 229 || true chmod 666 /dev/fuse || true fi export APPIMAGE_EXTRACT_AND_RUN=1 - name: Install CMake 3.19+ run: | apt-get install -y software-properties-common apt-get update apt-get install -y cmake=3.19.* || { wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | apt-key add - apt-add-repository 'deb https://apt.kitware.com/ubuntu/ focal main' apt-get update apt-get install -y cmake } - name: Cache Qt id: cache-qt uses: actions/cache@v4 with: path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }} key: ubuntu-20.04/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }} - name: Install Qt uses: jurplel/install-qt-action@v4.1.1 with: version: ${{ matrix.qt-ver }} cache: ${{ steps.cache-qt.outputs.cache-hit }} setup-python: false - name: Build Release shell: bash env: ENV_QT_PATH: ${{ github.workspace }}/Qt/${{ matrix.qt-ver }} run: | python3 ci/generate-version.py ci/linux/build_for_linux.sh "Release" - name: Get the version shell: bash id: get-version run: echo ::set-output name=version::${GITHUB_REF##*/} - name: Package AppImage shell: bash env: ENV_QT_PATH: ${{ github.workspace }}/Qt/${{ matrix.qt-ver }} run: | chmod +x ci/linux/package_appimage.sh ci/linux/package_appimage.sh "Release" - name: Upload AppImage Artifact uses: actions/upload-artifact@v4 with: name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.gcc-arch }}-${{ steps.get-version.outputs.version }}-AppImage path: output/appimage/*.AppImage if-no-files-found: error - name: Prepare AppImage for Release if: startsWith(github.ref, 'refs/tags/') run: | APPIMAGE_FILE=$(find output/appimage -name "QtScrcpy-*.AppImage" -type f | head -n 1) if [ -z "$APPIMAGE_FILE" ] || [ ! -f "$APPIMAGE_FILE" ]; then echo "Error: AppImage file not found" exit 1 fi FINAL_NAME="${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.gcc-arch }}-${{ steps.get-version.outputs.version }}.AppImage" cp "$APPIMAGE_FILE" "$FINAL_NAME" - name: Upload AppImage to Releases if: startsWith(github.ref, 'refs/tags/') uses: svenstaro/upload-release-action@2.3.0 with: file: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.gcc-arch }}-${{ steps.get-version.outputs.version }}.AppImage asset_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.gcc-arch }}-${{ steps.get-version.outputs.version }}.AppImage repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref }} overwrite: true ================================================ FILE: .github/workflows/windows.yml ================================================ name: Windows # 触发规则详解 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#on on: push: paths: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/windows.yml' - 'ci/win**' pull_request: paths: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/windows.yml' - 'ci/win**' jobs: build: name: Build # windows-latest目前是windows server 2022 # windows server 2019安装的是vs2019,windows server 2016安装的是vs2017 # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on runs-on: windows-latest # 矩阵配置 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix strategy: matrix: qt-ver: [5.15.2] qt-arch: [win64_msvc2019_64, win32_msvc2019] # 配置qt-arch的额外设置msvc-arch,qt-arch-install include: - qt-arch: win64_msvc2019_64 msvc-arch: x64 qt-arch-install: msvc2019_64 - qt-arch: win32_msvc2019 msvc-arch: x86 qt-arch-install: msvc2019 # job env,所有steps都可以访问 # 不同级别env详解 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#env # 使用表达式语法${{}}访问上下文 https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions env: target-name: QtScrcpy vcvarsall-path: 'C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat' vcinstall-path: 'C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\VC' qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }} plantform-des: win # 步骤 steps: - name: Cache Qt id: cache-qt uses: actions/cache@v4 with: path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }} key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch }} # 安装Qt - name: Install Qt # 使用外部action。这个action专门用来安装Qt uses: jurplel/install-qt-action@v4.1.1 with: # Version of Qt to install version: ${{ matrix.qt-ver }} # Target platform for build target: desktop # Architecture for Windows/Android arch: ${{ matrix.qt-arch }} cached: ${{ steps.cache-qt.outputs.cache-hit }} # 拉取代码 - uses: actions/checkout@v2 with: fetch-depth: 0 submodules: 'true' ssh-key: ${{ secrets.BOT_SSH_KEY }} # 编译msvc - name: Build MSVC # shell介绍 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell shell: cmd env: ENV_VCVARSALL: ${{ env.vcvarsall-path }} ENV_QT_PATH: ${{ env.qt-install-path }} run: | call python ci\generate-version.py call "ci\win\build_for_win.bat" RelWithDebInfo ${{ matrix.msvc-arch }} # 获取ref最后一个/后的内容 - name: Get the version shell: bash id: get-version # ${ GITHUB_REF/refs\/tags\// }是linux shell ${}的变量替换语法 run: echo ::set-output name=version::${GITHUB_REF##*/} # tag 打包 - name: Package id: package env: ENV_VCVARSALL: ${{ env.vcvarsall-path }} ENV_VCINSTALL: ${{ env.vcinstall-path }} ENV_QT_PATH: ${{ env.qt-install-path }} publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.msvc-arch }}-${{ steps.get-version.outputs.version }} run: | cmd.exe /c ci\win\publish_for_win.bat ${{ matrix.msvc-arch }} ..\build\${{ env.publish_name }} # 打包zip Compress-Archive -Path ci\build\${{ env.publish_name }} ci\build\${{ env.publish_name }}.zip echo "::set-output name=package-name::${{ env.publish_name }}" # 上传artifacts # https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts - uses: actions/upload-artifact@v4 with: name: ${{ steps.package.outputs.package-name }}.zip path: ci\build\${{ steps.package.outputs.package-name }} # Upload to release - name: Upload Release if: startsWith(github.ref, 'refs/tags/') uses: svenstaro/upload-release-action@v1-release with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ci\build\${{ steps.package.outputs.package-name }}.zip asset_name: ${{ steps.package.outputs.package-name }}.zip tag: ${{ github.ref }} overwrite: true ================================================ FILE: .gitignore ================================================ /output *.user /QtScrcpy/*.user /server/.gradle /server/.idea /server/build /server/gradle/wrapper/gradle-wrapper.jar /server/gradle/wrapper/gradle-wrapper.properties /server/gradlew /server/gradlew.bat /server/local.properties /build/ build-* *.DS_Store userdata.ini Info_Mac.plist /ci/build_temp ================================================ FILE: .gitmodules ================================================ [submodule "QtScrcpy/QtScrcpyCore"] path = QtScrcpy/QtScrcpyCore url = git@github.com:barry-ran/QtScrcpyCore.git ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.19 FATAL_ERROR) project(all) add_subdirectory(QtScrcpy) ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright (C) 2019 Rankun Copyright (C) 2019-2025 Rankun 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. ================================================ FILE: QtScrcpy/CMakeLists.txt ================================================ # For VS2019 and Xcode 12+ support. cmake_minimum_required(VERSION 3.19 FATAL_ERROR) # # Global config # # QC is "Qt CMake" # https://www.kdab.com/wp-content/uploads/stories/QTVTC20-Using-Modern-CMake-Kevin-Funk.pdf # QC Custom config set(QC_PROJECT_NAME "QtScrcpy") # Read version numbers from file file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/appversion QC_FILE_VERSION) set(QC_PROJECT_VERSION ${QC_FILE_VERSION}) # Project declare project(${QC_PROJECT_NAME} VERSION ${QC_PROJECT_VERSION} LANGUAGES CXX) message(STATUS "[${PROJECT_NAME}] Project ${PROJECT_NAME} ${PROJECT_VERSION}") # QC define # check arch if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(QC_CPU_ARCH x64) else() set(QC_CPU_ARCH x86) endif() # MacOS if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") # mac default arch arm64 if(NOT CMAKE_OSX_ARCHITECTURES) set(CMAKE_OSX_ARCHITECTURES arm64) endif() if (CMAKE_OSX_ARCHITECTURES MATCHES "arm64") set(QC_CPU_ARCH arm64) endif() endif() message(STATUS "[${PROJECT_NAME}] CPU_ARCH:${QC_CPU_ARCH}") # CMake set set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # default RelWithDebInfo if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo) endif() message(STATUS "[${PROJECT_NAME}] BUILD_TYPE:${CMAKE_BUILD_TYPE}") # Log configuration option(ENABLE_DETAILED_LOGS "Enable detailed log output with file and line info" OFF) if(ENABLE_DETAILED_LOGS) message(STATUS "[${PROJECT_NAME}] Detailed logs enabled") else() message(STATUS "[${PROJECT_NAME}] Simple logs enabled") endif() # Compiler set message(STATUS "[${PROJECT_NAME}] C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}") if (MSVC) # FFmpeg cannot be compiled natively by MSVC version < 12.0 (2013) if(MSVC_VERSION LESS 1800) message(FATAL_ERROR "[${PROJECT_NAME}] ERROR: MSVC version is older than 12.0 (2013).") endif() message(STATUS "[${PROJECT_NAME}] Set Warnings as error") # warning level 3 and all warnings as errors add_compile_options(/W3 /WX /wd4566) # avoid warning C4819 #add_compile_options(-source-charset:utf-8) # /utf-8 will set source charset and execution charset to utf-8, so we don't need to set source-charset:utf-8 add_compile_options(/utf-8) # ensure we use minimal "windows.h" lib without the crazy min max macros add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN) # disable SAFESEH - avoid "LNK2026: module unsafe"(Qt5.15&&vs2019) add_link_options(/SAFESEH:NO) endif() if (NOT MSVC) message(STATUS "[${PROJECT_NAME}] Set warnings as error") # lots of warnings and all warnings as errors add_compile_options(-Wall -Wextra -pedantic -Werror) # disable some warning add_compile_options(-Wno-nested-anon-types -Wno-c++17-extensions -Wno-overloaded-virtual) endif() # # Qt # # Find Qt version if (NOT QT_DESIRED_VERSION) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) message(" >>> Found Qt version: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}") set(QT_DESIRED_VERSION ${QT_VERSION_MAJOR}) endif() set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(qt_required_components Widgets Network Multimedia) if (QT_DESIRED_VERSION MATCHES 6) # list(APPEND qt_required_components Core5Compat) list(APPEND qt_required_components OpenGL) list(APPEND qt_required_components OpenGLWidgets) else() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND qt_required_components X11Extras ) endif() endif() find_package(Qt${QT_DESIRED_VERSION} REQUIRED COMPONENTS ${qt_required_components}) set(LINK_LIBS Qt${QT_DESIRED_VERSION}::Widgets Qt${QT_DESIRED_VERSION}::Network Qt${QT_DESIRED_VERSION}::Multimedia ) if (QT_DESIRED_VERSION MATCHES 6) # list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::Core5Compat) list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::GuiPrivate) list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::OpenGL) list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::OpenGLWidgets) else() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::X11Extras) endif() endif() message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_DESIRED_VERSION}") # # Sources # # fontawesome set(QC_FONTAWESOME_SOURCES fontawesome/iconhelper.h fontawesome/iconhelper.cpp ) source_group(fontawesome FILES ${QC_FONTAWESOME_SOURCES}) # uibase set(QC_UIBASE_SOURCES uibase/keepratiowidget.h uibase/keepratiowidget.cpp uibase/magneticwidget.h uibase/magneticwidget.cpp ) source_group(uibase FILES ${QC_UIBASE_SOURCES}) # audio set(QC_AUDIO_SOURCES audio/audiooutput.h audio/audiooutput.cpp ) source_group(audio FILES ${QC_AUDIO_SOURCES}) # ui set(QC_UI_SOURCES ui/toolform.h ui/toolform.cpp ui/toolform.ui ui/videoform.h ui/videoform.cpp ui/videoform.ui ui/dialog.cpp ui/dialog.h ui/dialog.ui render/qyuvopenglwidget.h render/qyuvopenglwidget.cpp ) source_group(ui FILES ${QC_UI_SOURCES}) # group controller set(QC_GROUP_CONTROLLER groupcontroller/groupcontroller.h groupcontroller/groupcontroller.cpp ) source_group(groupcontroller FILES ${QC_GROUP_CONTROLLER}) # util set(QC_UTIL_SOURCES util/config.h util/config.cpp util/mousetap/mousetap.h util/mousetap/mousetap.cpp ) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES} util/mousetap/winmousetap.h util/mousetap/winmousetap.cpp util/winutils.h util/winutils.cpp ) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES} util/mousetap/xmousetap.h util/mousetap/xmousetap.cpp ) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES} util/mousetap/cocoamousetap.h util/mousetap/cocoamousetap.mm util/path.h util/path.mm ) endif() source_group(util FILES ${QC_UTIL_SOURCES}) # qrc set(QC_QRC_SOURCES "res/res.qrc") # main set(QC_MAIN_SOURCES main.cpp ${QC_QRC_SOURCES} ) # plantform file if(CMAKE_SYSTEM_NAME STREQUAL "Windows") # Define VERSION macros for .rc file add_compile_definitions( VERSION_MAJOR=${PROJECT_VERSION_MAJOR} VERSION_MINOR=${PROJECT_VERSION_MINOR} VERSION_PATCH=${PROJECT_VERSION_PATCH} VERSION_RC_STR="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" ) set(QC_PLANTFORM_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.rc" ) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") # Step 1. add icns to source file, for MACOSX_PACKAGE_LOCATION copy set(QC_PLANTFORM_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.icns" ) endif() # 翻译相关(使用shell脚本替代cmake处理翻译) # add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/res/i18n) # all sources set(QC_PROJECT_SOURCES ${QC_FONTAWESOME_SOURCES} ${QC_UIBASE_SOURCES} ${QC_UI_SOURCES} ${QC_UTIL_SOURCES} ${QC_MAIN_SOURCES} ${QC_GROUP_CONTROLLER} ${QC_PLANTFORM_SOURCES} ${QC_AUDIO_SOURCES} ) if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(QC_RUNTIME_TYPE MACOSX_BUNDLE) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(QC_RUNTIME_TYPE WIN32) endif() add_executable(${PROJECT_NAME} ${QC_RUNTIME_TYPE} ${QC_PROJECT_SOURCES}) # Log compile definitions if(ENABLE_DETAILED_LOGS) target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_DETAILED_LOGS) endif() # # Internal include path (todo: remove this, use absolute path include) # target_include_directories(${PROJECT_NAME} PRIVATE fontawesome) target_include_directories(${PROJECT_NAME} PRIVATE util) target_include_directories(${PROJECT_NAME} PRIVATE uibase) target_include_directories(${PROJECT_NAME} PRIVATE ui) target_include_directories(${PROJECT_NAME} PRIVATE render) # output dir # https://cmake.org/cmake/help/latest/prop_gbl/GENERATOR_IS_MULTI_CONFIG.html get_property(QC_IS_MUTIL_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) message(STATUS "multi config:" QC_IS_MUTIL_CONFIG) # $<0:> 使用生成器表达式为每个config设置RUNTIME_OUTPUT_DIRECTORY,这样multi config就不会自动追加CMAKE_BUILD_TYPE子目录了 # 1. multi config介绍 https://cmake.org/cmake/help/latest/prop_gbl/GENERATOR_IS_MULTI_CONFIG.html # 2. multi config在不用表达式生成器时自动追加子目录说明 https://cmake.org/cmake/help/latest/prop_tgt/RUNTIME_OUTPUT_DIRECTORY.html # 3. 使用表达式生成器禁止multi config自动追加子目录解决方案 https://stackoverflow.com/questions/7747857/in-cmake-how-do-i-work-around-the-debug-and-release-directories-visual-studio-2 set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../output/${QC_CPU_ARCH}/${CMAKE_BUILD_TYPE}/$<0:>" ) # # plantform deps # # windows if(CMAKE_SYSTEM_NAME STREQUAL "Windows") get_target_property(QSC_BIN_OUTPUT_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) set(QSC_DEPLOY_PATH ${QSC_BIN_OUTPUT_PATH}) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.bat" "${QSC_BIN_OUTPUT_PATH}" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.apk" "${QSC_BIN_OUTPUT_PATH}" ) endif() # MacOS if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") # qt6 need 10.15 or later set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") # copy bundle file get_target_property(MACOS_BUNDLE_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) set(MACOS_BUNDLE_PATH ${MACOS_BUNDLE_PATH}/${PROJECT_NAME}.app/Contents) set(QSC_DEPLOY_PATH ${MACOS_BUNDLE_PATH}) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD # config file copy to Contents/MacOS/config COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../config/config.ini" "${MACOS_BUNDLE_PATH}/MacOS/config/config.ini" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.sh" "${MACOS_BUNDLE_PATH}/MacOS" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.apk" "${MACOS_BUNDLE_PATH}/MacOS" ) # Step 2. ues MACOSX_PACKAGE_LOCATION copy icns to Resources set_source_files_properties( ${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources ) # use MACOSX_BUNDLE_INFO_PLIST custom plist, not use MACOSX_BUNDLE_BUNDLE_NAME etc.. set(INFO_PLIST_TEMPLATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist.in") set(INFO_PLIST_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist") file(READ "${INFO_PLIST_TEMPLATE_FILE}" plist_contents) string(REPLACE "\${BUNDLE_VERSION}" "${PROJECT_VERSION}" plist_contents ${plist_contents}) file(WRITE ${INFO_PLIST_FILE} ${plist_contents}) set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${INFO_PLIST_FILE}" # "" disable code sign XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" ) # mac framework target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AppKit") endif() # Linux if(CMAKE_SYSTEM_NAME STREQUAL "Linux") get_target_property(QSC_BIN_OUTPUT_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) set(QSC_DEPLOY_PATH ${QSC_BIN_OUTPUT_PATH}) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.sh" "${QSC_BIN_OUTPUT_PATH}" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.apk" "${QSC_BIN_OUTPUT_PATH}" ) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(${PROJECT_NAME} PRIVATE # xcb https://doc.qt.io/qt-5/linux-requirements.html xcb # pthread Threads::Threads ) # linux set app icon: https://blog.csdn.net/MrNoboday/article/details/82870853 endif() # # common deps # add_subdirectory(QtScrcpyCore) # Qt target_link_libraries(${PROJECT_NAME} PRIVATE ${LINK_LIBS} QtScrcpyCore ) ================================================ FILE: QtScrcpy/appversion ================================================ 0.0.0 ================================================ FILE: QtScrcpy/audio/audiooutput.cpp ================================================ #include #include #include #include #include #include #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include #include #include #endif #include "audiooutput.h" AudioOutput::AudioOutput(QObject *parent) : QObject(parent) { m_running = false; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) m_audioOutput = nullptr; #else m_audioSink = nullptr; #endif connect(&m_sndcpy, &QProcess::readyReadStandardOutput, this, [this]() { qInfo() << QString("AudioOutput::") << QString(m_sndcpy.readAllStandardOutput()); }); connect(&m_sndcpy, &QProcess::readyReadStandardError, this, [this]() { qInfo() << QString("AudioOutput::") << QString(m_sndcpy.readAllStandardError()); }); } AudioOutput::~AudioOutput() { if (QProcess::NotRunning != m_sndcpy.state()) { m_sndcpy.kill(); } stop(); } bool AudioOutput::start(const QString& serial, int port) { if (m_running) { stop(); } QElapsedTimer timeConsumeCount; timeConsumeCount.start(); bool ret = runSndcpyProcess(serial, port); qInfo() << "AudioOutput::run sndcpy cost:" << timeConsumeCount.elapsed() << "milliseconds"; if (!ret) { return ret; } startAudioOutput(); startRecvData(port); m_running = true; return true; } void AudioOutput::stop() { if (!m_running) { return; } m_running = false; stopRecvData(); stopAudioOutput(); } void AudioOutput::installonly(const QString &serial, int port) { runSndcpyProcess(serial, port, false); } bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait) { if (QProcess::NotRunning != m_sndcpy.state()) { m_sndcpy.kill(); } #ifdef Q_OS_WIN32 QStringList params{serial, QString::number(port)}; m_sndcpy.start("sndcpy.bat", params); #else QStringList params{"sndcpy.sh", serial, QString::number(port)}; m_sndcpy.setWorkingDirectory(QCoreApplication::applicationDirPath()); m_sndcpy.start("bash", params); #endif if (!wait) { return true; } if (!m_sndcpy.waitForStarted()) { qWarning() << "AudioOutput::start sndcpy process failed"; return false; } if (!m_sndcpy.waitForFinished()) { qWarning() << "AudioOutput::sndcpy process crashed"; return false; } return true; } void AudioOutput::startAudioOutput() { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (m_audioOutput) { return; } QAudioFormat format; format.setSampleRate(48000); format.setChannelCount(2); format.setSampleSize(16); format.setCodec("audio/pcm"); format.setByteOrder(QAudioFormat::LittleEndian); format.setSampleType(QAudioFormat::SignedInt); QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); if (!info.isFormatSupported(format)) { qWarning() << "AudioOutput::audio format not supported, cannot play audio."; return; } m_audioOutput = new QAudioOutput(format, this); connect(m_audioOutput, &QAudioOutput::stateChanged, this, [](QAudio::State state) { qInfo() << "AudioOutput::audio state changed:" << state; }); m_audioOutput->setBufferSize(48000*2*15/1000 * 20); m_outputDevice = m_audioOutput->start(); #else if (m_audioSink) { return; } QAudioFormat format; format.setSampleRate(48000); format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Int16); QAudioDevice defaultDevice = QMediaDevices::defaultAudioOutput(); if (!defaultDevice.isFormatSupported(format)) { qWarning() << "AudioOutput::audio format not supported, cannot play audio."; return; } m_audioSink = new QAudioSink(defaultDevice, format, this); m_outputDevice = m_audioSink->start(); if (!m_outputDevice) { qWarning() << "AudioOutput::audio output device not available, cannot play audio."; delete m_audioSink; m_audioSink = nullptr; return; } #endif } void AudioOutput::stopAudioOutput() { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (m_audioOutput) { m_audioOutput->stop(); delete m_audioOutput; m_audioOutput = nullptr; } #else if (m_audioSink) { m_audioSink->stop(); delete m_audioSink; m_audioSink = nullptr; } #endif m_outputDevice = nullptr; } void AudioOutput::startRecvData(int port) { if (m_workerThread.isRunning()) { stopRecvData(); } auto audioSocket = new QTcpSocket(); audioSocket->moveToThread(&m_workerThread); connect(&m_workerThread, &QThread::finished, audioSocket, &QObject::deleteLater); connect(this, &AudioOutput::connectTo, audioSocket, [audioSocket](int port) { audioSocket->connectToHost(QHostAddress::LocalHost, port); if (!audioSocket->waitForConnected(500)) { qWarning("AudioOutput::audio socket connect failed"); return; } qInfo("AudioOutput::audio socket connect success"); }); connect(audioSocket, &QIODevice::readyRead, audioSocket, [this, audioSocket]() { qint64 recv = audioSocket->bytesAvailable(); //qDebug() << "AudioOutput::recv data:" << recv; if (!m_outputDevice) { return; } if (m_buffer.capacity() < recv) { m_buffer.reserve(recv); } qint64 count = audioSocket->read(m_buffer.data(), recv); m_outputDevice->write(m_buffer.data(), count); }); connect(audioSocket, &QTcpSocket::stateChanged, audioSocket, [](QAbstractSocket::SocketState state) { qInfo() << "AudioOutput::audio socket state changed:" << state; }); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(audioSocket, &QTcpSocket::errorOccurred, audioSocket, [](QAbstractSocket::SocketError error) { qInfo() << "AudioOutput::audio socket error occurred:" << error; }); #else connect(audioSocket, QOverload::of(&QAbstractSocket::error), audioSocket, [](QAbstractSocket::SocketError error) { qInfo() << "AudioOutput::audio socket error occurred:" << error; }); #endif m_workerThread.start(); emit connectTo(port); } void AudioOutput::stopRecvData() { if (!m_workerThread.isRunning()) { return; } m_workerThread.quit(); m_workerThread.wait(); } ================================================ FILE: QtScrcpy/audio/audiooutput.h ================================================ #ifndef AUDIOOUTPUT_H #define AUDIOOUTPUT_H #include #include #include #include class QAudioSink; class QAudioOutput; class QIODevice; class AudioOutput : public QObject { Q_OBJECT public: explicit AudioOutput(QObject *parent = nullptr); ~AudioOutput(); bool start(const QString& serial, int port); void stop(); void installonly(const QString& serial, int port); private: bool runSndcpyProcess(const QString& serial, int port, bool wait = true); void startAudioOutput(); void stopAudioOutput(); void startRecvData(int port); void stopRecvData(); signals: void connectTo(int port); private: QPointer m_outputDevice; QThread m_workerThread; QProcess m_sndcpy; QVector m_buffer; bool m_running = false; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QAudioOutput* m_audioOutput = nullptr; #else QAudioSink *m_audioSink = nullptr; #endif }; #endif // AUDIOOUTPUT_H ================================================ FILE: QtScrcpy/clang-format-all.sh ================================================ #!/bin/bash # # clang-format-all: a tool to run clang-format on an entire project # Copyright (C) 2016 Evan Klitzke # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . function usage { echo "Usage: $0 DIR..." exit 1 } if [ $# -eq 0 ]; then usage fi # Variable that will hold the name of the clang-format command FMT="" # Some distros just call it clang-format. Others (e.g. Ubuntu) are insistent # that the version number be part of the command. We prefer clang-format if # that's present, otherwise we work backwards from highest version to lowest # version. for clangfmt in clang-format{,-{4,3}.{9,8,7,6,5,4,3,2,1,0}}; do if which "$clangfmt" &>/dev/null; then FMT="$clangfmt" break fi done # Check if we found a working clang-format if [ -z "$FMT" ]; then echo "failed to find clang-format" exit 1 fi # Check all of the arguments first to make sure they're all directories for dir in "$@"; do if [ ! -d "${dir}" ]; then echo "${dir} is not a directory" usage fi done # Find a dominating file, starting from a given directory and going up. find-dominating-file() { if [ -r "$1"/"$2" ]; then return 0 fi if [ "$1" = "/" ]; then return 1 fi find-dominating-file "$(realpath "$1"/..)" "$2" return $? } # Run clang-format -i on all of the things for dir in "$@"; do pushd "${dir}" &>/dev/null if ! find-dominating-file . .clang-format; then echo "Failed to find dominating .clang-format starting at $PWD" continue fi find . \ \( -name '*.c' \ -o -name '*.cc' \ -o -name '*.cpp' \ -o -name '*.h' \ -o -name '*.hh' \ -o -name '*.hpp' \) \ -exec "${FMT}" -i '{}' \; popd &>/dev/null done ================================================ FILE: QtScrcpy/fontawesome/iconhelper.cpp ================================================ #include "iconhelper.h" IconHelper *IconHelper::_instance = 0; IconHelper::IconHelper(QObject *) : QObject(qApp) { int fontId = QFontDatabase::addApplicationFont(":/font/fontawesome-webfont.ttf"); QString fontName = QFontDatabase::applicationFontFamilies(fontId).at(0); iconFont = QFont(fontName); } void IconHelper::SetIcon(QLabel *lab, QChar c, int size) { iconFont.setPointSize(size); lab->setFont(iconFont); lab->setText(c); } void IconHelper::SetIcon(QPushButton *btn, QChar c, int size) { iconFont.setPointSize(size); btn->setFont(iconFont); btn->setText(c); } ================================================ FILE: QtScrcpy/fontawesome/iconhelper.h ================================================ #ifndef ICONHELPER_H #define ICONHELPER_H #include #include #include #include #include #include #include class IconHelper : public QObject { private: explicit IconHelper(QObject *parent = 0); QFont iconFont; static IconHelper *_instance; public: static IconHelper *Instance() { static QMutex mutex; if (!_instance) { QMutexLocker locker(&mutex); if (!_instance) { _instance = new IconHelper; } } return _instance; } void SetIcon(QLabel *lab, QChar c, int size = 10); void SetIcon(QPushButton *btn, QChar c, int size = 10); }; #endif // ICONHELPER_H ================================================ FILE: QtScrcpy/groupcontroller/groupcontroller.cpp ================================================ #include #include "groupcontroller.h" #include "videoform.h" GroupController::GroupController(QObject *parent) : QObject(parent) { } bool GroupController::isHost(const QString &serial) { auto data = qsc::IDeviceManage::getInstance().getDevice(serial)->getUserData(); if (!data) { return true; } return static_cast(data)->isHost(); } QSize GroupController::getFrameSize(const QString &serial) { auto data = qsc::IDeviceManage::getInstance().getDevice(serial)->getUserData(); if (!data) { return QSize(); } return static_cast(data)->frameSize(); } GroupController &GroupController::instance() { static GroupController gc; return gc; } void GroupController::updateDeviceState(const QString &serial) { if (!m_devices.contains(serial)) { return; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { return; } if (isHost(serial)) { device->registerDeviceObserver(this); } else { device->deRegisterDeviceObserver(this); } } void GroupController::addDevice(const QString &serial) { if (m_devices.contains(serial)) { return; } m_devices.append(serial); } void GroupController::removeDevice(const QString &serial) { if (!m_devices.contains(serial)) { return; } m_devices.removeOne(serial); auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { return; } if (isHost(serial)) { device->deRegisterDeviceObserver(this); } } void GroupController::mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize) { Q_UNUSED(frameSize); for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->mouseEvent(from, getFrameSize(serial), showSize); } } void GroupController::wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize) { Q_UNUSED(frameSize); for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->wheelEvent(from, getFrameSize(serial), showSize); } } void GroupController::keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize) { Q_UNUSED(frameSize); for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->keyEvent(from, getFrameSize(serial), showSize); } } void GroupController::postGoBack() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postGoBack(); } } void GroupController::postGoHome() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postGoHome(); } } void GroupController::postGoMenu() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postGoMenu(); } } void GroupController::postAppSwitch() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postAppSwitch(); } } void GroupController::postPower() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postPower(); } } void GroupController::postVolumeUp() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postVolumeUp(); } } void GroupController::postVolumeDown() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postVolumeDown(); } } void GroupController::postCopy() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postCopy(); } } void GroupController::postCut() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postCut(); } } void GroupController::setDisplayPower(bool on) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->setDisplayPower(on); } } void GroupController::expandNotificationPanel() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->expandNotificationPanel(); } } void GroupController::collapsePanel() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->collapsePanel(); } } void GroupController::postBackOrScreenOn(bool down) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postBackOrScreenOn(down); } } void GroupController::postTextInput(QString &text) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->postTextInput(text); } } void GroupController::requestDeviceClipboard() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->requestDeviceClipboard(); } } void GroupController::setDeviceClipboard(bool pause) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->setDeviceClipboard(pause); } } void GroupController::clipboardPaste() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->clipboardPaste(); } } void GroupController::pushFileRequest(const QString &file, const QString &devicePath) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->pushFileRequest(file, devicePath); } } void GroupController::installApkRequest(const QString &apkFile) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->installApkRequest(apkFile); } } void GroupController::screenshot() { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->screenshot(); } } void GroupController::showTouch(bool show) { for (const auto& serial : m_devices) { if (true == isHost(serial)) { continue; } auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { continue; } device->showTouch(show); } } ================================================ FILE: QtScrcpy/groupcontroller/groupcontroller.h ================================================ #ifndef GROUPCONTROLLER_H #define GROUPCONTROLLER_H #include #include #include "QtScrcpyCore.h" class GroupController : public QObject, public qsc::DeviceObserver { Q_OBJECT public: static GroupController& instance(); void updateDeviceState(const QString& serial); void addDevice(const QString& serial); void removeDevice(const QString& serial); private: // DeviceObserver void mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize) override; void wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize) override; void keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize) override; void postGoBack() override; void postGoHome() override; void postGoMenu() override; void postAppSwitch() override; void postPower() override; void postVolumeUp() override; void postVolumeDown() override; void postCopy() override; void postCut() override; void setDisplayPower(bool on) override; void expandNotificationPanel() override; void collapsePanel() override; void postBackOrScreenOn(bool down) override; void postTextInput(QString &text) override; void requestDeviceClipboard() override; void setDeviceClipboard(bool pause = true) override; void clipboardPaste() override; void pushFileRequest(const QString &file, const QString &devicePath = "") override; void installApkRequest(const QString &apkFile) override; void screenshot() override; void showTouch(bool show) override; private: explicit GroupController(QObject *parent = nullptr); bool isHost(const QString& serial); QSize getFrameSize(const QString& serial); private: QVector m_devices; }; #endif // GROUPCONTROLLER_H ================================================ FILE: QtScrcpy/main.cpp ================================================ #include #include #include #ifdef Q_OS_LINUX #include #include #endif #include #include #include #include #include #include "config.h" #include "dialog.h" #include "mousetap/mousetap.h" static Dialog *g_mainDlg = Q_NULLPTR; static QtMessageHandler g_oldMessageHandler = Q_NULLPTR; void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); void installTranslator(); static QtMsgType g_msgType = QtInfoMsg; QtMsgType covertLogLevel(const QString &logLevel); int main(int argc, char *argv[]) { // set env #ifdef Q_OS_WIN32 qputenv("QTSCRCPY_ADB_PATH", "../../../QtScrcpy/QtScrcpyCore/src/third_party/adb/win/adb.exe"); qputenv("QTSCRCPY_SERVER_PATH", "../../../QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server"); qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap"); qputenv("QTSCRCPY_CONFIG_PATH", "../../../config"); #endif #ifdef Q_OS_OSX qputenv("QTSCRCPY_ADB_PATH", "../../../../../../QtScrcpy/QtScrcpyCore/src/third_party/adb/mac/adb"); qputenv("QTSCRCPY_SERVER_PATH", "../../../../../../QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server"); qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../../../keymap"); qputenv("QTSCRCPY_CONFIG_PATH", "../../../../../../config"); #endif #ifdef Q_OS_LINUX // Only set environment variables if they are not already set (e.g., by AppImage AppRun) if (qgetenv("QTSCRCPY_ADB_PATH").isEmpty()) { qputenv("QTSCRCPY_ADB_PATH", "../../../QtScrcpy/QtScrcpyCore/src/third_party/adb/linux/adb"); } if (qgetenv("QTSCRCPY_SERVER_PATH").isEmpty()) { qputenv("QTSCRCPY_SERVER_PATH", "../../../QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server"); } if (qgetenv("QTSCRCPY_KEYMAP_PATH").isEmpty()) { qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap"); } if (qgetenv("QTSCRCPY_CONFIG_PATH").isEmpty()) { qputenv("QTSCRCPY_CONFIG_PATH", "../../../config"); } #endif g_msgType = covertLogLevel(Config::getInstance().getLogLevel()); // set on QApplication before // bug: config path is error on mac int opengl = Config::getInstance().getDesktopOpenGL(); if (0 == opengl) { QApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); } else if (1 == opengl) { QApplication::setAttribute(Qt::AA_UseOpenGLES); } else if (2 == opengl) { QApplication::setAttribute(Qt::AA_UseDesktopOpenGL); } #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #if (QT_VERSION >= QT_VERSION_CHECK(5,14,0)) QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif #endif QSurfaceFormat varFormat = QSurfaceFormat::defaultFormat(); varFormat.setVersion(2, 0); varFormat.setProfile(QSurfaceFormat::NoProfile); /* varFormat.setSamples(4); varFormat.setAlphaBufferSize(8); varFormat.setBlueBufferSize(8); varFormat.setRedBufferSize(8); varFormat.setGreenBufferSize(8); varFormat.setDepthBufferSize(24); */ QSurfaceFormat::setDefaultFormat(varFormat); g_oldMessageHandler = qInstallMessageHandler(myMessageOutput); QApplication a(argc, argv); // Set application icon for Linux (taskbar icon) #ifdef Q_OS_LINUX // Load icon from Qt resource (logo.png is included in res.qrc) QIcon appIcon(":/image/tray/logo.png"); if (!appIcon.isNull()) { a.setWindowIcon(appIcon); } #endif // windows下通过qmake VERSION变量或者rc设置版本号和应用名称后,这里可以直接拿到 // mac下拿到的是CFBundleVersion的值 qDebug() << a.applicationVersion(); qDebug() << a.applicationName(); //update version QStringList versionList = QCoreApplication::applicationVersion().split("."); if (versionList.size() >= 3) { QString version = versionList[0] + "." + versionList[1] + "." + versionList[2]; a.setApplicationVersion(version); } installTranslator(); #if defined(Q_OS_WIN32) || defined(Q_OS_OSX) MouseTap::getInstance()->initMouseEventTap(); #endif // load style sheet QFile file(":/qss/psblack.css"); if (file.open(QFile::ReadOnly)) { QString qss = QLatin1String(file.readAll()); QString paletteColor = qss.mid(20, 7); qApp->setPalette(QPalette(QColor(paletteColor))); qApp->setStyleSheet(qss); file.close(); } qsc::AdbProcess::setAdbPath(Config::getInstance().getAdbPath()); g_mainDlg = new Dialog {}; g_mainDlg->show(); qInfo() << QObject::tr("This software is completely open source and free. Use it at your own risk. You can download it at the " "following address:"); qInfo() << QString("QtScrcpy %1 ").arg(QCoreApplication::applicationVersion()); qInfo() << QObject::tr("If you need more professional batch control mirror software, you can try the following software:"); qInfo() << QString(QObject::tr("QuickMirror") + " "); qInfo() << QObject::tr("If you need more professional game keymap mirror software, you can try the following software:"); qInfo() << QString(QObject::tr("QuickAssistant") + " "); qInfo() << QObject::tr("If you need more professional PC remote software, you can try the following software:"); qInfo() << QString(QObject::tr("QuickDesk") + " "); qInfo() << QObject::tr("You can contact me with telegram "); int ret = a.exec(); delete g_mainDlg; #if defined(Q_OS_WIN32) || defined(Q_OS_OSX) MouseTap::getInstance()->quitMouseEventTap(); #endif return ret; } void installTranslator() { static QTranslator translator; QLocale locale; QLocale::Language language = locale.language(); if (Config::getInstance().getLanguage() == "zh_CN") { language = QLocale::Chinese; } else if (Config::getInstance().getLanguage() == "en_US") { language = QLocale::English; } else if (Config::getInstance().getLanguage() == "ja_JP") { language = QLocale::Japanese; } QString languagePath = ":/i18n/"; switch (language) { case QLocale::Chinese: languagePath += "zh_CN.qm"; break; case QLocale::Japanese: languagePath += "ja_JP.qm"; break; case QLocale::English: default: languagePath += "en_US.qm"; break; } auto loaded = translator.load(languagePath); if (!loaded) { qWarning() << "Failed to load translation file:" << languagePath; } qApp->installTranslator(&translator); } QtMsgType covertLogLevel(const QString &logLevel) { if ("debug" == logLevel) { return QtDebugMsg; } if ("info" == logLevel) { return QtInfoMsg; } if ("warn" == logLevel) { return QtWarningMsg; } if ("error" == logLevel) { return QtCriticalMsg; } #ifdef QT_NO_DEBUG return QtInfoMsg; #else return QtDebugMsg; #endif } void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QString outputMsg; #ifdef ENABLE_DETAILED_LOGS QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz"); if (context.file && context.line > 0) { QString fileName = QString::fromUtf8(context.file); int lastSlash = fileName.lastIndexOf('/'); if (lastSlash >= 0) { fileName = fileName.mid(lastSlash + 1); } lastSlash = fileName.lastIndexOf('\\'); if (lastSlash >= 0) { fileName = fileName.mid(lastSlash + 1); } outputMsg = QString("[ %1 %2: %3 ] %4").arg(timestamp).arg(fileName).arg(context.line).arg(msg); } else { outputMsg = QString("[%1] %2").arg(timestamp).arg(msg); } switch (type) { case QtDebugMsg: outputMsg.prepend("[debug] "); break; case QtInfoMsg: outputMsg.prepend("[info] "); break; case QtWarningMsg: outputMsg.prepend("[warring] "); break; case QtCriticalMsg: outputMsg.prepend("[critical] "); break; case QtFatalMsg: outputMsg.prepend("[fatal] "); break; } fprintf(stderr, "%s\n", outputMsg.toUtf8().constData()); #else outputMsg = msg; if (g_oldMessageHandler) { g_oldMessageHandler(type, context, outputMsg); } #endif // Is Qt log level higher than warning? float fLogLevel = g_msgType; if (QtInfoMsg == g_msgType) { fLogLevel = QtDebugMsg + 0.5f; } float fLogLevel2 = type; if (QtInfoMsg == type) { fLogLevel2 = QtDebugMsg + 0.5f; } if (fLogLevel <= fLogLevel2) { if (g_mainDlg && g_mainDlg->isVisible() && !g_mainDlg->filterLog(outputMsg)) { g_mainDlg->outLog(outputMsg); } } if (QtFatalMsg == type) { //abort(); } } ================================================ FILE: QtScrcpy/render/qyuvopenglwidget.cpp ================================================ #include #include #include #include "qyuvopenglwidget.h" // 存储顶点坐标和纹理坐标 // 存在一起缓存在vbo // 使用glVertexAttribPointer指定访问方式即可 static const GLfloat coordinate[] = { // 顶点坐标,存储4个xyz坐标 // 坐标范围为[-1,1],中心点为 0,0 // 二维图像z始终为0 // GL_TRIANGLE_STRIP的绘制方式: // 使用前3个坐标绘制一个三角形,使用后三个坐标绘制一个三角形,正好为一个矩形 // x y z -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, // 纹理坐标,存储4个xy坐标 // 坐标范围为[0,1],左下角为 0,0 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; // 顶点着色器 static const QString s_vertShader = R"( attribute vec3 vertexIn; // xyz顶点坐标 attribute vec2 textureIn; // xy纹理坐标 varying vec2 textureOut; // 传递给片段着色器的纹理坐标 void main(void) { gl_Position = vec4(vertexIn, 1.0); // 1.0表示vertexIn是一个顶点位置 textureOut = textureIn; // 纹理坐标直接传递给片段着色器 } )"; // 片段着色器 static QString s_fragShader = R"( varying vec2 textureOut; // 由顶点着色器传递过来的纹理坐标 uniform sampler2D textureY; // uniform 纹理单元,利用纹理单元可以使用多个纹理 uniform sampler2D textureU; // sampler2D是2D采样器 uniform sampler2D textureV; // 声明yuv三个纹理单元 void main(void) { vec3 yuv; vec3 rgb; // SDL2 BT709_SHADER_CONSTANTS // https://github.com/spurious/SDL-mirror/blob/4ddd4c445aa059bb127e101b74a8c5b59257fbe2/src/render/opengl/SDL_shaders_gl.c#L102 const vec3 Rcoeff = vec3(1.1644, 0.000, 1.7927); const vec3 Gcoeff = vec3(1.1644, -0.2132, -0.5329); const vec3 Bcoeff = vec3(1.1644, 2.1124, 0.000); // 根据指定的纹理textureY和坐标textureOut来采样 yuv.x = texture2D(textureY, textureOut).r; yuv.y = texture2D(textureU, textureOut).r - 0.5; yuv.z = texture2D(textureV, textureOut).r - 0.5; // 采样完转为rgb // 减少一些亮度 yuv.x = yuv.x - 0.0625; rgb.r = dot(yuv, Rcoeff); rgb.g = dot(yuv, Gcoeff); rgb.b = dot(yuv, Bcoeff); // 输出颜色值 gl_FragColor = vec4(rgb, 1.0); } )"; QYUVOpenGLWidget::QYUVOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) { /* QSurfaceFormat format = QSurfaceFormat::defaultFormat(); format.setColorSpace(QSurfaceFormat::sRGBColorSpace); format.setProfile(QSurfaceFormat::CompatibilityProfile); format.setMajorVersion(3); format.setMinorVersion(2); QSurfaceFormat::setDefaultFormat(format); */ } QYUVOpenGLWidget::~QYUVOpenGLWidget() { makeCurrent(); m_vbo.destroy(); deInitTextures(); doneCurrent(); } QSize QYUVOpenGLWidget::minimumSizeHint() const { return QSize(50, 50); } QSize QYUVOpenGLWidget::sizeHint() const { return size(); } void QYUVOpenGLWidget::setFrameSize(const QSize &frameSize) { if (m_frameSize != frameSize) { m_frameSize = frameSize; m_needUpdate = true; // inittexture immediately repaint(); } } const QSize &QYUVOpenGLWidget::frameSize() { return m_frameSize; } void QYUVOpenGLWidget::updateTextures(quint8 *dataY, quint8 *dataU, quint8 *dataV, quint32 linesizeY, quint32 linesizeU, quint32 linesizeV) { if (m_textureInited) { updateTexture(m_texture[0], 0, dataY, linesizeY); updateTexture(m_texture[1], 1, dataU, linesizeU); updateTexture(m_texture[2], 2, dataV, linesizeV); update(); } } void QYUVOpenGLWidget::initializeGL() { initializeOpenGLFunctions(); glDisable(GL_DEPTH_TEST); // 顶点缓冲对象初始化 m_vbo.create(); m_vbo.bind(); m_vbo.allocate(coordinate, sizeof(coordinate)); initShader(); // 设置背景清理色为黑色 glClearColor(0.0, 0.0, 0.0, 0.0); // 清理颜色背景 glClear(GL_COLOR_BUFFER_BIT); } void QYUVOpenGLWidget::paintGL() { m_shaderProgram.bind(); if (m_needUpdate) { deInitTextures(); initTextures(); m_needUpdate = false; } if (m_textureInited) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_texture[0]); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, m_texture[1]); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, m_texture[2]); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } m_shaderProgram.release(); } void QYUVOpenGLWidget::resizeGL(int width, int height) { glViewport(0, 0, width, height); repaint(); } void QYUVOpenGLWidget::initShader() { // opengles的float、int等要手动指定精度 if (QCoreApplication::testAttribute(Qt::AA_UseOpenGLES)) { s_fragShader.prepend(R"( precision mediump int; precision mediump float; )"); } m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, s_vertShader); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, s_fragShader); m_shaderProgram.link(); m_shaderProgram.bind(); // 指定顶点坐标在vbo中的访问方式 // 参数解释:顶点坐标在shader中的参数名称,顶点坐标为float,起始偏移为0,顶点坐标类型为vec3,步幅为3个float m_shaderProgram.setAttributeBuffer("vertexIn", GL_FLOAT, 0, 3, 3 * sizeof(float)); // 启用顶点属性 m_shaderProgram.enableAttributeArray("vertexIn"); // 指定纹理坐标在vbo中的访问方式 // 参数解释:纹理坐标在shader中的参数名称,纹理坐标为float,起始偏移为12个float(跳过前面存储的12个顶点坐标),纹理坐标类型为vec2,步幅为2个float m_shaderProgram.setAttributeBuffer("textureIn", GL_FLOAT, 12 * sizeof(float), 2, 2 * sizeof(float)); m_shaderProgram.enableAttributeArray("textureIn"); // 关联片段着色器中的纹理单元和opengl中的纹理单元(opengl一般提供16个纹理单元) m_shaderProgram.setUniformValue("textureY", 0); m_shaderProgram.setUniformValue("textureU", 1); m_shaderProgram.setUniformValue("textureV", 2); } void QYUVOpenGLWidget::initTextures() { // 创建纹理 glGenTextures(1, &m_texture[0]); glBindTexture(GL_TEXTURE_2D, m_texture[0]); // 设置纹理缩放时的策略 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 设置st方向上纹理超出坐标时的显示策略 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_frameSize.width(), m_frameSize.height(), 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr); glGenTextures(1, &m_texture[1]); glBindTexture(GL_TEXTURE_2D, m_texture[1]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_frameSize.width() / 2, m_frameSize.height() / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr); glGenTextures(1, &m_texture[2]); glBindTexture(GL_TEXTURE_2D, m_texture[2]); // 设置纹理缩放时的策略 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 设置st方向上纹理超出坐标时的显示策略 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_frameSize.width() / 2, m_frameSize.height() / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr); m_textureInited = true; } void QYUVOpenGLWidget::deInitTextures() { if (QOpenGLFunctions::isInitialized(QOpenGLFunctions::d_ptr)) { glDeleteTextures(3, m_texture); } memset(m_texture, 0, sizeof(m_texture)); m_textureInited = false; } void QYUVOpenGLWidget::updateTexture(GLuint texture, quint32 textureType, quint8 *pixels, quint32 stride) { if (!pixels) return; QSize size = 0 == textureType ? m_frameSize : m_frameSize / 2; makeCurrent(); glBindTexture(GL_TEXTURE_2D, texture); glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(stride)); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, size.width(), size.height(), GL_LUMINANCE, GL_UNSIGNED_BYTE, pixels); doneCurrent(); } ================================================ FILE: QtScrcpy/render/qyuvopenglwidget.h ================================================ #ifndef QYUVOPENGLWIDGET_H #define QYUVOPENGLWIDGET_H #include #include #include #include class QYUVOpenGLWidget : public QOpenGLWidget , protected QOpenGLFunctions { Q_OBJECT public: explicit QYUVOpenGLWidget(QWidget *parent = nullptr); virtual ~QYUVOpenGLWidget() override; QSize minimumSizeHint() const override; QSize sizeHint() const override; void setFrameSize(const QSize &frameSize); const QSize &frameSize(); void updateTextures(quint8 *dataY, quint8 *dataU, quint8 *dataV, quint32 linesizeY, quint32 linesizeU, quint32 linesizeV); protected: void initializeGL() override; void paintGL() override; void resizeGL(int width, int height) override; private: void initShader(); void initTextures(); void deInitTextures(); void updateTexture(GLuint texture, quint32 textureType, quint8 *pixels, quint32 stride); private: // 视频帧尺寸 QSize m_frameSize = { -1, -1 }; bool m_needUpdate = false; bool m_textureInited = false; // 顶点缓冲对象(Vertex Buffer Objects, VBO):默认即为VertexBuffer(GL_ARRAY_BUFFER)类型 QOpenGLBuffer m_vbo; // 着色器程序:编译链接着色器 QOpenGLShaderProgram m_shaderProgram; // YUV纹理,用于生成纹理贴图 GLuint m_texture[3] = { 0 }; }; #endif // QYUVOPENGLWIDGET_H ================================================ FILE: QtScrcpy/res/Info_Mac.plist.in ================================================ CFBundleDevelopmentRegion zh-Hans CFBundleExecutable QtScrcpy CFBundleGetInfoString Created by rankun CFBundleIconFile QtScrcpy CFBundleIdentifier rankun.QtScrcpy CFBundleInfoDictionaryVersion 6.0 CFBundleName QtScrcpy CFBundlePackageType APPL CFBundleShortVersionString ${BUNDLE_VERSION} CFBundleSupportedPlatforms MacOSX CFBundleVersion ${BUNDLE_VERSION} LSMinimumSystemVersion 10.10 NSAppleEventsUsageDescription NSHumanReadableCopyright Copyright © 2018-2038 rankun. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass NSApplication NSSupportsAutomaticGraphicsSwitching ================================================ FILE: QtScrcpy/res/QtScrcpy.rc ================================================ #include "winres.h" IDI_ICON1 ICON "QtScrcpy.ico" // GB2312编码的话,在中文系统上打包FileDescription可以显示中文 // 在github action(英文系统)打包后FileDescription是乱码,utf8编码也不行。。 VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x40004L FILETYPE 0x1L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "080404b0" BEGIN VALUE "CompanyName", "RanKun" VALUE "FileDescription", "Android real-time display control software" VALUE "FileVersion", VERSION_RC_STR VALUE "LegalCopyright", "Copyright (C) RanKun 2018-2038. All rights reserved." VALUE "ProductName", "QtScrcpy" VALUE "ProductVersion", VERSION_RC_STR END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x804, 1200 END END ================================================ FILE: QtScrcpy/res/i18n/CMakeLists.txt ================================================ # 声明ts文件 set(QC_TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/zh_CN.ts ${CMAKE_CURRENT_SOURCE_DIR}/en_US.ts ${CMAKE_CURRENT_SOURCE_DIR}/ja_JP.ts ) # 设置qm文件生成目录 set_source_files_properties(${QC_TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}") # 引入LinguistTools find_package(QT NAMES Qt6 Qt5 COMPONENTS LinguistTools REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS LinguistTools REQUIRED) # qt5_create_translation会依次执行 lupdate更新ts、lrelease更新qm qt5_create_translation(QM_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../.. ${QC_TS_FILES}) # 自定义目标依赖QM_FILES,否则不会生成qm文件 add_custom_target(QC_QM_GENERATOR DEPENDS ${QM_FILES}) # qt5_create_translation的bug:cmake clean的时候会删除翻译好的ts文件,导致翻译丢失 # (qt官方说qt6没问题,只用qt6的可以考虑qt5_create_translation) # 网上查到的CLEAN_NO_CUSTOM办法只能在makefile生成器下生效,解决不了问题 # https://cmake.org/cmake/help/latest/prop_dir/CLEAN_NO_CUSTOM.html # set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM true) # 目前唯一的解决办法是每次clean后,都手动在git中恢复一下ts文件 #[[ 总结: cmake qt项目下,利用cmake脚本有三种方式处理翻译: 1. 完全使用qt自带的cmake LinguistTools脚本:qt5_create_translation&qt5_add_translation 这两个脚本都满足不了需求: qt5_add_translation只能根据已有ts文件生成qm文件(lrelease),不能更新ts文件(lupdate) qt5_create_translation在cmake clean的时候会删除翻译好的ts文件,导致翻译丢失 2. cmake add_custom_command + cmake LinguistTools脚本(其实qt5_create_translation内部使用的也是add_custom_command) 例如add_custom_command执行lupdate,配合qt5_add_translation更新qm, 参考:https://github.com/maratnek/QtFirstProgrammCMake/blob/2c93b59e2ba85ff6ee0e727487e14003381687d3/CMakeLists.txt 3. 完全使用cmake命令来执行lupdate和lrelease 例如add_custom_command/add_custom_target/execute_process都可以实现执行lupdate和lrelease命令 上面3个方案都有一个共同问题:就是翻译文件处理都是和编译绑定在一起的,每次编译都会检测执行,实际的翻译工作是所有 编程工作都完成以后,统一执行一次lupdate、翻译、lrelease就可以了,不应该和编译绑定在一起 所以写两个shell脚本lupdate.sh和lrelease.sh来处理比较合适,其实非常简单: 1. 更新ts:lupdate -no-obsolete ./QtScrcpy -ts ./QtScrcpy/res/i18n/en_US.ts ./QtScrcpy/res/i18n/zh_CN.ts 2. 手动翻译ts 3. 发布:lrelease ./QtScrcpy/res/i18n/en_US.ts ./QtScrcpy/res/i18n/zh_CN.ts 参考文档 1. qt知道qt5_create_translation的bug,但是不肯解决,只确定了qt6没问题 https://bugreports.qt.io/browse/QTBUG-96549 2. https://doc.qt.io/qt-5/qtlinguist-cmake-qt5-add-translation.html 3. https://doc.qt.io/qt-5/qtlinguist-cmake-qt5-create-translation.html 4. execute_process 参考:https://blog.csdn.net/u010255072/article/details/120326833 5. add_custom_target 参考:https://www.cnblogs.com/apocelipes/p/14355460.html ================================================ FILE: QtScrcpy/res/i18n/en_US.ts ================================================ Dialog show show quit quit original original no lock no lock Notice Notice Hidden here! Hidden here! select path select path Clear History Clear History QObject This software is completely open source and free. Use it at your own risk. You can download it at the following address: This software is completely open source and free. Use it at your own risk. You can download it at the following address: QuickMirror QuickMirror If you need more professional batch control mirror software, you can try the following software: If you need more professional batch control mirror software, you can try the following software: If you need more professional game keymap mirror software, you can try the following software: If you need more professional game keymap mirror software, you can try the following software: QuickAssistant QuickAssistant You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1> You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1> ToolForm Tool Tool full screen full screen expand notify expand notify touch switch touch switch close screen close screen power power volume up volume up volume down volume down app switch app switch menu menu home home return return screen shot screen shot open screen open screen group control group control VideoForm file does not exist file does not exist Widget Wireless Wireless wireless connect wireless connect wireless disconnect wireless disconnect Start Config Start Config select path select path record format: record format: record screen record screen frameless frameless Use Simple Mode Use Simple Mode Use Simple Mode Simple Mode Simple Mode Simple Mode WIFI Connect WIFI Connect WIFI Connect USB Connect USB Connect USB Connect Double click to connect: Double click to connect: lock orientation: lock orientation: show fps show fps stay awake stay awake device name: device name: device name: update name update name update name stop all server stop all server adb command: adb command: terminate terminate execute execute clear clear reverse connection reverse connection background record background record screen-off screen-off apply apply max size: max size: always on top always on top refresh script refresh script get device IP get device IP USB line USB line stop server stop server start server start server device serial: device serial: bit rate: bit rate: start adbd start adbd refresh devices refresh devices install sndcpy install sndcpy start audio start audio stop audio stop audio auto update auto update show toolbar show toolbar record save path: record save path: ================================================ FILE: QtScrcpy/res/i18n/ja_JP.ts ================================================  Dialog show 表示 quit 終了 original オリジナル no lock ロックなし Notice お知らせ Hidden here! ここに隠れています! select path パスを選択 Clear History 履歴を消去 QObject This software is completely open source and free. Use it at your own risk. You can download it at the following address: このソフトウェアはオープンソースで完全無料です。自己責任でご利用ください。以下のアドレスからダウンロードできます: QuickMirror クイックミラー If you need more professional batch control mirror software, you can try the following software: より高度なバッチ制御が可能なミラーソフトウェアが必要な場合は、次のソフトウェアをお試しください: If you need more professional game keymap mirror software, you can try the following software: より高度なゲームキーマップが可能なミラーソフトウェアが必要な場合は、次のソフトウェアをお試しください: QuickAssistant クイックアシスタント You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1> Telegram で連絡ができます <https://t.me/+Ylf_5V_rDCMyODQ1> ToolForm Tool ツール full screen フルスクリーン expand notify 通知を展開 touch switch タッチ切り替え close screen 画面を閉じる power 電源 volume up 音量を上げる volume down 音量を下げる app switch アプリを切り替え menu メニュー home ホーム return 戻る screen shot スクリーンショット open screen 画面を開く group control グループコントロール VideoForm file does not exist ファイルが存在しません Widget Wireless ワイヤレス wireless connect ワイヤレスで接続 wireless disconnect ワイヤレスを切断 Start Config 構成を開始 select path パスを選択 record format: 録画の形式: record screen 画面を録画 frameless フレームレス Use Simple Mode シンプルモードを使用する シンプルモードを使用する Simple Mode シンプルモード シンプルモード WIFI Connect Wi-Fi 接続 Wi-Fi 接続 USB Connect USB 接続 USB 接続 Double click to connect: ダブルクリックで接続: lock orientation: 画面方向をロック: show fps FPS を表示 stay awake 画面を常時点灯 device name: デバイス名: デバイス名: update name 更新名 更新名 stop all server すべてのサーバーを停止 adb command: adb コマンド: terminate 停止 execute 実行 clear 消去 reverse connection リバース接続 background record バックグラウンド録画 screen-off 画面を OFF apply 適用 max size: 最大サイズ: always on top 常に手前に表示 refresh script スクリプトを更新 get device IP デバイス IP を取得 USB line USB ライン stop server サーバーを停止 start server サーバーを開始 device serial: デバイスシリアル: bit rate: ビットレート: start adbd adbd を開始 refresh devices デバイスを更新 install sndcpy Sndcpy をインストール start audio オーディオを開始 stop audio オーディオを停止 auto update 自動更新 show toolbar ツールバーを表示 record save path: 録画の保存先: ================================================ FILE: QtScrcpy/res/i18n/ko_KR.ts ================================================ Dialog show 표시 quit 종료 original 원본 no lock 잠금 없음 Notice 알림 Hidden here! 여기에 숨겨져 있어요! select path 경로 선택 Clear History 기록 지우기 QObject This software is completely open source and free. Use it at your own risk. You can download it at the following address: 이 소프트웨어는 완전히 오픈 소스이며 무료입니다. 본인의 위험을 감수하고 사용하세요. 다음 주소로 다운로드할 수 있습니다: QuickMirror 빠른미러 If you need more professional batch control mirror software, you can try the following software: 더 전문적인 일괄 제어 미러 소프트웨어가 필요하다면 다음 소프트웨어를 사용해 볼 수 있습니다: If you need more professional game keymap mirror software, you can try the following software: 더 전문적인 게임 키맵 미러 소프트웨어가 필요하다면 다음 소프트웨어를 사용해 보세요: QuickAssistant 빠른 도우미 You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1> 텔레그램으로 연락주세요 <https://t.me/+Ylf_5V_rDCMyODQ1> ToolForm Tool 도구 full screen 전체 화면 expand notify 확장 알림 touch switch 터치 스위치 close screen 화면 닫기 power 전ㅇ volume up 볼륨 높이기 volume down 볼륨 낮추기 app switch 앱 스위치 menu 메뉴 home home return 돌ㅇ가기 screen shot 스크린샷 open screen 화면 열기 group control 그룹 제어 VideoForm file does not exist 파일이 존재하지 않습니다 Widget Wireless 무선 wireless connect 무선 연결 wireless disconnect 무선 연결 끊기 Start Config 시작 구성 select path 경로 선택 record format: 기록 형식: record screen 화면 녹화 frameless 프레임 없음 Use Simple Mode Use Simple Mode 간단한 모드 사용 Simple Mode Simple Mode 간단한 모드 WIFI Connect WIFI Connect WIFI 연결 USB Connect USB Connect USB 연결 Double click to connect: 더블 클릭하여 연결: lock orientation: 잠금 방향: show fps fps 표시 stay awake 깨어 있기 device name: device name: 장치 이름: update name update name 업데이트 이름 stop all server 모든 서버 중지 adb command: adb 명령: terminate 끝내기 execute 실행 clear 지우기 reverse connection 역연결 background record 배경 기록 screen-off 화면 끄기 apply 적용 max size: 최대 크기: always on top 항상 위에 refresh script 스크립트 새로 고침 get device IP 장치 IP 가져오기 USB line USB 선 stop server 서버 중지 start server 서버 시작 device serial: 장치 직렬: bit rate: 비트 전송률: start adbd adbd 시작 refresh devices 장치 새로 고침 install sndcpy sndcpy 설치 start audio 오디오 시작 stop audio 오디오 중지 auto update 자동 업데이트 show toolbar 도구 모음 표시 record save path: 기록 저장 경로: ================================================ FILE: QtScrcpy/res/i18n/zh_CN.ts ================================================ Dialog show 显示 quit 退出 original 原始 no lock 不锁定 Notice 提示 Hidden here! 安卓录屏程序隐藏在这! select path 选择路径 Clear History 清理历史 QObject This software is completely open source and free. Use it at your own risk. You can download it at the following address: 本软件完全开源免费,作者不对使用该软件产生的一切后果负责。你可以在以下地址下载: QuickMirror 极限投屏 If you need more professional batch control mirror software, you can try the following software: 如果你需要更专业的批量控制投屏软件,你可以尝试下面软件: If you need more professional game keymap mirror software, you can try the following software: 如果你需要更专业的游戏映射投屏软件,你可以尝试下面软件: QuickAssistant 极限手游助手 You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1> 你可以通过QQ群联系我 <901736468> ToolForm Tool 工具 full screen 全屏 expand notify 下拉通知 touch switch 触摸显示开关 close screen 关闭屏幕 power 电源 volume up 音量加 volume down 音量减 app switch 切换应用 menu 菜单 home 主界面 return 返回 screen shot 截图 open screen 打开屏幕 group control 群控 VideoForm file does not exist 文件不存在 Widget Wireless 无线 wireless connect 无线连接 wireless disconnect 无线断开 Start Config 启动配置 select path 选择路径 record format: 录制格式: record screen 录制屏幕 frameless 无边框 Use Simple Mode 启用精简模式 启用精简模式 Simple Mode 精简模式 精简模式 WIFI Connect 一键WIFI连接 一键WIFI连接 USB Connect 一键USB连接 一键USB连接 Double click to connect: 双击连接: lock orientation: 锁定方向: show fps 显示fps stay awake 保持唤醒 device name: 设备名称: 设备名称: update name 更新设置名称 更新设置名称 stop all server 停止所有服务 adb command: adb命令: terminate 终止 execute 执行 clear 清理 reverse connection 反向连接 background record 后台录制 screen-off 自动息屏 apply 应用脚本 max size: 最大尺寸: always on top 窗口置顶 refresh script 刷新脚本 get device IP 获取设备IP USB line USB线 stop server 停止服务 start server 启动服务 device serial: 设备序列号: bit rate: 比特率: start adbd 启动adbd refresh devices 刷新设备列表 install sndcpy 安装sndcpy start audio 开始音频 stop audio 停止音频 auto update 自动刷新 show toolbar 显示工具栏 record save path: 录像保存路径 ================================================ FILE: QtScrcpy/res/qss/psblack.css ================================================ QPalette{background:#444444;}*{outline:0px;color:#DCDCDC;} QWidget[form="true"],QLabel[frameShape="1"]{ border:1px solid #242424; border-radius:0px; } QWidget[form="bottom"]{ background:#484848; } QWidget[form="bottom"] .QFrame{ border:1px solid #DCDCDC; } QWidget[form="bottom"] QLabel,QWidget[form="title"] QLabel{ border-radius:0px; color:#DCDCDC; background:none; border-style:none; } QWidget[form="title"],QWidget[nav="left"],QWidget[nav="top"] QAbstractButton{ border-style:none; border-radius:0px; padding:5px; color:#DCDCDC; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QWidget[nav="top"] QAbstractButton:hover,QWidget[nav="top"] QAbstractButton:pressed,QWidget[nav="top"] QAbstractButton:checked{ border-style:solid; border-width:0px 0px 2px 0px; padding:4px 4px 2px 4px; border-color:#00BB9E; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } QWidget[nav="left"] QAbstractButton{ border-radius:0px; color:#DCDCDC; background:none; border-style:none; } QWidget[nav="left"] QAbstractButton:hover{ color:#FFFFFF; background-color:#00BB9E; } QWidget[nav="left"] QAbstractButton:checked,QWidget[nav="left"] QAbstractButton:pressed{ color:#DCDCDC; border-style:solid; border-width:0px 0px 0px 2px; padding:4px 4px 4px 2px; border-color:#00BB9E; background-color:#444444; } QWidget[video="true"] QLabel{ color:#DCDCDC; border:1px solid #242424; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QWidget[video="true"] QLabel:focus{ border:1px solid #00BB9E; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ border:1px solid #242424; border-radius:3px; padding:2px; background:none; selection-background-color:#264F78; selection-color:#DCDCDC; } QLineEdit:focus,QTextEdit:focus,QPlainTextEdit:focus,QSpinBox:focus,QDoubleSpinBox:focus,QComboBox:focus,QDateEdit:focus,QTimeEdit:focus,QDateTimeEdit:focus,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QSpinBox:hover,QDoubleSpinBox:hover,QComboBox:hover,QDateEdit:hover,QTimeEdit:hover,QDateTimeEdit:hover{ border:1px solid #242424; } QLineEdit[echoMode="2"]{ lineedit-password-character:9679; } .QFrame{ border:1px solid #242424; border-radius:3px; } .QGroupBox{ border:1px solid #242424; border-radius:5px; margin-top:3ex; } .QGroupBox::title{ subcontrol-origin:margin; position:relative; left:10px; } .QPushButton,.QToolButton{ border-style:none; border:1px solid #242424; color:#DCDCDC; padding:5px; min-height:15px; border-radius:5px; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } .QPushButton:hover,.QToolButton:hover{ background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } .QPushButton:pressed,.QToolButton:pressed{ background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } .QToolButton::menu-indicator{ image:None; } QToolButton#btnMenu,QPushButton#btnMenu_Min,QPushButton#btnMenu_Max,QPushButton#btnMenu_Close{ border-radius:3px; color:#DCDCDC; padding:3px; margin:0px; background:none; border-style:none; } QToolButton#btnMenu:hover,QPushButton#btnMenu_Min:hover,QPushButton#btnMenu_Max:hover{ color:#FFFFFF; margin:1px 1px 2px 1px; background-color:rgba(51,127,209,230); } QPushButton#btnMenu_Close:hover{ color:#FFFFFF; margin:1px 1px 2px 1px; background-color:rgba(238,0,0,128); } QRadioButton::indicator{ width:15px; height:15px; } QRadioButton::indicator::unchecked{ image:url(:/qss/psblack/radiobutton_unchecked.png); } QRadioButton::indicator::unchecked:disabled{ image:url(:/qss/psblack/radiobutton_unchecked_disable.png); } QRadioButton::indicator::checked{ image:url(:/qss/psblack/radiobutton_checked.png); } QRadioButton::indicator::checked:disabled{ image:url(:/qss/psblack/radiobutton_checked_disable.png); } QGroupBox::indicator,QTreeWidget::indicator,QListWidget::indicator{ padding:0px -3px 0px 3px; } QCheckBox::indicator,QGroupBox::indicator,QTreeWidget::indicator,QListWidget::indicator{ width:13px; height:13px; } QCheckBox::indicator:unchecked,QGroupBox::indicator:unchecked,QTreeWidget::indicator:unchecked,QListWidget::indicator:unchecked{ image:url(:/qss/psblack/checkbox_unchecked.png); } QCheckBox::indicator:unchecked:disabled,QGroupBox::indicator:unchecked:disabled,QTreeWidget::indicator:unchecked:disabled,QListWidget::indicator:disabled{ image:url(:/qss/psblack/checkbox_unchecked_disable.png); } QCheckBox::indicator:checked,QGroupBox::indicator:checked,QTreeWidget::indicator:checked,QListWidget::indicator:checked{ image:url(:/qss/psblack/checkbox_checked.png); } QCheckBox::indicator:checked:disabled,QGroupBox::indicator:checked:disabled,QTreeWidget::indicator:checked:disabled,QListWidget::indicator:checked:disabled{ image:url(:/qss/psblack/checkbox_checked_disable.png); } QCheckBox::indicator:indeterminate,QGroupBox::indicator:indeterminate,QTreeWidget::indicator:indeterminate,QListWidget::indicator:indeterminate{ image:url(:/qss/psblack/checkbox_parcial.png); } QCheckBox::indicator:indeterminate:disabled,QGroupBox::indicator:indeterminate:disabled,QTreeWidget::indicator:indeterminate:disabled,QListWidget::indicator:indeterminate:disabled{ image:url(:/qss/psblack/checkbox_parcial_disable.png); } QTimeEdit::up-button,QDateEdit::up-button,QDateTimeEdit::up-button,QDoubleSpinBox::up-button,QSpinBox::up-button{ image:url(:/qss/psblack/add_top.png); width:10px; height:10px; padding:2px 5px 0px 0px; } QTimeEdit::down-button,QDateEdit::down-button,QDateTimeEdit::down-button,QDoubleSpinBox::down-button,QSpinBox::down-button{ image:url(:/qss/psblack/add_bottom.png); width:10px; height:10px; padding:0px 5px 2px 0px; } QTimeEdit::up-button:pressed,QDateEdit::up-button:pressed,QDateTimeEdit::up-button:pressed,QDoubleSpinBox::up-button:pressed,QSpinBox::up-button:pressed{ top:-2px; } QTimeEdit::down-button:pressed,QDateEdit::down-button:pressed,QDateTimeEdit::down-button:pressed,QDoubleSpinBox::down-button:pressed,QSpinBox::down-button:pressed,QSpinBox::down-button:pressed{ bottom:-2px; } QComboBox::down-arrow,QDateEdit[calendarPopup="true"]::down-arrow,QTimeEdit[calendarPopup="true"]::down-arrow,QDateTimeEdit[calendarPopup="true"]::down-arrow{ image:url(:/qss/psblack/add_bottom.png); width:10px; height:10px; right:2px; } QComboBox::drop-down,QDateEdit::drop-down,QTimeEdit::drop-down,QDateTimeEdit::drop-down{ subcontrol-origin:padding; subcontrol-position:top right; width:15px; border-left-width:0px; border-left-style:solid; border-top-right-radius:3px; border-bottom-right-radius:3px; border-left-color:#242424; } QComboBox::drop-down:on{ top:1px; } QMenuBar::item{ color:#DCDCDC; background-color:#484848; margin:0px; padding:3px 10px; } QMenu,QMenuBar,QMenu:disabled,QMenuBar:disabled{ color:#DCDCDC; background-color:#484848; border:1px solid #242424; margin:0px; } QMenu::item{ padding:3px 20px; } QMenu::indicator{ width:13px; height:13px; } QMenu::item:selected,QMenuBar::item:selected{ color:#DCDCDC; border:0px solid #242424; background:#646464; } QMenu::separator{ height:1px; background:#242424; } QProgressBar{ min-height:10px; background:#484848; border-radius:5px; text-align:center; border:1px solid #484848; } QProgressBar:chunk{ border-radius:5px; background-color:#242424; } QSlider::groove:horizontal{ background:#484848; height:8px; border-radius:4px; } QSlider::add-page:horizontal{ background:#484848; height:8px; border-radius:4px; } QSlider::sub-page:horizontal{ background:#242424; height:8px; border-radius:4px; } QSlider::handle:horizontal{ width:13px; margin-top:-3px; margin-bottom:-3px; border-radius:6px; background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #444444,stop:0.8 #242424); } QSlider::groove:vertical{ width:8px; border-radius:4px; background:#484848; } QSlider::add-page:vertical{ width:8px; border-radius:4px; background:#484848; } QSlider::sub-page:vertical{ width:8px; border-radius:4px; background:#242424; } QSlider::handle:vertical{ height:14px; margin-left:-3px; margin-right:-3px; border-radius:6px; background:qradialgradient(spread:pad,cx:0.5,cy:0.5,radius:0.5,fx:0.5,fy:0.5,stop:0.6 #444444,stop:0.8 #242424); } QScrollBar:horizontal{ background:#484848; padding:0px; border-radius:6px; max-height:12px; } QScrollBar::handle:horizontal{ background:#525252; min-width:50px; border-radius:6px; } QScrollBar::handle:horizontal:hover{ background:#242424; } QScrollBar::handle:horizontal:pressed{ background:#242424; } QScrollBar::add-page:horizontal{ background:none; } QScrollBar::sub-page:horizontal{ background:none; } QScrollBar::add-line:horizontal{ background:none; } QScrollBar::sub-line:horizontal{ background:none; } QScrollBar:vertical{ background:#484848; padding:0px; border-radius:6px; max-width:12px; } QScrollBar::handle:vertical{ background:#525252; min-height:50px; border-radius:6px; } QScrollBar::handle:vertical:hover{ background:#242424; } QScrollBar::handle:vertical:pressed{ background:#242424; } QScrollBar::add-page:vertical{ background:none; } QScrollBar::sub-page:vertical{ background:none; } QScrollBar::add-line:vertical{ background:none; } QScrollBar::sub-line:vertical{ background:none; } QScrollArea{ border:0px; } QTreeView,QListView,QTableView,QTabWidget::pane{ border:1px solid #242424; selection-background-color:#646464; selection-color:#DCDCDC; alternate-background-color:#525252; gridline-color:#242424; } QTreeView::branch:closed:has-children{ margin:4px; border-image:url(:/qss/psblack/branch_open.png); } QTreeView::branch:open:has-children{ margin:4px; border-image:url(:/qss/psblack/branch_close.png); } QTreeView,QListView,QTableView,QSplitter::handle,QTreeView::branch{ background:#444444; } QTableView::item:selected,QListView::item:selected,QTreeView::item:selected{ color:#DCDCDC; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QTableView::item:hover,QListView::item:hover,QTreeView::item:hover{ color:#DCDCDC; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } QTableView::item,QListView::item,QTreeView::item{ padding:1px; margin:0px; } QHeaderView::section,QTableCornerButton:section{ padding:3px; margin:0px; color:#DCDCDC; border:1px solid #242424; border-left-width:0px; border-right-width:1px; border-top-width:0px; border-bottom-width:1px; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } QTabBar::tab{ border:1px solid #242424; color:#DCDCDC; margin:0px; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } QTabBar::tab:selected,QTabBar::tab:hover{ border-style:solid; border-color:#00BB9E; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QTabBar::tab:top,QTabBar::tab:bottom{ padding:3px 8px 3px 8px; } QTabBar::tab:left,QTabBar::tab:right{ padding:8px 3px 8px 3px; } QTabBar::tab:top:selected,QTabBar::tab:top:hover{ border-width:2px 0px 0px 0px; } QTabBar::tab:right:selected,QTabBar::tab:right:hover{ border-width:0px 0px 0px 2px; } QTabBar::tab:bottom:selected,QTabBar::tab:bottom:hover{ border-width:0px 0px 2px 0px; } QTabBar::tab:left:selected,QTabBar::tab:left:hover{ border-width:0px 2px 0px 0px; } QTabBar::tab:first:top:selected,QTabBar::tab:first:top:hover,QTabBar::tab:first:bottom:selected,QTabBar::tab:first:bottom:hover{ border-left-width:1px; border-left-color:#242424; } QTabBar::tab:first:left:selected,QTabBar::tab:first:left:hover,QTabBar::tab:first:right:selected,QTabBar::tab:first:right:hover{ border-top-width:1px; border-top-color:#242424; } QTabBar::tab:last:top:selected,QTabBar::tab:last:top:hover,QTabBar::tab:last:bottom:selected,QTabBar::tab:last:bottom:hover{ border-right-width:1px; border-right-color:#242424; } QTabBar::tab:last:left:selected,QTabBar::tab:last:left:hover,QTabBar::tab:last:right:selected,QTabBar::tab:last:right:hover{ border-bottom-width:1px; border-bottom-color:#242424; } QStatusBar::item{ border:0px solid #484848; border-radius:3px; } QToolBox::tab,QGroupBox#gboxDevicePanel,QGroupBox#gboxDeviceTitle,QFrame#gboxDevicePanel,QFrame#gboxDeviceTitle{ padding:3px; border-radius:5px; color:#DCDCDC; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QToolTip{ border:0px solid #DCDCDC; padding:1px; color:#DCDCDC; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QToolBox::tab:selected{ background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #646464,stop:1 #525252); } QPrintPreviewDialog QToolButton{ border:0px solid #DCDCDC; border-radius:0px; margin:0px; padding:3px; background:none; } QColorDialog QPushButton,QFileDialog QPushButton{ min-width:80px; } QToolButton#qt_calendar_prevmonth{ icon-size:0px; min-width:20px; image:url(:/qss/psblack/calendar_prevmonth.png); } QToolButton#qt_calendar_nextmonth{ icon-size:0px; min-width:20px; image:url(:/qss/psblack/calendar_nextmonth.png); } QToolButton#qt_calendar_prevmonth,QToolButton#qt_calendar_nextmonth,QToolButton#qt_calendar_monthbutton,QToolButton#qt_calendar_yearbutton{ border:0px solid #DCDCDC; border-radius:3px; margin:3px 3px 3px 3px; padding:3px; background:none; } QToolButton#qt_calendar_prevmonth:hover,QToolButton#qt_calendar_nextmonth:hover,QToolButton#qt_calendar_monthbutton:hover,QToolButton#qt_calendar_yearbutton:hover,QToolButton#qt_calendar_prevmonth:pressed,QToolButton#qt_calendar_nextmonth:pressed,QToolButton#qt_calendar_monthbutton:pressed,QToolButton#qt_calendar_yearbutton:pressed{ border:1px solid #242424; } QCalendarWidget QSpinBox#qt_calendar_yearedit{ margin:2px; } QCalendarWidget QToolButton::menu-indicator{ image:None; } QCalendarWidget QTableView{ border-width:0px; } QCalendarWidget QWidget#qt_calendar_navigationbar{ border:1px solid #242424; border-width:1px 1px 0px 1px; background:qlineargradient(spread:pad,x1:0,y1:0,x2:0,y2:1,stop:0 #484848,stop:1 #383838); } QComboBox QAbstractItemView::item{ min-height:20px; min-width:10px; } QTableView[model="true"]::item{ padding:0px; margin:0px; } QTableView QLineEdit,QTableView QComboBox,QTableView QSpinBox,QTableView QDoubleSpinBox,QTableView QDateEdit,QTableView QTimeEdit,QTableView QDateTimeEdit{ border-width:0px; border-radius:0px; } QTableView QLineEdit:focus,QTableView QComboBox:focus,QTableView QSpinBox:focus,QTableView QDoubleSpinBox:focus,QTableView QDateEdit:focus,QTableView QTimeEdit:focus,QTableView QDateTimeEdit:focus{ border-width:0px; border-radius:0px; } QLineEdit,QTextEdit,QPlainTextEdit,QSpinBox,QDoubleSpinBox,QComboBox,QDateEdit,QTimeEdit,QDateTimeEdit{ background:#444444; } *:disabled{ background:#444444; border-color:#484848; } QMessageBox { background-color:#444444; color:#DCDCDC; } /*TextColor:#DCDCDC*/ /*PanelColor:#444444*/ /*BorderColor:#242424*/ /*NormalColorStart:#484848*/ /*NormalColorEnd:#383838*/ /*DarkColorStart:#646464*/ /*DarkColorEnd:#525252*/ /*HighColor:#00BB9E*/ ================================================ FILE: QtScrcpy/res/res.qrc ================================================ font/fontawesome-webfont.ttf image/videoform/phone-h.png image/videoform/phone-v.png qss/psblack.css qss/psblack/add_bottom.png qss/psblack/add_left.png qss/psblack/add_right.png qss/psblack/add_top.png qss/psblack/branch_close.png qss/psblack/branch_open.png qss/psblack/calendar_nextmonth.png qss/psblack/calendar_prevmonth.png qss/psblack/checkbox_checked.png qss/psblack/checkbox_checked_disable.png qss/psblack/checkbox_parcial.png qss/psblack/checkbox_parcial_disable.png qss/psblack/checkbox_unchecked.png qss/psblack/checkbox_unchecked_disable.png qss/psblack/radiobutton_checked.png qss/psblack/radiobutton_checked_disable.png qss/psblack/radiobutton_unchecked.png qss/psblack/radiobutton_unchecked_disable.png i18n/en_US.qm i18n/zh_CN.qm i18n/ja_JP.qm image/tray/logo.png ================================================ FILE: QtScrcpy/sndcpy/sndcpy.bat ================================================ @echo off echo Begin Runing... set SNDCPY_PORT=28200 set SNDCPY_APK=sndcpy.apk set ADB=adb.exe if not "%1"=="" ( set serial=-s %1 ) if not "%2"=="" ( set SNDCPY_PORT=%2 ) echo Waiting for device %1... %ADB% %serial% wait-for-device || goto :error echo Find device %1 for /f "delims=" %%i in ('%ADB% %serial% shell pm path com.rom1v.sndcpy') do set sndcpy_installed=%%i if "%sndcpy_installed%"=="" ( echo Install %SNDCPY_APK%... %ADB% %serial% uninstall com.rom1v.sndcpy || echo uninstall failed %ADB% %serial% install -t -r -g %SNDCPY_APK% || goto :error echo Install %SNDCPY_APK% success ) echo Request PROJECT_MEDIA permission... %ADB% %serial% shell appops set com.rom1v.sndcpy PROJECT_MEDIA allow echo Forward port %SNDCPY_PORT%... %ADB% %serial% forward tcp:%SNDCPY_PORT% localabstract:sndcpy || goto :error echo Start %SNDCPY_APK%... %ADB% %serial% shell am start com.rom1v.sndcpy/.MainActivity || goto :error :check_start echo Waiting %SNDCPY_APK% start... ::timeout /T 1 /NOBREAK > nul %ADB% %serial% shell sleep 0.1 for /f "delims=" %%i in ("%ADB% shell 'ps | grep com.rom1v.sndcpy'") do set sndcpy_started=%%i if "%sndcpy_started%"=="" ( goto :check_start ) echo %SNDCPY_APK% started... echo Ready playing... ::vlc.exe -Idummy --demux rawaud --network-caching=0 --play-and-exit tcp://localhost:%SNDCPY_PORT% ::ffplay.exe -nodisp -autoexit -probesize 32 -sync ext -f s16le -ar 48k -ac 2 tcp://localhost:%SNDCPY_PORT% goto :EOF :error echo Failed with error #%errorlevel%. exit /b %errorlevel% ================================================ FILE: QtScrcpy/sndcpy/sndcpy.sh ================================================ #!/bin/bash echo Begin Runing... SNDCPY_PORT=28200 SNDCPY_APK=sndcpy.apk ADB=./adb serial= if [[ $# -ge 2 ]] then serial="-s $1" SNDCPY_PORT=$2 fi echo "Waiting for device $1..." $ADB $serial wait-for-device echo "Find device $1" sndcpy_installed=$($ADB $serial shell pm path com.rom1v.sndcpy) if [[ $sndcpy_installed == "" ]]; then echo Install $SNDCPY_APK... $ADB $serial uninstall com.rom1v.sndcpy || echo uninstall failed $ADB $serial install -t -r -g $SNDCPY_APK echo Install $SNDCPY_APK success fi echo Request PROJECT_MEDIA permission... $ADB $serial shell appops set com.rom1v.sndcpy PROJECT_MEDIA allow echo Forward port $SNDCPY_PORT... $ADB $serial forward tcp:$SNDCPY_PORT localabstract:sndcpy echo Start $SNDCPY_APK... $ADB $serial shell am start com.rom1v.sndcpy/.MainActivity while ((1)) do echo Waiting $SNDCPY_APK start... sleep 0.1 sndcpy_started=$($ADB shell 'ps | grep com.rom1v.sndcpy') if [[ $sndcpy_started != "" ]]; then break fi done echo Ready playing... ================================================ FILE: QtScrcpy/ui/dialog.cpp ================================================ #include #include #include #include #include #include #include #include "config.h" #include "dialog.h" #include "ui_dialog.h" #include "videoform.h" #include "../groupcontroller/groupcontroller.h" #ifdef Q_OS_WIN32 #include "../util/winutils.h" #endif QString s_keyMapPath = ""; const QString &getKeyMapPath() { if (s_keyMapPath.isEmpty()) { s_keyMapPath = QString::fromLocal8Bit(qgetenv("QTSCRCPY_KEYMAP_PATH")); QFileInfo fileInfo(s_keyMapPath); if (s_keyMapPath.isEmpty() || !fileInfo.isDir()) { s_keyMapPath = QCoreApplication::applicationDirPath() + "/keymap"; } } return s_keyMapPath; } Dialog::Dialog(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); initUI(); updateBootConfig(true); on_useSingleModeCheck_clicked(); on_updateDevice_clicked(); connect(&m_autoUpdatetimer, &QTimer::timeout, this, &Dialog::on_updateDevice_clicked); if (ui->autoUpdatecheckBox->isChecked()) { m_autoUpdatetimer.start(5000); } connect(&m_adb, &qsc::AdbProcess::adbProcessResult, this, [this](qsc::AdbProcess::ADB_EXEC_RESULT processResult) { QString log = ""; bool newLine = true; QStringList args = m_adb.arguments(); switch (processResult) { case qsc::AdbProcess::AER_ERROR_START: break; case qsc::AdbProcess::AER_SUCCESS_START: log = "adb run"; newLine = false; break; case qsc::AdbProcess::AER_ERROR_EXEC: //log = m_adb.getErrorOut(); if (args.contains("ifconfig") && args.contains("wlan0")) { getIPbyIp(); } break; case qsc::AdbProcess::AER_ERROR_MISSING_BINARY: log = "adb not found"; break; case qsc::AdbProcess::AER_SUCCESS_EXEC: //log = m_adb.getStdOut(); if (args.contains("devices")) { QStringList devices = m_adb.getDevicesSerialFromStdOut(); ui->serialBox->clear(); ui->connectedPhoneList->clear(); for (auto &item : devices) { ui->serialBox->addItem(item); ui->connectedPhoneList->addItem(Config::getInstance().getNickName(item) + "-" + item); } } else if (args.contains("show") && args.contains("wlan0")) { QString ip = m_adb.getDeviceIPFromStdOut(); if (ip.isEmpty()) { log = "ip not find, connect to wifi?"; break; } ui->deviceIpEdt->setEditText(ip); } else if (args.contains("ifconfig") && args.contains("wlan0")) { QString ip = m_adb.getDeviceIPFromStdOut(); if (ip.isEmpty()) { log = "ip not find, connect to wifi?"; break; } ui->deviceIpEdt->setEditText(ip); } else if (args.contains("ip -o a")) { QString ip = m_adb.getDeviceIPByIpFromStdOut(); if (ip.isEmpty()) { log = "ip not find, connect to wifi?"; break; } ui->deviceIpEdt->setEditText(ip); } break; } if (!log.isEmpty()) { outLog(log, newLine); } }); m_hideIcon = new QSystemTrayIcon(this); m_hideIcon->setIcon(QIcon(":/image/tray/logo.png")); m_menu = new QMenu(this); m_quit = new QAction(this); m_showWindow = new QAction(this); m_showWindow->setText(tr("show")); m_quit->setText(tr("quit")); m_menu->addAction(m_showWindow); m_menu->addAction(m_quit); m_hideIcon->setContextMenu(m_menu); m_hideIcon->show(); connect(m_showWindow, &QAction::triggered, this, &Dialog::show); connect(m_quit, &QAction::triggered, this, [this]() { m_hideIcon->hide(); qApp->quit(); }); connect(m_hideIcon, &QSystemTrayIcon::activated, this, &Dialog::slotActivated); connect(&qsc::IDeviceManage::getInstance(), &qsc::IDeviceManage::deviceConnected, this, &Dialog::onDeviceConnected); connect(&qsc::IDeviceManage::getInstance(), &qsc::IDeviceManage::deviceDisconnected, this, &Dialog::onDeviceDisconnected); } Dialog::~Dialog() { qDebug() << "~Dialog()"; updateBootConfig(false); qsc::IDeviceManage::getInstance().disconnectAllDevice(); delete ui; } void Dialog::initUI() { setAttribute(Qt::WA_DeleteOnClose); //setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint | Qt::CustomizeWindowHint); setWindowTitle(Config::getInstance().getTitle()); #ifdef Q_OS_LINUX // Set window icon (inherits from application icon set in main.cpp) // If application icon was set, this will use it automatically if (!qApp->windowIcon().isNull()) { setWindowIcon(qApp->windowIcon()); } #endif #ifdef Q_OS_WIN32 WinUtils::setDarkBorderToWindow((HWND)this->winId(), true); #endif ui->bitRateEdit->setValidator(new QIntValidator(1, 99999, this)); ui->maxSizeBox->addItem("640"); ui->maxSizeBox->addItem("720"); ui->maxSizeBox->addItem("1080"); ui->maxSizeBox->addItem("1280"); ui->maxSizeBox->addItem("1920"); ui->maxSizeBox->addItem(tr("original")); ui->formatBox->addItem("mp4"); ui->formatBox->addItem("mkv"); ui->lockOrientationBox->addItem(tr("no lock")); ui->lockOrientationBox->addItem("0"); ui->lockOrientationBox->addItem("90"); ui->lockOrientationBox->addItem("180"); ui->lockOrientationBox->addItem("270"); ui->lockOrientationBox->setCurrentIndex(0); // 加载IP历史记录 loadIpHistory(); // 加载端口历史记录 loadPortHistory(); // 为deviceIpEdt添加右键菜单 if (ui->deviceIpEdt->lineEdit()) { ui->deviceIpEdt->lineEdit()->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->deviceIpEdt->lineEdit(), &QWidget::customContextMenuRequested, this, &Dialog::showIpEditMenu); } // 为devicePortEdt添加右键菜单 if (ui->devicePortEdt->lineEdit()) { ui->devicePortEdt->lineEdit()->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->devicePortEdt->lineEdit(), &QWidget::customContextMenuRequested, this, &Dialog::showPortEditMenu); } } void Dialog::updateBootConfig(bool toView) { if (toView) { UserBootConfig config = Config::getInstance().getUserBootConfig(); if (config.bitRate == 0) { ui->bitRateBox->setCurrentText("Mbps"); } else if (config.bitRate % 1000000 == 0) { ui->bitRateEdit->setText(QString::number(config.bitRate / 1000000)); ui->bitRateBox->setCurrentText("Mbps"); } else { ui->bitRateEdit->setText(QString::number(config.bitRate / 1000)); ui->bitRateBox->setCurrentText("Kbps"); } ui->maxSizeBox->setCurrentIndex(config.maxSizeIndex); ui->formatBox->setCurrentIndex(config.recordFormatIndex); ui->recordPathEdt->setText(config.recordPath); ui->lockOrientationBox->setCurrentIndex(config.lockOrientationIndex); ui->framelessCheck->setChecked(config.framelessWindow); ui->recordScreenCheck->setChecked(config.recordScreen); ui->notDisplayCheck->setChecked(config.recordBackground); ui->useReverseCheck->setChecked(config.reverseConnect); ui->fpsCheck->setChecked(config.showFPS); ui->alwaysTopCheck->setChecked(config.windowOnTop); ui->closeScreenCheck->setChecked(config.autoOffScreen); ui->stayAwakeCheck->setChecked(config.keepAlive); ui->useSingleModeCheck->setChecked(config.simpleMode); ui->autoUpdatecheckBox->setChecked(config.autoUpdateDevice); ui->showToolbar->setChecked(config.showToolbar); } else { UserBootConfig config; config.bitRate = getBitRate(); config.maxSizeIndex = ui->maxSizeBox->currentIndex(); config.recordFormatIndex = ui->formatBox->currentIndex(); config.recordPath = ui->recordPathEdt->text(); config.lockOrientationIndex = ui->lockOrientationBox->currentIndex(); config.recordScreen = ui->recordScreenCheck->isChecked(); config.recordBackground = ui->notDisplayCheck->isChecked(); config.reverseConnect = ui->useReverseCheck->isChecked(); config.showFPS = ui->fpsCheck->isChecked(); config.windowOnTop = ui->alwaysTopCheck->isChecked(); config.autoOffScreen = ui->closeScreenCheck->isChecked(); config.framelessWindow = ui->framelessCheck->isChecked(); config.keepAlive = ui->stayAwakeCheck->isChecked(); config.simpleMode = ui->useSingleModeCheck->isChecked(); config.autoUpdateDevice = ui->autoUpdatecheckBox->isChecked(); config.showToolbar = ui->showToolbar->isChecked(); // 保存当前IP到历史记录 QString currentIp = ui->deviceIpEdt->currentText().trimmed(); if (!currentIp.isEmpty()) { saveIpHistory(currentIp); } Config::getInstance().setUserBootConfig(config); } } void Dialog::execAdbCmd() { if (checkAdbRun()) { return; } QString cmd = ui->adbCommandEdt->text().trimmed(); outLog("adb " + cmd, false); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", Qt::SkipEmptyParts)); #else m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", QString::SkipEmptyParts)); #endif } void Dialog::delayMs(int ms) { QTime dieTime = QTime::currentTime().addMSecs(ms); while (QTime::currentTime() < dieTime) { QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } } QString Dialog::getGameScript(const QString &fileName) { if (fileName.isEmpty()) { return ""; } QFile loadFile(getKeyMapPath() + "/" + fileName); if (!loadFile.open(QIODevice::ReadOnly)) { outLog("open file failed:" + fileName, true); return ""; } QString ret = loadFile.readAll(); loadFile.close(); return ret; } void Dialog::slotActivated(QSystemTrayIcon::ActivationReason reason) { switch (reason) { case QSystemTrayIcon::Trigger: #ifdef Q_OS_WIN32 this->show(); #endif break; default: break; } } void Dialog::closeEvent(QCloseEvent *event) { this->hide(); if (!Config::getInstance().getTrayMessageShown()) { Config::getInstance().setTrayMessageShown(true); m_hideIcon->showMessage(tr("Notice"), tr("Hidden here!"), QSystemTrayIcon::Information, 3000); } event->ignore(); } void Dialog::on_updateDevice_clicked() { if (checkAdbRun()) { return; } outLog("update devices...", false); m_adb.execute("", QStringList() << "devices"); } void Dialog::on_startServerBtn_clicked() { outLog("start server...", false); // this is ok that "original" toUshort is 0 quint16 videoSize = ui->maxSizeBox->currentText().trimmed().toUShort(); qsc::DeviceParams params; params.serial = ui->serialBox->currentText().trimmed(); params.maxSize = videoSize; params.bitRate = getBitRate(); // on devices with Android >= 10, the capture frame rate can be limited params.maxFps = static_cast(Config::getInstance().getMaxFps()); params.closeScreen = ui->closeScreenCheck->isChecked(); params.useReverse = ui->useReverseCheck->isChecked(); params.display = !ui->notDisplayCheck->isChecked(); params.renderExpiredFrames = Config::getInstance().getRenderExpiredFrames(); if (ui->lockOrientationBox->currentIndex() > 0) { params.captureOrientationLock = 1; params.captureOrientation = (ui->lockOrientationBox->currentIndex() - 1) * 90; } params.stayAwake = ui->stayAwakeCheck->isChecked(); params.recordFile = ui->recordScreenCheck->isChecked(); params.recordPath = ui->recordPathEdt->text().trimmed(); params.recordFileFormat = ui->formatBox->currentText().trimmed(); params.serverLocalPath = getServerPath(); params.serverRemotePath = Config::getInstance().getServerPath(); params.pushFilePath = Config::getInstance().getPushFilePath(); params.gameScript = getGameScript(ui->gameBox->currentText()); params.logLevel = Config::getInstance().getLogLevel(); params.codecOptions = Config::getInstance().getCodecOptions(); params.codecName = Config::getInstance().getCodecName(); params.scid = QRandomGenerator::global()->bounded(1, 10000) & 0x7FFFFFFF; qsc::IDeviceManage::getInstance().connectDevice(params); } void Dialog::on_stopServerBtn_clicked() { if (qsc::IDeviceManage::getInstance().disconnectDevice(ui->serialBox->currentText().trimmed())) { outLog("stop server"); } } void Dialog::on_wirelessConnectBtn_clicked() { if (checkAdbRun()) { return; } QString addr = ui->deviceIpEdt->currentText().trimmed(); if (addr.isEmpty()) { outLog("error: device ip is null", false); return; } if (!ui->devicePortEdt->currentText().isEmpty()) { addr += ":"; addr += ui->devicePortEdt->currentText().trimmed(); } else if (!ui->devicePortEdt->lineEdit()->placeholderText().isEmpty()) { addr += ":"; addr += ui->devicePortEdt->lineEdit()->placeholderText().trimmed(); } else { outLog("error: device port is null", false); return; } // 保存IP历史记录 - 只保存IP部分,不包含端口 QString ip = addr.split(":").first(); if (!ip.isEmpty()) { saveIpHistory(ip); } // 保存端口历史记录 QString port = addr.split(":").last(); if (!port.isEmpty() && port != ip) { savePortHistory(port); } outLog("wireless connect...", false); QStringList adbArgs; adbArgs << "connect"; adbArgs << addr; m_adb.execute("", adbArgs); } void Dialog::on_startAdbdBtn_clicked() { if (checkAdbRun()) { return; } outLog("start devices adbd...", false); // adb tcpip 5555 QStringList adbArgs; adbArgs << "tcpip"; adbArgs << "5555"; m_adb.execute(ui->serialBox->currentText().trimmed(), adbArgs); } void Dialog::outLog(const QString &log, bool newLine) { // avoid sub thread update ui QString backLog = log; QTimer::singleShot(0, this, [this, backLog, newLine]() { ui->outEdit->append(backLog); if (newLine) { ui->outEdit->append("
"); } }); } bool Dialog::filterLog(const QString &log) { if (log.contains("app_proces")) { return true; } if (log.contains("Unable to set geometry")) { return true; } return false; } bool Dialog::checkAdbRun() { if (m_adb.isRuning()) { outLog("wait for the end of the current command to run"); } return m_adb.isRuning(); } void Dialog::on_getIPBtn_clicked() { if (checkAdbRun()) { return; } outLog("get ip...", false); // adb -s P7C0218510000537 shell ifconfig wlan0 // or // adb -s P7C0218510000537 shell ip -f inet addr show wlan0 QStringList adbArgs; #if 0 adbArgs << "shell"; adbArgs << "ip"; adbArgs << "-f"; adbArgs << "inet"; adbArgs << "addr"; adbArgs << "show"; adbArgs << "wlan0"; #else adbArgs << "shell"; adbArgs << "ifconfig"; adbArgs << "wlan0"; #endif m_adb.execute(ui->serialBox->currentText().trimmed(), adbArgs); } void Dialog::getIPbyIp() { if (checkAdbRun()) { return; } QStringList adbArgs; adbArgs << "shell"; adbArgs << "ip -o a"; m_adb.execute(ui->serialBox->currentText().trimmed(), adbArgs); } void Dialog::onDeviceConnected(bool success, const QString &serial, const QString &deviceName, const QSize &size) { Q_UNUSED(deviceName); if (!success) { return; } auto videoForm = new VideoForm(ui->framelessCheck->isChecked(), Config::getInstance().getSkin(), ui->showToolbar->isChecked()); videoForm->setSerial(serial); qsc::IDeviceManage::getInstance().getDevice(serial)->setUserData(static_cast(videoForm)); qsc::IDeviceManage::getInstance().getDevice(serial)->registerDeviceObserver(videoForm); videoForm->showFPS(ui->fpsCheck->isChecked()); if (ui->alwaysTopCheck->isChecked()) { videoForm->staysOnTop(); } #ifndef Q_OS_WIN32 // must be show before updateShowSize videoForm->show(); #endif QString name = Config::getInstance().getNickName(serial); if (name.isEmpty()) { name = Config::getInstance().getTitle(); } videoForm->setWindowTitle(name + "-" + serial); videoForm->updateShowSize(size); bool deviceVer = size.height() > size.width(); QRect rc = Config::getInstance().getRect(serial); bool rcVer = rc.height() > rc.width(); // same width/height rate if (rc.isValid() && (deviceVer == rcVer)) { // mark: resize is for fix setGeometry magneticwidget bug videoForm->resize(rc.size()); videoForm->setGeometry(rc); } #ifdef Q_OS_WIN32 // windows是show太早可以看到resize的过程 QTimer::singleShot(200, videoForm, [videoForm](){videoForm->show();}); #endif GroupController::instance().addDevice(serial); } void Dialog::onDeviceDisconnected(QString serial) { GroupController::instance().removeDevice(serial); auto device = qsc::IDeviceManage::getInstance().getDevice(serial); if (!device) { return; } auto data = device->getUserData(); if (data) { VideoForm* vf = static_cast(data); qsc::IDeviceManage::getInstance().getDevice(serial)->deRegisterDeviceObserver(vf); vf->close(); vf->deleteLater(); } } void Dialog::on_wirelessDisConnectBtn_clicked() { if (checkAdbRun()) { return; } QString addr = ui->deviceIpEdt->currentText().trimmed(); outLog("wireless disconnect...", false); QStringList adbArgs; adbArgs << "disconnect"; adbArgs << addr; m_adb.execute("", adbArgs); } void Dialog::on_selectRecordPathBtn_clicked() { QFileDialog::Options options = QFileDialog::DontResolveSymlinks | QFileDialog::ShowDirsOnly; QString directory = QFileDialog::getExistingDirectory(this, tr("select path"), "", options); ui->recordPathEdt->setText(directory); } void Dialog::on_recordPathEdt_textChanged(const QString &arg1) { ui->recordPathEdt->setToolTip(arg1.trimmed()); ui->notDisplayCheck->setCheckable(!arg1.trimmed().isEmpty()); } void Dialog::on_adbCommandBtn_clicked() { execAdbCmd(); } void Dialog::on_stopAdbBtn_clicked() { m_adb.kill(); } void Dialog::on_clearOut_clicked() { ui->outEdit->clear(); } void Dialog::on_stopAllServerBtn_clicked() { qsc::IDeviceManage::getInstance().disconnectAllDevice(); } void Dialog::on_refreshGameScriptBtn_clicked() { ui->gameBox->clear(); QDir dir(getKeyMapPath()); if (!dir.exists()) { outLog("keymap directory not find", true); return; } dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); QFileInfo fileInfo; int size = list.size(); for (int i = 0; i < size; ++i) { fileInfo = list.at(i); ui->gameBox->addItem(fileInfo.fileName()); } } void Dialog::on_applyScriptBtn_clicked() { auto curSerial = ui->serialBox->currentText().trimmed(); auto device = qsc::IDeviceManage::getInstance().getDevice(curSerial); if (!device) { return; } device->updateScript(getGameScript(ui->gameBox->currentText())); } void Dialog::on_recordScreenCheck_clicked(bool checked) { if (!checked) { return; } QString fileDir(ui->recordPathEdt->text().trimmed()); if (fileDir.isEmpty()) { qWarning() << "please select record save path!!!"; ui->recordScreenCheck->setChecked(false); } } void Dialog::on_usbConnectBtn_clicked() { on_stopAllServerBtn_clicked(); delayMs(200); on_updateDevice_clicked(); delayMs(200); int firstUsbDevice = findDeviceFromeSerialBox(false); if (-1 == firstUsbDevice) { qWarning() << "No use device is found!"; return; } ui->serialBox->setCurrentIndex(firstUsbDevice); on_startServerBtn_clicked(); } int Dialog::findDeviceFromeSerialBox(bool wifi) { QString regStr = "\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\:([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\\b"; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QRegExp regIP(regStr); #else QRegularExpression regIP(regStr); #endif for (int i = 0; i < ui->serialBox->count(); ++i) { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) bool isWifi = regIP.exactMatch(ui->serialBox->itemText(i)); #else bool isWifi = regIP.match(ui->serialBox->itemText(i)).hasMatch(); #endif bool found = wifi ? isWifi : !isWifi; if (found) { return i; } } return -1; } void Dialog::on_wifiConnectBtn_clicked() { on_stopAllServerBtn_clicked(); delayMs(200); on_updateDevice_clicked(); delayMs(200); int firstUsbDevice = findDeviceFromeSerialBox(false); if (-1 == firstUsbDevice) { qWarning() << "No use device is found!"; return; } ui->serialBox->setCurrentIndex(firstUsbDevice); on_getIPBtn_clicked(); delayMs(200); on_startAdbdBtn_clicked(); delayMs(1000); on_wirelessConnectBtn_clicked(); delayMs(2000); on_updateDevice_clicked(); delayMs(200); int firstWifiDevice = findDeviceFromeSerialBox(true); if (-1 == firstWifiDevice) { qWarning() << "No wifi device is found!"; return; } ui->serialBox->setCurrentIndex(firstWifiDevice); on_startServerBtn_clicked(); } void Dialog::on_connectedPhoneList_itemDoubleClicked(QListWidgetItem *item) { Q_UNUSED(item); ui->serialBox->setCurrentIndex(ui->connectedPhoneList->currentRow()); on_startServerBtn_clicked(); } void Dialog::on_updateNameBtn_clicked() { if (ui->serialBox->count() != 0) { if (ui->userNameEdt->text().isEmpty()) { Config::getInstance().setNickName(ui->serialBox->currentText(), "Phone"); } else { Config::getInstance().setNickName(ui->serialBox->currentText(), ui->userNameEdt->text()); } on_updateDevice_clicked(); qDebug() << "Update OK!"; } else { qWarning() << "No device is connected!"; } } void Dialog::on_useSingleModeCheck_clicked() { if (ui->useSingleModeCheck->isChecked()) { ui->rightWidget->hide(); } else { ui->rightWidget->show(); } adjustSize(); } void Dialog::on_serialBox_currentIndexChanged(const QString &arg1) { ui->userNameEdt->setText(Config::getInstance().getNickName(arg1)); } quint32 Dialog::getBitRate() { return ui->bitRateEdit->text().trimmed().toUInt() * (ui->bitRateBox->currentText() == QString("Mbps") ? 1000000 : 1000); } const QString &Dialog::getServerPath() { static QString serverPath; if (serverPath.isEmpty()) { serverPath = QString::fromLocal8Bit(qgetenv("QTSCRCPY_SERVER_PATH")); QFileInfo fileInfo(serverPath); if (serverPath.isEmpty() || !fileInfo.isFile()) { serverPath = QCoreApplication::applicationDirPath() + "/scrcpy-server"; } } return serverPath; } void Dialog::on_startAudioBtn_clicked() { if (ui->serialBox->count() == 0) { qWarning() << "No device is connected!"; return; } m_audioOutput.start(ui->serialBox->currentText(), 28200); } void Dialog::on_stopAudioBtn_clicked() { m_audioOutput.stop(); } void Dialog::on_installSndcpyBtn_clicked() { if (ui->serialBox->count() == 0) { qWarning() << "No device is connected!"; return; } m_audioOutput.installonly(ui->serialBox->currentText(), 28200); } void Dialog::on_autoUpdatecheckBox_toggled(bool checked) { if (checked) { m_autoUpdatetimer.start(5000); } else { m_autoUpdatetimer.stop(); } } void Dialog::loadIpHistory() { QStringList ipList = Config::getInstance().getIpHistory(); ui->deviceIpEdt->clear(); ui->deviceIpEdt->addItems(ipList); ui->deviceIpEdt->setContentsMargins(0, 0, 0, 0); if (ui->deviceIpEdt->lineEdit()) { ui->deviceIpEdt->lineEdit()->setMaxLength(128); ui->deviceIpEdt->lineEdit()->setPlaceholderText("192.168.0.1"); } } void Dialog::saveIpHistory(const QString &ip) { if (ip.isEmpty()) { return; } Config::getInstance().saveIpHistory(ip); // 更新ComboBox loadIpHistory(); ui->deviceIpEdt->setCurrentText(ip); } void Dialog::showIpEditMenu(const QPoint &pos) { QMenu *menu = ui->deviceIpEdt->lineEdit()->createStandardContextMenu(); menu->addSeparator(); QAction *clearHistoryAction = new QAction(tr("Clear History"), menu); connect(clearHistoryAction, &QAction::triggered, this, [this]() { Config::getInstance().clearIpHistory(); loadIpHistory(); }); menu->addAction(clearHistoryAction); menu->exec(ui->deviceIpEdt->lineEdit()->mapToGlobal(pos)); delete menu; } void Dialog::loadPortHistory() { QStringList portList = Config::getInstance().getPortHistory(); ui->devicePortEdt->clear(); ui->devicePortEdt->addItems(portList); ui->devicePortEdt->setContentsMargins(0, 0, 0, 0); if (ui->devicePortEdt->lineEdit()) { ui->devicePortEdt->lineEdit()->setMaxLength(6); ui->devicePortEdt->lineEdit()->setPlaceholderText("5555"); } } void Dialog::savePortHistory(const QString &port) { if (port.isEmpty()) { return; } Config::getInstance().savePortHistory(port); // 更新ComboBox loadPortHistory(); ui->devicePortEdt->setCurrentText(port); } void Dialog::showPortEditMenu(const QPoint &pos) { QMenu *menu = ui->devicePortEdt->lineEdit()->createStandardContextMenu(); menu->addSeparator(); QAction *clearHistoryAction = new QAction(tr("Clear History"), menu); connect(clearHistoryAction, &QAction::triggered, this, [this]() { Config::getInstance().clearPortHistory(); loadPortHistory(); }); menu->addAction(clearHistoryAction); menu->exec(ui->devicePortEdt->lineEdit()->mapToGlobal(pos)); delete menu; } ================================================ FILE: QtScrcpy/ui/dialog.h ================================================ #ifndef DIALOG_H #define DIALOG_H #include #include #include #include #include #include #include #include "adbprocess.h" #include "../QtScrcpyCore/include/QtScrcpyCore.h" #include "audio/audiooutput.h" namespace Ui { class Widget; } class QYUVOpenGLWidget; class Dialog : public QWidget { Q_OBJECT public: explicit Dialog(QWidget *parent = 0); ~Dialog(); void outLog(const QString &log, bool newLine = true); bool filterLog(const QString &log); void getIPbyIp(); private slots: void onDeviceConnected(bool success, const QString& serial, const QString& deviceName, const QSize& size); void onDeviceDisconnected(QString serial); void on_updateDevice_clicked(); void on_startServerBtn_clicked(); void on_stopServerBtn_clicked(); void on_wirelessConnectBtn_clicked(); void on_startAdbdBtn_clicked(); void on_getIPBtn_clicked(); void on_wirelessDisConnectBtn_clicked(); void on_selectRecordPathBtn_clicked(); void on_recordPathEdt_textChanged(const QString &arg1); void on_adbCommandBtn_clicked(); void on_stopAdbBtn_clicked(); void on_clearOut_clicked(); void on_stopAllServerBtn_clicked(); void on_refreshGameScriptBtn_clicked(); void on_applyScriptBtn_clicked(); void on_recordScreenCheck_clicked(bool checked); void on_usbConnectBtn_clicked(); void on_wifiConnectBtn_clicked(); void on_connectedPhoneList_itemDoubleClicked(QListWidgetItem *item); void on_updateNameBtn_clicked(); void on_useSingleModeCheck_clicked(); void on_serialBox_currentIndexChanged(const QString &arg1); void on_startAudioBtn_clicked(); void on_stopAudioBtn_clicked(); void on_installSndcpyBtn_clicked(); void on_autoUpdatecheckBox_toggled(bool checked); void showIpEditMenu(const QPoint &pos); private: bool checkAdbRun(); void initUI(); void updateBootConfig(bool toView = true); void execAdbCmd(); void delayMs(int ms); QString getGameScript(const QString &fileName); void slotActivated(QSystemTrayIcon::ActivationReason reason); int findDeviceFromeSerialBox(bool wifi); quint32 getBitRate(); const QString &getServerPath(); void loadIpHistory(); void saveIpHistory(const QString &ip); void loadPortHistory(); void savePortHistory(const QString &port); void showPortEditMenu(const QPoint &pos); protected: void closeEvent(QCloseEvent *event); private: Ui::Widget *ui; qsc::AdbProcess m_adb; QSystemTrayIcon *m_hideIcon; QMenu *m_menu; QAction *m_showWindow; QAction *m_quit; AudioOutput m_audioOutput; QTimer m_autoUpdatetimer; }; #endif // DIALOG_H ================================================ FILE: QtScrcpy/ui/dialog.ui ================================================ Widget 0 0 1293 502 QtScrcpy 0 0 0 0 0 0 0 0 Use Simple Mode false 0 0 Simple Mode false 0 0 WIFI Connect 0 0 USB Connect 0 0 0 Double click to connect: 0 0 auto update true 0 0 0 0 adb 3 5 5 5 5 0 0 adb command: adbCommandEdt 0 0 devices 0 0 execute 0 0 terminate 0 0 clear 0 0 Qt::NoFocus true 0 0 0 0 Start Config 3 5 5 5 5 0 0 0 0 0 0 0 0 bit rate: 0 0 2 0 0 Mbps Mbps Kbps 0 0 max size: 0 0 0 0 0 0 0 0 0 0 record format: 0 0 0 0 lock orientation: 0 0 0 0 0 0 0 0 0 0 record save path: recordPathEdt 0 0 true 0 0 select path false 0 0 0 0 0 0 0 0 0 0 refresh script 0 0 apply 0 0 0 0 0 0 0 0 show fps 0 0 background record false 0 0 always on top false 0 0 record screen 0 0 reverse connection true 0 0 screen-off 0 0 frameless 0 0 stay awake show toolbar 0 0 USB line 0 0 device name: 0 0 0 0 update name false 0 0 0 0 0 0 0 0 device serial: 0 0 0 0 start server false 0 0 stop server false 0 0 0 0 0 0 0 0 stop all server 0 0 refresh devices false 0 0 get device IP false 0 0 start adbd false 0 0 0 0 0 0 install sndcpy 0 0 start audio 0 0 stop audio 0 0 Wireless 3 5 5 5 5 0 0 200 0 true 0 0 : 0 0 100 0 true 0 0 wireless connect false 0 0 wireless disconnect false 0 0 Qt::Vertical QSizePolicy::Expanding 40 20 deviceIpEdt devicePortEdt wirelessConnectBtn wirelessDisConnectBtn adbCommandEdt adbCommandBtn stopAdbBtn clearOut ================================================ FILE: QtScrcpy/ui/toolform.cpp ================================================ #include #include #include #include #include "iconhelper.h" #include "toolform.h" #include "ui_toolform.h" #include "videoform.h" #include "../groupcontroller/groupcontroller.h" ToolForm::ToolForm(QWidget *adsorbWidget, AdsorbPositions adsorbPos) : MagneticWidget(adsorbWidget, adsorbPos), ui(new Ui::ToolForm) { ui->setupUi(this); setWindowFlags(windowFlags() | Qt::FramelessWindowHint); //setWindowFlags(windowFlags() & ~Qt::WindowMinMaxButtonsHint); updateGroupControl(); initStyle(); } ToolForm::~ToolForm() { delete ui; } void ToolForm::setSerial(const QString &serial) { m_serial = serial; } bool ToolForm::isHost() { return m_isHost; } void ToolForm::initStyle() { IconHelper::Instance()->SetIcon(ui->fullScreenBtn, QChar(0xf0b2), 15); IconHelper::Instance()->SetIcon(ui->menuBtn, QChar(0xf096), 15); IconHelper::Instance()->SetIcon(ui->homeBtn, QChar(0xf1db), 15); //IconHelper::Instance()->SetIcon(ui->returnBtn, QChar(0xf104), 15); IconHelper::Instance()->SetIcon(ui->returnBtn, QChar(0xf053), 15); IconHelper::Instance()->SetIcon(ui->appSwitchBtn, QChar(0xf24d), 15); IconHelper::Instance()->SetIcon(ui->volumeUpBtn, QChar(0xf028), 15); IconHelper::Instance()->SetIcon(ui->volumeDownBtn, QChar(0xf027), 15); IconHelper::Instance()->SetIcon(ui->openScreenBtn, QChar(0xf06e), 15); IconHelper::Instance()->SetIcon(ui->closeScreenBtn, QChar(0xf070), 15); IconHelper::Instance()->SetIcon(ui->powerBtn, QChar(0xf011), 15); IconHelper::Instance()->SetIcon(ui->expandNotifyBtn, QChar(0xf103), 15); IconHelper::Instance()->SetIcon(ui->screenShotBtn, QChar(0xf0c4), 15); IconHelper::Instance()->SetIcon(ui->touchBtn, QChar(0xf111), 15); IconHelper::Instance()->SetIcon(ui->groupControlBtn, QChar(0xf0c0), 15); IconHelper::Instance()->SetIcon(ui->clipboardBtn, QChar(0xf0c5), 15); } void ToolForm::updateGroupControl() { if (m_isHost) { ui->groupControlBtn->setStyleSheet("color: red"); } else { ui->groupControlBtn->setStyleSheet("color: green"); } GroupController::instance().updateDeviceState(m_serial); } void ToolForm::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) m_dragPosition = event->globalPos() - frameGeometry().topLeft(); #else m_dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft(); #endif event->accept(); } } void ToolForm::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) } void ToolForm::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) move(event->globalPos() - m_dragPosition); #else move(event->globalPosition().toPoint() - m_dragPosition); #endif event->accept(); } } void ToolForm::showEvent(QShowEvent *event) { Q_UNUSED(event) qDebug() << "show event"; } void ToolForm::hideEvent(QHideEvent *event) { Q_UNUSED(event) qDebug() << "hide event"; } void ToolForm::on_fullScreenBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } dynamic_cast(parent())->switchFullScreen(); } void ToolForm::on_returnBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postGoBack(); } void ToolForm::on_homeBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postGoHome(); } void ToolForm::on_menuBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postGoMenu(); } void ToolForm::on_appSwitchBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postAppSwitch(); } void ToolForm::on_powerBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postPower(); } void ToolForm::on_screenShotBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->screenshot(); } void ToolForm::on_volumeUpBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postVolumeUp(); } void ToolForm::on_volumeDownBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postVolumeDown(); } void ToolForm::on_closeScreenBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->setDisplayPower(false); } void ToolForm::on_expandNotifyBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->expandNotificationPanel(); } void ToolForm::on_touchBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } m_showTouch = !m_showTouch; device->showTouch(m_showTouch); } void ToolForm::on_groupControlBtn_clicked() { m_isHost = !m_isHost; updateGroupControl(); } void ToolForm::on_openScreenBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->setDisplayPower(true); } void ToolForm::on_clipboardBtn_clicked() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->requestDeviceClipboard(); } ================================================ FILE: QtScrcpy/ui/toolform.h ================================================ #ifndef TOOLFORM_H #define TOOLFORM_H #include #include #include "../QtScrcpyCore/include/QtScrcpyCore.h" #include "magneticwidget.h" namespace Ui { class ToolForm; } class Device; class ToolForm : public MagneticWidget { Q_OBJECT public: explicit ToolForm(QWidget *adsorbWidget, AdsorbPositions adsorbPos); ~ToolForm(); void setSerial(const QString& serial); bool isHost(); protected: void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void showEvent(QShowEvent *event); void hideEvent(QHideEvent *event); private slots: void on_fullScreenBtn_clicked(); void on_returnBtn_clicked(); void on_homeBtn_clicked(); void on_menuBtn_clicked(); void on_appSwitchBtn_clicked(); void on_powerBtn_clicked(); void on_screenShotBtn_clicked(); void on_volumeUpBtn_clicked(); void on_volumeDownBtn_clicked(); void on_closeScreenBtn_clicked(); void on_expandNotifyBtn_clicked(); void on_touchBtn_clicked(); void on_groupControlBtn_clicked(); void on_openScreenBtn_clicked(); void on_clipboardBtn_clicked(); private: void initStyle(); void updateGroupControl(); private: Ui::ToolForm *ui; QPoint m_dragPosition; QString m_serial; bool m_showTouch = false; bool m_isHost = false; }; #endif // TOOLFORM_H ================================================ FILE: QtScrcpy/ui/toolform.ui ================================================ ToolForm 0 0 63 537 Tool 30 group control full screen Qt::Vertical 20 40 expand notify touch switch open screen close screen power volume up volume down app switch menu home return screen shot copy clipboard text ================================================ FILE: QtScrcpy/ui/videoform.cpp ================================================ // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN32) #include #endif #include "config.h" #include "iconhelper.h" #include "qyuvopenglwidget.h" #include "toolform.h" #include "mousetap/mousetap.h" #include "ui_videoform.h" #include "videoform.h" VideoForm::VideoForm(bool framelessWindow, bool skin, bool showToolbar, QWidget *parent) : QWidget(parent), ui(new Ui::videoForm), m_skin(skin) { ui->setupUi(this); initUI(); installShortcut(); updateShowSize(size()); bool vertical = size().height() > size().width(); this->show_toolbar = showToolbar; if (m_skin) { updateStyleSheet(vertical); } if (framelessWindow) { setWindowFlags(windowFlags() | Qt::FramelessWindowHint); } } VideoForm::~VideoForm() { delete ui; } void VideoForm::initUI() { if (m_skin) { QPixmap phone; if (phone.load(":/res/phone.png")) { m_widthHeightRatio = 1.0f * phone.width() / phone.height(); } #ifndef Q_OS_OSX // mac下去掉标题栏影响showfullscreen // 去掉标题栏 setWindowFlags(windowFlags() | Qt::FramelessWindowHint); // 根据图片构造异形窗口 setAttribute(Qt::WA_TranslucentBackground); #endif } m_videoWidget = new QYUVOpenGLWidget(); m_videoWidget->hide(); ui->keepRatioWidget->setWidget(m_videoWidget); ui->keepRatioWidget->setWidthHeightRatio(m_widthHeightRatio); m_fpsLabel = new QLabel(m_videoWidget); QFont ft; ft.setPointSize(15); ft.setWeight(QFont::Light); ft.setBold(true); m_fpsLabel->setFont(ft); m_fpsLabel->move(5, 15); m_fpsLabel->setMinimumWidth(100); m_fpsLabel->setStyleSheet(R"(QLabel {color: #00FF00;})"); setMouseTracking(true); m_videoWidget->setMouseTracking(true); ui->keepRatioWidget->setMouseTracking(true); } QRect VideoForm::getGrabCursorRect() { QRect rc; #if defined(Q_OS_WIN32) rc = QRect(ui->keepRatioWidget->mapToGlobal(m_videoWidget->pos()), m_videoWidget->size()); // high dpi support rc.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatioF()); rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatioF()); rc.setX(rc.x() + 10); rc.setY(rc.y() + 10); rc.setWidth(rc.width() - 20); rc.setHeight(rc.height() - 20); #elif defined(Q_OS_OSX) rc = m_videoWidget->geometry(); rc.setTopLeft(ui->keepRatioWidget->mapToGlobal(rc.topLeft())); rc.setBottomRight(ui->keepRatioWidget->mapToGlobal(rc.bottomRight())); rc.setX(rc.x() + 10); rc.setY(rc.y() + 10); rc.setWidth(rc.width() - 20); rc.setHeight(rc.height() - 20); #elif defined(Q_OS_LINUX) rc = QRect(ui->keepRatioWidget->mapToGlobal(m_videoWidget->pos()), m_videoWidget->size()); // high dpi support -- taken from the WIN32 section and untested rc.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatioF()); rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatioF()); rc.setX(rc.x() + 10); rc.setY(rc.y() + 10); rc.setWidth(rc.width() - 20); rc.setHeight(rc.height() - 20); #endif return rc; } const QSize &VideoForm::frameSize() { return m_frameSize; } void VideoForm::resizeSquare() { QRect screenRect = getScreenRect(); if (screenRect.isEmpty()) { qWarning() << "getScreenRect is empty"; return; } resize(screenRect.height(), screenRect.height()); } void VideoForm::removeBlackRect() { resize(ui->keepRatioWidget->goodSize()); } void VideoForm::showFPS(bool show) { if (!m_fpsLabel) { return; } m_fpsLabel->setVisible(show); } void VideoForm::updateRender(int width, int height, uint8_t* dataY, uint8_t* dataU, uint8_t* dataV, int linesizeY, int linesizeU, int linesizeV) { if (m_videoWidget->isHidden()) { if (m_loadingWidget) { m_loadingWidget->close(); } m_videoWidget->show(); } updateShowSize(QSize(width, height)); m_videoWidget->setFrameSize(QSize(width, height)); m_videoWidget->updateTextures(dataY, dataU, dataV, linesizeY, linesizeU, linesizeV); } void VideoForm::setSerial(const QString &serial) { m_serial = serial; } void VideoForm::showToolForm(bool show) { if (!m_toolForm) { m_toolForm = new ToolForm(this, ToolForm::AP_OUTSIDE_RIGHT); m_toolForm->setSerial(m_serial); } m_toolForm->move(pos().x() + geometry().width(), pos().y() + 30); m_toolForm->setVisible(show); } void VideoForm::moveCenter() { QRect screenRect = getScreenRect(); if (screenRect.isEmpty()) { qWarning() << "getScreenRect is empty"; return; } // 窗口居中 move(screenRect.center() - QRect(0, 0, size().width(), size().height()).center()); } void VideoForm::installShortcut() { QShortcut *shortcut = nullptr; // switchFullScreen shortcut = new QShortcut(QKeySequence("Ctrl+f"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } switchFullScreen(); }); // resizeSquare shortcut = new QShortcut(QKeySequence("Ctrl+g"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { resizeSquare(); }); // removeBlackRect shortcut = new QShortcut(QKeySequence("Ctrl+w"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { removeBlackRect(); }); // postGoHome shortcut = new QShortcut(QKeySequence("Ctrl+h"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postGoHome(); }); // postGoBack shortcut = new QShortcut(QKeySequence("Ctrl+b"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postGoBack(); }); // postAppSwitch shortcut = new QShortcut(QKeySequence("Ctrl+s"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->postAppSwitch(); }); // postGoMenu shortcut = new QShortcut(QKeySequence("Ctrl+m"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } device->postGoMenu(); }); // postVolumeUp shortcut = new QShortcut(QKeySequence("Ctrl+up"), this); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->postVolumeUp(); }); // postVolumeDown shortcut = new QShortcut(QKeySequence("Ctrl+down"), this); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->postVolumeDown(); }); // postPower shortcut = new QShortcut(QKeySequence("Ctrl+p"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->postPower(); }); shortcut = new QShortcut(QKeySequence("Ctrl+o"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->setDisplayPower(false); }); // expandNotificationPanel shortcut = new QShortcut(QKeySequence("Ctrl+n"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->expandNotificationPanel(); }); // collapsePanel shortcut = new QShortcut(QKeySequence("Ctrl+Shift+n"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->collapsePanel(); }); // copy shortcut = new QShortcut(QKeySequence("Ctrl+c"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->postCopy(); }); // cut shortcut = new QShortcut(QKeySequence("Ctrl+x"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->postCut(); }); // clipboardPaste shortcut = new QShortcut(QKeySequence("Ctrl+v"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->setDeviceClipboard(); }); // setDeviceClipboard shortcut = new QShortcut(QKeySequence("Ctrl+Shift+v"), this); shortcut->setAutoRepeat(false); connect(shortcut, &QShortcut::activated, this, [this]() { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->clipboardPaste(); }); } QRect VideoForm::getScreenRect() { QRect screenRect; QScreen *screen = QGuiApplication::primaryScreen(); QWidget *win = window(); if (win) { QWindow *winHandle = win->windowHandle(); if (winHandle) { screen = winHandle->screen(); } } if (screen) { screenRect = screen->availableGeometry(); } return screenRect; } void VideoForm::updateStyleSheet(bool vertical) { if (vertical) { setStyleSheet(R"( #videoForm { border-image: url(:/image/videoform/phone-v.png) 150px 65px 85px 65px; border-width: 150px 65px 85px 65px; } )"); } else { setStyleSheet(R"( #videoForm { border-image: url(:/image/videoform/phone-h.png) 65px 85px 65px 150px; border-width: 65px 85px 65px 150px; } )"); } layout()->setContentsMargins(getMargins(vertical)); } QMargins VideoForm::getMargins(bool vertical) { QMargins margins; if (vertical) { margins = QMargins(10, 68, 12, 62); } else { margins = QMargins(68, 12, 62, 10); } return margins; } void VideoForm::updateShowSize(const QSize &newSize) { if (m_frameSize != newSize) { m_frameSize = newSize; m_widthHeightRatio = 1.0f * newSize.width() / newSize.height(); ui->keepRatioWidget->setWidthHeightRatio(m_widthHeightRatio); bool vertical = m_widthHeightRatio < 1.0f ? true : false; QSize showSize = newSize; QRect screenRect = getScreenRect(); if (screenRect.isEmpty()) { qWarning() << "getScreenRect is empty"; return; } if (vertical) { showSize.setHeight(qMin(newSize.height(), screenRect.height() - 200)); showSize.setWidth(showSize.height() * m_widthHeightRatio); } else { showSize.setWidth(qMin(newSize.width(), screenRect.width() / 2)); showSize.setHeight(showSize.width() / m_widthHeightRatio); } if (isFullScreen() && qsc::IDeviceManage::getInstance().getDevice(m_serial)) { switchFullScreen(); } if (isMaximized()) { showNormal(); } if (m_skin) { QMargins m = getMargins(vertical); showSize.setWidth(showSize.width() + m.left() + m.right()); showSize.setHeight(showSize.height() + m.top() + m.bottom()); } if (showSize != size()) { resize(showSize); if (m_skin) { updateStyleSheet(vertical); } moveCenter(); } } } void VideoForm::switchFullScreen() { if (isFullScreen()) { // 横屏全屏铺满全屏,恢复时,恢复保持宽高比 if (m_widthHeightRatio > 1.0f) { ui->keepRatioWidget->setWidthHeightRatio(m_widthHeightRatio); } showNormal(); // back to normal size. resize(m_normalSize); // fullscreen window will move (0,0). qt bug? move(m_fullScreenBeforePos); #ifdef Q_OS_OSX //setWindowFlags(windowFlags() | Qt::FramelessWindowHint); //show(); #endif if (m_skin) { updateStyleSheet(m_frameSize.height() > m_frameSize.width()); } showToolForm(this->show_toolbar); #ifdef Q_OS_WIN32 ::SetThreadExecutionState(ES_CONTINUOUS); #endif } else { // 横屏全屏铺满全屏,不保持宽高比 if (m_widthHeightRatio > 1.0f) { ui->keepRatioWidget->setWidthHeightRatio(-1.0f); } // record current size before fullscreen, it will be used to rollback size after exit fullscreen. m_normalSize = size(); m_fullScreenBeforePos = pos(); // 这种临时增加标题栏再全屏的方案会导致收不到mousemove事件,导致setmousetrack失效 // mac fullscreen must show title bar #ifdef Q_OS_OSX //setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); #endif showToolForm(false); if (m_skin) { layout()->setContentsMargins(0, 0, 0, 0); } showFullScreen(); // 全屏状态禁止电脑休眠、息屏 #ifdef Q_OS_WIN32 ::SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); #endif } } bool VideoForm::isHost() { if (!m_toolForm) { return false; } return m_toolForm->isHost(); } void VideoForm::updateFPS(quint32 fps) { //qDebug() << "FPS:" << fps; if (!m_fpsLabel) { return; } m_fpsLabel->setText(QString("FPS:%1").arg(fps)); } void VideoForm::grabCursor(bool grab) { QRect rc = getGrabCursorRect(); MouseTap::getInstance()->enableMouseEventTap(rc, grab); } void VideoForm::onFrame(int width, int height, uint8_t *dataY, uint8_t *dataU, uint8_t *dataV, int linesizeY, int linesizeU, int linesizeV) { updateRender(width, height, dataY, dataU, dataV, linesizeY, linesizeU, linesizeV); } void VideoForm::staysOnTop(bool top) { bool needShow = false; if (isVisible()) { needShow = true; } setWindowFlag(Qt::WindowStaysOnTopHint, top); if (m_toolForm) { m_toolForm->setWindowFlag(Qt::WindowStaysOnTopHint, top); } if (needShow) { show(); } } void VideoForm::mousePressEvent(QMouseEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (event->button() == Qt::MiddleButton) { if (device && !device->isCurrentCustomKeymap()) { device->postGoHome(); return; } } if (event->button() == Qt::RightButton) { if (device && !device->isCurrentCustomKeymap()) { device->postGoBack(); return; } } #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = event->localPos(); QPointF globalPos = event->globalPos(); #else QPointF localPos = event->position(); QPointF globalPos = event->globalPosition(); #endif if (m_videoWidget->geometry().contains(event->pos())) { if (!device) { return; } QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint()); QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers()); emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size()); // debug keymap pos if (event->button() == Qt::LeftButton) { qreal x = localPos.x() / m_videoWidget->size().width(); qreal y = localPos.y() / m_videoWidget->size().height(); QString posTip = QString(R"("pos": {"x": %1, "y": %2})").arg(x).arg(y); qInfo() << posTip.toStdString().c_str(); } } else { if (event->button() == Qt::LeftButton) { m_dragPosition = globalPos.toPoint() - frameGeometry().topLeft(); event->accept(); } } } void VideoForm::mouseReleaseEvent(QMouseEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (m_dragPosition.isNull()) { if (!device) { return; } #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = event->localPos(); QPointF globalPos = event->globalPos(); #else QPointF localPos = event->position(); QPointF globalPos = event->globalPosition(); #endif // local check QPointF local = m_videoWidget->mapFrom(this, localPos.toPoint()); if (local.x() < 0) { local.setX(0); } if (local.x() > m_videoWidget->width()) { local.setX(m_videoWidget->width()); } if (local.y() < 0) { local.setY(0); } if (local.y() > m_videoWidget->height()) { local.setY(m_videoWidget->height()); } QMouseEvent newEvent(event->type(), local, globalPos, event->button(), event->buttons(), event->modifiers()); emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } else { m_dragPosition = QPoint(0, 0); } } void VideoForm::mouseMoveEvent(QMouseEvent *event) { #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = event->localPos(); QPointF globalPos = event->globalPos(); #else QPointF localPos = event->position(); QPointF globalPos = event->globalPosition(); #endif auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (m_videoWidget->geometry().contains(event->pos())) { if (!device) { return; } QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint()); QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers()); emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } else if (!m_dragPosition.isNull()) { if (event->buttons() & Qt::LeftButton) { move(globalPos.toPoint() - m_dragPosition); event->accept(); } } } void VideoForm::mouseDoubleClickEvent(QMouseEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (event->button() == Qt::LeftButton && !m_videoWidget->geometry().contains(event->pos())) { if (!isMaximized()) { removeBlackRect(); } } if (event->button() == Qt::RightButton && device && !device->isCurrentCustomKeymap()) { emit device->postBackOrScreenOn(event->type() == QEvent::MouseButtonPress); } if (m_videoWidget->geometry().contains(event->pos())) { if (!device) { return; } #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPointF localPos = event->localPos(); QPointF globalPos = event->globalPos(); #else QPointF localPos = event->position(); QPointF globalPos = event->globalPosition(); #endif QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint()); QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers()); emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } } void VideoForm::wheelEvent(QWheelEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) if (m_videoWidget->geometry().contains(event->position().toPoint())) { if (!device) { return; } QPointF pos = m_videoWidget->mapFrom(this, event->position().toPoint()); QWheelEvent wheelEvent( pos, event->globalPosition(), event->pixelDelta(), event->angleDelta(), event->buttons(), event->modifiers(), event->phase(), event->inverted()); #else if (m_videoWidget->geometry().contains(event->pos())) { if (!device) { return; } QPointF pos = m_videoWidget->mapFrom(this, event->pos()); QWheelEvent wheelEvent( pos, event->globalPosF(), event->pixelDelta(), event->angleDelta(), event->delta(), event->orientation(), event->buttons(), event->modifiers(), event->phase(), event->source(), event->inverted()); #endif emit device->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } } void VideoForm::keyPressEvent(QKeyEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } if (Qt::Key_Escape == event->key() && !event->isAutoRepeat() && isFullScreen()) { switchFullScreen(); } emit device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } void VideoForm::keyReleaseEvent(QKeyEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } emit device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } void VideoForm::paintEvent(QPaintEvent *paint) { Q_UNUSED(paint) QStyleOption opt; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) opt.init(this); #else opt.initFrom(this); #endif QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } void VideoForm::showEvent(QShowEvent *event) { Q_UNUSED(event) if (!isFullScreen() && this->show_toolbar) { QTimer::singleShot(500, this, [this](){ showToolForm(this->show_toolbar); }); } } void VideoForm::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) QSize goodSize = ui->keepRatioWidget->goodSize(); if (goodSize.isEmpty()) { return; } QSize curSize = size(); // 限制VideoForm尺寸不能小于keepRatioWidget good size if (m_widthHeightRatio > 1.0f) { // hor if (curSize.height() <= goodSize.height()) { setMinimumHeight(goodSize.height()); } else { setMinimumHeight(0); } } else { // ver if (curSize.width() <= goodSize.width()) { setMinimumWidth(goodSize.width()); } else { setMinimumWidth(0); } } } void VideoForm::closeEvent(QCloseEvent *event) { Q_UNUSED(event) auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } Config::getInstance().setRect(device->getSerial(), geometry()); device->disconnectDevice(); } void VideoForm::dragEnterEvent(QDragEnterEvent *event) { event->acceptProposedAction(); } void VideoForm::dragMoveEvent(QDragMoveEvent *event) { Q_UNUSED(event) } void VideoForm::dragLeaveEvent(QDragLeaveEvent *event) { Q_UNUSED(event) } void VideoForm::dropEvent(QDropEvent *event) { auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial); if (!device) { return; } const QMimeData *qm = event->mimeData(); QList urls = qm->urls(); for (const QUrl &url : urls) { QString file = url.toLocalFile(); QFileInfo fileInfo(file); if (!fileInfo.exists()) { QMessageBox::warning(this, "QtScrcpy", tr("file does not exist"), QMessageBox::Ok); continue; } if (fileInfo.isFile() && fileInfo.suffix() == "apk") { emit device->installApkRequest(file); continue; } emit device->pushFileRequest(file, Config::getInstance().getPushFilePath() + fileInfo.fileName()); } } ================================================ FILE: QtScrcpy/ui/videoform.h ================================================ #ifndef VIDEOFORM_H #define VIDEOFORM_H #include #include #include "../QtScrcpyCore/include/QtScrcpyCore.h" namespace Ui { class videoForm; } class ToolForm; class FileHandler; class QYUVOpenGLWidget; class QLabel; class VideoForm : public QWidget, public qsc::DeviceObserver { Q_OBJECT public: explicit VideoForm(bool framelessWindow = false, bool skin = true, bool showToolBar = true, QWidget *parent = 0); ~VideoForm(); void staysOnTop(bool top = true); void updateShowSize(const QSize &newSize); void updateRender(int width, int height, uint8_t* dataY, uint8_t* dataU, uint8_t* dataV, int linesizeY, int linesizeU, int linesizeV); void setSerial(const QString& serial); QRect getGrabCursorRect(); const QSize &frameSize(); void resizeSquare(); void removeBlackRect(); void showFPS(bool show); void switchFullScreen(); bool isHost(); private: void onFrame(int width, int height, uint8_t* dataY, uint8_t* dataU, uint8_t* dataV, int linesizeY, int linesizeU, int linesizeV) override; void updateFPS(quint32 fps) override; void grabCursor(bool grab) override; void updateStyleSheet(bool vertical); QMargins getMargins(bool vertical); void initUI(); void showToolForm(bool show = true); void moveCenter(); void installShortcut(); QRect getScreenRect(); protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; void paintEvent(QPaintEvent *) override; void showEvent(QShowEvent *event) override; void resizeEvent(QResizeEvent *event) override; void closeEvent(QCloseEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; void dragLeaveEvent(QDragLeaveEvent *event) override; void dropEvent(QDropEvent *event) override; private: // ui Ui::videoForm *ui; QPointer m_toolForm; QPointer m_loadingWidget; QPointer m_videoWidget; QPointer m_fpsLabel; //inside member QSize m_frameSize; QSize m_normalSize; QPoint m_dragPosition; float m_widthHeightRatio = 0.5f; bool m_skin = true; QPoint m_fullScreenBeforePos; QString m_serial; //Whether to display the toolbar when connecting a device. bool show_toolbar = true; }; #endif // VIDEOFORM_H ================================================ FILE: QtScrcpy/ui/videoform.ui ================================================ videoForm 0 0 400 800 true #videoForm { border-image: url(:/res/phone-v.png) 150px 142px 85px 142px; border-width: 150px 142px 85px 142px; } 0 0 0 0 0 KeepRatioWidget QWidget
keepratiowidget.h
1
================================================ FILE: QtScrcpy/uibase/keepratiowidget.cpp ================================================ #include #include #include "keepratiowidget.h" KeepRatioWidget::KeepRatioWidget(QWidget *parent) : QWidget(parent) {} KeepRatioWidget::~KeepRatioWidget() {} void KeepRatioWidget::setWidget(QWidget *w) { if (!w) { return; } w->setParent(this); m_subWidget = w; } void KeepRatioWidget::setWidthHeightRatio(float widthHeightRatio) { if (fabs(m_widthHeightRatio - widthHeightRatio) < 0.000001f) { return; } m_widthHeightRatio = widthHeightRatio; adjustSubWidget(); } const QSize KeepRatioWidget::goodSize() { if (!m_subWidget || m_widthHeightRatio < 0.0f) { return QSize(); } return m_subWidget->size(); } void KeepRatioWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) adjustSubWidget(); } void KeepRatioWidget::adjustSubWidget() { if (!m_subWidget) { return; } QSize curSize = size(); QPoint pos(0, 0); int width = 0; int height = 0; if (m_widthHeightRatio > 1.0f) { // base width width = curSize.width(); height = curSize.width() / m_widthHeightRatio; pos.setY((curSize.height() - height) / 2); } else if (m_widthHeightRatio > 0.0f) { // base height height = curSize.height(); width = curSize.height() * m_widthHeightRatio; pos.setX((curSize.width() - width) / 2); } else { // full widget height = curSize.height(); width = curSize.width(); } m_subWidget->setGeometry(pos.x(), pos.y(), width, height); } ================================================ FILE: QtScrcpy/uibase/keepratiowidget.h ================================================ #ifndef KEEPRATIOWIDGET_H #define KEEPRATIOWIDGET_H #include #include class KeepRatioWidget : public QWidget { Q_OBJECT public: explicit KeepRatioWidget(QWidget *parent = nullptr); ~KeepRatioWidget(); void setWidget(QWidget *w); void setWidthHeightRatio(float widthHeightRatio); const QSize goodSize(); protected: void resizeEvent(QResizeEvent *event); void adjustSubWidget(); private: float m_widthHeightRatio = -1.0f; QPointer m_subWidget; QSize m_goodSize; }; #endif // KEEPRATIOWIDGET_H ================================================ FILE: QtScrcpy/uibase/magneticwidget.cpp ================================================ #include #include #include #include "magneticwidget.h" MagneticWidget::MagneticWidget(QWidget *adsorbWidget, AdsorbPositions adsorbPos) : QWidget(Q_NULLPTR), m_adsorbPos(adsorbPos), m_adsorbWidget(adsorbWidget) { Q_ASSERT(m_adsorbWidget); setParent(m_adsorbWidget); setWindowFlags(windowFlags() | Qt::Tool); m_adsorbWidgetSize = m_adsorbWidget->size(); m_adsorbWidget->installEventFilter(this); } MagneticWidget::~MagneticWidget() { if (m_adsorbWidget) { m_adsorbWidget->removeEventFilter(this); } } bool MagneticWidget::isAdsorbed() { return m_adsorbed; } bool MagneticWidget::eventFilter(QObject *watched, QEvent *event) { if (watched != m_adsorbWidget || !event) { return false; } // 始终记录adsorbWidget最新size if (QEvent::Resize == event->type()) { m_adsorbWidgetSize = m_adsorbWidget->size(); } if (m_adsorbed && QEvent::Move == event->type()) { move(m_adsorbWidget->pos() - m_relativePos); } if (m_adsorbed && (QEvent::Show == event->type() || QEvent::FocusIn == event->type())) { show(); raise(); } if (m_adsorbed && QEvent::Resize == event->type()) { QRect parentRect; QRect targetRect; getGeometry(parentRect, targetRect); QPoint pos(parentRect.left(), parentRect.top()); switch (m_curAdsorbPosition) { case AP_OUTSIDE_LEFT: pos.setX(pos.x() - width()); pos.setY(pos.y() - m_relativePos.y()); break; case AP_OUTSIDE_RIGHT: pos.setX(pos.x() + m_adsorbWidgetSize.width()); pos.setY(pos.y() - m_relativePos.y()); break; case AP_OUTSIDE_TOP: pos.setX(pos.x() - m_relativePos.x()); pos.setY(pos.y() - targetRect.height()); break; case AP_OUTSIDE_BOTTOM: pos.setX(pos.x() - m_relativePos.x()); pos.setY(pos.y() + parentRect.height()); break; case AP_INSIDE_LEFT: pos.setY(pos.y() - m_relativePos.y()); break; case AP_INSIDE_RIGHT: pos.setX(parentRect.right() - targetRect.width()); pos.setY(pos.y() - m_relativePos.y()); break; case AP_INSIDE_TOP: pos.setX(pos.x() - m_relativePos.x()); break; case AP_INSIDE_BOTTOM: pos.setX(pos.x() - m_relativePos.x()); pos.setY(parentRect.bottom() - targetRect.height()); break; default: break; } move(pos); } return false; } void MagneticWidget::moveEvent(QMoveEvent *event) { Q_UNUSED(event) if (!m_adsorbWidget) { return; } QRect parentRect; QRect targetRect; getGeometry(parentRect, targetRect); int parentLeft = parentRect.left(); int parentRight = parentRect.right(); int parentTop = parentRect.top(); int parentBottom = parentRect.bottom(); int targetLeft = targetRect.left(); int targetRight = targetRect.right(); int targetTop = targetRect.top(); int targetBottom = targetRect.bottom(); QPoint finalPosition = pos(); int adsorbDistance = 30; m_adsorbed = false; if (m_adsorbPos & AP_INSIDE_LEFT && parentRect.intersects(targetRect) && qAbs(parentLeft - targetLeft) < adsorbDistance) { finalPosition.setX(parentLeft); m_adsorbed |= true; m_curAdsorbPosition = AP_INSIDE_LEFT; } if (m_adsorbPos & AP_OUTSIDE_RIGHT && parentRect.intersects(targetRect.translated(-adsorbDistance, 0)) && qAbs(parentRight - targetLeft) < adsorbDistance) { finalPosition.setX(parentRight); m_adsorbed |= true; m_curAdsorbPosition = AP_OUTSIDE_RIGHT; } if (m_adsorbPos & AP_OUTSIDE_LEFT && parentRect.intersects(targetRect.translated(adsorbDistance, 0)) && qAbs(parentLeft - targetRight) < adsorbDistance) { finalPosition.setX(parentLeft - targetRect.width()); m_adsorbed |= true; m_curAdsorbPosition = AP_OUTSIDE_LEFT; } if (m_adsorbPos & AP_INSIDE_RIGHT && parentRect.intersects(targetRect) && qAbs(parentRight - targetRight) < adsorbDistance) { finalPosition.setX(parentRight - targetRect.width()); m_adsorbed |= true; m_curAdsorbPosition = AP_INSIDE_RIGHT; } if (m_adsorbPos & AP_INSIDE_TOP && parentRect.intersects(targetRect) && qAbs(parentTop - targetTop) < adsorbDistance) { finalPosition.setY(parentTop); m_adsorbed |= true; m_curAdsorbPosition = AP_INSIDE_TOP; } if (m_adsorbPos & AP_OUTSIDE_TOP && parentRect.intersects(targetRect.translated(0, adsorbDistance)) && qAbs(parentTop - targetBottom) < adsorbDistance) { finalPosition.setY(parentTop - targetRect.height()); m_adsorbed |= true; m_curAdsorbPosition = AP_OUTSIDE_TOP; } if (m_adsorbPos & AP_OUTSIDE_BOTTOM && parentRect.intersects(targetRect.translated(0, -adsorbDistance)) && qAbs(parentBottom - targetTop) < adsorbDistance) { finalPosition.setY(parentBottom); m_adsorbed |= true; m_curAdsorbPosition = AP_OUTSIDE_BOTTOM; } if (m_adsorbPos & AP_INSIDE_BOTTOM && parentRect.intersects(targetRect) && qAbs(parentBottom - targetBottom) < adsorbDistance) { finalPosition.setY(parentBottom - targetRect.height()); m_adsorbed |= true; m_curAdsorbPosition = AP_INSIDE_BOTTOM; } if (m_adsorbed) { m_relativePos = m_adsorbWidget->pos() - pos(); } move(finalPosition); } void MagneticWidget::getGeometry(QRect &relativeWidgetRect, QRect &targetWidgetRect) { relativeWidgetRect.setTopLeft(m_adsorbWidget->pos()); relativeWidgetRect.setWidth(m_adsorbWidgetSize.width()); relativeWidgetRect.setHeight(m_adsorbWidgetSize.height()); targetWidgetRect.setTopLeft(pos()); targetWidgetRect.setWidth(width()); targetWidgetRect.setHeight(height()); } ================================================ FILE: QtScrcpy/uibase/magneticwidget.h ================================================ #ifndef MAGNETICWIDGET_H #define MAGNETICWIDGET_H #include #include /* * a magnetic widget * window title bar support not good */ class MagneticWidget : public QWidget { Q_OBJECT public: enum AdsorbPosition { AP_OUTSIDE_LEFT = 0x01, // 吸附外部左边框 AP_OUTSIDE_TOP = 0x02, // 吸附外部上边框 AP_OUTSIDE_RIGHT = 0x04, // 吸附外部右边框 AP_OUTSIDE_BOTTOM = 0x08, // 吸附外部下边框 AP_INSIDE_LEFT = 0x10, // 吸附内部左边框 AP_INSIDE_TOP = 0x20, // 吸附内部上边框 AP_INSIDE_RIGHT = 0x40, // 吸附内部右边框 AP_INSIDE_BOTTOM = 0x80, // 吸附内部下边框 AP_ALL = 0xFF, // 全吸附 }; Q_DECLARE_FLAGS(AdsorbPositions, AdsorbPosition) public: explicit MagneticWidget(QWidget *adsorbWidget, AdsorbPositions adsorbPos = AP_ALL); ~MagneticWidget(); bool isAdsorbed(); protected: bool eventFilter(QObject *watched, QEvent *event) override; void moveEvent(QMoveEvent *event) override; private: void getGeometry(QRect &relativeWidgetRect, QRect &targetWidgetRect); private: AdsorbPositions m_adsorbPos = AP_ALL; QPoint m_relativePos; bool m_adsorbed = false; QPointer m_adsorbWidget; // 单独记录adsorbWidgetSize,因为Widget setGeometry的时候,会先收到Move事件,后收到Resize事件, // 但是收到Move事件时Widget的size()已经是setGeometry指定的size了 QSize m_adsorbWidgetSize; AdsorbPosition m_curAdsorbPosition; }; Q_DECLARE_OPERATORS_FOR_FLAGS(MagneticWidget::AdsorbPositions) #endif // MAGNETICWIDGET_H ================================================ FILE: QtScrcpy/util/config.cpp ================================================ #include #include #include #include #include "config.h" #ifdef Q_OS_OSX #include "path.h" #endif #define GROUP_COMMON "common" // config #define COMMON_LANGUAGE_KEY "Language" #define COMMON_LANGUAGE_DEF "Auto" #define COMMON_TITLE_KEY "WindowTitle" #define COMMON_TITLE_DEF QCoreApplication::applicationName() #define COMMON_PUSHFILE_KEY "PushFilePath" #define COMMON_PUSHFILE_DEF "/sdcard/" #define COMMON_SERVER_PATH_KEY "ServerPath" #define COMMON_SERVER_PATH_DEF "/data/local/tmp/scrcpy-server.jar" #define COMMON_MAX_FPS_KEY "MaxFps" #define COMMON_MAX_FPS_DEF 0 #define COMMON_DESKTOP_OPENGL_KEY "UseDesktopOpenGL" #define COMMON_DESKTOP_OPENGL_DEF -1 #define COMMON_SKIN_KEY "UseSkin" #define COMMON_SKIN_DEF 1 #define COMMON_RENDER_EXPIRED_FRAMES_KEY "RenderExpiredFrames" #define COMMON_RENDER_EXPIRED_FRAMES_DEF 0 #define COMMON_ADB_PATH_KEY "AdbPath" #define COMMON_ADB_PATH_DEF "" #define COMMON_LOG_LEVEL_KEY "LogLevel" #define COMMON_LOG_LEVEL_DEF "info" #define COMMON_CODEC_OPTIONS_KEY "CodecOptions" #define COMMON_CODEC_OPTIONS_DEF "" #define COMMON_CODEC_NAME_KEY "CodecName" #define COMMON_CODEC_NAME_DEF "" // user config #define COMMON_RECORD_KEY "RecordPath" #define COMMON_RECORD_DEF "" #define COMMON_BITRATE_KEY "BitRate" #define COMMON_BITRATE_DEF 2000000 #define COMMON_MAX_SIZE_INDEX_KEY "MaxSizeIndex" #define COMMON_MAX_SIZE_INDEX_DEF 2 #define COMMON_RECORD_FORMAT_INDEX_KEY "RecordFormatIndex" #define COMMON_RECORD_FORMAT_INDEX_DEF 0 #define COMMON_LOCK_ORIENTATION_INDEX_KEY "LockDirectionIndex" #define COMMON_LOCK_ORIENTATION_INDEX_DEF 0 #define COMMON_RECORD_SCREEN_KEY "RecordScreen" #define COMMON_RECORD_SCREEN_DEF false #define COMMON_RECORD_BACKGROUD_KEY "RecordBackGround" #define COMMON_RECORD_BACKGROUD_DEF false #define COMMON_REVERSE_CONNECT_KEY "ReverseConnect" #define COMMON_REVERSE_CONNECT_DEF true #define COMMON_SHOW_FPS_KEY "ShowFPS" #define COMMON_SHOW_FPS_DEF false #define COMMON_WINDOW_ON_TOP_KEY "WindowOnTop" #define COMMON_WINDOW_ON_TOP_DEF false #define COMMON_AUTO_OFF_SCREEN_KEY "AutoOffScreen" #define COMMON_AUTO_OFF_SCREEN_DEF false #define COMMON_FRAMELESS_WINDOW_KEY "FramelessWindow" #define COMMON_FRAMELESS_WINDOW_DEF false #define COMMON_KEEP_ALIVE_KEY "KeepAlive" #define COMMON_KEEP_ALIVE_DEF false #define COMMON_SIMPLE_MODE_KEY "SimpleMode" #define COMMON_SIMPLE_MODE_DEF false #define COMMON_AUTO_UPDATE_DEVICE_KEY "AutoUpdateDevice" #define COMMON_AUTO_UPDATE_DEVICE_DEF true #define COMMON_TRAY_MESSAGE_SHOWN_KEY "TrayMessageShown" #define COMMON_TRAY_MESSAGE_SHOWN_DEF false #define COMMON_SHOW_TOOLBAR_KEY "showToolbar" #define COMMON_SHOW_TOOLBAR_DEF true // device config #define SERIAL_WINDOW_RECT_KEY_X "WindowRectX" #define SERIAL_WINDOW_RECT_KEY_Y "WindowRectY" #define SERIAL_WINDOW_RECT_KEY_W "WindowRectW" #define SERIAL_WINDOW_RECT_KEY_H "WindowRectH" #define SERIAL_WINDOW_RECT_KEY_DEF -1 #define SERIAL_NICK_NAME_KEY "NickName" #define SERIAL_NICK_NAME_DEF "Phone" // IP history #define IP_HISTORY_KEY "IpHistory" #define IP_HISTORY_DEF "" #define IP_HISTORY_MAX 10 // Port history #define PORT_HISTORY_KEY "PortHistory" #define PORT_HISTORY_DEF "" #define PORT_HISTORY_MAX 10 QString Config::s_configPath = ""; Config::Config(QObject *parent) : QObject(parent) { m_settings = new QSettings(getConfigPath() + "/config.ini", QSettings::IniFormat); m_userData = new QSettings(getConfigPath() + "/userdata.ini", QSettings::IniFormat); #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) m_settings->setIniCodec("UTF-8"); m_userData->setIniCodec("UTF-8"); #endif qDebug()<childGroups(); } Config &Config::getInstance() { static Config config; return config; } const QString &Config::getConfigPath() { if (s_configPath.isEmpty()) { s_configPath = QString::fromLocal8Bit(qgetenv("QTSCRCPY_CONFIG_PATH")); QFileInfo fileInfo(s_configPath); if (s_configPath.isEmpty() || !fileInfo.isDir()) { // default application dir // mac系统当从finder打开app时,默认工作目录不再是可执行程序的目录了,而是"/" // 而Qt的获取工作目录的api都依赖QCoreApplication的初始化,所以使用mac api获取当前目录 #ifdef Q_OS_OSX // get */QtScrcpy.app path s_configPath = Path::GetCurrentPath(); s_configPath += "/Contents/MacOS/config"; #else s_configPath = "config"; #endif } } return s_configPath; } void Config::setUserBootConfig(const UserBootConfig &config) { m_userData->beginGroup(GROUP_COMMON); m_userData->setValue(COMMON_RECORD_KEY, config.recordPath); m_userData->setValue(COMMON_BITRATE_KEY, config.bitRate); m_userData->setValue(COMMON_MAX_SIZE_INDEX_KEY, config.maxSizeIndex); m_userData->setValue(COMMON_RECORD_FORMAT_INDEX_KEY, config.recordFormatIndex); m_userData->setValue(COMMON_FRAMELESS_WINDOW_KEY, config.framelessWindow); m_userData->setValue(COMMON_LOCK_ORIENTATION_INDEX_KEY, config.lockOrientationIndex); m_userData->setValue(COMMON_RECORD_SCREEN_KEY, config.recordScreen); m_userData->setValue(COMMON_RECORD_BACKGROUD_KEY, config.recordBackground); m_userData->setValue(COMMON_REVERSE_CONNECT_KEY, config.reverseConnect); m_userData->setValue(COMMON_SHOW_FPS_KEY, config.showFPS); m_userData->setValue(COMMON_WINDOW_ON_TOP_KEY, config.windowOnTop); m_userData->setValue(COMMON_AUTO_OFF_SCREEN_KEY, config.autoOffScreen); m_userData->setValue(COMMON_KEEP_ALIVE_KEY, config.keepAlive); m_userData->setValue(COMMON_SIMPLE_MODE_KEY, config.simpleMode); m_userData->setValue(COMMON_AUTO_UPDATE_DEVICE_KEY, config.autoUpdateDevice); m_userData->setValue(COMMON_SHOW_TOOLBAR_KEY, config.showToolbar); m_userData->endGroup(); m_userData->sync(); } UserBootConfig Config::getUserBootConfig() { UserBootConfig config; m_userData->beginGroup(GROUP_COMMON); config.recordPath = m_userData->value(COMMON_RECORD_KEY, COMMON_RECORD_DEF).toString(); config.bitRate = m_userData->value(COMMON_BITRATE_KEY, COMMON_BITRATE_DEF).toUInt(); config.maxSizeIndex = m_userData->value(COMMON_MAX_SIZE_INDEX_KEY, COMMON_MAX_SIZE_INDEX_DEF).toInt(); config.recordFormatIndex = m_userData->value(COMMON_RECORD_FORMAT_INDEX_KEY, COMMON_RECORD_FORMAT_INDEX_DEF).toInt(); config.lockOrientationIndex = m_userData->value(COMMON_LOCK_ORIENTATION_INDEX_KEY, COMMON_LOCK_ORIENTATION_INDEX_DEF).toInt(); config.framelessWindow = m_userData->value(COMMON_FRAMELESS_WINDOW_KEY, COMMON_FRAMELESS_WINDOW_DEF).toBool(); config.recordScreen = m_userData->value(COMMON_RECORD_SCREEN_KEY, COMMON_RECORD_SCREEN_DEF).toBool(); config.recordBackground = m_userData->value(COMMON_RECORD_BACKGROUD_KEY, COMMON_RECORD_BACKGROUD_DEF).toBool(); config.reverseConnect = m_userData->value(COMMON_REVERSE_CONNECT_KEY, COMMON_REVERSE_CONNECT_DEF).toBool(); config.showFPS = m_userData->value(COMMON_SHOW_FPS_KEY, COMMON_SHOW_FPS_DEF).toBool(); config.windowOnTop = m_userData->value(COMMON_WINDOW_ON_TOP_KEY, COMMON_WINDOW_ON_TOP_DEF).toBool(); config.autoOffScreen = m_userData->value(COMMON_AUTO_OFF_SCREEN_KEY, COMMON_AUTO_OFF_SCREEN_DEF).toBool(); config.keepAlive = m_userData->value(COMMON_KEEP_ALIVE_KEY, COMMON_KEEP_ALIVE_DEF).toBool(); config.simpleMode = m_userData->value(COMMON_SIMPLE_MODE_KEY, COMMON_SIMPLE_MODE_DEF).toBool(); config.autoUpdateDevice = m_userData->value(COMMON_AUTO_UPDATE_DEVICE_KEY, COMMON_AUTO_UPDATE_DEVICE_DEF).toBool(); config.showToolbar =m_userData->value(COMMON_SHOW_TOOLBAR_KEY,COMMON_SHOW_TOOLBAR_DEF).toBool(); m_userData->endGroup(); return config; } void Config::setTrayMessageShown(bool shown) { m_userData->beginGroup(GROUP_COMMON); m_userData->setValue(COMMON_TRAY_MESSAGE_SHOWN_KEY, shown); m_userData->endGroup(); m_userData->sync(); } bool Config::getTrayMessageShown() { bool shown; m_userData->beginGroup(GROUP_COMMON); shown = m_userData->value(COMMON_TRAY_MESSAGE_SHOWN_KEY, COMMON_TRAY_MESSAGE_SHOWN_DEF).toBool(); m_userData->endGroup(); return shown; } void Config::setRect(const QString &serial, const QRect &rc) { m_userData->beginGroup(serial); m_userData->setValue(SERIAL_WINDOW_RECT_KEY_X, rc.left()); m_userData->setValue(SERIAL_WINDOW_RECT_KEY_Y, rc.top()); m_userData->setValue(SERIAL_WINDOW_RECT_KEY_W, rc.width()); m_userData->setValue(SERIAL_WINDOW_RECT_KEY_H, rc.height()); m_userData->endGroup(); m_userData->sync(); } QRect Config::getRect(const QString &serial) { QRect rc; m_userData->beginGroup(serial); rc.setX(m_userData->value(SERIAL_WINDOW_RECT_KEY_X, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); rc.setY(m_userData->value(SERIAL_WINDOW_RECT_KEY_Y, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); rc.setWidth(m_userData->value(SERIAL_WINDOW_RECT_KEY_W, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); rc.setHeight(m_userData->value(SERIAL_WINDOW_RECT_KEY_H, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); m_userData->endGroup(); return rc; } void Config::setNickName(const QString &serial, const QString &name) { m_userData->beginGroup(serial); m_userData->setValue(SERIAL_NICK_NAME_KEY, name); m_userData->endGroup(); m_userData->sync(); } QString Config::getNickName(const QString &serial) { QString name; m_userData->beginGroup(serial); name = m_userData->value(SERIAL_NICK_NAME_KEY, SERIAL_NICK_NAME_DEF).toString(); m_userData->endGroup(); return name; } int Config::getMaxFps() { int fps = 0; m_settings->beginGroup(GROUP_COMMON); fps = m_settings->value(COMMON_MAX_FPS_KEY, COMMON_MAX_FPS_DEF).toInt(); m_settings->endGroup(); return fps; } int Config::getDesktopOpenGL() { int opengl = 0; m_settings->beginGroup(GROUP_COMMON); opengl = m_settings->value(COMMON_DESKTOP_OPENGL_KEY, COMMON_DESKTOP_OPENGL_DEF).toInt(); m_settings->endGroup(); return opengl; } int Config::getSkin() { // force disable skin return 0; int skin = 1; m_settings->beginGroup(GROUP_COMMON); skin = m_settings->value(COMMON_SKIN_KEY, COMMON_SKIN_DEF).toInt(); m_settings->endGroup(); return skin; } int Config::getRenderExpiredFrames() { int renderExpiredFrames = 1; m_settings->beginGroup(GROUP_COMMON); renderExpiredFrames = m_settings->value(COMMON_RENDER_EXPIRED_FRAMES_KEY, COMMON_RENDER_EXPIRED_FRAMES_DEF).toInt(); m_settings->endGroup(); return renderExpiredFrames; } QString Config::getPushFilePath() { QString pushFile; m_settings->beginGroup(GROUP_COMMON); pushFile = m_settings->value(COMMON_PUSHFILE_KEY, COMMON_PUSHFILE_DEF).toString(); m_settings->endGroup(); return pushFile; } QString Config::getServerPath() { QString serverPath; m_settings->beginGroup(GROUP_COMMON); serverPath = m_settings->value(COMMON_SERVER_PATH_KEY, COMMON_SERVER_PATH_DEF).toString(); m_settings->endGroup(); return serverPath; } QString Config::getAdbPath() { QString adbPath; m_settings->beginGroup(GROUP_COMMON); adbPath = m_settings->value(COMMON_ADB_PATH_KEY, COMMON_ADB_PATH_DEF).toString(); m_settings->endGroup(); return adbPath; } QString Config::getLogLevel() { QString logLevel; m_settings->beginGroup(GROUP_COMMON); logLevel = m_settings->value(COMMON_LOG_LEVEL_KEY, COMMON_LOG_LEVEL_DEF).toString(); m_settings->endGroup(); return logLevel; } QString Config::getCodecOptions() { QString codecOptions; m_settings->beginGroup(GROUP_COMMON); codecOptions = m_settings->value(COMMON_CODEC_OPTIONS_KEY, COMMON_CODEC_OPTIONS_DEF).toString(); m_settings->endGroup(); return codecOptions; } QString Config::getCodecName() { QString codecName; m_settings->beginGroup(GROUP_COMMON); codecName = m_settings->value(COMMON_CODEC_NAME_KEY, COMMON_CODEC_NAME_DEF).toString(); m_settings->endGroup(); return codecName; } QStringList Config::getConnectedGroups() { return m_userData->childGroups(); } void Config::deleteGroup(const QString &serial) { m_userData->remove(serial); } QString Config::getLanguage() { QString language; m_settings->beginGroup(GROUP_COMMON); language = m_settings->value(COMMON_LANGUAGE_KEY, COMMON_LANGUAGE_DEF).toString(); m_settings->endGroup(); return language; } QString Config::getTitle() { QString title; m_settings->beginGroup(GROUP_COMMON); title = m_settings->value(COMMON_TITLE_KEY, COMMON_TITLE_DEF).toString(); m_settings->endGroup(); return title; } void Config::saveIpHistory(const QString &ip) { QStringList ipList = getIpHistory(); // 移除已存在的相同IP(避免重复) ipList.removeAll(ip); // 将新IP添加到开头 ipList.prepend(ip); // 限制历史记录数量 while (ipList.size() > IP_HISTORY_MAX) { ipList.removeLast(); } m_userData->setValue(IP_HISTORY_KEY, ipList); m_userData->sync(); } QStringList Config::getIpHistory() { QStringList ipList = m_userData->value(IP_HISTORY_KEY, IP_HISTORY_DEF).toStringList(); ipList.removeAll(""); return ipList; } void Config::clearIpHistory() { m_userData->remove(IP_HISTORY_KEY); m_userData->sync(); } void Config::savePortHistory(const QString &port) { QStringList portList = getPortHistory(); // 移除已存在的相同Port(避免重复) portList.removeAll(port); // 将新Port添加到开头 portList.prepend(port); // 限制历史记录数量 while (portList.size() > PORT_HISTORY_MAX) { portList.removeLast(); } m_userData->setValue(PORT_HISTORY_KEY, portList); m_userData->sync(); } QStringList Config::getPortHistory() { QStringList portList = m_userData->value(PORT_HISTORY_KEY, PORT_HISTORY_DEF).toStringList(); portList.removeAll(""); return portList; } void Config::clearPortHistory() { m_userData->remove(PORT_HISTORY_KEY); m_userData->sync(); } ================================================ FILE: QtScrcpy/util/config.h ================================================ #ifndef CONFIG_H #define CONFIG_H #include #include #include struct UserBootConfig { QString recordPath = ""; quint32 bitRate = 2000000; int maxSizeIndex = 0; int recordFormatIndex = 0; int lockOrientationIndex = 0; bool recordScreen = false; bool recordBackground = false; bool reverseConnect = true; bool showFPS = false; bool windowOnTop = false; bool autoOffScreen = false; bool framelessWindow = false; bool keepAlive = false; bool simpleMode = false; bool autoUpdateDevice = true; bool showToolbar = true; }; class QSettings; class Config : public QObject { Q_OBJECT public: static Config &getInstance(); // config QString getLanguage(); QString getTitle(); int getMaxFps(); int getDesktopOpenGL(); int getSkin(); int getRenderExpiredFrames(); QString getPushFilePath(); QString getServerPath(); QString getAdbPath(); QString getLogLevel(); QString getCodecOptions(); QString getCodecName(); QStringList getConnectedGroups(); // user data:common void setUserBootConfig(const UserBootConfig &config); UserBootConfig getUserBootConfig(); void setTrayMessageShown(bool shown); bool getTrayMessageShown(); // user data:device void setNickName(const QString &serial, const QString &name); QString getNickName(const QString &serial); void setRect(const QString &serial, const QRect &rc); QRect getRect(const QString &serial); void deleteGroup(const QString &serial); // IP history methods void saveIpHistory(const QString &ip); QStringList getIpHistory(); void clearIpHistory(); // Port history methods void savePortHistory(const QString &port); QStringList getPortHistory(); void clearPortHistory(); private: explicit Config(QObject *parent = nullptr); const QString &getConfigPath(); private: static QString s_configPath; QPointer m_settings; QPointer m_userData; }; #endif // CONFIG_H ================================================ FILE: QtScrcpy/util/mousetap/cocoamousetap.h ================================================ #ifndef COCOAMOUSETAP_H #define COCOAMOUSETAP_H #include #include #include "mousetap.h" struct MouseEventTapData; class QWidget; class CocoaMouseTap : public MouseTap , public QThread { public: CocoaMouseTap(QObject *parent = Q_NULLPTR); virtual ~CocoaMouseTap(); void initMouseEventTap() override; void quitMouseEventTap() override; void enableMouseEventTap(QRect rc, bool enabled) override; protected: void run() override; private: MouseEventTapData *m_tapData = Q_NULLPTR; QSemaphore m_runloopStartedSemaphore; }; #endif // COCOAMOUSETAP_H ================================================ FILE: QtScrcpy/util/mousetap/cocoamousetap.mm ================================================ #import #include #include "cocoamousetap.h" static const CGEventMask movementEventsMask = CGEventMaskBit(kCGEventLeftMouseDragged) | CGEventMaskBit(kCGEventRightMouseDragged) | CGEventMaskBit(kCGEventMouseMoved); static const CGEventMask allGrabbedEventsMask = CGEventMaskBit(kCGEventLeftMouseDown) | CGEventMaskBit(kCGEventLeftMouseUp) | CGEventMaskBit(kCGEventRightMouseDown) | CGEventMaskBit(kCGEventRightMouseUp) | CGEventMaskBit(kCGEventOtherMouseDown) | CGEventMaskBit(kCGEventOtherMouseUp) | CGEventMaskBit(kCGEventLeftMouseDragged) | CGEventMaskBit(kCGEventRightMouseDragged) | CGEventMaskBit(kCGEventMouseMoved); typedef struct MouseEventTapData{ CFMachPortRef tap = Q_NULLPTR; CFRunLoopRef runloop = Q_NULLPTR; CFRunLoopSourceRef runloopSource = Q_NULLPTR; QRect rc; } MouseEventTapData; static CGEventRef Cocoa_MouseTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { Q_UNUSED(proxy); MouseEventTapData *tapdata = (MouseEventTapData*)refcon; switch (type) { case kCGEventTapDisabledByTimeout: { CGEventTapEnable(tapdata->tap, true); return nullptr; } case kCGEventTapDisabledByUserInput: { return nullptr; } default: break; } if (tapdata->rc.isEmpty()) { return event; } NSRect limitWindowRect = NSMakeRect(tapdata->rc.left(), tapdata->rc.top(), tapdata->rc.width(), tapdata->rc.height()); // check rect samll than limit rect NSRect checkWindowRect = NSMakeRect(limitWindowRect.origin.x + 10, limitWindowRect.origin.y + 10, limitWindowRect.size.width - 10, limitWindowRect.size.height - 10); /* This is in CGs global screenspace coordinate system, which has a * flipped Y. */ CGPoint eventLocation = CGEventGetLocation(event); if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), checkWindowRect, NO)) { if (eventLocation.x <= NSMinX(limitWindowRect)) { eventLocation.x = NSMinX(limitWindowRect) + 1.0; } else if (eventLocation.x >= NSMaxX(limitWindowRect)) { eventLocation.x = NSMaxX(limitWindowRect) - 1.0; } if (eventLocation.y <= NSMinY(limitWindowRect)) { eventLocation.y = NSMinY(limitWindowRect) + 1.0; } else if (eventLocation.y >= NSMaxY(limitWindowRect)) { eventLocation.y = NSMaxY(limitWindowRect) - 1.0; } CGWarpMouseCursorPosition(eventLocation); CGAssociateMouseAndMouseCursorPosition(YES); if ((CGEventMaskBit(type) & movementEventsMask) == 0) { /* For click events, we just constrain the event to the window, so * no other app receives the click event. We can't due the same to * movement events, since they mean that our warp cursor above * behaves strangely. */ CGEventSetLocation(event, eventLocation); } } return event; } static void SemaphorePostCallback(CFRunLoopTimerRef timer, void *info) { Q_UNUSED(timer); QSemaphore *runloopStartedSemaphore = (QSemaphore *)info; if (runloopStartedSemaphore) { runloopStartedSemaphore->release(); } } CocoaMouseTap::CocoaMouseTap(QObject *parent) : QThread(parent) { m_tapData = new MouseEventTapData; } CocoaMouseTap::~CocoaMouseTap() { if (m_tapData) { delete m_tapData; m_tapData = Q_NULLPTR; } } void CocoaMouseTap::initMouseEventTap() { if (!m_tapData) { return; } m_tapData->tap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, allGrabbedEventsMask, &Cocoa_MouseTapCallback, m_tapData); if (!m_tapData->tap) { return; } /* Tap starts disabled, until app requests mouse grab */ CGEventTapEnable(m_tapData->tap, false); start(); } void CocoaMouseTap::quitMouseEventTap() { bool status; if (m_tapData == Q_NULLPTR || m_tapData->tap == Q_NULLPTR) { /* event tap was already cleaned up (possibly due to CGEventTapCreate * returning null.) */ return; } /* Ensure that the runloop has been started first. * TODO: Move this to InitMouseEventTap, check for error conditions that can * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of * grabbing the mouse if it fails to Init. */ status = m_runloopStartedSemaphore.tryAcquire(1, 5000); if (status) { /* Then stop it, which will cause Cocoa_MouseTapThread to return. */ CFRunLoopStop(m_tapData->runloop); /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It * releases some of the pointers in tapdata. */ wait(); } } void CocoaMouseTap::enableMouseEventTap(QRect rc, bool enabled) { if (m_tapData && m_tapData->tap) { enabled ? m_tapData->rc = rc : m_tapData->rc = QRect(); CGEventTapEnable(m_tapData->tap, enabled); } } void CocoaMouseTap::run() { /* Tap was created on main thread but we own it now. */ CFMachPortRef eventTap = m_tapData->tap; if (eventTap) { /* Try to create a runloop source we can schedule. */ CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); if (runloopSource) { m_tapData->runloopSource = runloopSource; } else { CFRelease(eventTap); m_runloopStartedSemaphore.release(); /* TODO: Both here and in the return below, set some state in * tapdata to indicate that initialization failed, which we should * check in InitMouseEventTap, after we move the semaphore check * from Quit to Init. */ return; } } else { m_runloopStartedSemaphore.release(); return; } m_tapData->runloop = CFRunLoopGetCurrent(); CFRunLoopAddSource(m_tapData->runloop, m_tapData->runloopSource, kCFRunLoopCommonModes); CFRunLoopTimerContext context{}; context.info = &m_runloopStartedSemaphore; /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */ CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context); CFRunLoopAddTimer(m_tapData->runloop, timer, kCFRunLoopCommonModes); CFRelease(timer); /* Run the event loop to handle events in the event tap. */ CFRunLoopRun(); /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */ if (m_runloopStartedSemaphore.available() < 1) { m_runloopStartedSemaphore.release(); } CFRunLoopRemoveSource(m_tapData->runloop, m_tapData->runloopSource, kCFRunLoopCommonModes); /* Clean up. */ CGEventTapEnable(m_tapData->tap, false); CFRelease(m_tapData->runloopSource); CFRelease(m_tapData->tap); m_tapData->runloopSource = Q_NULLPTR; m_tapData->tap = Q_NULLPTR; return; } ================================================ FILE: QtScrcpy/util/mousetap/mousetap.cpp ================================================ #include #include "mousetap.h" #ifdef Q_OS_WIN32 #include "winmousetap.h" #endif #ifdef Q_OS_OSX #include "cocoamousetap.h" #endif #ifdef Q_OS_LINUX #include "xmousetap.h" #endif MouseTap *MouseTap::s_instance = Q_NULLPTR; MouseTap *MouseTap::getInstance() { if (s_instance == Q_NULLPTR) { #ifdef Q_OS_WIN32 s_instance = new WinMouseTap(); #endif #ifdef Q_OS_OSX s_instance = new CocoaMouseTap(); #endif #ifdef Q_OS_LINUX s_instance = new XMouseTap(); #endif } return s_instance; } ================================================ FILE: QtScrcpy/util/mousetap/mousetap.h ================================================ #ifndef MOUSETAP_H #define MOUSETAP_H #include class QWidget; class MouseTap { public: static MouseTap *getInstance(); virtual void initMouseEventTap() = 0; virtual void quitMouseEventTap() = 0; // rc base global screenspace coordinate system, which has a flipped Y. virtual void enableMouseEventTap(QRect rc, bool enabled) = 0; private: static MouseTap *s_instance; }; #endif // MOUSETAP_H ================================================ FILE: QtScrcpy/util/mousetap/winmousetap.cpp ================================================ #include #include #include #include "winmousetap.h" WinMouseTap::WinMouseTap() {} WinMouseTap::~WinMouseTap() {} void WinMouseTap::initMouseEventTap() {} void WinMouseTap::quitMouseEventTap() {} void WinMouseTap::enableMouseEventTap(QRect rc, bool enabled) { if (enabled && rc.isEmpty()) { return; } if (enabled) { RECT mainRect; mainRect.left = (LONG)rc.left(); mainRect.right = (LONG)rc.right(); mainRect.top = (LONG)rc.top(); mainRect.bottom = (LONG)rc.bottom(); ClipCursor(&mainRect); } else { ClipCursor(Q_NULLPTR); } } ================================================ FILE: QtScrcpy/util/mousetap/winmousetap.h ================================================ #ifndef WINMOUSETAP_H #define WINMOUSETAP_H #include "mousetap.h" class WinMouseTap : public MouseTap { public: WinMouseTap(); virtual ~WinMouseTap(); void initMouseEventTap() override; void quitMouseEventTap() override; void enableMouseEventTap(QRect rc, bool enabled) override; }; #endif // WINMOUSETAP_H ================================================ FILE: QtScrcpy/util/mousetap/xmousetap.cpp ================================================ #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include #else #include #endif #include #include #include #include "xmousetap.h" XMouseTap::XMouseTap() {} XMouseTap::~XMouseTap() {} void XMouseTap::initMouseEventTap() {} void XMouseTap::quitMouseEventTap() {} static void find_grab_window_recursive(xcb_connection_t *dpy, xcb_window_t window, QRect rc, int16_t offset_x, int16_t offset_y, xcb_window_t *grab_window, uint32_t *grab_window_size) { xcb_query_tree_cookie_t tree_cookie; xcb_query_tree_reply_t *tree; tree_cookie = xcb_query_tree(dpy, window); tree = xcb_query_tree_reply(dpy, tree_cookie, NULL); xcb_window_t *children = xcb_query_tree_children(tree); for (int i = 0; i < xcb_query_tree_children_length(tree); i++) { xcb_get_geometry_cookie_t gg_cookie; xcb_get_geometry_reply_t *gg; gg_cookie = xcb_get_geometry(dpy, children[i]); gg = xcb_get_geometry_reply(dpy, gg_cookie, NULL); if (gg->x + offset_x <= rc.left() && gg->x + offset_x + gg->width >= rc.right() && gg->y + offset_y <= rc.top() && gg->y + offset_y + gg->height >= rc.bottom()) { if (!*grab_window || gg->width * gg->height <= *grab_window_size) { *grab_window = children[i]; *grab_window_size = gg->width * gg->height; } } find_grab_window_recursive(dpy, children[i], rc, gg->x + offset_x, gg->y + offset_y, grab_window, grab_window_size); free(gg); } free(tree); } void XMouseTap::enableMouseEventTap(QRect rc, bool enabled) { if (enabled && rc.isEmpty()) { return; } xcb_connection_t *dpy = QX11Info::connection(); if (enabled) { // We grab the top-most smallest window xcb_window_t grab_window = 0; uint32_t grab_window_size = 0; find_grab_window_recursive(dpy, QX11Info::appRootWindow(QX11Info::appScreen()), rc, 0, 0, &grab_window, &grab_window_size); if (grab_window) { xcb_grab_pointer_cookie_t grab_cookie; xcb_grab_pointer_reply_t *grab; grab_cookie = xcb_grab_pointer(dpy, /* owner_events = */ 1, grab_window, /* event_mask = */ 0, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, grab_window, XCB_NONE, XCB_CURRENT_TIME); grab = xcb_grab_pointer_reply(dpy, grab_cookie, NULL); free(grab); } } else { xcb_void_cookie_t ungrab_cookie; xcb_generic_error_t *error; ungrab_cookie = xcb_ungrab_pointer_checked(dpy, XCB_CURRENT_TIME); error = xcb_request_check(dpy, ungrab_cookie); free(error); } } ================================================ FILE: QtScrcpy/util/mousetap/xmousetap.h ================================================ #ifndef XMOUSETAP_H #define XMOUSETAP_H #include "mousetap.h" class XMouseTap : public MouseTap { public: XMouseTap(); virtual ~XMouseTap(); void initMouseEventTap() override; void quitMouseEventTap() override; void enableMouseEventTap(QRect rc, bool enabled) override; }; #endif // XMOUSETAP_H ================================================ FILE: QtScrcpy/util/path.h ================================================ #pragma once class Path { public: static const char* GetCurrentPath(); }; ================================================ FILE: QtScrcpy/util/path.mm ================================================ #include "path.h" #import const char* Path::GetCurrentPath() { return [[[NSBundle mainBundle] bundlePath] UTF8String]; } ================================================ FILE: QtScrcpy/util/winutils.cpp ================================================ #include #include #include #pragma comment(lib, "dwmapi") #include "winutils.h" enum : WORD { DwmwaUseImmersiveDarkMode = 20, DwmwaUseImmersiveDarkModeBefore20h1 = 19 }; WinUtils::WinUtils(){}; WinUtils::~WinUtils(){}; // Set dark border to window // Reference: qt/qtbase.git/tree/src/plugins/platforms/windows/qwindowswindow.cpp bool WinUtils::setDarkBorderToWindow(const HWND &hwnd, const bool &d) { const BOOL darkBorder = d ? TRUE : FALSE; const bool ok = SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, &darkBorder, sizeof(darkBorder))) || SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeBefore20h1, &darkBorder, sizeof(darkBorder))); if (!ok) qWarning("%s: Unable to set dark window border.", __FUNCTION__); return ok; } ================================================ FILE: QtScrcpy/util/winutils.h ================================================ #ifndef WINUTILS_H #define WINUTILS_H #include #include class WinUtils { public: WinUtils(); ~WinUtils(); static bool setDarkBorderToWindow(const HWND &hwnd, const bool &d); }; #endif // WINUTILS_H ================================================ FILE: README.md ================================================ # QtScrcpy [![Financial Contributors to Open Collective](https://opencollective.com/QtScrcpy/all/badge.svg?label=financial+contributors)](https://opencollective.com/QtScrcpy) ![Windows](https://github.com/barry-ran/QtScrcpy/workflows/Windows/badge.svg) ![MacOS](https://github.com/barry-ran/QtScrcpy/workflows/MacOS/badge.svg) ![Ubuntu](https://github.com/barry-ran/QtScrcpy/workflows/Ubuntu/badge.svg) ![license](https://img.shields.io/badge/license-Apache2.0-blue.svg) ![release](https://img.shields.io/github/v/release/barry-ran/QtScrcpy.svg) ![star](https://img.shields.io/github/stars/barry-ran/QtScrcpy.svg) [中文用户?点我查看中文介绍](README_zh.md) QtScrcpy supports displaying and controlling Android devices via USB or over network. It does NOT require root privileges. It supports three major platforms: GNU/Linux, Windows and macOS. It focuses on: - **lightness** (displays only the device screen) - **performance** (30~60 fps) - **quality** (1920×1080 or above) - **low latency** ([35~70ms][lowlatency]) - **low startup time** (only about 1 second to display the first frame) - **non-intrusiveness** (nothing will be installed on the device) [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 ![win](screenshot/win-en.png) ![mac](screenshot/mac-en.png) ![linux](screenshot/linux-en.png) ## The author has developed a more professional screen casting software called `QuickMirror` QuickMirror function&features: - Equipment screen casting&control: batch screen casting, individual control, batch control - Group management - WiFi screen mirroring/OTG screen mirroring - Adb shell shortcut command - File transfer, apk installation - Multiple screen mirroring: In OTG mirroring mode, with low resolution and smoothness settings, a single computer can manage 500+phones simultaneously - Low latency: USB screen mirroring 1080p latency is within 30ms, which is lower than all screen mirroring software on the market in terms of latency at the same resolution and smoothness - Low CPU usage: pure C++development, high-performance GPU video rendering - High resolution: adjustable, maximum support for native resolution of Android terminals - Perfect Chinese input: Supports Xianyu app, supports Samsung phones - The free version can cast up to 10 screens, with unlimited functionality (except for automatic screen mirroring) - QuickMirror tutorial: https://lrbnfell4p.feishu.cn/docx/EMkvdfIvDowy3UxsXUCcpPV8nDh - QuickMirror Telegram communication group: https://t.me/+EnQNmb47C_liYmRl - Preview of QuickMirror Interface: ![quickmirror](docs/image/quickmirror.png) ## Mapping Keys You can write your script to map keyboard and mouse actions to touches and clicks of the mobile phone according to your needs. [Here](docs/KeyMapDes.md) are the script writing rules. Script for TikTok and some other games are provided by default. Once enabled, you can play the game with your keyboard and mouse. The default key mapping for PUBG Mobile is as follows: ![game](screenshot/game.png) Instruction for adding new customized mapping files. - Write a customized script and put it in the `keymap` directory - Click `refresh script` to show it - Select your script - Connect to your phone, start service and click `apply` - Press `~` key (the SwitchKey in the key map script) to switch to custom mapping mode - Press the ~ key again to switch back to normal mode - (For games such as PUBG Mobile) If you want to move vehicles with the STEER_WHEEL keys, you need to set the move mode to `single rocker mode`. If you don't know how to manually write mapping rules, you can also use the `QuickAssistant` developed by the author QuickAssistant Features&Functions: - Play Android mobile games smoothly through keyboard and mouse - Interface based editing of key mapping script - Support pausing the computer screen and using only keyboard and mouse operations - Screenshot&Recording of Mobile Screen - Simple batch control - Android 11+supports playing mobile audio on computers (under development...) - Mobile app installation free - Fast and instant connection - Low latency: USB screen mirroring 1080p latency is within 30ms, which is lower than all screen mirroring software on the market in terms of latency at the same resolution and smoothness - Low CPU usage: pure C++development, high-performance GPU video rendering - High resolution: adjustable, maximum support for native resolution of Android terminals - Telegram Group:https://t.me/+Ylf_5V_rDCMyODQ1 - [QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh) ## Group control You can control all your phones at the same time. ![group-control-demo](docs/image/group-control.gif) ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=barry-ran/QtScrcpy&type=Date)](https://star-history.com/#barry-ran/QtScrcpy&Date) ## Thanks QtScrcpy is based on [Genymobile](https://github.com/Genymobile)'s [scrcpy](https://github.com/Genymobile/scrcpy) project. Thanks a lot! The difference between QtScrcpy and the original scrcpy is as follows: key points|scrcpy|QtScrcpy --|:--:|:--: ui|sdl|qt video encode|ffmpeg|ffmpeg video render|sdl|opengl cross-platform|self implemented|provided by Qt language|C|C++ style|sync|async keymap|no custom keymap|support custom keymap build|meson+gradle|qmake or CMake - It's very easy to customize your GUI with Qt - Asynchronous programming of Qt-based signal slot mechanism improves performance - Easy to learn - Add support for multi-touch ## Learn If you are interested in it and want to learn how it works but do not know how to get started, you can choose to purchase my recorded video lessons. It details the development architecture and the development process of the entire software and helps you develop QtScrcpy from scratch. Course introduction:[https://blog.csdn.net/rankun1/article/details/87970523](https://blog.csdn.net/rankun1/article/details/87970523) You can join Telegram Group for QtScrcpy and exchange ideas with like-minded friends.: Telegram Group:https://t.me/+EnQNmb47C_liYmRl ## Requirements Android API >= 21 (Android 5.0). Make sure you have enabled [ADB debugging][enable-adb] on your device(s). [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling ## Download [gitee-download]: https://gitee.com/Barryda/QtScrcpy/releases [github-download]: https://github.com/barry-ran/QtScrcpy/releases ### Windows On Windows, for simplicity, prebuilt archives with all the dependencies (including ADB) are available at Releases: - [`QtScrcpy`][github-download] or you can [build it yourself](#Build) ### Mac OS On Mac OS, for simplicity, prebuilt archives with all the dependencies (including ADB) are available at Releases: - [`QtScrcpy`][github-download] or you can [build it yourself](#Build) ### Linux For Arch Linux Users, you can use AUR to install: `yay -Syu qtscrcpy` (may be outdated; maintainer: [yochananmarqos](https://aur.archlinux.org/account/yochananmarqos)) For users in other distros, you can use the prebuilt archives from Releases: - [`QtScrcpy`][github-download] or you can get it at [GitHub Actions](https://github.com/barry-ran/QtScrcpy/actions/workflows/ubuntu.yml), in branch `dev` and download the latest artifact. or you can [build it yourself](#Build) (not recommended, get it in Actions if you can) ## Run Connect to your Android device on your computer, then run the program and click `USB connect` or `WiFi connect` ### Wireless connection steps (ensure that the mobile phone and PC are on the same LAN): 1. Enable USB debugging in developer options on the Android device 2. Connect the Android device to the computer via USB 3. Click update device, and you will see that the device number is updated 4. Click get device IP 5. Click start adbd 6. Click wireless connect 7. Click update device again, and another device with an IP address will be found. Select this device. 8. Click start service Note: it is not necessary to keep your Android device connected via USB after you start adbd. ## Interface button introduction: - Start config: function parameter settings before starting the service You can set the bit rate, resolution, recording format, and video save path of the locally recorded video. - Background record: the Android device screen is not displayed after starting the service. It is recorded in the background. - Always on top: the video window for Android devices will be kept on the top - Close screen: automatically turn off the Android device screen to save power after starting the service - Reverse connection: service startup mode. You can uncheck it if you experience connection failure with a message `more than one device` - Refresh devices: Refresh the currently connected device - Start service: connect to the Android device - Stop service: disconnect from the Android device - Stop all services: disconnect all connected Android devices - Get device IP: Get the IP address of the Android device and update it to the "Wireless" area for the ease of wireless connection setting. - Start adbd: Start the adbd service of the Android device. You must start it before the wireless connection. - Wireless connect: Connect to Android devices wirelessly - Wireless disconnect: Disconnect wirelessly connected Android devices - adb command: execute customized ADB commands (blocking commands are not supported now, such as a shell) ## The main function - Display Android device screens in real-time - Real-time mouse and keyboard control of Android devices - Screen recording - Screenshot to png - Wireless connection - Supports multiple device connections - Full-screen display - Display on the top - Install apk: drag and drop apk to the video window to install - Transfer files: Drag files to the video window to send files to Android devices - Background recording: record only, no display interface - Copy-paste It is possible to synchronize clipboards between the computer and the device, in both directions: - `Ctrl + c` copies the device clipboard to the computer clipboard; - `Ctrl + Shift + v` copies the computer clipboard to the device clipboard; - `Ctrl + v` _pastes_ the computer clipboard as a sequence of text events (non-ASCII characters does not yet work). - Group control - Sync device speaker sound to the computer (based on [sndcpy](https://github.com/rom1v/sndcpy), Android 10+ only) ## Shortcuts | Action | Shortcut (Windows) | Shortcut (macOS) | -------------------------------------- |:----------------------------- |:----------------------------- | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` | Resize window to remove black borders | `Ctrl`+`w` \| _Double-click¹_ | `Cmd`+`w` \| _Double-click¹_ | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` | Power on | _Right-click²_ | _Right-click²_ | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | Copy to clipboard³ | `Ctrl`+`c` | `Cmd`+`c` | Cut to clipboard³ | `Ctrl`+`x` | `Cmd`+`x` | Synchronize clipboards and paste³ | `Ctrl`+`v` | `Cmd`+`v` | Inject computer clipboard text | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ _³Only on Android >= 7._ ## TODO [TODO](docs/TODO.md) ## FAQ [FAQ](docs/FAQ.md) ## DEVELOP [DEVELOP](docs/DEVELOP.md) Everyone is welcome to maintain this project and contribute your own code, but please follow these requirements: 1. Please open PRs to the dev branch instead of the master branch 2. Please rebase the original project before opening PRs 3. Please submit PRs on the principle of "small amounts, many times" (one PR for a change is recommended) 4. Please keep the code style consistent with the existing style. ## Why develop QtScrcpy? There are several reasons listed below according to importance (high to low). 1. In the process of learning Qt, I need a real project to try. 2. I have some background skills in audio and video and I am interested in them. 3. I have some Android development skills. But I have used it for a long time. I want to consolidate it. 4. I found scrcpy and decided to re-make it with the new technology stack (C++ + Qt + Opengl + FFmpeg). ## Build All the dependencies are provided and it is easy to compile. ### QtScrcpy #### Non-Arch Linux Users 1. Set up the Qt development environment with the official Qt installer or third-party tools such as [aqt](https://github.com/miurahr/aqtinstall) on the target platform. Qt version bigger than 5.12 is required. (use MSVC 2019 on Windows) 2. Clone the project with `git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git` 3. For Windows, open CMakeLists.txt with QtCreator and compile Release 4. For Linux, directly run `./ci/linux/build_for_linux.sh "Release"` Note: compiled artifacts are located at `output/x64/Release` #### Arch Linux Users 1. Install packages: `base-devel cmake qt5-base qt5-multimedia qt5-x11extras` (`qtcreator` is recommended) 2. Clone the project with `git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git` 3. Run `./ci/linux/build_for_linux.sh "Release"` ### Scrcpy-Server 1. Set up Android development environment on the target platform 2. Open server project in project root with Android Studio 3. The first time you open it, if you do not have the corresponding version of Gradle, you will be prompted to find Gradle, whether to upgrade Gradle or create it. Select Cancel. After cancelling, you will be prompted to select the location of existing Gradle. Cancel it too and it will download automatically. 4. After compiling the apk, rename it to scrcpy-server and replace QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server. ## Licence Since it is based on scrcpy, it uses the same license as scrcpy Copyright (C) 2025 Rankun 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. ## About the author [Barry CSDN](https://blog.csdn.net/rankun1) An ordinary programmer, working mainly in C++ for desktop client development, graduated from Shandong for more than a year of steel simulation education software, and later moved to Shanghai to work in security, online education-related fields, familiar with audio and video. I have an understanding of audio and video fields such as voice calls, live education, video conferencing and other related solutions. I also have experience in Android, Linux servers and other kinds of development. ## Contributors ### Code Contributors This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. ### Financial Contributors Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/QtScrcpy/contribute)] #### Individuals #### Organizations Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/QtScrcpy/contribute)] ================================================ FILE: README_zh.md ================================================ # QtScrcpy ![Windows](https://github.com/barry-ran/QtScrcpy/workflows/Windows/badge.svg) ![MacOS](https://github.com/barry-ran/QtScrcpy/workflows/MacOS/badge.svg) ![Ubuntu](https://github.com/barry-ran/QtScrcpy/workflows/Ubuntu/badge.svg) ![license](https://img.shields.io/badge/license-Apache2.0-blue.svg) ![release](https://img.shields.io/github/v/release/barry-ran/QtScrcpy.svg) ![star](https://img.shields.io/github/stars/barry-ran/QtScrcpy.svg) ![star](https://gitcode.com/barry-ran/QtScrcpy/star/badge.svg) [Speaks English? Click me for English introduction.](README.md) QtScrcpy 可以通过 USB / 网络连接Android设备,并进行显示和控制。无需root权限。 同时支持 GNU/Linux ,Windows 和 MacOS 三大主流桌面平台。 它专注于: - **精致** (仅显示设备屏幕) - **性能** (30~60fps) - **质量** (1920×1080以上) - **低延迟** ([35~70ms][低延迟]) - **快速启动** (1s 内就可以看到第一帧图像) - **非侵入性** (不在设备上安装任何软件) [低延迟]: https://github.com/Genymobile/scrcpy/pull/646 ![win](screenshot/win-zh.png) ![mac](screenshot/mac-zh.png) ![linux](screenshot/linux-zh.png) ## 作者开发了更加专业的投屏软件`极限投屏` 极限投屏功能&特点: - 设备投屏&控制:批量投屏、单个控制、批量控制 - 分组管理 - wifi投屏/OTG投屏 - adb shell快捷指令 - 文件传输、apk安装 - 投屏数量多:在OTG投屏模式,设置分辨率和流畅度为低的情况下,单台电脑可以同时管理500+台手机 - 低延迟:usb投屏1080p延迟在30ms以内,在相同分辨率流畅度情况下,比市面上所有投屏软件延迟都低 - cpu占用率低:纯C++开发,高性能GPU视频渲染 - 高分辨率:可调节,最大支持安卓终端的原生分辨率 - 完美中文输入:支持闲鱼app,支持三星手机 - 免费版最多投屏10台,功能无限制(除了自动重新投屏) - 极限投屏使用教程:https://lrbnfell4p.feishu.cn/docx/QRMhd9nImorAGgxVLlmczxSdnYf - 极限投屏qq交流群:822464342 - 极限投屏界面预览: ![quickmirror](docs/image/quickmirror.png) ## 自定义按键映射 可以根据需要,自己编写脚本将键盘按键映射为手机的触摸点击,编写规则在[这里](docs/KeyMapDes_zh.md)。 默认自带了针对和平精英手游和抖音进行键鼠映射的映射脚本,开启平精英手游后可以用键鼠像玩端游一样玩和平精英手游,开启抖音映射以后可以使用上下左右方向键模拟上下左右滑动,你也可以按照[编写规则](docs/KeyMapDes_zh.md)编写其他游戏的映射文件,默认按键映射如下: ![game](screenshot/game.png) 自定义按键映射操作方法如下: - 编写自定义脚本放入 keymap 目录 - 点击刷新脚本,确保脚本可以被检测到 - 选择需要的脚本 - 连接手机并启动服务之后,点击应用脚本 - 按`~`(即脚本中定义的 SwitchKey)键切换为自定义映射模式即可启用 - 再次按~键切换为正常控制模式 - (对于和平精英等游戏)若想使用方向盘控制载具,记得在载具设置中设置为单摇杆模式 如果不会自己手写映射规则,也可以去使用作者开发的`极限手游助手` 极限手游助手功能&特点: - 通过键盘鼠标畅玩安卓手机游戏 - 按键映射脚本界面化编辑 - 支持暂停电脑端画面,只使用键鼠操作 - 截图&录制手机画面 - 简单批量控制 - 安卓11+支持电脑播放手机音频(开发中...) - 手机端免安装App - 极速秒连接 - 低延迟:usb投屏1080p延迟在30ms以内,在相同分辨率流畅度情况下,比市面上所有投屏软件延迟都低 - cpu占用率低:纯C++开发,高性能GPU视频渲染 - 高分辨率:可调节,最大支持安卓终端的原生分辨率 - [QQ交流群:901736468](https://qm.qq.com/q/wRJJaWLWc8) - [极限手游助手说明文档](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh) ## 批量操作 你可以同时控制所有的手机 ## Star历史 [![Star History Chart](https://api.star-history.com/svg?repos=barry-ran/QtScrcpy&type=Date)](https://star-history.com/#barry-ran/QtScrcpy&Date) ![gc](docs/image/group-control.gif) ## 感谢 基于[Genymobile](https://github.com/Genymobile)的[scrcpy](https://github.com/Genymobile/scrcpy)项目进行复刻,重构,非常感谢。 ## 比较 QtScrcpy 和 Scrcpy 区别如下: 关键点|scrcpy|QtScrcpy --|:--:|:--: 界面|sdl|qt 视频解码|ffmpeg|ffmpeg 视频渲染|sdl|opengl 跨平台基础设施|自己封装|Qt 编程语言|C|C++ 编程方式|同步|异步 按键映射|不支持自定义|支持自定义按键映射 编译方式|Meson+Gradle|CMake - 使用Qt可以非常容易的定制自己的界面 - 基于Qt的信号槽机制的异步编程提高性能 - 方便新手学习 - 增加多点触控支持 ## 学习它 如果你对它感兴趣,想学习它的实现原理而又感觉无从下手,可以选择购买我录制的视频课程, 里面详细介绍了整个软件的开发架构以及开发流程,带你从无到有的开发 QtScrcpy: 课程介绍:[https://blog.csdn.net/rankun1/article/details/87970523](https://blog.csdn.net/rankun1/article/details/87970523) 或者你也可以加入我的 QtScrcpy QQ 群,和志同道合的朋友一块互相交流技术: QQ群号:901736468 ## 要求 Android 部分至少需要 API 21(Android 5.0)。 您要确保在 Android 设备上[启用adb调试][enable-adb]。 [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling ## 下载 [gitee-download]: https://gitee.com/Barryda/QtScrcpy/releases [github-download]: https://github.com/barry-ran/QtScrcpy/releases ### Windows Windows 平台,你可以直接使用我编译好的可执行程序: - [国内下载][gitee-download] - [国外下载][github-download] 你也可以[自己编译](##编译) ### Mac OS Mac OS 平台,你可以直接使用我编译好的可执行程序: - [国内下载][gitee-download] - [国外下载][github-download] 你也可以[自己编译](##编译) ### Linux 对于 Arch Linux 用户,可以使用 AUR 安装:`yay -Syu qtscrcpy`(可能版本并非最新;维护者:[yochananmarqos](https://aur.archlinux.org/account/yochananmarqos)) 其他发行版的用户可以直接使用我编译好的可执行程序: - [国外下载][github-download] 你也可以从 [GitHub Actions](https://github.com/UjhhgtgTeams/QtScrcpy/actions/workflows/ubuntu.yml) 获取最新的自动编译好的软件 当然,你也可以[自己编译](##编译)(不推荐,需要准备环境) 目前只在 Ubuntu 和 Arch Linux 上测试过编译过程 ## 运行 在你的电脑上接入Android设备,然后运行程序,点击 `一键USB连接` 或者 `一键WIFI连接` ### 无线连接步骤 1. 将手机和电脑连接到同一局域网 2. 安卓手机端在开发者选项中打开 USB 调试 3. 通过 USB 连接安卓手机到电脑 4. 点击刷新设备,会看到有设备号更新出来 5. 点击获取设备 IP 6. 点击启动 adbd 7. 无线连接 8. 再次点击刷新设备,发现多出了一个 IP 地址开头的设备,选择这个设备 9. 启动服务 备注:启动 adbd 以后无需继续连接 USB 线,以后连接断开都不再需要,除非 adbd 停止运行 ## 界面解释 - 启动配置:启动服务前的功能参数设置 分别可以设置本地录制视频的比特率、分辨率、录制格式、录像保存路径等。 - 仅后台录制:启动服务不显示界面,只录制 Android 设备屏幕 - 窗口置顶:Android 设备显示窗口置顶 - 自动息屏:启动服务以后,自动关闭 Android 设备屏幕以节省电量 - 使用 Reverse:服务启动模式,出现服务启动失败报错 "more than one device" 可以去掉这个勾选尝试连接 - 刷新设备列表:刷新当前连接的设备 - 启动服务:连接到 Android 设备 - 停止服务:断开与 Android 设备的连接 - 停止所有服务:断开所有已连接的 Android 设备 - 获取设备ip:获取到 Android 设备的 IP 地址,更新到无线区域中,方便进行无线连接 - 启动adbd:启动 Android 设备的 adbd 服务,无线连接之前,必须要启动 - 无线连接:使用无线方式连接 Android 设备 - 无线断开:断开无线方式连接的 Android 设备 - 命令行:执行自定义 adb 命令(目前不支持阻塞命令,例如shell) ## 功能 - 实时显示 Android 设备屏幕 - 实时键鼠控制Android设备 - 屏幕录制 - 截图 - 无线连接 - 多设备连接与批量操作 - 全屏显示 - 窗口置顶 - 安装 apk:拖拽apk到显示窗口即可安装 - 传输文件:拖拽文件到显示窗口即可发送文件到 Android 设备 - 后台录制:只录制屏幕,不显示界面 - 剪贴板同步: 在计算机和设备之间同步剪贴板: - `Ctrl + c`将设备剪贴板复制到计算机剪贴板; - `Ctrl + Shift + v`将计算机剪贴板复制到设备剪贴板; - `Ctrl + v` 将计算机剪贴板作为一系列文本事件发送到设备(不支持非ASCII字符) - 同步设备扬声器声音到电脑(基于[sndcpy](https://github.com/rom1v/sndcpy),仅支持安卓10级以上,目前不推荐使用,可使用蓝牙连接替代) ## 快捷键 | 功能 | 快捷键(Windows) | 快捷键 (macOS) | -------------------------------------- |:----------------------------- |:----------------------------- | 切换全屏 | `Ctrl`+`f` | `Cmd`+`f` | 调整窗口大小为 1:1 | `Ctrl`+`g` | `Cmd`+`g` | 调整窗口大小去除黑边 | `Ctrl`+`w` \| _左键双击_ | `Cmd`+`w` \| _左键双击_ | 点击 `主页` | `Ctrl`+`h` \| _点击鼠标中键_ | `Ctrl`+`h` \| _点击鼠标中键_ | 点击 `BACK` | `Ctrl`+`b` \| _右键双击_ | `Cmd`+`b` \| _右键双击_ | 点击 `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` | 点击 `MENU` | `Ctrl`+`m` | `Ctrl`+`m` | 点击 `VOLUME_UP` | `Ctrl`+`↑` _(上)_ | `Cmd`+`↑` _(上)_ | 点击 `VOLUME_DOWN` | `Ctrl`+`↓` _(下)_ | `Cmd`+`↓` _(下)_ | 点击 `POWER` | `Ctrl`+`p` | `Cmd`+`p` | 打开电源 | _右键双击_ | _右键双击_ | 关闭屏幕 (保持投屏) | `Ctrl`+`o` | `Cmd`+`o` | 打开下拉菜单 | `Ctrl`+`n` | `Cmd`+`n` | 关闭下拉菜单 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | 复制到剪切板 | `Ctrl`+`c` | `Cmd`+`c` | 剪切到剪切板 | `Ctrl`+`x` | `Cmd`+`x` | 同步剪切板并粘贴 | `Ctrl`+`v` | `Cmd`+`v` | 注入电脑剪切板文本 | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` 鼠标左键双击黑色区域可以去除黑色区域 如果电源关闭,鼠标右键双击打开电源;如果电源开启,鼠标右键双击相当于返回 ## TODO [后期计划](docs/TODO.md) ## FAQ [常见问题说明](docs/FAQ.md) ## 开发者 [开发相关](docs/DEVELOP.md) 欢迎大家一起维护这个项目,贡献自己的代码,不过请遵循以下几点要求: 1. PR 请推向 dev 分支,不要推向 master 分支 2. 提交 PR 之前请先变基原项目 3. PR 请以少量多次的原则提交(即一个功能点提交一个 PR) 4. 代码风格请保持和原有风格一致 ## 为什么开发 QtScrcpy? 综合起来有以下几个原因,比重从大到小排列: 1. 学习Qt的过程中需要一个项目实战一下 2. 本身具有音视频相关技能,对音视频很感兴趣 3. 本身具有 Android 开发技能,好久没用有点生疏,需要巩固一下 4. 发现了 Scrcpy,决定用新的技术栈(C++ + Qt + Opengl + FFmpeg)进行复刻 ## 编译 尽量提供了所有依赖资源,方便傻瓜式编译。 ### QtScrcpy #### 非 Arch Linux 1. 使用官方 Qt Installer 或非官方工具(如 [aqt](https://github.com/miurahr/aqtinstall))在目标平台上搭建Qt开发环境。 需要 5.12 以上版本 Qt(在 Windows 上使用 MSVC 2019) 2. 克隆该项目:`git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git` 3. Windows 使用 QtCreator 打开项目下 CMakeLists.txt 并编译 Release 4. Linux 用终端执行 `./ci/linux/build_for_linux.sh "Release"` 注:编译结果位于 `output/x64/Release` 中 #### Arch Linux 1. 安装以下包:`qt5-base qt5-multimedia qt5-x11extras`(推荐安装 `qtcreator`) 2. 克隆该项目:`git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git` 3. 用终端执行 `./ci/linux/build_for_linux.sh "Release"` 注:编译结果位于 `output/x64/Release` 中 ### Scrcpy-Server 1. 目标平台上搭建 Android 开发环境 2. 使用 Android Studio 打开项目根目录中的 server 3. 第一次打开时,如果你没有对应版本的 Gradle,Studio 会提示找不到 Gradle,是否升级 Gradle 并创建,选择取消,取消后会提示选择 Gradle 的位置,同样取消即可。Studio 会随后自动下载。 4. 按需编辑代码 5. 编译出 apk 以后改名为 scrcpy-server 并替换 `third_party/scrcpy-server` 即可 ## Licence 由于是复刻的 Scrcpy,尊重它的 Licence Copyright (C) 2025 Rankun 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. ## 关于作者 [Barry 的 CSDN](https://blog.csdn.net/rankun1) 一枚普通的程序员,工作中主要使用 C++ 进行桌面客户端开发,一毕业在山东做过一年多钢铁仿真教育软件,后来转战上海先后从事安防,在线教育相关领域工作,对音视频比较熟悉,对音视频领域如语音通话,直播教育,视频会议等相关解决方案有所了解。同时具有Android,Linux服务器等开发经验。 ================================================ FILE: backup/myconfig.sh ================================================ ./configure --disable-everything --disable-x86asm --prefix=../ffmpeg_build \ --enable-shared --enable-static \ --enable-decoder=h264 --enable-parser=h264 --enable-demuxer=h264 \ --enable-muxer=mp4 --enable-protocol=file ================================================ FILE: ci/generate-version.py ================================================ import sys import os if __name__ == '__main__': p = os.popen('git rev-list --tags --max-count=1') commit = p.read() p.close() p = os.popen('git describe --tags ' + commit) tag = p.read() p.close() # print('get tag:', tag) version = str(tag[1:]) version_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../QtScrcpy/appversion")) file=open(version_file, 'w') file.write(version) file.close() sys.exit(0) ================================================ FILE: ci/linux/build_for_linux.sh ================================================ echo --------------------------------------------------------------- echo Check \& Set Environment Variables echo --------------------------------------------------------------- # Get Qt path # ENV_QT_PATH example: /home/barry/Qt5.9.6/5.9.6 echo Current ENV_QT_PATH: $ENV_QT_PATH echo Current directory: $(pwd) # Set variables qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5 qt_gcc_path=$ENV_QT_PATH/gcc_64 export PATH=$qt_gcc_path/bin:$PATH # Remember working directory old_cd=$(pwd) # Set working dir to the script's path (go up two levels from ci/linux/ to project root) cd $(dirname "$0")/../../ echo echo echo --------------------------------------------------------------- echo Check Build Parameters echo --------------------------------------------------------------- echo Possible build modes: Debug/Release/MinSizeRel/RelWithDebInfo build_mode="$1" if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then echo "error: unknown build mode, exiting......" exit 1 fi echo Current build mode: $build_mode echo echo echo --------------------------------------------------------------- echo CMake Build Begins echo --------------------------------------------------------------- # Remove output folder output_path=./output if [ -d "$output_path" ]; then rm -rf $output_path fi cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode" cmake $cmake_params . if [ $? -ne 0 ] ;then echo "error: CMake failed, exiting......" exit 1 fi cmake --build . --config "$build_mode" -j8 if [ $? -ne 0 ] ;then echo "error: CMake build failed, exiting......" exit 1 fi echo echo echo --------------------------------------------------------------- echo CMake Build Succeeded echo --------------------------------------------------------------- # Resume current directory cd $old_cd exit 0 ================================================ FILE: ci/linux/package_appimage.sh ================================================ #!/bin/bash echo "Package AppImage" build_mode="$1" if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then echo "error: unknown build mode, exiting......" exit 1 fi # Qt path detection detected_qt_path="" if command -v qmake &> /dev/null; then qmake_path=$(which qmake) if [ -n "$qmake_path" ]; then qt_base=$(dirname "$(dirname "$(dirname "$qmake_path")")") if [ -d "$qt_base/gcc_64" ]; then detected_qt_path="$qt_base" fi fi fi if [ -n "$detected_qt_path" ]; then ENV_QT_PATH="$detected_qt_path" elif [ -n "$ENV_QT_PATH" ]; then if [ ! -d "$ENV_QT_PATH/gcc_64" ]; then detected_qt_path="" fi fi if [ -z "$ENV_QT_PATH" ] || [ ! -d "$ENV_QT_PATH/gcc_64" ]; then common_qt_paths=( "$HOME/Qt" "/opt/Qt" "/usr/local/Qt" "/usr/lib/qt5" ) for base_path in "${common_qt_paths[@]}"; do if [ -d "$base_path" ]; then latest_version=$(ls -1td "$base_path"/*/gcc_64 2>/dev/null | head -n 1 | sed 's|/gcc_64$||') if [ -n "$latest_version" ] && [ -d "$latest_version/gcc_64" ]; then ENV_QT_PATH="$latest_version" break fi fi done fi if [ ! -d "$ENV_QT_PATH/gcc_64" ]; then echo "error: Qt installation not found at $ENV_QT_PATH/gcc_64" exit 1 fi echo "Using Qt: $ENV_QT_PATH" script_dir=$(cd $(dirname "$0") && pwd) project_root=$(cd "$script_dir/../.." && pwd) old_cd=$(pwd) cd "$project_root" output_path="./output/x64/$build_mode" appimage_output_path="./output/appimage" appdir_path="$appimage_output_path/QtScrcpy.AppDir" app_name="QtScrcpy" app_version=$(cat QtScrcpy/appversion 2>/dev/null || echo "0.0.0") echo "Build mode: $build_mode" echo "App version: $app_version" if [ ! -f "$output_path/$app_name" ]; then echo "error: $app_name executable not found in $output_path" exit 1 fi # Clean previous build if [ -d "$appimage_output_path" ]; then rm -rf "$appimage_output_path" fi mkdir -p "$appimage_output_path" mkdir -p "$appdir_path/usr/bin" mkdir -p "$appdir_path/usr/lib" mkdir -p "$appdir_path/usr/share/applications" mkdir -p "$appdir_path/usr/share/icons/hicolor/"{16x16,24x24,32x32,48x48,64x64,128x128,256x256}"/apps" mkdir -p "$appdir_path/usr/share/metainfo" # Copy executable and resources cp "$output_path/$app_name" "$appdir_path/usr/bin/$app_name" chmod +x "$appdir_path/usr/bin/$app_name" if [ -f "$output_path/sndcpy.sh" ]; then cp "$output_path/sndcpy.sh" "$appdir_path/usr/bin/" chmod +x "$appdir_path/usr/bin/sndcpy.sh" fi if [ -f "$output_path/sndcpy.apk" ]; then cp "$output_path/sndcpy.apk" "$appdir_path/usr/bin/" fi if [ -d "$project_root/keymap" ]; then cp -r "$project_root/keymap" "$appdir_path/usr/share/" fi if [ -d "$project_root/config" ]; then cp -r "$project_root/config" "$appdir_path/usr/share/" fi # Copy ADB and scrcpy-server adb_source="$project_root/QtScrcpy/QtScrcpyCore/src/third_party/adb/linux/adb" server_source="$project_root/QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server" mkdir -p "$appdir_path/usr/lib/qtscrcpy" if [ -f "$adb_source" ]; then cp "$adb_source" "$appdir_path/usr/lib/qtscrcpy/adb" chmod +x "$appdir_path/usr/lib/qtscrcpy/adb" # Create symlink for sndcpy.sh compatibility if [ ! -f "$appdir_path/usr/bin/adb" ]; then ln -sf "../lib/qtscrcpy/adb" "$appdir_path/usr/bin/adb" fi fi if [ -f "$server_source" ]; then cp "$server_source" "$appdir_path/usr/lib/qtscrcpy/scrcpy-server" chmod +x "$appdir_path/usr/lib/qtscrcpy/scrcpy-server" fi # Process icon icon_file="" icon_source="" target_icon_path="$appdir_path/usr/share/icons/hicolor/256x256/apps/$app_name.png" if [ -f "$project_root/QtScrcpy/res/QtScrcpy.png" ]; then icon_source="$project_root/QtScrcpy/res/QtScrcpy.png" elif [ -f "$project_root/QtScrcpy/res/image/tray/logo.png" ]; then icon_source="$project_root/QtScrcpy/res/image/tray/logo.png" elif [ -f "$project_root/QtScrcpy/res/QtScrcpy.ico" ]; then icon_source="$project_root/QtScrcpy/res/QtScrcpy.ico" fi if [ -n "$icon_source" ] && [ -f "$icon_source" ]; then need_resize=false if command -v identify &> /dev/null; then icon_size=$(identify -format "%wx%h" "$icon_source" 2>/dev/null) [ "$icon_size" = "256x256" ] || need_resize=true elif command -v magick &> /dev/null; then icon_size=$(magick identify -format "%wx%h" "$icon_source" 2>/dev/null) [ "$icon_size" = "256x256" ] || need_resize=true elif [[ "$icon_source" == *.png ]]; then cp "$icon_source" "$target_icon_path" icon_file="$target_icon_path" else need_resize=true fi if [ "$need_resize" = true ]; then if command -v convert &> /dev/null; then convert "$icon_source" -resize 256x256 "$target_icon_path" && icon_file="$target_icon_path" elif command -v magick &> /dev/null; then magick "$icon_source" -resize 256x256 "$target_icon_path" && icon_file="$target_icon_path" elif command -v ffmpeg &> /dev/null; then ffmpeg -i "$icon_source" -vf scale=256:256 -y "$target_icon_path" 2>/dev/null && icon_file="$target_icon_path" else cp "$icon_source" "$target_icon_path" icon_file="$target_icon_path" fi fi fi if [ -z "$icon_file" ]; then if command -v convert &> /dev/null; then convert -size 256x256 xc:transparent "$target_icon_path" 2>/dev/null elif command -v magick &> /dev/null; then magick -size 256x256 xc:transparent "$target_icon_path" 2>/dev/null else touch "$target_icon_path" fi icon_file="$target_icon_path" fi if [ -n "$icon_file" ] && [ -f "$icon_file" ]; then icon_sizes=(16 24 32 48 64 128 256) for size in "${icon_sizes[@]}"; do icon_size_path="$appdir_path/usr/share/icons/hicolor/${size}x${size}/apps/$app_name.png" if command -v convert &> /dev/null; then convert "$icon_file" -resize ${size}x${size} "$icon_size_path" 2>/dev/null || cp "$icon_file" "$icon_size_path" elif command -v magick &> /dev/null; then magick "$icon_file" -resize ${size}x${size} "$icon_size_path" 2>/dev/null || cp "$icon_file" "$icon_size_path" else cp "$icon_file" "$icon_size_path" 2>/dev/null || true fi done fi # Create desktop file cat > "$appdir_path/usr/share/applications/$app_name.desktop" << EOF [Desktop Entry] Type=Application Name=QtScrcpy Comment=Display and control Android devices via USB or over network Exec=$app_name Icon=$app_name Categories=Utility; Terminal=false StartupNotify=true EOF # Create metainfo file app_id="com.github.barry-ran.QtScrcpy" cat > "$appdir_path/usr/share/metainfo/$app_name.appdata.xml" << EOF $app_id QtScrcpy Display and control Android devices via USB or over network

QtScrcpy supports displaying and controlling Android devices via USB or over network. It does NOT require root privileges.

$app_name $app_name.desktop https://github.com/barry-ran/QtScrcpy CC0-1.0 Apache-2.0
EOF # Create AppRun script IS_DOCKER_OR_CI=false if [ -f "/.dockerenv" ] || [ -n "${GITHUB_ACTIONS:-}" ] || [ -n "${CI:-}" ]; then IS_DOCKER_OR_CI=true fi if [ "$IS_DOCKER_OR_CI" = true ]; then cat > "$appdir_path/AppRun" << 'APPRUN_EOF' #!/bin/bash HERE="$(dirname "$(readlink -f "${0}")")" APPIMAGE_LIB_DIRS="$HERE/usr/lib:$HERE/usr/lib/x86_64-linux-gnu" export LD_LIBRARY_PATH="$APPIMAGE_LIB_DIRS:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib" if [ -f "$HERE/usr/lib/libQt5XcbQpa.so.5" ]; then export LD_PRELOAD="$HERE/usr/lib/libQt5XcbQpa.so.5" fi QT_PLUGINS_DIR="$HERE/usr/plugins" if [ -d "$QT_PLUGINS_DIR" ]; then export QT_PLUGIN_PATH="$QT_PLUGINS_DIR" export QT_QPA_PLATFORM_PLUGIN_PATH="$QT_PLUGINS_DIR/platforms" fi export QTSCRCPY_ADB_PATH="$HERE/usr/lib/qtscrcpy/adb" export QTSCRCPY_SERVER_PATH="$HERE/usr/lib/qtscrcpy/scrcpy-server" export QTSCRCPY_KEYMAP_PATH="$HERE/usr/share/keymap" export QTSCRCPY_CONFIG_PATH="$HERE/usr/share/config" exec "$HERE/usr/bin/QtScrcpy" "$@" APPRUN_EOF else cat > "$appdir_path/AppRun" << 'APPRUN_EOF' #!/bin/bash HERE="$(dirname "$(readlink -f "${0}")")" export QTSCRCPY_ADB_PATH="$HERE/usr/lib/qtscrcpy/adb" export QTSCRCPY_SERVER_PATH="$HERE/usr/lib/qtscrcpy/scrcpy-server" export QTSCRCPY_KEYMAP_PATH="$HERE/usr/share/keymap" export QTSCRCPY_CONFIG_PATH="$HERE/usr/share/config" exec "$HERE/usr/bin/QtScrcpy" "$@" APPRUN_EOF fi chmod +x "$appdir_path/AppRun" # Download linuxdeploy tools linuxdeploy_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" linuxdeploy_qt_url="https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" linuxdeploy_temp_dir="$appimage_output_path/.tools" linuxdeploy_path="$linuxdeploy_temp_dir/linuxdeploy.AppImage" linuxdeploy_qt_path="$linuxdeploy_temp_dir/linuxdeploy-plugin-qt.AppImage" mkdir -p "$linuxdeploy_temp_dir" if [ ! -f "$linuxdeploy_path" ]; then wget "$linuxdeploy_url" -O "$linuxdeploy_path" || exit 1 chmod +x "$linuxdeploy_path" fi if [ ! -f "$linuxdeploy_qt_path" ]; then wget "$linuxdeploy_qt_url" -O "$linuxdeploy_qt_path" || exit 1 chmod +x "$linuxdeploy_qt_path" fi linuxdeploy_path_abs=$(cd "$(dirname "$linuxdeploy_path")" && pwd)/$(basename "$linuxdeploy_path") linuxdeploy_qt_path_abs=$(cd "$(dirname "$linuxdeploy_qt_path")" && pwd)/$(basename "$linuxdeploy_qt_path") if [ ! -f "$linuxdeploy_path_abs" ] || [ ! -f "$linuxdeploy_qt_path_abs" ]; then echo "error: linuxdeploy tools not found" exit 1 fi linuxdeploy_path="$linuxdeploy_path_abs" linuxdeploy_qt_path="$linuxdeploy_qt_path_abs" export QMAKE="$ENV_QT_PATH/gcc_64/bin/qmake" export QML_SOURCES_PATHS="$project_root/QtScrcpy" export DEPLOY_CMD="$linuxdeploy_path" export PATH="$ENV_QT_PATH/gcc_64/bin:$PATH" # Pre-copy Qt plugins and libraries for CI environment if [ "$IS_DOCKER_OR_CI" = true ]; then qt_plugins_source="$ENV_QT_PATH/gcc_64/plugins" qt_plugins_target="$appdir_path/usr/plugins" qt_libs_source="$ENV_QT_PATH/gcc_64/lib" qt_libs_target="$appdir_path/usr/lib" if [ -d "$qt_plugins_source/platforms" ]; then mkdir -p "$qt_plugins_target/platforms" cp -r "$qt_plugins_source/platforms"/* "$qt_plugins_target/platforms/" 2>/dev/null || true fi if [ -d "$qt_libs_source" ]; then mkdir -p "$qt_libs_target" for lib in "libQt5XcbQpa.so.5" "libQt5XcbQpa.so"; do if [ -f "$qt_libs_source/$lib" ]; then cp "$qt_libs_source/$lib" "$qt_libs_target/" 2>/dev/null || true fi done fi fi # Run linuxdeploy cd "$project_root" export LINUXDEPLOY_PLUGIN_QT_PATH="$linuxdeploy_qt_path" linuxdeploy_args=( --appdir "$appdir_path" --plugin qt --output appimage --executable "$appdir_path/usr/bin/$app_name" --desktop-file "$appdir_path/usr/share/applications/$app_name.desktop" ) if [ -n "$icon_file" ] && [ -f "$icon_file" ]; then linuxdeploy_args+=(--icon-file "$icon_file") fi "$linuxdeploy_path" "${linuxdeploy_args[@]}" || { if [ -f "$appdir_path/usr/share/metainfo/$app_name.appdata.xml" ]; then mv "$appdir_path/usr/share/metainfo/$app_name.appdata.xml" "$appdir_path/usr/share/metainfo/$app_name.appdata.xml.bak" if "$linuxdeploy_path" "${linuxdeploy_args[@]}"; then rm -f "$appdir_path/usr/share/metainfo/$app_name.appdata.xml.bak" else mv "$appdir_path/usr/share/metainfo/$app_name.appdata.xml.bak" "$appdir_path/usr/share/metainfo/$app_name.appdata.xml" linuxdeploy_args_no_plugin=( --appdir "$appdir_path" --output appimage --executable "$appdir_path/usr/bin/$app_name" --desktop-file "$appdir_path/usr/share/applications/$app_name.desktop" ) [ -n "$icon_file" ] && [ -f "$icon_file" ] && linuxdeploy_args_no_plugin+=(--icon-file "$icon_file") "$linuxdeploy_path" "${linuxdeploy_args_no_plugin[@]}" || exit 1 fi else linuxdeploy_args_no_plugin=( --appdir "$appdir_path" --output appimage --executable "$appdir_path/usr/bin/$app_name" --desktop-file "$appdir_path/usr/share/applications/$app_name.desktop" ) [ -n "$icon_file" ] && [ -f "$icon_file" ] && linuxdeploy_args_no_plugin+=(--icon-file "$icon_file") "$linuxdeploy_path" "${linuxdeploy_args_no_plugin[@]}" || exit 1 fi } # Verify Qt plugins for CI environment if [ "$IS_DOCKER_OR_CI" = true ]; then qt_plugins_dir="$appdir_path/usr/plugins" qt_platforms_dir="$qt_plugins_dir/platforms" if [ ! -d "$qt_platforms_dir" ] || [ -z "$(find "$qt_platforms_dir" -name "*.so" -type f 2>/dev/null)" ]; then if [ -n "$ENV_QT_PATH" ] && [ -d "$ENV_QT_PATH/gcc_64/plugins/platforms" ]; then mkdir -p "$qt_platforms_dir" cp -r "$ENV_QT_PATH/gcc_64/plugins/platforms"/* "$qt_platforms_dir/" 2>/dev/null || true fi fi fi # Find generated AppImage appimage_file="" appimage_file=$(find "$project_root" -maxdepth 1 -name "*.AppImage" -type f 2>/dev/null | grep -v "linuxdeploy" | grep -v "plugin" | head -n 1) if [ -z "$appimage_file" ] || [ ! -f "$appimage_file" ]; then appimage_file=$(find "$(dirname "$appdir_path")" -maxdepth 1 -name "*.AppImage" -type f 2>/dev/null | grep -v "linuxdeploy" | grep -v "plugin" | head -n 1) fi if [ -n "$appimage_file" ] && [ -f "$appimage_file" ]; then final_appimage="$appimage_output_path/${app_name}-x86_64.AppImage" mv "$appimage_file" "$final_appimage" echo "AppImage created: $final_appimage ($(du -h "$final_appimage" | cut -f1))" else echo "error: AppImage file not found" exit 1 fi cd "$old_cd" echo "AppImage packaging completed successfully!" exit 0 ================================================ FILE: ci/linux/publish_for_ubuntu.sh.todo ================================================ echo echo echo --------------------------------------------------------------- echo check ENV echo --------------------------------------------------------------- # 从环境变量获取必要参数 # 例如 /home/barry/Qt5.9.6/5.9.6/gcc_64 echo ENV_QT_GCC $ENV_QT_GCC # 获取绝对路径,保证其他目录执行此脚本依然正确 { cd $(dirname "$0") script_path=$(pwd) cd - } &> /dev/null # disable output # 设置当前目录,cd的目录影响接下来执行程序的工作目录 old_cd=$(pwd) cd $(dirname "$0") # 启动参数声明 publish_dir=$1 # 提示 echo current publish dir: $publish_dir # 环境变量设置 keymap_path=$script_path/../../keymap # config_path=$script_path/../../config publish_path=$script_path/$publish_dir release_path=$script_path/../../output/linux/release export PATH=$ENV_QT_GCC/bin:$PATH if [ -d "$publish_path" ]; then rm -rf $publish_path fi # 复制要发布的包 cp -r $release_path $publish_path cp -r $keymap_path $publish_path/QtScrcpy.app/Contents/MacOS # cp -r $config_path $publish_path/QtScrcpy.app/Contents/MacOS # 添加qt依赖包 macdeployqt $publish_path/QtScrcpy.app # 删除多余qt依赖包 # PlugIns rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/iconengines # 截图功能需要libqjpeg.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqgif.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqicns.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqico.dylib # rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqjpeg.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqmacheif.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqmacjp2.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqtga.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqtiff.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqwbmp.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqwebp.dylib rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/virtualkeyboard rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/printsupport rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/platforminputcontexts rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/iconengines rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/bearer # Frameworks rm -rf $publish_path/QtScrcpy.app/Contents/Frameworks/QtVirtualKeyboard.framework rm -rf $publish_path/Contents/Frameworks/QtSvg.framework # qml rm -rf $publish_path/QtScrcpy.app/Contents/Frameworks/QtQml.framework rm -rf $publish_path/QtScrcpy.app/Contents/Frameworks/QtQuick.framework echo echo echo --------------------------------------------------------------- echo finish!!! echo --------------------------------------------------------------- # 恢复当前目录 cd $old_cd exit 0 ================================================ FILE: ci/lrelease.sh ================================================ # https://doc.qt.io/qt-5/linguist-manager.html#lrelease # lrelease -help lrelease ./QtScrcpy/res/i18n/en_US.ts ./QtScrcpy/res/i18n/zh_CN.ts ./QtScrcpy/res/i18n/ja_JP.ts ================================================ FILE: ci/lupdate.sh ================================================ # https://doc.qt.io/qt-5/linguist-manager.html#lupdate # lupdate -help # export PATH=/D/Qt/5.15.2/msvc2019/bin:$PATH lupdate -no-obsolete ./QtScrcpy -ts ./QtScrcpy/res/i18n/en_US.ts ./QtScrcpy/res/i18n/zh_CN.ts ./QtScrcpy/res/i18n/ja_JP.ts ================================================ FILE: ci/mac/build_for_mac.sh ================================================ echo echo echo --------------------------------------------------------------- echo check ENV echo --------------------------------------------------------------- # 从环境变量获取必要参数 # 例如 /Users/barry/Qt5.12.5/5.12.5 echo ENV_QT_PATH $ENV_QT_PATH # 获取绝对路径,保证其他目录执行此脚本依然正确 { cd $(dirname "$0") script_path=$(pwd) cd - } &> /dev/null # disable output # 设置当前目录,cd的目录影响接下来执行程序的工作目录 old_cd=$(pwd) cd $(dirname "$0") # 启动参数声明 build_mode=RelWithDebInfo cpu_arch=arm64 echo echo echo --------------------------------------------------------------- echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo] echo --------------------------------------------------------------- # 编译参数检查 build_mode=$(echo $1) if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then echo "error: unkonow build mode -- $1" exit 1 fi echo echo echo --------------------------------------------------------------- echo check cpu arch[x64/arm64] echo --------------------------------------------------------------- cpu_arch=$(echo $2) if [[ $cpu_arch != "x64" && $cpu_arch != "arm64" ]]; then echo "error: unkonow cpu mode -- $2" exit 1 fi # 提示 echo current build mode: $build_mode echo current cpu mode: $cpu_arch cmake_arch=x86_64 if [ $cpu_arch == "x64" ]; then qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5 cmake_arch=x86_64 else qt_cmake_path=$ENV_QT_PATH/macos/lib/cmake/Qt6 cmake_arch=arm64 fi echo echo echo --------------------------------------------------------------- echo begin cmake build echo --------------------------------------------------------------- # 删除输出目录 output_path=$script_path../../output if [ -d "$output_path" ]; then rm -rf $output_path fi # 删除编译目录 build_path=$script_path/../build_temp if [ -d "$build_path" ]; then rm -rf $build_path fi mkdir $build_path cd $build_path cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -DCMAKE_OSX_ARCHITECTURES=$cmake_arch" cmake $cmake_params ../.. if [ $? -ne 0 ] ;then echo "cmake failed" exit 1 fi cmake --build . --config $build_mode -j8 if [ $? -ne 0 ] ;then echo "cmake build failed" exit 1 fi echo echo echo --------------------------------------------------------------- echo finish!!! echo --------------------------------------------------------------- # 恢复当前目录 cd $old_cd exit 0 ================================================ FILE: ci/mac/package/dmg-settings.json ================================================ {"icon-size": 120, "format": "UDZO", "title": "QtScrcpy", "compression-level": 9, "window": {"position": {"y": 200, "x": 400}, "size": {"width": 780, "height": 480}}, "background": "/Users/barry/mygitcode/QtScrcpy/ci/mac/package/dmg-background.jpg", "contents": [{"y": 227, "x": 223, "type": "file", "path": "/Users/barry/mygitcode/QtScrcpy/ci/mac/package/../../build/QtScrcpy.app"}, {"y": 227, "x": 550, "type": "link", "path": "/Applications"}]} ================================================ FILE: ci/mac/package/package.py ================================================ import dmgbuild import os import json import sys current_file_path = os.path.dirname(os.path.realpath(__file__)) dmg_settings_path = '%s/dmg-settings.json' % current_file_path dmg_background_img = '%s/dmg-background.jpg' % current_file_path app_path = '%s/../../build/QtScrcpy.app' % current_file_path dmg_path = '%s/../../build/QtScrcpy.dmg' % current_file_path app_name = 'QtScrcpy' def console_print(msg): print(msg) sys.stdout.flush() def generate_dmg_info(): with open(dmg_settings_path, 'w') as file: info = { 'title': app_name, 'icon-size': 120, 'background': dmg_background_img, 'format': 'UDZO', 'compression-level': 9, 'window': { 'position': {'x': 400, 'y': 200}, 'size': {'width': 780, 'height': 480} }, 'contents': [ { 'x': 223, 'y': 227, 'type': 'file', 'path': app_path }, { 'x': 550, 'y': 227, 'type': 'link', 'path': '/Applications' } ] } json.dump(info, file) if __name__ == '__main__': console_print('generate dmg info') generate_dmg_info() console_print('build dmg: %s' % dmg_path) dmgbuild.build_dmg(dmg_path, app_name, dmg_settings_path) if not os.path.exists(dmg_path): console_print('fail to create %s' % dmg_path) sys.exit(1) sys.exit(0) ================================================ FILE: ci/mac/package/requirements.txt ================================================ dmgbuild==1.4.2 ================================================ FILE: ci/mac/package_for_mac.sh ================================================ # 获取绝对路径,保证其他目录执行此脚本依然正确 { cd $(dirname "$0") script_path=$(pwd) cd - } &> /dev/null # disable output # 设置当前目录,cd的目录影响接下来执行程序的工作目录 old_cd=$(pwd) cd $(dirname "$0") echo echo echo --------------------------------------------------------------- echo pip install requirements echo --------------------------------------------------------------- pip install -r $script_path/package/requirements.txt if [ $? -ne 0 ] ;then echo "pip install requirements failed" exit 1 fi echo echo echo --------------------------------------------------------------- echo create package echo --------------------------------------------------------------- python $script_path/package/package.py if [ $? -ne 0 ] ;then echo "create package failed" exit 1 fi # 恢复当前目录 cd $old_cd exit 0 ================================================ FILE: ci/mac/publish_for_mac.sh ================================================ echo echo echo --------------------------------------------------------------- echo check ENV echo --------------------------------------------------------------- # 从环境变量获取必要参数 # 例如 /Users/barry/Qt5.12.5/5.12.5 echo ENV_QT_PATH $ENV_QT_PATH # 获取绝对路径,保证其他目录执行此脚本依然正确 { cd $(dirname "$0") script_path=$(pwd) cd - } &> /dev/null # disable output # 设置当前目录,cd的目录影响接下来执行程序的工作目录 old_cd=$(pwd) cd $(dirname "$0") # 启动参数声明 publish_dir=$1 cpu_arch=$2 echo echo echo --------------------------------------------------------------- echo check cpu arch[x64/arm64] echo --------------------------------------------------------------- if [[ $cpu_arch != "x64" && $cpu_arch != "arm64" ]]; then echo "error: unkonow cpu mode -- $2" exit 1 fi # 提示 echo current cpu mode: $cpu_arch if [ $cpu_arch == "x64" ]; then qt_clang_path=$ENV_QT_PATH/clang_64 else qt_clang_path=$ENV_QT_PATH/macos fi # 提示 echo current publish dir: $publish_dir # 环境变量设置 keymap_path=$script_path/../../keymap # config_path=$script_path/../../config publish_path=$script_path/$publish_dir release_path=$script_path/../../output/$cpu_arch/RelWithDebInfo export PATH=$qt_clang_path/bin:$PATH if [ -d "$publish_path" ]; then rm -rf $publish_path fi # 复制要发布的包 cp -r $release_path $publish_path cp -r $keymap_path $publish_path/QtScrcpy.app/Contents/MacOS # cp -r $config_path $publish_path/QtScrcpy.app/Contents/MacOS # 添加qt依赖包 macdeployqt $publish_path/QtScrcpy.app # 删除多余qt依赖包 # PlugIns rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/iconengines # 截图功能需要libqjpeg.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqgif.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqicns.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqico.dylib # rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqjpeg.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqmacheif.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqmacjp2.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqtga.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqtiff.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqwbmp.dylib rm -f $publish_path/QtScrcpy.app/Contents/PlugIns/imageformats/libqwebp.dylib rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/virtualkeyboard rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/printsupport rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/platforminputcontexts rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/iconengines rm -rf $publish_path/QtScrcpy.app/Contents/PlugIns/bearer # Frameworks rm -rf $publish_path/QtScrcpy.app/Contents/Frameworks/QtVirtualKeyboard.framework rm -rf $publish_path/Contents/Frameworks/QtSvg.framework # qml rm -rf $publish_path/QtScrcpy.app/Contents/Frameworks/QtQml.framework rm -rf $publish_path/QtScrcpy.app/Contents/Frameworks/QtQuick.framework echo echo echo --------------------------------------------------------------- echo finish!!! echo --------------------------------------------------------------- # 恢复当前目录 cd $old_cd exit 0 ================================================ FILE: ci/win/build_for_win.bat ================================================ @echo off echo= echo= echo --------------------------------------------------------------- echo check ENV echo --------------------------------------------------------------- :: 从环境变量获取必要参数 :: example: D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat :: set vcvarsall="%ENV_VCVARSALL%" :: example: D:\Qt\Qt5.12.5\5.12.5 :: echo ENV_VCVARSALL %ENV_VCVARSALL% echo ENV_QT_PATH %ENV_QT_PATH% :: 获取脚本绝对路径 set script_path=%~dp0 :: 进入脚本所在目录,因为这会影响脚本中执行的程序的工作目录 set old_cd=%cd% cd /d %~dp0 :: 启动参数声明 set cpu_mode=x86 set build_mode=RelWithDebInfo set errno=1 echo= echo= echo --------------------------------------------------------------- echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo] echo --------------------------------------------------------------- :: 编译参数检查 if "%1"=="Debug" ( goto build_mode_ok ) if "%1"=="Release" ( goto build_mode_ok ) if "%1"=="MinSizeRel" ( goto build_mode_ok ) if "%1"=="RelWithDebInfo" ( goto build_mode_ok ) echo error: unknown build mode -- %1 goto return :build_mode_ok set build_mode=%1 set cmake_vs_build_mode=Win32 set qt_cmake_path=%ENV_QT_PATH%\msvc2019\lib\cmake\Qt5 if /i "%2"=="x86" ( set cpu_mode=x86 set cmake_vs_build_mode=Win32 set qt_cmake_path=%ENV_QT_PATH%\msvc2019\lib\cmake\Qt5 ) if /i "%2"=="x64" ( set cpu_mode=x64 set cmake_vs_build_mode=x64 set qt_cmake_path=%ENV_QT_PATH%\msvc2019_64\lib\cmake\Qt5 ) :: 提示 echo current build mode: %build_mode% %cpu_mode% echo qt cmake path: %qt_cmake_path% echo= echo= echo --------------------------------------------------------------- echo begin cmake build echo --------------------------------------------------------------- :: 删除输出目录 set output_path=%script_path%..\..\output if exist %output_path% ( rmdir /q /s %output_path% ) :: 删除临时目录 set temp_path=%script_path%..\build_temp if exist %temp_path% ( rmdir /q /s %temp_path% ) md %temp_path% cd %temp_path% set cmake_params=-DCMAKE_PREFIX_PATH=%qt_cmake_path% -DCMAKE_BUILD_TYPE=%build_mode% -G "Visual Studio 17 2022" -A %cmake_vs_build_mode% echo cmake params: %cmake_params% cmake %cmake_params% ../.. if not %errorlevel%==0 ( echo "cmake failed" goto return ) cmake --build . --config %build_mode% -j8 if not %errorlevel%==0 ( echo "cmake build failed" goto return ) echo= echo= echo --------------------------------------------------------------- echo finish!!! echo --------------------------------------------------------------- set errno=0 :return cd %old_cd% exit /B %errno% ================================================ FILE: ci/win/publish_for_win.bat ================================================ @echo off echo= echo= echo --------------------------------------------------------------- echo check ENV echo --------------------------------------------------------------- :: 从环境变量获取必要参数 :: example: D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat set vcvarsall="%ENV_VCVARSALL%" :: 例如 d:\a\QtScrcpy\Qt\5.12.7 set qt_msvc_path="%ENV_QT_PATH%" :: 设置了VCINSTALLDIR,windeployqt会自动copy vc_redist.x**.exe(vcruntime dll安装包) :: set VCINSTALLDIR="%ENV_VCINSTALL%" echo ENV_VCVARSALL %ENV_VCVARSALL% echo ENV_QT_PATH %ENV_QT_PATH% :: 获取脚本绝对路径 set script_path=%~dp0 :: 进入脚本所在目录,因为这会影响脚本中执行的程序的工作目录 set old_cd=%cd% cd /d %~dp0 :: 启动参数声明 set cpu_mode=x86 set publish_dir=%2 set errno=1 if /i "%1"=="x86" ( set cpu_mode=x86 ) if /i "%1"=="x64" ( set cpu_mode=x64 ) :: 提示 echo current build mode: %cpu_mode% echo current publish dir: %publish_dir% :: 环境变量设置 set adb_path=%script_path%..\..\QtScrcpy\QtScrcpyCore\src\third_party\adb\win\*.* set jar_path=%script_path%..\..\QtScrcpy\QtScrcpyCore\src\third_party\scrcpy-server set keymap_path=%script_path%..\..\keymap set config_path=%script_path%..\..\config if /i %cpu_mode% == x86 ( set publish_path=%script_path%%publish_dir%\ set release_path=%script_path%..\..\output\x86\RelWithDebInfo set qt_msvc_path=%qt_msvc_path%\msvc2019\bin ) else ( set publish_path=%script_path%%publish_dir%\ set release_path=%script_path%..\..\output\x64\RelWithDebInfo set qt_msvc_path=%qt_msvc_path%\msvc2019_64\bin ) set PATH=%qt_msvc_path%;%PATH% :: 注册vc环境(注册以后,windeployqt会把vc_redist复制过来(vcruntime安装包)) if /i %cpu_mode% == x86 ( call %vcvarsall% %cpu_mode% ) else ( call %vcvarsall% %cpu_mode% ) if exist %publish_path% ( rmdir /s/q %publish_path% ) :: 复制要发布的包 xcopy %release_path% %publish_path% /E /Y xcopy %adb_path% %publish_path% /Y xcopy %jar_path% %publish_path% /Y xcopy %keymap_path% %publish_path%keymap\ /E /Y xcopy %config_path% %publish_path%config\ /E /Y :: 添加qt依赖包 windeployqt %publish_path%\QtScrcpy.exe :: 删除多余qt依赖包 rmdir /s/q %publish_path%\iconengines rmdir /s/q %publish_path%\translations :: 截图功能需要qjpeg.dll del %publish_path%\imageformats\qgif.dll del %publish_path%\imageformats\qicns.dll del %publish_path%\imageformats\qico.dll ::del %publish_path%\imageformats\qjpeg.dll del %publish_path%\imageformats\qsvg.dll del %publish_path%\imageformats\qtga.dll del %publish_path%\imageformats\qtiff.dll del %publish_path%\imageformats\qwbmp.dll del %publish_path%\imageformats\qwebp.dll :: 删除vc_redist,自己copy vcruntime dll if /i %cpu_mode% == x86 ( del %publish_path%\vc_redist.x86.exe ) else ( del %publish_path%\vc_redist.x64.exe ) :: copy vcruntime dll if /i %cpu_mode% == x64 ( cp "C:\Windows\System32\msvcp140_1.dll" %publish_path%\msvcp140_1.dll cp "C:\Windows\System32\msvcp140.dll" %publish_path%\msvcp140.dll cp "C:\Windows\System32\vcruntime140.dll" %publish_path%\vcruntime140.dll :: 只有x64需要 cp "C:\Windows\System32\vcruntime140_1.dll" %publish_path%\vcruntime140_1.dll ) else ( cp "C:\Windows\SysWOW64\msvcp140_1.dll" %publish_path%\msvcp140_1.dll cp "C:\Windows\SysWOW64\msvcp140.dll" %publish_path%\msvcp140.dll cp "C:\Windows\SysWOW64\vcruntime140.dll" %publish_path%\vcruntime140.dll ) ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\VCRUNTIME140.dll" %publish_path%\VCRUNTIME140.dll ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\api-ms-win-crt-runtime-l1-1-0.dll" %publish_path%\api-ms-win-crt-runtime-l1-1-0.dll ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\api-ms-win-crt-heap-l1-1-0.dll" %publish_path%\api-ms-win-crt-heap-l1-1-0.dll ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\api-ms-win-crt-math-l1-1-0.dll" %publish_path%\api-ms-win-crt-math-l1-1-0.dll ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\api-ms-win-crt-stdio-l1-1-0.dll" %publish_path%\api-ms-win-crt-stdio-l1-1-0.dll ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\api-ms-win-crt-locale-l1-1-0.dll" %publish_path%\api-ms-win-crt-locale-l1-1-0.dll echo= echo= echo --------------------------------------------------------------- echo finish!!! echo --------------------------------------------------------------- set errno=0 :return cd %old_cd% exit /B %errno% ================================================ FILE: config/config.ini ================================================ [common] # 语言 Auto=自动,zh_CN=简体中文,en_US=English Language=Auto # 窗口标题 WindowTitle=QtScrcpy # 推送到安卓设备的文件保存路径(必须以/结尾) PushFilePath=/sdcard/ # 最大fps(仅支持Android 10以上) MaxFps=0 # 是否渲染过期视频帧(跳过过期视频帧意味着更低的延迟) RenderExpiredFrames=0 # 视频解码方式:-1 自动,0 软解,1 dx硬解,2 opengl硬解 UseDesktopOpenGL=-1 # scrcpy-server推送到安卓设备的路径 ServerPath=/data/local/tmp/scrcpy-server.jar # 自定义adb路径,例如D:/android/tools/adb.exe AdbPath= # 编码选项 ""表示默认 # 例如 CodecOptions="profile=1,level=2" # 更多编码选项参考 https://d.android.com/reference/android/media/MediaFormat CodecOptions="" # 指定编码器名称(必须是H.264编码器),""表示默认 # 例如 CodecName="OMX.qcom.video.encoder.avc" CodecName="" # Set the log level (verbose, debug, info, warn, error) LogLevel=verbose ================================================ FILE: docs/DEVELOP.md ================================================ # scrcpy for developers ## Overview This application is composed of two parts: - the server (`scrcpy-server`), to be executed on the device, - the client (the `scrcpy` binary), executed on the host computer. The client is responsible to push the server to the device and start its execution. Once the client and the server are connected to each other, the server initially sends device information (name and initial screen dimensions), then starts to send a raw H.264 video stream of the device screen. The client decodes the video frames, and display them as soon as possible, without buffering, to minimize latency. The client is not aware of the device rotation (which is handled by the server), it just knows the dimensions of the video frames. The client captures relevant keyboard and mouse events, that it transmits to the server, which injects them to the device. ## Server ### Privileges Capturing the screen requires some privileges, which are granted to `shell`. The server is a Java application (with a [`public static void main(String... args)`][main] method), compiled against the Android framework, and executed as `shell` on the Android device. [main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 To run such a Java application, the classes must be [_dexed_][dex] (typically, to `classes.dex`). If `my.package.MainClass` is the main class, compiled to `classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run with: adb shell CLASSPATH=/data/local/tmp/classes.dex \ app_process / my.package.MainClass _The path `/data/local/tmp` is a good candidate to push the server, since it's readable and writable by `shell`, but not world-writable, so a malicious application may not replace the server just before the client executes it._ Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle build system, the server is built to an (unsigned) APK (renamed to `scrcpy-server`). [dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [apk]: https://en.wikipedia.org/wiki/Android_application_package ### Hidden methods Although compiled against the Android framework, [hidden] methods and classes are not directly accessible (and they may differ from one Android version to another). They can be called using reflection though. The communication with hidden components is provided by [_wrappers_ classes][wrappers] and [aidl]. [hidden]: https://stackoverflow.com/a/31908373/1987178 [wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers [aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view ### Threading The server uses 3 threads: - the **main** thread, encoding and streaming the video to the client; - the **controller** thread, listening for _control messages_ (typically, keyboard and mouse events) from the client; - the **receiver** thread (managed by the controller), sending _device messges_ to the clients (currently, it is only used to send the device clipboard content). Since the video encoding is typically hardware, there would be no benefit in encoding and streaming in two different threads. ### Screen video encoding The encoding is managed by [`ScreenEncoder`]. The video is encoded using the [`MediaCodec`] API. The codec takes its input from a [surface] associated to the display, and writes the resulting H.264 stream to the provided output stream (the socket connected to the client). [`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html [surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69 On device [rotation], the codec, surface and display are reinitialized, and a new video stream is produced. New frames are produced only when changes occur on the surface. This is good because it avoids to send unnecessary frames, but there are drawbacks: - it does not send any frame on start if the device screen does not change, - after fast motion changes, the last frame may have poor quality. Both problems are [solved][repeat] by the flag [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. [rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 [repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER ### Input events injection _Control messages_ are received from the client by the [`Controller`] (run in a separate thread). There are several types of input events: - keycode (cf [`KeyEvent`]), - text (special characters may not be handled by keycodes directly), - mouse motion/click, - mouse scroll, - other commands (e.g. to switch the screen on or to copy the clipboard). Some of them need to inject input events to the system. To do so, they use the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our [`InputManager` wrapper][inject-wrapper]). [`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81 [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 ## Client The client relies on [SDL], which provides cross-platform API for UI, input events, threading, etc. The video stream is decoded by [libav] (FFmpeg). [SDL]: https://www.libsdl.org [libav]: https://www.libav.org/ ### Initialization On startup, in addition to _libav_ and _SDL_ initialization, the client must push and start the server on the device, and open two sockets (one for the video stream, one for control) so that they may communicate. Note that the client-server roles are expressed at the application level: - the server _serves_ video stream and handle requests from the client, - the client _controls_ the device through the server. However, the roles are reversed at the network level: - the client opens a server socket and listen on a port before starting the server, - the server connects to the client. This role inversion guarantees that the connection will not fail due to race conditions, and avoids polling. _(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb reverse`. See commit [1038bad] and [issue #5].)_ Once the server is connected, it sends the device information (name and initial screen dimensions). Thus, the client may init the window and renderer, before the first frame is available. To minimize startup time, SDL initialization is performed while listening for the connection from the server (see commit [90a46b4]). [1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172 [issue #5]: https://github.com/Genymobile/scrcpy/issues/5 [90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e ### Threading The client uses 4 threads: - the **main** thread, executing the SDL event loop, - the **stream** thread, receiving the video and used for decoding and recording, - the **controller** thread, sending _control messages_ to the server, - the **receiver** thread (managed by the controller), receiving _device messages_ from the server. In addition, another thread can be started if necessary to handle APK installation or file push requests (via drag&drop on the main window) or to print the framerate regularly in the console. ### Stream The video [stream] is received from the socket (connected to the server on the device) in a separate thread. If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_ to decode the H.264 stream from the socket, and notifies the main thread when a new frame is available. There are two [frames][video_buffer] simultaneously in memory: - the **decoding** frame, written by the decoder from the decoder thread, - the **rendering** frame, rendered in a texture from the main thread. When a new decoded frame is available, the decoder _swaps_ the decoding and rendering frame (with proper synchronization). Thus, it immediatly starts to decode a new frame while the main thread renders the last one. If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw H.264 packet to the output video file. [stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h [decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h [video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h [recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h ``` +----------+ +----------+ ---> | decoder | ---> | screen | +---------+ / +----------+ +----------+ socket ---> | stream | ---- +---------+ \ +----------+ ---> | recorder | +----------+ ``` ### Controller The [controller] is responsible to send _control messages_ to the device. It runs in a separate thread, to avoid I/O on the main thread. On SDL event, received on the main thread, the [input manager][inputmanager] creates appropriate [_control messages_][controlmsg]. It is responsible to convert SDL events to Android events (using [convert]). It pushes the _control messages_ to a queue hold by the controller. On its own thread, the controller takes messages from the queue, that it serializes and sends to the client. [controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h [controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h [inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h [convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h ### UI and event loop Initialization, input events and rendering are all [managed][scrcpy] in the main thread. Events are handled in the [event loop], which either updates the [screen] or delegates to the [input manager][inputmanager]. [scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c [event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201 [screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h ## Hack For more details, go read the code! If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-) ### Debug the server The server is pushed to the device by the client on startup. To debug it, enable the server debugger during configuration: ```bash meson x -Dserver_debugger=true # or, if x is already configured meson configure x -Dserver_debugger=true ``` If your device runs Android 8 or below, set the `server_debugger_method` to `old` in addition: ```bash meson x -Dserver_debugger=true -Dserver_debugger_method=old # or, if x is already configured meson configure x -Dserver_debugger=true -Dserver_debugger_method=old ``` Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. Redirect that port to the computer: ```bash adb forward tcp:5005 tcp:5005 ``` In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on `+`, _Remote_, and fill the form: - Host: `localhost` - Port: `5005` Then click on _Debug_. ================================================ FILE: docs/FAQ.md ================================================ # Frequently Asked Questions 一些经常问的问题 如果在此文档没有解决你的问题,描述你的问题,截图软件控制台中打印的日志,一起发到QQ群里提问。 # adb问题 ## ADB版本之间的冲突 ``` adb server version (41) doesn't match this client (39); killing... ``` 当你的电脑中运行不同版本的adb时,会发生此错误。你必须保证所有程序使用相同版本的adb。 现在你有两个办法解决这个问题: 1. 任务管理器找到adb进程并杀死 2. 配置QtScrcpy的config.ini中的AdbPath路径指向当前使用的adb ## 手机通过数据线连接电脑,刷新设备列表以后,没有任何设备出现 随便下载一个手机助手,尝试连接成功以后,再用QtScrcpy刷新设备列表连接 # 控制问题 ## 可以看到画面,但无法控制 有些手机(小米等手机)需要额外打开控制权限,检查是否USB调试里打开了允许模拟点击 ![image](image/USB调试(安全设置).jpg) # 其它 ## 支持声音(软件不做支持) [关于转发安卓声音到PC的讨论](https://github.com/Genymobile/scrcpy/issues/14#issuecomment-543204526) ## 画面不清晰 在Windows上,您可能需要配置缩放行为。 QtScrcpy.exe>属性>兼容性>更改高DPI设置>覆盖高DPI缩放行为>由以下人员执行缩放:应用程序。 如果视频窗口大小远远小于设备屏幕的大小,则画面会不清晰。这在文字上尤其明显 ## 玩和平精英上下车操作会失效 这是由于游戏中上车会创建新的界面,导致鼠标触摸点失效,目前技术上没有好的解决办法 可以通过`连续按两次~键(数字键1左边)`来恢复,这是目前最好的办法。 ## 无法输入中文 手机端安装搜狗输入法/QQ输入法就可以支持输入中文了 ## 可以控制,但无法看到画面 控制台错误信息可能会包含 QOpenGLShaderProgram::attributeLocation(vertexIn): shader program is not linked 一般是由于显卡不支持当前的视频渲染方式,config.ini里修改下解码方式,改成1或者2试试 ## 错误信息:AdbProcess::error:adb server version (40) doesnt match this client (41) 任务管理找到adb进程并杀死,重新操作即可 ## 错误信息:Could not open video stream 导致这个错误的原因有很多,最简单的解决方法是在分辨率设置中,选择一个较低的分辨率 ================================================ FILE: docs/KeyMapDes.md ================================================ # Custom key mapping instructions The key map file is in json format, and the new key map file needs to be placed in the keymap directory to be recognized by QtScrcpy. The specific writing format of the button mapping file will be introduced below, and you can also refer to the button mapping file that comes with it. ## Key mapping script format description ### General Instructions -The coordinate positions in the key map are all expressed by relative positions, and the width and height of the screen are expressed by 1, for example, the pixels of the screen are 1920x1080, then the coordinates (0.5,0.5) indicate Taking the upper left corner of the screen as the origin, the position of the pixel coordinates (1920,1080)*(0.5,0.5)=(960,540). Or when the left mouse button clicks, the console will output the pos at this time, just use this pos directly ![](image/debug-keymap-pos.png) -The key codes in the key map are represented by Qt enumerations, detailed description can be [refer to Qt documentation](https://doc.qt.io/qt-5/qt.html) (search for The key names used by Qt. can be quickly located). -Open the following two settings in the developer options, you can easily observe the coordinates of the touch point: ![](image/display pointer position.jpg) ### Mapping type description -switchKey: Switch the key of the custom key mapping. The default is the normal mapping. You need to use this key to switch between the normal mapping and the custom mapping. -mouseMoveMap: mouse movement mapping, the movement of the mouse will be mapped to startPos as the starting point, and the direction of the mouse movement as the direction of the finger drag operation (after the mouse movement map is turned on, the mouse will be hidden, limiting the range of mouse movement). Generally used to adjust the character field of vision in FPS mobile games. -startPos finger drag starting point -speedRatio mouse sensitivity of the finger dragging. The value must be at least 0.00225. The greater the value, the lower the sensitivity. The Y-axis translates with a ratio of 2.25. If this does not fit your phone screen, please use the following two settings to set individual sensitivity values. -speedRatioX sensitivity of the mouse X-axis. This value must be at least 0.001. -speedRatioY sensitivity of the mouse Y-axis. This value must be at least 0.001. -smallEyes The button that triggers the small eyes. After pressing this button, the mouse movement will be mapped to the finger drag operation with the smallEyes.pos as the starting point and the mouse movement direction as the movement direction -keyMapNodes general key map, json array, all general key maps are placed in this array, map the keys of the keyboard to ordinary finger clicks. There are several types of key mapping as follows: -type The type of key mapping, each element in keyMapNodes needs to be specified, and can be of the following types: -KMT_CLICK Ordinary click, key press simulates finger press, key lift simulates finger lift -KMT_CLICK_TWICE Double click, key press simulates finger press and then lift, key lift simulates finger press and then lift - KMT_CLICK_MULTI Click multiple times. According to the delay and pos in the clickNodes array, press one key to simulate touching multiple positions -KMT_DRAG drag and drop, the key press is simulated as a finger press and drag a distance, the key lift is simulated as a finger lift -KMT_STEER_WHEEL steering wheel mapping, which is dedicated to the mapping of the steering wheel for moving characters in FPS games, requires 4 buttons to cooperate. Description of the unique attributes of different key mapping types: -KMT_CLICK -key The key code to be mapped -pos simulates the location of the touch -Whether the switchMap releases the mouse. After clicking this button, besides the default simulated touch map, whether the mouse operation is released. (You can refer to the effect of M map mapping in Peace Elite Map) -KMT_CLICK_TWICE -key The key code to be mapped -pos Simulates the location of the touch -KMT_CLICK_MULTI -delay Delay `delay` ms before simulating touch -pos Simulates the location of the touch -KMT_DRAG -key The key code to be mapped -startPos Simulate the start position of touch drag -endPos Simulate the end position of touch drag -dragSpeed Speed of the drag movement (range 0-1, default 1.0). Higher values result in faster movements -startDelay Optional delay in milliseconds to wait after the initial touch before starting the drag movement -KMT_STEER_WHEEL -centerPos steering wheel center point -leftKey key control in the left direction -rightKey Right key control -UpKey key control -downKey key control in down direction -leftOffset After dragging the left arrow key, drag to the leftOffset horizontally to the centerPos -rightOffset After pressing the right direction key, drag it to the right offset of the center to the right of the centerPos position -upOffset After pressing the up arrow key, drag it to the upper offset position horizontally relative to the centerPos position -downOffset Press the down arrow key and drag it to the downOffset position horizontally relative to the centerPos position ## Visual Key Mapping Tool 1. Just use [QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh) ![game](../screenshot/game.png) 2. A web-based GUI tool is available to help you create and manage key mappings visually: [ScrcpyKeyMapper](https://github.com/w4po/ScrcpyKeyMapper) ![ScrcpyKeyMapper Screenshot](https://raw.githubusercontent.com/w4po/ScrcpyKeyMapper/main/assets/screenshot.png) You can use this tool to: - Create key mappings visually - Test your mappings in real-time - Export mappings as JSON files - Import existing mappings for editing Try it online: [ScrcpyKeyMapper Web App](https://w4po.github.io/ScrcpyKeyMapper) ================================================ FILE: docs/KeyMapDes_zh.md ================================================ # 自定义按键映射说明 按键映射文件为json格式,新增自己的按键映射文件需要放在keymap目录中才可以被QtScrcpy识别。 按键映射文件的具体编写格式下面会介绍,也可以参考自带的按键映射文件。 ## 按键映射脚本格式说明 ### 通用说明 - 按键映射中的坐标位置都是用相对位置表示的,屏幕的宽高都用1表示,例如屏幕的像素为1920x1080,那么坐标(0.5,0.5)则表示的是 以屏幕左上角为原点,像素坐标(1920,1080)*(0.5,0.5)=(960,540)的位置。 或者鼠标左键单击时控制台会输出此时的pos,直接使用这个pos即可 ![](image/debug-keymap-pos.png) - 按键映射中的按键码是用Qt的枚举表示的,详细说明可以[参考Qt文档]( https://doc.qt.io/qt-5/qt.html )(搜索 The key names used by Qt. 可以快速定位)。 - 开发人员选项中打开如下两个设置,可以方便的观察触摸点的坐标: ![](image/显示指针位置.jpg) ### 映射类型说明 - switchKey:切换自定义按键映射的开关键,默认为普通映射,需要使用这个按键在普通映射和自定义映射之间切换。 - mouseMoveMap:鼠标移动映射,鼠标的移动将被映射为以startPos为起点,以鼠标移动方向为移动方向的手指拖动操作(开启鼠标移动映射以后会隐藏鼠标,限制鼠标移动范围)。 一般在FPS手游中用来调整人物视野。 - startPos 手指拖动起始点 - speedRatio 鼠标移动映射为手指拖动的比例,可以控制鼠标灵敏度,数值要大于0.00225,数值越大,灵敏度越低,Y轴以2.25的比率平移。如果这不适合您的手机屏幕,请使用以下两种设置来设置单个灵敏度值。 - speedRatioX 鼠标X轴的速度比灵敏度。此值必须至少为0.001。 - speedRatioY 鼠标Y轴的速度比灵敏度。此值必须至少为0.001。 - smallEyes 触发小眼睛的按键,按下此按键以后,鼠标的移动将被映射为以smallEyes.pos为起点,以鼠标移动方向为移动方向的手指拖动操作 - keyMapNodes 一般按键的映射,json数组,所有一般按键映射都放在这个数组中,将键盘的按键映射为普通的手指点击。 一般按键映射有如下几种类型: - type 按键映射的类型,每个keyMapNodes中的元素都需要指明,可以是如下类型: - KMT_CLICK 普通点击,按键按下模拟为手指按下,按键抬起模拟为手指抬起 - KMT_CLICK_TWICE 两次点击,按键按下模拟为手指按下再抬起,按键抬起模拟为手指按下再抬起 - KMT_CLICK_MULTI 多次点击,根据clickNodes数组中的delay和pos实现一个按键多次点击 - KMT_DRAG 拖拽,按键按下模拟为手指按下并拖动一段距离,按键抬起模拟为手指抬起 - KMT_STEER_WHEEL 方向盘映射,专用于FPS游戏中移动人物脚步的方向盘的映射,需要4个按键来配合。 不同按键映射类型的专有属性说明: - KMT_CLICK - key 要映射的按键码 - pos 模拟触摸的位置 - switchMap 是否释放出鼠标,点击此按键后,除了默认的模拟触摸映射,是否释放出鼠标操作。(可以参考和平精英映射中M地图映射的效果) - KMT_CLICK_TWICE - key 要映射的按键码 - pos 模拟触摸的位置 - KMT_CLICK_MULTI - delay 延迟delay毫秒以后再模拟触摸 - pos 模拟触摸的位置 - KMT_DRAG - key 要映射的按键码 - startPos 模拟触摸拖动的开始位置 - endPos 模拟触摸拖动的结束位置 - dragSpeed 拖动移动的速度(范围0-1,默认1.0)。数值越大,移动越快 - startDelay 可选的延迟时间(毫秒),在开始拖动移动之前等待指定的时间 - KMT_STEER_WHEEL - centerPos 方向盘中心点 - leftKey 左方向的按键控制 - rightKey 右方向的按键控制 - upKey 上方向的按键控制 - downKey 下方向的按键控制 - leftOffset 按下左方向键后模拟拖动到相对centerPos位置水平偏左leftOffset处 - rightOffset 按下右方向键后模拟拖动到相对centerPos位置水平偏右rightOffset处 - upOffset 按下上方向键后模拟拖动到相对centerPos位置水平偏上upOffset处 - downOffset 按下下方向键后模拟拖动到相对centerPos位置水平偏下downOffset处 ## 可视化按键映射工具 1. 直接使用[QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh) ![game](../screenshot/game.png) 2. 还有一个基于Web的GUI工具可以帮助你直观地创建和管理按键映射:[ScrcpyKeyMapper](https://github.com/w4po/ScrcpyKeyMapper) ![ScrcpyKeyMapper截图](https://raw.githubusercontent.com/w4po/ScrcpyKeyMapper/main/assets/screenshot.png) 你可以使用这个工具来: - 直观地创建按键映射 - 实时测试你的映射 - 导出映射为JSON文件 - 导入现有映射进行编辑 在线试用:[ScrcpyKeyMapper网页应用](https://w4po.github.io/ScrcpyKeyMapper) ================================================ FILE: docs/TODO.md ================================================ # TODO ## 低优先级 - text转换 https://github.com/Genymobile/scrcpy/commit/c916af0984f72a60301d13fa8ef9a85112f54202?tdsourcetag=s_pctim_aiomsg - 关闭number lock时的数字小键盘处理 https://github.com/Genymobile/scrcpy/commit/cd69eb4a4fecf8167208399def4ef536b59c9d22 - mipmapping https://github.com/Genymobile/scrcpy/commit/bea7658807d276aeab7d18d856a366c83ee05827 ## 中优先级 - 脚本 - 某些机器软解不行 - opengles 3.0 兼容性参考[这里](https://github.com/libretro/glsl-shaders/blob/master/nnedi3/shaders/yuv-to-rgb-2x.glsl) - 通过host:track-devices实现自动连接 https://www.jianshu.com/p/2cb86c6de76c - 旋转 https://github.com/Genymobile/scrcpy/commit/d48b375a1dbc8bed92e3424b5967e59c2d8f6ca1 - 禁用屏幕保护 https://github.com/Genymobile/scrcpy/commit/dc7b60e6199b90a45ea26751988f6f30f8b2efdf - 自定义快捷键 https://github.com/Genymobile/scrcpy/commit/1b76d9fd78c3a88a8503a72d4cd2f65bdb836aa4 ## 高优先级 - linux打包以及版本号 - 关于 - 音频转发 https://github.com/rom1v/sndcpy # mark ## ffmpeg [ffmpeg编译参数详解](https://www.cnblogs.com/wainiwann/p/4204230.html) ## fontawesome [fontawesome 在线搜索](http://www.fontawesome.com.cn/cheatsheet/) ## adb 以下是 ADB 和 Fastboot 的谷歌官方下载链接: ADB和Fastboot for Windows https://dl.google.com/android/repository/platform-tools-latest-windows.zip ADB和Fastboot for Mac https://dl.google.com/android/repository/platform-tools-latest-darwin.zip ADB和Fastboot for Linux https://dl.google.com/android/repository/platform-tools-latest-linux.zip 由于这些是直接的 Google 链接,用户可以确保下载不仅是官方的,而且将始终能够获得最新版本的 ADB 和 Fastboot ================================================ FILE: keymap/FRAG.json ================================================ { "old-switchKey": "Key_QuoteLeft", "switchKey": "RightButton", "mouseMoveMap": { "startPos": { "x": 0.5, "y": 0.5 }, "speedRatioX": 3.25, "speedRatioY": 1.25 }, "keyMapNodes": [{ "comment": "Steering Wheel", "type": "KMT_STEER_WHEEL", "centerPos": { "x": 0.194792, "y": 0.716484 }, "leftOffset": 0.15, "rightOffset": 0.15, "upOffset": 0.15, "downOffset": 0.15, "leftKey": "Key_A", "rightKey": "Key_D", "upKey": "Key_W", "downKey": "Key_S" }, { "comment": "Activate item under crosshair", "type": "KMT_CLICK", "key": "LeftButton", "pos": { "x": 0.51875, "y": 0.496703 }, "switchMap": false }, { "comment": "Activate first special skill", "type": "KMT_CLICK", "key": "Key_E", "pos": { "x": 0.909375, "y": 0.542857 }, "switchMap": false }, { "comment": "Activate Chat", "type": "KMT_CLICK", "key": "Key_C", "pos": { "x": 0.905208, "y": 0.254945 }, "switchMap": false }, { "comment": "Chat option 1", "type": "KMT_CLICK", "key": "Key_1", "pos": { "x": 0.875, "y": 0.523077 }, "switchMap": false }, { "comment": "Chat option 2", "type": "KMT_CLICK", "key": "Key_2", "pos": { "x": 0.875, "y": 0.606593 }, "switchMap": false }, { "comment": "Chat option 3", "type": "KMT_CLICK", "key": "Key_3", "pos": { "x": 0.875, "y": 0.685714 }, "switchMap": false }, { "comment": "Chat option 4", "type": "KMT_CLICK", "key": "Key_4", "pos": { "x": 0.875, "y": 0.756044 }, "switchMap": false }, { "comment": "Chat option 5", "type": "KMT_CLICK", "key": "Key_5", "pos": { "x": 0.875, "y": 0.832967 }, "switchMap": false }, { "comment": "Chat option 6", "type": "KMT_CLICK", "key": "Key_6", "pos": { "x": 0.875, "y": 0.911273 }, "switchMap": false } ] } ================================================ FILE: keymap/gameforpeace.json ================================================ { "switchKey": "Key_QuoteLeft", "mouseMoveMap": { "startPos": { "x": 0.57, "y": 0.26 }, "speedRatioX": 3.25, "speedRatioY": 1.25, "smallEyes": { "comment": "小眼睛", "type": "KMT_CLICK", "key": "Key_Alt", "pos": { "x": 0.8, "y": 0.31 }, "switchMap": false }, "speedRatio": 10 }, "keyMapNodes": [ { "comment": "方向盘", "type": "KMT_STEER_WHEEL", "centerPos": { "x": 0.16, "y": 0.75 }, "leftOffset": 0.1, "rightOffset": 0.1, "upOffset": 0.27, "downOffset": 0.2, "leftKey": "Key_A", "rightKey": "Key_D", "upKey": "Key_W", "downKey": "Key_S" }, { "comment": "左探头", "type": "KMT_CLICK_TWICE", "key": "Key_Q", "pos": { "x": 0.12, "y": 0.35 } }, { "comment": "右探头", "type": "KMT_CLICK_TWICE", "key": "Key_E", "pos": { "x": 0.2, "y": 0.35 } }, { "comment": "自动跑", "type": "KMT_CLICK", "key": "Key_Equal", "pos": { "x": 0.84, "y": 0.26 }, "switchMap": false }, { "comment": "跳", "type": "KMT_CLICK", "key": "Key_Space", "pos": { "x": 0.96, "y": 0.7 }, "switchMap": false }, { "comment": "地图", "type": "KMT_CLICK", "key": "Key_M", "pos": { "x": 0.98, "y": 0.03 }, "switchMap": true }, { "comment": "背包", "type": "KMT_CLICK", "key": "Key_Tab", "pos": { "x": 0.06, "y": 0.9 }, "switchMap": true }, { "comment": "视角", "type": "KMT_CLICK", "key": "Key_V", "pos": { "x": 0.23, "y": 0.95 }, "switchMap": false }, { "comment": "趴", "type": "KMT_CLICK", "key": "Key_Z", "pos": { "x": 0.95, "y": 0.9 }, "switchMap": false }, { "comment": "蹲", "type": "KMT_CLICK", "key": "Key_C", "pos": { "x": 0.86, "y": 0.92 }, "switchMap": false }, { "comment": "换弹", "type": "KMT_CLICK", "key": "Key_R", "pos": { "x": 0.795, "y": 0.93 }, "switchMap": false }, { "comment": "捡东西1", "type": "KMT_CLICK", "key": "Key_F", "pos": { "x": 0.7, "y": 0.34 }, "switchMap": false }, { "comment": "捡东西2", "type": "KMT_CLICK", "key": "Key_G", "pos": { "x": 0.7, "y": 0.44 }, "switchMap": false }, { "comment": "捡东西3", "type": "KMT_CLICK", "key": "Key_H", "pos": { "x": 0.7, "y": 0.54 }, "switchMap": false }, { "comment": "换枪1", "type": "KMT_CLICK", "key": "Key_1", "pos": { "x": 0.45, "y": 0.9 }, "switchMap": false }, { "comment": "换枪2", "type": "KMT_CLICK", "key": "Key_2", "pos": { "x": 0.55, "y": 0.9 }, "switchMap": false }, { "comment": "手雷", "type": "KMT_CLICK", "key": "Key_3", "pos": { "x": 0.67, "y": 0.92 }, "switchMap": false }, { "comment": "快速打药", "type": "KMT_CLICK", "key": "Key_4", "pos": { "x": 0.33, "y": 0.95 }, "switchMap": false }, { "comment": "下车", "type": "KMT_CLICK", "key": "Key_5", "pos": { "x": 0.92, "y": 0.4 }, "switchMap": false }, { "comment": "救人", "type": "KMT_CLICK", "key": "Key_6", "pos": { "x": 0.49, "y": 0.63 }, "switchMap": false }, { "comment": "手枪", "type": "KMT_CLICK", "key": "Key_7", "pos": { "x": 0.63, "y": 0.82 }, "switchMap": false }, { "comment": "车加速", "type": "KMT_CLICK", "key": "Key_Shift", "pos": { "x": 0.8, "y": 0.8 }, "switchMap": false }, { "comment": "投掷物菜单", "type": "KMT_CLICK", "key": "Key_F1", "pos": { "x": 0.69, "y": 0.88 }, "switchMap": true }, { "comment": "药物菜单", "type": "KMT_CLICK", "key": "Key_F2", "pos": { "x": 0.31, "y": 0.88 }, "switchMap": true }, { "comment": "消息菜单", "type": "KMT_CLICK", "key": "Key_F3", "pos": { "x": 0.98, "y": 0.34 }, "switchMap": true }, { "comment": "表情菜单", "type": "KMT_CLICK", "key": "Key_F4", "pos": { "x": 0.81, "y": 0.03 }, "switchMap": true }, { "comment": "开关门", "type": "KMT_CLICK", "key": "Key_X", "pos": { "x": 0.7, "y": 0.7 }, "switchMap": false }, { "comment": "舔包", "type": "KMT_CLICK", "key": "Key_T", "pos": { "x": 0.72, "y": 0.26 }, "switchMap": false }, { "comment": "开枪", "type": "KMT_CLICK", "key": "LeftButton", "pos": { "x": 0.86, "y": 0.72 }, "switchMap": false }, { "comment": "开镜", "type": "KMT_CLICK", "key": "RightButton", "pos": { "x": 0.96, "y": 0.52 }, "switchMap": false } ] } ================================================ FILE: keymap/identityv.json ================================================ { "comment":"https://doc.qt.io/qt-5/qt.html#Key-enum", "old-switchKey": "Key_QuoteLeft", "switchKey": "RightButton", "mouseMoveMap": { "startPos": { "x": 0.700, "y": 0.410 }, "speedRatioX": 3.25, "speedRatioY": 1.25 }, "keyMapNodes": [{ "comment": "退出", "type": "KMT_CLICK", "key": "Key_Escape", "pos": { "x": 0.015, "y": 0.042 }, "switchMap": true }, { "comment": "方向盘", "type": "KMT_STEER_WHEEL", "centerPos": { "x": 0.16, "y": 0.70 }, "leftOffset": 0.1, "rightOffset": 0.1, "upOffset": 0.1, "downOffset": 0.1, "leftKey": "Key_A", "rightKey": "Key_D", "upKey": "Key_W", "downKey": "Key_S" }, { "comment": "动作", "type": "KMT_CLICK", "key": "LeftButton", "pos": { "x": 0.907, "y": 0.842 }, "switchMap": false }, { "comment": "动作", "type": "KMT_CLICK", "key": "Key_Space", "pos": { "x": 0.907, "y": 0.842 }, "switchMap": false }, { "comment": "蹲", "type": "KMT_CLICK", "key": "Key_Control", "pos": { "x": 0.8125, "y": 0.912 }, "switchMap": false }, { "comment": "走/翻越", "type": "KMT_CLICK", "key": "Key_C", "pos": { "x": 0.815, "y": 0.761 }, "switchMap": false }, { "comment": "跑/技能1", "type": "KMT_CLICK", "key": "Key_Z", "pos": { "x": 0.868, "y": 0.636 }, "switchMap": false }, { "comment": "技能2", "type": "KMT_CLICK", "key": "Key_E", "pos": { "x": 0.945, "y": 0.619 }, "switchMap": false }, { "comment": "特质/道具1", "type": "KMT_CLICK", "key": "Key_Q", "pos": { "x": 0.949, "y": 0.458 }, "switchMap": false }, { "comment": "底牌/切换1", "type": "KMT_CLICK", "key": "Key_Tab", "pos": { "x": 0.885, "y": 0.488 }, "switchMap": false }, { "comment": "发言/道具2", "type": "KMT_CLICK", "key": "Key_R", "pos": { "x": 0.950, "y": 0.308 }, "switchMap": false }, { "comment": "涂鸦", "type": "KMT_CLICK", "key": "Key_Y", "pos": { "x": 0.732, "y": 0.904 }, "switchMap": false }, { "comment": "盯红蝶/挂人", "type": "KMT_CLICK", "key": "Key_F", "pos": { "x": 0.815, "y": 0.514 }, "switchMap": false }, { "comment": "判定", "type": "KMT_CLICK", "key": "Key_T", "pos": { "x": 0.681, "y": 0.750 }, "switchMap": false }, { "comment": "中间", "type": "KMT_CLICK", "key": "Key_Shift", "pos": { "x": 0.5, "y": 0.6 }, "switchMap": false }, { "comment": "小丑零件1", "type": "KMT_DRAG", "key": "Key_1", "startPos": { "x": 0.951, "y": 0.615 }, "endPos": { "x": 0.911, "y": 0.472 } }, { "comment": "小丑零件2", "type": "KMT_DRAG", "key": "Key_2", "startPos": { "x": 0.951, "y": 0.615 }, "endPos": { "x": 0.861, "y": 0.615 } }, { "comment": "小丑零件3", "type": "KMT_DRAG", "key": "Key_3", "startPos": { "x": 0.951, "y": 0.615 }, "endPos": { "x": 0.907, "y": 0.774 } }, { "comment": "挣扎左", "type": "KMT_CLICK", "key": "Key_Left", "pos": { "x": 0.267, "y": 0.550 } }, { "comment": "挣扎右", "type": "KMT_CLICK", "key": "Key_Right", "pos": { "x": 0.736, "y": 0.550 } }, { "comment": "小镜头", "type": "KMT_CLICK", "key": "Key_Alt", "pos": { "x": 0.801, "y": 0.244 }, "speedRatio": 2 } ] } ================================================ FILE: keymap/test.json ================================================ { "switchKey": "Key_QuoteLeft", "keyMapNodes": [ { "comment": "测试一键多点", "type": "KMT_CLICK_MULTI", "key": "Key_Space", "clickNodes": [ { "delay": 500, "pos": { "x": 0.5, "y": 0.5 } }, { "delay": 500, "pos": { "x": 0.8, "y": 0.8 } } ] }, { "comment": "测试拖拽", "type": "KMT_DRAG", "key": "Key_Up", "startPos": { "x": 0.5, "y": 0.7 }, "endPos": { "x": 0.5, "y": 0.3 } } ] } ================================================ FILE: keymap/tiktok.json ================================================ { "switchKey": "Key_QuoteLeft", "keyMapNodes": [ { "comment": "暂停/继续", "type": "KMT_CLICK", "key": "Key_Space", "pos": { "x": 0.5, "y": 0.5 }, "switchMap": false }, { "comment": "上滑", "type": "KMT_DRAG", "key": "Key_Up", "startPos": { "x": 0.5, "y": 0.7 }, "endPos": { "x": 0.5, "y": 0.3 } }, { "comment": "下滑", "type": "KMT_DRAG", "key": "Key_Down", "startPos": { "x": 0.5, "y": 0.3 }, "endPos": { "x": 0.5, "y": 0.7 } }, { "comment": "左滑", "type": "KMT_DRAG", "key": "Key_Left", "startPos": { "x": 0.7, "y": 0.5 }, "endPos": { "x": 0.3, "y": 0.5 } }, { "comment": "右滑", "type": "KMT_DRAG", "key": "Key_Right", "startPos": { "x": 0.3, "y": 0.5 }, "endPos": { "x": 0.7, "y": 0.5 } } ] }